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/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(type):
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 given_event_classes:
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
- given_event_classes.add(event_spec)
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 = decorated_event_classes[self.event_decorator]
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
- given_event_classes: set[type] = set()
741
- decorated_methods: dict[type, CommandMethod] = {}
742
- 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)
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
- decorated_event_classes: dict[
746
- CommandMethodDecorator, type[MetaAggregate.DecoratedEvent]
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[MetaAggregate[Aggregate]] = set()
886
- _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)
887
940
 
888
941
 
889
- class MetaAggregate(type, Generic[TAggregate]):
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
- Initialises aggregate classes by defining event classes.
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
- class DecoratedEvent(CanMutateAggregate):
905
- def apply(self, aggregate: Aggregate) -> None:
906
- """
907
- Applies event to aggregate by calling method decorated by @event.
908
- """
909
- # Call super method, just in case any base classes need it.
910
- super().apply(aggregate)
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
- # Identify the method that was decorated.
913
- decorated_method = decorated_methods[type(self)]
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
- # Select event attributes mentioned in method signature.
916
- kwargs = _filter_kwargs_for_method_params(self.__dict__, decorated_method)
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
- # Call the original method with event attribute values.
919
- decorated_method(aggregate, **kwargs)
1101
+ @property
1102
+ def id(self) -> UUID:
1103
+ """
1104
+ The ID of the aggregate.
1105
+ """
1106
+ return self._id
920
1107
 
921
- _created_event_class: type[CanInitAggregate]
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 __new__(cls, *args: Any, **_: Any) -> MetaAggregate[Aggregate]:
1144
+ def trigger_event(
1145
+ self,
1146
+ event_class: type[CanMutateAggregate],
1147
+ **kwargs: Any,
1148
+ ) -> None:
924
1149
  """
925
- Configures aggregate class definition.
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
- class_annotations = args[2]["__annotations__"]
929
- except KeyError:
930
- class_annotations = None
931
- annotations_mention_id = False
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
- def __init__(
947
- cls: MetaAggregate[Aggregate],
948
- *args: Any,
949
- 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
950
1201
  ) -> None:
951
1202
  """
952
- Initialises aggregate class by completing the definition of its event classes.
1203
+ Initialises aggregate subclass by defining __init__ method and event classes.
953
1204
  """
954
- 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)
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: # type: ignore
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 base_cls is cls:
1065
- continue
1066
- base_created_event_cls = base_cls.__dict__.get(
1067
- "_created_event_class",
1068
- base_cls.__dict__.get("Created"),
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
- 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
+ )
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._created_event_class = created_event_class
1366
+ _created_event_classes[cls] = [created_event_class]
1110
1367
  else:
1111
1368
  # Prepare to disallow ambiguity of choice between created event classes.
1112
- aggregate_has_many_created_event_classes[cls] = list(created_event_classes)
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
- (cls.DecoratedEvent, given_subclass),
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
- (cls.DecoratedEvent, base_event_cls),
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
- decorated_methods[event_cls] = event_decorator.decorated_method
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
- decorated_event_classes[event_decorator] = cast(
1195
- type[MetaAggregate.DecoratedEvent], event_cls
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._create_id_param_names.append(name)
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 args[1]:
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
- # Impose the required common "created" event attribute values.
1326
- kwargs = kwargs.copy()
1327
- kwargs.update(
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
- assert agg is not None
1344
- # Append the domain event to pending list.
1345
- agg.pending_events.append(created_event)
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
- # @overload
1468
- # def aggregate(*, created_event_name: str) -> Callable[[Any], Type[Aggregate]]:
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