{"id":28823723,"url":"https://github.com/browser-use/bubus","last_synced_at":"2025-06-25T06:01:42.038Z","repository":{"id":299772921,"uuid":"1004114774","full_name":"browser-use/bubus","owner":"browser-use","description":"Advanced Pydantic-powered event bus with support for async and sync handler and forwarding betwen busses. Powers the browser-use library.","archived":false,"fork":false,"pushed_at":"2025-06-18T08:19:41.000Z","size":53,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-06-18T08:27:55.603Z","etag":null,"topics":["async","asyncio","concurrency","event-driven","event-driven-architecture","event-sourcing","eventbus","events","eventstore","message-broker","message-bus","messaging","observer","pydantic","python","python3"],"latest_commit_sha":null,"homepage":"https://github.com/browser-use/bubus","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/browser-use.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-06-18T06:24:21.000Z","updated_at":"2025-06-18T08:19:45.000Z","dependencies_parsed_at":"2025-06-18T08:39:54.460Z","dependency_job_id":null,"html_url":"https://github.com/browser-use/bubus","commit_stats":null,"previous_names":["browser-use/bubus"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/browser-use/bubus","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/browser-use%2Fbubus","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/browser-use%2Fbubus/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/browser-use%2Fbubus/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/browser-use%2Fbubus/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/browser-use","download_url":"https://codeload.github.com/browser-use/bubus/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/browser-use%2Fbubus/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":260658756,"owners_count":23043435,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["async","asyncio","concurrency","event-driven","event-driven-architecture","event-sourcing","eventbus","events","eventstore","message-broker","message-bus","messaging","observer","pydantic","python","python3"],"created_at":"2025-06-19T00:31:46.884Z","updated_at":"2025-06-24T05:03:06.862Z","avatar_url":"https://github.com/browser-use.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# `bubus`: Pydantic-based event bus for async Python\n\nBubus 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.\n\n## Quickstart\n\nInstall bubus and get started with a simple event-driven application:\n\n```bash\npip install bubus\n```\n\n```python\nimport asyncio\nfrom bubus import EventBus, BaseEvent\n\nclass UserLoginEvent(BaseEvent):\n    username: str\n    timestamp: float\n\nasync def handle_login(event: UserLoginEvent):\n    print(f\"User {event.username} logged in at {event.timestamp}\")\n    return {\"status\": \"success\", \"user\": event.username}\n\nbus = EventBus()\nbus.on('UserLoginEvent', handle_login)\n\nevent = bus.dispatch(UserLoginEvent(username=\"alice\", timestamp=1234567890))\nresult = await event\nprint(f\"Login handled: {result.event_results}\")\n```\n\n\u003cbr/\u003e\n\n---\n\n\u003cbr/\u003e\n\n## Features\n\n### Type-Safe Events with Pydantic\n\nDefine events as Pydantic models with full type checking and validation:\n\n```python\nfrom typing import Any\nfrom bubus import BaseEvent\n\nclass OrderCreatedEvent(BaseEvent):\n    order_id: str\n    customer_id: str\n    total_amount: float\n    items: list[dict[str, Any]]\n\n# Events are automatically validated\nevent = OrderCreatedEvent(\n    order_id=\"ORD-123\",\n    customer_id=\"CUST-456\", \n    total_amount=99.99,\n    items=[{\"sku\": \"ITEM-1\", \"quantity\": 2}]\n)\n```\n\n### Async and Sync Handler Support\n\nRegister both synchronous and asynchronous handlers for maximum flexibility:\n\n```python\n# Async handler\nasync def async_handler(event: BaseEvent):\n    await asyncio.sleep(0.1)  # Simulate async work\n    return \"async result\"\n\n# Sync handler\ndef sync_handler(event: BaseEvent):\n    return \"sync result\"\n\nbus.on('MyEvent', async_handler)\nbus.on('MyEvent', sync_handler)\n```\n\n### Event Pattern Matching\n\nSubscribe to events using multiple patterns:\n\n```python\n# By event type string\nbus.on('UserActionEvent', handler)\n\n# By event model class\nbus.on(UserActionEvent, handler)\n\n# Wildcard - handle all events\nbus.on('*', universal_handler)\n```\n\n### Forward `Events` Between `EventBus`s \n\nYou can define separate `EventBus` instances in different \"microservices\" to separate different areas of concern.\n`EventBus`s can be set up to forward events between each other (with automatic loop prevention):\n\n```python\n# Create a hierarchy of buses\nmain_bus = EventBus(name='MainBus')\nauth_bus = EventBus(name='AuthBus')\ndata_bus = EventBus(name='DataBus')\n\n# Forward events between buses (infinite loops are automatically prevented)\nmain_bus.on('AuthEvent', auth_bus.dispatch)\nauth_bus.on('*', data_bus.dispatch)\n\n# Events flow through the hierarchy with tracking\nevent = main_bus.dispatch(MyEvent())\nawait event\nprint(event.event_path)  # ['MainBus', 'AuthBus', 'DataBus']  # list of busses that have already procssed the event\n```\n\n### Event Results Aggregation\n\nCollect and aggregate results from multiple handlers:\n\n```python\nasync def config_handler_1(event):\n    return {\"debug\": True, \"port\": 8080}\n\nasync def config_handler_2(event):\n    return {\"debug\": False, \"timeout\": 30}\n\nbus.on('GetConfig', config_handler_1)\nbus.on('GetConfig', config_handler_2)\n\nevent = await bus.dispatch(BaseEvent(event_type='GetConfig'))\n\n# Merge all dict results\nconfig = await event.event_results_flat_dict()\n# {'debug': False, 'port': 8080, 'timeout': 30}\n\n# Or get individual results\nresults = await event.event_results_by_handler_id()\n```\n\n### FIFO Event Processing\n\nEvents are processed in strict FIFO order, maintaining consistency:\n\n```python\n# Events are processed in the order they were dispatched\nfor i in range(10):\n    bus.dispatch(ProcessTaskEvent(task_id=i))\n\n# Even with async handlers, order is preserved\nawait bus.wait_until_idle()\n```\n\nIf a handler dispatches and awaits any child events during exeuction, those events will jump the FIFO queue and be processed immediately:\n```python\ndef child_handler(event: SomeOtherEvent):\n    return 'xzy123'\n\ndef main_handler(event: MainEvent):\n    # enqueue event for processing after main_handler exits\n    child_event = bus.dispatch(SomeOtherEvent())\n    \n    # can also await child events to process immediately instead of adding to FIFO queue\n    completed_child_event = await child_event\n    return f'result from awaiting child event: {await completed_child_event.event_result()}'  # 'xyz123'\n\nbus.on(SomeOtherEvent, child_handler)\nbus.on(MainEvent, main_handler)\n\nawait bus.dispatch(MainEvent()).event_result()\n# result from awaiting child event: xyz123\n```\n\n### Parallel Handler Execution\n\nEnable parallel processing of handlers for better performance.  \nThe tradeoff is slightly less deterministic ordering as handler execution order will not be guaranteed when run in parallel.\n\n```python\n# Create bus with parallel handler execution\nbus = EventBus(parallel_handlers=True)\n\n# Multiple handlers run concurrently for each event\nbus.on('DataEvent', slow_handler_1)  # Takes 1 second\nbus.on('DataEvent', slow_handler_2)  # Takes 1 second\n\nstart = time.time()\nawait bus.dispatch(DataEvent())\n# Total time: ~1 second (not 2)\n```\n\n### Dispatch Nested Child Events From Handlers\n\nAutomatically track event relationships and causality tree:\n\n```python\nasync def parent_handler(event: BaseEvent):\n    # handlers can emit more events to be processed asynchronously after this handler completes\n    child_event_async = bus.dispatch(ChildEvent())\n    assert child_event_async.status != 'completed'\n    # ChildEvent handlers will run after parent_handler exits\n\n    # or you can dispatch an event and block until it finishes processing by awaiting the event\n    # this recursively waits for all handlers, including if event is forwarded to other busses\n    # (note: awaiting an event from inside a handler jumps the FIFO queue and will process it immediately, before any other pending events)\n    child_event_sync = await bus.dispatch(ChildEvent())\n    # ChildEvent handlers run immediately\n    assert child_event_sync.event_status == 'completed'\n\n    # in all cases, parent-child relationships are automagically tracked\n    assert child_event_async.event_parent_id == event.event_id\n    assert child_event_sync.event_parent_id == event.event_id\n\nparent_event = bus.dispatch(ParentEvent())\nprint(parent_event.event_children)           # show all the child events emitted during handling of an event\nprint(bus._log_tree())                       # print a nice pretty tree view of the entire event hierarchy\n```\n\n### Event Expectation (Async Waiting)\n\nWait for specific events to be seen on a bus with optional filtering:\n\n```python\n# Block until a specific event is seen (with optional timeout)\nrequest = await bus.dispatch(RequestEvent(...))\nresponse_event = await bus.expect('ResponseEvent', timeout=30)\n\n# Block until a specific event is seen (with optional predicate filtering)\nresponse_event = await bus.expect(\n    'ResponseEvent',\n    predicate=lambda e: e.request_id == my_request_id,\n    timeout=30\n)\n```\n\n\u003e [!IMPORTANT]\n\u003e `expect()` resolves when the event is first *dispatched* to the `EventBus`, not when it completes. `await response_event` to get the completed event.\n\n### Write-Ahead Logging\n\nPersist events automatically for durability and debugging:\n\n```python\n# Enable WAL persistence\nbus = EventBus(name='MyBus', wal_path='./events.jsonl')\n\n# All completed events are automatically persisted\nbus.dispatch(ImportantEvent(data=\"critical\"))\n\n# Events are saved as JSONL for easy processing\n# {\"event_type\": \"ImportantEvent\", \"data\": \"critical\", ...}\n```\n\n\u003cbr/\u003e\n\n---\n---\n\n\u003cbr/\u003e\n\n## API Documentation\n\n### `EventBus`\n\nThe main event bus class that manages event processing and handler execution.\n\n```python\nEventBus(\n    name: str | None = None,\n    wal_path: Path | str | None = None,\n    parallel_handlers: bool = False\n)\n```\n\n**Parameters:**\n\n- `name`: Optional unique name for the bus (auto-generated if not provided)\n- `wal_path`: Path for write-ahead logging of events to a `jsonl` file (optional)\n- `parallel_handlers`: If `True`, handlers run concurrently for each event, otherwise serially if `False` (the default)\n\n#### `EventBus` Properties\n\n- `name`: The bus identifier\n- `id`: Unique UUID7 for this bus instance\n- `event_history`: Dict of all events the bus has seen by event_id\n- `events_pending`: List of events waiting to be processed\n- `events_started`: List of events currently being processed\n- `events_completed`: List of completed events\n\n\n#### `EventBus` Methods\n\n##### `on(event_type: str | Type[BaseEvent], handler: Callable)`\n\nSubscribe a handler to events matching a specific event type or `'*'` for all events.\n\n```python\nbus.on('UserEvent', handler_func)  # By event type string\nbus.on(UserEvent, handler_func)    # By event class\nbus.on('*', handler_func)          # Wildcard - all events\n```\n\n##### `dispatch(event: BaseEvent) -\u003e BaseEvent`\n\nEnqueue an event for processing and return the pending `Event` immediately (synchronous).\n\n```python\nevent = bus.dispatch(MyEvent(data=\"test\"))\nresult = await event  # await the pending Event to get the completed Event\n```\n\n##### `expect(event_type: str | Type[BaseEvent], timeout: float | None=None, predicate: Callable[[BaseEvent], bool]=None) -\u003e BaseEvent`\n\nWait for a specific event to occur.\n\n```python\n# Wait for any UserEvent\nevent = await bus.expect('UserEvent', timeout=30)\n\n# Wait with custom filter\nevent = await bus.expect(\n    'UserEvent',\n    predicate=lambda e: e.user_id == 'specific_user'\n)\n```\n\n##### `wait_until_idle(timeout: float | None=None)`\n\nWait until all events are processed and the bus is idle.\n\n```python\nawait bus.wait_until_idle()             # wait indefinitely until EventBus has finished processing all events\n\nawait bus.wait_until_idle(timeout=5.0)  # wait up to 5 seconds\n```\n\n##### `stop(timeout: float | None=None)`\n\nStop the event bus, optionally waiting for pending events.\n\n```python\nawait bus.stop(timeout=1.0)  # Graceful stop, wait up to 1sec for pending and active events to finish processing\nawait bus.stop()             # Immediate shutdown, aborts all pending and actively processing events\n```\n\n---\n\n### `BaseEvent`\n\nBase class for all events. Subclass `BaseEvent` to define your own events.\n\nMake sure none of your own event data fields start with `event_` or `model_` to avoid clashing with `BaseEvent` or `pydantic` builtin attrs.\n\n#### `BaseEvent` Fields\n\n```python\nclass BaseEvent(BaseModel):\n    # Framework-managed fields\n    event_type: str              # Defaults to class name\n    event_id: str                # Unique UUID7 identifier, auto-generated if not provided\n    event_timeout: float = 60.0  # Maximum execution in seconds for each handler\n    event_schema: str            # Module.Class@version (auto-set based on class \u0026 LIBRARY_VERSION env var)\n    event_parent_id: str         # Parent event ID (auto-set)\n    event_path: list[str]        # List of bus names traversed (auto-set)\n    event_created_at: datetime   # When event was created, auto-generated\n    event_results: dict[str, EventResult]   # Handler results\n    \n    # Data fields\n    # ... subclass BaseEvent to add your own event data fields here ...\n    # some_key: str\n    # some_other_key: dict[str, int]\n    # ...\n```\n\n`event.event_results` contains a dict of pending `EventResult` objects that will be completed once handlers finish executing.\n\n\n#### `BaseEvent` Properties\n\n- `event_status`: `Literal['pending', 'started', 'complete']` Event status\n- `event_started_at`: `datetime` When first handler started processing\n- `event_completed_at`: `datetime` When all handlers completed processing\n\n#### `BaseEvent` Methods\n\n##### `await event`\n\nAwait the `Event` object directly to get the completed `Event` object once all handlers have finished executing.\n\n```python\nevent = bus.dispatch(MyEvent())\ncompleted_event = await event\n\nraw_result_values = [(await event_result) for event_result in completed_event.event_results.values()]\n# equivalent to: completed_event.event_results_list()  (see below)\n```\n\n##### `event_result(timeout: float | None=None) -\u003e Any`\n\nUtility method helper to execute all the handlers and return the first handler's raw result value.\n\n```python\nresult = await event.event_result()\n```\n\n##### `event_results_by_handler_id(timeout: float | None=None) -\u003e dict`\n\nUtility method helper to get all raw result values organized by `{handler_id: result_value}`.\n\n```python\nresults = await event.event_results_by_handler_id()\n# {'handler_id_1': result1, 'handler_id_2': result2}\n```\n\n##### `event_results_list(timeout: float | None=None) -\u003e list[Any]`\n\nUtility method helper to get all raw result values in a list.\n\n```python\nresults = await event.event_results_list()\n# [result1, result2]\n```\n\n##### `event_results_flat_dict(timeout: float | None=None) -\u003e dict`\n\nUtility method helper to merge all raw result values that are `dict`s into a single flat `dict`.\n\n```python\nresults = await event.event_results_flat_dict()\n# {'key1': 'value1', 'key2': 'value2'}\n```\n\n##### `event_results_flat_list(timeout: float | None=None) -\u003e list`\n\nUtility method helper to merge all raw result values that are `list`s into a single flat `list`.\n\n```python\nresults = await event.event_results_flat_list()\n# ['item1', 'item2', 'item3']\n```\n\n\n---\n\n### `EventResult`\n\nThe placeholder object that represents the pending result from a single handler executing an event.  \n`Event.event_results` contains a `dict[PythonIdStr, EventResult]` in the shape of `{handler_id: EventResult()}`.\n\nYou 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`.\n\n#### `EventResult` Fields\n\n```python\nclass EventResult(BaseModel):\n    id: str                    # Unique identifier\n    handler_id: str           # Handler function ID\n    handler_name: str         # Handler function name\n    eventbus_id: str          # Bus that executed this handler\n    eventbus_name: str        # Bus name\n    \n    status: str               # 'pending', 'started', 'completed', 'error'\n    result: Any               # Handler return value\n    error: str | None         # Error message if failed\n    \n    started_at: datetime      # When handler started\n    completed_at: datetime    # When handler completed\n    timeout: float            # Handler timeout in seconds\n```\n\n#### `EventResult` Methods\n\n##### `await result`\n\nAwait the `EventResult` object directly to get the raw result value.\n\n```python\nhandler_result = event.event_results['handler_id']\nvalue = await handler_result  # Returns result or raises an exception if handler hits an error\n```\n\n\u003cbr/\u003e\n\n---\n---\n\n\u003cbr/\u003e\n\n## Development\n\nSet up the development environment using `uv`:\n\n```bash\ngit clone https://github.com/browser-use/bubus \u0026\u0026 cd bubus\n\n# Create virtual environment with Python 3.12\nuv venv --python 3.12\n\n# Activate virtual environment (varies by OS)\nsource .venv/bin/activate  # On Unix/macOS\n# or\n.venv\\Scripts\\activate  # On Windows\n\n# Install dependencies\nuv sync --dev --all-extras\n```\n\n```bash\n# Run all tests\npytest tests -v x --full-trace\n\n# Run specific test file\npytest tests/test_eventbus.py\n```\n\n## Inspiration\n\n- https://www.cosmicpython.com/book/chapter_08_events_and_message_bus.html#message_bus_diagram ⭐️\n- https://developer.mozilla.org/en-US/docs/Web/API/EventTarget ⭐️\n- https://github.com/pytest-dev/pluggy ⭐️\n- https://github.com/teamhide/fastapi-event ⭐️\n- https://github.com/ethereum/lahja ⭐️\n- https://github.com/enricostara/eventure ⭐️\n- https://github.com/akhundMurad/diator ⭐️\n- https://github.com/n89nanda/pyeventbus\n- https://github.com/iunary/aioemit\n- https://github.com/dboslee/evently\n- https://github.com/ArcletProject/Letoderea\n- https://github.com/seanpar203/event-bus\n- https://github.com/n89nanda/pyeventbus\n- https://github.com/nicolaszein/py-async-bus\n- https://github.com/AngusWG/simple-event-bus\n- https://www.joeltok.com/posts/2021-03-building-an-event-bus-in-python/\n\n## License\n\nThis project is licensed under the MIT License. For more information, see the main browser-use repository: https://github.com/browser-use/browser-use\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbrowser-use%2Fbubus","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbrowser-use%2Fbubus","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbrowser-use%2Fbubus/lists"}