eventsourcing 9.4.0a6__py3-none-any.whl → 9.4.0a8__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 +4 -17
- eventsourcing/domain.py +355 -331
- eventsourcing/persistence.py +18 -24
- eventsourcing/popo.py +5 -1
- eventsourcing/postgres.py +41 -33
- eventsourcing/projection.py +22 -19
- eventsourcing/sqlite.py +5 -1
- eventsourcing/system.py +19 -11
- eventsourcing/tests/application.py +57 -49
- eventsourcing/tests/domain.py +8 -6
- eventsourcing/tests/persistence.py +162 -143
- eventsourcing/tests/postgres_utils.py +7 -8
- eventsourcing/utils.py +15 -10
- {eventsourcing-9.4.0a6.dist-info → eventsourcing-9.4.0a8.dist-info}/METADATA +1 -1
- eventsourcing-9.4.0a8.dist-info/RECORD +26 -0
- eventsourcing-9.4.0a6.dist-info/RECORD +0 -26
- {eventsourcing-9.4.0a6.dist-info → eventsourcing-9.4.0a8.dist-info}/AUTHORS +0 -0
- {eventsourcing-9.4.0a6.dist-info → eventsourcing-9.4.0a8.dist-info}/LICENSE +0 -0
- {eventsourcing-9.4.0a6.dist-info → eventsourcing-9.4.0a8.dist-info}/WHEEL +0 -0
eventsourcing/domain.py
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import dataclasses
|
|
4
|
+
import importlib
|
|
3
5
|
import inspect
|
|
4
6
|
import os
|
|
7
|
+
from collections import defaultdict
|
|
5
8
|
from dataclasses import dataclass
|
|
6
9
|
from datetime import datetime, tzinfo
|
|
7
10
|
from functools import cache
|
|
@@ -21,6 +24,8 @@ from typing import (
|
|
|
21
24
|
from uuid import UUID, uuid4
|
|
22
25
|
from warnings import warn
|
|
23
26
|
|
|
27
|
+
from typing_extensions import Self
|
|
28
|
+
|
|
24
29
|
from eventsourcing.utils import get_method_name, get_topic, resolve_topic
|
|
25
30
|
|
|
26
31
|
if TYPE_CHECKING:
|
|
@@ -39,6 +44,38 @@ domain and convert to local timezones when presenting values in user interfaces.
|
|
|
39
44
|
"""
|
|
40
45
|
|
|
41
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
|
+
|
|
42
79
|
@runtime_checkable
|
|
43
80
|
class DomainEventProtocol(Protocol):
|
|
44
81
|
"""
|
|
@@ -216,6 +253,7 @@ class CanMutateAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
|
|
|
216
253
|
method that evolves the state of an aggregate.
|
|
217
254
|
"""
|
|
218
255
|
|
|
256
|
+
# Todo: Move this to a HasTimestamp? Why is it here??
|
|
219
257
|
timestamp: datetime
|
|
220
258
|
"""Timezone-aware :class:`datetime` object representing when an event occurred."""
|
|
221
259
|
|
|
@@ -325,7 +363,7 @@ class CanInitAggregate(CanMutateAggregate):
|
|
|
325
363
|
return agg
|
|
326
364
|
|
|
327
365
|
|
|
328
|
-
class MetaDomainEvent(
|
|
366
|
+
class MetaDomainEvent(EventsourcingType):
|
|
329
367
|
"""
|
|
330
368
|
Metaclass which ensures all domain event classes are frozen dataclasses.
|
|
331
369
|
"""
|
|
@@ -336,11 +374,12 @@ class MetaDomainEvent(type):
|
|
|
336
374
|
event_cls = cast(
|
|
337
375
|
type[TDomainEvent], super().__new__(cls, name, bases, cls_dict)
|
|
338
376
|
)
|
|
339
|
-
event_cls = dataclass(frozen=True)(event_cls)
|
|
377
|
+
event_cls = dataclasses.dataclass(frozen=True)(event_cls)
|
|
340
378
|
event_cls.__signature__ = inspect.signature(event_cls.__init__) # type: ignore
|
|
341
379
|
return event_cls
|
|
342
380
|
|
|
343
381
|
|
|
382
|
+
@dataclass(frozen=True)
|
|
344
383
|
class DomainEvent(CanCreateTimestamp, metaclass=MetaDomainEvent):
|
|
345
384
|
"""
|
|
346
385
|
Frozen data class representing domain model events.
|
|
@@ -362,6 +401,7 @@ class AggregateEvent(CanMutateAggregate, DomainEvent):
|
|
|
362
401
|
"""
|
|
363
402
|
|
|
364
403
|
|
|
404
|
+
@dataclass(frozen=True)
|
|
365
405
|
class AggregateCreated(CanInitAggregate, AggregateEvent):
|
|
366
406
|
"""
|
|
367
407
|
Frozen data class representing the initial creation of an aggregate.
|
|
@@ -391,10 +431,6 @@ class LogEvent(DomainEvent):
|
|
|
391
431
|
"""
|
|
392
432
|
|
|
393
433
|
|
|
394
|
-
# Deprecated: Use TDomainEvent instead.
|
|
395
|
-
TLogEvent = TypeVar("TLogEvent", bound=DomainEventProtocol)
|
|
396
|
-
|
|
397
|
-
|
|
398
434
|
def _filter_kwargs_for_method_params(
|
|
399
435
|
kwargs: dict[str, Any], method: Callable[..., Any]
|
|
400
436
|
) -> dict[str, Any]:
|
|
@@ -441,12 +477,12 @@ class CommandMethodDecorator:
|
|
|
441
477
|
elif isinstance(event_spec, type) and issubclass(
|
|
442
478
|
event_spec, CanMutateAggregate
|
|
443
479
|
):
|
|
444
|
-
if event_spec in
|
|
480
|
+
if event_spec in _given_event_classes:
|
|
445
481
|
name = event_spec.__name__
|
|
446
482
|
msg = f"{name} event class used in more than one decorator"
|
|
447
483
|
raise TypeError(msg)
|
|
448
484
|
self.given_event_cls = event_spec
|
|
449
|
-
|
|
485
|
+
_given_event_classes.add(event_spec)
|
|
450
486
|
|
|
451
487
|
# Process a decorated property.
|
|
452
488
|
if isinstance(decorated_obj, property):
|
|
@@ -729,7 +765,7 @@ class BoundCommandMethodDecorator:
|
|
|
729
765
|
kwargs = _coerce_args_to_kwargs(
|
|
730
766
|
self.event_decorator.decorated_method, args, kwargs
|
|
731
767
|
)
|
|
732
|
-
event_cls =
|
|
768
|
+
event_cls = decorator_event_classes[self.event_decorator]
|
|
733
769
|
kwargs = _filter_kwargs_for_method_params(kwargs, event_cls)
|
|
734
770
|
self.aggregate.trigger_event(event_cls, **kwargs)
|
|
735
771
|
|
|
@@ -737,14 +773,30 @@ class BoundCommandMethodDecorator:
|
|
|
737
773
|
self.trigger(*args, **kwargs)
|
|
738
774
|
|
|
739
775
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
776
|
+
class DecoratorEvent(CanMutateAggregate):
|
|
777
|
+
def apply(self, aggregate: Aggregate) -> None:
|
|
778
|
+
"""
|
|
779
|
+
Applies event to aggregate by calling method decorated by @event.
|
|
780
|
+
"""
|
|
781
|
+
# Call super method, just in case any base classes need it.
|
|
782
|
+
super().apply(aggregate)
|
|
783
|
+
|
|
784
|
+
# Identify the method that was decorated.
|
|
785
|
+
decorated_method = _decorated_methods[type(self)]
|
|
786
|
+
|
|
787
|
+
# Select event attributes mentioned in method signature.
|
|
788
|
+
kwargs = _filter_kwargs_for_method_params(self.__dict__, decorated_method)
|
|
789
|
+
|
|
790
|
+
# Call the original method with event attribute values.
|
|
791
|
+
decorated_method(aggregate, **kwargs)
|
|
792
|
+
|
|
743
793
|
|
|
794
|
+
_given_event_classes: set[type] = set()
|
|
795
|
+
_decorated_methods: dict[type, CommandMethod] = {}
|
|
796
|
+
_created_event_classes: dict[type, list[type[CanInitAggregate]]] = {}
|
|
744
797
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
] = {}
|
|
798
|
+
|
|
799
|
+
decorator_event_classes: dict[CommandMethodDecorator, type[DecoratorEvent]] = {}
|
|
748
800
|
|
|
749
801
|
|
|
750
802
|
def _check_no_variable_params(method: FunctionType) -> None:
|
|
@@ -882,15 +934,86 @@ def _raise_missing_names_type_error(missing_names: list[str], msg: str) -> None:
|
|
|
882
934
|
raise TypeError(msg)
|
|
883
935
|
|
|
884
936
|
|
|
885
|
-
_annotations_mention_id: set[
|
|
886
|
-
_init_mentions_id: set[
|
|
937
|
+
_annotations_mention_id: set[type[Aggregate]] = set()
|
|
938
|
+
_init_mentions_id: set[type[Aggregate]] = set()
|
|
939
|
+
_create_id_param_names: dict[type[Aggregate], list[str]] = defaultdict(list)
|
|
887
940
|
|
|
888
941
|
|
|
889
|
-
class MetaAggregate(
|
|
942
|
+
class MetaAggregate(EventsourcingType, Generic[TAggregate], type):
|
|
890
943
|
"""
|
|
891
944
|
Metaclass for aggregate classes.
|
|
945
|
+
"""
|
|
946
|
+
|
|
947
|
+
def _define_event_class(
|
|
948
|
+
cls,
|
|
949
|
+
name: str,
|
|
950
|
+
bases: tuple[type[CanMutateAggregate], ...],
|
|
951
|
+
apply_method: CommandMethod | None,
|
|
952
|
+
) -> type[CanMutateAggregate]:
|
|
953
|
+
# Define annotations for the event class (specs the init method).
|
|
954
|
+
annotations = {}
|
|
955
|
+
if apply_method is not None:
|
|
956
|
+
method_signature = inspect.signature(apply_method)
|
|
957
|
+
supers = {
|
|
958
|
+
s for b in bases for s in b.__mro__ if hasattr(s, "__annotations__")
|
|
959
|
+
}
|
|
960
|
+
super_annotations = {a for s in supers for a in s.__annotations__}
|
|
961
|
+
for param_name, param in list(method_signature.parameters.items())[1:]:
|
|
962
|
+
# Don't define 'id' on a "created" class.
|
|
963
|
+
if param_name == "id" and apply_method.__name__ == "__init__":
|
|
964
|
+
continue
|
|
965
|
+
# Don't override super class annotations, unless no default on param.
|
|
966
|
+
if param_name not in super_annotations or param.default == param.empty:
|
|
967
|
+
annotations[param_name] = param.annotation or "typing.Any"
|
|
968
|
+
event_cls_qualname = f"{cls.__qualname__}.{name}"
|
|
969
|
+
event_cls_dict = {
|
|
970
|
+
"__annotations__": annotations,
|
|
971
|
+
"__module__": cls.__module__,
|
|
972
|
+
"__qualname__": event_cls_qualname,
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
# Create the event class object.
|
|
976
|
+
return cast(type[CanMutateAggregate], type(name, bases, event_cls_dict))
|
|
977
|
+
|
|
978
|
+
def __call__(
|
|
979
|
+
cls: MetaAggregate[TAggregate], *args: Any, **kwargs: Any
|
|
980
|
+
) -> TAggregate:
|
|
981
|
+
created_event_classes = _created_event_classes[cls]
|
|
982
|
+
if len(created_event_classes) > 1:
|
|
983
|
+
msg = (
|
|
984
|
+
f"{cls.__qualname__} can't decide which of many "
|
|
985
|
+
'"created" event classes to use: '
|
|
986
|
+
f"""'{"', '".join(c.__name__ for c in created_event_classes)}'. """
|
|
987
|
+
"Please use class arg 'created_event_name' or"
|
|
988
|
+
" @event decorator on __init__ method."
|
|
989
|
+
)
|
|
990
|
+
raise TypeError(msg)
|
|
991
|
+
|
|
992
|
+
cls_init: FunctionType | WrapperDescriptorType = cls.__init__ # type: ignore
|
|
993
|
+
kwargs = _coerce_args_to_kwargs(
|
|
994
|
+
cls_init,
|
|
995
|
+
args,
|
|
996
|
+
kwargs,
|
|
997
|
+
expects_id=cls in _annotations_mention_id,
|
|
998
|
+
)
|
|
999
|
+
return cls._create(
|
|
1000
|
+
event_class=created_event_classes[0],
|
|
1001
|
+
**kwargs,
|
|
1002
|
+
)
|
|
1003
|
+
|
|
1004
|
+
def _create(
|
|
1005
|
+
cls: MetaAggregate[TAggregate],
|
|
1006
|
+
event_class: type[CanInitAggregate],
|
|
1007
|
+
**kwargs: Any,
|
|
1008
|
+
) -> TAggregate:
|
|
1009
|
+
raise NotImplementedError # pragma: no cover
|
|
1010
|
+
|
|
1011
|
+
_created_event_class: type[CanInitAggregate]
|
|
892
1012
|
|
|
893
|
-
|
|
1013
|
+
|
|
1014
|
+
class Aggregate(metaclass=MetaAggregate):
|
|
1015
|
+
"""
|
|
1016
|
+
Base class for aggregates.
|
|
894
1017
|
"""
|
|
895
1018
|
|
|
896
1019
|
INITIAL_VERSION = 1
|
|
@@ -901,57 +1024,197 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
901
1024
|
class Created(Event, AggregateCreated):
|
|
902
1025
|
pass
|
|
903
1026
|
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1027
|
+
@staticmethod
|
|
1028
|
+
def create_id(*_: Any, **__: Any) -> UUID:
|
|
1029
|
+
"""
|
|
1030
|
+
Returns a new aggregate ID.
|
|
1031
|
+
"""
|
|
1032
|
+
return uuid4()
|
|
1033
|
+
|
|
1034
|
+
@classmethod
|
|
1035
|
+
def _create(
|
|
1036
|
+
cls: type[Self],
|
|
1037
|
+
event_class: type[CanInitAggregate],
|
|
1038
|
+
*,
|
|
1039
|
+
id: UUID | None = None, # noqa: A002
|
|
1040
|
+
**kwargs: Any,
|
|
1041
|
+
) -> Self:
|
|
1042
|
+
"""
|
|
1043
|
+
Constructs a new aggregate object instance.
|
|
1044
|
+
"""
|
|
1045
|
+
# Construct the domain event with an ID and a
|
|
1046
|
+
# version, and a topic for the aggregate class.
|
|
1047
|
+
create_id_kwargs = {
|
|
1048
|
+
k: v for k, v in kwargs.items() if k in _create_id_param_names[cls]
|
|
1049
|
+
}
|
|
1050
|
+
if id is not None:
|
|
1051
|
+
originator_id = id
|
|
1052
|
+
if not isinstance(originator_id, UUID):
|
|
1053
|
+
msg = f"Given id was not a UUID: {originator_id}"
|
|
1054
|
+
raise TypeError(msg)
|
|
1055
|
+
else:
|
|
1056
|
+
originator_id = cls.create_id(**create_id_kwargs)
|
|
1057
|
+
if not isinstance(originator_id, UUID):
|
|
1058
|
+
msg = (
|
|
1059
|
+
f"{cls.create_id.__module__}.{cls.create_id.__qualname__}"
|
|
1060
|
+
f" did not return UUID, it returned: {originator_id}"
|
|
1061
|
+
)
|
|
1062
|
+
raise TypeError(msg)
|
|
1063
|
+
|
|
1064
|
+
# Impose the required common "created" event attribute values.
|
|
1065
|
+
kwargs = kwargs.copy()
|
|
1066
|
+
kwargs.update(
|
|
1067
|
+
originator_topic=get_topic(cls),
|
|
1068
|
+
originator_id=originator_id,
|
|
1069
|
+
originator_version=cls.INITIAL_VERSION,
|
|
1070
|
+
)
|
|
1071
|
+
if kwargs.get("timestamp") is None:
|
|
1072
|
+
kwargs["timestamp"] = event_class.create_timestamp()
|
|
1073
|
+
|
|
1074
|
+
try:
|
|
1075
|
+
created_event = event_class(**kwargs)
|
|
1076
|
+
except TypeError as e:
|
|
1077
|
+
msg = f"Unable to construct '{event_class.__qualname__}' event: {e}"
|
|
1078
|
+
raise TypeError(msg) from e
|
|
1079
|
+
# Construct the aggregate object.
|
|
1080
|
+
agg = cast(Self, created_event.mutate(None))
|
|
911
1081
|
|
|
912
|
-
|
|
913
|
-
|
|
1082
|
+
assert agg is not None
|
|
1083
|
+
# Append the domain event to pending list.
|
|
1084
|
+
agg.pending_events.append(created_event)
|
|
1085
|
+
# Return the aggregate.
|
|
1086
|
+
return agg
|
|
914
1087
|
|
|
915
|
-
|
|
916
|
-
|
|
1088
|
+
def __base_init__(
|
|
1089
|
+
self, originator_id: UUID, originator_version: int, timestamp: datetime
|
|
1090
|
+
) -> None:
|
|
1091
|
+
"""
|
|
1092
|
+
Initialises an aggregate object with an :data:`id`, a :data:`version`
|
|
1093
|
+
number, and a :data:`timestamp`.
|
|
1094
|
+
"""
|
|
1095
|
+
self._id = originator_id
|
|
1096
|
+
self._version = originator_version
|
|
1097
|
+
self._created_on = timestamp
|
|
1098
|
+
self._modified_on = timestamp
|
|
1099
|
+
self._pending_events: list[CanMutateAggregate] = []
|
|
917
1100
|
|
|
918
|
-
|
|
919
|
-
|
|
1101
|
+
@property
|
|
1102
|
+
def id(self) -> UUID:
|
|
1103
|
+
"""
|
|
1104
|
+
The ID of the aggregate.
|
|
1105
|
+
"""
|
|
1106
|
+
return self._id
|
|
920
1107
|
|
|
921
|
-
|
|
1108
|
+
@property
|
|
1109
|
+
def version(self) -> int:
|
|
1110
|
+
"""
|
|
1111
|
+
The version number of the aggregate.
|
|
1112
|
+
"""
|
|
1113
|
+
return self._version
|
|
1114
|
+
|
|
1115
|
+
@version.setter
|
|
1116
|
+
def version(self, version: int) -> None:
|
|
1117
|
+
self._version = version
|
|
1118
|
+
|
|
1119
|
+
@property
|
|
1120
|
+
def created_on(self) -> datetime:
|
|
1121
|
+
"""
|
|
1122
|
+
The date and time when the aggregate was created.
|
|
1123
|
+
"""
|
|
1124
|
+
return self._created_on
|
|
1125
|
+
|
|
1126
|
+
@property
|
|
1127
|
+
def modified_on(self) -> datetime:
|
|
1128
|
+
"""
|
|
1129
|
+
The date and time when the aggregate was last modified.
|
|
1130
|
+
"""
|
|
1131
|
+
return self._modified_on
|
|
1132
|
+
|
|
1133
|
+
@modified_on.setter
|
|
1134
|
+
def modified_on(self, modified_on: datetime) -> None:
|
|
1135
|
+
self._modified_on = modified_on
|
|
1136
|
+
|
|
1137
|
+
@property
|
|
1138
|
+
def pending_events(self) -> list[CanMutateAggregate]:
|
|
1139
|
+
"""
|
|
1140
|
+
A list of pending events.
|
|
1141
|
+
"""
|
|
1142
|
+
return self._pending_events
|
|
922
1143
|
|
|
923
|
-
def
|
|
1144
|
+
def trigger_event(
|
|
1145
|
+
self,
|
|
1146
|
+
event_class: type[CanMutateAggregate],
|
|
1147
|
+
**kwargs: Any,
|
|
1148
|
+
) -> None:
|
|
924
1149
|
"""
|
|
925
|
-
|
|
1150
|
+
Triggers domain event of given type, by creating
|
|
1151
|
+
an event object and using it to mutate the aggregate.
|
|
926
1152
|
"""
|
|
1153
|
+
# Construct the domain event as the
|
|
1154
|
+
# next in the aggregate's sequence.
|
|
1155
|
+
# Use counting to generate the sequence.
|
|
1156
|
+
next_version = self.version + 1
|
|
1157
|
+
|
|
1158
|
+
# Impose the required common domain event attribute values.
|
|
1159
|
+
kwargs = kwargs.copy()
|
|
1160
|
+
kwargs.update(
|
|
1161
|
+
originator_id=self.id,
|
|
1162
|
+
originator_version=next_version,
|
|
1163
|
+
)
|
|
1164
|
+
if kwargs.get("timestamp") is None:
|
|
1165
|
+
kwargs["timestamp"] = event_class.create_timestamp()
|
|
1166
|
+
|
|
927
1167
|
try:
|
|
928
|
-
|
|
929
|
-
except
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
else:
|
|
933
|
-
try:
|
|
934
|
-
class_annotations.pop("id")
|
|
935
|
-
except KeyError:
|
|
936
|
-
annotations_mention_id = False
|
|
937
|
-
else:
|
|
938
|
-
annotations_mention_id = True
|
|
939
|
-
aggregate_cls = type.__new__(cls, *args)
|
|
940
|
-
if class_annotations:
|
|
941
|
-
aggregate_cls = dataclass(eq=False, repr=False)(aggregate_cls)
|
|
942
|
-
if annotations_mention_id:
|
|
943
|
-
_annotations_mention_id.add(aggregate_cls)
|
|
944
|
-
return aggregate_cls
|
|
1168
|
+
new_event = event_class(**kwargs)
|
|
1169
|
+
except TypeError as e:
|
|
1170
|
+
msg = f"Can't construct event {event_class}: {e}"
|
|
1171
|
+
raise TypeError(msg) from None
|
|
945
1172
|
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1173
|
+
# Mutate aggregate with domain event.
|
|
1174
|
+
new_event.mutate(self)
|
|
1175
|
+
# Append the domain event to pending list.
|
|
1176
|
+
self._pending_events.append(new_event)
|
|
1177
|
+
|
|
1178
|
+
def collect_events(self) -> Sequence[CanMutateAggregate]:
|
|
1179
|
+
"""
|
|
1180
|
+
Collects and returns a list of pending aggregate
|
|
1181
|
+
:class:`AggregateEvent` objects.
|
|
1182
|
+
"""
|
|
1183
|
+
collected = []
|
|
1184
|
+
while self._pending_events:
|
|
1185
|
+
collected.append(self._pending_events.pop(0))
|
|
1186
|
+
return collected
|
|
1187
|
+
|
|
1188
|
+
def __eq__(self, other: object) -> bool:
|
|
1189
|
+
return type(self) is type(other) and self.__dict__ == other.__dict__
|
|
1190
|
+
|
|
1191
|
+
def __repr__(self) -> str:
|
|
1192
|
+
attrs = [
|
|
1193
|
+
f"{k.lstrip('_')}={v!r}"
|
|
1194
|
+
for k, v in self.__dict__.items()
|
|
1195
|
+
if k != "_pending_events"
|
|
1196
|
+
]
|
|
1197
|
+
return f"{type(self).__name__}({', '.join(attrs)})"
|
|
1198
|
+
|
|
1199
|
+
def __init_subclass__(
|
|
1200
|
+
cls: type[Aggregate], *, created_event_name: str | None = None
|
|
950
1201
|
) -> None:
|
|
951
1202
|
"""
|
|
952
|
-
Initialises aggregate
|
|
1203
|
+
Initialises aggregate subclass by defining __init__ method and event classes.
|
|
953
1204
|
"""
|
|
954
|
-
super().
|
|
1205
|
+
super().__init_subclass__()
|
|
1206
|
+
|
|
1207
|
+
class_annotations = cls.__dict__.get("__annotations__", {})
|
|
1208
|
+
try:
|
|
1209
|
+
class_annotations.pop("id")
|
|
1210
|
+
_annotations_mention_id.add(cls)
|
|
1211
|
+
except KeyError:
|
|
1212
|
+
pass
|
|
1213
|
+
|
|
1214
|
+
if class_annotations or any(
|
|
1215
|
+
dataclasses.is_dataclass(base) for base in cls.__bases__
|
|
1216
|
+
):
|
|
1217
|
+
dataclasses.dataclass(eq=False, repr=False)(cls)
|
|
955
1218
|
|
|
956
1219
|
# Identify or define a base event class for this aggregate.
|
|
957
1220
|
base_event_name = "Event"
|
|
@@ -984,15 +1247,8 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
984
1247
|
if isinstance(value, type) and issubclass(value, CanInitAggregate):
|
|
985
1248
|
created_event_classes[name] = value
|
|
986
1249
|
|
|
987
|
-
# Disallow using both '_created_event_class' and 'created_event_name'.
|
|
988
|
-
created_event_class: type[CanInitAggregate] | None = cls.__dict__.get(
|
|
989
|
-
"_created_event_class"
|
|
990
|
-
)
|
|
991
|
-
if created_event_class and created_event_name:
|
|
992
|
-
msg = "Can't use both '_created_event_class' and 'created_event_name'"
|
|
993
|
-
raise TypeError(msg)
|
|
994
|
-
|
|
995
1250
|
# Identify or define the aggregate's "created" event class.
|
|
1251
|
+
created_event_class: type[CanInitAggregate] | None = None
|
|
996
1252
|
|
|
997
1253
|
# Is the init method decorated with a CommandMethodDecorator?
|
|
998
1254
|
if isinstance(cls.__dict__.get("__init__"), CommandMethodDecorator):
|
|
@@ -1005,16 +1261,13 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
1005
1261
|
if created_event_name:
|
|
1006
1262
|
msg = "Can't use both 'created_event_name' and decorator on __init__"
|
|
1007
1263
|
raise TypeError(msg)
|
|
1008
|
-
# Disallow using both '_created_event_class' and decorator on __init__.
|
|
1009
|
-
if created_event_class:
|
|
1010
|
-
msg = "Can't use both '_created_event_class' and decorator on __init__"
|
|
1011
|
-
raise TypeError(msg)
|
|
1012
1264
|
|
|
1013
1265
|
# Does the decorator specify a "created" event class?
|
|
1014
1266
|
if init_decorator.given_event_cls:
|
|
1015
1267
|
created_event_class = cast(
|
|
1016
1268
|
type[CanInitAggregate], init_decorator.given_event_cls
|
|
1017
1269
|
)
|
|
1270
|
+
|
|
1018
1271
|
# Does the decorator specify a "created" event name?
|
|
1019
1272
|
elif init_decorator.event_cls_name:
|
|
1020
1273
|
created_event_name = init_decorator.event_cls_name
|
|
@@ -1026,7 +1279,7 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
1026
1279
|
|
|
1027
1280
|
# TODO: Write a test to cover this when "Created" class is explicitly defined.
|
|
1028
1281
|
# Check if init mentions ID.
|
|
1029
|
-
for param_name in inspect.signature(cls.__init__).parameters:
|
|
1282
|
+
for param_name in inspect.signature(cls.__init__).parameters:
|
|
1030
1283
|
if param_name == "id":
|
|
1031
1284
|
_init_mentions_id.add(cls)
|
|
1032
1285
|
break
|
|
@@ -1060,17 +1313,21 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
1060
1313
|
if created_event_name and len(created_event_classes) == 1:
|
|
1061
1314
|
base_created_event_cls = next(iter(created_event_classes.values()))
|
|
1062
1315
|
else:
|
|
1316
|
+
# Todo: This could probably be improved.
|
|
1317
|
+
# Look for first class in MRO that has one specified "created" class.
|
|
1063
1318
|
for base_cls in cls.__mro__:
|
|
1064
|
-
if
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
base_cls
|
|
1069
|
-
)
|
|
1070
|
-
if base_created_event_cls:
|
|
1319
|
+
if (
|
|
1320
|
+
base_cls in _created_event_classes
|
|
1321
|
+
and len(_created_event_classes[base_cls]) == 1
|
|
1322
|
+
):
|
|
1323
|
+
base_created_event_cls = _created_event_classes[base_cls][0]
|
|
1071
1324
|
break
|
|
1072
1325
|
else: # pragma: no cover
|
|
1073
|
-
|
|
1326
|
+
# Todo: Write a test to cover this.
|
|
1327
|
+
msg = (
|
|
1328
|
+
"Can't find base aggregate class with "
|
|
1329
|
+
"a specified 'created' event class"
|
|
1330
|
+
)
|
|
1074
1331
|
raise TypeError(msg)
|
|
1075
1332
|
|
|
1076
1333
|
if not created_event_name:
|
|
@@ -1106,10 +1363,10 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
1106
1363
|
setattr(cls, created_event_name, created_event_class)
|
|
1107
1364
|
|
|
1108
1365
|
if created_event_class:
|
|
1109
|
-
cls
|
|
1366
|
+
_created_event_classes[cls] = [created_event_class]
|
|
1110
1367
|
else:
|
|
1111
1368
|
# Prepare to disallow ambiguity of choice between created event classes.
|
|
1112
|
-
|
|
1369
|
+
_created_event_classes[cls] = list(created_event_classes.values())
|
|
1113
1370
|
|
|
1114
1371
|
# Prepare the subsequent event classes.
|
|
1115
1372
|
for attr_name, attr_value in tuple(cls.__dict__.items()):
|
|
@@ -1163,7 +1420,7 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
1163
1420
|
)
|
|
1164
1421
|
event_cls = cls._define_event_class(
|
|
1165
1422
|
event_decorator.given_event_cls.__name__,
|
|
1166
|
-
(
|
|
1423
|
+
(DecoratorEvent, given_subclass),
|
|
1167
1424
|
None,
|
|
1168
1425
|
)
|
|
1169
1426
|
|
|
@@ -1180,19 +1437,19 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
1180
1437
|
# Define event class from signature of original method.
|
|
1181
1438
|
event_cls = cls._define_event_class(
|
|
1182
1439
|
event_decorator.event_cls_name,
|
|
1183
|
-
(
|
|
1440
|
+
(DecoratorEvent, base_event_cls),
|
|
1184
1441
|
event_decorator.decorated_method,
|
|
1185
1442
|
)
|
|
1186
1443
|
|
|
1187
1444
|
# Cache the decorated method for the event class to use.
|
|
1188
|
-
|
|
1445
|
+
_decorated_methods[event_cls] = event_decorator.decorated_method
|
|
1189
1446
|
|
|
1190
1447
|
# Set the event class as an attribute of the aggregate class.
|
|
1191
1448
|
setattr(cls, event_cls.__name__, event_cls)
|
|
1192
1449
|
|
|
1193
1450
|
# Remember which event class to trigger.
|
|
1194
|
-
|
|
1195
|
-
type[
|
|
1451
|
+
decorator_event_classes[event_decorator] = cast(
|
|
1452
|
+
type[DecoratorEvent], event_cls
|
|
1196
1453
|
)
|
|
1197
1454
|
|
|
1198
1455
|
# Check any create_id method defined on this class is static or class method.
|
|
@@ -1206,13 +1463,12 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
1206
1463
|
raise TypeError(msg)
|
|
1207
1464
|
|
|
1208
1465
|
# Get the parameters of the create_id method that will be used by this class.
|
|
1209
|
-
cls._create_id_param_names: list[str] = []
|
|
1210
1466
|
for name, param in inspect.signature(cls.create_id).parameters.items():
|
|
1211
1467
|
if param.kind in [param.KEYWORD_ONLY, param.POSITIONAL_OR_KEYWORD]:
|
|
1212
|
-
cls.
|
|
1468
|
+
_create_id_param_names[cls].append(name)
|
|
1213
1469
|
|
|
1214
1470
|
# Define event classes for all events on bases.
|
|
1215
|
-
for aggregate_base_class in
|
|
1471
|
+
for aggregate_base_class in cls.__bases__:
|
|
1216
1472
|
for name, value in aggregate_base_class.__dict__.items():
|
|
1217
1473
|
if (
|
|
1218
1474
|
isinstance(value, type)
|
|
@@ -1225,253 +1481,20 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
1225
1481
|
)
|
|
1226
1482
|
setattr(cls, name, sub_class)
|
|
1227
1483
|
|
|
1228
|
-
def _define_event_class(
|
|
1229
|
-
cls,
|
|
1230
|
-
name: str,
|
|
1231
|
-
bases: tuple[type[CanMutateAggregate], ...],
|
|
1232
|
-
apply_method: CommandMethod | None,
|
|
1233
|
-
) -> type[CanMutateAggregate]:
|
|
1234
|
-
# Define annotations for the event class (specs the init method).
|
|
1235
|
-
annotations = {}
|
|
1236
|
-
if apply_method is not None:
|
|
1237
|
-
method_signature = inspect.signature(apply_method)
|
|
1238
|
-
supers = {
|
|
1239
|
-
s for b in bases for s in b.__mro__ if hasattr(s, "__annotations__")
|
|
1240
|
-
}
|
|
1241
|
-
super_annotations = {a for s in supers for a in s.__annotations__}
|
|
1242
|
-
for param_name, param in list(method_signature.parameters.items())[1:]:
|
|
1243
|
-
# Don't define 'id' on a "created" class.
|
|
1244
|
-
if param_name == "id" and apply_method.__name__ == "__init__":
|
|
1245
|
-
continue
|
|
1246
|
-
# Don't override super class annotations, unless no default on param.
|
|
1247
|
-
if param_name not in super_annotations or param.default == param.empty:
|
|
1248
|
-
annotations[param_name] = param.annotation or "typing.Any"
|
|
1249
|
-
event_cls_qualname = f"{cls.__qualname__}.{name}"
|
|
1250
|
-
event_cls_dict = {
|
|
1251
|
-
"__annotations__": annotations,
|
|
1252
|
-
"__module__": cls.__module__,
|
|
1253
|
-
"__qualname__": event_cls_qualname,
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
# Create the event class object.
|
|
1257
|
-
return cast(type[CanMutateAggregate], type(name, bases, event_cls_dict))
|
|
1258
|
-
|
|
1259
|
-
def __call__(
|
|
1260
|
-
cls: MetaAggregate[TAggregate], *args: Any, **kwargs: Any
|
|
1261
|
-
) -> TAggregate:
|
|
1262
|
-
try:
|
|
1263
|
-
created_event_classes = aggregate_has_many_created_event_classes[cls]
|
|
1264
|
-
msg = (
|
|
1265
|
-
"""Can't decide which of many "created" event classes to use: """
|
|
1266
|
-
f"""'{"', '".join(created_event_classes)}'. Please use class """
|
|
1267
|
-
"arg 'created_event_name' or @event decorator on __init__ method."
|
|
1268
|
-
)
|
|
1269
|
-
raise TypeError(msg)
|
|
1270
|
-
except KeyError:
|
|
1271
|
-
pass
|
|
1272
|
-
|
|
1273
|
-
cls_init: FunctionType | WrapperDescriptorType = cls.__init__ # type: ignore
|
|
1274
|
-
kwargs = _coerce_args_to_kwargs(
|
|
1275
|
-
cls_init,
|
|
1276
|
-
args,
|
|
1277
|
-
kwargs,
|
|
1278
|
-
expects_id=cls in _annotations_mention_id,
|
|
1279
|
-
)
|
|
1280
|
-
return cls._create(
|
|
1281
|
-
event_class=cls._created_event_class,
|
|
1282
|
-
**kwargs,
|
|
1283
|
-
)
|
|
1284
|
-
|
|
1285
|
-
def _create(
|
|
1286
|
-
cls: MetaAggregate[TAggregate],
|
|
1287
|
-
event_class: type[CanInitAggregate],
|
|
1288
|
-
**kwargs: Any,
|
|
1289
|
-
) -> TAggregate:
|
|
1290
|
-
raise NotImplementedError # pragma: no cover
|
|
1291
|
-
|
|
1292
|
-
@staticmethod
|
|
1293
|
-
def create_id(**_: Any) -> UUID:
|
|
1294
|
-
"""
|
|
1295
|
-
Returns a new aggregate ID.
|
|
1296
|
-
"""
|
|
1297
|
-
return uuid4()
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
class Aggregate(metaclass=MetaAggregate):
|
|
1301
|
-
"""
|
|
1302
|
-
Base class for aggregate roots.
|
|
1303
|
-
|
|
1304
|
-
.. automethod:: _create
|
|
1305
|
-
"""
|
|
1306
|
-
|
|
1307
|
-
@classmethod
|
|
1308
|
-
def _create(
|
|
1309
|
-
cls: type[TAggregate],
|
|
1310
|
-
event_class: type[CanInitAggregate],
|
|
1311
|
-
*,
|
|
1312
|
-
id: UUID | None = None, # noqa: A002
|
|
1313
|
-
**kwargs: Any,
|
|
1314
|
-
) -> TAggregate:
|
|
1315
|
-
"""
|
|
1316
|
-
Constructs a new aggregate object instance.
|
|
1317
|
-
"""
|
|
1318
|
-
# Construct the domain event with an ID and a
|
|
1319
|
-
# version, and a topic for the aggregate class.
|
|
1320
|
-
create_id_kwargs = {
|
|
1321
|
-
k: v for k, v in kwargs.items() if k in cls._create_id_param_names
|
|
1322
|
-
}
|
|
1323
|
-
originator_id = id or cls.create_id(**create_id_kwargs)
|
|
1324
1484
|
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
originator_topic=get_topic(cls),
|
|
1329
|
-
originator_id=originator_id,
|
|
1330
|
-
originator_version=cls.INITIAL_VERSION,
|
|
1331
|
-
)
|
|
1332
|
-
if kwargs.get("timestamp") is None:
|
|
1333
|
-
kwargs["timestamp"] = event_class.create_timestamp()
|
|
1485
|
+
# Special case for the Aggregate class because
|
|
1486
|
+
# it's not processed by Aggregate.__init_subclass__.
|
|
1487
|
+
_created_event_classes[Aggregate] = [Aggregate.Created]
|
|
1334
1488
|
|
|
1335
|
-
try:
|
|
1336
|
-
created_event = event_class(**kwargs)
|
|
1337
|
-
except TypeError as e:
|
|
1338
|
-
msg = f"Unable to construct '{event_class.__name__}' event: {e}"
|
|
1339
|
-
raise TypeError(msg) from None
|
|
1340
|
-
# Construct the aggregate object.
|
|
1341
|
-
agg = cast(TAggregate, created_event.mutate(None))
|
|
1342
1489
|
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
# Return the aggregate.
|
|
1347
|
-
return agg
|
|
1348
|
-
|
|
1349
|
-
def __base_init__(
|
|
1350
|
-
self, originator_id: UUID, originator_version: int, timestamp: datetime
|
|
1351
|
-
) -> None:
|
|
1352
|
-
"""
|
|
1353
|
-
Initialises an aggregate object with an :data:`id`, a :data:`version`
|
|
1354
|
-
number, and a :data:`timestamp`.
|
|
1355
|
-
"""
|
|
1356
|
-
self._id = originator_id
|
|
1357
|
-
self._version = originator_version
|
|
1358
|
-
self._created_on = timestamp
|
|
1359
|
-
self._modified_on = timestamp
|
|
1360
|
-
self._pending_events: list[CanMutateAggregate] = []
|
|
1361
|
-
|
|
1362
|
-
@property
|
|
1363
|
-
def id(self) -> UUID:
|
|
1364
|
-
"""
|
|
1365
|
-
The ID of the aggregate.
|
|
1366
|
-
"""
|
|
1367
|
-
return self._id
|
|
1368
|
-
|
|
1369
|
-
@property
|
|
1370
|
-
def version(self) -> int:
|
|
1371
|
-
"""
|
|
1372
|
-
The version number of the aggregate.
|
|
1373
|
-
"""
|
|
1374
|
-
return self._version
|
|
1375
|
-
|
|
1376
|
-
@version.setter
|
|
1377
|
-
def version(self, version: int) -> None:
|
|
1378
|
-
self._version = version
|
|
1379
|
-
|
|
1380
|
-
@property
|
|
1381
|
-
def created_on(self) -> datetime:
|
|
1382
|
-
"""
|
|
1383
|
-
The date and time when the aggregate was created.
|
|
1384
|
-
"""
|
|
1385
|
-
return self._created_on
|
|
1386
|
-
|
|
1387
|
-
@property
|
|
1388
|
-
def modified_on(self) -> datetime:
|
|
1389
|
-
"""
|
|
1390
|
-
The date and time when the aggregate was last modified.
|
|
1391
|
-
"""
|
|
1392
|
-
return self._modified_on
|
|
1393
|
-
|
|
1394
|
-
@modified_on.setter
|
|
1395
|
-
def modified_on(self, modified_on: datetime) -> None:
|
|
1396
|
-
self._modified_on = modified_on
|
|
1397
|
-
|
|
1398
|
-
@property
|
|
1399
|
-
def pending_events(self) -> list[CanMutateAggregate]:
|
|
1400
|
-
"""
|
|
1401
|
-
A list of pending events.
|
|
1402
|
-
"""
|
|
1403
|
-
return self._pending_events
|
|
1404
|
-
|
|
1405
|
-
class Event(AggregateEvent):
|
|
1406
|
-
pass
|
|
1407
|
-
|
|
1408
|
-
class Created(Event, AggregateCreated):
|
|
1409
|
-
pass
|
|
1410
|
-
|
|
1411
|
-
def __eq__(self, other: object) -> bool:
|
|
1412
|
-
return type(self) is type(other) and self.__dict__ == other.__dict__
|
|
1413
|
-
|
|
1414
|
-
def __repr__(self) -> str:
|
|
1415
|
-
attrs = [
|
|
1416
|
-
f"{k.lstrip('_')}={v!r}"
|
|
1417
|
-
for k, v in self.__dict__.items()
|
|
1418
|
-
if k != "_pending_events"
|
|
1419
|
-
]
|
|
1420
|
-
return f"{type(self).__name__}({', '.join(attrs)})"
|
|
1421
|
-
|
|
1422
|
-
def trigger_event(
|
|
1423
|
-
self,
|
|
1424
|
-
event_class: type[CanMutateAggregate],
|
|
1425
|
-
**kwargs: Any,
|
|
1426
|
-
) -> None:
|
|
1427
|
-
"""
|
|
1428
|
-
Triggers domain event of given type, by creating
|
|
1429
|
-
an event object and using it to mutate the aggregate.
|
|
1430
|
-
"""
|
|
1431
|
-
# Construct the domain event as the
|
|
1432
|
-
# next in the aggregate's sequence.
|
|
1433
|
-
# Use counting to generate the sequence.
|
|
1434
|
-
next_version = self.version + 1
|
|
1435
|
-
|
|
1436
|
-
# Impose the required common domain event attribute values.
|
|
1437
|
-
kwargs = kwargs.copy()
|
|
1438
|
-
kwargs.update(
|
|
1439
|
-
originator_id=self.id,
|
|
1440
|
-
originator_version=next_version,
|
|
1441
|
-
)
|
|
1442
|
-
if kwargs.get("timestamp") is None:
|
|
1443
|
-
kwargs["timestamp"] = event_class.create_timestamp()
|
|
1444
|
-
|
|
1445
|
-
try:
|
|
1446
|
-
new_event = event_class(**kwargs)
|
|
1447
|
-
except TypeError as e:
|
|
1448
|
-
msg = f"Can't construct event {event_class}: {e}"
|
|
1449
|
-
raise TypeError(msg) from None
|
|
1450
|
-
|
|
1451
|
-
# Mutate aggregate with domain event.
|
|
1452
|
-
new_event.mutate(self)
|
|
1453
|
-
# Append the domain event to pending list.
|
|
1454
|
-
self._pending_events.append(new_event)
|
|
1455
|
-
|
|
1456
|
-
def collect_events(self) -> Sequence[CanMutateAggregate]:
|
|
1457
|
-
"""
|
|
1458
|
-
Collects and returns a list of pending aggregate
|
|
1459
|
-
:class:`AggregateEvent` objects.
|
|
1460
|
-
"""
|
|
1461
|
-
collected = []
|
|
1462
|
-
while self._pending_events:
|
|
1463
|
-
collected.append(self._pending_events.pop(0))
|
|
1464
|
-
return collected
|
|
1490
|
+
@overload
|
|
1491
|
+
def aggregate(*, created_event_name: str) -> Callable[[Any], type[Aggregate]]:
|
|
1492
|
+
pass # pragma: no cover
|
|
1465
1493
|
|
|
1466
1494
|
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
#
|
|
1470
|
-
#
|
|
1471
|
-
#
|
|
1472
|
-
# @overload
|
|
1473
|
-
# def aggregate(cls: Any) -> Type[Aggregate]:
|
|
1474
|
-
# ...
|
|
1495
|
+
@overload
|
|
1496
|
+
def aggregate(cls: Any) -> type[Aggregate]:
|
|
1497
|
+
pass # pragma: no cover
|
|
1475
1498
|
|
|
1476
1499
|
|
|
1477
1500
|
def aggregate(
|
|
@@ -1617,6 +1640,7 @@ class CanSnapshotAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
|
|
|
1617
1640
|
return aggregate
|
|
1618
1641
|
|
|
1619
1642
|
|
|
1643
|
+
@dataclass(frozen=True)
|
|
1620
1644
|
class Snapshot(CanSnapshotAggregate, DomainEvent):
|
|
1621
1645
|
"""
|
|
1622
1646
|
Snapshots represent the state of an aggregate at a particular
|