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.
- event_engine/__infra__.pxd +2 -0
- event_engine/__init__.pxd +2 -0
- event_engine/__init__.py +62 -0
- event_engine/base/__infra__.pxd +9 -0
- event_engine/base/__init__.pxd +9 -0
- event_engine/base/__init__.py +31 -0
- event_engine/base/c_allocator_protocol.c +14310 -0
- event_engine/base/c_allocator_protocol.cp315-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 +193 -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 +32542 -0
- event_engine/capi/c_engine.cp315-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 +478 -0
- event_engine/capi/c_engine.pyx +673 -0
- event_engine/capi/c_event.c +22826 -0
- event_engine/capi/c_event.cp315-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 +265 -0
- event_engine/capi/c_event.pyx +444 -0
- event_engine/capi/c_topic.c +34545 -0
- event_engine/capi/c_topic.cp315-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 +644 -0
- event_engine/capi/c_topic.pyx +818 -0
- event_engine/capi/fallback_engine.py +707 -0
- event_engine/config_view.c +10271 -0
- event_engine/config_view.cp315-win_amd64.pyd +0 -0
- event_engine/config_view.pyx +55 -0
- event_engine/includes/event_engine/base/c_allocator_protocol.c +14310 -0
- event_engine/includes/event_engine/base/c_allocator_protocol.h +23 -0
- event_engine/includes/event_engine/capi/c_engine.c +32542 -0
- event_engine/includes/event_engine/capi/c_engine.h +393 -0
- event_engine/includes/event_engine/capi/c_event.c +22826 -0
- event_engine/includes/event_engine/capi/c_event.h +342 -0
- event_engine/includes/event_engine/capi/c_topic.c +34545 -0
- event_engine/includes/event_engine/capi/c_topic.h +1007 -0
- event_engine/includes/event_engine/config_view.c +10271 -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 +793 -0
- event_engine/native/event.py +469 -0
- event_engine/native/topic.py +943 -0
- pyeventengine-0.6.2.dist-info/METADATA +189 -0
- pyeventengine-0.6.2.dist-info/RECORD +56 -0
- pyeventengine-0.6.2.dist-info/WHEEL +5 -0
- pyeventengine-0.6.2.dist-info/licenses/LICENSE +21 -0
- pyeventengine-0.6.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
LOGGER = None
|
|
4
|
+
|
|
5
|
+
# Indicate we are using the native (fallback) implementation
|
|
6
|
+
USING_FALLBACK = True
|
|
7
|
+
|
|
8
|
+
from .topic import (
|
|
9
|
+
PyTopicType, PyTopicPart, PyTopicPartExact, PyTopicPartAny, PyTopicPartRange, PyTopicPartPattern,
|
|
10
|
+
PyTopicMatchResult, PyTopic,
|
|
11
|
+
init_internal_map, clear_internal_map, get_internal_topic, get_internal_map, init_allocator,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from .event import PyMessagePayload, EventHook as EventHookBase, EventHookEx
|
|
15
|
+
from .engine import Full, Empty, EventEngine as EventEngineBase, EventEngineEx
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def set_logger(logger: logging.Logger):
|
|
19
|
+
"""Set the root EventEngine logger and propagate to native submodules."""
|
|
20
|
+
global LOGGER
|
|
21
|
+
from . import topic, event, engine
|
|
22
|
+
topic.LOGGER = logger
|
|
23
|
+
event.LOGGER = logger
|
|
24
|
+
engine.LOGGER = logger
|
|
25
|
+
LOGGER = logger
|
|
26
|
+
|
|
27
|
+
# Update native submodules if they expose LOGGER
|
|
28
|
+
try:
|
|
29
|
+
from . import event as _event
|
|
30
|
+
_event.LOGGER = logger.getChild('Event')
|
|
31
|
+
except Exception:
|
|
32
|
+
pass
|
|
33
|
+
try:
|
|
34
|
+
from . import engine as _engine
|
|
35
|
+
_engine.LOGGER = logger.getChild('Engine')
|
|
36
|
+
except Exception:
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# alias for consistency
|
|
41
|
+
TopicType = PyTopicType
|
|
42
|
+
TopicPart = PyTopicPart
|
|
43
|
+
TopicPartExact = PyTopicPartExact
|
|
44
|
+
TopicPartAny = PyTopicPartAny
|
|
45
|
+
TopicPartRange = PyTopicPartRange
|
|
46
|
+
TopicPartPattern = PyTopicPartPattern
|
|
47
|
+
TopicMatchResult = PyTopicMatchResult
|
|
48
|
+
Topic = PyTopic
|
|
49
|
+
MessagePayload = PyMessagePayload
|
|
50
|
+
EventHook = EventHookEx
|
|
51
|
+
EventEngine = EventEngineEx
|
|
52
|
+
|
|
53
|
+
__all__ = [
|
|
54
|
+
'TopicType', 'TopicPart', 'TopicPartExact', 'TopicPartAny', 'TopicPartRange', 'TopicPartPattern',
|
|
55
|
+
'TopicMatchResult', 'Topic',
|
|
56
|
+
'init_internal_map', 'clear_internal_map', 'get_internal_topic', 'get_internal_map', 'init_allocator',
|
|
57
|
+
'MessagePayload', 'EventHookBase', 'EventHook',
|
|
58
|
+
'Full', 'Empty', 'EventEngineBase', 'EventEngine', 'USING_FALLBACK',
|
|
59
|
+
'set_logger'
|
|
60
|
+
]
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DEPRECATED: This module is deprecated and will be removed in a future version.
|
|
3
|
+
|
|
4
|
+
This was an old implementation that doesn't match the Cython API.
|
|
5
|
+
Please use event_engine.native.event instead, which provides a proper
|
|
6
|
+
Python fallback implementation that mimics the Cython c_event module.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import warnings
|
|
10
|
+
import datetime
|
|
11
|
+
import enum
|
|
12
|
+
import inspect
|
|
13
|
+
import time
|
|
14
|
+
import traceback
|
|
15
|
+
from collections import deque
|
|
16
|
+
from logging import Logger
|
|
17
|
+
from threading import Thread, Semaphore
|
|
18
|
+
from typing import Iterable, TypedDict, NotRequired, Callable
|
|
19
|
+
|
|
20
|
+
from . import LOGGER, LOG_LEVEL_EVENT, Topic, DEBUG
|
|
21
|
+
|
|
22
|
+
warnings.warn(
|
|
23
|
+
"event_engine.native._event is deprecated. Use event_engine.native.event instead.",
|
|
24
|
+
DeprecationWarning,
|
|
25
|
+
stacklevel=2
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
LOGGER = LOGGER.getChild('Event')
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class EventDict(TypedDict):
|
|
32
|
+
topic: str
|
|
33
|
+
args: NotRequired[tuple]
|
|
34
|
+
kwargs: NotRequired[dict]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class EventHookBase(object):
|
|
38
|
+
"""
|
|
39
|
+
Event object with
|
|
40
|
+
a string topic for event engine to distribute event,
|
|
41
|
+
and a list of handler to process data
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, topic: Topic, logger: Logger = None, max_size: int = None):
|
|
45
|
+
self.topic = topic
|
|
46
|
+
self.logger = LOGGER.getChild(f'EventHook.{topic}') if logger is None else logger
|
|
47
|
+
self.handlers: deque[Callable] = deque(maxlen=max_size)
|
|
48
|
+
|
|
49
|
+
def __call__(self, *args, **kwargs):
|
|
50
|
+
self.trigger(topic=self.topic, args=args, kwargs=kwargs)
|
|
51
|
+
|
|
52
|
+
def __iadd__(self, handler: Callable):
|
|
53
|
+
self.add_handler(handler)
|
|
54
|
+
return self
|
|
55
|
+
|
|
56
|
+
def __isub__(self, handler: Callable):
|
|
57
|
+
self.remove_handler(handler)
|
|
58
|
+
return self
|
|
59
|
+
|
|
60
|
+
def trigger(self, topic: Topic, args: tuple = None, kwargs: dict = None):
|
|
61
|
+
if args is None:
|
|
62
|
+
args = ()
|
|
63
|
+
|
|
64
|
+
if kwargs is None:
|
|
65
|
+
kwargs = {}
|
|
66
|
+
|
|
67
|
+
for handler in self.handlers:
|
|
68
|
+
try:
|
|
69
|
+
try:
|
|
70
|
+
handler(topic=topic, *args, **kwargs)
|
|
71
|
+
except TypeError as e:
|
|
72
|
+
if e.__str__().endswith("unexpected keyword argument 'topic'"):
|
|
73
|
+
handler(*args, **kwargs)
|
|
74
|
+
else:
|
|
75
|
+
raise e
|
|
76
|
+
except Exception as _:
|
|
77
|
+
self.logger.error(traceback.format_exc())
|
|
78
|
+
|
|
79
|
+
def add_handler(self, handler: Callable):
|
|
80
|
+
if handler in self.handlers:
|
|
81
|
+
LOGGER.warning(f'Handler {handler} already in {self}. This action might cause it to trigger twice.')
|
|
82
|
+
self.handlers.append(handler)
|
|
83
|
+
|
|
84
|
+
def remove_handler(self, handler: Callable):
|
|
85
|
+
try:
|
|
86
|
+
self.handlers.remove(handler)
|
|
87
|
+
except ValueError as e:
|
|
88
|
+
self.logger.error(f'Handler {handler} not found in {self}.')
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class EventHook(EventHookBase):
|
|
92
|
+
def __init__(self, topic: Topic, logger: Logger = None, max_size: int = None, handler: list[Callable] | Callable | None = None):
|
|
93
|
+
super().__init__(topic=topic, logger=logger, max_size=max_size)
|
|
94
|
+
self.with_topic: deque[bool] = deque()
|
|
95
|
+
|
|
96
|
+
if handler is None:
|
|
97
|
+
pass
|
|
98
|
+
elif callable(handler):
|
|
99
|
+
self.add_handler(handler)
|
|
100
|
+
elif isinstance(handler, Iterable):
|
|
101
|
+
for _handler in handler:
|
|
102
|
+
self.add_handler(handler=_handler)
|
|
103
|
+
else:
|
|
104
|
+
raise ValueError(f'Invalid handler {handler}, expect a Callable or a list of Callable.')
|
|
105
|
+
|
|
106
|
+
def trigger(self, topic: Topic, args: tuple = None, kwargs: dict = None):
|
|
107
|
+
ts = time.time()
|
|
108
|
+
if args is None:
|
|
109
|
+
args = ()
|
|
110
|
+
|
|
111
|
+
if kwargs is None:
|
|
112
|
+
kwargs = {}
|
|
113
|
+
|
|
114
|
+
for handler, with_topic in zip(self.handlers, self.with_topic):
|
|
115
|
+
try:
|
|
116
|
+
if with_topic:
|
|
117
|
+
handler(topic=topic, *args, **kwargs)
|
|
118
|
+
else:
|
|
119
|
+
handler(*args, **kwargs)
|
|
120
|
+
except Exception as _:
|
|
121
|
+
self.logger.error(traceback.format_exc())
|
|
122
|
+
|
|
123
|
+
if DEBUG:
|
|
124
|
+
self.logger.log(LOG_LEVEL_EVENT, f'EventHook {self.topic} tasks triggered {len(self.handlers):,} handlers, complete in {(time.time() - ts) * 1000:.3f}ms.')
|
|
125
|
+
|
|
126
|
+
def add_handler(self, handler: Callable, with_topic: bool = None):
|
|
127
|
+
sig = inspect.signature(handler)
|
|
128
|
+
|
|
129
|
+
if with_topic is None:
|
|
130
|
+
for param in sig.parameters.values():
|
|
131
|
+
if param.name == 'topic' or param.kind == param.VAR_KEYWORD:
|
|
132
|
+
with_topic = True
|
|
133
|
+
break
|
|
134
|
+
|
|
135
|
+
super().add_handler(handler=handler)
|
|
136
|
+
self.with_topic.append(with_topic)
|
|
137
|
+
|
|
138
|
+
def remove_handler(self, handler: Callable):
|
|
139
|
+
try:
|
|
140
|
+
idx = self.handlers.index(handler)
|
|
141
|
+
self.handlers.__delitem__(idx)
|
|
142
|
+
self.with_topic.__delitem__(idx)
|
|
143
|
+
except ValueError as e:
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class EventEngineBase(object):
|
|
148
|
+
EventHook = EventHook
|
|
149
|
+
|
|
150
|
+
def __init__(self, logger: Logger = None, buffer_size: int = 0):
|
|
151
|
+
self.logger = LOGGER.getChild(f'EventEngine') if logger is None else logger
|
|
152
|
+
self._buffer_size = buffer_size
|
|
153
|
+
self._put_lock = Semaphore(self._buffer_size)
|
|
154
|
+
self._get_lock = Semaphore(0)
|
|
155
|
+
self._deque: deque[EventDict] = deque(maxlen=buffer_size if buffer_size else None)
|
|
156
|
+
self._active: bool = False
|
|
157
|
+
self._engine: Thread = None
|
|
158
|
+
self._event_hooks: dict[Topic, EventHook] = {}
|
|
159
|
+
|
|
160
|
+
if buffer_size and buffer_size < 8:
|
|
161
|
+
self.logger.info(f'buffer_size={buffer_size} too small. This might cause a dead lock.')
|
|
162
|
+
|
|
163
|
+
def _run(self) -> None:
|
|
164
|
+
"""
|
|
165
|
+
Get event from queue and then process it.
|
|
166
|
+
"""
|
|
167
|
+
while self._active:
|
|
168
|
+
self._get_lock.acquire(blocking=True, timeout=None)
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
event_dict = self._deque.popleft()
|
|
172
|
+
except IndexError as e:
|
|
173
|
+
if not self._active:
|
|
174
|
+
return
|
|
175
|
+
raise e
|
|
176
|
+
|
|
177
|
+
topic = event_dict['topic']
|
|
178
|
+
args = event_dict.get('args', ())
|
|
179
|
+
kwargs = event_dict.get('kwargs', {})
|
|
180
|
+
self._process(topic=topic, *args, **kwargs)
|
|
181
|
+
|
|
182
|
+
if self._buffer_size:
|
|
183
|
+
self._put_lock.release()
|
|
184
|
+
|
|
185
|
+
def _process(self, topic: str, *args, **kwargs) -> None:
|
|
186
|
+
"""
|
|
187
|
+
distribute data to registered event hook in the order of registration
|
|
188
|
+
"""
|
|
189
|
+
for event_topic, event_hook in self._event_hooks.items():
|
|
190
|
+
if matched_topic := event_topic.match(topic=topic):
|
|
191
|
+
event_hook.trigger(topic=matched_topic, args=args, kwargs=kwargs)
|
|
192
|
+
|
|
193
|
+
def start(self) -> None:
|
|
194
|
+
"""
|
|
195
|
+
Start event engine to process events and generate timer events.
|
|
196
|
+
"""
|
|
197
|
+
if self._active:
|
|
198
|
+
self.logger.warning(f'{self} already started!')
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
self._active = True
|
|
202
|
+
self._engine = Thread(target=self._run, name='EventEngine')
|
|
203
|
+
self._engine.start()
|
|
204
|
+
|
|
205
|
+
def stop(self) -> None:
|
|
206
|
+
"""
|
|
207
|
+
Stop event engine.
|
|
208
|
+
"""
|
|
209
|
+
if not self._active:
|
|
210
|
+
self.logger.warning('EventEngine already stopped!')
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
self._active = False
|
|
214
|
+
self._get_lock.release()
|
|
215
|
+
self._engine.join()
|
|
216
|
+
|
|
217
|
+
def clear(self) -> None:
|
|
218
|
+
if self._active:
|
|
219
|
+
self.logger.error('EventEngine must be stopped before cleared!')
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
self._event_hooks.clear()
|
|
223
|
+
self._deque.clear()
|
|
224
|
+
|
|
225
|
+
if self._buffer_size:
|
|
226
|
+
self._put_lock._value = self._buffer_size
|
|
227
|
+
self._get_lock._value = 0
|
|
228
|
+
|
|
229
|
+
def put(self, topic: str | Topic, block: bool = True, timeout: float = None, *args, **kwargs):
|
|
230
|
+
"""
|
|
231
|
+
fast way to put an event, kwargs MUST NOT contain "topic", "block" and "timeout" keywords
|
|
232
|
+
:param topic: the topic to put into engine
|
|
233
|
+
:param block: block if necessary until a free slot is available
|
|
234
|
+
:param timeout: If 'timeout' is a non-negative number, it blocks at most 'timeout' seconds and raises the Full exception
|
|
235
|
+
:param args: args for handlers
|
|
236
|
+
:param kwargs: kwargs for handlers
|
|
237
|
+
:return: nothing
|
|
238
|
+
"""
|
|
239
|
+
self.publish(topic=topic, block=block, timeout=timeout, args=args, kwargs=kwargs)
|
|
240
|
+
|
|
241
|
+
def publish(self, topic: str | Topic, block: bool = True, timeout: float = None, args=None, kwargs=None):
|
|
242
|
+
"""
|
|
243
|
+
safe way to publish an event
|
|
244
|
+
:param topic: the topic to put into engine
|
|
245
|
+
:param block: block if necessary until a free slot is available
|
|
246
|
+
:param timeout: If 'timeout' is a non-negative number, it blocks at most 'timeout' seconds and raises the Full exception
|
|
247
|
+
:param args: a list / tuple, args for handlers
|
|
248
|
+
:param kwargs: a dict, kwargs for handlers
|
|
249
|
+
:return: nothing
|
|
250
|
+
"""
|
|
251
|
+
if isinstance(topic, Topic):
|
|
252
|
+
topic = topic.value
|
|
253
|
+
elif not isinstance(topic, str):
|
|
254
|
+
raise ValueError(f'Invalid topic {topic}')
|
|
255
|
+
|
|
256
|
+
if self._buffer_size:
|
|
257
|
+
self._put_lock.acquire()
|
|
258
|
+
|
|
259
|
+
event_dict = {'topic': topic}
|
|
260
|
+
|
|
261
|
+
if args is not None:
|
|
262
|
+
event_dict['args'] = args
|
|
263
|
+
|
|
264
|
+
if kwargs is not None:
|
|
265
|
+
event_dict['kwargs'] = kwargs
|
|
266
|
+
|
|
267
|
+
self._deque.append(event_dict)
|
|
268
|
+
|
|
269
|
+
self._get_lock.release()
|
|
270
|
+
|
|
271
|
+
def register_hook(self, hook: EventHook) -> None:
|
|
272
|
+
"""
|
|
273
|
+
register a hook event
|
|
274
|
+
"""
|
|
275
|
+
if hook.topic in self._event_hooks:
|
|
276
|
+
for handler in hook.handlers:
|
|
277
|
+
self._event_hooks[hook.topic].add_handler(handler)
|
|
278
|
+
else:
|
|
279
|
+
self._event_hooks[hook.topic] = hook
|
|
280
|
+
|
|
281
|
+
def unregister_hook(self, topic: Topic) -> None:
|
|
282
|
+
"""
|
|
283
|
+
Unregister an existing hook
|
|
284
|
+
"""
|
|
285
|
+
if topic in self._event_hooks:
|
|
286
|
+
self._event_hooks.pop(topic)
|
|
287
|
+
|
|
288
|
+
def register_handler(self, topic: Topic, handler: Iterable[Callable] | Callable) -> None:
|
|
289
|
+
"""
|
|
290
|
+
Register one or more handler for a specific topic
|
|
291
|
+
"""
|
|
292
|
+
|
|
293
|
+
if not isinstance(topic, Topic):
|
|
294
|
+
raise TypeError(f'Invalid topic {topic}')
|
|
295
|
+
|
|
296
|
+
if topic not in self._event_hooks:
|
|
297
|
+
self._event_hooks[topic] = self.EventHook(topic=topic, handler=handler, logger=self.logger.getChild(topic.value))
|
|
298
|
+
else:
|
|
299
|
+
self._event_hooks[topic].add_handler(handler)
|
|
300
|
+
|
|
301
|
+
def unregister_handler(self, topic: Topic, handler: Callable) -> None:
|
|
302
|
+
"""
|
|
303
|
+
Unregister an existing handler function.
|
|
304
|
+
"""
|
|
305
|
+
if topic in self._event_hooks:
|
|
306
|
+
self._event_hooks[topic].remove_handler(handler=handler)
|
|
307
|
+
|
|
308
|
+
@property
|
|
309
|
+
def buffer_size(self):
|
|
310
|
+
return self._buffer_size
|
|
311
|
+
|
|
312
|
+
@property
|
|
313
|
+
def active(self) -> bool:
|
|
314
|
+
return self._active
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class EventEngine(EventEngineBase):
|
|
318
|
+
EventHook = EventHook
|
|
319
|
+
|
|
320
|
+
def __init__(self, buffer_size=0):
|
|
321
|
+
super().__init__(buffer_size=buffer_size)
|
|
322
|
+
self.timer: dict[float | str, Thread] = {}
|
|
323
|
+
|
|
324
|
+
def register_handler(self, topic: Topic | str | enum.Enum, handler: Callable):
|
|
325
|
+
topic = Topic.cast(topic)
|
|
326
|
+
super().register_handler(topic=topic, handler=handler)
|
|
327
|
+
|
|
328
|
+
def publish(self, topic, block: bool = True, timeout: float = None, args=None, kwargs=None):
|
|
329
|
+
topic = Topic.cast(topic)
|
|
330
|
+
super().publish(topic=topic, block=block, timeout=timeout, args=args, kwargs=kwargs)
|
|
331
|
+
|
|
332
|
+
def unregister_hook(self, topic) -> None:
|
|
333
|
+
topic = Topic.cast(topic)
|
|
334
|
+
super().unregister_hook(topic=topic)
|
|
335
|
+
|
|
336
|
+
def unregister_handler(self, topic, handler) -> None:
|
|
337
|
+
topic = Topic.cast(topic)
|
|
338
|
+
|
|
339
|
+
try:
|
|
340
|
+
super().unregister_handler(topic=topic, handler=handler)
|
|
341
|
+
except ValueError as _:
|
|
342
|
+
raise ValueError(f'unregister topic {topic} failed! handler {handler} not found!')
|
|
343
|
+
|
|
344
|
+
def get_timer(self, interval: datetime.timedelta | float | int, activate_time: datetime.datetime = None) -> Topic:
|
|
345
|
+
"""
|
|
346
|
+
Start a timer, if not exist, and get topic of the timer event
|
|
347
|
+
:param interval: timer event interval in seconds
|
|
348
|
+
:param activate_time: UTC, timer event only start after active_time. This arg has no effect if timer already started.
|
|
349
|
+
:return: the topic of timer event hook
|
|
350
|
+
"""
|
|
351
|
+
if isinstance(interval, datetime.timedelta):
|
|
352
|
+
interval = interval.total_seconds()
|
|
353
|
+
|
|
354
|
+
if interval == 1:
|
|
355
|
+
topic = Topic('EventEngine.Internal.Timer.Second')
|
|
356
|
+
timer = Thread(target=self._second_timer, kwargs={'topic': topic})
|
|
357
|
+
elif interval == 60:
|
|
358
|
+
topic = Topic('EventEngine.Internal.Timer.Minute')
|
|
359
|
+
timer = Thread(target=self._minute_timer, kwargs={'topic': topic})
|
|
360
|
+
else:
|
|
361
|
+
topic = Topic(f'EventEngine.Internal.Timer.{interval}')
|
|
362
|
+
timer = Thread(target=self._run_timer, kwargs={'interval': interval, 'topic': topic, 'activate_time': activate_time})
|
|
363
|
+
|
|
364
|
+
if interval not in self.timer:
|
|
365
|
+
self.timer[interval] = timer
|
|
366
|
+
timer.start()
|
|
367
|
+
else:
|
|
368
|
+
if activate_time is not None:
|
|
369
|
+
self.logger.debug(f'Timer thread with interval [{datetime.timedelta(seconds=interval)}] already initialized! Argument [activate_time] takes no effect!')
|
|
370
|
+
|
|
371
|
+
return topic
|
|
372
|
+
|
|
373
|
+
def _run_timer(self, interval: datetime.timedelta | float | int, topic: Topic, activate_time: datetime.datetime = None) -> None:
|
|
374
|
+
if isinstance(interval, datetime.timedelta):
|
|
375
|
+
interval = interval.total_seconds()
|
|
376
|
+
|
|
377
|
+
if activate_time is None:
|
|
378
|
+
scheduled_time = datetime.datetime.utcnow()
|
|
379
|
+
else:
|
|
380
|
+
scheduled_time = activate_time
|
|
381
|
+
|
|
382
|
+
while self._active:
|
|
383
|
+
sleep_time = (scheduled_time - datetime.datetime.utcnow()).total_seconds()
|
|
384
|
+
|
|
385
|
+
if sleep_time > 0:
|
|
386
|
+
time.sleep(sleep_time)
|
|
387
|
+
self.put(topic=topic, interval=interval, trigger_time=scheduled_time)
|
|
388
|
+
|
|
389
|
+
while scheduled_time < datetime.datetime.utcnow():
|
|
390
|
+
scheduled_time += datetime.timedelta(seconds=interval)
|
|
391
|
+
|
|
392
|
+
def _minute_timer(self, topic: Topic):
|
|
393
|
+
while self._active:
|
|
394
|
+
t = time.time()
|
|
395
|
+
scheduled_time = t // 60 * 60
|
|
396
|
+
next_time = scheduled_time + 60
|
|
397
|
+
sleep_time = next_time - t
|
|
398
|
+
time.sleep(sleep_time)
|
|
399
|
+
self.put(topic=topic, interval=60., timestamp=scheduled_time)
|
|
400
|
+
|
|
401
|
+
def _second_timer(self, topic: Topic):
|
|
402
|
+
while self._active:
|
|
403
|
+
t = time.time()
|
|
404
|
+
scheduled_time = t // 1
|
|
405
|
+
next_time = scheduled_time + 1
|
|
406
|
+
sleep_time = next_time - t
|
|
407
|
+
time.sleep(sleep_time)
|
|
408
|
+
self.put(topic=topic, interval=1., timestamp=scheduled_time)
|
|
409
|
+
|
|
410
|
+
def stop(self) -> None:
|
|
411
|
+
super().stop()
|
|
412
|
+
|
|
413
|
+
for timer in self.timer.values():
|
|
414
|
+
timer.join()
|
|
415
|
+
|
|
416
|
+
def clear(self) -> None:
|
|
417
|
+
for t in self.timer.values():
|
|
418
|
+
t.join(timeout=0)
|
|
419
|
+
|
|
420
|
+
self.timer.clear()
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DEPRECATED: This module is deprecated and will be removed in a future version.
|
|
3
|
+
|
|
4
|
+
This was an old implementation that doesn't match the Cython API.
|
|
5
|
+
Please use event_engine.native.topic instead, which provides a proper
|
|
6
|
+
Python fallback implementation that mimics the Cython c_topic module.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import warnings
|
|
10
|
+
import re
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from string import Formatter
|
|
13
|
+
from typing import Self, Type
|
|
14
|
+
|
|
15
|
+
warnings.warn(
|
|
16
|
+
"event_engine.native._topic is deprecated. Use event_engine.native.topic instead.",
|
|
17
|
+
DeprecationWarning,
|
|
18
|
+
stacklevel=2
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Topic(dict):
|
|
23
|
+
"""
|
|
24
|
+
topic for event hook. e.g. "TickData.002410.SZ.Realtime"
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
class Error(Exception):
|
|
28
|
+
def __init__(self, msg):
|
|
29
|
+
super().__init__(msg)
|
|
30
|
+
|
|
31
|
+
def __init__(self, topic: str, *args, **kwargs):
|
|
32
|
+
self._value = topic
|
|
33
|
+
super().__init__(*args, **kwargs)
|
|
34
|
+
|
|
35
|
+
def __repr__(self):
|
|
36
|
+
return f'<{self.__class__.__name__}>({self._value}){super().__repr__()}'
|
|
37
|
+
|
|
38
|
+
def __str__(self):
|
|
39
|
+
return self.value
|
|
40
|
+
|
|
41
|
+
def __bool__(self):
|
|
42
|
+
return True
|
|
43
|
+
|
|
44
|
+
def __hash__(self):
|
|
45
|
+
return self.value.__hash__()
|
|
46
|
+
|
|
47
|
+
def match(self, topic: str) -> Self | None:
|
|
48
|
+
if self._value == topic:
|
|
49
|
+
# return self.__class__(topic=topic)
|
|
50
|
+
return self
|
|
51
|
+
else:
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def cast(cls, topic: Self | str | Enum, dtype: Type[Self] = None) -> Self:
|
|
56
|
+
if isinstance(topic, cls):
|
|
57
|
+
return topic
|
|
58
|
+
elif isinstance(topic, Enum):
|
|
59
|
+
t = topic.value
|
|
60
|
+
return cls.cast(t)
|
|
61
|
+
elif isinstance(topic, str):
|
|
62
|
+
if dtype is None:
|
|
63
|
+
if re.search(r'{(.+?)}', topic):
|
|
64
|
+
t = PatternTopic(pattern=topic)
|
|
65
|
+
elif '*' in topic or '+' in topic or '|' in topic:
|
|
66
|
+
re.compile(pattern=topic)
|
|
67
|
+
t = RegularTopic(pattern=topic)
|
|
68
|
+
else:
|
|
69
|
+
t = Topic(topic=topic)
|
|
70
|
+
else:
|
|
71
|
+
t = dtype(topic)
|
|
72
|
+
else:
|
|
73
|
+
raise NotImplementedError(f'Can not cast {topic} into {cls}.')
|
|
74
|
+
|
|
75
|
+
return t
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def value(self) -> str:
|
|
79
|
+
return self._value
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class RegularTopic(Topic):
|
|
83
|
+
"""
|
|
84
|
+
topic in regular expression. e.g. "TickData.(.+).((SZ)|(SH)).((Realtime)|(History))"
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(self, pattern: str):
|
|
88
|
+
super().__init__(topic=pattern)
|
|
89
|
+
|
|
90
|
+
def match(self, topic: str) -> Topic | None:
|
|
91
|
+
if re.match(self._value, topic):
|
|
92
|
+
match = Topic(topic=topic)
|
|
93
|
+
match['pattern'] = self._value
|
|
94
|
+
return match
|
|
95
|
+
else:
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class PatternTopic(Topic):
|
|
100
|
+
"""
|
|
101
|
+
topic for event hook. e.g. "TickData.{symbol}.{market}.{flag}"
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
class NotMatchError(Topic.Error):
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
def __init__(self, pattern: str):
|
|
108
|
+
super().__init__(topic=pattern)
|
|
109
|
+
|
|
110
|
+
def __call__(self, **kwargs):
|
|
111
|
+
return self.format_map(kwargs)
|
|
112
|
+
|
|
113
|
+
# @classmethod
|
|
114
|
+
# def extract_mapping(cls, target: str, pattern: str):
|
|
115
|
+
# pattern = re.escape(pattern)
|
|
116
|
+
# regex = re.sub(r'\\{(.+?)\\}', r'(?P<_\1>.+)', pattern)
|
|
117
|
+
# match = re.match(regex, target)
|
|
118
|
+
# if match:
|
|
119
|
+
# values = list(match.groups())
|
|
120
|
+
# keys = re.findall(r'\\{(.+?)\\}', pattern)
|
|
121
|
+
# m = dict(zip(keys, values))
|
|
122
|
+
# return m
|
|
123
|
+
# else:
|
|
124
|
+
# raise Topic.Error(f'pattern {pattern} not in string {target} found!')
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
def extract_mapping(cls, target: str, pattern: str) -> dict[str, str]:
|
|
128
|
+
dictionary = {}
|
|
129
|
+
|
|
130
|
+
result_parts = target.split('.')
|
|
131
|
+
pattern_parts = pattern.split('.')
|
|
132
|
+
|
|
133
|
+
# Check if the number of parts in result and pattern are the same
|
|
134
|
+
if len(result_parts) < len(pattern_parts):
|
|
135
|
+
raise cls.NotMatchError(f'Target {target} not match with pattern {pattern}.')
|
|
136
|
+
|
|
137
|
+
# Generate the mapping dictionary
|
|
138
|
+
for result_part, pattern_part in zip(result_parts, pattern_parts):
|
|
139
|
+
if pattern_part[0] == '{' and pattern_part[-1] == '}':
|
|
140
|
+
content: str = pattern_part[1:-1]
|
|
141
|
+
dictionary[content] = result_part
|
|
142
|
+
else:
|
|
143
|
+
if result_part != pattern_part:
|
|
144
|
+
dictionary.clear()
|
|
145
|
+
raise cls.NotMatchError(f'Target {target} not match with pattern {pattern}.')
|
|
146
|
+
|
|
147
|
+
return dictionary
|
|
148
|
+
|
|
149
|
+
def format_map(self, mapping: dict) -> Topic:
|
|
150
|
+
for key in self.keys():
|
|
151
|
+
if key not in mapping:
|
|
152
|
+
mapping[key] = f'{{{key}}}'
|
|
153
|
+
|
|
154
|
+
return Topic.cast(self._value.format_map(mapping))
|
|
155
|
+
|
|
156
|
+
def keys(self):
|
|
157
|
+
keys = [i[1] for i in Formatter().parse(self._value) if i[1] is not None]
|
|
158
|
+
return keys
|
|
159
|
+
|
|
160
|
+
def match(self, topic: str) -> Topic | None:
|
|
161
|
+
try:
|
|
162
|
+
keyword_dict = self.extract_mapping(target=topic, pattern=self._value)
|
|
163
|
+
match = Topic(topic=topic)
|
|
164
|
+
match.update(keyword_dict)
|
|
165
|
+
return match
|
|
166
|
+
except self.NotMatchError as _:
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def value(self) -> str:
|
|
171
|
+
return self._value.format_map({_: '*' for _ in self.keys()})
|