salesforce-data-customcode 7.0.0rc3__tar.gz → 7.0.0rc5__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 (129) hide show
  1. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/PKG-INFO +59 -3
  2. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/README.md +58 -2
  3. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/pyproject.toml +7 -7
  4. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/client.py +39 -10
  5. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/deploy.py +8 -11
  6. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/io/writer/base.py +33 -1
  7. salesforce_data_customcode-7.0.0rc5/src/datacustomcode/lineage/__init__.py +137 -0
  8. salesforce_data_customcode-7.0.0rc5/src/datacustomcode/lineage/_worker.py +97 -0
  9. salesforce_data_customcode-7.0.0rc5/src/datacustomcode/lineage/capture.py +289 -0
  10. salesforce_data_customcode-7.0.0rc5/src/datacustomcode/lineage/tracer.py +566 -0
  11. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/named_credential/spark_base.py +8 -2
  12. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/named_credential/spark_default.py +40 -4
  13. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/run.py +60 -1
  14. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/scan.py +26 -6
  15. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/LICENSE.txt +0 -0
  16. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/__init__.py +0 -0
  17. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/auth.py +0 -0
  18. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/cli.py +0 -0
  19. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/cmd.py +0 -0
  20. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/common_config.py +0 -0
  21. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/config.py +0 -0
  22. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/config.yaml +0 -0
  23. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/constants.py +0 -0
  24. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/credentials.py +0 -0
  25. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/einstein_platform_client.py +0 -0
  26. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/einstein_platform_config.py +0 -0
  27. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/einstein_predictions/__init__.py +0 -0
  28. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/einstein_predictions/base.py +0 -0
  29. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/einstein_predictions/errors.py +0 -0
  30. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/einstein_predictions/impl/default.py +0 -0
  31. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/einstein_predictions/spark_base.py +0 -0
  32. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/einstein_predictions/spark_default.py +0 -0
  33. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/einstein_predictions/types.py +0 -0
  34. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/einstein_predictions_config.py +0 -0
  35. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/file/__init__.py +0 -0
  36. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/file/base.py +0 -0
  37. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/file/path/__init__.py +0 -0
  38. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/file/path/default.py +0 -0
  39. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/function/__init__.py +0 -0
  40. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/function/base.py +0 -0
  41. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/function/feature_types/__init__.py +0 -0
  42. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/function/feature_types/chunking.py +0 -0
  43. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/function/runtime.py +0 -0
  44. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/function_utils.py +0 -0
  45. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/io/__init__.py +0 -0
  46. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/io/base.py +0 -0
  47. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/io/cdf.py +0 -0
  48. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/io/reader/__init__.py +0 -0
  49. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/io/reader/base.py +0 -0
  50. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/io/reader/local_deltas.py +0 -0
  51. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/io/reader/query_api.py +0 -0
  52. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/io/reader/sf_cli.py +0 -0
  53. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/io/reader/streaming_seeder.py +0 -0
  54. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/io/reader/utils.py +0 -0
  55. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/io/writer/__init__.py +0 -0
  56. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/io/writer/csv.py +0 -0
  57. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/io/writer/local_deltas.py +0 -0
  58. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/io/writer/print.py +0 -0
  59. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/llm_gateway/__init__.py +0 -0
  60. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/llm_gateway/base.py +0 -0
  61. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/llm_gateway/default.py +0 -0
  62. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/llm_gateway/errors.py +0 -0
  63. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/llm_gateway/spark_base.py +0 -0
  64. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/llm_gateway/spark_default.py +0 -0
  65. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/llm_gateway/types/__init__.py +0 -0
  66. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/llm_gateway/types/generate_text_request.py +0 -0
  67. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/llm_gateway/types/generate_text_request_builder.py +0 -0
  68. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/llm_gateway/types/generate_text_response.py +0 -0
  69. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/llm_gateway/types/generate_text_response_builder.py +0 -0
  70. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/llm_gateway_config.py +0 -0
  71. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/mixin.py +0 -0
  72. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/named_credential/__init__.py +0 -0
  73. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/named_credential/base.py +0 -0
  74. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/named_credential/default.py +0 -0
  75. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/named_credential/direct/__init__.py +0 -0
  76. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/named_credential/direct/auth.py +0 -0
  77. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/named_credential/direct/credentials.py +0 -0
  78. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/named_credential/direct/transport.py +0 -0
  79. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/named_credential/direct/url_resolver.py +0 -0
  80. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/named_credential/errors.py +0 -0
  81. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/named_credential/types/__init__.py +0 -0
  82. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/named_credential/types/http_method.py +0 -0
  83. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/named_credential/types/http_request.py +0 -0
  84. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/named_credential/types/http_request_builder.py +0 -0
  85. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/named_credential/types/http_response.py +0 -0
  86. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/named_credential/types/http_response_builder.py +0 -0
  87. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/named_credential_config.py +0 -0
  88. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/py.typed +0 -0
  89. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/spark/__init__.py +0 -0
  90. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/spark/base.py +0 -0
  91. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/spark/column_hints.py +0 -0
  92. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/spark/default.py +0 -0
  93. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/template.py +0 -0
  94. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/__init__.py +0 -0
  95. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/function/.devcontainer/devcontainer.json +0 -0
  96. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/function/Dockerfile.dependencies +0 -0
  97. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/function/README.md +0 -0
  98. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/function/build_native_dependencies.sh +0 -0
  99. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/function/chunking/payload/config.json +0 -0
  100. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/function/chunking/payload/entrypoint.py +0 -0
  101. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/function/chunking/requirements.txt +0 -0
  102. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/function/example/chunking_with_llm/config.json +0 -0
  103. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/function/example/chunking_with_llm/entrypoint.py +0 -0
  104. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/function/example/chunking_with_llm/files/chunking_prompt.txt +0 -0
  105. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/function/example/chunking_with_llm/tests/test.json +0 -0
  106. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/function/example/chunking_with_prediction/config.json +0 -0
  107. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/function/example/chunking_with_prediction/entrypoint.py +0 -0
  108. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/function/example/chunking_with_prediction/tests/test.json +0 -0
  109. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/function/payload/config.json +0 -0
  110. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/function/payload/entrypoint.py +0 -0
  111. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/function/payload/utility.py +0 -0
  112. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/function/requirements-dev.txt +0 -0
  113. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/function/requirements.txt +0 -0
  114. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/script/.devcontainer/devcontainer.json +0 -0
  115. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/script/Dockerfile +0 -0
  116. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/script/Dockerfile.dependencies +0 -0
  117. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/script/README.md +0 -0
  118. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/script/account.ipynb +0 -0
  119. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/script/build_native_dependencies.sh +0 -0
  120. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/script/examples/employee_hierarchy/employee_data.csv +0 -0
  121. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/script/examples/employee_hierarchy/entrypoint.py +0 -0
  122. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/script/examples/streaming_deltas/entrypoint.py +0 -0
  123. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/script/jupyterlab.sh +0 -0
  124. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/script/payload/config.json +0 -0
  125. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/script/payload/entrypoint.py +0 -0
  126. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/script/requirements-dev.txt +0 -0
  127. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/script/requirements.txt +0 -0
  128. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/token_provider.py +0 -0
  129. {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: salesforce-data-customcode
3
- Version: 7.0.0rc3
3
+ Version: 7.0.0rc5
4
4
  Summary: Data Cloud Custom Code SDK
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE.txt
@@ -176,13 +176,14 @@ For a batch transform, use `Client`. You should only need the following methods:
176
176
  * `find_file_path(file_name)` – Resolve a bundled file (placed under `payload/files/`) to a `pathlib.Path` that exists. Works the same locally and inside Data Cloud — see [Bundled file resolution](#bundled-file-resolution) below for the full lookup order. Raises `FileNotFoundError` if the file isn't found.
177
177
  * `read_dlo(name)` – Read from a Data Lake Object by name
178
178
  * `read_dmo(name)` – Read from a Data Model Object by name
179
- * `write_to_dlo(name, spark_dataframe, write_mode)` – Write to a Data Model Object by name with a Spark dataframe
180
- * `write_to_dmo(name, spark_dataframe, write_mode)` – Write to a Data Lake Object by name with a Spark dataframe
179
+ * `write_to_dlo(name, spark_dataframe, write_mode)` – Write to a Data Lake Object by name with a Spark dataframe
180
+ * `write_to_dmo(name, spark_dataframe, write_mode)` – Write to a Data Model Object by name with a Spark dataframe
181
181
 
182
182
  For a streaming (delta) transform, use `StreamingClient`, which exposes the streaming counterparts:
183
183
  * `read_dlo_deltas()` – Read the streaming change feed (deltas) of a Data Lake Object as a streaming DataFrame.
184
184
  * `read_dmo_deltas()` – Read the streaming change feed (deltas) of a Data Model Object as a streaming DataFrame.
185
185
  * `write_dlo_deltas(name, spark_dataframe)` – Write a streaming DataFrame of deltas to a Data Lake Object; returns the started `StreamingQuery`
186
+ * `write_dmo_deltas(name, spark_dataframe)` – Write a streaming DataFrame of deltas to a Data Model Object; returns the started `StreamingQuery`
186
187
 
187
188
  `find_file_path`, `llm_gateway_generate_text`, and `einstein_predict` are available on both clients.
188
189
 
@@ -201,6 +202,27 @@ client.write_to_dlo('output_DLO')
201
202
  > [!WARNING]
202
203
  > Currently we only support reading from DMOs and writing to DMOs or reading from DLOs and writing to DLOs, but they cannot mix.
203
204
 
205
+ ### Writing Currency fields
206
+
207
+ A DLO or DMO with a Currency field has a system field, `cdp_sys_record_currency__c`, that holds each record's ISO currency code. Data Explorer UI uses it to display currency values, and shows a blank value if it's null (Query Editor still shows it).
208
+
209
+ Code Extensions don't set `cdp_sys_record_currency__c` for you, since only your script knows what currency its amounts are in. When writing to an object with Currency fields, set it yourself:
210
+
211
+ ```python
212
+ from pyspark.sql.functions import lit
213
+
214
+ # Either source has Currency fields: keep its cdp_sys_record_currency__c in your output
215
+ df = source.select("id__c", "amount__c", "cdp_sys_record_currency__c")
216
+
217
+ # OR amounts are in a known currency
218
+ df = df.withColumn("cdp_sys_record_currency__c", lit("EUR"))
219
+ ```
220
+
221
+ - Use an ISO code that exists in your org's configured currency rates.
222
+ - When aggregating, group by the currency code so amounts in different currencies aren't summed together.
223
+
224
+ See [`docs/examples/script/currency_totals`](docs/examples/script/currency_totals) for a complete example.
225
+
204
226
  ### Streaming (delta) transforms
205
227
 
206
228
  Streaming BYOC transforms process a Data Lake Object's Change Data Feed continuously instead of reading a bounded snapshot. Use a `StreamingClient` and its `*_deltas` methods in place of the batch `Client` read/write methods:
@@ -226,6 +248,15 @@ query = client.write_dlo_deltas("Output__dll", transformed)
226
248
  query.awaitTermination()
227
249
  ```
228
250
 
251
+ A streaming transform can also read and write Data Model Objects with `read_dmo_deltas()` and `write_dmo_deltas()`:
252
+
253
+ ```python
254
+ deltas = client.read_dmo_deltas()
255
+ transformed = deltas.withColumn("description__c", upper(col("description__c")))
256
+ query = client.write_dmo_deltas("Output__dlm", transformed)
257
+ query.awaitTermination()
258
+ ```
259
+
229
260
  Notes:
230
261
 
231
262
  - These methods only run inside the Data Cloud streaming (`DELTA_SYNC`) runtime. Locally (`datacustomcode run`) they raise `NotImplementedError`, since there is no change feed to stream.
@@ -249,6 +280,29 @@ If none of these exist, `find_file_path` raises `FileNotFoundError` with the lis
249
280
 
250
281
  `$LIBRARY_PATH` is set automatically to the root of the package at runtime inside Data Cloud.
251
282
 
283
+ ### Generating lineage
284
+
285
+ `generate_lineage` runs a script entry point locally and derives the field-level `dataLineage` section of `config.json` from the Spark plans of the DataFrames it writes:
286
+
287
+ ```python
288
+ from datacustomcode.lineage import generate_lineage
289
+
290
+ result = generate_lineage("payload/entrypoint.py", sf_cli_org="my-org")
291
+ print(result.to_dict()["dataLineage"])
292
+ # [{"targetField": "Output__dll.name__c",
293
+ # "sourceFields": ["Input__dll.first__c", "Input__dll.last__c"]}, ...]
294
+ ```
295
+
296
+ The entry point runs exactly as it would locally (same config, reader, and credentials), except that writes are traced and validated against the target DLO instead of being performed. It runs in a separate Python process, which inherits your working directory, environment, and `sys.path`; errors raised by the script are re-raised from `generate_lineage`. Reads use the reader's `default_row_limit` (1000 by default; set it in your `config.yaml`).
297
+
298
+ > **Review the output before you use it.** The generated lineage is a starting point for review, not a guarantee. It reflects only the code paths and data your local run exercised, and it can't see inside UDFs. Accurate and complete `dataLineage` in your package is your responsibility as the package author.
299
+
300
+ - A target field's sources are the fields its *value* is computed from. Fields that only decide which rows, groups or branch are used aren't sources, however the logic is written: filter conditions, join keys, `groupBy` keys, sort keys, window `partitionBy`/`orderBy` keys, `when()`/`if()` conditions, and aggregate `FILTER (WHERE …)` clauses. For example, `groupBy("region").agg(sum("amount"))` and `sum("amount").over(Window.partitionBy("region"))` both trace to `amount` only.
301
+ - Fields computed only from literals are listed in `noSources`. So are fields whose value doesn't come from any input field's value, such as `row_number()`, `rank()`, `count("*")`, or `sum(when(cond, 1))`.
302
+ - Fields that can't be traced are listed in `unresolved` with a reason and get no lineage. This covers data that didn't come from `read_dlo`/`read_dmo` (for example a `toPandas()` → `createDataFrame` round trip), `mapInPandas`, and subqueries.
303
+ - `pairCount` is the total number of (target, source) field pairs.
304
+ - Only batch script packages are supported.
305
+
252
306
 
253
307
  ## CLI
254
308
 
@@ -364,6 +418,8 @@ Options:
364
418
  #### `datacustomcode zip`
365
419
  Zip a transformation job in preparation to upload to Data Cloud. Make sure to change directory into your code package folder (e.g., `my_package`) before running this command.
366
420
 
421
+ Symlinks inside the payload folder are not followed or packaged. Each symlinked file that is skipped is reported with a warning, and symlinked directories are not included. `datacustomcode deploy` zips the same way.
422
+
367
423
  Arguments:
368
424
  - `PATH`: Path to the code directory i.e. the payload folder (default: "payload")
369
425
 
@@ -151,13 +151,14 @@ For a batch transform, use `Client`. You should only need the following methods:
151
151
  * `find_file_path(file_name)` – Resolve a bundled file (placed under `payload/files/`) to a `pathlib.Path` that exists. Works the same locally and inside Data Cloud — see [Bundled file resolution](#bundled-file-resolution) below for the full lookup order. Raises `FileNotFoundError` if the file isn't found.
152
152
  * `read_dlo(name)` – Read from a Data Lake Object by name
153
153
  * `read_dmo(name)` – Read from a Data Model Object by name
154
- * `write_to_dlo(name, spark_dataframe, write_mode)` – Write to a Data Model Object by name with a Spark dataframe
155
- * `write_to_dmo(name, spark_dataframe, write_mode)` – Write to a Data Lake Object by name with a Spark dataframe
154
+ * `write_to_dlo(name, spark_dataframe, write_mode)` – Write to a Data Lake Object by name with a Spark dataframe
155
+ * `write_to_dmo(name, spark_dataframe, write_mode)` – Write to a Data Model Object by name with a Spark dataframe
156
156
 
157
157
  For a streaming (delta) transform, use `StreamingClient`, which exposes the streaming counterparts:
158
158
  * `read_dlo_deltas()` – Read the streaming change feed (deltas) of a Data Lake Object as a streaming DataFrame.
159
159
  * `read_dmo_deltas()` – Read the streaming change feed (deltas) of a Data Model Object as a streaming DataFrame.
160
160
  * `write_dlo_deltas(name, spark_dataframe)` – Write a streaming DataFrame of deltas to a Data Lake Object; returns the started `StreamingQuery`
161
+ * `write_dmo_deltas(name, spark_dataframe)` – Write a streaming DataFrame of deltas to a Data Model Object; returns the started `StreamingQuery`
161
162
 
162
163
  `find_file_path`, `llm_gateway_generate_text`, and `einstein_predict` are available on both clients.
163
164
 
@@ -176,6 +177,27 @@ client.write_to_dlo('output_DLO')
176
177
  > [!WARNING]
177
178
  > Currently we only support reading from DMOs and writing to DMOs or reading from DLOs and writing to DLOs, but they cannot mix.
178
179
 
180
+ ### Writing Currency fields
181
+
182
+ A DLO or DMO with a Currency field has a system field, `cdp_sys_record_currency__c`, that holds each record's ISO currency code. Data Explorer UI uses it to display currency values, and shows a blank value if it's null (Query Editor still shows it).
183
+
184
+ Code Extensions don't set `cdp_sys_record_currency__c` for you, since only your script knows what currency its amounts are in. When writing to an object with Currency fields, set it yourself:
185
+
186
+ ```python
187
+ from pyspark.sql.functions import lit
188
+
189
+ # Either source has Currency fields: keep its cdp_sys_record_currency__c in your output
190
+ df = source.select("id__c", "amount__c", "cdp_sys_record_currency__c")
191
+
192
+ # OR amounts are in a known currency
193
+ df = df.withColumn("cdp_sys_record_currency__c", lit("EUR"))
194
+ ```
195
+
196
+ - Use an ISO code that exists in your org's configured currency rates.
197
+ - When aggregating, group by the currency code so amounts in different currencies aren't summed together.
198
+
199
+ See [`docs/examples/script/currency_totals`](docs/examples/script/currency_totals) for a complete example.
200
+
179
201
  ### Streaming (delta) transforms
180
202
 
181
203
  Streaming BYOC transforms process a Data Lake Object's Change Data Feed continuously instead of reading a bounded snapshot. Use a `StreamingClient` and its `*_deltas` methods in place of the batch `Client` read/write methods:
@@ -201,6 +223,15 @@ query = client.write_dlo_deltas("Output__dll", transformed)
201
223
  query.awaitTermination()
202
224
  ```
203
225
 
226
+ A streaming transform can also read and write Data Model Objects with `read_dmo_deltas()` and `write_dmo_deltas()`:
227
+
228
+ ```python
229
+ deltas = client.read_dmo_deltas()
230
+ transformed = deltas.withColumn("description__c", upper(col("description__c")))
231
+ query = client.write_dmo_deltas("Output__dlm", transformed)
232
+ query.awaitTermination()
233
+ ```
234
+
204
235
  Notes:
205
236
 
206
237
  - These methods only run inside the Data Cloud streaming (`DELTA_SYNC`) runtime. Locally (`datacustomcode run`) they raise `NotImplementedError`, since there is no change feed to stream.
@@ -224,6 +255,29 @@ If none of these exist, `find_file_path` raises `FileNotFoundError` with the lis
224
255
 
225
256
  `$LIBRARY_PATH` is set automatically to the root of the package at runtime inside Data Cloud.
226
257
 
258
+ ### Generating lineage
259
+
260
+ `generate_lineage` runs a script entry point locally and derives the field-level `dataLineage` section of `config.json` from the Spark plans of the DataFrames it writes:
261
+
262
+ ```python
263
+ from datacustomcode.lineage import generate_lineage
264
+
265
+ result = generate_lineage("payload/entrypoint.py", sf_cli_org="my-org")
266
+ print(result.to_dict()["dataLineage"])
267
+ # [{"targetField": "Output__dll.name__c",
268
+ # "sourceFields": ["Input__dll.first__c", "Input__dll.last__c"]}, ...]
269
+ ```
270
+
271
+ The entry point runs exactly as it would locally (same config, reader, and credentials), except that writes are traced and validated against the target DLO instead of being performed. It runs in a separate Python process, which inherits your working directory, environment, and `sys.path`; errors raised by the script are re-raised from `generate_lineage`. Reads use the reader's `default_row_limit` (1000 by default; set it in your `config.yaml`).
272
+
273
+ > **Review the output before you use it.** The generated lineage is a starting point for review, not a guarantee. It reflects only the code paths and data your local run exercised, and it can't see inside UDFs. Accurate and complete `dataLineage` in your package is your responsibility as the package author.
274
+
275
+ - A target field's sources are the fields its *value* is computed from. Fields that only decide which rows, groups or branch are used aren't sources, however the logic is written: filter conditions, join keys, `groupBy` keys, sort keys, window `partitionBy`/`orderBy` keys, `when()`/`if()` conditions, and aggregate `FILTER (WHERE …)` clauses. For example, `groupBy("region").agg(sum("amount"))` and `sum("amount").over(Window.partitionBy("region"))` both trace to `amount` only.
276
+ - Fields computed only from literals are listed in `noSources`. So are fields whose value doesn't come from any input field's value, such as `row_number()`, `rank()`, `count("*")`, or `sum(when(cond, 1))`.
277
+ - Fields that can't be traced are listed in `unresolved` with a reason and get no lineage. This covers data that didn't come from `read_dlo`/`read_dmo` (for example a `toPandas()` → `createDataFrame` round trip), `mapInPandas`, and subqueries.
278
+ - `pairCount` is the total number of (target, source) field pairs.
279
+ - Only batch script packages are supported.
280
+
227
281
 
228
282
  ## CLI
229
283
 
@@ -339,6 +393,8 @@ Options:
339
393
  #### `datacustomcode zip`
340
394
  Zip a transformation job in preparation to upload to Data Cloud. Make sure to change directory into your code package folder (e.g., `my_package`) before running this command.
341
395
 
396
+ Symlinks inside the payload folder are not followed or packaged. Each symlinked file that is skipped is reported with a warning, and symlinked directories are not included. `datacustomcode deploy` zips the same way.
397
+
342
398
  Arguments:
343
399
  - `PATH`: Path to the code directory i.e. the payload folder (default: "payload")
344
400
 
@@ -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 = "7.0.0rc3"
21
+ version = "7.0.0rc5"
22
22
 
23
23
  [tool.black]
24
24
  exclude = '''
@@ -88,10 +88,10 @@ warn_unused_configs = true
88
88
 
89
89
  [tool.poetry]
90
90
  include = [
91
- {path = "src/datacustomcode/templates/**/*"},
92
- {path = "src/datacustomcode/config.yaml"}
91
+ { path = "src/datacustomcode/templates/**/*" },
92
+ { path = "src/datacustomcode/config.yaml" }
93
93
  ]
94
- packages = [{include = "datacustomcode", from = "src"}]
94
+ packages = [{ include = "datacustomcode", from = "src" }]
95
95
 
96
96
  [tool.poetry.build]
97
97
  generate-setup-file = false
@@ -115,7 +115,7 @@ coverage = ">=7.0.0,<8.0.0"
115
115
  ipykernel = "^6.29.5"
116
116
  mypy = "*"
117
117
  pipreqs = "*"
118
- poetry-dynamic-versioning = {extras = ["plugin"], version = "^1.8.2"}
118
+ poetry-dynamic-versioning = { extras = ["plugin"], version = "^1.8.2" }
119
119
  pre-commit = "*"
120
120
  pytest = "*"
121
121
  pytest-cov = "*"
@@ -127,7 +127,7 @@ types-requests = "*"
127
127
  [tool.poetry.plugins."poetry.plugin"]
128
128
 
129
129
  [tool.poetry.requires-plugins]
130
- poetry-dynamic-versioning = {version = ">=1.0.0,<2.0.0", extras = ["plugin"]}
130
+ poetry-dynamic-versioning = { version = ">=1.0.0,<2.0.0", extras = ["plugin"] }
131
131
 
132
132
  [tool.poetry.scripts]
133
133
  datacustomcode = "datacustomcode.cli:cli"
@@ -152,7 +152,6 @@ testpaths = ["tests"]
152
152
  [tool.ruff]
153
153
  fix = true
154
154
  line-length = 88
155
- target-version = 'py310'
156
155
  lint.ignore = [
157
156
  # do not assign a lambda expression, use a def
158
157
  'E731',
@@ -211,6 +210,7 @@ lint.select = [
211
210
  'RUF',
212
211
  'S102'
213
212
  ]
213
+ target-version = 'py310'
214
214
 
215
215
  [tool.setuptools.package_data]
216
216
  datacustomcode = ["templates/**/*", "config.yaml"]
@@ -231,11 +231,12 @@ def einstein_predict_col(
231
231
  def named_credential_request_col(
232
232
  request: "HTTPRequest",
233
233
  body: Optional["Column"] = None,
234
+ path: Optional["Column"] = None,
234
235
  ) -> "Column":
235
236
  """Build a Spark Column that makes one Named Credential callout per row.
236
237
 
237
- The endpoint, method, and headers are fixed for the call (taken from
238
- ``request``); only ``body`` varies per row. Use this instead of
238
+ The method and headers are fixed for the call (taken from ``request``);
239
+ ``body`` and, optionally, ``path`` vary per row. Use this instead of
239
240
  :meth:`Client.named_credential_request` when the callout runs across a
240
241
  DataFrame so each row is dispatched independently rather than one-shot on
241
242
  the driver.
@@ -253,13 +254,20 @@ def named_credential_request_col(
253
254
  headers are applied to every row.
254
255
  body: Optional per-row ``Column`` holding the request body as a
255
256
  string (or null for no body).
257
+ path: Optional per-row ``Column`` appended to ``request.url``, e.g.
258
+ ``concat(lit("?address="), col("address"))`` or ``lit("/v1/models")``.
259
+ The callout always stays on ``request.url``'s Named Credential: a
260
+ path not starting with ``/`` or ``?`` is joined with ``/``. A null
261
+ or empty row value uses ``request.url`` as is.
256
262
 
257
263
  Returns:
258
264
  A Spark ``Column`` of ``StructType`` with fields ``status``,
259
265
  ``response``, ``error_code``, and ``error_message``.
260
266
  """
261
267
  named_credential = Client()._get_spark_named_credential()
262
- return named_credential.request_col(request, body=body)
268
+ if path is None:
269
+ return named_credential.request_col(request, body=body)
270
+ return named_credential.request_col(request, body=body, path=path)
263
271
 
264
272
 
265
273
  class DataCloudObjectType(Enum):
@@ -368,7 +376,13 @@ class _BaseClient:
368
376
  instance._spark_named_credential = spark_named_credential
369
377
  # Initialize Readers and Writers from config
370
378
  # and/or provided reader and writer
371
- if reader is None or writer is None:
379
+ reader_forced = (
380
+ config.reader_config is not None and config.reader_config.force
381
+ )
382
+ writer_forced = (
383
+ config.writer_config is not None and config.writer_config.force
384
+ )
385
+ if reader is None or writer is None or reader_forced or writer_forced:
372
386
  # We need a spark because we will initialize readers and writers.
373
387
  # Reuse the process-wide session if one client already built it,
374
388
  # so a Client and a StreamingClient share a single connection.
@@ -396,9 +410,7 @@ class _BaseClient:
396
410
  raise ValueError(
397
411
  "Reader config is required when reader is not provided"
398
412
  )
399
- elif reader is None or (
400
- config.reader_config is not None and config.reader_config.force
401
- ):
413
+ elif reader is None or reader_forced:
402
414
  reader_init = config.reader_config.to_object(spark) # type: ignore
403
415
  else:
404
416
  reader_init = reader
@@ -406,9 +418,7 @@ class _BaseClient:
406
418
  raise ValueError(
407
419
  "Writer config is required when writer is not provided"
408
420
  )
409
- elif writer is None or (
410
- config.writer_config is not None and config.writer_config.force
411
- ):
421
+ elif writer is None or writer_forced:
412
422
  writer_init = config.writer_config.to_object(spark) # type: ignore
413
423
  else:
414
424
  writer_init = writer
@@ -716,6 +726,25 @@ class StreamingClient(_BaseClient):
716
726
  self._validate_data_layer_history_does_not_contain(DataCloudObjectType.DMO)
717
727
  return self._writer.write_dlo_deltas(name, dataframe, **kwargs) # type: ignore[no-any-return]
718
728
 
729
+ def write_dmo_deltas(
730
+ self, name: str, dataframe: PySparkDataFrame, **kwargs
731
+ ) -> StreamingQuery:
732
+ """Write a streaming DataFrame of deltas to a DMO in Data Cloud.
733
+
734
+ Starts a streaming query that writes each micro-batch to the
735
+ target DMO and returns the ``StreamingQuery`` handle; the caller
736
+ typically calls ``query.awaitTermination()``.
737
+
738
+ Args:
739
+ name: The name of the DMO to write to.
740
+ dataframe: The streaming PySpark DataFrame to write.
741
+
742
+ Returns:
743
+ The started ``StreamingQuery``.
744
+ """
745
+ self._validate_data_layer_history_does_not_contain(DataCloudObjectType.DLO)
746
+ return self._writer.write_dmo_deltas(name, dataframe, **kwargs) # type: ignore[no-any-return]
747
+
719
748
  def auto_write_to_dlo(self, name: str, dataframe: PySparkDataFrame) -> None:
720
749
  """Write a PySpark DataFrame to a DLO in Data Cloud automatically picking
721
750
  the WriteMode.
@@ -415,13 +415,7 @@ class DataTransformConfig(BaseConfig):
415
415
  def _validate_layers(self) -> "DataTransformConfig":
416
416
  read_is_dlo = isinstance(self.permissions.read, DloPermission)
417
417
  write_is_dlo = isinstance(self.permissions.write, DloPermission)
418
- if self.is_streaming:
419
- if not write_is_dlo:
420
- raise ValueError(
421
- "A streaming transform must write to a DLO "
422
- "(permissions.write must be a 'dlo' entry)."
423
- )
424
- elif read_is_dlo != write_is_dlo:
418
+ if read_is_dlo != write_is_dlo:
425
419
  raise ValueError(
426
420
  "permissions.read and permissions.write must both reference "
427
421
  "DLOs or both reference DMOs (got "
@@ -630,12 +624,15 @@ def zip(
630
624
  logger.debug(f"Zipping directory... {directory}")
631
625
 
632
626
  with zipfile.ZipFile(ZIP_FILE_NAME, "w", zipfile.ZIP_DEFLATED) as zipf:
633
- for root, dirs, files in os.walk(directory):
634
- # Skip .DS_Store and local credentials.
627
+ for root, dirs, files in os.walk(directory, followlinks=False):
628
+ # Skip symlinks, .DS_Store and local credentials.
635
629
  for file in files:
630
+ abs_path = os.path.join(root, file)
631
+ arcname = os.path.relpath(abs_path, directory)
632
+ if os.path.islink(abs_path):
633
+ logger.warning(f"Skipping symlink file {arcname}")
634
+ continue
636
635
  if file not in EXCLUDED_FILES:
637
- abs_path = os.path.join(root, file)
638
- arcname = os.path.relpath(abs_path, directory)
639
636
  zipf.write(abs_path, arcname)
640
637
 
641
638
  logger.debug(f"Created zip file: {ZIP_FILE_NAME}")
@@ -87,7 +87,7 @@ class BaseDataCloudWriter(BaseDataAccessLayer):
87
87
  Args:
88
88
  name: Target Data Lake Object name.
89
89
  dataframe: Streaming PySpark DataFrame produced from a
90
- ``read_dlo_deltas`` / ``read_dmo_deltas`` source.
90
+ ``read_dlo_deltas`` source.
91
91
 
92
92
  Returns:
93
93
  The started ``StreamingQuery``; the caller drives its lifecycle
@@ -102,3 +102,35 @@ class BaseDataCloudWriter(BaseDataAccessLayer):
102
102
  "streaming runtime; the local writer does not support streaming "
103
103
  "deltas."
104
104
  )
105
+
106
+ def write_dmo_deltas(
107
+ self, name: str, dataframe: PySparkDataFrame
108
+ ) -> StreamingQuery:
109
+ """Write a streaming DataFrame of deltas to a Data Model Object.
110
+
111
+ Streaming counterpart to :meth:`write_to_dmo`. Starts a streaming query
112
+ that writes each micro-batch to the target DMO via the Data Cloud
113
+ streaming sink and returns the resulting ``StreamingQuery`` handle. The
114
+ runtime owns the trigger and checkpoint location; callers pass only the
115
+ table name. Concrete streaming behavior is provided by the deployed
116
+ Data Cloud runtime; the base implementation raises
117
+ :class:`NotImplementedError`.
118
+
119
+ Args:
120
+ name: Target Data Model Object name.
121
+ dataframe: Streaming PySpark DataFrame produced from a
122
+ ``read_dmo_deltas`` source.
123
+
124
+ Returns:
125
+ The started ``StreamingQuery``; the caller drives its lifecycle
126
+ (typically ``query.awaitTermination()``).
127
+
128
+ Raises:
129
+ NotImplementedError: If the active writer does not support streaming
130
+ deltas (e.g. the local development writers).
131
+ """
132
+ raise NotImplementedError(
133
+ "write_dmo_deltas is only supported when running in the Data Cloud "
134
+ "streaming runtime; the local writer does not support streaming "
135
+ "deltas."
136
+ )
@@ -0,0 +1,137 @@
1
+ # Copyright (c) 2025, Salesforce, Inc.
2
+ # SPDX-License-Identifier: Apache-2
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ """Generate config.json ``dataLineage`` by tracing a script's Spark plans."""
16
+
17
+ from __future__ import annotations
18
+
19
+ import builtins
20
+ import json
21
+ import os
22
+ import subprocess
23
+ import sys
24
+ import tempfile
25
+ from typing import (
26
+ Any,
27
+ Dict,
28
+ NoReturn,
29
+ Optional,
30
+ Sequence,
31
+ )
32
+
33
+ from datacustomcode.lineage.capture import (
34
+ LineageCaptureReader,
35
+ LineageCaptureWriter,
36
+ LineageCollector,
37
+ LineageResult,
38
+ install_capture,
39
+ )
40
+ from datacustomcode.lineage.tracer import LineageTraceError
41
+
42
+ __all__ = [
43
+ "LineageCaptureReader",
44
+ "LineageCaptureWriter",
45
+ "LineageCollector",
46
+ "LineageResult",
47
+ "LineageTraceError",
48
+ "generate_lineage",
49
+ "install_capture",
50
+ ]
51
+
52
+
53
+ def generate_lineage(
54
+ entrypoint: str,
55
+ config_file: Optional[str] = None,
56
+ dependencies: Sequence[str] = (),
57
+ profile: str = "default",
58
+ sf_cli_org: Optional[str] = None,
59
+ ) -> LineageResult:
60
+ """Run a script entrypoint and return the field-level lineage it produces.
61
+
62
+ The entrypoint runs exactly as with ``run`` (same config resolution, reader
63
+ and credentials), except that writes are traced instead of performed. It runs
64
+ in a fresh Python process, so no client or config from the caller's process
65
+ leaks into the capture. The process inherits this one's working directory,
66
+ environment, ``sys.path`` and stdout/stderr.
67
+
68
+ Args:
69
+ entrypoint: The script entrypoint to run.
70
+ config_file: Optional SDK config file, as for ``run``.
71
+ dependencies: Modules to import before running, as for ``run``.
72
+ profile: The credentials profile to use.
73
+ sf_cli_org: Optional SF CLI org alias or username.
74
+
75
+ Returns:
76
+ The captured :class:`LineageResult`.
77
+
78
+ Raises:
79
+ ValueError: If the package is not a batch script.
80
+ Exception: Whatever the run raised, re-raised as the same built-in
81
+ exception type or :class:`LineageTraceError`, else ``RuntimeError``.
82
+ """
83
+ with tempfile.TemporaryDirectory(prefix="datacustomcode-lineage-") as tmp:
84
+ request_file = os.path.join(tmp, "request.json")
85
+ response_file = os.path.join(tmp, "response.json")
86
+ with open(request_file, "w", encoding="utf-8") as f:
87
+ json.dump(
88
+ {
89
+ "args": {
90
+ "entrypoint": entrypoint,
91
+ "config_file": config_file,
92
+ "dependencies": list(dependencies),
93
+ "profile": profile,
94
+ "sf_cli_org": sf_cli_org,
95
+ },
96
+ "response_file": response_file,
97
+ },
98
+ f,
99
+ )
100
+ # The worker sees the same modules this process does
101
+ env = dict(os.environ)
102
+ env["PYTHONPATH"] = os.pathsep.join(path for path in sys.path if path)
103
+ completed = subprocess.run(
104
+ [sys.executable, "-m", "datacustomcode.lineage._worker", request_file],
105
+ env=env,
106
+ check=False,
107
+ )
108
+ if not os.path.exists(response_file):
109
+ raise RuntimeError(
110
+ f"Lineage generation exited with code {completed.returncode} "
111
+ "without a result"
112
+ )
113
+ with open(response_file, encoding="utf-8") as f:
114
+ response = json.load(f)
115
+ if "error" in response:
116
+ _raise(response["error"])
117
+ return LineageResult.from_dict(response["result"])
118
+
119
+
120
+ def _raise(error: Dict[str, Any]) -> NoReturn:
121
+ """Re-raise an error reported by the worker as the closest local type."""
122
+ message = error["message"]
123
+ if error["type"] == LineageTraceError.__name__:
124
+ raise LineageTraceError(message)
125
+ exc_type = getattr(builtins, error["type"], None)
126
+ if (
127
+ error["module"] == "builtins"
128
+ and isinstance(exc_type, type)
129
+ and issubclass(exc_type, Exception)
130
+ ):
131
+ try:
132
+ exc = exc_type(message)
133
+ except TypeError: # e.g. UnicodeDecodeError takes more arguments
134
+ pass
135
+ else:
136
+ raise exc
137
+ raise RuntimeError(f"{error['module']}.{error['type']}: {message}")