A small, type-safe, async event bus for Python
Project description
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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file transitbus-0.1.0.tar.gz.
File metadata
- Download URL: transitbus-0.1.0.tar.gz
- Upload date:
- Size: 13.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2a9fbbc82c20ddc9134949294612f6afaccc21f9bc010ec765a6c6c0b46e3ff7
|
|
| MD5 |
800da6f7ed138026f0bf6b2c7e90ac52
|
|
| BLAKE2b-256 |
63a19d5ac2a329ea20b29746a06c51897691675f000165b3707036c9d2dd8a8c
|
File details
Details for the file transitbus-0.1.0-py3-none-any.whl.
File metadata
- Download URL: transitbus-0.1.0-py3-none-any.whl
- Upload date:
- Size: 8.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c78c277670d95080e055f6d7a86890d74c260ddf3b209683b28d038ff85f3613
|
|
| MD5 |
c83ad8868dbcc095bf0b4ccd14885b66
|
|
| BLAKE2b-256 |
5f768152513d16ee43a235a582392e1f1a1ec6b23d8b099628f8072aaf64cc89
|