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,673 @@
1
+ from threading import Thread
2
+
3
+ from cpython.datetime cimport timedelta
4
+ from cpython.object cimport PyObject
5
+ from cpython.ref cimport Py_INCREF, Py_XDECREF, Py_XINCREF
6
+ from cpython.time cimport perf_counter
7
+ from cpython.unicode cimport PyUnicode_FromStringAndSize
8
+
9
+ from cbase.bytemap.c_bytemap cimport bytemap_ret_code, bytemap, bytemap_entry, c_bytemap_new, c_bytemap_clear, c_bytemap_free, c_bytemap_get, c_bytemap_set, c_bytemap_pop, c_bytemap_entry_value
10
+
11
+ from .c_event cimport EMPTY_ARGS, MessagePayload, c_evt_hook_invoke, c_evt_payload_free, c_evt_payload_new, evt_py_payload
12
+ from .c_topic cimport Topic, c_topic_match_bool
13
+ from ..base.c_allocator_protocol cimport EE_HEAP_ALLOCATOR
14
+ from ..base import LOGGER
15
+
16
+ LOGGER = LOGGER.getChild('Engine')
17
+
18
+
19
+ class Full(Exception):
20
+ pass
21
+
22
+
23
+ class Empty(Exception):
24
+ pass
25
+
26
+
27
+ cdef class EventEngine:
28
+ def __cinit__(self, size_t capacity=DEFAULT_MQ_CAPACITY, object logger=None):
29
+ self.logger = LOGGER.getChild(f'EventEngine') if logger is None else logger
30
+ cdef allocator_protocol* allocator = EE_HEAP_ALLOCATOR
31
+
32
+ self.mq = c_mq_new(capacity, NULL, allocator)
33
+ if not self.mq:
34
+ raise MemoryError(f'Failed to allocate MessageQueue for {self.__class__.__name__}.')
35
+
36
+ self.exact_topic_hooks = c_bytemap_new(0, NULL)
37
+ if not self.exact_topic_hooks:
38
+ c_mq_free(self.mq)
39
+ self.mq = NULL
40
+ raise MemoryError(f'Failed to allocate exact_topic_hooks for {self.__class__.__name__}.')
41
+
42
+ self.generic_topic_hooks = c_bytemap_new(0, NULL)
43
+ if not self.generic_topic_hooks:
44
+ c_mq_free(self.mq)
45
+ c_bytemap_free(self.exact_topic_hooks)
46
+ self.mq = NULL
47
+ raise MemoryError(f'Failed to allocate generic_topic_hooks for {self.__class__.__name__}.')
48
+
49
+ self.seq_id = 0
50
+
51
+ def __dealloc__(self):
52
+ if self.mq:
53
+ c_mq_free(self.mq)
54
+ self.mq = NULL
55
+
56
+ if self.exact_topic_hooks:
57
+ c_bytemap_free(self.exact_topic_hooks)
58
+ self.exact_topic_hooks = NULL
59
+
60
+ if self.generic_topic_hooks:
61
+ c_bytemap_free(self.generic_topic_hooks)
62
+ self.generic_topic_hooks = NULL
63
+
64
+ cdef inline void c_loop(self):
65
+ if not self.mq:
66
+ raise RuntimeError('Not initialized!')
67
+
68
+ cdef evt_message_payload* msg = NULL
69
+ cdef evt_py_payload* py_payload = NULL
70
+ cdef message_queue* mq = self.mq
71
+ cdef int ret_code
72
+
73
+ while self.active:
74
+ # Step 1: Await message
75
+ with nogil:
76
+ ret_code = c_mq_get_hybrid(mq, &msg, DEFAULT_MQ_SPIN_LIMIT, DEFAULT_MQ_TIMEOUT_SECONDS)
77
+ if ret_code != 0:
78
+ continue
79
+
80
+ # Trigger message callbacks
81
+ self.c_trigger(msg)
82
+
83
+ # Clean up the message payload
84
+ c_evt_payload_free(msg)
85
+
86
+ cdef inline evt_message_payload* c_get(self, bint block, size_t max_spin, double timeout):
87
+ cdef evt_message_payload* msg = NULL
88
+ cdef int ret_code
89
+ if block:
90
+ ret_code = c_mq_get_hybrid(self.mq, &msg, max_spin, timeout)
91
+ else:
92
+ ret_code = c_mq_get(self.mq, &msg)
93
+
94
+ if ret_code != 0:
95
+ return NULL
96
+ return msg
97
+
98
+ cdef inline int c_publish(self, Topic topic, tuple args, dict kwargs, bint block, size_t max_spin, double timeout):
99
+ if not topic.header.is_exact:
100
+ raise ValueError('Topic must be all of exact parts')
101
+
102
+ # Step 0: Request payload buffer (MUST be done with GIL held - allocator is NOT thread-safe)
103
+ cdef evt_message_payload* payload = c_evt_payload_new(topic, args, kwargs)
104
+
105
+ # Step 1: Assembling payload (MUST be done with GIL held - touching Python objects)
106
+ payload.seq_id = self.seq_id
107
+ self.seq_id += 1
108
+
109
+ # Step 2: Send the payload (can be done without GIL - queue is thread-safe)
110
+ cdef int ret_code
111
+ with nogil:
112
+ if block:
113
+ ret_code = c_mq_put_hybrid(self.mq, payload, max_spin, timeout)
114
+ else:
115
+ ret_code = c_mq_put(self.mq, payload)
116
+
117
+ # Step 4: Handle failure case (undo increfs and free payload)
118
+ if not ret_code:
119
+ return ret_code
120
+
121
+ self.seq_id -= 1
122
+ c_evt_payload_free(payload)
123
+ return ret_code
124
+
125
+ cdef inline void c_trigger(self, evt_message_payload* msg):
126
+ cdef evt_topic* msg_topic = msg.topic
127
+ # Step 1: Match exact_topic_hooks
128
+ cdef PyObject* hook_ptr = NULL
129
+ cdef EventHook event_hook
130
+
131
+ cdef int ret_code = c_bytemap_get(self.exact_topic_hooks, msg_topic.key, msg_topic.key_len, <void**> &hook_ptr)
132
+ if hook_ptr:
133
+ event_hook = <EventHook> hook_ptr
134
+ c_evt_hook_invoke(event_hook.header, msg)
135
+
136
+ # Step 2: Match generic_topic_hooks
137
+ cdef bytemap_entry* entry = self.generic_topic_hooks.first
138
+ cdef int is_matched
139
+ while entry:
140
+ hook_ptr = <PyObject*> c_bytemap_entry_value(entry)
141
+ if not hook_ptr:
142
+ entry = entry.next
143
+ continue
144
+ event_hook = <EventHook> <PyObject*> hook_ptr
145
+ is_matched = c_topic_match_bool(event_hook.topic.header, msg_topic)
146
+ if is_matched:
147
+ c_evt_hook_invoke(event_hook.header, msg)
148
+ entry = entry.next
149
+
150
+ cdef inline EventHook c_get_hook(self, Topic topic):
151
+ cdef evt_topic* topic_ptr = topic.header
152
+ cdef PyObject* hook_ptr = NULL
153
+ cdef EventHook event_hook
154
+ cdef bytemap* hook_map
155
+
156
+ if topic_ptr.is_exact:
157
+ hook_map = self.exact_topic_hooks
158
+ else:
159
+ hook_map = self.generic_topic_hooks
160
+
161
+ cdef int ret_code = c_bytemap_get(hook_map, topic_ptr.key, topic_ptr.key_len, <void**> &hook_ptr)
162
+ if ret_code == bytemap_ret_code.BYTEMAP_ERR_NOT_FOUND:
163
+ raise KeyError(f'No EventHook registered for {topic.value}')
164
+ event_hook = <EventHook> hook_ptr
165
+ Py_XINCREF(hook_ptr)
166
+ return event_hook
167
+
168
+ cdef inline void c_register_hook(self, EventHook hook):
169
+ cdef evt_topic* topic_ptr = hook.topic.header
170
+ cdef PyObject* existing_hook_ptr = NULL
171
+ cdef bytemap* hook_map
172
+
173
+ if topic_ptr.is_exact:
174
+ hook_map = self.exact_topic_hooks
175
+ else:
176
+ hook_map = self.generic_topic_hooks
177
+
178
+ cdef int ret_code = c_bytemap_get(hook_map, topic_ptr.key, topic_ptr.key_len, <void**> &existing_hook_ptr)
179
+ if existing_hook_ptr and existing_hook_ptr != <PyObject*> hook:
180
+ raise KeyError(f'Another EventHook already registered for {hook.topic.value}')
181
+ ret_code = c_bytemap_set(hook_map, topic_ptr.key, topic_ptr.key_len, <void*> <PyObject*> hook, NULL)
182
+ Py_INCREF(hook)
183
+
184
+ cdef inline EventHook c_unregister_hook(self, Topic topic):
185
+ cdef evt_topic* topic_ptr = topic.header
186
+ cdef PyObject* existing_hook_ptr = NULL
187
+ cdef bytemap* hook_map
188
+
189
+ if topic_ptr.is_exact:
190
+ hook_map = self.exact_topic_hooks
191
+ else:
192
+ hook_map = self.generic_topic_hooks
193
+ cdef int ret_code = c_bytemap_pop(hook_map, <char*> topic_ptr.key, topic_ptr.key_len, <void**> &existing_hook_ptr)
194
+ if not existing_hook_ptr:
195
+ raise KeyError(f'No EventHook registered for {topic.value}')
196
+ cdef EventHook hook = <EventHook> existing_hook_ptr
197
+ Py_XDECREF(<PyObject*> hook)
198
+ return hook
199
+
200
+ cdef inline void c_register_handler(self, Topic topic, object py_callable, bint deduplicate):
201
+ cdef evt_topic* topic_ptr = topic.header
202
+ cdef PyObject* hook_ptr = NULL
203
+ cdef EventHook event_hook
204
+ cdef bytemap* hook_map
205
+
206
+ if topic_ptr.is_exact:
207
+ hook_map = self.exact_topic_hooks
208
+ else:
209
+ hook_map = self.generic_topic_hooks
210
+
211
+ cdef int ret_code = c_bytemap_get(hook_map, topic_ptr.key, topic_ptr.key_len, <void**> &hook_ptr)
212
+ if ret_code == bytemap_ret_code.BYTEMAP_ERR_NOT_FOUND:
213
+ event_hook = EventHook.__new__(EventHook, topic, self.logger)
214
+ c_bytemap_set(hook_map, topic_ptr.key, topic_ptr.key_len, <void*> <PyObject*> event_hook, NULL)
215
+ Py_INCREF(event_hook)
216
+ elif not hook_ptr:
217
+ raise BufferError(f'Corrected buffer, invalid registered event hook.')
218
+ else:
219
+ event_hook = <EventHook> hook_ptr
220
+ event_hook.add_handler(py_callable, None, deduplicate)
221
+
222
+ cdef inline void c_unregister_handler(self, Topic topic, object py_callable):
223
+ cdef evt_topic* topic_ptr = topic.header
224
+ cdef PyObject* hook_ptr = NULL
225
+ cdef EventHook event_hook
226
+ cdef bytemap* hook_map
227
+
228
+ if topic_ptr.is_exact:
229
+ hook_map = self.exact_topic_hooks
230
+ else:
231
+ hook_map = self.generic_topic_hooks
232
+
233
+ cdef int ret_code = c_bytemap_get(hook_map, topic_ptr.key, topic_ptr.key_len, <void**> &hook_ptr)
234
+ if ret_code == bytemap_ret_code.BYTEMAP_ERR_NOT_FOUND:
235
+ LOGGER.error(f'No EventHook registered for "{topic.value}"')
236
+ return
237
+ event_hook = <EventHook> hook_ptr
238
+ event_hook.remove_handler(py_callable)
239
+ if len(event_hook) == 0:
240
+ c_bytemap_pop(hook_map, topic_ptr.key, topic_ptr.key_len, NULL)
241
+ Py_XDECREF(<PyObject*> event_hook)
242
+
243
+ cdef inline void c_clear(self):
244
+ cdef bytemap_entry* entry
245
+ cdef EventHook event_hook
246
+
247
+ # Clear exact_topic_hooks
248
+ entry = self.exact_topic_hooks.first
249
+ while entry:
250
+ if c_bytemap_entry_value(entry):
251
+ event_hook = <EventHook> <PyObject*> c_bytemap_entry_value(entry)
252
+ event_hook.clear()
253
+ Py_XDECREF(<PyObject*> event_hook)
254
+ entry = entry.next
255
+ c_bytemap_clear(self.exact_topic_hooks)
256
+
257
+ # Clear generic_topic_hooks
258
+ entry = self.generic_topic_hooks.first
259
+ while entry:
260
+ if c_bytemap_entry_value(entry):
261
+ event_hook = <EventHook> <PyObject*> c_bytemap_entry_value(entry)
262
+ event_hook.clear()
263
+ Py_XDECREF(<PyObject*> event_hook)
264
+ entry = entry.next
265
+ c_bytemap_clear(self.generic_topic_hooks)
266
+
267
+ # --- Python Interfaces---
268
+
269
+ def __len__(self):
270
+ count = 0
271
+ entry = self.exact_topic_hooks.first
272
+ while entry:
273
+ if c_bytemap_entry_value(entry):
274
+ count += 1
275
+ entry = entry.next
276
+ entry = self.generic_topic_hooks.first
277
+ while entry:
278
+ if c_bytemap_entry_value(entry):
279
+ count += 1
280
+ entry = entry.next
281
+ return count
282
+
283
+ def __repr__(self):
284
+ return f'<{self.__class__.__name__} {"active" if self.active else "idle"}>(capacity={self.capacity})'
285
+
286
+ def __getitem__(self, Topic topic) -> list[EventHook]:
287
+ cdef list out = []
288
+ cdef evt_topic* topic_ptr = topic.header
289
+ cdef PyObject* hook_ptr = NULL
290
+ cdef EventHook event_hook
291
+
292
+ cdef int ret_code = c_bytemap_get(self.exact_topic_hooks, topic_ptr.key, topic_ptr.key_len, <void**> &hook_ptr)
293
+ if hook_ptr:
294
+ event_hook = <EventHook> hook_ptr
295
+ Py_XINCREF(hook_ptr)
296
+ out.append(event_hook)
297
+
298
+ cdef bytemap_entry* entry = self.generic_topic_hooks.first
299
+ cdef int is_matched
300
+ while entry:
301
+ hook_ptr = <PyObject*> c_bytemap_entry_value(entry)
302
+ if not hook_ptr:
303
+ entry = entry.next
304
+ continue
305
+ event_hook = <EventHook> hook_ptr
306
+ is_matched = c_topic_match_bool(event_hook.topic.header, topic_ptr)
307
+ if is_matched:
308
+ Py_XINCREF(hook_ptr)
309
+ out.append(event_hook)
310
+ entry = entry.next
311
+ return out
312
+
313
+ def activate(self):
314
+ self.active = True
315
+
316
+ def deactivate(self):
317
+ self.active = False
318
+
319
+ def run(self):
320
+ self.c_loop()
321
+
322
+ def start(self):
323
+ if self.active:
324
+ self.logger.warning(f'{self} already started!')
325
+ return
326
+ self.active = True
327
+ self.engine = Thread(target=self.run, name='EventEngine')
328
+ self.engine.start()
329
+ self.logger.info(f'{self} started.')
330
+
331
+ def stop(self) -> None:
332
+ if not self.active:
333
+ self.logger.warning('EventEngine already stopped!')
334
+ return
335
+
336
+ self.active = False
337
+ self.engine.join()
338
+
339
+ def clear(self) -> None:
340
+ if self.active:
341
+ self.logger.error('EventEngine must be stopped before cleared!')
342
+ return
343
+
344
+ self.c_clear()
345
+
346
+ def get(self, bint block=True, size_t max_spin=DEFAULT_MQ_SPIN_LIMIT, double timeout=0.0) -> MessagePayload:
347
+ cdef evt_message_payload* msg = self.c_get(block, max_spin, timeout)
348
+ if not msg:
349
+ raise Empty()
350
+ cdef MessagePayload payload = MessagePayload.c_from_header(msg, True)
351
+ return payload
352
+
353
+ def put(self, Topic topic, *args, bint block=True, size_t max_spin=DEFAULT_MQ_SPIN_LIMIT, double timeout=0.0, **kwargs):
354
+ cdef int ret_code = self.c_publish(topic, args, kwargs, block, max_spin, timeout)
355
+ if ret_code:
356
+ raise Full()
357
+
358
+ def publish(self, Topic topic, tuple args, dict kwargs, bint block=True, size_t max_spin=DEFAULT_MQ_SPIN_LIMIT, double timeout=0.0):
359
+ cdef int ret_code = self.c_publish(topic, args, kwargs, block, max_spin, timeout)
360
+ if ret_code:
361
+ raise Full()
362
+
363
+ def get_hook(self, Topic topic):
364
+ return self.c_get_hook(topic)
365
+
366
+ def register_hook(self, EventHook hook):
367
+ self.c_register_hook(hook)
368
+
369
+ def unregister_hook(self, Topic topic) -> EventHook:
370
+ return self.c_unregister_hook(topic)
371
+
372
+ def register_handler(self, Topic topic, object handler, bint deduplicate=False):
373
+ self.c_register_handler(topic, handler, deduplicate)
374
+
375
+ def unregister_handler(self, Topic topic, object handler):
376
+ self.c_unregister_handler(topic, handler)
377
+
378
+ def event_hooks(self):
379
+ entry = self.exact_topic_hooks.first
380
+ while entry:
381
+ if c_bytemap_entry_value(entry):
382
+ yield <EventHook> <PyObject*> c_bytemap_entry_value(entry)
383
+ entry = entry.next
384
+ entry = self.generic_topic_hooks.first
385
+ while entry:
386
+ if c_bytemap_entry_value(entry):
387
+ yield <EventHook> <PyObject*> c_bytemap_entry_value(entry)
388
+ entry = entry.next
389
+
390
+ def topics(self):
391
+ entry = self.exact_topic_hooks.first
392
+ while entry:
393
+ if c_bytemap_entry_value(entry):
394
+ yield (<EventHook> <PyObject*> c_bytemap_entry_value(entry)).topic
395
+ entry = entry.next
396
+ entry = self.generic_topic_hooks.first
397
+ while entry:
398
+ if c_bytemap_entry_value(entry):
399
+ yield (<EventHook> <PyObject*> c_bytemap_entry_value(entry)).topic
400
+ entry = entry.next
401
+
402
+ def items(self):
403
+ entry = self.exact_topic_hooks.first
404
+ while entry:
405
+ if c_bytemap_entry_value(entry):
406
+ hook = <EventHook> <PyObject*> c_bytemap_entry_value(entry)
407
+ yield (hook.topic, hook)
408
+ entry = entry.next
409
+ entry = self.generic_topic_hooks.first
410
+ while entry:
411
+ if c_bytemap_entry_value(entry):
412
+ hook = <EventHook> <PyObject*> c_bytemap_entry_value(entry)
413
+ yield (hook.topic, hook)
414
+ entry = entry.next
415
+
416
+ property capacity:
417
+ def __get__(self):
418
+ return self.mq.capacity
419
+
420
+ property occupied:
421
+ def __get__(self):
422
+ return c_mq_occupied(self.mq)
423
+
424
+ property exact_topic_hook_map:
425
+ def __get__(self):
426
+ cdef dict out = {}
427
+ cdef bytemap_entry* entry = self.exact_topic_hooks.first
428
+ cdef str topic_str
429
+ cdef EventHook event_hook
430
+ while entry:
431
+ topic_str = PyUnicode_FromStringAndSize(entry.key, entry.key_length)
432
+ event_hook = <EventHook> <PyObject*> c_bytemap_entry_value(entry)
433
+ entry = entry.next
434
+ Py_XINCREF(<PyObject*> event_hook)
435
+ out[Topic(topic_str)] = event_hook
436
+ return out
437
+
438
+ property generic_topic_hook_map:
439
+ def __get__(self):
440
+ cdef dict out = {}
441
+ cdef bytemap_entry* entry = self.generic_topic_hooks.first
442
+ cdef str topic_str
443
+ cdef EventHook event_hook
444
+ while entry:
445
+ topic_str = PyUnicode_FromStringAndSize(entry.key, entry.key_length)
446
+ event_hook = <EventHook> <PyObject*> c_bytemap_entry_value(entry)
447
+ entry = entry.next
448
+ Py_XINCREF(<PyObject*> event_hook)
449
+ out[Topic(topic_str)] = event_hook
450
+ return out
451
+
452
+
453
+ cdef class EventEngineEx(EventEngine):
454
+ def __cinit__(self, size_t capacity=DEFAULT_MQ_CAPACITY, object logger=None):
455
+ self.timer = {}
456
+
457
+ cdef inline void c_timer_loop(self, double interval, Topic topic, datetime activate_time):
458
+ from time import sleep
459
+ cdef datetime scheduled_time
460
+
461
+ if activate_time is None:
462
+ scheduled_time = datetime.now()
463
+ else:
464
+ scheduled_time = activate_time
465
+
466
+ cdef dict kwargs = {'interval': interval, 'trigger_time': scheduled_time}
467
+
468
+ while self.active:
469
+ sleep_time = (scheduled_time - datetime.now()).total_seconds()
470
+
471
+ if sleep_time > 0:
472
+ sleep(sleep_time)
473
+ self.c_publish(topic, EMPTY_ARGS, kwargs, True, DEFAULT_MQ_SPIN_LIMIT, 0.0)
474
+
475
+ while scheduled_time < datetime.now():
476
+ scheduled_time += timedelta(seconds=interval)
477
+ kwargs['trigger_time'] = scheduled_time
478
+
479
+ cdef inline void c_minute_timer_loop(self, Topic topic):
480
+ from time import time, sleep
481
+ cdef double t, scheduled_time, next_time, sleep_time
482
+ cdef dict kwargs = {'interval': 60}
483
+
484
+ while self.active:
485
+ t = time()
486
+ scheduled_time = t // 60 * 60
487
+ next_time = scheduled_time + 60
488
+ sleep_time = next_time - t
489
+ sleep(sleep_time)
490
+ kwargs['timestamp'] = scheduled_time
491
+ self.c_publish(topic, EMPTY_ARGS, kwargs, True, DEFAULT_MQ_SPIN_LIMIT, 0.0)
492
+
493
+ cdef inline void c_second_timer_loop(self, Topic topic):
494
+ from time import time, sleep
495
+ cdef double t, scheduled_time, next_time, sleep_time
496
+ cdef dict kwargs = {'interval': 1}
497
+
498
+ while self.active:
499
+ t = time()
500
+ scheduled_time = t // 1
501
+ next_time = scheduled_time + 1
502
+ sleep_time = next_time - t
503
+ sleep(sleep_time)
504
+ kwargs['timestamp'] = scheduled_time
505
+ self.c_publish(topic, EMPTY_ARGS, kwargs, True, DEFAULT_MQ_SPIN_LIMIT, 0.0)
506
+
507
+ # --- Python Interfaces ---
508
+
509
+ def __repr__(self):
510
+ return f'<{self.__class__.__name__} {"active" if self.active else "idle"}>(capacity={self.capacity}, timers={list(self.timer.keys())})'
511
+
512
+ def run_timer(self, double interval, Topic topic, datetime activate_time=None):
513
+ if not self.active:
514
+ raise RuntimeError('EventEngine must be started before getting timer!')
515
+ self.c_timer_loop(interval, topic, activate_time)
516
+
517
+ def minute_timer(self, Topic topic):
518
+ if not self.active:
519
+ raise RuntimeError('EventEngine must be started before getting timer!')
520
+ self.c_minute_timer_loop(topic)
521
+
522
+ def second_timer(self, Topic topic):
523
+ if not self.active:
524
+ raise RuntimeError('EventEngine must be started before getting timer!')
525
+ self.c_second_timer_loop(topic)
526
+
527
+ def get_timer(self, double interval, datetime activate_time=None) -> Topic:
528
+ if not self.active:
529
+ raise RuntimeError('EventEngine must be started before getting timer!')
530
+
531
+ if interval == 1:
532
+ topic = Topic('EventEngine.Internal.Timer.Second')
533
+ timer = Thread(target=self.second_timer, kwargs={'topic': topic})
534
+ elif interval == 60:
535
+ topic = Topic('EventEngine.Internal.Timer.Minute')
536
+ timer = Thread(target=self.minute_timer, kwargs={'topic': topic})
537
+ else:
538
+ topic = Topic.join(['EventEngine', 'Internal', 'Timer', str(interval)])
539
+ timer = Thread(target=self.run_timer, kwargs={'interval': interval, 'topic': topic, 'activate_time': activate_time})
540
+
541
+ if interval not in self.timer:
542
+ self.timer[interval] = timer
543
+ timer.start()
544
+ else:
545
+ if activate_time is not None:
546
+ self.logger.debug(f'Timer thread with interval [{timedelta(seconds=interval)}] already initialized! Argument [activate_time] takes no effect!')
547
+
548
+ return topic
549
+
550
+ def stop(self) -> None:
551
+ super().stop()
552
+
553
+ for timer in self.timer.values():
554
+ timer.join()
555
+
556
+ def clear(self) -> None:
557
+ super().clear()
558
+
559
+ for t in self.timer.values():
560
+ t.join(timeout=0)
561
+ self.timer.clear()
562
+
563
+
564
+ cdef class EngineTestToolkit:
565
+ """Relays internal C message-queue / hook-map state to Python for test validation.
566
+
567
+ Exposes the raw ``message_queue`` bookkeeping (head/tail/count/capacity),
568
+ the exact/generic hook bytemap contents, and a raw C message-queue
569
+ micro-benchmark. These internals are deliberately not part of the public
570
+ Python API, but are needed to verify queue-state transitions and to
571
+ measure the C-level hot path without Python overhead.
572
+
573
+ Note:
574
+ Test-only class. Not declared in any ``.pxd``; not part of the
575
+ public package API.
576
+ """
577
+
578
+ @staticmethod
579
+ def get_mq_capacity(EventEngine engine) -> size_t:
580
+ """Capacity of the engine's C message queue."""
581
+ return engine.mq.capacity
582
+
583
+ @staticmethod
584
+ def get_mq_head(EventEngine engine) -> size_t:
585
+ """Head index of the engine's C message queue."""
586
+ return engine.mq.head
587
+
588
+ @staticmethod
589
+ def get_mq_tail(EventEngine engine) -> size_t:
590
+ """Tail index of the engine's C message queue."""
591
+ return engine.mq.tail
592
+
593
+ @staticmethod
594
+ def get_mq_count(EventEngine engine) -> size_t:
595
+ """Occupancy counter of the engine's C message queue."""
596
+ return engine.mq.count
597
+
598
+ @staticmethod
599
+ def get_exact_hook_map_size(EventEngine engine) -> size_t:
600
+ """Number of entries in the exact-topic hook bytemap."""
601
+ cdef bytemap_entry* entry = engine.exact_topic_hooks.first
602
+ cdef size_t n = 0
603
+ while entry:
604
+ n += 1
605
+ entry = entry.next
606
+ return n
607
+
608
+ @staticmethod
609
+ def get_generic_hook_map_size(EventEngine engine) -> size_t:
610
+ """Number of entries in the generic-topic hook bytemap."""
611
+ cdef bytemap_entry* entry = engine.generic_topic_hooks.first
612
+ cdef size_t n = 0
613
+ while entry:
614
+ n += 1
615
+ entry = entry.next
616
+ return n
617
+
618
+ @staticmethod
619
+ def get_exact_hook_map_keys(EventEngine engine) -> list:
620
+ """Keys of all entries in the exact-topic hook bytemap."""
621
+ cdef list out = []
622
+ cdef bytemap_entry* entry = engine.exact_topic_hooks.first
623
+ while entry:
624
+ out.append(PyUnicode_FromStringAndSize(entry.key, entry.key_length))
625
+ entry = entry.next
626
+ return out
627
+
628
+ @staticmethod
629
+ def get_generic_hook_map_keys(EventEngine engine) -> list:
630
+ """Keys of all entries in the generic-topic hook bytemap."""
631
+ cdef list out = []
632
+ cdef bytemap_entry* entry = engine.generic_topic_hooks.first
633
+ while entry:
634
+ out.append(PyUnicode_FromStringAndSize(entry.key, entry.key_length))
635
+ entry = entry.next
636
+ return out
637
+
638
+ @staticmethod
639
+ def bench_mq_put_get(size_t n, size_t capacity=DEFAULT_MQ_CAPACITY) -> float:
640
+ """Average seconds per put/get round trip on a raw C message queue.
641
+
642
+ Allocates a standalone ``message_queue`` and a single payload, then
643
+ times ``n`` put/get cycles on the bare C API — no engine, no Python
644
+ dispatch, no handler invocation. Returns seconds per operation.
645
+
646
+ Args:
647
+ n: Number of put/get round trips to time.
648
+ capacity: Capacity of the raw message queue.
649
+
650
+ Returns:
651
+ Average elapsed seconds per round trip.
652
+ """
653
+ cdef message_queue* mq = c_mq_new(capacity, NULL, EE_HEAP_ALLOCATOR)
654
+ if not mq:
655
+ raise MemoryError('Failed to allocate message queue for benchmark')
656
+
657
+ cdef Topic topic = Topic('bench.mq.topic')
658
+ cdef evt_message_payload* payload = c_evt_payload_new(topic, EMPTY_ARGS, {})
659
+ if not payload:
660
+ c_mq_free(mq)
661
+ raise MemoryError('Failed to allocate payload for benchmark')
662
+
663
+ cdef double t0 = perf_counter()
664
+ cdef size_t i
665
+ cdef evt_message_payload* out = NULL
666
+ for i in range(n):
667
+ c_mq_put(mq, payload)
668
+ c_mq_get(mq, &out)
669
+ cdef double elapsed = perf_counter() - t0
670
+
671
+ c_evt_payload_free(payload)
672
+ c_mq_free(mq)
673
+ return elapsed / n