brokoli 0.5.0__tar.gz
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.
Potentially problematic release.
This version of brokoli might be problematic. Click here for more details.
- brokoli-0.5.0/PKG-INFO +503 -0
- brokoli-0.5.0/README.md +468 -0
- brokoli-0.5.0/brokoli/__init__.py +143 -0
- brokoli-0.5.0/brokoli/async_client.py +322 -0
- brokoli-0.5.0/brokoli/cli.py +1073 -0
- brokoli-0.5.0/brokoli/client.py +550 -0
- brokoli-0.5.0/brokoli/compatibility.py +216 -0
- brokoli-0.5.0/brokoli/decorators.py +446 -0
- brokoli-0.5.0/brokoli/exceptions.py +65 -0
- brokoli-0.5.0/brokoli/ir.py +146 -0
- brokoli-0.5.0/brokoli/nodes.py +998 -0
- brokoli-0.5.0/brokoli/pagination.py +218 -0
- brokoli-0.5.0/brokoli/parsing.py +181 -0
- brokoli-0.5.0/brokoli/pipeline.py +2203 -0
- brokoli-0.5.0/brokoli/py.typed +0 -0
- brokoli-0.5.0/brokoli/resources.py +166 -0
- brokoli-0.5.0/brokoli/result.py +73 -0
- brokoli-0.5.0/brokoli/sentinel.py +34 -0
- brokoli-0.5.0/brokoli/testing.py +283 -0
- brokoli-0.5.0/brokoli/validation.py +506 -0
- brokoli-0.5.0/brokoli.egg-info/PKG-INFO +503 -0
- brokoli-0.5.0/brokoli.egg-info/SOURCES.txt +60 -0
- brokoli-0.5.0/brokoli.egg-info/dependency_links.txt +1 -0
- brokoli-0.5.0/brokoli.egg-info/entry_points.txt +2 -0
- brokoli-0.5.0/brokoli.egg-info/requires.txt +17 -0
- brokoli-0.5.0/brokoli.egg-info/top_level.txt +1 -0
- brokoli-0.5.0/pyproject.toml +65 -0
- brokoli-0.5.0/setup.cfg +4 -0
- brokoli-0.5.0/tests/test_async_client.py +265 -0
- brokoli-0.5.0/tests/test_authoring_context.py +104 -0
- brokoli-0.5.0/tests/test_authoring_honesty.py +92 -0
- brokoli-0.5.0/tests/test_cli_deploy.py +275 -0
- brokoli-0.5.0/tests/test_cli_environments.py +105 -0
- brokoli-0.5.0/tests/test_cli_ir.py +360 -0
- brokoli-0.5.0/tests/test_cli_lifecycle.py +172 -0
- brokoli-0.5.0/tests/test_cli_loader_packaging.py +95 -0
- brokoli-0.5.0/tests/test_cli_run_status.py +210 -0
- brokoli-0.5.0/tests/test_cli_version.py +15 -0
- brokoli-0.5.0/tests/test_client.py +279 -0
- brokoli-0.5.0/tests/test_closure_capture.py +149 -0
- brokoli-0.5.0/tests/test_codegen.py +121 -0
- brokoli-0.5.0/tests/test_compatibility.py +295 -0
- brokoli-0.5.0/tests/test_decorator_packaging.py +150 -0
- brokoli-0.5.0/tests/test_decorators.py +269 -0
- brokoli-0.5.0/tests/test_deterministic_ids.py +513 -0
- brokoli-0.5.0/tests/test_discovery_side_effects.py +90 -0
- brokoli-0.5.0/tests/test_feature_preflight.py +143 -0
- brokoli-0.5.0/tests/test_ir.py +177 -0
- brokoli-0.5.0/tests/test_ir_digest.py +65 -0
- brokoli-0.5.0/tests/test_m2_imports_decorators.py +93 -0
- brokoli-0.5.0/tests/test_module_packaging.py +214 -0
- brokoli-0.5.0/tests/test_options_and_hooks.py +87 -0
- brokoli-0.5.0/tests/test_pagination.py +421 -0
- brokoli-0.5.0/tests/test_phase0.py +419 -0
- brokoli-0.5.0/tests/test_pipeline.py +804 -0
- brokoli-0.5.0/tests/test_resources.py +138 -0
- brokoli-0.5.0/tests/test_schema_parity.py +114 -0
- brokoli-0.5.0/tests/test_testing_harness.py +200 -0
- brokoli-0.5.0/tests/test_transform_rename.py +113 -0
- brokoli-0.5.0/tests/test_typed_refs.py +377 -0
- brokoli-0.5.0/tests/test_validate.py +294 -0
- brokoli-0.5.0/tests/test_validation_completeness.py +98 -0
brokoli-0.5.0/PKG-INFO
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: brokoli
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Brokoli Python SDK — define data pipelines in Python, run them visually
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Project-URL: Homepage, https://brokoli.orkestri.site
|
|
7
|
+
Project-URL: Documentation, https://docs-brokoli.orkestri.site
|
|
8
|
+
Project-URL: Repository, https://github.com/Tnsor-Labs/brokoli-sdk
|
|
9
|
+
Project-URL: Issues, https://github.com/Tnsor-Labs/brokoli-sdk/issues
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Topic :: Database
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Requires-Dist: pyyaml>=6.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
25
|
+
Requires-Dist: jsonschema>=4.0; extra == "dev"
|
|
26
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
27
|
+
Requires-Dist: mypy>=1.8; extra == "dev"
|
|
28
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
29
|
+
Provides-Extra: watch
|
|
30
|
+
Requires-Dist: sodp-client>=0.2.2; extra == "watch"
|
|
31
|
+
Provides-Extra: pandas
|
|
32
|
+
Requires-Dist: pandas>=1.5; extra == "pandas"
|
|
33
|
+
Provides-Extra: pyarrow
|
|
34
|
+
Requires-Dist: pyarrow>=12.0; extra == "pyarrow"
|
|
35
|
+
|
|
36
|
+
# Brokoli Python SDK
|
|
37
|
+
|
|
38
|
+
Define data pipelines in Python, run them visually.
|
|
39
|
+
|
|
40
|
+
See the [developer-experience roadmap](docs/developer-experience-roadmap.md)
|
|
41
|
+
for the current support boundaries and planned SDK/backend work.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install brokoli
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Quick Start
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from brokoli import Pipeline, task, source_api, quality_check, sink_file
|
|
51
|
+
|
|
52
|
+
with Pipeline("my_pipeline", schedule="0 6 * * *") as p:
|
|
53
|
+
|
|
54
|
+
data = source_api("Fetch Data", url="https://api.example.com/data", retries=3)
|
|
55
|
+
|
|
56
|
+
@task("Transform")
|
|
57
|
+
def clean(raw):
|
|
58
|
+
return [r for r in raw if r.get("status") == "active"]
|
|
59
|
+
|
|
60
|
+
cleaned = clean(data)
|
|
61
|
+
|
|
62
|
+
quality_check("Validate", cleaned, rules=["not_null(id)", "unique(id)", "row_count(min=1)"])
|
|
63
|
+
|
|
64
|
+
cleaned >> sink_file("Save", path="/tmp/output.csv")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# Deploy to Brokoli server (validates first)
|
|
69
|
+
brokoli deploy my_pipeline.py --server http://localhost:9900
|
|
70
|
+
|
|
71
|
+
# Pipeline appears in the visual editor instantly
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Core Concepts
|
|
75
|
+
|
|
76
|
+
### Pipeline
|
|
77
|
+
|
|
78
|
+
Context manager that collects nodes and edges into a DAG.
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
with Pipeline(
|
|
82
|
+
"name",
|
|
83
|
+
description="...",
|
|
84
|
+
schedule="0 6 * * *", # cron expression
|
|
85
|
+
sla="07:30 America/New_York", # must finish by this time
|
|
86
|
+
depends_on=["other_pipeline"], # wait for upstream
|
|
87
|
+
tags=["etl", "production"],
|
|
88
|
+
catch_up=True, # backfill missed runs
|
|
89
|
+
webhook=True, # enable HTTP trigger
|
|
90
|
+
on_success=lambda p, run: ..., # lifecycle hooks
|
|
91
|
+
on_failure=lambda p, run, err: ...,
|
|
92
|
+
) as p:
|
|
93
|
+
...
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Nodes
|
|
97
|
+
|
|
98
|
+
14 built-in node types:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
# Sources
|
|
102
|
+
source_db("Name", query="SELECT ...", conn_id="pg", retries=3)
|
|
103
|
+
source_api("Name", url="https://...", headers={...}, retries=3, timeout=30)
|
|
104
|
+
source_file("Name", path="/data/input.csv", format="csv")
|
|
105
|
+
|
|
106
|
+
# Processing
|
|
107
|
+
transform("Name", input, rules=["drop_null(id)", "deduplicate(email)"])
|
|
108
|
+
join("Name", left, right, on="id=customer_id", how="left")
|
|
109
|
+
quality_check("Name", input, rules=["not_null(id)", "unique(id)", "min(amount, 0)"])
|
|
110
|
+
|
|
111
|
+
# Outputs
|
|
112
|
+
sink_db("Name", input, table="output", mode="append", conn_id="pg")
|
|
113
|
+
sink_file("Name", input, path="/tmp/out.csv", format="csv")
|
|
114
|
+
sink_api("Name", input, url="https://hooks.slack.com/...", method="POST")
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Every node-producing API accepts `node_key=` for explicit logical identity:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
orders = source_db("Daily Orders", query="SELECT ...", node_key="orders-source")
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Keys are used exactly as IDs and must match
|
|
124
|
+
`^[a-z][a-z0-9_-]{0,63}$`; invalid or duplicate keys fail pipeline
|
|
125
|
+
construction. Without a key, IDs are deterministic within each pipeline:
|
|
126
|
+
the canonical display-name base plus a per-base counter, such as
|
|
127
|
+
`daily_orders_1` and `daily_orders_2`. Adding another node with the same
|
|
128
|
+
canonical name can therefore renumber later same-name nodes; use explicit
|
|
129
|
+
keys where identity must survive reordering or display-name changes.
|
|
130
|
+
|
|
131
|
+
**Migration:** releases before deterministic identity generated random node
|
|
132
|
+
IDs. The first deployment after upgrading will replace those old IDs once;
|
|
133
|
+
assign `node_key` to important nodes before that deployment if downstream
|
|
134
|
+
history or references need a deliberate stable identity.
|
|
135
|
+
|
|
136
|
+
### @task — Python Functions as Nodes
|
|
137
|
+
|
|
138
|
+
Real Python functions with full IDE support. Source code is extracted at deploy time.
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
@task("Compute Features", retries=2, timeout=120)
|
|
142
|
+
def compute(df):
|
|
143
|
+
import pandas as pd
|
|
144
|
+
df = pd.DataFrame(df)
|
|
145
|
+
df["score"] = df["revenue"] * 0.3 + df["usage"] * 0.7
|
|
146
|
+
return df.to_dict("records")
|
|
147
|
+
|
|
148
|
+
result = compute(input_data)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
- Functions receive `rows` (list of dicts) from upstream node
|
|
152
|
+
- Return a list of dicts (rows) or a pandas DataFrame
|
|
153
|
+
- Imports inside functions are fine — only needed on the server, not at deploy time
|
|
154
|
+
- Full pytest support — test functions locally before deploying
|
|
155
|
+
|
|
156
|
+
#### Module context — constants, helpers, and imports
|
|
157
|
+
|
|
158
|
+
By default (`package="auto"`), a task's deployed source isn't just its
|
|
159
|
+
isolated function body — any module-level constant or same-module helper
|
|
160
|
+
function it references is auto-detected (by inspecting the function's
|
|
161
|
+
bytecode) and included, and any module-level import it needs is re-emitted
|
|
162
|
+
too:
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
API_BASE = "https://api.example.com" # module-level constant
|
|
166
|
+
|
|
167
|
+
def _normalize(row, base): # module-level helper
|
|
168
|
+
row["base"] = base
|
|
169
|
+
return row
|
|
170
|
+
|
|
171
|
+
@task
|
|
172
|
+
def clean(rows):
|
|
173
|
+
return [_normalize(r, API_BASE) for r in rows]
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`clean`'s deployed package includes `API_BASE`, `_normalize`, and `clean`
|
|
177
|
+
itself. If a task references something that can't be safely auto-included
|
|
178
|
+
this way — an imported class instance, a bound method, or any other object
|
|
179
|
+
that isn't JSON-serializable data, a same-module function, or an imported
|
|
180
|
+
name — pipeline construction fails **locally**, naming exactly what's
|
|
181
|
+
missing, instead of deploying something that only breaks once it runs
|
|
182
|
+
remotely.
|
|
183
|
+
|
|
184
|
+
For cases auto-detection can't handle (e.g. a task that legitimately needs
|
|
185
|
+
a whole helper module), pass `package="module"` to skip auto-detection and
|
|
186
|
+
deploy the task's entire containing module verbatim instead. This is
|
|
187
|
+
broader and heavier than the default — the whole file ships, including any
|
|
188
|
+
unrelated top-level code in it — so treat it as an escape hatch, not the
|
|
189
|
+
default choice:
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
@task(package="module")
|
|
193
|
+
def clean(rows):
|
|
194
|
+
return heavy_helpers.transform(rows)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
> Deploying a task with a custom Python `runtime=`/`requirements=` or a
|
|
198
|
+
> container `image=` isn't supported yet — both need backend runtime/image
|
|
199
|
+
> dispatch that doesn't exist yet.
|
|
200
|
+
|
|
201
|
+
### Conditional branching
|
|
202
|
+
|
|
203
|
+
Use a runtime-supported condition expression and label each outgoing branch
|
|
204
|
+
explicitly. Conditional pipelines emit IR 2.1; ordinary pipelines remain on
|
|
205
|
+
IR 2.0.
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
gate = condition_node("Has rows?", expression="row_count > 100", input=data)
|
|
209
|
+
gate.when(sink_db("Production", table="output", conn_id="prod"))
|
|
210
|
+
gate.otherwise(sink_file("Quarantine", path="/tmp/quarantine.csv"))
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
`@condition` predicates are rejected until the runtime IR can distinguish a
|
|
214
|
+
predicate result from the unchanged branch payload. Nested conditional routing
|
|
215
|
+
is also rejected instead of compiling an ambiguous graph.
|
|
216
|
+
|
|
217
|
+
### Operators — Chaining & Fan-out
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
# Sequential
|
|
221
|
+
a >> b >> c
|
|
222
|
+
|
|
223
|
+
# Fan-out (one source, multiple destinations)
|
|
224
|
+
source >> [sink_db(...), sink_file(...), sink_api(...)]
|
|
225
|
+
|
|
226
|
+
# Fan-in (multiple sources converge)
|
|
227
|
+
a = source_db("A", ...)
|
|
228
|
+
b = source_db("B", ...)
|
|
229
|
+
joined = join("Merge", a, b, on="id")
|
|
230
|
+
|
|
231
|
+
# Fan-out then fan-in
|
|
232
|
+
source >> [branch_a, branch_b] >> merge >> output
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Typed References & Dynamic Expansion
|
|
236
|
+
|
|
237
|
+
> **Scope note:** everything in this section is SDK API surface and IR
|
|
238
|
+
> compilation only. There's no backend support yet for actually
|
|
239
|
+
> scheduling dynamic per-item task instances, combining dataset
|
|
240
|
+
> manifests, or running partition transforms — the primitives below let
|
|
241
|
+
> you author and validate the IR shape today; execution is
|
|
242
|
+
> physical-planner work that hasn't landed yet.
|
|
243
|
+
|
|
244
|
+
Node-building functions return one of five typed references (all are
|
|
245
|
+
`NodeRef` subclasses, so `>>`, fan-out/fan-in, and everything else above
|
|
246
|
+
keeps working unchanged):
|
|
247
|
+
|
|
248
|
+
- **`DatasetRef`** — a tabular dataset (rows). Returned by `source_db`,
|
|
249
|
+
`source_file`, `source_api(..., response="dataset")` (the default),
|
|
250
|
+
`transform`, `join`, `migrate`, `dbt`.
|
|
251
|
+
- **`ScalarRef`** — a single value. Returned by
|
|
252
|
+
`source_api(..., response="scalar")`.
|
|
253
|
+
- **`ArtifactRef`** — an opaque file/binary blob. Returned by
|
|
254
|
+
`source_api(..., response="artifact")`.
|
|
255
|
+
- **`CollectionRef`** — a dynamic collection of items whose size isn't
|
|
256
|
+
known until the pipeline runs (e.g. one entry per file/page). Not
|
|
257
|
+
returned by any built-in source yet — only produced by
|
|
258
|
+
`@task.expand()` below.
|
|
259
|
+
- **`ConditionRef`** — a condition node with `.when()` and `.otherwise()`
|
|
260
|
+
methods for explicit true/false routing.
|
|
261
|
+
|
|
262
|
+
Sinks, `quality_check`, `code`, and `notify` keep returning a plain
|
|
263
|
+
`NodeRef` — their output shape is either a
|
|
264
|
+
side-effect/gate or genuinely ambiguous (a `code` node can produce
|
|
265
|
+
anything), so they aren't force-fit into one of the typed kinds.
|
|
266
|
+
|
|
267
|
+
#### `.expand()` — dynamic fan-out
|
|
268
|
+
|
|
269
|
+
`a >> [b, c]` fans a node out to a fixed, Python-source-literal list of
|
|
270
|
+
destinations. `.expand()` does the same thing driven by *runtime* data —
|
|
271
|
+
one dynamic task instance per item of an upstream `CollectionRef` —
|
|
272
|
+
compiling to a **single** IR node with an `expansion` policy block, not N
|
|
273
|
+
static nodes:
|
|
274
|
+
|
|
275
|
+
```python
|
|
276
|
+
@task("Parse File")
|
|
277
|
+
def parse(rows):
|
|
278
|
+
...
|
|
279
|
+
|
|
280
|
+
# `files` is a CollectionRef (e.g. from a future paginated/listing source)
|
|
281
|
+
parsed = parse.expand(
|
|
282
|
+
file=files,
|
|
283
|
+
key=lambda f: f["path"], # per-item expansion identity
|
|
284
|
+
node_key="parse-files", # identity of the logical expand node
|
|
285
|
+
)
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
`key=` is optional and gives each dynamic instance a stable identity
|
|
289
|
+
across re-runs. It's never executed locally or turned into a runnable
|
|
290
|
+
script — only a name/description reference is recorded; the real per-item
|
|
291
|
+
keying happens server-side once backend support for dynamic instances
|
|
292
|
+
exists. `.expand()` returns a `CollectionRef` (the dynamic collection of
|
|
293
|
+
per-instance outputs) — chain `.collect(mode="union")` on it to merge
|
|
294
|
+
results back into one dataset.
|
|
295
|
+
|
|
296
|
+
`key=` and `node_key=` are distinct: `key=` derives each runtime item's
|
|
297
|
+
expansion identity, while `node_key=` identifies the single logical node in
|
|
298
|
+
the compiled graph.
|
|
299
|
+
|
|
300
|
+
For backward compatibility, a task parameter literally named `node_key`
|
|
301
|
+
still works: `parse.expand(node_key=files, other=metadata)` treats a
|
|
302
|
+
`CollectionRef` value as an expansion input and uses the decorator/default
|
|
303
|
+
logical identity. A string value, such as
|
|
304
|
+
`parse.expand(node_key="parse-files", file=files)`, is the logical node ID.
|
|
305
|
+
The per-item `key=callable` behavior is unchanged.
|
|
306
|
+
|
|
307
|
+
#### `union()` / `.collect(mode="union")` — combine into one dataset
|
|
308
|
+
|
|
309
|
+
Both compile to the same `union` IR node (capabilities `compute`,
|
|
310
|
+
`dataset-output`) — a dedicated dataset-manifest-combination node, not a
|
|
311
|
+
chain of individual merge edges:
|
|
312
|
+
|
|
313
|
+
```python
|
|
314
|
+
from brokoli import union
|
|
315
|
+
|
|
316
|
+
# Explicit refs known at authoring time
|
|
317
|
+
combined = union("Combine Pages", page_a, page_b, page_c)
|
|
318
|
+
|
|
319
|
+
# Equivalent, for a single upstream dynamic collection:
|
|
320
|
+
combined = parsed.collect(mode="union")
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
#### `DatasetRef.map()` / `.filter()` vs. `@map` / `@filter`
|
|
324
|
+
|
|
325
|
+
Two similar-looking styles exist on purpose, for different jobs — pick
|
|
326
|
+
based on whether you want something that actually runs today:
|
|
327
|
+
|
|
328
|
+
| | `@map` / `@filter` decorators | `DatasetRef.map()` / `.filter()` |
|
|
329
|
+
|---|---|---|
|
|
330
|
+
| Executes today? | **Yes** — generates a runnable script, registers a `code` node | **No** — IR only; the function is recorded as a name reference |
|
|
331
|
+
| Operates on | The whole node output at once (a list of row dicts) | Conceptually, one partition at a time (RFC §12.1) |
|
|
332
|
+
| Node type | `code` | `dataset_map` / `dataset_filter` |
|
|
333
|
+
|
|
334
|
+
```python
|
|
335
|
+
@map("Enrich") # runs today, whole-output
|
|
336
|
+
def enrich(row): ...
|
|
337
|
+
|
|
338
|
+
data.map(enrich_partition) # IR-only, per-partition (no backend support yet)
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Use `@map`/`@filter` for anything you need to actually run right now. Use
|
|
342
|
+
`DatasetRef.map()`/`.filter()` only when you're deliberately describing a
|
|
343
|
+
future per-partition transform ahead of backend support.
|
|
344
|
+
|
|
345
|
+
### Quality Rules
|
|
346
|
+
|
|
347
|
+
String-based quality rules — no boilerplate:
|
|
348
|
+
|
|
349
|
+
```python
|
|
350
|
+
quality_check("Validate", data, rules=[
|
|
351
|
+
"not_null(email)", # column must not have nulls
|
|
352
|
+
"unique(id)", # all values unique
|
|
353
|
+
"min(amount, 0)", # minimum value
|
|
354
|
+
"max(amount, 1000000)", # maximum value
|
|
355
|
+
"range(score, 0, 100)", # value range
|
|
356
|
+
"row_count(min=100)", # minimum row count
|
|
357
|
+
"row_count(min=1, max=10000)", # row count range
|
|
358
|
+
"regex(email, .*@.*\\..*)", # regex pattern match
|
|
359
|
+
"freshness(updated_at, max_hours=24)", # data freshness
|
|
360
|
+
])
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
## CLI
|
|
364
|
+
|
|
365
|
+
```bash
|
|
366
|
+
# Deploy pipeline to server (validates before pushing)
|
|
367
|
+
brokoli deploy pipeline.py --server http://localhost:9900 --api-key $API_KEY
|
|
368
|
+
|
|
369
|
+
# Deploy all pipelines in a directory
|
|
370
|
+
brokoli deploy pipelines/ --server http://localhost:9900
|
|
371
|
+
|
|
372
|
+
# Validate without deploying
|
|
373
|
+
brokoli validate pipeline.py --server http://localhost:9900
|
|
374
|
+
|
|
375
|
+
# Export pipeline as JSON (no server needed)
|
|
376
|
+
brokoli export pipeline.py -o pipeline.json
|
|
377
|
+
|
|
378
|
+
# Write a canonical normalized JSON comparison snapshot
|
|
379
|
+
brokoli compile pipeline.py --normalized > pipeline.snapshot.json
|
|
380
|
+
|
|
381
|
+
# Validate and normalize locally without printing IR or calling the server
|
|
382
|
+
brokoli compile pipeline.py --check
|
|
383
|
+
|
|
384
|
+
# Compare local pipeline semantics with deployed definitions
|
|
385
|
+
brokoli diff pipeline.py --server http://localhost:9900 --api-key $API_KEY
|
|
386
|
+
|
|
387
|
+
# Skip validation (not recommended)
|
|
388
|
+
brokoli deploy pipeline.py --skip-validation
|
|
389
|
+
|
|
390
|
+
# Trusted legacy servers without GET /api/capabilities only
|
|
391
|
+
brokoli deploy pipeline.py --allow-legacy-server
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
`deploy` and `validate` verify the target server's supported pipeline IR
|
|
395
|
+
versions before ordinary validation or persistence. Compatibility failures
|
|
396
|
+
block by default, including when `--skip-validation` is used. The
|
|
397
|
+
`--allow-legacy-server` escape hatch only permits a trusted server whose
|
|
398
|
+
capability endpoint is unavailable; it cannot override a version mismatch
|
|
399
|
+
reported by a reachable server.
|
|
400
|
+
|
|
401
|
+
Normalized snapshots are stable comparison artifacts, not deployment payloads.
|
|
402
|
+
They omit server metadata and layout and normalize only semantically unordered
|
|
403
|
+
values or equivalent defaults. Node, capability, tag, and `depends_on` order is
|
|
404
|
+
normalized; edge order is preserved because it can define input order.
|
|
405
|
+
`--normalized` always emits JSON and overrides the compile format; multiple
|
|
406
|
+
pipelines are emitted as one JSON array. Use the
|
|
407
|
+
ordinary deploy/export paths for wire output. Assign explicit `node_key` values
|
|
408
|
+
to important nodes for durable snapshots, especially where same-name node
|
|
409
|
+
insertion could renumber generated IDs.
|
|
410
|
+
|
|
411
|
+
`compile`, `diff`, and other file-based commands import pipeline modules.
|
|
412
|
+
Imports still execute module top-level code and its side effects.
|
|
413
|
+
|
|
414
|
+
### Validation
|
|
415
|
+
|
|
416
|
+
The SDK validates before deploying:
|
|
417
|
+
|
|
418
|
+
- Missing required fields (query, url, path, table, conn_id)
|
|
419
|
+
- Empty code nodes
|
|
420
|
+
- Missing quality check rules
|
|
421
|
+
- Missing join keys
|
|
422
|
+
- Disconnected nodes
|
|
423
|
+
- Referenced connections that don't exist on the server
|
|
424
|
+
|
|
425
|
+
```
|
|
426
|
+
$ brokoli deploy pipeline.py
|
|
427
|
+
Validating Revenue Pipeline...
|
|
428
|
+
✗ [ERROR] Write to DWH: Connection 'dwh_postgres' does not exist on the server
|
|
429
|
+
Deploy BLOCKED — fix 1 error(s) above
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Authentication
|
|
433
|
+
|
|
434
|
+
```bash
|
|
435
|
+
# Via CLI flag
|
|
436
|
+
brokoli deploy pipeline.py --api-key eyJhbG...
|
|
437
|
+
|
|
438
|
+
# Via environment variable
|
|
439
|
+
export BROKOLI_TOKEN=eyJhbG...
|
|
440
|
+
brokoli deploy pipeline.py
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
## Template Variables
|
|
444
|
+
|
|
445
|
+
Use Jinja-style templates in node configs — resolved at runtime:
|
|
446
|
+
|
|
447
|
+
```python
|
|
448
|
+
source_db("Extract", query="SELECT * FROM orders WHERE date = '{{ ds }}'")
|
|
449
|
+
source_api("Fetch", url="https://api.example.com/data?date={{ ds }}")
|
|
450
|
+
source_api("Auth API", headers={"Authorization": "Bearer {{ secret.api_key }}"})
|
|
451
|
+
sink_file("Save", path="/data/output_{{ ds }}.csv")
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
| Variable | Description |
|
|
455
|
+
|----------|-------------|
|
|
456
|
+
| `{{ ds }}` | Execution date (YYYY-MM-DD) |
|
|
457
|
+
| `{{ next_ds }}` | Next execution date |
|
|
458
|
+
| `{{ ts }}` | Execution timestamp (ISO 8601) |
|
|
459
|
+
| `{{ var.key }}` | Variable from Brokoli Variables store |
|
|
460
|
+
| `{{ secret.key }}` | Secret from environment (`BROKED_SECRET_*`) |
|
|
461
|
+
| `{{ param.key }}` | Runtime parameter |
|
|
462
|
+
|
|
463
|
+
## Examples
|
|
464
|
+
|
|
465
|
+
See [`examples/`](examples/) for runnable and architecture-reference pipelines:
|
|
466
|
+
|
|
467
|
+
| Example | Use Case |
|
|
468
|
+
|---------|----------|
|
|
469
|
+
| [Hello World](examples/01_hello_world.py) | Public API enrichment and CSV export |
|
|
470
|
+
| [API to Database](examples/02_api_to_database.py) | REST ingestion, validation, and warehouse load |
|
|
471
|
+
| [Join and Quality](examples/03_join_and_quality.py) | Multi-source join, quality gate, and fan-out |
|
|
472
|
+
| [dbt with Alerts](examples/04_dbt_with_alerts.py) | dbt orchestration with conditional notifications |
|
|
473
|
+
| [Custom Sensor Pipeline](examples/05_sensor_custom_pipeline.py) | Decorator-only ingestion with a file sensor |
|
|
474
|
+
|
|
475
|
+
## How It Works
|
|
476
|
+
|
|
477
|
+
1. You write Python pipelines using the SDK
|
|
478
|
+
2. `brokoli deploy` extracts function source code via `inspect.getsource()`
|
|
479
|
+
3. The SDK generates Brokoli pipeline JSON with auto-computed visual layout
|
|
480
|
+
4. Validates all nodes, edges, and server connections
|
|
481
|
+
5. Pushes to the Brokoli server via API
|
|
482
|
+
6. Pipeline appears in the visual editor — editable in both code and UI
|
|
483
|
+
|
|
484
|
+
```
|
|
485
|
+
Python SDK Brokoli Server Visual Editor
|
|
486
|
+
┌──────────┐ ┌──────────────┐ ┌──────────────┐
|
|
487
|
+
│ @task │ │ │ │ │
|
|
488
|
+
│ condition │──────▶│ Pipeline │──────▶│ Drag & Drop │
|
|
489
|
+
│ >> │deploy │ Engine │render │ Canvas │
|
|
490
|
+
│ quality_ │ │ + Profiling │ │ + Preview │
|
|
491
|
+
│ check() │ │ + Alerts │ │ + Profiling │
|
|
492
|
+
└──────────┘ └──────────────┘ └──────────────┘
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
## Requirements
|
|
496
|
+
|
|
497
|
+
- Python 3.9+
|
|
498
|
+
- Brokoli server running (for deploy)
|
|
499
|
+
- No external dependencies (stdlib only)
|
|
500
|
+
|
|
501
|
+
## License
|
|
502
|
+
|
|
503
|
+
Apache 2.0
|