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.
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/PKG-INFO +59 -3
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/README.md +58 -2
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/pyproject.toml +7 -7
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/client.py +39 -10
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/deploy.py +8 -11
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/io/writer/base.py +33 -1
- salesforce_data_customcode-7.0.0rc5/src/datacustomcode/lineage/__init__.py +137 -0
- salesforce_data_customcode-7.0.0rc5/src/datacustomcode/lineage/_worker.py +97 -0
- salesforce_data_customcode-7.0.0rc5/src/datacustomcode/lineage/capture.py +289 -0
- salesforce_data_customcode-7.0.0rc5/src/datacustomcode/lineage/tracer.py +566 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/named_credential/spark_base.py +8 -2
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/named_credential/spark_default.py +40 -4
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/run.py +60 -1
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/scan.py +26 -6
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/LICENSE.txt +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/__init__.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/auth.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/cli.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/cmd.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/common_config.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/config.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/config.yaml +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/constants.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/credentials.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/einstein_platform_client.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/einstein_platform_config.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/einstein_predictions/__init__.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/einstein_predictions/base.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/einstein_predictions/errors.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/einstein_predictions/impl/default.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/einstein_predictions/spark_base.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/einstein_predictions/spark_default.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/einstein_predictions/types.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/einstein_predictions_config.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/file/__init__.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/file/base.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/file/path/__init__.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/file/path/default.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/function/__init__.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/function/base.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/function/feature_types/__init__.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/function/feature_types/chunking.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/function/runtime.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/function_utils.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/io/__init__.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/io/base.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/io/cdf.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/io/reader/__init__.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/io/reader/base.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/io/reader/local_deltas.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/io/reader/query_api.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/io/reader/sf_cli.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/io/reader/streaming_seeder.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/io/reader/utils.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/io/writer/__init__.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/io/writer/csv.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/io/writer/local_deltas.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/io/writer/print.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/llm_gateway/__init__.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/llm_gateway/base.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/llm_gateway/default.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/llm_gateway/errors.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/llm_gateway/spark_base.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/llm_gateway/spark_default.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/llm_gateway/types/__init__.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/llm_gateway/types/generate_text_request.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/llm_gateway/types/generate_text_request_builder.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/llm_gateway/types/generate_text_response.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/llm_gateway/types/generate_text_response_builder.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/llm_gateway_config.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/mixin.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/named_credential/__init__.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/named_credential/base.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/named_credential/default.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/named_credential/direct/__init__.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/named_credential/direct/auth.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/named_credential/direct/credentials.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/named_credential/direct/transport.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/named_credential/direct/url_resolver.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/named_credential/errors.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/named_credential/types/__init__.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/named_credential/types/http_method.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/named_credential/types/http_request.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/named_credential/types/http_request_builder.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/named_credential/types/http_response.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/named_credential/types/http_response_builder.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/named_credential_config.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/py.typed +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/spark/__init__.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/spark/base.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/spark/column_hints.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/spark/default.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/template.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/__init__.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/function/.devcontainer/devcontainer.json +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/function/Dockerfile.dependencies +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/function/README.md +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/function/build_native_dependencies.sh +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/function/chunking/payload/config.json +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/function/chunking/payload/entrypoint.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/function/chunking/requirements.txt +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/function/example/chunking_with_llm/config.json +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/function/example/chunking_with_llm/entrypoint.py +0 -0
- {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
- {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
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/function/example/chunking_with_prediction/config.json +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/function/example/chunking_with_prediction/entrypoint.py +0 -0
- {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
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/function/payload/config.json +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/function/payload/entrypoint.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/function/payload/utility.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/function/requirements-dev.txt +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/function/requirements.txt +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/script/.devcontainer/devcontainer.json +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/script/Dockerfile +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/script/Dockerfile.dependencies +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/script/README.md +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/script/account.ipynb +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/script/build_native_dependencies.sh +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/script/examples/employee_hierarchy/employee_data.csv +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/script/examples/employee_hierarchy/entrypoint.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/script/examples/streaming_deltas/entrypoint.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/script/jupyterlab.sh +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/script/payload/config.json +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/script/payload/entrypoint.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/script/requirements-dev.txt +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/templates/script/requirements.txt +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc5}/src/datacustomcode/token_provider.py +0 -0
- {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.
|
|
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
|
|
180
|
-
* `write_to_dmo(name, spark_dataframe, write_mode)` – Write to a Data
|
|
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
|
|
155
|
-
* `write_to_dmo(name, spark_dataframe, write_mode)` – Write to a Data
|
|
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.
|
|
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
|
|
238
|
-
``
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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``
|
|
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}")
|