API reference¶
adag ¶
adag: a cached, content-addressed DAG engine for Python data pipelines.
This module exports every public name of the package, including the lower-level pieces
(executors, dispatchers, ResolveContext, walk).
JsonValue ¶
A value json.dumps accepts: the type of Pipeline.metadata.
Sequence and Mapping (not list and dict) so a dict[str, int] is accepted under
strict type checking. Narrow it (isinstance) before you use a part of it.
CachedStep
dataclass
¶
Bases: Step[CT]
Deprecated: a Step with direct save/load/is_cached keyed on the step hash.
Source code in src/adag/base.py
output_path ¶
Return cache_dir/<step hash>, or None when an input is missing.
is_cached ¶
Return whether the output and its .done marker exist and validate passes.
Source code in src/adag/base.py
is_partial ¶
Return whether the output exists without its .done marker (a crash mid-save).
Source code in src/adag/base.py
save ¶
Write result as JSON to output_path, then the .done marker. Both atomic.
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
an input is missing, so the step has no hash. |
Source code in src/adag/base.py
load ¶
Read the result from output_path as JSON.
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
no output, or the step has no |
Source code in src/adag/base.py
validate ¶
Return whether path parses as the result type. An empty file is not valid.
Source code in src/adag/base.py
metadata ¶
BatchItem
dataclass
¶
A single work unit in a BatchStep.
key: cache key for this item (determines cache identity) data: arbitrary payload passed to process_item()
Source code in src/adag/batch.py
BatchStep
dataclass
¶
Bases: Step[T], Generic[T, R]
A step that processes items individually with per-item disk caching.
Subclasses implement three methods:
items(**inputs): declare work units and their cache keysprocess_item(item, **inputs): handle one itemassemble(results, **inputs): combine all results into final output
The framework automatically: - Partitions items into cached/uncached - Skips already-processed items - Saves each result to disk immediately after processing - Logs progress (cached/total, processing each item) - Only marks .done when ALL items are complete
On crash: restart picks up where it left off, only uncached items run.
Per-item cache key derivation. The framework auto-derives each
item's cache key from sha256(step_signature + inputs_signature +
item_data). The inputs signature includes every kwarg passed to
:meth:execute except those listed in :attr:items_source_keys.
Those are treated as containers of items rather than shared
parameters. Subclasses that take a large input list and extract items
from it should set items_source_keys to the kwarg name(s) so that
adding or removing items doesn't invalidate every other item's cache::
class Greet(BatchStep[Greetings, Greeting]):
items_source_keys = ("names",) # the list of items
# An input that is not listed here (a prompt, for example) is shared by
# every item, so a change to it invalidates all per-item caches.
Source code in src/adag/batch.py
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 | |
items_data ¶
Declare the per-item DATA only, framework derives the cache key.
Recommended override path. The default items() (below) calls
this and wraps each entry in a BatchItem with an auto-computed
key that includes the step's content_hash + the item's repr.
Returns the list of data objects (any type) that
process_item(item, ...) will receive as item.data.
Override this method when you don't need custom key topology. Then no cache-poisoning bug from manual key composition is possible.
Source code in src/adag/batch.py
items ¶
Declare the work units. Called once at the start.
Default implementation calls items_data(**inputs) and assigns
each item an auto-generated cache key derived from
(step.content_hash, **inputs, item_data). The inputs signature
is included so that step kwargs which AFFECT the per-item LLM call
(e.g. a notes payload passed alongside the report shortlists)
invalidate item caches when they change, even though they are
not step dataclass fields.
Override this only when you need custom key topology (e.g. deliberately sharing a cached result across two slightly-different inputs).
Source code in src/adag/batch.py
process_item
abstractmethod
¶
Process a single item. Default implementation called by process_batch().
Override this for simple one-at-a-time processing. Receives the BatchItem plus all loaded input kwargs.
Source code in src/adag/batch.py
process_batch ¶
Process a batch of uncached items, saving each result to cache.
Override this to control batching strategy (concurrent LLM calls, chunked API requests, etc.). The default processes one at a time.
Each result MUST be saved to cache immediately via cache.save()
so a crash loses at most one in-flight item.
Source code in src/adag/batch.py
assemble
abstractmethod
¶
Combine all per-item results into the final step result.
Called after all items (cached + freshly processed) are available. Receives the ordered list of results plus all loaded input kwargs.
Source code in src/adag/batch.py
item_cache_namespace ¶
Namespace for the ItemCache. Default: class name in snake_case.
Source code in src/adag/batch.py
run ¶
Raise: a BatchStep is driven through execute(), never run().
execute ¶
Orchestrate: items → partition → process uncached → assemble.
Called by StepResult.resolve with the current ResolveContext so the per-item cache can live under the pipeline's cache directory. Subclasses implement items/process_item/assemble, not this.
Source code in src/adag/batch.py
run_concurrent ¶
Drive a BatchStep outside a Pipeline.resolve() context.
Use this when another Step's run() body needs to invoke a
BatchStep over derived data (e.g. running Stage-2 classification
from inside a larger Step). This helper builds a minimal
ResolveContext rooted at cache_dir and dispatches through
:meth:execute, which guarantees:
- Item cache keys are auto-derived from
sha256(step.content_hash() + item.model_dump_json())[:16], so any change to step configuration (model, max_candidates, notes, etc.) OR to a single item's input invalidates that item's cache entry. No stale results. - Partition / process_batch / collect / assemble run in order.
DO NOT build BatchItem(key=..., data=...) manually and call
process_batch(items, cache, **inputs) directly. That bypasses
the framework's content-aware key derivation. Custom keys that
encode only part of the inputs (e.g. external_id but not the
actual data being sent to a model) silently return stale results
when the un-encoded inputs change. run_concurrent exists
specifically to make this footgun harder to find.
Source code in src/adag/batch.py
Config ¶
Bases: Generic[T]
Wrap execution-only parameters that do NOT affect the output hash.
Usage in step fields::
@dataclass(frozen=True)
class FindCandidates(Step[Candidates]):
model: str = "my-model" # hashed, affects output
concurrency: Config[int] = Config(20) # NOT hashed, execution only
batch_size: Config[int] = Config(256) # NOT hashed, execution only
tracker: Config[Tracker | None] = Config(None)
In run(), the value is unwrapped automatically::
def run(self, model: str, concurrency: int, batch_size: int) -> Candidates:
...
Generic over T so you can wrap anything an execution-time knob
can hold (primitives, objects, optional handles). The framework's
content_hash() skips Config fields entirely, so the wrapped
type's hashability doesn't matter.
Source code in src/adag/config.py
AsyncExecutor ¶
Bases: Protocol
The async counterpart of Executor: decides where a step runs on the ASYNC resolve path
(StepResult.aresolve / Pipeline.aexecute). Carried on ResolveContext.async_executor.
Mirrors Executor exactly except run is awaitable, so a step body that awaits I/O (a DB
session, an HTTP/LLM call) runs ON the event loop, no thread offload, so the event loop stays
responsive and cancellation propagates through the awaits. The default LocalAsyncExecutor runs the
step in-process; a remote async executor would dispatch by content hash and await the result.
Source code in src/adag/deferred.py
BranchDispatcher ¶
Bases: Protocol
Decides where the branches of one fan-out are resolved.
FanOutStep calls dispatch_all once per fan-out with every branch. A dispatcher
returns {branch.key: result}, or raises FanOutBranchError naming the failed keys.
A worker writes its result to the shared cache under branch.branch_hash, and the
dispatcher loads it from there. The default InlineDispatcher resolves in-process.
Source code in src/adag/deferred.py
BranchRef
dataclass
¶
Everything a worker needs to re-derive and resolve one fan-out branch.
The worker imports pipeline_module, finds the FanOutStep bound to
fan_out_var, looks up its own over[key] (so the value never has to
serialize), calls factory(key, value) to rebuild the branch sub-DAG (so
the closure never serializes either), and resolves it. branch_hash is the
branch StepResult's content hash: the cache key the worker writes to and
the main process polls/loads from. params are the run's InputParam
values, which the worker needs to resolve to the same hash.
Source code in src/adag/deferred.py
Deferred ¶
Bases: Generic[T]
A value of type T that doesn't exist yet. Resolved at execution time.
Class attribute
deferred_type: the concrete type T, auto-set when inheriting Deferred[ConcreteType].
Source code in src/adag/deferred.py
aresolve
async
¶
Async resolution. The default delegates to the synchronous resolve, correct for LEAF
Deferreds whose resolution does no awaitable work (InputParam reads context.params,
FileSource reads bytes). StepResult overrides this to await async step bodies and resolve
upstreams on the event loop, so an existing sync graph runs unchanged on the async path.
Source code in src/adag/deferred.py
Executor ¶
Bases: Protocol
Decides where a step runs. The graph stays the same; the executor is a
property of the run, carried on ResolveContext.
run receives the step, the active context, and the already-resolved input
values, and must return the step's result. The default LocalExecutor runs
it in-process. A remote executor would instead dispatch the step (by its
content hash) to another process or machine, wait for the result to land in
the shared cache, and return it, without the graph knowing the difference.
Source code in src/adag/deferred.py
InlineDispatcher ¶
Resolves each branch in-process, one after another. The default.
FanOutStep does not call it: it resolves inline branches itself, so it can honour
max_workers. dispatch_all is here so the class meets the protocol.
Source code in src/adag/deferred.py
dispatch ¶
Resolve one branch in-process. Kept from the pre-0.2.11 one-branch protocol.
InputParam
dataclass
¶
Bases: Deferred[T]
A named runtime parameter. Use with_default() for optional parameters.
value_type tells the CLI how to parse --param. It is needed on a required parameter,
because InputParam[int]("x") loses the int at runtime. It is not part of the cache key.
Source code in src/adag/deferred.py
with_default
staticmethod
¶
Create a parameter with a default value (even if the default is None).
Source code in src/adag/deferred.py
LocalAsyncExecutor ¶
Awaits each step's async body in-process, the default async executor. The mirror of
LocalExecutor for the aresolve path.
Source code in src/adag/deferred.py
LocalExecutor ¶
NullObserver ¶
Silent observer: every hook is a no-op. Use it to turn the per-step logging off. Concrete observers inherit this and override only the hooks they care about, so new hooks never break an existing observer.
Source code in src/adag/deferred.py
PipelineObserver ¶
Bases: Protocol
Hooks a pipeline run calls so you can watch it: logs, timing, progress.
StepResult.resolve calls on_step_start before it resolves a step's inputs, and
on_step_cached or on_step_finished after. A child's hooks fall between its parent's
start and finish. RunTreeObserver builds its tree from that order.
Subclass NullObserver and override the hooks you need. For a value of your own (a score,
a vector), declare a typed hook: see "Typed hooks for your own values" in docs/observers.md.
Source code in src/adag/deferred.py
Progress
dataclass
¶
ResolveContext
dataclass
¶
The state of one run: where results are cached, the params, the observer and the memo.
Build one per run and pass it to every resolve, aresolve or aresolve_all call.
cache_dir: files for step results.storereplaces it with anyCacheStore. With neither, nothing is cached.params: theInputParamvalues of the run.observer:NullObserverby default here.Pipeline.executepassesLoggingObserverunless you give another.- The memo: each node resolves once per context, so every downstream step gets the same object. Threads or tasks that reach one node at the same time run its body once.
Source code in src/adag/deferred.py
result_store ¶
Where step results are cached: store, else files under cache_dir, else None.
Source code in src/adag/deferred.py
AdagDeprecationWarning ¶
DirSource
dataclass
¶
Bases: Deferred[str]
A folder that acts as a leaf node in the step graph.
Hashed by the SHA-256 of a deterministic stream of
(relative_path, file_hash) pairs across every file matching
pattern inside the folder. If the folder doesn't
exist, the hash returns None, blocking any downstream step that
depends on it.
pattern defaults to "*.jsonl", which is not recursive. Pass another glob (for
example "**/*.json") for nested folders.
Source code in src/adag/dir_source.py
content_hash ¶
SHA-256 over (relative_path, file_hash) for each file.
First 12 hex chars to match FileSource. Returns None if
the directory doesn't exist (treats as blocked).
Source code in src/adag/dir_source.py
BranchPolicy
dataclass
¶
How hard to try a branch and how patiently to wait for it.
timeout_seconds counts from when the job is RUNNING. queue_timeout_seconds counts
from submit while the job is PENDING; a batch queue can hold a job for hours.
result_grace_seconds is how long to wait for the result to appear after a job
SUCCEEDED, because a network filesystem can show a new file late.
Source code in src/adag/dispatch.py
SubprocessDispatcher ¶
Bases: RemoteDispatcher
Resolve each branch in a local OS process via adag resolve-branch.
RemoteDispatcher with LocalJobBackend. Runs at most max_concurrent processes at
once (default: the number of CPUs). Workers share results through cache_dir, or the
store at store_url. The pipeline must have one of them.
Source code in src/adag/dispatch.py
FanOutBranchError ¶
Bases: RuntimeError
One or more distributed fan-out branches failed.
Succeeded branches' results are already in the shared cache (written by their workers, keyed by content hash), so a rerun re-dispatches only the failed keys.
Source code in src/adag/fan_out.py
FanOutStep
dataclass
¶
Bases: Step[dict[K, V]], Generic[K, V]
Apply a sub-pipeline factory to each value in over; collect into a dict.
Fields
over: dict whose keys identify each fan-out branch and whose values
are passed verbatim to factory for that branch.
factory: callable (key, value) -> StepResult[V] building the
sub-DAG for one branch. Called once per key during execute;
each returned StepResult is resolved under the same
ResolveContext as the parent so caches are shared.
max_workers: run branches in-process on this many threads. Ignored by a dispatcher.
instance_label: overrides the visualization label.
pipeline_module: file path or module name a dispatch worker imports.
fan_out_var: module-level variable that holds this fan-out's StepResult.
Source code in src/adag/fan_out.py
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 | |
execute ¶
Resolve one sub-DAG per key in over.
In-process (InlineDispatcher, the default) each branch resolves under the parent
context: branch-by-branch when max_workers is None (identical to the pre-dispatch
behaviour) or concurrently in a thread pool when it is set. With a real dispatcher,
branches are dispatched to workers and rejoined through the shared cache by content hash.
Source code in src/adag/fan_out.py
aexecute
async
¶
Async entry: run execute in a worker thread so the event loop is not blocked.
Branches resolve on the sync path inside that thread, the same as under execute.
asyncio.to_thread copies the current context, so the observer reaches the branches.
Source code in src/adag/fan_out.py
content_hash ¶
Identity = class + each branch's sub-DAG content_hash.
factory itself can't be hashed (closures aren't stably
hashable), so we materialize each branch once and fold the
resulting StepResult.content_hash() into our own digest.
Keys are included so reordering / renaming branches invalidates
the cache; values only contribute via the sub-DAG hash they
produce. That is what matters for correctness.
Returns None when a branch's hash is blocked (e.g. unsatisfied upstream FileSource), so the parent pipeline behaves like any other unsatisfied step.
Source code in src/adag/fan_out.py
UnhashableValueError ¶
FileSource
dataclass
¶
Bases: Deferred[str]
Reads the file's full text content into a single string.
Source code in src/adag/io.py
JSONLSource
dataclass
¶
Bases: Deferred[list[T]], Generic[T]
Reads a JSONL file and validates each non-blank line against record_type.
record_type is required (not optional). The Pydantic class is what
gives this leaf its precise output type. Making it optional would force
a list[T] | list[Any] return signature and erase the typing benefit.
If you want untyped lines, use LinesSource and call json.loads in
a downstream Step.
Usage::
source = JSONLSource(path="data/reports.jsonl", record_type=ReportEntry)
records: list[ReportEntry] = source.resolve(context)
A line that fails to parse or validate raises ValueError with the file
path and line number: the original error is chained via raise … from.
Blank lines are skipped silently.
Source code in src/adag/io.py
LinesSource
dataclass
¶
Bases: Deferred[list[str]]
Reads the file as a list of lines (raw, line endings stripped).
Blank lines are preserved: this is the literal contents of the file split
on newlines, equivalent to Path(path).read_text().splitlines(). Use
JSONLSource if you want blanks skipped and each line parsed.
Source code in src/adag/io.py
ItemCache ¶
Bases: Generic[R]
Per-item cache for expensive batch operations.
Items are stored as individual JSON values keyed by a content hash. This survives parameter changes: only the item's own identity matters.
Usage::
cache = ItemCache[NoteSchema](cache_dir, "note_generation", NoteSchema)
keys = [cache.key(item.code, style_hash, model) for item in items]
cached, uncached_indices = cache.partition(keys)
for index in uncached_indices:
result = expensive_llm_call(items[index])
cache.save(keys[index], result)
all_results = cache.collect(keys)
Pass store= to back the cache with something other than the local filesystem (e.g. a
RedisCacheStore for a deployed multi-instance service). Default = FileSystemCacheStore rooted at
<cache_dir>/.items (the historical layout).
Source code in src/adag/item_cache.py
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 | |
key ¶
exists ¶
list_keys ¶
mark_inflight ¶
Record that processing for item_key has started.
Writes a <key>.inflight sentinel. If the worker crashes before
save(), this sentinel persists and can be surfaced on next
resume to flag work-in-progress items that were lost. Call
clear_inflight() after a successful save.
Source code in src/adag/item_cache.py
clear_inflight ¶
inflight_keys ¶
All currently-marked in-flight item keys (typically from a crashed run).
Source code in src/adag/item_cache.py
save ¶
Save a single item result with a schema-version stamp.
Stamping the schema version on every write lets a future
load() detect when the cached payload was produced under a
different schema and refuse to deserialize it. Old, pre-stamp
caches still load via the legacy path below for backwards
compatibility.
Source code in src/adag/item_cache.py
load ¶
Load a single item result, honoring the schema-version stamp.
Three on-disk shapes are accepted:
- Stamped (current):
{"_schema_version": "...", "_data": {...}}→ version-checked. Mismatch raisesValueError;partition()treats this as a cache miss and re-processes. - Legacy (pre-stamp): top-level Pydantic JSON
→ loaded via
model_validate; surfaces as a "stale" miss only if the schema fields no longer match. - Corrupt: invalid JSON or schema mismatch → raises;
partition()catches this and treats as a miss.
Source code in src/adag/item_cache.py
partition ¶
Split keys into cached results and uncached indices.
If a cached item fails validation (e.g. schema changed), it is treated as a cache miss and will be re-processed. Any in-flight markers from a prior crashed run are also reported.
Source code in src/adag/item_cache.py
collect ¶
Load all items by keys.
Default behavior: raises ValueError if any key is missing, the
downstream step probably can't proceed without a complete result set.
With allow_missing=True: silently skip missing keys and return
whatever was actually cached. Used by batch steps that tolerate
partial failure (e.g. LLM calls where a small percentage may
legitimately fail due to rate limits / transient errors, but the
successful majority is still worth keeping).
Source code in src/adag/item_cache.py
ComponentKind ¶
Bases: StrEnum
The kind of one input to a cache key.
Source code in src/adag/key.py
IDENTITY
class-attribute
instance-attribute
¶
The step's class name or kind; for a node, the step's own key.
PINNED
class-attribute
instance-attribute
¶
Step.cache_key is set. It replaces identity, code and fields.
FILE
class-attribute
instance-attribute
¶
A FileSource field, hashed by the file's content.
NESTED_STEP
class-attribute
instance-attribute
¶
A field that holds another step, hashed by that step's key.
UPSTREAM
class-attribute
instance-attribute
¶
An input wired to another node, hashed by that node's key.
PARAM
class-attribute
instance-attribute
¶
An input wired to an InputParam, hashed by the run's value or the default.
DEFERRED
class-attribute
instance-attribute
¶
An input wired to another source, such as a FileSource.
SKIPPED
class-attribute
instance-attribute
¶
Not part of the key: a Config field, node_label, or a value that cannot be
hashed when strict_content_hash = False.
KeyComponent
dataclass
¶
One input to a cache key.
digest is the first 12 hex characters of sha256 over the bytes this component adds to the
key. Two components with the same (kind, name) and the same digest added the same bytes.
It is empty for a SKIPPED component and for one that blocked the key.
Source code in src/adag/key.py
build
staticmethod
¶
Build a component from one _key_chunks tuple.
Source code in src/adag/key.py
KeyExplanation
dataclass
¶
Every component of one node's cache key, in the order adag hashes them.
key is None when a component blocked it (a missing file, an upstream with no key);
blocked_by then names that component. An UPSTREAM component shows the upstream step
and its key: explain that node next to follow a change up the graph.
Source code in src/adag/key.py
redacted ¶
A copy with the value of every SKIPPED component removed.
A skipped component is not in the key, and its value can be a client, a path or a
secret (a Config field). The key record is written to disk or to a shared store, so
it keeps only the name.
Source code in src/adag/key.py
to_dict ¶
from_dict
staticmethod
¶
Rebuild an explanation from to_dict output.
Source code in src/adag/key.py
LoggingObserver ¶
Bases: NullObserver
The default observer (installed by Pipeline.execute): logs each step's
lifecycle as a structlog event.
This is the single place per-step logging happens. resolve emits the
events and this turns them into log lines. To make one event louder or quieter,
change its level here (one place). For full silence, pass NullObserver.
Source code in src/adag/observers.py
RunTreeObserver ¶
Bases: NullObserver
Builds an indented tree of a single pipeline run, annotated with timing and cache-hit markers.
Render it with render_tree() after execute(). Use one observer per execute(): it
is not reusable across runs. It is safe under threads: per-thread stacks keep each worker's
bracketing consistent, and roots merge under a lock. Under parallel=True the tree is flat,
because level-scheduled execution has no recursion to nest.
Source code in src/adag/observers.py
render_tree ¶
Render the collected run as an indented tree string.
CyclicPipelineError ¶
Bases: ValueError
Raised at construction when the Deferred graph contains a cycle (a step transitively depends on itself).
Pipeline ¶
Bases: Generic[Out]
Wraps a Deferred graph. Discovers steps and parameters, provides execute().
tokenizer = Tokenizer(max_length=256) text = InputParam("text") tokens = tokenizer(text=text) pipeline = Pipeline(tokens, cache_dir=Path("cache")) result = pipeline.execute(text="hello world")
With neither cache_dir nor store (the default), nothing is cached and every
execute() runs the full graph. Pass store to cache in a CacheStore instead of
files under cache_dir.
Source code in src/adag/pipeline.py
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 | |
targets
property
¶
All terminal / target nodes the pipeline was built to resolve.
steps
property
¶
All steps in dependency (topological) order.
Every step appears after the steps it depends on. The post-order discovery walk in
_discover guarantees it.
input_names
property
¶
Names of every InputParam this pipeline expects from execute(**kwargs).
Public alternative to reaching into _inputs from outside the package. Callers that
branch on "does this pipeline accept a threshold kwarg" should use this, so the
private list shape stays free to evolve.
inputs
property
¶
Every InputParam this pipeline expects, in discovery order.
Public accessor for callers (e.g. the CLI) that need each param's name,
type, and default (not just the names from input_names) to build
help text or coerce string arguments.
__init__ ¶
__init__(
output: StepResult[Out],
cache_dir: Path | None = None,
on_progress: Callable[[Progress], None] | None = None,
*,
kind: str | None = None,
extra_targets: list[StepResult[Any]] | None = None,
store: CacheStore | None = None,
metadata: JsonValue = None,
) -> None
Wrap a Deferred graph.
| PARAMETER | DESCRIPTION |
|---|---|
output
|
the canonical terminal node of the DAG.
TYPE:
|
cache_dir
|
directory for step results. With neither
TYPE:
|
store
|
a
TYPE:
|
on_progress
|
optional progress callback (passed to
TYPE:
|
kind
|
a tag for the pipeline, for example
TYPE:
|
metadata
|
caller data saved with the pipeline: any JSON value (a string, a number, a
list, a dict).
TYPE:
|
extra_targets
|
additional terminal or intermediate nodes whose
sub-DAGs should be discovered too. Use this when the
pipeline has multiple "ending points" so visualization and
TYPE:
|
Source code in src/adag/pipeline.py
from_targets
classmethod
¶
from_targets(
targets: list[StepResult[Any]],
cache_dir: Path | None = None,
on_progress: Callable[[Progress], None] | None = None,
*,
kind: str | None = None,
store: CacheStore | None = None,
metadata: JsonValue = None,
) -> Pipeline[Any]
Build a Pipeline that covers multiple terminal nodes.
Real DAGs often have more than one ending point (e.g. the main
result plus a separate audit / dedup artifact). Pass them all
here and the Pipeline discovers the union of their sub-graphs.
The first target is treated as the canonical output so a
zero-arg execute() still has a single thing to return.
Downstream callers use resolve(target) to pick any of them.
Source code in src/adag/pipeline.py
topological_order ¶
Steps in a guaranteed topological order (each step after its dependencies).
A named contract (delegates to steps). Consumers that depend on the ordering
(deterministic execution, left-to-right layered layout, level grouping) rely on this
rather than on the steps discovery detail.
Source code in src/adag/pipeline.py
topological_levels ¶
Steps grouped into dependency levels: every step lands one level past its deepest dependency. Steps within a level are mutually independent, so a concurrent executor can run a whole level in parallel; level i+1 starts once level i is resolved.
Pure analysis: no execution change. The foundation for opt-in parallel execution (run each level's steps together) and for correct left-to-right layered layout.
Source code in src/adag/pipeline.py
explain_keys ¶
Explain the cache key of every node, upstream nodes first.
| PARAMETER | DESCRIPTION |
|---|---|
params
|
the run's params, as passed to
TYPE:
|
Source code in src/adag/pipeline.py
validate ¶
Validate the graph before execute(): every step's required run() input is bound.
Cycles are already rejected at construction (see _discover). This adds the
missing-input check: a required parameter of a step's run() that is neither a
wired source nor one of the step's dataclass fields would otherwise fail late at
execute() as a bare TypeError: run() missing 1 required argument. Opt-in
(not run at construction) so a consumer building a graph from dynamic/untrusted
input can validate it up front and reject with a 4xx instead of crashing
mid-resolution. Raises UnboundInputError naming the step and the unbound params.
Walks the target StepResults directly (visited by id, the same node identity
resolve() uses) rather than caching a derived step->sources map.
Source code in src/adag/pipeline.py
step_name ¶
Directory name for a step: {index}_{name}, falling back to the lowercased class name.
Source code in src/adag/pipeline.py
item_cache ¶
Create an ItemCache in this pipeline's store, or under its cache directory.
Source code in src/adag/pipeline.py
execute ¶
execute(
*,
observer: PipelineObserver | None = None,
executor: Executor | None = None,
dispatcher: BranchDispatcher | None = None,
parallel: bool = False,
max_workers: int | None = None,
**params: Any,
) -> Out
Run the graph and return the result of output.
| PARAMETER | DESCRIPTION |
|---|---|
observer
|
receives the run's hooks. Default:
TYPE:
|
executor
|
where each step runs. Default:
TYPE:
|
dispatcher
|
where fan-out branches run. Default:
TYPE:
|
parallel
|
run the independent steps of each level on a thread pool. Use it for steps
that block on I/O.
TYPE:
|
max_workers
|
thread pool size for
TYPE:
|
**params
|
one value per
TYPE:
|
The five keyword names above are reserved: do not use them as InputParam names.
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
a required |
Source code in src/adag/pipeline.py
aexecute
async
¶
aexecute(
*,
observer: PipelineObserver | None = None,
async_executor: AsyncExecutor | None = None,
**params: Any,
) -> Out
Run the graph on the running event loop and return the result of output.
AsyncStep bodies are awaited on the loop. Sync step bodies also run on the loop and block
it while they run. One step body runs at a time (aresolve_all with max_concurrency=1),
so a shared resource such as one database session is never used by two steps at once.
Cancelling the task starts no further step.
observer and async_executor are reserved: do not use them as InputParam names.
There is no dispatcher: fan-out branches run in this process.
Source code in src/adag/pipeline.py
resolve ¶
resolve(
target: StepResult[R],
*,
observer: PipelineObserver | None = None,
executor: Executor | None = None,
dispatcher: BranchDispatcher | None = None,
**params: Any,
) -> R
Resolve one node of this pipeline and return its result, from the cache when it is there.
Use it to read an intermediate result after execute(). Do not call step.run() for
that: it skips the cache and can return a value that differs from the one downstream steps
used.
| PARAMETER | DESCRIPTION |
|---|---|
target
|
a node of this pipeline (the object you wired).
TYPE:
|
**params
|
the
TYPE:
|
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
|
Source code in src/adag/pipeline.py
save ¶
Save all steps and the pipeline graph to a directory.
Source code in src/adag/pipeline.py
from_directory
classmethod
¶
from_directory(
path: str | Path,
step_registry: dict[str, type[Step[Any]]],
*,
cache_dir: Path | None = None,
) -> Pipeline
Reconstruct a saved Pipeline from disk.
Reads pipeline.json, loads each step via its class in step_registry (keyed by kind),
replays the edge list to rebuild the DAG, and wraps the terminal StepResult.
cache_dir defaults to None: loaded pipelines do not write step results back to disk. Pass cache_dir=Path(path) (or another writable location) when you want the cross-run cache for training or notebook iteration.
Source code in src/adag/pipeline.py
status ¶
Log status of all steps.
Source code in src/adag/pipeline.py
UnboundInputError ¶
Bases: ValueError
Raised by Pipeline.validate() when a step has a required run() input that is neither wired nor a dataclass field.
SerializableStep
dataclass
¶
Bases: Step[Out]
A Step that Pipeline.save writes to disk and Pipeline.from_directory rebuilds.
The contract a subclass implements:
kind: a non-empty class attribute, for example"my-tokenizer/v1". The registry passed toPipeline.from_directorymaps it back to the class. Raised at class definition if missing.spec: an optional dataclass field holding aBaseModel.save_artifactswrites it tostep.json.from_directory(cls, path): a classmethod that returns the step saved atpath. The default rebuilds a step whose fields arespecplus fields with defaults. A step with any other required field must override it, usually withcls(spec=cls.load_spec(path, Spec)). Saving a step that cannot be rebuilt raisesTypeErrorat save time, not at load time.save_artifacts(self, path): override only to write extra state (weights, tokenizer files) next tostep.json. Callsuper().save_artifacts(path)first.
Source code in src/adag/serializable.py
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | |
__init_subclass__ ¶
Enforce at class definition time that kind is set and spec is a BaseModel.
Source code in src/adag/serializable.py
save_artifacts ¶
Write step.json with kind and spec (if a spec field exists). Override to add state.
Atomic via atomic_write_text so a crash during Pipeline.save
leaves either the previous step.json intact or the complete new
one: readers (from_directory, load_spec) never see a partial
JSON.
Source code in src/adag/serializable.py
from_directory
classmethod
¶
Return the step saved at path. Override when the default cannot rebuild it.
The default reads step.json, checks kind, and passes spec (when the class has
that field) to the constructor. Every other field keeps its default.
Source code in src/adag/serializable.py
load_spec
classmethod
¶
Read step.json, assert kind matches, and return the validated spec.
Source code in src/adag/serializable.py
assert_kind
classmethod
¶
Read step.json and assert the kind matches. For steps without a spec.
Source code in src/adag/serializable.py
WriteJSONL
dataclass
¶
Bases: Step[str]
Write the single list[BaseModel] field of a Pydantic result as JSONL.
Reads the upstream Pydantic model, finds its lone list[BaseModel]
field, and writes one record per line to path. Returns path
so callers can chain a downstream FileSource if they want.
source is typed BaseModel: StepResult._validate is
Liskov-aware (accepts any subclass), so a concrete Pydantic result
type wires in directly without dropping to Any at the call site.
Raises TypeError if the upstream result has zero or more than one
list[BaseModel] fields.
Source code in src/adag/sinks.py
AsyncStep
dataclass
¶
Bases: Step[Out]
A step whose body awaits I/O (a DB session, an HTTP/LLM call). Implement arun (async) instead of
run (sync), and resolve it through Pipeline.aexecute / StepResult.aresolve.
An AsyncStep has no synchronous body: it does not override run, so the synchronous path
(execute/resolve) hits Step.run's default, which raises a clear message: a sync
resolve cannot await an async body. Sync steps are unaffected: their inherited arun just
runs run, so a graph can freely MIX sync and async steps and resolve the whole thing with
aexecute (each sync step runs inline, each async step is awaited).
Per-request resources (e.g. a SQLAlchemy AsyncSession) flow in like any other input: wire an
InputParam and pass it to aexecute. Nothing special is needed on the context.
Source code in src/adag/step.py
arun
abstractmethod
async
¶
Step
dataclass
¶
Bases: Generic[Out]
Base class of every step: a frozen dataclass with config fields and a run() method.
Fields are part of the cache key, except Config fields. adag passes every field to
run() by name, together with the wired inputs.
Class attributes
kind: a stable name for the step. It replaces the class name in the key and in
step.json.
label, description: text for the graph drawing.
result_type: the BaseModel the result is cached as. Inferred from Step[X].
cache_key: a fixed key that replaces the computed hash.
strict_content_hash: set to False to skip fields the key cannot encode.
pure: False records that a rerun would not give the same value (a model call, a
clock read). adag still caches the step and never reads pure.
timeout_s: seconds allowed for the async body under aexecute or aresolve. A longer
run raises StepTimeout. execute() ignores it, and it cannot stop a blocking
sync body. Use a SubprocessDispatcher when you need a hard limit.
produces, accepts: optional port names for graph editors. See accepts_from.
Equality is by identity (eq=False), which FanOutStep relies on. A subclass declared
with @dataclass(frozen=True) compares by fields.
Source code in src/adag/step.py
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 | |
result_model ¶
The concrete pydantic model the result is cached as (JSON round-trip). Defaults to the class
result_type inferred from Step[X]; override when the result type is generic (e.g. Dataset[T])
so the parametrized model is known at cache-load time, not the unparametrized base.
Source code in src/adag/step.py
output_type ¶
The deferred output type used for wiring / connection checks. Defaults to the class return_type;
override alongside result_model when the result type is generic.
Source code in src/adag/step.py
save_result ¶
Write result to path. Override with load_result for a non-JSON result.
path does not exist yet. Create it as a file or as a directory (a directory holds a
result plus any sidecar files). adag passes a temporary path and renames it into place
when this returns, so a crash never leaves a half-written result. The default writes the
BaseModel result as JSON.
Source code in src/adag/step.py
load_result ¶
Read the result that save_result wrote at path.
Raise any exception when the entry is not valid: adag treats it as a cache miss and runs
the step again. The default reads JSON with result_model().
Source code in src/adag/step.py
caches_to_path
classmethod
¶
Return whether this step overrides save_result/load_result.
Such a step is cached under cache_dir as a file or directory, never in a store.
Source code in src/adag/step.py
accepts_from ¶
Return whether this step can consume producer's output port.
producer is a step (its produces is read) or a port name. Returns True when
either side declares no port; otherwise producer.produces must be in accepts.
Source code in src/adag/step.py
__call__ ¶
Wire this step into the DAG. Returns a StepResult that chains downstream.
Warns AdagDeprecationWarning when an input has the name of a field that holds a Step:
the field copy is hashed but never run, and the input value is what run() gets. The
first release documented this pattern, so it still works. It becomes a TypeError in
adag 1.0. Wire the upstream step once, as an input.
| RAISES | DESCRIPTION |
|---|---|
TypeError
|
a field holds a |
Source code in src/adag/step.py
run ¶
Execute the step's synchronous logic; receives resolved inputs as kwargs.
Override in a synchronous step. Deliberately NOT @abstractmethod: an
AsyncStep (implements arun) and a FanOutStep (driven via execute)
have no synchronous run, and marking it abstract would make them
un-instantiable. The default raise instead guards the real misuse cases, a sync
step that forgot run, or a sync resolve of an async step.
Source code in src/adag/step.py
execute ¶
Framework entry point. Override if the step needs per-call state
(e.g. BatchStep needs context.cache_dir for its ItemCache). Most
steps don't: the default binds the run's observer as the ambient
current_observer() (so step internals, incl. worker threads and
asyncio tasks, can emit observer hooks) and delegates to run().
Source code in src/adag/step.py
arun
async
¶
Async step body. Default delegates to the sync run so a sync step works unchanged on the
async path; a step that awaits I/O subclasses AsyncStep and overrides this.
Source code in src/adag/step.py
aexecute
async
¶
Async framework entry point: the awaitable mirror of execute. Override for per-call state;
the default delegates to arun.
Source code in src/adag/step.py
content_hash ¶
Return the step's own hash (class, code and fields), or None when a field blocks it.
Identity is kind when set, else the class name. The class source is hashed in both cases,
so a code edit changes the hash. Returns cache_key unchanged when the class sets it.
Configfields: skipped.FileSourcefields: hashed by file content. A missing file blocks the hash.- A field that holds a
Step: hashed by that step's hash. - Other values: encoded by
adag.hashing.encode_value.
| RAISES | DESCRIPTION |
|---|---|
UnhashableValueError
|
a field has no stable encoding and |
Source code in src/adag/step.py
save_artifacts ¶
Write step.json with the step's kind and serialized config fields.
Default implementation captures the dataclass fields that _serialize_step_field
recognises (scalars, Path, UUID, Enum, BaseModel, composites thereof).
Fields representing runtime state (Step instances, tokenizers, HF clients,
model handles) are skipped: those subclasses override this method, call
super().save_artifacts(path), then write their own state files
(safetensors, tokenizer files, etc.) next to step.json.
SerializableStep overrides to keep the legacy data["spec"] = model_dump
format that load_spec() expects; the formats stay distinct because
flattening would break round-trip for every existing saved pipeline.
Source code in src/adag/step.py
load_artifacts ¶
Load step-specific artifacts from a directory.
Override in steps that carry heavy state (neural network modules, etc.).
StepResult ¶
Bases: Deferred[Out]
Deferred result of a Step. Handles caching, upstream resolution, and type validation.
Source code in src/adag/step.py
530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 | |
resolve ¶
Resolve upstream sources, run the step, cache if applicable.
Memoizes by id(self) so multiple downstream consumers of the SAME StepResult
share one computed result within a Pipeline.execute. This is critical for non-cached
steps returning non-serializable objects (e.g. model handles), where consumers must
see the same instance, not two re-executions. The cache/memo/observer skeleton lives
in _check_cache/_finalize (shared with aresolve); only the two lines that
genuinely differ (upstream resolution and the step body) stay here, run inline.
In-flight dedup makes that guarantee hold under CONCURRENCY too: the memo only
collapses resolutions that do not overlap in time, so N branch workers reaching one
shared upstream at once would each miss the memo and run the body N times. Here the
first thread to miss the memo claims ownership (an Event in context._inflight);
concurrent threads find that Event, wait on it, then loop: a success left the value in
the memo (returned), a failure removed the marker so they re-claim and resolve it
themselves. Uncontended (single-threaded) there is never a waiter, so behaviour and
cost are unchanged bar creating one Event that is set immediately.
Source code in src/adag/step.py
aresolve
async
¶
Resolve this node on the event loop and return its result. Public API.
Use it to schedule async steps yourself, for example one asyncio.gather per
topological level inside an async request handler. Pass the same ResolveContext to
every call: it memoizes each node, so a shared upstream runs once and a later call reuses
earlier results. Upstream nodes are awaited, async step bodies run on the loop, and sync
bodies run unchanged. A cancelled task starts no further step.
Gathered calls that reach the same node at the same time run its body once: the first task claims the node and the others wait for it. If that body raises, a waiting task resolves the node itself.
Source code in src/adag/step.py
content_hash ¶
Content hash: step identity + source hashes (memoized per params identity).
Source code in src/adag/step.py
explain_key ¶
List every component of this node's cache key.
Use it when a node recomputes and you do not know why. The components are the step's
identity, code digest and fields, then one per input. The key is built from the same
components, so explain_key(p).key == content_hash(p).
| PARAMETER | DESCRIPTION |
|---|---|
params
|
the run's params, as passed to
TYPE:
|
Source code in src/adag/step.py
is_cached ¶
Return True when a complete, valid result for this node is in where.
where is a cache directory or a CacheStore. For a step that overrides
save_result, where must be a directory, and the check calls load_result: an
entry that load_result rejects is not cached, the same answer the resolver gives.
This makes the check as costly as one load for such a step.
Source code in src/adag/step.py
load ¶
Load the cached result from a cache directory or a CacheStore.
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
no result is stored for this node. |
Source code in src/adag/step.py
StepTimeout ¶
Bases: TimeoutError
Raised when a step's async body exceeds its timeout_s budget. Subclasses the builtin
TimeoutError so callers can catch either.
SVG ¶
Wrapper around an SVG string that Jupyter renders as an image.
Source code in src/adag/visualization.py
Edge
dataclass
¶
Node
dataclass
¶
A node in the walked graph.
name is the node's unique KEY (edges reference it, layout positions by it). title, when set, is the
human display label; empty → the renderer derives one from name. A Step may expose a per-INSTANCE label
via a node_label attribute (walk() then uses it as both name and title; the name gets a suffix on collision) so
two instances of the SAME Step class render as two distinct boxes instead of collapsing into one.
Source code in src/adag/visualization.py
NodeMetadata
dataclass
¶
Pre-extracted display info for a graph node, decoupled from Step introspection.
Source code in src/adag/visualization.py
SvgOptions
dataclass
¶
What the renderer emits.
include_styles=False leaves the SVG unpainted apart from its classes,
for a host that supplies the whole stylesheet. show_params=False drops
the per-step parameter lines, which also shrinks the boxes: the layout is
computed from the same node data that gets drawn.
Source code in src/adag/visualization.py
current_observer ¶
The observer bound for the in-flight step, or a NullObserver outside any run.
Set for the duration of a step body by :func:observing (which Step.execute enters), so nested
asyncio tasks and worker threads can emit hooks
without threading an observer argument through every signature. Propagates across asyncio.to_thread
and asyncio tasks automatically; a raw ThreadPoolExecutor submit must run inside a copied
context (contextvars.copy_context().run) to inherit it.
Source code in src/adag/deferred.py
observing ¶
Bind observer as the ambient :func:current_observer for the duration of the block.
Source code in src/adag/deferred.py
aresolve_all
async
¶
aresolve_all(
targets: Sequence[StepResult[Any]],
context: ResolveContext,
*,
max_concurrency: int | None = None,
) -> list[Any]
Resolve targets level by level on the running loop; results in targets order.
Use it inside your own event loop (a request handler, a script) instead of sorting the graph
and gathering aresolve per level yourself. context holds the params, observer, cache
and the memo shared by every node.
- Each level's nodes run together with
asyncio.gather; the next level starts when this one is done. A node with a saved result is not expanded: its upstream nodes do not run. - Nodes in one level with the same cache key run once.
max_concurrencybounds how many step bodies run at once.Noneruns a whole level.- The first failure cancels the rest of its level and is raised as it is (no
ExceptionGroup). - Sync step bodies and cache reads run on the loop and block it while they run.
FanOutStepbranches are resolved inside the step, somax_concurrencydoes not bound them.
| PARAMETER | DESCRIPTION |
|---|---|
targets
|
the nodes to resolve.
TYPE:
|
context
|
shared by every node; build one per run.
TYPE:
|
max_concurrency
|
the most step bodies running at the same time.
TYPE:
|
Source code in src/adag/pipeline.py
compute_state ¶
Return the state a node is drawn with: CACHED, PENDING, BLOCKED or SOURCE.
SOURCE is an InputParam or a file. BLOCKED is a step with no hash, for example a
FileSource field whose file is missing. Without cache_dir every step is PENDING.
The key is computed with InputParam defaults, not with the values of a run.
Source code in src/adag/visualization.py
dag ¶
dag(
output: Deferred[Any],
*,
cache_dir: Path | None = None,
path: str | Path | None = None,
prefix: str = "",
options: SvgOptions = _DEFAULT_SVG_OPTIONS,
) -> SVG
Render the DAG as SVG.
| PARAMETER | DESCRIPTION |
|---|---|
output
|
Any Deferred: walks backward to discover the full graph.
TYPE:
|
cache_dir
|
Cache directory (for cached/pending state coloring).
TYPE:
|
path
|
Save SVG to this path.
TYPE:
|
prefix
|
Text removed from the start of step names in titles.
TYPE:
|
options
|
Theme, and whether to embed the stylesheet, paint a background or draw the per-step parameters.
TYPE:
|
Source code in src/adag/visualization.py
node_metadata ¶
node_metadata(
nodes: list[Node],
*,
cache_dir: Path | None = None,
prefix: str = "",
) -> list[NodeMetadata]
Return the display data for each node from walk, in the same order.
Use it to draw a pipeline your own way: walk the graph, change the NodeMetadata you want
(dataclasses.replace a title, a description or the params), then pass the list to
render_graph_svg. dag does the same with no changes.
| PARAMETER | DESCRIPTION |
|---|---|
nodes
|
the nodes returned by
TYPE:
|
cache_dir
|
colour each step by its cache state, as
TYPE:
|
prefix
|
strip this from step names in the titles.
TYPE:
|
Source code in src/adag/visualization.py
render_graph_svg ¶
render_graph_svg(
nodes: list[NodeMetadata],
edges: list[tuple[str, str]],
*,
output_label: str = "",
final_step_name: str | None = None,
options: SvgOptions = _DEFAULT_SVG_OPTIONS,
) -> str
Return the SVG for explicit node metadata and (source, target) edge pairs.
Use it to draw a graph that is not a live pipeline, for example one built from your own
records. dag(output) and render_pipeline_svg(pipeline_dir) call the same layout.
Source code in src/adag/visualization.py
render_pipeline_svg ¶
render_pipeline_svg(
pipeline_dir: str | Path,
*,
options: SvgOptions = _DEFAULT_SVG_OPTIONS,
) -> str | None
Render the pipeline at pipeline_dir as SVG from its on-disk manifest.
Reads pipeline.json and each step's step.json, builds the graph,
and produces an SVG string. Returns None when there is no manifest
or the manifest declares no steps.
Source code in src/adag/visualization.py
walk ¶
Walk any Deferred backward and return the full dependency graph.
Step nodes are identified by instance (id(step)), not class name. A shared upstream (the
same instance reached on two paths) is one node. Two distinct instances of the same class are
two nodes. Each node's label is its node_label attribute when present, else the class name.
The key gets a suffix (Name#2) only when two instances would otherwise collide.
Source code in src/adag/visualization.py
__getattr__ ¶
Resolve deprecated top-level names on first use, so import adag does not warn.
Source code in src/adag/__init__.py
Dispatch¶
adag.dispatch ¶
Branch dispatchers: resolve fan-out branches on workers, rejoin through the cache.
A BranchDispatcher (see deferred.py) decides where the branches of a FanOutStep
run. The default InlineDispatcher runs them in-process.
RemoteDispatcher runs each branch as a job on a JobBackend. The worker runs
adag resolve-branch, which re-imports the pipeline, re-derives the branch, resolves it and
writes the result to the shared cache under the branch content hash. The dispatcher then loads
the result by that hash. Only the BranchJob crosses to the worker.
SubprocessDispatcher is RemoteDispatcher with LocalJobBackend: one local process
per branch.
EXIT_BAD_BRANCH
module-attribute
¶
Worker exit code for a bad branch reference or a hash mismatch. Never retried.
BranchExecutionError ¶
Bases: RuntimeError
Deprecated: adag no longer raises this. Branch failures arrive as FanOutBranchError.
Kept so except BranchExecutionError and custom dispatchers that raise it still import.
Removed in 1.0.
Source code in src/adag/dispatch.py
BranchPolicy
dataclass
¶
How hard to try a branch and how patiently to wait for it.
timeout_seconds counts from when the job is RUNNING. queue_timeout_seconds counts
from submit while the job is PENDING; a batch queue can hold a job for hours.
result_grace_seconds is how long to wait for the result to appear after a job
SUCCEEDED, because a network filesystem can show a new file late.
Source code in src/adag/dispatch.py
BranchReferenceError ¶
Resources
dataclass
¶
What one branch job asks the scheduler for. None means the scheduler's default.
Source code in src/adag/dispatch.py
BranchJob
dataclass
¶
One attempt at one fan-out branch, as a backend submits it.
A backend runs command() in an environment that has the pipeline code and can reach
cache_dir, or the store at store_url. Success means the result is in the cache
under branch.branch_hash. The job's exit code alone is not trusted.
Source code in src/adag/dispatch.py
command ¶
The worker command line. prefix is how to start adag (python -m adag).
Source code in src/adag/dispatch.py
JobState ¶
Bases: Enum
Where a submitted job is. SUCCEEDED and FAILED are final.
Source code in src/adag/dispatch.py
JobStatus
dataclass
¶
A job's state, with detail (an error or exit code) for FAILED.
exit_code is set when the backend knows it. EXIT_BAD_BRANCH is never retried.
Source code in src/adag/dispatch.py
JobBackend ¶
Bases: Protocol
Runs branch jobs on some scheduler: a local process, AWS Batch, Kubernetes, SLURM.
RemoteDispatcher owns the cache check, retries, timeouts and cancel. A backend only
starts jobs and reports on them. It must turn off the scheduler's own retries, or the
attempts multiply.
Source code in src/adag/dispatch.py
RemoteDispatcher ¶
Run each fan-out branch as a job on backend and load its result from the cache.
The dispatcher skips branches already in the cache, submits the rest (at most
max_concurrent at a time), polls backend.status once per
policy.poll_interval_seconds for all live jobs, retries failed attempts up to
policy.max_retries, and cancels every live job when it stops early (an error, a
timeout, or Ctrl-C). A job that exits with EXIT_BAD_BRANCH is not retried.
When the pipeline has a store, pass store_url (see store_from_url): a store
object cannot cross to a worker, so the worker opens the same store from the URL.
clock and sleep are injectable so tests run without waiting.
Source code in src/adag/dispatch.py
200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 | |
dispatch ¶
Resolve one branch. Kept from the pre-0.2.11 one-branch protocol; calls dispatch_all.
dispatch_all ¶
dispatch_all(
branches: Sequence[tuple[BranchRef, StepResult[Any]]],
context: ResolveContext,
) -> dict[str, Any]
Resolve every branch on the backend; return {key: result}.
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
the run has neither |
TypeError
|
a param is not JSON-serializable. |
FanOutBranchError
|
one or more branches failed. Results of the others are in the cache, so a rerun submits only the failed keys. |
Source code in src/adag/dispatch.py
LocalJobBackend ¶
Run each branch job as a local python -m adag resolve-branch process.
stderr goes to a temporary file, so a chatty worker cannot block on a full pipe. Its last
500 characters become the detail of a FAILED status.
Source code in src/adag/dispatch.py
SubprocessDispatcher ¶
Bases: RemoteDispatcher
Resolve each branch in a local OS process via adag resolve-branch.
RemoteDispatcher with LocalJobBackend. Runs at most max_concurrent processes at
once (default: the number of CPUs). Workers share results through cache_dir, or the
store at store_url. The pipeline must have one of them.
Source code in src/adag/dispatch.py
params_json ¶
Render run params as one JSON object, so the worker gets the same types.
| RAISES | DESCRIPTION |
|---|---|
TypeError
|
a param value is not JSON-serializable, so it cannot reach a worker. |
Source code in src/adag/dispatch.py
Cache stores¶
adag.cache_store ¶
CacheStore: the key-to-text backend behind every adag cache.
Step results, BatchStep item caches and dispatched fan-out branches are all stored as text
under a key. Pass a store with Pipeline(..., store=). Without one, the pipeline uses
FileSystemCacheStore on cache_dir.
A backend implements five methods over str values: get, set, exists, delete,
iter_keys. ItemCache builds its schema stamp and in-flight markers from those five, so a
backend does not need to know about them.
Stores with no extra install:
FileSystemCacheStore: one text file per key, atomic writes. The default.SqliteCacheStore: one SQLite file, on stdlibsqlite3.MemoryCacheStore: a dict in this process, for tests.
Stores behind an extra (the driver is imported only when the store is built):
SqlCacheStore: any SQLAlchemy database. Extra:adag[sql].RedisCacheStore: Redis or Valkey. Extra:adag[redis].
For dispatch, every worker and the main process must see the same store: Redis, a SQL database, or a filesystem all of them mount. A disk local to one machine does not work.
CacheStore ¶
Bases: Protocol
A string key to string value store: the five methods a cache backend implements.
Implement them over any storage: Redis, a database, S3, memory.
Keys are opaque strings the cache layer builds (content hashes, namespaced like
"namespace/abc123"). Values are UTF-8 text (the cache layer JSON-encodes its
own payloads). get returns None for a missing key (never raises for absence).
Source code in src/adag/cache_store.py
FileSystemCacheStore ¶
Stores one text file per key under root. The default.
Keys may contain / to nest into sub-directories. Writes are atomic (temp file, then
rename).
Source code in src/adag/cache_store.py
RedisCacheStore ¶
Stores keys in Redis or Valkey, under key_prefix. Needs adag[redis].
The prefix lets one Redis host many apps. redis is imported lazily: only building
this class (or passing a client) needs the package installed.
Pass an existing client (RedisCacheStore(client=...)) or a URL
(RedisCacheStore(url="redis://…")). ttl_seconds optionally expires entries
(None = persist; the DAG cache is content-addressed, so stale entries can't be
wrong; TTL only controls eviction and cost).
Source code in src/adag/cache_store.py
MemoryCacheStore ¶
Stores keys in a dict in this process. Cannot be shared with a worker.
Use it for tests and single-process runs that should not touch disk.
Source code in src/adag/cache_store.py
SqliteCacheStore ¶
Stores keys in one SQLite file, on stdlib sqlite3.
One row per
key in a cache(key, value) table; the DB is a single .db file, so it's a durable
single-host cache without standing up Redis. Each op opens its own short-lived connection,
so it's safe across the dispatch thread pool (SQLite's file locking serialises writers).
Right for a single-host service or a CLI run that wants the cache to survive a restart; NOT for heavy multi-instance write contention across hosts (use Redis/SQL there).
Source code in src/adag/cache_store.py
SqlCacheStore ¶
Stores keys in one table of any SQLAlchemy database. Needs adag[sql].
Works with any URL SQLAlchemy supports (postgresql://…,
mysql://…, sqlite:///path.db), so the DAG cache can live in the same database the
rest of a deployed service already uses. sqlalchemy is a lazily-imported optional
dependency.
One key/value table (name configurable). set overwrites via delete-then-insert in
one transaction, which works on every dialect without a dialect-specific UPSERT. Pass a URL
(SqlCacheStore(url="postgresql://…")) or an existing engine (SqlCacheStore(engine=…))
to share a connection pool with the host app.
Source code in src/adag/cache_store.py
285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 | |
store_from_url ¶
Open a CacheStore from a URL, so a remote worker can reach the main process's store.
file:///pathor a plain path:FileSystemCacheStore.sqlite:///relative.dborsqlite:////absolute.db:SqliteCacheStore(stdlib).redis://...orrediss://...:RedisCacheStore(needsadag[redis]).- any other
scheme://...:SqlCacheStorethrough SQLAlchemy (needsadag[sql]).
Credentials belong in the URL as the worker's environment provides it, never in params.
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
|
Source code in src/adag/cache_store.py
Hashing¶
adag.hashing ¶
Content-addressable hashing utilities for the DAG engine.
KEY_FORMAT
module-attribute
¶
Version of the cache key bytes. It changes only when a release changes every key on purpose.
1 is the first release; 2 is the framed encoding from 0.2.0. tests/test_key_golden.py pins the
current format.
FileFingerprint
dataclass
¶
Identity of a file's content for caching: same fingerprint ⇒ same bytes (cheap stand-in for re-reading). Inode + device are part of the identity because mtime resolution on many filesystems is 1s, and an atomic-replace inside the same second produces a new inode but identical (path, mtime, size): without st_ino the cache would return the stale hash. Frozen ⇒ hashable ⇒ usable as a dict key.
Source code in src/adag/hashing.py
UnhashableValueError ¶
file_hash ¶
SHA-256 of file content, first 12 hex chars. Session-cached by file fingerprint.
Source code in src/adag/hashing.py
is_list ¶
is_tuple ¶
Type guard for tuple[Any, ...]. Use instead of isinstance to avoid tuple[Unknown, ...].
is_dict ¶
Type guard for dict[str, Any]. Use instead of isinstance to avoid dict[Unknown, Unknown].
encode_value ¶
Return the cache-key bytes for one value.
Every value is framed with a type tag and a byte length, so (1, 23) and
(12, 3), or 1 and "1", never encode the same. A Path is hashed by
its text, not its file content: use FileSource to hash content.
| RAISES | DESCRIPTION |
|---|---|
UnhashableValueError
|
|
Source code in src/adag/hashing.py
hash_scalar ¶
Return json.dumps(value, sort_keys=True) as bytes, for building your own item keys.
Not used by adag's own cache keys (those use encode_value). The bytes are the same as in
the first release, so keys built with it stay valid.
Source code in src/adag/hashing.py
hash_list ¶
Return json.dumps(value, sort_keys=True) as bytes. Same bytes as hash_scalar.
class_source_hash ¶
Return a short hash of the class's own source code, or None if it has none.
Only the source of the class body is hashed: the first class in the MRO whose
source inspect can read. Edits to a base class or to a helper function the
step calls do NOT change the hash. Bump a field or kind version when such an
edit changes results. Any edit inside the class body, comments included, does.
Memoized per class: inspect.getsource() re-reads + re-tokenizes the source file on every call, and this is invoked once per step instance across the whole graph, so caching turns a per-call file tokenization into a one-off.