eventsourcing 9.3.4__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 -188
- 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 +253 -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.4.dist-info → eventsourcing-9.4.0.dist-info}/LICENSE +1 -1
- {eventsourcing-9.3.4.dist-info → eventsourcing-9.4.0.dist-info}/METADATA +14 -13
- eventsourcing-9.4.0.dist-info/RECORD +26 -0
- {eventsourcing-9.3.4.dist-info → eventsourcing-9.4.0.dist-info}/WHEEL +1 -1
- eventsourcing-9.3.4.dist-info/RECORD +0 -24
- {eventsourcing-9.3.4.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
|
|
@@ -599,33 +588,17 @@ class ProcessingEvent:
|
|
|
599
588
|
self.collect_events(*aggregates, **kwargs)
|
|
600
589
|
|
|
601
590
|
|
|
602
|
-
class RecordingEvent:
|
|
603
|
-
def __init__(
|
|
604
|
-
self,
|
|
605
|
-
application_name: str,
|
|
606
|
-
recordings: List[Recording],
|
|
607
|
-
previous_max_notification_id: int | None,
|
|
608
|
-
):
|
|
609
|
-
self.application_name = application_name
|
|
610
|
-
self.recordings = recordings
|
|
611
|
-
self.previous_max_notification_id = previous_max_notification_id
|
|
612
|
-
|
|
613
|
-
|
|
614
591
|
class Application:
|
|
615
|
-
"""
|
|
616
|
-
Base class for event-sourced applications.
|
|
617
|
-
"""
|
|
592
|
+
"""Base class for event-sourced applications."""
|
|
618
593
|
|
|
619
594
|
name = "Application"
|
|
620
|
-
env: ClassVar[
|
|
595
|
+
env: ClassVar[dict[str, str]] = {}
|
|
621
596
|
is_snapshotting_enabled: bool = False
|
|
622
|
-
snapshotting_intervals: ClassVar[
|
|
623
|
-
Dict[Type[MutableOrImmutableAggregate], int] | None
|
|
624
|
-
] = None
|
|
597
|
+
snapshotting_intervals: ClassVar[dict[type[MutableOrImmutableAggregate], int]] = {}
|
|
625
598
|
snapshotting_projectors: ClassVar[
|
|
626
|
-
|
|
627
|
-
] =
|
|
628
|
-
snapshot_class:
|
|
599
|
+
dict[type[MutableOrImmutableAggregate], ProjectorFunction[Any, Any]]
|
|
600
|
+
] = {}
|
|
601
|
+
snapshot_class: type[SnapshotProtocol] = Snapshot
|
|
629
602
|
log_section_size = 10
|
|
630
603
|
notify_topics: Sequence[str] = []
|
|
631
604
|
|
|
@@ -639,8 +612,7 @@ class Application:
|
|
|
639
612
|
cls.name = cls.__name__
|
|
640
613
|
|
|
641
614
|
def __init__(self, env: EnvType | None = None) -> None:
|
|
642
|
-
"""
|
|
643
|
-
Initialises an application with an
|
|
615
|
+
"""Initialises an application with an
|
|
644
616
|
:class:`~eventsourcing.persistence.InfrastructureFactory`,
|
|
645
617
|
a :class:`~eventsourcing.persistence.Mapper`,
|
|
646
618
|
an :class:`~eventsourcing.persistence.ApplicationRecorder`,
|
|
@@ -659,21 +631,15 @@ class Application:
|
|
|
659
631
|
self._repository = self.construct_repository()
|
|
660
632
|
self._notification_log = self.construct_notification_log()
|
|
661
633
|
self.closing = Event()
|
|
662
|
-
self.previous_max_notification_id: int | None = (
|
|
663
|
-
self.recorder.max_notification_id()
|
|
664
|
-
)
|
|
665
634
|
|
|
666
635
|
@property
|
|
667
636
|
def repository(self) -> Repository:
|
|
668
|
-
"""
|
|
669
|
-
An application's repository reconstructs aggregates from stored events.
|
|
670
|
-
"""
|
|
637
|
+
"""An application's repository reconstructs aggregates from stored events."""
|
|
671
638
|
return self._repository
|
|
672
639
|
|
|
673
640
|
@property
|
|
674
641
|
def notification_log(self) -> LocalNotificationLog:
|
|
675
|
-
"""
|
|
676
|
-
An application's notification log presents all the aggregate events
|
|
642
|
+
"""An application's notification log presents all the aggregate events
|
|
677
643
|
of an application in the order they were recorded as a sequence of event
|
|
678
644
|
notifications.
|
|
679
645
|
"""
|
|
@@ -689,9 +655,7 @@ class Application:
|
|
|
689
655
|
return self._notification_log
|
|
690
656
|
|
|
691
657
|
def construct_env(self, name: str, env: EnvType | None = None) -> Environment:
|
|
692
|
-
"""
|
|
693
|
-
Constructs environment from which application will be configured.
|
|
694
|
-
"""
|
|
658
|
+
"""Constructs environment from which application will be configured."""
|
|
695
659
|
_env = dict(type(self).env)
|
|
696
660
|
if type(self).is_snapshotting_enabled or type(self).snapshotting_intervals:
|
|
697
661
|
_env["IS_SNAPSHOTTING_ENABLED"] = "y"
|
|
@@ -701,31 +665,28 @@ class Application:
|
|
|
701
665
|
return Environment(name, _env)
|
|
702
666
|
|
|
703
667
|
def construct_factory(self, env: Environment) -> InfrastructureFactory:
|
|
704
|
-
"""
|
|
705
|
-
Constructs an :class:`~eventsourcing.persistence.InfrastructureFactory`
|
|
668
|
+
"""Constructs an :class:`~eventsourcing.persistence.InfrastructureFactory`
|
|
706
669
|
for use by the application.
|
|
707
670
|
"""
|
|
708
671
|
return InfrastructureFactory.construct(env)
|
|
709
672
|
|
|
710
673
|
def construct_mapper(self) -> Mapper:
|
|
711
|
-
"""
|
|
712
|
-
Constructs a :class:`~eventsourcing.persistence.Mapper`
|
|
674
|
+
"""Constructs a :class:`~eventsourcing.persistence.Mapper`
|
|
713
675
|
for use by the application.
|
|
714
676
|
"""
|
|
715
677
|
return self.factory.mapper(transcoder=self.construct_transcoder())
|
|
716
678
|
|
|
717
679
|
def construct_transcoder(self) -> Transcoder:
|
|
718
|
-
"""
|
|
719
|
-
Constructs a :class:`~eventsourcing.persistence.Transcoder`
|
|
680
|
+
"""Constructs a :class:`~eventsourcing.persistence.Transcoder`
|
|
720
681
|
for use by the application.
|
|
721
682
|
"""
|
|
722
683
|
transcoder = self.factory.transcoder()
|
|
723
|
-
|
|
684
|
+
if isinstance(transcoder, JSONTranscoder):
|
|
685
|
+
self.register_transcodings(transcoder)
|
|
724
686
|
return transcoder
|
|
725
687
|
|
|
726
|
-
def register_transcodings(self, transcoder:
|
|
727
|
-
"""
|
|
728
|
-
Registers :class:`~eventsourcing.persistence.Transcoding`
|
|
688
|
+
def register_transcodings(self, transcoder: JSONTranscoder) -> None:
|
|
689
|
+
"""Registers :class:`~eventsourcing.persistence.Transcoding`
|
|
729
690
|
objects on given :class:`~eventsourcing.persistence.JSONTranscoder`.
|
|
730
691
|
"""
|
|
731
692
|
transcoder.register(UUIDAsHex())
|
|
@@ -733,15 +694,13 @@ class Application:
|
|
|
733
694
|
transcoder.register(DatetimeAsISO())
|
|
734
695
|
|
|
735
696
|
def construct_recorder(self) -> ApplicationRecorder:
|
|
736
|
-
"""
|
|
737
|
-
Constructs an :class:`~eventsourcing.persistence.ApplicationRecorder`
|
|
697
|
+
"""Constructs an :class:`~eventsourcing.persistence.ApplicationRecorder`
|
|
738
698
|
for use by the application.
|
|
739
699
|
"""
|
|
740
700
|
return self.factory.application_recorder()
|
|
741
701
|
|
|
742
702
|
def construct_event_store(self) -> EventStore:
|
|
743
|
-
"""
|
|
744
|
-
Constructs an :class:`~eventsourcing.persistence.EventStore`
|
|
703
|
+
"""Constructs an :class:`~eventsourcing.persistence.EventStore`
|
|
745
704
|
for use by the application to store and retrieve aggregate
|
|
746
705
|
:class:`~eventsourcing.domain.AggregateEvent` objects.
|
|
747
706
|
"""
|
|
@@ -751,8 +710,7 @@ class Application:
|
|
|
751
710
|
)
|
|
752
711
|
|
|
753
712
|
def construct_snapshot_store(self) -> EventStore:
|
|
754
|
-
"""
|
|
755
|
-
Constructs an :class:`~eventsourcing.persistence.EventStore`
|
|
713
|
+
"""Constructs an :class:`~eventsourcing.persistence.EventStore`
|
|
756
714
|
for use by the application to store and retrieve aggregate
|
|
757
715
|
:class:`~eventsourcing.domain.Snapshot` objects.
|
|
758
716
|
"""
|
|
@@ -763,9 +721,7 @@ class Application:
|
|
|
763
721
|
)
|
|
764
722
|
|
|
765
723
|
def construct_repository(self) -> Repository:
|
|
766
|
-
"""
|
|
767
|
-
Constructs a :class:`Repository` for use by the application.
|
|
768
|
-
"""
|
|
724
|
+
"""Constructs a :class:`Repository` for use by the application."""
|
|
769
725
|
cache_maxsize_envvar = self.env.get(self.AGGREGATE_CACHE_MAXSIZE)
|
|
770
726
|
cache_maxsize = int(cache_maxsize_envvar) if cache_maxsize_envvar else None
|
|
771
727
|
return Repository(
|
|
@@ -782,18 +738,15 @@ class Application:
|
|
|
782
738
|
)
|
|
783
739
|
|
|
784
740
|
def construct_notification_log(self) -> LocalNotificationLog:
|
|
785
|
-
"""
|
|
786
|
-
Constructs a :class:`LocalNotificationLog` for use by the application.
|
|
787
|
-
"""
|
|
741
|
+
"""Constructs a :class:`LocalNotificationLog` for use by the application."""
|
|
788
742
|
return LocalNotificationLog(self.recorder, section_size=self.log_section_size)
|
|
789
743
|
|
|
790
744
|
def save(
|
|
791
745
|
self,
|
|
792
746
|
*objs: MutableOrImmutableAggregate | DomainEventProtocol | None,
|
|
793
747
|
**kwargs: Any,
|
|
794
|
-
) ->
|
|
795
|
-
"""
|
|
796
|
-
Collects pending events from given aggregates and
|
|
748
|
+
) -> list[Recording]:
|
|
749
|
+
"""Collects pending events from given aggregates and
|
|
797
750
|
puts them in the application's event store.
|
|
798
751
|
"""
|
|
799
752
|
processing_event = ProcessingEvent()
|
|
@@ -804,10 +757,8 @@ class Application:
|
|
|
804
757
|
self.notify(processing_event.events) # Deprecated.
|
|
805
758
|
return recordings
|
|
806
759
|
|
|
807
|
-
def _record(self, processing_event: ProcessingEvent) ->
|
|
808
|
-
"""
|
|
809
|
-
Records given process event in the application's recorder.
|
|
810
|
-
"""
|
|
760
|
+
def _record(self, processing_event: ProcessingEvent) -> list[Recording]:
|
|
761
|
+
"""Records given process event in the application's recorder."""
|
|
811
762
|
recordings = self.events.put(
|
|
812
763
|
processing_event.events,
|
|
813
764
|
tracking=processing_event.tracking,
|
|
@@ -828,12 +779,9 @@ class Application:
|
|
|
828
779
|
continue
|
|
829
780
|
interval = self.snapshotting_intervals.get(type(aggregate))
|
|
830
781
|
if interval is not None and event.originator_version % interval == 0:
|
|
831
|
-
|
|
832
|
-
self.snapshotting_projectors
|
|
833
|
-
and type(aggregate) in self.snapshotting_projectors
|
|
834
|
-
):
|
|
782
|
+
try:
|
|
835
783
|
projector_func = self.snapshotting_projectors[type(aggregate)]
|
|
836
|
-
|
|
784
|
+
except KeyError:
|
|
837
785
|
projector_func = project_aggregate
|
|
838
786
|
if projector_func is project_aggregate and not isinstance(
|
|
839
787
|
event, CanMutateProtocol
|
|
@@ -864,8 +812,7 @@ class Application:
|
|
|
864
812
|
TMutableOrImmutableAggregate, TDomainEvent
|
|
865
813
|
] = project_aggregate,
|
|
866
814
|
) -> None:
|
|
867
|
-
"""
|
|
868
|
-
Takes a snapshot of the recorded state of the aggregate,
|
|
815
|
+
"""Takes a snapshot of the recorded state of the aggregate,
|
|
869
816
|
and puts the snapshot in the snapshot store.
|
|
870
817
|
"""
|
|
871
818
|
if self.snapshots is None:
|
|
@@ -884,9 +831,8 @@ class Application:
|
|
|
884
831
|
snapshot = snapshot_class.take(aggregate)
|
|
885
832
|
self.snapshots.put([snapshot])
|
|
886
833
|
|
|
887
|
-
def notify(self, new_events:
|
|
888
|
-
"""
|
|
889
|
-
Deprecated.
|
|
834
|
+
def notify(self, new_events: list[DomainEventProtocol]) -> None:
|
|
835
|
+
"""Deprecated.
|
|
890
836
|
|
|
891
837
|
Called after new aggregate events have been saved. This
|
|
892
838
|
method on this class doesn't actually do anything,
|
|
@@ -894,9 +840,8 @@ class Application:
|
|
|
894
840
|
need to take action when new domain events have been saved.
|
|
895
841
|
"""
|
|
896
842
|
|
|
897
|
-
def _notify(self, recordings:
|
|
898
|
-
"""
|
|
899
|
-
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
|
|
900
845
|
method on this class doesn't actually do anything,
|
|
901
846
|
but this method may be implemented by subclasses that
|
|
902
847
|
need to take action when new domain events have been saved.
|
|
@@ -910,23 +855,14 @@ class Application:
|
|
|
910
855
|
TApplication = TypeVar("TApplication", bound=Application)
|
|
911
856
|
|
|
912
857
|
|
|
913
|
-
|
|
914
|
-
"
|
|
915
|
-
)
|
|
916
|
-
class AggregateNotFound(EventSourcingError): # noqa: N818
|
|
917
|
-
pass
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
class AggregateNotFoundError(AggregateNotFound):
|
|
921
|
-
"""
|
|
922
|
-
Raised when an :class:`~eventsourcing.domain.Aggregate`
|
|
858
|
+
class AggregateNotFoundError(EventSourcingError):
|
|
859
|
+
"""Raised when an :class:`~eventsourcing.domain.Aggregate`
|
|
923
860
|
object is not found in a :class:`Repository`.
|
|
924
861
|
"""
|
|
925
862
|
|
|
926
863
|
|
|
927
864
|
class EventSourcedLog(Generic[TDomainEvent]):
|
|
928
|
-
"""
|
|
929
|
-
Constructs a sequence of domain events, like an aggregate.
|
|
865
|
+
"""Constructs a sequence of domain events, like an aggregate.
|
|
930
866
|
But unlike an aggregate the events can be triggered
|
|
931
867
|
and selected for use in an application without
|
|
932
868
|
reconstructing a current state from all the events.
|
|
@@ -943,7 +879,7 @@ class EventSourcedLog(Generic[TDomainEvent]):
|
|
|
943
879
|
self,
|
|
944
880
|
events: EventStore,
|
|
945
881
|
originator_id: UUID,
|
|
946
|
-
logged_cls:
|
|
882
|
+
logged_cls: type[TDomainEvent], # TODO: Rename to 'event_class' in v10.
|
|
947
883
|
):
|
|
948
884
|
self.events = events
|
|
949
885
|
self.originator_id = originator_id
|
|
@@ -954,9 +890,7 @@ class EventSourcedLog(Generic[TDomainEvent]):
|
|
|
954
890
|
next_originator_version: int | None = None,
|
|
955
891
|
**kwargs: Any,
|
|
956
892
|
) -> TDomainEvent:
|
|
957
|
-
"""
|
|
958
|
-
Constructs and returns a new log event.
|
|
959
|
-
"""
|
|
893
|
+
"""Constructs and returns a new log event."""
|
|
960
894
|
return self._trigger_event(
|
|
961
895
|
logged_cls=self.logged_cls,
|
|
962
896
|
next_originator_version=next_originator_version,
|
|
@@ -965,13 +899,11 @@ class EventSourcedLog(Generic[TDomainEvent]):
|
|
|
965
899
|
|
|
966
900
|
def _trigger_event(
|
|
967
901
|
self,
|
|
968
|
-
logged_cls:
|
|
902
|
+
logged_cls: type[SDomainEvent],
|
|
969
903
|
next_originator_version: int | None = None,
|
|
970
904
|
**kwargs: Any,
|
|
971
|
-
) ->
|
|
972
|
-
"""
|
|
973
|
-
Constructs and returns a new log event.
|
|
974
|
-
"""
|
|
905
|
+
) -> SDomainEvent:
|
|
906
|
+
"""Constructs and returns a new log event."""
|
|
975
907
|
if next_originator_version is None:
|
|
976
908
|
last_logged = self.get_last()
|
|
977
909
|
if last_logged is None:
|
|
@@ -979,26 +911,22 @@ class EventSourcedLog(Generic[TDomainEvent]):
|
|
|
979
911
|
else:
|
|
980
912
|
next_originator_version = last_logged.originator_version + 1
|
|
981
913
|
|
|
982
|
-
return logged_cls(
|
|
914
|
+
return logged_cls(
|
|
983
915
|
originator_id=self.originator_id,
|
|
984
916
|
originator_version=next_originator_version,
|
|
985
|
-
timestamp=
|
|
917
|
+
timestamp=datetime_now_with_tzinfo(),
|
|
986
918
|
**kwargs,
|
|
987
919
|
)
|
|
988
920
|
|
|
989
921
|
def get_first(self) -> TDomainEvent | None:
|
|
990
|
-
"""
|
|
991
|
-
Selects the first logged event.
|
|
992
|
-
"""
|
|
922
|
+
"""Selects the first logged event."""
|
|
993
923
|
try:
|
|
994
924
|
return next(self.get(limit=1))
|
|
995
925
|
except StopIteration:
|
|
996
926
|
return None
|
|
997
927
|
|
|
998
928
|
def get_last(self) -> TDomainEvent | None:
|
|
999
|
-
"""
|
|
1000
|
-
Selects the last logged event.
|
|
1001
|
-
"""
|
|
929
|
+
"""Selects the last logged event."""
|
|
1002
930
|
try:
|
|
1003
931
|
return next(self.get(desc=True, limit=1))
|
|
1004
932
|
except StopIteration:
|
|
@@ -1012,12 +940,11 @@ class EventSourcedLog(Generic[TDomainEvent]):
|
|
|
1012
940
|
desc: bool = False,
|
|
1013
941
|
limit: int | None = None,
|
|
1014
942
|
) -> Iterator[TDomainEvent]:
|
|
1015
|
-
"""
|
|
1016
|
-
Selects a range of logged events with limit,
|
|
943
|
+
"""Selects a range of logged events with limit,
|
|
1017
944
|
with ascending or descending order.
|
|
1018
945
|
"""
|
|
1019
946
|
return cast(
|
|
1020
|
-
Iterator[TDomainEvent],
|
|
947
|
+
"Iterator[TDomainEvent]",
|
|
1021
948
|
self.events.get(
|
|
1022
949
|
originator_id=self.originator_id,
|
|
1023
950
|
gt=gt,
|