eventsourcing 9.4.0a7__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/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
  """
@@ -217,6 +253,7 @@ class CanMutateAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
217
253
  method that evolves the state of an aggregate.
218
254
  """
219
255
 
256
+ # Todo: Move this to a HasTimestamp? Why is it here??
220
257
  timestamp: datetime
221
258
  """Timezone-aware :class:`datetime` object representing when an event occurred."""
222
259
 
@@ -326,7 +363,7 @@ class CanInitAggregate(CanMutateAggregate):
326
363
  return agg
327
364
 
328
365
 
329
- class MetaDomainEvent(type):
366
+ class MetaDomainEvent(EventsourcingType):
330
367
  """
331
368
  Metaclass which ensures all domain event classes are frozen dataclasses.
332
369
  """
@@ -342,6 +379,7 @@ class MetaDomainEvent(type):
342
379
  return event_cls
343
380
 
344
381
 
382
+ @dataclass(frozen=True)
345
383
  class DomainEvent(CanCreateTimestamp, metaclass=MetaDomainEvent):
346
384
  """
347
385
  Frozen data class representing domain model events.
@@ -363,6 +401,7 @@ class AggregateEvent(CanMutateAggregate, DomainEvent):
363
401
  """
364
402
 
365
403
 
404
+ @dataclass(frozen=True)
366
405
  class AggregateCreated(CanInitAggregate, AggregateEvent):
367
406
  """
368
407
  Frozen data class representing the initial creation of an aggregate.
@@ -392,10 +431,6 @@ class LogEvent(DomainEvent):
392
431
  """
393
432
 
394
433
 
395
- # Deprecated: Use TDomainEvent instead.
396
- TLogEvent = TypeVar("TLogEvent", bound=DomainEventProtocol)
397
-
398
-
399
434
  def _filter_kwargs_for_method_params(
400
435
  kwargs: dict[str, Any], method: Callable[..., Any]
401
436
  ) -> dict[str, Any]:
@@ -442,12 +477,12 @@ class CommandMethodDecorator:
442
477
  elif isinstance(event_spec, type) and issubclass(
443
478
  event_spec, CanMutateAggregate
444
479
  ):
445
- if event_spec in given_event_classes:
480
+ if event_spec in _given_event_classes:
446
481
  name = event_spec.__name__
447
482
  msg = f"{name} event class used in more than one decorator"
448
483
  raise TypeError(msg)
449
484
  self.given_event_cls = event_spec
450
- given_event_classes.add(event_spec)
485
+ _given_event_classes.add(event_spec)
451
486
 
452
487
  # Process a decorated property.
453
488
  if isinstance(decorated_obj, property):
@@ -730,7 +765,7 @@ class BoundCommandMethodDecorator:
730
765
  kwargs = _coerce_args_to_kwargs(
731
766
  self.event_decorator.decorated_method, args, kwargs
732
767
  )
733
- event_cls = decorated_event_classes[self.event_decorator]
768
+ event_cls = decorator_event_classes[self.event_decorator]
734
769
  kwargs = _filter_kwargs_for_method_params(kwargs, event_cls)
735
770
  self.aggregate.trigger_event(event_cls, **kwargs)
736
771
 
@@ -738,14 +773,30 @@ class BoundCommandMethodDecorator:
738
773
  self.trigger(*args, **kwargs)
739
774
 
740
775
 
741
- given_event_classes: set[type] = set()
742
- decorated_methods: dict[type, CommandMethod] = {}
743
- aggregate_has_many_created_event_classes: dict[type, list[str]] = {}
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)
744
792
 
745
793
 
746
- decorated_event_classes: dict[
747
- CommandMethodDecorator, type[MetaAggregate.DecoratedEvent]
748
- ] = {}
794
+ _given_event_classes: set[type] = set()
795
+ _decorated_methods: dict[type, CommandMethod] = {}
796
+ _created_event_classes: dict[type, list[type[CanInitAggregate]]] = {}
797
+
798
+
799
+ decorator_event_classes: dict[CommandMethodDecorator, type[DecoratorEvent]] = {}
749
800
 
