https://github.com/browser-use/bubus
Advanced Pydantic-powered event bus with support for async and sync handler and forwarding betwen busses. Powers the browser-use library.
https://github.com/browser-use/bubus
async asyncio concurrency event-driven event-driven-architecture event-sourcing eventbus events eventstore message-broker message-bus messaging observer pydantic python python3
Last synced: 7 months ago
JSON representation
Advanced Pydantic-powered event bus with support for async and sync handler and forwarding betwen busses. Powers the browser-use library.
- Host: GitHub
- URL: https://github.com/browser-use/bubus
- Owner: browser-use
- License: mit
- Created: 2025-06-18T06:24:21.000Z (7 months ago)
- Default Branch: main
- Last Pushed: 2025-06-18T08:19:41.000Z (7 months ago)
- Last Synced: 2025-06-18T08:27:55.603Z (7 months ago)
- Topics: async, asyncio, concurrency, event-driven, event-driven-architecture, event-sourcing, eventbus, events, eventstore, message-broker, message-bus, messaging, observer, pydantic, python, python3
- Language: Python
- Homepage: https://github.com/browser-use/bubus
- Size: 51.8 KB
- Stars: 2
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# `bubus`: Pydantic-based event bus for async Python
Bubus is an advanced Pydantic-powered event bus with async support, designed for building reactive, event-driven applications with Python. It provides a powerful yet simple API for implementing publish-subscribe patterns with type safety, async handlers, and advanced features like event forwarding between buses.
## Quickstart
Install bubus and get started with a simple event-driven application:
```bash
pip install bubus
```
```python
import asyncio
from bubus import EventBus, BaseEvent
class UserLoginEvent(BaseEvent):
username: str
timestamp: float
async def handle_login(event: UserLoginEvent):
print(f"User {event.username} logged in at {event.timestamp}")
return {"status": "success", "user": event.username}
bus = EventBus()
bus.on('UserLoginEvent', handle_login)
event = bus.dispatch(UserLoginEvent(username="alice", timestamp=1234567890))
result = await event
print(f"Login handled: {result.event_results}")
```
---
## Features
### Type-Safe Events with Pydantic
Define events as Pydantic models with full type checking and validation:
```python
from typing import Any
from bubus import BaseEvent
class OrderCreatedEvent(BaseEvent):
order_id: str
customer_id: str
total_amount: float
items: list[dict[str, Any]]
# Events are automatically validated
event = OrderCreatedEvent(
order_id="ORD-123",
customer_id="CUST-456",
total_amount=99.99,
items=[{"sku": "ITEM-1", "quantity": 2}]
)
```
### Async and Sync Handler Support
Register both synchronous and asynchronous handlers for maximum flexibility:
```python
# Async handler
async def async_handler(event: BaseEvent):
await asyncio.sleep(0.1) # Simulate async work
return "async result"
# Sync handler
def sync_handler(event: BaseEvent):
return "sync result"
bus.on('MyEvent', async_handler)
bus.on('MyEvent', sync_handler)
```
### Event Pattern Matching
Subscribe to events using multiple patterns:
```python
# By event type string
bus.on('UserActionEvent', handler)
# By event model class
bus.on(UserActionEvent, handler)
# Wildcard - handle all events
bus.on('*', universal_handler)
```
### Forward `Events` Between `EventBus`s
You can define separate `EventBus` instances in different "microservices" to separate different areas of concern.
`EventBus`s can be set up to forward events between each other (with automatic loop prevention):
```python
# Create a hierarchy of buses
main_bus = EventBus(name='MainBus')
auth_bus = EventBus(name='AuthBus')
data_bus = EventBus(name='DataBus')
# Forward events between buses (infinite loops are automatically prevented)
main_bus.on('AuthEvent', auth_bus.dispatch)
auth_bus.on('*', data_bus.dispatch)
# Events flow through the hierarchy with tracking
event = main_bus.dispatch(MyEvent())
await event
print(event.event_path) # ['MainBus', 'AuthBus', 'DataBus'] # list of busses that have already procssed the event
```
### Event Results Aggregation
Collect and aggregate results from multiple handlers:
```python
async def config_handler_1(event):
return {"debug": True, "port": 8080}
async def config_handler_2(event):
return {"debug": False, "timeout": 30}
bus.on('GetConfig', config_handler_1)
bus.on('GetConfig', config_handler_2)
event = await bus.dispatch(BaseEvent(event_type='GetConfig'))
# Merge all dict results
config = await event.event_results_flat_dict()
# {'debug': False, 'port': 8080, 'timeout': 30}
# Or get individual results
results = await event.event_results_by_handler_id()
```
### FIFO Event Processing
Events are processed in strict FIFO order, maintaining consistency:
```python
# Events are processed in the order they were dispatched
for i in range(10):
bus.dispatch(ProcessTaskEvent(task_id=i))
# Even with async handlers, order is preserved
await bus.wait_until_idle()
```
If a handler dispatches and awaits any child events during exeuction, those events will jump the FIFO queue and be processed immediately:
```python
def child_handler(event: SomeOtherEvent):
return 'xzy123'
def main_handler(event: MainEvent):
# enqueue event for processing after main_handler exits
child_event = bus.dispatch(SomeOtherEvent())
# can also await child events to process immediately instead of adding to FIFO queue
completed_child_event = await child_event
return f'result from awaiting child event: {await completed_child_event.event_result()}' # 'xyz123'
bus.on(SomeOtherEvent, child_handler)
bus.on(MainEvent, main_handler)
await bus.dispatch(MainEvent()).event_result()
# result from awaiting child event: xyz123
```
### Parallel Handler Execution
Enable parallel processing of handlers for better performance.
The tradeoff is slightly less deterministic ordering as handler execution order will not be guaranteed when run in parallel.
```python
# Create bus with parallel handler execution
bus = EventBus(parallel_handlers=True)
# Multiple handlers run concurrently for each event
bus.on('DataEvent', slow_handler_1) # Takes 1 second
bus.on('DataEvent', slow_handler_2) # Takes 1 second
start = time.time()
await bus.dispatch(DataEvent())
# Total time: ~1 second (not 2)
```
### Dispatch Nested Child Events From Handlers
Automatically track event relationships and causality tree:
```python
async def parent_handler(event: BaseEvent):
# handlers can emit more events to be processed asynchronously after this handler completes
child_event_async = bus.dispatch(ChildEvent())
assert child_event_async.status != 'completed'
# ChildEvent handlers will run after parent_handler exits
# or you can dispatch an event and block until it finishes processing by awaiting the event
# this recursively waits for all handlers, including if event is forwarded to other busses
# (note: awaiting an event from inside a handler jumps the FIFO queue and will process it immediately, before any other pending events)
child_event_sync = await bus.dispatch(ChildEvent())
# ChildEvent handlers run immediately
assert child_event_sync.event_status == 'completed'
# in all cases, parent-child relationships are automagically tracked
assert child_event_async.event_parent_id == event.event_id
assert child_event_sync.event_parent_id == event.event_id
parent_event = bus.dispatch(ParentEvent())
print(parent_event.event_children) # show all the child events emitted during handling of an event
print(bus._log_tree()) # print a nice pretty tree view of the entire event hierarchy
```
### Event Expectation (Async Waiting)
Wait for specific events to be seen on a bus with optional filtering:
```python
# Block until a specific event is seen (with optional timeout)
request = await bus.dispatch(RequestEvent(...))
response_event = await bus.expect('ResponseEvent', timeout=30)
# Block until a specific event is seen (with optional predicate filtering)
response_event = await bus.expect(
'ResponseEvent',
predicate=lambda e: e.request_id == my_request_id,
timeout=30
)
```
> [!IMPORTANT]
> `expect()` resolves when the event is first *dispatched* to the `EventBus`, not when it completes. `await response_event` to get the completed event.
### Write-Ahead Logging
Persist events automatically for durability and debugging:
```python
# Enable WAL persistence
bus = EventBus(name='MyBus', wal_path='./events.jsonl')
# All completed events are automatically persisted
bus.dispatch(ImportantEvent(data="critical"))
# Events are saved as JSONL for easy processing
# {"event_type": "ImportantEvent", "data": "critical", ...}
```
---
---
## API Documentation
### `EventBus`
The main event bus class that manages event processing and handler execution.
```python
EventBus(
name: str | None = None,
wal_path: Path | str | None = None,
parallel_handlers: bool = False
)
```
**Parameters:**
- `name`: Optional unique name for the bus (auto-generated if not provided)
- `wal_path`: Path for write-ahead logging of events to a `jsonl` file (optional)
- `parallel_handlers`: If `True`, handlers run concurrently for each event, otherwise serially if `False` (the default)
#### `EventBus` Properties
- `name`: The bus identifier
- `id`: Unique UUID7 for this bus instance
- `event_history`: Dict of all events the bus has seen by event_id
- `events_pending`: List of events waiting to be processed
- `events_started`: List of events currently being processed
- `events_completed`: List of completed events
#### `EventBus` Methods
##### `on(event_type: str | Type[BaseEvent], handler: Callable)`
Subscribe a handler to events matching a specific event type or `'*'` for all events.
```python
bus.on('UserEvent', handler_func) # By event type string
bus.on(UserEvent, handler_func) # By event class
bus.on('*', handler_func) # Wildcard - all events
```
##### `dispatch(event: BaseEvent) -> BaseEvent`
Enqueue an event for processing and return the pending `Event` immediately (synchronous).
```python
event = bus.dispatch(MyEvent(data="test"))
result = await event # await the pending Event to get the completed Event
```
##### `expect(event_type: str | Type[BaseEvent], timeout: float | None=None, predicate: Callable[[BaseEvent], bool]=None) -> BaseEvent`
Wait for a specific event to occur.
```python
# Wait for any UserEvent
event = await bus.expect('UserEvent', timeout=30)
# Wait with custom filter
event = await bus.expect(
'UserEvent',
predicate=lambda e: e.user_id == 'specific_user'
)
```
##### `wait_until_idle(timeout: float | None=None)`
Wait until all events are processed and the bus is idle.
```python
await bus.wait_until_idle() # wait indefinitely until EventBus has finished processing all events
await bus.wait_until_idle(timeout=5.0) # wait up to 5 seconds
```
##### `stop(timeout: float | None=None)`
Stop the event bus, optionally waiting for pending events.
```python
await bus.stop(timeout=1.0) # Graceful stop, wait up to 1sec for pending and active events to finish processing
await bus.stop() # Immediate shutdown, aborts all pending and actively processing events
```
---
### `BaseEvent`
Base class for all events. Subclass `BaseEvent` to define your own events.
Make sure none of your own event data fields start with `event_` or `model_` to avoid clashing with `BaseEvent` or `pydantic` builtin attrs.
#### `BaseEvent` Fields
```python
class BaseEvent(BaseModel):
# Framework-managed fields
event_type: str # Defaults to class name
event_id: str # Unique UUID7 identifier, auto-generated if not provided
event_timeout: float = 60.0 # Maximum execution in seconds for each handler
event_schema: str # Module.Class@version (auto-set based on class & LIBRARY_VERSION env var)
event_parent_id: str # Parent event ID (auto-set)
event_path: list[str] # List of bus names traversed (auto-set)
event_created_at: datetime # When event was created, auto-generated
event_results: dict[str, EventResult] # Handler results
# Data fields
# ... subclass BaseEvent to add your own event data fields here ...
# some_key: str
# some_other_key: dict[str, int]
# ...
```
`event.event_results` contains a dict of pending `EventResult` objects that will be completed once handlers finish executing.
#### `BaseEvent` Properties
- `event_status`: `Literal['pending', 'started', 'complete']` Event status
- `event_started_at`: `datetime` When first handler started processing
- `event_completed_at`: `datetime` When all handlers completed processing
#### `BaseEvent` Methods
##### `await event`
Await the `Event` object directly to get the completed `Event` object once all handlers have finished executing.
```python
event = bus.dispatch(MyEvent())
completed_event = await event
raw_result_values = [(await event_result) for event_result in completed_event.event_results.values()]
# equivalent to: completed_event.event_results_list() (see below)
```
##### `event_result(timeout: float | None=None) -> Any`
Utility method helper to execute all the handlers and return the first handler's raw result value.
```python
result = await event.event_result()
```
##### `event_results_by_handler_id(timeout: float | None=None) -> dict`
Utility method helper to get all raw result values organized by `{handler_id: result_value}`.
```python
results = await event.event_results_by_handler_id()
# {'handler_id_1': result1, 'handler_id_2': result2}
```
##### `event_results_list(timeout: float | None=None) -> list[Any]`
Utility method helper to get all raw result values in a list.
```python
results = await event.event_results_list()
# [result1, result2]
```
##### `event_results_flat_dict(timeout: float | None=None) -> dict`
Utility method helper to merge all raw result values that are `dict`s into a single flat `dict`.
```python
results = await event.event_results_flat_dict()
# {'key1': 'value1', 'key2': 'value2'}
```
##### `event_results_flat_list(timeout: float | None=None) -> list`
Utility method helper to merge all raw result values that are `list`s into a single flat `list`.
```python
results = await event.event_results_flat_list()
# ['item1', 'item2', 'item3']
```
---
### `EventResult`
The placeholder object that represents the pending result from a single handler executing an event.
`Event.event_results` contains a `dict[PythonIdStr, EventResult]` in the shape of `{handler_id: EventResult()}`.
You shouldn't need to ever directly use this class, it's an internal wrapper to track pending and completed results from each handler within `BaseEvent.event_results`.
#### `EventResult` Fields
```python
class EventResult(BaseModel):
id: str # Unique identifier
handler_id: str # Handler function ID
handler_name: str # Handler function name
eventbus_id: str # Bus that executed this handler
eventbus_name: str # Bus name
status: str # 'pending', 'started', 'completed', 'error'
result: Any # Handler return value
error: str | None # Error message if failed
started_at: datetime # When handler started
completed_at: datetime # When handler completed
timeout: float # Handler timeout in seconds
```
#### `EventResult` Methods
##### `await result`
Await the `EventResult` object directly to get the raw result value.
```python
handler_result = event.event_results['handler_id']
value = await handler_result # Returns result or raises an exception if handler hits an error
```
---
---
## Development
Set up the development environment using `uv`:
```bash
git clone https://github.com/browser-use/bubus && cd bubus
# Create virtual environment with Python 3.12
uv venv --python 3.12
# Activate virtual environment (varies by OS)
source .venv/bin/activate # On Unix/macOS
# or
.venv\Scripts\activate # On Windows
# Install dependencies
uv sync --dev --all-extras
```
```bash
# Run all tests
pytest tests -v x --full-trace
# Run specific test file
pytest tests/test_eventbus.py
```
## Inspiration
- https://www.cosmicpython.com/book/chapter_08_events_and_message_bus.html#message_bus_diagram ⭐️
- https://developer.mozilla.org/en-US/docs/Web/API/EventTarget ⭐️
- https://github.com/pytest-dev/pluggy ⭐️
- https://github.com/teamhide/fastapi-event ⭐️
- https://github.com/ethereum/lahja ⭐️
- https://github.com/enricostara/eventure ⭐️
- https://github.com/akhundMurad/diator ⭐️
- https://github.com/n89nanda/pyeventbus
- https://github.com/iunary/aioemit
- https://github.com/dboslee/evently
- https://github.com/ArcletProject/Letoderea
- https://github.com/seanpar203/event-bus
- https://github.com/n89nanda/pyeventbus
- https://github.com/nicolaszein/py-async-bus
- https://github.com/AngusWG/simple-event-bus
- https://www.joeltok.com/posts/2021-03-building-an-event-bus-in-python/
## License
This project is licensed under the MIT License. For more information, see the main browser-use repository: https://github.com/browser-use/browser-use