salesforce-data-customcode 6.1.0.dev3__py3-none-any.whl → 6.1.0.dev5__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.
Files changed (45) hide show
  1. datacustomcode/__init__.py +5 -0
  2. datacustomcode/cli.py +30 -5
  3. datacustomcode/client.py +267 -192
  4. datacustomcode/config.py +5 -0
  5. datacustomcode/config.yaml +0 -6
  6. datacustomcode/constants.py +9 -1
  7. datacustomcode/deploy.py +58 -24
  8. datacustomcode/function/runtime.py +0 -16
  9. datacustomcode/io/reader/base.py +42 -0
  10. datacustomcode/io/writer/base.py +45 -0
  11. datacustomcode/io/writer/csv.py +8 -0
  12. datacustomcode/io/writer/print.py +7 -0
  13. datacustomcode/run.py +11 -7
  14. datacustomcode/scan.py +164 -29
  15. datacustomcode/template.py +13 -1
  16. datacustomcode/templates/script/examples/streaming_deltas/entrypoint.py +62 -0
  17. datacustomcode/templates/script/jupyterlab.sh +18 -4
  18. {salesforce_data_customcode-6.1.0.dev3.dist-info → salesforce_data_customcode-6.1.0.dev5.dist-info}/METADATA +42 -5
  19. {salesforce_data_customcode-6.1.0.dev3.dist-info → salesforce_data_customcode-6.1.0.dev5.dist-info}/RECORD +22 -44
  20. datacustomcode/named_credential/__init__.py +0 -28
  21. datacustomcode/named_credential/base.py +0 -54
  22. datacustomcode/named_credential/default.py +0 -93
  23. datacustomcode/named_credential/direct/__init__.py +0 -19
  24. datacustomcode/named_credential/direct/auth.py +0 -63
  25. datacustomcode/named_credential/direct/credentials.py +0 -121
  26. datacustomcode/named_credential/direct/transport.py +0 -110
  27. datacustomcode/named_credential/direct/url_resolver.py +0 -112
  28. datacustomcode/named_credential/errors.py +0 -36
  29. datacustomcode/named_credential/spark_base.py +0 -93
  30. datacustomcode/named_credential/spark_default.py +0 -154
  31. datacustomcode/named_credential/types/__init__.py +0 -14
  32. datacustomcode/named_credential/types/http_method.py +0 -29
  33. datacustomcode/named_credential/types/http_request.py +0 -63
  34. datacustomcode/named_credential/types/http_request_builder.py +0 -55
  35. datacustomcode/named_credential/types/http_response.py +0 -43
  36. datacustomcode/named_credential/types/http_response_builder.py +0 -24
  37. datacustomcode/named_credential_config.py +0 -105
  38. datacustomcode/templates/function/example/chunking_with_external_callout/README.md +0 -119
  39. datacustomcode/templates/function/example/chunking_with_external_callout/config.json +0 -3
  40. datacustomcode/templates/function/example/chunking_with_external_callout/entrypoint.py +0 -161
  41. datacustomcode/templates/function/example/chunking_with_external_callout/external_callout_config.json +0 -11
  42. datacustomcode/templates/function/example/chunking_with_external_callout/tests/test.json +0 -16
  43. {salesforce_data_customcode-6.1.0.dev3.dist-info → salesforce_data_customcode-6.1.0.dev5.dist-info}/WHEEL +0 -0
  44. {salesforce_data_customcode-6.1.0.dev3.dist-info → salesforce_data_customcode-6.1.0.dev5.dist-info}/entry_points.txt +0 -0
  45. {salesforce_data_customcode-6.1.0.dev3.dist-info → salesforce_data_customcode-6.1.0.dev5.dist-info}/licenses/LICENSE.txt +0 -0
datacustomcode/scan.py CHANGED
@@ -15,6 +15,7 @@
15
15
  from __future__ import annotations
16
16
 
17
17
  import ast
18
+ import copy
18
19
  import json
19
20
  import os
20
21
  import sys
@@ -32,6 +33,8 @@ import pydantic
32
33
  from datacustomcode.version import get_version
33
34
 
34
35
  DATA_ACCESS_METHODS = ["read_dlo", "read_dmo", "write_to_dlo", "write_to_dmo"]
36
+ STREAMING_READ_METHODS = ["read_dlo_deltas", "read_dmo_deltas"]
37
+ STREAMING_WRITE_METHODS = ["write_dlo_deltas"]
35
38
 
36
39
  DATA_TRANSFORM_CONFIG_TEMPLATE = {
37
40
  "sdkVersion": get_version(),
@@ -43,6 +46,20 @@ DATA_TRANSFORM_CONFIG_TEMPLATE = {
43
46
  },
44
47
  }
45
48
 
49
+ STREAMING_TRANSFORM_CONFIG_TEMPLATE = {
50
+ "sdkVersion": get_version(),
51
+ "entryPoint": "",
52
+ "dataspace": "default",
53
+ "streamingSource": {
54
+ "type": "dlo",
55
+ "name": "",
56
+ },
57
+ "permissions": {
58
+ "read": {},
59
+ "write": {},
60
+ },
61
+ }
62
+
46
63
  FUNCTION_CONFIG_TEMPLATE = {
47
64
  "entryPoint": "",
48
65
  }
@@ -160,6 +177,35 @@ class DataAccessLayerCalls(pydantic.BaseModel):
160
177
  return next(iter(self.write_to_dmo))
161
178
 
162
179
 
