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.
Files changed (52) hide show
  1. event_engine/__infra__.pxd +2 -0
  2. event_engine/__init__.pxd +2 -0
  3. event_engine/__init__.py +37 -0
  4. event_engine/base/__infra__.pxd +9 -0
  5. event_engine/base/__init__.pxd +9 -0
  6. event_engine/base/__init__.py +82 -0
  7. event_engine/base/c_allocator_protocol.c +10825 -0
  8. event_engine/base/c_allocator_protocol.cp314-win_amd64.pyd +0 -0
  9. event_engine/base/c_allocator_protocol.h +23 -0
  10. event_engine/base/c_allocator_protocol.pxd +31 -0
  11. event_engine/base/c_allocator_protocol.pyx +99 -0
  12. event_engine/capi/__infra__.pxd +189 -0
  13. event_engine/capi/__init__.pxd +189 -0
  14. event_engine/capi/__init__.py +84 -0
  15. event_engine/capi/c_engine.c +28719 -0
  16. event_engine/capi/c_engine.cp314-win_amd64.pyd +0 -0
  17. event_engine/capi/c_engine.h +393 -0
  18. event_engine/capi/c_engine.pxd +84 -0
  19. event_engine/capi/c_engine.pyi +424 -0
  20. event_engine/capi/c_engine.pyx +560 -0
  21. event_engine/capi/c_event.c +19929 -0
  22. event_engine/capi/c_event.cp314-win_amd64.pyd +0 -0
  23. event_engine/capi/c_event.h +342 -0
  24. event_engine/capi/c_event.pxd +159 -0
  25. event_engine/capi/c_event.pyi +226 -0
  26. event_engine/capi/c_event.pyx +387 -0
  27. event_engine/capi/c_topic.c +30986 -0
  28. event_engine/capi/c_topic.cp314-win_amd64.pyd +0 -0
  29. event_engine/capi/c_topic.h +1007 -0
  30. event_engine/capi/c_topic.pxd +147 -0
  31. event_engine/capi/c_topic.pyi +580 -0
  32. event_engine/capi/c_topic.pyx +725 -0
  33. event_engine/capi/fallback_engine.py +670 -0
  34. event_engine/includes/event_engine/base/c_allocator_protocol.c +10825 -0
  35. event_engine/includes/event_engine/base/c_allocator_protocol.h +23 -0
  36. event_engine/includes/event_engine/capi/c_engine.c +28719 -0
  37. event_engine/includes/event_engine/capi/c_engine.h +393 -0
  38. event_engine/includes/event_engine/capi/c_event.c +19929 -0
  39. event_engine/includes/event_engine/capi/c_event.h +342 -0
  40. event_engine/includes/event_engine/capi/c_topic.c +30986 -0
  41. event_engine/includes/event_engine/capi/c_topic.h +1007 -0
  42. event_engine/native/__init__.py +60 -0
  43. event_engine/native/_event.py +420 -0
  44. event_engine/native/_topic.py +171 -0
  45. event_engine/native/engine.py +746 -0
  46. event_engine/native/event.py +467 -0
  47. event_engine/native/topic.py +945 -0
  48. pyeventengine-0.6.0.post3.dist-info/METADATA +189 -0
  49. pyeventengine-0.6.0.post3.dist-info/RECORD +52 -0
  50. pyeventengine-0.6.0.post3.dist-info/WHEEL +5 -0
  51. pyeventengine-0.6.0.post3.dist-info/licenses/LICENSE +21 -0
  52. pyeventengine-0.6.0.post3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,226 @@
