abxbus 2.4.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
abxbus-2.4.2/PKG-INFO ADDED
@@ -0,0 +1,1256 @@
1
+ Metadata-Version: 2.3
2
+ Name: abxbus
3
+ Version: 2.4.2
4
+ Summary: Advanced Pydantic-powered event bus with async support
5
+ Author: Nick Sweeting, ArchiveBox
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Dist: aiofiles>=24.1.0
10
+ Requires-Dist: anyio>=4.9.0
11
+ Requires-Dist: portalocker>=2.7.0
12
+ Requires-Dist: pydantic>=2.11.5
13
+ Requires-Dist: typing-extensions>=4.12.2
14
+ Requires-Dist: uuid7>=0.1.0
15
+ Requires-Dist: asyncpg>=0.31.0 ; extra == 'bridges'
16
+ Requires-Dist: nats-py>=2.13.1 ; extra == 'bridges'
17
+ Requires-Dist: redis>=7.1.1 ; extra == 'bridges'
18
+ Requires-Dist: nats-py>=2.13.1 ; extra == 'nats'
19
+ Requires-Dist: asyncpg>=0.31.0 ; extra == 'postgres'
20
+ Requires-Dist: redis>=7.1.1 ; extra == 'redis'
21
+ Requires-Python: >=3.11
22
+ Project-URL: Homepage, https://abxbus.archivebox.io
23
+ Project-URL: Repository, https://github.com/ArchiveBox/abxbus
24
+ Project-URL: Issue Tracker, https://github.com/ArchiveBox/abxbus/issues
25
+ Project-URL: Documentation, https://abxbus.archivebox.io
26
+ Project-URL: DeepWiki, https://deepwiki.com/ArchiveBox/abxbus
27
+ Project-URL: PyPI, https://pypi.org/project/abxbus/
28
+ Project-URL: NPM, https://www.npmjs.com/package/abxbus
29
+ Provides-Extra: bridges
30
+ Provides-Extra: nats
31
+ Provides-Extra: postgres
32
+ Provides-Extra: redis
33
+ Description-Content-Type: text/markdown
34
+
35
+ # `abxbus`: 📢 Production-ready multi-language event bus
36
+
37
+ <img width="200" alt="image" src="https://github.com/user-attachments/assets/b3525c24-51ba-496c-b327-ccdfe46a7362" align="right" />
38
+
39
+ [![DeepWiki: Python](https://img.shields.io/badge/DeepWiki-abxbus%2FPython-yellow.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAyCAYAAAAnWDnqAAAAAXNSR0IArs4c6QAAA05JREFUaEPtmUtyEzEQhtWTQyQLHNak2AB7ZnyXZMEjXMGeK/AIi+QuHrMnbChYY7MIh8g01fJoopFb0uhhEqqcbWTp06/uv1saEDv4O3n3dV60RfP947Mm9/SQc0ICFQgzfc4CYZoTPAswgSJCCUJUnAAoRHOAUOcATwbmVLWdGoH//PB8mnKqScAhsD0kYP3j/Yt5LPQe2KvcXmGvRHcDnpxfL2zOYJ1mFwrryWTz0advv1Ut4CJgf5uhDuDj5eUcAUoahrdY/56ebRWeraTjMt/00Sh3UDtjgHtQNHwcRGOC98BJEAEymycmYcWwOprTgcB6VZ5JK5TAJ+fXGLBm3FDAmn6oPPjR4rKCAoJCal2eAiQp2x0vxTPB3ALO2CRkwmDy5WohzBDwSEFKRwPbknEggCPB/imwrycgxX2NzoMCHhPkDwqYMr9tRcP5qNrMZHkVnOjRMWwLCcr8ohBVb1OMjxLwGCvjTikrsBOiA6fNyCrm8V1rP93iVPpwaE+gO0SsWmPiXB+jikdf6SizrT5qKasx5j8ABbHpFTx+vFXp9EnYQmLx02h1QTTrl6eDqxLnGjporxl3NL3agEvXdT0WmEost648sQOYAeJS9Q7bfUVoMGnjo4AZdUMQku50McDcMWcBPvr0SzbTAFDfvJqwLzgxwATnCgnp4wDl6Aa+Ax283gghmj+vj7feE2KBBRMW3FzOpLOADl0Isb5587h/U4gGvkt5v60Z1VLG8BhYjbzRwyQZemwAd6cCR5/XFWLYZRIMpX39AR0tjaGGiGzLVyhse5C9RKC6ai42ppWPKiBagOvaYk8lO7DajerabOZP46Lby5wKjw1HCRx7p9sVMOWGzb/vA1hwiWc6jm3MvQDTogQkiqIhJV0nBQBTU+3okKCFDy9WwferkHjtxib7t3xIUQtHxnIwtx4mpg26/HfwVNVDb4oI9RHmx5WGelRVlrtiw43zboCLaxv46AZeB3IlTkwouebTr1y2NjSpHz68WNFjHvupy3q8TFn3Hos2IAk4Ju5dCo8B3wP7VPr/FGaKiG+T+v+TQqIrOqMTL1VdWV1DdmcbO8KXBz6esmYWYKPwDL5b5FA1a0hwapHiom0r/cKaoqr+27/XcrS5UwSMbQAAAABJRU5ErkJggg==)](https://deepwiki.com/ArchiveBox/abxbus) [![PyPI - Version](https://img.shields.io/pypi/v/abxbus)](https://pypi.org/project/abxbus/) [![PyPi Downloads/week](https://static.pepy.tech/badge/abxbus/week)](https://pepy.tech/projects/abxbus) ![GitHub last commit](https://img.shields.io/github/last-commit/ArchiveBox/abxbus)
40
+
41
+ [![DeepWiki: TS](https://img.shields.io/badge/DeepWiki-abxbus%2FTypescript-blue.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAyCAYAAAAnWDnqAAAAAXNSR0IArs4c6QAAA05JREFUaEPtmUtyEzEQhtWTQyQLHNak2AB7ZnyXZMEjXMGeK/AIi+QuHrMnbChYY7MIh8g01fJoopFb0uhhEqqcbWTp06/uv1saEDv4O3n3dV60RfP947Mm9/SQc0ICFQgzfc4CYZoTPAswgSJCCUJUnAAoRHOAUOcATwbmVLWdGoH//PB8mnKqScAhsD0kYP3j/Yt5LPQe2KvcXmGvRHcDnpxfL2zOYJ1mFwrryWTz0advv1Ut4CJgf5uhDuDj5eUcAUoahrdY/56ebRWeraTjMt/00Sh3UDtjgHtQNHwcRGOC98BJEAEymycmYcWwOprTgcB6VZ5JK5TAJ+fXGLBm3FDAmn6oPPjR4rKCAoJCal2eAiQp2x0vxTPB3ALO2CRkwmDy5WohzBDwSEFKRwPbknEggCPB/imwrycgxX2NzoMCHhPkDwqYMr9tRcP5qNrMZHkVnOjRMWwLCcr8ohBVb1OMjxLwGCvjTikrsBOiA6fNyCrm8V1rP93iVPpwaE+gO0SsWmPiXB+jikdf6SizrT5qKasx5j8ABbHpFTx+vFXp9EnYQmLx02h1QTTrl6eDqxLnGjporxl3NL3agEvXdT0WmEost648sQOYAeJS9Q7bfUVoMGnjo4AZdUMQku50McDcMWcBPvr0SzbTAFDfvJqwLzgxwATnCgnp4wDl6Aa+Ax283gghmj+vj7feE2KBBRMW3FzOpLOADl0Isb5587h/U4gGvkt5v60Z1VLG8BhYjbzRwyQZemwAd6cCR5/XFWLYZRIMpX39AR0tjaGGiGzLVyhse5C9RKC6ai42ppWPKiBagOvaYk8lO7DajerabOZP46Lby5wKjw1HCRx7p9sVMOWGzb/vA1hwiWc6jm3MvQDTogQkiqIhJV0nBQBTU+3okKCFDy9WwferkHjtxib7t3xIUQtHxnIwtx4mpg26/HfwVNVDb4oI9RHmx5WGelRVlrtiw43zboCLaxv46AZeB3IlTkwouebTr1y2NjSpHz68WNFjHvupy3q8TFn3Hos2IAk4Ju5dCo8B3wP7VPr/FGaKiG+T+v+TQqIrOqMTL1VdWV1DdmcbO8KXBz6esmYWYKPwDL5b5FA1a0hwapHiom0r/cKaoqr+27/XcrS5UwSMbQAAAABJRU5ErkJggg==)](https://deepwiki.com/ArchiveBox/abxbus/3-typescript-implementation) [![NPM Version](https://img.shields.io/npm/v/abxbus)](https://www.npmjs.com/package/abxbus) [![PyPi Downloads/month](https://static.pepy.tech/badge/abxbus/month)](https://pepy.tech/projects/abxbus) [![GitHub License](https://img.shields.io/github/license/ArchiveBox/abxbus)](https://github.com/ArchiveBox/abxbus)
42
+
43
+ AbxBus is an in-memory event bus library for async Python and TS (node/browser).
44
+
45
+ It's designed for quickly building resilient, predictable, complex event-driven apps.
46
+
47
+ It "just works" with an intuitive, but powerful event JSON format + emit API that's consistent across both languages and scales consistently from one event up to millions (~0.2ms/event):
48
+
49
+ ```python
50
+ class SomeEvent(BaseEvent):
51
+ some_data: int
52
+
53
+ def handle_some_event(event: SomeEvent):
54
+ print('hi!')
55
+
56
+ bus.on(SomeEvent, some_function)
57
+ await bus.emit(SomeEvent({some_data: 132}))
58
+ # "hi!""
59
+ ```
60
+
61
+ It's async native, has proper automatic nested event tracking, and powerful concurrency control options. The API is inspired by `EventEmitter` or [`emittery`](https://github.com/sindresorhus/emittery) in JS, but it takes it a step further:
62
+
63
+ - nice Pydantic / Zod schemas for events that can be exchanged between both languages
64
+ - automatic UUIDv7s and monotonic nanosecond timestamps for ordering events globally
65
+ - built in locking options to force strict global FIFO processing or fully parallel processing
66
+
67
+ ---
68
+
69
+ ♾️ It's inspired by the simplicity of async and events in `JS` but with baked-in features that allow to eliminate most of the tedious repetitive complexity in event-driven codebases:
70
+
71
+ - correct timeout enforcement across multiple levels of events, if a parent times out it correctly aborts all child event processing
72
+ - ability to strongly type hint and enforce the return type of event handlers at compile-time
73
+ - ability to queue events on the bus, or inline await them for immediate execution like a normal function call
74
+ - handles thousands of events/sec/core in both languages; see the runtime matrix below for current measured numbers
75
+
76
+ <br/>
77
+
78
+ ## 🔢 Quickstart
79
+
80
+ Install abxbus and get started with a simple event-driven application:
81
+
82
+ ```bash
83
+ pip install abxbus # see ./abxbus-ts/README.md for JS instructions
84
+ ```
85
+
86
+ ```python
87
+ import asyncio
88
+ from abxbus import EventBus, BaseEvent
89
+ from your_auth_events import AuthRequestEvent, AuthResponseEvent
90
+
91
+ class UserLoginEvent(BaseEvent[str]):
92
+ username: str
93
+ is_admin: bool
94
+
95
+ async def handle_login(event: UserLoginEvent) -> str:
96
+ auth_request = await event.event_bus.emit(AuthRequestEvent(...)) # nested events supported
97
+ auth_response = await event.event_bus.find(AuthResponseEvent, child_of=auth_request, future=30)
98
+ return f"User {event.username} logged in admin={event.is_admin} with API response: {await auth_response.event_result()}"
99
+
100
+ bus = EventBus()
101
+ bus.on(UserLoginEvent, handle_login)
102
+ bus.on(AuthRequestEvent, AuthAPI.post)
103
+
104
+ event = bus.emit(UserLoginEvent(username="alice", is_admin=True))
105
+ print(await event.event_result())
106
+ # User alice logged in admin=True with API response: {...}
107
+ ```
108
+
109
+ <br/>
110
+
111
+ ---
112
+
113
+ <br/>
114
+
115
+ ## ✨ Features
116
+
117
+ <br/>
118
+
119
+ <details>
120
+ <summary><strong>🔎 Event Pattern Matching</strong></summary>
121
+
122
+ Subscribe to events using multiple patterns:
123
+
124
+ ```python
125
+ # By event model class (recommended for best type hinting)
126
+ bus.on(UserActionEvent, handler)
127
+
128
+ # By event type string
129
+ bus.on('UserActionEvent', handler)
130
+
131
+ # Wildcard - handle all events
132
+ bus.on('*', universal_handler)
133
+ ```
134
+
135
+ <br/>
136
+
137
+ </details>
138
+
139
+ <details>
140
+ <summary><strong>🔀 Async and Sync Handler Support</strong></summary>
141
+
142
+ Register both synchronous and asynchronous handlers for maximum flexibility:
143
+
144
+ ```python
145
+ # Async handler
146
+ async def async_handler(event: SomeEvent) -> str:
147
+ await asyncio.sleep(0.1) # Simulate async work
148
+ return "async result"
149
+
150
+ # Sync handler
151
+ def sync_handler(event: SomeEvent) -> str:
152
+ return "sync result"
153
+
154
+ bus.on(SomeEvent, async_handler)
155
+ bus.on(SomeEvent, sync_handler)
156
+ ```
157
+
158
+ Handlers can also be defined under classes for easier organization:
159
+
160
+ ```python
161
+ class SomeService:
162
+ some_value = 'this works'
163
+
164
+ async def handlers_can_be_methods(self, event: SomeEvent) -> str:
165
+ return self.some_value
166
+
167
+ @classmethod
168
+ async def handler_can_be_classmethods(cls, event: SomeEvent) -> str:
169
+ return cls.some_value
170
+
171
+ @staticmethod
172
+ async def handlers_can_be_staticmethods(event: SomeEvent) -> str:
173
+ return 'this works too'
174
+
175
+ # All usage patterns behave the same:
176
+ bus.on(SomeEvent, SomeService().handlers_can_be_methods)
177
+ bus.on(SomeEvent, SomeService.handler_can_be_classmethods)
178
+ bus.on(SomeEvent, SomeService.handlers_can_be_staticmethods)
179
+ ```
180
+
181
+ <br/>
182
+
183
+ </details>
184
+
185
+ <details>
186
+ <summary><strong>🔠 Type-Safe Events with Pydantic</strong></summary>
187
+
188
+ Define events as Pydantic models with full type checking and validation:
189
+
190
+ ```python
191
+ from typing import Any
192
+ from abxbus import BaseEvent
193
+
194
+ class OrderCreatedEvent(BaseEvent):
195
+ order_id: str
196
+ customer_id: str
197
+ total_amount: float
198
+ items: list[dict[str, Any]]
199
+
200
+ # Events are automatically validated
201
+ event = OrderCreatedEvent(
202
+ order_id="ORD-123",
203
+ customer_id="CUST-456",
204
+ total_amount=99.99,
205
+ items=[{"sku": "ITEM-1", "quantity": 2}]
206
+ )
207
+ ```
208
+
209
+ > [!TIP]
210
+ > You can also enforce the types of [event handler return values](#-event-handler-return-values).
211
+
212
+ <br/>
213
+
214
+ </details>
215
+
216
+ <details>
217
+ <summary><strong>⏩ Forward `Events` Between `EventBus`s</strong></summary>
218
+
219
+ You can define separate `EventBus` instances in different "microservices" to separate different areas of concern.
220
+ `EventBus`s can be set up to forward events between each other (with automatic loop prevention):
221
+
222
+ ```python
223
+ # Create a hierarchy of buses
224
+ main_bus = EventBus(name='MainBus')
225
+ auth_bus = EventBus(name='AuthBus')
226
+ data_bus = EventBus(name='DataBus')
227
+
228
+ # Share all or specific events between buses
229
+ main_bus.on('*', auth_bus.emit) # if main bus gets LoginEvent, will forward to AuthBus
230
+ auth_bus.on('*', data_bus.emit) # auth bus will forward everything to DataBus
231
+ data_bus.on('*', main_bus.emit) # don't worry! event will only be processed once by each, no infinite loop occurs
232
+
233
+ # Events flow through the hierarchy with tracking
234
+ event = main_bus.emit(LoginEvent())
235
+ await event
236
+ print(event.event_path) # ['MainBus#ab12', 'AuthBus#cd34', 'DataBus#ef56'] # list of bus labels that already processed the event
237
+ ```
238
+
239
+ <br/>
240
+
241
+ </details>
242
+
243
+ <details>
244
+ <summary><strong>🔱 Event Results Aggregation</strong></summary>
245
+
246
+ Collect results from multiple handlers:
247
+
248
+ ```python
249
+ async def load_user_config(event: GetConfigEvent) -> dict[str, Any]:
250
+ return {"debug": True, "port": 8080}
251
+
252
+ async def load_system_config(event: GetConfigEvent) -> dict[str, Any]:
253
+ return {"debug": False, "timeout": 30}
254
+
255
+ bus.on(GetConfigEvent, load_user_config)
256
+ bus.on(GetConfigEvent, load_system_config)
257
+
258
+ # Get all handler result values
259
+ event = await bus.emit(GetConfigEvent())
260
+ results = await event.event_results_list()
261
+
262
+ # Inspect per-handler metadata when needed
263
+ for handler_id, event_result in event.event_results.items():
264
+ print(handler_id, event_result.handler_name, event_result.result)
265
+ ```
266
+
267
+ <br/>
268
+
269
+ </details>
270
+
271
+ <details>
272
+ <summary><strong>🚦 FIFO Event Processing</strong></summary>
273
+
274
+ Events are processed in strict FIFO order, maintaining consistency:
275
+
276
+ ```python
277
+ # Events are processed in the order they were emitted
278
+ for i in range(10):
279
+ bus.emit(ProcessTaskEvent(task_id=i))
280
+
281
+ # Even with async handlers, order is preserved
282
+ await bus.wait_until_idle(timeout=30.0)
283
+ ```
284
+
285
+ If a handler emits and awaits any child events during execution, those events will jump the FIFO queue and be processed immediately:
286
+
287
+ ```python
288
+ def child_handler(event: SomeOtherEvent) -> str:
289
+ return 'xzy123'
290
+
291
+ def main_handler(event: MainEvent) -> str:
292
+ # enqueue event for processing after main_handler exits
293
+ child_event = bus.emit(SomeOtherEvent())
294
+
295
+ # can also await child events to process immediately instead of adding to FIFO queue
296
+ completed_child_event = await child_event
297
+ return f'result from awaiting child event: {await completed_child_event.event_result()}' # 'xyz123'
298
+
299
+ bus.on(SomeOtherEvent, child_handler)
300
+ bus.on(MainEvent, main_handler)
301
+
302
+ await bus.emit(MainEvent()).event_result()
303
+ # result from awaiting child event: xyz123
304
+ ```
305
+
306
+ <br/>
307
+
308
+ </details>
309
+
310
+ <details>
311
+ <summary><strong>🪆 Emit Nested Child Events From Handlers</strong></summary>
312
+
313
+ Automatically track event relationships and causality tree:
314
+
315
+ ```python
316
+ async def parent_handler(event: BaseEvent):
317
+ # handlers can emit more events to be processed asynchronously after this handler completes
318
+ child = ChildEvent()
319
+ child_event_async = event.event_bus.emit(child) # equivalent to bus.emit(...)
320
+ assert child.event_status != 'completed'
321
+ assert child_event_async.event_parent_id == event.event_id
322
+ await child_event_async
323
+
324
+ # or you can emit an event and block until it finishes processing by awaiting the event
325
+ # this recursively waits for all handlers, including if event is forwarded to other buses
326
+ # (note: awaiting an event from inside a handler jumps the FIFO queue and will process it immediately, before any other pending events)
327
+ child_event_sync = await bus.emit(ChildEvent())
328
+ # ChildEvent handlers run immediately
329
+ assert child_event_sync.event_status == 'completed'
330
+
331
+ # in all cases, parent-child relationships are automagically tracked
332
+ assert child_event_sync.event_parent_id == event.event_id
333
+
334
+ async def run_main():
335
+ bus.on(ChildEvent, child_handler)
336
+ bus.on(ParentEvent, parent_handler)
337
+
338
+ parent_event = bus.emit(ParentEvent())
339
+ print(parent_event.event_children) # show all the child events emitted during handling of an event
340
+ await parent_event
341
+ print(bus.log_tree())
342
+ await bus.stop()
343
+
344
+ if __name__ == '__main__':
345
+ asyncio.run(run_main())
346
+ ```
347
+
348
+ <img width="100%" alt="show the whole tree of events at any time using the logging helpers" src="https://github.com/user-attachments/assets/f94684a6-7694-4066-b948-46925f47b56c" /><br/>
349
+ <img width="100%" alt="intelligent timeout handling to differentiate handler that timed out from handler that was interrupted" src="https://github.com/user-attachments/assets/8da341fd-6c26-4c68-8fec-aef1ca55c189" />
350
+
351
+ <br/><br/>
352
+
353
+ </details>
354
+
355
+ <details>
356
+ <summary><strong>🔎 Find Events in History or Wait for Future Events</strong></summary>
357
+
358
+ `find()` is the single lookup API: search history, wait for future events, or combine both.
359
+
360
+ ```python
361
+ # Default: non-blocking history lookup (past=True, future=False)
362
+ existing = await bus.find(ResponseEvent)
363
+
364
+ # Wait only for future matches
365
+ future = await bus.find(ResponseEvent, past=False, future=5)
366
+
367
+ # Combine event predicate + event metadata filters
368
+ match = await bus.find(
369
+ ResponseEvent,
370
+ where=lambda e: e.request_id == my_id,
371
+ event_status='completed',
372
+ future=5,
373
+ )
374
+
375
+ # Wildcard: match any event type, filtered by metadata/predicate
376
+ any_completed = await bus.find(
377
+ '*',
378
+ where=lambda e: e.event_type.endswith('ResultEvent'),
379
+ event_status='completed',
380
+ future=5,
381
+ )
382
+ ```
383
+
384
+ #### Finding Child Events
385
+
386
+ When you emit an event that triggers child events, use `child_of` to find specific descendants:
387
+
388
+ ```python
389
+ # Emit a parent event that triggers child events
390
+ nav_event = await bus.emit(NavigateToUrlEvent(url="https://example.com"))
391
+
392
+ # Find a child event (already fired while NavigateToUrlEvent was being handled)
393
+ new_tab = await bus.find(TabCreatedEvent, child_of=nav_event, past=5)
394
+ if new_tab:
395
+ print(f"New tab created: {new_tab.tab_id}")
396
+ ```
397
+
398
+ This solves race conditions where child events fire before you start waiting for them.
399
+
400
+ See the `EventBus.find(...)` API section below for full parameter details.
401
+
402
+ > [!IMPORTANT]
403
+ > `find()` resolves when the event is first _emitted_ to the `EventBus`, not when it completes.
404
+ > Use `await event` for immediate-await semantics (queue-jumps when called inside a handler), or `await event.event_completed()` to always wait in normal queue order.
405
+ > If no match is found (or future timeout elapses), `find()` returns `None`.
406
+
407
+ <br/>
408
+
409
+ </details>
410
+
411
+ <details>
412
+ <summary><strong>🔁 Event Debouncing</strong></summary>
413
+
414
+ Avoid re-running expensive work by reusing recent events. The `find()` method makes debouncing simple:
415
+
416
+ ```python
417
+ # Simple debouncing: reuse event from last 10 seconds, or emit new
418
+ event = await (
419
+ await bus.find(ScreenshotEvent, past=10, future=False) # Check last 10s of history (instant)
420
+ or bus.emit(ScreenshotEvent())
421
+ )
422
+
423
+ # Advanced: check history, wait briefly for new event to appear, fallback to emit new event
424
+ event = (
425
+ await bus.find(SyncEvent, past=True, future=False) # Check all history (instant)
426
+ or await bus.find(SyncEvent, past=False, future=5) # Wait up to 5s for in-flight
427
+ or bus.emit(SyncEvent()) # Fallback: emit new
428
+ )
429
+ await event # get completed event
430
+ ```
431
+
432
+ <br/>
433
+
434
+ </details>
435
+
436
+ <details>
437
+ <summary><strong>🎯 Event Handler Return Values</strong></summary>
438
+
439
+ There are two ways to get return values from event handlers:
440
+
441
+ **1. Have handlers return their values directly, which puts them in `event.event_results`:**
442
+
443
+ ```python
444
+ class DoSomeMathEvent(BaseEvent[int]): # BaseEvent[int] = handlers are validated as returning int
445
+ a: int
446
+ b: int
447
+
448
+ # int passed above gets saved to:
449
+ # event_result_type = int
450
+
451
+ def do_some_math(event: DoSomeMathEvent) -> int:
452
+ return event.a + event.b
453
+
454
+ event_bus.on(DoSomeMathEvent, do_some_math)
455
+ print(await event_bus.emit(DoSomeMathEvent(a=100, b=120)).event_result())
456
+ # 220
457
+ ```
458
+
459
+ You can use these helpers to interact with the results returned by handlers:
460
+
461
+ - `BaseEvent.event_result()`
462
+ - `BaseEvent.event_results_list()`
463
+ - Inspect raw per-handler entries via `BaseEvent.event_results`
464
+
465
+ **2. Have the handler do the work, then emit another event containing the result value, which other code can find:**
466
+
467
+ ```python
468
+ def do_some_math(event: DoSomeMathEvent[int]) -> int:
469
+ result = event.a + event.b
470
+ event.event_bus.emit(MathCompleteEvent(final_sum=result))
471
+
472
+ event_bus.on(DoSomeMathEvent, do_some_math)
473
+ await event_bus.emit(DoSomeMathEvent(a=100, b=120))
474
+ result_event = await event_bus.find(MathCompleteEvent, past=False, future=30)
475
+ print(result_event.final_sum)
476
+ # 220
477
+ ```
478
+
479
+ #### Annotating Event Handler Return Value Types
480
+
481
+ AbxBus supports optional strict typing for Event handler return values using a generic parameter passed to `BaseEvent[ReturnTypeHere]`.
482
+ For example if you use `BaseEvent[str]`, abxbus would enforce that all handler functions must return `str | None` at compile-time via IDE/`mypy`/`pyright`/`ty` type hints, and at runtime when each handler finishes.
483
+
484
+ ```python
485
+ class ScreenshotEvent(BaseEvent[bytes]): # BaseEvent[bytes] will enforce that handlers can only return bytes
486
+ width: int
487
+ height: int
488
+
489
+ async def on_ScreenshotEvent(event: ScreenshotEvent) -> bytes:
490
+ return b'someimagebytes...' # ✅ IDE type-hints & runtime both enforce return type matches expected: bytes
491
+ return 123 # ❌ will show mypy/pyright issue + raise TypeError if the wrong type is returned
492
+
493
+ event_bus.on(ScreenshotEvent, on_ScreenshotEvent)
494
+
495
+ # Handler return values are automatically validated against the bytes type
496
+ returned_bytes = await event_bus.emit(ScreenshotEvent(...)).event_result()
497
+ assert isinstance(returned_bytes, bytes)
498
+ ```
499
+
500
+ **Important:** The validation uses Pydantic's `TypeAdapter`, which validates but does not coerce types. Handlers must return the exact type specified or `None`:
501
+
502
+ ```python
503
+ class StringEvent(BaseEvent[str]):
504
+ pass
505
+
506
+ # ✅ This works - returns the expected str type
507
+ def good_handler(event: StringEvent) -> str:
508
+ return "hello"
509
+
510
+ # ❌ This fails validation - returns int instead of str
511
+ def bad_handler(event: StringEvent) -> str:
512
+ return 42 # ValidationError: expected str, got int
513
+ ```
514
+
515
+ This also works with complex types and Pydantic models:
516
+
517
+ ```python
518
+ class EmailMessage(BaseModel):
519
+ subject: str
520
+ content_len: int
521
+ email_from: str
522
+
523
+ class FetchInboxEvent(BaseEvent[list[EmailMessage]]):
524
+ account_id: UUID
525
+ auth_key: str
526
+
527
+ async def fetch_from_gmail(event: FetchInboxEvent) -> list[EmailMessage]:
528
+ return [EmailMessage(subject=msg.subj, ...) for msg in GmailAPI.get_msgs(event.account_id, ...)]
529
+
530
+ event_bus.on(FetchInboxEvent, fetch_from_gmail)
531
+
532
+ # Return values are automatically validated as list[EmailMessage]
533
+ email_list = await event_bus.emit(FetchInboxEvent(account_id='124', ...)).event_result()
534
+ ```
535
+
536
+ For pure Python usage, `event_result_type` can be any Python/Pydantic type you want. For cross-language JSON roundtrips, object-like shapes (e.g. `TypedDict`, `dataclass`, model-like dict schemas) rehydrate on Python as Pydantic models, map keys are constrained to JSON object string keys, and fine-grained string constraints/custom field validator logic is not preserved.
537
+
538
+ <br/>
539
+
540
+ </details>
541
+
542
+ <details>
543
+ <summary><strong>🧵 ContextVar Propagation</strong></summary>
544
+
545
+ ContextVars set before `emit()` are automatically propagated to event handlers. This is essential for request-scoped context like request IDs, user sessions, or tracing spans:
546
+
547
+ ```python
548
+ from contextvars import ContextVar
549
+
550
+ # Define your context variables
551
+ request_id: ContextVar[str] = ContextVar('request_id', default='<unset>')
552
+ user_id: ContextVar[str] = ContextVar('user_id', default='<unset>')
553
+
554
+ async def handler(event: MyEvent) -> str:
555
+ # Handler sees the context values that were set before emit()
556
+ print(f"Request: {request_id.get()}, User: {user_id.get()}")
557
+ return "done"
558
+
559
+ bus.on(MyEvent, handler)
560
+
561
+ # Set context before emit (e.g., in FastAPI middleware)
562
+ request_id.set('req-12345')
563
+ user_id.set('user-abc')
564
+
565
+ # Handler will see request_id='req-12345' and user_id='user-abc'
566
+ await bus.emit(MyEvent())
567
+ ```
568
+
569
+ **Context propagates through nested handlers:**
570
+
571
+ ```python
572
+ async def parent_handler(event: ParentEvent) -> str:
573
+ # Context is captured at emit time
574
+ print(f"Parent sees: {request_id.get()}") # 'req-12345'
575
+
576
+ # Child events inherit the same context
577
+ await bus.emit(ChildEvent())
578
+ return "parent_done"
579
+
580
+ async def child_handler(event: ChildEvent) -> str:
581
+ # Child also sees the original emit context
582
+ print(f"Child sees: {request_id.get()}") # 'req-12345'
583
+ return "child_done"
584
+ ```
585
+
586
+ **Context isolation between emits:**
587
+
588
+ Each emit captures its own context snapshot. Concurrent emits with different context values are properly isolated:
589
+
590
+ ```python
591
+ request_id.set('req-A')
592
+ event_a = bus.emit(MyEvent()) # Handler A sees 'req-A'
593
+
594
+ request_id.set('req-B')
595
+ event_b = bus.emit(MyEvent()) # Handler B sees 'req-B'
596
+
597
+ await event_a # Still sees 'req-A'
598
+ await event_b # Still sees 'req-B'
599
+ ```
600
+
601
+ > [!NOTE]
602
+ > Context is captured at `emit()` time, not when the handler executes. This ensures handlers see the context from the call site, even if the event is processed later from a queue.
603
+
604
+ <br/>
605
+
606
+ </details>
607
+
608
+ <details>
609
+ <summary><strong>🧹 Memory Management</strong></summary>
610
+
611
+ EventBus includes automatic memory management to prevent unbounded growth in long-running applications:
612
+
613
+ ```python
614
+ # Create a bus with memory limits (default: 50 events)
615
+ bus = EventBus(max_history_size=100) # Keep max 100 events in history
616
+
617
+ # Or disable memory limits for unlimited history
618
+ bus = EventBus(max_history_size=None)
619
+
620
+ # Or keep only in-flight events in history (drop each event as soon as it completes)
621
+ bus = EventBus(max_history_size=0)
622
+
623
+ # Or reject new emits when history is full (instead of dropping old history)
624
+ bus = EventBus(max_history_size=100, max_history_drop=False)
625
+ ```
626
+
627
+ **Automatic Cleanup:**
628
+
629
+ - When `max_history_size` is set and `max_history_drop=True`, EventBus removes old events when the limit is exceeded
630
+ - If `max_history_size=0`, history keeps only pending/started events and drops each event immediately after completion
631
+ - If `max_history_drop=True`, the bus may drop oldest history entries even if they are uncompleted events
632
+ - Completed events are removed first (oldest first), then started events, then pending events
633
+ - This ensures active events are preserved while cleaning up old completed events
634
+
635
+ **Manual Memory Management:**
636
+
637
+ ```python
638
+ # For request-scoped buses (e.g. web servers), clear all memory after each request
639
+ try:
640
+ event_service = EventService() # Creates internal EventBus
641
+ await event_service.process_request()
642
+ finally:
643
+ # Clear all event history and remove from global tracking
644
+ await event_service.eventbus.stop(clear=True)
645
+ ```
646
+
647
+ **Memory Monitoring:**
648
+
649
+ - EventBus automatically monitors total memory usage across all instances
650
+ - Warnings are logged when total memory exceeds 50MB
651
+ - Use `bus.stop(clear=True)` to completely free memory for unused buses
652
+ - To avoid memory leaks from big events, the default limits are intentionally kept low. events are normally processed as they come in, and there is rarely a need to keep every event in memory longer after its complete. long-term storage should be accomplished using other mechanisms, like the WAL
653
+
654
+ <br/>
655
+
656
+ </details>
657
+
658
+ <details>
659
+ <summary><strong>⛓️ Parallel Handler Execution</strong></summary>
660
+
661
+ > [!CAUTION]
662
+ > **Not Recommended.** Only for advanced users willing to implement their own concurrency control.
663
+
664
+ Enable parallel processing of handlers for better performance.
665
+ The harsh tradeoff is less deterministic ordering as handler execution order will not be guaranteed when run in parallel.
666
+ (It's very hard to write non-flaky/reliable applications when handler execution order is not guaranteed.)
667
+
668
+ ```python
669
+ # Create bus with parallel handler execution
670
+ bus = EventBus(event_handler_concurrency='parallel')
671
+
672
+ # Multiple handlers run concurrently for each event
673
+ bus.on('DataEvent', slow_handler_1) # Takes 1 second
674
+ bus.on('DataEvent', slow_handler_2) # Takes 1 second
675
+
676
+ start = time.time()
677
+ await bus.emit(DataEvent())
678
+ # Total time: ~1 second (not 2)
679
+ ```
680
+
681
+ <br/>
682
+
683
+ </details>
684
+
685
+ <details>
686
+ <summary><strong>🧩 Middlewares</strong></summary>
687
+
688
+ Middlewares can observe or mutate the `EventResult` at each step, emit additional events, or trigger other side effects (metrics, retries, auth checks, etc.).
689
+
690
+ ```python
691
+ from abxbus import EventBus
692
+ from abxbus.middlewares import LoggerEventBusMiddleware, WALEventBusMiddleware, SQLiteHistoryMirrorMiddleware, OtelTracingMiddleware
693
+
694
+ bus = EventBus(
695
+ name='MyBus',
696
+ middlewares=[
697
+ SQLiteHistoryMirrorMiddleware('./events.sqlite3'),
698
+ WALEventBusMiddleware('./events.jsonl'),
699
+ LoggerEventBusMiddleware('./events.log'),
700
+ OtelTracingMiddleware(),
701
+ # ...
702
+ ],
703
+ )
704
+
705
+ await bus.emit(SecondEventAbc(some_key="banana"))
706
+ # will persist all events to sqlite + events.jsonl + events.log
707
+ ```
708
+
709
+ Built-in middlewares you can import from `abxbus.middlewares.*`:
710
+
711
+ - `AutoErrorEventMiddleware`: on handler error, fire-and-forget emits `OriginalEventTypeErrorEvent` with `{error, error_type}` (skips `*ErrorEvent`/`*ResultEvent` sources). Useful when downstream/remote consumers only see events and need explicit failure notifications.
712
+ - `AutoReturnEventMiddleware`: on non-`None` handler return, fire-and-forget emits `OriginalEventTypeResultEvent` with `{data}` (skips `*ErrorEvent`/`*ResultEvent` sources). Useful for bridges/remote systems since handler return values do not cross bridge boundaries, but events do.
713
+ - `AutoHandlerChangeEventMiddleware`: emits `BusHandlerRegisteredEvent({handler})` / `BusHandlerUnregisteredEvent({handler})` when handlers are added/removed via `.on()` / `.off()`.
714
+ - `OtelTracingMiddleware`: emits OpenTelemetry spans for events and handlers with parent-child linking; can be exported to Sentry via Sentry's OpenTelemetry integration.
715
+ - `WALEventBusMiddleware`: persists completed events to JSONL for replay/debugging.
716
+ - `LoggerEventBusMiddleware`: writes event/handler transitions to stdout and optionally to file.
717
+ - `SQLiteHistoryMirrorMiddleware`: mirrors event and handler snapshots into append-only SQLite `events_log` and `event_results_log` tables for auditing/debugging.
718
+
719
+ #### Defining a custom middleware
720
+
721
+ Handler middlewares subclass `EventBusMiddleware` and override whichever lifecycle hooks they need (`on_event_change`, `on_event_result_change`, `on_bus_handlers_change`):
722
+
723
+ ```python
724
+ from abxbus.middlewares import EventBusMiddleware
725
+
726
+ class AnalyticsMiddleware(EventBusMiddleware):
727
+ async def on_event_result_change(self, eventbus, event, event_result, status):
728
+ if status == 'started':
729
+ await analytics_bus.emit(HandlerStartedAnalyticsEvent(event_id=event_result.event_id))
730
+ elif status == 'completed':
731
+ await analytics_bus.emit(
732
+ HandlerCompletedAnalyticsEvent(
733
+ event_id=event_result.event_id,
734
+ error=repr(event_result.error) if event_result.error else None,
735
+ )
736
+ )
737
+
738
+ async def on_bus_handlers_change(self, eventbus, handler, registered):
739
+ await analytics_bus.emit(
740
+ HandlerRegistryChangedEvent(handler_id=handler.id, registered=registered, bus=eventbus.name)
741
+ )
742
+ ```
743
+
744
+ <br/>
745
+
746
+ ---
747
+
748
+ ---
749
+
750
+ <br/>
751
+
752
+ </details>
753
+
754
+ ## 📚 API Documentation
755
+
756
+ <details>
757
+ <summary><strong><code>EventBus</code></strong></summary>
758
+
759
+ The main event bus class that manages event processing and handler execution.
760
+
761
+ ```python
762
+ EventBus(
763
+ name: str | None = None,
764
+ event_handler_concurrency: Literal['serial', 'parallel'] = 'serial',
765
+ event_handler_completion: Literal['all', 'first'] = 'all',
766
+ event_timeout: float | None = 60.0,
767
+ event_slow_timeout: float | None = 300.0,
768
+ event_handler_slow_timeout: float | None = 30.0,
769
+ event_handler_detect_file_paths: bool = True,
770
+ max_history_size: int | None = 50,
771
+ max_history_drop: bool = False,
772
+ middlewares: Sequence[EventBusMiddleware | type[EventBusMiddleware]] | None = None,
773
+ )
774
+ ```
775
+
776
+ **Parameters:**
777
+
778
+ - `name`: Optional unique name for the bus (auto-generated if not provided)
779
+ - `event_handler_concurrency`: Default handler execution mode for events on this bus: `'serial'` (default) or `'parallel'` (resolved at processing time when `event.event_handler_concurrency` is unset)
780
+ - `event_handler_completion`: Handler completion mode for each event: `'all'` (default, wait for all handlers) or `'first'` (complete once first successful non-`None` result is available)
781
+ - `event_timeout`: Default per-event timeout in seconds resolved at processing time when `event.event_timeout` is `None`
782
+ - `event_slow_timeout`: Default slow-event warning threshold in seconds
783
+ - `event_handler_slow_timeout`: Default slow-handler warning threshold in seconds
784
+ - `event_handler_detect_file_paths`: Whether to auto-detect handler source file paths at registration time (slightly slower when enabled)
785
+ - `max_history_size`: Maximum number of events to keep in history (default: 50, `None` = unlimited, `0` = keep only in-flight events and drop completed events immediately)
786
+ - `max_history_drop`: If `True`, drop oldest history entries when full (even uncompleted events). If `False` (default), reject new emits once history reaches `max_history_size` (except when `max_history_size=0`, which never rejects on history size)
787
+ - `middlewares`: Optional list of `EventBusMiddleware` subclasses or instances that hook into handler execution for analytics, logging, retries, etc. (see [Middlewares](#middlewares) for more info)
788
+
789
+ Timeout precedence matches TS:
790
+
791
+ - Effective handler timeout = `min(resolved_handler_timeout, event_timeout)` where `resolved_handler_timeout` resolves in order: `handler.handler_timeout` -> `event.event_handler_timeout` -> `bus.event_timeout`.
792
+ - Slow handler warning threshold resolves in order: `handler.handler_slow_timeout` -> `event.event_handler_slow_timeout` -> `event.event_slow_timeout` -> `bus.event_handler_slow_timeout` -> `bus.event_slow_timeout`.
793
+
794
+ #### `EventBus` Properties
795
+
796
+ - `name`: The bus identifier
797
+ - `id`: Unique UUID7 for this bus instance
798
+ - `event_history`: Dict of all events the bus has seen by event_id (limited by `max_history_size`)
799
+ - `events_pending`: List of events waiting to be processed
800
+ - `events_started`: List of events currently being processed
801
+ - `events_completed`: List of completed events
802
+ - `all_instances`: Class-level WeakSet tracking all active EventBus instances (for memory monitoring)
803
+
804
+ #### `EventBus` Methods
805
+
806
+ ##### `on(event_type: str | Type[BaseEvent], handler: Callable)`
807
+
808
+ Subscribe a handler to events matching a specific event type or `'*'` for all events.
809
+
810
+ ```python
811
+ bus.on('UserEvent', handler_func) # By event type string
812
+ bus.on(UserEvent, handler_func) # By event class
813
+ bus.on('*', handler_func) # Wildcard - all events
814
+ ```
815
+
816
+ ##### `emit(event: BaseEvent) -> BaseEvent`
817
+
818
+ Enqueue an event for processing and return the pending `Event` immediately (synchronous).
819
+
820
+ ```python
821
+ event = bus.emit(MyEvent(data="test"))
822
+ result = await event # immediate-await path (queue-jumps when called inside a handler)
823
+ result_in_queue_order = await event.event_completed() # always waits in normal queue order
824
+ ```
825
+
826
+ **Note:** Queueing is unbounded. History pressure is controlled by `max_history_size` + `max_history_drop`:
827
+
828
+ - `max_history_drop=True`: absorb new events and trim old history entries (even uncompleted events).
829
+ - `max_history_drop=False`: raise `RuntimeError` when history is full.
830
+ - `max_history_size=0`: keep pending/in-flight events only; completed events are immediately removed from history.
831
+
832
+ ##### `find(event_type: str | Literal['*'] | Type[BaseEvent], *, where: Callable[[BaseEvent], bool]=None, child_of: BaseEvent | None=None, past: bool | float | timedelta=True, future: bool | float=False, **event_fields) -> BaseEvent | None`
833
+
834
+ Find an event matching criteria in history and/or future. This is the recommended unified method for event lookup.
835
+
836
+ **Parameters:**
837
+
838
+ - `event_type`: The event type string, `'*'` wildcard, or model class to find
839
+ - `where`: Predicate function for filtering (default: matches all)
840
+ - `child_of`: Only match events that are descendants of this parent event
841
+ - `past`: Controls history search behavior (default: `True`)
842
+ - `True`: search all history
843
+ - `False`: skip history search
844
+ - `float`/`timedelta`: search events from last N seconds only
845
+ - `future`: Controls future wait behavior (default: `False`)
846
+ - `True`: wait forever for matching event
847
+ - `False`: don't wait for future events
848
+ - `float`: wait up to N seconds for matching event
849
+ - `**event_fields`: Optional equality filters for any event fields (for example `event_status='completed'`, `user_id='u-1'`)
850
+
851
+ ```python
852
+ # Default call is non-blocking history lookup (past=True, future=False)
853
+ event = await bus.find(ResponseEvent)
854
+
855
+ # Find child of a specific parent event
856
+ child = await bus.find(ChildEvent, child_of=parent_event, future=5)
857
+
858
+ # Wait only for future events (ignore history)
859
+ event = await bus.find(ResponseEvent, past=False, future=5)
860
+
861
+ # Search recent history + optionally wait
862
+ event = await bus.find(ResponseEvent, past=5, future=5)
863
+
864
+ # Filter by event metadata
865
+ completed = await bus.find(ResponseEvent, event_status='completed')
866
+
867
+ # Wildcard match across all event types
868
+ any_completed = await bus.find('*', event_status='completed', past=True, future=False)
869
+ ```
870
+
871
+ ##### `event_is_child_of(event: BaseEvent, ancestor: BaseEvent) -> bool`
872
+
873
+ Check if event is a descendant of ancestor (child, grandchild, etc.).
874
+
875
+ ```python
876
+ if bus.event_is_child_of(child_event, parent_event):
877
+ print("child_event is a descendant of parent_event")
878
+ ```
879
+
880
+ ##### `event_is_parent_of(event: BaseEvent, descendant: BaseEvent) -> bool`
881
+
882
+ Check if event is an ancestor of descendant (parent, grandparent, etc.).
883
+
884
+ ```python
885
+ if bus.event_is_parent_of(parent_event, child_event):
886
+ print("parent_event is an ancestor of child_event")
887
+ ```
888
+
889
+ ##### `wait_until_idle(timeout: float | None=None)`
890
+
891
+ Wait until all events are processed and the bus is idle.
892
+
893
+ ```python
894
+ await bus.wait_until_idle() # wait indefinitely until EventBus has finished processing all events
895
+
896
+ await bus.wait_until_idle(timeout=5.0) # wait up to 5 seconds
897
+ ```
898
+
899
+ ##### `stop(timeout: float | None=None, clear: bool=False)`
900
+
901
+ Stop the event bus, optionally waiting for pending events and clearing memory.
902
+
903
+ ```python
904
+ await bus.stop(timeout=1.0) # Graceful stop, wait up to 1sec for pending and active events to finish processing
905
+ await bus.stop() # Immediate shutdown, aborts all pending and actively processing events
906
+ await bus.stop(clear=True) # Stop and clear all event history and handlers to free memory
907
+ ```
908
+
909
+ ---
910
+
911
+ </details>
912
+
913
+ <details>
914
+ <summary><strong><code>BaseEvent</code></strong></summary>
915
+
916
+ Base class for all events. Subclass `BaseEvent` to define your own events.
917
+
918
+ Make sure none of your own event data fields start with `event_` or `model_` to avoid clashing with `BaseEvent` or `pydantic` builtin attrs.
919
+
920
+ #### `BaseEvent` Fields
921
+
922
+ ```python
923
+ T_EventResultType = TypeVar('T_EventResultType', bound=Any, default=None)
924
+
925
+ class BaseEvent(BaseModel, Generic[T_EventResultType]):
926
+ # special config fields
927
+ event_id: str # Unique UUID7 identifier, auto-generated if not provided
928
+ event_type: str # Defaults to class name e.g. 'BaseEvent'
929
+ event_result_type: Any | None # Pydantic model/python type to validate handler return values, defaults to T_EventResultType
930
+ event_version: str # Defaults to '0.0.1' (override per class/instance for event payload versioning)
931
+ event_timeout: float | None = None # Event timeout in seconds (bus default resolved at processing time if None)
932
+ event_handler_timeout: float | None = None # Optional per-event handler timeout cap in seconds
933
+ event_handler_slow_timeout: float | None = None # Optional per-event slow-handler warning threshold
934
+ event_handler_concurrency: Literal['serial', 'parallel'] | None = None # optional per-event handler scheduling override (None -> bus default at processing time)
935
+ event_handler_completion: Literal['all', 'first'] | None = None # optional per-event completion override (None -> bus default at processing time)
936
+
937
+ # runtime state fields
938
+ event_status: Literal['pending', 'started', 'completed'] # event processing status (auto-set)
939
+ event_created_at: str # Canonical ISO timestamp with 9 fractional digits (auto-set)
940
+ event_started_at: str | None # Set when first handler starts
941
+ event_completed_at: str | None # Set when event processing completes
942
+ event_parent_id: str | None # Parent event ID that led to this event during handling (auto-set)
943
+ event_path: list[str] # List of bus labels traversed, e.g. BusName#ab12 (auto-set)
944
+ event_results: dict[str, EventResult] # Handler results {<handler uuid>: EventResult} (auto-set)
945
+ event_children: list[BaseEvent] # getter property to list any child events emitted during handling
946
+ event_bus: EventBus # getter property to get the bus the event was emitted on
947
+
948
+ # payload fields
949
+ # ... subclass BaseEvent to add your own event payload fields here ...
950
+ # some_key: str
951
+ # some_other_key: dict[str, int]
952
+ # ...
953
+ # (they should not start with event_* to avoid conflict with special built-in fields)
954
+ ```
955
+
956
+ #### `BaseEvent` Methods
957
+
958
+ ##### `await event`
959
+
960
+ Immediate-await path for the `Event` object.
961
+
962
+ - Outside a handler: waits for normal completion and returns the completed event.
963
+ - Inside a handler: queue-jumps this child event so it can run immediately, then returns the completed event.
964
+
965
+ ```python
966
+ event = bus.emit(MyEvent())
967
+ completed_event = await event
968
+
969
+ raw_result_values = [(await event_result) for event_result in completed_event.event_results.values()]
970
+ # equivalent to: completed_event.event_results_list() (see below)
971
+ ```
972
+
973
+ ##### `event_completed() -> Self`
974
+
975
+ Queue-order await path for an event.
976
+
977
+ - Never queue-jumps.
978
+ - Waits until the event is completed by normal runloop queue order.
979
+
980
+ ```python
981
+ event = bus.emit(MyEvent())
982
+ completed_event = await event.event_completed()
983
+ ```
984
+
985
+ ##### `first(timeout: float | None=None, *, raise_if_any: bool=False, raise_if_none: bool=False) -> Any`
986
+
987
+ Set `event_handler_completion='first'`, wait for completion, and return the first successful non-`None` handler result.
988
+
989
+ ```python
990
+ event = bus.emit(MyEvent())
991
+ value = await event.first()
992
+ ```
993
+
994
+ ##### `reset() -> Self`
995
+
996
+ Return a fresh event copy with runtime processing state reset back to pending.
997
+
998
+ - Intended for re-emitting an already-seen event as a fresh event (for example after crossing a bridge boundary).
999
+ - The original event object is not mutated, it returns a new copy with some fields reset.
1000
+ - A new UUIDv7 `event_id` is generated for the returned copy (to allow it to process as a separate event it needs a new unique uuid)
1001
+ - Runtime completion state is cleared (`event_results`, completion signal/flags, processed timestamp, emit context).
1002
+
1003
+ ##### `event_result_update(handler, eventbus: EventBus | None=None, **kwargs) -> EventResult`
1004
+
1005
+ Create or update a single `EventResult` entry for a handler.
1006
+
1007
+ - If no entry exists yet for the handler id, a pending result row is created.
1008
+ - Useful for deterministic seeding/rehydration before normal processing resumes.
1009
+ - Supports `status`, `result`, `error`, and `timeout` updates through `**kwargs`.
1010
+
1011
+ ```python
1012
+ seeded = event.event_result_update(handler=handler_entry, eventbus=bus, status='pending')
1013
+ seeded.update(status='completed', result='seeded')
1014
+ ```
1015
+
1016
+ ##### `event_result(timeout: float | None=None, include: EventResultFilter=None, raise_if_any: bool=True, raise_if_none: bool=True) -> Any`
1017
+
1018
+ Utility method helper to execute all the handlers and return the first handler's raw result value.
1019
+
1020
+ **Parameters:**
1021
+
1022
+ - `timeout`: Maximum time to wait for handlers to complete (None = use default event timeout)
1023
+ - `include`: Filter function to include only specific results (default: only non-None, non-exception results)
1024
+ - `raise_if_any`: If `True`, raise exception if any handler raises any `Exception` (`default: True`)
1025
+ - `raise_if_none`: If `True`, raise exception if results are empty / all results are `None` or `Exception` (`default: True`)
1026
+
1027
+ ```python
1028
+ # by default it returns the first successful non-None result value
1029
+ result = await event.event_result()
1030
+
1031
+ # Get result from first handler that returns a string
1032
+ valid_result = await event.event_result(include=lambda r: isinstance(r.result, str) and len(r.result) > 100)
1033
+
1034
+ # Get result but don't raise exceptions or error for 0 results, just return None
1035
+ result_or_none = await event.event_result(raise_if_any=False, raise_if_none=False)
1036
+ ```
1037
+
1038
+ ##### `event_results_list(timeout: float | None=None, include: EventResultFilter=None, raise_if_any: bool=True, raise_if_none: bool=True) -> list[Any]`
1039
+
1040
+ Utility method helper to get all raw result values in a list.
1041
+
1042
+ **Parameters:**
1043
+
1044
+ - `timeout`: Maximum time to wait for handlers to complete (None = use default event timeout)
1045
+ - `include`: Filter function to include only specific results (default: only non-None, non-exception results)
1046
+ - `raise_if_any`: If `True`, raise exception if any handler raises any `Exception` (`default: True`)
1047
+ - `raise_if_none`: If `True`, raise exception if results are empty / all results are `None` or `Exception` (`default: True`)
1048
+
1049
+ ```python
1050
+ # by default it returns all successful non-None result values
1051
+ results = await event.event_results_list()
1052
+ # [result1, result2]
1053
+
1054
+ # Only include results that are strings longer than 10 characters
1055
+ filtered_results = await event.event_results_list(include=lambda r: isinstance(r.result, str) and len(r.result) > 10)
1056
+
1057
+ # Get all results without raising on errors
1058
+ all_results = await event.event_results_list(raise_if_any=False, raise_if_none=False)
1059
+ ```
1060
+
1061
+ `event_results_list()` is the canonical collection helper for multiple handler return values.
1062
+
1063
+ ##### `event_bus` (property)
1064
+
1065
+ Shortcut to get the `EventBus` that is currently processing this event. Can be used to avoid having to pass an `EventBus` instance to your handlers.
1066
+
1067
+ ```python
1068
+ bus = EventBus()
1069
+
1070
+ async def some_handler(event: MyEvent):
1071
+ # You can always emit directly to any bus you have a reference to
1072
+ child_event = bus.emit(ChildEvent())
1073
+
1074
+ # OR use the event.event_bus shortcut to get the current bus:
1075
+ child_event = await event.event_bus.emit(ChildEvent())
1076
+ ```
1077
+
1078
+ ---
1079
+
1080
+ </details>
1081
+
1082
+ <details>
1083
+ <summary><strong><code>EventResult</code></strong></summary>
1084
+
1085
+ The placeholder object that represents the pending result from a single handler executing an event.
1086
+ `Event.event_results` contains a `dict[PythonIdStr, EventResult]` in the shape of `{handler_id: EventResult()}`.
1087
+
1088
+ You generally won't interact with this class directly—the bus instantiates and updates it for you—but its API is documented here for advanced integrations and custom emit loops.
1089
+
1090
+ #### `EventResult` Fields
1091
+
1092
+ ```python
1093
+ class EventResult(BaseModel):
1094
+ id: str # Unique identifier
1095
+ handler_id: str # Handler function ID
1096
+ handler_name: str # Handler function name
1097
+ eventbus_id: str # Bus that executed this handler
1098
+ eventbus_name: str # Bus name
1099
+
1100
+ status: str # 'pending', 'started', 'completed', 'error'
1101
+ result: Any # Handler return value
1102
+ error: BaseException | None # Captured exception if the handler failed
1103
+
1104
+ started_at: str | None # Canonical ISO timestamp when handler started
1105
+ completed_at: str | None # Canonical ISO timestamp when handler completed
1106
+ timeout: float | None # Handler timeout in seconds
1107
+ event_children: list[BaseEvent] # child events emitted during handler execution
1108
+ ```
1109
+
1110
+ #### `EventResult` Methods
1111
+
1112
+ ##### `await result`
1113
+
1114
+ Await the `EventResult` object directly to get the raw result value.
1115
+
1116
+ ```python
1117
+ handler_result = event.event_results['handler_id']
1118
+ value = await handler_result # Returns result or raises an exception if handler hits an error
1119
+ ```
1120
+
1121
+ - `run_handler(event, handler, *, eventbus, timeout, enter_handler_context, exit_handler_context, format_exception_for_log)`
1122
+ Low-level helper that runs the handler, updates timing/status fields, captures errors, and notifies its completion signal. `EventBus._run_handler()` (private/internal) delegates to this; you generally should not call either directly unless you are extending internals.
1123
+
1124
+ </details>
1125
+
1126
+ <details>
1127
+ <summary><strong><code>EventHandler</code></strong></summary>
1128
+
1129
+ Serializable metadata wrapper around a registered handler callable.
1130
+
1131
+ You usually get an `EventHandler` back from `bus.on(...)`, can pass it to `bus.off(...)`, and may see it in middleware hooks like `on_bus_handlers_change(...)`.
1132
+
1133
+ #### `EventHandler` Fields
1134
+
1135
+ ```python
1136
+ class EventHandler(BaseModel):
1137
+ id: str # Stable handler identifier
1138
+ handler_name: str # Callable name
1139
+ handler_file_path: str | None # Source file path (if known)
1140
+ handler_timeout: float | None # Optional per-handler timeout override
1141
+ handler_slow_timeout: float | None # Optional "slow handler" threshold
1142
+ handler_registered_at: str # Registration timestamp (ISO string, 9 fractional digits)
1143
+ event_pattern: str # Registered event pattern (type name or '*')
1144
+ eventbus_name: str # Owning EventBus name
1145
+ eventbus_id: str # Owning EventBus ID
1146
+ ```
1147
+
1148
+ The raw callable is stored on `handler`, but is excluded from JSON serialization (`model_dump(mode='json', exclude={'handler'})`).
1149
+
1150
+ #### `EventHandler` Properties and Methods
1151
+
1152
+ - `label` (property): Short display label like `my_handler#abcd`.
1153
+ - `model_dump(mode='json', exclude={'handler'}) -> dict[str, Any]`: JSON-compatible metadata dict (callable excluded).
1154
+ - `from_json_dict(data, handler=None) -> EventHandler`: Rebuilds metadata; optional callable reattachment.
1155
+ - `from_callable(...) -> EventHandler`: Build a new handler entry from a callable plus bus/pattern metadata.
1156
+
1157
+ ---
1158
+
1159
+ </details>
1160
+
1161
+ ## 🏃 Performance (Python)
1162
+
1163
+ ```bash
1164
+ uv run tests/performance_runtime.py # run the performance test suite in python
1165
+ ```
1166
+
1167
+ | Runtime | 1 bus x 50k events x 1 handler | 500 buses x 100 events x 1 handler | 1 bus x 1 event x 50k parallel handlers | 1 bus x 50k events x 50k one-off handlers | Worst case (N buses x N events x N handlers) |
1168
+ | ------- | -------------------------------- | ---------------------------------- | --------------------------------------- | ----------------------------------------- | -------------------------------------------- |
1169
+ | Python | `0.179ms/event`, `0.235kb/event` | `0.191ms/event`, `0.191kb/event` | `0.035ms/handler`, `8.164kb/handler` | `0.255ms/event`, `0.185kb/event` | `0.351ms/event`, `5.867kb/event` |
1170
+
1171
+ <br/>
1172
+
1173
+ ---
1174
+
1175
+ ---
1176
+
1177
+ <br/>
1178
+
1179
+ ## 👾 Development
1180
+
1181
+ Set up the python development environment using `uv`:
1182
+
1183
+ ```bash
1184
+ git clone https://github.com/ArchiveBox/abxbus && cd abxbus
1185
+
1186
+ # Create virtual environment with Python 3.12
1187
+ uv venv --python 3.12
1188
+
1189
+ # Activate virtual environment (varies by OS)
1190
+ source .venv/bin/activate # On Unix/macOS
1191
+ # or
1192
+ .venv\Scripts\activate # On Windows
1193
+
1194
+ # Install dependencies
1195
+ uv sync --dev --all-extras
1196
+ ```
1197
+
1198
+ Recommended once per clone:
1199
+
1200
+ ```bash
1201
+ prek install # install pre-commit hooks
1202
+ prek run --all-files # run pre-commit hooks on all files manually
1203
+ ```
1204
+
1205
+ ```bash
1206
+ # Run linter & type checker
1207
+ uv run ruff check --fix
1208
+ uv run ruff format
1209
+ uv run pyright
1210
+
1211
+ # Run all tests
1212
+ uv run pytest -vxs --full-trace tests/
1213
+
1214
+ # Run specific test file
1215
+ uv run pytest tests/test_eventbus.py
1216
+
1217
+ # Run Python perf suite
1218
+ uv run tests/performance_runtime.py
1219
+
1220
+ # Run the entire lint+test+examples+perf suite for both python and ts
1221
+ ./test.sh
1222
+ ```
1223
+
1224
+ > For AbxBus-TS development see the `abxbus-ts/README.md` `# Development` section.
1225
+
1226
+ ## 🔗 Inspiration
1227
+
1228
+ - https://www.cosmicpython.com/book/chapter_08_events_and_message_bus.html#message_bus_diagram ⭐️
1229
+ - https://developer.mozilla.org/en-US/docs/Web/API/EventTarget ⭐️
1230
+ - https://github.com/sindresorhus/emittery ⭐️ (equivalent for JS), https://github.com/EventEmitter2/EventEmitter2, https://github.com/vitaly-t/sub-events
1231
+ - https://github.com/pytest-dev/pluggy ⭐️
1232
+ - https://github.com/teamhide/fastapi-event ⭐️
1233
+ - https://github.com/ethereum/lahja ⭐️
1234
+ - https://github.com/enricostara/eventure ⭐️
1235
+ - https://github.com/akhundMurad/diator ⭐️
1236
+ - https://github.com/n89nanda/pyeventbus
1237
+ - https://github.com/iunary/aioemit
1238
+ - https://github.com/dboslee/evently
1239
+ - https://github.com/faust-streaming/faust
1240
+ - https://github.com/ArcletProject/Letoderea
1241
+ - https://github.com/seanpar203/event-bus
1242
+ - https://github.com/n89nanda/pyeventbus
1243
+ - https://github.com/nicolaszein/py-async-bus
1244
+ - https://github.com/AngusWG/simple-event-bus
1245
+ - https://www.joeltok.com/posts/2021-03-building-an-event-bus-in-python/
1246
+
1247
+ ---
1248
+
1249
+ > [🧠 DeepWiki Docs](https://deepwiki.com/ArchiveBox/abxbus)
1250
+ > <img width="400" alt="image" src="https://github.com/user-attachments/assets/cedb5a2e-0643-4240-9a3d-5f27cb8b5741" /><img width="400" alt="image" src="https://github.com/user-attachments/assets/3ee0ee8c-8322-449f-979b-5c99ba6bd960" />
1251
+
1252
+ ## 🏛️ License
1253
+
1254
+ This project is licensed under the MIT License.
1255
+
1256
+ This repo is the main active fork that adds many new features and performance enhancements over the original project (which has since gone stale): https://github.com/browser-use/abxbus