without¶
A decoupled-IO substrate for connecting streams of events to stateful processors
backed by contexts, aiming for maximum concurrency from testable,
dependency-injected code. I/O is not banned, it is separated into the right
abstractions (sources at the edge, behaviors via sample, effects contained in
a processor's step) so the parts stay reusable.
The bet¶
Python has many frameworks with similar-but-subtly-different shapes (ASGI apps,
Kafka consumers, asyncio protocols, config reloaders) that do not interoperate
because none of them names the shared lower layer. without names that layer as
a narrow interface, so the pieces compose. It is meant to feel like a library
(your control flow stays visible) rather than a framework.
The Philosophy page rests on one idea: an ecosystem of thin layers with narrow interfaces, where every boundary is a value you can hold, so you meet one altitude, descend when you need to, and can replace a layer without rewriting the rest. The stateful stream processor is the vocabulary those layers speak, which is what keeps the interfaces between them narrow. Read it first to get the mindset the code is shaped around.
The substrate¶
Three types carry the whole model (without_streams.interfaces):
- 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, a clock, and an in-memory list are all just streams. - 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. - 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.
The packages¶
This is a uv workspace of flat, version-locked
packages. Each is its own top-level import.
without-async: the asyncio primitives everything else is built from, speaking only the standard library.without-streams: the substrate interfaces every plugin speaks, and the stream connectors.without-env: a staticContextparsed from environment variables withpydantic-settings.without-configmap: config from a Kubernetes mount, the context-updated-by-a-stream half of the model.without-asgi: adapters that turn an ASGI app'sreceive/sendinto typed event streams and back, in both directions.without-web: an opinionated HTTP/WebSocket router with trie matching, typed path params, mounting, scoped middleware, exception handlers, and OpenAPI.without-cli: command-line parsing as values, with typed tokens that are the parse, the help, and the read at once, and options that read the environment and secret mounts through the same validation.without-http: anasyncioASGI server and HTTP client built on the sans-IOh11/h2/wsprotostate machines.without-html: HTML as immutable Python values, with escaping and HTML's own constraints carried in the types.without-dag: bounded-concurrency execution of DAG-shaped async workflows, liftable straight into aProcessor.without-durability: durable workflows over a checkpoint any process can read, with the store interfaces that make one writer at a time enforceable. Its stores are Redis, Postgres, and SQLite.
The package dependency graph is derived from the
declared dependencies, and each package's API reference (its Reference page,
e.g. without-streams) is recovered from the
source docstrings.
Installing¶
pip install without-streams # the substrate interfaces
pip install without-web # plus whichever plugins you need
Development¶
uv sync
just test # start the services in compose.yaml, then mypy + pytest
just docs # serve this site with live reload
A few tests drive a real backing service rather than a fake. just test starts the
services in compose.yaml with docker or
podman, whichever it finds, and stops them again when it exits;
install either to run those tests, or let them skip.
For contributing to without itself, see the
Releasing runbook for how the workspace is
versioned and published, and the
Mutation testing guide for driving each
package's suite to zero surviving mutants.