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/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="Aggregate")
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: Aggregate) -> None:
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(type):
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 given_event_classes:
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
- given_event_classes.add(event_spec)
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 = decorated_event_classes[self.event_decorator]
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
- given_event_classes: set[type] = set()
742
- decorated_methods: dict[type, CommandMethod] = {}
743
- aggregate_has_many_created_event_classes: dict[type, list[str]] = {}
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
- decorated_event_classes: dict[
747
- CommandMethodDecorator, type[MetaAggregate.DecoratedEvent]
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[MetaAggregate[Aggregate]] = set()
887
- _init_mentions_id: set[MetaAggregate[Aggregate]] = 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
- def _ensure_idempotent_dataclass(module_name: str) -> None:
891
- module = importlib.import_module(module_name)
892
- if (
893
- "dataclass" in module.__dict__
894
- and module.__dict__["dataclass"] == dataclasses.dataclass
895
- and "__original_dataclass_func__" not in module.__dict__
896
- ):
897
- module.__dict__["__original_dataclass_func__"] = module.__dict__["dataclass"]
898
- module.__dict__["dataclass"] = _idempotent_dataclass
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 _idempotent_dataclass(cls: type[object] | None = None, /, **kwargs: Any) -> Any:
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
- def idempotent_wrap(cls: type[object]) -> type[object]:
904
- # Avoid processing dataclass twice.
905
- if "__dataclass_fields__" in cls.__dict__:
906
- return cls
907
- return dataclasses.dataclass(**kwargs)(cls)
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
- # See if we're being called as @dataclass or @dataclass().
910
- if cls is None:
911
- # We're called with parens.
912
- return idempotent_wrap
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
- # We're called as @dataclass without parens.
915
- return idempotent_wrap(cls)
1023
+ _created_event_class: type[CanInitAggregate]
916
1024
 
917
1025
 
918
- class MetaAggregate(type, Generic[TAggregate]):
1026
+ class BaseAggregate(metaclass=MetaAggregate):
919
1027
  """
920
- Metaclass for aggregate classes.
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
- class Event(AggregateEvent):
928
- pass
1033
+ @staticmethod
1034
+ def create_id(*_: Any, **__: Any) -> UUID:
1035
+ """
1036
+ Returns a new aggregate ID.
1037
+ """
1038
+ return uuid4()
929
1039
 
930
- class Created(Event, AggregateCreated):
931
- pass
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
- class DecoratedEvent(CanMutateAggregate):
934
- def apply(self, aggregate: Aggregate) -> None:
935
- """
936
- Applies event to aggregate by calling method decorated by @event.
937
- """
938
- # Call super method, just in case any base classes need it.
939
- super().apply(aggregate)
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
- # Identify the method that was decorated.
942
- decorated_method = decorated_methods[type(self)]
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
- # Select event attributes mentioned in method signature.
945
- kwargs = _filter_kwargs_for_method_params(self.__dict__, decorated_method)
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
- # Call the original method with event attribute values.
948
- decorated_method(aggregate, **kwargs)
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
- _created_event_class: type[CanInitAggregate]
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
- def __new__(cls, *args: Any, **_: Any) -> MetaAggregate[Aggregate]:
1143
+ @property
1144
+ def pending_events(self) -> list[CanMutateAggregate]:
953
1145
  """
954
- Configures aggregate class definition.
1146
+ A list of pending events.
955
1147
  """
1148
+ return self._pending_events
956
1149
 
957
- # Avoid processing dataclass twice. This avoids dataclasses.Field(init=False)
958
- # attributes being reduced to annotation only and then appearing in __init__
959
- # method signature when class is reprocessed, and other similar problems.
960
- _ensure_idempotent_dataclass(module_name=args[2]["__module__"])
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
- class_annotations = args[2]["__annotations__"]
964
- except KeyError:
965
- class_annotations = None
966
- annotations_mention_id = False
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
- def __init__(
982
- cls: MetaAggregate[Aggregate],
983
- *args: Any,
984
- created_event_name: str = "",
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 class by completing the definition of its event classes.
1209
+ Initialises aggregate subclass by defining __init__ method and event classes.
988
1210
  """
989
- super().__init__(*args)
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, (cls.Event,), None
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: # type: ignore
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 base_cls is cls:
1100
- continue
1101
- base_created_event_cls = base_cls.__dict__.get(
1102
- "_created_event_class",
1103
- base_cls.__dict__.get("Created"),
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
- msg = "Can't decide base class for new 'created' event class"
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._created_event_class = created_event_class
1376
+ _created_event_classes[cls] = [created_event_class]
1145
1377
  else:
1146
1378
  # Prepare to disallow ambiguity of choice between created event classes.
1147
- aggregate_has_many_created_event_classes[cls] = list(created_event_classes)
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
- (cls.DecoratedEvent, given_subclass),
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
- (cls.DecoratedEvent, base_event_cls),
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
- decorated_methods[event_cls] = event_decorator.decorated_method
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
- decorated_event_classes[event_decorator] = cast(
1230
- type[MetaAggregate.DecoratedEvent], event_cls
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._create_id_param_names.append(name)
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 args[1]:
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
- # Mutate aggregate with domain event.
1487
- new_event.mutate(self)
1488
- # Append the domain event to pending list.
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
- # @overload
1503
- # def aggregate(*, created_event_name: str) -> Callable[[Any], Type[Aggregate]]:
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( # type: ignore
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