eventsourcing 9.4.0a7__py3-none-any.whl → 9.4.0b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of eventsourcing might be problematic. Click here for more details.
- eventsourcing/application.py +22 -30
- eventsourcing/cipher.py +3 -1
- eventsourcing/dispatch.py +52 -11
- eventsourcing/domain.py +373 -360
- eventsourcing/interface.py +1 -1
- eventsourcing/persistence.py +26 -28
- eventsourcing/popo.py +5 -1
- eventsourcing/postgres.py +174 -127
- eventsourcing/projection.py +82 -26
- eventsourcing/sqlite.py +5 -1
- eventsourcing/system.py +14 -9
- eventsourcing/tests/application.py +57 -49
- eventsourcing/tests/domain.py +8 -6
- eventsourcing/tests/persistence.py +170 -143
- eventsourcing/tests/postgres_utils.py +12 -9
- eventsourcing/utils.py +27 -17
- {eventsourcing-9.4.0a7.dist-info → eventsourcing-9.4.0b1.dist-info}/METADATA +2 -2
- eventsourcing-9.4.0b1.dist-info/RECORD +26 -0
- eventsourcing-9.4.0a7.dist-info/RECORD +0 -26
- {eventsourcing-9.4.0a7.dist-info → eventsourcing-9.4.0b1.dist-info}/AUTHORS +0 -0
- {eventsourcing-9.4.0a7.dist-info → eventsourcing-9.4.0b1.dist-info}/LICENSE +0 -0
- {eventsourcing-9.4.0a7.dist-info → eventsourcing-9.4.0b1.dist-info}/WHEEL +0 -0
eventsourcing/domain.py
CHANGED
|
@@ -4,6 +4,8 @@ import dataclasses
|
|
|
4
4
|
import importlib
|
|
5
5
|
import inspect
|
|
6
6
|
import os
|
|
7
|
+
from collections import defaultdict
|
|
8
|
+
from dataclasses import dataclass
|
|
7
9
|
from datetime import datetime, tzinfo
|
|
8
10
|
from functools import cache
|
|
9
11
|
from types import FunctionType, WrapperDescriptorType
|
|
@@ -22,6 +24,8 @@ from typing import (
|
|
|
22
24
|
from uuid import UUID, uuid4
|
|
23
25
|
from warnings import warn
|
|
24
26
|
|
|
27
|
+
from typing_extensions import Self
|
|
28
|
+
|
|
25
29
|
from eventsourcing.utils import get_method_name, get_topic, resolve_topic
|
|
26
30
|
|
|
27
31
|
if TYPE_CHECKING:
|
|
@@ -40,6 +44,38 @@ domain and convert to local timezones when presenting values in user interfaces.
|
|
|
40
44
|
"""
|
|
41
45
|
|
|
42
46
|
|
|
47
|
+
class EventsourcingType(type):
|
|
48
|
+
"""
|
|
49
|
+
Base type for event sourcing domain model types (aggregates and events).
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
_T = TypeVar("_T")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def patch_dataclasses_process_class() -> None:
|
|
57
|
+
dataclasses_module = importlib.import_module("dataclasses")
|
|
58
|
+
original_process_class_func = dataclasses_module.__dict__["_process_class"]
|
|
59
|
+
|
|
60
|
+
def _patched_dataclasses_process_class(
|
|
61
|
+
cls: type[_T], *args: Any, **kwargs: Any
|
|
62
|
+
) -> type[_T]:
|
|
63
|
+
# Avoid processing aggregate and event dataclasses twice,
|
|
64
|
+
# because doing so screws up non-init and default fields.
|
|
65
|
+
if (
|
|
66
|
+
cls
|
|
67
|
+
and isinstance(cls, EventsourcingType)
|
|
68
|
+
and "__dataclass_fields__" in cls.__dict__
|
|
69
|
+
):
|
|
70
|
+
return cls
|
|
71
|
+
return original_process_class_func(cls, *args, **kwargs)
|
|
72
|
+
|
|
73
|
+
dataclasses_module.__dict__["_process_class"] = _patched_dataclasses_process_class
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
patch_dataclasses_process_class()
|
|
77
|
+
|
|
78
|
+
|
|
43
79
|
@runtime_checkable
|
|
44
80
|
class DomainEventProtocol(Protocol):
|
|
45
81
|
"""
|
|
@@ -52,20 +88,26 @@ class DomainEventProtocol(Protocol):
|
|
|
52
88
|
kinds of domain event classes, such as Pydantic classes.
|
|
53
89
|
"""
|
|
54
90
|
|
|
91
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
92
|
+
pass # pragma: no cover
|
|
93
|
+
|
|
55
94
|
@property
|
|
56
95
|
def originator_id(self) -> UUID:
|
|
57
96
|
"""
|
|
58
97
|
UUID identifying an aggregate to which the event belongs.
|
|
59
98
|
"""
|
|
99
|
+
raise NotImplementedError # pragma: no cover
|
|
60
100
|
|
|
61
101
|
@property
|
|
62
102
|
def originator_version(self) -> int:
|
|
63
103
|
"""
|
|
64
104
|
Integer identifying the version of the aggregate when the event occurred.
|
|
65
105
|
"""
|
|
106
|
+
raise NotImplementedError # pragma: no cover
|
|
66
107
|
|
|
67
108
|
|
|
68
109
|
TDomainEvent = TypeVar("TDomainEvent", bound=DomainEventProtocol)
|
|
110
|
+
SDomainEvent = TypeVar("SDomainEvent", bound=DomainEventProtocol)
|
|
69
111
|
|
|
70
112
|
|
|
71
113
|
class MutableAggregateProtocol(Protocol):
|
|
@@ -84,18 +126,21 @@ class MutableAggregateProtocol(Protocol):
|
|
|
84
126
|
"""
|
|
85
127
|
Mutable aggregates have a read-only ID that is a UUID.
|
|
86
128
|
"""
|
|
129
|
+
raise NotImplementedError # pragma: no cover
|
|
87
130
|
|
|
88
131
|
@property
|
|
89
132
|
def version(self) -> int:
|
|
90
133
|
"""
|
|
91
134
|
Mutable aggregates have a read-write version that is an int.
|
|
92
135
|
"""
|
|
136
|
+
raise NotImplementedError # pragma: no cover
|
|
93
137
|
|
|
94
138
|
@version.setter
|
|
95
139
|
def version(self, value: int) -> None:
|
|
96
140
|
"""
|
|
97
141
|
Mutable aggregates have a read-write version that is an int.
|
|
98
142
|
"""
|
|
143
|
+
raise NotImplementedError # pragma: no cover
|
|
99
144
|
|
|
100
145
|
|
|
101
146
|
class ImmutableAggregateProtocol(Protocol):
|
|
@@ -114,12 +159,14 @@ class ImmutableAggregateProtocol(Protocol):
|
|
|
114
159
|
"""
|
|
115
160
|
Immutable aggregates have a read-only ID that is a UUID.
|
|
116
161
|
"""
|
|
162
|
+
raise NotImplementedError # pragma: no cover
|
|
117
163
|
|
|
118
164
|
@property
|
|
119
165
|
def version(self) -> int:
|
|
120
166
|
"""
|
|
121
167
|
Immutable aggregates have a read-only version that is an int.
|
|
122
168
|
"""
|
|
169
|
+
raise NotImplementedError # pragma: no cover
|
|
123
170
|
|
|
124
171
|
|
|
125
172
|
MutableOrImmutableAggregate = Union[
|
|
@@ -144,6 +191,7 @@ class CollectEventsProtocol(Protocol):
|
|
|
144
191
|
"""
|
|
145
192
|
Returns a sequence of events.
|
|
146
193
|
"""
|
|
194
|
+
raise NotImplementedError # pragma: no cover
|
|
147
195
|
|
|
148
196
|
|
|
149
197
|
@runtime_checkable
|
|
@@ -197,7 +245,7 @@ class CanCreateTimestamp:
|
|
|
197
245
|
return datetime_now_with_tzinfo()
|
|
198
246
|
|
|
199
247
|
|
|
200
|
-
TAggregate = TypeVar("TAggregate", bound="
|
|
248
|
+
TAggregate = TypeVar("TAggregate", bound="BaseAggregate")
|
|
201
249
|
|
|
202
250
|
|
|
203
251
|
class HasOriginatorIDVersion:
|
|
@@ -217,6 +265,7 @@ class CanMutateAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
|
|
|
217
265
|
method that evolves the state of an aggregate.
|
|
218
266
|
"""
|
|
219
267
|
|
|
268
|
+
# Todo: Move this to a HasTimestamp? Why is it here??
|
|
220
269
|
timestamp: datetime
|
|
221
270
|
"""Timezone-aware :class:`datetime` object representing when an event occurred."""
|
|
222
271
|
|
|
@@ -263,7 +312,7 @@ class CanMutateAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
|
|
|
263
312
|
# Return the mutated aggregate.
|
|
264
313
|
return aggregate
|
|
265
314
|
|
|
266
|
-
def apply(self, aggregate:
|
|
315
|
+
def apply(self, aggregate: Any) -> None:
|
|
267
316
|
"""
|
|
268
317
|
Applies the domain event to its aggregate.
|
|
269
318
|
|
|
@@ -326,7 +375,7 @@ class CanInitAggregate(CanMutateAggregate):
|
|
|
326
375
|
return agg
|
|
327
376
|
|
|
328
377
|
|
|
329
|
-
class MetaDomainEvent(
|
|
378
|
+
class MetaDomainEvent(EventsourcingType):
|
|
330
379
|
"""
|
|
331
380
|
Metaclass which ensures all domain event classes are frozen dataclasses.
|
|
332
381
|
"""
|
|
@@ -342,6 +391,7 @@ class MetaDomainEvent(type):
|
|
|
342
391
|
return event_cls
|
|
343
392
|
|
|
344
393
|
|
|
394
|
+
@dataclass(frozen=True)
|
|
345
395
|
class DomainEvent(CanCreateTimestamp, metaclass=MetaDomainEvent):
|
|
346
396
|
"""
|
|
347
397
|
Frozen data class representing domain model events.
|
|
@@ -363,6 +413,7 @@ class AggregateEvent(CanMutateAggregate, DomainEvent):
|
|
|
363
413
|
"""
|
|
364
414
|
|
|
365
415
|
|
|
416
|
+
@dataclass(frozen=True)
|
|
366
417
|
class AggregateCreated(CanInitAggregate, AggregateEvent):
|
|
367
418
|
"""
|
|
368
419
|
Frozen data class representing the initial creation of an aggregate.
|
|
@@ -392,10 +443,6 @@ class LogEvent(DomainEvent):
|
|
|
392
443
|
"""
|
|
393
444
|
|
|
394
445
|
|
|
395
|
-
# Deprecated: Use TDomainEvent instead.
|
|
396
|
-
TLogEvent = TypeVar("TLogEvent", bound=DomainEventProtocol)
|
|
397
|
-
|
|
398
|
-
|
|
399
446
|
def _filter_kwargs_for_method_params(
|
|
400
447
|
kwargs: dict[str, Any], method: Callable[..., Any]
|
|
401
448
|
) -> dict[str, Any]:
|
|
@@ -442,12 +489,12 @@ class CommandMethodDecorator:
|
|
|
442
489
|
elif isinstance(event_spec, type) and issubclass(
|
|
443
490
|
event_spec, CanMutateAggregate
|
|
444
491
|
):
|
|
445
|
-
if event_spec in
|
|
492
|
+
if event_spec in _given_event_classes:
|
|
446
493
|
name = event_spec.__name__
|
|
447
494
|
msg = f"{name} event class used in more than one decorator"
|
|
448
495
|
raise TypeError(msg)
|
|
449
496
|
self.given_event_cls = event_spec
|
|
450
|
-
|
|
497
|
+
_given_event_classes.add(event_spec)
|
|
451
498
|
|
|
452
499
|
# Process a decorated property.
|
|
453
500
|
if isinstance(decorated_obj, property):
|
|
@@ -730,7 +777,7 @@ class BoundCommandMethodDecorator:
|
|
|
730
777
|
kwargs = _coerce_args_to_kwargs(
|
|
731
778
|
self.event_decorator.decorated_method, args, kwargs
|
|
732
779
|
)
|
|
733
|
-
event_cls =
|
|
780
|
+
event_cls = decorator_event_classes[self.event_decorator]
|
|
734
781
|
kwargs = _filter_kwargs_for_method_params(kwargs, event_cls)
|
|
735
782
|
self.aggregate.trigger_event(event_cls, **kwargs)
|
|
736
783
|
|
|
@@ -738,14 +785,30 @@ class BoundCommandMethodDecorator:
|
|
|
738
785
|
self.trigger(*args, **kwargs)
|
|
739
786
|
|
|
740
787
|
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
788
|
+
class DecoratorEvent(CanMutateAggregate):
|
|
789
|
+
def apply(self, aggregate: Aggregate) -> None:
|
|
790
|
+
"""
|
|
791
|
+
Applies event to aggregate by calling method decorated by @event.
|
|
792
|
+
"""
|
|
793
|
+
# Call super method, just in case any base classes need it.
|
|
794
|
+
super().apply(aggregate)
|
|
795
|
+
|
|
796
|
+
# Identify the method that was decorated.
|
|
797
|
+
decorated_method = _decorated_methods[type(self)]
|
|
744
798
|
|
|
799
|
+
# Select event attributes mentioned in method signature.
|
|
800
|
+
kwargs = _filter_kwargs_for_method_params(self.__dict__, decorated_method)
|
|
745
801
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
802
|
+
# Call the original method with event attribute values.
|
|
803
|
+
decorated_method(aggregate, **kwargs)
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
_given_event_classes: set[type] = set()
|
|
807
|
+
_decorated_methods: dict[type, CommandMethod] = {}
|
|
808
|
+
_created_event_classes: dict[type, list[type[CanInitAggregate]]] = {}
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
decorator_event_classes: dict[CommandMethodDecorator, type[DecoratorEvent]] = {}
|
|
749
812
|
|
|
750
813
|
|
|
751
814
|
def _check_no_variable_params(method: FunctionType) -> None:
|
|
@@ -883,110 +946,283 @@ def _raise_missing_names_type_error(missing_names: list[str], msg: str) -> None:
|
|
|
883
946
|
raise TypeError(msg)
|
|
884
947
|
|
|
885
948
|
|
|
886
|
-
_annotations_mention_id: set[
|
|
887
|
-
_init_mentions_id: set[
|
|
949
|
+
_annotations_mention_id: set[type[BaseAggregate]] = set()
|
|
950
|
+
_init_mentions_id: set[type[BaseAggregate]] = set()
|
|
951
|
+
_create_id_param_names: dict[type[BaseAggregate], list[str]] = defaultdict(list)
|
|
888
952
|
|
|
889
953
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
954
|
+
class MetaAggregate(EventsourcingType, Generic[TAggregate], type):
|
|
955
|
+
"""
|
|
956
|
+
Metaclass for aggregate classes.
|
|
957
|
+
"""
|
|
958
|
+
|
|
959
|
+
def _define_event_class(
|
|
960
|
+
cls,
|
|
961
|
+
name: str,
|
|
962
|
+
bases: tuple[type[CanMutateAggregate], ...],
|
|
963
|
+
apply_method: CommandMethod | None,
|
|
964
|
+
) -> type[CanMutateAggregate]:
|
|
965
|
+
# Define annotations for the event class (specs the init method).
|
|
966
|
+
annotations = {}
|
|
967
|
+
if apply_method is not None:
|
|
968
|
+
method_signature = inspect.signature(apply_method)
|
|
969
|
+
supers = {
|
|
970
|
+
s for b in bases for s in b.__mro__ if hasattr(s, "__annotations__")
|
|
971
|
+
}
|
|
972
|
+
super_annotations = {a for s in supers for a in s.__annotations__}
|
|
973
|
+
for param_name, param in list(method_signature.parameters.items())[1:]:
|
|
974
|
+
# Don't define 'id' on a "created" class.
|
|
975
|
+
if param_name == "id" and apply_method.__name__ == "__init__":
|
|
976
|
+
continue
|
|
977
|
+
# Don't override super class annotations, unless no default on param.
|
|
978
|
+
if param_name not in super_annotations or param.default == param.empty:
|
|
979
|
+
annotations[param_name] = param.annotation or "typing.Any"
|
|
980
|
+
event_cls_qualname = f"{cls.__qualname__}.{name}"
|
|
981
|
+
event_cls_dict = {
|
|
982
|
+
"__annotations__": annotations,
|
|
983
|
+
"__module__": cls.__module__,
|
|
984
|
+
"__qualname__": event_cls_qualname,
|
|
985
|
+
}
|
|
899
986
|
|
|
987
|
+
# Create the event class object.
|
|
988
|
+
return cast(type[CanMutateAggregate], type(name, bases, event_cls_dict))
|
|
900
989
|
|
|
901
|
-
def
|
|
990
|
+
def __call__(
|
|
991
|
+
cls: MetaAggregate[TAggregate], *args: Any, **kwargs: Any
|
|
992
|
+
) -> TAggregate:
|
|
993
|
+
created_event_classes = _created_event_classes[cls]
|
|
994
|
+
if len(created_event_classes) > 1:
|
|
995
|
+
msg = (
|
|
996
|
+
f"{cls.__qualname__} can't decide which of many "
|
|
997
|
+
'"created" event classes to use: '
|
|
998
|
+
f"""'{"', '".join(c.__name__ for c in created_event_classes)}'. """
|
|
999
|
+
"Please use class arg 'created_event_name' or"
|
|
1000
|
+
" @event decorator on __init__ method."
|
|
1001
|
+
)
|
|
1002
|
+
raise TypeError(msg)
|
|
902
1003
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
1004
|
+
cls_init: FunctionType | WrapperDescriptorType = cls.__init__ # type: ignore
|
|
1005
|
+
kwargs = _coerce_args_to_kwargs(
|
|
1006
|
+
cls_init,
|
|
1007
|
+
args,
|
|
1008
|
+
kwargs,
|
|
1009
|
+
expects_id=cls in _annotations_mention_id,
|
|
1010
|
+
)
|
|
1011
|
+
return cls._create(
|
|
1012
|
+
event_class=created_event_classes[0],
|
|
1013
|
+
**kwargs,
|
|
1014
|
+
)
|
|
908
1015
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
1016
|
+
def _create(
|
|
1017
|
+
cls: MetaAggregate[TAggregate],
|
|
1018
|
+
event_class: type[CanInitAggregate],
|
|
1019
|
+
**kwargs: Any,
|
|
1020
|
+
) -> TAggregate:
|
|
1021
|
+
raise NotImplementedError # pragma: no cover
|
|
913
1022
|
|
|
914
|
-
|
|
915
|
-
return idempotent_wrap(cls)
|
|
1023
|
+
_created_event_class: type[CanInitAggregate]
|
|
916
1024
|
|
|
917
1025
|
|
|
918
|
-
class MetaAggregate
|
|
1026
|
+
class BaseAggregate(metaclass=MetaAggregate):
|
|
919
1027
|
"""
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
Initialises aggregate classes by defining event classes.
|
|
1028
|
+
Base class for aggregates.
|
|
923
1029
|
"""
|
|
924
1030
|
|
|
925
1031
|
INITIAL_VERSION = 1
|
|
926
1032
|
|
|
927
|
-
|
|
928
|
-
|
|
1033
|
+
@staticmethod
|
|
1034
|
+
def create_id(*_: Any, **__: Any) -> UUID:
|
|
1035
|
+
"""
|
|
1036
|
+
Returns a new aggregate ID.
|
|
1037
|
+
"""
|
|
1038
|
+
return uuid4()
|
|
929
1039
|
|
|
930
|
-
|
|
931
|
-
|
|
1040
|
+
@classmethod
|
|
1041
|
+
def _create(
|
|
1042
|
+
cls: type[Self],
|
|
1043
|
+
event_class: type[CanInitAggregate],
|
|
1044
|
+
*,
|
|
1045
|
+
id: UUID | None = None, # noqa: A002
|
|
1046
|
+
**kwargs: Any,
|
|
1047
|
+
) -> Self:
|
|
1048
|
+
"""
|
|
1049
|
+
Constructs a new aggregate object instance.
|
|
1050
|
+
"""
|
|
1051
|
+
# Construct the domain event with an ID and a
|
|
1052
|
+
# version, and a topic for the aggregate class.
|
|
1053
|
+
create_id_kwargs = {
|
|
1054
|
+
k: v for k, v in kwargs.items() if k in _create_id_param_names[cls]
|
|
1055
|
+
}
|
|
1056
|
+
if id is not None:
|
|
1057
|
+
originator_id = id
|
|
1058
|
+
if not isinstance(originator_id, UUID):
|
|
1059
|
+
msg = f"Given id was not a UUID: {originator_id}"
|
|
1060
|
+
raise TypeError(msg)
|
|
1061
|
+
else:
|
|
1062
|
+
originator_id = cls.create_id(**create_id_kwargs)
|
|
1063
|
+
if not isinstance(originator_id, UUID):
|
|
1064
|
+
msg = (
|
|
1065
|
+
f"{cls.create_id.__module__}.{cls.create_id.__qualname__}"
|
|
1066
|
+
f" did not return UUID, it returned: {originator_id}"
|
|
1067
|
+
)
|
|
1068
|
+
raise TypeError(msg)
|
|
932
1069
|
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1070
|
+
# Impose the required common "created" event attribute values.
|
|
1071
|
+
kwargs = kwargs.copy()
|
|
1072
|
+
kwargs.update(
|
|
1073
|
+
originator_topic=get_topic(cls),
|
|
1074
|
+
originator_id=originator_id,
|
|
1075
|
+
originator_version=cls.INITIAL_VERSION,
|
|
1076
|
+
)
|
|
1077
|
+
if kwargs.get("timestamp") is None:
|
|
1078
|
+
kwargs["timestamp"] = event_class.create_timestamp()
|
|
940
1079
|
|
|
941
|
-
|
|
942
|
-
|
|
1080
|
+
try:
|
|
1081
|
+
created_event = event_class(**kwargs)
|
|
1082
|
+
except TypeError as e:
|
|
1083
|
+
msg = f"Unable to construct '{event_class.__qualname__}' event: {e}"
|
|
1084
|
+
raise TypeError(msg) from e
|
|
1085
|
+
# Construct the aggregate object.
|
|
1086
|
+
agg = cast(Self, created_event.mutate(None))
|
|
943
1087
|
|
|
944
|
-
|
|
945
|
-
|
|
1088
|
+
assert agg is not None
|
|
1089
|
+
# Append the domain event to pending list.
|
|
1090
|
+
agg._pending_events.append(created_event)
|
|
1091
|
+
# Return the aggregate.
|
|
1092
|
+
return agg
|
|
946
1093
|
|
|
947
|
-
|
|
948
|
-
|
|
1094
|
+
def __base_init__(
|
|
1095
|
+
self, originator_id: UUID, originator_version: int, timestamp: datetime
|
|
1096
|
+
) -> None:
|
|
1097
|
+
"""
|
|
1098
|
+
Initialises an aggregate object with an :data:`id`, a :data:`version`
|
|
1099
|
+
number, and a :data:`timestamp`.
|
|
1100
|
+
"""
|
|
1101
|
+
self._id = originator_id
|
|
1102
|
+
self._version = originator_version
|
|
1103
|
+
self._created_on = timestamp
|
|
1104
|
+
self._modified_on = timestamp
|
|
1105
|
+
self._pending_events: list[CanMutateAggregate] = []
|
|
949
1106
|
|
|
950
|
-
|
|
1107
|
+
@property
|
|
1108
|
+
def id(self) -> UUID:
|
|
1109
|
+
"""
|
|
1110
|
+
The ID of the aggregate.
|
|
1111
|
+
"""
|
|
1112
|
+
return self._id
|
|
1113
|
+
|
|
1114
|
+
@property
|
|
1115
|
+
def version(self) -> int:
|
|
1116
|
+
"""
|
|
1117
|
+
The version number of the aggregate.
|
|
1118
|
+
"""
|
|
1119
|
+
return self._version
|
|
1120
|
+
|
|
1121
|
+
@version.setter
|
|
1122
|
+
def version(self, version: int) -> None:
|
|
1123
|
+
self._version = version
|
|
1124
|
+
|
|
1125
|
+
@property
|
|
1126
|
+
def created_on(self) -> datetime:
|
|
1127
|
+
"""
|
|
1128
|
+
The date and time when the aggregate was created.
|
|
1129
|
+
"""
|
|
1130
|
+
return self._created_on
|
|
1131
|
+
|
|
1132
|
+
@property
|
|
1133
|
+
def modified_on(self) -> datetime:
|
|
1134
|
+
"""
|
|
1135
|
+
The date and time when the aggregate was last modified.
|
|
1136
|
+
"""
|
|
1137
|
+
return self._modified_on
|
|
1138
|
+
|
|
1139
|
+
@modified_on.setter
|
|
1140
|
+
def modified_on(self, modified_on: datetime) -> None:
|
|
1141
|
+
self._modified_on = modified_on
|
|
951
1142
|
|
|
952
|
-
|
|
1143
|
+
@property
|
|
1144
|
+
def pending_events(self) -> list[CanMutateAggregate]:
|
|
953
1145
|
"""
|
|
954
|
-
|
|
1146
|
+
A list of pending events.
|
|
955
1147
|
"""
|
|
1148
|
+
return self._pending_events
|
|
956
1149
|
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1150
|
+
def trigger_event(
|
|
1151
|
+
self,
|
|
1152
|
+
event_class: type[CanMutateAggregate],
|
|
1153
|
+
**kwargs: Any,
|
|
1154
|
+
) -> None:
|
|
1155
|
+
"""
|
|
1156
|
+
Triggers domain event of given type, by creating
|
|
1157
|
+
an event object and using it to mutate the aggregate.
|
|
1158
|
+
"""
|
|
1159
|
+
# Construct the domain event as the
|
|
1160
|
+
# next in the aggregate's sequence.
|
|
1161
|
+
# Use counting to generate the sequence.
|
|
1162
|
+
next_version = self.version + 1
|
|
1163
|
+
|
|
1164
|
+
# Impose the required common domain event attribute values.
|
|
1165
|
+
kwargs = kwargs.copy()
|
|
1166
|
+
kwargs.update(
|
|
1167
|
+
originator_id=self.id,
|
|
1168
|
+
originator_version=next_version,
|
|
1169
|
+
)
|
|
1170
|
+
if kwargs.get("timestamp") is None:
|
|
1171
|
+
kwargs["timestamp"] = event_class.create_timestamp()
|
|
961
1172
|
|
|
962
1173
|
try:
|
|
963
|
-
|
|
964
|
-
except
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
else:
|
|
968
|
-
try:
|
|
969
|
-
class_annotations.pop("id")
|
|
970
|
-
except KeyError:
|
|
971
|
-
annotations_mention_id = False
|
|
972
|
-
else:
|
|
973
|
-
annotations_mention_id = True
|
|
974
|
-
aggregate_cls = type.__new__(cls, *args)
|
|
975
|
-
if class_annotations or any(dataclasses.is_dataclass(base) for base in args[1]):
|
|
976
|
-
aggregate_cls = dataclasses.dataclass(eq=False, repr=False)(aggregate_cls)
|
|
977
|
-
if annotations_mention_id:
|
|
978
|
-
_annotations_mention_id.add(aggregate_cls)
|
|
979
|
-
return aggregate_cls
|
|
1174
|
+
new_event = event_class(**kwargs)
|
|
1175
|
+
except TypeError as e:
|
|
1176
|
+
msg = f"Can't construct event {event_class}: {e}"
|
|
1177
|
+
raise TypeError(msg) from None
|
|
980
1178
|
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1179
|
+
# Mutate aggregate with domain event.
|
|
1180
|
+
new_event.mutate(self)
|
|
1181
|
+
# Append the domain event to pending list.
|
|
1182
|
+
self._pending_events.append(new_event)
|
|
1183
|
+
|
|
1184
|
+
def collect_events(self) -> Sequence[CanMutateAggregate]:
|
|
1185
|
+
"""
|
|
1186
|
+
Collects and returns a list of pending aggregate
|
|
1187
|
+
:class:`AggregateEvent` objects.
|
|
1188
|
+
"""
|
|
1189
|
+
collected = []
|
|
1190
|
+
while self._pending_events:
|
|
1191
|
+
collected.append(self._pending_events.pop(0))
|
|
1192
|
+
return collected
|
|
1193
|
+
|
|
1194
|
+
def __eq__(self, other: object) -> bool:
|
|
1195
|
+
return type(self) is type(other) and self.__dict__ == other.__dict__
|
|
1196
|
+
|
|
1197
|
+
def __repr__(self) -> str:
|
|
1198
|
+
attrs = [
|
|
1199
|
+
f"{k.lstrip('_')}={v!r}"
|
|
1200
|
+
for k, v in self.__dict__.items()
|
|
1201
|
+
if k != "_pending_events"
|
|
1202
|
+
]
|
|
1203
|
+
return f"{type(self).__name__}({', '.join(attrs)})"
|
|
1204
|
+
|
|
1205
|
+
def __init_subclass__(
|
|
1206
|
+
cls: type[BaseAggregate], *, created_event_name: str | None = None
|
|
985
1207
|
) -> None:
|
|
986
1208
|
"""
|
|
987
|
-
Initialises aggregate
|
|
1209
|
+
Initialises aggregate subclass by defining __init__ method and event classes.
|
|
988
1210
|
"""
|
|
989
|
-
super().
|
|
1211
|
+
super().__init_subclass__()
|
|
1212
|
+
|
|
1213
|
+
class_annotations = cls.__dict__.get("__annotations__", {})
|
|
1214
|
+
try:
|
|
1215
|
+
class_annotations.pop("id")
|
|
1216
|
+
_annotations_mention_id.add(cls)
|
|
1217
|
+
except KeyError:
|
|
1218
|
+
pass
|
|
1219
|
+
|
|
1220
|
+
if (
|
|
1221
|
+
class_annotations
|
|
1222
|
+
or cls in _annotations_mention_id
|
|
1223
|
+
or any(dataclasses.is_dataclass(base) for base in cls.__bases__)
|
|
1224
|
+
):
|
|
1225
|
+
dataclasses.dataclass(eq=False, repr=False)(cls)
|
|
990
1226
|
|
|
991
1227
|
# Identify or define a base event class for this aggregate.
|
|
992
1228
|
base_event_name = "Event"
|
|
@@ -995,7 +1231,9 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
995
1231
|
base_event_cls = cls.__dict__[base_event_name]
|
|
996
1232
|
except KeyError:
|
|
997
1233
|
base_event_cls = cls._define_event_class(
|
|
998
|
-
base_event_name,
|
|
1234
|
+
name=base_event_name,
|
|
1235
|
+
bases=(getattr(cls, base_event_name, AggregateEvent),),
|
|
1236
|
+
apply_method=None,
|
|
999
1237
|
)
|
|
1000
1238
|
setattr(cls, base_event_name, base_event_cls)
|
|
1001
1239
|
|
|
@@ -1019,15 +1257,8 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
1019
1257
|
if isinstance(value, type) and issubclass(value, CanInitAggregate):
|
|
1020
1258
|
created_event_classes[name] = value
|
|
1021
1259
|
|
|
1022
|
-
# Disallow using both '_created_event_class' and 'created_event_name'.
|
|
1023
|
-
created_event_class: type[CanInitAggregate] | None = cls.__dict__.get(
|
|
1024
|
-
"_created_event_class"
|
|
1025
|
-
)
|
|
1026
|
-
if created_event_class and created_event_name:
|
|
1027
|
-
msg = "Can't use both '_created_event_class' and 'created_event_name'"
|
|
1028
|
-
raise TypeError(msg)
|
|
1029
|
-
|
|
1030
1260
|
# Identify or define the aggregate's "created" event class.
|
|
1261
|
+
created_event_class: type[CanInitAggregate] | None = None
|
|
1031
1262
|
|
|
1032
1263
|
# Is the init method decorated with a CommandMethodDecorator?
|
|
1033
1264
|
if isinstance(cls.__dict__.get("__init__"), CommandMethodDecorator):
|
|
@@ -1040,16 +1271,13 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
1040
1271
|
if created_event_name:
|
|
1041
1272
|
msg = "Can't use both 'created_event_name' and decorator on __init__"
|
|
1042
1273
|
raise TypeError(msg)
|
|
1043
|
-
# Disallow using both '_created_event_class' and decorator on __init__.
|
|
1044
|
-
if created_event_class:
|
|
1045
|
-
msg = "Can't use both '_created_event_class' and decorator on __init__"
|
|
1046
|
-
raise TypeError(msg)
|
|
1047
1274
|
|
|
1048
1275
|
# Does the decorator specify a "created" event class?
|
|
1049
1276
|
if init_decorator.given_event_cls:
|
|
1050
1277
|
created_event_class = cast(
|
|
1051
1278
|
type[CanInitAggregate], init_decorator.given_event_cls
|
|
1052
1279
|
)
|
|
1280
|
+
|
|
1053
1281
|
# Does the decorator specify a "created" event name?
|
|
1054
1282
|
elif init_decorator.event_cls_name:
|
|
1055
1283
|
created_event_name = init_decorator.event_cls_name
|
|
@@ -1061,7 +1289,7 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
1061
1289
|
|
|
1062
1290
|
# TODO: Write a test to cover this when "Created" class is explicitly defined.
|
|
1063
1291
|
# Check if init mentions ID.
|
|
1064
|
-
for param_name in inspect.signature(cls.__init__).parameters:
|
|
1292
|
+
for param_name in inspect.signature(cls.__init__).parameters:
|
|
1065
1293
|
if param_name == "id":
|
|
1066
1294
|
_init_mentions_id.add(cls)
|
|
1067
1295
|
break
|
|
@@ -1095,17 +1323,21 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
1095
1323
|
if created_event_name and len(created_event_classes) == 1:
|
|
1096
1324
|
base_created_event_cls = next(iter(created_event_classes.values()))
|
|
1097
1325
|
else:
|
|
1326
|
+
# Todo: This could probably be improved.
|
|
1327
|
+
# Look for first class in MRO that has one specified "created" class.
|
|
1098
1328
|
for base_cls in cls.__mro__:
|
|
1099
|
-
if
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
base_cls
|
|
1104
|
-
)
|
|
1105
|
-
if base_created_event_cls:
|
|
1329
|
+
if (
|
|
1330
|
+
base_cls in _created_event_classes
|
|
1331
|
+
and len(_created_event_classes[base_cls]) == 1
|
|
1332
|
+
):
|
|
1333
|
+
base_created_event_cls = _created_event_classes[base_cls][0]
|
|
1106
1334
|
break
|
|
1107
1335
|
else: # pragma: no cover
|
|
1108
|
-
|
|
1336
|
+
# Todo: Write a test to cover this.
|
|
1337
|
+
msg = (
|
|
1338
|
+
"Can't find base aggregate class with "
|
|
1339
|
+
"a specified 'created' event class"
|
|
1340
|
+
)
|
|
1109
1341
|
raise TypeError(msg)
|
|
1110
1342
|
|
|
1111
1343
|
if not created_event_name:
|
|
@@ -1141,10 +1373,10 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
1141
1373
|
setattr(cls, created_event_name, created_event_class)
|
|
1142
1374
|
|
|
1143
1375
|
if created_event_class:
|
|
1144
|
-
cls
|
|
1376
|
+
_created_event_classes[cls] = [created_event_class]
|
|
1145
1377
|
else:
|
|
1146
1378
|
# Prepare to disallow ambiguity of choice between created event classes.
|
|
1147
|
-
|
|
1379
|
+
_created_event_classes[cls] = list(created_event_classes.values())
|
|
1148
1380
|
|
|
1149
1381
|
# Prepare the subsequent event classes.
|
|
1150
1382
|
for attr_name, attr_value in tuple(cls.__dict__.items()):
|
|
@@ -1198,7 +1430,7 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
1198
1430
|
)
|
|
1199
1431
|
event_cls = cls._define_event_class(
|
|
1200
1432
|
event_decorator.given_event_cls.__name__,
|
|
1201
|
-
(
|
|
1433
|
+
(DecoratorEvent, given_subclass),
|
|
1202
1434
|
None,
|
|
1203
1435
|
)
|
|
1204
1436
|
|
|
@@ -1215,19 +1447,19 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
1215
1447
|
# Define event class from signature of original method.
|
|
1216
1448
|
event_cls = cls._define_event_class(
|
|
1217
1449
|
event_decorator.event_cls_name,
|
|
1218
|
-
(
|
|
1450
|
+
(DecoratorEvent, base_event_cls),
|
|
1219
1451
|
event_decorator.decorated_method,
|
|
1220
1452
|
)
|
|
1221
1453
|
|
|
1222
1454
|
# Cache the decorated method for the event class to use.
|
|
1223
|
-
|
|
1455
|
+
_decorated_methods[event_cls] = event_decorator.decorated_method
|
|
1224
1456
|
|
|
1225
1457
|
# Set the event class as an attribute of the aggregate class.
|
|
1226
1458
|
setattr(cls, event_cls.__name__, event_cls)
|
|
1227
1459
|
|
|
1228
1460
|
# Remember which event class to trigger.
|
|
1229
|
-
|
|
1230
|
-
type[
|
|
1461
|
+
decorator_event_classes[event_decorator] = cast(
|
|
1462
|
+
type[DecoratorEvent], event_cls
|
|
1231
1463
|
)
|
|
1232
1464
|
|
|
1233
1465
|
# Check any create_id method defined on this class is static or class method.
|
|
@@ -1241,13 +1473,12 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
1241
1473
|
raise TypeError(msg)
|
|
1242
1474
|
|
|
1243
1475
|
# Get the parameters of the create_id method that will be used by this class.
|
|
1244
|
-
cls._create_id_param_names: list[str] = []
|
|
1245
1476
|
for name, param in inspect.signature(cls.create_id).parameters.items():
|
|
1246
1477
|
if param.kind in [param.KEYWORD_ONLY, param.POSITIONAL_OR_KEYWORD]:
|
|
1247
|
-
cls.
|
|
1478
|
+
_create_id_param_names[cls].append(name)
|
|
1248
1479
|
|
|
1249
1480
|
# Define event classes for all events on bases.
|
|
1250
|
-
for aggregate_base_class in
|
|
1481
|
+
for aggregate_base_class in cls.__bases__:
|
|
1251
1482
|
for name, value in aggregate_base_class.__dict__.items():
|
|
1252
1483
|
if (
|
|
1253
1484
|
isinstance(value, type)
|
|
@@ -1260,253 +1491,23 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
1260
1491
|
)
|
|
1261
1492
|
setattr(cls, name, sub_class)
|
|
1262
1493
|
|
|
1263
|
-
def _define_event_class(
|
|
1264
|
-
cls,
|
|
1265
|
-
name: str,
|
|
1266
|
-
bases: tuple[type[CanMutateAggregate], ...],
|
|
1267
|
-
apply_method: CommandMethod | None,
|
|
1268
|
-
) -> type[CanMutateAggregate]:
|
|
1269
|
-
# Define annotations for the event class (specs the init method).
|
|
1270
|
-
annotations = {}
|
|
1271
|
-
if apply_method is not None:
|
|
1272
|
-
method_signature = inspect.signature(apply_method)
|
|
1273
|
-
supers = {
|
|
1274
|
-
s for b in bases for s in b.__mro__ if hasattr(s, "__annotations__")
|
|
1275
|
-
}
|
|
1276
|
-
super_annotations = {a for s in supers for a in s.__annotations__}
|
|
1277
|
-
for param_name, param in list(method_signature.parameters.items())[1:]:
|
|
1278
|
-
# Don't define 'id' on a "created" class.
|
|
1279
|
-
if param_name == "id" and apply_method.__name__ == "__init__":
|
|
1280
|
-
continue
|
|
1281
|
-
# Don't override super class annotations, unless no default on param.
|
|
1282
|
-
if param_name not in super_annotations or param.default == param.empty:
|
|
1283
|
-
annotations[param_name] = param.annotation or "typing.Any"
|
|
1284
|
-
event_cls_qualname = f"{cls.__qualname__}.{name}"
|
|
1285
|
-
event_cls_dict = {
|
|
1286
|
-
"__annotations__": annotations,
|
|
1287
|
-
"__module__": cls.__module__,
|
|
1288
|
-
"__qualname__": event_cls_qualname,
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
# Create the event class object.
|
|
1292
|
-
return cast(type[CanMutateAggregate], type(name, bases, event_cls_dict))
|
|
1293
|
-
|
|
1294
|
-
def __call__(
|
|
1295
|
-
cls: MetaAggregate[TAggregate], *args: Any, **kwargs: Any
|
|
1296
|
-
) -> TAggregate:
|
|
1297
|
-
try:
|
|
1298
|
-
created_event_classes = aggregate_has_many_created_event_classes[cls]
|
|
1299
|
-
msg = (
|
|
1300
|
-
"""Can't decide which of many "created" event classes to use: """
|
|
1301
|
-
f"""'{"', '".join(created_event_classes)}'. Please use class """
|
|
1302
|
-
"arg 'created_event_name' or @event decorator on __init__ method."
|
|
1303
|
-
)
|
|
1304
|
-
raise TypeError(msg)
|
|
1305
|
-
except KeyError:
|
|
1306
|
-
pass
|
|
1307
|
-
|
|
1308
|
-
cls_init: FunctionType | WrapperDescriptorType = cls.__init__ # type: ignore
|
|
1309
|
-
kwargs = _coerce_args_to_kwargs(
|
|
1310
|
-
cls_init,
|
|
1311
|
-
args,
|
|
1312
|
-
kwargs,
|
|
1313
|
-
expects_id=cls in _annotations_mention_id,
|
|
1314
|
-
)
|
|
1315
|
-
return cls._create(
|
|
1316
|
-
event_class=cls._created_event_class,
|
|
1317
|
-
**kwargs,
|
|
1318
|
-
)
|
|
1319
|
-
|
|
1320
|
-
def _create(
|
|
1321
|
-
cls: MetaAggregate[TAggregate],
|
|
1322
|
-
event_class: type[CanInitAggregate],
|
|
1323
|
-
**kwargs: Any,
|
|
1324
|
-
) -> TAggregate:
|
|
1325
|
-
raise NotImplementedError # pragma: no cover
|
|
1326
|
-
|
|
1327
|
-
@staticmethod
|
|
1328
|
-
def create_id(**_: Any) -> UUID:
|
|
1329
|
-
"""
|
|
1330
|
-
Returns a new aggregate ID.
|
|
1331
|
-
"""
|
|
1332
|
-
return uuid4()
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
class Aggregate(metaclass=MetaAggregate):
|
|
1336
|
-
"""
|
|
1337
|
-
Base class for aggregate roots.
|
|
1338
|
-
|
|
1339
|
-
.. automethod:: _create
|
|
1340
|
-
"""
|
|
1341
|
-
|
|
1342
|
-
@classmethod
|
|
1343
|
-
def _create(
|
|
1344
|
-
cls: type[TAggregate],
|
|
1345
|
-
event_class: type[CanInitAggregate],
|
|
1346
|
-
*,
|
|
1347
|
-
id: UUID | None = None, # noqa: A002
|
|
1348
|
-
**kwargs: Any,
|
|
1349
|
-
) -> TAggregate:
|
|
1350
|
-
"""
|
|
1351
|
-
Constructs a new aggregate object instance.
|
|
1352
|
-
"""
|
|
1353
|
-
# Construct the domain event with an ID and a
|
|
1354
|
-
# version, and a topic for the aggregate class.
|
|
1355
|
-
create_id_kwargs = {
|
|
1356
|
-
k: v for k, v in kwargs.items() if k in cls._create_id_param_names
|
|
1357
|
-
}
|
|
1358
|
-
originator_id = id or cls.create_id(**create_id_kwargs)
|
|
1359
|
-
|
|
1360
|
-
# Impose the required common "created" event attribute values.
|
|
1361
|
-
kwargs = kwargs.copy()
|
|
1362
|
-
kwargs.update(
|
|
1363
|
-
originator_topic=get_topic(cls),
|
|
1364
|
-
originator_id=originator_id,
|
|
1365
|
-
originator_version=cls.INITIAL_VERSION,
|
|
1366
|
-
)
|
|
1367
|
-
if kwargs.get("timestamp") is None:
|
|
1368
|
-
kwargs["timestamp"] = event_class.create_timestamp()
|
|
1369
|
-
|
|
1370
|
-
try:
|
|
1371
|
-
created_event = event_class(**kwargs)
|
|
1372
|
-
except TypeError as e:
|
|
1373
|
-
msg = f"Unable to construct '{event_class.__name__}' event: {e}"
|
|
1374
|
-
raise TypeError(msg) from None
|
|
1375
|
-
# Construct the aggregate object.
|
|
1376
|
-
agg = cast(TAggregate, created_event.mutate(None))
|
|
1377
|
-
|
|
1378
|
-
assert agg is not None
|
|
1379
|
-
# Append the domain event to pending list.
|
|
1380
|
-
agg.pending_events.append(created_event)
|
|
1381
|
-
# Return the aggregate.
|
|
1382
|
-
return agg
|
|
1383
|
-
|
|
1384
|
-
def __base_init__(
|
|
1385
|
-
self, originator_id: UUID, originator_version: int, timestamp: datetime
|
|
1386
|
-
) -> None:
|
|
1387
|
-
"""
|
|
1388
|
-
Initialises an aggregate object with an :data:`id`, a :data:`version`
|
|
1389
|
-
number, and a :data:`timestamp`.
|
|
1390
|
-
"""
|
|
1391
|
-
self._id = originator_id
|
|
1392
|
-
self._version = originator_version
|
|
1393
|
-
self._created_on = timestamp
|
|
1394
|
-
self._modified_on = timestamp
|
|
1395
|
-
self._pending_events: list[CanMutateAggregate] = []
|
|
1396
|
-
|
|
1397
|
-
@property
|
|
1398
|
-
def id(self) -> UUID:
|
|
1399
|
-
"""
|
|
1400
|
-
The ID of the aggregate.
|
|
1401
|
-
"""
|
|
1402
|
-
return self._id
|
|
1403
|
-
|
|
1404
|
-
@property
|
|
1405
|
-
def version(self) -> int:
|
|
1406
|
-
"""
|
|
1407
|
-
The version number of the aggregate.
|
|
1408
|
-
"""
|
|
1409
|
-
return self._version
|
|
1410
|
-
|
|
1411
|
-
@version.setter
|
|
1412
|
-
def version(self, version: int) -> None:
|
|
1413
|
-
self._version = version
|
|
1414
|
-
|
|
1415
|
-
@property
|
|
1416
|
-
def created_on(self) -> datetime:
|
|
1417
|
-
"""
|
|
1418
|
-
The date and time when the aggregate was created.
|
|
1419
|
-
"""
|
|
1420
|
-
return self._created_on
|
|
1421
|
-
|
|
1422
|
-
@property
|
|
1423
|
-
def modified_on(self) -> datetime:
|
|
1424
|
-
"""
|
|
1425
|
-
The date and time when the aggregate was last modified.
|
|
1426
|
-
"""
|
|
1427
|
-
return self._modified_on
|
|
1428
|
-
|
|
1429
|
-
@modified_on.setter
|
|
1430
|
-
def modified_on(self, modified_on: datetime) -> None:
|
|
1431
|
-
self._modified_on = modified_on
|
|
1432
|
-
|
|
1433
|
-
@property
|
|
1434
|
-
def pending_events(self) -> list[CanMutateAggregate]:
|
|
1435
|
-
"""
|
|
1436
|
-
A list of pending events.
|
|
1437
|
-
"""
|
|
1438
|
-
return self._pending_events
|
|
1439
1494
|
|
|
1495
|
+
class Aggregate(BaseAggregate):
|
|
1440
1496
|
class Event(AggregateEvent):
|
|
1441
1497
|
pass
|
|
1442
1498
|
|
|
1443
1499
|
class Created(Event, AggregateCreated):
|
|
1444
1500
|
pass
|
|
1445
1501
|
|
|
1446
|
-
def __eq__(self, other: object) -> bool:
|
|
1447
|
-
return type(self) is type(other) and self.__dict__ == other.__dict__
|
|
1448
|
-
|
|
1449
|
-
def __repr__(self) -> str:
|
|
1450
|
-
attrs = [
|
|
1451
|
-
f"{k.lstrip('_')}={v!r}"
|
|
1452
|
-
for k, v in self.__dict__.items()
|
|
1453
|
-
if k != "_pending_events"
|
|
1454
|
-
]
|
|
1455
|
-
return f"{type(self).__name__}({', '.join(attrs)})"
|
|
1456
|
-
|
|
1457
|
-
def trigger_event(
|
|
1458
|
-
self,
|
|
1459
|
-
event_class: type[CanMutateAggregate],
|
|
1460
|
-
**kwargs: Any,
|
|
1461
|
-
) -> None:
|
|
1462
|
-
"""
|
|
1463
|
-
Triggers domain event of given type, by creating
|
|
1464
|
-
an event object and using it to mutate the aggregate.
|
|
1465
|
-
"""
|
|
1466
|
-
# Construct the domain event as the
|
|
1467
|
-
# next in the aggregate's sequence.
|
|
1468
|
-
# Use counting to generate the sequence.
|
|
1469
|
-
next_version = self.version + 1
|
|
1470
|
-
|
|
1471
|
-
# Impose the required common domain event attribute values.
|
|
1472
|
-
kwargs = kwargs.copy()
|
|
1473
|
-
kwargs.update(
|
|
1474
|
-
originator_id=self.id,
|
|
1475
|
-
originator_version=next_version,
|
|
1476
|
-
)
|
|
1477
|
-
if kwargs.get("timestamp") is None:
|
|
1478
|
-
kwargs["timestamp"] = event_class.create_timestamp()
|
|
1479
|
-
|
|
1480
|
-
try:
|
|
1481
|
-
new_event = event_class(**kwargs)
|
|
1482
|
-
except TypeError as e:
|
|
1483
|
-
msg = f"Can't construct event {event_class}: {e}"
|
|
1484
|
-
raise TypeError(msg) from None
|
|
1485
1502
|
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
self._pending_events.append(new_event)
|
|
1490
|
-
|
|
1491
|
-
def collect_events(self) -> Sequence[CanMutateAggregate]:
|
|
1492
|
-
"""
|
|
1493
|
-
Collects and returns a list of pending aggregate
|
|
1494
|
-
:class:`AggregateEvent` objects.
|
|
1495
|
-
"""
|
|
1496
|
-
collected = []
|
|
1497
|
-
while self._pending_events:
|
|
1498
|
-
collected.append(self._pending_events.pop(0))
|
|
1499
|
-
return collected
|
|
1503
|
+
@overload
|
|
1504
|
+
def aggregate(*, created_event_name: str) -> Callable[[Any], type[Aggregate]]:
|
|
1505
|
+
pass # pragma: no cover
|
|
1500
1506
|
|
|
1501
1507
|
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
#
|
|
1505
|
-
#
|
|
1506
|
-
#
|
|
1507
|
-
# @overload
|
|
1508
|
-
# def aggregate(cls: Any) -> Type[Aggregate]:
|
|
1509
|
-
# ...
|
|
1508
|
+
@overload
|
|
1509
|
+
def aggregate(cls: Any) -> type[Aggregate]:
|
|
1510
|
+
pass # pragma: no cover
|
|
1510
1511
|
|
|
1511
1512
|
|
|
1512
1513
|
def aggregate(
|
|
@@ -1590,6 +1591,7 @@ class SnapshotProtocol(DomainEventProtocol, Protocol):
|
|
|
1590
1591
|
"""
|
|
1591
1592
|
Snapshots have a read-only 'state'.
|
|
1592
1593
|
"""
|
|
1594
|
+
raise NotImplementedError # pragma: no cover
|
|
1593
1595
|
|
|
1594
1596
|
# TODO: Improve on this 'Any'.
|
|
1595
1597
|
@classmethod
|
|
@@ -1606,6 +1608,16 @@ class CanSnapshotAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
|
|
|
1606
1608
|
topic: str
|
|
1607
1609
|
state: Any
|
|
1608
1610
|
|
|
1611
|
+
def __init__(
|
|
1612
|
+
self,
|
|
1613
|
+
originator_id: UUID,
|
|
1614
|
+
originator_version: int,
|
|
1615
|
+
timestamp: datetime,
|
|
1616
|
+
topic: str,
|
|
1617
|
+
state: Any,
|
|
1618
|
+
) -> None:
|
|
1619
|
+
raise NotImplementedError # pragma: no cover
|
|
1620
|
+
|
|
1609
1621
|
@classmethod
|
|
1610
1622
|
def take(
|
|
1611
1623
|
cls: type[TCanSnapshotAggregate],
|
|
@@ -1622,7 +1634,7 @@ class CanSnapshotAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
|
|
|
1622
1634
|
aggregate_state.pop("_id")
|
|
1623
1635
|
aggregate_state.pop("_version")
|
|
1624
1636
|
aggregate_state.pop("_pending_events")
|
|
1625
|
-
return cls(
|
|
1637
|
+
return cls(
|
|
1626
1638
|
originator_id=aggregate.id,
|
|
1627
1639
|
originator_version=aggregate.version,
|
|
1628
1640
|
timestamp=cls.create_timestamp(),
|
|
@@ -1652,6 +1664,7 @@ class CanSnapshotAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
|
|
|
1652
1664
|
return aggregate
|
|
1653
1665
|
|
|
1654
1666
|
|
|
1667
|
+
@dataclass(frozen=True)
|
|
1655
1668
|
class Snapshot(CanSnapshotAggregate, DomainEvent):
|
|
1656
1669
|
"""
|
|
1657
1670
|
Snapshots represent the state of an aggregate at a particular
|