PyEventEngine 0.6.0.post3__cp314-cp314t-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.cp314t-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.cp314t-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.cp314t-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.cp314t-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,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
|