180
+ class StreamingDataAccessLayerCalls(pydantic.BaseModel):
181
+ read_dlo_deltas: bool
182
+ read_dmo_deltas: bool
183
+ write_dlo_deltas: frozenset[str]
184
+
185
+ @pydantic.model_validator(mode="after")
186
+ def validate_access_layer(self) -> StreamingDataAccessLayerCalls:
187
+ if self.read_dlo_deltas and self.read_dmo_deltas:
188
+ raise ValueError(
189
+ "Cannot read DLO and DMO deltas in the same streaming transform."
190
+ )
191
+ if not self.read_dlo_deltas and not self.read_dmo_deltas:
192
+ raise ValueError(
193
+ "A streaming transform must read from at least one DLO or DMO "
194
+ "delta stream (read_dlo_deltas / read_dmo_deltas)."
195
+ )
196
+ if not self.write_dlo_deltas:
197
+ raise ValueError(
198
+ "A streaming transform must write to at least one DLO via "
199
+ "write_dlo_deltas."
200
+ )
201
+ return self
202
+
203
+ @property
204
+ def read_layer(self) -> str:
205
+ """Return the read source layer, ``"dlo"`` or ``"dmo"``."""
206
+ return "dlo" if self.read_dlo_deltas else "dmo"
207
+
208
+
163
209
  class ClientMethodVisitor(ast.NodeVisitor):
164
210
  """AST Visitor that finds all instances of Client read/write method calls."""
165
211
 
@@ -168,6 +214,9 @@ class ClientMethodVisitor(ast.NodeVisitor):
168
214
  self._read_dmo_instances: set[str] = set()
169
215
  self._write_to_dlo_instances: set[str] = set()
170
216
  self._write_to_dmo_instances: set[str] = set()
217
+ self._read_dlo_deltas: bool = False
218
+ self._read_dmo_deltas: bool = False
219
+ self._write_dlo_deltas_instances: set[str] = set()
171
220
  self.variable_values: Dict[str, Union[str, None]] = {}
172
221
 
173
222
  def visit_Assign(self, node: ast.Assign) -> None:
@@ -189,14 +238,15 @@ class ClientMethodVisitor(ast.NodeVisitor):
189
238
  node.func.value, ast.Name
190
239
  ):
191
240
  method_name = node.func.attr
241
+
242
+ if method_name == "read_dlo_deltas":
243
+ self._read_dlo_deltas = True
244
+ elif method_name == "read_dmo_deltas":
245
+ self._read_dmo_deltas = True
246
+
192
247
  if method_name in DATA_ACCESS_METHODS and node.args:
193
248
  arg = node.args[0]
194
- name = None
195
-
196
- if isinstance(arg, ast.Constant) and isinstance(arg.value, str):
197
- name = arg.value
198
- elif isinstance(arg, ast.Name) and arg.id in self.variable_values:
199
- name = self.variable_values[arg.id]
249
+ name = self._resolve_name_arg(arg)
200
250
 
201
251
  if name:
202
252
  if method_name == "read_dlo":
@@ -207,8 +257,29 @@ class ClientMethodVisitor(ast.NodeVisitor):
207
257
  self._write_to_dlo_instances.add(name)
208
258
  elif method_name == "write_to_dmo":
209
259
  self._write_to_dmo_instances.add(name)
260
+ elif method_name in STREAMING_WRITE_METHODS and node.args:
261
+ name = self._resolve_name_arg(node.args[0])
262
+ if name and method_name == "write_dlo_deltas":
263
+ self._write_dlo_deltas_instances.add(name)
210
264
  self.generic_visit(node)
211
265
 
266
+ def _resolve_name_arg(self, arg: ast.expr) -> Union[str, None]:
267
+ """Resolve a string-literal or tracked-variable first argument."""
268
+ if isinstance(arg, ast.Constant) and isinstance(arg.value, str):
269
+ return arg.value
270
+ if isinstance(arg, ast.Name) and arg.id in self.variable_values:
271
+ return self.variable_values[arg.id]
272
+ return None
273
+
274
+ @property
275
+ def is_streaming(self) -> bool:
276
+ """Whether any streaming (delta) access method was found."""
277
+ return (
278
+ self._read_dlo_deltas
279
+ or self._read_dmo_deltas
280
+ or bool(self._write_dlo_deltas_instances)
281
+ )
282
+
212
283
  def found(self) -> DataAccessLayerCalls:
213
284
  return DataAccessLayerCalls(
214
285
  read_dlo=frozenset(self._read_dlo_instances),
@@ -217,6 +288,13 @@ class ClientMethodVisitor(ast.NodeVisitor):
217
288
  write_to_dmo=frozenset(self._write_to_dmo_instances),
218
289
  )
219
290
 
291
+ def found_streaming(self) -> StreamingDataAccessLayerCalls:
292
+ return StreamingDataAccessLayerCalls(
293
+ read_dlo_deltas=self._read_dlo_deltas,
294
+ read_dmo_deltas=self._read_dmo_deltas,
295
+ write_dlo_deltas=frozenset(self._write_dlo_deltas_instances),
296
+ )
297
+
220
298
 
221
299
  class ImportVisitor(ast.NodeVisitor):
222
300
  """AST Visitor that extracts external package imports from Python code."""
@@ -301,23 +379,51 @@ def write_requirements_file(file_path: str) -> str:
301
379
  return requirements_path
302
380
 
303
381
 
304
- def scan_file(file_path: str) -> DataAccessLayerCalls:
305
- """Scan a single Python file for Client read/write method calls."""
382
+ def _visit_file(file_path: str) -> ClientMethodVisitor:
383
+ """Parse a Python file and return the populated method visitor."""
306
384
  with open(file_path, "r") as f:
307
- code = f.read()
308
- tree = ast.parse(code)
309
- visitor = ClientMethodVisitor()
310
- visitor.visit(tree)
311
- return visitor.found()
385
+ tree = ast.parse(f.read())
386
+ visitor = ClientMethodVisitor()
387
+ visitor.visit(tree)
388
+ return visitor
389
+
390
+
391
+ def scan_file(file_path: str) -> DataAccessLayerCalls:
392
+ """Scan a single Python file for batch Client read/write method calls."""
393
+ return _visit_file(file_path).found()
394
+
395
+
396
+ def scan_file_streaming(file_path: str) -> StreamingDataAccessLayerCalls:
397
+ """Scan a single Python file for StreamingClient delta method calls."""
398
+ return _visit_file(file_path).found_streaming()
399
+
312
400
 