750
801
 
751
802
  def _check_no_variable_params(method: FunctionType) -> None:
@@ -883,43 +934,86 @@ def _raise_missing_names_type_error(missing_names: list[str], msg: str) -> None:
883
934
  raise TypeError(msg)
884
935
 
885
936
 
886
- _annotations_mention_id: set[MetaAggregate[Aggregate]] = set()
887
- _init_mentions_id: set[MetaAggregate[Aggregate]] = 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)
888
940
 
889
941
 
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
942
+ class MetaAggregate(EventsourcingType, Generic[TAggregate], type):
943
+ """
944
+ Metaclass for aggregate classes.
945
+ """
899
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
+ }
900
974
 
901
- def _idempotent_dataclass(cls: type[object] | None = None, /, **kwargs: Any) -> Any:
975
+ # Create the event class object.
976
+ return cast(type[CanMutateAggregate], type(name, bases, event_cls_dict))
902
977
 
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)
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
+ )
908
1003
 
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
1004
+ def _create(
1005
+ cls: MetaAggregate[TAggregate],
1006
+ event_class: type[CanInitAggregate],
1007
+ **kwargs: Any,
1008
+ ) -> TAggregate:
1009
+ raise NotImplementedError # pragma: no cover
913
1010
 
914
- # We're called as @dataclass without parens.
915
- return idempotent_wrap(cls)
1011
+ _created_event_class: type[CanInitAggregate]
916
1012
 
917
1013
 
918
- class MetaAggregate(type, Generic[TAggregate]):
1014
+ class Aggregate(metaclass=MetaAggregate):
919
1015
  """
920
- Metaclass for aggregate classes.
921
-
922
- Initialises aggregate classes by defining event classes.
1016
+ Base class for aggregates.
923
1017
  """
924
1018
 
925
1019
  INITIAL_VERSION = 1
@@ -930,63 +1024,197 @@ class MetaAggregate(type, Generic[TAggregate]):
930
1024
  class Created(Event, AggregateCreated):
931
1025
  pass
932
1026
 
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)
1027
+ @staticmethod
1028
+ def create_id(*_: Any, **__: Any) -> UUID:
1029
+ """
1030
+ Returns a new aggregate ID.
1031
+ """
1032
+ return uuid4()
940
1033
 
941
- # Identify the method that was decorated.
942
- decorated_method = decorated_methods[type(self)]
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)
943
1063
 
944
- # Select event attributes mentioned in method signature.
945
- kwargs = _filter_kwargs_for_method_params(self.__dict__, decorated_method)
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()
946
1073
 
947
- # Call the original method with event attribute values.
948
- decorated_method(aggregate, **kwargs)
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))
949
1081
 
950
- _created_event_class: type[CanInitAggregate]
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
1087
+
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] = []
1100
+
1101
+ @property
1102
+ def id(self) -> UUID:
1103
+ """
1104
+ The ID of the aggregate.
1105
+ """
1106
+ return self._id
1107
+
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
951
1125
 
952
- def __new__(cls, *args: Any, **_: Any) -> MetaAggregate[Aggregate]:
1126
+ @property
1127
+ def modified_on(self) -> datetime:
953
1128
  """
954
- Configures aggregate class definition.
1129
+ The date and time when the aggregate was last modified.
955
1130
  """
1131
+ return self._modified_on
956
1132
 
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__"])
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
1143
+
1144
+ def trigger_event(
1145
+ self,
1146
+ event_class: type[CanMutateAggregate],
1147
+ **kwargs: Any,
1148
+ ) -> None:
1149
+ """
1150
+ Triggers domain event of given type, by creating
1151
+ an event object and using it to mutate the aggregate.
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()
961
1166
 
962
1167
  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
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
980
1172
 
981
- def __init__(
982
- cls: MetaAggregate[Aggregate],
983
- *args: Any,
984
- created_event_name: str = "",
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
985
1201
  ) -> None:
986
1202
  """
