Batch steps¶
Use BatchStep when a step does the same expensive call for many items. Each item's result
is cached on its own, so a rerun after a failure computes only the missing items.
Implement three methods:
items(**inputs): return oneBatchItem(key=..., data=...)per item.process_item(item, **inputs): compute one item.assemble(results, **inputs): combine the per-item results into the step result.
Instead of items(), you can implement items_data() and return plain data. adag then derives
each item key from three things: the step's own hash (class, code, fields), the item data, and
every other input of the step.
Set items_source_keys to the name of the input that holds the items, for example
items_source_keys = ("names",). Without it, the whole list is part of every item key, so
adding one item recomputes all of them. The example in Cache stores sets it.
Override process_batch() to process items concurrently.
"""BatchStep: each item is cached on its own, so a rerun resumes after a failure."""
import tempfile
from dataclasses import dataclass
from pathlib import Path
from pydantic import BaseModel
from adag import BatchItem, BatchStep, InputParam, NullObserver, Pipeline
CALLS: list[str] = []
FAIL_ON: set[str] = {"carol"}
class Greeting(BaseModel):
text: str
class Greetings(BaseModel):
lines: list[str]
@dataclass(frozen=True)
class Greet(BatchStep[Greetings, Greeting]):
def items(self, names: list[str]) -> list[BatchItem]:
return [BatchItem(key=name, data=name) for name in names]
def process_item(self, item: BatchItem, names: list[str]) -> Greeting:
CALLS.append(item.data)
if item.data in FAIL_ON:
raise RuntimeError(f"API error for {item.data}")
return Greeting(text=f"hello {item.data}")
def assemble(self, results: list[Greeting], names: list[str]) -> Greetings:
return Greetings(lines=[result.text for result in results])
names = InputParam[list[str]]("names")
pipeline = Pipeline(Greet()(names=names), cache_dir=Path(tempfile.mkdtemp()))
everyone = ["alice", "bob", "carol", "dave"]
try:
pipeline.execute(observer=NullObserver(), names=everyone)
except RuntimeError as error:
print("first run failed:", error, "| called:", CALLS)
CALLS.clear()
FAIL_ON.clear()
result = pipeline.execute(observer=NullObserver(), names=everyone)
print("second run called:", CALLS)
print(result.lines)
first run failed: API error for carol | called: ['alice', 'bob', 'carol']
second run called: ['carol', 'dave']
['hello alice', 'hello bob', 'hello carol', 'hello dave']
flowchart TB
items["items(): one BatchItem per item"] --> each{"Item result saved?"}
each -- yes --> load["Load it"]
each -- no --> proc["process_item(): compute it"] --> save["Save it right away"]
load --> asm["assemble(): combine all results"]
save --> asm
asm --> done(["Step result"])
A crash loses only the item in progress: items already saved are loaded on the next run.
Item results live under <cache_dir>/.items/<namespace>/. The namespace is the class name in
snake case (greet above).