eventsourcing 9.2.22__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 +116 -135
- eventsourcing/cipher.py +15 -12
- eventsourcing/dispatch.py +31 -91
- eventsourcing/domain.py +220 -226
- 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 +2 -2
- eventsourcing/persistence.py +85 -81
- eventsourcing/popo.py +30 -31
- eventsourcing/postgres.py +379 -590
- eventsourcing/sqlite.py +91 -99
- eventsourcing/system.py +52 -57
- 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 +47 -50
- {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0.dist-info}/METADATA +29 -79
- eventsourcing-9.3.0.dist-info/RECORD +145 -0
- {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0.dist-info}/WHEEL +1 -2
- eventsourcing-9.2.22.dist-info/RECORD +0 -25
- eventsourcing-9.2.22.dist-info/top_level.txt +0 -1
- {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0.dist-info}/AUTHORS +0 -0
- {eventsourcing-9.2.22.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
|
@@ -7,8 +7,10 @@ from dataclasses import dataclass
|
|
|
7
7
|
from itertools import chain
|
|
8
8
|
from threading import Event, Lock
|
|
9
9
|
from typing import (
|
|
10
|
+
TYPE_CHECKING,
|
|
10
11
|
Any,
|
|
11
12
|
Callable,
|
|
13
|
+
ClassVar,
|
|
12
14
|
Dict,
|
|
13
15
|
Generic,
|
|
14
16
|
Iterable,
|
|
@@ -19,16 +21,10 @@ from typing import (
|
|
|
19
21
|
Tuple,
|
|
20
22
|
Type,
|
|
21
23
|
TypeVar,
|
|
22
|
-
Union,
|
|
23
24
|
cast,
|
|
24
25
|
)
|
|
25
|
-
from uuid import UUID
|
|
26
26
|
from warnings import warn
|
|
27
27
|
|
|
28
|
-
# For backwards compatibility of import statements...
|
|
29
|
-
from eventsourcing.domain import LogEvent # noqa: F401
|
|
30
|
-
from eventsourcing.domain import TLogEvent # noqa: F401
|
|
31
|
-
from eventsourcing.domain import create_utc_datetime_now # noqa: F401
|
|
32
28
|
from eventsourcing.domain import (
|
|
33
29
|
Aggregate,
|
|
34
30
|
CanMutateProtocol,
|
|
@@ -41,6 +37,7 @@ from eventsourcing.domain import (
|
|
|
41
37
|
SnapshotProtocol,
|
|
42
38
|
TDomainEvent,
|
|
43
39
|
TMutableOrImmutableAggregate,
|
|
40
|
+
create_utc_datetime_now,
|
|
44
41
|
)
|
|
45
42
|
from eventsourcing.persistence import (
|
|
46
43
|
ApplicationRecorder,
|
|
@@ -57,6 +54,9 @@ from eventsourcing.persistence import (
|
|
|
57
54
|
)
|
|
58
55
|
from eventsourcing.utils import Environment, EnvType, strtobool
|
|
59
56
|
|
|
57
|
+
if TYPE_CHECKING: # pragma: nocover
|
|
58
|
+
from uuid import UUID
|
|
59
|
+
|
|
60
60
|
ProjectorFunction = Callable[
|
|
61
61
|
[Optional[TMutableOrImmutableAggregate], Iterable[TDomainEvent]],
|
|
62
62
|
Optional[TMutableOrImmutableAggregate],
|
|
@@ -69,9 +69,9 @@ MutatorFunction = Callable[
|
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
def project_aggregate(
|
|
72
|
-
aggregate:
|
|
72
|
+
aggregate: TMutableOrImmutableAggregate | None,
|
|
73
73
|
domain_events: Iterable[DomainEventProtocol],
|
|
74
|
-
) ->
|
|
74
|
+
) -> TMutableOrImmutableAggregate | None:
|
|
75
75
|
"""
|
|
76
76
|
Projector function for aggregate projections, which works
|
|
77
77
|
by successively calling aggregate mutator function mutate()
|
|
@@ -91,13 +91,12 @@ class Cache(Generic[S, T]):
|
|
|
91
91
|
def __init__(self) -> None:
|
|
92
92
|
self.cache: Dict[S, Any] = {}
|
|
93
93
|
|
|
94
|
-
def get(self, key: S, evict: bool = False) -> T:
|
|
94
|
+
def get(self, key: S, *, evict: bool = False) -> T:
|
|
95
95
|
if evict:
|
|
96
96
|
return self.cache.pop(key)
|
|
97
|
-
|
|
98
|
-
return self.cache[key]
|
|
97
|
+
return self.cache[key]
|
|
99
98
|
|
|
100
|
-
def put(self, key: S, value: T) ->
|
|
99
|
+
def put(self, key: S, value: T) -> T | None:
|
|
101
100
|
if value is not None:
|
|
102
101
|
self.cache[key] = value
|
|
103
102
|
return None
|
|
@@ -132,7 +131,7 @@ class LRUCache(Cache[S, T]):
|
|
|
132
131
|
None,
|
|
133
132
|
] # initialize by pointing to self
|
|
134
133
|
|
|
135
|
-
def get(self, key: S, evict: bool = False) -> T:
|
|
134
|
+
def get(self, key: S, *, evict: bool = False) -> T:
|
|
136
135
|
with self.lock:
|
|
137
136
|
link = self.cache.get(key)
|
|
138
137
|
if link is not None:
|
|
@@ -153,10 +152,9 @@ class LRUCache(Cache[S, T]):
|
|
|
153
152
|
self.full = self.cache.__len__() >= self.maxsize
|
|
154
153
|
|
|
155
154
|
return result
|
|
156
|
-
|
|
157
|
-
raise KeyError
|
|
155
|
+
raise KeyError
|
|
158
156
|
|
|
159
|
-
def put(self, key: S, value: T) ->
|
|
157
|
+
def put(self, key: S, value: T) -> Any | None:
|
|
160
158
|
evicted_key = None
|
|
161
159
|
evicted_value = None
|
|
162
160
|
with self.lock:
|
|
@@ -215,8 +213,9 @@ class Repository:
|
|
|
215
213
|
def __init__(
|
|
216
214
|
self,
|
|
217
215
|
event_store: EventStore,
|
|
218
|
-
|
|
219
|
-
|
|
216
|
+
*,
|
|
217
|
+
snapshot_store: EventStore | None = None,
|
|
218
|
+
cache_maxsize: int | None = None,
|
|
220
219
|
fastforward: bool = True,
|
|
221
220
|
fastforward_skipping: bool = False,
|
|
222
221
|
deepcopy_from_cache: bool = True,
|
|
@@ -233,7 +232,7 @@ class Repository:
|
|
|
233
232
|
self.snapshot_store = snapshot_store
|
|
234
233
|
|
|
235
234
|
if cache_maxsize is None:
|
|
236
|
-
self.cache:
|
|
235
|
+
self.cache: Cache[UUID, MutableOrImmutableAggregate] | None = None
|
|
237
236
|
elif cache_maxsize <= 0:
|
|
238
237
|
self.cache = Cache()
|
|
239
238
|
else:
|
|
@@ -252,7 +251,8 @@ class Repository:
|
|
|
252
251
|
def get(
|
|
253
252
|
self,
|
|
254
253
|
aggregate_id: UUID,
|
|
255
|
-
|
|
254
|
+
*,
|
|
255
|
+
version: int | None = None,
|
|
256
256
|
projector_func: ProjectorFunction[
|
|
257
257
|
TMutableOrImmutableAggregate, TDomainEvent
|
|
258
258
|
] = project_aggregate,
|
|
@@ -291,9 +291,8 @@ class Repository:
|
|
|
291
291
|
aggregate, cast(Iterable[TDomainEvent], new_events)
|
|
292
292
|
)
|
|
293
293
|
if _aggregate is None:
|
|
294
|
-
raise
|
|
295
|
-
|
|
296
|
-
aggregate = _aggregate
|
|
294
|
+
raise AggregateNotFoundError(aggregate_id)
|
|
295
|
+
aggregate = _aggregate
|
|
297
296
|
finally:
|
|
298
297
|
fastforward_lock.release()
|
|
299
298
|
finally:
|
|
@@ -312,10 +311,10 @@ class Repository:
|
|
|
312
311
|
def _reconstruct_aggregate(
|
|
313
312
|
self,
|
|
314
313
|
aggregate_id: UUID,
|
|
315
|
-
version:
|
|
314
|
+
version: int | None,
|
|
316
315
|
projector_func: ProjectorFunction[TMutableOrImmutableAggregate, TDomainEvent],
|
|
317
316
|
) -> TMutableOrImmutableAggregate:
|
|
318
|
-
gt:
|
|
317
|
+
gt: int | None = None
|
|
319
318
|
|
|
320
319
|
if self.snapshot_store is not None:
|
|
321
320
|
# Try to get a snapshot.
|
|
@@ -340,7 +339,7 @@ class Repository:
|
|
|
340
339
|
)
|
|
341
340
|
|
|
342
341
|
# Reconstruct the aggregate from its events.
|
|
343
|
-
initial:
|
|
342
|
+
initial: TMutableOrImmutableAggregate | None = None
|
|
344
343
|
aggregate = projector_func(
|
|
345
344
|
initial,
|
|
346
345
|
chain(
|
|
@@ -351,10 +350,9 @@ class Repository:
|
|
|
351
350
|
|
|
352
351
|
# Raise exception if "not found".
|
|
353
352
|
if aggregate is None:
|
|
354
|
-
raise
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
return aggregate
|
|
353
|
+
raise AggregateNotFoundError((aggregate_id, version))
|
|
354
|
+
# Return the aggregate.
|
|
355
|
+
return aggregate
|
|
358
356
|
|
|
359
357
|
def _use_fastforward_lock(self, aggregate_id: UUID) -> Lock:
|
|
360
358
|
with self._fastforward_locks_lock:
|
|
@@ -388,7 +386,7 @@ class Repository:
|
|
|
388
386
|
"""
|
|
389
387
|
try:
|
|
390
388
|
self.get(aggregate_id=item)
|
|
391
|
-
except
|
|
389
|
+
except AggregateNotFoundError:
|
|
392
390
|
return False
|
|
393
391
|
else:
|
|
394
392
|
return True
|
|
@@ -414,9 +412,9 @@ class Section:
|
|
|
414
412
|
:param Optional[str] next_id: section ID of the following section
|
|
415
413
|
"""
|
|
416
414
|
|
|
417
|
-
id:
|
|
415
|
+
id: str | None
|
|
418
416
|
items: List[Notification]
|
|
419
|
-
next_id:
|
|
417
|
+
next_id: str | None
|
|
420
418
|
|
|
421
419
|
|
|
422
420
|
class NotificationLog(ABC):
|
|
@@ -437,7 +435,7 @@ class NotificationLog(ABC):
|
|
|
437
435
|
self,
|
|
438
436
|
start: int,
|
|
439
437
|
limit: int,
|
|
440
|
-
stop:
|
|
438
|
+
stop: int | None = None,
|
|
441
439
|
topics: Sequence[str] = (),
|
|
442
440
|
) -> List[Notification]:
|
|
443
441
|
"""
|
|
@@ -497,8 +495,8 @@ class LocalNotificationLog(NotificationLog):
|
|
|
497
495
|
notifications = self.select(start, limit)
|
|
498
496
|
|
|
499
497
|
# Get next section ID.
|
|
500
|
-
actual_section_id:
|
|
501
|
-
next_id:
|
|
498
|
+
actual_section_id: str | None
|
|
499
|
+
next_id: str | None
|
|
502
500
|
if len(notifications):
|
|
503
501
|
last_notification_id = notifications[-1].id
|
|
504
502
|
actual_section_id = self.format_section_id(
|
|
@@ -525,7 +523,7 @@ class LocalNotificationLog(NotificationLog):
|
|
|
525
523
|
self,
|
|
526
524
|
start: int,
|
|
527
525
|
limit: int,
|
|
528
|
-
stop:
|
|
526
|
+
stop: int | None = None,
|
|
529
527
|
topics: Sequence[str] = (),
|
|
530
528
|
) -> List[Notification]:
|
|
531
529
|
"""
|
|
@@ -534,16 +532,17 @@ class LocalNotificationLog(NotificationLog):
|
|
|
534
532
|
from the notification log.
|
|
535
533
|
"""
|
|
536
534
|
if limit > self.section_size:
|
|
537
|
-
|
|
535
|
+
msg = (
|
|
538
536
|
f"Requested limit {limit} greater than section size {self.section_size}"
|
|
539
537
|
)
|
|
538
|
+
raise ValueError(msg)
|
|
540
539
|
return self.recorder.select_notifications(
|
|
541
540
|
start=start, limit=limit, stop=stop, topics=topics
|
|
542
541
|
)
|
|
543
542
|
|
|
544
543
|
@staticmethod
|
|
545
544
|
def format_section_id(first_id: int, last_id: int) -> str:
|
|
546
|
-
return "{},{}"
|
|
545
|
+
return f"{first_id},{last_id}"
|
|
547
546
|
|
|
548
547
|
|
|
549
548
|
class ProcessingEvent:
|
|
@@ -554,7 +553,7 @@ class ProcessingEvent:
|
|
|
554
553
|
new domain events that result from processing that notification.
|
|
555
554
|
"""
|
|
556
555
|
|
|
557
|
-
def __init__(self, tracking:
|
|
556
|
+
def __init__(self, tracking: Tracking | None = None):
|
|
558
557
|
"""
|
|
559
558
|
Initialises the process event with the given tracking object.
|
|
560
559
|
"""
|
|
@@ -565,7 +564,7 @@ class ProcessingEvent:
|
|
|
565
564
|
|
|
566
565
|
def collect_events(
|
|
567
566
|
self,
|
|
568
|
-
*objs:
|
|
567
|
+
*objs: MutableOrImmutableAggregate | DomainEventProtocol | None,
|
|
569
568
|
**kwargs: Any,
|
|
570
569
|
) -> None:
|
|
571
570
|
"""
|
|
@@ -574,7 +573,7 @@ class ProcessingEvent:
|
|
|
574
573
|
for obj in objs:
|
|
575
574
|
if obj is None:
|
|
576
575
|
continue
|
|
577
|
-
|
|
576
|
+
if isinstance(obj, DomainEventProtocol):
|
|
578
577
|
self.events.append(obj)
|
|
579
578
|
else:
|
|
580
579
|
if isinstance(obj, CollectEventsProtocol):
|
|
@@ -586,7 +585,7 @@ class ProcessingEvent:
|
|
|
586
585
|
|
|
587
586
|
def save(
|
|
588
587
|
self,
|
|
589
|
-
*aggregates:
|
|
588
|
+
*aggregates: MutableOrImmutableAggregate | DomainEventProtocol | None,
|
|
590
589
|
**kwargs: Any,
|
|
591
590
|
) -> None:
|
|
592
591
|
warn(
|
|
@@ -598,31 +597,12 @@ class ProcessingEvent:
|
|
|
598
597
|
self.collect_events(*aggregates, **kwargs)
|
|
599
598
|
|
|
600
599
|
|
|
601
|
-
class ProcessEvent(ProcessingEvent):
|
|
602
|
-
"""Deprecated, use :class:`ProcessingEvent` instead.
|
|
603
|
-
|
|
604
|
-
Keeps together a :class:`~eventsourcing.persistence.Tracking`
|
|
605
|
-
object, which represents the position of a domain event notification
|
|
606
|
-
in the notification log of a particular application, and the
|
|
607
|
-
new domain events that result from processing that notification.
|
|
608
|
-
"""
|
|
609
|
-
|
|
610
|
-
def __init__(self, tracking: Optional[Tracking] = None):
|
|
611
|
-
warn(
|
|
612
|
-
"'ProcessEvent' is deprecated, use 'ProcessingEvent' instead",
|
|
613
|
-
DeprecationWarning,
|
|
614
|
-
stacklevel=2,
|
|
615
|
-
)
|
|
616
|
-
|
|
617
|
-
super().__init__(tracking)
|
|
618
|
-
|
|
619
|
-
|
|
620
600
|
class RecordingEvent:
|
|
621
601
|
def __init__(
|
|
622
602
|
self,
|
|
623
603
|
application_name: str,
|
|
624
604
|
recordings: List[Recording],
|
|
625
|
-
previous_max_notification_id:
|
|
605
|
+
previous_max_notification_id: int | None,
|
|
626
606
|
):
|
|
627
607
|
self.application_name = application_name
|
|
628
608
|
self.recordings = recordings
|
|
@@ -635,13 +615,13 @@ class Application:
|
|
|
635
615
|
"""
|
|
636
616
|
|
|
637
617
|
name = "Application"
|
|
638
|
-
env:
|
|
618
|
+
env: ClassVar[Dict[str, str]] = {}
|
|
639
619
|
is_snapshotting_enabled: bool = False
|
|
640
|
-
snapshotting_intervals:
|
|
641
|
-
Dict[Type[MutableOrImmutableAggregate], int]
|
|
620
|
+
snapshotting_intervals: ClassVar[
|
|
621
|
+
Dict[Type[MutableOrImmutableAggregate], int] | None
|
|
642
622
|
] = None
|
|
643
|
-
snapshotting_projectors:
|
|
644
|
-
Dict[Type[MutableOrImmutableAggregate], ProjectorFunction[Any, Any]]
|
|
623
|
+
snapshotting_projectors: ClassVar[
|
|
624
|
+
Dict[Type[MutableOrImmutableAggregate], ProjectorFunction[Any, Any]] | None
|
|
645
625
|
] = None
|
|
646
626
|
snapshot_class: Type[SnapshotProtocol] = Snapshot
|
|
647
627
|
log_section_size = 10
|
|
@@ -656,7 +636,7 @@ class Application:
|
|
|
656
636
|
if "name" not in cls.__dict__:
|
|
657
637
|
cls.name = cls.__name__
|
|
658
638
|
|
|
659
|
-
def __init__(self, env:
|
|
639
|
+
def __init__(self, env: EnvType | None = None) -> None:
|
|
660
640
|
"""
|
|
661
641
|
Initialises an application with an
|
|
662
642
|
:class:`~eventsourcing.persistence.InfrastructureFactory`,
|
|
@@ -666,20 +646,20 @@ class Application:
|
|
|
666
646
|
a :class:`~eventsourcing.application.Repository`, and
|
|
667
647
|
a :class:`~eventsourcing.application.LocalNotificationLog`.
|
|
668
648
|
"""
|
|
669
|
-
self.env = self.construct_env(self.name, env)
|
|
649
|
+
self.env = self.construct_env(self.name, env) # type: ignore[misc]
|
|
670
650
|
self.factory = self.construct_factory(self.env)
|
|
671
651
|
self.mapper = self.construct_mapper()
|
|
672
652
|
self.recorder = self.construct_recorder()
|
|
673
653
|
self.events = self.construct_event_store()
|
|
674
|
-
self.snapshots:
|
|
654
|
+
self.snapshots: EventStore | None = None
|
|
675
655
|
if self.factory.is_snapshotting_enabled():
|
|
676
656
|
self.snapshots = self.construct_snapshot_store()
|
|
677
657
|
self._repository = self.construct_repository()
|
|
678
658
|
self._notification_log = self.construct_notification_log()
|
|
679
659
|
self.closing = Event()
|
|
680
|
-
self.previous_max_notification_id:
|
|
681
|
-
|
|
682
|
-
|
|
660
|
+
self.previous_max_notification_id: int | None = (
|
|
661
|
+
self.recorder.max_notification_id()
|
|
662
|
+
)
|
|
683
663
|
|
|
684
664
|
@property
|
|
685
665
|
def repository(self) -> Repository:
|
|
@@ -706,7 +686,7 @@ class Application:
|
|
|
706
686
|
)
|
|
707
687
|
return self._notification_log
|
|
708
688
|
|
|
709
|
-
def construct_env(self, name: str, env:
|
|
689
|
+
def construct_env(self, name: str, env: EnvType | None = None) -> Environment:
|
|
710
690
|
"""
|
|
711
691
|
Constructs environment from which application will be configured.
|
|
712
692
|
"""
|
|
@@ -785,10 +765,7 @@ class Application:
|
|
|
785
765
|
Constructs a :class:`Repository` for use by the application.
|
|
786
766
|
"""
|
|
787
767
|
cache_maxsize_envvar = self.env.get(self.AGGREGATE_CACHE_MAXSIZE)
|
|
788
|
-
if cache_maxsize_envvar
|
|
789
|
-
cache_maxsize = int(cache_maxsize_envvar)
|
|
790
|
-
else:
|
|
791
|
-
cache_maxsize = None
|
|
768
|
+
cache_maxsize = int(cache_maxsize_envvar) if cache_maxsize_envvar else None
|
|
792
769
|
return Repository(
|
|
793
770
|
event_store=self.events,
|
|
794
771
|
snapshot_store=self.snapshots,
|
|
@@ -810,7 +787,7 @@ class Application:
|
|
|
810
787
|
|
|
811
788
|
def save(
|
|
812
789
|
self,
|
|
813
|
-
*objs:
|
|
790
|
+
*objs: MutableOrImmutableAggregate | DomainEventProtocol | None,
|
|
814
791
|
**kwargs: Any,
|
|
815
792
|
) -> List[Recording]:
|
|
816
793
|
"""
|
|
@@ -848,43 +825,39 @@ class Application:
|
|
|
848
825
|
except KeyError:
|
|
849
826
|
continue
|
|
850
827
|
interval = self.snapshotting_intervals.get(type(aggregate))
|
|
851
|
-
if interval is not None:
|
|
852
|
-
if
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
)
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
" with class variable 'snapshotting_projectors', "
|
|
873
|
-
f"to be a dict that has {type(aggregate)} as a key "
|
|
874
|
-
"with the aggregate projector function for "
|
|
875
|
-
f"{type(aggregate)} as the value for that key."
|
|
876
|
-
)
|
|
877
|
-
)
|
|
878
|
-
self.take_snapshot(
|
|
879
|
-
aggregate_id=event.originator_id,
|
|
880
|
-
version=event.originator_version,
|
|
881
|
-
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."
|
|
882
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
|
+
)
|
|
883
856
|
|
|
884
857
|
def take_snapshot(
|
|
885
858
|
self,
|
|
886
859
|
aggregate_id: UUID,
|
|
887
|
-
version:
|
|
860
|
+
version: int | None = None,
|
|
888
861
|
projector_func: ProjectorFunction[
|
|
889
862
|
TMutableOrImmutableAggregate, TDomainEvent
|
|
890
863
|
] = project_aggregate,
|
|
@@ -894,22 +867,20 @@ class Application:
|
|
|
894
867
|
and puts the snapshot in the snapshot store.
|
|
895
868
|
"""
|
|
896
869
|
if self.snapshots is None:
|
|
897
|
-
|
|
870
|
+
msg = (
|
|
898
871
|
"Can't take snapshot without snapshots store. Please "
|
|
899
872
|
"set environment variable IS_SNAPSHOTTING_ENABLED to "
|
|
900
873
|
"a true value (e.g. 'y'), or set 'is_snapshotting_enabled' "
|
|
901
874
|
"on application class, or set 'snapshotting_intervals' on "
|
|
902
875
|
"application class."
|
|
903
876
|
)
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
snapshot = snapshot_class.take(aggregate)
|
|
912
|
-
self.snapshots.put([snapshot])
|
|
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])
|
|
913
884
|
|
|
914
885
|
def notify(self, new_events: List[DomainEventProtocol]) -> None:
|
|
915
886
|
"""
|
|
@@ -937,13 +908,23 @@ class Application:
|
|
|
937
908
|
TApplication = TypeVar("TApplication", bound=Application)
|
|
938
909
|
|
|
939
910
|
|
|
940
|
-
class
|
|
911
|
+
class AggregateNotFoundError(EventSourcingError):
|
|
941
912
|
"""
|
|
942
913
|
Raised when an :class:`~eventsourcing.domain.Aggregate`
|
|
943
914
|
object is not found in a :class:`Repository`.
|
|
944
915
|
"""
|
|
945
916
|
|
|
946
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
|
+
|
|
947
928
|
class EventSourcedLog(Generic[TDomainEvent]):
|
|
948
929
|
"""
|
|
949
930
|
Constructs a sequence of domain events, like an aggregate.
|
|
@@ -963,15 +944,15 @@ class EventSourcedLog(Generic[TDomainEvent]):
|
|
|
963
944
|
self,
|
|
964
945
|
events: EventStore,
|
|
965
946
|
originator_id: UUID,
|
|
966
|
-
logged_cls: Type[TDomainEvent], #
|
|
947
|
+
logged_cls: Type[TDomainEvent], # TODO: Rename to 'event_class' in v10.
|
|
967
948
|
):
|
|
968
949
|
self.events = events
|
|
969
950
|
self.originator_id = originator_id
|
|
970
|
-
self.logged_cls = logged_cls #
|
|
951
|
+
self.logged_cls = logged_cls # TODO: Rename to 'event_class' in v10.
|
|
971
952
|
|
|
972
953
|
def trigger_event(
|
|
973
954
|
self,
|
|
974
|
-
next_originator_version:
|
|
955
|
+
next_originator_version: int | None = None,
|
|
975
956
|
**kwargs: Any,
|
|
976
957
|
) -> TDomainEvent:
|
|
977
958
|
"""
|
|
@@ -985,8 +966,8 @@ class EventSourcedLog(Generic[TDomainEvent]):
|
|
|
985
966
|
|
|
986
967
|
def _trigger_event(
|
|
987
968
|
self,
|
|
988
|
-
logged_cls:
|
|
989
|
-
next_originator_version:
|
|
969
|
+
logged_cls: Type[T] | None,
|
|
970
|
+
next_originator_version: int | None = None,
|
|
990
971
|
**kwargs: Any,
|
|
991
972
|
) -> T:
|
|
992
973
|
"""
|
|
@@ -999,15 +980,14 @@ class EventSourcedLog(Generic[TDomainEvent]):
|
|
|
999
980
|
else:
|
|
1000
981
|
next_originator_version = last_logged.originator_version + 1
|
|
1001
982
|
|
|
1002
|
-
|
|
983
|
+
return logged_cls( # type: ignore
|
|
1003
984
|
originator_id=self.originator_id,
|
|
1004
985
|
originator_version=next_originator_version,
|
|
1005
986
|
timestamp=create_utc_datetime_now(),
|
|
1006
987
|
**kwargs,
|
|
1007
988
|
)
|
|
1008
|
-
return logged_event
|
|
1009
989
|
|
|
1010
|
-
def get_first(self) ->
|
|
990
|
+
def get_first(self) -> TDomainEvent | None:
|
|
1011
991
|
"""
|
|
1012
992
|
Selects the first logged event.
|
|
1013
993
|
"""
|
|
@@ -1016,7 +996,7 @@ class EventSourcedLog(Generic[TDomainEvent]):
|
|
|
1016
996
|
except StopIteration:
|
|
1017
997
|
return None
|
|
1018
998
|
|
|
1019
|
-
def get_last(self) ->
|
|
999
|
+
def get_last(self) -> TDomainEvent | None:
|
|
1020
1000
|
"""
|
|
1021
1001
|
Selects the last logged event.
|
|
1022
1002
|
"""
|
|
@@ -1027,10 +1007,11 @@ class EventSourcedLog(Generic[TDomainEvent]):
|
|
|
1027
1007
|
|
|
1028
1008
|
def get(
|
|
1029
1009
|
self,
|
|
1030
|
-
|
|
1031
|
-
|
|
1010
|
+
*,
|
|
1011
|
+
gt: int | None = None,
|
|
1012
|
+
lte: int | None = None,
|
|
1032
1013
|
desc: bool = False,
|
|
1033
|
-
limit:
|
|
1014
|
+
limit: int | None = None,
|
|
1034
1015
|
) -> Iterator[TDomainEvent]:
|
|
1035
1016
|
"""
|
|
1036
1017
|
Selects a range of logged events with limit,
|
eventsourcing/cipher.py
CHANGED
|
@@ -2,13 +2,16 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
from base64 import b64decode, b64encode
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
5
6
|
|
|
6
7
|
from Crypto.Cipher import AES
|
|
7
8
|
from Crypto.Cipher._mode_gcm import GcmMode
|
|
8
9
|
from Crypto.Cipher.AES import key_size
|
|
9
10
|
|
|
10
11
|
from eventsourcing.persistence import Cipher
|
|
11
|
-
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING: # pragma: nocover
|
|
14
|
+
from eventsourcing.utils import Environment
|
|
12
15
|
|
|
13
16
|
|
|
14
17
|
class AESCipher(Cipher):
|
|
@@ -33,9 +36,8 @@ class AESCipher(Cipher):
|
|
|
33
36
|
@staticmethod
|
|
34
37
|
def check_key_size(num_bytes: int) -> None:
|
|
35
38
|
if num_bytes not in AESCipher.KEY_SIZES:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
)
|
|
39
|
+
msg = f"Invalid key size: {num_bytes} not in {AESCipher.KEY_SIZES}"
|
|
40
|
+
raise ValueError(msg)
|
|
39
41
|
|
|
40
42
|
@staticmethod
|
|
41
43
|
def random_bytes(num_bytes: int) -> bytes:
|
|
@@ -49,7 +51,8 @@ class AESCipher(Cipher):
|
|
|
49
51
|
"""
|
|
50
52
|
cipher_key = environment.get(self.CIPHER_KEY)
|
|
51
53
|
if not cipher_key:
|
|
52
|
-
|
|
54
|
+
msg = f"'{self.CIPHER_KEY}' not in env"
|
|
55
|
+
raise OSError(msg)
|
|
53
56
|
key = b64decode(cipher_key.encode("utf8"))
|
|
54
57
|
AESCipher.check_key_size(len(key))
|
|
55
58
|
self.key = key
|
|
@@ -66,11 +69,8 @@ class AESCipher(Cipher):
|
|
|
66
69
|
encrypted = result[0]
|
|
67
70
|
tag = result[1]
|
|
68
71
|
|
|
69
|
-
# Combine with nonce.
|
|
70
|
-
ciphertext = nonce + tag + encrypted
|
|
71
|
-
|
|
72
72
|
# Return ciphertext.
|
|
73
|
-
return
|
|
73
|
+
return nonce + tag + encrypted
|
|
74
74
|
|
|
75
75
|
def construct_cipher(self, nonce: bytes) -> GcmMode:
|
|
76
76
|
cipher = AES.new(
|
|
@@ -87,11 +87,13 @@ class AESCipher(Cipher):
|
|
|
87
87
|
# Split out the nonce, tag, and encrypted data.
|
|
88
88
|
nonce = ciphertext[:12]
|
|
89
89
|
if len(nonce) != 12:
|
|
90
|
-
|
|
90
|
+
msg = "Damaged cipher text: invalid nonce length"
|
|
91
|
+
raise ValueError(msg)
|
|
91
92
|
|
|
92
93
|
tag = ciphertext[12:28]
|
|
93
94
|
if len(tag) != 16:
|
|
94
|
-
|
|
95
|
+
msg = "Damaged cipher text: invalid tag length"
|
|
96
|
+
raise ValueError(msg)
|
|
95
97
|
encrypted = ciphertext[28:]
|
|
96
98
|
|
|
97
99
|
# Construct AES cipher, with old nonce.
|
|
@@ -101,5 +103,6 @@ class AESCipher(Cipher):
|
|
|
101
103
|
try:
|
|
102
104
|
plaintext = cipher.decrypt_and_verify(encrypted, tag)
|
|
103
105
|
except ValueError as e:
|
|
104
|
-
|
|
106
|
+
msg = f"Cipher text is damaged: {e}"
|
|
107
|
+
raise ValueError(msg) from None
|
|
105
108
|
return plaintext
|