PyEventEngine 0.6.0.post3__cp314-cp314-win_amd64.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.
- event_engine/__infra__.pxd +2 -0
- event_engine/__init__.pxd +2 -0
- event_engine/__init__.py +37 -0
- event_engine/base/__infra__.pxd +9 -0
- event_engine/base/__init__.pxd +9 -0
- event_engine/base/__init__.py +82 -0
- event_engine/base/c_allocator_protocol.c +10825 -0
- event_engine/base/c_allocator_protocol.cp314-win_amd64.pyd +0 -0
- event_engine/base/c_allocator_protocol.h +23 -0
- event_engine/base/c_allocator_protocol.pxd +31 -0
- event_engine/base/c_allocator_protocol.pyx +99 -0
- event_engine/capi/__infra__.pxd +189 -0
- event_engine/capi/__init__.pxd +189 -0
- event_engine/capi/__init__.py +84 -0
- event_engine/capi/c_engine.c +28719 -0
- event_engine/capi/c_engine.cp314-win_amd64.pyd +0 -0
- event_engine/capi/c_engine.h +393 -0
- event_engine/capi/c_engine.pxd +84 -0
- event_engine/capi/c_engine.pyi +424 -0
- event_engine/capi/c_engine.pyx +560 -0
- event_engine/capi/c_event.c +19929 -0
- event_engine/capi/c_event.cp314-win_amd64.pyd +0 -0
- event_engine/capi/c_event.h +342 -0
- event_engine/capi/c_event.pxd +159 -0
- event_engine/capi/c_event.pyi +226 -0
- event_engine/capi/c_event.pyx +387 -0
- event_engine/capi/c_topic.c +30986 -0
- event_engine/capi/c_topic.cp314-win_amd64.pyd +0 -0
- event_engine/capi/c_topic.h +1007 -0
- event_engine/capi/c_topic.pxd +147 -0
- event_engine/capi/c_topic.pyi +580 -0
- event_engine/capi/c_topic.pyx +725 -0
- event_engine/capi/fallback_engine.py +670 -0
- event_engine/includes/event_engine/base/c_allocator_protocol.c +10825 -0
- event_engine/includes/event_engine/base/c_allocator_protocol.h +23 -0
- event_engine/includes/event_engine/capi/c_engine.c +28719 -0
- event_engine/includes/event_engine/capi/c_engine.h +393 -0
- event_engine/includes/event_engine/capi/c_event.c +19929 -0
- event_engine/includes/event_engine/capi/c_event.h +342 -0
- event_engine/includes/event_engine/capi/c_topic.c +30986 -0
- event_engine/includes/event_engine/capi/c_topic.h +1007 -0
- event_engine/native/__init__.py +60 -0
- event_engine/native/_event.py +420 -0
- event_engine/native/_topic.py +171 -0
- event_engine/native/engine.py +746 -0
- event_engine/native/event.py +467 -0
- event_engine/native/topic.py +945 -0
- pyeventengine-0.6.0.post3.dist-info/METADATA +189 -0
- pyeventengine-0.6.0.post3.dist-info/RECORD +52 -0
- pyeventengine-0.6.0.post3.dist-info/WHEEL +5 -0
- pyeventengine-0.6.0.post3.dist-info/licenses/LICENSE +21 -0
- pyeventengine-0.6.0.post3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Native Python fallback implementation for PyMessagePayload and EventHook classes.
|
|
3
|
+
|
|
4
|
+
This module provides a pure Python implementation that mimics the behavior of the
|
|
5
|
+
Cython-based c_event module. It is used as a fallback when the Cython extension
|
|
6
|
+
cannot be compiled (e.g., due to lack of Cython, GCC, or Clang).
|
|
7
|
+
|
|
8
|
+
The API is designed to match event_engine.capi.c_event as closely as possible.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import inspect
|
|
14
|
+
import time
|
|
15
|
+
import traceback
|
|
16
|
+
from collections.abc import Callable, Iterator
|
|
17
|
+
from logging import Logger
|
|
18
|
+
from typing import TypedDict
|
|
19
|
+
|
|
20
|
+
from .topic import PyTopic
|
|
21
|
+
|
|
22
|
+
# Get logger from base module
|
|
23
|
+
try:
|
|
24
|
+
from ..base import LOGGER
|
|
25
|
+
except ImportError:
|
|
26
|
+
import logging
|
|
27
|
+
|
|
28
|
+
LOGGER = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
LOGGER = LOGGER.getChild('Event')
|
|
31
|
+
|
|
32
|
+
# Internal constants
|
|
33
|
+
_TOPIC_FIELD_NAME = 'topic'
|
|
34
|
+
_TOPIC_UNEXPECTED_ERROR = f"an unexpected keyword argument '{_TOPIC_FIELD_NAME}'"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PyMessagePayload:
|
|
38
|
+
"""
|
|
39
|
+
Python wrapper for a message payload structure.
|
|
40
|
+
|
|
41
|
+
In native Python, all instances own their underlying data (owner, args_owner, kwargs_owner are always True).
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
__slots__ = ('_topic', '_args', '_kwargs', '_seq_id')
|
|
45
|
+
|
|
46
|
+
def __init__(self, alloc: bool = False) -> None:
|
|
47
|
+
"""
|
|
48
|
+
Initialize a ``PyMessagePayload`` instance.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
alloc: If ``True``, allocate a new message payload (always True in Python).
|
|
52
|
+
"""
|
|
53
|
+
self._topic: PyTopic | None = None
|
|
54
|
+
self._args: tuple | None = None
|
|
55
|
+
self._kwargs: dict | None = None
|
|
56
|
+
self._seq_id: int = 0
|
|
57
|
+
|
|
58
|
+
def __repr__(self) -> str:
|
|
59
|
+
"""
|
|
60
|
+
Return a string representation of the payload.
|
|
61
|
+
"""
|
|
62
|
+
if self._topic:
|
|
63
|
+
return f'<PyMessagePayload "{self.topic.value}">(seq_id={self.seq_id}, args={self.args}, kwargs={self.kwargs})'
|
|
64
|
+
return f'<PyMessagePayload NO_TOPIC>(seq_id={self.seq_id}, args={self.args}, kwargs={self.kwargs})'
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def owner(self) -> bool:
|
|
68
|
+
"""bool: Whether this instance owns the underlying payload (always True in native Python)."""
|
|
69
|
+
return True
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def args_owner(self) -> bool:
|
|
73
|
+
"""bool: Whether this instance owns the positional arguments (always True in native Python)."""
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def kwargs_owner(self) -> bool:
|
|
78
|
+
"""bool: Whether this instance owns the keyword arguments (always True in native Python)."""
|
|
79
|
+
return True
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def topic(self) -> PyTopic | None:
|
|
83
|
+
"""
|
|
84
|
+
The topic associated with this payload.
|
|
85
|
+
"""
|
|
86
|
+
return self._topic
|
|
87
|
+
|
|
88
|
+
@topic.setter
|
|
89
|
+
def topic(self, value: PyTopic) -> None:
|
|
90
|
+
"""Set the topic."""
|
|
91
|
+
self._topic = value
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def args(self) -> tuple | None:
|
|
95
|
+
"""
|
|
96
|
+
The positional arguments of the payload.
|
|
97
|
+
"""
|
|
98
|
+
return self._args
|
|
99
|
+
|
|
100
|
+
@args.setter
|
|
101
|
+
def args(self, value: tuple) -> None:
|
|
102
|
+
"""Set the positional arguments."""
|
|
103
|
+
self._args = value
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def kwargs(self) -> dict | None:
|
|
107
|
+
"""
|
|
108
|
+
The keyword arguments of the payload.
|
|
109
|
+
"""
|
|
110
|
+
return self._kwargs
|
|
111
|
+
|
|
112
|
+
@kwargs.setter
|
|
113
|
+
def kwargs(self, value: dict) -> None:
|
|
114
|
+
"""Set the keyword arguments."""
|
|
115
|
+
self._kwargs = value
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def seq_id(self) -> int:
|
|
119
|
+
"""
|
|
120
|
+
The sequence ID of the payload.
|
|
121
|
+
"""
|
|
122
|
+
return self._seq_id
|
|
123
|
+
|
|
124
|
+
@seq_id.setter
|
|
125
|
+
def seq_id(self, value: int) -> None:
|
|
126
|
+
"""Set the sequence ID."""
|
|
127
|
+
self._seq_id = value
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class EventHook:
|
|
131
|
+
"""
|
|
132
|
+
Event dispatcher for registering and triggering handlers.
|
|
133
|
+
|
|
134
|
+
Handlers are triggered with a ``PyMessagePayload``. The dispatcher supports two calling conventions:
|
|
135
|
+
- **With-topic**: the handler receives the topic as a positional or keyword argument.
|
|
136
|
+
- **No-topic**: the handler receives only ``args`` and ``kwargs`` from the payload.
|
|
137
|
+
|
|
138
|
+
Handlers that accept ``**kwargs`` are recommended to ensure compatibility with both conventions.
|
|
139
|
+
|
|
140
|
+
Attributes:
|
|
141
|
+
topic (PyTopic): The topic associated with this hook.
|
|
142
|
+
logger (Logger | None): Optional logger instance.
|
|
143
|
+
retry_on_unexpected_topic (bool): If ``True``, retries with no-topic calling convention if a with-topic handler raises a ``TypeError`` and the error message indicates an unexpected topic argument.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
__slots__ = ('topic', 'logger', 'retry_on_unexpected_topic', '_handlers_no_topic', '_handlers_with_topic')
|
|
147
|
+
|
|
148
|
+
def __init__(self, topic: PyTopic, logger: Logger = None, retry_on_unexpected_topic: bool = False) -> None:
|
|
149
|
+
"""
|
|
150
|
+
Initialize an ``EventHook``.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
topic: The topic associated with this hook.
|
|
154
|
+
logger: Optional logger instance.
|
|
155
|
+
retry_on_unexpected_topic: If ``True``, enables retrying on unexpected topic argument errors.
|
|
156
|
+
"""
|
|
157
|
+
self.topic: PyTopic = topic
|
|
158
|
+
self.logger: Logger = LOGGER.getChild(f'EventHook.{topic}') if logger is None else logger
|
|
159
|
+
self.retry_on_unexpected_topic: bool = retry_on_unexpected_topic
|
|
160
|
+
self._handlers_no_topic: list[Callable] = []
|
|
161
|
+
self._handlers_with_topic: list[Callable] = []
|
|
162
|
+
|
|
163
|
+
def __call__(self, msg: PyMessagePayload) -> None:
|
|
164
|
+
"""
|
|
165
|
+
Trigger all registered handlers with the given message payload.
|
|
166
|
+
|
|
167
|
+
Alias for method ``trigger``.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
msg: The message payload to dispatch to handlers.
|
|
171
|
+
"""
|
|
172
|
+
self.trigger(msg)
|
|
173
|
+
|
|
174
|
+
def __iadd__(self, handler: Callable) -> EventHook:
|
|
175
|
+
"""
|
|
176
|
+
Add a handler using the ``+=`` operator.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
handler: The callable to register.
|
|
180
|
+
Returns:
|
|
181
|
+
Self, for chaining.
|
|
182
|
+
"""
|
|
183
|
+
self.add_handler(handler)
|
|
184
|
+
return self
|
|
185
|
+
|
|
186
|
+
def __isub__(self, handler: Callable) -> EventHook:
|
|
187
|
+
"""
|
|
188
|
+
Remove a handler using the ``-=`` operator.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
handler: The callable to unregister.
|
|
192
|
+
Returns:
|
|
193
|
+
Self, for chaining.
|
|
194
|
+
"""
|
|
195
|
+
self.remove_handler(handler)
|
|
196
|
+
return self
|
|
197
|
+
|
|
198
|
+
def __len__(self) -> int:
|
|
199
|
+
"""
|
|
200
|
+
Return the number of registered handlers.
|
|
201
|
+
"""
|
|
202
|
+
return len(self._handlers_no_topic) + len(self._handlers_with_topic)
|
|
203
|
+
|
|
204
|
+
def __repr__(self) -> str:
|
|
205
|
+
"""
|
|
206
|
+
Return a string representation of the ``EventHook``.
|
|
207
|
+
"""
|
|
208
|
+
return f'<EventHook topic="{self.topic}" handlers={len(self)}>'
|
|
209
|
+
|
|
210
|
+
def __iter__(self) -> Iterator[Callable]:
|
|
211
|
+
"""
|
|
212
|
+
Iterate over all registered handlers.
|
|
213
|
+
"""
|
|
214
|
+
yield from self._handlers_no_topic
|
|
215
|
+
yield from self._handlers_with_topic
|
|
216
|
+
|
|
217
|
+
def __contains__(self, handler: Callable) -> bool:
|
|
218
|
+
"""
|
|
219
|
+
Check if a handler is registered.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
handler: The callable to check.
|
|
223
|
+
Returns:
|
|
224
|
+
``True`` if the handler is registered; ``False`` otherwise.
|
|
225
|
+
"""
|
|
226
|
+
return handler in self._handlers_no_topic or handler in self._handlers_with_topic
|
|
227
|
+
|
|
228
|
+
def trigger(self, msg: PyMessagePayload) -> None:
|
|
229
|
+
"""
|
|
230
|
+
Trigger all registered handlers with the given message payload.
|
|
231
|
+
|
|
232
|
+
Handlers are executed in registration order:
|
|
233
|
+
1. All **no-topic** handlers (called with ``*args, **kwargs`` only).
|
|
234
|
+
2. All **with-topic** handlers (called with ``topic, *args, **kwargs``).
|
|
235
|
+
In each group, handlers are invoked in the order they were added.
|
|
236
|
+
|
|
237
|
+
If ``retry_on_unexpected_topic`` flag is on and a with-topic handler raises a ``TypeError`` and the error message indicates an unexpected topic argument,
|
|
238
|
+
the dispatcher retries the call without the topic.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
msg: The message payload to dispatch.
|
|
242
|
+
"""
|
|
243
|
+
args = msg.args if msg.args is not None else ()
|
|
244
|
+
kwargs = msg.kwargs if msg.kwargs is not None else {}
|
|
245
|
+
topic = msg.topic
|
|
246
|
+
|
|
247
|
+
# Trigger no-topic handlers first
|
|
248
|
+
for handler in self._handlers_no_topic:
|
|
249
|
+
try:
|
|
250
|
+
handler(*args, **kwargs)
|
|
251
|
+
except Exception:
|
|
252
|
+
self.logger.error(traceback.format_exc())
|
|
253
|
+
|
|
254
|
+
# Trigger with-topic handlers
|
|
255
|
+
# Create a new kwargs dict with topic added
|
|
256
|
+
kwargs_with_topic = kwargs.copy()
|
|
257
|
+
kwargs_with_topic[_TOPIC_FIELD_NAME] = topic
|
|
258
|
+
|
|
259
|
+
for handler in self._handlers_with_topic:
|
|
260
|
+
try:
|
|
261
|
+
handler(*args, **kwargs_with_topic)
|
|
262
|
+
except TypeError as e:
|
|
263
|
+
# Check if this is an "unexpected keyword argument 'topic'" error
|
|
264
|
+
if self.retry_on_unexpected_topic and _TOPIC_UNEXPECTED_ERROR in str(e):
|
|
265
|
+
try:
|
|
266
|
+
handler(*args, **kwargs)
|
|
267
|
+
except Exception:
|
|
268
|
+
self.logger.error(traceback.format_exc())
|
|
269
|
+
else:
|
|
270
|
+
self.logger.error(traceback.format_exc())
|
|
271
|
+
except Exception:
|
|
272
|
+
self.logger.error(traceback.format_exc())
|
|
273
|
+
|
|
274
|
+
def add_handler(self, handler: Callable, deduplicate: bool = False) -> None:
|
|
275
|
+
"""
|
|
276
|
+
Register a new handler.
|
|
277
|
+
|
|
278
|
+
It is strongly recommended that handlers accept ``**kwargs`` to remain compatible with both
|
|
279
|
+
with-topic and no-topic calling conventions.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
handler: The callable to register.
|
|
283
|
+
deduplicate: If ``True``, skip registration if the handler is already present.
|
|
284
|
+
"""
|
|
285
|
+
if not callable(handler):
|
|
286
|
+
raise TypeError(f'Handler must be callable, got {type(handler)}')
|
|
287
|
+
|
|
288
|
+
# Check if handler is already registered
|
|
289
|
+
if deduplicate and handler in self:
|
|
290
|
+
return
|
|
291
|
+
|
|
292
|
+
# Inspect the handler signature to determine if it accepts 'topic'
|
|
293
|
+
with_topic = False
|
|
294
|
+
try:
|
|
295
|
+
sig = inspect.signature(handler)
|
|
296
|
+
for param in sig.parameters.values():
|
|
297
|
+
if param.name == _TOPIC_FIELD_NAME or param.kind == param.VAR_KEYWORD:
|
|
298
|
+
with_topic = True
|
|
299
|
+
break
|
|
300
|
+
except (ValueError, TypeError):
|
|
301
|
+
# Can't inspect signature, assume no topic
|
|
302
|
+
pass
|
|
303
|
+
|
|
304
|
+
if with_topic:
|
|
305
|
+
self._handlers_with_topic.append(handler)
|
|
306
|
+
else:
|
|
307
|
+
self._handlers_no_topic.append(handler)
|
|
308
|
+
|
|
309
|
+
def remove_handler(self, handler: Callable) -> EventHook:
|
|
310
|
+
"""
|
|
311
|
+
Remove a handler from the hook.
|
|
312
|
+
|
|
313
|
+
Only the first matching occurrence is removed. If the same callable was added multiple times,
|
|
314
|
+
subsequent instances remain registered.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
handler: The callable to remove.
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
Self, for chaining.
|
|
321
|
+
"""
|
|
322
|
+
try:
|
|
323
|
+
self._handlers_no_topic.remove(handler)
|
|
324
|
+
except ValueError:
|
|
325
|
+
try:
|
|
326
|
+
self._handlers_with_topic.remove(handler)
|
|
327
|
+
except ValueError:
|
|
328
|
+
pass # Handler not found, silently ignore
|
|
329
|
+
return self
|
|
330
|
+
|
|
331
|
+
def clear(self) -> None:
|
|
332
|
+
"""
|
|
333
|
+
Remove all registered handlers.
|
|
334
|
+
"""
|
|
335
|
+
self._handlers_no_topic.clear()
|
|
336
|
+
self._handlers_with_topic.clear()
|
|
337
|
+
|
|
338
|
+
@property
|
|
339
|
+
def handlers(self) -> list[Callable]:
|
|
340
|
+
"""
|
|
341
|
+
List all registered handlers.
|
|
342
|
+
|
|
343
|
+
Handlers are ordered as follows:
|
|
344
|
+
- First, all no-topic handlers (in registration order).
|
|
345
|
+
- Then, all with-topic handlers (in registration order).
|
|
346
|
+
"""
|
|
347
|
+
return self._handlers_no_topic + self._handlers_with_topic
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class HandlerStats(TypedDict):
|
|
351
|
+
"""Statistics for a handler."""
|
|
352
|
+
calls: int
|
|
353
|
+
total_time: float
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
class EventHookEx(EventHook):
|
|
357
|
+
"""
|
|
358
|
+
Extended ``EventHook`` that tracks per-handler execution statistics.
|
|
359
|
+
"""
|
|
360
|
+
|
|
361
|
+
__slots__ = ('_stats',)
|
|
362
|
+
|
|
363
|
+
def __init__(self, topic: PyTopic, logger: Logger = None, retry_on_unexpected_topic: bool = False) -> None:
|
|
364
|
+
"""
|
|
365
|
+
Initialize an ``EventHookEx``.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
topic: The topic associated with this hook.
|
|
369
|
+
logger: Optional logger instance.
|
|
370
|
+
retry_on_unexpected_topic: If ``True``, enables retrying on unexpected topic argument errors.
|
|
371
|
+
"""
|
|
372
|
+
super().__init__(topic, logger, retry_on_unexpected_topic)
|
|
373
|
+
self._stats: dict[int, HandlerStats] = {}
|
|
374
|
+
|
|
375
|
+
def trigger(self, msg: PyMessagePayload) -> None:
|
|
376
|
+
"""
|
|
377
|
+
Trigger all registered handlers with the given message payload, tracking execution time.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
msg: The message payload to dispatch.
|
|
381
|
+
"""
|
|
382
|
+
args = msg.args if msg.args is not None else ()
|
|
383
|
+
kwargs = msg.kwargs if msg.kwargs is not None else {}
|
|
384
|
+
topic = msg.topic
|
|
385
|
+
|
|
386
|
+
# Trigger no-topic handlers first
|
|
387
|
+
for handler in self._handlers_no_topic:
|
|
388
|
+
handler_id = id(handler)
|
|
389
|
+
if handler_id not in self._stats:
|
|
390
|
+
self._stats[handler_id] = {'calls': 0, 'total_time': 0.0}
|
|
391
|
+
|
|
392
|
+
start_time = time.perf_counter()
|
|
393
|
+
try:
|
|
394
|
+
handler(*args, **kwargs)
|
|
395
|
+
except Exception:
|
|
396
|
+
self.logger.error(traceback.format_exc())
|
|
397
|
+
finally:
|
|
398
|
+
elapsed = time.perf_counter() - start_time
|
|
399
|
+
self._stats[handler_id]['calls'] += 1
|
|
400
|
+
self._stats[handler_id]['total_time'] += elapsed
|
|
401
|
+
|
|
402
|
+
# Trigger with-topic handlers
|
|
403
|
+
# Create a new kwargs dict with topic added
|
|
404
|
+
kwargs_with_topic = kwargs.copy()
|
|
405
|
+
kwargs_with_topic[_TOPIC_FIELD_NAME] = topic
|
|
406
|
+
|
|
407
|
+
for handler in self._handlers_with_topic:
|
|
408
|
+
handler_id = id(handler)
|
|
409
|
+
if handler_id not in self._stats:
|
|
410
|
+
self._stats[handler_id] = {'calls': 0, 'total_time': 0.0}
|
|
411
|
+
|
|
412
|
+
start_time = time.perf_counter()
|
|
413
|
+
try:
|
|
414
|
+
handler(*args, **kwargs_with_topic)
|
|
415
|
+
except TypeError as e:
|
|
416
|
+
# Check if this is an "unexpected keyword argument 'topic'" error
|
|
417
|
+
if self.retry_on_unexpected_topic and _TOPIC_UNEXPECTED_ERROR in str(e):
|
|
418
|
+
try:
|
|
419
|
+
handler(*args, **kwargs)
|
|
420
|
+
except Exception:
|
|
421
|
+
self.logger.error(traceback.format_exc())
|
|
422
|
+
else:
|
|
423
|
+
self.logger.error(traceback.format_exc())
|
|
424
|
+
except Exception:
|
|
425
|
+
self.logger.error(traceback.format_exc())
|
|
426
|
+
finally:
|
|
427
|
+
elapsed = time.perf_counter() - start_time
|
|
428
|
+
self._stats[handler_id]['calls'] += 1
|
|
429
|
+
self._stats[handler_id]['total_time'] += elapsed
|
|
430
|
+
|
|
431
|
+
def get_stats(self, py_callable: Callable) -> HandlerStats | None:
|
|
432
|
+
"""
|
|
433
|
+
Retrieve execution statistics for a specific handler.
|
|
434
|
+
|
|
435
|
+
Args:
|
|
436
|
+
py_callable: The handler to query.
|
|
437
|
+
Returns:
|
|
438
|
+
A dictionary with keys ``'calls'`` (number of invocations) and ``'total_time'`` (cumulative execution time in seconds),
|
|
439
|
+
or ``None`` if the handler is not registered or the HandlerStats is not registered.
|
|
440
|
+
"""
|
|
441
|
+
handler_id = id(py_callable)
|
|
442
|
+
return self._stats.get(handler_id)
|
|
443
|
+
|
|
444
|
+
@property
|
|
445
|
+
def stats(self) -> Iterator[tuple[Callable, HandlerStats]]:
|
|
446
|
+
"""
|
|
447
|
+
Iterate over all registered handlers and their execution statistics.
|
|
448
|
+
|
|
449
|
+
Returns:
|
|
450
|
+
An iterator yielding ``(handler, stats_dict)`` pairs.
|
|
451
|
+
"""
|
|
452
|
+
for handler in self._handlers_no_topic:
|
|
453
|
+
handler_id = id(handler)
|
|
454
|
+
if handler_id in self._stats:
|
|
455
|
+
yield handler, self._stats[handler_id]
|
|
456
|
+
|
|
457
|
+
for handler in self._handlers_with_topic:
|
|
458
|
+
handler_id = id(handler)
|
|
459
|
+
if handler_id in self._stats:
|
|
460
|
+
yield handler, self._stats[handler_id]
|
|
461
|
+
|
|
462
|
+
def clear(self) -> None:
|
|
463
|
+
"""
|
|
464
|
+
Remove all registered handlers and clear statistics.
|
|
465
|
+
"""
|
|
466
|
+
super().clear()
|
|
467
|
+
self._stats.clear()
|