PyEventEngine 0.3.2__py3-none-any.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Bolun Han
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.1
2
+ Name: PyEventEngine
3
+ Version: 0.3.2
4
+ Summary: Basic event engine
5
+ Home-page: https://github.com/BolunHan/PyEventEngine.git
6
+ Author: Bolun.Han
7
+ Author-email: Bolun.Han@outlook.com
8
+ License: MIT
9
+ Classifier: Programming Language :: Python :: 3.8
10
+ Classifier: Programming Language :: Python :: 3.9
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Operating System :: OS Independent
15
+ Requires-Python: >=3.12
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+
19
+ # PyEventEngine
20
+
21
+ python native event engine
22
+
23
+ # Install
24
+
25
+ ```shell
26
+ pip install git+https://github.com/BolunHan/PyEventEngine.git
27
+ ```
28
+
29
+ or
30
+
31
+ ```shell
32
+ pip install PyEventEngine
33
+ ```
34
+
35
+ # Use
36
+
37
+ ## basic usage
38
+
39
+ ```python
40
+ # init event engine
41
+ import time
42
+ from event_engine import EventEngine, Topic
43
+
44
+ EVENT_ENGINE = EventEngine()
45
+ EVENT_ENGINE.start()
46
+
47
+
48
+ # register handler
49
+ def test_handler(msg, **kwargs):
50
+ print(msg)
51
+
52
+
53
+ EVENT_ENGINE.register_handler(topic=Topic('SimpleTopic'), handler=test_handler)
54
+
55
+ # publish message
56
+ EVENT_ENGINE.put(topic=Topic('SimpleTopic'), msg='topic called')
57
+ time.sleep(1)
58
+ EVENT_ENGINE.stop()
59
+ ```
60
+
61
+ ## regular topic
62
+
63
+ ```python
64
+ # init event engine
65
+ import time
66
+ from event_engine import EventEngine, Topic, RegularTopic
67
+
68
+ EVENT_ENGINE = EventEngine()
69
+ EVENT_ENGINE.start()
70
+
71
+
72
+ # register handler
73
+ def test_handler(msg, **kwargs):
74
+ print(msg)
75
+
76
+
77
+ EVENT_ENGINE.register_handler(topic=RegularTopic('RegularTopic.*'), handler=test_handler)
78
+
79
+ # publish message
80
+ EVENT_ENGINE.put(topic=Topic('RegularTopic.ChildTopic0'), msg='topic called')
81
+ time.sleep(1)
82
+ EVENT_ENGINE.stop()
83
+ ```
84
+
85
+ ## timer topic
86
+
87
+ ```python
88
+ # init event engine
89
+ import time
90
+ from event_engine import EventEngine, Topic, RegularTopic
91
+
92
+ EVENT_ENGINE = EventEngine()
93
+ EVENT_ENGINE.start()
94
+
95
+
96
+ # register handler
97
+ def test_handler(**kwargs):
98
+ print(kwargs)
99
+
100
+
101
+ topic = EVENT_ENGINE.get_timer(interval=1)
102
+ EVENT_ENGINE.register_handler(topic=topic, handler=test_handler)
103
+
104
+ # publish message
105
+ time.sleep(5)
106
+ EVENT_ENGINE.stop()
107
+ ```
108
+
109
+ See more advanced usage at .Demo
@@ -0,0 +1,10 @@
1
+ event_engine/__init__.py,sha256=0z5AJNA4EwsTqQFxSKYa8YefS1acGuKxlVLQPQk4Xzw,45
2
+ event_engine/native/__init__.py,sha256=_HhWK3NYaS8e4JvGYVyC6MX5EKfNCRNZ3d0ItFj0Pis,3227
3
+ event_engine/native/_event.py,sha256=w_k2znuX3newAnjUbFkYxh8_DFEr3WMkAyyzSc7QBhY,14346
4
+ event_engine/native/_topic.py,sha256=fBcnKEsFoj0vmKZXq3MkEJJ_JyepvZLbDvnAzDjMNjI,4726
5
+ event_engine/native/_topic_c.py,sha256=8CXsuHeRwbcxKqyfsIpp-QDKnk0vjOG4MT9nYoaUY2Y,2907
6
+ PyEventEngine-0.3.2.dist-info/LICENSE,sha256=bTmmkRlafTj1JKDkDoGfnLHD8OiuIdXaXDa35W3Ii4g,1066
7
+ PyEventEngine-0.3.2.dist-info/METADATA,sha256=EGiCnbUBokVgw4D-z5zh2VxLSyv2dGi8XvnB8eSuCzA,2107
8
+ PyEventEngine-0.3.2.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
9
+ PyEventEngine-0.3.2.dist-info/top_level.txt,sha256=rIh2LcDfofRLIIqMSlulp5yAXTf1DNOmlofiyIHuylQ,13
10
+ PyEventEngine-0.3.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.2.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ event_engine
@@ -0,0 +1,3 @@
1
+ __version__ = '0.3.2'
2
+
3
+ from .native import *
@@ -0,0 +1,99 @@
1
+ import logging
2
+ import sys
3
+ import time
4
+
5
+ __all__ = ['set_logger', 'LOG_LEVEL_EVENT', 'Topic', 'RegularTopic', 'PatternTopic', 'EventHook', 'EventEngine', 'LOGGER', 'use_cpp_override']
6
+ LOGGER: logging.Logger | None = None
7
+ DEBUG = False
8
+ LOG_LEVEL = logging.INFO
9
+ LOG_LEVEL_EVENT = LOG_LEVEL - 5
10
+
11
+
12
+ class ColoredFormatter(logging.Formatter):
13
+ """Logging Formatter to add colors and count warning / errors"""
14
+
15
+ def __init__(self, fmt=None, datefmt=None, style='{', validate=True):
16
+ self.format_str = '[{asctime} {name} - {threadName} - {module}:{lineno} - {levelname}] {message}' if fmt is None else fmt
17
+ self.date_fmt = '%Y-%m-%d %H:%M:%S' if datefmt is None else datefmt
18
+ self.style = style
19
+
20
+ super().__init__(fmt=fmt, datefmt=datefmt, style=style, validate=validate)
21
+
22
+ def _get_format(self, level: int, select=False):
23
+ bold_red = f"\33[31;1;3;4{';7' if select else ''}m"
24
+ red = f"\33[31;1{';7' if select else ''}m"
25
+ green = f"\33[32;1{';7' if select else ''}m"
26
+ yellow = f"\33[33;1{';7' if select else ''}m"
27
+ blue = f"\33[34;1{';7' if select else ''}m"
28
+ reset = "\33[0m"
29
+
30
+ if level <= logging.NOTSET:
31
+ fmt = self.format_str
32
+ elif level <= logging.DEBUG:
33
+ fmt = blue + self.format_str + reset
34
+ elif level <= logging.INFO:
35
+ fmt = green + self.format_str + reset
36
+ elif level <= logging.WARNING:
37
+ fmt = yellow + self.format_str + reset
38
+ elif level <= logging.ERROR:
39
+ fmt = red + self.format_str + reset
40
+ else:
41
+ fmt = bold_red + self.format_str + reset
42
+
43
+ return fmt
44
+
45
+ def format(self, record):
46
+ log_fmt = self._get_format(level=record.levelno)
47
+ formatter = logging.Formatter(log_fmt, datefmt=self.date_fmt, style=self.style)
48
+ return formatter.format(record)
49
+
50
+
51
+ def get_logger(**kwargs) -> logging.Logger:
52
+ level = kwargs.get('level', LOG_LEVEL)
53
+ stream_io = kwargs.get('stream_io', sys.stdout)
54
+ formatter = kwargs.get('formatter', ColoredFormatter())
55
+ global LOGGER
56
+
57
+ if LOGGER is not None:
58
+ return LOGGER
59
+
60
+ LOGGER = logging.getLogger('EventEngine')
61
+ LOGGER.setLevel(level)
62
+ logging.Formatter.converter = time.gmtime
63
+
64
+ if stream_io:
65
+ have_handler = False
66
+ for handler in LOGGER.handlers:
67
+ # noinspection PyUnresolvedReferences
68
+ if type(handler) == logging.StreamHandler and handler.stream == stream_io:
69
+ have_handler = True
70
+ break
71
+
72
+ if not have_handler:
73
+ logger_ch = logging.StreamHandler(stream=stream_io)
74
+ logger_ch.setLevel(level=level)
75
+ logger_ch.setFormatter(fmt=formatter)
76
+ LOGGER.addHandler(logger_ch)
77
+
78
+ return LOGGER
79
+
80
+
81
+ def set_logger(logger: logging.Logger):
82
+ global LOGGER
83
+ LOGGER = logger
84
+
85
+ _event.LOGGER = LOGGER.getChild('Event')
86
+
87
+
88
+ def use_cpp_override():
89
+ try:
90
+ from ._topic_c import Topic, RegularTopic, PatternTopic
91
+ from . import _topic as _Topic_native
92
+ except Exception as _:
93
+ LOGGER.warning(_)
94
+
95
+
96
+ _ = get_logger()
97
+
98
+ from ._topic import Topic, RegularTopic, PatternTopic
99
+ from ._event import EventHook, EventEngine
@@ -0,0 +1,405 @@
1
+ import datetime
2
+ import enum
3
+ import inspect
4
+ import time
5
+ import traceback
6
+ from collections import deque
7
+ from logging import Logger
8
+ from threading import Thread, Semaphore
9
+ from typing import Iterable, TypedDict, NotRequired, Callable
10
+
11
+ from . import LOGGER, LOG_LEVEL_EVENT, Topic, DEBUG
12
+
13
+ LOGGER = LOGGER.getChild('Event')
14
+
15
+
16
+ class EventDict(TypedDict):
17
+ topic: str
18
+ args: NotRequired[tuple]
19
+ kwargs: NotRequired[dict]
20
+
21
+
22
+ class EventHookBase(object):
23
+ """
24
+ Event object with
25
+ a string topic for event engine to distribute event,
26
+ and a list of handler to process data
27
+ """
28
+
29
+ def __init__(self, topic: Topic, logger: Logger = None, max_size: int = None):
30
+ self.topic = topic
31
+ self.logger = LOGGER.getChild(f'EventHook.{topic}') if logger is None else logger
32
+ self.handlers: deque[Callable] = deque(maxlen=max_size)
33
+
34
+ def __call__(self, *args, **kwargs):
35
+ self.trigger(topic=self.topic, args=args, kwargs=kwargs)
36
+
37
+ def __iadd__(self, handler: Callable):
38
+ self.add_handler(handler)
39
+ return self
40
+
41
+ def __isub__(self, handler: Callable):
42
+ self.remove_handler(handler)
43
+ return self
44
+
45
+ def trigger(self, topic: Topic, args: tuple = None, kwargs: dict = None):
46
+ if args is None:
47
+ args = ()
48
+
49
+ if kwargs is None:
50
+ kwargs = {}
51
+
52
+ for handler in self.handlers:
53
+ try:
54
+ try:
55
+ handler(topic=topic, *args, **kwargs)
56
+ except TypeError as e:
57
+ if e.__str__().endswith("unexpected keyword argument 'topic'"):
58
+ handler(*args, **kwargs)
59
+ else:
60
+ raise e
61
+ except Exception as _:
62
+ self.logger.error(traceback.format_exc())
63
+
64
+ def add_handler(self, handler: Callable):
65
+ if handler in self.handlers:
66
+ LOGGER.warning(f'Handler {handler} already in {self}. This action might cause it to trigger twice.')
67
+ self.handlers.append(handler)
68
+
69
+ def remove_handler(self, handler: Callable):
70
+ try:
71
+ self.handlers.remove(handler)
72
+ except ValueError as e:
73
+ self.logger.error(f'Handler {handler} not found in {self}.')
74
+
75
+
76
+ class EventHook(EventHookBase):
77
+ def __init__(self, topic: Topic, logger: Logger = None, max_size: int = None, handler: list[Callable] | Callable | None = None):
78
+ super().__init__(topic=topic, logger=logger, max_size=max_size)
79
+ self.with_topic: deque[bool] = deque()
80
+
81
+ if handler is None:
82
+ pass
83
+ elif callable(handler):
84
+ self.add_handler(handler)
85
+ elif isinstance(handler, Iterable):
86
+ for _handler in handler:
87
+ self.add_handler(handler=_handler)
88
+ else:
89
+ raise ValueError(f'Invalid handler {handler}, expect a Callable or a list of Callable.')
90
+
91
+ def trigger(self, topic: Topic, args: tuple = None, kwargs: dict = None):
92
+ ts = time.time()
93
+ if args is None:
94
+ args = ()
95
+
96
+ if kwargs is None:
97
+ kwargs = {}
98
+
99
+ for handler, with_topic in zip(self.handlers, self.with_topic):
100
+ try:
101
+ if with_topic:
102
+ handler(topic=topic, *args, **kwargs)
103
+ else:
104
+ handler(*args, **kwargs)
105
+ except Exception as _:
106
+ self.logger.error(traceback.format_exc())
107
+
108
+ if DEBUG:
109
+ self.logger.log(LOG_LEVEL_EVENT, f'EventHook {self.topic} tasks triggered {len(self.handlers):,} handlers, complete in {(time.time() - ts) * 1000:.3f}ms.')
110
+
111
+ def add_handler(self, handler: Callable, with_topic: bool = None):
112
+ sig = inspect.signature(handler)
113
+
114
+ if with_topic is None:
115
+ for param in sig.parameters.values():
116
+ if param.name == 'topic' or param.kind == param.VAR_KEYWORD:
117
+ with_topic = True
118
+ break
119
+
120
+ super().add_handler(handler=handler)
121
+ self.with_topic.append(with_topic)
122
+
123
+ def remove_handler(self, handler: Callable):
124
+ try:
125
+ idx = self.handlers.index(handler)
126
+ self.handlers.__delitem__(idx)
127
+ self.with_topic.__delitem__(idx)
128
+ except ValueError as e:
129
+ pass
130
+
131
+
132
+ class EventEngineBase(object):
133
+ EventHook = EventHook
134
+
135
+ def __init__(self, logger: Logger = None, buffer_size: int = 0):
136
+ self.logger = LOGGER.getChild(f'EventEngine') if logger is None else logger
137
+ self._buffer_size = buffer_size
138
+ self._put_lock = Semaphore(self._buffer_size)
139
+ self._get_lock = Semaphore(0)
140
+ self._deque: deque[EventDict] = deque(maxlen=buffer_size if buffer_size else None)
141
+ self._active: bool = False
142
+ self._engine: Thread = None
143
+ self._event_hooks: dict[Topic, EventHook] = {}
144
+
145
+ if buffer_size and buffer_size < 8:
146
+ self.logger.info(f'buffer_size={buffer_size} too small. This might cause a dead lock.')
147
+
148
+ def _run(self) -> None:
149
+ """
150
+ Get event from queue and then process it.
151
+ """
152
+ while self._active:
153
+ self._get_lock.acquire(blocking=True, timeout=None)
154
+
155
+ try:
156
+ event_dict = self._deque.popleft()
157
+ except IndexError as e:
158
+ if not self._active:
159
+ return
160
+ raise e
161
+
162
+ topic = event_dict['topic']
163
+ args = event_dict.get('args', ())
164
+ kwargs = event_dict.get('kwargs', {})
165
+ self._process(topic=topic, *args, **kwargs)
166
+
167
+ if self._buffer_size:
168
+ self._put_lock.release()
169
+
170
+ def _process(self, topic: str, *args, **kwargs) -> None:
171
+ """
172
+ distribute data to registered event hook in the order of registration
173
+ """
174
+ for event_topic, event_hook in self._event_hooks.items():
175
+ if matched_topic := event_topic.match(topic=topic):
176
+ event_hook.trigger(topic=matched_topic, args=args, kwargs=kwargs)
177
+
178
+ def start(self) -> None:
179
+ """
180
+ Start event engine to process events and generate timer events.
181
+ """
182
+ if self._active:
183
+ self.logger.warning(f'{self} already started!')
184
+ return
185
+
186
+ self._active = True
187
+ self._engine = Thread(target=self._run, name='EventEngine')
188
+ self._engine.start()
189
+
190
+ def stop(self) -> None:
191
+ """
192
+ Stop event engine.
193
+ """
194
+ if not self._active:
195
+ self.logger.warning('EventEngine already stopped!')
196
+ return
197
+
198
+ self._active = False
199
+ self._get_lock.release()
200
+ self._engine.join()
201
+
202
+ def clear(self) -> None:
203
+ if self._active:
204
+ self.logger.error('EventEngine must be stopped before cleared!')
205
+ return
206
+
207
+ self._event_hooks.clear()
208
+ self._deque.clear()
209
+
210
+ if self._buffer_size:
211
+ self._put_lock._value = self._buffer_size
212
+ self._get_lock._value = 0
213
+
214
+ def put(self, topic: str | Topic, block: bool = True, timeout: float = None, *args, **kwargs):
215
+ """
216
+ fast way to put an event, kwargs MUST NOT contain "topic", "block" and "timeout" keywords
217
+ :param topic: the topic to put into engine
218
+ :param block: block if necessary until a free slot is available
219
+ :param timeout: If 'timeout' is a non-negative number, it blocks at most 'timeout' seconds and raises the Full exception
220
+ :param args: args for handlers
221
+ :param kwargs: kwargs for handlers
222
+ :return: nothing
223
+ """
224
+ self.publish(topic=topic, block=block, timeout=timeout, args=args, kwargs=kwargs)
225
+
226
+ def publish(self, topic: str | Topic, block: bool = True, timeout: float = None, args=None, kwargs=None):
227
+ """
228
+ safe way to publish an event
229
+ :param topic: the topic to put into engine
230
+ :param block: block if necessary until a free slot is available
231
+ :param timeout: If 'timeout' is a non-negative number, it blocks at most 'timeout' seconds and raises the Full exception
232
+ :param args: a list / tuple, args for handlers
233
+ :param kwargs: a dict, kwargs for handlers
234
+ :return: nothing
235
+ """
236
+ if isinstance(topic, Topic):
237
+ topic = topic.value
238
+ elif not isinstance(topic, str):
239
+ raise ValueError(f'Invalid topic {topic}')
240
+
241
+ if self._buffer_size:
242
+ self._put_lock.acquire()
243
+
244
+ event_dict = {'topic': topic}
245
+
246
+ if args is not None:
247
+ event_dict['args'] = args
248
+
249
+ if kwargs is not None:
250
+ event_dict['kwargs'] = kwargs
251
+
252
+ self._deque.append(event_dict)
253
+
254
+ self._get_lock.release()
255
+
256
+ def register_hook(self, hook: EventHook) -> None:
257
+ """
258
+ register a hook event
259
+ """
260
+ if hook.topic in self._event_hooks:
261
+ for handler in hook.handlers:
262
+ self._event_hooks[hook.topic].add_handler(handler)
263
+ else:
264
+ self._event_hooks[hook.topic] = hook
265
+
266
+ def unregister_hook(self, topic: Topic) -> None:
267
+ """
268
+ Unregister an existing hook
269
+ """
270
+ if topic in self._event_hooks:
271
+ self._event_hooks.pop(topic)
272
+
273
+ def register_handler(self, topic: Topic, handler: Iterable[Callable] | Callable) -> None:
274
+ """
275
+ Register one or more handler for a specific topic
276
+ """
277
+
278
+ if not isinstance(topic, Topic):
279
+ raise TypeError(f'Invalid topic {topic}')
280
+
281
+ if topic not in self._event_hooks:
282
+ self._event_hooks[topic] = self.EventHook(topic=topic, handler=handler, logger=self.logger.getChild(topic.value))
283
+ else:
284
+ self._event_hooks[topic].add_handler(handler)
285
+
286
+ def unregister_handler(self, topic: Topic, handler: Callable) -> None:
287
+ """
288
+ Unregister an existing handler function.
289
+ """
290
+ if topic in self._event_hooks:
291
+ self._event_hooks[topic].remove_handler(handler=handler)
292
+
293
+ @property
294
+ def buffer_size(self):
295
+ return self._buffer_size
296
+
297
+ @property
298
+ def active(self) -> bool:
299
+ return self._active
300
+
301
+
302
+ class EventEngine(EventEngineBase):
303
+ EventHook = EventHook
304
+
305
+ def __init__(self, buffer_size=0):
306
+ super().__init__(buffer_size=buffer_size)
307
+ self.timer: dict[float | str, Thread] = {}
308
+
309
+ def register_handler(self, topic: Topic | str | enum.Enum, handler: Callable):
310
+ topic = Topic.cast(topic)
311
+ super().register_handler(topic=topic, handler=handler)
312
+
313
+ def publish(self, topic, block: bool = True, timeout: float = None, args=None, kwargs=None):
314
+ topic = Topic.cast(topic)
315
+ super().publish(topic=topic, block=block, timeout=timeout, args=args, kwargs=kwargs)
316
+
317
+ def unregister_hook(self, topic) -> None:
318
+ topic = Topic.cast(topic)
319
+ super().unregister_hook(topic=topic)
320
+
321
+ def unregister_handler(self, topic, handler) -> None:
322
+ topic = Topic.cast(topic)
323
+
324
+ try:
325
+ super().unregister_handler(topic=topic, handler=handler)
326
+ except ValueError as _:
327
+ raise ValueError(f'unregister topic {topic} failed! handler {handler} not found!')
328
+
329
+ def get_timer(self, interval: datetime.timedelta | float | int, activate_time: datetime.datetime = None) -> Topic:
330
+ """
331
+ Start a timer, if not exist, and get topic of the timer event
332
+ :param interval: timer event interval in seconds
333
+ :param activate_time: UTC, timer event only start after active_time. This arg has no effect if timer already started.
334
+ :return: the topic of timer event hook
335
+ """
336
+ if isinstance(interval, datetime.timedelta):
337
+ interval = interval.total_seconds()
338
+
339
+ if interval == 1:
340
+ topic = Topic('EventEngine.Internal.Timer.Second')
341
+ timer = Thread(target=self._second_timer, kwargs={'topic': topic})
342
+ elif interval == 60:
343
+ topic = Topic('EventEngine.Internal.Timer.Minute')
344
+ timer = Thread(target=self._minute_timer, kwargs={'topic': topic})
345
+ else:
346
+ topic = Topic(f'EventEngine.Internal.Timer.{interval}')
347
+ timer = Thread(target=self._run_timer, kwargs={'interval': interval, 'topic': topic, 'activate_time': activate_time})
348
+
349
+ if interval not in self.timer:
350
+ self.timer[interval] = timer
351
+ timer.start()
352
+ else:
353
+ if activate_time is not None:
354
+ self.logger.debug(f'Timer thread with interval [{datetime.timedelta(seconds=interval)}] already initialized! Argument [activate_time] takes no effect!')
355
+
356
+ return topic
357
+
358
+ def _run_timer(self, interval: datetime.timedelta | float | int, topic: Topic, activate_time: datetime.datetime = None) -> None:
359
+ if isinstance(interval, datetime.timedelta):
360
+ interval = interval.total_seconds()
361
+
362
+ if activate_time is None:
363
+ scheduled_time = datetime.datetime.utcnow()
364
+ else:
365
+ scheduled_time = activate_time
366
+
367
+ while self._active:
368
+ sleep_time = (scheduled_time - datetime.datetime.utcnow()).total_seconds()
369
+
370
+ if sleep_time > 0:
371
+ time.sleep(sleep_time)
372
+ self.put(topic=topic, interval=interval, trigger_time=scheduled_time)
373
+
374
+ while scheduled_time < datetime.datetime.utcnow():
375
+ scheduled_time += datetime.timedelta(seconds=interval)
376
+
377
+ def _minute_timer(self, topic: Topic):
378
+ while self._active:
379
+ t = time.time()
380
+ scheduled_time = t // 60 * 60
381
+ next_time = scheduled_time + 60
382
+ sleep_time = next_time - t
383
+ time.sleep(sleep_time)
384
+ self.put(topic=topic, interval=60., timestamp=scheduled_time)
385
+
386
+ def _second_timer(self, topic: Topic):
387
+ while self._active:
388
+ t = time.time()
389
+ scheduled_time = t // 1
390
+ next_time = scheduled_time + 1
391
+ sleep_time = next_time - t
392
+ time.sleep(sleep_time)
393
+ self.put(topic=topic, interval=1., timestamp=scheduled_time)
394
+
395
+ def stop(self) -> None:
396
+ super().stop()
397
+
398
+ for timer in self.timer.values():
399
+ timer.join()
400
+
401
+ def clear(self) -> None:
402
+ for t in self.timer.values():
403
+ t.join(timeout=0)
404
+
405
+ self.timer.clear()
@@ -0,0 +1,156 @@
1
+ import re
2
+ from enum import Enum
3
+ from string import Formatter
4
+ from typing import Self, Type
5
+
6
+
7
+ class Topic(dict):
8
+ """
9
+ topic for event hook. e.g. "TickData.002410.SZ.Realtime"
10
+ """
11
+
12
+ class Error(Exception):
13
+ def __init__(self, msg):
14
+ super().__init__(msg)
15
+
16
+ def __init__(self, topic: str, *args, **kwargs):
17
+ self._value = topic
18
+ super().__init__(*args, **kwargs)
19
+
20
+ def __repr__(self):
21
+ return f'<{self.__class__.__name__}>({self._value}){super().__repr__()}'
22
+
23
+ def __str__(self):
24
+ return self.value
25
+
26
+ def __bool__(self):
27
+ return True
28
+
29
+ def __hash__(self):
30
+ return self.value.__hash__()
31
+
32
+ def match(self, topic: str) -> Self | None:
33
+ if self._value == topic:
34
+ # return self.__class__(topic=topic)
35
+ return self
36
+ else:
37
+ return None
38
+
39
+ @classmethod
40
+ def cast(cls, topic: Self | str | Enum, dtype: Type[Self] = None) -> Self:
41
+ if isinstance(topic, cls):
42
+ return topic
43
+ elif isinstance(topic, Enum):
44
+ t = topic.value
45
+ return cls.cast(t)
46
+ elif isinstance(topic, str):
47
+ if dtype is None:
48
+ if re.search(r'{(.+?)}', topic):
49
+ t = PatternTopic(pattern=topic)
50
+ elif '*' in topic or '+' in topic or '|' in topic:
51
+ re.compile(pattern=topic)
52
+ t = RegularTopic(pattern=topic)
53
+ else:
54
+ t = Topic(topic=topic)
55
+ else:
56
+ t = dtype(topic)
57
+ else:
58
+ raise NotImplementedError(f'Can not cast {topic} into {cls}.')
59
+
60
+ return t
61
+
62
+ @property
63
+ def value(self) -> str:
64
+ return self._value
65
+
66
+
67
+ class RegularTopic(Topic):
68
+ """
69
+ topic in regular expression. e.g. "TickData.(.+).((SZ)|(SH)).((Realtime)|(History))"
70
+ """
71
+
72
+ def __init__(self, pattern: str):
73
+ super().__init__(topic=pattern)
74
+
75
+ def match(self, topic: str) -> Topic | None:
76
+ if re.match(self._value, topic):
77
+ match = Topic(topic=topic)
78
+ match['pattern'] = self._value
79
+ return match
80
+ else:
81
+ return None
82
+
83
+
84
+ class PatternTopic(Topic):
85
+ """
86
+ topic for event hook. e.g. "TickData.{symbol}.{market}.{flag}"
87
+ """
88
+
89
+ class NotMatchError(Topic.Error):
90
+ pass
91
+
92
+ def __init__(self, pattern: str):
93
+ super().__init__(topic=pattern)
94
+
95
+ def __call__(self, **kwargs):
96
+ return self.format_map(kwargs)
97
+
98
+ # @classmethod
99
+ # def extract_mapping(cls, target: str, pattern: str):
100
+ # pattern = re.escape(pattern)
101
+ # regex = re.sub(r'\\{(.+?)\\}', r'(?P<_\1>.+)', pattern)
102
+ # match = re.match(regex, target)
103
+ # if match:
104
+ # values = list(match.groups())
105
+ # keys = re.findall(r'\\{(.+?)\\}', pattern)
106
+ # m = dict(zip(keys, values))
107
+ # return m
108
+ # else:
109
+ # raise Topic.Error(f'pattern {pattern} not in string {target} found!')
110
+
111
+ @classmethod
112
+ def extract_mapping(cls, target: str, pattern: str) -> dict[str, str]:
113
+ dictionary = {}
114
+
115
+ result_parts = target.split('.')
116
+ pattern_parts = pattern.split('.')
117
+
118
+ # Check if the number of parts in result and pattern are the same
119
+ if len(result_parts) < len(pattern_parts):
120
+ raise cls.NotMatchError(f'Target {target} not match with pattern {pattern}.')
121
+
122
+ # Generate the mapping dictionary
123
+ for result_part, pattern_part in zip(result_parts, pattern_parts):
124
+ if pattern_part[0] == '{' and pattern_part[-1] == '}':
125
+ content: str = pattern_part[1:-1]
126
+ dictionary[content] = result_part
127
+ else:
128
+ if result_part != pattern_part:
129
+ dictionary.clear()
130
+ raise cls.NotMatchError(f'Target {target} not match with pattern {pattern}.')
131
+
132
+ return dictionary
133
+
134
+ def format_map(self, mapping: dict) -> Topic:
135
+ for key in self.keys():
136
+ if key not in mapping:
137
+ mapping[key] = f'{{{key}}}'
138
+
139
+ return Topic.cast(self._value.format_map(mapping))
140
+
141
+ def keys(self):
142
+ keys = [i[1] for i in Formatter().parse(self._value) if i[1] is not None]
143
+ return keys
144
+
145
+ def match(self, topic: str) -> Topic | None:
146
+ try:
147
+ keyword_dict = self.extract_mapping(target=topic, pattern=self._value)
148
+ match = Topic(topic=topic)
149
+ match.update(keyword_dict)
150
+ return match
151
+ except self.NotMatchError as _:
152
+ return None
153
+
154
+ @property
155
+ def value(self) -> str:
156
+ return self._value.format_map({_: '*' for _ in self.keys()})
@@ -0,0 +1,94 @@
1
+ from __future__ import annotations
2
+
3
+ import ctypes
4
+ import pathlib
5
+ import platform
6
+ import re
7
+
8
+ from ._topic import Topic, RegularTopic as RegularTopicBase, PatternTopic as PatternTopicBase
9
+
10
+ __all__ = ['Topic', 'RegularTopic', 'PatternTopic']
11
+
12
+ topic_lib = None
13
+
14
+ if platform.system() == 'Windows':
15
+ package_name = 'topic_api.(.*).pyd'
16
+ elif platform.system() == 'Darwin':
17
+ package_name = '^topic_api(.*).so$'
18
+ else:
19
+ package_name = '^topic_api(.*).so$'
20
+
21
+ ROOT_DIR = pathlib.Path(__file__).parent.parent
22
+ ENCODING = 'utf-8'
23
+
24
+ for _ in ROOT_DIR.iterdir():
25
+ if lib_path := re.search(package_name, _.name):
26
+ topic_lib = ctypes.CDLL(str(_))
27
+ break
28
+
29
+ # Load the shared library
30
+ if topic_lib is None:
31
+ raise ImportError(f'EventEngine.Topic c extension {package_name} not found in {ROOT_DIR}! Fallback to native lib!')
32
+
33
+ # Function prototypes
34
+ # topic_lib.delete_vector.restype = [ctypes.POINTER]
35
+ topic_lib.get_vector_value.restype = ctypes.c_char_p
36
+ topic_lib.is_regular_match.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
37
+ topic_lib.is_regular_match.restype = ctypes.c_int
38
+
39
+
40
+ # Define the RegularTopic class in Python
41
+ class RegularTopic(RegularTopicBase):
42
+ """
43
+ topic in regular expression. e.g. "TickData.(.+).((SZ)|(SH)).((Realtime)|(History))"
44
+ """
45
+
46
+ def match(self, topic: str):
47
+ match = topic_lib.is_regular_match(topic.encode(ENCODING), self._value.encode(ENCODING))
48
+
49
+ if match:
50
+ return Topic(topic=topic, pattern=self.value)
51
+ else:
52
+ return None
53
+
54
+
55
+ # Define the PatternTopic class in Python
56
+ class PatternTopic(PatternTopicBase):
57
+ """
58
+ topic for event hook. e.g. "TickData.{symbol}.{market}.{flag}"
59
+ """
60
+
61
+ def match(self, topic: str):
62
+ mapping = self.extract_mapping(target=topic, pattern=self._value, encoding=ENCODING)
63
+
64
+ if mapping:
65
+ return Topic(topic, pattern=self._value, **mapping)
66
+ else:
67
+ return None
68
+
69
+ @classmethod
70
+ def extract_mapping(cls, target: str, pattern: str, encoding: str = ENCODING) -> dict[str, str]:
71
+ # noinspection PyArgumentList
72
+ keys_ptr, values_ptr = ctypes.POINTER(ctypes.c_void_p)(), ctypes.POINTER(ctypes.c_void_p)()
73
+ mapping = {}
74
+
75
+ topic_lib.extract_mapping(
76
+ target.encode(encoding),
77
+ pattern.encode(encoding),
78
+ ctypes.byref(keys_ptr),
79
+ ctypes.byref(values_ptr)
80
+ )
81
+
82
+ for i in range(topic_lib.vector_size(ctypes.byref(keys_ptr))):
83
+ key = topic_lib.get_vector_value(ctypes.byref(keys_ptr), i)
84
+ value = topic_lib.get_vector_value(ctypes.byref(values_ptr), i)
85
+ # print(key, value)
86
+ mapping[key.decode(encoding)] = value.decode(encoding)
87
+
88
+ # topic_lib.delete_vector(ctypes.byref(keys_ptr))
89
+ # topic_lib.delete_vector(ctypes.byref(values_ptr))
90
+
91
+ del keys_ptr
92
+ del values_ptr
93
+
94
+ return mapping