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