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