eventsourcing 9.2.21__py3-none-any.whl → 9.3.0__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/__init__.py +1 -1
- eventsourcing/application.py +137 -132
- eventsourcing/cipher.py +17 -12
- eventsourcing/compressor.py +2 -0
- eventsourcing/dispatch.py +30 -56
- eventsourcing/domain.py +221 -227
- eventsourcing/examples/__init__.py +0 -0
- eventsourcing/examples/aggregate1/__init__.py +0 -0
- eventsourcing/examples/aggregate1/application.py +27 -0
- eventsourcing/examples/aggregate1/domainmodel.py +16 -0
- eventsourcing/examples/aggregate1/test_application.py +37 -0
- eventsourcing/examples/aggregate2/__init__.py +0 -0
- eventsourcing/examples/aggregate2/application.py +27 -0
- eventsourcing/examples/aggregate2/domainmodel.py +22 -0
- eventsourcing/examples/aggregate2/test_application.py +37 -0
- eventsourcing/examples/aggregate3/__init__.py +0 -0
- eventsourcing/examples/aggregate3/application.py +27 -0
- eventsourcing/examples/aggregate3/domainmodel.py +38 -0
- eventsourcing/examples/aggregate3/test_application.py +37 -0
- eventsourcing/examples/aggregate4/__init__.py +0 -0
- eventsourcing/examples/aggregate4/application.py +27 -0
- eventsourcing/examples/aggregate4/domainmodel.py +114 -0
- eventsourcing/examples/aggregate4/test_application.py +38 -0
- eventsourcing/examples/aggregate5/__init__.py +0 -0
- eventsourcing/examples/aggregate5/application.py +27 -0
- eventsourcing/examples/aggregate5/domainmodel.py +131 -0
- eventsourcing/examples/aggregate5/test_application.py +38 -0
- eventsourcing/examples/aggregate6/__init__.py +0 -0
- eventsourcing/examples/aggregate6/application.py +30 -0
- eventsourcing/examples/aggregate6/domainmodel.py +123 -0
- eventsourcing/examples/aggregate6/test_application.py +38 -0
- eventsourcing/examples/aggregate6a/__init__.py +0 -0
- eventsourcing/examples/aggregate6a/application.py +40 -0
- eventsourcing/examples/aggregate6a/domainmodel.py +149 -0
- eventsourcing/examples/aggregate6a/test_application.py +45 -0
- eventsourcing/examples/aggregate7/__init__.py +0 -0
- eventsourcing/examples/aggregate7/application.py +48 -0
- eventsourcing/examples/aggregate7/domainmodel.py +144 -0
- eventsourcing/examples/aggregate7/persistence.py +57 -0
- eventsourcing/examples/aggregate7/test_application.py +38 -0
- eventsourcing/examples/aggregate7/test_compression_and_encryption.py +45 -0
- eventsourcing/examples/aggregate7/test_snapshotting_intervals.py +67 -0
- eventsourcing/examples/aggregate7a/__init__.py +0 -0
- eventsourcing/examples/aggregate7a/application.py +56 -0
- eventsourcing/examples/aggregate7a/domainmodel.py +170 -0
- eventsourcing/examples/aggregate7a/test_application.py +46 -0
- eventsourcing/examples/aggregate7a/test_compression_and_encryption.py +45 -0
- eventsourcing/examples/aggregate8/__init__.py +0 -0
- eventsourcing/examples/aggregate8/application.py +47 -0
- eventsourcing/examples/aggregate8/domainmodel.py +65 -0
- eventsourcing/examples/aggregate8/persistence.py +57 -0
- eventsourcing/examples/aggregate8/test_application.py +37 -0
- eventsourcing/examples/aggregate8/test_compression_and_encryption.py +44 -0
- eventsourcing/examples/aggregate8/test_snapshotting_intervals.py +38 -0
- eventsourcing/examples/bankaccounts/__init__.py +0 -0
- eventsourcing/examples/bankaccounts/application.py +70 -0
- eventsourcing/examples/bankaccounts/domainmodel.py +56 -0
- eventsourcing/examples/bankaccounts/test.py +173 -0
- eventsourcing/examples/cargoshipping/__init__.py +0 -0
- eventsourcing/examples/cargoshipping/application.py +126 -0
- eventsourcing/examples/cargoshipping/domainmodel.py +330 -0
- eventsourcing/examples/cargoshipping/interface.py +143 -0
- eventsourcing/examples/cargoshipping/test.py +231 -0
- eventsourcing/examples/contentmanagement/__init__.py +0 -0
- eventsourcing/examples/contentmanagement/application.py +118 -0
- eventsourcing/examples/contentmanagement/domainmodel.py +69 -0
- eventsourcing/examples/contentmanagement/test.py +180 -0
- eventsourcing/examples/contentmanagement/utils.py +26 -0
- eventsourcing/examples/contentmanagementsystem/__init__.py +0 -0
- eventsourcing/examples/contentmanagementsystem/application.py +54 -0
- eventsourcing/examples/contentmanagementsystem/postgres.py +17 -0
- eventsourcing/examples/contentmanagementsystem/sqlite.py +17 -0
- eventsourcing/examples/contentmanagementsystem/system.py +14 -0
- eventsourcing/examples/contentmanagementsystem/test_system.py +180 -0
- eventsourcing/examples/searchablecontent/__init__.py +0 -0
- eventsourcing/examples/searchablecontent/application.py +45 -0
- eventsourcing/examples/searchablecontent/persistence.py +23 -0
- eventsourcing/examples/searchablecontent/postgres.py +118 -0
- eventsourcing/examples/searchablecontent/sqlite.py +136 -0
- eventsourcing/examples/searchablecontent/test_application.py +110 -0
- eventsourcing/examples/searchablecontent/test_recorder.py +68 -0
- eventsourcing/examples/searchabletimestamps/__init__.py +0 -0
- eventsourcing/examples/searchabletimestamps/application.py +32 -0
- eventsourcing/examples/searchabletimestamps/persistence.py +20 -0
- eventsourcing/examples/searchabletimestamps/postgres.py +110 -0
- eventsourcing/examples/searchabletimestamps/sqlite.py +99 -0
- eventsourcing/examples/searchabletimestamps/test_searchabletimestamps.py +94 -0
- eventsourcing/examples/test_invoice.py +176 -0
- eventsourcing/examples/test_parking_lot.py +206 -0
- eventsourcing/interface.py +4 -2
- eventsourcing/persistence.py +88 -82
- eventsourcing/popo.py +32 -31
- eventsourcing/postgres.py +388 -593
- eventsourcing/sqlite.py +100 -102
- eventsourcing/system.py +66 -71
- eventsourcing/tests/application.py +20 -32
- eventsourcing/tests/application_tests/__init__.py +0 -0
- eventsourcing/tests/application_tests/test_application_with_automatic_snapshotting.py +55 -0
- eventsourcing/tests/application_tests/test_application_with_popo.py +22 -0
- eventsourcing/tests/application_tests/test_application_with_postgres.py +75 -0
- eventsourcing/tests/application_tests/test_application_with_sqlite.py +72 -0
- eventsourcing/tests/application_tests/test_cache.py +134 -0
- eventsourcing/tests/application_tests/test_event_sourced_log.py +162 -0
- eventsourcing/tests/application_tests/test_notificationlog.py +232 -0
- eventsourcing/tests/application_tests/test_notificationlogreader.py +126 -0
- eventsourcing/tests/application_tests/test_processapplication.py +110 -0
- eventsourcing/tests/application_tests/test_processingpolicy.py +109 -0
- eventsourcing/tests/application_tests/test_repository.py +504 -0
- eventsourcing/tests/application_tests/test_snapshotting.py +68 -0
- eventsourcing/tests/application_tests/test_upcasting.py +459 -0
- eventsourcing/tests/docs_tests/__init__.py +0 -0
- eventsourcing/tests/docs_tests/test_docs.py +293 -0
- eventsourcing/tests/domain.py +1 -1
- eventsourcing/tests/domain_tests/__init__.py +0 -0
- eventsourcing/tests/domain_tests/test_aggregate.py +1180 -0
- eventsourcing/tests/domain_tests/test_aggregate_decorators.py +1604 -0
- eventsourcing/tests/domain_tests/test_domainevent.py +80 -0
- eventsourcing/tests/interface_tests/__init__.py +0 -0
- eventsourcing/tests/interface_tests/test_remotenotificationlog.py +258 -0
- eventsourcing/tests/persistence.py +52 -50
- eventsourcing/tests/persistence_tests/__init__.py +0 -0
- eventsourcing/tests/persistence_tests/test_aes.py +93 -0
- eventsourcing/tests/persistence_tests/test_connection_pool.py +722 -0
- eventsourcing/tests/persistence_tests/test_eventstore.py +72 -0
- eventsourcing/tests/persistence_tests/test_infrastructure_factory.py +21 -0
- eventsourcing/tests/persistence_tests/test_mapper.py +113 -0
- eventsourcing/tests/persistence_tests/test_noninterleaving_notification_ids.py +69 -0
- eventsourcing/tests/persistence_tests/test_popo.py +124 -0
- eventsourcing/tests/persistence_tests/test_postgres.py +1119 -0
- eventsourcing/tests/persistence_tests/test_sqlite.py +348 -0
- eventsourcing/tests/persistence_tests/test_transcoder.py +44 -0
- eventsourcing/tests/postgres_utils.py +7 -7
- eventsourcing/tests/system_tests/__init__.py +0 -0
- eventsourcing/tests/system_tests/test_runner.py +935 -0
- eventsourcing/tests/system_tests/test_system.py +284 -0
- eventsourcing/tests/utils_tests/__init__.py +0 -0
- eventsourcing/tests/utils_tests/test_utils.py +226 -0
- eventsourcing/utils.py +49 -50
- {eventsourcing-9.2.21.dist-info → eventsourcing-9.3.0.dist-info}/METADATA +30 -33
- eventsourcing-9.3.0.dist-info/RECORD +145 -0
- {eventsourcing-9.2.21.dist-info → eventsourcing-9.3.0.dist-info}/WHEEL +1 -2
- eventsourcing-9.2.21.dist-info/RECORD +0 -25
- eventsourcing-9.2.21.dist-info/top_level.txt +0 -1
- {eventsourcing-9.2.21.dist-info → eventsourcing-9.3.0.dist-info}/AUTHORS +0 -0
- {eventsourcing-9.2.21.dist-info → eventsourcing-9.3.0.dist-info}/LICENSE +0 -0
eventsourcing/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "9.
|
|
1
|
+
__version__ = "9.3.0dev0"
|
eventsourcing/application.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import os
|
|
2
4
|
from abc import ABC, abstractmethod
|
|
3
5
|
from copy import deepcopy
|
|
@@ -5,8 +7,10 @@ from dataclasses import dataclass
|
|
|
5
7
|
from itertools import chain
|
|
6
8
|
from threading import Event, Lock
|
|
7
9
|
from typing import (
|
|
10
|
+
TYPE_CHECKING,
|
|
8
11
|
Any,
|
|
9
12
|
Callable,
|
|
13
|
+
ClassVar,
|
|
10
14
|
Dict,
|
|
11
15
|
Generic,
|
|
12
16
|
Iterable,
|
|
@@ -17,16 +21,10 @@ from typing import (
|
|
|
17
21
|
Tuple,
|
|
18
22
|
Type,
|
|
19
23
|
TypeVar,
|
|
20
|
-
Union,
|
|
21
24
|
cast,
|
|
22
25
|
)
|
|
23
|
-
from uuid import UUID
|
|
24
26
|
from warnings import warn
|
|
25
27
|
|
|
26
|
-
# For backwards compatibility of import statements...
|
|
27
|
-
from eventsourcing.domain import LogEvent # noqa: F401
|
|
28
|
-
from eventsourcing.domain import TLogEvent # noqa: F401
|
|
29
|
-
from eventsourcing.domain import create_utc_datetime_now # noqa: F401
|
|
30
28
|
from eventsourcing.domain import (
|
|
31
29
|
Aggregate,
|
|
32
30
|
CanMutateProtocol,
|
|
@@ -39,6 +37,7 @@ from eventsourcing.domain import (
|
|
|
39
37
|
SnapshotProtocol,
|
|
40
38
|
TDomainEvent,
|
|
41
39
|
TMutableOrImmutableAggregate,
|
|
40
|
+
create_utc_datetime_now,
|
|
42
41
|
)
|
|
43
42
|
from eventsourcing.persistence import (
|
|
44
43
|
ApplicationRecorder,
|
|
@@ -55,6 +54,9 @@ from eventsourcing.persistence import (
|
|
|
55
54
|
)
|
|
56
55
|
from eventsourcing.utils import Environment, EnvType, strtobool
|
|
57
56
|
|
|
57
|
+
if TYPE_CHECKING: # pragma: nocover
|
|
58
|
+
from uuid import UUID
|
|
59
|
+
|
|
58
60
|
ProjectorFunction = Callable[
|
|
59
61
|
[Optional[TMutableOrImmutableAggregate], Iterable[TDomainEvent]],
|
|
60
62
|
Optional[TMutableOrImmutableAggregate],
|
|
@@ -67,9 +69,9 @@ MutatorFunction = Callable[
|
|
|
67
69
|
|
|
68
70
|
|
|
69
71
|
def project_aggregate(
|
|
70
|
-
aggregate:
|
|
72
|
+
aggregate: TMutableOrImmutableAggregate | None,
|
|
71
73
|
domain_events: Iterable[DomainEventProtocol],
|
|
72
|
-
) ->
|
|
74
|
+
) -> TMutableOrImmutableAggregate | None:
|
|
73
75
|
"""
|
|
74
76
|
Projector function for aggregate projections, which works
|
|
75
77
|
by successively calling aggregate mutator function mutate()
|
|
@@ -89,13 +91,12 @@ class Cache(Generic[S, T]):
|
|
|
89
91
|
def __init__(self) -> None:
|
|
90
92
|
self.cache: Dict[S, Any] = {}
|
|
91
93
|
|
|
92
|
-
def get(self, key: S, evict: bool = False) -> T:
|
|
94
|
+
def get(self, key: S, *, evict: bool = False) -> T:
|
|
93
95
|
if evict:
|
|
94
96
|
return self.cache.pop(key)
|
|
95
|
-
|
|
96
|
-
return self.cache[key]
|
|
97
|
+
return self.cache[key]
|
|
97
98
|
|
|
98
|
-
def put(self, key: S, value: T) ->
|
|
99
|
+
def put(self, key: S, value: T) -> T | None:
|
|
99
100
|
if value is not None:
|
|
100
101
|
self.cache[key] = value
|
|
101
102
|
return None
|
|
@@ -130,7 +131,7 @@ class LRUCache(Cache[S, T]):
|
|
|
130
131
|
None,
|
|
131
132
|
] # initialize by pointing to self
|
|
132
133
|
|
|
133
|
-
def get(self, key: S, evict: bool = False) -> T:
|
|
134
|
+
def get(self, key: S, *, evict: bool = False) -> T:
|
|
134
135
|
with self.lock:
|
|
135
136
|
link = self.cache.get(key)
|
|
136
137
|
if link is not None:
|
|
@@ -151,10 +152,9 @@ class LRUCache(Cache[S, T]):
|
|
|
151
152
|
self.full = self.cache.__len__() >= self.maxsize
|
|
152
153
|
|
|
153
154
|
return result
|
|
154
|
-
|
|
155
|
-
raise KeyError
|
|
155
|
+
raise KeyError
|
|
156
156
|
|
|
157
|
-
def put(self, key: S, value: T) ->
|
|
157
|
+
def put(self, key: S, value: T) -> Any | None:
|
|
158
158
|
evicted_key = None
|
|
159
159
|
evicted_value = None
|
|
160
160
|
with self.lock:
|
|
@@ -213,8 +213,9 @@ class Repository:
|
|
|
213
213
|
def __init__(
|
|
214
214
|
self,
|
|
215
215
|
event_store: EventStore,
|
|
216
|
-
|
|
217
|
-
|
|
216
|
+
*,
|
|
217
|
+
snapshot_store: EventStore | None = None,
|
|
218
|
+
cache_maxsize: int | None = None,
|
|
218
219
|
fastforward: bool = True,
|
|
219
220
|
fastforward_skipping: bool = False,
|
|
220
221
|
deepcopy_from_cache: bool = True,
|
|
@@ -231,7 +232,7 @@ class Repository:
|
|
|
231
232
|
self.snapshot_store = snapshot_store
|
|
232
233
|
|
|
233
234
|
if cache_maxsize is None:
|
|
234
|
-
self.cache:
|
|
235
|
+
self.cache: Cache[UUID, MutableOrImmutableAggregate] | None = None
|
|
235
236
|
elif cache_maxsize <= 0:
|
|
236
237
|
self.cache = Cache()
|
|
237
238
|
else:
|
|
@@ -250,7 +251,8 @@ class Repository:
|
|
|
250
251
|
def get(
|
|
251
252
|
self,
|
|
252
253
|
aggregate_id: UUID,
|
|
253
|
-
|
|
254
|
+
*,
|
|
255
|
+
version: int | None = None,
|
|
254
256
|
projector_func: ProjectorFunction[
|
|
255
257
|
TMutableOrImmutableAggregate, TDomainEvent
|
|
256
258
|
] = project_aggregate,
|
|
@@ -289,9 +291,8 @@ class Repository:
|
|
|
289
291
|
aggregate, cast(Iterable[TDomainEvent], new_events)
|
|
290
292
|
)
|
|
291
293
|
if _aggregate is None:
|
|
292
|
-
raise
|
|
293
|
-
|
|
294
|
-
aggregate = _aggregate
|
|
294
|
+
raise AggregateNotFoundError(aggregate_id)
|
|
295
|
+
aggregate = _aggregate
|
|
295
296
|
finally:
|
|
296
297
|
fastforward_lock.release()
|
|
297
298
|
finally:
|
|
@@ -310,10 +311,10 @@ class Repository:
|
|
|
310
311
|
def _reconstruct_aggregate(
|
|
311
312
|
self,
|
|
312
313
|
aggregate_id: UUID,
|
|
313
|
-
version:
|
|
314
|
+
version: int | None,
|
|
314
315
|
projector_func: ProjectorFunction[TMutableOrImmutableAggregate, TDomainEvent],
|
|
315
316
|
) -> TMutableOrImmutableAggregate:
|
|
316
|
-
gt:
|
|
317
|
+
gt: int | None = None
|
|
317
318
|
|
|
318
319
|
if self.snapshot_store is not None:
|
|
319
320
|
# Try to get a snapshot.
|
|
@@ -338,7 +339,7 @@ class Repository:
|
|
|
338
339
|
)
|
|
339
340
|
|
|
340
341
|
# Reconstruct the aggregate from its events.
|
|
341
|
-
initial:
|
|
342
|
+
initial: TMutableOrImmutableAggregate | None = None
|
|
342
343
|
aggregate = projector_func(
|
|
343
344
|
initial,
|
|
344
345
|
chain(
|
|
@@ -349,10 +350,9 @@ class Repository:
|
|
|
349
350
|
|
|
350
351
|
# Raise exception if "not found".
|
|
351
352
|
if aggregate is None:
|
|
352
|
-
raise
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
return aggregate
|
|
353
|
+
raise AggregateNotFoundError((aggregate_id, version))
|
|
354
|
+
# Return the aggregate.
|
|
355
|
+
return aggregate
|
|
356
356
|
|
|
357
357
|
def _use_fastforward_lock(self, aggregate_id: UUID) -> Lock:
|
|
358
358
|
with self._fastforward_locks_lock:
|
|
@@ -386,7 +386,7 @@ class Repository:
|
|
|
386
386
|
"""
|
|
387
387
|
try:
|
|
388
388
|
self.get(aggregate_id=item)
|
|
389
|
-
except
|
|
389
|
+
except AggregateNotFoundError:
|
|
390
390
|
return False
|
|
391
391
|
else:
|
|
392
392
|
return True
|
|
@@ -412,9 +412,9 @@ class Section:
|
|
|
412
412
|
:param Optional[str] next_id: section ID of the following section
|
|
413
413
|
"""
|
|
414
414
|
|
|
415
|
-
id:
|
|
415
|
+
id: str | None
|
|
416
416
|
items: List[Notification]
|
|
417
|
-
next_id:
|
|
417
|
+
next_id: str | None
|
|
418
418
|
|
|
419
419
|
|
|
420
420
|
class NotificationLog(ABC):
|
|
@@ -435,7 +435,7 @@ class NotificationLog(ABC):
|
|
|
435
435
|
self,
|
|
436
436
|
start: int,
|
|
437
437
|
limit: int,
|
|
438
|
-
stop:
|
|
438
|
+
stop: int | None = None,
|
|
439
439
|
topics: Sequence[str] = (),
|
|
440
440
|
) -> List[Notification]:
|
|
441
441
|
"""
|
|
@@ -495,8 +495,8 @@ class LocalNotificationLog(NotificationLog):
|
|
|
495
495
|
notifications = self.select(start, limit)
|
|
496
496
|
|
|
497
497
|
# Get next section ID.
|
|
498
|
-
actual_section_id:
|
|
499
|
-
next_id:
|
|
498
|
+
actual_section_id: str | None
|
|
499
|
+
next_id: str | None
|
|
500
500
|
if len(notifications):
|
|
501
501
|
last_notification_id = notifications[-1].id
|
|
502
502
|
actual_section_id = self.format_section_id(
|
|
@@ -523,7 +523,7 @@ class LocalNotificationLog(NotificationLog):
|
|
|
523
523
|
self,
|
|
524
524
|
start: int,
|
|
525
525
|
limit: int,
|
|
526
|
-
stop:
|
|
526
|
+
stop: int | None = None,
|
|
527
527
|
topics: Sequence[str] = (),
|
|
528
528
|
) -> List[Notification]:
|
|
529
529
|
"""
|
|
@@ -532,16 +532,17 @@ class LocalNotificationLog(NotificationLog):
|
|
|
532
532
|
from the notification log.
|
|
533
533
|
"""
|
|
534
534
|
if limit > self.section_size:
|
|
535
|
-
|
|
535
|
+
msg = (
|
|
536
536
|
f"Requested limit {limit} greater than section size {self.section_size}"
|
|
537
537
|
)
|
|
538
|
+
raise ValueError(msg)
|
|
538
539
|
return self.recorder.select_notifications(
|
|
539
540
|
start=start, limit=limit, stop=stop, topics=topics
|
|
540
541
|
)
|
|
541
542
|
|
|
542
543
|
@staticmethod
|
|
543
544
|
def format_section_id(first_id: int, last_id: int) -> str:
|
|
544
|
-
return "{},{}"
|
|
545
|
+
return f"{first_id},{last_id}"
|
|
545
546
|
|
|
546
547
|
|
|
547
548
|
class ProcessingEvent:
|
|
@@ -552,7 +553,7 @@ class ProcessingEvent:
|
|
|
552
553
|
new domain events that result from processing that notification.
|
|
553
554
|
"""
|
|
554
555
|
|
|
555
|
-
def __init__(self, tracking:
|
|
556
|
+
def __init__(self, tracking: Tracking | None = None):
|
|
556
557
|
"""
|
|
557
558
|
Initialises the process event with the given tracking object.
|
|
558
559
|
"""
|
|
@@ -563,7 +564,7 @@ class ProcessingEvent:
|
|
|
563
564
|
|
|
564
565
|
def collect_events(
|
|
565
566
|
self,
|
|
566
|
-
*objs:
|
|
567
|
+
*objs: MutableOrImmutableAggregate | DomainEventProtocol | None,
|
|
567
568
|
**kwargs: Any,
|
|
568
569
|
) -> None:
|
|
569
570
|
"""
|
|
@@ -572,7 +573,7 @@ class ProcessingEvent:
|
|
|
572
573
|
for obj in objs:
|
|
573
574
|
if obj is None:
|
|
574
575
|
continue
|
|
575
|
-
|
|
576
|
+
if isinstance(obj, DomainEventProtocol):
|
|
576
577
|
self.events.append(obj)
|
|
577
578
|
else:
|
|
578
579
|
if isinstance(obj, CollectEventsProtocol):
|
|
@@ -584,7 +585,7 @@ class ProcessingEvent:
|
|
|
584
585
|
|
|
585
586
|
def save(
|
|
586
587
|
self,
|
|
587
|
-
*aggregates:
|
|
588
|
+
*aggregates: MutableOrImmutableAggregate | DomainEventProtocol | None,
|
|
588
589
|
**kwargs: Any,
|
|
589
590
|
) -> None:
|
|
590
591
|
warn(
|
|
@@ -596,31 +597,12 @@ class ProcessingEvent:
|
|
|
596
597
|
self.collect_events(*aggregates, **kwargs)
|
|
597
598
|
|
|
598
599
|
|
|
599
|
-
class ProcessEvent(ProcessingEvent):
|
|
600
|
-
"""Deprecated, use :class:`ProcessingEvent` instead.
|
|
601
|
-
|
|
602
|
-
Keeps together a :class:`~eventsourcing.persistence.Tracking`
|
|
603
|
-
object, which represents the position of a domain event notification
|
|
604
|
-
in the notification log of a particular application, and the
|
|
605
|
-
new domain events that result from processing that notification.
|
|
606
|
-
"""
|
|
607
|
-
|
|
608
|
-
def __init__(self, tracking: Optional[Tracking] = None):
|
|
609
|
-
warn(
|
|
610
|
-
"'ProcessEvent' is deprecated, use 'ProcessingEvent' instead",
|
|
611
|
-
DeprecationWarning,
|
|
612
|
-
stacklevel=2,
|
|
613
|
-
)
|
|
614
|
-
|
|
615
|
-
super().__init__(tracking)
|
|
616
|
-
|
|
617
|
-
|
|
618
600
|
class RecordingEvent:
|
|
619
601
|
def __init__(
|
|
620
602
|
self,
|
|
621
603
|
application_name: str,
|
|
622
604
|
recordings: List[Recording],
|
|
623
|
-
previous_max_notification_id:
|
|
605
|
+
previous_max_notification_id: int | None,
|
|
624
606
|
):
|
|
625
607
|
self.application_name = application_name
|
|
626
608
|
self.recordings = recordings
|
|
@@ -633,13 +615,13 @@ class Application:
|
|
|
633
615
|
"""
|
|
634
616
|
|
|
635
617
|
name = "Application"
|
|
636
|
-
env:
|
|
618
|
+
env: ClassVar[Dict[str, str]] = {}
|
|
637
619
|
is_snapshotting_enabled: bool = False
|
|
638
|
-
snapshotting_intervals:
|
|
639
|
-
Dict[Type[MutableOrImmutableAggregate], int]
|
|
620
|
+
snapshotting_intervals: ClassVar[
|
|
621
|
+
Dict[Type[MutableOrImmutableAggregate], int] | None
|
|
640
622
|
] = None
|
|
641
|
-
snapshotting_projectors:
|
|
642
|
-
Dict[Type[MutableOrImmutableAggregate], ProjectorFunction[Any, Any]]
|
|
623
|
+
snapshotting_projectors: ClassVar[
|
|
624
|
+
Dict[Type[MutableOrImmutableAggregate], ProjectorFunction[Any, Any]] | None
|
|
643
625
|
] = None
|
|
644
626
|
snapshot_class: Type[SnapshotProtocol] = Snapshot
|
|
645
627
|
log_section_size = 10
|
|
@@ -654,7 +636,7 @@ class Application:
|
|
|
654
636
|
if "name" not in cls.__dict__:
|
|
655
637
|
cls.name = cls.__name__
|
|
656
638
|
|
|
657
|
-
def __init__(self, env:
|
|
639
|
+
def __init__(self, env: EnvType | None = None) -> None:
|
|
658
640
|
"""
|
|
659
641
|
Initialises an application with an
|
|
660
642
|
:class:`~eventsourcing.persistence.InfrastructureFactory`,
|
|
@@ -664,20 +646,36 @@ class Application:
|
|
|
664
646
|
a :class:`~eventsourcing.application.Repository`, and
|
|
665
647
|
a :class:`~eventsourcing.application.LocalNotificationLog`.
|
|
666
648
|
"""
|
|
667
|
-
self.env = self.construct_env(self.name, env)
|
|
649
|
+
self.env = self.construct_env(self.name, env) # type: ignore[misc]
|
|
668
650
|
self.factory = self.construct_factory(self.env)
|
|
669
651
|
self.mapper = self.construct_mapper()
|
|
670
652
|
self.recorder = self.construct_recorder()
|
|
671
653
|
self.events = self.construct_event_store()
|
|
672
|
-
self.snapshots:
|
|
654
|
+
self.snapshots: EventStore | None = None
|
|
673
655
|
if self.factory.is_snapshotting_enabled():
|
|
674
656
|
self.snapshots = self.construct_snapshot_store()
|
|
675
|
-
self.
|
|
676
|
-
self.
|
|
657
|
+
self._repository = self.construct_repository()
|
|
658
|
+
self._notification_log = self.construct_notification_log()
|
|
677
659
|
self.closing = Event()
|
|
678
|
-
self.previous_max_notification_id:
|
|
679
|
-
|
|
680
|
-
|
|
660
|
+
self.previous_max_notification_id: int | None = (
|
|
661
|
+
self.recorder.max_notification_id()
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
@property
|
|
665
|
+
def repository(self) -> Repository:
|
|
666
|
+
"""
|
|
667
|
+
An application's repository reconstructs aggregates from stored events.
|
|
668
|
+
"""
|
|
669
|
+
return self._repository
|
|
670
|
+
|
|
671
|
+
@property
|
|
672
|
+
def notification_log(self) -> LocalNotificationLog:
|
|
673
|
+
"""
|
|
674
|
+
An application's notification log presents all the aggregate events
|
|
675
|
+
of an application in the order they were recorded as a sequence of event
|
|
676
|
+
notifications.
|
|
677
|
+
"""
|
|
678
|
+
return self._notification_log
|
|
681
679
|
|
|
682
680
|
@property
|
|
683
681
|
def log(self) -> LocalNotificationLog:
|
|
@@ -686,9 +684,9 @@ class Application:
|
|
|
686
684
|
DeprecationWarning,
|
|
687
685
|
stacklevel=2,
|
|
688
686
|
)
|
|
689
|
-
return self.
|
|
687
|
+
return self._notification_log
|
|
690
688
|
|
|
691
|
-
def construct_env(self, name: str, env:
|
|
689
|
+
def construct_env(self, name: str, env: EnvType | None = None) -> Environment:
|
|
692
690
|
"""
|
|
693
691
|
Constructs environment from which application will be configured.
|
|
694
692
|
"""
|
|
@@ -767,10 +765,7 @@ class Application:
|
|
|
767
765
|
Constructs a :class:`Repository` for use by the application.
|
|
768
766
|
"""
|
|
769
767
|
cache_maxsize_envvar = self.env.get(self.AGGREGATE_CACHE_MAXSIZE)
|
|
770
|
-
if cache_maxsize_envvar
|
|
771
|
-
cache_maxsize = int(cache_maxsize_envvar)
|
|
772
|
-
else:
|
|
773
|
-
cache_maxsize = None
|
|
768
|
+
cache_maxsize = int(cache_maxsize_envvar) if cache_maxsize_envvar else None
|
|
774
769
|
return Repository(
|
|
775
770
|
event_store=self.events,
|
|
776
771
|
snapshot_store=self.snapshots,
|
|
@@ -792,7 +787,7 @@ class Application:
|
|
|
792
787
|
|
|
793
788
|
def save(
|
|
794
789
|
self,
|
|
795
|
-
*objs:
|
|
790
|
+
*objs: MutableOrImmutableAggregate | DomainEventProtocol | None,
|
|
796
791
|
**kwargs: Any,
|
|
797
792
|
) -> List[Recording]:
|
|
798
793
|
"""
|
|
@@ -830,40 +825,39 @@ class Application:
|
|
|
830
825
|
except KeyError:
|
|
831
826
|
continue
|
|
832
827
|
interval = self.snapshotting_intervals.get(type(aggregate))
|
|
833
|
-
if interval is not None:
|
|
834
|
-
if
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
"snapshotting_projectors on application class."
|
|
855
|
-
)
|
|
856
|
-
)
|
|
857
|
-
self.take_snapshot(
|
|
858
|
-
aggregate_id=event.originator_id,
|
|
859
|
-
version=event.originator_version,
|
|
860
|
-
projector_func=projector_func,
|
|
828
|
+
if interval is not None and event.originator_version % interval == 0:
|
|
829
|
+
if (
|
|
830
|
+
self.snapshotting_projectors
|
|
831
|
+
and type(aggregate) in self.snapshotting_projectors
|
|
832
|
+
):
|
|
833
|
+
projector_func = self.snapshotting_projectors[type(aggregate)]
|
|
834
|
+
else:
|
|
835
|
+
projector_func = project_aggregate
|
|
836
|
+
if projector_func is project_aggregate and not isinstance(
|
|
837
|
+
event, CanMutateProtocol
|
|
838
|
+
):
|
|
839
|
+
msg = (
|
|
840
|
+
f"Cannot take snapshot for {type(aggregate)} with "
|
|
841
|
+
"default project_aggregate() function, because its "
|
|
842
|
+
f"domain event {type(event)} does not implement "
|
|
843
|
+
"the 'can mutate' protocol (see CanMutateProtocol)."
|
|
844
|
+
f" Please define application class {type(self)}"
|
|
845
|
+
" with class variable 'snapshotting_projectors', "
|
|
846
|
+
f"to be a dict that has {type(aggregate)} as a key "
|
|
847
|
+
"with the aggregate projector function for "
|
|
848
|
+
f"{type(aggregate)} as the value for that key."
|
|
861
849
|
)
|
|
850
|
+
raise ProgrammingError(msg)
|
|
851
|
+
self.take_snapshot(
|
|
852
|
+
aggregate_id=event.originator_id,
|
|
853
|
+
version=event.originator_version,
|
|
854
|
+
projector_func=projector_func,
|
|
855
|
+
)
|
|
862
856
|
|
|
863
857
|
def take_snapshot(
|
|
864
858
|
self,
|
|
865
859
|
aggregate_id: UUID,
|
|
866
|
-
version:
|
|
860
|
+
version: int | None = None,
|
|
867
861
|
projector_func: ProjectorFunction[
|
|
868
862
|
TMutableOrImmutableAggregate, TDomainEvent
|
|
869
863
|
] = project_aggregate,
|
|
@@ -873,19 +867,20 @@ class Application:
|
|
|
873
867
|
and puts the snapshot in the snapshot store.
|
|
874
868
|
"""
|
|
875
869
|
if self.snapshots is None:
|
|
876
|
-
|
|
870
|
+
msg = (
|
|
877
871
|
"Can't take snapshot without snapshots store. Please "
|
|
878
872
|
"set environment variable IS_SNAPSHOTTING_ENABLED to "
|
|
879
873
|
"a true value (e.g. 'y'), or set 'is_snapshotting_enabled' "
|
|
880
874
|
"on application class, or set 'snapshotting_intervals' on "
|
|
881
875
|
"application class."
|
|
882
876
|
)
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
877
|
+
raise AssertionError(msg)
|
|
878
|
+
aggregate = self.repository.get(
|
|
879
|
+
aggregate_id, version=version, projector_func=projector_func
|
|
880
|
+
)
|
|
881
|
+
snapshot_class = getattr(type(aggregate), "Snapshot", type(self).snapshot_class)
|
|
882
|
+
snapshot = snapshot_class.take(aggregate)
|
|
883
|
+
self.snapshots.put([snapshot])
|
|
889
884
|
|
|
890
885
|
def notify(self, new_events: List[DomainEventProtocol]) -> None:
|
|
891
886
|
"""
|
|
@@ -913,13 +908,23 @@ class Application:
|
|
|
913
908
|
TApplication = TypeVar("TApplication", bound=Application)
|
|
914
909
|
|
|
915
910
|
|
|
916
|
-
class
|
|
911
|
+
class AggregateNotFoundError(EventSourcingError):
|
|
917
912
|
"""
|
|
918
913
|
Raised when an :class:`~eventsourcing.domain.Aggregate`
|
|
919
914
|
object is not found in a :class:`Repository`.
|
|
920
915
|
"""
|
|
921
916
|
|
|
922
917
|
|
|
918
|
+
class AggregateNotFound(AggregateNotFoundError): # noqa: N818
|
|
919
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
920
|
+
warn(
|
|
921
|
+
"AggregateNotFound is deprecated, use AggregateNotFoundError instead",
|
|
922
|
+
DeprecationWarning,
|
|
923
|
+
stacklevel=2,
|
|
924
|
+
)
|
|
925
|
+
super().__init__(*args, **kwargs)
|
|
926
|
+
|
|
927
|
+
|
|
923
928
|
class EventSourcedLog(Generic[TDomainEvent]):
|
|
924
929
|
"""
|
|
925
930
|
Constructs a sequence of domain events, like an aggregate.
|
|
@@ -939,15 +944,15 @@ class EventSourcedLog(Generic[TDomainEvent]):
|
|
|
939
944
|
self,
|
|
940
945
|
events: EventStore,
|
|
941
946
|
originator_id: UUID,
|
|
942
|
-
logged_cls: Type[TDomainEvent], #
|
|
947
|
+
logged_cls: Type[TDomainEvent], # TODO: Rename to 'event_class' in v10.
|
|
943
948
|
):
|
|
944
949
|
self.events = events
|
|
945
950
|
self.originator_id = originator_id
|
|
946
|
-
self.logged_cls = logged_cls #
|
|
951
|
+
self.logged_cls = logged_cls # TODO: Rename to 'event_class' in v10.
|
|
947
952
|
|
|
948
953
|
def trigger_event(
|
|
949
954
|
self,
|
|
950
|
-
next_originator_version:
|
|
955
|
+
next_originator_version: int | None = None,
|
|
951
956
|
**kwargs: Any,
|
|
952
957
|
) -> TDomainEvent:
|
|
953
958
|
"""
|
|
@@ -961,8 +966,8 @@ class EventSourcedLog(Generic[TDomainEvent]):
|
|
|
961
966
|
|
|
962
967
|
def _trigger_event(
|
|
963
968
|
self,
|
|
964
|
-
logged_cls:
|
|
965
|
-
next_originator_version:
|
|
969
|
+
logged_cls: Type[T] | None,
|
|
970
|
+
next_originator_version: int | None = None,
|
|
966
971
|
**kwargs: Any,
|
|
967
972
|
) -> T:
|
|
968
973
|
"""
|
|
@@ -975,15 +980,14 @@ class EventSourcedLog(Generic[TDomainEvent]):
|
|
|
975
980
|
else:
|
|
976
981
|
next_originator_version = last_logged.originator_version + 1
|
|
977
982
|
|
|
978
|
-
|
|
983
|
+
return logged_cls( # type: ignore
|
|
979
984
|
originator_id=self.originator_id,
|
|
980
985
|
originator_version=next_originator_version,
|
|
981
986
|
timestamp=create_utc_datetime_now(),
|
|
982
987
|
**kwargs,
|
|
983
988
|
)
|
|
984
|
-
return logged_event
|
|
985
989
|
|
|
986
|
-
def get_first(self) ->
|
|
990
|
+
def get_first(self) -> TDomainEvent | None:
|
|
987
991
|
"""
|
|
988
992
|
Selects the first logged event.
|
|
989
993
|
"""
|
|
@@ -992,7 +996,7 @@ class EventSourcedLog(Generic[TDomainEvent]):
|
|
|
992
996
|
except StopIteration:
|
|
993
997
|
return None
|
|
994
998
|
|
|
995
|
-
def get_last(self) ->
|
|
999
|
+
def get_last(self) -> TDomainEvent | None:
|
|
996
1000
|
"""
|
|
997
1001
|
Selects the last logged event.
|
|
998
1002
|
"""
|
|
@@ -1003,10 +1007,11 @@ class EventSourcedLog(Generic[TDomainEvent]):
|
|
|
1003
1007
|
|
|
1004
1008
|
def get(
|
|
1005
1009
|
self,
|
|
1006
|
-
|
|
1007
|
-
|
|
1010
|
+
*,
|
|
1011
|
+
gt: int | None = None,
|
|
1012
|
+
lte: int | None = None,
|
|
1008
1013
|
desc: bool = False,
|
|
1009
|
-
limit:
|
|
1014
|
+
limit: int | None = None,
|
|
1010
1015
|
) -> Iterator[TDomainEvent]:
|
|
1011
1016
|
"""
|
|
1012
1017
|
Selects a range of logged events with limit,
|
eventsourcing/cipher.py
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import os
|
|
2
4
|
from base64 import b64decode, b64encode
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
3
6
|
|
|
4
7
|
from Crypto.Cipher import AES
|
|
5
8
|
from Crypto.Cipher._mode_gcm import GcmMode
|
|
6
9
|
from Crypto.Cipher.AES import key_size
|
|
7
10
|
|
|
8
11
|
from eventsourcing.persistence import Cipher
|
|
9
|
-
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING: # pragma: nocover
|
|
14
|
+
from eventsourcing.utils import Environment
|
|
10
15
|
|
|
11
16
|
|
|
12
17
|
class AESCipher(Cipher):
|
|
@@ -31,9 +36,8 @@ class AESCipher(Cipher):
|
|
|
31
36
|
@staticmethod
|
|
32
37
|
def check_key_size(num_bytes: int) -> None:
|
|
33
38
|
if num_bytes not in AESCipher.KEY_SIZES:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
)
|
|
39
|
+
msg = f"Invalid key size: {num_bytes} not in {AESCipher.KEY_SIZES}"
|
|
40
|
+
raise ValueError(msg)
|
|
37
41
|
|
|
38
42
|
@staticmethod
|
|
39
43
|
def random_bytes(num_bytes: int) -> bytes:
|
|
@@ -47,7 +51,8 @@ class AESCipher(Cipher):
|
|
|
47
51
|
"""
|
|
48
52
|
cipher_key = environment.get(self.CIPHER_KEY)
|
|
49
53
|
if not cipher_key:
|
|
50
|
-
|
|
54
|
+
msg = f"'{self.CIPHER_KEY}' not in env"
|
|
55
|
+
raise OSError(msg)
|
|
51
56
|
key = b64decode(cipher_key.encode("utf8"))
|
|
52
57
|
AESCipher.check_key_size(len(key))
|
|
53
58
|
self.key = key
|
|
@@ -64,11 +69,8 @@ class AESCipher(Cipher):
|
|
|
64
69
|
encrypted = result[0]
|
|
65
70
|
tag = result[1]
|
|
66
71
|
|
|
67
|
-
# Combine with nonce.
|
|
68
|
-
ciphertext = nonce + tag + encrypted
|
|
69
|
-
|
|
70
72
|
# Return ciphertext.
|
|
71
|
-
return
|
|
73
|
+
return nonce + tag + encrypted
|
|
72
74
|
|
|
73
75
|
def construct_cipher(self, nonce: bytes) -> GcmMode:
|
|
74
76
|
cipher = AES.new(
|
|
@@ -85,11 +87,13 @@ class AESCipher(Cipher):
|
|
|
85
87
|
# Split out the nonce, tag, and encrypted data.
|
|
86
88
|
nonce = ciphertext[:12]
|
|
87
89
|
if len(nonce) != 12:
|
|
88
|
-
|
|
90
|
+
msg = "Damaged cipher text: invalid nonce length"
|
|
91
|
+
raise ValueError(msg)
|
|
89
92
|
|
|
90
93
|
tag = ciphertext[12:28]
|
|
91
94
|
if len(tag) != 16:
|
|
92
|
-
|
|
95
|
+
msg = "Damaged cipher text: invalid tag length"
|
|
96
|
+
raise ValueError(msg)
|
|
93
97
|
encrypted = ciphertext[28:]
|
|
94
98
|
|
|
95
99
|
# Construct AES cipher, with old nonce.
|
|
@@ -99,5 +103,6 @@ class AESCipher(Cipher):
|
|
|
99
103
|
try:
|
|
100
104
|
plaintext = cipher.decrypt_and_verify(encrypted, tag)
|
|
101
105
|
except ValueError as e:
|
|
102
|
-
|
|
106
|
+
msg = f"Cipher text is damaged: {e}"
|
|
107
|
+
raise ValueError(msg) from None
|
|
103
108
|
return plaintext
|