Skip to content
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