salesforce-data-customcode 6.0.4.dev3__py3-none-any.whl → 6.0.6.dev1__py3-none-any.whl

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.
@@ -17,10 +17,13 @@ __all__ = [
17
17
  "AuthType",
18
18
  "Client",
19
19
  "Credentials",
20
+ "DefaultSparkEinsteinPredictions",
20
21
  "DefaultSparkLLMGateway",
21
22
  "PrintDataCloudWriter",
22
23
  "QueryAPIDataCloudReader",
24
+ "SparkEinsteinPredictions",
23
25
  "SparkLLMGateway",
26
+ "einstein_predict_col",
24
27
  "llm_gateway_generate_text_col",
25
28
  ]
26
29
 
@@ -59,4 +62,16 @@ def __getattr__(name: str):
59
62
  from datacustomcode.client import llm_gateway_generate_text_col
60
63
 
61
64
  return llm_gateway_generate_text_col
65
+ elif name == "SparkEinsteinPredictions":
66
+ from datacustomcode.einstein_predictions import SparkEinsteinPredictions
67
+
68
+ return SparkEinsteinPredictions
69
+ elif name == "DefaultSparkEinsteinPredictions":
70
+ from datacustomcode.einstein_predictions import DefaultSparkEinsteinPredictions
71
+
72
+ return DefaultSparkEinsteinPredictions
73
+ elif name == "einstein_predict_col":
74
+ from datacustomcode.client import einstein_predict_col
75
+
76
+ return einstein_predict_col
62
77
  raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
datacustomcode/client.py CHANGED
@@ -17,6 +17,7 @@ from __future__ import annotations
17
17
  from enum import Enum
