Skip to content

Commit 87454ec

Browse files
committed
Use local SDK config.
Added a local SDK-specific config file to store the type of code package for use with SDK commands.
1 parent c029e28 commit 87454ec

File tree

4 files changed

+177
-22
lines changed

4 files changed

+177
-22
lines changed

src/datacustomcode/cli.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,11 @@ def deploy(
147147
@click.argument("directory", default=".")
148148
@click.option("--type", default="script", type=click.Choice(["script", "function"]))
149149
def init(directory: str, type: str):
150-
from datacustomcode.scan import dc_config_json_from_file, update_config
150+
from datacustomcode.scan import (
151+
dc_config_json_from_file,
152+
update_config,
153+
write_sdk_config,
154+
)
151155
from datacustomcode.template import copy_function_template, copy_script_template
152156

153157
click.echo("Copying template to " + click.style(directory, fg="blue", bold=True))
@@ -157,6 +161,11 @@ def init(directory: str, type: str):
157161
copy_function_template(directory)
158162
entrypoint_path = os.path.join(directory, "payload", "entrypoint.py")
159163
config_location = os.path.join(os.path.dirname(entrypoint_path), "config.json")
164+
165+
# Write package type to SDK-specific config
166+
sdk_config = {"type": type}
167+
write_sdk_config(directory, sdk_config)
168+
160169
config_json = dc_config_json_from_file(entrypoint_path, type)
161170
with open(config_location, "w") as f:
162171
json.dump(config_json, f, indent=2)

src/datacustomcode/scan.py

Lines changed: 112 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
import ast
1818
import json
19-
import logging
2019
import os
2120
import sys
2221
from typing import (
@@ -27,17 +26,15 @@
2726
Union,
2827
)
2928

29+
from loguru import logger
3030
import pydantic
3131

3232
from datacustomcode.version import get_version
3333

34-
logger = logging.getLogger(__name__)
35-
3634
DATA_ACCESS_METHODS = ["read_dlo", "read_dmo", "write_to_dlo", "write_to_dmo"]
3735

3836
DATA_TRANSFORM_CONFIG_TEMPLATE = {
3937
"sdkVersion": get_version(),
40-
"type": "script",
4138
"entryPoint": "",
4239
"dataspace": "default",
4340
"permissions": {
@@ -48,11 +45,90 @@
4845

4946
FUNCTION_CONFIG_TEMPLATE = {
5047
"sdkVersion": get_version(),
51-
"type": "function",
5248
"entryPoint": "",
5349
}
5450
STANDARD_LIBS = set(sys.stdlib_module_names)
5551

52+
SDK_CONFIG_DIR = ".datacustomcode_proj"
53+
SDK_CONFIG_FILE = "sdk_config.json"
54+
55+
56+
def get_sdk_config_path(base_directory: str) -> str:
57+
"""Get the path to the SDK-specific config file.
58+
59+
Args:
60+
base_directory: The base directory of the project
61+
(where .datacustomcode should be)
62+
63+
Returns:
64+
The path to the SDK config file
65+
"""
66+
sdk_config_dir = os.path.join(base_directory, SDK_CONFIG_DIR)
67+
return os.path.join(sdk_config_dir, SDK_CONFIG_FILE)
68+
69+
70+
def read_sdk_config(base_directory: str) -> dict[str, Any]:
71+
"""Read the SDK-specific config file.
72+
73+
Args:
74+
base_directory: The base directory of the project
75+
76+
Returns:
77+
The SDK config dictionary, or empty dict if file doesn't exist
78+
"""
79+
config_path = get_sdk_config_path(base_directory)
80+
if os.path.exists(config_path) and os.path.isfile(config_path):
81+
try:
82+
with open(config_path, "r") as f:
83+
config_data: dict[str, Any] = json.load(f)
84+
return config_data
85+
except json.JSONDecodeError as e:
86+
raise ValueError(f"Failed to parse JSON from {config_path}: {e}") from e
87+
except OSError as e:
88+
raise OSError(f"Failed to read SDK config file {config_path}: {e}") from e
89+
else:
90+
raise FileNotFoundError(f"SDK config file not found at {config_path}")
91+
92+
93+
def write_sdk_config(base_directory: str, config: dict[str, Any]) -> None:
94+
"""Write the SDK-specific config file.
95+
96+
Args:
97+
base_directory: The base directory of the project
98+
config: The config dictionary to write
99+
"""
100+
config_path = get_sdk_config_path(base_directory)
101+
sdk_config_dir = os.path.dirname(config_path)
102+
os.makedirs(sdk_config_dir, exist_ok=True)
103+
with open(config_path, "w") as f:
104+
json.dump(config, f, indent=2)
105+
106+
107+
def get_package_type(base_directory: str) -> str:
108+
"""Get the package type (script or function) from SDK config.
109+
110+
Args:
111+
base_directory: The base directory of the project
112+
113+
Returns:
114+
The package type ("script" or "function")
115+
116+
Raises:
117+
ValueError: If the type is not found in the SDK config
118+
"""
119+
try:
120+
sdk_config = read_sdk_config(base_directory)
121+
except FileNotFoundError as e:
122+
logger.debug(f"Defaulting to script package type. {e}")
123+
return "script"
124+
if "type" not in sdk_config:
125+
config_path = get_sdk_config_path(base_directory)
126+
raise ValueError(
127+
f"Package type not found in SDK config at {config_path}. "
128+
"Please run 'datacustomcode init' to initialize the project."
129+
)
130+
return str(sdk_config["type"])
131+
56132

57133
class DataAccessLayerCalls(pydantic.BaseModel):
58134
read_dlo: frozenset[str]
@@ -247,6 +323,31 @@ def dc_config_json_from_file(file_path: str, type: str) -> dict[str, Any]:
247323
return config
248324

249325

326+
def find_base_directory(file_path: str) -> str:
327+
"""Find the base directory containing .datacustomcode by walking up from file_path.
328+
329+
Args:
330+
file_path: Path to a file in the project
331+
332+
Returns:
333+
The base directory path, or the directory containing the file if not found
334+
"""
335+
current_dir = os.path.dirname(os.path.abspath(file_path))
336+
root = os.path.abspath(os.sep)
337+
338+
while current_dir != root:
339+
if os.path.exists(os.path.join(current_dir, SDK_CONFIG_DIR)):
340+
return current_dir
341+
current_dir = os.path.dirname(current_dir)
342+
343+
# If not found, assume the payload directory's parent is the base
344+
# (payload/entrypoint.py -> base directory is parent of payload)
345+
file_dir = os.path.dirname(os.path.abspath(file_path))
346+
if os.path.basename(file_dir) == "payload":
347+
return os.path.dirname(file_dir)
348+
return file_dir
349+
350+
250351
def update_config(file_path: str) -> dict[str, Any]:
251352
file_dir = os.path.dirname(file_path)
252353
config_json_path = os.path.join(file_dir, "config.json")
@@ -263,7 +364,12 @@ def update_config(file_path: str) -> dict[str, Any]:
263364
raise OSError(f"Failed to read config file {config_json_path}: {e}") from e
264365
else:
265366
raise ValueError(f"config.json not found at {config_json_path}")
266-
if existing_config["type"] == "script":
367+
368+
# Get package type from SDK config
369+
base_directory = find_base_directory(file_path)
370+
package_type = get_package_type(base_directory)
371+
372+
if package_type == "script":
267373
existing_config["dataspace"] = get_dataspace(existing_config)
268374
output = scan_file(file_path)
269375
read: dict[str, list[str]] = {}

tests/test_cli.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@ class TestInit:
1111
@patch("datacustomcode.template.copy_script_template")
1212
@patch("datacustomcode.scan.update_config")
1313
@patch("datacustomcode.scan.dc_config_json_from_file")
14+
@patch("datacustomcode.scan.write_sdk_config")
1415
@patch("builtins.open", new_callable=mock_open)
15-
def test_init_command(self, mock_file, mock_scan, mock_update, mock_copy):
16+
def test_init_command(
17+
self, mock_file, mock_write_sdk, mock_scan, mock_update, mock_copy
18+
):
1619
"""Test init command."""
1720
mock_scan.return_value = {
1821
"sdkVersion": "1.0.0",
@@ -42,6 +45,8 @@ def test_init_command(self, mock_file, mock_scan, mock_update, mock_copy):
4245

4346
assert result.exit_code == 0
4447
mock_copy.assert_called_once_with("test_dir")
48+
# Verify SDK config was written
49+
mock_write_sdk.assert_called_once_with("test_dir", {"type": "script"})
4550
mock_scan.assert_called_once_with(
4651
os.path.join("test_dir", "payload", "entrypoint.py"), "script"
4752
)

tests/test_scan.py

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@
99
import pytest
1010

1111
from datacustomcode.scan import (
12+
SDK_CONFIG_DIR,
1213
DataAccessLayerCalls,
1314
dc_config_json_from_file,
1415
scan_file,
1516
scan_file_for_imports,
1617
update_config,
1718
write_requirements_file,
19+
write_sdk_config,
1820
)
1921
from datacustomcode.version import get_version
2022

@@ -27,6 +29,21 @@ def create_test_script(content: str) -> str:
2729
return temp_path
2830

2931

32+
def create_sdk_config(base_directory: str, package_type: str = "script") -> str:
33+
"""Create SDK config file for testing.
34+
35+
Args:
36+
base_directory: The base directory where .datacustomcode should be created
37+
package_type: The package type ("script" or "function")
38+
39+
Returns:
40+
Path to the created SDK config file
41+
"""
42+
sdk_config = {"type": package_type}
43+
write_sdk_config(base_directory, sdk_config)
44+
return os.path.join(base_directory, SDK_CONFIG_DIR, "config.json")
45+
46+
3047
class TestClientMethodVisitor:
3148
def test_variable_tracking(self):
3249
"""Test that the visitor can track variable assignments."""
@@ -301,6 +318,9 @@ def test_dlo_to_dlo_config(self):
301318
"""
302319
)
303320
temp_path = create_test_script(content)
321+
file_dir = os.path.dirname(temp_path)
322+
# Create SDK config in the same directory as the script
323+
sdk_config_path = create_sdk_config(file_dir, "script")
304324
try:
305325
result = dc_config_json_from_file(temp_path, "script")
306326
assert result["entryPoint"] == os.path.basename(temp_path)
@@ -315,6 +335,9 @@ def test_dlo_to_dlo_config(self):
315335
assert result["permissions"]["write"]["dlo"] == ["output_dlo"]
316336
finally:
317337
os.remove(temp_path)
338+
if os.path.exists(sdk_config_path):
339+
os.remove(sdk_config_path)
340+
os.rmdir(os.path.dirname(sdk_config_path))
318341

319342
def test_dmo_to_dmo_config(self):
320343
"""Test generating config JSON for DMO to DMO operations."""
@@ -332,6 +355,9 @@ def test_dmo_to_dmo_config(self):
332355
"""
333356
)
334357
temp_path = create_test_script(content)
358+
file_dir = os.path.dirname(temp_path)
359+
# Create SDK config in the same directory as the script
360+
sdk_config_path = create_sdk_config(file_dir, "script")
335361
try:
336362
config = dc_config_json_from_file(temp_path, "script")
337363
assert config["entryPoint"] == os.path.basename(temp_path)
@@ -342,6 +368,9 @@ def test_dmo_to_dmo_config(self):
342368
assert config["permissions"]["write"]["dmo"] == ["output_dmo"]
343369
finally:
344370
os.remove(temp_path)
371+
if os.path.exists(sdk_config_path):
372+
os.remove(sdk_config_path)
373+
os.rmdir(os.path.dirname(sdk_config_path))
345374

346375
@patch(
347376
"datacustomcode.scan.DATA_TRANSFORM_CONFIG_TEMPLATE",
@@ -373,7 +402,10 @@ def test_preserves_existing_dataspace(self):
373402
config_path = os.path.join(file_dir, "config.json")
374403

375404
try:
405+
# Create SDK config
406+
sdk_config_path = create_sdk_config(file_dir, "script")
376407
# Create an existing config.json with a custom dataspace
408+
# (without type field)
377409
existing_config = {
378410
"sdkVersion": "1.0.0",
379411
"entryPoint": "test.py",
@@ -396,11 +428,13 @@ def test_preserves_existing_dataspace(self):
396428
os.remove(temp_path)
397429
if os.path.exists(config_path):
398430
os.remove(config_path)
431+
if os.path.exists(sdk_config_path):
432+
os.remove(sdk_config_path)
433+
os.rmdir(os.path.dirname(sdk_config_path))
399434

400-
def test_uses_default_for_empty_dataspace(self, caplog):
435+
def test_uses_default_for_empty_dataspace(self):
401436
"""Test that empty dataspace value uses default and logs warning."""
402437
import json
403-
import logging
404438

405439
content = textwrap.dedent(
406440
"""
@@ -416,11 +450,12 @@ def test_uses_default_for_empty_dataspace(self, caplog):
416450
config_path = os.path.join(file_dir, "config.json")
417451

418452
try:
419-
# Create an existing config.json with empty dataspace
453+
# Create SDK config
454+
sdk_config_path = create_sdk_config(file_dir, "script")
455+
# Create an existing config.json with empty dataspace (without type field)
420456
existing_config = {
421457
"sdkVersion": "1.0.0",
422458
"entryPoint": "test.py",
423-
"type": "script",
424459
"dataspace": "",
425460
"permissions": {
426461
"read": {"dlo": ["old_dlo"]},
@@ -431,24 +466,19 @@ def test_uses_default_for_empty_dataspace(self, caplog):
431466
json.dump(existing_config, f)
432467

433468
# Should use "default" for empty dataspace (not raise error)
434-
with caplog.at_level(logging.WARNING):
435-
result = update_config(temp_path)
469+
result = update_config(temp_path)
436470

437471
assert result["dataspace"] == "default"
438472
assert result["permissions"]["read"]["dlo"] == ["input_dlo"]
439473
assert result["permissions"]["write"]["dlo"] == ["output_dlo"]
440474

441-
# Verify that a warning was logged
442-
assert len(caplog.records) > 0
443-
assert any(
444-
"dataspace" in record.message.lower()
445-
and "empty" in record.message.lower()
446-
for record in caplog.records
447-
)
448475
finally:
449476
os.remove(temp_path)
450477
if os.path.exists(config_path):
451478
os.remove(config_path)
479+
if os.path.exists(sdk_config_path):
480+
os.remove(sdk_config_path)
481+
os.rmdir(os.path.dirname(sdk_config_path))
452482

453483
def test_uses_default_dataspace_when_no_config(self):
454484
"""Test missing config.json uses default dataspace."""
@@ -488,11 +518,13 @@ def test_rejects_missing_dataspace(self):
488518
config_path = os.path.join(file_dir, "config.json")
489519

490520
try:
521+
# Create SDK config
522+
sdk_config_path = create_sdk_config(file_dir, "script")
491523
# Create an existing config.json without dataspace field
524+
# (without type field)
492525
existing_config = {
493526
"sdkVersion": "1.0.0",
494527
"entryPoint": "test.py",
495-
"type": "script",
496528
"permissions": {
497529
"read": {"dlo": ["old_dlo"]},
498530
"write": {"dlo": ["old_output"]},
@@ -512,6 +544,9 @@ def test_rejects_missing_dataspace(self):
512544
os.remove(temp_path)
513545
if os.path.exists(config_path):
514546
os.remove(config_path)
547+
if os.path.exists(sdk_config_path):
548+
os.remove(sdk_config_path)
549+
os.rmdir(os.path.dirname(sdk_config_path))
515550

516551
def test_raises_error_on_invalid_json(self):
517552
"""Test that invalid JSON in config.json raises an error."""

0 commit comments

Comments
 (0)