|
16 | 16 |
|
17 | 17 | import ast |
18 | 18 | import json |
19 | | -import logging |
20 | 19 | import os |
21 | 20 | import sys |
22 | 21 | from typing import ( |
|
27 | 26 | Union, |
28 | 27 | ) |
29 | 28 |
|
| 29 | +from loguru import logger |
30 | 30 | import pydantic |
31 | 31 |
|
32 | 32 | from datacustomcode.version import get_version |
33 | 33 |
|
34 | | -logger = logging.getLogger(__name__) |
35 | | - |
36 | 34 | DATA_ACCESS_METHODS = ["read_dlo", "read_dmo", "write_to_dlo", "write_to_dmo"] |
37 | 35 |
|
38 | 36 | DATA_TRANSFORM_CONFIG_TEMPLATE = { |
39 | 37 | "sdkVersion": get_version(), |
40 | 38 | "entryPoint": "", |
41 | | - "dataspace": "", |
| 39 | + "dataspace": "default", |
42 | 40 | "permissions": { |
43 | 41 | "read": {}, |
44 | 42 | "write": {}, |
45 | 43 | }, |
46 | 44 | } |
47 | 45 |
|
| 46 | +FUNCTION_CONFIG_TEMPLATE = { |
| 47 | + "sdkVersion": get_version(), |
| 48 | + "entryPoint": "", |
| 49 | +} |
48 | 50 | STANDARD_LIBS = set(sys.stdlib_module_names) |
49 | 51 |
|
| 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 | + |
50 | 132 |
|
51 | 133 | class DataAccessLayerCalls(pydantic.BaseModel): |
52 | 134 | read_dlo: frozenset[str] |
@@ -230,57 +312,96 @@ def scan_file(file_path: str) -> DataAccessLayerCalls: |
230 | 312 | return visitor.found() |
231 | 313 |
|
232 | 314 |
|
233 | | -def dc_config_json_from_file(file_path: str) -> dict[str, Any]: |
| 315 | +def dc_config_json_from_file(file_path: str, type: str) -> dict[str, Any]: |
234 | 316 | """Create a Data Cloud Custom Code config JSON from a script.""" |
235 | | - output = scan_file(file_path) |
236 | | - config = DATA_TRANSFORM_CONFIG_TEMPLATE.copy() |
| 317 | + config: dict[str, Any] |
| 318 | + if type == "script": |
| 319 | + config = DATA_TRANSFORM_CONFIG_TEMPLATE.copy() |
| 320 | + elif type == "function": |
| 321 | + config = FUNCTION_CONFIG_TEMPLATE.copy() |
237 | 322 | config["entryPoint"] = file_path.rpartition("/")[-1] |
| 323 | + return config |
| 324 | + |
| 325 | + |
| 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) |
238 | 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 | + |
| 351 | +def update_config(file_path: str) -> dict[str, Any]: |
239 | 352 | file_dir = os.path.dirname(file_path) |
240 | 353 | config_json_path = os.path.join(file_dir, "config.json") |
241 | | - |
| 354 | + existing_config: dict[str, Any] |
242 | 355 | if os.path.exists(config_json_path) and os.path.isfile(config_json_path): |
243 | 356 | try: |
244 | 357 | with open(config_json_path, "r") as f: |
245 | 358 | existing_config = json.load(f) |
246 | | - |
247 | | - if "dataspace" in existing_config: |
248 | | - dataspace_value = existing_config["dataspace"] |
249 | | - if not dataspace_value or ( |
250 | | - isinstance(dataspace_value, str) and dataspace_value.strip() == "" |
251 | | - ): |
252 | | - logger.warning( |
253 | | - f"dataspace in {config_json_path} is empty or None. " |
254 | | - f"Updating config file to use dataspace 'default'. " |
255 | | - ) |
256 | | - config["dataspace"] = "default" |
257 | | - else: |
258 | | - config["dataspace"] = dataspace_value |
259 | | - else: |
260 | | - raise ValueError( |
261 | | - f"dataspace must be defined in {config_json_path}. " |
262 | | - f"Please add a 'dataspace' field to the config.json file. " |
263 | | - ) |
264 | 359 | except json.JSONDecodeError as e: |
265 | 360 | raise ValueError( |
266 | 361 | f"Failed to parse JSON from {config_json_path}: {e}" |
267 | 362 | ) from e |
268 | 363 | except OSError as e: |
269 | 364 | raise OSError(f"Failed to read config file {config_json_path}: {e}") from e |
270 | 365 | else: |
271 | | - config["dataspace"] = "default" |
272 | | - |
273 | | - read: dict[str, list[str]] = {} |
274 | | - if output.read_dlo: |
275 | | - read["dlo"] = list(output.read_dlo) |
276 | | - else: |
277 | | - read["dmo"] = list(output.read_dmo) |
278 | | - write: dict[str, list[str]] = {} |
279 | | - if output.write_to_dlo: |
280 | | - write["dlo"] = list(output.write_to_dlo) |
| 366 | + raise ValueError(f"config.json not found at {config_json_path}") |
| 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": |
| 373 | + existing_config["dataspace"] = get_dataspace(existing_config) |
| 374 | + output = scan_file(file_path) |
| 375 | + read: dict[str, list[str]] = {} |
| 376 | + if output.read_dlo: |
| 377 | + read["dlo"] = list(output.read_dlo) |
| 378 | + else: |
| 379 | + read["dmo"] = list(output.read_dmo) |
| 380 | + write: dict[str, list[str]] = {} |
| 381 | + if output.write_to_dlo: |
| 382 | + write["dlo"] = list(output.write_to_dlo) |
| 383 | + else: |
| 384 | + write["dmo"] = list(output.write_to_dmo) |
| 385 | + |
| 386 | + existing_config["permissions"] = {"read": read, "write": write} |
| 387 | + return existing_config |
| 388 | + |
| 389 | + |
| 390 | +def get_dataspace(existing_config: dict[str, str]) -> str: |
| 391 | + if "dataspace" in existing_config: |
| 392 | + dataspace_value = existing_config["dataspace"] |
| 393 | + if not dataspace_value or ( |
| 394 | + isinstance(dataspace_value, str) and dataspace_value.strip() == "" |
| 395 | + ): |
| 396 | + logger.warning( |
| 397 | + "dataspace is empty or None. " |
| 398 | + "Updating config file to use dataspace 'default'. " |
| 399 | + ) |
| 400 | + return "default" |
| 401 | + else: |
| 402 | + return dataspace_value |
281 | 403 | else: |
282 | | - write["dmo"] = list(output.write_to_dmo) |
283 | | - |
284 | | - config["permissions"] = {"read": read, "write": write} |
285 | | - |
286 | | - return config |
| 404 | + raise ValueError( |
| 405 | + "dataspace must be defined. " |
| 406 | + "Please add a 'dataspace' field to the config.json file. " |
| 407 | + ) |
0 commit comments