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.
- eventsourcing/__init__.py +0 -0
- eventsourcing/application.py +998 -0
- eventsourcing/cipher.py +107 -0
- eventsourcing/compressor.py +15 -0
- eventsourcing/cryptography.py +91 -0
- eventsourcing/dcb/__init__.py +0 -0
- eventsourcing/dcb/api.py +144 -0
- eventsourcing/dcb/application.py +159 -0
- eventsourcing/dcb/domain.py +369 -0
- eventsourcing/dcb/msgpack.py +38 -0
- eventsourcing/dcb/persistence.py +193 -0
- eventsourcing/dcb/popo.py +178 -0
- eventsourcing/dcb/postgres_tt.py +704 -0
- eventsourcing/dcb/tests.py +608 -0
- eventsourcing/dispatch.py +80 -0
- eventsourcing/domain.py +1964 -0
- eventsourcing/interface.py +164 -0
- eventsourcing/persistence.py +1429 -0
- eventsourcing/popo.py +267 -0
- eventsourcing/postgres.py +1441 -0
- eventsourcing/projection.py +502 -0
- eventsourcing/py.typed +0 -0
- eventsourcing/sqlite.py +816 -0
- eventsourcing/system.py +1203 -0
- eventsourcing/tests/__init__.py +3 -0
- eventsourcing/tests/application.py +483 -0
- eventsourcing/tests/domain.py +105 -0
- eventsourcing/tests/persistence.py +1744 -0
- eventsourcing/tests/postgres_utils.py +131 -0
- eventsourcing/utils.py +257 -0
- eventsourcing-9.5.0b3.dist-info/METADATA +253 -0
- eventsourcing-9.5.0b3.dist-info/RECORD +35 -0
- eventsourcing-9.5.0b3.dist-info/WHEEL +4 -0
- eventsourcing-9.5.0b3.dist-info/licenses/AUTHORS +10 -0
- eventsourcing-9.5.0b3.dist-info/licenses/LICENSE +29 -0
|
@@ -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)
|