salesforce-data-customcode 6.0.2__tar.gz → 6.0.3__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-6.0.2 → salesforce_data_customcode-6.0.3}/PKG-INFO +51 -1
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/README.md +49 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/pyproject.toml +2 -1
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/__init__.py +15 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/client.py +104 -1
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/config.yaml +3 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/deploy.py +109 -19
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/einstein_platform_config.py +8 -4
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/einstein_predictions_config.py +1 -1
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/function/feature_types/chunking.py +1 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/llm_gateway/__init__.py +6 -0
- salesforce_data_customcode-6.0.2/src/datacustomcode/proxy/base.py → salesforce_data_customcode-6.0.3/src/datacustomcode/llm_gateway/errors.py +17 -5
- salesforce_data_customcode-6.0.3/src/datacustomcode/llm_gateway/spark_base.py +62 -0
- salesforce_data_customcode-6.0.3/src/datacustomcode/llm_gateway/spark_default.py +184 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/llm_gateway_config.py +43 -2
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/example/chunking_with_llm/entrypoint.py +3 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/example/chunking_with_prediction/entrypoint.py +3 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/payload/entrypoint.py +11 -12
- salesforce_data_customcode-6.0.3/src/datacustomcode/templates/script/payload/entrypoint.py +53 -0
- salesforce_data_customcode-6.0.2/src/datacustomcode/proxy/__init__.py +0 -14
- salesforce_data_customcode-6.0.2/src/datacustomcode/proxy/client/__init__.py +0 -14
- salesforce_data_customcode-6.0.2/src/datacustomcode/proxy/client/base.py +0 -32
- salesforce_data_customcode-6.0.2/src/datacustomcode/templates/script/payload/entrypoint.py +0 -25
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/LICENSE.txt +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/auth.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/cli.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/cmd.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/common_config.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/config.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/constants.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/credentials.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/einstein_platform_client.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/einstein_predictions/__init__.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/einstein_predictions/base.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/einstein_predictions/impl/default.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/einstein_predictions/types.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/file/__init__.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/file/base.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/file/path/__init__.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/file/path/default.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/function/__init__.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/function/base.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/function/feature_types/__init__.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/function/runtime.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/function_utils.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/io/__init__.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/io/base.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/io/reader/__init__.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/io/reader/base.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/io/reader/query_api.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/io/reader/sf_cli.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/io/reader/utils.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/io/writer/__init__.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/io/writer/base.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/io/writer/csv.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/io/writer/print.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/llm_gateway/base.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/llm_gateway/default.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/llm_gateway/types/__init__.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/llm_gateway/types/generate_text_request.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/llm_gateway/types/generate_text_request_builder.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/llm_gateway/types/generate_text_response.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/llm_gateway/types/generate_text_response_builder.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/mixin.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/py.typed +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/run.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/scan.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/spark/__init__.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/spark/base.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/spark/default.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/template.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/__init__.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/.devcontainer/devcontainer.json +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/Dockerfile.dependencies +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/README.md +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/build_native_dependencies.sh +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/chunking/payload/config.json +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/chunking/payload/entrypoint.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/chunking/requirements.txt +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/example/chunking_with_llm/config.json +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/example/chunking_with_llm/files/chunking_prompt.txt +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/example/chunking_with_llm/tests/test.json +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/example/chunking_with_prediction/config.json +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/example/chunking_with_prediction/tests/test.json +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/payload/config.json +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/requirements-dev.txt +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/requirements.txt +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/script/.devcontainer/devcontainer.json +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/script/Dockerfile +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/script/Dockerfile.dependencies +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/script/README.md +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/script/account.ipynb +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/script/build_native_dependencies.sh +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/script/examples/employee_hierarchy/employee_data.csv +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/script/examples/employee_hierarchy/entrypoint.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/script/jupyterlab.sh +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/script/payload/config.json +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/script/requirements-dev.txt +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/script/requirements.txt +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/token_provider.py +0 -0
- {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: salesforce-data-customcode
|
|
3
|
-
Version: 6.0.
|
|
3
|
+
Version: 6.0.3
|
|
4
4
|
Summary: Data Cloud Custom Code SDK
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
License-File: LICENSE.txt
|
|
@@ -15,6 +15,7 @@ Requires-Dist: click (>=8.1.8,<9.0.0)
|
|
|
15
15
|
Requires-Dist: loguru (>=0.7.3,<0.8.0)
|
|
16
16
|
Requires-Dist: numpy
|
|
17
17
|
Requires-Dist: pandas
|
|
18
|
+
Requires-Dist: pipreqs
|
|
18
19
|
Requires-Dist: pydantic (==2.13.1)
|
|
19
20
|
Requires-Dist: pyspark (==3.5.1)
|
|
20
21
|
Requires-Dist: pyyaml (>=6.0,<7.0)
|
|
@@ -330,6 +331,55 @@ Options:
|
|
|
330
331
|
- `--function-invoke-opt TEXT`: Currently we support only `UnstructuredChunking` for functions.
|
|
331
332
|
|
|
332
333
|
|
|
334
|
+
## Testing LLM Gateway
|
|
335
|
+
|
|
336
|
+
You can use AI models configured in Salesforce to generate responses while transforming your data. Below is a sample code example:
|
|
337
|
+
|
|
338
|
+
```
|
|
339
|
+
from datacustomcode.client import Client, llm_gateway_generate_text_col
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def main():
|
|
343
|
+
client = Client()
|
|
344
|
+
df = client.read_dlo("Input__dll")
|
|
345
|
+
# llm_gateway_generate_text_col returns a struct
|
|
346
|
+
# {status, response, error_code, error_message} per row, so per-row
|
|
347
|
+
# failures don't abort the Spark job. Pick the field you want with [].
|
|
348
|
+
df_generated = df.withColumn(
|
|
349
|
+
"greeting__c",
|
|
350
|
+
llm_gateway_generate_text_col(
|
|
351
|
+
"In one sentence, greet {name} from {city}.",
|
|
352
|
+
{"name": col("name__c"), "city": col("homecity__c")},
|
|
353
|
+
model_id="sfdc_ai__DefaultGPT4Omni", # An AI model in your org
|
|
354
|
+
)["response"],
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
dlo_name = "Output_dll"
|
|
358
|
+
client.write_to_dlo(dlo_name, df_upper1, write_mode=WriteMode.APPEND)
|
|
359
|
+
|
|
360
|
+
greeting = client.llm_gateway_generate_text("In one sentence, generate a greeting message", "sfdc_ai__DefaultGPT52")
|
|
361
|
+
|
|
362
|
+
if __name__ == "__main__":
|
|
363
|
+
main()
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
In order to test this code on your local machine before deploying it to Data Cloud, you must first set up an External Client App that allows access to the Agent API. Follow this guide to create the ECA https://developer.salesforce.com/docs/ai/agentforce/guide/agent-api-get-started.html#create-a-salesforce-app. You must use `http://localhost:1717/OauthRedirect` as the callback URL.
|
|
367
|
+
|
|
368
|
+
Once the ECA is set up, log in to your org using this ECA
|
|
369
|
+
```
|
|
370
|
+
sf org login web \
|
|
371
|
+
--alias myorg \
|
|
372
|
+
--instance-url https://{MY_DOMAIN_URL} \
|
|
373
|
+
--client-id {CONSUMER_KEY} \
|
|
374
|
+
--scopes "sfap_api api"
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
then you can test your code using `myorg` alias
|
|
378
|
+
```
|
|
379
|
+
datacustomcode run ./payload/entrypoint.py --sf-cli-org myorg
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
|
|
333
383
|
## Docker usage
|
|
334
384
|
|
|
335
385
|
The SDK provides Docker-based development options that allow you to test your code in an environment that closely resembles Data Cloud's execution environment.
|
|
@@ -305,6 +305,55 @@ Options:
|
|
|
305
305
|
- `--function-invoke-opt TEXT`: Currently we support only `UnstructuredChunking` for functions.
|
|
306
306
|
|
|
307
307
|
|
|
308
|
+
## Testing LLM Gateway
|
|
309
|
+
|
|
310
|
+
You can use AI models configured in Salesforce to generate responses while transforming your data. Below is a sample code example:
|
|
311
|
+
|
|
312
|
+
```
|
|
313
|
+
from datacustomcode.client import Client, llm_gateway_generate_text_col
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def main():
|
|
317
|
+
client = Client()
|
|
318
|
+
df = client.read_dlo("Input__dll")
|
|
319
|
+
# llm_gateway_generate_text_col returns a struct
|
|
320
|
+
# {status, response, error_code, error_message} per row, so per-row
|
|
321
|
+
# failures don't abort the Spark job. Pick the field you want with [].
|
|
322
|
+
df_generated = df.withColumn(
|
|
323
|
+
"greeting__c",
|
|
324
|
+
llm_gateway_generate_text_col(
|
|
325
|
+
"In one sentence, greet {name} from {city}.",
|
|
326
|
+
{"name": col("name__c"), "city": col("homecity__c")},
|
|
327
|
+
model_id="sfdc_ai__DefaultGPT4Omni", # An AI model in your org
|
|
328
|
+
)["response"],
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
dlo_name = "Output_dll"
|
|
332
|
+
client.write_to_dlo(dlo_name, df_upper1, write_mode=WriteMode.APPEND)
|
|
333
|
+
|
|
334
|
+
greeting = client.llm_gateway_generate_text("In one sentence, generate a greeting message", "sfdc_ai__DefaultGPT52")
|
|
335
|
+
|
|
336
|
+
if __name__ == "__main__":
|
|
337
|
+
main()
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
In order to test this code on your local machine before deploying it to Data Cloud, you must first set up an External Client App that allows access to the Agent API. Follow this guide to create the ECA https://developer.salesforce.com/docs/ai/agentforce/guide/agent-api-get-started.html#create-a-salesforce-app. You must use `http://localhost:1717/OauthRedirect` as the callback URL.
|
|
341
|
+
|
|
342
|
+
Once the ECA is set up, log in to your org using this ECA
|
|
343
|
+
```
|
|
344
|
+
sf org login web \
|
|
345
|
+
--alias myorg \
|
|
346
|
+
--instance-url https://{MY_DOMAIN_URL} \
|
|
347
|
+
--client-id {CONSUMER_KEY} \
|
|
348
|
+
--scopes "sfap_api api"
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
then you can test your code using `myorg` alias
|
|
352
|
+
```
|
|
353
|
+
datacustomcode run ./payload/entrypoint.py --sf-cli-org myorg
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
|
|
308
357
|
## Docker usage
|
|
309
358
|
|
|
310
359
|
The SDK provides Docker-based development options that allow you to test your code in an environment that closely resembles Data Cloud's execution environment.
|
|
@@ -18,7 +18,7 @@ license = "Apache-2.0"
|
|
|
18
18
|
name = "salesforce-data-customcode"
|
|
19
19
|
readme = "README.md"
|
|
20
20
|
requires-python = ">=3.10,<3.12"
|
|
21
|
-
version = "6.0.
|
|
21
|
+
version = "6.0.3"
|
|
22
22
|
|
|
23
23
|
[tool.black]
|
|
24
24
|
exclude = '''
|
|
@@ -101,6 +101,7 @@ click = "^8.1.8"
|
|
|
101
101
|
loguru = "^0.7.3"
|
|
102
102
|
numpy = "*"
|
|
103
103
|
pandas = "*"
|
|
104
|
+
pipreqs = "*"
|
|
104
105
|
pydantic = "2.13.1"
|
|
105
106
|
pyspark = "3.5.1"
|
|
106
107
|
python = ">=3.10,<3.12"
|
{salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/__init__.py
RENAMED
|
@@ -17,8 +17,11 @@ __all__ = [
|
|
|
17
17
|
"AuthType",
|
|
18
18
|
"Client",
|
|
19
19
|
"Credentials",
|
|
20
|
+
"DefaultSparkLLMGateway",
|
|
20
21
|
"PrintDataCloudWriter",
|
|
21
22
|
"QueryAPIDataCloudReader",
|
|
23
|
+
"SparkLLMGateway",
|
|
24
|
+
"llm_gateway_generate_text_col",
|
|
22
25
|
]
|
|
23
26
|
|
|
24
27
|
|
|
@@ -44,4 +47,16 @@ def __getattr__(name: str):
|
|
|
44
47
|
from datacustomcode.io.reader.query_api import QueryAPIDataCloudReader
|
|
45
48
|
|
|
46
49
|
return QueryAPIDataCloudReader
|
|
50
|
+
elif name == "SparkLLMGateway":
|
|
51
|
+
from datacustomcode.llm_gateway import SparkLLMGateway
|
|
52
|
+
|
|
53
|
+
return SparkLLMGateway
|
|
54
|
+
elif name == "DefaultSparkLLMGateway":
|
|
55
|
+
from datacustomcode.llm_gateway import DefaultSparkLLMGateway
|
|
56
|
+
|
|
57
|
+
return DefaultSparkLLMGateway
|
|
58
|
+
elif name == "llm_gateway_generate_text_col":
|
|
59
|
+
from datacustomcode.client import llm_gateway_generate_text_col
|
|
60
|
+
|
|
61
|
+
return llm_gateway_generate_text_col
|
|
47
62
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
{salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/client.py
RENAMED
|
@@ -18,24 +18,87 @@ from enum import Enum
|
|
|
18
18
|
from typing import (
|
|
19
19
|
TYPE_CHECKING,
|
|
20
20
|
ClassVar,
|
|
21
|
+
Dict,
|
|
21
22
|
Optional,
|
|
23
|
+
Union,
|
|
22
24
|
)
|
|
23
25
|
|
|
24
26
|
from datacustomcode.config import config
|
|
25
27
|
from datacustomcode.file.path.default import DefaultFindFilePath
|
|
26
28
|
from datacustomcode.io.reader.base import BaseDataCloudReader
|
|
29
|
+
from datacustomcode.llm_gateway_config import spark_llm_gateway_config
|
|
27
30
|
from datacustomcode.spark.default import DefaultSparkSessionProvider
|
|
28
31
|
|
|
29
32
|
if TYPE_CHECKING:
|
|
30
33
|
from pathlib import Path
|
|
31
34
|
|
|
32
|
-
from pyspark.sql import DataFrame as PySparkDataFrame
|
|
35
|
+
from pyspark.sql import Column, DataFrame as PySparkDataFrame
|
|
33
36
|
|
|
34
37
|
from datacustomcode.io.reader.base import BaseDataCloudReader
|
|
35
38
|
from datacustomcode.io.writer.base import BaseDataCloudWriter, WriteMode
|
|
39
|
+
from datacustomcode.llm_gateway.spark_base import SparkLLMGateway
|
|
36
40
|
from datacustomcode.spark.base import BaseSparkSessionProvider
|
|
37
41
|
|
|
38
42
|
|
|
43
|
+
def _build_spark_llm_gateway() -> "SparkLLMGateway":
|
|
44
|
+
"""Instantiate the SDK-configured :class:`SparkLLMGateway`.
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
RuntimeError: If no ``spark_llm_gateway_config`` has been loaded.
|
|
48
|
+
"""
|
|
49
|
+
cfg = spark_llm_gateway_config.spark_llm_gateway_config
|
|
50
|
+
if cfg is None:
|
|
51
|
+
raise RuntimeError(
|
|
52
|
+
"spark_llm_gateway_config is not configured. Add a "
|
|
53
|
+
"'spark_llm_gateway_config' section to config.yaml."
|
|
54
|
+
)
|
|
55
|
+
return cfg.to_object()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def llm_gateway_generate_text_col(
|
|
59
|
+
template: str,
|
|
60
|
+
values: Union[Dict[str, "Column"], "Column"],
|
|
61
|
+
model_id: Optional[str] = None,
|
|
62
|
+
) -> "Column":
|
|
63
|
+
"""Build a Spark Column that runs the LLM Gateway per row.
|
|
64
|
+
|
|
65
|
+
The returned Column yields a struct ``{status, response, error_code,
|
|
66
|
+
error_message}`` for each row. Use ``[...]`` (or ``getField``) to pick the
|
|
67
|
+
field you want, e.g. ``llm_gateway_generate_text_col(...)["response"]``.
|
|
68
|
+
Per-row failures populate ``status`` / ``error_code`` / ``error_message``
|
|
69
|
+
so a single bad row does not abort the whole Spark job.
|
|
70
|
+
|
|
71
|
+
Example:
|
|
72
|
+
|
|
73
|
+
>>> result = llm_gateway_generate_text_col(
|
|
74
|
+
... "In one sentence, greet {name} from {city}.",
|
|
75
|
+
... {"name": col("name__c"), "city": col("homecity__c")},
|
|
76
|
+
... model_id="sfdc_ai__DefaultGPT4Omni",
|
|
77
|
+
... )
|
|
78
|
+
>>> df.withColumn("greeting__c", result["response"])
|
|
79
|
+
>>> # …or keep the struct around and inspect failures:
|
|
80
|
+
>>> df.withColumn("llm", result).select(
|
|
81
|
+
... "llm.status", "llm.response", "llm.error_message"
|
|
82
|
+
... )
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
template: The prompt template, with ``{field}`` placeholders matching
|
|
86
|
+
keys in ``values``. Substitution uses ``str.format``.
|
|
87
|
+
values: Either a mapping from placeholder name to Spark ``Column``, or
|
|
88
|
+
a single ``Column`` whose value is already a struct.
|
|
89
|
+
model_id: LLM model id. Defaults to ``sfdc_ai__DefaultGPT4Omni``.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
A Spark ``Column`` of ``StructType`` with fields ``status``,
|
|
93
|
+
``response``, ``error_code``, and ``error_message`` (all nullable
|
|
94
|
+
strings). On success, ``status == "SUCCESS"`` and ``response`` holds
|
|
95
|
+
the generated text; on failure, ``status == "ERROR"`` and the
|
|
96
|
+
``error_*`` fields carry diagnostic detail.
|
|
97
|
+
"""
|
|
98
|
+
gateway = Client()._get_spark_llm_gateway()
|
|
99
|
+
return gateway.llm_gateway_generate_text_col(template, values, model_id=model_id)
|
|
100
|
+
|
|
101
|
+
|
|
39
102
|
class DataCloudObjectType(Enum):
|
|
40
103
|
DLO = "dlo"
|
|
41
104
|
DMO = "dmo"
|
|
@@ -94,18 +157,21 @@ class Client:
|
|
|
94
157
|
finder: Find a file path
|
|
95
158
|
reader: A custom reader to use for reading Data Cloud objects.
|
|
96
159
|
writer: A custom writer to use for writing Data Cloud objects.
|
|
160
|
+
spark_llm_gateway: Optional custom :class:`SparkLLMGateway`.
|
|
97
161
|
|
|
98
162
|
Example:
|
|
99
163
|
>>> client = Client()
|
|
100
164
|
>>> file_path = client.find_file_path("data.csv")
|
|
101
165
|
>>> dlo = client.read_dlo("my_dlo")
|
|
102
166
|
>>> client.write_to_dmo("my_dmo", dlo)
|
|
167
|
+
>>> answer = client.llm_gateway_generate_text("Generate a greeting message")
|
|
103
168
|
"""
|
|
104
169
|
|
|
105
170
|
_instance: ClassVar[Optional[Client]] = None
|
|
106
171
|
_reader: BaseDataCloudReader
|
|
107
172
|
_writer: BaseDataCloudWriter
|
|
108
173
|
_file: DefaultFindFilePath
|
|
174
|
+
_spark_llm_gateway: Optional[SparkLLMGateway]
|
|
109
175
|
_data_layer_history: dict[DataCloudObjectType, set[str]]
|
|
110
176
|
_code_type: str
|
|
111
177
|
|
|
@@ -114,11 +180,13 @@ class Client:
|
|
|
114
180
|
reader: Optional[BaseDataCloudReader] = None,
|
|
115
181
|
writer: Optional[BaseDataCloudWriter] = None,
|
|
116
182
|
spark_provider: Optional[BaseSparkSessionProvider] = None,
|
|
183
|
+
spark_llm_gateway: Optional[SparkLLMGateway] = None,
|
|
117
184
|
code_type: str = "script",
|
|
118
185
|
) -> Client:
|
|
119
186
|
|
|
120
187
|
if cls._instance is None:
|
|
121
188
|
cls._instance = super().__new__(cls)
|
|
189
|
+
cls._instance._spark_llm_gateway = spark_llm_gateway
|
|
122
190
|
# Initialize Readers and Writers from config
|
|
123
191
|
# and/or provided reader and writer
|
|
124
192
|
if reader is None or writer is None:
|
|
@@ -225,6 +293,41 @@ class Client:
|
|
|
225
293
|
|
|
226
294
|
return self._file.find_file_path(file_name) # type: ignore[no-any-return]
|
|
227
295
|
|
|
296
|
+
def llm_gateway_generate_text(
|
|
297
|
+
self,
|
|
298
|
+
prompt: str,
|
|
299
|
+
model_id: Optional[str] = None,
|
|
300
|
+
) -> str:
|
|
301
|
+
"""Issue a one-shot LLM Gateway call. This is the scalar counterpart to
|
|
302
|
+
:func:`llm_gateway_generate_text_col`: it runs **once** — not per row.
|
|
303
|
+
Use the column helper method instead when you want to fan a prompt out across
|
|
304
|
+
every row of a DataFrame.
|
|
305
|
+
|
|
306
|
+
Example:
|
|
307
|
+
|
|
308
|
+
>>> response = Client().llm_gateway_generate_text(
|
|
309
|
+
... "Generate a greeting message"
|
|
310
|
+
... )
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
prompt: The literal prompt to send. Plain text — no
|
|
314
|
+
``{field}`` substitution is performed on this string.
|
|
315
|
+
model_id: LLM model id to target. Defaults to
|
|
316
|
+
``sfdc_ai__DefaultGPT4Omni`` when ``None``.
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
The generated text as a plain Python ``str``; empty when the
|
|
320
|
+
gateway response carries no generated text.
|
|
321
|
+
"""
|
|
322
|
+
return self._get_spark_llm_gateway().llm_gateway_generate_text(
|
|
323
|
+
prompt, model_id=model_id
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
def _get_spark_llm_gateway(self) -> SparkLLMGateway:
|
|
327
|
+
if self._spark_llm_gateway is None:
|
|
328
|
+
self._spark_llm_gateway = _build_spark_llm_gateway()
|
|
329
|
+
return self._spark_llm_gateway
|
|
330
|
+
|
|
228
331
|
def _validate_data_layer_history_does_not_contain(
|
|
229
332
|
self, data_cloud_object_type: DataCloudObjectType
|
|
230
333
|
) -> None:
|
{salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/deploy.py
RENAMED
|
@@ -26,12 +26,13 @@ from typing import (
|
|
|
26
26
|
Callable,
|
|
27
27
|
Dict,
|
|
28
28
|
List,
|
|
29
|
+
Optional,
|
|
29
30
|
Union,
|
|
30
31
|
)
|
|
31
32
|
|
|
32
33
|
from loguru import logger
|
|
33
34
|
import pydantic
|
|
34
|
-
from pydantic import BaseModel
|
|
35
|
+
from pydantic import BaseModel, model_validator
|
|
35
36
|
import requests
|
|
36
37
|
|
|
37
38
|
from datacustomcode.cmd import cmd_output
|
|
@@ -366,23 +367,86 @@ class BaseConfig(BaseModel):
|
|
|
366
367
|
entryPoint: str
|
|
367
368
|
|
|
368
369
|
|
|
370
|
+
class DataObjectField(BaseModel):
|
|
371
|
+
name: str
|
|
372
|
+
label: str
|
|
373
|
+
dataType: str
|
|
374
|
+
isPrimaryKey: bool = False
|
|
375
|
+
keyQualifierFieldName: Optional[str] = None
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
class DataObject(BaseModel):
|
|
379
|
+
name: str
|
|
380
|
+
label: str
|
|
381
|
+
type: str
|
|
382
|
+
category: str
|
|
383
|
+
fields: list[DataObjectField]
|
|
384
|
+
|
|
385
|
+
|
|
369
386
|
class DataTransformConfig(BaseConfig):
|
|
370
387
|
sdkVersion: str
|
|
371
388
|
dataspace: str
|
|
372
389
|
permissions: Permissions
|
|
390
|
+
dataObjects: Optional[list[DataObject]] = None
|
|
373
391
|
|
|
374
392
|
|
|
375
393
|
class FunctionConfig(BaseConfig):
|
|
376
394
|
pass
|
|
377
395
|
|
|
378
396
|
|
|
397
|
+
class DloPermission(BaseModel):
|
|
398
|
+
dlo: list[str]
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
class DmoPermission(BaseModel):
|
|
402
|
+
dmo: list[str]
|
|
403
|
+
|
|
404
|
+
|
|
379
405
|
class Permissions(BaseModel):
|
|
380
|
-
read: Union[DloPermission]
|
|
381
|
-
write: Union[DloPermission]
|
|
406
|
+
read: Union[DloPermission, DmoPermission]
|
|
407
|
+
write: Union[DloPermission, DmoPermission]
|
|
408
|
+
|
|
409
|
+
@model_validator(mode="after")
|
|
410
|
+
def _no_mixed_layers(self) -> "Permissions":
|
|
411
|
+
read_is_dlo = isinstance(self.read, DloPermission)
|
|
412
|
+
write_is_dlo = isinstance(self.write, DloPermission)
|
|
413
|
+
if read_is_dlo != write_is_dlo:
|
|
414
|
+
raise ValueError(
|
|
415
|
+
"permissions.read and permissions.write must both reference "
|
|
416
|
+
"DLOs or both reference DMOs (got "
|
|
417
|
+
f"read={type(self.read).__name__}, "
|
|
418
|
+
f"write={type(self.write).__name__})"
|
|
419
|
+
)
|
|
420
|
+
return self
|
|
382
421
|
|
|
383
422
|
|
|
384
|
-
|
|
385
|
-
|
|
423
|
+
def _permission_entries(perm: Union[DloPermission, DmoPermission]) -> list[str]:
|
|
424
|
+
"""Return the list of object names regardless of layer (DLO or DMO)."""
|
|
425
|
+
if isinstance(perm, DloPermission):
|
|
426
|
+
return perm.dlo
|
|
427
|
+
return perm.dmo
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _data_object_to_output(obj: DataObject) -> dict[str, Any]:
|
|
431
|
+
"""Convert a config.json DataObject into an outputDataObjects entry."""
|
|
432
|
+
fields: list[dict[str, Any]] = []
|
|
433
|
+
for field in obj.fields:
|
|
434
|
+
entry: dict[str, Any] = {
|
|
435
|
+
"isPrimaryKey": field.isPrimaryKey,
|
|
436
|
+
"label": field.label,
|
|
437
|
+
"name": field.name,
|
|
438
|
+
"type": field.dataType,
|
|
439
|
+
}
|
|
440
|
+
if field.keyQualifierFieldName is not None:
|
|
441
|
+
entry["keyQualifierField"] = field.keyQualifierFieldName
|
|
442
|
+
fields.append(entry)
|
|
443
|
+
return {
|
|
444
|
+
"category": obj.category,
|
|
445
|
+
"fields": fields,
|
|
446
|
+
"label": obj.label,
|
|
447
|
+
"name": obj.name,
|
|
448
|
+
"type": obj.type,
|
|
449
|
+
}
|
|
386
450
|
|
|
387
451
|
|
|
388
452
|
def get_config(directory: str) -> BaseConfig:
|
|
@@ -404,10 +468,17 @@ def get_config(directory: str) -> BaseConfig:
|
|
|
404
468
|
except json.JSONDecodeError as err:
|
|
405
469
|
raise ValueError(f"config.json at {config_path} is not valid JSON") from err
|
|
406
470
|
except pydantic.ValidationError as err:
|
|
407
|
-
|
|
471
|
+
errors = err.errors()
|
|
472
|
+
missing = [e for e in errors if e.get("type") == "missing"]
|
|
473
|
+
if missing and len(missing) == len(errors):
|
|
474
|
+
missing_fields = [str(e["loc"][0]) for e in missing]
|
|
475
|
+
raise ValueError(
|
|
476
|
+
f"config.json at {config_path} is missing required "
|
|
477
|
+
f"fields: {', '.join(missing_fields)}"
|
|
478
|
+
) from err
|
|
479
|
+
messages = [str(e.get("msg", "")) for e in errors]
|
|
408
480
|
raise ValueError(
|
|
409
|
-
f"config.json at {config_path} is
|
|
410
|
-
f"fields: {', '.join(missing_fields)}"
|
|
481
|
+
f"config.json at {config_path} is invalid: {'; '.join(messages)}"
|
|
411
482
|
) from err
|
|
412
483
|
|
|
413
484
|
|
|
@@ -421,26 +492,45 @@ def create_data_transform(
|
|
|
421
492
|
script_name = metadata.name
|
|
422
493
|
request_hydrated = DATA_TRANSFORM_REQUEST_TEMPLATE.copy()
|
|
423
494
|
|
|
424
|
-
# Add nodes for each write DLO
|
|
425
|
-
for i,
|
|
495
|
+
# Add nodes for each write entry (DLO or DMO)
|
|
496
|
+
for i, name in enumerate(
|
|
497
|
+
_permission_entries(data_transform_config.permissions.write), 1
|
|
498
|
+
):
|
|
426
499
|
request_hydrated["nodes"][f"node{i}"] = {
|
|
427
|
-
"relation_name":
|
|
500
|
+
"relation_name": name,
|
|
428
501
|
"config": {"materialized": "table"},
|
|
429
502
|
"compiled_code": "",
|
|
430
503
|
}
|
|
431
504
|
|
|
432
|
-
# Add sources for each read DLO
|
|
433
|
-
for i,
|
|
434
|
-
|
|
505
|
+
# Add sources for each read entry (DLO or DMO)
|
|
506
|
+
for i, name in enumerate(
|
|
507
|
+
_permission_entries(data_transform_config.permissions.read), 1
|
|
508
|
+
):
|
|
509
|
+
request_hydrated["sources"][f"source{i}"] = {"relation_name": name}
|
|
435
510
|
|
|
436
511
|
request_hydrated["macros"]["macro.byoc"]["arguments"][0]["name"] = script_name
|
|
437
512
|
|
|
513
|
+
definition: dict[str, Any] = {
|
|
514
|
+
"type": "DCSQL",
|
|
515
|
+
"manifest": request_hydrated,
|
|
516
|
+
"version": "56.0",
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
# outputDataObjects is only set for DMO-backed transforms. The server requires
|
|
520
|
+
# the schema of any DMO created/updated by the transform; DLO transforms use
|
|
521
|
+
# an existing materialized table and must not include this field.
|
|
522
|
+
if isinstance(data_transform_config.permissions.write, DmoPermission):
|
|
523
|
+
if not data_transform_config.dataObjects:
|
|
524
|
+
raise ValueError(
|
|
525
|
+
"DMO transforms require 'dataObjects' in config.json describing "
|
|
526
|
+
"the schema of each output DMO."
|
|
527
|
+
)
|
|
528
|
+
definition["outputDataObjects"] = [
|
|
529
|
+
_data_object_to_output(obj) for obj in data_transform_config.dataObjects
|
|
530
|
+
]
|
|
531
|
+
|
|
438
532
|
body = {
|
|
439
|
-
"definition":
|
|
440
|
-
"type": "DCSQL",
|
|
441
|
-
"manifest": request_hydrated,
|
|
442
|
-
"version": "56.0",
|
|
443
|
-
},
|
|
533
|
+
"definition": definition,
|
|
444
534
|
"label": f"{metadata.name}",
|
|
445
535
|
"name": f"{metadata.name}",
|
|
446
536
|
"type": "BATCH",
|
|
@@ -15,20 +15,23 @@
|
|
|
15
15
|
|
|
16
16
|
from typing import (
|
|
17
17
|
ClassVar,
|
|
18
|
+
Generic,
|
|
18
19
|
Optional,
|
|
19
20
|
Type,
|
|
20
|
-
|
|
21
|
+
TypeVar,
|
|
21
22
|
)
|
|
22
23
|
|
|
23
24
|
from datacustomcode.common_config import BaseObjectConfig
|
|
24
25
|
|
|
26
|
+
_T = TypeVar("_T")
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
|
|
29
|
+
class CredentialsObjectConfig(BaseObjectConfig, Generic[_T]):
|
|
27
30
|
type_to_create: ClassVar[Type]
|
|
28
31
|
credentials_profile: Optional[str] = None
|
|
29
32
|
sf_cli_org: Optional[str] = None
|
|
30
33
|
|
|
31
|
-
def to_object(self):
|
|
34
|
+
def to_object(self) -> _T:
|
|
32
35
|
"""Create an object instance, automatically including credentials in options"""
|
|
33
36
|
|
|
34
37
|
options = self.options.copy()
|
|
@@ -38,4 +41,5 @@ class CredentialsObjectConfig(BaseObjectConfig):
|
|
|
38
41
|
options["sf_cli_org"] = self.sf_cli_org
|
|
39
42
|
|
|
40
43
|
type_ = self.type_to_create.subclass_from_config_name(self.type_config_name)
|
|
41
|
-
|
|
44
|
+
instance: _T = type_(**options)
|
|
45
|
+
return instance
|
|
@@ -28,7 +28,7 @@ from datacustomcode.einstein_predictions.base import EinsteinPredictions
|
|
|
28
28
|
_E = TypeVar("_E", bound=EinsteinPredictions)
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
class EinsteinPredictionsObjectConfig(CredentialsObjectConfig, Generic[_E]):
|
|
31
|
+
class EinsteinPredictionsObjectConfig(CredentialsObjectConfig[_E], Generic[_E]):
|
|
32
32
|
type_to_create: ClassVar[Type[EinsteinPredictions]] = EinsteinPredictions # type: ignore[type-abstract]
|
|
33
33
|
|
|
34
34
|
|
|
@@ -15,8 +15,14 @@
|
|
|
15
15
|
|
|
16
16
|
from datacustomcode.llm_gateway.base import LLMGateway
|
|
17
17
|
from datacustomcode.llm_gateway.default import DefaultLLMGateway
|
|
18
|
+
from datacustomcode.llm_gateway.errors import LLMGatewayCallError
|
|
19
|
+
from datacustomcode.llm_gateway.spark_base import SparkLLMGateway
|
|
20
|
+
from datacustomcode.llm_gateway.spark_default import DefaultSparkLLMGateway
|
|
18
21
|
|
|
19
22
|
__all__ = [
|
|
20
23
|
"DefaultLLMGateway",
|
|
24
|
+
"DefaultSparkLLMGateway",
|
|
21
25
|
"LLMGateway",
|
|
26
|
+
"LLMGatewayCallError",
|
|
27
|
+
"SparkLLMGateway",
|
|
22
28
|
]
|
|
@@ -12,13 +12,25 @@
|
|
|
12
12
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
13
|
# See the License for the specific language governing permissions and
|
|
14
14
|
# limitations under the License.
|
|
15
|
+
"""Exceptions raised by LLM Gateway implementations."""
|
|
16
|
+
|
|
15
17
|
from __future__ import annotations
|
|
16
18
|
|
|
17
|
-
from
|
|
19
|
+
from typing import Optional
|
|
18
20
|
|
|
19
|
-
from datacustomcode.mixin import UserExtendableNamedConfigMixin
|
|
20
21
|
|
|
22
|
+
class LLMGatewayCallError(RuntimeError):
|
|
23
|
+
"""Raised when an LLM Gateway call returns an error."""
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
message: str,
|
|
28
|
+
*,
|
|
29
|
+
status: Optional[object] = None,
|
|
30
|
+
error_code: Optional[str] = None,
|
|
31
|
+
error_message: Optional[str] = None,
|
|
32
|
+
) -> None:
|
|
33
|
+
super().__init__(message)
|
|
34
|
+
self.status = status
|
|
35
|
+
self.error_code = error_code
|
|
36
|
+
self.error_message = error_message
|