987
- Initialises aggregate class by completing the definition of its event classes.
1203
+ Initialises aggregate subclass by defining __init__ method and event classes.
988
1204
  """
989
- super().__init__(*args)
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)
990
1218
 
991
1219
  # Identify or define a base event class for this aggregate.
992
1220
  base_event_name = "Event"
@@ -1019,15 +1247,8 @@ class MetaAggregate(type, Generic[TAggregate]):
1019
1247
  if isinstance(value, type) and issubclass(value, CanInitAggregate):
1020
1248
  created_event_classes[name] = value
1021
1249
 
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
1250
  # Identify or define the aggregate's "created" event class.
1251
+ created_event_class: type[CanInitAggregate] | None = None
1031
1252
 
1032
1253
  # Is the init method decorated with a CommandMethodDecorator?
1033
1254
  if isinstance(cls.__dict__.get("__init__"), CommandMethodDecorator):
@@ -1040,16 +1261,13 @@ class MetaAggregate(type, Generic[TAggregate]):
1040
1261
  if created_event_name:
1041
1262
  msg = "Can't use both 'created_event_name' and decorator on __init__"
1042
1263
  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
1264
 
1048
1265
  # Does the decorator specify a "created" event class?
1049
1266
  if init_decorator.given_event_cls:
1050
1267
  created_event_class = cast(
1051
1268
  type[CanInitAggregate], init_decorator.given_event_cls
1052
1269
  )
1270
+
1053
1271
  # Does the decorator specify a "created" event name?
1054
1272
  elif init_decorator.event_cls_name:
1055
1273
  created_event_name = init_decorator.event_cls_name
@@ -1061,7 +1279,7 @@ class MetaAggregate(type, Generic[TAggregate]):
1061
1279
 
1062
1280
  # TODO: Write a test to cover this when "Created" class is explicitly defined.
1063
1281
  # Check if init mentions ID.
1064
- for param_name in inspect.signature(cls.__init__).parameters: # type: ignore
1282
+ for param_name in inspect.signature(cls.__init__).parameters:
1065
1283
  if param_name == "id":
1066
1284
  _init_mentions_id.add(cls)
1067
1285
  break
@@ -1095,17 +1313,21 @@ class MetaAggregate(type, Generic[TAggregate]):
1095
1313
  if created_event_name and len(created_event_classes) == 1:
1096
1314
  base_created_event_cls = next(iter(created_event_classes.values()))
1097
1315
  else:
1316
+ # Todo: This could probably be improved.
1317
+ # Look for first class in MRO that has one specified "created" class.
1098
1318
  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:
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]
1106
1324
  break
1107
1325
  else: # pragma: no cover
1108
- msg = "Can't decide base class for new 'created' event class"
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
+ )
1109
1331
  raise TypeError(msg)
1110
1332
 
1111
1333
  if not created_event_name:
@@ -1141,10 +1363,10 @@ class MetaAggregate(type, Generic[TAggregate]):
1141
1363
  setattr(cls, created_event_name, created_event_class)
1142
1364
 
1143
1365
  if created_event_class:
1144
- cls._created_event_class = created_event_class
1366
+ _created_event_classes[cls] = [created_event_class]
1145
1367
  else:
1146
1368
  # Prepare to disallow ambiguity of choice between created event classes.
1147
- aggregate_has_many_created_event_classes[cls] = list(created_event_classes)
1369
+ _created_event_classes[cls] = list(created_event_classes.values())
1148
1370
 
1149
1371
  # Prepare the subsequent event classes.
1150
1372
  for attr_name, attr_value in tuple(cls.__dict__.items()):
@@ -1198,7 +1420,7 @@ class MetaAggregate(type, Generic[TAggregate]):
1198
1420
  )
1199
1421
  event_cls = cls._define_event_class(
1200
1422
  event_decorator.given_event_cls.__name__,
1201
- (cls.DecoratedEvent, given_subclass),
1423
+ (DecoratorEvent, given_subclass),
1202
1424
  None,
1203
1425
  )
1204
1426
 
@@ -1215,19 +1437,19 @@ class MetaAggregate(type, Generic[TAggregate]):
1215
1437
  # Define event class from signature of original method.
1216
1438
  event_cls = cls._define_event_class(
1217
1439
  event_decorator.event_cls_name,
1218
- (cls.DecoratedEvent, base_event_cls),
1440
+ (DecoratorEvent, base_event_cls),
1219
1441
  event_decorator.decorated_method,
1220
1442
  )
1221
1443
 
1222
1444
  # Cache the decorated method for the event class to use.
1223
- decorated_methods[event_cls] = event_decorator.decorated_method
1445
+ _decorated_methods[event_cls] = event_decorator.decorated_method
1224
1446
 
1225
1447
  # Set the event class as an attribute of the aggregate class.
1226
1448
  setattr(cls, event_cls.__name__, event_cls)
1227
1449
 
1228
1450
  # Remember which event class to trigger.
1229
- decorated_event_classes[event_decorator] = cast(
1230
- type[MetaAggregate.DecoratedEvent], event_cls
1451
+ decorator_event_classes[event_decorator] = cast(
1452
+ type[DecoratorEvent], event_cls
1231
1453
  )
1232
1454
 
1233
1455
  # Check any create_id method defined on this class is static or class method.
@@ -1241,13 +1463,12 @@ class MetaAggregate(type, Generic[TAggregate]):
1241
1463
  raise TypeError(msg)
1242
1464
 
1243
1465
  # Get the parameters of the create_id method that will be used by this class.
1244
- cls._create_id_param_names: list[str] = []
1245
1466
  for name, param in inspect.signature(cls.create_id).parameters.items():
1246
1467
  if param.kind in [param.KEYWORD_ONLY, param.POSITIONAL_OR_KEYWORD]:
1247
- cls._create_id_param_names.append(name)
1468
+ _create_id_param_names[cls].append(name)
1248
1469
 
1249
1470
  # Define event classes for all events on bases.
1250
- for aggregate_base_class in args[1]:
1471
+ for aggregate_base_class in cls.__bases__:
1251
1472
  for name, value in aggregate_base_class.__dict__.items():
1252
1473
  if (
1253
1474
  isinstance(value, type)
@@ -1260,253 +1481,20 @@ class MetaAggregate(type, Generic[TAggregate]):
1260
1481
  )
1261
1482
  setattr(cls, name, sub_class)
1262
1483
 
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
1484
 
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()
1485
+ # Special case for the Aggregate class because
1486
+ # it's not processed by Aggregate.__init_subclass__.
1487
+ _created_event_classes[Aggregate] = [Aggregate.Created]
1369
1488
 
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
1489
 
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
-
1440
- class Event(AggregateEvent):
1441
- pass
1442
-
1443
- class Created(Event, AggregateCreated):
1444
- pass
1445
-
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
-
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
1490
+ @overload
1491
+ def aggregate(*, created_event_name: str) -> Callable[[Any], type[Aggregate]]:
1492
+ pass # pragma: no cover
1500
1493
 
1501
1494
 
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
- # ...
1495
+ @overload
1496
+ def aggregate(cls: Any) -> type[Aggregate]:
1497
+ pass # pragma: no cover
1510
1498
 
1511
1499
 
1512
1500
  def aggregate(
@@ -1652,6 +1640,7 @@ class CanSnapshotAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
1652
1640
  return aggregate
1653
1641
 
1654
1642
 
1643
+ @dataclass(frozen=True)
1655
1644
  class Snapshot(CanSnapshotAggregate, DomainEvent):
1656
1645
  """
1657
1646
  Snapshots represent the state of an aggregate at a particular