PyEventEngine 0.6.2__cp315-cp315t-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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.cp315t-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.cp315t-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.cp315t-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.cp315t-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.cp315t-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,644 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
import re
|
|
3
|
+
from collections.abc import Iterable, Iterator
|
|
4
|
+
from typing import Any, TypedDict, overload
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_internal_topic(key: str, owner: bool = False) -> Topic | None:
|
|
8
|
+
""" Get a registered topic from the internal map, if there is any.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
key: the literal of the topic to look up.
|
|
12
|
+
owner: whether the returned Topic owns the underlying memory. If so, when the Topic is deallocated,
|
|
13
|
+
the underlying memory will be freed and de-registered. If False, the Topic is just a wrapper around the internal memory.
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
Topic instance if found or None.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_internal_map() -> dict[str, Topic]:
|
|
21
|
+
"""Return a dictionary view of the internal topic map.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
A dictionary mapping topic literal strings to Topic instances.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TopicType(enum.IntEnum):
|
|
29
|
+
"""Enumeration of topic part types.
|
|
30
|
+
|
|
31
|
+
Maps to the underlying C-level TopicType constants.
|
|
32
|
+
"""
|
|
33
|
+
TOPIC_PART_EXACT: int
|
|
34
|
+
TOPIC_PART_ANY: int
|
|
35
|
+
TOPIC_PART_RANGE: int
|
|
36
|
+
TOPIC_PART_PATTERN: int
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TopicPart:
|
|
40
|
+
"""Base Python wrapper for a single topic part.
|
|
41
|
+
|
|
42
|
+
Attributes:
|
|
43
|
+
owner: Whether this Python object owns/free the underlying memory.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
owner: bool
|
|
47
|
+
|
|
48
|
+
def __init__(self, *args: Any, alloc: bool = False, **kwargs: Any) -> None:
|
|
49
|
+
"""Create or attach to a topic part.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
alloc: If True, allocate and initialize internal C structures.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def next(self) -> TopicPart:
|
|
56
|
+
"""Return the next topic part.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
The next TopicPart instance.
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
StopIteration: If this is the last part.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def ttype(self) -> TopicType:
|
|
67
|
+
"""int: The topic part type as a TopicType."""
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def addr(self) -> int:
|
|
71
|
+
"""int: Numeric address / id of the underlying C structure."""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class TopicPartExact(TopicPart):
|
|
75
|
+
"""Topic part representing an exact literal segment."""
|
|
76
|
+
|
|
77
|
+
def __init__(self, part: str = None, *args: Any, alloc: bool = False, **kwargs: Any) -> None:
|
|
78
|
+
"""Create an exact topic part.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
part: Literal string to store. If omitted and alloc is True, an empty initialized part is created.
|
|
82
|
+
alloc: If True, allocate underlying C memory.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def __repr__(self) -> str:
|
|
86
|
+
"""Return human-readable representation."""
|
|
87
|
+
|
|
88
|
+
def __len__(self) -> int:
|
|
89
|
+
"""Return the length of the stored literal in bytes."""
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def part(self) -> str:
|
|
93
|
+
"""The literal string value for this part."""
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class TopicPartAny(TopicPart):
|
|
97
|
+
"""Topic part representing a named wildcard."""
|
|
98
|
+
|
|
99
|
+
def __init__(self, name: str = None, *args: Any, alloc: bool = False, **kwargs: Any) -> None:
|
|
100
|
+
"""Create an 'any' topic part.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
name: Optional name for the wildcard.
|
|
104
|
+
alloc: If True, allocate underlying C memory.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def __repr__(self) -> str:
|
|
108
|
+
"""Return human-readable representation."""
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def name(self) -> str:
|
|
112
|
+
"""The wildcard name (identifier)."""
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class TopicPartRange(TopicPart):
|
|
116
|
+
"""Topic part representing a range (choice) among multiple literals."""
|
|
117
|
+
|
|
118
|
+
def __init__(self, options: list[str] = None, *args: Any, alloc: bool = False, **kwargs: Any) -> None:
|
|
119
|
+
"""Create a range part.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
options: List of literal option strings.
|
|
123
|
+
alloc: If True, allocate underlying C memory.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
def __repr__(self) -> str:
|
|
127
|
+
"""Return human-readable representation."""
|
|
128
|
+
|
|
129
|
+
def __len__(self) -> int:
|
|
130
|
+
"""Return the number of options."""
|
|
131
|
+
|
|
132
|
+
def __iter__(self) -> Iterator[str]:
|
|
133
|
+
"""Iterate over option strings."""
|
|
134
|
+
|
|
135
|
+
def options(self) -> Iterator[str]:
|
|
136
|
+
"""Yield option strings in order.
|
|
137
|
+
|
|
138
|
+
Yields:
|
|
139
|
+
Each option as a Python string.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class TopicPartPattern(TopicPart):
|
|
144
|
+
"""Topic part representing a regex pattern."""
|
|
145
|
+
|
|
146
|
+
def __init__(self, regex: str = None, *args: Any, alloc: bool = False, **kwargs: Any) -> None:
|
|
147
|
+
"""Create a pattern topic part.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
regex: Regular expression string.
|
|
151
|
+
alloc: If True, allocate underlying C memory.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
def __repr__(self) -> str:
|
|
155
|
+
"""Return human-readable representation."""
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def pattern(self) -> str:
|
|
159
|
+
"""str: The raw regex pattern string."""
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def regex(self) -> re.Pattern:
|
|
163
|
+
"""re.Pattern: Compiled regex object for the pattern."""
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class TopicMatchNode(TypedDict):
|
|
167
|
+
"""TypedDict describing a single match node returned by TopicMatchResult accessors.
|
|
168
|
+
|
|
169
|
+
Keys:
|
|
170
|
+
matched: Whether this node matched.
|
|
171
|
+
part_a: The left-side topic part (or None).
|
|
172
|
+
part_b: The right-side topic part (or None).
|
|
173
|
+
literal: The literal string associated with this node (if any).
|
|
174
|
+
"""
|
|
175
|
+
matched: bool
|
|
176
|
+
part_a: TopicPart | None
|
|
177
|
+
part_b: TopicPart | None
|
|
178
|
+
literal: str | None
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class TopicMatchResult:
|
|
182
|
+
"""Container for topic part match results (linked list-like).
|
|
183
|
+
|
|
184
|
+
Provides iteration and indexing over match nodes and utilities to convert results.
|
|
185
|
+
|
|
186
|
+
The public API yields `TopicMatchNode` entries for per-node accessors.
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
owner: bool
|
|
190
|
+
|
|
191
|
+
def __init__(self, n_parts: int = 0, alloc: bool = False, **kwargs: Any) -> None:
|
|
192
|
+
"""Allocate or attach a chain of match result nodes.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
n_parts: Number of nodes to pre-create.
|
|
196
|
+
alloc: If True, allocate underlying C structures.
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
def __repr__(self) -> str:
|
|
200
|
+
"""Return a compact representation with success/failure and length."""
|
|
201
|
+
|
|
202
|
+
def __bool__(self) -> bool:
|
|
203
|
+
"""True if all nodes matched."""
|
|
204
|
+
|
|
205
|
+
def __len__(self) -> int:
|
|
206
|
+
"""Return number of nodes in the result chain."""
|
|
207
|
+
|
|
208
|
+
def __getitem__(self, idx: int) -> TopicMatchNode:
|
|
209
|
+
"""Return a single node's info as a TopicMatchNode.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
idx: Index of the node (supports negative indexing).
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
A TopicMatchNode TypedDict containing 'matched', 'part_a', 'part_b', 'literal'.
|
|
216
|
+
|
|
217
|
+
Raises:
|
|
218
|
+
IndexError: If idx is out of range.
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
def __iter__(self) -> Iterator[TopicMatchNode]:
|
|
222
|
+
"""Iterate over node info dicts in sequence."""
|
|
223
|
+
|
|
224
|
+
def to_dict(self) -> dict[str, TopicPart]:
|
|
225
|
+
"""Convert match results into a dictionary mapping literal -> matched part.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
A mapping from literal string to the matched TopicPart.
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
@property
|
|
232
|
+
def length(self) -> int:
|
|
233
|
+
"""int: Number of nodes in the result chain."""
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def matched(self) -> bool:
|
|
237
|
+
"""bool: True if every node in the chain reports matched == True."""
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class Topic:
|
|
241
|
+
"""High-level Python representation of a parsed topic.
|
|
242
|
+
|
|
243
|
+
Topic instances internalize their literal content into a shared internal buffer / StrMap.
|
|
244
|
+
All topics created via the normal constructor are internalized into the global StrMap and do not
|
|
245
|
+
own the underlying character storage (the buffer is bound to the global map).
|
|
246
|
+
|
|
247
|
+
Examples:
|
|
248
|
+
|
|
249
|
+
>>> # This internalizes the literal string "Realtime.TickData.600010.SH" into the global StrMap
|
|
250
|
+
... t1 = Topic("Realtime.TickData.600010.SH")
|
|
251
|
+
>>> assert not t1.owner
|
|
252
|
+
|
|
253
|
+
>>> # Because t1 does not own its buffer we can create another topic with the same literal string
|
|
254
|
+
... t2 = Topic.__new__(Topic, "Realtime.TickData.600010.SH")
|
|
255
|
+
>>> # Creation of t2 is faster due to internalization.
|
|
256
|
+
... assert id(t1) != id(t2)
|
|
257
|
+
|
|
258
|
+
``t1`` and ``t2`` are different Python objects but may share the same underlying buffer address.
|
|
259
|
+
|
|
260
|
+
Topic parser
|
|
261
|
+
The separator, option delimiter and wildcard/pattern markers can be customized at build time by
|
|
262
|
+
changing the following C macros before compiling the extension:
|
|
263
|
+
|
|
264
|
+
```
|
|
265
|
+
#define DEFAULT_TOPIC_SEP '.'
|
|
266
|
+
#define DEFAULT_OPTION_SEP '|'
|
|
267
|
+
#define DEFAULT_RANGE_BRACKETS "()"
|
|
268
|
+
#define DEFAULT_WILDCARD_MARKER '+'
|
|
269
|
+
#define DEFAULT_PATTERN_DELIM '/'
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
For example, compile with ``-DDEFAULT_TOPIC_SEP='/'`` to change the topic separator to ``/``.
|
|
273
|
+
|
|
274
|
+
When parsing a topic string:
|
|
275
|
+
- If the pattern delimiter appears in balanced pairs, the enclosed text is parsed as a regex
|
|
276
|
+
pattern part (``TopicPartPattern``).
|
|
277
|
+
- A part beginning with the wildcard marker is parsed as a wildcard part (``TopicPartAny``).
|
|
278
|
+
- A part enclosed by the range brackets and containing the option separator (or NUL) is parsed
|
|
279
|
+
as a range part (``TopicPartRange``). Options are separated by the option separator or NUL;
|
|
280
|
+
empty options are not allowed.
|
|
281
|
+
- All other parts are parsed as exact literal parts (``TopicPartExact``).
|
|
282
|
+
|
|
283
|
+
Example:
|
|
284
|
+
|
|
285
|
+
>>> t = Topic(r'Realtime.(TickData|TradeData)./^[0-9]{6}\.(SZ|SH)$/.+suffix')
|
|
286
|
+
... for tp in t:
|
|
287
|
+
>>> print(tp)
|
|
288
|
+
<TopicPartExact>(topic="Realtime")
|
|
289
|
+
<TopicPartRange>(n=2, options=['TickData', 'TradeData'])
|
|
290
|
+
<TopicPartPattern>(regex="^[0-9]{6}\.(SZ|SH)$")
|
|
291
|
+
<TopicPartAny>(name="suffix")
|
|
292
|
+
|
|
293
|
+
>>> t = Topic('Realtime.TickData.{ticker}')
|
|
294
|
+
... for tp in t:
|
|
295
|
+
... print(tp)
|
|
296
|
+
<TopicPartExact>(topic="Realtime")
|
|
297
|
+
<TopicPartExact>(topic="TickData")
|
|
298
|
+
<TopicPartAny>(name="ticker")
|
|
299
|
+
Malformed or ill-formatted literal strings may raise MemoryError during parsing.
|
|
300
|
+
|
|
301
|
+
Owning a topic (lazy init)
|
|
302
|
+
To create a topic that owns its internal buffer (``owner`` is True), use lazy initialization:
|
|
303
|
+
|
|
304
|
+
>>> t3 = Topic.__new__(Topic, alloc=True) # create an empty topic with allocated buffer
|
|
305
|
+
>>> assert t3.owner
|
|
306
|
+
>>> t3.value = "Realtime.TickData.600010.SH" # set the value once
|
|
307
|
+
... assert t3.value == "Realtime.TickData.600010.SH"
|
|
308
|
+
>>> assert t3.owner
|
|
309
|
+
|
|
310
|
+
Note:
|
|
311
|
+
assigning a literal that includes the topic separator inside a logical part (for example a ticker like ``"600010.SH"``)
|
|
312
|
+
will be split by the parser into multiple parts:
|
|
313
|
+
|
|
314
|
+
>>> t3[-2], t3[-1]
|
|
315
|
+
(<TopicPartExact>(topic="600010"), <TopicPartExact>(topic="SH"))
|
|
316
|
+
|
|
317
|
+
To avoid unwanted splitting, construct the topic from explicit parts:
|
|
318
|
+
|
|
319
|
+
>>> t4 = Topic.from_parts([
|
|
320
|
+
... TopicPartExact("Realtime", alloc=True),
|
|
321
|
+
... TopicPartExact("TickData", alloc=True),
|
|
322
|
+
... TopicPartExact("600010.SH", alloc=True),
|
|
323
|
+
... ])
|
|
324
|
+
>>> assert t4.value == "Realtime.TickData.600010.SH"
|
|
325
|
+
>>> t4[-1]
|
|
326
|
+
<TopicPartExact>(topic="600010.SH")
|
|
327
|
+
|
|
328
|
+
Or use the convenience join helper:
|
|
329
|
+
|
|
330
|
+
>>> t5 = Topic.join(["Realtime", "TickData", "600010.SH"])
|
|
331
|
+
... assert t5.value == "Realtime.TickData.600010.SH"
|
|
332
|
+
>>> t5[-1]
|
|
333
|
+
<TopicPartExact>(topic="600010.SH")
|
|
334
|
+
|
|
335
|
+
Empty parts and parsing caveats
|
|
336
|
+
The topic string must not contain empty parts. For example, ``"Realtime..TickData"``:
|
|
337
|
+
|
|
338
|
+
>>> t6 = Topic("Realtime..TickData")
|
|
339
|
+
... for tp in t6:
|
|
340
|
+
>>> print(tp)
|
|
341
|
+
<TopicPartExact>(topic="Realtime")
|
|
342
|
+
<TopicPartExact>(topic="TickData")
|
|
343
|
+
|
|
344
|
+
The empty part is ignored during parsing (parsed parts omit the empty entry) but the literal string
|
|
345
|
+
remains unchanged:
|
|
346
|
+
|
|
347
|
+
>>> t6.value
|
|
348
|
+
'Realtime..TickData'
|
|
349
|
+
|
|
350
|
+
This inconsistency between the literal string and parsed parts can be confusing—use with caution.
|
|
351
|
+
|
|
352
|
+
Notes on internalization and lifecycle
|
|
353
|
+
- Topic instances internalize their literal content into a shared global StrMap.
|
|
354
|
+
- When two Topic objects are created from the same literal string key, the underlying buffer
|
|
355
|
+
for that literal is shared (same numeric address in the StrMap). Topics remain distinct Python
|
|
356
|
+
objects but can share storage.
|
|
357
|
+
- Because of internalization, avoid frequent create/destroy cycles; prefer reusing instances or a
|
|
358
|
+
global TopicSet-style registry.
|
|
359
|
+
- Setting the ``value`` attribute mutates internalized storage and triggers de-register / re-register
|
|
360
|
+
in the internal mapping. This is an expensive operation; prefer lazy-init (create an empty owning
|
|
361
|
+
topic and set ``value`` once).
|
|
362
|
+
- ``__init__`` does not support escape characters; for complex topics that require escaping use
|
|
363
|
+
``from_parts`` or ``join``.
|
|
364
|
+
- Empty topic parts are not allowed; inputs with empty parts are parsed by ignoring the empties and
|
|
365
|
+
preserving the original literal string.
|
|
366
|
+
"""
|
|
367
|
+
|
|
368
|
+
owner: int
|
|
369
|
+
|
|
370
|
+
def __init__(self, topic: str = None, *args: Any, alloc: bool = True, **kwargs: Any):
|
|
371
|
+
"""Create a Topic from a topic string.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
topic: Topic string to parse.
|
|
375
|
+
alloc: If True, allocate and initialize (default for normal usage).
|
|
376
|
+
args: Reserved for subclassing.
|
|
377
|
+
kwargs: Reserved for subclassing.
|
|
378
|
+
|
|
379
|
+
Raises:
|
|
380
|
+
MemoryError: If parsing or allocation fails, or actually do run out of memory.
|
|
381
|
+
"""
|
|
382
|
+
|
|
383
|
+
def __len__(self) -> int:
|
|
384
|
+
"""Return the number of parts in the topic."""
|
|
385
|
+
|
|
386
|
+
def __iter__(self) -> Iterator[TopicPart]:
|
|
387
|
+
"""Iterate over topic parts (yields TopicPart subclasses)."""
|
|
388
|
+
|
|
389
|
+
def __getitem__(self, idx: int) -> TopicPart:
|
|
390
|
+
"""Return the topic part at index `idx`.
|
|
391
|
+
|
|
392
|
+
Supports negative indexing.
|
|
393
|
+
|
|
394
|
+
Raises:
|
|
395
|
+
IndexError: If index is out of range.
|
|
396
|
+
"""
|
|
397
|
+
|
|
398
|
+
@overload
|
|
399
|
+
def __add__(self, topic: Topic) -> Topic: ...
|
|
400
|
+
|
|
401
|
+
@overload
|
|
402
|
+
def __add__(self, topic: TopicPart) -> Topic: ...
|
|
403
|
+
|
|
404
|
+
def __add__(self, topic: Topic | TopicPart) -> Topic:
|
|
405
|
+
"""Return a new Topic that aggregates this topic with another Topic or TopicPart.
|
|
406
|
+
|
|
407
|
+
Behavior:
|
|
408
|
+
- `__add__` creates and returns a copy; both operands remain unchanged.
|
|
409
|
+
- Use `append` or `__iadd__` for in-place modifications.
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
topic: Either a Topic or TopicPart.
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
A new aggregated Topic.
|
|
416
|
+
|
|
417
|
+
Raises:
|
|
418
|
+
TypeError: When the operand type is unsupported.
|
|
419
|
+
"""
|
|
420
|
+
|
|
421
|
+
@overload
|
|
422
|
+
def __iadd__(self, topic: Topic) -> Topic: ...
|
|
423
|
+
|
|
424
|
+
@overload
|
|
425
|
+
def __iadd__(self, topic: TopicPart) -> Topic: ...
|
|
426
|
+
|
|
427
|
+
def __iadd__(self, topic: Topic | TopicPart) -> Topic:
|
|
428
|
+
"""In-place append another Topic or TopicPart.
|
|
429
|
+
|
|
430
|
+
Behavior:
|
|
431
|
+
- Modifies `self` in place and leaves the other operand unchanged.
|
|
432
|
+
- This is equivalent to `self.append(...)` and returns `self`.
|
|
433
|
+
"""
|
|
434
|
+
|
|
435
|
+
def __hash__(self):
|
|
436
|
+
"""Return hash based on the topic's literal value.
|
|
437
|
+
|
|
438
|
+
The hash is precomputed during initialization for efficiency.
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
A uint64_t hash integer.
|
|
442
|
+
"""
|
|
443
|
+
|
|
444
|
+
def __eq__(self, other: Topic) -> bool:
|
|
445
|
+
"""Check equality between this topic and another topic.
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
other: The other Topic to compare against.
|
|
449
|
+
|
|
450
|
+
Returns:
|
|
451
|
+
True if both topics have the same literal value.
|
|
452
|
+
"""
|
|
453
|
+
|
|
454
|
+
def __call__(self, **kwargs) -> Topic:
|
|
455
|
+
""" Alias of ``format`` method to format the topic by replacing named wildcards with provided values."""
|
|
456
|
+
|
|
457
|
+
@classmethod
|
|
458
|
+
def from_parts(cls, topic_parts: Iterable[TopicPart]) -> Topic:
|
|
459
|
+
"""Build a Topic from an iterable of TopicPart instances."""
|
|
460
|
+
|
|
461
|
+
@classmethod
|
|
462
|
+
def join(cls, topic_parts: Iterable[str]) -> Topic:
|
|
463
|
+
"""Build a Topic from an iterable of literal strings.
|
|
464
|
+
|
|
465
|
+
Each string is appended as an exact part.
|
|
466
|
+
|
|
467
|
+
Notes:
|
|
468
|
+
- This is a higher-level helper for simple literal-only topics.
|
|
469
|
+
- For complex parts that need escaping or patterns, use `from_parts`.
|
|
470
|
+
"""
|
|
471
|
+
|
|
472
|
+
def append(self, topic_part: TopicPart) -> Topic:
|
|
473
|
+
"""Append a TopicPart to this topic (high-level API).
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
topic_part: The part to append.
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
Self for chaining.
|
|
480
|
+
|
|
481
|
+
Raises:
|
|
482
|
+
RuntimeError: If either topic or part is uninitialized.
|
|
483
|
+
"""
|
|
484
|
+
|
|
485
|
+
def match(self, other: Topic) -> TopicMatchResult:
|
|
486
|
+
"""Match this topic against another topic.
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
other: The topic to match against.
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
A TopicMatchResult describing per-part matches.
|
|
493
|
+
"""
|
|
494
|
+
|
|
495
|
+
def update_literal(self) -> Topic:
|
|
496
|
+
"""Update the internal literal buffer to reflect the current parts.
|
|
497
|
+
|
|
498
|
+
This is useful after in-place modifications of the subordinate TopicParts.
|
|
499
|
+
To avoid inconsistencies, call this method to regenerate the internal literal.
|
|
500
|
+
|
|
501
|
+
Notes:
|
|
502
|
+
For TopicPartRange, the reconstructed literal may not be the same as the original.
|
|
503
|
+
e.g.
|
|
504
|
+
|
|
505
|
+
>>> t = Topic(r'Realtime.(TickData|TradeData)./^[0-9]{6}\.(SZ|SH)$/.+suffix')
|
|
506
|
+
... print(t)
|
|
507
|
+
Realtime.(TickData|TradeData)./^[0-9]{6}\.(SZ|SH)$/.+suffix
|
|
508
|
+
>>> t.update_literal()
|
|
509
|
+
<Topic Generic>(value="Realtime.(TickData|TradeData)./^[0-9]{6}\.(SZ|SH)$/.{suffix}", n_parts=4)
|
|
510
|
+
>>> print(t)
|
|
511
|
+
Realtime.(TickData|TradeData)./^[0-9]{6}\.(SZ|SH)$/.{suffix}
|
|
512
|
+
|
|
513
|
+
As show above, there are 2 ways to represent a range part: a ``+suffix`` or ``{suffix}``.
|
|
514
|
+
Recommend using the breakets format, so that the .format is done Intuitively.
|
|
515
|
+
|
|
516
|
+
Returns:
|
|
517
|
+
Self for chaining.
|
|
518
|
+
|
|
519
|
+
Raises:
|
|
520
|
+
RuntimeError: If the topic is uninitialized.
|
|
521
|
+
"""
|
|
522
|
+
|
|
523
|
+
def format(self, **kwargs) -> Topic:
|
|
524
|
+
"""Format the topic by replacing named wildcards with provided values.
|
|
525
|
+
|
|
526
|
+
Args:
|
|
527
|
+
**kwargs: Mapping from wildcard names to replacement strings.
|
|
528
|
+
|
|
529
|
+
Returns:
|
|
530
|
+
A new Topic with wildcards replaced by the provided values.
|
|
531
|
+
|
|
532
|
+
Raises:
|
|
533
|
+
KeyError: If a required wildcard name is missing in kwargs.
|
|
534
|
+
ValueError: If having subordinate parts that are not exact or wildcards.
|
|
535
|
+
"""
|
|
536
|
+
|
|
537
|
+
def format_map(self, mapping: dict[str, str], internalized: bool = True, strict: bool = False) -> Topic:
|
|
538
|
+
"""Format the topic by replacing named wildcards with provided values from a mapping.
|
|
539
|
+
|
|
540
|
+
Args:
|
|
541
|
+
mapping: Dictionary mapping wildcard names to replacement strings.
|
|
542
|
+
internalized: If True, the returned topic is internalized into the global StrMap and not owning its underlying buffer.
|
|
543
|
+
If False, the returned topic owns its internal buffer.
|
|
544
|
+
strict: If True, raise KeyError if any wildcard name is missing in mapping. Otherwise, leave unmatched wildcards as-is.
|
|
545
|
+
|
|
546
|
+
Returns:
|
|
547
|
+
A new Topic with wildcards replaced by the provided values. If strict, the new Topic is guaranteed to be an exact Topic.
|
|
548
|
+
|
|
549
|
+
Raises:
|
|
550
|
+
KeyError: If a required wildcard name is missing in mapping.
|
|
551
|
+
ValueError: If having subordinate parts that are not exact or wildcards.
|
|
552
|
+
"""
|
|
553
|
+
|
|
554
|
+
@property
|
|
555
|
+
def value(self) -> str:
|
|
556
|
+
"""str: The full topic literal value.
|
|
557
|
+
|
|
558
|
+
Getting returns the current literal. Setting attempts to assign and will raise
|
|
559
|
+
ValueError on syntax errors. Setting mutates the internalized buffer and will
|
|
560
|
+
re-register the topic in the internal mapping.
|
|
561
|
+
"""
|
|
562
|
+
|
|
563
|
+
@value.setter
|
|
564
|
+
def value(self, value: str) -> None:
|
|
565
|
+
"""Set the topic literal value.
|
|
566
|
+
|
|
567
|
+
Args:
|
|
568
|
+
value: New topic string to assign.
|
|
569
|
+
|
|
570
|
+
Raises:
|
|
571
|
+
ValueError: If assignment fails due to syntax error.
|
|
572
|
+
|
|
573
|
+
Notes:
|
|
574
|
+
- Mutating `value` is an expensive operation (de-register / re-register). Avoid frequent calls.
|
|
575
|
+
- Prefer lazy-init: create an empty topic and set `value` once when needed.
|
|
576
|
+
"""
|
|
577
|
+
|
|
578
|
+
@property
|
|
579
|
+
def is_exact(self) -> bool:
|
|
580
|
+
"""bool: True if the topic consists only of exact parts (no wildcards, ranges, or patterns)."""
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
class TopicTestToolkit:
|
|
584
|
+
"""Relays internal C topic state to Python for test validation.
|
|
585
|
+
|
|
586
|
+
Note:
|
|
587
|
+
Test-only class. Not declared in any ``.pxd``; not part of the
|
|
588
|
+
public package API.
|
|
589
|
+
"""
|
|
590
|
+
|
|
591
|
+
@staticmethod
|
|
592
|
+
def get_n_parts(topic: Topic) -> int:
|
|
593
|
+
"""Number of parts in the topic's C part chain."""
|
|
594
|
+
...
|
|
595
|
+
|
|
596
|
+
@staticmethod
|
|
597
|
+
def get_hash(topic: Topic) -> int:
|
|
598
|
+
"""Internalized hash of the topic literal."""
|
|
599
|
+
...
|
|
600
|
+
|
|
601
|
+
@staticmethod
|
|
602
|
+
def get_is_exact(topic: Topic) -> bool:
|
|
603
|
+
"""C-level is_exact flag."""
|
|
604
|
+
...
|
|
605
|
+
|
|
606
|
+
@staticmethod
|
|
607
|
+
def get_key(topic: Topic) -> str:
|
|
608
|
+
"""The internalized key literal stored on the C topic."""
|
|
609
|
+
...
|
|
610
|
+
|
|
611
|
+
@staticmethod
|
|
612
|
+
def get_part_ttype(topic: Topic, idx: int) -> TopicType:
|
|
613
|
+
"""Type of the part at ``idx`` in the C part array."""
|
|
614
|
+
...
|
|
615
|
+
|
|
616
|
+
@staticmethod
|
|
617
|
+
def get_part_chain_len(part: TopicPart) -> int:
|
|
618
|
+
"""Length of the C linked chain starting at ``part`` (inclusive)."""
|
|
619
|
+
...
|
|
620
|
+
|
|
621
|
+
@staticmethod
|
|
622
|
+
def get_part_addr(part: TopicPart) -> int:
|
|
623
|
+
"""Address of the C header backing ``part``."""
|
|
624
|
+
...
|
|
625
|
+
|
|
626
|
+
@staticmethod
|
|
627
|
+
def get_range_options(part: TopicPartRange) -> list[str]:
|
|
628
|
+
"""Option strings of a range part (via the C option array)."""
|
|
629
|
+
...
|
|
630
|
+
|
|
631
|
+
@staticmethod
|
|
632
|
+
def get_range_num_options(part: TopicPartRange) -> int:
|
|
633
|
+
"""Number of options stored on a range part."""
|
|
634
|
+
...
|
|
635
|
+
|
|
636
|
+
@staticmethod
|
|
637
|
+
def get_internal_map_size() -> int:
|
|
638
|
+
"""Number of entries in the global internal topic map."""
|
|
639
|
+
...
|
|
640
|
+
|
|
641
|
+
@staticmethod
|
|
642
|
+
def get_internal_map_has(key: str) -> bool:
|
|
643
|
+
"""Whether the global internal map contains ``key``."""
|
|
644
|
+
...
|