401
+ def file_is_streaming(file_path: str) -> bool:
402
+ """Return whether the entrypoint uses streaming (delta) access methods."""
403
+ return _visit_file(file_path).is_streaming
313
404
 
314
- def dc_config_json_from_file(file_path: str, type: str) -> dict[str, Any]:
315
- """Create a Data Cloud Custom Code config JSON from a script."""
405
+
406
+ def dc_config_json_from_file(
407
+ file_path: str, type: str, streaming: bool = False
408
+ ) -> dict[str, Any]:
409
+ """Create a Data Cloud Custom Code config JSON from a script.
410
+
411
+ Args:
412
+ file_path: Path to the entrypoint.
413
+ type: Package type, ``"script"`` or ``"function"``.
414
+ streaming: For scripts, a streaming
415
+ (``streamingSource``) config instead of a batch one.
416
+ """
316
417
  config: dict[str, Any]
317
418
  if type == "script":
318
- config = DATA_TRANSFORM_CONFIG_TEMPLATE.copy()
419
+ template = (
420
+ STREAMING_TRANSFORM_CONFIG_TEMPLATE
421
+ if streaming
422
+ else DATA_TRANSFORM_CONFIG_TEMPLATE
423
+ )
424
+ config = copy.deepcopy(template)
319
425
  elif type == "function":
320
- config = FUNCTION_CONFIG_TEMPLATE.copy()
426
+ config = copy.deepcopy(FUNCTION_CONFIG_TEMPLATE)
321
427
  config["entryPoint"] = os.path.basename(file_path)
322
428
  return config
323
429
 
@@ -372,22 +478,51 @@ def update_config(file_path: str) -> dict[str, Any]:
372
478
 
373
479
  if package_type == "script":
374
480
  existing_config["dataspace"] = get_dataspace(existing_config)
375
- output = scan_file(file_path)
376
- read: dict[str, list[str]] = {}
377
- if output.read_dlo:
378
- read["dlo"] = list(output.read_dlo)
379
- else:
380
- read["dmo"] = list(output.read_dmo)
381
- write: dict[str, list[str]] = {}
382
- if output.write_to_dlo:
383
- write["dlo"] = list(output.write_to_dlo)
481
+ if file_is_streaming(file_path):
482
+ _update_streaming_config(existing_config, file_path)
384
483
  else:
385
- write["dmo"] = list(output.write_to_dmo)
386
-
387
- existing_config["permissions"] = {"read": read, "write": write}
484
+ existing_config.pop("streamingSource", None)
485
+ output = scan_file(file_path)
486
+ read: dict[str, list[str]] = {}
487
+ if output.read_dlo:
488
+ read["dlo"] = list(output.read_dlo)
489
+ else:
490
+ read["dmo"] = list(output.read_dmo)
491
+ write: dict[str, list[str]] = {}
492
+ if output.write_to_dlo:
493
+ write["dlo"] = list(output.write_to_dlo)
494
+ else:
495
+ write["dmo"] = list(output.write_to_dmo)
496
+
497
+ existing_config["permissions"] = {"read": read, "write": write}
388
498
  return existing_config
389
499
 
390
500
 
501
+ def _update_streaming_config(existing_config: dict[str, Any], file_path: str) -> None:
502
+ output = scan_file_streaming(file_path)
503
+ read_layer = output.read_layer
504
+
505
+ source = existing_config.get("streamingSource")
506
+ if not isinstance(source, dict):
507
+ source = {}
508
+ source_name = source.get("name", "")
509
+ existing_config["streamingSource"] = {"type": read_layer, "name": source_name}
510
+
511
+ if not source_name:
512
+ logger.warning(
513
+ "streamingSource.name is empty in config.json. A streaming "
514
+ "transform must declare its read source; set streamingSource.name "
515
+ "to the DLO/DMO the transform reads from."
516
+ )
517
+
518
+ read_names = [source_name] if source_name else []
519
+ write_names = list(output.write_dlo_deltas)
520
+ existing_config["permissions"] = {
521
+ "read": {read_layer: read_names},
522
+ "write": {"dlo": write_names},
523
+ }
524
+
525
+
391
526
  def get_dataspace(existing_config: dict[str, str]) -> str:
392
527
  if "dataspace" in existing_config:
393
528
  dataspace_value = existing_config["dataspace"]
@@ -23,8 +23,12 @@ from datacustomcode.constants import FEATURE_TEMPLATE_MAPPING
23
23
  script_template_dir = os.path.join(os.path.dirname(__file__), "templates", "script")
24
24
  function_template_dir = os.path.join(os.path.dirname(__file__), "templates", "function")
25
25
 
26
+ STREAMING_EXAMPLE_ENTRYPOINT = os.path.join(
27
+ script_template_dir, "examples", "streaming_deltas", "entrypoint.py"
28
+ )
26
29
 
27
- def copy_script_template(target_dir: str) -> None:
30
+
31
+ def copy_script_template(target_dir: str, streaming: bool = False) -> None:
28
32
  """Copy the template to the target directory."""
29
33
  os.makedirs(target_dir, exist_ok=True)
30
34
 
@@ -39,6 +43,14 @@ def copy_script_template(target_dir: str) -> None:
39
43
  logger.debug(f"Copying file {source} to {destination}...")
40
44
  shutil.copy2(source, destination)
41
45
 
46
+ if streaming:
47
+ destination = os.path.join(target_dir, "payload", "entrypoint.py")
48
+ logger.debug(
49
+ f"Copying streaming example {STREAMING_EXAMPLE_ENTRYPOINT} to "
50
+ f"{destination}..."
51
+ )
52
+ shutil.copy2(STREAMING_EXAMPLE_ENTRYPOINT, destination)
53
+
42
54
 
43
55
  def copy_function_template(target_dir: str, use_in_feature: Optional[str]) -> None:
44
56
  os.makedirs(target_dir, exist_ok=True)
