PyEventEngine 0.6.2__cp315-cp315-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. event_engine/__infra__.pxd +2 -0
  2. event_engine/__init__.pxd +2 -0
  3. event_engine/__init__.py +62 -0
  4. event_engine/base/__infra__.pxd +9 -0
  5. event_engine/base/__init__.pxd +9 -0
  6. event_engine/base/__init__.py +31 -0
  7. event_engine/base/c_allocator_protocol.c +14310 -0
  8. event_engine/base/c_allocator_protocol.cp315-win_amd64.pyd +0 -0
  9. event_engine/base/c_allocator_protocol.h +23 -0
  10. event_engine/base/c_allocator_protocol.pxd +31 -0
  11. event_engine/base/c_allocator_protocol.pyx +193 -0
  12. event_engine/capi/__infra__.pxd +189 -0
  13. event_engine/capi/__init__.pxd +189 -0
  14. event_engine/capi/__init__.py +84 -0
  15. event_engine/capi/c_engine.c +32542 -0
  16. event_engine/capi/c_engine.cp315-win_amd64.pyd +0 -0
  17. event_engine/capi/c_engine.h +393 -0
  18. event_engine/capi/c_engine.pxd +84 -0
  19. event_engine/capi/c_engine.pyi +478 -0
  20. event_engine/capi/c_engine.pyx +673 -0
  21. event_engine/capi/c_event.c +22826 -0
  22. event_engine/capi/c_event.cp315-win_amd64.pyd +0 -0
  23. event_engine/capi/c_event.h +342 -0
  24. event_engine/capi/c_event.pxd +159 -0
  25. event_engine/capi/c_event.pyi +265 -0
  26. event_engine/capi/c_event.pyx +444 -0
  27. event_engine/capi/c_topic.c +34545 -0
  28. event_engine/capi/c_topic.cp315-win_amd64.pyd +0 -0
  29. event_engine/capi/c_topic.h +1007 -0
  30. event_engine/capi/c_topic.pxd +147 -0
  31. event_engine/capi/c_topic.pyi +644 -0
  32. event_engine/capi/c_topic.pyx +818 -0
  33. event_engine/capi/fallback_engine.py +707 -0
  34. event_engine/config_view.c +10271 -0
  35. event_engine/config_view.cp315-win_amd64.pyd +0 -0
  36. event_engine/config_view.pyx +55 -0
  37. event_engine/includes/event_engine/base/c_allocator_protocol.c +14310 -0
  38. event_engine/includes/event_engine/base/c_allocator_protocol.h +23 -0
  39. event_engine/includes/event_engine/capi/c_engine.c +32542 -0
  40. event_engine/includes/event_engine/capi/c_engine.h +393 -0
  41. event_engine/includes/event_engine/capi/c_event.c +22826 -0
  42. event_engine/includes/event_engine/capi/c_event.h +342 -0
  43. event_engine/includes/event_engine/capi/c_topic.c +34545 -0
  44. event_engine/includes/event_engine/capi/c_topic.h +1007 -0
  45. event_engine/includes/event_engine/config_view.c +10271 -0
  46. event_engine/native/__init__.py +60 -0
  47. event_engine/native/_event.py +420 -0
  48. event_engine/native/_topic.py +171 -0
  49. event_engine/native/engine.py +793 -0
  50. event_engine/native/event.py +469 -0
  51. event_engine/native/topic.py +943 -0
  52. pyeventengine-0.6.2.dist-info/METADATA +189 -0
  53. pyeventengine-0.6.2.dist-info/RECORD +56 -0
  54. pyeventengine-0.6.2.dist-info/WHEEL +5 -0
  55. pyeventengine-0.6.2.dist-info/licenses/LICENSE +21 -0
  56. pyeventengine-0.6.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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()})