Skip to content

Commit 00e3f66

Browse files
authored
Merge pull request #40 from forcedotcom/profiles
@W-19620735 Credential profiles are a thing
2 parents 45fbc66 + 1ae23c9 commit 00e3f66

File tree

8 files changed

+112
-11
lines changed

8 files changed

+112
-11
lines changed

src/datacustomcode/cli.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ def zip(path: str):
8383
@click.option("--name", required=True)
8484
@click.option("--version", default="0.0.1")
8585
@click.option("--description", default="Custom Data Transform Code")
86+
@click.option("--profile", default="default")
8687
@click.option(
8788
"--cpu-size",
8889
default="CPU_2XL",
@@ -96,7 +97,9 @@ def zip(path: str):
9697
9798
Choose based on your workload requirements.""",
9899
)
99-
def deploy(path: str, name: str, version: str, description: str, cpu_size: str):
100+
def deploy(
101+
path: str, name: str, version: str, description: str, cpu_size: str, profile: str
102+
):
100103
from datacustomcode.credentials import Credentials
101104
from datacustomcode.deploy import TransformationJobMetadata, deploy_full
102105

@@ -122,7 +125,7 @@ def deploy(path: str, name: str, version: str, description: str, cpu_size: str):
122125
computeType=COMPUTE_TYPES[cpu_size],
123126
)
124127
try:
125-
credentials = Credentials.from_available()
128+
credentials = Credentials.from_available(profile=profile)
126129
except ValueError as e:
127130
click.secho(
128131
f"Error: {e}",
@@ -192,7 +195,13 @@ def scan(filename: str, config: str, dry_run: bool, no_requirements: bool):
192195
@click.argument("entrypoint")
193196
@click.option("--config-file", default=None)
194197
@click.option("--dependencies", default=[], multiple=True)
195-
def run(entrypoint: str, config_file: Union[str, None], dependencies: List[str]):
198+
@click.option("--profile", default="default")
199+
def run(
200+
entrypoint: str,
201+
config_file: Union[str, None],
202+
dependencies: List[str],
203+
profile: str,
204+
):
196205
from datacustomcode.run import run_entrypoint
197206

198-
run_entrypoint(entrypoint, config_file, dependencies)
207+
run_entrypoint(entrypoint, config_file, dependencies, profile)

src/datacustomcode/config.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
reader_config:
22
type_config_name: QueryAPIDataCloudReader
3+
options:
4+
credentials_profile: default
35

46
writer_config:
57
type_config_name: PrintDataCloudWriter
8+
options:
9+
credentials_profile: default
610

711
spark_config:
812
app_name: DC Custom Code Python SDK Testing

src/datacustomcode/credentials.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,11 @@ def from_env(cls) -> Credentials:
6565
) from exc
6666

6767
@classmethod
68-
def from_available(cls) -> Credentials:
68+
def from_available(cls, profile: str = "default") -> Credentials:
6969
if os.environ.get("SFDC_USERNAME"):
7070
return cls.from_env()
7171
if os.path.exists(INI_FILE):
72-
return cls.from_ini()
72+
return cls.from_ini(profile=profile)
7373
raise ValueError(
7474
"Credentials not found in env or ini file. "
7575
"Run `datacustomcode configure` to create a credentials file."

src/datacustomcode/io/reader/query_api.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,11 @@ class QueryAPIDataCloudReader(BaseDataCloudReader):
7575

7676
CONFIG_NAME = "QueryAPIDataCloudReader"
7777

78-
def __init__(self, spark: SparkSession) -> None:
78+
def __init__(
79+
self, spark: SparkSession, credentials_profile: str = "default"
80+
) -> None:
7981
self.spark = spark
80-
credentials = Credentials.from_available()
82+
credentials = Credentials.from_available(profile=credentials_profile)
8183

8284
self._conn = SalesforceCDPConnection(
8385
credentials.login_url,

src/datacustomcode/io/writer/print.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,17 @@ class PrintDataCloudWriter(BaseDataCloudWriter):
2626
CONFIG_NAME = "PrintDataCloudWriter"
2727

2828
def __init__(
29-
self, spark: SparkSession, reader: Optional[QueryAPIDataCloudReader] = None
29+
self,
30+
spark: SparkSession,
31+
reader: Optional[QueryAPIDataCloudReader] = None,
32+
credentials_profile: str = "default",
3033
) -> None:
3134
super().__init__(spark)
32-
self.reader = QueryAPIDataCloudReader(self.spark) if reader is None else reader
35+
self.reader = (
36+
QueryAPIDataCloudReader(self.spark, credentials_profile)
37+
if reader is None
38+
else reader
39+
)
3340

3441
def validate_dataframe_columns_against_dlo(
3542
self,

src/datacustomcode/run.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,25 @@
2020

2121

2222
def run_entrypoint(
23-
entrypoint: str, config_file: Union[str, None], dependencies: List[str]
23+
entrypoint: str,
24+
config_file: Union[str, None],
25+
dependencies: List[str],
26+
profile: str,
2427
) -> None:
2528
"""Run the entrypoint script with the given config and dependencies.
2629
2730
Args:
2831
entrypoint: The entrypoint script to run.
2932
config_file: The config file to use.
3033
dependencies: The dependencies to import.
34+
profile: The profile to use.
3135
"""
36+
if profile != "default":
37+
if config.reader_config and hasattr(config.reader_config, "options"):
38+
config.reader_config.options["credentials_profile"] = profile
39+
if config.writer_config and hasattr(config.writer_config, "options"):
40+
config.writer_config.options["credentials_profile"] = profile
41+
3242
if config_file:
3343
config.load(config_file)
3444
for dependency in dependencies:

tests/test_credentials.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,3 +244,70 @@ def test_update_ini_new_profile(self):
244244

245245
# Check that existing profile was not modified
246246
assert mock_config["existing"]["username"] == "existing_user"
247+
248+
def test_from_available_with_custom_profile(self):
249+
"""Test that from_available uses custom profile when specified."""
250+
ini_content = """
251+
[default]
252+
username = default_user
253+
password = default_pass
254+
client_id = default_client_id
255+
client_secret = default_secret
256+
login_url = https://default.login.url
257+
258+
[custom_profile]
259+
username = custom_user
260+
password = custom_pass
261+
client_id = custom_client_id
262+
client_secret = custom_secret
263+
login_url = https://custom.login.url
264+
"""
265+
266+
with (
267+
patch("datacustomcode.credentials.INI_FILE", "fake_path"),
268+
patch("os.path.exists", return_value=True),
269+
patch("builtins.open", mock_open(read_data=ini_content)),
270+
):
271+
# Mock the configparser behavior for reading the file
272+
mock_config = configparser.ConfigParser()
273+
mock_config.read_string(ini_content)
274+
275+
with patch.object(configparser, "ConfigParser", return_value=mock_config):
276+
# Test default profile
277+
creds_default = Credentials.from_available()
278+
assert creds_default.username == "default_user"
279+
assert creds_default.login_url == "https://default.login.url"
280+
281+
# Test custom profile
282+
creds_custom = Credentials.from_available(profile="custom_profile")
283+
assert creds_custom.username == "custom_user"
284+
assert creds_custom.password == "custom_pass"
285+
assert creds_custom.client_id == "custom_client_id"
286+
assert creds_custom.client_secret == "custom_secret"
287+
assert creds_custom.login_url == "https://custom.login.url"
288+
289+
def test_from_available_fallback_to_default(self):
290+
"""Test that from_available falls back to default when no profile specified."""
291+
ini_content = """
292+
[default]
293+
username = default_user
294+
password = default_pass
295+
client_id = default_client_id
296+
client_secret = default_secret
297+
login_url = https://default.login.url
298+
"""
299+
300+
with (
301+
patch("datacustomcode.credentials.INI_FILE", "fake_path"),
302+
patch("os.path.exists", return_value=True),
303+
patch("builtins.open", mock_open(read_data=ini_content)),
304+
):
305+
# Mock the configparser behavior for reading the file
306+
mock_config = configparser.ConfigParser()
307+
mock_config.read_string(ini_content)
308+
309+
with patch.object(configparser, "ConfigParser", return_value=mock_config):
310+
# Test that no profile parameter defaults to "default"
311+
creds = Credentials.from_available()
312+
assert creds.username == "default_user"
313+
assert creds.login_url == "https://default.login.url"

tests/test_run.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ def test_run_entrypoint_preserves_config(test_config_file, test_entrypoint_file)
9696
entrypoint=test_entrypoint_file,
9797
config_file=test_config_file,
9898
dependencies=[],
99+
profile="default",
99100
)
100101

101102
# Check that config was maintained
@@ -180,6 +181,7 @@ def test_run_entrypoint_with_dependencies():
180181
entrypoint=entrypoint_file,
181182
config_file=config_file,
182183
dependencies=[module_name],
184+
profile="default",
183185
)
184186

185187
# Verify dependency was imported and used

0 commit comments

Comments
 (0)