PyEventEngine 0.6.2__cp315-cp315-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 +62 -0
- event_engine/base/__infra__.pxd +9 -0
- event_engine/base/__init__.pxd +9 -0
- event_engine/base/__init__.py +31 -0
- event_engine/base/c_allocator_protocol.c +14310 -0
- event_engine/base/c_allocator_protocol.cp315-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 +193 -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 +32542 -0
- event_engine/capi/c_engine.cp315-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 +478 -0
- event_engine/capi/c_engine.pyx +673 -0
- event_engine/capi/c_event.c +22826 -0
- event_engine/capi/c_event.cp315-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 +265 -0
- event_engine/capi/c_event.pyx +444 -0
- event_engine/capi/c_topic.c +34545 -0
- event_engine/capi/c_topic.cp315-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 +644 -0
- event_engine/capi/c_topic.pyx +818 -0
- event_engine/capi/fallback_engine.py +707 -0
- event_engine/config_view.c +10271 -0
- event_engine/config_view.cp315-win_amd64.pyd +0 -0
- event_engine/config_view.pyx +55 -0
- event_engine/includes/event_engine/base/c_allocator_protocol.c +14310 -0
- event_engine/includes/event_engine/base/c_allocator_protocol.h +23 -0
- event_engine/includes/event_engine/capi/c_engine.c +32542 -0
- event_engine/includes/event_engine/capi/c_engine.h +393 -0
- event_engine/includes/event_engine/capi/c_event.c +22826 -0
- event_engine/includes/event_engine/capi/c_event.h +342 -0
- event_engine/includes/event_engine/capi/c_topic.c +34545 -0
- event_engine/includes/event_engine/capi/c_topic.h +1007 -0
- event_engine/includes/event_engine/config_view.c +10271 -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 +793 -0
- event_engine/native/event.py +469 -0
- event_engine/native/topic.py +943 -0
- pyeventengine-0.6.2.dist-info/METADATA +189 -0
- pyeventengine-0.6.2.dist-info/RECORD +56 -0
- pyeventengine-0.6.2.dist-info/WHEEL +5 -0
- pyeventengine-0.6.2.dist-info/licenses/LICENSE +21 -0
- pyeventengine-0.6.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,469 @@
|
|
|
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, topic: PyTopic = None, args: tuple = None, kwargs: dict = None, alloc: bool = False) -> None:
|
|
47
|
+
"""
|
|
48
|
+
Initialize a ``PyMessagePayload`` instance.
|
|
49
|
+
|
|
50
|
+
Mirrors the Cython ``MessagePayload`` constructor: ``topic``, ``args``
|
|
51
|
+
and ``kwargs`` may be supplied directly. When omitted (e.g. the engine
|
|
52
|
+
allocating an empty payload), the fields are left ``None`` and can be
|
|
53
|
+
assigned afterwards via the setters.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
topic: Topic to associate with the payload (may be omitted).
|
|
57
|
+
args: Positional arguments for the payload (may be omitted).
|
|
58
|
+
kwargs: Keyword arguments for the payload (may be omitted).
|
|
59
|
+
alloc: If ``True``, allocate a new message payload (always True in Python).
|
|
60
|
+
"""
|
|
61
|
+
self._topic: PyTopic | None = topic
|
|
62
|
+
self._args: tuple | None = args
|
|
63
|
+
self._kwargs: dict | None = kwargs
|
|
64
|
+
self._seq_id: int = 0
|
|
65
|
+
|
|
66
|
+
def __repr__(self) -> str:
|
|
67
|
+
"""
|
|
68
|
+
Return a string representation of the payload.
|
|
69
|
+
"""
|
|
70
|
+
if self._topic:
|
|
71
|
+
return f'<PyMessagePayload "{self.topic.value}">(seq_id={self.seq_id}, args={self.args}, kwargs={self.kwargs})'
|
|
72
|
+
return f'<PyMessagePayload NO_TOPIC>(seq_id={self.seq_id}, args={self.args}, kwargs={self.kwargs})'
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def owner(self) -> bool:
|
|
76
|
+
"""bool: Whether this instance owns the underlying payload (always True in native Python)."""
|
|
77
|
+
return True
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def args_owner(self) -> bool:
|
|
81
|
+
"""bool: Whether this instance owns the positional arguments (always True in native Python)."""
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def kwargs_owner(self) -> bool:
|
|
86
|
+
"""bool: Whether this instance owns the keyword arguments (always True in native Python)."""
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def topic(self) -> PyTopic | None:
|
|
91
|
+
"""
|
|
92
|
+
The topic associated with this payload.
|
|
93
|
+
"""
|
|
94
|
+
return self._topic
|
|
95
|
+
|
|
96
|
+
@topic.setter
|
|
97
|
+
def topic(self, value: PyTopic) -> None:
|
|
98
|
+
"""Set the topic."""
|
|
99
|
+
self._topic = value
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def args(self) -> tuple | None:
|
|
103
|
+
"""
|
|
104
|
+
The positional arguments of the payload.
|
|
105
|
+
"""
|
|
106
|
+
return self._args
|
|
107
|
+
|
|
108
|
+
@args.setter
|
|
109
|
+
def args(self, value: tuple) -> None:
|
|
110
|
+
"""Set the positional arguments."""
|
|
111
|
+
self._args = value
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def kwargs(self) -> dict | None:
|
|
115
|
+
"""
|
|
116
|
+
The keyword arguments of the payload.
|
|
117
|
+
"""
|
|
118
|
+
return self._kwargs
|
|
119
|
+
|
|
120
|
+
@kwargs.setter
|
|
121
|
+
def kwargs(self, value: dict) -> None:
|
|
122
|
+
"""Set the keyword arguments."""
|
|
123
|
+
self._kwargs = value
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def kwargs_with_topic(self) -> dict | None:
|
|
127
|
+
"""
|
|
128
|
+
A copy of the payload kwargs with the ``topic`` key injected
|
|
129
|
+
(mirrors the Cython ``MessagePayload.kwargs_with_topic``).
|
|
130
|
+
"""
|
|
131
|
+
if self._kwargs is None:
|
|
132
|
+
return None
|
|
133
|
+
aggregated = dict(self._kwargs)
|
|
134
|
+
aggregated[_TOPIC_FIELD_NAME] = self._topic
|
|
135
|
+
return aggregated
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def seq_id(self) -> int:
|
|
139
|
+
"""
|
|
140
|
+
The sequence ID of the payload.
|
|
141
|
+
"""
|
|
142
|
+
return self._seq_id
|
|
143
|
+
|
|
144
|
+
@seq_id.setter
|
|
145
|
+
def seq_id(self, value: int) -> None:
|
|
146
|
+
"""Set the sequence ID."""
|
|
147
|
+
self._seq_id = value
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class EventHook:
|
|
151
|
+
"""
|
|
152
|
+
Event dispatcher for registering and triggering handlers.
|
|
153
|
+
|
|
154
|
+
Handlers are triggered with a ``PyMessagePayload``. The dispatcher supports two calling conventions:
|
|
155
|
+
- **With-topic**: the handler receives the topic as a positional or keyword argument.
|
|
156
|
+
- **No-topic**: the handler receives only ``args`` and ``kwargs`` from the payload.
|
|
157
|
+
|
|
158
|
+
Handlers that accept ``**kwargs`` are recommended to ensure compatibility with both conventions.
|
|
159
|
+
|
|
160
|
+
Attributes:
|
|
161
|
+
topic (PyTopic): The topic associated with this hook.
|
|
162
|
+
logger (Logger | None): Optional logger instance.
|
|
163
|
+
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.
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
__slots__ = ('topic', 'logger', 'retry_on_unexpected_topic', '_handlers', '_handler_loggers')
|
|
167
|
+
|
|
168
|
+
def __init__(self, topic: PyTopic, logger: Logger = None, retry_on_unexpected_topic: bool = False) -> None:
|
|
169
|
+
"""
|
|
170
|
+
Initialize an ``EventHook``.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
topic: The topic associated with this hook.
|
|
174
|
+
logger: Optional logger instance.
|
|
175
|
+
retry_on_unexpected_topic: If ``True``, enables retrying on unexpected topic argument errors.
|
|
176
|
+
"""
|
|
177
|
+
self.topic: PyTopic = topic
|
|
178
|
+
self.logger: Logger = LOGGER.getChild(f'EventHook.{topic}') if logger is None else logger
|
|
179
|
+
self.retry_on_unexpected_topic: bool = retry_on_unexpected_topic
|
|
180
|
+
self._handlers: list[tuple[Callable, bool]] = [] # (callable, with_topic)
|
|
181
|
+
self._handler_loggers: dict[int, Logger] = {}
|
|
182
|
+
|
|
183
|
+
def __call__(self, msg: PyMessagePayload) -> None:
|
|
184
|
+
"""
|
|
185
|
+
Trigger all registered handlers with the given message payload.
|
|
186
|
+
|
|
187
|
+
Alias for method ``trigger``.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
msg: The message payload to dispatch to handlers.
|
|
191
|
+
"""
|
|
192
|
+
self.trigger(msg)
|
|
193
|
+
|
|
194
|
+
def __iadd__(self, handler: Callable) -> EventHook:
|
|
195
|
+
"""
|
|
196
|
+
Add a handler using the ``+=`` operator (deduplicated, mirroring the
|
|
197
|
+
Cython ``EventHook.__iadd__``).
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
handler: The callable to register.
|
|
201
|
+
Returns:
|
|
202
|
+
Self, for chaining.
|
|
203
|
+
"""
|
|
204
|
+
self.add_handler(handler, deduplicate=True)
|
|
205
|
+
return self
|
|
206
|
+
|
|
207
|
+
def __isub__(self, handler: Callable) -> EventHook:
|
|
208
|
+
"""
|
|
209
|
+
Remove a handler using the ``-=`` operator.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
handler: The callable to unregister.
|
|
213
|
+
Returns:
|
|
214
|
+
Self, for chaining.
|
|
215
|
+
"""
|
|
216
|
+
self.remove_handler(handler)
|
|
217
|
+
return self
|
|
218
|
+
|
|
219
|
+
def __len__(self) -> int:
|
|
220
|
+
"""Return the number of registered handlers."""
|
|
221
|
+
return len(self._handlers)
|
|
222
|
+
|
|
223
|
+
def __repr__(self) -> str:
|
|
224
|
+
"""Return a string representation of the ``EventHook``."""
|
|
225
|
+
return f'<EventHook topic="{self.topic}" handlers={len(self)}>'
|
|
226
|
+
|
|
227
|
+
def __iter__(self) -> Iterator[dict]:
|
|
228
|
+
"""Iterate over handler descriptor dicts (mirroring the Cython ``EventHook``)."""
|
|
229
|
+
return iter(self.handlers)
|
|
230
|
+
|
|
231
|
+
def __contains__(self, handler: Callable) -> bool:
|
|
232
|
+
"""Check if a handler is registered (by function identity or bound-method equivalence)."""
|
|
233
|
+
for fn, _ in self._handlers:
|
|
234
|
+
if fn is handler:
|
|
235
|
+
return True
|
|
236
|
+
if inspect.ismethod(handler) and inspect.ismethod(fn):
|
|
237
|
+
if handler.__self__ is fn.__self__ and handler.__func__ is fn.__func__:
|
|
238
|
+
return True
|
|
239
|
+
return False
|
|
240
|
+
|
|
241
|
+
def trigger(self, msg: PyMessagePayload) -> None:
|
|
242
|
+
"""Trigger all registered handlers in registration order."""
|
|
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
|
+
kwargs_with_topic = kwargs.copy()
|
|
247
|
+
kwargs_with_topic[_TOPIC_FIELD_NAME] = topic
|
|
248
|
+
|
|
249
|
+
for handler, with_topic in self._handlers:
|
|
250
|
+
try:
|
|
251
|
+
if with_topic:
|
|
252
|
+
handler(*args, **kwargs_with_topic)
|
|
253
|
+
else:
|
|
254
|
+
handler(*args, **kwargs)
|
|
255
|
+
except TypeError as e:
|
|
256
|
+
if self.retry_on_unexpected_topic and with_topic and _TOPIC_UNEXPECTED_ERROR in str(e):
|
|
257
|
+
try:
|
|
258
|
+
handler(*args, **kwargs)
|
|
259
|
+
except Exception:
|
|
260
|
+
self.logger.error(traceback.format_exc())
|
|
261
|
+
else:
|
|
262
|
+
self.logger.error(traceback.format_exc())
|
|
263
|
+
except Exception:
|
|
264
|
+
self.logger.error(traceback.format_exc())
|
|
265
|
+
|
|
266
|
+
def add_handler(self, py_callable: Callable, logger: Logger = None, deduplicate: bool = False) -> None:
|
|
267
|
+
"""
|
|
268
|
+
Register a new handler.
|
|
269
|
+
|
|
270
|
+
It is strongly recommended that handlers accept ``**kwargs`` to remain compatible with both
|
|
271
|
+
with-topic and no-topic calling conventions.
|
|
272
|
+
|
|
273
|
+
Signature mirrors the Cython ``EventHook.add_handler``: the optional
|
|
274
|
+
``logger`` is recorded per callable and surfaced through ``handlers``.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
py_callable: The callable to register.
|
|
278
|
+
logger: Optional per-handler logger (defaults to the hook logger).
|
|
279
|
+
deduplicate: If ``True``, skip registration if the handler is already present.
|
|
280
|
+
"""
|
|
281
|
+
if not callable(py_callable):
|
|
282
|
+
raise TypeError(f'Handler must be callable, got {type(py_callable)}')
|
|
283
|
+
|
|
284
|
+
# Check if handler is already registered
|
|
285
|
+
if deduplicate and py_callable in self:
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
# Inspect the handler signature to determine if it accepts 'topic'
|
|
289
|
+
with_topic = False
|
|
290
|
+
try:
|
|
291
|
+
sig = inspect.signature(py_callable)
|
|
292
|
+
for param in sig.parameters.values():
|
|
293
|
+
if param.name == _TOPIC_FIELD_NAME or param.kind == param.VAR_KEYWORD:
|
|
294
|
+
with_topic = True
|
|
295
|
+
break
|
|
296
|
+
except (ValueError, TypeError):
|
|
297
|
+
# Can't inspect signature, assume no topic
|
|
298
|
+
pass
|
|
299
|
+
|
|
300
|
+
self._handler_loggers[id(py_callable)] = self.logger if logger is None else logger
|
|
301
|
+
self._handlers.append((py_callable, with_topic))
|
|
302
|
+
|
|
303
|
+
def remove_handler(self, py_callable: Callable) -> EventHook:
|
|
304
|
+
"""
|
|
305
|
+
Remove a handler from the hook.
|
|
306
|
+
|
|
307
|
+
Only the first matching occurrence is removed. Mirrors the Cython
|
|
308
|
+
``EventHook.remove_handler``: removing an unknown callable logs a warning.
|
|
309
|
+
"""
|
|
310
|
+
for i, (fn, _) in enumerate(self._handlers):
|
|
311
|
+
if fn is py_callable or (
|
|
312
|
+
inspect.ismethod(py_callable) and inspect.ismethod(fn)
|
|
313
|
+
and py_callable.__self__ is fn.__self__
|
|
314
|
+
and py_callable.__func__ is fn.__func__):
|
|
315
|
+
del self._handlers[i]
|
|
316
|
+
self._handler_loggers.pop(id(py_callable), None)
|
|
317
|
+
return self
|
|
318
|
+
LOGGER.warning(f'{py_callable} not exist in {self} call stacks')
|
|
319
|
+
return self
|
|
320
|
+
|
|
321
|
+
def clear(self) -> None:
|
|
322
|
+
"""Remove all registered handlers."""
|
|
323
|
+
self._handlers.clear()
|
|
324
|
+
self._handler_loggers.clear()
|
|
325
|
+
|
|
326
|
+
@property
|
|
327
|
+
def handlers(self) -> list[dict]:
|
|
328
|
+
"""List of handler descriptor dicts in registration order (mirrors Cython)."""
|
|
329
|
+
return [
|
|
330
|
+
{
|
|
331
|
+
'fn': fn,
|
|
332
|
+
'logger': self._handler_loggers.get(id(fn), self.logger),
|
|
333
|
+
'idx': i,
|
|
334
|
+
'with_topic': with_topic,
|
|
335
|
+
}
|
|
336
|
+
for i, (fn, with_topic) in enumerate(self._handlers)
|
|
337
|
+
]
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class HandlerStats(TypedDict):
|
|
341
|
+
"""Statistics for a handler."""
|
|
342
|
+
calls: int
|
|
343
|
+
total_time: float
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
class EventHookEx(EventHook):
|
|
347
|
+
"""
|
|
348
|
+
Extended ``EventHook`` that tracks per-handler execution statistics.
|
|
349
|
+
"""
|
|
350
|
+
|
|
351
|
+
__slots__ = ('_stats', '_hook_stats')
|
|
352
|
+
|
|
353
|
+
def __init__(self, topic: PyTopic, logger: Logger = None, retry_on_unexpected_topic: bool = False) -> None:
|
|
354
|
+
"""
|
|
355
|
+
Initialize an ``EventHookEx``.
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
topic: The topic associated with this hook.
|
|
359
|
+
logger: Optional logger instance.
|
|
360
|
+
retry_on_unexpected_topic: If ``True``, enables retrying on unexpected topic argument errors.
|
|
361
|
+
"""
|
|
362
|
+
super().__init__(topic, logger, retry_on_unexpected_topic)
|
|
363
|
+
self._stats: dict[int, HandlerStats] = {}
|
|
364
|
+
self._hook_stats: dict = {
|
|
365
|
+
'n_calls': 0,
|
|
366
|
+
'last_call_start': 0.0,
|
|
367
|
+
'last_call_complete': 0.0,
|
|
368
|
+
'elapsed_seconds': 0.0,
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
def trigger(self, msg: PyMessagePayload) -> None:
|
|
372
|
+
"""
|
|
373
|
+
Trigger all registered handlers with the given message payload, tracking execution time.
|
|
374
|
+
|
|
375
|
+
Hook-level statistics (``stats``) accumulate across trigger calls,
|
|
376
|
+
mirroring the Cython ``EventHookEx`` watcher-based stats.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
msg: The message payload to dispatch.
|
|
380
|
+
"""
|
|
381
|
+
self._hook_stats['n_calls'] += 1
|
|
382
|
+
self._hook_stats['last_call_start'] = time.perf_counter()
|
|
383
|
+
try:
|
|
384
|
+
self._trigger_impl(msg)
|
|
385
|
+
finally:
|
|
386
|
+
self._hook_stats['last_call_complete'] = time.perf_counter()
|
|
387
|
+
self._hook_stats['elapsed_seconds'] += (
|
|
388
|
+
self._hook_stats['last_call_complete'] - self._hook_stats['last_call_start']
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
def _trigger_impl(self, msg: PyMessagePayload) -> None:
|
|
392
|
+
"""Per-handler dispatch and timing in registration order."""
|
|
393
|
+
args = msg.args if msg.args is not None else ()
|
|
394
|
+
kwargs = msg.kwargs if msg.kwargs is not None else {}
|
|
395
|
+
topic = msg.topic
|
|
396
|
+
kwargs_with_topic = kwargs.copy()
|
|
397
|
+
kwargs_with_topic[_TOPIC_FIELD_NAME] = topic
|
|
398
|
+
|
|
399
|
+
for handler, with_topic in self._handlers:
|
|
400
|
+
handler_id = id(handler)
|
|
401
|
+
if handler_id not in self._stats:
|
|
402
|
+
self._stats[handler_id] = {'calls': 0, 'total_time': 0.0}
|
|
403
|
+
|
|
404
|
+
start_time = time.perf_counter()
|
|
405
|
+
try:
|
|
406
|
+
if with_topic:
|
|
407
|
+
handler(*args, **kwargs_with_topic)
|
|
408
|
+
else:
|
|
409
|
+
handler(*args, **kwargs)
|
|
410
|
+
except TypeError as e:
|
|
411
|
+
if self.retry_on_unexpected_topic and with_topic and _TOPIC_UNEXPECTED_ERROR in str(e):
|
|
412
|
+
try:
|
|
413
|
+
handler(*args, **kwargs)
|
|
414
|
+
except Exception:
|
|
415
|
+
self.logger.error(traceback.format_exc())
|
|
416
|
+
else:
|
|
417
|
+
self.logger.error(traceback.format_exc())
|
|
418
|
+
except Exception:
|
|
419
|
+
self.logger.error(traceback.format_exc())
|
|
420
|
+
finally:
|
|
421
|
+
elapsed = time.perf_counter() - start_time
|
|
422
|
+
self._stats[handler_id]['calls'] += 1
|
|
423
|
+
self._stats[handler_id]['total_time'] += elapsed
|
|
424
|
+
|
|
425
|
+
def get_stats(self, py_callable: Callable) -> HandlerStats | None:
|
|
426
|
+
"""
|
|
427
|
+
Retrieve execution statistics for a specific handler.
|
|
428
|
+
|
|
429
|
+
Args:
|
|
430
|
+
py_callable: The handler to query.
|
|
431
|
+
Returns:
|
|
432
|
+
A dictionary with keys ``'calls'`` (number of invocations) and ``'total_time'`` (cumulative execution time in seconds),
|
|
433
|
+
or ``None`` if the handler is not registered or the HandlerStats is not registered.
|
|
434
|
+
"""
|
|
435
|
+
handler_id = id(py_callable)
|
|
436
|
+
return self._stats.get(handler_id)
|
|
437
|
+
|
|
438
|
+
@property
|
|
439
|
+
def stats(self) -> dict:
|
|
440
|
+
"""
|
|
441
|
+
Hook-level execution statistics (mirroring the Cython
|
|
442
|
+
``EventHookEx.stats`` shape).
|
|
443
|
+
|
|
444
|
+
Returns:
|
|
445
|
+
A dict with keys ``'n_calls'``, ``'last_call_start'``,
|
|
446
|
+
``'last_call_complete'`` and ``'elapsed_seconds'``.
|
|
447
|
+
"""
|
|
448
|
+
return dict(self._hook_stats)
|
|
449
|
+
|
|
450
|
+
@property
|
|
451
|
+
def handler_stats(self) -> Iterator[tuple[Callable, HandlerStats]]:
|
|
452
|
+
"""Iterate over all registered handlers and their per-handler stats."""
|
|
453
|
+
for handler, _ in self._handlers:
|
|
454
|
+
handler_id = id(handler)
|
|
455
|
+
if handler_id in self._stats:
|
|
456
|
+
yield handler, self._stats[handler_id]
|
|
457
|
+
|
|
458
|
+
def clear(self) -> None:
|
|
459
|
+
"""
|
|
460
|
+
Remove all registered handlers and clear statistics.
|
|
461
|
+
"""
|
|
462
|
+
super().clear()
|
|
463
|
+
self._stats.clear()
|
|
464
|
+
self._hook_stats = {
|
|
465
|
+
'n_calls': 0,
|
|
466
|
+
'last_call_start': 0.0,
|
|
467
|
+
'last_call_complete': 0.0,
|
|
468
|
+
'elapsed_seconds': 0.0,
|
|
469
|
+
}
|