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,818 @@
1
+ import enum
2
+ import re
3
+ from collections.abc import Iterable
4
+
5
+ from cpython.unicode cimport PyUnicode_AsUTF8AndSize, PyUnicode_FromStringAndSize
6
+ from libc.stdint cimport uint8_t, uintptr_t
7
+ from libc.string cimport memcpy, strlen
8
+
9
+ from cbase.allocator_protocol.c_allocator_protocol cimport c_ap_alloc, c_ap_free, c_ap_protocol_from_ptr
10
+ from cbase.bytemap.c_bytemap cimport bytemap_entry, c_bytemap_entry_value, c_bytemap_get, c_bytemap_new
11
+
12
+ from event_engine.base.c_allocator_protocol cimport EE_DEFAULT_ALLOCATOR, EE_HEAP_ALLOCATOR
13
+
14
+
15
+ class TopicType(enum.IntEnum):
16
+ TOPIC_PART_EXACT = evt_topic_type.TOPIC_PART_EXACT
17
+ TOPIC_PART_ANY = evt_topic_type.TOPIC_PART_ANY
18
+ TOPIC_PART_RANGE = evt_topic_type.TOPIC_PART_RANGE
19
+ TOPIC_PART_PATTERN = evt_topic_type.TOPIC_PART_PATTERN
20
+
21
+
22
+ cdef class TopicPart:
23
+ def __cinit__(self, *arg, bint alloc=False, **kwargs):
24
+ if not alloc:
25
+ return
26
+
27
+ self.header = <evt_topic_part_variant*> c_ap_alloc(sizeof(evt_topic_part_variant), EE_HEAP_ALLOCATOR)
28
+ if not self.header:
29
+ raise MemoryError('Failed to allocate memory!')
30
+ self.owner = True
31
+
32
+ def __dealloc__(self):
33
+ if self.owner and self.header:
34
+ c_ap_free(self.header)
35
+ self.header = NULL
36
+
37
+ @staticmethod
38
+ cdef TopicPart c_from_header(evt_topic_part_variant* header, bint owner=False):
39
+ cdef TopicPart instance = TopicPart.__new__(TopicPart, alloc=False)
40
+ instance.header = header
41
+ instance.owner = owner
42
+ return instance
43
+
44
+ cdef object c_cast(self):
45
+ cdef uint8_t ttype = self.header.header.ttype
46
+ cdef object casted
47
+ if ttype == evt_topic_type.TOPIC_PART_EXACT:
48
+ casted = TopicPartExact.__new__(TopicPartExact, alloc=False)
49
+ elif ttype == evt_topic_type.TOPIC_PART_ANY:
50
+ casted = TopicPartAny.__new__(TopicPartAny, alloc=False)
51
+ elif ttype == evt_topic_type.TOPIC_PART_RANGE:
52
+ casted = TopicPartRange.__new__(TopicPartRange, alloc=False)
53
+ elif ttype == evt_topic_type.TOPIC_PART_PATTERN:
54
+ casted = TopicPartPattern.__new__(TopicPartPattern, alloc=False)
55
+ else:
56
+ raise ValueError(f'Invalid ttype {ttype}')
57
+ (<TopicPart> casted).header = self.header
58
+ (<TopicPart> casted).owner = self.owner
59
+ self.owner = False
60
+ return casted
61
+
62
+ # --- Python Interfaces ---
63
+
64
+ def next(self):
65
+ if self.header.header.next:
66
+ return TopicPart.c_from_header(self.header.header.next, False).c_cast()
67
+ raise StopIteration
68
+
69
+ property ttype:
70
+ def __get__(self) -> TopicType:
71
+ if self.header:
72
+ return TopicType(self.header.header.ttype)
73
+ raise RuntimeError('Not initialized!')
74
+
75
+ property addr:
76
+ def __get__(self) -> int:
77
+ if self.header:
78
+ return <uintptr_t> self.header
79
+
80
+
81
+ cdef class TopicPartExact(TopicPart):
82
+ def __cinit__(self, str part=None, *arg, bint alloc=False, **kwargs):
83
+ if not alloc:
84
+ if part:
85
+ raise RuntimeError('Can not assign part string when uninitialized!')
86
+ return
87
+
88
+ if not part:
89
+ return
90
+
91
+ cdef Py_ssize_t length
92
+ cdef const char* key_ptr = PyUnicode_AsUTF8AndSize(part, &length)
93
+ cdef char* part_ptr = <char*> c_ap_alloc(length + 1, EE_HEAP_ALLOCATOR)
94
+ if not part_ptr:
95
+ c_ap_free(self.header)
96
+ self.header = NULL
97
+ raise MemoryError('Failed to allocate memory!')
98
+ memcpy(part_ptr, key_ptr, length)
99
+
100
+ self.header.exact.part = part_ptr
101
+ self.header.exact.part_len = length
102
+ self.header.header.ttype = evt_topic_type.TOPIC_PART_EXACT
103
+
104
+ def __dealloc__(self):
105
+ if self.owner and self.header:
106
+ if self.header.exact.part:
107
+ c_ap_free(self.header.exact.part)
108
+
109
+ def __repr__(self):
110
+ if self.header:
111
+ return f'<TopicPartExact>(topic="{self.part}")'
112
+ return f'<TopicPartExact uninitialized>'
113
+
114
+ def __len__(self):
115
+ return self.header.exact.part_len
116
+
117
+ property part:
118
+ def __get__(self) -> str:
119
+ return PyUnicode_FromStringAndSize(self.header.exact.part, self.header.exact.part_len)
120
+
121
+
122
+ cdef class TopicPartAny(TopicPart):
123
+ def __cinit__(self, str name=None, *arg, bint alloc=False, **kwargs):
124
+ if not alloc:
125
+ if name:
126
+ raise RuntimeError('Can not assign name string when uninitialized!')
127
+ return
128
+
129
+ if not name:
130
+ return
131
+
132
+ cdef Py_ssize_t length
133
+ cdef const char* key_ptr = PyUnicode_AsUTF8AndSize(name, &length)
134
+ cdef char* name_ptr = <char*> c_ap_alloc(length + 1, EE_HEAP_ALLOCATOR)
135
+ if not name_ptr:
136
+ c_ap_free(self.header)
137
+ self.header = NULL
138
+ raise MemoryError('Failed to allocate memory!')
139
+ memcpy(name_ptr, key_ptr, length)
140
+
141
+ self.header.any.name = name_ptr
142
+ self.header.any.name_len = length
143
+ self.header.header.ttype = evt_topic_type.TOPIC_PART_ANY
144
+
145
+ def __dealloc__(self):
146
+ if self.owner and self.header:
147
+ if self.header.any.name:
148
+ c_ap_free(self.header.any.name)
149
+
150
+ def __repr__(self):
151
+ if self.header:
152
+ return f'<TopicPartAny>(name="{self.name}")'
153
+ return f'<TopicPartAny uninitialized>'
154
+
155
+ property name:
156
+ def __get__(self) -> str:
157
+ return PyUnicode_FromStringAndSize(self.header.any.name, self.header.any.name_len)
158
+
159
+
160
+ cdef class TopicPartRange(TopicPart):
161
+ def __cinit__(self, list options=None, *arg, bint alloc=False, **kwargs):
162
+ if not alloc:
163
+ if options:
164
+ raise RuntimeError('Can not assign options when uninitialized!')
165
+ return
166
+
167
+ if not options:
168
+ return
169
+
170
+ cdef size_t n_internal = len(options) - 1 + sum(len(_) for _ in options)
171
+ cdef char* internal = <char*> c_ap_alloc(n_internal + 1, EE_HEAP_ALLOCATOR)
172
+ if not internal:
173
+ c_ap_free(self.header)
174
+ self.header = NULL
175
+ raise MemoryError('Failed to allocate memory!')
176
+
177
+ cdef size_t n_options = len(options)
178
+ cdef char** option_array = <char**> c_ap_alloc(n_options * sizeof(char*), EE_HEAP_ALLOCATOR)
179
+ if not option_array:
180
+ c_ap_free(internal)
181
+ c_ap_free(self.header)
182
+ self.header = NULL
183
+ raise MemoryError('Failed to allocate memory!')
184
+
185
+ cdef str option
186
+ cdef Py_ssize_t option_length
187
+ cdef const char* option_ptr
188
+ cdef size_t start = 0, i = 0
189
+
190
+ for option in options:
191
+ option_ptr = PyUnicode_AsUTF8AndSize(option, &option_length)
192
+ memcpy(<char*> internal + start, <void*> option_ptr, option_length)
193
+ option_array[i] = <char*> internal + start
194
+ if not option_array[i]:
195
+ c_ap_free(internal)
196
+ c_ap_free(option_array)
197
+ c_ap_free(self.header)
198
+ self.header = NULL
199
+ raise MemoryError('Failed to allocate memory!')
200
+ i += 1
201
+ start += option_length + 1
202
+
203
+ self.header.range.options = option_array
204
+ self.header.range.num_options = n_options
205
+ self.header.range.literal = internal
206
+ self.header.range.literal_len = n_internal
207
+ self.header.header.ttype = evt_topic_type.TOPIC_PART_RANGE
208
+
209
+ def __dealloc__(self):
210
+ if self.owner and self.header:
211
+ if self.header.range.options:
212
+ c_ap_free(self.header.range.options)
213
+ if self.header.range.literal:
214
+ c_ap_free(self.header.range.literal)
215
+
216
+ def __repr__(self):
217
+ if self.header:
218
+ return f'<TopicPartRange>(n={self.header.range.num_options}, options={str(list(self.options()))})'
219
+ return f'<TopicPartRange uninitialized>'
220
+
221
+ def __len__(self):
222
+ return self.header.range.num_options
223
+
224
+ def __iter__(self):
225
+ return self.options()
226
+
227
+ def options(self):
228
+ if not self.header:
229
+ raise RuntimeError('Not initialized!')
230
+
231
+ cdef char** option_array = self.header.range.options
232
+ cdef size_t num_options = self.header.range.num_options
233
+ cdef size_t i
234
+ cdef char* option_ptr
235
+ cdef size_t option_length
236
+
237
+ for i in range(num_options):
238
+ option_ptr = option_array[i]
239
+ option_length = strlen(option_ptr)
240
+ yield PyUnicode_FromStringAndSize(option_ptr, option_length)
241
+
242
+
243
+ cdef class TopicPartPattern(TopicPart):
244
+ def __cinit__(self, str regex=None, *arg, bint alloc=False, **kwargs):
245
+ if not alloc:
246
+ if regex:
247
+ raise RuntimeError('Can not assign regex string when uninitialized!')
248
+ return
249
+
250
+ if not regex:
251
+ return
252
+
253
+ cdef Py_ssize_t length
254
+ cdef const char* key_ptr = PyUnicode_AsUTF8AndSize(regex, &length)
255
+ cdef char* regex_ptr = <char*> c_ap_alloc(length + 1, EE_HEAP_ALLOCATOR)
256
+ if not regex_ptr:
257
+ c_ap_free(self.header)
258
+ self.header = NULL
259
+ raise MemoryError('Failed to allocate memory!')
260
+ memcpy(regex_ptr, key_ptr, length)
261
+
262
+ self.header.pattern.pattern = regex_ptr
263
+ self.header.pattern.pattern_len = length
264
+ self.header.header.ttype = evt_topic_type.TOPIC_PART_PATTERN
265
+
266
+ def __dealloc__(self):
267
+ if self.owner and self.header:
268
+ if self.header.pattern.pattern:
269
+ c_ap_free(self.header.pattern.pattern)
270
+
271
+ def __repr__(self):
272
+ if self.header:
273
+ return f'<TopicPartPattern>(regex="{self.pattern}")'
274
+ return f'<TopicPartPattern uninitialized>'
275
+
276
+ property pattern:
277
+ def __get__(self) -> str:
278
+ return PyUnicode_FromStringAndSize(self.header.pattern.pattern, self.header.pattern.pattern_len)
279
+
280
+ property regex:
281
+ def __get__(self):
282
+ return re.compile(self.pattern)
283
+
284
+
285
+ cdef allocator_protocol* TOPIC_ALLOCATOR = EE_DEFAULT_ALLOCATOR
286
+
287
+ # Module init: set up the global internal map using the default allocator
288
+ GLOBAL_INTERNAL_MAP = c_bytemap_new(0, EE_DEFAULT_ALLOCATOR)
289
+
290
+
291
+ cpdef Topic get_internal_topic(str key, bint owner=False):
292
+ global GLOBAL_INTERNAL_MAP
293
+ if not GLOBAL_INTERNAL_MAP:
294
+ raise RuntimeError('Internal map not initialized!')
295
+
296
+ cdef Py_ssize_t key_length
297
+ cdef const char* key_ptr = PyUnicode_AsUTF8AndSize(key, &key_length)
298
+ cdef evt_topic* topic = NULL
299
+ cdef int ret_code = c_bytemap_get(GLOBAL_INTERNAL_MAP, key_ptr, key_length, <void**> &topic)
300
+ if topic is NULL:
301
+ return None
302
+ return Topic.c_from_header(topic, owner)
303
+
304
+
305
+ cpdef dict get_internal_map():
306
+ global GLOBAL_INTERNAL_MAP
307
+ if not GLOBAL_INTERNAL_MAP:
308
+ raise RuntimeError('Internal map not initialized!')
309
+
310
+ cdef str key
311
+ cdef evt_topic* topic
312
+ cdef bytemap_entry* entry = GLOBAL_INTERNAL_MAP.first
313
+ cdef dict out = {}
314
+ while entry:
315
+ key = PyUnicode_FromStringAndSize(entry.key, entry.key_length)
316
+ topic = <evt_topic*> c_bytemap_entry_value(entry)
317
+ out[key] = Topic.c_from_header(topic, False)
318
+ entry = entry.next
319
+ return out
320
+
321
+
322
+ cdef class TopicMatchResult:
323
+ def __cinit__(self, size_t n_parts=0, bint alloc=False, **kwargs):
324
+ if not alloc:
325
+ if n_parts:
326
+ raise RuntimeError('Can not allocator buffer when uninitialized!')
327
+ return
328
+
329
+ cdef size_t i
330
+ cdef evt_topic_match* node = NULL
331
+ for i in range(n_parts):
332
+ node = c_topic_match_new(node, EE_DEFAULT_ALLOCATOR)
333
+ if not node:
334
+ c_topic_match_free(self.header)
335
+ elif not self.header:
336
+ self.header = node
337
+ self.owner = True
338
+
339
+ def __dealloc__(self):
340
+ if self.owner and self.header:
341
+ c_topic_match_free(self.header)
342
+
343
+ @staticmethod
344
+ cdef TopicMatchResult c_from_header(evt_topic_match* node, bint owner=True):
345
+ cdef TopicMatchResult instance = TopicMatchResult.__new__(TopicMatchResult, alloc=False)
346
+ instance.header = node
347
+ instance.owner = owner
348
+ return instance
349
+
350
+ @staticmethod
351
+ cdef dict c_match_res(evt_topic_match* node):
352
+ if not node:
353
+ raise RuntimeError('Not initialized!')
354
+ cdef dict info = {
355
+ 'matched': node.matched,
356
+ 'part_a': TopicPart.c_from_header(node.part_a, False).c_cast() if node.part_a else None,
357
+ 'part_b': TopicPart.c_from_header(node.part_b, False).c_cast() if node.part_b else None,
358
+ 'literal': PyUnicode_FromStringAndSize(node.literal, node.literal_len) if node.literal else None,
359
+ }
360
+ return info
361
+
362
+ def __repr__(self):
363
+ return f'<evt_topic_match {"success" if self.matched else "failed"}>(nodes={self.length})'
364
+
365
+ def __bool__(self):
366
+ return self.matched
367
+
368
+ def __len__(self):
369
+ return self.length
370
+
371
+ def __getitem__(self, ssize_t idx):
372
+ cdef ssize_t length = self.length
373
+ if idx < -length:
374
+ raise IndexError(f'Index {idx} out of range!')
375
+ elif idx < 0:
376
+ idx += length
377
+ elif idx >= length:
378
+ raise IndexError(f'Index {idx} out of range!')
379
+
380
+ cdef ssize_t i = 0
381
+ cdef evt_topic_match* node = self.header
382
+ while i < idx:
383
+ i += 1
384
+ node = node.next
385
+
386
+ return TopicMatchResult.c_match_res(node)
387
+
388
+ def __iter__(self):
389
+ cdef evt_topic_match* node = self.header
390
+ while node:
391
+ yield TopicMatchResult.c_match_res(node)
392
+ node = node.next
393
+
394
+ def to_dict(self):
395
+ cdef dict out = {}
396
+ cdef evt_topic_match* node = self.header
397
+ cdef str literal
398
+
399
+ while node:
400
+ literal = PyUnicode_FromStringAndSize(node.literal, node.literal_len)
401
+ out[literal] = TopicPart.c_from_header(node.part_b, False).c_cast()
402
+ node = node.next
403
+ return out
404
+
405
+ property length:
406
+ def __get__(self) -> size_t:
407
+ if not self.header:
408
+ raise RuntimeError('Not initialized!')
409
+ cdef size_t i = 0
410
+ cdef evt_topic_match* node = self.header
411
+ while node:
412
+ i += 1
413
+ node = node.next
414
+ return i
415
+
416
+ property matched:
417
+ def __get__(self) -> bint:
418
+ if not self.header:
419
+ raise RuntimeError('Not initialized!')
420
+
421
+ cdef evt_topic_match* node = self.header
422
+ while node:
423
+ if not node.matched:
424
+ return False
425
+ node = node.next
426
+ return True
427
+
428
+
429
+ cdef class Topic:
430
+ def __cinit__(self, str topic=None, *arg, bint alloc=True, **kwargs):
431
+ if not alloc:
432
+ if topic:
433
+ raise RuntimeError('Can not assign topic string when uninitialized!')
434
+ return
435
+
436
+ if not topic:
437
+ self.header = c_topic_new(NULL, 0, EE_DEFAULT_ALLOCATOR)
438
+ self.owner = True
439
+ if not self.header:
440
+ raise MemoryError(f'Failed to init topic ":{topic}", check if the syntax is correct!')
441
+ return
442
+
443
+ cdef Py_ssize_t topic_length
444
+ cdef const char* topic_ptr = PyUnicode_AsUTF8AndSize(topic, &topic_length)
445
+ self.header = c_topic_new(topic_ptr, topic_length, EE_DEFAULT_ALLOCATOR)
446
+ if not self.header:
447
+ raise MemoryError('Failed to allocate memory!')
448
+
449
+ def __dealloc__(self):
450
+ if self.owner and self.header:
451
+ c_topic_free(self.header)
452
+
453
+ @staticmethod
454
+ cdef Topic c_from_header(evt_topic* header, bint owner=False):
455
+ cdef Topic instance = Topic.__new__(Topic, alloc=False)
456
+ instance.header = header
457
+ instance.owner = owner
458
+ return instance
459
+
460
+ cdef void c_append(self, evt_topic_part_variant* tpart):
461
+ cdef evt_topic_type ttype = tpart.header.ttype
462
+ if ttype == evt_topic_type.TOPIC_PART_EXACT:
463
+ literal = tpart.exact.part
464
+ literal_len = tpart.exact.part_len
465
+ elif ttype == evt_topic_type.TOPIC_PART_ANY:
466
+ literal = tpart.any.name
467
+ literal_len = tpart.any.name_len
468
+ elif ttype == evt_topic_type.TOPIC_PART_RANGE:
469
+ literal = tpart.range.literal
470
+ literal_len = tpart.range.literal_len
471
+ elif ttype == evt_topic_type.TOPIC_PART_PATTERN:
472
+ literal = tpart.pattern.pattern
473
+ literal_len = tpart.pattern.pattern_len
474
+ else:
475
+ raise RuntimeError(f'Unknown topic type {TopicType(ttype)}')
476
+ c_topic_append(self.header, literal, literal_len, ttype)
477
+
478
+ cdef void c_update_literal(self):
479
+ c_topic_update_literal(self.header)
480
+
481
+ # --- Python Interface ---
482
+
483
+ def __len__(self):
484
+ if not self.header:
485
+ raise RuntimeError('Not initialized!')
486
+ return self.header.n
487
+
488
+ def __bool__(self):
489
+ if not self.header:
490
+ return False
491
+ return self.header.n > 0
492
+
493
+ def __hash__(self):
494
+ if not self.header:
495
+ raise RuntimeError('Not initialized!')
496
+ return self.header.hash
497
+
498
+ def __eq__(self, Topic other):
499
+ if not self.header or not other.header:
500
+ raise RuntimeError('Not initialized!')
501
+ return self.value == other.value
502
+
503
+ def __repr__(self):
504
+ if not self.header:
505
+ return f'<{self.__class__.__name__} uninitialized>'
506
+ return f'<{self.__class__.__name__} {"Exact" if self.header.is_exact else "Generic"}>(value="{self.value}", n_parts={self.header.n})'
507
+
508
+ def __str__(self):
509
+ if not self.header:
510
+ raise RuntimeError('Not initialized!')
511
+ return self.value
512
+
513
+ def __iter__(self):
514
+ if not self.header:
515
+ raise RuntimeError('Not initialized!')
516
+
517
+ cdef evt_topic_part_variant* part = self.header.parts
518
+ while part:
519
+ yield TopicPart.c_from_header(part, False).c_cast()
520
+ part = part.header.next
521
+
522
+ def __getitem__(self, ssize_t idx):
523
+ if not self.header:
524
+ raise RuntimeError('Not initialized!')
525
+
526
+ cdef ssize_t n_parts = self.header.n
527
+ if idx < -n_parts:
528
+ raise IndexError(f'Index {idx} out of range!')
529
+ elif idx < 0:
530
+ idx += n_parts
531
+ elif idx >= n_parts:
532
+ raise IndexError(f'Index {idx} out of range!')
533
+
534
+ cdef ssize_t i = 0
535
+ cdef evt_topic_part_variant* part = self.header.parts
536
+
537
+ while part:
538
+ if i == idx:
539
+ return TopicPart.c_from_header(part, False).c_cast()
540
+ i += 1
541
+ part = part.header.next
542
+ raise IndexError('i')
543
+
544
+ def __add__(self, object topic):
545
+ if not self.header:
546
+ raise RuntimeError('Not initialized!')
547
+
548
+ cdef Topic aggregated = Topic.__new__(Topic)
549
+ cdef evt_topic_part_variant* other_part
550
+ cdef evt_topic_part_variant* node = self.header.parts
551
+ cdef size_t i, n
552
+ if isinstance(topic, Topic):
553
+ other_part = (<Topic> topic).header.parts
554
+ while node:
555
+ aggregated.c_append(node)
556
+ node = node.header.next
557
+ node = other_part
558
+ while node:
559
+ aggregated.c_append(node)
560
+ node = node.header.next
561
+ elif isinstance(topic, TopicPart):
562
+ other_part = (<TopicPart> topic).header
563
+ node = self.header.parts
564
+ while node:
565
+ aggregated.c_append(node)
566
+ node = node.header.next
567
+ aggregated.c_append(other_part)
568
+ else:
569
+ raise TypeError(f'Can not add {topic} to {self}, expected ether a {Topic} or {TopicPart}')
570
+ aggregated.c_update_literal()
571
+ return aggregated
572
+
573
+ def __iadd__(self, object topic):
574
+ if not self.header:
575
+ raise RuntimeError('Not initialized!')
576
+
577
+ cdef evt_topic_part_variant* node
578
+ if isinstance(topic, Topic):
579
+ node = (<Topic> topic).header.parts
580
+ while node:
581
+ self.c_append(node)
582
+ node = node.header.next
583
+ elif isinstance(topic, TopicPart):
584
+ self.c_append((<TopicPart> topic).header)
585
+ else:
586
+ raise TypeError(f'Can not add {topic} to {self}, expected ether a {Topic} or {TopicPart}')
587
+ self.c_update_literal()
588
+ return self
589
+
590
+ def __call__(self, **kwargs):
591
+ return self.format_map(kwargs, internalized=True, strict=False)
592
+
593
+ @classmethod
594
+ def from_parts(cls, topic_parts: Iterable[TopicPart]) -> Topic:
595
+ cdef Topic aggregated = Topic.__new__(Topic)
596
+ cdef TopicPart tpart
597
+ for tpart in topic_parts:
598
+ aggregated.c_append(tpart.header)
599
+ aggregated.c_update_literal()
600
+ return aggregated
601
+
602
+ @classmethod
603
+ def join(cls, topic_parts: Iterable[str]) -> Topic:
604
+ cdef Topic aggregated = Topic.__new__(Topic)
605
+ cdef str part
606
+ cdef Py_ssize_t literal_len
607
+ cdef const char* literal
608
+ cdef evt_topic* topic = aggregated.header
609
+
610
+ for tpart in topic_parts:
611
+ literal = PyUnicode_AsUTF8AndSize(tpart, &literal_len)
612
+ c_topic_append(topic, literal, literal_len, evt_topic_type.TOPIC_PART_EXACT)
613
+ c_topic_update_literal(topic)
614
+ return aggregated
615
+
616
+ cpdef Topic append(self, TopicPart topic_part):
617
+ if not self.header or not topic_part.header:
618
+ raise RuntimeError('Not initialized!')
619
+
620
+ cdef evt_topic_part_variant* part = topic_part.header
621
+ cdef evt_topic* topic = self.header
622
+ cdef evt_topic_part_variant* curr = topic.parts
623
+ cdef allocator_protocol* allocator = c_ap_protocol_from_ptr(self.header)
624
+ cdef char* literal
625
+ cdef size_t literal_len
626
+
627
+ # shortcut: just appending the buffer if it is owned and topic not using allocator.
628
+ if topic_part.owner and not allocator:
629
+ topic_part.owner = False
630
+ part = topic_part.header
631
+ part.header.next = NULL
632
+
633
+ if not curr:
634
+ topic.parts = part
635
+ else:
636
+ while curr.header.next:
637
+ curr = curr.header.next
638
+ curr.header.next = part
639
+ topic.n += 1
640
+ if part.header.ttype != evt_topic_type.TOPIC_PART_EXACT:
641
+ self.header.is_exact = 0
642
+ else:
643
+ self.c_append(part)
644
+ self.c_update_literal()
645
+ return self
646
+
647
+ cpdef TopicMatchResult match(self, Topic other):
648
+ if not self.header:
649
+ raise RuntimeError('Not initialized!')
650
+
651
+ cdef evt_topic_match* match_res = c_topic_match(self.header, other.header, NULL)
652
+ return TopicMatchResult.c_from_header(match_res, True)
653
+
654
+ def update_literal(self) -> Topic:
655
+ self.c_update_literal()
656
+ return self
657
+
658
+ cpdef Topic format_map(self, dict mapping, bint internalized=True, bint strict=False):
659
+ cdef evt_topic_part_variant* tpart = self.header.parts
660
+ cdef evt_topic_type ttype
661
+ cdef evt_topic* formatted = c_topic_new(NULL, 0, c_ap_protocol_from_ptr(self.header))
662
+ cdef str key
663
+ cdef const char* literal
664
+ cdef Py_ssize_t literal_len
665
+
666
+ while tpart:
667
+ ttype = tpart.header.ttype
668
+ # Case 1: For an exact part, simply append it.
669
+ if ttype == evt_topic_type.TOPIC_PART_EXACT:
670
+ c_topic_append(formatted, tpart.exact.part, tpart.exact.part_len, ttype)
671
+ # Case 2: For a named any part, check from the mapping dict.
672
+ elif ttype == evt_topic_type.TOPIC_PART_ANY:
673
+ key = PyUnicode_FromStringAndSize(tpart.any.name, tpart.any.name_len)
674
+ # Raise KeyError if not found.
675
+ if key not in mapping:
676
+ if strict:
677
+ c_topic_free(formatted)
678
+ raise KeyError(key)
679
+ else:
680
+ c_topic_append(formatted, tpart.any.name, tpart.any.name_len, evt_topic_type.TOPIC_PART_ANY)
681
+ else:
682
+ # Append the mapped value as an exact part.
683
+ literal = PyUnicode_AsUTF8AndSize(mapping[key], &literal_len)
684
+ c_topic_append(formatted, literal, literal_len, evt_topic_type.TOPIC_PART_EXACT)
685
+ else:
686
+ c_topic_free(formatted)
687
+ raise ValueError(f'Not supported topic type {TopicType(ttype)}')
688
+ tpart = tpart.header.next
689
+
690
+ c_topic_update_literal(formatted)
691
+ # If internalized, the TopicPart will not own the literal buffers.
692
+ # Requested or not, in any way, the topic literal must be added to the internal map.
693
+ return Topic.c_from_header(formatted, not internalized)
694
+
695
+ def format(self, **kwargs) -> Topic:
696
+ return self.format_map(kwargs, internalized=True, strict=False)
697
+
698
+ property value:
699
+ def __get__(self):
700
+ if not self.header:
701
+ raise RuntimeError('Not initialized!')
702
+
703
+ cdef str literal = PyUnicode_FromStringAndSize(self.header.key, self.header.key_len)
704
+ return literal
705
+
706
+ def __set__(self, str value):
707
+ if not self.header:
708
+ raise RuntimeError('Not initialized!')
709
+
710
+ cdef Py_ssize_t topic_length
711
+ cdef const char* topic_ptr = PyUnicode_AsUTF8AndSize(value, &topic_length)
712
+ cdef int assign_ret = c_topic_assign(self.header, topic_ptr, topic_length)
713
+ if assign_ret:
714
+ raise ValueError(f'Failed to assign topic "{value}", check if syntax is correct!')
715
+
716
+ property is_exact:
717
+ def __get__(self) -> bint:
718
+ if not self.header:
719
+ raise RuntimeError('Not initialized!')
720
+ return self.header.is_exact
721
+
722
+ property addr:
723
+ def __get__(self) -> uintptr_t:
724
+ if self.header:
725
+ return <uintptr_t> self.header
726
+ return 0
727
+
728
+
729
+ cdef class TopicTestToolkit:
730
+ """Relays internal C topic state to Python for test validation.
731
+
732
+ The toolkit exposes internal ``evt_topic`` / ``evt_topic_part_variant``
733
+ fields that are deliberately not part of the public Python API, so that
734
+ unit tests can verify the C-level layout (part chain length, keys, hash,
735
+ internal map bookkeeping) without relying on public properties alone.
736
+
737
+ Note:
738
+ Test-only class. Not declared in any ``.pxd``; not part of the
739
+ public package API.
740
+ """
741
+
742
+ @staticmethod
743
+ def get_n_parts(Topic topic) -> size_t:
744
+ """Number of parts in the topic's C part chain."""
745
+ return topic.header.n
746
+
747
+ @staticmethod
748
+ def get_hash(Topic topic) -> uint64_t:
749
+ """Internalized hash of the topic literal."""
750
+ return topic.header.hash
751
+
752
+ @staticmethod
753
+ def get_is_exact(Topic topic) -> bint:
754
+ """C-level is_exact flag."""
755
+ return topic.header.is_exact
756
+
757
+ @staticmethod
758
+ def get_key(Topic topic) -> str:
759
+ """The internalized key literal stored on the C topic."""
760
+ if topic.header.key:
761
+ return PyUnicode_FromStringAndSize(topic.header.key, topic.header.key_len)
762
+ return ''
763
+
764
+ @staticmethod
765
+ def get_part_ttype(Topic topic, size_t idx) -> TopicType:
766
+ """Type of the part at ``idx`` in the C part linked list."""
767
+ cdef evt_topic_part_variant* node = topic.header.parts
768
+ cdef size_t i = 0
769
+ while node:
770
+ if i == idx:
771
+ return TopicType(node.header.ttype)
772
+ i += 1
773
+ node = node.header.next
774
+ raise IndexError(f'Index {idx} out of range!')
775
+
776
+ @staticmethod
777
+ def get_part_chain_len(TopicPart part) -> size_t:
778
+ """Length of the C linked chain starting at ``part`` (inclusive)."""
779
+ cdef evt_topic_part_variant* node = part.header
780
+ cdef size_t n = 0
781
+ while node:
782
+ n += 1
783
+ node = node.header.next
784
+ return n
785
+
786
+ @staticmethod
787
+ def get_part_addr(TopicPart part) -> uintptr_t:
788
+ """Address of the C header backing ``part``."""
789
+ return <uintptr_t> part.header
790
+
791
+ @staticmethod
792
+ def get_range_options(TopicPartRange part) -> list:
793
+ """Option strings of a range part (via the C option array)."""
794
+ return list(part.options())
795
+
796
+ @staticmethod
797
+ def get_range_num_options(TopicPartRange part) -> size_t:
798
+ """Number of options stored on a range part."""
799
+ return part.header.range.num_options
800
+
801
+ @staticmethod
802
+ def get_internal_map_size() -> size_t:
803
+ """Number of entries in the global internal topic map."""
804
+ cdef bytemap_entry* entry = GLOBAL_INTERNAL_MAP.first
805
+ cdef size_t n = 0
806
+ while entry:
807
+ n += 1
808
+ entry = entry.next
809
+ return n
810
+
811
+ @staticmethod
812
+ def get_internal_map_has(str key) -> bint:
813
+ """Whether the global internal map contains ``key``."""
814
+ cdef Py_ssize_t key_length
815
+ cdef const char* key_ptr = PyUnicode_AsUTF8AndSize(key, &key_length)
816
+ cdef evt_topic* topic = NULL
817
+ cdef int ret_code = c_bytemap_get(GLOBAL_INTERNAL_MAP, key_ptr, key_length, <void**> &topic)
818
+ return topic != NULL