18
18
  from typing import (
19
19
  TYPE_CHECKING,
20
+ Any,
20
21
  ClassVar,
21
22
  Dict,
22
23
  Optional,
@@ -24,6 +25,7 @@ from typing import (
24
25
  )
25
26
 
26
27
  from datacustomcode.config import config
28
+ from datacustomcode.einstein_predictions_config import spark_einstein_predictions_config
27
29
  from datacustomcode.file.path.default import DefaultFindFilePath
28
30
  from datacustomcode.io.reader.base import BaseDataCloudReader
29
31
  from datacustomcode.llm_gateway_config import spark_llm_gateway_config
@@ -34,6 +36,8 @@ if TYPE_CHECKING:
34
36
 
35
37
  from pyspark.sql import Column, DataFrame as PySparkDataFrame
36
38
 
39
+ from datacustomcode.einstein_predictions.spark_base import SparkEinsteinPredictions
40
+ from datacustomcode.einstein_predictions.types import PredictionType
37
41
  from datacustomcode.io.reader.base import BaseDataCloudReader
38
42
  from datacustomcode.io.writer.base import BaseDataCloudWriter, WriteMode
39
43
  from datacustomcode.llm_gateway.spark_base import SparkLLMGateway
@@ -99,6 +103,70 @@ def llm_gateway_generate_text_col(
99
103
  return gateway.llm_gateway_generate_text_col(template, values, model_id=model_id)
100
104
 
101
105
 
106
+ def _build_spark_einstein_predictions() -> "SparkEinsteinPredictions":
107
+ """Instantiate the SDK-configured :class:`SparkEinsteinPredictions`.
108
+
109
+ Raises:
110
+ RuntimeError: If no ``spark_einstein_predictions_config`` has been loaded.
111
+ """
112
+ cfg = spark_einstein_predictions_config.spark_einstein_predictions_config
113
+ if cfg is None:
114
+ raise RuntimeError(
115
+ "spark_einstein_predictions_config is not configured. Add a "
116
+ "'spark_einstein_predictions_config' section to config.yaml."
117
+ )
118
+ return cfg.to_object()
119
+
120
+
121
+ def einstein_predict_col(
122
+ model_api_name: str,
123
+ prediction_type: "PredictionType",
124
+ features: Dict[str, "Column"],
125
+ settings: Optional[Dict[str, Any]] = None,
126
+ ) -> "Column":
127
+ """Build a Spark Column that runs an Einstein prediction per row.
128
+
129
+ The returned Column yields a struct ``{status, response, error_code,
130
+ error_message}`` for each row. Use ``[...]`` (or ``getField``) to pick the
131
+ field you want, e.g. ``einstein_predict_col(...)["response"]``. ``response``
132
+ holds the prediction response payload as a JSON string. Per-row failures
133
+ populate ``status`` / ``error_code`` / ``error_message`` so a single bad row
134
+ does not abort the whole Spark job.
135
+
136
+ Example:
137
+
138
+ >>> from datacustomcode.einstein_predictions.types import PredictionType
139
+ >>> result = einstein_predict_col(
140
+ ... "my_regression_model",
141
+ ... PredictionType.REGRESSION,
142
+ ... {"square_feet": col("square_feet__c"), "beds": col("beds__c")},
143
+ ... )
144
+ >>> df.withColumn("prediction__c", result["response"])
145
+ >>> # …or keep the struct around and inspect failures:
146
+ >>> df.withColumn("pred", result).select(
147
+ ... "pred.status", "pred.response", "pred.error_message"
148
+ ... )
149
+
150
+ Args:
151
+ model_api_name: API name of the Einstein model to invoke.
152
+ prediction_type: The :class:`PredictionType` of the model.
153
+ features: A mapping from model feature column name to a Spark ``Column``
154
+ supplying that feature's per-row value.
155
+ settings: Optional prediction settings forwarded to the model.
156
+
157
+ Returns:
158
+ A Spark ``Column`` of ``StructType`` with fields ``status``,
159
+ ``response``, ``error_code``, and ``error_message`` (all nullable
160
+ strings). On success, ``status == "SUCCESS"`` and ``response`` holds
161
+ the JSON-serialized prediction payload; on failure, ``status ==
162
+ "ERROR"`` and the ``error_*`` fields carry diagnostic detail.
163
+ """
164
+ predictions = Client()._get_spark_einstein_predictions()
165
+ return predictions.einstein_predict_col(
166
+ model_api_name, prediction_type, features, settings=settings
167
+ )
168
+
169
+
102
170
  class DataCloudObjectType(Enum):
103
171
  DLO = "dlo"
104
172
  DMO = "dmo"
@@ -158,6 +226,8 @@ class Client:
158
226
  reader: A custom reader to use for reading Data Cloud objects.
159
227
  writer: A custom writer to use for writing Data Cloud objects.
160
228
  spark_llm_gateway: Optional custom :class:`SparkLLMGateway`.
229
+ spark_einstein_predictions: Optional custom
230
+ :class:`SparkEinsteinPredictions`.
161
231
 
162
232
  Example:
163
233
  >>> client = Client()
@@ -172,6 +242,7 @@ class Client:
172
242
  _writer: BaseDataCloudWriter
173
243
  _file: DefaultFindFilePath
174
244
  _spark_llm_gateway: Optional[SparkLLMGateway]
245
+ _spark_einstein_predictions: Optional[SparkEinsteinPredictions]
175
246
  _data_layer_history: dict[DataCloudObjectType, set[str]]
176
247
  _code_type: str
177
248
 
@@ -181,12 +252,14 @@ class Client:
181
252
  writer: Optional[BaseDataCloudWriter] = None,
182
253
  spark_provider: Optional[BaseSparkSessionProvider] = None,
183
254
  spark_llm_gateway: Optional[SparkLLMGateway] = None,
255
+ spark_einstein_predictions: Optional[SparkEinsteinPredictions] = None,
184
256
  code_type: str = "script",
185
257
  ) -> Client:
186
258
 
187
259
  if cls._instance is None:
188
260
  cls._instance = super().__new__(cls)
189
261
  cls._instance._spark_llm_gateway = spark_llm_gateway
262
+ cls._instance._spark_einstein_predictions = spark_einstein_predictions
190
263
  # Initialize Readers and Writers from config
191
264
  # and/or provided reader and writer
192
265
  if reader is None or writer is None:
@@ -358,6 +431,49 @@ class Client:
358
431
  self._spark_llm_gateway = _build_spark_llm_gateway()
359
432
  return self._spark_llm_gateway
360
433
 
434
+ def einstein_predict(
435
+ self,
436
+ model_api_name: str,
437
+ prediction_type: "PredictionType",
438
+ features: Dict[str, Any],
439
+ settings: Optional[Dict[str, Any]] = None,
440
+ ) -> Dict[str, Any]:
441
+ """Issue a one-shot Einstein prediction. This is the scalar counterpart
442
+ to :func:`einstein_predict_col`: it runs **once** — not per row. Use the
443
+ column helper method instead when you want to fan a prediction out
444
+ across every row of a DataFrame.
445
+
446
+ Example:
447
+
448
+ >>> from datacustomcode.einstein_predictions.types import PredictionType
449
+ >>> response = Client().einstein_predict(
450
+ ... "my_regression_model",
451
+ ... PredictionType.REGRESSION,
452
+ ... {"square_feet": 1800, "beds": 3},
453
+ ... )
454
+
455
+ Args:
456
+ model_api_name: API name of the Einstein model to invoke.
457
+ prediction_type: The :class:`PredictionType` of the model.
458
+ features: A mapping from model feature column name to a single
459
+ scalar value (``str`` / ``float`` / ``bool``).
460
+ settings: Optional prediction settings forwarded to the model.
461
+
462
+ Returns:
463
+ The prediction response payload as a plain Python ``dict``.
464
+
465
+ Raises:
466
+ EinsteinPredictionsCallError: If the prediction call fails.
467
+ """
468
+ return self._get_spark_einstein_predictions().einstein_predict(
469
+ model_api_name, prediction_type, features, settings=settings
470
+ )
471
+
472
+ def _get_spark_einstein_predictions(self) -> SparkEinsteinPredictions:
473
+ if self._spark_einstein_predictions is None:
474
+ self._spark_einstein_predictions = _build_spark_einstein_predictions()
475
+ return self._spark_einstein_predictions
476
+
361
477
  def _validate_data_layer_history_does_not_contain(
362
478
  self, data_cloud_object_type: DataCloudObjectType
363
479
  ) -> None:
@@ -24,6 +24,9 @@ einstein_predictions_config:
24
24
  options:
25
25
  credentials_profile: default
26
26
 
27
+ spark_einstein_predictions_config:
28
+ type_config_name: DefaultSparkEinsteinPredictions
29
+
27
30
  llm_gateway_config:
28
31
  type_config_name: DefaultLLMGateway
29
32
  options:
@@ -14,9 +14,17 @@
14
14
  # limitations under the License.
15
15
 
16
16
  from datacustomcode.einstein_predictions.base import EinsteinPredictions
17
+ from datacustomcode.einstein_predictions.errors import EinsteinPredictionsCallError
17
18
  from datacustomcode.einstein_predictions.impl.default import DefaultEinsteinPredictions
19
+ from datacustomcode.einstein_predictions.spark_base import SparkEinsteinPredictions
20
+ from datacustomcode.einstein_predictions.spark_default import (
21
+ DefaultSparkEinsteinPredictions,
22
+ )
18
23
 
19
24
  __all__ = [
20
25
  "DefaultEinsteinPredictions",
26
+ "DefaultSparkEinsteinPredictions",
21
27
  "EinsteinPredictions",
28
+ "EinsteinPredictionsCallError",
29
+ "SparkEinsteinPredictions",
22
30
  ]
@@ -0,0 +1,36 @@
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
+ """Exceptions raised by Einstein Predictions implementations."""
16
+
17
+ from __future__ import annotations
18
+
19
+ from typing import Optional
20
+
21
+
22
+ class EinsteinPredictionsCallError(RuntimeError):
23
+ """Raised when an Einstein Predictions call returns an error."""
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
@@ -0,0 +1,72 @@
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 (
19
+ TYPE_CHECKING,
20
+ Any,
21
+ Dict,
22
+ Optional,
23
+ )
24
+
25
+ from datacustomcode.mixin import UserExtendableNamedConfigMixin
26
+
27
+ if TYPE_CHECKING:
28
+ from pyspark.sql import Column
29
+
30
+ from datacustomcode.einstein_predictions.types import PredictionType
31
+
32
+
33
+ class SparkEinsteinPredictions(ABC, UserExtendableNamedConfigMixin):
34
+ CONFIG_NAME: str
35
+
36
+ def __init__(self, **kwargs: Any) -> None:
37
+ pass
38
+
39
+ @abstractmethod
40
+ def einstein_predict(
41
+ self,
42
+ model_api_name: str,
43
+ prediction_type: PredictionType,
44
+ features: Dict[str, Any],
45
+ settings: Optional[Dict[str, Any]] = None,
46
+ ) -> Dict[str, Any]:
47
+ """Issue a one-shot Einstein prediction and return the response data.
48
+
49
+ ``features`` maps each model feature column name to a single scalar
50
+ value (``str``/``float``/``bool``). The value is wrapped into a
51
+ single-element prediction column of the appropriate type.
52
+ """
53
+
54
+ @abstractmethod
55
+ def einstein_predict_col(
56
+ self,
57
+ model_api_name: str,
58
+ prediction_type: PredictionType,
59
+ features: Dict[str, "Column"],
60
+ settings: Optional[Dict[str, Any]] = None,
61
+ ) -> "Column":
62
+ """Build a Spark ``Column`` that invokes Einstein predict per row and
63
+ yields a struct ``{status, response, error_code, error_message}``.
64
+
65
+ ``features`` maps each model feature column name to a Spark ``Column``
66
+ supplying that feature's per-row value. Select an individual field,
67
+ e.g. ``einstein_predict_col(...)["response"]``. ``response`` holds the
68
+ prediction response payload as a JSON string. Returning a struct means
69
+ a single failing row leaves the rest of the DataFrame intact — callers
70
+ can inspect ``status`` / ``error_code`` per row instead of having the
71
+ Spark job abort.
72
+ """
@@ -0,0 +1,239 @@
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
+ import json
18
+ from typing import (
19
+ TYPE_CHECKING,
20
+ Any,
21
+ Dict,
22
+ Optional,
23
+ )
24
+
25
+ from datacustomcode.einstein_predictions.spark_base import SparkEinsteinPredictions
26
+ from datacustomcode.einstein_predictions.types import (
27
+ PredictionColumn,
28
+ PredictionRequest,
29
+ PredictionType,
30
+ )
31
+
32
+ if TYPE_CHECKING:
33
+ from pyspark.sql import Column
34
+
35
+ from datacustomcode.einstein_predictions.base import EinsteinPredictions
36
+ from datacustomcode.einstein_predictions.types import PredictionResponse
37
+
38
+
39
+ _STATUS_SUCCESS = "SUCCESS"
40
+ _STATUS_ERROR = "ERROR"
41
+
42
+
43
+ class DefaultSparkEinsteinPredictions(SparkEinsteinPredictions):
44
+
45
+ CONFIG_NAME = "DefaultSparkEinsteinPredictions"
46
+
47
+ def __init__(
48
+ self,
49
+ einstein_predictions: Optional["EinsteinPredictions"] = None,
50
+ **kwargs: Any,
51
+ ) -> None:
52
+ super().__init__(**kwargs)
53
+ if einstein_predictions is None:
54
+ einstein_predictions = _build_underlying_predictions()
55
+ self._einstein_predictions: "EinsteinPredictions" = einstein_predictions
56
+
57
+ def einstein_predict(
58
+ self,
59
+ model_api_name: str,
60
+ prediction_type: PredictionType,
61
+ features: Dict[str, Any],
62
+ settings: Optional[Dict[str, Any]] = None,
63
+ ) -> Dict[str, Any]:
64
+ return _invoke_predictions(
65
+ self._einstein_predictions,
66
+ model_api_name,
67
+ prediction_type,
68
+ features,
69
+ settings,
70
+ )
71
+
72
+ def einstein_predict_col(
73
+ self,
74
+ model_api_name: str,
75
+ prediction_type: PredictionType,
76
+ features: Dict[str, "Column"],
77
+ settings: Optional[Dict[str, Any]] = None,
78
+ ) -> "Column":
79
+ """Build a per-row UDF that returns a struct ``{status, response,
80
+ error_code, error_message}`` so per-row failures do not abort the
81
+ Spark job. Callers select the field they want, e.g.
82
+ ``einstein_predict_col(...)["response"]``. ``response`` carries the
83
+ prediction response payload serialized as a JSON string.
84
+ """
85
+ from pyspark.sql.functions import struct, udf
86
+ from pyspark.sql.types import (
87
+ StringType,
88
+ StructField,
89
+ StructType,
90
+ )
91
+
92
+ feature_names = list(features.keys())
93
+ values_col = struct(*[features[name].alias(name) for name in feature_names])
94
+
95
+ predictions = self._einstein_predictions
96
+ result_schema = StructType(
97
+ [
98
+ StructField("status", StringType(), True),
99
+ StructField("response", StringType(), True),
100
+ StructField("error_code", StringType(), True),
101
+ StructField("error_message", StringType(), True),
102
+ ]
103
+ )
104
+
105
+ def _predict(values_row: Any) -> Dict[str, Optional[str]]:
106
+ if values_row is None:
107
+ return {
108
+ "status": _STATUS_ERROR,
109
+ "response": None,
110
+ "error_code": None,
111
+ "error_message": "features column was null for this row",
112
+ }
113
+ row_features = (
114
+ values_row.asDict()
115
+ if hasattr(values_row, "asDict")
116
+ else dict(values_row)
117
+ )
118
+ return _invoke_predictions_as_struct(
119
+ predictions,
120
+ model_api_name,
121
+ prediction_type,
122
+ row_features,
123
+ settings,
124
+ )
125
+
126
+ return udf(_predict, result_schema)(values_col)
127
+
128
+
129
+ def _build_underlying_predictions() -> "EinsteinPredictions":
130
+ from datacustomcode.einstein_predictions_config import einstein_predictions_config
131
+
132
+ cfg = einstein_predictions_config.einstein_predictions_config
133
+ if cfg is None:
134
+ raise RuntimeError(
135
+ "einstein_predictions_config is not configured. Add an "
136
+ "'einstein_predictions_config' section to config.yaml."
137
+ )
138
+ return cfg.to_object()
139
+
140
+
141
+ def _feature_to_prediction_column(name: str, value: Any) -> PredictionColumn:
142
+ """Wrap a single scalar feature value into a one-element prediction column.
143
+
144
+ The value's Python type selects the prediction column value type. ``bool``
145
+ is checked before ``int``/``float`` because ``bool`` is a subclass of
146
+ ``int`` in Python.
147
+ """
148
+ if isinstance(value, bool):
149
+ return PredictionColumn(column_name=name, boolean_values=[value])
150
+ if isinstance(value, (int, float)):
151
+ return PredictionColumn(column_name=name, double_values=[float(value)])
152
+ return PredictionColumn(column_name=name, string_values=[str(value)])
153
+
154
+
155
+ def _build_request(
156
+ model_api_name: str,
157
+ prediction_type: PredictionType,
158
+ features: Dict[str, Any],
159
+ settings: Optional[Dict[str, Any]],
160
+ ) -> PredictionRequest:
161
+ prediction_columns = [
162
+ _feature_to_prediction_column(name, value) for name, value in features.items()
163
+ ]
164
+ return PredictionRequest(
165
+ prediction_type=prediction_type,
166
+ model_api_name=model_api_name,
167
+ prediction_columns=prediction_columns,
168
+ settings=settings,
169
+ )
170
+
171
+
172
+ def _call_predictions(
173
+ predictions: "EinsteinPredictions",
174
+ model_api_name: str,
175
+ prediction_type: PredictionType,
176
+ features: Dict[str, Any],
177
+ settings: Optional[Dict[str, Any]],
178
+ ) -> "PredictionResponse":
179
+ """Build the request and dispatch it to the underlying predictions resource."""
180
+ request = _build_request(model_api_name, prediction_type, features, settings)
181
+ return predictions.predict(request)
182
+
183
+
184
+ def _invoke_predictions(
185
+ predictions: "EinsteinPredictions",
186
+ model_api_name: str,
187
+ prediction_type: PredictionType,
188
+ features: Dict[str, Any],
189
+ settings: Optional[Dict[str, Any]],
190
+ ) -> Dict[str, Any]:
191
+ from datacustomcode.einstein_predictions.errors import EinsteinPredictionsCallError
192
+
193
+ response = _call_predictions(
194
+ predictions, model_api_name, prediction_type, features, settings
195
+ )
196
+ if not response.is_success:
197
+ error_code = _extract_error_code(response)
198
+ raise EinsteinPredictionsCallError(
199
+ f"Einstein Predictions call failed: "
200
+ f"status_code={response.status_code}, "
201
+ f"error_code={error_code!r}, message={response.data!r}",
202
+ status=response.status_code,
203
+ error_code=error_code,
204
+ error_message=str(response.data) if response.data else None,
205
+ )
206
+ return response.data or {}
207
+
208
+
209
+ def _invoke_predictions_as_struct(
210
+ predictions: "EinsteinPredictions",
211
+ model_api_name: str,
212
+ prediction_type: PredictionType,
213
+ features: Dict[str, Any],
214
+ settings: Optional[Dict[str, Any]],
215
+ ) -> Dict[str, Optional[str]]:
216
+ response = _call_predictions(
217
+ predictions, model_api_name, prediction_type, features, settings
218
+ )
219
+ if not response.is_success:
220
+ return {
221
+ "status": _STATUS_ERROR,
222
+ "response": None,
223
+ "error_code": _extract_error_code(response),
224
+ "error_message": str(response.data) if response.data else None,
225
+ }
226
+ return {
227
+ "status": _STATUS_SUCCESS,
228
+ "response": json.dumps(response.data) if response.data is not None else None,
229
+ "error_code": None,
230
+ "error_message": None,
231
+ }
232
+
233
+
234
+ def _extract_error_code(response: "PredictionResponse") -> Optional[str]:
235
+ if response.data:
236
+ error_code = response.data.get("errorCode")
237
+ if error_code is not None:
238
+ return str(error_code)
239
+ return None
@@ -21,11 +21,17 @@ from typing import (
21
21
  Union,
22
22
  )
23
23
 
24
- from datacustomcode.common_config import BaseConfig, default_config_file
24
+ from datacustomcode.common_config import (
25
+ BaseConfig,
26
+ BaseObjectConfig,
27
+ default_config_file,
28
+ )
25
29
  from datacustomcode.einstein_platform_config import CredentialsObjectConfig
26
30
  from datacustomcode.einstein_predictions.base import EinsteinPredictions
31
+ from datacustomcode.einstein_predictions.spark_base import SparkEinsteinPredictions
27
32
 
28
33
  _E = TypeVar("_E", bound=EinsteinPredictions)
34
+ _S = TypeVar("_S", bound=SparkEinsteinPredictions)
29
35
 
30
36
 
31
37
  class EinsteinPredictionsObjectConfig(CredentialsObjectConfig[_E], Generic[_E]):
@@ -54,6 +60,44 @@ class EinsteinPredictionsConfig(BaseConfig):
54
60
  return self
55
61
 
56
62
 
63
+ class SparkEinsteinPredictionsObjectConfig(BaseObjectConfig, Generic[_S]):
64
+ type_to_create: ClassVar[Type[SparkEinsteinPredictions]] = SparkEinsteinPredictions # type: ignore[type-abstract]
65
+
66
+ def to_object(self) -> SparkEinsteinPredictions:
67
+ type_ = self.type_to_create.subclass_from_config_name(self.type_config_name)
68
+ return type_(**self.options)
69
+
70
+
71
+ class SparkEinsteinPredictionsConfig(BaseConfig):
72
+ spark_einstein_predictions_config: Union[
73
+ SparkEinsteinPredictionsObjectConfig[SparkEinsteinPredictions], None
74
+ ] = None
75
+
76
+ def update(
77
+ self, other: "SparkEinsteinPredictionsConfig"
78
+ ) -> "SparkEinsteinPredictionsConfig":
79
+ def merge(
80
+ config_a: Union[SparkEinsteinPredictionsObjectConfig, None],
81
+ config_b: Union[SparkEinsteinPredictionsObjectConfig, None],
82
+ ) -> Union[SparkEinsteinPredictionsObjectConfig, None]:
83
+ if config_a is not None and config_a.force:
84
+ return config_a
85
+ if config_b:
86
+ return config_b
87
+ return config_a
88
+
89
+ self.spark_einstein_predictions_config = merge(
90
+ self.spark_einstein_predictions_config,
91
+ other.spark_einstein_predictions_config,
92
+ )
93
+ return self
94
+
95
+
57
96
  # Global Einstein Predictions config instance
58
97
  einstein_predictions_config = EinsteinPredictionsConfig()
59
98
  einstein_predictions_config.load(default_config_file())
99
+
100
+
101
+ # Global Spark Einstein Predictions config instance
102
+ spark_einstein_predictions_config = SparkEinsteinPredictionsConfig()
103
+ spark_einstein_predictions_config.load(default_config_file())
@@ -38,6 +38,23 @@ def main():
38
38
  >>> generated_text = client.llm_gateway_generate_text(
39
39
  ... prompt, model_id
40
40
  ... )
41
+
42
+ You can also score rows with an Einstein Studio model. The per-row helper
43
+ returns the same ``{status, response, error_code, error_message}`` struct,
44
+ where ``response`` is the prediction payload as a JSON string.
45
+
46
+ Example:
47
+
48
+ >>> from datacustomcode.client import einstein_predict_col
49
+ >>> from datacustomcode.einstein_predictions.types import PredictionType
50
+ df_scored = df.withColumn(
51
+ ... "prediction__c",
52
+ ... einstein_predict_col(
53
+ ... "my_regression_model",
54
+ ... PredictionType.REGRESSION,
55
+ ... {"beds": col("beds__c"), "baths": col("baths__c")},
56
+ ... )["response"],
57
+ ... )
41
58
  """
42
59
 
43
60
  # Drop specific columns related to relationships
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: salesforce-data-customcode
3
- Version: 6.0.4.dev3
3
+ Version: 6.0.6.dev1
4
4
  Summary: Data Cloud Custom Code SDK
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE.txt
7
7
  Requires-Python: >=3.10,<3.12
8
- Classifier: Development Status :: 4 - Beta
8
+ Classifier: Development Status :: 5 - Production/Stable
9
9
  Classifier: Operating System :: Unix
10
10
  Classifier: Programming Language :: Python
11
11
  Classifier: Programming Language :: Python :: 3
@@ -23,7 +23,7 @@ Requires-Dist: salesforce-cdp-connector (>=1.0.19)
23
23
  Requires-Dist: setuptools_scm (>=7.1.0,<8.0.0)
24
24
  Description-Content-Type: text/markdown
25
25
 
26
- # Data Cloud Custom Code SDK (BETA)
26
+ # Data Cloud Custom Code SDK
27
27
 
28
28
  This package provides a development kit for creating custom data transformations in [Data Cloud](https://www.salesforce.com/data/). It allows you to write your own data processing logic in Python while leveraging Data Cloud's infrastructure for data access and running data transformations, mapping execution into Data Cloud data structures like [Data Model Objects](https://help.salesforce.com/s/articleView?id=data.c360_a_data_model_objects.htm&type=5) and [Data Lake Objects](https://help.salesforce.com/s/articleView?id=sf.c360_a_data_lake_objects.htm&language=en_US&type=5).
29
29
 
@@ -36,7 +36,7 @@ Use of this project with Salesforce is subject to the [TERMS OF USE](./TERMS_OF_
36
36
  - **Python 3.11 only** (currently supported version - if your system version is different, we recommend using [pyenv](https://github.com/pyenv/pyenv) to configure 3.11)
37
37
  - JDK 17
38
38
  - Docker support like [Docker Desktop](https://docs.docker.com/desktop/)
39
- - A salesforce org with some DLOs or DMOs with data and this feature enabled (it is not GA)
39
+ - A salesforce org with some DLOs or DMOs with data and this feature enabled
40
40
  - **One of the following** for authentication:
41
41
  - A Salesforce org already authenticated via the [Salesforce CLI](https://developer.salesforce.com/tools/salesforcecli)
42
42
  (simplest — no External Client App needed)
@@ -116,7 +116,7 @@ After modifying the `entrypoint.py` as needed, using any dependencies you add in
116
116
  ```zsh
117
117
  cd my_package
118
118
  datacustomcode scan ./payload/entrypoint.py
119
- datacustomcode deploy --path ./payload --name my_custom_script --cpu-size CPU_L
119
+ datacustomcode deploy --path ./payload --name my_custom_script --cpu-size CPU_L --sf-cli-org myorg
120
120
  ```
121
121
 
122
122
  > [!TIP]
@@ -345,6 +345,7 @@ Options:
345
345
  - `--description TEXT`: Description of the transformation job (default: "")
346
346
  - `--network TEXT`: docker network (default: "default")
347
347
  - `--cpu-size TEXT`: CPU size for the deployment (default: `CPU_2XL`). Available options: CPU_L(Large), CPU_XL(Extra Large), CPU_2XL(2X Large), CPU_4XL(4X Large)
348
+ - `--sf-cli-org TEXT`: Salesforce CLI org alias or username (e.g. `myorg`). Fetches credentials via `sf org display` — no `datacustomcode configure` step needed. Takes precedence over `--profile` if both are supplied.
348
349
  - `--function-invoke-opt TEXT`: Currently we support only `UnstructuredChunking` for functions.
349
350
 
350
351
 
@@ -397,6 +398,56 @@ datacustomcode run ./payload/entrypoint.py --sf-cli-org myorg
397
398
  ```
398
399
 
399
400
 
401
+ ## Testing Einstein Predictions
402
+
403
+ You can use AI models configured in Einstein Studio to score your data while
404
+ transforming it. As with the LLM Gateway, there are two flavors: a one-shot
405
+ scalar call (`client.einstein_predict`) and a per-row column helper
406
+ (`einstein_predict_col`). Below is a sample code example:
407
+
408
+ ```
409
+ from datacustomcode.client import Client, einstein_predict_col
410
+ from datacustomcode.einstein_predictions.types import PredictionType
411
+
412
+
413
+ def main():
414
+ client = Client()
415
+ df = client.read_dlo("Input__dll")
416
+ # einstein_predict_col returns a struct
417
+ # {status, response, error_code, error_message} per row, so per-row
418
+ # failures don't abort the Spark job. `response` is the prediction
419
+ # payload as a JSON string. Pick the field you want with [].
420
+ df_scored = df.withColumn(
421
+ "prediction__c",
422
+ einstein_predict_col(
423
+ "my_regression_model", # An AI model in your org
424
+ PredictionType.REGRESSION,
425
+ {"square_feet": col("square_feet__c"), "beds": col("beds__c")},
426
+ )["response"],
427
+ )
428
+
429
+ dlo_name = "Output_dll"
430
+ client.write_to_dlo(dlo_name, df_scored, write_mode=WriteMode.APPEND)
431
+
432
+ # One-shot scalar prediction returns the response payload as a dict
433
+ prediction = client.einstein_predict(
434
+ "my_regression_model",
435
+ PredictionType.REGRESSION,
436
+ {"square_feet": 1800, "beds": 3},
437
+ )
438
+
439
+ if __name__ == "__main__":
440
+ main()
441
+ ```
442
+
443
+ Testing this code locally uses the same External Client App setup described in
444
+ [Testing LLM Gateway](#testing-llm-gateway). Once your `myorg` alias is set up,
445
+ run:
446
+ ```
447
+ datacustomcode run ./payload/entrypoint.py --sf-cli-org myorg
448
+ ```
449
+
450
+
400
451
  ## Docker usage
401
452
 
402
453
  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.
@@ -508,7 +559,7 @@ sf --version
508
559
  **Browser-based (recommended for developer orgs and sandboxes):**
509
560
  ```zsh
510
561
  # Production / Developer Edition
511
- sf org login web --alias myorg
562
+ sf org login web --alias myorg --instance-url <your-instance-url>
512
563
 
513
564
  # Sandbox
514
565
  sf org login web --alias mysandbox --instance-url https://test.salesforce.com
@@ -1,21 +1,24 @@
1
- datacustomcode/__init__.py,sha256=qx-X3U9lU_791ZbAxhGqQeyDw6eqTi9-E9AbYnwJfxU,2071
1
+ datacustomcode/__init__.py,sha256=SGsDnWDTihWkLNnWdqvH08zklQgxykar73JAcPL2plI,2666
2
2
  datacustomcode/auth.py,sha256=fpSjhIBdv9trC8yq2vuljAix_Euu-4Ah7HDCGhYjOxI,8309
3
3
  datacustomcode/cli.py,sha256=i7hPoQqJskTmx3Odz2wVgkxBnf_PFaIawI12FXhaRUE,13264
4
- datacustomcode/client.py,sha256=fc3JmDbAFcTT-yP_k_0jNiGqNp_KwZaIpEsI-NPxNcw,15115
4
+ datacustomcode/client.py,sha256=x0JN_hHXfpEdjHeINQEdUQt-as3j17v6uoDJHjfLe-Y,20241
5
5
  datacustomcode/cmd.py,sha256=ZMs46aydJw2EaU26JgCtZmnqESQFHvvaJz10hnjZTBk,3537
6
6
  datacustomcode/common_config.py,sha256=SAUnxj3kqmOeWwPmFoYq4tuxokMgURVm4QwcGi-avL4,1928
7
7
  datacustomcode/config.py,sha256=2Pk61ieQsEWSxKDxlS66_rliKE8iX0j-9LweEm0aaGo,4072
8
- datacustomcode/config.yaml,sha256=Wd8Zd2f1__hRv7166H5IBbU5YoOEOdQNHgAb4ONbFnA,845
8
+ datacustomcode/config.yaml,sha256=ldssmQYPghmYSSPaelm3F2mC99njavhrWsINRSzWymM,933
9
9
  datacustomcode/constants.py,sha256=SskQpUPnQE7LjKh4Z3AdZ2p191_0Kf73jV8gdBXDPLU,1436
10
10
  datacustomcode/credentials.py,sha256=D-7Zd3Nh_wStdj8_wUy5cnC8M_mdbfBijhIAi-2EbXs,9467
11
11
  datacustomcode/deploy.py,sha256=TZ6L4VrAnL-SlmAUMbEYguVpCgOm_Ncv8NdaBR3LX4Y,21906
12
12
  datacustomcode/einstein_platform_client.py,sha256=ON2B_m--vdbCVVMVcLc81T-BRZ5-UuxVCer8E6OEQPA,4083
13
13
  datacustomcode/einstein_platform_config.py,sha256=6lb_FRbEzdt5a8-2cR15f13ZKw674uvRJ4Xq0o_pQXE,1490
14
- datacustomcode/einstein_predictions/__init__.py,sha256=o-iask36phwGA7hGi_sxJpwbITO6h_STDe0J92qD2_M,859
14
+ datacustomcode/einstein_predictions/__init__.py,sha256=_RIDTqcEHDRu6AsCsj9lRBykNkHI8Ch8JBILL1ioPtA,1237
15
15
  datacustomcode/einstein_predictions/base.py,sha256=OBV445dpZRtCF5cpdHH49w_6RU_jZY1A-e7VtHTQCFU,1061
16
+ datacustomcode/einstein_predictions/errors.py,sha256=EoxVU5Mf14AIAyVBedI1JnbsS_o853kY-iMA1u-KPcw,1224
16
17
  datacustomcode/einstein_predictions/impl/default.py,sha256=fEFhrosozhG0yT87w4Gu8wbe1NgHxBqdAduwWHdWGN4,3011
18
+ datacustomcode/einstein_predictions/spark_base.py,sha256=MnFH2-pJ7ugFnc9iUvCdMt1vPAPAmiImptjjIGh9bvo,2567
19
+ datacustomcode/einstein_predictions/spark_default.py,sha256=tYL6u1DY3Ogu8xF7o1VqXqTGrcP2VUUMI6BShjXWvns,8119
17
20
  datacustomcode/einstein_predictions/types.py,sha256=W7H3sFzm_NdBtnia7O576w0OvM7jwzfGVQmFiZTO8Og,6098
18
- datacustomcode/einstein_predictions_config.py,sha256=oxKgp2u7t5SdHVsasBBxsvd3KAfXuZ9uQXTrtpcCUlU,2135
21
+ datacustomcode/einstein_predictions_config.py,sha256=7zUot5fUnXiihiu_9NStvk0-e9Otv55-Xk7dKqrMfBY,3758
19
22
  datacustomcode/file/__init__.py,sha256=gamfOD1VnAtEslRBpqh-yKiVjkG_wYWYdSatGIfsN-w,621
20
23
  datacustomcode/file/base.py,sha256=2IUlQufPvpHccgyoeZlKhcktO4pS4NKdha03Whq33Zk,745
21
24
  datacustomcode/file/path/__init__.py,sha256=gamfOD1VnAtEslRBpqh-yKiVjkG_wYWYdSatGIfsN-w,621
@@ -87,13 +90,13 @@ datacustomcode/templates/script/examples/employee_hierarchy/employee_data.csv,sh
87
90
  datacustomcode/templates/script/examples/employee_hierarchy/entrypoint.py,sha256=Mfm3iQtEHTQRW6cmZTwRKdTh3IeGWR-teXrd6x-YLSY,2223
88
91
  datacustomcode/templates/script/jupyterlab.sh,sha256=IHR3YQ8d_busuyvesByhJgzCKULIFPI-Hqogs0NPhCs,2432
89
92
  datacustomcode/templates/script/payload/config.json,sha256=0d2mEMt4NHIeOUTsgiPMuRKdOLIQxmh-Wq-IfEqU-gc,28
90
- datacustomcode/templates/script/payload/entrypoint.py,sha256=bLUhC4l03H-OQJKdMf11vGXmBnga6QitRXo-g9HibZg,1741
93
+ datacustomcode/templates/script/payload/entrypoint.py,sha256=4Uph0ILa5ukk_6C90paK3uzQn0zZvNLr9ho3o_ZwErQ,2474
91
94
  datacustomcode/templates/script/requirements-dev.txt,sha256=OWwuy1awesqOZOS3Zizmdd6BDnSePpGFN5bq5IrALvc,176
92
95
  datacustomcode/templates/script/requirements.txt,sha256=qJbqzs5z0Hrx590U3T7dHq_m8UQEx2TdhgrJSz-QHIQ,40
93
96
  datacustomcode/token_provider.py,sha256=kGPxdxGZEr6VFknW7jFb9P6wOvRaRo-sfdBN2cUjEZY,6792
94
97
  datacustomcode/version.py,sha256=9LlbVrzwBvut1L308QbwNBgxdQk5CrghH39JARVSxms,989
95
- salesforce_data_customcode-6.0.4.dev3.dist-info/METADATA,sha256=Tg_qVJWkMhhj1pz1hSHwVpLh00hppX1cDMkmts7ycJM,23234
96
- salesforce_data_customcode-6.0.4.dev3.dist-info/WHEEL,sha256=eY7nduwzv-ldUxpzbRlxwvC693Hg6PX8bWDjEHjZ_dk,88
97
- salesforce_data_customcode-6.0.4.dev3.dist-info/entry_points.txt,sha256=WpQ94UB7UuRCYGOLtJV3vAgm1tl7iVf43VYRWIuNu5Y,74
98
- salesforce_data_customcode-6.0.4.dev3.dist-info/licenses/LICENSE.txt,sha256=iOi8EmQpfkFhMENi7VYtkl2EqS14pEOccoXHiW2dyPU,11443
99
- salesforce_data_customcode-6.0.4.dev3.dist-info/RECORD,,
98
+ salesforce_data_customcode-6.0.6.dev1.dist-info/METADATA,sha256=k11RadLqAMw2inLBjUV6rLcjor-Itqf3wX7u1EJhuO8,25115
99
+ salesforce_data_customcode-6.0.6.dev1.dist-info/WHEEL,sha256=eY7nduwzv-ldUxpzbRlxwvC693Hg6PX8bWDjEHjZ_dk,88
100
+ salesforce_data_customcode-6.0.6.dev1.dist-info/entry_points.txt,sha256=WpQ94UB7UuRCYGOLtJV3vAgm1tl7iVf43VYRWIuNu5Y,74
101
+ salesforce_data_customcode-6.0.6.dev1.dist-info/licenses/LICENSE.txt,sha256=iOi8EmQpfkFhMENi7VYtkl2EqS14pEOccoXHiW2dyPU,11443
102
+ salesforce_data_customcode-6.0.6.dev1.dist-info/RECORD,,