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 +1256 -0
- abxbus-2.4.2/README.md +1222 -0
- abxbus-2.4.2/abxbus/__init__.py +70 -0
- abxbus-2.4.2/abxbus/base_event.py +1675 -0
- abxbus-2.4.2/abxbus/bridge_jsonl.py +156 -0
- abxbus-2.4.2/abxbus/bridge_nats.py +131 -0
- abxbus-2.4.2/abxbus/bridge_postgres.py +333 -0
- abxbus-2.4.2/abxbus/bridge_redis.py +228 -0
- abxbus-2.4.2/abxbus/bridge_sqlite.py +331 -0
- abxbus-2.4.2/abxbus/bridges.py +375 -0
- abxbus-2.4.2/abxbus/event_bus.py +2309 -0
- abxbus-2.4.2/abxbus/event_handler.py +412 -0
- abxbus-2.4.2/abxbus/event_history.py +253 -0
- abxbus-2.4.2/abxbus/event_result.py +11 -0
- abxbus-2.4.2/abxbus/events_suck.py +205 -0
- abxbus-2.4.2/abxbus/helpers.py +307 -0
- abxbus-2.4.2/abxbus/jsonschema.py +397 -0
- abxbus-2.4.2/abxbus/lock_manager.py +231 -0
- abxbus-2.4.2/abxbus/logging.py +468 -0
- abxbus-2.4.2/abxbus/middlewares.py +565 -0
- abxbus-2.4.2/abxbus/retry.py +561 -0
- abxbus-2.4.2/abxbus/typings/uuid_extensions/__init__.pyi +1 -0
- abxbus-2.4.2/pyproject.toml +168 -0
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
|
+
[](https://deepwiki.com/ArchiveBox/abxbus) [](https://pypi.org/project/abxbus/) [](https://pepy.tech/projects/abxbus) 
|
|
40
|
+
|
|
41
|
+
[](https://deepwiki.com/ArchiveBox/abxbus/3-typescript-implementation) [](https://www.npmjs.com/package/abxbus) [](https://pepy.tech/projects/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
|