Skip to main content

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 to Event observes everything; there is no '*'.
  • Lean events. An Event is just data plus id, parent_id, created_at and path. Results live on the Dispatch handle that dispatch() 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 in JsonlWAL for a file, or subclass WAL for 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

MIT

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)

Source distribution for transitbus 0.1.0
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)

Table of built distributions (wheels) for transitbus 0.1.0
File Interpreter ABI Platform Actions
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.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page