transitbus
A small, type-safe, async event bus for Python. Events are plain Pydantic models, handlers subscribe by event type, and the whole thing is a few hundred lines you can read in one sitting.
import asyncio
from transitbus import Event, EventBus
class ComputeSum(Event[int]):
a: int
b: int
async def main() -> None:
bus = EventBus()
bus.on(ComputeSum, lambda e: e.a + e.b)
total = await bus.dispatch(ComputeSum(a=100, b=120)).result()
print(total) # 220
asyncio.run(main())
Design in one breath
- Subscribe by type, not by string.
bus.on(OrderPlaced, handler)— and because matching is structural, a handler on a base class also receives every subclass. Subscribing toEventobserves everything; there is no'*'. - Lean events. An
Eventis just data plusid,parent_id,created_atandpath. Results live on theDispatchhandle thatdispatch()returns, not bolted onto the event. - Automatic causality. An event dispatched from inside a handler becomes a
child of the running event, tracked with a
ContextVar— no globals, no wiring by hand. - Pluggable write-ahead log. The bus writes completed events to a
WAL; drop inJsonlWALfor a file, or subclassWALfor anything else.
Usage
Subscribing
# direct
bus.on(OrderPlaced, on_order)
# as a decorator
@bus.on(OrderPlaced)
async def on_order(event: OrderPlaced) -> None: ...
# infer the type from the annotation
@bus.subscribe
async def on_order(event: OrderPlaced) -> None: ...
Both async and plain def handlers work. A handler subscribed to a base
class receives its subclasses too:
bus.on(Event, audit) # audit() sees every event on the bus
Dispatching and results
dispatch() returns immediately with an awaitable Dispatch handle:
handle = bus.dispatch(ComputeSum(a=1, b=2))
await handle # wait for all handlers, get the handle back
await handle.result() # first non-None value returned by a handler
await handle.values() # every non-None value
await handle.by_handler() # {handler name: value}
await handle.results() # list[HandlerResult] incl. any exceptions
If a handler raises, its exception is captured on the HandlerResult; asking
for a result()/values() re-raises it as HandlerError (opt out with
raise_on_error=False).
Parent / child causality
@bus.on(OrderPlaced)
async def charge(event: OrderPlaced) -> None:
# parent_id is set automatically from the running event
await bus.dispatch(PaymentCharged(order_id=event.order_id))
Top-level dispatches are processed one at a time, in order (FIFO). A child dispatched from within a handler runs on its own, so awaiting it inside the handler processes it right away without blocking the queue.
Waiting for an event
# resolves as soon as a matching event is dispatched
paid = await bus.expect(PaymentCharged, where=lambda e: e.order_id == "A-1", timeout=30)
Forwarding between buses
Because dispatch is itself a valid handler, one bus can subscribe to another.
forward_to is the readable form of on(Event, other.dispatch):
main.forward_to(auth)
auth.forward_to(data)
data.forward_to(main) # cycles are fine
Each event carries path — the names of the buses it has visited. A bus skips
an event it has already seen, so cycles terminate on their own; no handler
introspection involved. Within a bus, more specific subscriptions run first, so
forwarding (a base-Event subscription) happens after local handlers.
Write-ahead log
from transitbus import EventBus, JsonlWAL
bus = EventBus(wal=JsonlWAL("events.jsonl"))
Every processed event is appended as one JSON object per line, tagged with its
type. Awaiting a dispatch guarantees its event has been written. Subclass WAL
and implement async append(event) to write anywhere else.
Development
uv sync
uv run pytest
uv run ruff check
The examples/ directory has one runnable script per feature (see
its README); examples/orders.py is
an end-to-end walkthrough.
See CONTRIBUTING.md for how to install with uv and open a
pull request.
License
Metadata
Release files for transitbus 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| transitbus-0.1.0.tar.gz | 13.4 kB | Without provenance | Details for transitbus-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| transitbus-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for transitbus-0.1.0-py3-none-any.whl |
Total release size: 22.1 kB
Release files / transitbus-0.1.0.tar.gz
| Download URL | transitbus-0.1.0.tar.gz |
|---|---|
| Size | 13.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2a9fbbc82c20ddc9134949294612f6afaccc21f9bc010ec765a6c6c0b46e3ff7
|
|
BLAKE2b-256 checksum How to use checksums |
63a19d5ac2a329ea20b29746a06c51897691675f000165b3707036c9d2dd8a8c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.2
|
Release files / transitbus-0.1.0-py3-none-any.whl
| Download URL | transitbus-0.1.0-py3-none-any.whl |
|---|---|
| Size | 8.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c78c277670d95080e055f6d7a86890d74c260ddf3b209683b28d038ff85f3613
|
|
BLAKE2b-256 checksum How to use checksums |
5f768152513d16ee43a235a582392e1f1a1ec6b23d8b099628f8072aaf64cc89
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.2
|
Security for transitbus 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.