PyEventEngine 0.6.2__cp315-cp315t-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 (56) hide show
  1. event_engine/__infra__.pxd +2 -0
  2. event_engine/__init__.pxd +2 -0
  3. event_engine/__init__.py +62 -0
  4. event_engine/base/__infra__.pxd +9 -0
  5. event_engine/base/__init__.pxd +9 -0
  6. event_engine/base/__init__.py +31 -0
  7. event_engine/base/c_allocator_protocol.c +14310 -0
  8. event_engine/base/c_allocator_protocol.cp315t-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 +193 -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 +32542 -0
  16. event_engine/capi/c_engine.cp315t-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 +478 -0
  20. event_engine/capi/c_engine.pyx +673 -0
  21. event_engine/capi/c_event.c +22826 -0
  22. event_engine/capi/c_event.cp315t-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 +265 -0
  26. event_engine/capi/c_event.pyx +444 -0
  27. event_engine/capi/c_topic.c +34545 -0
  28. event_engine/capi/c_topic.cp315t-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 +644 -0
  32. event_engine/capi/c_topic.pyx +818 -0
  33. event_engine/capi/fallback_engine.py +707 -0
  34. event_engine/config_view.c +10271 -0
  35. event_engine/config_view.cp315t-win_amd64.pyd +0 -0
  36. event_engine/config_view.pyx +55 -0
  37. event_engine/includes/event_engine/base/c_allocator_protocol.c +14310 -0
  38. event_engine/includes/event_engine/base/c_allocator_protocol.h +23 -0
  39. event_engine/includes/event_engine/capi/c_engine.c +32542 -0
  40. event_engine/includes/event_engine/capi/c_engine.h +393 -0
  41. event_engine/includes/event_engine/capi/c_event.c +22826 -0
  42. event_engine/includes/event_engine/capi/c_event.h +342 -0
  43. event_engine/includes/event_engine/capi/c_topic.c +34545 -0
  44. event_engine/includes/event_engine/capi/c_topic.h +1007 -0
  45. event_engine/includes/event_engine/config_view.c +10271 -0
  46. event_engine/native/__init__.py +60 -0
  47. event_engine/native/_event.py +420 -0
  48. event_engine/native/_topic.py +171 -0
  49. event_engine/native/engine.py +793 -0
  50. event_engine/native/event.py +469 -0
  51. event_engine/native/topic.py +943 -0
  52. pyeventengine-0.6.2.dist-info/METADATA +189 -0
  53. pyeventengine-0.6.2.dist-info/RECORD +56 -0
  54. pyeventengine-0.6.2.dist-info/WHEEL +5 -0
  55. pyeventengine-0.6.2.dist-info/licenses/LICENSE +21 -0
  56. pyeventengine-0.6.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,265 @@
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
+ ...
227
+
228
+
229
+ class EventTestToolkit:
230
+ """Relays internal C event-hook state to Python for test validation.
231
+
232
+ Note:
233
+ Test-only class. Not declared in any ``.pxd``; not part of the
234
+ public package API.
235
+ """
236
+
237
+ @staticmethod
238
+ def get_n_callbacks(hook: EventHook) -> int:
239
+ """Number of C callbacks registered on the hook."""
240
+ ...
241
+
242
+ @staticmethod
243
+ def get_n_pre_watchers(hook: EventHook) -> int:
244
+ """Number of pre-invoked watchers registered on the hook."""
245
+ ...
246
+
247
+ @staticmethod
248
+ def get_n_post_watchers(hook: EventHook) -> int:
249
+ """Number of post-invoked watchers registered on the hook."""
250
+ ...
251
+
252
+ @staticmethod
253
+ def get_callable_count(hook: EventHook) -> int:
254
+ """Length of the Python-callable linked list."""
255
+ ...
256
+
257
+ @staticmethod
258
+ def get_callable_with_topic(hook: EventHook, idx: int) -> bool:
259
+ """Whether the callable at ``idx`` is invoked with the topic kwarg."""
260
+ ...
261
+
262
+ @staticmethod
263
+ def get_hook_topic_addr(hook: EventHook) -> int:
264
+ """Address of the C topic backing the hook."""
265
+ ...
@@ -0,0 +1,444 @@
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 libc.stdint cimport uintptr_t
9
+
10
+ from cbase.allocator_protocol.c_allocator_protocol cimport c_ap_alloc, c_ap_free, c_ap_protocol_from_ptr
11
+
12
+ from ..base.c_allocator_protocol cimport EE_HEAP_ALLOCATOR
13
+ from ..base import LOGGER
14
+
15
+ LOGGER = LOGGER.getChild('Event')
16
+
17
+ cdef tuple EMPTY_ARGS = ()
18
+ cdef str TOPIC_FIELD_NAME = 'topic'
19
+
20
+
21
+ cdef inline evt_message_payload* c_evt_payload_new(Topic topic, tuple args, dict kwargs):
22
+ cdef size_t payload_size = sizeof(evt_message_payload) + sizeof(evt_py_payload)
23
+ cdef evt_message_payload* c_payload = <evt_message_payload*> c_ap_alloc(payload_size, EE_HEAP_ALLOCATOR)
24
+ if not c_payload:
25
+ return NULL
26
+
27
+ cdef evt_py_payload* py_payload = <evt_py_payload*> (c_payload + 1)
28
+ cdef PyObject* kwargs_aggregated = PyDict_Copy(<PyObject*> kwargs)
29
+ PyDict_SetDefault(kwargs_aggregated, <PyObject*> TOPIC_FIELD_NAME, <PyObject*> topic)
30
+
31
+ py_payload.py_topic = <PyObject*> topic
32
+ py_payload.py_args = <PyObject*> args
33
+ py_payload.py_kwargs = <PyObject*> kwargs
34
+ py_payload.py_kwargs_aggregated = kwargs_aggregated
35
+
36
+ Py_XINCREF(<PyObject*> topic)
37
+ Py_XINCREF(<PyObject*> args)
38
+ Py_XINCREF(<PyObject*> kwargs)
39
+ # Py_XINCREF(<PyObject*> kwargs_aggregated)
40
+
41
+ c_payload.args = py_payload
42
+ c_payload.topic = topic.header
43
+ return c_payload
44
+
45
+
46
+ cdef inline void c_evt_payload_free(evt_message_payload* payload):
47
+ cdef evt_py_payload* py_payload = <evt_py_payload*> (payload + 1)
48
+
49
+ Py_XDECREF(py_payload.py_topic)
50
+ Py_XDECREF(py_payload.py_args)
51
+ Py_XDECREF(py_payload.py_kwargs)
52
+ Py_XDECREF(py_payload.py_kwargs_aggregated)
53
+
54
+ c_ap_free(payload)
55
+
56
+
57
+ cdef class MessagePayload:
58
+ def __init__(self, Topic topic, tuple args, dict kwargs):
59
+ self.header = c_evt_payload_new(topic, args, kwargs)
60
+ if not self.header:
61
+ raise MemoryError('Failed to allocate memory for evt_message_payload')
62
+ self.owner = True
63
+
64
+ def __dealloc__(self):
65
+ if not self.owner:
66
+ return
67
+
68
+ if self.header:
69
+ c_evt_payload_free(self.header)
70
+
71
+ @staticmethod
72
+ cdef MessagePayload c_from_header(evt_message_payload* payload, bint owner=False):
73
+ cdef MessagePayload instance = MessagePayload.__new__(MessagePayload)
74
+ instance.header = payload
75
+ instance.owner = owner
76
+ return instance
77
+
78
+ def __repr__(self):
79
+ if not self.header:
80
+ return '<MessagePayload uninitialized>'
81
+ if self.header.topic:
82
+ return f'<MessagePayload "{self.topic.value}">(seq_id={self.seq_id}, args={self.args}, kwargs={self.kwargs})'
83
+ return f'<MessagePayload NO_TOPIC>(seq_id={self.seq_id}, args={self.args}, kwargs={self.kwargs})'
84
+
85
+ property topic:
86
+ def __get__(self):
87
+ if not self.header:
88
+ raise RuntimeError('Uninitialized c payload')
89
+ if not self.header.args:
90
+ raise RuntimeError('Uninitialized python payload')
91
+ cdef evt_py_payload* py_payload = <evt_py_payload*> self.header.args
92
+ cdef PyObject* py_topic = py_payload.py_topic
93
+ if py_topic:
94
+ Py_XINCREF(py_topic)
95
+ return <Topic> py_topic
96
+ cdef evt_topic* topic = self.header.topic
97
+ if topic:
98
+ return Topic.c_from_header(topic, False)
99
+ return None
100
+
101
+ property args:
102
+ def __get__(self):
103
+ if not self.header:
104
+ raise RuntimeError('Uninitialized c payload')
105
+ if not self.header.args:
106
+ raise RuntimeError('Uninitialized python payload')
107
+ cdef evt_py_payload* py_payload = <evt_py_payload*> self.header.args
108
+ cdef PyObject* py_args = py_payload.py_args
109
+ if py_args:
110
+ Py_XINCREF(py_args)
111
+ return <tuple> py_args
112
+ return None
113
+
114
+ property kwargs:
115
+ def __get__(self):
116
+ if not self.header:
117
+ raise RuntimeError('Uninitialized c payload')
118
+ if not self.header.args:
119
+ raise RuntimeError('Uninitialized python payload')
120
+ cdef evt_py_payload* py_payload = <evt_py_payload*> self.header.args
121
+ cdef PyObject* py_kwargs = py_payload.py_kwargs
122
+ if py_kwargs:
123
+ Py_XINCREF(py_kwargs)
124
+ return <dict> py_kwargs
125
+ return None
126
+
127
+ property kwargs_with_topic:
128
+ def __get__(self):
129
+ if not self.header:
130
+ raise RuntimeError('Uninitialized c payload')
131
+ if not self.header.args:
132
+ raise RuntimeError('Uninitialized python payload')
133
+ cdef evt_py_payload* py_payload = <evt_py_payload*> self.header.args
134
+ cdef PyObject* py_kwargs_aggregated = py_payload.py_kwargs_aggregated
135
+ if py_kwargs_aggregated:
136
+ Py_XINCREF(py_kwargs_aggregated)
137
+ return <dict> py_kwargs_aggregated
138
+ return None
139
+
140
+ property seq_id:
141
+ def __get__(self):
142
+ if not self.header:
143
+ raise RuntimeError('Uninitialized c payload')
144
+ return self.header.seq_id
145
+
146
+ def __set__(self, uint64_t seq_id):
147
+ if not self.header:
148
+ raise RuntimeError('Uninitialized c payload')
149
+ self.header.seq_id = seq_id
150
+
151
+
152
+ cdef inline bint py_callable_same(PyObject* a, PyObject* b):
153
+ if a == b:
154
+ return True
155
+
156
+ # Both are bound methods
157
+ cdef object fn_a = <object> a
158
+ cdef object fn_b = <object> b
159
+ if PyMethod_Check(fn_a) and PyMethod_Check(fn_b):
160
+ return PyMethod_GET_SELF(fn_a) == PyMethod_GET_SELF(fn_b) and PyMethod_GET_FUNCTION(fn_a) == PyMethod_GET_FUNCTION(fn_b)
161
+ return False
162
+
163
+
164
+ cdef class EventHook:
165
+ def __cinit__(self, Topic topic, object logger=None):
166
+ self.header = c_evt_hook_new(topic.header, EE_HEAP_ALLOCATOR)
167
+ if not self.header:
168
+ raise MemoryError(f'Failed to allocate memory for {self.__class__.__name__}')
169
+ self.callables = NULL
170
+ self.topic = topic
171
+ self.logger = LOGGER.getChild(f'EventHook.{topic}') if logger is None else logger
172
+
173
+ def __dealloc__(self):
174
+ self.c_free_py_callable()
175
+ if self.header:
176
+ c_evt_hook_free(self.header)
177
+
178
+ @staticmethod
179
+ cdef inline void c_invoke_py_callable(evt_message_payload* payload, void* user_data) with gil:
180
+ cdef evt_py_callable* ctx = <evt_py_callable*> user_data
181
+ cdef bint with_topic = ctx.with_topic
182
+ cdef evt_py_payload* py_payload = <evt_py_payload*> payload.args
183
+
184
+ cdef PyObject* fn = ctx.fn
185
+ cdef PyObject* args = py_payload.py_args
186
+ cdef PyObject* kwargs = py_payload.py_kwargs_aggregated if with_topic else py_payload.py_kwargs
187
+ cdef PyObject* res = PyObject_Call(fn, args, kwargs)
188
+
189
+ if res:
190
+ Py_XDECREF(res)
191
+ return
192
+
193
+ # Fetch the current Python exception (steals references; clears the indicator)
194
+ cdef PyObject* etype = NULL
195
+ cdef PyObject* evalue = NULL
196
+ cdef PyObject* etrace = NULL
197
+ cdef object logger = <object> ctx.logger
198
+ cdef object formatted
199
+
200
+ PyErr_Fetch(&etype, &evalue, &etrace)
201
+ formatted = traceback.format_exception(<object> etype, (<object> evalue) if evalue else None, (<object> etrace) if etrace else None)
202
+ logger.error("".join(formatted))
203
+ Py_XDECREF(etype)
204
+ Py_XDECREF(evalue)
205
+ Py_XDECREF(etrace)
206
+ PyErr_Clear()
207
+
208
+ cdef inline evt_py_callable* c_add_py_callable(self, PyObject* py_callable, PyObject* logger, bint with_topic, bint deduplicate):
209
+ if not PyCallable_Check(py_callable):
210
+ raise ValueError('Callback handler must be callable')
211
+
212
+ cdef evt_py_callable* callable_frame = self.callables
213
+
214
+ # Walk list to detect duplicates and position at tail
215
+ cdef evt_py_callable* tail = NULL
216
+ while callable_frame:
217
+ if py_callable_same(callable_frame.fn, py_callable):
218
+ if deduplicate:
219
+ return callable_frame
220
+ else:
221
+ self.logger.warning(f'Handler {<object> py_callable} already registered in {self}. Adding again will be called multiple times when triggered.')
222
+ tail = callable_frame
223
+ callable_frame = callable_frame.next
224
+
225
+ # Allocate new node
226
+ callable_frame = <evt_py_callable*> c_ap_alloc(sizeof(evt_py_callable), EE_HEAP_ALLOCATOR)
227
+ if not callable_frame:
228
+ raise MemoryError('Failed to allocate memory for new evt_py_callable')
229
+
230
+ Py_XINCREF(logger)
231
+ Py_XINCREF(py_callable)
232
+
233
+ callable_frame.fn = py_callable
234
+ callable_frame.logger = logger
235
+ callable_frame.idx = self.header.n_callbacks
236
+ callable_frame.with_topic = with_topic
237
+
238
+ if tail:
239
+ tail.next = callable_frame
240
+ else:
241
+ self.callables = callable_frame
242
+
243
+ c_evt_hook_register_callback(
244
+ self.header,
245
+ <const void*> EventHook.c_invoke_py_callable,
246
+ evt_callback_type.EVT_CALLBACK_WITH_PAYLOAD_USERDATA,
247
+ <void*> callable_frame,
248
+ 0
249
+ )
250
+ return callable_frame
251
+
252
+ cdef inline int c_remove_py_callable(self, PyObject* py_callable):
253
+ cdef evt_py_callable* curr = self.callables
254
+ cdef evt_py_callable* prior = NULL
255
+
256
+ while curr:
257
+ if py_callable_same(curr.fn, py_callable):
258
+ Py_XDECREF(curr.fn)
259
+ Py_XDECREF(curr.logger)
260
+ if prior:
261
+ prior.next = curr.next
262
+ else:
263
+ self.callables = curr.next
264
+ c_evt_hook_pop_callback(self.header, curr.idx)
265
+ prior = curr.next
266
+ while prior:
267
+ prior.idx -= 1
268
+ prior = prior.next
269
+ c_ap_free(curr)
270
+ return 1
271
+ prior = curr
272
+ curr = curr.next
273
+ return 0
274
+
275
+ cdef inline bint c_contains_py_callable(self, PyObject* py_callable):
276
+ cdef evt_py_callable* curr = self.callables
277
+
278
+ while curr:
279
+ if py_callable_same(curr.fn, py_callable):
280
+ return True
281
+ curr = curr.next
282
+ return False
283
+
284
+ cdef void c_free_py_callable(self):
285
+ cdef evt_py_callable* curr = self.callables
286
+ cdef evt_py_callable* next
287
+ while curr:
288
+ next = curr.next
289
+ Py_XDECREF(curr.fn)
290
+ Py_XDECREF(curr.logger)
291
+ c_evt_hook_pop_callback(self.header, 0)
292
+ c_ap_free(curr)
293
+ curr = next
294
+ self.callables = NULL
295
+
296
+ def __call__(self, MessagePayload msg):
297
+ c_evt_hook_invoke(self.header, msg.header)
298
+
299
+ def __iadd__(self, object py_callable):
300
+ self.add_handler(py_callable, None, True)
301
+ return self
302
+
303
+ def __isub__(self, object py_callable):
304
+ self.c_remove_py_callable(<PyObject*> py_callable)
305
+ return self
306
+
307
+ def __len__(self):
308
+ return self.header.n_callbacks
309
+
310
+ def __repr__(self):
311
+ if self.topic:
312
+ return f'<{self.__class__.__name__} "{self.topic.value}">(n={len(self)})'
313
+ return f'<{self.__class__.__name__} NO_TOPIC>(n={len(self)})'
314
+
315
+ def __iter__(self):
316
+ return self.handlers.__iter__()
317
+
318
+ def __contains__(self, object py_callable):
319
+ return self.c_contains_py_callable(<PyObject*> py_callable)
320
+
321
+ def trigger(self, MessagePayload msg):
322
+ c_evt_hook_invoke(self.header, msg.header)
323
+
324
+ def add_handler(self, object py_callable, object logger=None, bint deduplicate=False):
325
+ cdef object sig = inspect.signature(py_callable)
326
+ cdef object param
327
+ cdef bint with_topic = False
328
+
329
+ for param in sig.parameters.values():
330
+ if param.name == TOPIC_FIELD_NAME or param.kind == inspect.Parameter.VAR_KEYWORD:
331
+ with_topic = True
332
+ break
333
+
334
+ if logger is None:
335
+ logger = self.logger
336
+
337
+ self.c_add_py_callable(<PyObject*> py_callable, <PyObject*> logger, with_topic, deduplicate)
338
+
339
+ def remove_handler(self, object py_callable):
340
+ cdef int removed = self.c_remove_py_callable(<PyObject*> py_callable)
341
+ if not removed:
342
+ LOGGER.warning(f'{py_callable} not exist in {self} call stacks')
343
+
344
+ def clear(self):
345
+ self.c_free_py_callable()
346
+
347
+ property handlers:
348
+ def __get__(self):
349
+ cdef evt_py_callable* curr = self.callables
350
+ cdef list out = []
351
+ while curr:
352
+ out.append(dict(
353
+ fn=<object> curr.fn,
354
+ logger=<object> curr.logger,
355
+ idx=curr.idx,
356
+ with_topic=curr.with_topic
357
+ ))
358
+ Py_XINCREF(curr.fn)
359
+ Py_XINCREF(curr.logger)
360
+ curr = curr.next
361
+ return out
362
+
363
+
364
+ cdef inline void c_hook_enter(evt_hook* hook, evt_hook_watcher_type watcher_type, evt_message_payload* payload, void* user_data) noexcept nogil:
365
+ cdef evt_hook_stats* stats = <evt_hook_stats*> user_data
366
+ stats.n_calls += 1
367
+ stats.ts_call_start = perf_counter()
368
+
369
+
370
+ cdef inline void c_hook_exit(evt_hook* hook, evt_hook_watcher_type watcher_type, evt_message_payload* payload, void* user_data) noexcept nogil:
371
+ cdef evt_hook_stats* stats = <evt_hook_stats*> user_data
372
+ stats.ts_call_complete = perf_counter()
373
+ stats.elapsed_seconds += stats.ts_call_complete - stats.ts_call_start
374
+
375
+
376
+ cdef class EventHookEx(EventHook):
377
+ def __cinit__(self, Topic topic, object logger=None):
378
+ c_evt_hook_add_watcher(self.header, c_hook_enter, <void*> &self.hook_stats, evt_hook_watcher_type.EVT_HOOK_WATCHER_PRE_INVOKED)
379
+ c_evt_hook_add_watcher(self.header, c_hook_exit, <void*> &self.hook_stats, evt_hook_watcher_type.EVT_HOOK_WATCHER_POST_INVOKED)
380
+
381
+ property stats:
382
+ def __get__(self):
383
+ cdef dict stats = {
384
+ 'n_calls': self.hook_stats.n_calls,
385
+ 'last_call_start': self.hook_stats.ts_call_start,
386
+ 'last_call_complete': self.hook_stats.ts_call_complete,
387
+ 'elapsed_seconds': self.hook_stats.elapsed_seconds
388
+ }
389
+ return stats
390
+
391
+
392
+ cdef class EventTestToolkit:
393
+ """Relays internal C event-hook state to Python for test validation.
394
+
395
+ Exposes the raw ``evt_hook`` counters (callback and watcher arrays) and
396
+ the ``evt_py_callable`` linked list, which are internal C state not
397
+ exposed through the public Python API.
398
+
399
+ Note:
400
+ Test-only class. Not declared in any ``.pxd``; not part of the
401
+ public package API.
402
+ """
403
+
404
+ @staticmethod
405
+ def get_n_callbacks(EventHook hook) -> size_t:
406
+ """Number of C callbacks registered on the hook."""
407
+ return hook.header.n_callbacks
408
+
409
+ @staticmethod
410
+ def get_n_pre_watchers(EventHook hook) -> size_t:
411
+ """Number of pre-invoked watchers registered on the hook."""
412
+ return hook.header.n_pre_watchers
413
+
414
+ @staticmethod
415
+ def get_n_post_watchers(EventHook hook) -> size_t:
416
+ """Number of post-invoked watchers registered on the hook."""
417
+ return hook.header.n_post_watchers
418
+
419
+ @staticmethod
420
+ def get_callable_count(EventHook hook) -> size_t:
421
+ """Length of the Python-callable linked list."""
422
+ cdef evt_py_callable* curr = hook.callables
423
+ cdef size_t n = 0
424
+ while curr:
425
+ n += 1
426
+ curr = curr.next
427
+ return n
428
+
429
+ @staticmethod
430
+ def get_callable_with_topic(EventHook hook, size_t idx) -> bint:
431
+ """Whether the callable at ``idx`` is invoked with the topic kwarg."""
432
+ cdef evt_py_callable* curr = hook.callables
433
+ cdef size_t i = 0
434
+ while curr:
435
+ if i == idx:
436
+ return curr.with_topic
437
+ i += 1
438
+ curr = curr.next
439
+ raise IndexError(f'Index {idx} out of range!')
440
+
441
+ @staticmethod
442
+ def get_hook_topic_addr(EventHook hook) -> uintptr_t:
443
+ """Address of the C topic backing the hook."""
444
+ return <uintptr_t> hook.header.topic