@@ -0,0 +1,62 @@
1
+ """Streaming BYOC transform: read a DLO change feed and write the deltas back.
2
+
3
+ This example is the streaming counterpart to a normal batch entrypoint. Instead
4
+ of a batch ``Client`` with ``read_dlo`` / ``write_to_dlo`` (which read and write
5
+ a bounded snapshot), it uses a :class:`StreamingClient` and its streaming delta
6
+ methods.
7
+
8
+ The first run of a streaming job will use the run mode INITIAL_SYNC which behaves
9
+ like a batch run on the streaming source. A streaming transform can also use run
10
+ mode REBUILD to do the same thing on demand. Note that these will process all
11
+ source rows and overwrite the target.
12
+
13
+ The transform in between is ordinary PySpark. Because the source is a change
14
+ feed, keep the metadata columns on the DataFrame you hand to
15
+ ``write_dlo_deltas`` — the sink relies on them to merge changes correctly.
16
+
17
+ This entrypoint only runs inside the Data Cloud runtime;
18
+ the local ``datacustomcode run`` readers/writers raise
19
+ ``NotImplementedError`` for the delta methods.
20
+ """
21
+
22
+ from pyspark.sql import DataFrame
23
+ from pyspark.sql.functions import col, upper
24
+
25
+ from datacustomcode.client import (
26
+ RunMode,
27
+ StreamingClient,
28
+ get_run_mode,
29
+ )
30
+
31
+
32
+ def main():
33
+ target_dlo = "Account_std_copy__dll"
34
+ client = StreamingClient()
35
+
36
+ if get_run_mode() == RunMode.DELTA_SYNC:
37
+ # Streaming DataFrame over the source DLO's change feed.
38
+ dataframe = client.read_dlo_deltas()
39
+ # Ordinary PySpark transform.
40
+ transformed = transform(dataframe)
41
+
42
+ # Start the streaming write. write_dlo_deltas returns the StreamingQuery;
43
+ # the trigger and checkpoint location are provided by the runtime.
44
+ query = client.write_dlo_deltas(target_dlo, transformed)
45
+
46
+ # Drive the query's lifecycle. In the streaming runtime this blocks until
47
+ # the job is stopped by the platform.
48
+ query.awaitTermination()
49
+ else:
50
+ # initial sync and rebuild read the entire streaming source DLO and
51
+ # write using a server-decided mode based on the run mode
52
+ dataframe = client.read_dlo()
53
+ transformed = transform(dataframe)
54
+ client.auto_write_to_dlo(target_dlo, transformed)
55
+
56
+
57
+ def transform(dataframe: DataFrame) -> DataFrame:
58
+ return dataframe.withColumn("description__c", upper(col("description__c")))
59
+
60
+
61
+ if __name__ == "__main__":
62
+ main()
@@ -45,13 +45,24 @@ check_docker() {
45
45
  echo "Docker daemon is running"
46
46
  }
47
47
 
48
+ # Function to check if openssl is installed
49
+ check_openssl() {
50
+ if ! command -v openssl &> /dev/null; then
51
+ echo "openssl is not installed. It is required to generate a secure JupyterLab access token."
52
+ exit 1
53
+ fi
54
+ }
55
+
48
56
  # Function to start Jupyter server
49
57
  start_jupyter() {
50
58
  echo "Building the docker image"
51
59
  docker build -t datacloud-customcode .
52
60
 
61
+ local TOKEN
62
+ TOKEN=$(openssl rand -hex 32)
63
+
53
64
  echo "Running the docker container"
54
- docker run -d --rm -p 8888:8888 \
65
+ docker run -d --rm -p 127.0.0.1:8888:8888 \
55
66
  -v $(pwd):/workspace \
56
67
  --name jupyter-server \
57
68
  datacloud-customcode jupyter lab \
@@ -59,12 +70,14 @@ start_jupyter() {
59
70
  --port=8888 \
60
71
  --no-browser \
61
72
  --allow-root \
62
- --NotebookApp.token='' \
63
- --NotebookApp.password='' \
73
+ --NotebookApp.token="$TOKEN" \
64
74
  --notebook-dir=/workspace
65
75
 
66
76
  sleep 3 # Wait for server to start
67
- open_browser "http://localhost:8888"
77
+ local URL
78
+ URL="http://localhost:8888/?token=$TOKEN"
79
+ echo "Opening $URL"
80
+ open_browser $URL
68
81
  }
69
82
 
70
83
  # Function to stop Jupyter server
@@ -82,6 +95,7 @@ stop_jupyter() {
82
95
  case "$1" in
83
96
  "start")
84
97
  check_docker
98
+ check_openssl
85
99
  start_jupyter
86
100
  ;;
87
101
  "stop")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: salesforce-data-customcode
