eventsourcing 9.3.5__py3-none-any.whl → 9.4.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 +0 -1
- eventsourcing/application.py +115 -173
- eventsourcing/cipher.py +9 -10
- eventsourcing/compressor.py +2 -6
- eventsourcing/cryptography.py +91 -0
- eventsourcing/dispatch.py +52 -11
- eventsourcing/domain.py +733 -690
- eventsourcing/interface.py +39 -32
- eventsourcing/persistence.py +412 -287
- eventsourcing/popo.py +136 -44
- eventsourcing/postgres.py +404 -187
- eventsourcing/projection.py +428 -0
- eventsourcing/sqlite.py +167 -55
- eventsourcing/system.py +230 -341
- eventsourcing/tests/__init__.py +3 -0
- eventsourcing/tests/application.py +195 -129
- eventsourcing/tests/domain.py +19 -37
- eventsourcing/tests/persistence.py +533 -235
- eventsourcing/tests/postgres_utils.py +12 -9
- eventsourcing/utils.py +39 -47
- {eventsourcing-9.3.5.dist-info → eventsourcing-9.4.0.dist-info}/LICENSE +1 -1
- {eventsourcing-9.3.5.dist-info → eventsourcing-9.4.0.dist-info}/METADATA +14 -13
- eventsourcing-9.4.0.dist-info/RECORD +26 -0
- {eventsourcing-9.3.5.dist-info → eventsourcing-9.4.0.dist-info}/WHEEL +1 -1
- eventsourcing-9.3.5.dist-info/RECORD +0 -24
- {eventsourcing-9.3.5.dist-info → eventsourcing-9.4.0.dist-info}/AUTHORS +0 -0
eventsourcing/application.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import contextlib
|
|
3
4
|
import os
|
|
4
5
|
from abc import ABC, abstractmethod
|
|
6
|
+
from collections.abc import Iterable, Iterator, Sequence
|
|
5
7
|
from copy import deepcopy
|
|
6
8
|
from dataclasses import dataclass
|
|
7
9
|
from itertools import chain
|
|
@@ -11,22 +13,13 @@ from typing import (
|
|
|
11
13
|
Any,
|
|
12
14
|
Callable,
|
|
13
15
|
ClassVar,
|
|
14
|
-
Dict,
|
|
15
16
|
Generic,
|
|
16
|
-
Iterable,
|
|
17
|
-
Iterator,
|
|
18
|
-
List,
|
|
19
17
|
Optional,
|
|
20
|
-
Sequence,
|
|
21
|
-
Tuple,
|
|
22
|
-
Type,
|
|
23
18
|
TypeVar,
|
|
24
19
|
cast,
|
|
25
20
|
)
|
|
26
21
|
from warnings import warn
|
|
27
22
|
|
|
28
|
-
from typing_extensions import deprecated
|
|
29
|
-
|
|
30
23
|
from eventsourcing.domain import (
|
|
31
24
|
Aggregate,
|
|
32
25
|
CanMutateProtocol,
|
|
@@ -34,12 +27,12 @@ from eventsourcing.domain import (
|
|
|
34
27
|
DomainEventProtocol,
|
|
35
28
|
EventSourcingError,
|
|
36
29
|
MutableOrImmutableAggregate,
|
|
37
|
-
|
|
30
|
+
SDomainEvent,
|
|
38
31
|
Snapshot,
|
|
39
32
|
SnapshotProtocol,
|
|
40
33
|
TDomainEvent,
|
|
41
34
|
TMutableOrImmutableAggregate,
|
|
42
|
-
|
|
35
|
+
datetime_now_with_tzinfo,
|
|
43
36
|
)
|
|
44
37
|
from eventsourcing.persistence import (
|
|
45
38
|
ApplicationRecorder,
|
|
@@ -47,6 +40,7 @@ from eventsourcing.persistence import (
|
|
|
47
40
|
DecimalAsStr,
|
|
48
41
|
EventStore,
|
|
49
42
|
InfrastructureFactory,
|
|
43
|
+
JSONTranscoder,
|
|
50
44
|
Mapper,
|
|
51
45
|
Notification,
|
|
52
46
|
Recording,
|
|
@@ -56,7 +50,7 @@ from eventsourcing.persistence import (
|
|
|
56
50
|
)
|
|
57
51
|
from eventsourcing.utils import Environment, EnvType, strtobool
|
|
58
52
|
|
|
59
|
-
if TYPE_CHECKING:
|
|
53
|
+
if TYPE_CHECKING:
|
|
60
54
|
from uuid import UUID
|
|
61
55
|
|
|
62
56
|
ProjectorFunction = Callable[
|
|
@@ -70,12 +64,15 @@ MutatorFunction = Callable[
|
|
|
70
64
|
]
|
|
71
65
|
|
|
72
66
|
|
|
67
|
+
class ProgrammingError(Exception):
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
|
|
73
71
|
def project_aggregate(
|
|
74
72
|
aggregate: TMutableOrImmutableAggregate | None,
|
|
75
73
|
domain_events: Iterable[DomainEventProtocol],
|
|
76
74
|
) -> TMutableOrImmutableAggregate | None:
|
|
77
|
-
"""
|
|
78
|
-
Projector function for aggregate projections, which works
|
|
75
|
+
"""Projector function for aggregate projections, which works
|
|
79
76
|
by successively calling aggregate mutator function mutate()
|
|
80
77
|
on each of the given list of domain events in turn.
|
|
81
78
|
"""
|
|
@@ -91,22 +88,20 @@ T = TypeVar("T")
|
|
|
91
88
|
|
|
92
89
|
class Cache(Generic[S, T]):
|
|
93
90
|
def __init__(self) -> None:
|
|
94
|
-
self.cache:
|
|
91
|
+
self.cache: dict[S, Any] = {}
|
|
95
92
|
|
|
96
93
|
def get(self, key: S, *, evict: bool = False) -> T:
|
|
97
94
|
if evict:
|
|
98
95
|
return self.cache.pop(key)
|
|
99
96
|
return self.cache[key]
|
|
100
97
|
|
|
101
|
-
def put(self, key: S, value: T) ->
|
|
98
|
+
def put(self, key: S, value: T | None) -> None:
|
|
102
99
|
if value is not None:
|
|
103
100
|
self.cache[key] = value
|
|
104
|
-
return None
|
|
105
101
|
|
|
106
102
|
|
|
107
103
|
class LRUCache(Cache[S, T]):
|
|
108
|
-
"""
|
|
109
|
-
Size limited caching that tracks accesses by recency.
|
|
104
|
+
"""Size limited caching that tracks accesses by recency.
|
|
110
105
|
|
|
111
106
|
This is basically copied from functools.lru_cache. But
|
|
112
107
|
we need to know when there was a cache hit, so we can
|
|
@@ -122,7 +117,7 @@ class LRUCache(Cache[S, T]):
|
|
|
122
117
|
self.maxsize = maxsize
|
|
123
118
|
self.full = False
|
|
124
119
|
self.lock = Lock() # because linkedlist updates aren't threadsafe
|
|
125
|
-
self.root:
|
|
120
|
+
self.root: list[Any] = [] # root of the circular doubly linked list
|
|
126
121
|
self.clear()
|
|
127
122
|
|
|
128
123
|
def clear(self) -> None:
|
|
@@ -156,7 +151,7 @@ class LRUCache(Cache[S, T]):
|
|
|
156
151
|
return result
|
|
157
152
|
raise KeyError
|
|
158
153
|
|
|
159
|
-
def put(self, key: S, value: T) -> Any | None:
|
|
154
|
+
def put(self, key: S, value: T | None) -> Any | None:
|
|
160
155
|
evicted_key = None
|
|
161
156
|
evicted_value = None
|
|
162
157
|
with self.lock:
|
|
@@ -208,7 +203,8 @@ class Repository:
|
|
|
208
203
|
"""Reconstructs aggregates from events in an
|
|
209
204
|
:class:`~eventsourcing.persistence.EventStore`,
|
|
210
205
|
possibly using snapshot store to avoid replaying
|
|
211
|
-
all events.
|
|
206
|
+
all events.
|
|
207
|
+
"""
|
|
212
208
|
|
|
213
209
|
FASTFORWARD_LOCKS_CACHE_MAXSIZE = 50
|
|
214
210
|
|
|
@@ -222,8 +218,7 @@ class Repository:
|
|
|
222
218
|
fastforward_skipping: bool = False,
|
|
223
219
|
deepcopy_from_cache: bool = True,
|
|
224
220
|
):
|
|
225
|
-
"""
|
|
226
|
-
Initialises repository with given event store (an
|
|
221
|
+
"""Initialises repository with given event store (an
|
|
227
222
|
:class:`~eventsourcing.persistence.EventStore` for aggregate
|
|
228
223
|
:class:`~eventsourcing.domain.AggregateEvent` objects)
|
|
229
224
|
and optionally a snapshot store (an
|
|
@@ -248,7 +243,7 @@ class Repository:
|
|
|
248
243
|
self._fastforward_locks_cache: LRUCache[UUID, Lock] = LRUCache(
|
|
249
244
|
maxsize=self.FASTFORWARD_LOCKS_CACHE_MAXSIZE
|
|
250
245
|
)
|
|
251
|
-
self._fastforward_locks_inuse:
|
|
246
|
+
self._fastforward_locks_inuse: dict[UUID, tuple[Lock, int]] = {}
|
|
252
247
|
|
|
253
248
|
def get(
|
|
254
249
|
self,
|
|
@@ -261,15 +256,14 @@ class Repository:
|
|
|
261
256
|
fastforward_skipping: bool = False,
|
|
262
257
|
deepcopy_from_cache: bool = True,
|
|
263
258
|
) -> TMutableOrImmutableAggregate:
|
|
264
|
-
"""
|
|
265
|
-
Reconstructs an :class:`~eventsourcing.domain.Aggregate` for a
|
|
259
|
+
"""Reconstructs an :class:`~eventsourcing.domain.Aggregate` for a
|
|
266
260
|
given ID from stored events, optionally at a particular version.
|
|
267
261
|
"""
|
|
268
262
|
if self.cache and version is None:
|
|
269
263
|
try:
|
|
270
264
|
# Look for aggregate in the cache.
|
|
271
265
|
aggregate = cast(
|
|
272
|
-
TMutableOrImmutableAggregate, self.cache.get(aggregate_id)
|
|
266
|
+
"TMutableOrImmutableAggregate", self.cache.get(aggregate_id)
|
|
273
267
|
)
|
|
274
268
|
except KeyError:
|
|
275
269
|
# Reconstruct aggregate from stored events.
|
|
@@ -290,7 +284,11 @@ class Repository:
|
|
|
290
284
|
originator_id=aggregate_id, gt=aggregate.version
|
|
291
285
|
)
|
|
292
286
|
_aggregate = projector_func(
|
|
293
|
-
aggregate,
|
|
287
|
+
aggregate,
|
|
288
|
+
cast(
|
|
289
|
+
"Iterable[TDomainEvent]",
|
|
290
|
+
new_events,
|
|
291
|
+
),
|
|
294
292
|
)
|
|
295
293
|
if _aggregate is None:
|
|
296
294
|
raise AggregateNotFoundError(aggregate_id)
|
|
@@ -345,8 +343,8 @@ class Repository:
|
|
|
345
343
|
aggregate = projector_func(
|
|
346
344
|
initial,
|
|
347
345
|
chain(
|
|
348
|
-
cast(Iterable[TDomainEvent], snapshots),
|
|
349
|
-
cast(Iterable[TDomainEvent], aggregate_events),
|
|
346
|
+
cast("Iterable[TDomainEvent]", snapshots),
|
|
347
|
+
cast("Iterable[TDomainEvent]", aggregate_events),
|
|
350
348
|
),
|
|
351
349
|
)
|
|
352
350
|
|
|
@@ -357,19 +355,18 @@ class Repository:
|
|
|
357
355
|
return aggregate
|
|
358
356
|
|
|
359
357
|
def _use_fastforward_lock(self, aggregate_id: UUID) -> Lock:
|
|
358
|
+
lock: Lock | None = None
|
|
360
359
|
with self._fastforward_locks_lock:
|
|
361
|
-
|
|
360
|
+
num_users = 0
|
|
361
|
+
with contextlib.suppress(KeyError):
|
|
362
362
|
lock, num_users = self._fastforward_locks_inuse[aggregate_id]
|
|
363
|
-
|
|
364
|
-
|
|
363
|
+
if lock is None:
|
|
364
|
+
with contextlib.suppress(KeyError):
|
|
365
365
|
lock = self._fastforward_locks_cache.get(aggregate_id, evict=True)
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
finally:
|
|
371
|
-
num_users += 1
|
|
372
|
-
self._fastforward_locks_inuse[aggregate_id] = (lock, num_users)
|
|
366
|
+
if lock is None:
|
|
367
|
+
lock = Lock()
|
|
368
|
+
num_users += 1
|
|
369
|
+
self._fastforward_locks_inuse[aggregate_id] = (lock, num_users)
|
|
373
370
|
return lock
|
|
374
371
|
|
|
375
372
|
def _disuse_fastforward_lock(self, aggregate_id: UUID) -> None:
|
|
@@ -383,9 +380,7 @@ class Repository:
|
|
|
383
380
|
self._fastforward_locks_inuse[aggregate_id] = (lock_, num_users)
|
|
384
381
|
|
|
385
382
|
def __contains__(self, item: UUID) -> bool:
|
|
386
|
-
"""
|
|
387
|
-
Tests to see if an aggregate exists in the repository.
|
|
388
|
-
"""
|
|
383
|
+
"""Tests to see if an aggregate exists in the repository."""
|
|
389
384
|
try:
|
|
390
385
|
self.get(aggregate_id=item)
|
|
391
386
|
except AggregateNotFoundError:
|
|
@@ -396,8 +391,7 @@ class Repository:
|
|
|
396
391
|
|
|
397
392
|
@dataclass(frozen=True)
|
|
398
393
|
class Section:
|
|
399
|
-
"""
|
|
400
|
-
Frozen dataclass that represents a section from a :class:`NotificationLog`.
|
|
394
|
+
"""Frozen dataclass that represents a section from a :class:`NotificationLog`.
|
|
401
395
|
The :data:`items` attribute contains a list of
|
|
402
396
|
:class:`~eventsourcing.persistence.Notification` objects.
|
|
403
397
|
The :data:`id` attribute is the section ID, two integers
|
|
@@ -410,24 +404,21 @@ class Section:
|
|
|
410
404
|
Constructor arguments:
|
|
411
405
|
|
|
412
406
|
:param Optional[str] id: section ID of this section e.g. "1,10"
|
|
413
|
-
:param
|
|
407
|
+
:param list[Notification] items: a list of event notifications
|
|
414
408
|
:param Optional[str] next_id: section ID of the following section
|
|
415
409
|
"""
|
|
416
410
|
|
|
417
411
|
id: str | None
|
|
418
|
-
items:
|
|
412
|
+
items: list[Notification]
|
|
419
413
|
next_id: str | None
|
|
420
414
|
|
|
421
415
|
|
|
422
416
|
class NotificationLog(ABC):
|
|
423
|
-
"""
|
|
424
|
-
Abstract base class for notification logs.
|
|
425
|
-
"""
|
|
417
|
+
"""Abstract base class for notification logs."""
|
|
426
418
|
|
|
427
419
|
@abstractmethod
|
|
428
420
|
def __getitem__(self, section_id: str) -> Section:
|
|
429
|
-
"""
|
|
430
|
-
Returns a :class:`Section` of
|
|
421
|
+
"""Returns a :class:`Section` of
|
|
431
422
|
:class:`~eventsourcing.persistence.Notification` objects
|
|
432
423
|
from the notification log.
|
|
433
424
|
"""
|
|
@@ -435,21 +426,21 @@ class NotificationLog(ABC):
|
|
|
435
426
|
@abstractmethod
|
|
436
427
|
def select(
|
|
437
428
|
self,
|
|
438
|
-
start: int,
|
|
429
|
+
start: int | None,
|
|
439
430
|
limit: int,
|
|
440
431
|
stop: int | None = None,
|
|
441
432
|
topics: Sequence[str] = (),
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
433
|
+
*,
|
|
434
|
+
inclusive_of_start: bool = True,
|
|
435
|
+
) -> list[Notification]:
|
|
436
|
+
"""Returns a selection of
|
|
445
437
|
:class:`~eventsourcing.persistence.Notification` objects
|
|
446
438
|
from the notification log.
|
|
447
439
|
"""
|
|
448
440
|
|
|
449
441
|
|
|
450
442
|
class LocalNotificationLog(NotificationLog):
|
|
451
|
-
"""
|
|
452
|
-
Notification log that presents sections of event notifications
|
|
443
|
+
"""Notification log that presents sections of event notifications
|
|
453
444
|
retrieved from an :class:`~eventsourcing.persistence.ApplicationRecorder`.
|
|
454
445
|
"""
|
|
455
446
|
|
|
@@ -460,8 +451,7 @@ class LocalNotificationLog(NotificationLog):
|
|
|
460
451
|
recorder: ApplicationRecorder,
|
|
461
452
|
section_size: int = DEFAULT_SECTION_SIZE,
|
|
462
453
|
):
|
|
463
|
-
"""
|
|
464
|
-
Initialises a local notification object with given
|
|
454
|
+
"""Initialises a local notification object with given
|
|
465
455
|
:class:`~eventsourcing.persistence.ApplicationRecorder`
|
|
466
456
|
and an optional section size.
|
|
467
457
|
|
|
@@ -476,8 +466,7 @@ class LocalNotificationLog(NotificationLog):
|
|
|
476
466
|
self.section_size = section_size
|
|
477
467
|
|
|
478
468
|
def __getitem__(self, requested_section_id: str) -> Section:
|
|
479
|
-
"""
|
|
480
|
-
Returns a :class:`Section` of event notifications
|
|
469
|
+
"""Returns a :class:`Section` of event notifications
|
|
481
470
|
based on the requested section ID. The section ID of
|
|
482
471
|
the returned section will describe the event
|
|
483
472
|
notifications that are actually contained in
|
|
@@ -523,13 +512,14 @@ class LocalNotificationLog(NotificationLog):
|
|
|
523
512
|
|
|
524
513
|
def select(
|
|
525
514
|
self,
|
|
526
|
-
start: int,
|
|
515
|
+
start: int | None,
|
|
527
516
|
limit: int,
|
|
528
517
|
stop: int | None = None,
|
|
529
518
|
topics: Sequence[str] = (),
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
519
|
+
*,
|
|
520
|
+
inclusive_of_start: bool = True,
|
|
521
|
+
) -> list[Notification]:
|
|
522
|
+
"""Returns a selection of
|
|
533
523
|
:class:`~eventsourcing.persistence.Notification` objects
|
|
534
524
|
from the notification log.
|
|
535
525
|
"""
|
|
@@ -539,7 +529,11 @@ class LocalNotificationLog(NotificationLog):
|
|
|
539
529
|
)
|
|
540
530
|
raise ValueError(msg)
|
|
541
531
|
return self.recorder.select_notifications(
|
|
542
|
-
start=start,
|
|
532
|
+
start=start,
|
|
533
|
+
limit=limit,
|
|
534
|
+
stop=stop,
|
|
535
|
+
topics=topics,
|
|
536
|
+
inclusive_of_start=inclusive_of_start,
|
|
543
537
|
)
|
|
544
538
|
|
|
545
539
|
@staticmethod
|
|
@@ -548,30 +542,25 @@ class LocalNotificationLog(NotificationLog):
|
|
|
548
542
|
|
|
549
543
|
|
|
550
544
|
class ProcessingEvent:
|
|
551
|
-
"""
|
|
552
|
-
Keeps together a :class:`~eventsourcing.persistence.Tracking`
|
|
545
|
+
"""Keeps together a :class:`~eventsourcing.persistence.Tracking`
|
|
553
546
|
object, which represents the position of a domain event notification
|
|
554
547
|
in the notification log of a particular application, and the
|
|
555
548
|
new domain events that result from processing that notification.
|
|
556
549
|
"""
|
|
557
550
|
|
|
558
551
|
def __init__(self, tracking: Tracking | None = None):
|
|
559
|
-
"""
|
|
560
|
-
Initialises the process event with the given tracking object.
|
|
561
|
-
"""
|
|
552
|
+
"""Initialises the process event with the given tracking object."""
|
|
562
553
|
self.tracking = tracking
|
|
563
|
-
self.events:
|
|
564
|
-
self.aggregates:
|
|
565
|
-
self.saved_kwargs:
|
|
554
|
+
self.events: list[DomainEventProtocol] = []
|
|
555
|
+
self.aggregates: dict[UUID, MutableOrImmutableAggregate] = {}
|
|
556
|
+
self.saved_kwargs: dict[Any, Any] = {}
|
|
566
557
|
|
|
567
558
|
def collect_events(
|
|
568
559
|
self,
|
|
569
560
|
*objs: MutableOrImmutableAggregate | DomainEventProtocol | None,
|
|
570
561
|
**kwargs: Any,
|
|
571
562
|
) -> None:
|
|
572
|
-
"""
|
|
573
|
-
Collects pending domain events from the given aggregate.
|
|
574
|
-
"""
|
|
563
|
+
"""Collects pending domain events from the given aggregate."""
|
|
575
564
|
for obj in objs:
|
|
576
565
|
if obj is None:
|
|
577
566
|
continue
|
|
@@ -600,20 +589,16 @@ class ProcessingEvent:
|
|
|
600
589
|
|
|
601
590
|
|
|
602
591
|
class Application:
|
|
603
|
-
"""
|
|
604
|
-
Base class for event-sourced applications.
|
|
605
|
-
"""
|
|
592
|
+
"""Base class for event-sourced applications."""
|
|
606
593
|
|
|
607
594
|
name = "Application"
|
|
608
|
-
env: ClassVar[
|
|
595
|
+
env: ClassVar[dict[str, str]] = {}
|
|
609
596
|
is_snapshotting_enabled: bool = False
|
|
610
|
-
snapshotting_intervals: ClassVar[
|
|
611
|
-
Dict[Type[MutableOrImmutableAggregate], int] | None
|
|
612
|
-
] = None
|
|
597
|
+
snapshotting_intervals: ClassVar[dict[type[MutableOrImmutableAggregate], int]] = {}
|
|
613
598
|
snapshotting_projectors: ClassVar[
|
|
614
|
-
|
|
615
|
-
] =
|
|
616
|
-
snapshot_class:
|
|
599
|
+
dict[type[MutableOrImmutableAggregate], ProjectorFunction[Any, Any]]
|
|
600
|
+
] = {}
|
|
601
|
+
snapshot_class: type[SnapshotProtocol] = Snapshot
|
|
617
602
|
log_section_size = 10
|
|
618
603
|
notify_topics: Sequence[str] = []
|
|
619
604
|
|
|
@@ -627,8 +612,7 @@ class Application:
|
|
|
627
612
|
cls.name = cls.__name__
|
|
628
613
|
|
|
629
614
|
def __init__(self, env: EnvType | None = None) -> None:
|
|
630
|
-
"""
|
|
631
|
-
Initialises an application with an
|
|
615
|
+
"""Initialises an application with an
|
|
632
616
|
:class:`~eventsourcing.persistence.InfrastructureFactory`,
|
|
633
617
|
a :class:`~eventsourcing.persistence.Mapper`,
|
|
634
618
|
an :class:`~eventsourcing.persistence.ApplicationRecorder`,
|
|
@@ -650,15 +634,12 @@ class Application:
|
|
|
650
634
|
|
|
651
635
|
@property
|
|
652
636
|
def repository(self) -> Repository:
|
|
653
|
-
"""
|
|
654
|
-
An application's repository reconstructs aggregates from stored events.
|
|
655
|
-
"""
|
|
637
|
+
"""An application's repository reconstructs aggregates from stored events."""
|
|
656
638
|
return self._repository
|
|
657
639
|
|
|
658
640
|
@property
|
|
659
641
|
def notification_log(self) -> LocalNotificationLog:
|
|
660
|
-
"""
|
|
661
|
-
An application's notification log presents all the aggregate events
|
|
642
|
+
"""An application's notification log presents all the aggregate events
|
|
662
643
|
of an application in the order they were recorded as a sequence of event
|
|
663
644
|
notifications.
|
|
664
645
|
"""
|
|
@@ -674,9 +655,7 @@ class Application:
|
|
|
674
655
|
return self._notification_log
|
|
675
656
|
|
|
676
657
|
def construct_env(self, name: str, env: EnvType | None = None) -> Environment:
|
|
677
|
-
"""
|
|
678
|
-
Constructs environment from which application will be configured.
|
|
679
|
-
"""
|
|
658
|
+
"""Constructs environment from which application will be configured."""
|
|
680
659
|
_env = dict(type(self).env)
|
|
681
660
|
if type(self).is_snapshotting_enabled or type(self).snapshotting_intervals:
|
|
682
661
|
_env["IS_SNAPSHOTTING_ENABLED"] = "y"
|
|
@@ -686,31 +665,28 @@ class Application:
|
|
|
686
665
|
return Environment(name, _env)
|
|
687
666
|
|
|
688
667
|
def construct_factory(self, env: Environment) -> InfrastructureFactory:
|
|
689
|
-
"""
|
|
690
|
-
Constructs an :class:`~eventsourcing.persistence.InfrastructureFactory`
|
|
668
|
+
"""Constructs an :class:`~eventsourcing.persistence.InfrastructureFactory`
|
|
691
669
|
for use by the application.
|
|
692
670
|
"""
|
|
693
671
|
return InfrastructureFactory.construct(env)
|
|
694
672
|
|
|
695
673
|
def construct_mapper(self) -> Mapper:
|
|
696
|
-
"""
|
|
697
|
-
Constructs a :class:`~eventsourcing.persistence.Mapper`
|
|
674
|
+
"""Constructs a :class:`~eventsourcing.persistence.Mapper`
|
|
698
675
|
for use by the application.
|
|
699
676
|
"""
|
|
700
677
|
return self.factory.mapper(transcoder=self.construct_transcoder())
|
|
701
678
|
|
|
702
679
|
def construct_transcoder(self) -> Transcoder:
|
|
703
|
-
"""
|
|
704
|
-
Constructs a :class:`~eventsourcing.persistence.Transcoder`
|
|
680
|
+
"""Constructs a :class:`~eventsourcing.persistence.Transcoder`
|
|
705
681
|
for use by the application.
|
|
706
682
|
"""
|
|
707
683
|
transcoder = self.factory.transcoder()
|
|
708
|
-
|
|
684
|
+
if isinstance(transcoder, JSONTranscoder):
|
|
685
|
+
self.register_transcodings(transcoder)
|
|
709
686
|
return transcoder
|
|
710
687
|
|
|
711
|
-
def register_transcodings(self, transcoder:
|
|
712
|
-
"""
|
|
713
|
-
Registers :class:`~eventsourcing.persistence.Transcoding`
|
|
688
|
+
def register_transcodings(self, transcoder: JSONTranscoder) -> None:
|
|
689
|
+
"""Registers :class:`~eventsourcing.persistence.Transcoding`
|
|
714
690
|
objects on given :class:`~eventsourcing.persistence.JSONTranscoder`.
|
|
715
691
|
"""
|
|
716
692
|
transcoder.register(UUIDAsHex())
|
|
@@ -718,15 +694,13 @@ class Application:
|
|
|
718
694
|
transcoder.register(DatetimeAsISO())
|
|
719
695
|
|
|
720
696
|
def construct_recorder(self) -> ApplicationRecorder:
|
|
721
|
-
"""
|
|
722
|
-
Constructs an :class:`~eventsourcing.persistence.ApplicationRecorder`
|
|
697
|
+
"""Constructs an :class:`~eventsourcing.persistence.ApplicationRecorder`
|
|
723
698
|
for use by the application.
|
|
724
699
|
"""
|
|
725
700
|
return self.factory.application_recorder()
|
|
726
701
|
|
|
727
702
|
def construct_event_store(self) -> EventStore:
|
|
728
|
-
"""
|
|
729
|
-
Constructs an :class:`~eventsourcing.persistence.EventStore`
|
|
703
|
+
"""Constructs an :class:`~eventsourcing.persistence.EventStore`
|
|
730
704
|
for use by the application to store and retrieve aggregate
|
|
731
705
|
:class:`~eventsourcing.domain.AggregateEvent` objects.
|
|
732
706
|
"""
|
|
@@ -736,8 +710,7 @@ class Application:
|
|
|
736
710
|
)
|
|
737
711
|
|
|
738
712
|
def construct_snapshot_store(self) -> EventStore:
|
|
739
|
-
"""
|
|
740
|
-
Constructs an :class:`~eventsourcing.persistence.EventStore`
|
|
713
|
+
"""Constructs an :class:`~eventsourcing.persistence.EventStore`
|
|
741
714
|
for use by the application to store and retrieve aggregate
|
|
742
715
|
:class:`~eventsourcing.domain.Snapshot` objects.
|
|
743
716
|
"""
|
|
@@ -748,9 +721,7 @@ class Application:
|
|
|
748
721
|
)
|
|
749
722
|
|
|
750
723
|
def construct_repository(self) -> Repository:
|
|
751
|
-
"""
|
|
752
|
-
Constructs a :class:`Repository` for use by the application.
|
|
753
|
-
"""
|
|
724
|
+
"""Constructs a :class:`Repository` for use by the application."""
|
|
754
725
|
cache_maxsize_envvar = self.env.get(self.AGGREGATE_CACHE_MAXSIZE)
|
|
755
726
|
cache_maxsize = int(cache_maxsize_envvar) if cache_maxsize_envvar else None
|
|
756
727
|
return Repository(
|
|
@@ -767,18 +738,15 @@ class Application:
|
|
|
767
738
|
)
|
|
768
739
|
|
|
769
740
|
def construct_notification_log(self) -> LocalNotificationLog:
|
|
770
|
-
"""
|
|
771
|
-
Constructs a :class:`LocalNotificationLog` for use by the application.
|
|
772
|
-
"""
|
|
741
|
+
"""Constructs a :class:`LocalNotificationLog` for use by the application."""
|
|
773
742
|
return LocalNotificationLog(self.recorder, section_size=self.log_section_size)
|
|
774
743
|
|
|
775
744
|
def save(
|
|
776
745
|
self,
|
|
777
746
|
*objs: MutableOrImmutableAggregate | DomainEventProtocol | None,
|
|
778
747
|
**kwargs: Any,
|
|
779
|
-
) ->
|
|
780
|
-
"""
|
|
781
|
-
Collects pending events from given aggregates and
|
|
748
|
+
) -> list[Recording]:
|
|
749
|
+
"""Collects pending events from given aggregates and
|
|
782
750
|
puts them in the application's event store.
|
|
783
751
|
"""
|
|
784
752
|
processing_event = ProcessingEvent()
|
|
@@ -789,10 +757,8 @@ class Application:
|
|
|
789
757
|
self.notify(processing_event.events) # Deprecated.
|
|
790
758
|
return recordings
|
|
791
759
|
|
|
792
|
-
def _record(self, processing_event: ProcessingEvent) ->
|
|
793
|
-
"""
|
|
794
|
-
Records given process event in the application's recorder.
|
|
795
|
-
"""
|
|
760
|
+
def _record(self, processing_event: ProcessingEvent) -> list[Recording]:
|
|
761
|
+
"""Records given process event in the application's recorder."""
|
|
796
762
|
recordings = self.events.put(
|
|
797
763
|
processing_event.events,
|
|
798
764
|
tracking=processing_event.tracking,
|
|
@@ -813,12 +779,9 @@ class Application:
|
|
|
813
779
|
continue
|
|
814
780
|
interval = self.snapshotting_intervals.get(type(aggregate))
|
|
815
781
|
if interval is not None and event.originator_version % interval == 0:
|
|
816
|
-
|
|
817
|
-
self.snapshotting_projectors
|
|
818
|
-
and type(aggregate) in self.snapshotting_projectors
|
|
819
|
-
):
|
|
782
|
+
try:
|
|
820
783
|
projector_func = self.snapshotting_projectors[type(aggregate)]
|
|
821
|
-
|
|
784
|
+
except KeyError:
|
|
822
785
|
projector_func = project_aggregate
|
|
823
786
|
if projector_func is project_aggregate and not isinstance(
|
|
824
787
|
event, CanMutateProtocol
|
|
@@ -849,8 +812,7 @@ class Application:
|
|
|
849
812
|
TMutableOrImmutableAggregate, TDomainEvent
|
|
850
813
|
] = project_aggregate,
|
|
851
814
|
) -> None:
|
|
852
|
-
"""
|
|
853
|
-
Takes a snapshot of the recorded state of the aggregate,
|
|
815
|
+
"""Takes a snapshot of the recorded state of the aggregate,
|
|
854
816
|
and puts the snapshot in the snapshot store.
|
|
855
817
|
"""
|
|
856
818
|
if self.snapshots is None:
|
|
@@ -869,9 +831,8 @@ class Application:
|
|
|
869
831
|
snapshot = snapshot_class.take(aggregate)
|
|
870
832
|
self.snapshots.put([snapshot])
|
|
871
833
|
|
|
872
|
-
def notify(self, new_events:
|
|
873
|
-
"""
|
|
874
|
-
Deprecated.
|
|
834
|
+
def notify(self, new_events: list[DomainEventProtocol]) -> None:
|
|
835
|
+
"""Deprecated.
|
|
875
836
|
|
|
876
837
|
Called after new aggregate events have been saved. This
|
|
877
838
|
method on this class doesn't actually do anything,
|
|
@@ -879,9 +840,8 @@ class Application:
|
|
|
879
840
|
need to take action when new domain events have been saved.
|
|
880
841
|
"""
|
|
881
842
|
|
|
882
|
-
def _notify(self, recordings:
|
|
883
|
-
"""
|
|
884
|
-
Called after new aggregate events have been saved. This
|
|
843
|
+
def _notify(self, recordings: list[Recording]) -> None:
|
|
844
|
+
"""Called after new aggregate events have been saved. This
|
|
885
845
|
method on this class doesn't actually do anything,
|
|
886
846
|
but this method may be implemented by subclasses that
|
|
887
847
|
need to take action when new domain events have been saved.
|
|
@@ -895,23 +855,14 @@ class Application:
|
|
|
895
855
|
TApplication = TypeVar("TApplication", bound=Application)
|
|
896
856
|
|
|
897
857
|
|
|
898
|
-
|
|
899
|
-
"
|
|
900
|
-
)
|
|
901
|
-
class AggregateNotFound(EventSourcingError): # noqa: N818
|
|
902
|
-
pass
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
class AggregateNotFoundError(AggregateNotFound):
|
|
906
|
-
"""
|
|
907
|
-
Raised when an :class:`~eventsourcing.domain.Aggregate`
|
|
858
|
+
class AggregateNotFoundError(EventSourcingError):
|
|
859
|
+
"""Raised when an :class:`~eventsourcing.domain.Aggregate`
|
|
908
860
|
object is not found in a :class:`Repository`.
|
|
909
861
|
"""
|
|
910
862
|
|
|
911
863
|
|
|
912
864
|
class EventSourcedLog(Generic[TDomainEvent]):
|
|
913
|
-
"""
|
|
914
|
-
Constructs a sequence of domain events, like an aggregate.
|
|
865
|
+
"""Constructs a sequence of domain events, like an aggregate.
|
|
915
866
|
But unlike an aggregate the events can be triggered
|
|
916
867
|
and selected for use in an application without
|
|
917
868
|
reconstructing a current state from all the events.
|
|
@@ -928,7 +879,7 @@ class EventSourcedLog(Generic[TDomainEvent]):
|
|
|
928
879
|
self,
|
|
929
880
|
events: EventStore,
|
|
930
881
|
originator_id: UUID,
|
|
931
|
-
logged_cls:
|
|
882
|
+
logged_cls: type[TDomainEvent], # TODO: Rename to 'event_class' in v10.
|
|
932
883
|
):
|
|
933
884
|
self.events = events
|
|
934
885
|
self.originator_id = originator_id
|
|
@@ -939,9 +890,7 @@ class EventSourcedLog(Generic[TDomainEvent]):
|
|
|
939
890
|
next_originator_version: int | None = None,
|
|
940
891
|
**kwargs: Any,
|
|
941
892
|
) -> TDomainEvent:
|
|
942
|
-
"""
|
|
943
|
-
Constructs and returns a new log event.
|
|
944
|
-
"""
|
|
893
|
+
"""Constructs and returns a new log event."""
|
|
945
894
|
return self._trigger_event(
|
|
946
895
|
logged_cls=self.logged_cls,
|
|
947
896
|
next_originator_version=next_originator_version,
|
|
@@ -950,13 +899,11 @@ class EventSourcedLog(Generic[TDomainEvent]):
|
|
|
950
899
|
|
|
951
900
|
def _trigger_event(
|
|
952
901
|
self,
|
|
953
|
-
logged_cls:
|
|
902
|
+
logged_cls: type[SDomainEvent],
|
|
954
903
|
next_originator_version: int | None = None,
|
|
955
904
|
**kwargs: Any,
|
|
956
|
-
) ->
|
|
957
|
-
"""
|
|
958
|
-
Constructs and returns a new log event.
|
|
959
|
-
"""
|
|
905
|
+
) -> SDomainEvent:
|
|
906
|
+
"""Constructs and returns a new log event."""
|
|
960
907
|
if next_originator_version is None:
|
|
961
908
|
last_logged = self.get_last()
|
|
962
909
|
if last_logged is None:
|
|
@@ -964,26 +911,22 @@ class EventSourcedLog(Generic[TDomainEvent]):
|
|
|
964
911
|
else:
|
|
965
912
|
next_originator_version = last_logged.originator_version + 1
|
|
966
913
|
|
|
967
|
-
return logged_cls(
|
|
914
|
+
return logged_cls(
|
|
968
915
|
originator_id=self.originator_id,
|
|
969
916
|
originator_version=next_originator_version,
|
|
970
|
-
timestamp=
|
|
917
|
+
timestamp=datetime_now_with_tzinfo(),
|
|
971
918
|
**kwargs,
|
|
972
919
|
)
|
|
973
920
|
|
|
974
921
|
def get_first(self) -> TDomainEvent | None:
|
|
975
|
-
"""
|
|
976
|
-
Selects the first logged event.
|
|
977
|
-
"""
|
|
922
|
+
"""Selects the first logged event."""
|
|
978
923
|
try:
|
|
979
924
|
return next(self.get(limit=1))
|
|
980
925
|
except StopIteration:
|
|
981
926
|
return None
|
|
982
927
|
|
|
983
928
|
def get_last(self) -> TDomainEvent | None:
|
|
984
|
-
"""
|
|
985
|
-
Selects the last logged event.
|
|
986
|
-
"""
|
|
929
|
+
"""Selects the last logged event."""
|
|
987
930
|
try:
|
|
988
931
|
return next(self.get(desc=True, limit=1))
|
|
989
932
|
except StopIteration:
|
|
@@ -997,12 +940,11 @@ class EventSourcedLog(Generic[TDomainEvent]):
|
|
|
997
940
|
desc: bool = False,
|
|
998
941
|
limit: int | None = None,
|
|
999
942
|
) -> Iterator[TDomainEvent]:
|
|
1000
|
-
"""
|
|
1001
|
-
Selects a range of logged events with limit,
|
|
943
|
+
"""Selects a range of logged events with limit,
|
|
1002
944
|
with ascending or descending order.
|
|
1003
945
|
"""
|
|
1004
946
|
return cast(
|
|
1005
|
-
Iterator[TDomainEvent],
|
|
947
|
+
"Iterator[TDomainEvent]",
|
|
1006
948
|
self.events.get(
|
|
1007
949
|
originator_id=self.originator_id,
|
|
1008
950
|
gt=gt,
|