Recipes
Patterns that go past the quickstart. Every example
uses RockyClient from rocky-sdk.
Stream live progress
Section titled “Stream live progress”run() takes a log_callback. It receives the engine’s stderr one line at a
time while the run executes. Point it at print, at a logger, or at your
orchestrator’s logging:
import logging
from rocky_sdk import RockyClient
log = logging.getLogger("my_pipeline")client = RockyClient(config_path="rocky.toml")client.run(filter="tenant=acme", log_callback=log.info)The typed RunResult still comes back when the run finishes.
Handle failures
Section titled “Handle failures”The exception types and the timeout rules are in Errors. To give one run a larger budget, pass timeout_seconds= to that run() call:
client.run(filter="tenant=acme", timeout_seconds=7200)Partial success
Section titled “Partial success”run() returns its RunResult even when some tables fail. It does not raise.
You can act on what landed and report the rest:
run = client.run(filter="tenant=acme")if run.tables_failed: for err in run.errors: print(f"{'/'.join(err.asset_key)} failed: {err.error}") # decide: raise, alert, or proceed with the tables that did succeedif run.check_gate_failed: for table in run.check_results: for check in table.checks: if not check.passed and check.severity == "error": print(f"{'/'.join(table.asset_key)}: check {check.name} failed") # the data has landed; a resume re-runs none of these checksCheck both fields. tables_failed counts tables and models, never checks. A run
that copied every table and then failed an error-severity check exits 2 with
tables_failed == 0 and check_gate_failed == True, so reading the count alone
reports that run as clean.
To make a non-zero run raise instead of returning a partial result, call the
lower-level run_cli(args, allow_partial=False). It raises
RockyPartialFailure, and the partial JSON is on exc.stdout.
Use a long-lived server
Section titled “Use a long-lived server”For repeated read-only calls, point the client at a running rocky serve
instead of spawning a subprocess per call. Set server_url on the client:
client = RockyClient(config_path="rocky.toml", server_url="http://localhost:8080")client.compile() # served over HTTPclient.lineage("revenue_summary")Three rules govern this mode.
- Only
compile,lineage, andmetricshonourserver_url.run()and every write path always spawn a subprocess. - Each endpoint serves its command’s default output.
lineage’scolumnargument is supported. - Arguments the endpoints do not serve raise
ValueErrorrather than being ignored:compile’smodel_filter, andmetrics’strend,column, andalerts.
Run inside any orchestrator
Section titled “Run inside any orchestrator”rocky-sdk is how a non-Dagster orchestrator integrates with Rocky. Construct a
RockyClient inside a task and branch on the typed result. Dagster users get
the dagster-rocky integration instead.
Airflow — wrap a run in a @task:
from airflow.decorators import task
from rocky_sdk import RockyClient
@taskdef materialize(tenant: str) -> int: client = RockyClient(config_path="rocky.toml") run = client.run(filter=f"tenant={tenant}") if run.tables_failed: raise RuntimeError(f"{run.tables_failed} tables failed: {run.errors}") return len(run.materializations)Prefect — the same client inside a @flow:
from prefect import flow, task
from rocky_sdk import RockyClient
@taskdef materialize(tenant: str): client = RockyClient(config_path="rocky.toml") return client.run(filter=f"tenant={tenant}")
@flowdef rocky_pipeline(tenants: list[str]): for tenant in tenants: materialize(tenant)Both examples are illustrative. They need apache-airflow or prefect
installed, and the rocky binary on PATH.