Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
# Changelog

## 6.1.0

### Added

- **`StreamingClient` for BYOC streaming (delta) transforms.**

A dedicated `StreamingClient` (alongside the batch `Client`) lets an entry point process a Data Lake Object's Change Data Feed continuously instead of reading a bounded snapshot.

- `read_dlo_deltas()` / `read_dmo_deltas()` – return a streaming DataFrame over the object's change feed.
- `write_dlo_deltas(name, dataframe)` – start a streaming query that writes each micro-batch to the target DLO and return the `StreamingQuery` handle.

The shared functions (`find_file_path`, `llm_gateway_generate_text`, `einstein_predict`) are available on both `Client` and `StreamingClient`.

```python
from datacustomcode import StreamingClient

client = StreamingClient()
deltas = client.read_dlo_deltas()
transformed = deltas.withColumn("description__c", upper(col("description__c")))
query = client.write_dlo_deltas("Output__dll", transformed)
query.awaitTermination()
```

These methods run only inside the Data Cloud streaming (`DELTA_SYNC`) runtime; locally they raise `NotImplementedError`. See the `examples/streaming_deltas/entrypoint.py` example and the "Streaming (delta) transforms" section of the README.

## 6.0.0

### Breaking Changes
Expand Down
41 changes: 39 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,15 +145,22 @@ Your Python dependencies can be packaged as .py files, .zip archives (containing

## API

Your entry point script will define logic using the `Client` object which wraps data access layers.
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.

You should only need the following methods:
For a batch transform, use `Client`. You should only need the following methods:
* `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.
* `read_dlo(name)` – Read from a Data Lake Object by name
* `read_dmo(name)` – Read from a Data Model Object by name
* `write_to_dlo(name, spark_dataframe, write_mode)` – Write to a Data Model Object by name with a Spark dataframe
* `write_to_dmo(name, spark_dataframe, write_mode)` – Write to a Data Lake Object by name with a Spark dataframe

For a streaming (delta) transform, use `StreamingClient`, which exposes the streaming counterparts:
* `read_dlo_deltas()` – Read the streaming change feed (deltas) of a Data Lake Object as a streaming DataFrame.
* `read_dmo_deltas()` – Read the streaming change feed (deltas) of a Data Model Object as a streaming DataFrame.
* `write_dlo_deltas(name, spark_dataframe)` – Write a streaming DataFrame of deltas to a Data Lake Object; returns the started `StreamingQuery`

`find_file_path`, `llm_gateway_generate_text`, and `einstein_predict` are available on both clients.

For example:
```python
from datacustomcode import Client
Expand All @@ -169,6 +176,36 @@ client.write_to_dlo('output_DLO')
> [!WARNING]
> Currently we only support reading from DMOs and writing to DMOs or reading from DLOs and writing to DLOs, but they cannot mix.

### Streaming (delta) transforms

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:

```python
from pyspark.sql.functions import col, upper

from datacustomcode import StreamingClient

client = StreamingClient()

# read_dlo_deltas returns a *streaming* DataFrame over the change feed.
# The runtime resolves the single streaming source, so no name is passed.
deltas = client.read_dlo_deltas()

# Ordinary PySpark transform.
transformed = deltas.withColumn("description__c", upper(col("description__c")))

# write_dlo_deltas starts a streaming query and returns the StreamingQuery.
# The runtime owns the trigger and checkpoint location; you
# choose only the target table.
query = client.write_dlo_deltas("Output__dll", transformed)
query.awaitTermination()
```

Notes:

- 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.
- A complete runnable entry point is provided in [`examples/streaming_deltas/entrypoint.py`](src/datacustomcode/templates/script/examples/streaming_deltas/entrypoint.py).

### Bundled file resolution

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:
Expand Down
5 changes: 5 additions & 0 deletions src/datacustomcode/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"QueryAPIDataCloudReader",
"SparkEinsteinPredictions",
"SparkLLMGateway",
"StreamingClient",
"einstein_predict_col",
"llm_gateway_generate_text_col",
]
Expand All @@ -34,6 +35,10 @@ def __getattr__(name: str):
from datacustomcode.client import Client

return Client
elif name == "StreamingClient":
from datacustomcode.client import StreamingClient

return StreamingClient
elif name == "AuthType":
from datacustomcode.credentials import AuthType

Expand Down
33 changes: 29 additions & 4 deletions src/datacustomcode/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,20 +283,43 @@ def deploy(
)
@click.option(
"--use-in-feature",
default="SearchIndexChunking",
help="Feature where this function will be used (only applicable for function).",
"-u",
default=None,
help=(
"Invoke option for this package. For scripts: 'BatchTransform' "
"(default) or 'StreamingTransform'. For functions: 'SearchIndexChunking'."
),
)
def init(directory: str, code_type: str, use_in_feature: Optional[str]):
from datacustomcode.constants import (
SCRIPT_USE_IN_FEATURE_BATCH,
SCRIPT_USE_IN_FEATURE_OPTIONS,
SCRIPT_USE_IN_FEATURE_STREAMING,
)
from datacustomcode.scan import (
dc_config_json_from_file,
update_config,
write_sdk_config,
)
from datacustomcode.template import copy_function_template, copy_script_template

streaming = False
if code_type == "script":
use_in_feature = use_in_feature or SCRIPT_USE_IN_FEATURE_BATCH
if use_in_feature not in SCRIPT_USE_IN_FEATURE_OPTIONS:
click.secho(
f"Error: Invalid --use-in-feature '{use_in_feature}' for a "
f"script. Valid options: {', '.join(SCRIPT_USE_IN_FEATURE_OPTIONS)}.",
fg="red",
)
raise click.Abort()
streaming = use_in_feature == SCRIPT_USE_IN_FEATURE_STREAMING
else:
use_in_feature = use_in_feature or "SearchIndexChunking"

click.echo("Copying template to " + click.style(directory, fg="blue", bold=True))
if code_type == "script":
copy_script_template(directory)
copy_script_template(directory, streaming=streaming)
elif code_type == "function":
copy_function_template(directory, use_in_feature)
entrypoint_path = os.path.join(directory, PAYLOAD_DIR, ENTRYPOINT_FILE)
Expand All @@ -306,7 +329,9 @@ def init(directory: str, code_type: str, use_in_feature: Optional[str]):
sdk_config = {"type": code_type}
write_sdk_config(directory, sdk_config)

config_json = dc_config_json_from_file(entrypoint_path, code_type)
config_json = dc_config_json_from_file(
entrypoint_path, code_type, streaming=streaming
)
with open(config_location, "w") as f:
json.dump(config_json, f, indent=2)

Expand Down
Loading
Loading