flowevent 1.2__py3-none-any.whl
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.
- eventflow/__init__.py +123 -0
- eventflow/_callbacks.py +22 -0
- eventflow/_lifecycle.py +31 -0
- eventflow/actions.py +53 -0
- eventflow/async_event_bus.py +640 -0
- eventflow/base.py +180 -0
- eventflow/callback.py +318 -0
- eventflow/compatibility.py +14 -0
- eventflow/context.py +103 -0
- eventflow/contracts.py +40 -0
- eventflow/engine.py +466 -0
- eventflow/errors.py +21 -0
- eventflow/event_bus.py +594 -0
- eventflow/filtering.py +57 -0
- eventflow/history.py +111 -0
- eventflow/observability.py +133 -0
- eventflow/pipeline/__init__.py +7 -0
- eventflow/pipeline/async_.py +129 -0
- eventflow/pipeline/base.py +203 -0
- eventflow/pipeline/contracts.py +24 -0
- eventflow/pipeline/registry.py +95 -0
- eventflow/pipeline/sync.py +92 -0
- eventflow/py.typed +0 -0
- eventflow/scopes.py +185 -0
- eventflow/stream.py +241 -0
- eventflow/subscription.py +116 -0
- flowevent-1.2.dist-info/METADATA +1171 -0
- flowevent-1.2.dist-info/RECORD +31 -0
- flowevent-1.2.dist-info/WHEEL +5 -0
- flowevent-1.2.dist-info/licenses/LICENSE +21 -0
- flowevent-1.2.dist-info/top_level.txt +1 -0
eventflow/engine.py
ADDED
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
"""Application-level engines for managing named event channels."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import threading
|
|
6
|
+
from collections.abc import Awaitable, Callable
|
|
7
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .actions import (
|
|
11
|
+
AsyncEventBusActions,
|
|
12
|
+
EventBusActions,
|
|
13
|
+
EventErrorContext,
|
|
14
|
+
EventTimeoutContext,
|
|
15
|
+
)
|
|
16
|
+
from .async_event_bus import AsyncEventBus
|
|
17
|
+
from .base import validate_positive_capacity
|
|
18
|
+
from .compatibility import ExceptionGroup
|
|
19
|
+
from .contracts import DeliveryMode, ErrorPolicy, PublishResult
|
|
20
|
+
from .errors import EventBusClosedError
|
|
21
|
+
from .event_bus import EventBus, _validate_executor
|
|
22
|
+
from .filtering import EventFilter, EventType
|
|
23
|
+
from .subscription import AsyncSubscription, Subscription
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _channel_name(name: str) -> str:
|
|
27
|
+
normalized = str(name).strip().lower()
|
|
28
|
+
if not normalized:
|
|
29
|
+
raise ValueError("Event channel name cannot be empty")
|
|
30
|
+
return normalized
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _validate_history_capacity(history: object) -> int | None:
|
|
34
|
+
if history is None:
|
|
35
|
+
return None
|
|
36
|
+
if isinstance(history, bool) or not isinstance(history, int):
|
|
37
|
+
raise TypeError("Engine history must be a positive integer capacity or None")
|
|
38
|
+
if history <= 0:
|
|
39
|
+
raise ValueError("Engine history capacity must be positive")
|
|
40
|
+
return history
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class EventEngine:
|
|
44
|
+
"""Create and manage thread-safe event buses by a readable channel name."""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
*,
|
|
49
|
+
history: int | None = None,
|
|
50
|
+
error_policy: ErrorPolicy = ErrorPolicy.ISOLATE,
|
|
51
|
+
executor: ThreadPoolExecutor | None = None,
|
|
52
|
+
actions: EventBusActions[Any] | None = None,
|
|
53
|
+
on_before: Callable[[Any], None] | None = None,
|
|
54
|
+
on_after: Callable[[Any, PublishResult], None] | None = None,
|
|
55
|
+
on_error: Callable[[EventErrorContext[Any]], None] | None = None,
|
|
56
|
+
on_timeout: Callable[[EventTimeoutContext[Any]], None] | None = None,
|
|
57
|
+
) -> None:
|
|
58
|
+
self._history = _validate_history_capacity(history)
|
|
59
|
+
self._error_policy = ErrorPolicy(error_policy)
|
|
60
|
+
self._executor = _validate_executor(executor)
|
|
61
|
+
direct_actions = (on_before, on_after, on_error, on_timeout)
|
|
62
|
+
if actions is not None and any(action is not None for action in direct_actions):
|
|
63
|
+
raise ValueError("Use actions or direct on_* callbacks, not both")
|
|
64
|
+
self._actions = actions or EventBusActions(
|
|
65
|
+
on_before=on_before,
|
|
66
|
+
on_after=on_after,
|
|
67
|
+
on_error=on_error,
|
|
68
|
+
on_timeout=on_timeout,
|
|
69
|
+
)
|
|
70
|
+
self._channels: dict[str, EventBus[Any]] = {}
|
|
71
|
+
self._pending_close: dict[int, EventBus[Any]] = {}
|
|
72
|
+
self._lock = threading.RLock()
|
|
73
|
+
self._running = True
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def running(self) -> bool:
|
|
77
|
+
with self._lock:
|
|
78
|
+
return self._running
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def channel_names(self) -> tuple[str, ...]:
|
|
82
|
+
with self._lock:
|
|
83
|
+
return tuple(sorted(self._channels))
|
|
84
|
+
|
|
85
|
+
def channel(self, name: str) -> EventBus[Any]:
|
|
86
|
+
"""Return a channel, creating it with engine defaults when necessary."""
|
|
87
|
+
|
|
88
|
+
name = _channel_name(name)
|
|
89
|
+
with self._lock:
|
|
90
|
+
bus = self._channels.get(name)
|
|
91
|
+
if bus is None:
|
|
92
|
+
bus = EventBus(
|
|
93
|
+
history=self._history,
|
|
94
|
+
error_policy=self._error_policy,
|
|
95
|
+
executor=self._executor,
|
|
96
|
+
actions=self._actions,
|
|
97
|
+
)
|
|
98
|
+
self._channels[name] = bus
|
|
99
|
+
self._running = True
|
|
100
|
+
return bus
|
|
101
|
+
|
|
102
|
+
def add(self, name: str, bus: EventBus[Any]) -> EventBus[Any]:
|
|
103
|
+
"""Register a bus owned by this engine under a channel name.
|
|
104
|
+
|
|
105
|
+
An open bus cannot be claimed by another engine; that registration raises
|
|
106
|
+
ValueError.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
if not isinstance(bus, EventBus):
|
|
110
|
+
raise TypeError("Event engine channels must be EventBus instances")
|
|
111
|
+
if bus.closed:
|
|
112
|
+
raise EventBusClosedError("Cannot add a closed EventBus to an engine")
|
|
113
|
+
name = _channel_name(name)
|
|
114
|
+
with self._lock:
|
|
115
|
+
if name in self._channels:
|
|
116
|
+
raise ValueError(f"Event channel already exists: {name}")
|
|
117
|
+
if any(channel is bus for channel in self._channels.values()) or any(
|
|
118
|
+
channel is bus for channel in self._pending_close.values()
|
|
119
|
+
):
|
|
120
|
+
raise ValueError("An EventBus instance can belong to only one channel")
|
|
121
|
+
self._channels[name] = bus
|
|
122
|
+
if not bus._claim_engine_ownership(self):
|
|
123
|
+
del self._channels[name]
|
|
124
|
+
raise ValueError("An EventBus instance can belong to only one engine")
|
|
125
|
+
self._running = True
|
|
126
|
+
return bus
|
|
127
|
+
|
|
128
|
+
def publish(self, channel: str, event: Any) -> PublishResult:
|
|
129
|
+
return self.channel(channel).publish(event)
|
|
130
|
+
|
|
131
|
+
def subscribe(
|
|
132
|
+
self,
|
|
133
|
+
channel: str,
|
|
134
|
+
callback: Callable[[Any], None],
|
|
135
|
+
*,
|
|
136
|
+
event_filter: EventFilter[Any] | None = None,
|
|
137
|
+
event_type: EventType | None = None,
|
|
138
|
+
predicate: Callable[[Any], bool] | None = None,
|
|
139
|
+
once: bool = False,
|
|
140
|
+
times: int | None = None,
|
|
141
|
+
replay: int = 0,
|
|
142
|
+
) -> Subscription:
|
|
143
|
+
return self.channel(channel).subscribe(
|
|
144
|
+
callback,
|
|
145
|
+
event_filter=event_filter,
|
|
146
|
+
event_type=event_type,
|
|
147
|
+
predicate=predicate,
|
|
148
|
+
once=once,
|
|
149
|
+
times=times,
|
|
150
|
+
replay=replay,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def once(
|
|
154
|
+
self,
|
|
155
|
+
channel: str,
|
|
156
|
+
callback: Callable[[Any], None],
|
|
157
|
+
*,
|
|
158
|
+
event_filter: EventFilter[Any] | None = None,
|
|
159
|
+
event_type: EventType | None = None,
|
|
160
|
+
predicate: Callable[[Any], bool] | None = None,
|
|
161
|
+
replay: int = 0,
|
|
162
|
+
) -> Subscription:
|
|
163
|
+
return self.channel(channel).once(
|
|
164
|
+
callback,
|
|
165
|
+
event_filter=event_filter,
|
|
166
|
+
event_type=event_type,
|
|
167
|
+
predicate=predicate,
|
|
168
|
+
replay=replay,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def publish_every(
|
|
172
|
+
self,
|
|
173
|
+
channel: str,
|
|
174
|
+
event: Any,
|
|
175
|
+
interval: float,
|
|
176
|
+
*,
|
|
177
|
+
times: int | None = None,
|
|
178
|
+
immediately: bool = True,
|
|
179
|
+
) -> Subscription:
|
|
180
|
+
return self.channel(channel).publish_every(
|
|
181
|
+
event,
|
|
182
|
+
interval,
|
|
183
|
+
times=times,
|
|
184
|
+
immediately=immediately,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def wait_for(
|
|
188
|
+
self,
|
|
189
|
+
channel: str,
|
|
190
|
+
*,
|
|
191
|
+
event_filter: EventFilter[Any] | None = None,
|
|
192
|
+
event_type: EventType | None = None,
|
|
193
|
+
predicate: Callable[[Any], bool] | None = None,
|
|
194
|
+
timeout: float | None = None,
|
|
195
|
+
) -> Any | None:
|
|
196
|
+
return self.channel(channel).wait_for(
|
|
197
|
+
event_filter=event_filter,
|
|
198
|
+
event_type=event_type,
|
|
199
|
+
predicate=predicate,
|
|
200
|
+
timeout=timeout,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
def remove(self, name: str) -> bool:
|
|
204
|
+
name = _channel_name(name)
|
|
205
|
+
with self._lock:
|
|
206
|
+
bus = self._channels.pop(name, None)
|
|
207
|
+
if bus is not None:
|
|
208
|
+
self._pending_close[id(bus)] = bus
|
|
209
|
+
if bus is None:
|
|
210
|
+
return False
|
|
211
|
+
bus.close()
|
|
212
|
+
with self._lock:
|
|
213
|
+
self._pending_close.pop(id(bus), None)
|
|
214
|
+
return True
|
|
215
|
+
|
|
216
|
+
def start(self) -> None:
|
|
217
|
+
"""Keep Service-style lifecycle symmetry; channels start lazily."""
|
|
218
|
+
|
|
219
|
+
with self._lock:
|
|
220
|
+
self._running = True
|
|
221
|
+
|
|
222
|
+
def stop(self) -> None:
|
|
223
|
+
with self._lock:
|
|
224
|
+
channels = tuple(self._channels.values())
|
|
225
|
+
self._channels.clear()
|
|
226
|
+
for channel in channels:
|
|
227
|
+
self._pending_close[id(channel)] = channel
|
|
228
|
+
pending = tuple(self._pending_close.values())
|
|
229
|
+
self._running = False
|
|
230
|
+
errors: list[Exception] = []
|
|
231
|
+
for channel in pending:
|
|
232
|
+
try:
|
|
233
|
+
channel.close()
|
|
234
|
+
except Exception as exc:
|
|
235
|
+
errors.append(exc)
|
|
236
|
+
else:
|
|
237
|
+
with self._lock:
|
|
238
|
+
self._pending_close.pop(id(channel), None)
|
|
239
|
+
if errors:
|
|
240
|
+
raise ExceptionGroup("One or more event channels failed to close", errors)
|
|
241
|
+
|
|
242
|
+
close = stop
|
|
243
|
+
|
|
244
|
+
def __enter__(self) -> "EventEngine":
|
|
245
|
+
return self
|
|
246
|
+
|
|
247
|
+
def __exit__(self, *_args: object) -> None:
|
|
248
|
+
self.stop()
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class AsyncEventEngine:
|
|
252
|
+
"""Create and manage asyncio-native event buses by channel name."""
|
|
253
|
+
|
|
254
|
+
def __init__(
|
|
255
|
+
self,
|
|
256
|
+
*,
|
|
257
|
+
history: int | None = None,
|
|
258
|
+
error_policy: ErrorPolicy = ErrorPolicy.ISOLATE,
|
|
259
|
+
delivery_mode: DeliveryMode = DeliveryMode.SEQUENTIAL,
|
|
260
|
+
max_concurrency: int | None = None,
|
|
261
|
+
actions: AsyncEventBusActions[Any] | None = None,
|
|
262
|
+
on_before: Callable[[Any], Awaitable[None]] | None = None,
|
|
263
|
+
on_after: Callable[[Any, PublishResult], Awaitable[None]] | None = None,
|
|
264
|
+
on_error: Callable[[EventErrorContext[Any]], Awaitable[None]] | None = None,
|
|
265
|
+
on_timeout: Callable[[EventTimeoutContext[Any]], Awaitable[None]] | None = None,
|
|
266
|
+
) -> None:
|
|
267
|
+
self._history = _validate_history_capacity(history)
|
|
268
|
+
self._error_policy = ErrorPolicy(error_policy)
|
|
269
|
+
self._delivery_mode = DeliveryMode(delivery_mode)
|
|
270
|
+
self._max_concurrency = (
|
|
271
|
+
None
|
|
272
|
+
if max_concurrency is None
|
|
273
|
+
else validate_positive_capacity(
|
|
274
|
+
max_concurrency,
|
|
275
|
+
name="Maximum concurrent deliveries",
|
|
276
|
+
)
|
|
277
|
+
)
|
|
278
|
+
if self._max_concurrency is not None and self._delivery_mode is not DeliveryMode.CONCURRENT:
|
|
279
|
+
raise ValueError("max_concurrency requires DeliveryMode.CONCURRENT")
|
|
280
|
+
direct_actions = (on_before, on_after, on_error, on_timeout)
|
|
281
|
+
if actions is not None and any(action is not None for action in direct_actions):
|
|
282
|
+
raise ValueError("Use actions or direct on_* callbacks, not both")
|
|
283
|
+
self._actions = actions or AsyncEventBusActions(
|
|
284
|
+
on_before=on_before,
|
|
285
|
+
on_after=on_after,
|
|
286
|
+
on_error=on_error,
|
|
287
|
+
on_timeout=on_timeout,
|
|
288
|
+
)
|
|
289
|
+
self._channels: dict[str, AsyncEventBus[Any]] = {}
|
|
290
|
+
self._pending_close: dict[int, AsyncEventBus[Any]] = {}
|
|
291
|
+
self._lock = threading.RLock()
|
|
292
|
+
self._running = True
|
|
293
|
+
|
|
294
|
+
@property
|
|
295
|
+
def running(self) -> bool:
|
|
296
|
+
with self._lock:
|
|
297
|
+
return self._running
|
|
298
|
+
|
|
299
|
+
@property
|
|
300
|
+
def channel_names(self) -> tuple[str, ...]:
|
|
301
|
+
with self._lock:
|
|
302
|
+
return tuple(sorted(self._channels))
|
|
303
|
+
|
|
304
|
+
def channel(self, name: str) -> AsyncEventBus[Any]:
|
|
305
|
+
name = _channel_name(name)
|
|
306
|
+
with self._lock:
|
|
307
|
+
bus = self._channels.get(name)
|
|
308
|
+
if bus is None:
|
|
309
|
+
bus = AsyncEventBus(
|
|
310
|
+
history=self._history,
|
|
311
|
+
error_policy=self._error_policy,
|
|
312
|
+
delivery_mode=self._delivery_mode,
|
|
313
|
+
max_concurrency=self._max_concurrency,
|
|
314
|
+
actions=self._actions,
|
|
315
|
+
)
|
|
316
|
+
self._channels[name] = bus
|
|
317
|
+
self._running = True
|
|
318
|
+
return bus
|
|
319
|
+
|
|
320
|
+
def add(self, name: str, bus: AsyncEventBus[Any]) -> AsyncEventBus[Any]:
|
|
321
|
+
"""Register a bus owned by this engine under a channel name.
|
|
322
|
+
|
|
323
|
+
An open bus cannot be claimed by another engine; that registration raises
|
|
324
|
+
ValueError.
|
|
325
|
+
"""
|
|
326
|
+
|
|
327
|
+
if not isinstance(bus, AsyncEventBus):
|
|
328
|
+
raise TypeError("Async event engine channels must be AsyncEventBus instances")
|
|
329
|
+
if bus.closed:
|
|
330
|
+
raise EventBusClosedError("Cannot add a closed AsyncEventBus to an engine")
|
|
331
|
+
name = _channel_name(name)
|
|
332
|
+
with self._lock:
|
|
333
|
+
if name in self._channels:
|
|
334
|
+
raise ValueError(f"Async event channel already exists: {name}")
|
|
335
|
+
if any(channel is bus for channel in self._channels.values()) or any(
|
|
336
|
+
channel is bus for channel in self._pending_close.values()
|
|
337
|
+
):
|
|
338
|
+
raise ValueError("An AsyncEventBus instance can belong to only one channel")
|
|
339
|
+
self._channels[name] = bus
|
|
340
|
+
if not bus._claim_engine_ownership(self):
|
|
341
|
+
del self._channels[name]
|
|
342
|
+
raise ValueError("An AsyncEventBus instance can belong to only one engine")
|
|
343
|
+
self._running = True
|
|
344
|
+
return bus
|
|
345
|
+
|
|
346
|
+
async def publish(self, channel: str, event: Any) -> PublishResult:
|
|
347
|
+
return await self.channel(channel).publish(event)
|
|
348
|
+
|
|
349
|
+
async def subscribe(
|
|
350
|
+
self,
|
|
351
|
+
channel: str,
|
|
352
|
+
callback: Callable[[Any], Awaitable[None]],
|
|
353
|
+
*,
|
|
354
|
+
event_filter: EventFilter[Any] | None = None,
|
|
355
|
+
event_type: EventType | None = None,
|
|
356
|
+
predicate: Callable[[Any], bool] | None = None,
|
|
357
|
+
once: bool = False,
|
|
358
|
+
times: int | None = None,
|
|
359
|
+
replay: int = 0,
|
|
360
|
+
) -> AsyncSubscription:
|
|
361
|
+
return await self.channel(channel).subscribe(
|
|
362
|
+
callback,
|
|
363
|
+
event_filter=event_filter,
|
|
364
|
+
event_type=event_type,
|
|
365
|
+
predicate=predicate,
|
|
366
|
+
once=once,
|
|
367
|
+
times=times,
|
|
368
|
+
replay=replay,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
async def once(
|
|
372
|
+
self,
|
|
373
|
+
channel: str,
|
|
374
|
+
callback: Callable[[Any], Awaitable[None]],
|
|
375
|
+
*,
|
|
376
|
+
event_filter: EventFilter[Any] | None = None,
|
|
377
|
+
event_type: EventType | None = None,
|
|
378
|
+
predicate: Callable[[Any], bool] | None = None,
|
|
379
|
+
replay: int = 0,
|
|
380
|
+
) -> AsyncSubscription:
|
|
381
|
+
return await self.channel(channel).once(
|
|
382
|
+
callback,
|
|
383
|
+
event_filter=event_filter,
|
|
384
|
+
event_type=event_type,
|
|
385
|
+
predicate=predicate,
|
|
386
|
+
replay=replay,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
async def publish_every(
|
|
390
|
+
self,
|
|
391
|
+
channel: str,
|
|
392
|
+
event: Any,
|
|
393
|
+
interval: float,
|
|
394
|
+
*,
|
|
395
|
+
times: int | None = None,
|
|
396
|
+
immediately: bool = True,
|
|
397
|
+
) -> AsyncSubscription:
|
|
398
|
+
return await self.channel(channel).publish_every(
|
|
399
|
+
event,
|
|
400
|
+
interval,
|
|
401
|
+
times=times,
|
|
402
|
+
immediately=immediately,
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
async def wait_for(
|
|
406
|
+
self,
|
|
407
|
+
channel: str,
|
|
408
|
+
*,
|
|
409
|
+
event_filter: EventFilter[Any] | None = None,
|
|
410
|
+
event_type: EventType | None = None,
|
|
411
|
+
predicate: Callable[[Any], bool] | None = None,
|
|
412
|
+
timeout: float | None = None,
|
|
413
|
+
) -> Any | None:
|
|
414
|
+
return await self.channel(channel).wait_for(
|
|
415
|
+
event_filter=event_filter,
|
|
416
|
+
event_type=event_type,
|
|
417
|
+
predicate=predicate,
|
|
418
|
+
timeout=timeout,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
async def remove(self, name: str) -> bool:
|
|
422
|
+
name = _channel_name(name)
|
|
423
|
+
with self._lock:
|
|
424
|
+
bus = self._channels.pop(name, None)
|
|
425
|
+
if bus is not None:
|
|
426
|
+
self._pending_close[id(bus)] = bus
|
|
427
|
+
if bus is None:
|
|
428
|
+
return False
|
|
429
|
+
await bus.close()
|
|
430
|
+
with self._lock:
|
|
431
|
+
self._pending_close.pop(id(bus), None)
|
|
432
|
+
return True
|
|
433
|
+
|
|
434
|
+
async def start(self) -> None:
|
|
435
|
+
"""Keep lifecycle symmetry; channels bind to a loop when first used."""
|
|
436
|
+
|
|
437
|
+
with self._lock:
|
|
438
|
+
self._running = True
|
|
439
|
+
|
|
440
|
+
async def stop(self) -> None:
|
|
441
|
+
with self._lock:
|
|
442
|
+
channels = tuple(self._channels.values())
|
|
443
|
+
self._channels.clear()
|
|
444
|
+
for channel in channels:
|
|
445
|
+
self._pending_close[id(channel)] = channel
|
|
446
|
+
pending = tuple(self._pending_close.values())
|
|
447
|
+
self._running = False
|
|
448
|
+
errors: list[Exception] = []
|
|
449
|
+
for channel in pending:
|
|
450
|
+
try:
|
|
451
|
+
await channel.close()
|
|
452
|
+
except Exception as exc:
|
|
453
|
+
errors.append(exc)
|
|
454
|
+
else:
|
|
455
|
+
with self._lock:
|
|
456
|
+
self._pending_close.pop(id(channel), None)
|
|
457
|
+
if errors:
|
|
458
|
+
raise ExceptionGroup("One or more async event channels failed to close", errors)
|
|
459
|
+
|
|
460
|
+
close = stop
|
|
461
|
+
|
|
462
|
+
async def __aenter__(self) -> "AsyncEventEngine":
|
|
463
|
+
return self
|
|
464
|
+
|
|
465
|
+
async def __aexit__(self, *_args: object) -> None:
|
|
466
|
+
await self.stop()
|
eventflow/errors.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Errors raised by the in-process event buses."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class EventBusError(RuntimeError):
|
|
5
|
+
"""Base error for event bus operations."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class EventBusClosedError(EventBusError):
|
|
9
|
+
"""Raised when an operation requires an open event bus."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class InvalidEventHandlerError(EventBusError, TypeError):
|
|
13
|
+
"""Raised when a handler is not compatible with the selected bus."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CallbackTimeoutError(EventBusError, TimeoutError):
|
|
17
|
+
"""Raised when a callback exceeds its configured execution budget."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class EventShutdownTimeoutError(EventBusError, TimeoutError):
|
|
21
|
+
"""Raised when a synchronous schedule cannot stop within its budget."""
|