1
+ from collections.abc import Callable, Iterator
2
+ from logging import Logger
3
+ from typing import TypedDict
4
+
5
+ from .c_topic import Topic
6
+
7
+
8
+ class MessagePayload:
9
+ """
10
+ Python wrapper for a C message payload structure.
11
+
12
+ Attributes:
13
+ owner (bool): Indicates whether this instance owns the underlying C payload.
14
+ """
15
+
16
+ owner: bool
17
+
18
+ def __init__(self, topic: Topic, args: tuple, kwargs: dict) -> None:
19
+ """
20
+ Initialize a ``MessagePayload`` instance.
21
+
22
+ Args:
23
+ topic (Topic): Topic for this payload
24
+ args (tuple): Positional arguments for this payload
25
+ kwargs (dict): Keyword arguments for this payload
26
+ """
27
+
28
+ def __repr__(self) -> str:
29
+ """
30
+ Return a string representation of the payload.
31
+ """
32
+
33
+ @property
34
+ def topic(self) -> Topic:
35
+ """
36
+ The topic associated with this payload.
37
+ """
38
+
39
+ @property
40
+ def args(self) -> tuple | None:
41
+ """
42
+ The positional arguments of the payload.
43
+ """
44
+
45
+ @property
46
+ def kwargs(self) -> dict | None:
47
+ """
48
+ The keyword arguments of the payload.
49
+ """
50
+
51
+ @property
52
+ def seq_id(self) -> int:
53
+ """
54
+ The sequence ID of the payload.
55
+ """
56
+
57
+
58
+ class EvtPyCallable(TypedDict):
59
+ fn: Callable[...]
60
+ logger: Logger
61
+ idx: int
62
+ with_topic: bool
63
+
64
+
65
+ class EventHook:
66
+ """
67
+ Event dispatcher for registering and triggering handlers.
68
+
69
+ Handlers are triggered with a ``MessagePayload``. The dispatcher supports two calling conventions:
70
+ - **With-topic**: the handler receives the topic as a positional or keyword argument.
71
+ - **No-topic**: the handler receives only ``args`` and ``kwargs`` from the payload.
72
+
73
+ Handlers that accept ``**kwargs`` are recommended to ensure compatibility with both conventions.
74
+
75
+ Attributes:
76
+ topic (Topic): The topic associated with this hook.
77
+ logger (Logger | None): Optional logger instance. If not provided, use module global logger.
78
+ """
79
+
80
+ topic: Topic
81
+ logger: Logger
82
+
83
+ def __init__(self, topic: Topic, logger: Logger = None) -> None:
84
+ """
85
+ Initialize an ``EventHook``.
86
+
87
+ Args:
88
+ topic: The topic associated with this hook.
89
+ logger: Optional logger instance.
90
+ """
91
+
92
+ def __call__(self, msg: MessagePayload) -> None:
93
+ """
94
+ Trigger all registered handlers with the given message payload.
95
+
96
+ Alias for method ``trigger``.
97
+
98
+ Args:
99
+ msg: The message payload to dispatch to handlers.
100
+ """
101
+
102
+ def __iadd__(self, handler: Callable) -> EventHook:
103
+ """
104
+ Add a handler using the ``+=`` operator.
105
+
106
+ Args:
107
+ handler: The callable to register.
108
+ Returns:
109
+ Self, for chaining.
110
+ """
111
+
112
+ def __isub__(self, handler: Callable) -> EventHook:
113
+ """
114
+ Remove a handler using the ``-=`` operator.
115
+
116
+ Args:
117
+ handler: The callable to unregister.
118
+ Returns:
119
+ Self, for chaining.
120
+ """
121
+
122
+ def __len__(self) -> int:
123
+ """
124
+ Return the number of registered handlers.
125
+ """
126
+
127
+ def __repr__(self) -> str:
128
+ """
129
+ Return a string representation of the ``EventHook``.
130
+ """
131
+
132
+ def __iter__(self) -> Iterator[Callable]:
133
+ """
134
+ Iterate over all registered handlers.
135
+ """
136
+
137
+ def __contains__(self, handler: Callable) -> bool:
138
+ """
139
+ Check if a handler is registered.
140
+
141
+ Args:
142
+ handler: The callable to check.
143
+ Returns:
144
+ ``True`` if the handler is registered; ``False`` otherwise.
145
+ """
146
+
147
+ def trigger(self, msg: MessagePayload) -> None:
148
+ """
149
+ Trigger all registered handlers with the given message payload.
150
+
151
+ Args:
152
+ msg: The message payload to dispatch.
153
+ """
154
+
155
+ def add_handler(self, handler: Callable, logger: Logger = None, deduplicate: bool = False) -> None:
156
+ """
157
+ Register a new handler.
158
+
159
+ It is strongly recommended that handlers accept ``**kwargs`` to remain compatible with both
160
+ with-topic and no-topic calling conventions.
161
+
162
+ Args:
163
+ handler: The callable to register.
164
+ logger: Optional logger instance for this handler. If not provided, use the hook's logger.
165
+ deduplicate: If ``True``, skip registration if the handler is already present.
166
+ """
167
+
168
+ def remove_handler(self, handler: Callable) -> EventHook:
169
+ """
170
+ Remove a handler from the hook.
171
+
172
+ Only the first matching occurrence is removed. If the same callable was added multiple times,
173
+ subsequent instances remain registered.
174
+
175
+ Args:
176
+ handler: The callable to remove.
177
+
178
+ Returns:
179
+ Self, for chaining.
180
+ """
181
+
182
+ def clear(self) -> None:
183
+ """
184
+ Remove all registered handlers.
185
+ """
186
+
187
+ @property
188
+ def handlers(self) -> list[EvtPyCallable]:
189
+ """
190
+ List all registered handlers.
191
+
192
+ Returns:
193
+ A list of dictionaries, each containing information about a registered handler.
194
+ """
195
+
196
+
197
+ class HandlerStats(TypedDict):
198
+ n_calls: int
199
+ last_call_start: float
200
+ last_call_complete: float
201
+ elapsed_seconds: float
202
+
203
+
204
+ class EventHookEx(EventHook):
205
+ """
206
+ Extended ``EventHook`` that tracks per-handler execution statistics.
207
+ """
208
+
209
+ def __init__(self, topic: object, logger: object = None) -> None:
210
+ """
211
+ Initialize an ``EventHookEx``.
212
+
213
+ Args:
214
+ topic: The topic associated with this hook.
215
+ logger: Optional logger instance.
216
+ """
217
+
218
+ @property
219
+ def stats(self) -> HandlerStats:
220
+ """
221
+ Get aggregate statistics for this EventHook
222
+
223
+ Returns:
224
+ A dictionary with total calls and total execution time.
225
+ """
226
+ ...
@@ -0,0 +1,387 @@
1
+ import inspect
2
+ import traceback
3
+
4
+ from cpython.exc cimport PyErr_Clear, PyErr_Fetch
5
+ from cpython.method cimport PyMethod_Check, PyMethod_GET_FUNCTION, PyMethod_GET_SELF
6
+ from cpython.ref cimport Py_XDECREF, Py_XINCREF
7
+ from cpython.time cimport perf_counter
8
+ from cbase.allocator_protocol.c_allocator_protocol cimport c_ap_alloc, c_ap_free, c_ap_protocol_from_ptr
9
+
10
+ from ..base.c_allocator_protocol cimport EE_HEAP_ALLOCATOR
11
+ from ..base import LOGGER
12
+
13
+ LOGGER = LOGGER.getChild('Event')
14
+
15
+ cdef tuple EMPTY_ARGS = ()
16
+ cdef str TOPIC_FIELD_NAME = 'topic'
17
+
18
+
19
+ cdef inline evt_message_payload* c_evt_payload_new(Topic topic, tuple args, dict kwargs):
20
+ cdef size_t payload_size = sizeof(evt_message_payload) + sizeof(evt_py_payload)
21
+ cdef evt_message_payload* c_payload = <evt_message_payload*> c_ap_alloc(payload_size, EE_HEAP_ALLOCATOR)
22
+ if not c_payload:
23
+ return NULL
24
+
25
+ cdef evt_py_payload* py_payload = <evt_py_payload*> (c_payload + 1)
26
+ cdef PyObject* kwargs_aggregated = PyDict_Copy(<PyObject*> kwargs)
27
+ PyDict_SetDefault(kwargs_aggregated, <PyObject*> TOPIC_FIELD_NAME, <PyObject*> topic)
28
+
29
+ py_payload.py_topic = <PyObject*> topic
30
+ py_payload.py_args = <PyObject*> args
31
+ py_payload.py_kwargs = <PyObject*> kwargs
32
+ py_payload.py_kwargs_aggregated = kwargs_aggregated
33
+
34
+ Py_XINCREF(<PyObject*> topic)
35
+ Py_XINCREF(<PyObject*> args)
36
+ Py_XINCREF(<PyObject*> kwargs)
37
+ # Py_XINCREF(<PyObject*> kwargs_aggregated)
38
+
39
+ c_payload.args = py_payload
40
+ c_payload.topic = topic.header
41
+ return c_payload
42
+
43
+
44
+ cdef inline void c_evt_payload_free(evt_message_payload* payload):
45
+ cdef evt_py_payload* py_payload = <evt_py_payload*> (payload + 1)
46
+
47
+ Py_XDECREF(py_payload.py_topic)
48
+ Py_XDECREF(py_payload.py_args)
49
+ Py_XDECREF(py_payload.py_kwargs)
50
+ Py_XDECREF(py_payload.py_kwargs_aggregated)
51
+
52
+ c_ap_free(payload)
53
+
54
+
55
+ cdef class MessagePayload:
56
+ def __init__(self, Topic topic, tuple args, dict kwargs):
57
+ self.header = c_evt_payload_new(topic, args, kwargs)
58
+ if not self.header:
59
+ raise MemoryError('Failed to allocate memory for evt_message_payload')
60
+ self.owner = True
61
+
62
+ def __dealloc__(self):
63
+ if not self.owner:
64
+ return
65
+
66
+ if self.header:
67
+ c_evt_payload_free(self.header)
68
+
69
+ @staticmethod
70
+ cdef MessagePayload c_from_header(evt_message_payload* payload, bint owner=False):
71
+ cdef MessagePayload instance = MessagePayload.__new__(MessagePayload)
72
+ instance.header = payload
73
+ instance.owner = owner
74
+ return instance
75
+
76
+ def __repr__(self):
77
+ if not self.header:
78
+ return '<MessagePayload uninitialized>'
79
+ if self.header.topic:
80
+ return f'<MessagePayload "{self.topic.value}">(seq_id={self.seq_id}, args={self.args}, kwargs={self.kwargs})'
81
+ return f'<MessagePayload NO_TOPIC>(seq_id={self.seq_id}, args={self.args}, kwargs={self.kwargs})'
82
+
83
+ property topic:
84
+ def __get__(self):
85
+ if not self.header:
86
+ raise RuntimeError('Uninitialized c payload')
87
+ if not self.header.args:
88
+ raise RuntimeError('Uninitialized python payload')
89
+ cdef evt_py_payload* py_payload = <evt_py_payload*> self.header.args
90
+ cdef PyObject* py_topic = py_payload.py_topic
91
+ if py_topic:
92
+ Py_XINCREF(py_topic)
93
+ return <Topic> py_topic
94
+ cdef evt_topic* topic = self.header.topic
95
+ if topic:
96
+ return Topic.c_from_header(topic, False)
97
+ return None
98
+
99
+ property args:
100
+ def __get__(self):
101
+ if not self.header:
102
+ raise RuntimeError('Uninitialized c payload')
103
+ if not self.header.args:
104
+ raise RuntimeError('Uninitialized python payload')
105
+ cdef evt_py_payload* py_payload = <evt_py_payload*> self.header.args
106
+ cdef PyObject* py_args = py_payload.py_args
107
+ if py_args:
108
+ Py_XINCREF(py_args)
109
+ return <tuple> py_args
110
+ return None
111
+
112
+ property kwargs:
113
+ def __get__(self):
114
+ if not self.header:
115
+ raise RuntimeError('Uninitialized c payload')
116
+ if not self.header.args:
117
+ raise RuntimeError('Uninitialized python payload')
118
+ cdef evt_py_payload* py_payload = <evt_py_payload*> self.header.args
119
+ cdef PyObject* py_kwargs = py_payload.py_kwargs
120
+ if py_kwargs:
121
+ Py_XINCREF(py_kwargs)
122
+ return <dict> py_kwargs
123
+ return None
124
+
125
+ property kwargs_with_topic:
126
+ def __get__(self):
127
+ if not self.header:
128
+ raise RuntimeError('Uninitialized c payload')
129
+ if not self.header.args:
130
+ raise RuntimeError('Uninitialized python payload')
131
+ cdef evt_py_payload* py_payload = <evt_py_payload*> self.header.args
132
+ cdef PyObject* py_kwargs_aggregated = py_payload.py_kwargs_aggregated
133
+ if py_kwargs_aggregated:
134
+ Py_XINCREF(py_kwargs_aggregated)
135
+ return <dict> py_kwargs_aggregated
136
+ return None
137
+
138
+ property seq_id:
139
+ def __get__(self):
140
+ if not self.header:
141
+ raise RuntimeError('Uninitialized c payload')
142
+ return self.header.seq_id
143
+
144
+ def __set__(self, uint64_t seq_id):
145
+ if not self.header:
146
+ raise RuntimeError('Uninitialized c payload')
147
+ self.header.seq_id = seq_id
148
+
149
+
150
+ cdef inline bint py_callable_same(PyObject* a, PyObject* b):
151
+ if a == b:
152
+ return True
153
+
154
+ # Both are bound methods
155
+ cdef object fn_a = <object> a
156
+ cdef object fn_b = <object> b
157
+ if PyMethod_Check(fn_a) and PyMethod_Check(fn_b):
158
+ return PyMethod_GET_SELF(fn_a) == PyMethod_GET_SELF(fn_b) and PyMethod_GET_FUNCTION(fn_a) == PyMethod_GET_FUNCTION(fn_b)
159
+ return False
160
+
161
+
162
+ cdef class EventHook:
163
+ def __cinit__(self, Topic topic, object logger=None):
164
+ self.header = c_evt_hook_new(topic.header, EE_HEAP_ALLOCATOR)
165
+ if not self.header:
166
+ raise MemoryError(f'Failed to allocate memory for {self.__class__.__name__}')
167
+ self.callables = NULL
168
+ self.topic = topic
169
+ self.logger = LOGGER.getChild(f'EventHook.{topic}') if logger is None else logger
170
+
171
+ def __dealloc__(self):
172
+ self.c_free_py_callable()
173
+ if self.header:
174
+ c_evt_hook_free(self.header)
175
+
176
+ @staticmethod
177
+ cdef inline void c_invoke_py_callable(evt_message_payload* payload, void* user_data) with gil:
178
+ cdef evt_py_callable* ctx = <evt_py_callable*> user_data
179
+ cdef bint with_topic = ctx.with_topic
180
+ cdef evt_py_payload* py_payload = <evt_py_payload*> payload.args
181
+
182
+ cdef PyObject* fn = ctx.fn
183
+ cdef PyObject* args = py_payload.py_args
184
+ cdef PyObject* kwargs = py_payload.py_kwargs_aggregated if with_topic else py_payload.py_kwargs
185
+ cdef PyObject* res = PyObject_Call(fn, args, kwargs)
186
+
187
+ if res:
188
+ Py_XDECREF(res)
189
+ return
190
+
191
+ # Fetch the current Python exception (steals references; clears the indicator)
192
+ cdef PyObject* etype = NULL
193
+ cdef PyObject* evalue = NULL
194
+ cdef PyObject* etrace = NULL
195
+ cdef object logger = <object> ctx.logger
196
+ cdef object formatted
197
+
198
+ PyErr_Fetch(&etype, &evalue, &etrace)
199
+ formatted = traceback.format_exception(<object> etype, (<object> evalue) if evalue else None, (<object> etrace) if etrace else None)
200
+ logger.error("".join(formatted))
201
+ Py_XDECREF(etype)
202
+ Py_XDECREF(evalue)
203
+ Py_XDECREF(etrace)
204
+ PyErr_Clear()
205
+
206
+ cdef inline evt_py_callable* c_add_py_callable(self, PyObject* py_callable, PyObject* logger, bint with_topic, bint deduplicate):
207
+ if not PyCallable_Check(py_callable):
208
+ raise ValueError('Callback handler must be callable')
209
+
210
+ cdef evt_py_callable* callable_frame = self.callables
211
+
212
+ # Walk list to detect duplicates and position at tail
213
+ cdef evt_py_callable* tail = NULL
214
+ while callable_frame:
215
+ if py_callable_same(callable_frame.fn, py_callable):
216
+ if deduplicate:
217
+ return callable_frame
218
+ else:
219
+ self.logger.warning(f'Handler {<object> py_callable} already registered in {self}. Adding again will be called multiple times when triggered.')
220
+ tail = callable_frame
221
+ callable_frame = callable_frame.next
222
+
223
+ # Allocate new node
224
+ callable_frame = <evt_py_callable*> c_ap_alloc(sizeof(evt_py_callable), EE_HEAP_ALLOCATOR)
225
+ if not callable_frame:
226
+ raise MemoryError('Failed to allocate memory for new evt_py_callable')
227
+
228
+ Py_XINCREF(logger)
229
+ Py_XINCREF(py_callable)
230
+
231
+ callable_frame.fn = py_callable
232
+ callable_frame.logger = logger
233
+ callable_frame.idx = self.header.n_callbacks
234
+ callable_frame.with_topic = with_topic
235
+
236
+ if tail:
237
+ tail.next = callable_frame
238
+ else:
239
+ self.callables = callable_frame
240
+
241
+ c_evt_hook_register_callback(
242
+ self.header,
243
+ <const void*> EventHook.c_invoke_py_callable,
244
+ evt_callback_type.EVT_CALLBACK_WITH_PAYLOAD_USERDATA,
245
+ <void*> callable_frame,
246
+ 0
247
+ )
248
+ return callable_frame
249
+
250
+ cdef inline int c_remove_py_callable(self, PyObject* py_callable):
251
+ cdef evt_py_callable* curr = self.callables
252
+ cdef evt_py_callable* prior = NULL
253
+
254
+ while curr:
255
+ if py_callable_same(curr.fn, py_callable):
256
+ Py_XDECREF(curr.fn)
257
+ Py_XDECREF(curr.logger)
258
+ if prior:
259
+ prior.next = curr.next
260
+ else:
261
+ self.callables = curr.next
262
+ c_evt_hook_pop_callback(self.header, curr.idx)
263
+ prior = curr.next
264
+ while prior:
265
+ prior.idx -= 1
266
+ prior = prior.next
267
+ c_ap_free(curr)
268
+ return 1
269
+ prior = curr
270
+ curr = curr.next
271
+ return 0
272
+
273
+ cdef inline bint c_contains_py_callable(self, PyObject* py_callable):
274
+ cdef evt_py_callable* curr = self.callables
275
+
276
+ while curr:
277
+ if py_callable_same(curr.fn, py_callable):
278
+ return True
279
+ curr = curr.next
280
+ return False
281
+
282
+ cdef void c_free_py_callable(self):
283
+ cdef evt_py_callable* curr = self.callables
284
+ cdef evt_py_callable* next
285
+ while curr:
286
+ next = curr.next
287
+ Py_XDECREF(curr.fn)
288
+ Py_XDECREF(curr.logger)
289
+ c_evt_hook_pop_callback(self.header, 0)
290
+ c_ap_free(curr)
291
+ curr = next
292
+ self.callables = NULL
293
+
294
+ def __call__(self, MessagePayload msg):
295
+ c_evt_hook_invoke(self.header, msg.header)
296
+
297
+ def __iadd__(self, object py_callable):
298
+ self.add_handler(py_callable, None, True)
299
+ return self
300
+
301
+ def __isub__(self, object py_callable):
302
+ self.c_remove_py_callable(<PyObject*> py_callable)
303
+ return self
304
+
305
+ def __len__(self):
306
+ return self.header.n_callbacks
307
+
308
+ def __repr__(self):
309
+ if self.topic:
310
+ return f'<{self.__class__.__name__} "{self.topic.value}">(n={len(self)})'
311
+ return f'<{self.__class__.__name__} NO_TOPIC>(n={len(self)})'
312
+
313
+ def __iter__(self):
314
+ return self.handlers.__iter__()
315
+
316
+ def __contains__(self, object py_callable):
317
+ return self.c_contains_py_callable(<PyObject*> py_callable)
318
+
319
+ def trigger(self, MessagePayload msg):
320
+ c_evt_hook_invoke(self.header, msg.header)
321
+
322
+ def add_handler(self, object py_callable, object logger=None, bint deduplicate=False):
323
+ cdef object sig = inspect.signature(py_callable)
324
+ cdef object param
325
+ cdef bint with_topic = False
326
+
327
+ for param in sig.parameters.values():
328
+ if param.name == TOPIC_FIELD_NAME or param.kind == inspect.Parameter.VAR_KEYWORD:
329
+ with_topic = True
330
+ break
331
+
332
+ if logger is None:
333
+ logger = self.logger
334
+
335
+ self.c_add_py_callable(<PyObject*> py_callable, <PyObject*> logger, with_topic, deduplicate)
336
+
337
+ def remove_handler(self, object py_callable):
338
+ cdef int removed = self.c_remove_py_callable(<PyObject*> py_callable)
339
+ if not removed:
340
+ LOGGER.warning(f'{py_callable} not exist in {self} call stacks')
341
+
342
+ def clear(self):
343
+ self.c_free_py_callable()
344
+
345
+ property handlers:
346
+ def __get__(self):
347
+ cdef evt_py_callable* curr = self.callables
348
+ cdef list out = []
349
+ while curr:
350
+ out.append(dict(
351
+ fn=<object> curr.fn,
352
+ logger=<object> curr.logger,
353
+ idx=curr.idx,
354
+ with_topic=curr.with_topic
355
+ ))
356
+ Py_XINCREF(curr.fn)
357
+ Py_XINCREF(curr.logger)
358
+ curr = curr.next
359
+ return out
360
+
361
+
362
+ cdef inline void c_hook_enter(evt_hook* hook, evt_hook_watcher_type watcher_type, evt_message_payload* payload, void* user_data) noexcept nogil:
363
+ cdef evt_hook_stats* stats = <evt_hook_stats*> user_data
364
+ stats.n_calls += 1
365
+ stats.ts_call_start = perf_counter()
366
+
367
+
368
+ cdef inline void c_hook_exit(evt_hook* hook, evt_hook_watcher_type watcher_type, evt_message_payload* payload, void* user_data) noexcept nogil:
369
+ cdef evt_hook_stats* stats = <evt_hook_stats*> user_data
370
+ stats.ts_call_complete = perf_counter()
371
+ stats.elapsed_seconds += stats.ts_call_complete - stats.ts_call_start
372
+
373
+
374
+ cdef class EventHookEx(EventHook):
375
+ def __cinit__(self, Topic topic, object logger=None):
376
+ c_evt_hook_add_watcher(self.header, c_hook_enter, <void*> &self.hook_stats, evt_hook_watcher_type.EVT_HOOK_WATCHER_PRE_INVOKED)
377
+ c_evt_hook_add_watcher(self.header, c_hook_exit, <void*> &self.hook_stats, evt_hook_watcher_type.EVT_HOOK_WATCHER_POST_INVOKED)
378
+
379
+ property stats:
380
+ def __get__(self):
381
+ cdef dict stats = {
382
+ 'n_calls': self.hook_stats.n_calls,
383
+ 'last_call_start': self.hook_stats.ts_call_start,
384
+ 'last_call_complete': self.hook_stats.ts_call_complete,
385
+ 'elapsed_seconds': self.hook_stats.elapsed_seconds
386
+ }
387
+ return stats