PyEventEngine 0.6.2__cp315-cp315-win_amd64.whl

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