eventsourcing 9.4.0a7__py3-none-any.whl → 9.4.0a8__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.
Potentially problematic release.
This version of eventsourcing might be problematic. Click here for more details.
- eventsourcing/application.py +3 -7
- eventsourcing/domain.py +346 -357
- eventsourcing/persistence.py +18 -24
- eventsourcing/popo.py +5 -1
- eventsourcing/postgres.py +41 -33
- eventsourcing/projection.py +22 -19
- eventsourcing/sqlite.py +5 -1
- eventsourcing/system.py +19 -11
- eventsourcing/tests/application.py +57 -49
- eventsourcing/tests/domain.py +8 -6
- eventsourcing/tests/persistence.py +162 -143
- eventsourcing/tests/postgres_utils.py +7 -8
- eventsourcing/utils.py +15 -10
- {eventsourcing-9.4.0a7.dist-info → eventsourcing-9.4.0a8.dist-info}/METADATA +1 -1
- eventsourcing-9.4.0a8.dist-info/RECORD +26 -0
- eventsourcing-9.4.0a7.dist-info/RECORD +0 -26
- {eventsourcing-9.4.0a7.dist-info → eventsourcing-9.4.0a8.dist-info}/AUTHORS +0 -0
- {eventsourcing-9.4.0a7.dist-info → eventsourcing-9.4.0a8.dist-info}/LICENSE +0 -0
- {eventsourcing-9.4.0a7.dist-info → eventsourcing-9.4.0a8.dist-info}/WHEEL +0 -0
|
@@ -34,7 +34,7 @@ class ExampleApplicationTestCase(TestCase):
|
|
|
34
34
|
counts: ClassVar[dict[type[TestCase], int]] = {}
|
|
35
35
|
expected_factory_topic: str
|
|
36
36
|
|
|
37
|
-
def test_example_application(self):
|
|
37
|
+
def test_example_application(self) -> None:
|
|
38
38
|
app = BankAccounts(env={"IS_SNAPSHOTTING_ENABLED": "y"})
|
|
39
39
|
|
|
40
40
|
self.assertEqual(get_topic(type(app.factory)), self.expected_factory_topic)
|
|
@@ -80,30 +80,32 @@ class ExampleApplicationTestCase(TestCase):
|
|
|
80
80
|
# Take snapshot (specify version).
|
|
81
81
|
app.take_snapshot(account_id, version=Aggregate.INITIAL_VERSION + 1)
|
|
82
82
|
|
|
83
|
+
assert app.snapshots is not None # for mypy
|
|
83
84
|
snapshots = list(app.snapshots.get(account_id))
|
|
84
85
|
self.assertEqual(len(snapshots), 1)
|
|
85
86
|
self.assertEqual(snapshots[0].originator_version, Aggregate.INITIAL_VERSION + 1)
|
|
86
87
|
|
|
87
|
-
|
|
88
|
+
from_snapshot1: BankAccount = app.repository.get(
|
|
88
89
|
account_id, version=Aggregate.INITIAL_VERSION + 2
|
|
89
90
|
)
|
|
90
|
-
self.assertIsInstance(
|
|
91
|
-
self.assertEqual(
|
|
92
|
-
self.assertEqual(
|
|
91
|
+
self.assertIsInstance(from_snapshot1, BankAccount)
|
|
92
|
+
self.assertEqual(from_snapshot1.version, Aggregate.INITIAL_VERSION + 2)
|
|
93
|
+
self.assertEqual(from_snapshot1.balance, Decimal("35.00"))
|
|
93
94
|
|
|
94
95
|
# Take snapshot (don't specify version).
|
|
95
96
|
app.take_snapshot(account_id)
|
|
97
|
+
assert app.snapshots is not None # for mypy
|
|
96
98
|
snapshots = list(app.snapshots.get(account_id))
|
|
97
99
|
self.assertEqual(len(snapshots), 2)
|
|
98
100
|
self.assertEqual(snapshots[0].originator_version, Aggregate.INITIAL_VERSION + 1)
|
|
99
101
|
self.assertEqual(snapshots[1].originator_version, Aggregate.INITIAL_VERSION + 3)
|
|
100
102
|
|
|
101
|
-
|
|
102
|
-
self.assertIsInstance(
|
|
103
|
-
self.assertEqual(
|
|
104
|
-
self.assertEqual(
|
|
103
|
+
from_snapshot2: BankAccount = app.repository.get(account_id)
|
|
104
|
+
self.assertIsInstance(from_snapshot2, BankAccount)
|
|
105
|
+
self.assertEqual(from_snapshot2.version, Aggregate.INITIAL_VERSION + 3)
|
|
106
|
+
self.assertEqual(from_snapshot2.balance, Decimal("65.00"))
|
|
105
107
|
|
|
106
|
-
def test__put_performance(self):
|
|
108
|
+
def test__put_performance(self) -> None:
|
|
107
109
|
app = BankAccounts()
|
|
108
110
|
|
|
109
111
|
# Open an account.
|
|
@@ -113,7 +115,7 @@ class ExampleApplicationTestCase(TestCase):
|
|
|
113
115
|
)
|
|
114
116
|
account = app.get_account(account_id)
|
|
115
117
|
|
|
116
|
-
def put():
|
|
118
|
+
def put() -> None:
|
|
117
119
|
# Credit the account.
|
|
118
120
|
account.append_transaction(Decimal("10.00"))
|
|
119
121
|
app.save(account)
|
|
@@ -125,14 +127,14 @@ class ExampleApplicationTestCase(TestCase):
|
|
|
125
127
|
duration = timeit(put, number=self.timeit_number)
|
|
126
128
|
self.print_time("store events", duration)
|
|
127
129
|
|
|
128
|
-
def test__get_performance_with_snapshotting_enabled(self):
|
|
130
|
+
def test__get_performance_with_snapshotting_enabled(self) -> None:
|
|
129
131
|
print()
|
|
130
132
|
self._test_get_performance(is_snapshotting_enabled=True)
|
|
131
133
|
|
|
132
|
-
def test__get_performance_without_snapshotting_enabled(self):
|
|
134
|
+
def test__get_performance_without_snapshotting_enabled(self) -> None:
|
|
133
135
|
self._test_get_performance(is_snapshotting_enabled=False)
|
|
134
136
|
|
|
135
|
-
def _test_get_performance(self, *, is_snapshotting_enabled: bool):
|
|
137
|
+
def _test_get_performance(self, *, is_snapshotting_enabled: bool) -> None:
|
|
136
138
|
app = BankAccounts(
|
|
137
139
|
env={"IS_SNAPSHOTTING_ENABLED": "y" if is_snapshotting_enabled else "n"}
|
|
138
140
|
)
|
|
@@ -143,7 +145,7 @@ class ExampleApplicationTestCase(TestCase):
|
|
|
143
145
|
email_address="alice@example.com",
|
|
144
146
|
)
|
|
145
147
|
|
|
146
|
-
def read():
|
|
148
|
+
def read() -> None:
|
|
147
149
|
# Get the account.
|
|
148
150
|
app.get_account(account_id)
|
|
149
151
|
|
|
@@ -158,7 +160,7 @@ class ExampleApplicationTestCase(TestCase):
|
|
|
158
160
|
test_label = "get without snapshotting"
|
|
159
161
|
self.print_time(test_label, duration)
|
|
160
162
|
|
|
161
|
-
def print_time(self, test_label, duration):
|
|
163
|
+
def print_time(self, test_label: str, duration: float) -> None:
|
|
162
164
|
cls = type(self)
|
|
163
165
|
if cls not in self.started_ats:
|
|
164
166
|
self.started_ats[cls] = datetime.now()
|
|
@@ -176,8 +178,8 @@ class ExampleApplicationTestCase(TestCase):
|
|
|
176
178
|
)
|
|
177
179
|
|
|
178
180
|
if self.counts[cls] == 3:
|
|
179
|
-
|
|
180
|
-
print(f"{cls.__name__: <29} timeit duration: {
|
|
181
|
+
cls_duration = datetime.now() - cls.started_ats[cls]
|
|
182
|
+
print(f"{cls.__name__: <29} timeit duration: {cls_duration}")
|
|
181
183
|
sys.stdout.flush()
|
|
182
184
|
|
|
183
185
|
|
|
@@ -199,7 +201,7 @@ class BankAccounts(Application):
|
|
|
199
201
|
super().register_transcodings(transcoder)
|
|
200
202
|
transcoder.register(EmailAddressAsStr())
|
|
201
203
|
|
|
202
|
-
def open_account(self, full_name, email_address):
|
|
204
|
+
def open_account(self, full_name: str, email_address: str) -> UUID:
|
|
203
205
|
account = BankAccount.open(
|
|
204
206
|
full_name=full_name,
|
|
205
207
|
email_address=email_address,
|
|
@@ -218,7 +220,7 @@ class BankAccounts(Application):
|
|
|
218
220
|
|
|
219
221
|
def get_account(self, account_id: UUID) -> BankAccount:
|
|
220
222
|
try:
|
|
221
|
-
aggregate = self.repository.get(account_id)
|
|
223
|
+
aggregate: BankAccount = self.repository.get(account_id)
|
|
222
224
|
except AggregateNotFoundError:
|
|
223
225
|
raise self.AccountNotFoundError(account_id) from None
|
|
224
226
|
else:
|
|
@@ -230,7 +232,7 @@ class BankAccounts(Application):
|
|
|
230
232
|
|
|
231
233
|
|
|
232
234
|
class ApplicationTestCase(TestCase):
|
|
233
|
-
def test_name(self):
|
|
235
|
+
def test_name(self) -> None:
|
|
234
236
|
self.assertEqual(Application.name, "Application")
|
|
235
237
|
|
|
236
238
|
class MyApplication1(Application):
|
|
@@ -243,7 +245,7 @@ class ApplicationTestCase(TestCase):
|
|
|
243
245
|
|
|
244
246
|
self.assertEqual(MyApplication2.name, "MyBoundedContext")
|
|
245
247
|
|
|
246
|
-
def test_resolve_persistence_topics(self):
|
|
248
|
+
def test_resolve_persistence_topics(self) -> None:
|
|
247
249
|
# None specified.
|
|
248
250
|
app = Application()
|
|
249
251
|
self.assertIsInstance(app.factory, InfrastructureFactory)
|
|
@@ -279,7 +281,7 @@ class ApplicationTestCase(TestCase):
|
|
|
279
281
|
"eventsourcing.application:Application",
|
|
280
282
|
)
|
|
281
283
|
|
|
282
|
-
def test_save_returns_recording_event(self):
|
|
284
|
+
def test_save_returns_recording_event(self) -> None:
|
|
283
285
|
app = Application()
|
|
284
286
|
|
|
285
287
|
recordings = app.save()
|
|
@@ -301,7 +303,9 @@ class ApplicationTestCase(TestCase):
|
|
|
301
303
|
self.assertEqual(recordings[0].notification.id, 3)
|
|
302
304
|
self.assertEqual(recordings[1].notification.id, 4)
|
|
303
305
|
|
|
304
|
-
def test_take_snapshot_raises_assertion_error_if_snapshotting_not_enabled(
|
|
306
|
+
def test_take_snapshot_raises_assertion_error_if_snapshotting_not_enabled(
|
|
307
|
+
self,
|
|
308
|
+
) -> None:
|
|
305
309
|
app = Application()
|
|
306
310
|
with self.assertRaises(AssertionError) as cm:
|
|
307
311
|
app.take_snapshot(uuid4())
|
|
@@ -314,12 +318,13 @@ class ApplicationTestCase(TestCase):
|
|
|
314
318
|
"application class.",
|
|
315
319
|
)
|
|
316
320
|
|
|
317
|
-
def test_application_with_cached_aggregates_and_fastforward(self):
|
|
321
|
+
def test_application_with_cached_aggregates_and_fastforward(self) -> None:
|
|
318
322
|
app = Application(env={"AGGREGATE_CACHE_MAXSIZE": "10"})
|
|
319
323
|
|
|
320
324
|
aggregate = Aggregate()
|
|
321
325
|
app.save(aggregate)
|
|
322
326
|
# Should not put the aggregate in the cache.
|
|
327
|
+
assert app.repository.cache is not None # for mypy
|
|
323
328
|
with self.assertRaises(KeyError):
|
|
324
329
|
self.assertEqual(aggregate, app.repository.cache.get(aggregate.id))
|
|
325
330
|
|
|
@@ -339,7 +344,7 @@ class ApplicationTestCase(TestCase):
|
|
|
339
344
|
app.repository.get(aggregate.id)
|
|
340
345
|
self.assertEqual(aggregate, app.repository.cache.get(aggregate.id))
|
|
341
346
|
|
|
342
|
-
def test_application_fastforward_skipping_during_contention(self):
|
|
347
|
+
def test_application_fastforward_skipping_during_contention(self) -> None:
|
|
343
348
|
app = Application(
|
|
344
349
|
env={
|
|
345
350
|
"AGGREGATE_CACHE_MAXSIZE": "10",
|
|
@@ -354,18 +359,18 @@ class ApplicationTestCase(TestCase):
|
|
|
354
359
|
stopped = Event()
|
|
355
360
|
|
|
356
361
|
# Trigger, save, get, check.
|
|
357
|
-
def trigger_save_get_check():
|
|
362
|
+
def trigger_save_get_check() -> None:
|
|
358
363
|
while not stopped.is_set():
|
|
359
364
|
try:
|
|
360
|
-
aggregate = app.repository.get(aggregate_id)
|
|
365
|
+
aggregate: Aggregate = app.repository.get(aggregate_id)
|
|
361
366
|
aggregate.trigger_event(Aggregate.Event)
|
|
362
367
|
saved_version = aggregate.version
|
|
363
368
|
try:
|
|
364
369
|
app.save(aggregate)
|
|
365
370
|
except IntegrityError:
|
|
366
371
|
continue
|
|
367
|
-
|
|
368
|
-
if saved_version >
|
|
372
|
+
cached: Aggregate = app.repository.get(aggregate_id)
|
|
373
|
+
if saved_version > cached.version:
|
|
369
374
|
print(f"Skipped fast-forwarding at version {saved_version}")
|
|
370
375
|
stopped.set()
|
|
371
376
|
if aggregate.version % 1000 == 0:
|
|
@@ -384,7 +389,7 @@ class ApplicationTestCase(TestCase):
|
|
|
384
389
|
self.fail("Didn't skip fast forwarding before test timed out...")
|
|
385
390
|
executor.shutdown()
|
|
386
391
|
|
|
387
|
-
def test_application_fastforward_blocking_during_contention(self):
|
|
392
|
+
def test_application_fastforward_blocking_during_contention(self) -> None:
|
|
388
393
|
app = Application(
|
|
389
394
|
env={
|
|
390
395
|
"AGGREGATE_CACHE_MAXSIZE": "10",
|
|
@@ -398,18 +403,18 @@ class ApplicationTestCase(TestCase):
|
|
|
398
403
|
stopped = Event()
|
|
399
404
|
|
|
400
405
|
# Trigger, save, get, check.
|
|
401
|
-
def trigger_save_get_check():
|
|
406
|
+
def trigger_save_get_check() -> None:
|
|
402
407
|
while not stopped.is_set():
|
|
403
408
|
try:
|
|
404
|
-
aggregate = app.repository.get(aggregate_id)
|
|
409
|
+
aggregate: Aggregate = app.repository.get(aggregate_id)
|
|
405
410
|
aggregate.trigger_event(Aggregate.Event)
|
|
406
411
|
saved_version = aggregate.version
|
|
407
412
|
try:
|
|
408
413
|
app.save(aggregate)
|
|
409
414
|
except IntegrityError:
|
|
410
415
|
continue
|
|
411
|
-
|
|
412
|
-
if saved_version >
|
|
416
|
+
cached: Aggregate = app.repository.get(aggregate_id)
|
|
417
|
+
if saved_version > cached.version:
|
|
413
418
|
print(f"Skipped fast-forwarding at version {saved_version}")
|
|
414
419
|
stopped.set()
|
|
415
420
|
if aggregate.version % 1000 == 0:
|
|
@@ -429,7 +434,7 @@ class ApplicationTestCase(TestCase):
|
|
|
429
434
|
self.fail("Wrongly skipped fast forwarding")
|
|
430
435
|
executor.shutdown()
|
|
431
436
|
|
|
432
|
-
def test_application_with_cached_aggregates_not_fastforward(self):
|
|
437
|
+
def test_application_with_cached_aggregates_not_fastforward(self) -> None:
|
|
433
438
|
app = Application(
|
|
434
439
|
env={
|
|
435
440
|
"AGGREGATE_CACHE_MAXSIZE": "10",
|
|
@@ -439,11 +444,12 @@ class ApplicationTestCase(TestCase):
|
|
|
439
444
|
aggregate = Aggregate()
|
|
440
445
|
app.save(aggregate)
|
|
441
446
|
# Should put the aggregate in the cache.
|
|
447
|
+
assert app.repository.cache is not None # for mypy
|
|
442
448
|
self.assertEqual(aggregate, app.repository.cache.get(aggregate.id))
|
|
443
449
|
app.repository.get(aggregate.id)
|
|
444
450
|
self.assertEqual(aggregate, app.repository.cache.get(aggregate.id))
|
|
445
451
|
|
|
446
|
-
def test_application_with_deepcopy_from_cache_arg(self):
|
|
452
|
+
def test_application_with_deepcopy_from_cache_arg(self) -> None:
|
|
447
453
|
app = Application(
|
|
448
454
|
env={
|
|
449
455
|
"AGGREGATE_CACHE_MAXSIZE": "10",
|
|
@@ -452,14 +458,15 @@ class ApplicationTestCase(TestCase):
|
|
|
452
458
|
aggregate = Aggregate()
|
|
453
459
|
app.save(aggregate)
|
|
454
460
|
self.assertEqual(aggregate.version, 1)
|
|
455
|
-
|
|
456
|
-
|
|
461
|
+
reconstructed: Aggregate = app.repository.get(aggregate.id)
|
|
462
|
+
reconstructed.version = 101
|
|
463
|
+
assert app.repository.cache is not None # for mypy
|
|
457
464
|
self.assertEqual(app.repository.cache.get(aggregate.id).version, 1)
|
|
458
|
-
|
|
459
|
-
|
|
465
|
+
cached: Aggregate = app.repository.get(aggregate.id, deepcopy_from_cache=False)
|
|
466
|
+
cached.version = 101
|
|
460
467
|
self.assertEqual(app.repository.cache.get(aggregate.id).version, 101)
|
|
461
468
|
|
|
462
|
-
def test_application_with_deepcopy_from_cache_attribute(self):
|
|
469
|
+
def test_application_with_deepcopy_from_cache_attribute(self) -> None:
|
|
463
470
|
app = Application(
|
|
464
471
|
env={
|
|
465
472
|
"AGGREGATE_CACHE_MAXSIZE": "10",
|
|
@@ -468,15 +475,16 @@ class ApplicationTestCase(TestCase):
|
|
|
468
475
|
aggregate = Aggregate()
|
|
469
476
|
app.save(aggregate)
|
|
470
477
|
self.assertEqual(aggregate.version, 1)
|
|
471
|
-
|
|
472
|
-
|
|
478
|
+
reconstructed: Aggregate = app.repository.get(aggregate.id)
|
|
479
|
+
reconstructed.version = 101
|
|
480
|
+
assert app.repository.cache is not None # for mypy
|
|
473
481
|
self.assertEqual(app.repository.cache.get(aggregate.id).version, 1)
|
|
474
482
|
app.repository.deepcopy_from_cache = False
|
|
475
|
-
|
|
476
|
-
|
|
483
|
+
cached: Aggregate = app.repository.get(aggregate.id)
|
|
484
|
+
cached.version = 101
|
|
477
485
|
self.assertEqual(app.repository.cache.get(aggregate.id).version, 101)
|
|
478
486
|
|
|
479
|
-
def test_application_log(self):
|
|
487
|
+
def test_application_log(self) -> None:
|
|
480
488
|
# Check the old 'log' attribute presents the 'notification log' object.
|
|
481
489
|
app = Application()
|
|
482
490
|
|
|
@@ -486,6 +494,6 @@ class ApplicationTestCase(TestCase):
|
|
|
486
494
|
|
|
487
495
|
self.assertEqual(1, len(w))
|
|
488
496
|
self.assertIs(w[-1].category, DeprecationWarning)
|
|
489
|
-
self.
|
|
490
|
-
"'log' is deprecated, use 'notifications' instead", w[-1].message
|
|
497
|
+
self.assertIn(
|
|
498
|
+
"'log' is deprecated, use 'notifications' instead", str(w[-1].message)
|
|
491
499
|
)
|
eventsourcing/tests/domain.py
CHANGED
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from decimal import Decimal
|
|
5
|
+
from typing import cast
|
|
5
6
|
from uuid import uuid4
|
|
6
7
|
|
|
7
8
|
from eventsourcing.domain import Aggregate, AggregateCreated, AggregateEvent
|
|
@@ -59,6 +60,7 @@ class BankAccount(Aggregate):
|
|
|
59
60
|
if self.balance + amount < -self.overdraft_limit:
|
|
60
61
|
raise InsufficientFundsError({"account_id": self.id})
|
|
61
62
|
|
|
63
|
+
@dataclass(frozen=True)
|
|
62
64
|
class TransactionAppended(AggregateEvent):
|
|
63
65
|
"""
|
|
64
66
|
Domain event for when transaction
|
|
@@ -67,11 +69,11 @@ class BankAccount(Aggregate):
|
|
|
67
69
|
|
|
68
70
|
amount: Decimal
|
|
69
71
|
|
|
70
|
-
def apply(self, account:
|
|
72
|
+
def apply(self, account: Aggregate) -> None:
|
|
71
73
|
"""
|
|
72
74
|
Increments the account balance.
|
|
73
75
|
"""
|
|
74
|
-
account.balance += self.amount
|
|
76
|
+
cast(BankAccount, account).balance += self.amount
|
|
75
77
|
|
|
76
78
|
def set_overdraft_limit(self, overdraft_limit: Decimal) -> None:
|
|
77
79
|
"""
|
|
@@ -93,8 +95,8 @@ class BankAccount(Aggregate):
|
|
|
93
95
|
|
|
94
96
|
overdraft_limit: Decimal
|
|
95
97
|
|
|
96
|
-
def apply(self, account:
|
|
97
|
-
account.overdraft_limit = self.overdraft_limit
|
|
98
|
+
def apply(self, account: Aggregate) -> None:
|
|
99
|
+
cast(BankAccount, account).overdraft_limit = self.overdraft_limit
|
|
98
100
|
|
|
99
101
|
def close(self) -> None:
|
|
100
102
|
"""
|
|
@@ -107,8 +109,8 @@ class BankAccount(Aggregate):
|
|
|
107
109
|
Domain event for when account is closed.
|
|
108
110
|
"""
|
|
109
111
|
|
|
110
|
-
def apply(self, account:
|
|
111
|
-
account.is_closed = True
|
|
112
|
+
def apply(self, account: Aggregate) -> None:
|
|
113
|
+
cast(BankAccount, account).is_closed = True
|
|
112
114
|
|
|
113
115
|
|
|
114
116
|
class AccountClosedError(Exception):
|