3
- Version: 6.1.0.dev3
3
+ Version: 6.1.0.dev5
4
4
  Summary: Data Cloud Custom Code SDK
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE.txt
@@ -170,15 +170,22 @@ Your Python dependencies can be packaged as .py files, .zip archives (containing
170
170
 
171
171
  ## API
172
172
 
173
- Your entry point script will define logic using the `Client` object which wraps data access layers.
173
+ Your entry point script will define logic using the `Client` object (for batch transforms) or the `StreamingClient` object (for streaming delta transforms), which wrap the data access layers. Both are singletons; a single transform should use one or the other, not both.
174
174
 
175
- You should only need the following methods:
175
+ For a batch transform, use `Client`. You should only need the following methods:
176
176
  * `find_file_path(file_name)` – Resolve a bundled file (placed under `payload/files/`) to a `pathlib.Path` that exists. Works the same locally and inside Data Cloud — see [Bundled file resolution](#bundled-file-resolution) below for the full lookup order. Raises `FileNotFoundError` if the file isn't found.
177
177
  * `read_dlo(name)` – Read from a Data Lake Object by name
178
178
  * `read_dmo(name)` – Read from a Data Model Object by name
179
179
  * `write_to_dlo(name, spark_dataframe, write_mode)` – Write to a Data Model Object by name with a Spark dataframe
180
180
  * `write_to_dmo(name, spark_dataframe, write_mode)` – Write to a Data Lake Object by name with a Spark dataframe
181
181
 
182
+ For a streaming (delta) transform, use `StreamingClient`, which exposes the streaming counterparts:
183
+ * `read_dlo_deltas()` – Read the streaming change feed (deltas) of a Data Lake Object as a streaming DataFrame.
184
+ * `read_dmo_deltas()` – Read the streaming change feed (deltas) of a Data Model Object as a streaming DataFrame.
185
+ * `write_dlo_deltas(name, spark_dataframe)` – Write a streaming DataFrame of deltas to a Data Lake Object; returns the started `StreamingQuery`
186
+
187
+ `find_file_path`, `llm_gateway_generate_text`, and `einstein_predict` are available on both clients.
188
+
182
189
  For example:
183
190
  ```python
184
191
  from datacustomcode import Client
@@ -194,6 +201,36 @@ client.write_to_dlo('output_DLO')
194
201
  > [!WARNING]
195
202
  > Currently we only support reading from DMOs and writing to DMOs or reading from DLOs and writing to DLOs, but they cannot mix.
196
203
 
204
+ ### Streaming (delta) transforms
205
+
206
+ Streaming BYOC transforms process a Data Lake Object's Change Data Feed continuously instead of reading a bounded snapshot. Use a `StreamingClient` and its `*_deltas` methods in place of the batch `Client` read/write methods:
207
+
208
+ ```python
209
+ from pyspark.sql.functions import col, upper
210
+
211
+ from datacustomcode import StreamingClient
212
+
213
+ client = StreamingClient()
214
+
215
+ # read_dlo_deltas returns a *streaming* DataFrame over the change feed.
216
+ # The runtime resolves the single streaming source, so no name is passed.
217
+ deltas = client.read_dlo_deltas()
218
+
219
+ # Ordinary PySpark transform.
220
+ transformed = deltas.withColumn("description__c", upper(col("description__c")))
221
+
222
+ # write_dlo_deltas starts a streaming query and returns the StreamingQuery.
223
+ # The runtime owns the trigger and checkpoint location; you
224
+ # choose only the target table.
225
+ query = client.write_dlo_deltas("Output__dll", transformed)
226
+ query.awaitTermination()
227
+ ```
228
+
229
+ Notes:
230
+
231
+ - These methods only run inside the Data Cloud streaming (`DELTA_SYNC`) runtime. Locally (`datacustomcode run`) they raise `NotImplementedError`, since there is no change feed to stream.
232
+ - A complete runnable entry point is provided in [`examples/streaming_deltas/entrypoint.py`](src/datacustomcode/templates/script/examples/streaming_deltas/entrypoint.py).
233
+
197
234
  ### Bundled file resolution
198
235
 
199
236
  Place bundled files (CSVs, prompt files, etc.) under `payload/files/`. The same `client.find_file_path("data.csv")` call resolves consistently across all three runtimes:
@@ -497,7 +534,7 @@ exploration. Instead of running an entire script, one can run one code cell at
497
534
 
498
535
  You can read more about Jupyter Notebooks here: https://jupyter.org/
499
536
 
500
- 1. Within the root project of your package folder, run `./jupyterlab.sh start`
537
+ 1. Within the root project of your package folder, run `./jupyterlab.sh start`. This prints an access token and opens an already-authenticated JupyterLab session in your browser. If the browser doesn't open automatically, copy the printed `http://localhost:8888/?token=...` URL into your browser.
501
538
  1. Double-click on "account.ipynb" file, which provides a starting point for a notebook
502
539
  1. Use shift+enter to execute each cell within the notebook. Add/edit/delete cells of code as needed for your data exploration.
503
540
  1. Don't forget to run `./jupyterlab.sh stop` to stop the docker container
@@ -588,5 +625,5 @@ If you're using OAuth Tokens authentication, the initial configure will retrieve
588
625
  ## Other docs
589
626
 
590
627
  - [Troubleshooting](./docs/troubleshooting.md)
591
- - [For Contributors](./FOR_CONTRIBUTORS.md)
628
+ - [Contributing](./CONTRIBUTING.md)
592
629
 
@@ -1,14 +1,14 @@
1
- datacustomcode/__init__.py,sha256=SGsDnWDTihWkLNnWdqvH08zklQgxykar73JAcPL2plI,2666
1
+ datacustomcode/__init__.py,sha256=5vvWRk5gb2mZIaVZ9BSPRL45uKSUwmSerxhLp6A4Q-4,2815
2
2
  datacustomcode/auth.py,sha256=fpSjhIBdv9trC8yq2vuljAix_Euu-4Ah7HDCGhYjOxI,8309
3
- datacustomcode/cli.py,sha256=qAUEFztKjYaG6Ws8xuE8rUpUr06snmFbUEuNRggjdBY,13256
4
- datacustomcode/client.py,sha256=lbOseiGaDru-vUtdRXgAORhrJa5V5WrFkbcEIfzBkaU,24325
3
+ datacustomcode/cli.py,sha256=K4LHL5xrb7-erXbjYz7pT4Kg4jG6Wf81c9wl_8tLabE,14162
4
+ datacustomcode/client.py,sha256=1vX41y2KvKC-qvs48Vrp0ylpQovytECXS4LrsAdMCC4,26568
5
5
  datacustomcode/cmd.py,sha256=ZMs46aydJw2EaU26JgCtZmnqESQFHvvaJz10hnjZTBk,3537
6
6
  datacustomcode/common_config.py,sha256=SAUnxj3kqmOeWwPmFoYq4tuxokMgURVm4QwcGi-avL4,1928
7
- datacustomcode/config.py,sha256=2Pk61ieQsEWSxKDxlS66_rliKE8iX0j-9LweEm0aaGo,4072
8
- datacustomcode/config.yaml,sha256=LqpbkZhzN0nZOPEPnaz2qOCbchp8fxC4GHamT_b56D0,1082
9
- datacustomcode/constants.py,sha256=wcWy82JAZgMCzIrDLb6k0nl-qtNHawGedGBxY4atL8E,1435
7
+ datacustomcode/config.py,sha256=lqed3jcWfoIweikSFZelaAoyBa0cXXScL8O6vaqdeRU,4372
8
+ datacustomcode/config.yaml,sha256=ldssmQYPghmYSSPaelm3F2mC99njavhrWsINRSzWymM,933
9
+ datacustomcode/constants.py,sha256=NCGptcql30En5N7WAH96TYD_DTzHFYdh1sZZ5f327cs,1686
10
10
  datacustomcode/credentials.py,sha256=D-7Zd3Nh_wStdj8_wUy5cnC8M_mdbfBijhIAi-2EbXs,9467
11
- datacustomcode/deploy.py,sha256=eN5k7Rbxe-Yf8c_CeeHICZJqzJqMuvIVNu11agNW8NA,22111
11
+ datacustomcode/deploy.py,sha256=HvKoXcUfBwBRsTWCDp22Qg3fJV1qRZ-TFNQ-Od8AATE,23441
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
14
  datacustomcode/einstein_predictions/__init__.py,sha256=_RIDTqcEHDRu6AsCsj9lRBykNkHI8Ch8JBILL1ioPtA,1237
@@ -27,19 +27,19 @@ datacustomcode/function/__init__.py,sha256=oag8FevDxbx1vR0tFhtxPd5SvmUEYDj8esRo4
27
27
  datacustomcode/function/base.py,sha256=H_i80GcsFScvHJJ6bYLry5phTpsEU1KubpR8D3qnsjo,691
28
28
  datacustomcode/function/feature_types/__init__.py,sha256=gamfOD1VnAtEslRBpqh-yKiVjkG_wYWYdSatGIfsN-w,621
29
29
  datacustomcode/function/feature_types/chunking.py,sha256=_tCCHj7slD59-aLLEZUFRPOX37q_MEKkL4thi9CNwMw,7180
30
- datacustomcode/function/runtime.py,sha256=Q6h-vcW1TNhOh3q1rlmQ24zs-OloNkJexdtbFyIxt4o,4437
30
+ datacustomcode/function/runtime.py,sha256=jUduf-pve-0JAv0Lg0HPyLCZ9_Hgcxe25dI7t-DgX9o,3696
31
31
  datacustomcode/function_utils.py,sha256=y6vaoSSaJ9CeHhjLCNyqzJT4vx6yCePWRxNLz38ddjQ,12920
32
32
  datacustomcode/io/__init__.py,sha256=gamfOD1VnAtEslRBpqh-yKiVjkG_wYWYdSatGIfsN-w,621
33
33
  datacustomcode/io/base.py,sha256=gbwZWWVUbCbGR4jIg_4h4qOz8tOMjE4RDTleD23WFKo,973
34
34
  datacustomcode/io/reader/__init__.py,sha256=gamfOD1VnAtEslRBpqh-yKiVjkG_wYWYdSatGIfsN-w,621
35
- datacustomcode/io/reader/base.py,sha256=JRKg8KfEPJGR58wqicsEdpAPiXTl2K1eV3eHQyxYWaE,1390
35
+ datacustomcode/io/reader/base.py,sha256=2hAqaZvzHvvr5KBEgQSjUrHpidcHkgUXXTbGQG-b8io,3176
36
36
  datacustomcode/io/reader/query_api.py,sha256=eVrohrcnTnhSMsGfPRHu5XltjFtGG0hyHt1o4q1hp2w,9340
37
37
  datacustomcode/io/reader/sf_cli.py,sha256=x5QacVqRZaZSyph1_wwxo67s8r29wsOcUsOaiF9cDWE,6324
38
38
  datacustomcode/io/reader/utils.py,sha256=HlHhPZoHfmWA3mF8kTnd3m4Nd2exaz4NsOJJfQY7Pew,1656
39
39
  datacustomcode/io/writer/__init__.py,sha256=gamfOD1VnAtEslRBpqh-yKiVjkG_wYWYdSatGIfsN-w,621
40
- datacustomcode/io/writer/base.py,sha256=6e2Yszb6BiuN1zCV2rjvZ5CQ0YikQdUgdYAkutoy9pE,1787
41
- datacustomcode/io/writer/csv.py,sha256=asty5teBpNQ1fMGHZ7wA3suLhq0sk0lVQPN5x7TOSRk,1555
42
- datacustomcode/io/writer/print.py,sha256=0g2sP_1Wb95UIyEELWJt3dqM18Iv_CWhDW3mDxcIhns,5105
40
+ datacustomcode/io/writer/base.py,sha256=W0QmsmszCjOIYPlEqbAjld0O855CAFvZjm_FT7szyb4,3806
41
+ datacustomcode/io/writer/csv.py,sha256=LYQpTZlIQdn0RRx_dRah22V2Vgm-InKP4KHRKfWGiRY,1963
42
+ datacustomcode/io/writer/print.py,sha256=Tpy5-0NmJ_renC6uah5TdHQZFzDbx23ltm1tIPK0978,5388
43
43
  datacustomcode/llm_gateway/__init__.py,sha256=rcoGpSkv36tZuAOcTxKkhNpM0qV03qgmer_ej07Vmfc,1088
44
44
  datacustomcode/llm_gateway/base.py,sha256=CTUhZiZ0JFlmWj6kQpTR4_-mUvqkBKl_Fkdvv9VwxY8,1262
45
45
  datacustomcode/llm_gateway/default.py,sha256=QiwrDaUvrh00I-fYYhmogTtqC5r0fmm-JrXxNQLvTc0,1854
@@ -53,31 +53,13 @@ datacustomcode/llm_gateway/types/generate_text_response.py,sha256=NSvj6nvzR4cZ0c
53
53
  datacustomcode/llm_gateway/types/generate_text_response_builder.py,sha256=6MV6JZuTyHUId3Slw9k7m0KBgjj-5S9HI4qtd94Jl2A,1272
54
54
  datacustomcode/llm_gateway_config.py,sha256=ECm9CFqOFOaLHwenvVFAjiRSzG9Eg9fY4pb8nHbQ34c,3312
55
55
  datacustomcode/mixin.py,sha256=ZtROqO1W1_D7MV8lw4cw6zzcchJLoZS0aAiXrolYR4k,4914
56
- datacustomcode/named_credential/__init__.py,sha256=XD_UI8ggOkFvkqCOZI_3dP1AkviqPIZFOt3RI1_RDe4,1163
57
- datacustomcode/named_credential/base.py,sha256=-LMskZkKNoaFg-Zc80M_4IPLF7ru5pfnbkfLI7YXrFc,1816
58
- datacustomcode/named_credential/default.py,sha256=z03B0daZvTPiTmJWGAtDCICB1srelESg7QmPH2iRK5o,3339
59
- datacustomcode/named_credential/direct/__init__.py,sha256=zqErMI1GqEhseLnYkrWtq3WipRi_dP153c8wwLvgI-o,799
60
- datacustomcode/named_credential/direct/auth.py,sha256=U09jO0Pn3OEOEudmvcBmtkwoXqE0Ae_Mv6TAMqb3eG4,2201
61
- datacustomcode/named_credential/direct/credentials.py,sha256=fibIZTyBT1IbAGFjXwAXqYcQt9o_sB8zFmuixGmibE4,4534
62
- datacustomcode/named_credential/direct/transport.py,sha256=UL5eJJsyoAjr8ovJDQKQY3O20DivvbWFPGRyM8Y2or4,4078
63
- datacustomcode/named_credential/direct/url_resolver.py,sha256=KItv12Az0qoxnVacCTcM9t9AY7qB9_tqPWMgiP-7Y6s,3823
64
- datacustomcode/named_credential/errors.py,sha256=zyMEgljTf85digXrLrlR9HBh5Un4WtG0WsZx0K4EWt0,1212
65
- datacustomcode/named_credential/spark_base.py,sha256=w34B-Pg6m_hcUqUbjmB2b6zUme_MSgg6VeghbMMYYN4,3288
66
- datacustomcode/named_credential/spark_default.py,sha256=DY_STX9-1le8cS5M_vGqB3VUtQ2XCZZYDAmKId7YekA,5219
67
- datacustomcode/named_credential/types/__init__.py,sha256=gamfOD1VnAtEslRBpqh-yKiVjkG_wYWYdSatGIfsN-w,621
68
- datacustomcode/named_credential/types/http_method.py,sha256=NFf8emmW_Y73q8q2pseWHqOQ-1SoFwKp2vCMr1OdmEU,952
69
- datacustomcode/named_credential/types/http_request.py,sha256=LXW4EcvUfyOJHf8-VtFSRWy6JsTJJsd0N4-X6WxxFnA,2170
70
- datacustomcode/named_credential/types/http_request_builder.py,sha256=qdToSlX2xw3FzuLF6Kas3VzCp1ubdXZb4_WkjQ5fBY0,1862
71
- datacustomcode/named_credential/types/http_response.py,sha256=ZqX52RMjSdn8OpHSDkKJo9730QRx8o4_ZT-1noSMRe8,1480
72
- datacustomcode/named_credential/types/http_response_builder.py,sha256=MoUjpT-P5W2uT1-h1rIfKpBQHpMj_ZA_nUmn-FyBdbQ,896
73
- datacustomcode/named_credential_config.py,sha256=M07-oWzYj0KTrfA0fqvk3BQvaaDaw7C6nxbR0s3lOUk,3645
74
56
  datacustomcode/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
75
- datacustomcode/run.py,sha256=PXfpIwcgqyW6KLawTUuzGSt4FeAcxtafg-FgBHGXOLg,8091
76
- datacustomcode/scan.py,sha256=Zb9mE733H_xaqyDI-LZjjnIcctMC9j1AaD_kcr9qTh4,14325
57
+ datacustomcode/run.py,sha256=Ky58Di6Nh7gk6Ic-5Jxt3Y4xB3vRNmzlUcoSlK-OLvo,8169
58
+ datacustomcode/scan.py,sha256=9rq1Wae_tYACJykaOmBOv1covYvOSdky8w13exnmyJ0,19170
77
59
  datacustomcode/spark/__init__.py,sha256=12drVVlRiczCxOQw-EzuGtLsikCM8baBXvDEgwvelCI,860
78
60
  datacustomcode/spark/base.py,sha256=tlGqM4LxuLoDa7OxJF5nVeb7phO5uvD1DHBQeH7NMMs,1036
79
61
  datacustomcode/spark/default.py,sha256=aMB8CaTPYwHbQE_7XqBLQUZPGRdDJcRL72qTVh0aG7M,1433
80
- datacustomcode/template.py,sha256=FNNi8YPfd-JB-hUt1DDWSFK5AqwKnTnRSy_KpqYluaU,3282
62
+ datacustomcode/template.py,sha256=1u0U86coPX7-8g0ZixCbFGDBOMNcOqMhTXdJDAN9TaU,3726
81
63
  datacustomcode/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
82
64
  datacustomcode/templates/function/.devcontainer/devcontainer.json,sha256=21RNTadhC-rynON2-VOtr5U174Hxnr_MA4RQ4592rsg,166
83
65
  datacustomcode/templates/function/Dockerfile.dependencies,sha256=AfHRddm5l3ujv4vdrf0d-SMB-qxPCOa0XQm6ptP2Euw,174
@@ -86,11 +68,6 @@ datacustomcode/templates/function/build_native_dependencies.sh,sha256=dk9vhzlObd
86
68
  datacustomcode/templates/function/chunking/payload/config.json,sha256=RBNvo1WzZ4oRRq0W9-hknpT7T8If536DEMBg9hyq_4o,2
87
69
  datacustomcode/templates/function/chunking/payload/entrypoint.py,sha256=tzpQJjODulwzLilOoKdOo8GjpdIlCDByjnawpHIQYQA,4517
88
70
  datacustomcode/templates/function/chunking/requirements.txt,sha256=Ih0KsVdiTdo7KuIsqFB_AJMRT2r0ao_xDEb3hiDmf48,46
89
- datacustomcode/templates/function/example/chunking_with_external_callout/README.md,sha256=RIgoUysFniAE4lC5LiZl5u55Rro1w5gmM2mMnsQF_HI,4716
90
- datacustomcode/templates/function/example/chunking_with_external_callout/config.json,sha256=EvjuOVssRg2SjcUb5F50RRMGDqag_f69bv8AOflaK1g,38
91
- datacustomcode/templates/function/example/chunking_with_external_callout/entrypoint.py,sha256=QqJLeJHgtW4RkDwTaUIsb2ng_ODwwClXpn5F7XyXBes,5296
92
- datacustomcode/templates/function/example/chunking_with_external_callout/external_callout_config.json,sha256=PUCmw_humbVoCyswFneuGYx0INbqSTPOumDqtGOztiM,320
93
- datacustomcode/templates/function/example/chunking_with_external_callout/tests/test.json,sha256=ZqUthCT9F9HoyT493tXg4nFDSbEAeaFHY1iHo-9Qx7g,2703
94
71
  datacustomcode/templates/function/example/chunking_with_llm/config.json,sha256=EvjuOVssRg2SjcUb5F50RRMGDqag_f69bv8AOflaK1g,38
95
72
  datacustomcode/templates/function/example/chunking_with_llm/entrypoint.py,sha256=6AMVf_busCCDALktG8vkCtjnzJheMgM-OOLML5iLKOg,3437
96
73
  datacustomcode/templates/function/example/chunking_with_llm/files/chunking_prompt.txt,sha256=w_gSYNXwZmbMTMjZ_YYwm1IpMsVpI3hyC3ew60zqs_0,446
@@ -111,15 +88,16 @@ datacustomcode/templates/script/account.ipynb,sha256=LIbxgiVxflNASdspF2lfpMKkKAT
111
88
  datacustomcode/templates/script/build_native_dependencies.sh,sha256=ICRrp4f1ATBwPaUiuVGjY-MwiubFswNJLf8gMGU6YNg,197
112
89
  datacustomcode/templates/script/examples/employee_hierarchy/employee_data.csv,sha256=C7ggLBfoyi3M2BdMLNyOeKqF-5OO-Da76lkwgWg2cVQ,302
113
90
  datacustomcode/templates/script/examples/employee_hierarchy/entrypoint.py,sha256=Mfm3iQtEHTQRW6cmZTwRKdTh3IeGWR-teXrd6x-YLSY,2223
114
- datacustomcode/templates/script/jupyterlab.sh,sha256=IHR3YQ8d_busuyvesByhJgzCKULIFPI-Hqogs0NPhCs,2432
91
+ datacustomcode/templates/script/examples/streaming_deltas/entrypoint.py,sha256=UQr1EhJURFFGI5626efvv-MxGHltzcpJcEJVmz5ZQdo,2333
92
+ datacustomcode/templates/script/jupyterlab.sh,sha256=0O_rPZ8w2DM1hxPo9Wig9j-ymhS2dRzVO2PlFClWdlA,2786
115
93
  datacustomcode/templates/script/payload/config.json,sha256=0d2mEMt4NHIeOUTsgiPMuRKdOLIQxmh-Wq-IfEqU-gc,28
116
94
  datacustomcode/templates/script/payload/entrypoint.py,sha256=4Uph0ILa5ukk_6C90paK3uzQn0zZvNLr9ho3o_ZwErQ,2474
117
95
  datacustomcode/templates/script/requirements-dev.txt,sha256=OWwuy1awesqOZOS3Zizmdd6BDnSePpGFN5bq5IrALvc,176
118
96
  datacustomcode/templates/script/requirements.txt,sha256=qJbqzs5z0Hrx590U3T7dHq_m8UQEx2TdhgrJSz-QHIQ,40
119
97
  datacustomcode/token_provider.py,sha256=qA_e4vqSXnxn0MccFFPULWO8ZeUOEAu5QpBnoYCg-fc,6839
120
98
  datacustomcode/version.py,sha256=9LlbVrzwBvut1L308QbwNBgxdQk5CrghH39JARVSxms,989
121
- salesforce_data_customcode-6.1.0.dev3.dist-info/METADATA,sha256=FDlrw8kdEPaFPCw8PJ0knkNqmS3vIuntw2oznOqA-A0,25115
122
- salesforce_data_customcode-6.1.0.dev3.dist-info/WHEEL,sha256=eY7nduwzv-ldUxpzbRlxwvC693Hg6PX8bWDjEHjZ_dk,88
123
- salesforce_data_customcode-6.1.0.dev3.dist-info/entry_points.txt,sha256=WpQ94UB7UuRCYGOLtJV3vAgm1tl7iVf43VYRWIuNu5Y,74
124
- salesforce_data_customcode-6.1.0.dev3.dist-info/licenses/LICENSE.txt,sha256=iOi8EmQpfkFhMENi7VYtkl2EqS14pEOccoXHiW2dyPU,11443
125
- salesforce_data_customcode-6.1.0.dev3.dist-info/RECORD,,
99
+ salesforce_data_customcode-6.1.0.dev5.dist-info/METADATA,sha256=VCspB8hb-Qia1FvP5047hBU8M8uEO-VcuNP-6Wf_7nc,27417
100
+ salesforce_data_customcode-6.1.0.dev5.dist-info/WHEEL,sha256=eY7nduwzv-ldUxpzbRlxwvC693Hg6PX8bWDjEHjZ_dk,88
101
+ salesforce_data_customcode-6.1.0.dev5.dist-info/entry_points.txt,sha256=WpQ94UB7UuRCYGOLtJV3vAgm1tl7iVf43VYRWIuNu5Y,74
102
+ salesforce_data_customcode-6.1.0.dev5.dist-info/licenses/LICENSE.txt,sha256=iOi8EmQpfkFhMENi7VYtkl2EqS14pEOccoXHiW2dyPU,11443
103
+ salesforce_data_customcode-6.1.0.dev5.dist-info/RECORD,,
@@ -1,28 +0,0 @@
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
- ]