salesforce-data-customcode 6.0.6.dev2__tar.gz → 6.0.6.dev3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/PKG-INFO +1 -1
  2. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/pyproject.toml +1 -1
  3. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/cli.py +29 -4
  4. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/constants.py +8 -0
  5. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/deploy.py +55 -16
  6. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/scan.py +164 -29
  7. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/template.py +13 -1
  8. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/LICENSE.txt +0 -0
  9. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/README.md +0 -0
  10. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/__init__.py +0 -0
  11. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/auth.py +0 -0
  12. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/client.py +0 -0
  13. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/cmd.py +0 -0
  14. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/common_config.py +0 -0
  15. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/config.py +0 -0
  16. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/config.yaml +0 -0
  17. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/credentials.py +0 -0
  18. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/einstein_platform_client.py +0 -0
  19. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/einstein_platform_config.py +0 -0
  20. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/einstein_predictions/__init__.py +0 -0
  21. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/einstein_predictions/base.py +0 -0
  22. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/einstein_predictions/errors.py +0 -0
  23. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/einstein_predictions/impl/default.py +0 -0
  24. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/einstein_predictions/spark_base.py +0 -0
  25. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/einstein_predictions/spark_default.py +0 -0
  26. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/einstein_predictions/types.py +0 -0
  27. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/einstein_predictions_config.py +0 -0
  28. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/file/__init__.py +0 -0
  29. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/file/base.py +0 -0
  30. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/file/path/__init__.py +0 -0
  31. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/file/path/default.py +0 -0
  32. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/function/__init__.py +0 -0
  33. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/function/base.py +0 -0
  34. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/function/feature_types/__init__.py +0 -0
  35. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/function/feature_types/chunking.py +0 -0
  36. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/function/runtime.py +0 -0
  37. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/function_utils.py +0 -0
  38. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/io/__init__.py +0 -0
  39. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/io/base.py +0 -0
  40. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/io/reader/__init__.py +0 -0
  41. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/io/reader/base.py +0 -0
  42. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/io/reader/query_api.py +0 -0
  43. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/io/reader/sf_cli.py +0 -0
  44. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/io/reader/utils.py +0 -0
  45. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/io/writer/__init__.py +0 -0
  46. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/io/writer/base.py +0 -0
  47. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/io/writer/csv.py +0 -0
  48. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/io/writer/print.py +0 -0
  49. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/llm_gateway/__init__.py +0 -0
  50. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/llm_gateway/base.py +0 -0
  51. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/llm_gateway/default.py +0 -0
  52. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/llm_gateway/errors.py +0 -0
  53. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/llm_gateway/spark_base.py +0 -0
  54. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/llm_gateway/spark_default.py +0 -0
  55. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/llm_gateway/types/__init__.py +0 -0
  56. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/llm_gateway/types/generate_text_request.py +0 -0
  57. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/llm_gateway/types/generate_text_request_builder.py +0 -0
  58. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/llm_gateway/types/generate_text_response.py +0 -0
  59. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/llm_gateway/types/generate_text_response_builder.py +0 -0
  60. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/llm_gateway_config.py +0 -0
  61. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/mixin.py +0 -0
  62. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/py.typed +0 -0
  63. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/run.py +0 -0
  64. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/spark/__init__.py +0 -0
  65. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/spark/base.py +0 -0
  66. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/spark/default.py +0 -0
  67. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/__init__.py +0 -0
  68. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/function/.devcontainer/devcontainer.json +0 -0
  69. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/function/Dockerfile.dependencies +0 -0
  70. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/function/README.md +0 -0
  71. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/function/build_native_dependencies.sh +0 -0
  72. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/function/chunking/payload/config.json +0 -0
  73. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/function/chunking/payload/entrypoint.py +0 -0
  74. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/function/chunking/requirements.txt +0 -0
  75. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/function/example/chunking_with_llm/config.json +0 -0
  76. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/function/example/chunking_with_llm/entrypoint.py +0 -0
  77. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/function/example/chunking_with_llm/files/chunking_prompt.txt +0 -0
  78. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/function/example/chunking_with_llm/tests/test.json +0 -0
  79. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/function/example/chunking_with_prediction/config.json +0 -0
  80. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/function/example/chunking_with_prediction/entrypoint.py +0 -0
  81. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/function/example/chunking_with_prediction/tests/test.json +0 -0
  82. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/function/payload/config.json +0 -0
  83. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/function/payload/entrypoint.py +0 -0
  84. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/function/payload/utility.py +0 -0
  85. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/function/requirements-dev.txt +0 -0
  86. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/function/requirements.txt +0 -0
  87. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/script/.devcontainer/devcontainer.json +0 -0
  88. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/script/Dockerfile +0 -0
  89. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/script/Dockerfile.dependencies +0 -0
  90. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/script/README.md +0 -0
  91. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/script/account.ipynb +0 -0
  92. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/script/build_native_dependencies.sh +0 -0
  93. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/script/examples/employee_hierarchy/employee_data.csv +0 -0
  94. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/script/examples/employee_hierarchy/entrypoint.py +0 -0
  95. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/script/examples/streaming_deltas/entrypoint.py +0 -0
  96. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/script/jupyterlab.sh +0 -0
  97. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/script/payload/config.json +0 -0
  98. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/script/payload/entrypoint.py +0 -0
  99. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/script/requirements-dev.txt +0 -0
  100. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/script/requirements.txt +0 -0
  101. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/token_provider.py +0 -0
  102. {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: salesforce-data-customcode
3
- Version: 6.0.6.dev2
3
+ Version: 6.0.6.dev3
4
4
  Summary: Data Cloud Custom Code SDK
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE.txt
@@ -18,7 +18,7 @@ license = "Apache-2.0"
18
18
  name = "salesforce-data-customcode"
19
19
  readme = "README.md"
20
20
  requires-python = ">=3.10,<3.12"
21
- version = "6.0.6.dev2"
21
+ version = "6.0.6.dev3"
22
22
 
23
23
  [tool.black]
24
24
  exclude = '''
@@ -283,10 +283,19 @@ def deploy(
283
283
  )
284
284
  @click.option(
285
285
  "--use-in-feature",
286
- default="SearchIndexChunking",
287
- help="Feature where this function will be used (only applicable for function).",
286
+ "-u",
287
+ default=None,
288
+ help=(
289
+ "Invoke option for this package. For scripts: 'BatchTransform' "
290
+ "(default) or 'StreamingTransform'. For functions: 'SearchIndexChunking'."
291
+ ),
288
292
  )
289
293
  def init(directory: str, code_type: str, use_in_feature: Optional[str]):
294
+ from datacustomcode.constants import (
295
+ SCRIPT_USE_IN_FEATURE_BATCH,
296
+ SCRIPT_USE_IN_FEATURE_OPTIONS,
297
+ SCRIPT_USE_IN_FEATURE_STREAMING,
298
+ )
290
299
  from datacustomcode.scan import (
291
300
  dc_config_json_from_file,
292
301
  update_config,
@@ -294,9 +303,23 @@ def init(directory: str, code_type: str, use_in_feature: Optional[str]):
294
303
  )
295
304
  from datacustomcode.template import copy_function_template, copy_script_template
296
305
 
306
+ streaming = False
307
+ if code_type == "script":
308
+ use_in_feature = use_in_feature or SCRIPT_USE_IN_FEATURE_BATCH
309
+ if use_in_feature not in SCRIPT_USE_IN_FEATURE_OPTIONS:
310
+ click.secho(
311
+ f"Error: Invalid --use-in-feature '{use_in_feature}' for a "
312
+ f"script. Valid options: {', '.join(SCRIPT_USE_IN_FEATURE_OPTIONS)}.",
313
+ fg="red",
314
+ )
315
+ raise click.Abort()
316
+ streaming = use_in_feature == SCRIPT_USE_IN_FEATURE_STREAMING
317
+ else:
318
+ use_in_feature = use_in_feature or "SearchIndexChunking"
319
+
297
320
  click.echo("Copying template to " + click.style(directory, fg="blue", bold=True))
298
321
  if code_type == "script":
299
- copy_script_template(directory)
322
+ copy_script_template(directory, streaming=streaming)
300
323
  elif code_type == "function":
301
324
  copy_function_template(directory, use_in_feature)
302
325
  entrypoint_path = os.path.join(directory, PAYLOAD_DIR, ENTRYPOINT_FILE)
@@ -306,7 +329,9 @@ def init(directory: str, code_type: str, use_in_feature: Optional[str]):
306
329
  sdk_config = {"type": code_type}
307
330
  write_sdk_config(directory, sdk_config)
308
331
 
309
- config_json = dc_config_json_from_file(entrypoint_path, code_type)
332
+ config_json = dc_config_json_from_file(
333
+ entrypoint_path, code_type, streaming=streaming
334
+ )
310
335
  with open(config_location, "w") as f:
311
336
  json.dump(config_json, f, indent=2)
312
337
 
@@ -38,6 +38,14 @@ USE_IN_FEATURE_MAPPING_FOR_CONNECT_API = {
38
38
  "SearchIndexChunking": "UnstructuredChunking",
39
39
  }
40
40
 
41
+ # Script (data transform) invoke options
42
+ SCRIPT_USE_IN_FEATURE_BATCH = "BatchTransform"
43
+ SCRIPT_USE_IN_FEATURE_STREAMING = "StreamingTransform"
44
+ SCRIPT_USE_IN_FEATURE_OPTIONS = [
45
+ SCRIPT_USE_IN_FEATURE_BATCH,
46
+ SCRIPT_USE_IN_FEATURE_STREAMING,
47
+ ]
48
+
41
49
  # Pydantic request/response type names to feature names
42
50
  REQUEST_TYPE_TO_FEATURE = {
43
51
  "SearchIndexChunkingV1Request": "SearchIndexChunking",
@@ -14,6 +14,7 @@
14
14
  # limitations under the License.
15
15
  from __future__ import annotations
16
16
 
17
+ import copy
17
18
  from html import unescape
18
19
  import json
19
20
  import os
@@ -41,6 +42,7 @@ from datacustomcode.scan import find_base_directory, get_package_type
41
42
 
42
43
  DATA_CUSTOM_CODE_PATH = "services/data/v63.0/ssot/data-custom-code"
43
44
  DATA_TRANSFORMS_PATH = "services/data/v63.0/ssot/data-transforms"
45
+ DATA_CUSTOM_CODE_INVOKE_OPTIONS_PATH = "services/data/v67.0/ssot/data-custom-code"
44
46
  WAIT_FOR_DEPLOYMENT_TIMEOUT = 3000
45
47
 
46
48
  # Available compute types for Data Cloud deployments.
@@ -108,6 +110,7 @@ class CodeExtensionMetadata(BaseModel):
108
110
  computeType: str
109
111
  codeType: str
110
112
  functionInvokeOptions: Union[list[str], None] = None
113
+ invokeOptions: Union[list[str], None] = None
111
114
 
112
115
  def __init__(self, **data):
113
116
  name = data.get("name", "")
@@ -200,7 +203,14 @@ def create_deployment(
200
203
  access_token: AccessTokenResponse, metadata: CodeExtensionMetadata
201
204
  ) -> CreateDeploymentResponse:
202
205
  """Create a custom code deployment in the DataCloud."""
203
- url = _join_strip_url(access_token.instance_url, DATA_CUSTOM_CODE_PATH)
206
+ # invokeOptions only binds at v67.0; route there when it is set so the
207
+ # option isn't silently dropped. Everything else stays on v63.0.
208
+ code_custom_code_path = (
209
+ DATA_CUSTOM_CODE_INVOKE_OPTIONS_PATH
210
+ if metadata.invokeOptions
211
+ else DATA_CUSTOM_CODE_PATH
212
+ )
213
+ url = _join_strip_url(access_token.instance_url, code_custom_code_path)
204
214
  body = dict[str, Any](
205
215
  {
206
216
  "label": metadata.name,
@@ -213,6 +223,8 @@ def create_deployment(
213
223
  )
214
224
  if metadata.functionInvokeOptions:
215
225
  body["functionInvokeOptions"] = metadata.functionInvokeOptions
226
+ if metadata.invokeOptions:
227
+ body["invokeOptions"] = metadata.invokeOptions
216
228
  logger.debug(f"Creating deployment {metadata.name}...")
217
229
  try:
218
230
  response = _make_api_call(
@@ -388,6 +400,30 @@ class DataTransformConfig(BaseConfig):
388
400
  dataspace: str
389
401
  permissions: Permissions
390
402
  dataObjects: Optional[list[DataObject]] = None
403
+ streamingSource: Optional[StreamingSource] = None
404
+
405
+ @property
406
+ def is_streaming(self) -> bool:
407
+ return self.streamingSource is not None
408
+
409
+ @model_validator(mode="after")
410
+ def _validate_layers(self) -> "DataTransformConfig":
411
+ read_is_dlo = isinstance(self.permissions.read, DloPermission)
412
+ write_is_dlo = isinstance(self.permissions.write, DloPermission)
413
+ if self.is_streaming:
414
+ if not write_is_dlo:
415
+ raise ValueError(
416
+ "A streaming transform must write to a DLO "
417
+ "(permissions.write must be a 'dlo' entry)."
418
+ )
419
+ elif read_is_dlo != write_is_dlo:
420
+ raise ValueError(
421
+ "permissions.read and permissions.write must both reference "
422
+ "DLOs or both reference DMOs (got "
423
+ f"read={type(self.permissions.read).__name__}, "
424
+ f"write={type(self.permissions.write).__name__})"
425
+ )
426
+ return self
391
427
 
392
428
 
393
429
  class FunctionConfig(BaseConfig):
@@ -402,23 +438,15 @@ class DmoPermission(BaseModel):
402
438
  dmo: list[str]
403
439
 
404
440
 
441
+ class StreamingSource(BaseModel):
442
+ type: str
443
+ name: str
444
+
445
+
405
446
  class Permissions(BaseModel):
406
447
  read: Union[DloPermission, DmoPermission]
407
448
  write: Union[DloPermission, DmoPermission]
408
449
 
409
- @model_validator(mode="after")
410
- def _no_mixed_layers(self) -> "Permissions":
411
- read_is_dlo = isinstance(self.read, DloPermission)
412
- write_is_dlo = isinstance(self.write, DloPermission)
413
- if read_is_dlo != write_is_dlo:
414
- raise ValueError(
415
- "permissions.read and permissions.write must both reference "
416
- "DLOs or both reference DMOs (got "
417
- f"read={type(self.read).__name__}, "
418
- f"write={type(self.write).__name__})"
419
- )
420
- return self
421
-
422
450
 
423
451
  def _permission_entries(perm: Union[DloPermission, DmoPermission]) -> list[str]:
424
452
  """Return the list of object names regardless of layer (DLO or DMO)."""
@@ -490,7 +518,9 @@ def create_data_transform(
490
518
  ) -> dict:
491
519
  """Create a data transform in the DataCloud."""
492
520
  script_name = metadata.name
493
- request_hydrated = DATA_TRANSFORM_REQUEST_TEMPLATE.copy()
521
+ # Deep copy: the template's nested nodes/sources/macros dicts would
522
+ # otherwise be shared across calls and accumulate entries between deploys.
523
+ request_hydrated = copy.deepcopy(DATA_TRANSFORM_REQUEST_TEMPLATE)
494
524
 
495
525
  # Add nodes for each write entry (DLO or DMO)
496
526
  for i, name in enumerate(
@@ -533,7 +563,7 @@ def create_data_transform(
533
563
  "definition": definition,
534
564
  "label": f"{metadata.name}",
535
565
  "name": f"{metadata.name}",
536
- "type": "BATCH",
566
+ "type": "STREAMING" if data_transform_config.is_streaming else "BATCH",
537
567
  "dataSpaceName": data_transform_config.dataspace,
538
568
  }
539
569
 
@@ -616,9 +646,18 @@ def deploy_full(
616
646
  callback=None,
617
647
  ) -> AccessTokenResponse:
618
648
  """Deploy a data transform in the DataCloud."""
649
+ from datacustomcode.constants import SCRIPT_USE_IN_FEATURE_STREAMING
650
+
619
651
  # prepare payload
620
652
  config = get_config(directory)
621
653
 
654
+ if (
655
+ isinstance(config, DataTransformConfig)
656
+ and config.is_streaming
657
+ and not metadata.invokeOptions
658
+ ):
659
+ metadata.invokeOptions = [SCRIPT_USE_IN_FEATURE_STREAMING]
660
+
622
661
  # create deployment and upload payload
623
662
  deployment = create_deployment(access_token, metadata)
624
663
  zip(directory, docker_network, metadata.codeType)
@@ -15,6 +15,7 @@
15
15
  from __future__ import annotations
16
16
 
17
17
  import ast
18
+ import copy
18
19
  import json
19
20
  import os
20
21
  import sys
@@ -32,6 +33,8 @@ import pydantic
32
33
  from datacustomcode.version import get_version
33
34
 
34
35
  DATA_ACCESS_METHODS = ["read_dlo", "read_dmo", "write_to_dlo", "write_to_dmo"]
36
+ STREAMING_READ_METHODS = ["read_dlo_deltas", "read_dmo_deltas"]
37
+ STREAMING_WRITE_METHODS = ["write_dlo_deltas"]
35
38
 
36
39
  DATA_TRANSFORM_CONFIG_TEMPLATE = {
37
40
  "sdkVersion": get_version(),
@@ -43,6 +46,20 @@ DATA_TRANSFORM_CONFIG_TEMPLATE = {
43
46
  },
44
47
  }
45
48
 
49
+ STREAMING_TRANSFORM_CONFIG_TEMPLATE = {
50
+ "sdkVersion": get_version(),
51
+ "entryPoint": "",
52
+ "dataspace": "default",
53
+ "streamingSource": {
54
+ "type": "dlo",
55
+ "name": "",
56
+ },
57
+ "permissions": {
58
+ "read": {},
59
+ "write": {},
60
+ },
61
+ }
62
+
46
63
  FUNCTION_CONFIG_TEMPLATE = {
47
64
  "entryPoint": "",
48
65
  }
@@ -160,6 +177,35 @@ class DataAccessLayerCalls(pydantic.BaseModel):
160
177
  return next(iter(self.write_to_dmo))
161
178
 
162
179
 
180
+ class StreamingDataAccessLayerCalls(pydantic.BaseModel):
181
+ read_dlo_deltas: bool
182
+ read_dmo_deltas: bool
183
+ write_dlo_deltas: frozenset[str]
184
+
185
+ @pydantic.model_validator(mode="after")
186
+ def validate_access_layer(self) -> StreamingDataAccessLayerCalls:
187
+ if self.read_dlo_deltas and self.read_dmo_deltas:
188
+ raise ValueError(
189
+ "Cannot read DLO and DMO deltas in the same streaming transform."
190
+ )
191
+ if not self.read_dlo_deltas and not self.read_dmo_deltas:
192
+ raise ValueError(
193
+ "A streaming transform must read from at least one DLO or DMO "
194
+ "delta stream (read_dlo_deltas / read_dmo_deltas)."
195
+ )
196
+ if not self.write_dlo_deltas:
197
+ raise ValueError(
198
+ "A streaming transform must write to at least one DLO via "
199
+ "write_dlo_deltas."
200
+ )
201
+ return self
202
+
203
+ @property
204
+ def read_layer(self) -> str:
205
+ """Return the read source layer, ``"dlo"`` or ``"dmo"``."""
206
+ return "dlo" if self.read_dlo_deltas else "dmo"
207
+
208
+
163
209
  class ClientMethodVisitor(ast.NodeVisitor):
164
210
  """AST Visitor that finds all instances of Client read/write method calls."""
165
211
 
@@ -168,6 +214,9 @@ class ClientMethodVisitor(ast.NodeVisitor):
168
214
  self._read_dmo_instances: set[str] = set()
169
215
  self._write_to_dlo_instances: set[str] = set()
170
216
  self._write_to_dmo_instances: set[str] = set()
217
+ self._read_dlo_deltas: bool = False
218
+ self._read_dmo_deltas: bool = False
219
+ self._write_dlo_deltas_instances: set[str] = set()
171
220
  self.variable_values: Dict[str, Union[str, None]] = {}
172
221
 
173
222
  def visit_Assign(self, node: ast.Assign) -> None:
@@ -189,14 +238,15 @@ class ClientMethodVisitor(ast.NodeVisitor):
189
238
  node.func.value, ast.Name
190
239
  ):
191
240
  method_name = node.func.attr
241
+
242
+ if method_name == "read_dlo_deltas":
243
+ self._read_dlo_deltas = True
244
+ elif method_name == "read_dmo_deltas":
245
+ self._read_dmo_deltas = True
246
+
192
247
  if method_name in DATA_ACCESS_METHODS and node.args:
193
248
  arg = node.args[0]
194
- name = None
195
-
196
- if isinstance(arg, ast.Constant) and isinstance(arg.value, str):
197
- name = arg.value
198
- elif isinstance(arg, ast.Name) and arg.id in self.variable_values:
199
- name = self.variable_values[arg.id]
249
+ name = self._resolve_name_arg(arg)
200
250
 
201
251
  if name:
202
252
  if method_name == "read_dlo":
@@ -207,8 +257,29 @@ class ClientMethodVisitor(ast.NodeVisitor):
207
257
  self._write_to_dlo_instances.add(name)
208
258
  elif method_name == "write_to_dmo":
209
259
  self._write_to_dmo_instances.add(name)
260
+ elif method_name in STREAMING_WRITE_METHODS and node.args:
261
+ name = self._resolve_name_arg(node.args[0])
262
+ if name and method_name == "write_dlo_deltas":
263
+ self._write_dlo_deltas_instances.add(name)
210
264
  self.generic_visit(node)
211
265
 
266
+ def _resolve_name_arg(self, arg: ast.expr) -> Union[str, None]:
267
+ """Resolve a string-literal or tracked-variable first argument."""
268
+ if isinstance(arg, ast.Constant) and isinstance(arg.value, str):
269
+ return arg.value
270
+ if isinstance(arg, ast.Name) and arg.id in self.variable_values:
271
+ return self.variable_values[arg.id]
272
+ return None
273
+
274
+ @property
275
+ def is_streaming(self) -> bool:
276
+ """Whether any streaming (delta) access method was found."""
277
+ return (
278
+ self._read_dlo_deltas
279
+ or self._read_dmo_deltas
280
+ or bool(self._write_dlo_deltas_instances)
281
+ )
282
+
212
283
  def found(self) -> DataAccessLayerCalls:
213
284
  return DataAccessLayerCalls(
214
285
  read_dlo=frozenset(self._read_dlo_instances),
@@ -217,6 +288,13 @@ class ClientMethodVisitor(ast.NodeVisitor):
217
288
  write_to_dmo=frozenset(self._write_to_dmo_instances),
218
289
  )
219
290
 
291
+ def found_streaming(self) -> StreamingDataAccessLayerCalls:
292
+ return StreamingDataAccessLayerCalls(
293
+ read_dlo_deltas=self._read_dlo_deltas,
294
+ read_dmo_deltas=self._read_dmo_deltas,
295
+ write_dlo_deltas=frozenset(self._write_dlo_deltas_instances),
296
+ )
297
+
220
298
 
221
299
  class ImportVisitor(ast.NodeVisitor):
222
300
  """AST Visitor that extracts external package imports from Python code."""
@@ -301,23 +379,51 @@ def write_requirements_file(file_path: str) -> str:
301
379
  return requirements_path
302
380
 
303
381
 
304
- def scan_file(file_path: str) -> DataAccessLayerCalls:
305
- """Scan a single Python file for Client read/write method calls."""
382
+ def _visit_file(file_path: str) -> ClientMethodVisitor:
383
+ """Parse a Python file and return the populated method visitor."""
306
384
  with open(file_path, "r") as f:
307
- code = f.read()
308
- tree = ast.parse(code)
309
- visitor = ClientMethodVisitor()
310
- visitor.visit(tree)
311
- return visitor.found()
385
+ tree = ast.parse(f.read())
386
+ visitor = ClientMethodVisitor()
387
+ visitor.visit(tree)
388
+ return visitor
389
+
390
+
391
+ def scan_file(file_path: str) -> DataAccessLayerCalls:
392
+ """Scan a single Python file for batch Client read/write method calls."""
393
+ return _visit_file(file_path).found()
394
+
395
+
396
+ def scan_file_streaming(file_path: str) -> StreamingDataAccessLayerCalls:
397
+ """Scan a single Python file for StreamingClient delta method calls."""
398
+ return _visit_file(file_path).found_streaming()
399
+
312
400
 
401
+ def file_is_streaming(file_path: str) -> bool:
402
+ """Return whether the entrypoint uses streaming (delta) access methods."""
403
+ return _visit_file(file_path).is_streaming
313
404
 
314
- def dc_config_json_from_file(file_path: str, type: str) -> dict[str, Any]:
315
- """Create a Data Cloud Custom Code config JSON from a script."""
405
+
406
+ def dc_config_json_from_file(
407
+ file_path: str, type: str, streaming: bool = False
408
+ ) -> dict[str, Any]:
409
+ """Create a Data Cloud Custom Code config JSON from a script.
410
+
411
+ Args:
412
+ file_path: Path to the entrypoint.
413
+ type: Package type, ``"script"`` or ``"function"``.
414
+ streaming: For scripts, a streaming
415
+ (``streamingSource``) config instead of a batch one.
416
+ """
316
417
  config: dict[str, Any]
317
418
  if type == "script":
318
- config = DATA_TRANSFORM_CONFIG_TEMPLATE.copy()
419
+ template = (
420
+ STREAMING_TRANSFORM_CONFIG_TEMPLATE
421
+ if streaming
422
+ else DATA_TRANSFORM_CONFIG_TEMPLATE
423
+ )
424
+ config = copy.deepcopy(template)
319
425
  elif type == "function":
320
- config = FUNCTION_CONFIG_TEMPLATE.copy()
426
+ config = copy.deepcopy(FUNCTION_CONFIG_TEMPLATE)
321
427
  config["entryPoint"] = os.path.basename(file_path)
322
428
  return config
323
429
 
@@ -372,22 +478,51 @@ def update_config(file_path: str) -> dict[str, Any]:
372
478
 
373
479
  if package_type == "script":
374
480
  existing_config["dataspace"] = get_dataspace(existing_config)
375
- output = scan_file(file_path)
376
- read: dict[str, list[str]] = {}
377
- if output.read_dlo:
378
- read["dlo"] = list(output.read_dlo)
379
- else:
380
- read["dmo"] = list(output.read_dmo)
381
- write: dict[str, list[str]] = {}
382
- if output.write_to_dlo:
383
- write["dlo"] = list(output.write_to_dlo)
481
+ if file_is_streaming(file_path):
482
+ _update_streaming_config(existing_config, file_path)
384
483
  else:
385
- write["dmo"] = list(output.write_to_dmo)
386
-
387
- existing_config["permissions"] = {"read": read, "write": write}
484
+ existing_config.pop("streamingSource", None)
485
+ output = scan_file(file_path)
486
+ read: dict[str, list[str]] = {}
487
+ if output.read_dlo:
488
+ read["dlo"] = list(output.read_dlo)
489
+ else:
490
+ read["dmo"] = list(output.read_dmo)
491
+ write: dict[str, list[str]] = {}
492
+ if output.write_to_dlo:
493
+ write["dlo"] = list(output.write_to_dlo)
494
+ else:
495
+ write["dmo"] = list(output.write_to_dmo)
496
+
497
+ existing_config["permissions"] = {"read": read, "write": write}
388
498
  return existing_config
389
499
 
390
500
 
501
+ def _update_streaming_config(existing_config: dict[str, Any], file_path: str) -> None:
502
+ output = scan_file_streaming(file_path)
503
+ read_layer = output.read_layer
504
+
505
+ source = existing_config.get("streamingSource")
506
+ if not isinstance(source, dict):
507
+ source = {}
508
+ source_name = source.get("name", "")
509
+ existing_config["streamingSource"] = {"type": read_layer, "name": source_name}
510
+
511
+ if not source_name:
512
+ logger.warning(
513
+ "streamingSource.name is empty in config.json. A streaming "
514
+ "transform must declare its read source; set streamingSource.name "
515
+ "to the DLO/DMO the transform reads from."
516
+ )
517
+
518
+ read_names = [source_name] if source_name else []
519
+ write_names = list(output.write_dlo_deltas)
520
+ existing_config["permissions"] = {
521
+ "read": {read_layer: read_names},
522
+ "write": {"dlo": write_names},
523
+ }
524
+
525
+
391
526
  def get_dataspace(existing_config: dict[str, str]) -> str:
392
527
  if "dataspace" in existing_config:
393
528
  dataspace_value = existing_config["dataspace"]
@@ -23,8 +23,12 @@ from datacustomcode.constants import FEATURE_TEMPLATE_MAPPING
23
23
  script_template_dir = os.path.join(os.path.dirname(__file__), "templates", "script")
24
24
  function_template_dir = os.path.join(os.path.dirname(__file__), "templates", "function")
25
25
 
26
+ STREAMING_EXAMPLE_ENTRYPOINT = os.path.join(
27
+ script_template_dir, "examples", "streaming_deltas", "entrypoint.py"
28
+ )
26
29
 
27
- def copy_script_template(target_dir: str) -> None:
30
+
31
+ def copy_script_template(target_dir: str, streaming: bool = False) -> None:
28
32
  """Copy the template to the target directory."""
29
33
  os.makedirs(target_dir, exist_ok=True)
30
34
 
@@ -39,6 +43,14 @@ def copy_script_template(target_dir: str) -> None:
39
43
  logger.debug(f"Copying file {source} to {destination}...")
40
44
  shutil.copy2(source, destination)
41
45
 
46
+ if streaming:
47
+ destination = os.path.join(target_dir, "payload", "entrypoint.py")
48
+ logger.debug(
49
+ f"Copying streaming example {STREAMING_EXAMPLE_ENTRYPOINT} to "
50
+ f"{destination}..."
51
+ )
52
+ shutil.copy2(STREAMING_EXAMPLE_ENTRYPOINT, destination)
53
+
42
54
 
43
55
  def copy_function_template(target_dir: str, use_in_feature: Optional[str]) -> None:
44
56
  os.makedirs(target_dir, exist_ok=True)