salesforce-data-customcode 7.0.0rc3__tar.gz → 7.0.0rc4__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.0rc4}/PKG-INFO +47 -3
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/README.md +46 -2
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/pyproject.toml +7 -7
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/client.py +20 -10
- salesforce_data_customcode-7.0.0rc4/src/datacustomcode/lineage/__init__.py +137 -0
- salesforce_data_customcode-7.0.0rc4/src/datacustomcode/lineage/_worker.py +97 -0
- salesforce_data_customcode-7.0.0rc4/src/datacustomcode/lineage/capture.py +289 -0
- salesforce_data_customcode-7.0.0rc4/src/datacustomcode/lineage/tracer.py +566 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/named_credential/spark_base.py +8 -2
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/named_credential/spark_default.py +40 -4
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/run.py +60 -1
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/LICENSE.txt +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/__init__.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/auth.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/cli.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/cmd.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/common_config.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/config.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/config.yaml +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/constants.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/credentials.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/deploy.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/einstein_platform_client.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/einstein_platform_config.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/einstein_predictions/__init__.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/einstein_predictions/base.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/einstein_predictions/errors.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/einstein_predictions/impl/default.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/einstein_predictions/spark_base.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/einstein_predictions/spark_default.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/einstein_predictions/types.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/einstein_predictions_config.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/file/__init__.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/file/base.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/file/path/__init__.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/file/path/default.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/function/__init__.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/function/base.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/function/feature_types/__init__.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/function/feature_types/chunking.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/function/runtime.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/function_utils.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/io/__init__.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/io/base.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/io/cdf.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/io/reader/__init__.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/io/reader/base.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/io/reader/local_deltas.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/io/reader/query_api.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/io/reader/sf_cli.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/io/reader/streaming_seeder.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/io/reader/utils.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/io/writer/__init__.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/io/writer/base.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/io/writer/csv.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/io/writer/local_deltas.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/io/writer/print.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/llm_gateway/__init__.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/llm_gateway/base.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/llm_gateway/default.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/llm_gateway/errors.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/llm_gateway/spark_base.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/llm_gateway/spark_default.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/llm_gateway/types/__init__.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/llm_gateway/types/generate_text_request.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/llm_gateway/types/generate_text_request_builder.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/llm_gateway/types/generate_text_response.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/llm_gateway/types/generate_text_response_builder.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/llm_gateway_config.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/mixin.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/named_credential/__init__.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/named_credential/base.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/named_credential/default.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/named_credential/direct/__init__.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/named_credential/direct/auth.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/named_credential/direct/credentials.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/named_credential/direct/transport.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/named_credential/direct/url_resolver.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/named_credential/errors.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/named_credential/types/__init__.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/named_credential/types/http_method.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/named_credential/types/http_request.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/named_credential/types/http_request_builder.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/named_credential/types/http_response.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/named_credential/types/http_response_builder.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/named_credential_config.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/py.typed +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/scan.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/spark/__init__.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/spark/base.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/spark/column_hints.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/spark/default.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/template.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/templates/__init__.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/templates/function/.devcontainer/devcontainer.json +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/templates/function/Dockerfile.dependencies +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/templates/function/README.md +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/templates/function/build_native_dependencies.sh +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/templates/function/chunking/payload/config.json +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/templates/function/chunking/payload/entrypoint.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/templates/function/chunking/requirements.txt +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/templates/function/example/chunking_with_llm/config.json +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/templates/function/example/chunking_with_llm/entrypoint.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/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.0rc4}/src/datacustomcode/templates/function/example/chunking_with_llm/tests/test.json +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/templates/function/example/chunking_with_prediction/config.json +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/templates/function/example/chunking_with_prediction/entrypoint.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/templates/function/example/chunking_with_prediction/tests/test.json +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/templates/function/payload/config.json +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/templates/function/payload/entrypoint.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/templates/function/payload/utility.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/templates/function/requirements-dev.txt +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/templates/function/requirements.txt +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/templates/script/.devcontainer/devcontainer.json +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/templates/script/Dockerfile +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/templates/script/Dockerfile.dependencies +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/templates/script/README.md +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/templates/script/account.ipynb +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/templates/script/build_native_dependencies.sh +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/templates/script/examples/employee_hierarchy/employee_data.csv +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/templates/script/examples/employee_hierarchy/entrypoint.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/templates/script/examples/streaming_deltas/entrypoint.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/templates/script/jupyterlab.sh +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/templates/script/payload/config.json +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/templates/script/payload/entrypoint.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/templates/script/requirements-dev.txt +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/templates/script/requirements.txt +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/src/datacustomcode/token_provider.py +0 -0
- {salesforce_data_customcode-7.0.0rc3 → salesforce_data_customcode-7.0.0rc4}/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.0rc4
|
|
4
4
|
Summary: Data Cloud Custom Code SDK
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
License-File: LICENSE.txt
|
|
@@ -176,8 +176,8 @@ 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.
|
|
@@ -201,6 +201,27 @@ client.write_to_dlo('output_DLO')
|
|
|
201
201
|
> [!WARNING]
|
|
202
202
|
> Currently we only support reading from DMOs and writing to DMOs or reading from DLOs and writing to DLOs, but they cannot mix.
|
|
203
203
|
|
|
204
|
+
### Writing Currency fields
|
|
205
|
+
|
|
206
|
+
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).
|
|
207
|
+
|
|
208
|
+
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:
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
from pyspark.sql.functions import lit
|
|
212
|
+
|
|
213
|
+
# Either source has Currency fields: keep its cdp_sys_record_currency__c in your output
|
|
214
|
+
df = source.select("id__c", "amount__c", "cdp_sys_record_currency__c")
|
|
215
|
+
|
|
216
|
+
# OR amounts are in a known currency
|
|
217
|
+
df = df.withColumn("cdp_sys_record_currency__c", lit("EUR"))
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
- Use an ISO code that exists in your org's configured currency rates.
|
|
221
|
+
- When aggregating, group by the currency code so amounts in different currencies aren't summed together.
|
|
222
|
+
|
|
223
|
+
See [`docs/examples/script/currency_totals`](docs/examples/script/currency_totals) for a complete example.
|
|
224
|
+
|
|
204
225
|
### Streaming (delta) transforms
|
|
205
226
|
|
|
206
227
|
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:
|
|
@@ -249,6 +270,29 @@ If none of these exist, `find_file_path` raises `FileNotFoundError` with the lis
|
|
|
249
270
|
|
|
250
271
|
`$LIBRARY_PATH` is set automatically to the root of the package at runtime inside Data Cloud.
|
|
251
272
|
|
|
273
|
+
### Generating lineage
|
|
274
|
+
|
|
275
|
+
`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:
|
|
276
|
+
|
|
277
|
+
```python
|
|
278
|
+
from datacustomcode.lineage import generate_lineage
|
|
279
|
+
|
|
280
|
+
result = generate_lineage("payload/entrypoint.py", sf_cli_org="my-org")
|
|
281
|
+
print(result.to_dict()["dataLineage"])
|
|
282
|
+
# [{"targetField": "Output__dll.name__c",
|
|
283
|
+
# "sourceFields": ["Input__dll.first__c", "Input__dll.last__c"]}, ...]
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
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`).
|
|
287
|
+
|
|
288
|
+
> **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.
|
|
289
|
+
|
|
290
|
+
- 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.
|
|
291
|
+
- 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))`.
|
|
292
|
+
- 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.
|
|
293
|
+
- `pairCount` is the total number of (target, source) field pairs.
|
|
294
|
+
- Only batch script packages are supported.
|
|
295
|
+
|
|
252
296
|
|
|
253
297
|
## CLI
|
|
254
298
|
|
|
@@ -151,8 +151,8 @@ 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.
|
|
@@ -176,6 +176,27 @@ client.write_to_dlo('output_DLO')
|
|
|
176
176
|
> [!WARNING]
|
|
177
177
|
> Currently we only support reading from DMOs and writing to DMOs or reading from DLOs and writing to DLOs, but they cannot mix.
|
|
178
178
|
|
|
179
|
+
### Writing Currency fields
|
|
180
|
+
|
|
181
|
+
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).
|
|
182
|
+
|
|
183
|
+
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:
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
from pyspark.sql.functions import lit
|
|
187
|
+
|
|
188
|
+
# Either source has Currency fields: keep its cdp_sys_record_currency__c in your output
|
|
189
|
+
df = source.select("id__c", "amount__c", "cdp_sys_record_currency__c")
|
|
190
|
+
|
|
191
|
+
# OR amounts are in a known currency
|
|
192
|
+
df = df.withColumn("cdp_sys_record_currency__c", lit("EUR"))
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
- Use an ISO code that exists in your org's configured currency rates.
|
|
196
|
+
- When aggregating, group by the currency code so amounts in different currencies aren't summed together.
|
|
197
|
+
|
|
198
|
+
See [`docs/examples/script/currency_totals`](docs/examples/script/currency_totals) for a complete example.
|
|
199
|
+
|
|
179
200
|
### Streaming (delta) transforms
|
|
180
201
|
|
|
181
202
|
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:
|
|
@@ -224,6 +245,29 @@ If none of these exist, `find_file_path` raises `FileNotFoundError` with the lis
|
|
|
224
245
|
|
|
225
246
|
`$LIBRARY_PATH` is set automatically to the root of the package at runtime inside Data Cloud.
|
|
226
247
|
|
|
248
|
+
### Generating lineage
|
|
249
|
+
|
|
250
|
+
`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:
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
from datacustomcode.lineage import generate_lineage
|
|
254
|
+
|
|
255
|
+
result = generate_lineage("payload/entrypoint.py", sf_cli_org="my-org")
|
|
256
|
+
print(result.to_dict()["dataLineage"])
|
|
257
|
+
# [{"targetField": "Output__dll.name__c",
|
|
258
|
+
# "sourceFields": ["Input__dll.first__c", "Input__dll.last__c"]}, ...]
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
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`).
|
|
262
|
+
|
|
263
|
+
> **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.
|
|
264
|
+
|
|
265
|
+
- 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.
|
|
266
|
+
- 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))`.
|
|
267
|
+
- 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.
|
|
268
|
+
- `pairCount` is the total number of (target, source) field pairs.
|
|
269
|
+
- Only batch script packages are supported.
|
|
270
|
+
|
|
227
271
|
|
|
228
272
|
## CLI
|
|
229
273
|
|
|
@@ -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.0rc4"
|
|
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
|
|
@@ -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}")
|
|
@@ -0,0 +1,97 @@
|
|
|
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
|
+
"""Runs one lineage capture in a fresh process, for ``generate_lineage``.
|
|
16
|
+
|
|
17
|
+
Usage: ``python -m datacustomcode.lineage._worker <request.json>``. The request
|
|
18
|
+
holds the ``run_capture`` arguments and the path of a response file, which gets
|
|
19
|
+
either ``{"result": LineageResult.to_dict()}`` or
|
|
20
|
+
``{"error": {"module", "type", "message"}}``. The response goes to a file rather
|
|
21
|
+
than stdout because the entrypoint prints freely to stdout.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import json
|
|
27
|
+
import sys
|
|
28
|
+
import traceback
|
|
29
|
+
from typing import (
|
|
30
|
+
List,
|
|
31
|
+
Optional,
|
|
32
|
+
Sequence,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
from datacustomcode.lineage.capture import (
|
|
36
|
+
LineageCollector,
|
|
37
|
+
LineageResult,
|
|
38
|
+
install_capture,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def run_capture(
|
|
43
|
+
entrypoint: str,
|
|
44
|
+
config_file: Optional[str],
|
|
45
|
+
dependencies: Sequence[str],
|
|
46
|
+
profile: str,
|
|
47
|
+
sf_cli_org: Optional[str],
|
|
48
|
+
) -> LineageResult:
|
|
49
|
+
"""Run the entrypoint with capture installed. Call only in a fresh process."""
|
|
50
|
+
from datacustomcode.run import (
|
|
51
|
+
execute_entrypoint,
|
|
52
|
+
import_dependencies,
|
|
53
|
+
prepare_run,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Dependencies are imported after capture is installed, so a Client they
|
|
57
|
+
# create at import time is captured too
|
|
58
|
+
context = prepare_run(entrypoint, config_file, [], profile, sf_cli_org)
|
|
59
|
+
if context.package_type != "script":
|
|
60
|
+
raise ValueError(
|
|
61
|
+
f"Lineage can only be generated for script packages, not "
|
|
62
|
+
f"{context.package_type} packages"
|
|
63
|
+
)
|
|
64
|
+
if context.config_json.get("streamingSource"):
|
|
65
|
+
raise ValueError("Lineage generation does not support streaming scripts")
|
|
66
|
+
|
|
67
|
+
collector = LineageCollector()
|
|
68
|
+
install_capture(collector)
|
|
69
|
+
import_dependencies(list(dependencies))
|
|
70
|
+
execute_entrypoint(entrypoint, context)
|
|
71
|
+
return collector.result()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def main(argv: List[str]) -> int:
|
|
75
|
+
with open(argv[0], encoding="utf-8") as f:
|
|
76
|
+
request = json.load(f)
|
|
77
|
+
try:
|
|
78
|
+
result = run_capture(**request["args"])
|
|
79
|
+
response: dict = {"result": result.to_dict()}
|
|
80
|
+
except Exception as exc:
|
|
81
|
+
# The caller re-raises from the response; the traceback is for the
|
|
82
|
+
# customer, pointing into their entrypoint
|
|
83
|
+
traceback.print_exc()
|
|
84
|
+
response = {
|
|
85
|
+
"error": {
|
|
86
|
+
"module": type(exc).__module__,
|
|
87
|
+
"type": type(exc).__name__,
|
|
88
|
+
"message": str(exc),
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
with open(request["response_file"], "w", encoding="utf-8") as f:
|
|
92
|
+
json.dump(response, f)
|
|
93
|
+
return 1 if "error" in response else 0
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
if __name__ == "__main__":
|
|
97
|
+
sys.exit(main(sys.argv[1:]))
|