salesforce-data-customcode 6.1.0.dev5__tar.gz → 6.1.0.dev7__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.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/PKG-INFO +1 -1
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/pyproject.toml +1 -1
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/client.py +99 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/config.yaml +6 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/deploy.py +7 -2
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/function/runtime.py +16 -0
- salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/named_credential/__init__.py +28 -0
- salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/named_credential/base.py +54 -0
- salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/named_credential/default.py +93 -0
- salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/named_credential/direct/__init__.py +19 -0
- salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/named_credential/direct/auth.py +191 -0
- salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/named_credential/direct/credentials.py +122 -0
- salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/named_credential/direct/transport.py +110 -0
- salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/named_credential/direct/url_resolver.py +112 -0
- salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/named_credential/errors.py +36 -0
- salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/named_credential/spark_base.py +93 -0
- salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/named_credential/spark_default.py +154 -0
- salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/named_credential/types/__init__.py +14 -0
- salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/named_credential/types/http_method.py +29 -0
- salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/named_credential/types/http_request.py +63 -0
- salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/named_credential/types/http_request_builder.py +88 -0
- salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/named_credential/types/http_response.py +43 -0
- salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/named_credential/types/http_response_builder.py +24 -0
- salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/named_credential_config.py +105 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/run.py +7 -0
- salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/templates/function/example/chunking_with_external_callout/README.md +120 -0
- salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/templates/function/example/chunking_with_external_callout/entrypoint.py +162 -0
- salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/templates/function/example/chunking_with_external_callout/external_callout_config.json +11 -0
- salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/templates/function/example/chunking_with_external_callout/tests/test.json +16 -0
- salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/templates/function/example/chunking_with_prediction/config.json +3 -0
- salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/templates/script/examples/external_callout/README.md +140 -0
- salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/templates/script/examples/external_callout/entrypoint.py +144 -0
- salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/templates/script/examples/external_callout/external_callout_config.json +11 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/LICENSE.txt +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/README.md +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/__init__.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/auth.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/cli.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/cmd.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/common_config.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/config.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/constants.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/credentials.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/einstein_platform_client.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/einstein_platform_config.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/einstein_predictions/__init__.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/einstein_predictions/base.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/einstein_predictions/errors.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/einstein_predictions/impl/default.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/einstein_predictions/spark_base.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/einstein_predictions/spark_default.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/einstein_predictions/types.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/einstein_predictions_config.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/file/__init__.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/file/base.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/file/path/__init__.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/file/path/default.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/function/__init__.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/function/base.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/function/feature_types/__init__.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/function/feature_types/chunking.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/function_utils.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/io/__init__.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/io/base.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/io/reader/__init__.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/io/reader/base.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/io/reader/query_api.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/io/reader/sf_cli.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/io/reader/utils.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/io/writer/__init__.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/io/writer/base.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/io/writer/csv.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/io/writer/print.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/llm_gateway/__init__.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/llm_gateway/base.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/llm_gateway/default.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/llm_gateway/errors.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/llm_gateway/spark_base.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/llm_gateway/spark_default.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/llm_gateway/types/__init__.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/llm_gateway/types/generate_text_request.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/llm_gateway/types/generate_text_request_builder.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/llm_gateway/types/generate_text_response.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/llm_gateway/types/generate_text_response_builder.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/llm_gateway_config.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/mixin.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/py.typed +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/scan.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/spark/__init__.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/spark/base.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/spark/default.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/template.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/__init__.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/function/.devcontainer/devcontainer.json +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/function/Dockerfile.dependencies +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/function/README.md +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/function/build_native_dependencies.sh +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/function/chunking/payload/config.json +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/function/chunking/payload/entrypoint.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/function/chunking/requirements.txt +0 -0
- {salesforce_data_customcode-6.1.0.dev5/src/datacustomcode/templates/function/example/chunking_with_llm → salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/templates/function/example/chunking_with_external_callout}/config.json +0 -0
- {salesforce_data_customcode-6.1.0.dev5/src/datacustomcode/templates/function/example/chunking_with_prediction → salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/templates/function/example/chunking_with_llm}/config.json +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/function/example/chunking_with_llm/entrypoint.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/function/example/chunking_with_llm/files/chunking_prompt.txt +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/function/example/chunking_with_llm/tests/test.json +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/function/example/chunking_with_prediction/entrypoint.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/function/example/chunking_with_prediction/tests/test.json +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/function/payload/config.json +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/function/payload/entrypoint.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/function/payload/utility.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/function/requirements-dev.txt +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/function/requirements.txt +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/script/.devcontainer/devcontainer.json +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/script/Dockerfile +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/script/Dockerfile.dependencies +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/script/README.md +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/script/account.ipynb +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/script/build_native_dependencies.sh +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/script/examples/employee_hierarchy/employee_data.csv +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/script/examples/employee_hierarchy/entrypoint.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/script/examples/streaming_deltas/entrypoint.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/script/jupyterlab.sh +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/script/payload/config.json +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/script/payload/entrypoint.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/script/requirements-dev.txt +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/script/requirements.txt +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/token_provider.py +0 -0
- {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/version.py +0 -0
|
@@ -32,6 +32,7 @@ from datacustomcode.einstein_predictions_config import spark_einstein_prediction
|
|
|
32
32
|
from datacustomcode.file.path.default import DefaultFindFilePath
|
|
33
33
|
from datacustomcode.io.reader.base import BaseDataCloudReader
|
|
34
34
|
from datacustomcode.llm_gateway_config import spark_llm_gateway_config
|
|
35
|
+
from datacustomcode.named_credential_config import spark_named_credential_config
|
|
35
36
|
from datacustomcode.spark.default import DefaultSparkSessionProvider
|
|
36
37
|
|
|
37
38
|
if TYPE_CHECKING:
|
|
@@ -49,6 +50,9 @@ if TYPE_CHECKING:
|
|
|
49
50
|
from datacustomcode.io.reader.base import BaseDataCloudReader
|
|
50
51
|
from datacustomcode.io.writer.base import BaseDataCloudWriter, WriteMode
|
|
51
52
|
from datacustomcode.llm_gateway.spark_base import SparkLLMGateway
|
|
53
|
+
from datacustomcode.named_credential.spark_base import SparkNamedCredential
|
|
54
|
+
from datacustomcode.named_credential.types.http_request import HTTPRequest
|
|
55
|
+
from datacustomcode.named_credential.types.http_response import HTTPResponse
|
|
52
56
|
from datacustomcode.spark.base import BaseSparkSessionProvider
|
|
53
57
|
|
|
54
58
|
|
|
@@ -160,6 +164,21 @@ def _build_spark_einstein_predictions() -> "SparkEinsteinPredictions":
|
|
|
160
164
|
return cfg.to_object()
|
|
161
165
|
|
|
162
166
|
|
|
167
|
+
def _build_spark_named_credential() -> "SparkNamedCredential":
|
|
168
|
+
"""Instantiate the SDK-configured :class:`SparkNamedCredential`.
|
|
169
|
+
|
|
170
|
+
Raises:
|
|
171
|
+
RuntimeError: If no ``spark_named_credential_config`` has been loaded.
|
|
172
|
+
"""
|
|
173
|
+
cfg = spark_named_credential_config.spark_named_credential_config
|
|
174
|
+
if cfg is None:
|
|
175
|
+
raise RuntimeError(
|
|
176
|
+
"spark_named_credential_config is not configured. Add a "
|
|
177
|
+
"'spark_named_credential_config' section to config.yaml."
|
|
178
|
+
)
|
|
179
|
+
return cfg.to_object()
|
|
180
|
+
|
|
181
|
+
|
|
163
182
|
def einstein_predict_col(
|
|
164
183
|
model_api_name: str,
|
|
165
184
|
prediction_type: "PredictionType",
|
|
@@ -209,6 +228,40 @@ def einstein_predict_col(
|
|
|
209
228
|
)
|
|
210
229
|
|
|
211
230
|
|
|
231
|
+
def named_credential_request_col(
|
|
232
|
+
request: "HTTPRequest",
|
|
233
|
+
body: Optional["Column"] = None,
|
|
234
|
+
) -> "Column":
|
|
235
|
+
"""Build a Spark Column that makes one Named Credential callout per row.
|
|
236
|
+
|
|
237
|
+
The endpoint, method, and headers are fixed for the call (taken from
|
|
238
|
+
``request``); only ``body`` varies per row. Use this instead of
|
|
239
|
+
:meth:`Client.named_credential_request` when the callout runs across a
|
|
240
|
+
DataFrame so each row is dispatched independently rather than one-shot on
|
|
241
|
+
the driver.
|
|
242
|
+
|
|
243
|
+
The returned Column yields a struct ``{status, response, error_code,
|
|
244
|
+
error_message}`` for each row. ``response`` is itself a struct
|
|
245
|
+
``{status_code, body, headers}``. Use ``[...]`` to pick a field, e.g.
|
|
246
|
+
``named_credential_request_col(...)["response"]["status_code"]``. A transport
|
|
247
|
+
failure sets ``status`` to ``ERROR`` and populates ``error_message`` (a non-2xx
|
|
248
|
+
HTTP response is still ``SUCCESS`` with its code in ``response.status_code``),
|
|
249
|
+
so a single bad row does not abort the whole Spark job.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
request: The callout template — its symbolic reference, method, and
|
|
253
|
+
headers are applied to every row.
|
|
254
|
+
body: Optional per-row ``Column`` holding the request body as a
|
|
255
|
+
string (or null for no body).
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
A Spark ``Column`` of ``StructType`` with fields ``status``,
|
|
259
|
+
``response``, ``error_code``, and ``error_message``.
|
|
260
|
+
"""
|
|
261
|
+
named_credential = Client()._get_spark_named_credential()
|
|
262
|
+
return named_credential.request_col(request, body=body)
|
|
263
|
+
|
|
264
|
+
|
|
212
265
|
class DataCloudObjectType(Enum):
|
|
213
266
|
DLO = "dlo"
|
|
214
267
|
DMO = "dmo"
|
|
@@ -266,6 +319,14 @@ class _BaseClient:
|
|
|
266
319
|
spark_llm_gateway: Optional custom :class:`SparkLLMGateway`.
|
|
267
320
|
spark_einstein_predictions: Optional custom
|
|
268
321
|
:class:`SparkEinsteinPredictions`.
|
|
322
|
+
spark_named_credential: Optional custom :class:`SparkNamedCredential`.
|
|
323
|
+
|
|
324
|
+
Example:
|
|
325
|
+
>>> client = Client()
|
|
326
|
+
>>> file_path = client.find_file_path("data.csv")
|
|
327
|
+
>>> dlo = client.read_dlo("my_dlo")
|
|
328
|
+
>>> client.write_to_dmo("my_dmo", dlo)
|
|
329
|
+
>>> answer = client.llm_gateway_generate_text("Generate a greeting message")
|
|
269
330
|
"""
|
|
270
331
|
|
|
271
332
|
# Each concrete subclass gets its own ``_instance`` slot: reads fall through
|
|
@@ -285,6 +346,7 @@ class _BaseClient:
|
|
|
285
346
|
_file: DefaultFindFilePath
|
|
286
347
|
_spark_llm_gateway: Optional[SparkLLMGateway]
|
|
287
348
|
_spark_einstein_predictions: Optional[SparkEinsteinPredictions]
|
|
349
|
+
_spark_named_credential: Optional[SparkNamedCredential]
|
|
288
350
|
_data_layer_history: dict[DataCloudObjectType, set[str]]
|
|
289
351
|
_code_type: str
|
|
290
352
|
|
|
@@ -295,6 +357,7 @@ class _BaseClient:
|
|
|
295
357
|
spark_provider: Optional[BaseSparkSessionProvider] = None,
|
|
296
358
|
spark_llm_gateway: Optional[SparkLLMGateway] = None,
|
|
297
359
|
spark_einstein_predictions: Optional[SparkEinsteinPredictions] = None,
|
|
360
|
+
spark_named_credential: Optional[SparkNamedCredential] = None,
|
|
298
361
|
code_type: str = "script",
|
|
299
362
|
) -> _ClientT:
|
|
300
363
|
|
|
@@ -302,6 +365,7 @@ class _BaseClient:
|
|
|
302
365
|
instance = super().__new__(cls)
|
|
303
366
|
instance._spark_llm_gateway = spark_llm_gateway
|
|
304
367
|
instance._spark_einstein_predictions = spark_einstein_predictions
|
|
368
|
+
instance._spark_named_credential = spark_named_credential
|
|
305
369
|
# Initialize Readers and Writers from config
|
|
306
370
|
# and/or provided reader and writer
|
|
307
371
|
if reader is None or writer is None:
|
|
@@ -474,6 +538,41 @@ class _BaseClient:
|
|
|
474
538
|
self._spark_einstein_predictions = _build_spark_einstein_predictions()
|
|
475
539
|
return self._spark_einstein_predictions
|
|
476
540
|
|
|
541
|
+
def named_credential_request(
|
|
542
|
+
self,
|
|
543
|
+
request: "HTTPRequest",
|
|
544
|
+
body: Optional[str] = None,
|
|
545
|
+
) -> "HTTPResponse":
|
|
546
|
+
"""Issue a one-shot Named Credential external callout. This is the
|
|
547
|
+
scalar counterpart to :func:`named_credential_request_col`: it runs
|
|
548
|
+
**once** on the driver — not per row. Use the column helper method
|
|
549
|
+
instead when you want to fan a callout out across every row of a
|
|
550
|
+
DataFrame.
|
|
551
|
+
|
|
552
|
+
Example:
|
|
553
|
+
|
|
554
|
+
>>> from datacustomcode.named_credential.types.http_request_builder \\
|
|
555
|
+
... import HTTPRequestBuilder
|
|
556
|
+
>>> request = (
|
|
557
|
+
... HTTPRequestBuilder().set_url("callout:NC/search").build()
|
|
558
|
+
... )
|
|
559
|
+
>>> response = Client().named_credential_request(request)
|
|
560
|
+
|
|
561
|
+
Args:
|
|
562
|
+
request: The callout request
|
|
563
|
+
body: Optional request body. Set the ``Content-Type`` header to
|
|
564
|
+
match the format; the SDK does not assume or inject one.
|
|
565
|
+
|
|
566
|
+
Returns:
|
|
567
|
+
The external service's response.
|
|
568
|
+
"""
|
|
569
|
+
return self._get_spark_named_credential().request(request, body=body)
|
|
570
|
+
|
|
571
|
+
def _get_spark_named_credential(self) -> SparkNamedCredential:
|
|
572
|
+
if self._spark_named_credential is None:
|
|
573
|
+
self._spark_named_credential = _build_spark_named_credential()
|
|
574
|
+
return self._spark_named_credential
|
|
575
|
+
|
|
477
576
|
def _validate_data_layer_history_does_not_contain(
|
|
478
577
|
self, data_cloud_object_type: DataCloudObjectType
|
|
479
578
|
) -> None:
|
|
@@ -38,6 +38,9 @@ import requests
|
|
|
38
38
|
|
|
39
39
|
from datacustomcode.cmd import cmd_output
|
|
40
40
|
from datacustomcode.constants import REQUEST_TYPE_TO_FEATURE
|
|
41
|
+
from datacustomcode.named_credential.direct.credentials import (
|
|
42
|
+
EXTERNAL_CALLOUT_CREDENTIAL,
|
|
43
|
+
)
|
|
41
44
|
from datacustomcode.scan import find_base_directory, get_package_type
|
|
42
45
|
|
|
43
46
|
DATA_CUSTOM_CODE_PATH = "services/data/v63.0/ssot/data-custom-code"
|
|
@@ -248,6 +251,8 @@ DEPENDENCIES_ARCHIVE_PATH = os.path.join(
|
|
|
248
251
|
)
|
|
249
252
|
PY_FILES_PATH = os.path.join("payload", "py-files")
|
|
250
253
|
ZIP_FILE_NAME = "deployment.zip"
|
|
254
|
+
# Local-only files that must never be packaged into the deployment zip.
|
|
255
|
+
EXCLUDED_FILES = (".DS_Store", EXTERNAL_CALLOUT_CREDENTIAL)
|
|
251
256
|
|
|
252
257
|
|
|
253
258
|
def prepare_dependency_archive(
|
|
@@ -628,9 +633,9 @@ def zip(
|
|
|
628
633
|
|
|
629
634
|
with zipfile.ZipFile(ZIP_FILE_NAME, "w", zipfile.ZIP_DEFLATED) as zipf:
|
|
630
635
|
for root, dirs, files in os.walk(directory):
|
|
631
|
-
# Skip .DS_Store
|
|
636
|
+
# Skip .DS_Store and local credentials.
|
|
632
637
|
for file in files:
|
|
633
|
-
if file
|
|
638
|
+
if file not in EXCLUDED_FILES:
|
|
634
639
|
abs_path = os.path.join(root, file)
|
|
635
640
|
arcname = os.path.relpath(abs_path, directory)
|
|
636
641
|
zipf.write(abs_path, arcname)
|
|
@@ -23,6 +23,8 @@ from datacustomcode.file.path.default import DefaultFindFilePath
|
|
|
23
23
|
from datacustomcode.function.base import BaseRuntime
|
|
24
24
|
from datacustomcode.llm_gateway.base import LLMGateway
|
|
25
25
|
from datacustomcode.llm_gateway_config import llm_gateway_config
|
|
26
|
+
from datacustomcode.named_credential.base import NamedCredential
|
|
27
|
+
from datacustomcode.named_credential_config import named_credential_config
|
|
26
28
|
|
|
27
29
|
|
|
28
30
|
class Runtime(BaseRuntime):
|
|
@@ -69,6 +71,7 @@ class Runtime(BaseRuntime):
|
|
|
69
71
|
self._llm_gateway: Optional[LLMGateway] = None
|
|
70
72
|
self._file = DefaultFindFilePath()
|
|
71
73
|
self._einstein_predictions: Optional[EinsteinPredictions] = None
|
|
74
|
+
self._named_credential: Optional[NamedCredential] = None
|
|
72
75
|
|
|
73
76
|
@property
|
|
74
77
|
def llm_gateway(self) -> LLMGateway:
|
|
@@ -98,3 +101,16 @@ class Runtime(BaseRuntime):
|
|
|
98
101
|
einstein_predictions_config.einstein_predictions_config.to_object()
|
|
99
102
|
)
|
|
100
103
|
return self._einstein_predictions
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def named_credential(self) -> NamedCredential:
|
|
107
|
+
if self._named_credential is None:
|
|
108
|
+
if named_credential_config.named_credential_config is None:
|
|
109
|
+
raise RuntimeError(
|
|
110
|
+
"Named Credential is not configured. Add "
|
|
111
|
+
"'named_credential_config' section to config.yaml"
|
|
112
|
+
)
|
|
113
|
+
self._named_credential = (
|
|
114
|
+
named_credential_config.named_credential_config.to_object()
|
|
115
|
+
)
|
|
116
|
+
return self._named_credential
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
|
|
16
|
+
from datacustomcode.named_credential.base import NamedCredential
|
|
17
|
+
from datacustomcode.named_credential.default import DefaultNamedCredential
|
|
18
|
+
from datacustomcode.named_credential.errors import NamedCredentialCallError
|
|
19
|
+
from datacustomcode.named_credential.spark_base import SparkNamedCredential
|
|
20
|
+
from datacustomcode.named_credential.spark_default import DefaultSparkNamedCredential
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"DefaultNamedCredential",
|
|
24
|
+
"DefaultSparkNamedCredential",
|
|
25
|
+
"NamedCredential",
|
|
26
|
+
"NamedCredentialCallError",
|
|
27
|
+
"SparkNamedCredential",
|
|
28
|
+
]
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from abc import ABC, abstractmethod
|
|
18
|
+
from typing import TYPE_CHECKING, Optional
|
|
19
|
+
|
|
20
|
+
from datacustomcode.mixin import UserExtendableNamedConfigMixin
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from datacustomcode.named_credential.types.http_request import HTTPRequest
|
|
24
|
+
from datacustomcode.named_credential.types.http_response import HTTPResponse
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class NamedCredential(ABC, UserExtendableNamedConfigMixin):
|
|
28
|
+
CONFIG_NAME: str
|
|
29
|
+
|
|
30
|
+
def __init__(self, **kwargs):
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def request(
|
|
35
|
+
self,
|
|
36
|
+
request: HTTPRequest,
|
|
37
|
+
body: Optional[str] = None,
|
|
38
|
+
) -> HTTPResponse:
|
|
39
|
+
"""Make an external callout through a Named Credential.
|
|
40
|
+
|
|
41
|
+
The endpoint and its authentication are resolved server-side from the
|
|
42
|
+
Named Credential referenced by ``request.url``; the function never sees
|
|
43
|
+
the external credential.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
request: The callout request
|
|
47
|
+
body: Optional request body. Set the
|
|
48
|
+
``Content-Type`` header to match the format; the SDK
|
|
49
|
+
does not assume or inject one.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
The external service's response.
|
|
53
|
+
"""
|
|
54
|
+
...
|
|
@@ -0,0 +1,93 @@
|
|
|
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
|
+
|
|
16
|
+
from typing import (
|
|
17
|
+
Any,
|
|
18
|
+
Dict,
|
|
19
|
+
Optional,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from datacustomcode.named_credential.base import NamedCredential
|
|
23
|
+
from datacustomcode.named_credential.types.http_request import HTTPRequest
|
|
24
|
+
from datacustomcode.named_credential.types.http_response import HTTPResponse
|
|
25
|
+
from datacustomcode.named_credential.types.http_response_builder import (
|
|
26
|
+
HTTPResponseBuilder,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class DefaultNamedCredential(NamedCredential):
|
|
31
|
+
"""
|
|
32
|
+
Executes the callout directly via :class:`DirectCalloutTransport`, resolving
|
|
33
|
+
the URL from the Named Credential Connect API (falling back to
|
|
34
|
+
``external_callout_config.json``) and injecting auth from the same file.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
CONFIG_NAME = "DefaultNamedCredential"
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
credentials_profile: str = "default",
|
|
42
|
+
sf_cli_org: Optional[str] = None,
|
|
43
|
+
credential_file: Optional[str] = None,
|
|
44
|
+
**kwargs: Any,
|
|
45
|
+
) -> None:
|
|
46
|
+
super().__init__(**kwargs)
|
|
47
|
+
self._credentials_profile = credentials_profile
|
|
48
|
+
self._sf_cli_org = sf_cli_org
|
|
49
|
+
self._credential_file = credential_file
|
|
50
|
+
self._transport: Optional[Any] = None
|
|
51
|
+
|
|
52
|
+
def request(
|
|
53
|
+
self,
|
|
54
|
+
request: HTTPRequest,
|
|
55
|
+
body: Optional[str] = None,
|
|
56
|
+
) -> HTTPResponse:
|
|
57
|
+
# Body and response are treated as opaque strings; the SDK makes no
|
|
58
|
+
# assumption about their format (JSON, XML, text, ...).
|
|
59
|
+
callout_request = {
|
|
60
|
+
"path": request.url,
|
|
61
|
+
"method": request.method,
|
|
62
|
+
"headers": dict(request.headers),
|
|
63
|
+
"body": body if body is not None else "",
|
|
64
|
+
}
|
|
65
|
+
callout_response = self._callout(callout_request)
|
|
66
|
+
return HTTPResponseBuilder.build(
|
|
67
|
+
{
|
|
68
|
+
"status_code": callout_response.get("status_code"),
|
|
69
|
+
"headers": callout_response.get("headers", {}),
|
|
70
|
+
"body": callout_response.get("body") or "",
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def _callout(self, callout_request: Dict[str, Any]) -> Dict[str, Any]:
|
|
75
|
+
"""Execute the callout via the transport.
|
|
76
|
+
|
|
77
|
+
Returns a dict with ``status_code``, ``headers``, and ``body``.
|
|
78
|
+
"""
|
|
79
|
+
result: Dict[str, Any] = self._get_transport().callout(callout_request)
|
|
80
|
+
return result
|
|
81
|
+
|
|
82
|
+
def _get_transport(self) -> Any:
|
|
83
|
+
if self._transport is None:
|
|
84
|
+
from datacustomcode.named_credential.direct.transport import (
|
|
85
|
+
DirectCalloutTransport,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
self._transport = DirectCalloutTransport(
|
|
89
|
+
credentials_profile=self._credentials_profile,
|
|
90
|
+
sf_cli_org=self._sf_cli_org,
|
|
91
|
+
credential_file=self._credential_file,
|
|
92
|
+
)
|
|
93
|
+
return self._transport
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
"""Named Credential callout path.
|
|
16
|
+
|
|
17
|
+
This resolves the endpoint via the NamedCredential Connect API
|
|
18
|
+
and injects auth from a developer-provided ``external_callout_config.json``
|
|
19
|
+
"""
|
|
@@ -0,0 +1,191 @@
|
|
|
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
|
+
"""
|
|
16
|
+
Inject external credential auth into an outgoing request.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import base64
|
|
22
|
+
import datetime
|
|
23
|
+
import hashlib
|
|
24
|
+
import hmac
|
|
25
|
+
from typing import (
|
|
26
|
+
TYPE_CHECKING,
|
|
27
|
+
Any,
|
|
28
|
+
Dict,
|
|
29
|
+
)
|
|
30
|
+
import urllib.parse
|
|
31
|
+
|
|
32
|
+
from requests.auth import AuthBase
|
|
33
|
+
|
|
34
|
+
from datacustomcode.named_credential.direct.credentials import AuthType
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from requests.models import PreparedRequest
|
|
38
|
+
|
|
39
|
+
_SIGV4_ALGORITHM = "AWS4-HMAC-SHA256"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class DynamicAuthHandler(AuthBase):
|
|
43
|
+
def __init__(self, cred_config: Dict[str, Any]) -> None:
|
|
44
|
+
self.config = cred_config
|
|
45
|
+
self.auth_type = cred_config.get("auth_type")
|
|
46
|
+
|
|
47
|
+
def __call__(self, request: PreparedRequest) -> PreparedRequest:
|
|
48
|
+
if self.auth_type == AuthType.BASIC.value:
|
|
49
|
+
user = self.config.get("username", "")
|
|
50
|
+
pwd = self.config.get("password", "")
|
|
51
|
+
token = base64.b64encode(f"{user}:{pwd}".encode()).decode()
|
|
52
|
+
request.headers["Authorization"] = f"Basic {token}"
|
|
53
|
+
|
|
54
|
+
elif self.auth_type == AuthType.CUSTOM.value:
|
|
55
|
+
for name, value in self.config.get("custom_headers", {}).items():
|
|
56
|
+
request.headers[name] = value
|
|
57
|
+
|
|
58
|
+
elif self.auth_type in (AuthType.OAUTH.value, AuthType.JWT.value):
|
|
59
|
+
bearer = self.config.get("access_token") or self.config.get("token")
|
|
60
|
+
if not bearer:
|
|
61
|
+
raise ValueError(
|
|
62
|
+
f"'{self.auth_type}' auth requires an 'access_token' or 'token'."
|
|
63
|
+
)
|
|
64
|
+
request.headers["Authorization"] = f"Bearer {bearer}"
|
|
65
|
+
|
|
66
|
+
elif self.auth_type == AuthType.AWS_SIG_V4.value:
|
|
67
|
+
self._sign_aws_sigv4(request)
|
|
68
|
+
|
|
69
|
+
else:
|
|
70
|
+
raise ValueError(f"Unsupported auth_type '{self.auth_type}'.")
|
|
71
|
+
|
|
72
|
+
return request
|
|
73
|
+
|
|
74
|
+
def _sign_aws_sigv4(self, request: PreparedRequest) -> None:
|
|
75
|
+
"""Sign ``request`` with AWS Signature Version 4.
|
|
76
|
+
|
|
77
|
+
Requires ``aws_access_key_id``, ``aws_secret_access_key``, ``aws_region``,
|
|
78
|
+
and ``aws_service`` in the credential config; ``aws_session_token`` is
|
|
79
|
+
optional (for temporary credentials). The signed date, payload hash, and
|
|
80
|
+
(when present) session token are added as ``x-amz-*`` headers so the sent
|
|
81
|
+
request matches what was signed.
|
|
82
|
+
"""
|
|
83
|
+
access_key = self.config.get("aws_access_key_id")
|
|
84
|
+
secret_key = self.config.get("aws_secret_access_key")
|
|
85
|
+
region = self.config.get("aws_region")
|
|
86
|
+
service = self.config.get("aws_service")
|
|
87
|
+
session_token = self.config.get("aws_session_token")
|
|
88
|
+
missing = [
|
|
89
|
+
name
|
|
90
|
+
for name, value in (
|
|
91
|
+
("aws_access_key_id", access_key),
|
|
92
|
+
("aws_secret_access_key", secret_key),
|
|
93
|
+
("aws_region", region),
|
|
94
|
+
("aws_service", service),
|
|
95
|
+
)
|
|
96
|
+
if not value
|
|
97
|
+
]
|
|
98
|
+
if missing:
|
|
99
|
+
raise ValueError(f"'{self.auth_type}' auth requires {', '.join(missing)}.")
|
|
100
|
+
access_key = str(access_key)
|
|
101
|
+
secret_key = str(secret_key)
|
|
102
|
+
region = str(region)
|
|
103
|
+
service = str(service)
|
|
104
|
+
|
|
105
|
+
parsed = urllib.parse.urlsplit(str(request.url or ""))
|
|
106
|
+
host = parsed.netloc
|
|
107
|
+
canonical_uri = urllib.parse.quote(parsed.path or "/", safe="/-_.~")
|
|
108
|
+
canonical_query = _canonical_query_string(parsed.query)
|
|
109
|
+
|
|
110
|
+
body = request.body or b""
|
|
111
|
+
if isinstance(body, str):
|
|
112
|
+
body = body.encode("utf-8")
|
|
113
|
+
payload_hash = hashlib.sha256(body).hexdigest()
|
|
114
|
+
|
|
115
|
+
now = datetime.datetime.now(datetime.timezone.utc)
|
|
116
|
+
amz_date = now.strftime("%Y%m%dT%H%M%SZ")
|
|
117
|
+
datestamp = now.strftime("%Y%m%d")
|
|
118
|
+
|
|
119
|
+
request.headers["x-amz-date"] = amz_date
|
|
120
|
+
request.headers["x-amz-content-sha256"] = payload_hash
|
|
121
|
+
if session_token:
|
|
122
|
+
request.headers["x-amz-security-token"] = session_token
|
|
123
|
+
|
|
124
|
+
signed = {
|
|
125
|
+
"host": host,
|
|
126
|
+
"x-amz-content-sha256": payload_hash,
|
|
127
|
+
"x-amz-date": amz_date,
|
|
128
|
+
}
|
|
129
|
+
if session_token:
|
|
130
|
+
signed["x-amz-security-token"] = session_token
|
|
131
|
+
signed_headers = ";".join(sorted(signed))
|
|
132
|
+
canonical_headers = "".join(
|
|
133
|
+
f"{name}:{signed[name]}\n" for name in sorted(signed)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
canonical_request = "\n".join(
|
|
137
|
+
[
|
|
138
|
+
request.method or "GET",
|
|
139
|
+
canonical_uri,
|
|
140
|
+
canonical_query,
|
|
141
|
+
canonical_headers,
|
|
142
|
+
signed_headers,
|
|
143
|
+
payload_hash,
|
|
144
|
+
]
|
|
145
|
+
)
|
|
146
|
+
credential_scope = f"{datestamp}/{region}/{service}/aws4_request"
|
|
147
|
+
string_to_sign = "\n".join(
|
|
148
|
+
[
|
|
149
|
+
_SIGV4_ALGORITHM,
|
|
150
|
+
amz_date,
|
|
151
|
+
credential_scope,
|
|
152
|
+
hashlib.sha256(canonical_request.encode("utf-8")).hexdigest(),
|
|
153
|
+
]
|
|
154
|
+
)
|
|
155
|
+
signing_key = _derive_signing_key(secret_key, datestamp, region, service)
|
|
156
|
+
signature = hmac.new(
|
|
157
|
+
signing_key, string_to_sign.encode("utf-8"), hashlib.sha256
|
|
158
|
+
).hexdigest()
|
|
159
|
+
|
|
160
|
+
request.headers["Authorization"] = (
|
|
161
|
+
f"{_SIGV4_ALGORITHM} Credential={access_key}/{credential_scope}, "
|
|
162
|
+
f"SignedHeaders={signed_headers}, Signature={signature}"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _canonical_query_string(query: str) -> str:
|
|
167
|
+
"""Build the AWS Sig V4 canonical query string from a raw query string."""
|
|
168
|
+
pairs = urllib.parse.parse_qsl(query, keep_blank_values=True)
|
|
169
|
+
encoded = [
|
|
170
|
+
(
|
|
171
|
+
urllib.parse.quote(key, safe="-_.~"),
|
|
172
|
+
urllib.parse.quote(value, safe="-_.~"),
|
|
173
|
+
)
|
|
174
|
+
for key, value in pairs
|
|
175
|
+
]
|
|
176
|
+
encoded.sort()
|
|
177
|
+
return "&".join(f"{key}={value}" for key, value in encoded)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _derive_signing_key(
|
|
181
|
+
secret_key: str, datestamp: str, region: str, service: str
|
|
182
|
+
) -> bytes:
|
|
183
|
+
"""Derive the AWS Sig V4 signing key via the chained HMAC-SHA256 sequence."""
|
|
184
|
+
|
|
185
|
+
def _hmac(key: bytes, msg: str) -> bytes:
|
|
186
|
+
return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
|
|
187
|
+
|
|
188
|
+
k_date = _hmac(f"AWS4{secret_key}".encode(), datestamp)
|
|
189
|
+
k_region = _hmac(k_date, region)
|
|
190
|
+
k_service = _hmac(k_region, service)
|
|
191
|
+
return _hmac(k_service, "aws4_request")
|