enrich_many / resolve_many work when you have the whole list in
memory. When records arrive from a stream — a CSV cursor, a SQS poller, a
Kafka consumer, a generator — you don’t want to materialize everything
first. The Batcher and AsyncBatcher helpers let you add() items one
at a time. They buffer up to size, fire the batch, and drain whatever’s
left on context exit.
They’re generic over the callable. The SDK’s own mc.api.enrich /
mc.api.resolve fit because they take list[Record]. But anything that
matches Callable[[list[T]], R] plugs in — a DB upsert, a webhook
fan-out, a file writer.
Sync
Async
add
returns immediately and the next batch starts buffering while the
previous one is in flight. The context manager’s exit awaits all
outstanding tasks. Set block=True to serialize instead, or
max_concurrency=N to cap how many run in parallel.
Custom callables
The batcher is not enrich-specific. Drop in any function that takes alist[T]:
on_result runs after every batch. When set, results are not
accumulated on b.results — the callback is the sink.
Custom Minerva endpoints via mc.api.call
If you’re hitting a preview / client-specific endpoint that doesn’t have
a typed wrapper yet, wrap mc.api.call in a thin
function so it matches the list[T] -> R shape. You get the same
authentication, rate limiting, and MinervaTransientError retry as the
typed methods:
Bulk fan-out: 10k records through a custom endpoint
Same pattern handles the “I have a big list, fan it out” case — just iterate the list intoadd(). With max_concurrency you cap how many batches run
in parallel:
size=500 + max_concurrency=8:
await b.add(row)fills an internal buffer- Every 500 rows, the buffer flushes —
post_specialfires as a background task - The semaphore caps in-flight HTTP calls at 8; the other 12 queue and run as slots free up
async withexit awaits all outstanding tasks before returningb.resultsends up with 20 entries — one per batch, in completion order
MinervaTransientError wrapping
applies — so a connection drop or 5xx on one batch raises and (with the
default swallow_errors=False) the context exit re-raises from flush().
To keep going past per-batch failures, pass swallow_errors=True and
inspect b.errors after exit.
Why no mc.api.call_many?
The typed methods (enrich_many, resolve_many) auto-flatten per-batch
results into one merged list, because the SDK knows the response shape.
For an arbitrary custom endpoint, the SDK doesn’t know which key holds
the records — so it can’t flatten safely. AsyncBatcher + mc.api.call is
the equivalent primitive; you flatten client-side once per call site.
If you find yourself writing the same post_special(...) wrapper in many
places, that’s a signal to add a typed method on mc.api.* for that
endpoint (with a pydantic response model) — you’ll get auto-flattening,
better autocompletion, and validated outputs for free.
Pydantic validation at add()
Pass schema= to validate every item against a pydantic model before
it’s queued. The original input is still what gets buffered — pydantic
is a shape gate, not a transformer.
"warn" is the default because the SDK shouldn’t second-guess the API.
If pydantic is stricter than the server, dropping the row hides records
the server would have accepted. Flip to "skip" when you’d rather keep
the batch clean than ship something that might 4xx, or "raise" when
the pipeline should fail fast on bad input.
To silence the warning entirely:
Error handling
By default, if the batch call raises, the error surfaces fromadd() or
flush() and aborts the loop. For pipelines that should keep going past
a failure, pass swallow_errors=True:
What lives on the batcher
When to reach for enrich_many instead
Both pipe through the same
_request_data path, so rate-limiting +
MinervaTransientError compose with either choice.