without¶
The narrow waist of the project: the interfaces every plugin speaks, plus the
stream connectors and a with-scoped background task helper. See the
Philosophy for why the model is shaped this way, and the
without API reference for the full surface.
The substrate (without.interfaces)¶
Three types carry the whole model:
- A
Stream[T]is an asynchronous sequence of values: the one shape every connection takes, whoever does the I/O (a socket, a file watcher, an in-memory list). - A
Processor[In, Out]transforms a stream of inputs into a stream of outputs. It is the only node type and the only thing a user writes; one processor's output is another's input, all the way down. - A
Context[T]is a stream viewed as its latest sampled value:current()reads the latest and never blocks, the way long-lived state (config, a pool) is read.
Processors are built, not subclassed. Four builders cover a 2×2 of stateful-vs-stateless and emitting-vs-terminal:
| emits one output per event | collapses to a final value | |
|---|---|---|
| stateless | from_map(step) |
from_sink(step) (effects only) |
| stateful | from_scan(initial, step) (a scan) |
from_fold(initial, step) (a reduce) |
A scan emits at every step; a fold yields only the final accumulated value when
the stream ends. The step is always async, so it MAY await contained I/O
(reading dependencies from injected Context values), but MUST complete the
effect within the call rather than handing a half-open resource back.
Two more builders subset a stream by a predicate without transforming it:
from_selector(keep) re-emits the events matching keep and drops the rest (the
keep-the-matches sense of Python's built-in filter), and from_filter(reject)
is its polarity-dual, dropping the matches. They are the zero-or-one case the
Processor protocol always allowed, so no new machinery. Their predicate is
async like every other builder step (one color of function throughout, so a
decision that needs to await I/O composes without ceremony; a pure one just
never awaits). Emitting several outputs per event, by contrast, is a wiring
concern, not a builder: fan-out to several sinks is tee (below), and the
fan-in family (broadcast, merge) is reserved in issue #13.
Wiring (without.wiring)¶
compose chains one processor into the next on the event edge: pure composition,
the only connector that needs nothing running. When its second argument is a
Sink rather than a Processor, the result is a Sink too, which is how a
middleware chain (a filter, an enrichment) is prefixed onto a terminal consumer.
tee is its terminal fan-out counterpart: tee(*sinks) returns one Sink that
splits a stream across several branches, each with its own tail, so a shared
prefix composed ahead of it runs once and every branch consumes its own copy.
sample is the behavior edge: it
exposes a stream's latest value as a Context (latest-wins, no backpressure),
driven by a background_task for the life of its with block. The source and
terminal adapters sit alongside: stream_from_iterable lifts a fixed iterable
into a Stream, collect drains one to a list, stream_from_queue turns a
push-based queue (an accept loop, a callback client) into the pull-based stream
the rest of the system consumes, and spool drives a source ahead of its
consumer (read-ahead) by pumping it into a bounded queue on a background task.
stack
composes middleware (any (handler, *context) -> handler) into one, serving both
server handlers and client exchanges.
Tasks (without.tasks)¶
The async task helpers: sleep_forever; the with-scoped background_task
(starts a task on entry, cancels-then-awaits it on exit, so nothing leaks);
timeout, a timedelta | None-typed wrapper over asyncio.timeout that models
"no limit" as None (an always-open context) rather than a sentinel float;
limit_concurrency, a bounded-concurrency driver that pulls work from a source
only while below the limit (so a lazy source is never advanced past it); and its
building blocks cancel_futures (cancel a set, then await them all) and
as_async_iterator (normalize a sync or async iterable into one async iterator).