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.
Files changed (128) hide show
  1. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/PKG-INFO +1 -1
  2. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/pyproject.toml +1 -1
  3. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/client.py +99 -0
  4. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/config.yaml +6 -0
  5. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/deploy.py +7 -2
  6. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/function/runtime.py +16 -0
  7. salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/named_credential/__init__.py +28 -0
  8. salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/named_credential/base.py +54 -0
  9. salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/named_credential/default.py +93 -0
  10. salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/named_credential/direct/__init__.py +19 -0
  11. salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/named_credential/direct/auth.py +191 -0
  12. salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/named_credential/direct/credentials.py +122 -0
  13. salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/named_credential/direct/transport.py +110 -0
  14. salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/named_credential/direct/url_resolver.py +112 -0
  15. salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/named_credential/errors.py +36 -0
  16. salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/named_credential/spark_base.py +93 -0
  17. salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/named_credential/spark_default.py +154 -0
  18. salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/named_credential/types/__init__.py +14 -0
  19. salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/named_credential/types/http_method.py +29 -0
  20. salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/named_credential/types/http_request.py +63 -0
  21. salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/named_credential/types/http_request_builder.py +88 -0
  22. salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/named_credential/types/http_response.py +43 -0
  23. salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/named_credential/types/http_response_builder.py +24 -0
  24. salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/named_credential_config.py +105 -0
  25. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/run.py +7 -0
  26. salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/templates/function/example/chunking_with_external_callout/README.md +120 -0
  27. salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/templates/function/example/chunking_with_external_callout/entrypoint.py +162 -0
  28. salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/templates/function/example/chunking_with_external_callout/external_callout_config.json +11 -0
  29. salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/templates/function/example/chunking_with_external_callout/tests/test.json +16 -0
  30. salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/templates/function/example/chunking_with_prediction/config.json +3 -0
  31. salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/templates/script/examples/external_callout/README.md +140 -0
  32. salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/templates/script/examples/external_callout/entrypoint.py +144 -0
  33. salesforce_data_customcode-6.1.0.dev7/src/datacustomcode/templates/script/examples/external_callout/external_callout_config.json +11 -0
  34. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/LICENSE.txt +0 -0
  35. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/README.md +0 -0
  36. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/__init__.py +0 -0
  37. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/auth.py +0 -0
  38. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/cli.py +0 -0
  39. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/cmd.py +0 -0
  40. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/common_config.py +0 -0
  41. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/config.py +0 -0
  42. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/constants.py +0 -0
  43. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/credentials.py +0 -0
  44. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/einstein_platform_client.py +0 -0
  45. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/einstein_platform_config.py +0 -0
  46. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/einstein_predictions/__init__.py +0 -0
  47. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/einstein_predictions/base.py +0 -0
  48. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/einstein_predictions/errors.py +0 -0
  49. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/einstein_predictions/impl/default.py +0 -0
  50. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/einstein_predictions/spark_base.py +0 -0
  51. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/einstein_predictions/spark_default.py +0 -0
  52. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/einstein_predictions/types.py +0 -0
  53. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/einstein_predictions_config.py +0 -0
  54. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/file/__init__.py +0 -0
  55. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/file/base.py +0 -0
  56. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/file/path/__init__.py +0 -0
  57. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/file/path/default.py +0 -0
  58. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/function/__init__.py +0 -0
  59. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/function/base.py +0 -0
  60. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/function/feature_types/__init__.py +0 -0
  61. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/function/feature_types/chunking.py +0 -0
  62. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/function_utils.py +0 -0
  63. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/io/__init__.py +0 -0
  64. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/io/base.py +0 -0
  65. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/io/reader/__init__.py +0 -0
  66. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/io/reader/base.py +0 -0
  67. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/io/reader/query_api.py +0 -0
  68. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/io/reader/sf_cli.py +0 -0
  69. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/io/reader/utils.py +0 -0
  70. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/io/writer/__init__.py +0 -0
  71. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/io/writer/base.py +0 -0
  72. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/io/writer/csv.py +0 -0
  73. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/io/writer/print.py +0 -0
  74. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/llm_gateway/__init__.py +0 -0
  75. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/llm_gateway/base.py +0 -0
  76. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/llm_gateway/default.py +0 -0
  77. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/llm_gateway/errors.py +0 -0
  78. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/llm_gateway/spark_base.py +0 -0
  79. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/llm_gateway/spark_default.py +0 -0
  80. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/llm_gateway/types/__init__.py +0 -0
  81. {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
  82. {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
  83. {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
  84. {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
  85. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/llm_gateway_config.py +0 -0
  86. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/mixin.py +0 -0
  87. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/py.typed +0 -0
  88. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/scan.py +0 -0
  89. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/spark/__init__.py +0 -0
  90. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/spark/base.py +0 -0
  91. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/spark/default.py +0 -0
  92. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/template.py +0 -0
  93. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/__init__.py +0 -0
  94. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/function/.devcontainer/devcontainer.json +0 -0
  95. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/function/Dockerfile.dependencies +0 -0
  96. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/function/README.md +0 -0
  97. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/function/build_native_dependencies.sh +0 -0
  98. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/function/chunking/payload/config.json +0 -0
  99. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/function/chunking/payload/entrypoint.py +0 -0
  100. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/function/chunking/requirements.txt +0 -0
  101. {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
  102. {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
  103. {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
  104. {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
  105. {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
  106. {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
  107. {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
  108. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/function/payload/config.json +0 -0
  109. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/function/payload/entrypoint.py +0 -0
  110. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/function/payload/utility.py +0 -0
  111. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/function/requirements-dev.txt +0 -0
  112. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/function/requirements.txt +0 -0
  113. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/script/.devcontainer/devcontainer.json +0 -0
  114. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/script/Dockerfile +0 -0
  115. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/script/Dockerfile.dependencies +0 -0
  116. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/script/README.md +0 -0
  117. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/script/account.ipynb +0 -0
  118. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/script/build_native_dependencies.sh +0 -0
  119. {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
  120. {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
  121. {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
  122. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/script/jupyterlab.sh +0 -0
  123. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/script/payload/config.json +0 -0
  124. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/script/payload/entrypoint.py +0 -0
  125. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/script/requirements-dev.txt +0 -0
  126. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/templates/script/requirements.txt +0 -0
  127. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/src/datacustomcode/token_provider.py +0 -0
  128. {salesforce_data_customcode-6.1.0.dev5 → salesforce_data_customcode-6.1.0.dev7}/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.1.0.dev5
3
+ Version: 6.1.0.dev7
4
4
  Summary: Data Cloud Custom Code SDK
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE.txt
@@ -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.1.0.dev5"
21
+ version = "6.1.0.dev7"
22
22
 
23
23
  [tool.black]
24
24
  exclude = '''
@@ -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:
@@ -34,3 +34,9 @@ llm_gateway_config:
34
34
 
35
35
  spark_llm_gateway_config:
36
36
  type_config_name: DefaultSparkLLMGateway
37
+
38
+ named_credential_config:
39
+ type_config_name: DefaultNamedCredential
40
+
41
+ spark_named_credential_config:
42
+ type_config_name: DefaultSparkNamedCredential
@@ -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 files when adding to zip
636
+ # Skip .DS_Store and local credentials.
632
637
  for file in files:
633
- if file != ".DS_Store":
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")