eventsourcing 9.5.0b3__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,608 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import threading
5
+ from unittest import TestCase
6
+ from uuid import uuid4
7
+
8
+ from eventsourcing.dcb.api import (
9
+ DCBAppendCondition,
10
+ DCBEvent,
11
+ DCBQuery,
12
+ DCBQueryItem,
13
+ DCBRecorder,
14
+ DCBSequencedEvent,
15
+ DCBSubscription,
16
+ TDCBRecorder_co,
17
+ )
18
+ from eventsourcing.persistence import IntegrityError
19
+
20
+
21
+ class DCBRecorderTestCase(TestCase):
22
+
23
+ def _test_append_read(
24
+ self, recorder: DCBRecorder, initial_position: int = 0
25
+ ) -> None:
26
+ # Read all, expect no results.
27
+ read_response = recorder.read()
28
+ result = list(read_response)
29
+ self.assertEqual(initial_position, len(list(result)))
30
+ self.assertEqual(initial_position, read_response.head or 0)
31
+
32
+ # Append one event.
33
+ event1 = DCBEvent(type="type1", data=b"data1", tags=["tagX"])
34
+ position = recorder.append(events=[event1])
35
+
36
+ # Check the returned position is 1.
37
+ self.assertEqual(1 + initial_position, position)
38
+
39
+ # Read all, expect one event.
40
+ read_response = recorder.read(after=initial_position)
41
+ result = list(read_response)
42
+ self.assertEqual(1, len(result))
43
+ self.assertEqual(event1.data, result[0].event.data)
44
+ self.assertEqual(1 + initial_position, read_response.head)
45
+
46
+ # Read all after 1, expect no events.
47
+ read_response = recorder.read(after=1 + initial_position)
48
+ result = list(read_response)
49
+ self.assertEqual(0, len(result))
50
+ self.assertEqual(1 + initial_position, read_response.head)
51
+
52
+ # Read all limit 1, expect one event.
53
+ read_response = recorder.read(after=initial_position, limit=1)
54
+ result = list(read_response)
55
+ self.assertEqual(1, len(result))
56
+ self.assertEqual(event1.data, result[0].event.data)
57
+ self.assertEqual(1 + initial_position, read_response.head)
58
+
59
+ # Read all limit 0, expect no events (and read_response.head is None).
60
+ read_response = recorder.read(limit=0)
61
+ result = list(read_response)
62
+ self.assertEqual(0, len(result))
63
+ self.assertEqual(None, read_response.head)
64
+
65
+ # Read events with type1, expect 1 event.
66
+ query_type1 = DCBQuery(items=[DCBQueryItem(types=["type1"])])
67
+ read_response = recorder.read(query_type1, after=initial_position)
68
+ result = list(read_response)
69
+ self.assertEqual(1, len(result))
70
+ self.assertEqual(event1.data, result[0].event.data)
71
+ self.assertEqual(1 + initial_position, read_response.head)
72
+
73
+ # Read events with type2, expect no events.
74
+ query_type2 = DCBQuery(items=[DCBQueryItem(types=["type2"])])
75
+ read_response = recorder.read(query_type2, after=initial_position)
76
+ result = list(read_response)
77
+ self.assertEqual(0, len(result))
78
+ self.assertEqual(1 + initial_position, read_response.head)
79
+
80
+ # Read events with tagX, expect one event.
81
+ query_tag_x = DCBQuery(items=[DCBQueryItem(tags=["tagX"])])
82
+ read_response = recorder.read(query_tag_x, after=initial_position)
83
+ result = list(read_response)
84
+ self.assertEqual(1, len(result))
85
+ self.assertEqual(event1.data, result[0].event.data)
86
+ self.assertEqual(1 + initial_position, read_response.head)
87
+
88
+ # Read events with tagY, expect no events.
89
+ query_tag_y = DCBQuery(items=[DCBQueryItem(tags=["tagY"])])
90
+ read_response = recorder.read(query=query_tag_y, after=initial_position)
91
+ result = list(read_response)
92
+ self.assertEqual(0, len(result), result)
93
+ self.assertEqual(1 + initial_position, read_response.head)
94
+
95
+ # Read events with type1 and tagX, expect one event.
96
+ query_type1_tag_x = DCBQuery(
97
+ items=[DCBQueryItem(types=["type1"], tags=["tagX"])]
98
+ )
99
+ read_response = recorder.read(query_type1_tag_x, after=initial_position)
100
+ result = list(read_response)
101
+ self.assertEqual(1, len(result))
102
+ self.assertEqual(1 + initial_position, read_response.head)
103
+
104
+ # Read events with type1 and tagY, expect no events.
105
+ query_type1_tag_y = DCBQuery(
106
+ items=[DCBQueryItem(types=["type1"], tags=["tagY"])]
107
+ )
108
+ read_response = recorder.read(query_type1_tag_y, after=initial_position)
109
+ result = list(read_response)
110
+ self.assertEqual(0, len(result))
111
+ self.assertEqual(1 + initial_position, read_response.head)
112
+
113
+ # Read events with type2 and tagX, expect no events.
114
+ query_type2_tag_x = DCBQuery(
115
+ items=[DCBQueryItem(types=["type2"], tags=["tagX"])]
116
+ )
117
+ read_response = recorder.read(query_type2_tag_x, after=initial_position)
118
+ result = list(read_response)
119
+ self.assertEqual(0, len(result))
120
+ self.assertEqual(1 + initial_position, read_response.head)
121
+
122
+ # Append two more events.
123
+ event2 = DCBEvent(type="type2", data=b"data2", tags=["tagA", "tagB"])
124
+ event3 = DCBEvent(type="type3", data=b"data3", tags=["tagA", "tagC"])
125
+ position = recorder.append(events=[event2, event3])
126
+
127
+ # Check the returned position is 3
128
+ self.assertEqual(3 + initial_position, position)
129
+
130
+ # Read all, expect 3 events (in ascending order).
131
+ read_response = recorder.read(after=initial_position)
132
+ result = list(read_response)
133
+ self.assertEqual(3, len(result))
134
+ self.assertEqual(event1.data, result[0].event.data)
135
+ self.assertEqual(event2.data, result[1].event.data)
136
+ self.assertEqual(event3.data, result[2].event.data)
137
+ self.assertEqual(3 + initial_position, read_response.head)
138
+
139
+ # Read all after 1, expect two events.
140
+ read_response = recorder.read(after=1 + initial_position)
141
+ result = list(read_response)
142
+ self.assertEqual(2, len(result))
143
+ self.assertEqual(event2.data, result[0].event.data)
144
+ self.assertEqual(event3.data, result[1].event.data)
145
+ self.assertEqual(3 + initial_position, read_response.head)
146
+
147
+ # Read all after 2, expect one event.
148
+ read_response = recorder.read(after=2 + initial_position)
149
+ result = list(read_response)
150
+ self.assertEqual(1, len(result))
151
+ self.assertEqual(event3.data, result[0].event.data)
152
+ self.assertEqual(3 + initial_position, read_response.head)
153
+
154
+ # Read all after 1, limit 1, expect one event.
155
+ read_response = recorder.read(after=1 + initial_position, limit=1)
156
+ result = list(read_response)
157
+ self.assertEqual(1, len(result))
158
+ self.assertEqual(event2.data, result[0].event.data)
159
+ self.assertEqual(2 + initial_position, read_response.head)
160
+
161
+ # Read type1 after 1, expect no events.
162
+ read_response = recorder.read(query_type1, after=1 + initial_position)
163
+ result = list(read_response)
164
+ self.assertEqual(0, len(result))
165
+ self.assertEqual(3 + initial_position, read_response.head)
166
+
167
+ # Read tagX after 1, expect no events.
168
+ read_response = recorder.read(query_tag_x, after=1 + initial_position)
169
+ result = list(read_response)
170
+ self.assertEqual(0, len(result))
171
+ self.assertEqual(3 + initial_position, read_response.head)
172
+
173
+ # Read type1 and tagX after 1, expect no events.
174
+ read_response = recorder.read(query_type1_tag_x, after=1 + initial_position)
175
+ result = list(read_response)
176
+ self.assertEqual(0, len(result))
177
+ self.assertEqual(3 + initial_position, read_response.head)
178
+
179
+ # Read events with tagA, expect two events.
180
+ query_tag_a = DCBQuery(items=[DCBQueryItem(tags=["tagA"])])
181
+ read_response = recorder.read(query_tag_a, after=initial_position)
182
+ result = list(read_response)
183
+ self.assertEqual(2, len(result))
184
+ self.assertEqual(event2.data, result[0].event.data)
185
+ self.assertEqual(event3.data, result[1].event.data)
186
+ self.assertEqual(3 + initial_position, read_response.head)
187
+
188
+ # Read events with tagA and tagB, expect one event.
189
+ query_tag_a_and_b = DCBQuery(items=[DCBQueryItem(tags=["tagA", "tagB"])])
190
+ read_response = recorder.read(query_tag_a_and_b, after=initial_position)
191
+ result = list(read_response)
192
+ self.assertEqual(1, len(result))
193
+ self.assertEqual(event2.data, result[0].event.data)
194
+ self.assertEqual(3 + initial_position, read_response.head)
195
+
196
+ # Read events with tagB or tagC, expect two events.
197
+ query_tag_b_or_c = DCBQuery(
198
+ items=[
199
+ DCBQueryItem(tags=["tagB"]),
200
+ DCBQueryItem(tags=["tagC"]),
201
+ ]
202
+ )
203
+ read_response = recorder.read(query_tag_b_or_c, after=initial_position)
204
+ result = list(read_response)
205
+ self.assertEqual(2, len(result))
206
+ self.assertEqual(event2.data, result[0].event.data)
207
+ self.assertEqual(event3.data, result[1].event.data)
208
+ self.assertEqual(3 + initial_position, read_response.head)
209
+
210
+ # Read events with tagX or tagY, expect one event.
211
+ query_tag_x_or_y = DCBQuery(
212
+ items=[
213
+ DCBQueryItem(tags=["tagX"]),
214
+ DCBQueryItem(tags=["tagY"]),
215
+ ]
216
+ )
217
+ read_response = recorder.read(query_tag_x_or_y, after=initial_position)
218
+ result = list(read_response)
219
+ self.assertEqual(1, len(result))
220
+ self.assertEqual(event1.data, result[0].event.data)
221
+ self.assertEqual(3 + initial_position, read_response.head)
222
+
223
+ # Read events with type2 and tagA, expect one event.
224
+ query_type2_tag_a = DCBQuery(
225
+ items=[DCBQueryItem(types=["type2"], tags=["tagA"])]
226
+ )
227
+ read_response = recorder.read(query_type2_tag_a, after=initial_position)
228
+ result = list(read_response)
229
+ self.assertEqual(1, len(result))
230
+ self.assertEqual(event2.data, result[0].event.data)
231
+ self.assertEqual(3 + initial_position, read_response.head)
232
+
233
+ # Read events with type2 and tagA after 2, expect no events.
234
+ query_type2_tag_a = DCBQuery(
235
+ items=[DCBQueryItem(types=["type2"], tags=["tagA"])]
236
+ )
237
+ read_response = recorder.read(query_type2_tag_a, after=2 + initial_position)
238
+ result = list(read_response)
239
+ self.assertEqual(0, len(result))
240
+ self.assertEqual(3 + initial_position, read_response.head)
241
+
242
+ # Read events with type2 and tagA, expect one event.
243
+ query_type2_tag_a = DCBQuery(
244
+ items=[DCBQueryItem(types=["type2"], tags=["tagA"])]
245
+ )
246
+ read_response = recorder.read(query_type2_tag_a, after=initial_position)
247
+ result = list(read_response)
248
+ self.assertEqual(1, len(result))
249
+ self.assertEqual(event2.data, result[0].event.data)
250
+ self.assertEqual(3 + initial_position, read_response.head)
251
+
252
+ # Read events with type2 and tagB, or with type3 and tagC, expect two events.
253
+ query_type2_tag_b_or_type3_tagc = DCBQuery(
254
+ items=[
255
+ DCBQueryItem(types=["type2"], tags=["tagB"]),
256
+ DCBQueryItem(types=["type3"], tags=["tagC"]),
257
+ ]
258
+ )
259
+ read_response = recorder.read(
260
+ query_type2_tag_b_or_type3_tagc, after=initial_position
261
+ )
262
+ result = list(read_response)
263
+ self.assertEqual(2, len(result), result)
264
+ self.assertEqual(event2.data, result[0].event.data)
265
+ self.assertEqual(event3.data, result[1].event.data)
266
+ self.assertEqual(3 + initial_position, read_response.head)
267
+
268
+ # Repeat with query items in different order, expect events in ascending order.
269
+ query_type3_tag_c_or_type2_tag_b = DCBQuery(
270
+ items=[
271
+ DCBQueryItem(types=["type3"], tags=["tagC"]),
272
+ DCBQueryItem(types=["type2"], tags=["tagB"]),
273
+ ]
274
+ )
275
+ read_response = recorder.read(
276
+ query_type3_tag_c_or_type2_tag_b, after=initial_position
277
+ )
278
+ result = list(read_response)
279
+ self.assertEqual(2, len(result))
280
+ self.assertEqual(event2.data, result[0].event.data)
281
+ self.assertEqual(event3.data, result[1].event.data)
282
+ self.assertEqual(3 + initial_position, read_response.head)
283
+
284
+ # Append must fail if recorded events match condition.
285
+ event4 = DCBEvent(type="type4", data=b"data4")
286
+
287
+ # Fail because condition matches all.
288
+ new = [event4]
289
+ with self.assertRaises(IntegrityError):
290
+ recorder.append(new, DCBAppendCondition())
291
+
292
+ # Fail because condition matches all after 1.
293
+ with self.assertRaises(IntegrityError):
294
+ recorder.append(new, DCBAppendCondition(after=1))
295
+
296
+ # Fail because condition matches type1.
297
+ with self.assertRaises(IntegrityError):
298
+ recorder.append(new, DCBAppendCondition(query_type1))
299
+
300
+ # Fail because condition matches type2 after 1.
301
+ with self.assertRaises(IntegrityError):
302
+ recorder.append(new, DCBAppendCondition(query_type2, after=1))
303
+
304
+ # Fail because condition matches tagX.
305
+ with self.assertRaises(IntegrityError):
306
+ recorder.append(new, DCBAppendCondition(query_tag_x))
307
+
308
+ # Fail because condition matches tagA after 1.
309
+ with self.assertRaises(IntegrityError):
310
+ recorder.append(new, DCBAppendCondition(query_tag_a, after=1))
311
+
312
+ # Fail because condition matches type1 and tagX.
313
+ with self.assertRaises(IntegrityError):
314
+ recorder.append(new, DCBAppendCondition(query_type1_tag_x))
315
+
316
+ # Fail because condition matches type2 and tagA after 1.
317
+ with self.assertRaises(IntegrityError):
318
+ recorder.append(new, DCBAppendCondition(query_type2_tag_a, after=1))
319
+
320
+ # Fail because condition matches tagA and tagB.
321
+ with self.assertRaises(IntegrityError):
322
+ recorder.append(new, DCBAppendCondition(query_tag_a_and_b))
323
+
324
+ # Fail because condition matches tagB or tagC.
325
+ with self.assertRaises(IntegrityError):
326
+ recorder.append(new, DCBAppendCondition(query_tag_b_or_c))
327
+
328
+ # Fail because condition matches tagX or tagY.
329
+ with self.assertRaises(IntegrityError):
330
+ recorder.append(new, DCBAppendCondition(query_tag_x_or_y))
331
+
332
+ # Fail because condition matches with type2 and tagB, or with type3 and tagC.
333
+ with self.assertRaises(IntegrityError):
334
+ recorder.append(new, DCBAppendCondition(query_type2_tag_b_or_type3_tagc))
335
+
336
+ # Can append after 3.
337
+ recorder.append(new)
338
+
339
+ # Can append match type_n.
340
+ query_type_n = DCBQuery(items=[DCBQueryItem(types=["typeN"])])
341
+ recorder.append(new, DCBAppendCondition(query_type_n))
342
+
343
+ # Can append match tagY.
344
+ recorder.append(new, DCBAppendCondition(query_tag_y))
345
+
346
+ # Can append match type1 after 1.
347
+ recorder.append(
348
+ new, DCBAppendCondition(query_type1, after=1 + initial_position)
349
+ )
350
+
351
+ # Can append match tagX after 1.
352
+ recorder.append(
353
+ new, DCBAppendCondition(query_tag_x, after=1 + initial_position)
354
+ )
355
+
356
+ # Can append match type1 and tagX after 1.
357
+ recorder.append(
358
+ new, DCBAppendCondition(query_type1_tag_x, after=1 + initial_position)
359
+ )
360
+
361
+ # Can append match tagX, after 1.
362
+ recorder.append(
363
+ new, DCBAppendCondition(query_tag_x, after=1 + initial_position)
364
+ )
365
+
366
+ # Check it works with course subscription consistency boundaries and events.
367
+ student_id = f"student1-{uuid4()}"
368
+ student_registered = DCBEvent(
369
+ type="StudentRegistered",
370
+ data=json.dumps({"name": "Student1", "max_courses": 10}).encode(),
371
+ tags=[student_id],
372
+ )
373
+ course_id = f"course1-{uuid4()}"
374
+ course_registered = DCBEvent(
375
+ type="CourseRegistered",
376
+ data=json.dumps({"name": "Course1", "places": 10}).encode(),
377
+ tags=[course_id],
378
+ )
379
+ student_joined_course = DCBEvent(
380
+ type="StudentJoinedCourse",
381
+ data=json.dumps(
382
+ {"student_id": student_id, "course_id": course_id}
383
+ ).encode(),
384
+ tags=[course_id, student_id],
385
+ )
386
+
387
+ recorder.append(
388
+ events=[student_registered],
389
+ condition=DCBAppendCondition(
390
+ fail_if_events_match=DCBQuery(
391
+ items=[
392
+ DCBQueryItem(
393
+ tags=student_registered.tags, types=["StudentRegistered"]
394
+ )
395
+ ],
396
+ ),
397
+ after=3,
398
+ ),
399
+ )
400
+ recorder.append(
401
+ events=[course_registered],
402
+ condition=DCBAppendCondition(
403
+ fail_if_events_match=DCBQuery(
404
+ items=[DCBQueryItem(tags=course_registered.tags)],
405
+ ),
406
+ after=3,
407
+ ),
408
+ )
409
+ recorder.append(
410
+ events=[student_joined_course],
411
+ condition=DCBAppendCondition(
412
+ fail_if_events_match=DCBQuery(
413
+ items=[DCBQueryItem(tags=student_joined_course.tags)],
414
+ ),
415
+ after=3,
416
+ ),
417
+ )
418
+
419
+ read_response = recorder.read(after=initial_position)
420
+ result = list(read_response)
421
+ self.assertEqual(13, len(result))
422
+ self.assertEqual(result[-3].event.type, student_registered.type)
423
+ self.assertEqual(result[-2].event.type, course_registered.type)
424
+ self.assertEqual(result[-1].event.type, student_joined_course.type)
425
+ self.assertEqual(result[-3].event.data, student_registered.data)
426
+ self.assertEqual(result[-2].event.data, course_registered.data)
427
+ self.assertEqual(result[-1].event.data, student_joined_course.data)
428
+ self.assertEqual(result[-3].event.tags, student_registered.tags)
429
+ self.assertEqual(result[-2].event.tags, course_registered.tags)
430
+ self.assertEqual(result[-1].event.tags, student_joined_course.tags)
431
+ self.assertEqual(13 + initial_position, read_response.head)
432
+
433
+ read_response = recorder.read(
434
+ query=DCBQuery(
435
+ items=[DCBQueryItem(tags=student_registered.tags)],
436
+ ),
437
+ after=initial_position,
438
+ )
439
+ self.assertEqual(2, len(list(read_response)))
440
+ self.assertEqual(13 + initial_position, read_response.head)
441
+
442
+ read_response = recorder.read(
443
+ query=DCBQuery(
444
+ items=[DCBQueryItem(tags=course_registered.tags)],
445
+ ),
446
+ after=initial_position,
447
+ )
448
+ self.assertEqual(2, len(list(read_response)))
449
+ self.assertEqual(13 + initial_position, read_response.head)
450
+
451
+ read_response = recorder.read(
452
+ query=DCBQuery(
453
+ items=[DCBQueryItem(tags=student_joined_course.tags)],
454
+ ),
455
+ after=initial_position,
456
+ )
457
+ self.assertEqual(1, len(list(read_response)))
458
+ self.assertEqual(13 + initial_position, read_response.head)
459
+
460
+ read_response = recorder.read(
461
+ query=DCBQuery(
462
+ items=[DCBQueryItem(tags=student_registered.tags)],
463
+ ),
464
+ after=2 + initial_position,
465
+ )
466
+ self.assertEqual(2, len(list(read_response)))
467
+ self.assertEqual(13 + initial_position, read_response.head)
468
+
469
+ read_response = recorder.read(
470
+ query=DCBQuery(
471
+ items=[DCBQueryItem(tags=course_registered.tags)],
472
+ ),
473
+ after=2 + initial_position,
474
+ )
475
+ self.assertEqual(2, len(list(read_response)))
476
+ self.assertEqual(13 + initial_position, read_response.head)
477
+
478
+ read_response = recorder.read(
479
+ query=DCBQuery(
480
+ items=[DCBQueryItem(tags=student_joined_course.tags)],
481
+ ),
482
+ after=2 + initial_position,
483
+ )
484
+ self.assertEqual(1, len(list(read_response)))
485
+ self.assertEqual(13 + initial_position, read_response.head)
486
+
487
+ read_response = recorder.read(
488
+ query=DCBQuery(
489
+ items=[DCBQueryItem(tags=student_registered.tags)],
490
+ ),
491
+ after=2 + initial_position,
492
+ limit=1,
493
+ )
494
+ self.assertEqual(1, len(list(read_response)))
495
+ self.assertEqual(11 + initial_position, read_response.head)
496
+
497
+ read_response = recorder.read(
498
+ query=DCBQuery(
499
+ items=[DCBQueryItem(tags=course_registered.tags)],
500
+ ),
501
+ after=2 + initial_position,
502
+ limit=1,
503
+ )
504
+ self.assertEqual(1, len(list(read_response)))
505
+ self.assertEqual(12 + initial_position, read_response.head)
506
+
507
+ read_response = recorder.read(
508
+ query=DCBQuery(
509
+ items=[DCBQueryItem(tags=student_joined_course.tags)],
510
+ ),
511
+ after=2 + initial_position,
512
+ limit=1,
513
+ )
514
+ self.assertEqual(1, len(list(read_response)))
515
+ self.assertEqual(13 + initial_position, read_response.head)
516
+
517
+ consistency_boundary = DCBQuery(
518
+ items=[
519
+ DCBQueryItem(
520
+ types=["StudentRegistered", "StudentJoinedCourse"],
521
+ tags=[student_id],
522
+ ),
523
+ DCBQueryItem(
524
+ types=["CourseRegistered", "StudentJoinedCourse"],
525
+ tags=[course_id],
526
+ ),
527
+ ]
528
+ )
529
+ read_response = recorder.read(
530
+ query=consistency_boundary,
531
+ after=initial_position,
532
+ )
533
+ self.assertEqual(3, len(list(read_response)))
534
+ self.assertEqual(13 + initial_position, read_response.head)
535
+
536
+ # # Check it works with appointment booking.
537
+ # appointment_id = f"appointment-{uuid4()}"
538
+ # tags = [
539
+ # f"slot:2025-07-10-{hour:02}-{minute:02}"
540
+ # for hour in range(13, 18)
541
+ # for minute in range(0, 60)
542
+ # ]
543
+ # appointment_scheduled = DCBEvent(
544
+ # type="AppointmentSchedules",
545
+ # data=json.dumps({"name": "ABC"}).encode(),
546
+ # tags=tags,
547
+ # )
548
+ # started = datetime.datetime.now()
549
+ # eventstore.append(
550
+ # events=[appointment_scheduled],
551
+ # condition=DCBAppendCondition(
552
+ # fail_if_events_match=DCBQuery(
553
+ # items=[DCBQueryItem(tags=tags)],
554
+ # ),
555
+ # ),
556
+ # )
557
+ # print("Event appended:", datetime.datetime.now() - started)
558
+ # started = datetime.datetime.now()
559
+ # with self.assertRaises(IntegrityError):
560
+ # eventstore.append(
561
+ # events=[appointment_scheduled],
562
+ # condition=DCBAppendCondition(
563
+ # fail_if_events_match=DCBQuery(
564
+ # items=[DCBQueryItem(tags=tags)],
565
+ # ),
566
+ # ),
567
+ # )
568
+ # print("Conflict detected:", datetime.datetime.now() - started)
569
+
570
+ def _test_append_subscribe(
571
+ self, recorder: DCBRecorder, initial_position: int = 0
572
+ ) -> None:
573
+ # Append one event.
574
+ event1 = DCBEvent(type="type1", data=b"data1", tags=["tagX"])
575
+ position1 = recorder.append(events=[event1])
576
+ self.assertEqual(1 + initial_position, position1)
577
+
578
+ # Start subscription.
579
+ with recorder.subscribe(after=initial_position) as subscription:
580
+ self.assertEqual(position1, next(subscription).position)
581
+
582
+ thread = EnsureSubscriptionBlockAndReceive(subscription)
583
+ thread.has_blocked.wait()
584
+ self.assertFalse(thread.has_received.wait(timeout=0.5))
585
+
586
+ # Append one more event.
587
+ event2 = DCBEvent(type="type1", data=b"data1", tags=["tagX"])
588
+ position2 = recorder.append(events=[event2])
589
+ self.assertEqual(2 + initial_position, position2)
590
+
591
+ self.assertTrue(thread.has_received.wait(timeout=1))
592
+ assert thread.received_event is not None # for mypy
593
+ self.assertEqual(position2, thread.received_event.position)
594
+
595
+
596
+ class EnsureSubscriptionBlockAndReceive(threading.Thread):
597
+ def __init__(self, subscription: DCBSubscription[TDCBRecorder_co]):
598
+ super().__init__()
599
+ self.subscription = subscription
600
+ self.has_blocked = threading.Event()
601
+ self.has_received = threading.Event()
602
+ self.received_event: DCBSequencedEvent | None = None
603
+ self.start()
604
+
605
+ def run(self) -> None:
606
+ self.has_blocked.set()
607
+ self.received_event = next(self.subscription)
608
+ self.has_received.set()
@@ -0,0 +1,80 @@
1
+ from __future__ import annotations
2
+
3
+ import functools
4
+ from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast, overload
5
+
6
+ _T = TypeVar("_T")
7
+ _S = TypeVar("_S")
8
+
9
+ if TYPE_CHECKING:
10
+ from collections.abc import Callable
11
+
12
+ class _singledispatchmethod(functools.singledispatchmethod[_T]): # noqa: N801
13
+ pass
14
+
15
+ else:
16
+
17
+ class _singledispatchmethod( # noqa: N801
18
+ functools.singledispatchmethod, Generic[_T]
19
+ ):
20
+ pass
21
+
22
+
23
+ class singledispatchmethod(_singledispatchmethod[_T]): # noqa: N801
24
+ def __init__(self, func: Callable[..., _T]) -> None:
25
+ super().__init__(func)
26
+ self.deferred_registrations: list[
27
+ tuple[type[Any] | Callable[..., _T], Callable[..., _T] | None]
28
+ ] = []
29
+
30
+ @overload
31
+ def register(
32
+ self, cls: type[Any], method: None = None
33
+ ) -> Callable[[Callable[..., _T]], Callable[..., _T]]: ... # pragma: no cover
34
+ @overload
35
+ def register(
36
+ self, cls: Callable[..., _T], method: None = None
37
+ ) -> Callable[..., _T]: ... # pragma: no cover
38
+
39
+ @overload
40
+ def register(
41
+ self, cls: type[Any], method: Callable[..., _T]
42
+ ) -> Callable[..., _T]: ... # pragma: no cover
43
+
44
+ def register(
45
+ self,
46
+ cls: type[Any] | Callable[..., _T],
47
+ method: Callable[..., _T] | None = None,
48
+ ) -> Callable[[Callable[..., _T]], Callable[..., _T]] | Callable[..., _T]:
49
+ """generic_method.register(cls, func) -> func
50
+
51
+ Registers a new implementation for the given *cls* on a *generic_method*.
52
+ """
53
+ if isinstance(cls, (classmethod, staticmethod)):
54
+ first_annotation = {}
55
+ for k, v in cls.__func__.__annotations__.items():
56
+ first_annotation[k] = v
57
+ break
58
+ cls.__annotations__ = first_annotation
59
+
60
+ # for globals in typing.get_type_hints() in Python 3.8 and 3.9
61
+ if not hasattr(cls, "__wrapped__"):
62
+ cls.__dict__["__wrapped__"] = cls.__func__
63
+ # cls.__wrapped__ = cls.__func__
64
+
65
+ try:
66
+ return self.dispatcher.register(cast("type[Any]", cls), func=method)
67
+ except (NameError, TypeError): # NameError <= Py3.13, TypeError >= Py3.14
68
+ self.deferred_registrations.append(
69
+ (cls, method) # pyright: ignore [reportArgumentType]
70
+ )
71
+ # TODO: Fix this....
72
+ return method or cls # pyright: ignore [reportReturnType]
73
+
74
+ def __get__(self, obj: _S, cls: type[_S] | None = None) -> Callable[..., _T]:
75
+ for registered_cls, registered_method in self.deferred_registrations:
76
+ self.dispatcher.register(
77
+ cast("type[Any]", registered_cls), func=registered_method
78
+ )
79
+ self.deferred_registrations = []
80
+ return super().__get__(obj, cls=cls)