Lightweight event bus over Redis Streams. Define typed events, publish from one service, consume in others without boilerplate or framework dependencies.
pip install streambusuv add streambusDefine an event:
from dataclasses import dataclass
from streambus import StreamBusEvent
@dataclass(kw_only=True)
class OrderCreated(StreamBusEvent):
order_id: str
customer_id: str
total: strPublish it:
from streambus import EventPublisher, RedisConfig, RedisStreamsTransport
transport = RedisStreamsTransport(RedisConfig(url="redis://localhost:6379"))
publisher = EventPublisher(stream="orders:events", transport=transport)
publisher.publish(
OrderCreated(
event_type="order.created",
order_id="ord_123",
customer_id="cust_456",
total="99.90",
)
)Consume it:
from streambus import EventListener, RedisConfig, RedisStreamsTransport
def handle_order(event: OrderCreated) -> None:
print(f"New order {event.order_id}: total {event.total}")
EventListener(
stream="orders:events",
group="notifications",
handler=handle_order,
event_class=OrderCreated,
transport=RedisStreamsTransport(RedisConfig(url="redis://localhost:6379")),
).run()streambus uses Redis Streams for transport: persistent storage, consumer groups, and at-least-once delivery. The library manages group creation, the blocking read loop, message acknowledgement, and automatic reconnection on connection loss.
Each consumer group processes events independently. Delivery is at least once: unacknowledged messages can be redelivered, so handlers must be idempotent. The listener reclaims idle pending entries using Redis XAUTOCLAIM before reading new entries (Redis 6.2+ required).
All events extend StreamBusEvent:
| Field | Type | Required | Notes |
|---|---|---|---|
event_type |
str |
yes | Identifies the event. Convention: noun.verb |
occurred_at |
str |
no | ISO 8601 timestamp. Auto-set on creation. |
Subclasses declare only the fields they need. Unknown fields arriving from the stream are silently discarded — each consumer maps what it uses, nothing more.
Both StreamBusEvent and its subclasses must use @dataclass(kw_only=True). This is required: the base class has a field with a default (occurred_at), and kw_only=True prevents the ordering conflict that would otherwise block subclasses from adding required fields.
RedisConfig
RedisConfig(
url="redis://localhost:6379", # required
socket_timeout=None, # None = no timeout (correct for blocking reads)
socket_connect_timeout=5.0, # seconds to wait on initial connect
)EventListener
EventListener(
stream="stream:name", # required
group="consumer-group", # required
handler=my_handler, # required — callable(event) -> None
event_class=MyEvent, # required — must subclass StreamBusEvent
transport=RedisStreamsTransport(RedisConfig(...)), # required
consumer=None, # defaults to "{group}-1"
batch_size=10,
block_ms=5000,
retry_delay=3.0,
pending_idle_ms=60000, # minimum idle time before retrying pending entries
)EventPublisher
EventPublisher(
stream="stream:name", # required
transport=RedisStreamsTransport(RedisConfig(...)), # required
)| Exception | When |
|---|---|
ConfigurationError |
Invalid or missing parameters at construction time |
EventValidationError |
publish() receives a non-StreamBusEvent or empty event_type |
StreambusError |
Base class for all streambus exceptions |
git clone https://github.com/jmeiracorbal/streambus
cd streambus
uv sync --group dev
uv run pytest tests/ -vRun the example (requires Docker):
cd example
docker compose up -d # start Redis
uv sync
uv run python consume.py # terminal 1
uv run python publish.py # terminal 2MIT