eventsourcing 9.4.4__py3-none-any.whl → 9.4.5__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,9 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import contextlib
3
4
  import dataclasses
4
5
  import importlib
5
6
  import inspect
6
7
  import os
8
+ from abc import ABCMeta
7
9
  from collections import defaultdict
8
10
  from dataclasses import dataclass
9
11
  from datetime import datetime, tzinfo
@@ -13,18 +15,27 @@ from typing import (
13
15
  TYPE_CHECKING,
14
16
  Any,
15
17
  Callable,
18
+ ClassVar,
16
19
  Generic,
17
20
  Protocol,
18
21
  TypeVar,
19
22
  Union,
20
23
  cast,
24
+ get_args,
25
+ get_origin,
21
26
  overload,
22
27
  runtime_checkable,
23
28
  )
24
29
  from uuid import UUID, uuid4
25
30
  from warnings import warn
26
31
 
27
- from eventsourcing.utils import get_method_name, get_topic, resolve_topic
32
+ from eventsourcing.utils import (
33
+ TopicError,
34
+ get_method_name,
35
+ get_topic,
36
+ register_topic,
37
+ resolve_topic,
38
+ )
28
39
 
29
40
  if TYPE_CHECKING:
30
41
  from collections.abc import Iterable, Sequence
@@ -74,8 +85,12 @@ def patch_dataclasses_process_class() -> None:
74
85
  patch_dataclasses_process_class()
75
86
 
76
87
 
88
+ TAggregateID = TypeVar("TAggregateID", bound=Union[UUID, str])
89
+ TAggregateID_co = TypeVar("TAggregateID_co", bound=Union[UUID, str], covariant=True)
90
+
91
+
77
92
  @runtime_checkable
78
- class DomainEventProtocol(Protocol):
93
+ class DomainEventProtocol(Protocol[TAggregateID_co]):
79
94
  """Protocol for domain event objects.
80
95
 
81
96
  A protocol is defined to allow the event sourcing mechanisms
@@ -89,7 +104,7 @@ class DomainEventProtocol(Protocol):
89
104
  pass # pragma: no cover
90
105
 
91
106
  @property
92
- def originator_id(self) -> UUID:
107
+ def originator_id(self) -> TAggregateID_co:
93
108
  """UUID identifying an aggregate to which the event belongs."""
94
109
  raise NotImplementedError # pragma: no cover
95
110
 
@@ -99,11 +114,11 @@ class DomainEventProtocol(Protocol):
99
114
  raise NotImplementedError # pragma: no cover
100
115
 
101
116
 
102
- TDomainEvent = TypeVar("TDomainEvent", bound=DomainEventProtocol)
103
- SDomainEvent = TypeVar("SDomainEvent", bound=DomainEventProtocol)
117
+ TDomainEvent = TypeVar("TDomainEvent", bound=DomainEventProtocol[Any])
118
+ SDomainEvent = TypeVar("SDomainEvent", bound=DomainEventProtocol[Any])
104
119
 
105
120
 
106
- class MutableAggregateProtocol(Protocol):
121
+ class MutableAggregateProtocol(Protocol[TAggregateID_co]):
107
122
  """Protocol for mutable aggregate objects.
108
123
 
109
124
  A protocol is defined to allow the event sourcing mechanisms
@@ -114,7 +129,7 @@ class MutableAggregateProtocol(Protocol):
114
129
  """
115
130
 
116
131
  @property
117
- def id(self) -> UUID:
132
+ def id(self) -> TAggregateID_co:
118
133
  """Mutable aggregates have a read-only ID that is a UUID."""
119
134
  raise NotImplementedError # pragma: no cover
120
135
 
@@ -129,7 +144,7 @@ class MutableAggregateProtocol(Protocol):
129
144
  raise NotImplementedError # pragma: no cover
130
145
 
131
146
 
132
- class ImmutableAggregateProtocol(Protocol):
147
+ class ImmutableAggregateProtocol(Protocol[TAggregateID_co]):
133
148
  """Protocol for immutable aggregate objects.
134
149
 
135
150
  A protocol is defined to allow the event sourcing mechanisms
@@ -140,7 +155,7 @@ class ImmutableAggregateProtocol(Protocol):
140
155
  """
141
156
 
142
157
  @property
143
- def id(self) -> UUID:
158
+ def id(self) -> TAggregateID_co:
144
159
  """Immutable aggregates have a read-only ID that is a UUID."""
145
160
  raise NotImplementedError # pragma: no cover
146
161
 
@@ -151,13 +166,14 @@ class ImmutableAggregateProtocol(Protocol):
151
166
 
152
167
 
153
168
  MutableOrImmutableAggregate = Union[
154
- ImmutableAggregateProtocol, MutableAggregateProtocol
169
+ ImmutableAggregateProtocol[TAggregateID],
170
+ MutableAggregateProtocol[TAggregateID],
155
171
  ]
156
172
  """Type alias defining a union of mutable and immutable aggregate protocols."""
157
173
 
158
174
 
159
175
  TMutableOrImmutableAggregate = TypeVar(
160
- "TMutableOrImmutableAggregate", bound=MutableOrImmutableAggregate
176
+ "TMutableOrImmutableAggregate", bound=MutableOrImmutableAggregate[Any]
161
177
  )
162
178
  """Type variable bound by the union of mutable and immutable aggregate protocols."""
163
179
 
@@ -166,13 +182,15 @@ TMutableOrImmutableAggregate = TypeVar(
166
182
  class CollectEventsProtocol(Protocol):
167
183
  """Protocol for aggregates that support collecting pending events."""
168
184
 
169
- def collect_events(self) -> Sequence[DomainEventProtocol]:
185
+ def collect_events(self) -> Sequence[DomainEventProtocol[Any]]:
170
186
  """Returns a sequence of events."""
171
187
  raise NotImplementedError # pragma: no cover
172
188
 
173
189
 
174
190
  @runtime_checkable
175
- class CanMutateProtocol(DomainEventProtocol, Protocol[TMutableOrImmutableAggregate]):
191
+ class CanMutateProtocol(
192
+ DomainEventProtocol[Any], Protocol[TMutableOrImmutableAggregate]
193
+ ):
176
194
  """Protocol for events that have a mutate method."""
177
195
 
178
196
  def mutate(
@@ -216,20 +234,39 @@ class CanCreateTimestamp:
216
234
  return datetime_now_with_tzinfo()
217
235
 
218
236
 
219
- TAggregate = TypeVar("TAggregate", bound="BaseAggregate")
237
+ TAggregate = TypeVar("TAggregate", bound="BaseAggregate[Any]")
220
238
 
221
239
 
222
- class HasOriginatorIDVersion:
240
+ class HasOriginatorIDVersion(Generic[TAggregateID_co]):
223
241
  """Declares ``originator_id`` and ``originator_version`` attributes."""
224
242
 
225
- originator_id: UUID
243
+ originator_id: TAggregateID_co
226
244
  """UUID identifying an aggregate to which the event belongs."""
227
245
  originator_version: int
228
246
  """Integer identifying the version of the aggregate when the event occurred."""
229
247
 
248
+ type_originator_id: ClassVar[type[Union[UUID, str]]] # noqa: UP007
230
249
 
231
- class CanMutateAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
232
- """Implements a :func:`~eventsourcing.domain.CanMutateAggregate.mutate`
250
+ def __init_subclass__(cls) -> None:
251
+ cls.find_originator_id_type(HasOriginatorIDVersion)
252
+
253
+ @classmethod
254
+ def find_originator_id_type(cls: type, generic_cls: type) -> None:
255
+ """Store the type argument of TAggregateID_co on the subclass."""
256
+ for orig_base in cls.__orig_bases__: # type: ignore[attr-defined]
257
+ type_originator_id = orig_base.__dict__.get("type_originator_id", "")
258
+ if type_originator_id in (UUID, str):
259
+ cls.type_originator_id = type_originator_id # type: ignore[attr-defined]
260
+ break
261
+ if get_origin(orig_base) is generic_cls:
262
+ type_originator_id = get_args(orig_base)[0]
263
+ if type_originator_id in (UUID, str):
264
+ cls.type_originator_id = type_originator_id # type: ignore[attr-defined]
265
+ break
266
+
267
+
268
+ class CanMutateAggregate(HasOriginatorIDVersion[TAggregateID_co], CanCreateTimestamp):
269
+ """Implements a :py:func:`~eventsourcing.domain.CanMutateAggregate.mutate`
233
270
  method that evolves the state of an aggregate.
234
271
  """
235
272
 
@@ -237,9 +274,12 @@ class CanMutateAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
237
274
  timestamp: datetime
238
275
  """Timezone-aware :class:`datetime` object representing when an event occurred."""
239
276
 
277
+ def __init_subclass__(cls) -> None:
278
+ cls.find_originator_id_type(CanMutateAggregate)
279
+
240
280
  def mutate(self, aggregate: TAggregate | None) -> TAggregate | None:
241
- """Validates and adjustes the attributes of the given ``aggregate`
242
- argument. The argument typed as ``Optional`` but the value is
281
+ """Validates and adjusts the attributes of the given ``aggregate``
282
+ argument. The argument is typed as ``Optional``, but the value is
243
283
  expected to be not ``None``.
244
284
 
245
285
  Validates the ``aggregate`` argument by checking the event's
@@ -289,8 +329,11 @@ class CanMutateAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
289
329
  of an aggregate.
290
330
  """
291
331
 
332
+ def _as_dict(self) -> dict[str, Any]:
333
+ return self.__dict__
292
334
 
293
- class CanInitAggregate(CanMutateAggregate):
335
+
336
+ class CanInitAggregate(CanMutateAggregate[TAggregateID_co]):
294
337
  """Implements a :func:`~eventsourcing.domain.CanMutateAggregate.mutate`
295
338
  method that constructs the initial state of an aggregate.
296
339
  """
@@ -298,6 +341,9 @@ class CanInitAggregate(CanMutateAggregate):
298
341
  originator_topic: str
299
342
  """String describing the path to an aggregate class."""
300
343
 
344
+ def __init_subclass__(cls) -> None:
345
+ cls.find_originator_id_type(CanInitAggregate)
346
+
301
347
  def mutate(self, aggregate: TAggregate | None) -> TAggregate | None:
302
348
  """Constructs an aggregate instance according to the attributes of an event.
303
349
 
@@ -313,8 +359,9 @@ class CanInitAggregate(CanMutateAggregate):
313
359
  agg = aggregate_class.__new__(aggregate_class)
314
360
 
315
361
  # Pick out event attributes for the aggregate base class init method.
362
+ self_dict = self._as_dict()
316
363
  base_kwargs = _filter_kwargs_for_method_params(
317
- self.__dict__, type(agg).__base_init__
364
+ self_dict, type(agg).__base_init__
318
365
  )
319
366
 
320
367
  # Call the base class init method (so we don't need to always write
@@ -322,13 +369,11 @@ class CanInitAggregate(CanMutateAggregate):
322
369
  agg.__base_init__(**base_kwargs)
323
370
 
324
371
  # Pick out event attributes for aggregate subclass class init method.
325
- init_kwargs = _filter_kwargs_for_method_params(
326
- self.__dict__, type(agg).__init__
327
- )
372
+ init_kwargs = _filter_kwargs_for_method_params(self_dict, type(agg).__init__)
328
373
 
329
374
  # Provide the aggregate id, if the __init__ method expects it.
330
375
  if aggregate_class in _init_mentions_id:
331
- init_kwargs["id"] = self.__dict__["originator_id"]
376
+ init_kwargs["id"] = self_dict["originator_id"]
332
377
 
333
378
  # Call the aggregate subclass class init method.
334
379
  agg.__init__(**init_kwargs) # type: ignore[misc]
@@ -365,8 +410,17 @@ class DomainEvent(CanCreateTimestamp, metaclass=MetaDomainEvent):
365
410
  timestamp: datetime
366
411
  """Timezone-aware :class:`datetime` object representing when an event occurred."""
367
412
 
413
+ def __post_init__(self) -> None:
414
+ if not isinstance(self.originator_id, UUID):
415
+ msg = (
416
+ f"{type(self)} "
417
+ f"was initialized with a non-UUID originator_id: "
418
+ f"{self.originator_id!r}"
419
+ )
420
+ raise TypeError(msg)
421
+
368
422
 
369
- class AggregateEvent(CanMutateAggregate, DomainEvent):
423
+ class AggregateEvent(CanMutateAggregate[UUID], DomainEvent):
370
424
  """Frozen data class representing aggregate events.
371
425
 
372
426
  Subclasses represent original decisions made by domain model aggregates.
@@ -374,7 +428,7 @@ class AggregateEvent(CanMutateAggregate, DomainEvent):
374
428
 
375
429
 
376
430
  @dataclass(frozen=True)
377
- class AggregateCreated(CanInitAggregate, AggregateEvent):
431
+ class AggregateCreated(CanInitAggregate[UUID], AggregateEvent):
378
432
  """Frozen data class representing the initial creation of an aggregate."""
379
433
 
380
434
  originator_topic: str
@@ -410,7 +464,7 @@ def _spec_filter_kwargs_for_method_params(method: Callable[..., Any]) -> set[str
410
464
 
411
465
 
412
466
  if TYPE_CHECKING:
413
- EventSpecType = Union[str, type[CanMutateAggregate]]
467
+ EventSpecType = Union[str, type[CanMutateAggregate[Any]]]
414
468
 
415
469
  CallableType = Callable[..., None]
416
470
  DecoratableType = Union[CallableType, property]
@@ -422,14 +476,16 @@ class CommandMethodDecorator:
422
476
  self,
423
477
  event_spec: EventSpecType | None,
424
478
  decorated_obj: DecoratableType,
479
+ event_topic: str | None = None,
425
480
  ):
426
481
  self.is_name_inferred_from_method = False
427
- self.given_event_cls: type[CanMutateAggregate] | None = None
482
+ self.given_event_cls: type[CanMutateAggregate[Any]] | None = None
428
483
  self.event_cls_name: str | None = None
429
484
  self.decorated_property: property | None = None
430
485
  self.is_property_setter = False
431
486
  self.property_setter_arg_name: str | None = None
432
487
  self.decorated_func: CallableType
488
+ self.event_topic = event_topic
433
489
 
434
490
  # Event name has been specified.
435
491
  if isinstance(event_spec, str):
@@ -525,7 +581,7 @@ class CommandMethodDecorator:
525
581
 
526
582
  @overload
527
583
  def __get__(
528
- self, instance: None, owner: type[BaseAggregate]
584
+ self, instance: None, owner: type[BaseAggregate[Any]]
529
585
  ) -> UnboundCommandMethodDecorator | property:
530
586
  """
531
587
  Descriptor protocol for getting decorated method or property on class object.
@@ -533,14 +589,14 @@ class CommandMethodDecorator:
533
589
 
534
590
  @overload
535
591
  def __get__(
536
- self, instance: BaseAggregate, owner: type[BaseAggregate]
592
+ self, instance: BaseAggregate[Any], owner: type[BaseAggregate[Any]]
537
593
  ) -> BoundCommandMethodDecorator | Any:
538
594
  """
539
595
  Descriptor protocol for getting decorated method or property on instance object.
540
596
  """
541
597
 
542
598
  def __get__(
543
- self, instance: BaseAggregate | None, owner: type[BaseAggregate]
599
+ self, instance: BaseAggregate[Any] | None, owner: type[BaseAggregate[Any]]
544
600
  ) -> BoundCommandMethodDecorator | UnboundCommandMethodDecorator | property | Any:
545
601
  """Descriptor protocol for getting decorated method or property."""
546
602
  # If we are decorating a property, then delegate to the property's __get__.
@@ -558,7 +614,7 @@ class CommandMethodDecorator:
558
614
  # Return an "unbound" command method decorator if we have no instance.
559
615
  return UnboundCommandMethodDecorator(self)
560
616
 
561
- def __set__(self, instance: BaseAggregate, value: Any) -> None:
617
+ def __set__(self, instance: BaseAggregate[Any], value: Any) -> None:
562
618
  """Descriptor protocol for assigning to decorated property."""
563
619
  # Set decorated property indirectly by triggering an event.
564
620
  assert self.property_setter_arg_name
@@ -568,26 +624,31 @@ class CommandMethodDecorator:
568
624
 
569
625
 
570
626
  @overload
571
- def event(arg: TDecoratableType) -> TDecoratableType:
627
+ def event(arg: TDecoratableType, /) -> TDecoratableType:
572
628
  """Signature for calling ``@event`` decorator with decorated method."""
573
629
 
574
630
 
575
631
  @overload
576
632
  def event(
577
- arg: EventSpecType,
633
+ arg: type[CanMutateAggregate[Any]], /
578
634
  ) -> Callable[[TDecoratableType], TDecoratableType]:
579
- """Signature for calling ``@event`` decorator with event specification."""
635
+ """Signature for calling ``@event`` decorator with event class."""
580
636
 
581
637
 
582
638
  @overload
583
639
  def event(
584
- arg: None = None,
640
+ arg: str, /, *, topic: str | None = None
585
641
  ) -> Callable[[TDecoratableType], TDecoratableType]:
642
+ """Signature for calling ``@event`` decorator with event name."""
643
+
644
+
645
+ @overload
646
+ def event(arg: None = None, /) -> Callable[[TDecoratableType], TDecoratableType]:
586
647
  """Signature for calling ``@event`` decorator without event specification."""
587
648
 
588
649
 
589
650
  def event(
590
- arg: EventSpecType | TDecoratableType | None = None,
651
+ arg: EventSpecType | TDecoratableType | None = None, /, *, topic: str | None = None
591
652
  ) -> TDecoratableType | Callable[[TDecoratableType], TDecoratableType]:
592
653
  """Event-triggering decorator for aggregate command methods and property setters.
593
654
 
@@ -660,6 +721,7 @@ def event(
660
721
  command_method_decorator = CommandMethodDecorator(
661
722
  event_spec=event_spec,
662
723
  decorated_obj=decorated_obj,
724
+ event_topic=topic,
663
725
  )
664
726
  return cast("TDecoratableType", command_method_decorator)
665
727
 
@@ -690,6 +752,7 @@ class UnboundCommandMethodDecorator:
690
752
  # functools.update_wrapper(self, event_decorator.decorated_method)
691
753
 
692
754
  def __call__(self, *args: Any, **kwargs: Any) -> None:
755
+ # TODO: Review this, because other subclasses of BaseAggregate might too....
693
756
  # Expect first argument is an aggregate instance.
694
757
  if len(args) < 1 or not isinstance(args[0], Aggregate):
695
758
  msg = "Expected aggregate as first argument"
@@ -707,7 +770,7 @@ class BoundCommandMethodDecorator:
707
770
  """
708
771
 
709
772
  def __init__(
710
- self, event_decorator: CommandMethodDecorator, aggregate: BaseAggregate
773
+ self, event_decorator: CommandMethodDecorator, aggregate: BaseAggregate[Any]
711
774
  ):
712
775
  """:param CommandMethodDecorator event_decorator:
713
776
  :param Aggregate aggregate:
@@ -732,14 +795,15 @@ class BoundCommandMethodDecorator:
732
795
  self.trigger(*args, **kwargs)
733
796
 
734
797
 
735
- class DecoratorEvent(CanMutateAggregate):
736
- def apply(self, aggregate: BaseAggregate) -> None:
798
+ class DecoratorEvent(CanMutateAggregate[Any]):
799
+ def apply(self, aggregate: BaseAggregate[Any]) -> None:
737
800
  """Applies event to aggregate by calling method decorated by @event."""
738
801
  # Identify the function that was decorated.
739
802
  decorated_func = _decorated_funcs[type(self)]
740
803
 
741
804
  # Select event attributes mentioned in function signature.
742
- kwargs = _filter_kwargs_for_method_params(self.__dict__, decorated_func)
805
+ self_dict = self._as_dict()
806
+ kwargs = _filter_kwargs_for_method_params(self_dict, decorated_func)
743
807
 
744
808
  # Call the original method with event attribute values.
745
809
  decorated_method = decorated_func.__get__(aggregate, type(aggregate))
@@ -751,7 +815,7 @@ class DecoratorEvent(CanMutateAggregate):
751
815
 
752
816
  _given_event_classes: set[type] = set()
753
817
  _decorated_funcs: dict[type, CallableType] = {}
754
- _created_event_classes: dict[type, list[type[CanInitAggregate]]] = {}
818
+ _created_event_classes: dict[type, list[type[CanInitAggregate[Any]]]] = {}
755
819
 
756
820
 
757
821
  decorator_event_classes: dict[CommandMethodDecorator, type[DecoratorEvent]] = {}
@@ -906,20 +970,23 @@ def _raise_missing_names_type_error(missing_names: list[str], msg: str) -> None:
906
970
  raise TypeError(msg)
907
971
 
908
972
 
909
- _annotations_mention_id: set[type[BaseAggregate]] = set()
910
- _init_mentions_id: set[type[BaseAggregate]] = set()
911
- _create_id_param_names: dict[type[BaseAggregate], list[str]] = defaultdict(list)
973
+ _annotations_mention_id: set[type[BaseAggregate[Any]]] = set()
974
+ _init_mentions_id: set[type[BaseAggregate[Any]]] = set()
975
+ _create_id_param_names: dict[type[BaseAggregate[Any]], list[str]] = defaultdict(list)
912
976
 
977
+ ENVVAR_DISABLE_REDEFINITION_CHECK = "EVENTSOURCING_DISABLE_REDEFINITION_CHECK"
913
978
 
914
- class MetaAggregate(EventsourcingType, Generic[TAggregate], type):
979
+
980
+ class MetaAggregate(EventsourcingType, Generic[TAggregate], ABCMeta):
915
981
  """Metaclass for aggregate classes."""
916
982
 
917
983
  def _define_event_class(
918
984
  cls,
919
985
  name: str,
920
- bases: tuple[type[CanMutateAggregate], ...],
986
+ bases: tuple[type[CanMutateAggregate[Any]], ...],
921
987
  apply_method: CallableType | None,
922
- ) -> type[CanMutateAggregate]:
988
+ event_topic: str | None = None,
989
+ ) -> type[CanMutateAggregate[Any]]:
923
990
  # Define annotations for the event class (specs the init method).
924
991
  annotations = {}
925
992
  if apply_method is not None:
@@ -941,22 +1008,28 @@ class MetaAggregate(EventsourcingType, Generic[TAggregate], type):
941
1008
  "__module__": cls.__module__,
942
1009
  "__qualname__": event_cls_qualname,
943
1010
  }
1011
+ if event_topic:
1012
+ event_cls_dict["TOPIC"] = event_topic
944
1013
 
945
1014
  # Create the event class object.
946
1015
  _new_class = type(name, bases, event_cls_dict)
947
- return cast("type[CanMutateAggregate]", _new_class)
1016
+ return cast("type[CanMutateAggregate[Any]]", _new_class)
948
1017
 
949
1018
  def __call__(
950
1019
  cls: MetaAggregate[TAggregate], *args: Any, **kwargs: Any
951
1020
  ) -> TAggregate:
952
1021
  if cls is BaseAggregate:
953
- msg = "BaseAggregate class cannot be instantiated directly"
1022
+ msg = "Please define or use subclasses of BaseAggregate."
954
1023
  raise TypeError(msg)
955
1024
  created_event_classes = _created_event_classes[cls]
956
1025
  # Here, unlike when calling _create(), we don't have a given event class,
957
1026
  # so we need to check that there is one "created" event class to use here.
958
1027
  # We don't check this in __init_subclass__ to allow for alternatives that
959
1028
  # can be selected by developers by calling _create(event_class=...).
1029
+ if len(created_event_classes) == 0:
1030
+ msg = f"No \"created\" event classes defined on class '{cls.__name__}'."
1031
+ raise TypeError(msg)
1032
+
960
1033
  if len(created_event_classes) > 1:
961
1034
  msg = (
962
1035
  f"{cls.__qualname__} can't decide which of many "
@@ -980,32 +1053,35 @@ class MetaAggregate(EventsourcingType, Generic[TAggregate], type):
980
1053
 
981
1054
  def _create(
982
1055
  cls: MetaAggregate[TAggregate],
983
- event_class: type[CanInitAggregate],
1056
+ event_class: type[CanInitAggregate[Any]],
984
1057
  **kwargs: Any,
985
1058
  ) -> TAggregate:
986
1059
  # Just define method signature for the __call__() method.
987
1060
  raise NotImplementedError # pragma: no cover
988
1061
 
989
1062
 
990
- class BaseAggregate(metaclass=MetaAggregate):
1063
+ class BaseAggregate(Generic[TAggregateID], metaclass=MetaAggregate):
991
1064
  """Base class for aggregates."""
992
1065
 
993
1066
  INITIAL_VERSION: int = 1
994
1067
 
995
1068
  @staticmethod
996
- def create_id(*_: Any, **__: Any) -> UUID:
1069
+ def create_id(*_: Any, **__: Any) -> TAggregateID:
997
1070
  """Returns a new aggregate ID."""
998
- return uuid4()
1071
+ raise NotImplementedError
999
1072
 
1000
1073
  @classmethod
1001
1074
  def _create(
1002
1075
  cls: type[Self],
1003
- event_class: type[CanInitAggregate],
1076
+ event_class: type[CanInitAggregate[TAggregateID]],
1004
1077
  *,
1005
- id: UUID | None = None, # noqa: A002
1078
+ id: UUID | str | None = None, # noqa: A002
1006
1079
  **kwargs: Any,
1007
1080
  ) -> Self:
1008
1081
  """Constructs a new aggregate object instance."""
1082
+ if getattr(cls, "TOPIC", None):
1083
+ _check_explicit_topic_is_registered(event_class)
1084
+
1009
1085
  # Construct the domain event with an ID and a
1010
1086
  # version, and a topic for the aggregate class.
1011
1087
  create_id_kwargs = {
@@ -1013,15 +1089,20 @@ class BaseAggregate(metaclass=MetaAggregate):
1013
1089
  }
1014
1090
  if id is not None:
1015
1091
  originator_id = id
1016
- if not isinstance(originator_id, UUID):
1017
- msg = f"Given id was not a UUID: {originator_id}"
1092
+ if not isinstance(originator_id, (UUID, str)):
1093
+ msg = f"Given id was not a UUID or str: {originator_id!r}"
1018
1094
  raise TypeError(msg)
1019
1095
  else:
1020
- originator_id = cls.create_id(**create_id_kwargs)
1021
- if not isinstance(originator_id, UUID):
1096
+ try:
1097
+ originator_id = cls.create_id(**create_id_kwargs)
1098
+ except NotImplementedError as e:
1099
+ msg = f"Please pass an 'id' arg or define a create_id() method on {cls}"
1100
+ raise NotImplementedError(msg) from e
1101
+
1102
+ if not isinstance(originator_id, (UUID, str)):
1022
1103
  msg = (
1023
1104
  f"{cls.create_id.__module__}.{cls.create_id.__qualname__}"
1024
- f" did not return UUID, it returned: {originator_id}"
1105
+ f" did not return UUID or str, it returned: {originator_id!r}"
1025
1106
  )
1026
1107
  raise TypeError(msg)
1027
1108
 
@@ -1050,19 +1131,22 @@ class BaseAggregate(metaclass=MetaAggregate):
1050
1131
  return agg
1051
1132
 
1052
1133
  def __base_init__(
1053
- self, originator_id: UUID, originator_version: int, timestamp: datetime
1134
+ self,
1135
+ originator_id: Any,
1136
+ originator_version: int,
1137
+ timestamp: datetime,
1054
1138
  ) -> None:
1055
1139
  """Initialises an aggregate object with an :data:`id`, a :data:`version`
1056
1140
  number, and a :data:`timestamp`.
1057
1141
  """
1058
- self._id = originator_id
1142
+ self._id: TAggregateID = originator_id
1059
1143
  self._version = originator_version
1060
1144
  self._created_on = timestamp
1061
1145
  self._modified_on = timestamp
1062
- self._pending_events: list[CanMutateAggregate] = []
1146
+ self._pending_events: list[CanMutateAggregate[TAggregateID]] = []
1063
1147
 
1064
1148
  @property
1065
- def id(self) -> UUID:
1149
+ def id(self) -> TAggregateID:
1066
1150
  """The ID of the aggregate."""
1067
1151
  return self._id
1068
1152
 
@@ -1090,18 +1174,24 @@ class BaseAggregate(metaclass=MetaAggregate):
1090
1174
  self._modified_on = modified_on
1091
1175
 
1092
1176
  @property
1093
- def pending_events(self) -> list[CanMutateAggregate]:
1177
+ def pending_events(self) -> list[CanMutateAggregate[TAggregateID]]:
1094
1178
  """A list of pending events."""
1095
1179
  return self._pending_events
1096
1180
 
1097
1181
  def trigger_event(
1098
1182
  self,
1099
- event_class: type[CanMutateAggregate],
1183
+ event_class: type[CanMutateAggregate[TAggregateID]],
1100
1184
  **kwargs: Any,
1101
1185
  ) -> None:
1102
1186
  """Triggers domain event of given type, by creating
1103
1187
  an event object and using it to mutate the aggregate.
1104
1188
  """
1189
+ if getattr(type(self), "TOPIC", None):
1190
+ if event_class.__name__ == "Event":
1191
+ msg = "Triggering base 'Event' class is prohibited."
1192
+ raise ProgrammingError(msg)
1193
+ _check_explicit_topic_is_registered(event_class)
1194
+
1105
1195
  # Construct the domain event as the
1106
1196
  # next in the aggregate's sequence.
1107
1197
  # Use counting to generate the sequence.
@@ -1127,7 +1217,7 @@ class BaseAggregate(metaclass=MetaAggregate):
1127
1217
  # Append the domain event to pending list.
1128
1218
  self._pending_events.append(new_event)
1129
1219
 
1130
- def collect_events(self) -> Sequence[CanMutateAggregate]:
1220
+ def collect_events(self) -> Sequence[CanMutateAggregate[TAggregateID]]:
1131
1221
  """Collects and returns a list of pending aggregate
1132
1222
  :class:`AggregateEvent` objects.
1133
1223
  """
@@ -1148,7 +1238,7 @@ class BaseAggregate(metaclass=MetaAggregate):
1148
1238
  return f"{type(self).__name__}({', '.join(attrs)})"
1149
1239
 
1150
1240
  def __init_subclass__(
1151
- cls: type[BaseAggregate], *, created_event_name: str = ""
1241
+ cls: type[BaseAggregate[TAggregateID]], *, created_event_name: str = ""
1152
1242
  ) -> None:
1153
1243
  """
1154
1244
  Initialises aggregate subclass by defining __init__ method and event classes.
@@ -1159,7 +1249,10 @@ class BaseAggregate(metaclass=MetaAggregate):
1159
1249
  # because annotations can get confused when using singledispatchmethod
1160
1250
  # during class definition e.g. on an aggregate projector function.
1161
1251
  _module = importlib.import_module(cls.__module__)
1162
- if cls.__name__ in _module.__dict__:
1252
+ if (
1253
+ cls.__name__ in _module.__dict__
1254
+ and ENVVAR_DISABLE_REDEFINITION_CHECK not in os.environ
1255
+ ):
1163
1256
  msg = f"Name '{cls.__name__}' already defined in '{cls.__module__}' module"
1164
1257
  raise ProgrammingError(msg)
1165
1258
 
@@ -1208,41 +1301,88 @@ class BaseAggregate(metaclass=MetaAggregate):
1208
1301
 
1209
1302
  # Identify or define a base event class for this aggregate.
1210
1303
  base_event_name = "Event"
1211
- base_event_cls: type[CanMutateAggregate]
1304
+ base_event_cls: type[CanMutateAggregate[TAggregateID]] | None = None
1305
+ msg = f"Base event class 'Event' not defined on {cls} or ancestors"
1306
+ base_event_class_not_defined_error = TypeError(msg)
1307
+
1212
1308
  try:
1213
1309
  base_event_cls = cls.__dict__[base_event_name]
1214
1310
  except KeyError:
1215
- base_event_cls = cls._define_event_class(
1216
- name=base_event_name,
1217
- bases=(getattr(cls, base_event_name, AggregateEvent),),
1218
- apply_method=None,
1219
- )
1220
- setattr(cls, base_event_name, base_event_cls)
1311
+ try:
1312
+ super_base_event_cls = getattr(cls, base_event_name)
1313
+ except AttributeError:
1314
+ pass
1315
+ else:
1316
+ base_event_cls = cls._define_event_class(
1317
+ name=base_event_name,
1318
+ bases=(super_base_event_cls,),
1319
+ apply_method=None,
1320
+ )
1321
+ setattr(cls, base_event_name, base_event_cls)
1322
+
1323
+ # Remember which events have been redefined, to preserve apparent hierarchy,
1324
+ # in a mapping from the original class to the redefined class.
1325
+ redefined_event_classes: dict[
1326
+ type[CanMutateAggregate[TAggregateID]],
1327
+ type[CanMutateAggregate[TAggregateID]],
1328
+ ] = {}
1329
+
1330
+ # Remember any "created" event classes that are discovered.
1331
+ created_event_classes: dict[str, type[CanInitAggregate[TAggregateID]]] = {}
1221
1332
 
1222
- # Ensure all events defined on this class are subclasses of base event class.
1223
- created_event_classes: dict[str, type[CanInitAggregate]] = {}
1333
+ # TODO: Review decorator processing below to see if subclassing can be improved.
1334
+ # - basically, look at the decorators first, build a plan for defining events
1335
+
1336
+ # Ensure events defined on this class are subclasses of the base event class.
1224
1337
  for name, value in tuple(cls.__dict__.items()):
1338
+ # Don't subclass the base event class again.
1225
1339
  if name == base_event_name:
1226
- # Don't subclass the base event class again.
1227
1340
  continue
1341
+
1342
+ # Don't subclass lowercase named attributes.
1228
1343
  if name.lower() == name:
1229
- # Don't subclass lowercase named attributes.
1230
1344
  continue
1231
- if isinstance(value, type) and issubclass(value, CanMutateAggregate):
1232
- if not issubclass(value, base_event_cls):
1233
- event_class = cls._define_event_class(
1234
- name, (value, base_event_cls), None
1235
- )
1236
- setattr(cls, name, event_class)
1237
- else:
1238
- event_class = value
1239
1345
 
1240
- # Remember all "created" event classes defined on this class.
1241
- if issubclass(event_class, CanInitAggregate):
1242
- created_event_classes[name] = event_class
1346
+ # Only consider "event" classes (implement "CanMutateAggregate" protocol).
1347
+ if not isinstance(value, type) or not issubclass(value, CanMutateAggregate):
1348
+ continue
1349
+
1350
+ # Check we have a base event class.
1351
+ if base_event_cls is None:
1352
+ raise base_event_class_not_defined_error
1353
+
1354
+ # Redefine events that aren't already subclass of the base event class.
1355
+ if not issubclass(value, base_event_cls):
1356
+ # Decide base classes of redefined event class: it must be
1357
+ # a subclass of the original class, all redefined classes that
1358
+ # were in its bases, and the aggregate's base event class.
1359
+ redefined_bases = [
1360
+ redefined_event_classes[b]
1361
+ for b in value.__bases__
1362
+ if b in redefined_event_classes
1363
+ ]
1364
+ event_class_bases = (
1365
+ value,
1366
+ *redefined_bases,
1367
+ base_event_cls,
1368
+ )
1369
+
1370
+ # Define event class.
1371
+ event_class = cls._define_event_class(name, event_class_bases, None)
1372
+ setattr(cls, name, event_class)
1373
+
1374
+ # Remember which events have been redefined.
1375
+ redefined_event_classes[value] = event_class
1376
+ else:
1377
+ event_class = value
1378
+
1379
+ # Remember all "created" event classes defined on this class.
1380
+ if issubclass(event_class, CanInitAggregate):
1381
+ created_event_classes[name] = event_class
1243
1382
 
1244
1383
  # Identify or define the aggregate's "created" event class.
1245
- created_event_class: type[CanInitAggregate] | None = None
1384
+ created_event_class: type[CanInitAggregate[TAggregateID]] | None = None
1385
+ created_event_topic: str | None = None
1246
1386
 
1247
1387
  # Analyse __init__ method decorator.
1248
1388
  if init_decorator:
@@ -1279,6 +1419,7 @@ class BaseAggregate(metaclass=MetaAggregate):
1279
1419
 
1280
1420
  # Does the decorator specify an event name?
1281
1421
  elif init_decorator.event_cls_name:
1422
+ created_event_topic = init_decorator.event_topic
1282
1423
  # Disallow conflicts between 'created_event_name' and given name.
1283
1424
  if (
1284
1425
  created_event_name
@@ -1306,18 +1447,24 @@ class BaseAggregate(metaclass=MetaAggregate):
1306
1447
  elif not created_event_name and len(created_event_classes) == 1:
1307
1448
  created_event_class = next(iter(created_event_classes.values()))
1308
1449
 
1309
- # Otherwise, if there are no "created" events, or a name is
1310
- # specified that hasn't matched, then define a "created" event class.
1450
+ # Otherwise, if there are no "created" event classes, or a name
1451
+ # is specified that hasn't matched, then try to define one.
1311
1452
  elif len(created_event_classes) == 0 or created_event_name:
1312
1453
  # Decide the base "created" event class.
1313
1454
 
1314
- try:
1455
+ base_created_event_cls: type[CanInitAggregate[TAggregateID]] | None = (
1456
+ None
1457
+ )
1458
+
1459
+ if created_event_name:
1315
1460
  # Look for a base class with the same name.
1316
- base_created_event_cls = cast(
1317
- "type[CanInitAggregate]",
1318
- getattr(cls, created_event_name),
1319
- )
1320
- except AttributeError:
1461
+ with contextlib.suppress(AttributeError):
1462
+ base_created_event_cls = cast(
1463
+ type[CanInitAggregate[TAggregateID]],
1464
+ getattr(cls, created_event_name),
1465
+ )
1466
+
1467
+ if base_created_event_cls is None:
1321
1468
  # Look for base class with one nominated "created" event.
1322
1469
  for base_cls in cls.__mro__:
1323
1470
  if (
@@ -1326,48 +1473,54 @@ class BaseAggregate(metaclass=MetaAggregate):
1326
1473
  ):
1327
1474
  base_created_event_cls = _created_event_classes[base_cls][0]
1328
1475
  break
1329
- else:
1330
- msg = (
1331
- "Can't identify suitable base class for "
1332
- f"\"created\" event class on class '{cls.__name__}'"
1333
- )
1334
- raise TypeError(msg) from None
1335
-
1336
- if not created_event_name:
1337
- created_event_name = base_created_event_cls.__name__
1338
-
1339
- # Disallow init method from having variable params, because
1340
- # we are using it to define a "created" event class.
1341
- if init_method:
1342
- _raise_type_error_if_func_has_variable_params(init_method)
1343
-
1344
- # Don't subclass from base event class twice.
1345
- assert isinstance(base_created_event_cls, type), base_created_event_cls
1346
- assert not issubclass(
1347
- base_created_event_cls, base_event_cls
1348
- ), base_created_event_cls
1349
-
1350
- # Define "created" event class.
1351
- assert created_event_name
1352
- assert issubclass(base_created_event_cls, CanInitAggregate)
1353
- created_event_class_bases = (base_created_event_cls, base_event_cls)
1354
- created_event_class = cast(
1355
- "type[CanInitAggregate]",
1356
- cls._define_event_class(
1357
- created_event_name,
1358
- created_event_class_bases,
1359
- init_method,
1360
- ),
1361
- )
1362
- # Set the event class as an attribute of the aggregate class.
1363
- setattr(cls, created_event_name, created_event_class)
1364
1476
 
1365
- assert created_event_class or len(created_event_classes) > 1
1477
+ if base_created_event_cls:
1478
+ if not created_event_name:
1479
+ created_event_name = base_created_event_cls.__name__
1480
+
1481
+ # Disallow init method from having variable params, because
1482
+ # we are using it to define a "created" event class.
1483
+ if init_method:
1484
+ _raise_type_error_if_func_has_variable_params(init_method)
1485
+
1486
+ # Sanity check: we have a base event class.
1487
+ assert base_event_cls is not None
1488
+ # Sanity check: the base created event class is a class.
1489
+ assert isinstance(
1490
+ base_created_event_cls, type
1491
+ ), base_created_event_cls
1492
+ # Sanity check: base created event not subclass of base event class.
1493
+ assert not issubclass(
1494
+ base_created_event_cls, base_event_cls
1495
+ ), base_created_event_cls
1496
+
1497
+ # Define "created" event class.
1498
+ assert created_event_name
1499
+ assert issubclass(base_created_event_cls, CanInitAggregate)
1500
+ created_event_class_bases = (base_created_event_cls, base_event_cls)
1501
+ created_event_class = cast(
1502
+ type[CanInitAggregate[TAggregateID]],
1503
+ cls._define_event_class(
1504
+ created_event_name,
1505
+ created_event_class_bases,
1506
+ init_method,
1507
+ event_topic=created_event_topic,
1508
+ ),
1509
+ )
1510
+ # Set the event class as an attribute of the aggregate class.
1511
+ setattr(cls, created_event_name, created_event_class)
1512
+
1513
+ elif created_event_name:
1514
+ msg = (
1515
+ 'Can\'t defined "created" event class '
1516
+ f"for name '{created_event_name}'"
1517
+ )
1518
+ raise TypeError(msg)
1366
1519
 
1367
1520
  if created_event_class:
1368
1521
  _created_event_classes[cls] = [created_event_class]
1369
1522
  else:
1370
- # Prepare to disallow ambiguity of choice between created event classes.
1523
+ # Prepare to disallow any ambiguity of choice between created event classes.
1371
1524
  _created_event_classes[cls] = list(created_event_classes.values())
1372
1525
 
1373
1526
  # Find and analyse any @event decorators.
@@ -1424,9 +1577,11 @@ class BaseAggregate(metaclass=MetaAggregate):
1424
1577
 
1425
1578
  # Define event class as subclass of given class.
1426
1579
  given_subclass = cast(
1427
- "type[CanMutateAggregate]",
1580
+ type[CanMutateAggregate[TAggregateID]],
1428
1581
  getattr(cls, event_decorator.given_event_cls.__name__),
1429
1582
  )
1583
+ # TODO: Check if this subclassing means we can avoid some of
1584
+ # the subclassing of events above? Maybe do this first?
1430
1585
  event_cls = cls._define_event_class(
1431
1586
  event_decorator.given_event_cls.__name__,
1432
1587
  (DecoratorEvent, given_subclass),
@@ -1443,11 +1598,16 @@ class BaseAggregate(metaclass=MetaAggregate):
1443
1598
  )
1444
1599
  raise TypeError(msg)
1445
1600
 
1601
+ # Check we have a base event class.
1602
+ if base_event_cls is None:
1603
+ raise base_event_class_not_defined_error
1604
+
1446
1605
  # Define event class from signature of original method.
1447
1606
  event_cls = cls._define_event_class(
1448
1607
  event_decorator.event_cls_name,
1449
1608
  (DecoratorEvent, base_event_cls),
1450
1609
  event_decorator.decorated_func,
1610
+ event_topic=event_decorator.event_topic,
1451
1611
  )
1452
1612
 
1453
1613
  # Cache the decorated method for the event class to use.
@@ -1481,78 +1641,70 @@ class BaseAggregate(metaclass=MetaAggregate):
1481
1641
  for name, value in aggregate_base_class.__dict__.items():
1482
1642
  if (
1483
1643
  isinstance(value, type)
1484
- and issubclass(value, AggregateEvent)
1644
+ and issubclass(value, CanMutateAggregate)
1485
1645
  and name not in cls.__dict__
1486
1646
  and name.lower() != name
1487
1647
  ):
1648
+ # Sanity check: we have a base event class.
1649
+ assert base_event_cls is not None
1488
1650
  event_class = cls._define_event_class(
1489
1651
  name, (base_event_cls, value), None
1490
1652
  )
1491
1653
  setattr(cls, name, event_class)
1492
1654
 
1655
+ if getattr(cls, "TOPIC", None):
1493
1656
 
1494
- class Aggregate(BaseAggregate):
1495
- class Event(AggregateEvent):
1496
- pass
1497
-
1498
- class Created(Event, AggregateCreated):
1499
- pass
1500
-
1501
-
1502
- @overload
1503
- def aggregate(*, created_event_name: str) -> Callable[[Any], type[Aggregate]]:
1504
- pass # pragma: no cover
1505
-
1506
-
1507
- @overload
1508
- def aggregate(cls: Any) -> type[Aggregate]:
1509
- pass # pragma: no cover
1510
-
1511
-
1512
- def aggregate(
1513
- cls: Any | None = None,
1514
- *,
1515
- created_event_name: str = "",
1516
- ) -> type[Aggregate] | Callable[[Any], type[Aggregate]]:
1517
- """Converts the class that was passed in to inherit from Aggregate.
1518
-
1519
- .. code-block:: python
1520
-
1521
- @aggregate
1522
- class MyAggregate:
1523
- pass
1657
+ explicit_topic = cls.__dict__.get("TOPIC", None)
1524
1658
 
1525
- ...is equivalent to...
1659
+ if not explicit_topic:
1660
+ msg = f"Explicit topic not defined on {cls}"
1661
+ raise ProgrammingError(msg)
1526
1662
 
1527
- .. code-block:: python
1528
-
1529
- class MyAggregate(Aggregate):
1530
- pass
1531
- """
1663
+ try:
1664
+ register_topic(explicit_topic, cls)
1665
+ except TopicError:
1666
+ msg = (
1667
+ f"Explicit topic '{explicit_topic}' of {cls} "
1668
+ f"already registered for {resolve_topic(explicit_topic)}"
1669
+ )
1670
+ raise ProgrammingError(msg) from None
1532
1671
 
1533
- def decorator(cls_: Any) -> type[Aggregate]:
1534
- if issubclass(cls_, Aggregate):
1535
- msg = f"{cls_.__qualname__} is already an Aggregate"
1536
- raise TypeError(msg)
1537
- bases = cls_.__bases__
1538
- if bases == (object,):
1539
- bases = (Aggregate,)
1540
- else:
1541
- bases += (Aggregate,)
1542
- cls_dict = {}
1543
- cls_dict.update(cls_.__dict__)
1544
- cls_ = MetaAggregate(
1545
- cls_.__qualname__,
1546
- bases,
1547
- cls_dict,
1548
- created_event_name=created_event_name,
1672
+ for name, obj in cls.__dict__.items():
1673
+ if (
1674
+ isinstance(obj, type)
1675
+ and issubclass(obj, CanMutateAggregate)
1676
+ and name != "Event"
1677
+ ):
1678
+ explicit_topic = getattr(obj, "TOPIC", None)
1679
+ if not explicit_topic:
1680
+ msg = f"Explicit topic not defined on {obj}"
1681
+ raise ProgrammingError(msg)
1682
+ try:
1683
+ register_topic(explicit_topic, obj)
1684
+ except TopicError:
1685
+ msg = (
1686
+ f"Explicit topic '{explicit_topic}' of {obj} "
1687
+ f"already registered for {resolve_topic(explicit_topic)}"
1688
+ )
1689
+ raise ProgrammingError(msg) from None
1690
+
1691
+
1692
+ def _check_explicit_topic_is_registered(event_class: type[object]) -> None:
1693
+ explicit_topic = getattr(event_class, "TOPIC", None)
1694
+ if not explicit_topic:
1695
+ msg = f"Explicit topic not defined on {event_class}"
1696
+ raise ProgrammingError(msg)
1697
+ try:
1698
+ resolved_obj = resolve_topic(explicit_topic)
1699
+ except TopicError:
1700
+ msg = f"Explicit topic '{explicit_topic}' on {event_class} is not registered"
1701
+ raise ProgrammingError(msg) from None
1702
+ if resolved_obj is not event_class:
1703
+ msg = (
1704
+ f"Explicit topic '{explicit_topic}' on {event_class} "
1705
+ f"already registered for {resolved_obj}"
1549
1706
  )
1550
- assert issubclass(cls_, Aggregate)
1551
- return cls_
1552
-
1553
- if cls:
1554
- return decorator(cls)
1555
- return decorator
1707
+ raise ProgrammingError(msg) from None
1556
1708
 
1557
1709
 
1558
1710
  class OriginatorIDError(EventSourcingError):
@@ -1571,9 +1723,9 @@ class OriginatorVersionError(EventSourcingError):
1571
1723
  """
1572
1724
 
1573
1725
 
1574
- class SnapshotProtocol(DomainEventProtocol, Protocol):
1726
+ class SnapshotProtocol(DomainEventProtocol[TAggregateID_co], Protocol):
1575
1727
  @property
1576
- def state(self) -> dict[str, Any]:
1728
+ def state(self) -> Any:
1577
1729
  """Snapshots have a read-only 'state'."""
1578
1730
  raise NotImplementedError # pragma: no cover
1579
1731
 
@@ -1583,27 +1735,32 @@ class SnapshotProtocol(DomainEventProtocol, Protocol):
1583
1735
  """Snapshots have a 'take()' class method."""
1584
1736
 
1585
1737
 
1586
- TCanSnapshotAggregate = TypeVar("TCanSnapshotAggregate", bound="CanSnapshotAggregate")
1738
+ TCanSnapshotAggregate = TypeVar(
1739
+ "TCanSnapshotAggregate", bound="CanSnapshotAggregate[Any]"
1740
+ )
1587
1741
 
1588
1742
 
1589
- class CanSnapshotAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
1743
+ class CanSnapshotAggregate(HasOriginatorIDVersion[TAggregateID_co], CanCreateTimestamp):
1590
1744
  topic: str
1591
1745
  state: Any
1592
1746
 
1593
- def __init__(
1594
- self,
1595
- originator_id: UUID,
1596
- originator_version: int,
1597
- timestamp: datetime,
1598
- topic: str,
1599
- state: Any,
1600
- ) -> None:
1601
- raise NotImplementedError # pragma: no cover
1747
+ def __init_subclass__(cls) -> None:
1748
+ cls.find_originator_id_type(CanSnapshotAggregate)
1749
+
1750
+ # def __init__(
1751
+ # self,
1752
+ # originator_id: UUID,
1753
+ # originator_version: int,
1754
+ # timestamp: datetime,
1755
+ # topic: str,
1756
+ # state: Any,
1757
+ # ) -> None:
1758
+ # raise NotImplementedError # pragma: no cover
1602
1759
 
1603
1760
  @classmethod
1604
1761
  def take(
1605
1762
  cls,
1606
- aggregate: MutableOrImmutableAggregate,
1763
+ aggregate: MutableOrImmutableAggregate[TAggregateID_co],
1607
1764
  ) -> Self:
1608
1765
  """Creates a snapshot of the given :class:`Aggregate` object."""
1609
1766
  aggregate_state = dict(aggregate.__dict__)
@@ -1615,16 +1772,16 @@ class CanSnapshotAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
1615
1772
  aggregate_state.pop("_version")
1616
1773
  aggregate_state.pop("_pending_events")
1617
1774
  return cls(
1618
- originator_id=aggregate.id,
1619
- originator_version=aggregate.version,
1620
- timestamp=cls.create_timestamp(),
1621
- topic=get_topic(type(aggregate)),
1622
- state=aggregate_state,
1775
+ originator_id=aggregate.id, # type: ignore[call-arg]
1776
+ originator_version=aggregate.version, # pyright: ignore[reportCallIssue]
1777
+ timestamp=cls.create_timestamp(), # pyright: ignore[reportCallIssue]
1778
+ topic=get_topic(type(aggregate)), # pyright: ignore[reportCallIssue]
1779
+ state=aggregate_state, # pyright: ignore[reportCallIssue]
1623
1780
  )
1624
1781
 
1625
- def mutate(self, _: None) -> Aggregate:
1782
+ def mutate(self, _: None) -> BaseAggregate[TAggregateID_co]:
1626
1783
  """Reconstructs the snapshotted :class:`Aggregate` object."""
1627
- cls = cast("type[Aggregate]", resolve_topic(self.topic))
1784
+ cls = cast(type[BaseAggregate[TAggregateID_co]], resolve_topic(self.topic))
1628
1785
  aggregate_state = dict(self.state)
1629
1786
  from_version = aggregate_state.pop("class_version", 1)
1630
1787
  class_version = getattr(cls, "class_version", 1)
@@ -1643,7 +1800,7 @@ class CanSnapshotAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
1643
1800
 
1644
1801
 
1645
1802
  @dataclass(frozen=True)
1646
- class Snapshot(CanSnapshotAggregate, DomainEvent):
1803
+ class Snapshot(CanSnapshotAggregate[UUID], DomainEvent):
1647
1804
  """Snapshots represent the state of an aggregate at a particular
1648
1805
  version.
1649
1806
 
@@ -1653,8 +1810,79 @@ class Snapshot(CanSnapshotAggregate, DomainEvent):
1653
1810
  :param int originator_version: version of originating aggregate.
1654
1811
  :param datetime timestamp: date-time of the event
1655
1812
  :param str topic: string that includes a class and its module
1656
- :param dict state: version of originating aggregate.
1813
+ :param dict state: state of originating aggregate.
1657
1814
  """
1658
1815
 
1659
1816
  topic: str
1660
1817
  state: dict[str, Any]
1818
+
1819
+
1820
+ class Aggregate(BaseAggregate[UUID]):
1821
+ @staticmethod
1822
+ def create_id(*_: Any, **__: Any) -> UUID:
1823
+ """Returns a new aggregate ID."""
1824
+ return uuid4()
1825
+
1826
+ class Event(AggregateEvent):
1827
+ pass
1828
+
1829
+ class Created(Event, AggregateCreated):
1830
+ pass
1831
+
1832
+ Snapshot = Snapshot
1833
+
1834
+
1835
+ @overload
1836
+ def aggregate(*, created_event_name: str) -> Callable[[Any], type[Aggregate]]:
1837
+ pass # pragma: no cover
1838
+
1839
+
1840
+ @overload
1841
+ def aggregate(cls: Any) -> type[Aggregate]:
1842
+ pass # pragma: no cover
1843
+
1844
+
1845
+ def aggregate(
1846
+ cls: Any | None = None,
1847
+ *,
1848
+ created_event_name: str = "",
1849
+ ) -> type[Aggregate] | Callable[[Any], type[Aggregate]]:
1850
+ """Converts the class that was passed in to inherit from Aggregate.
1851
+
1852
+ .. code-block:: python
1853
+
1854
+ @aggregate
1855
+ class MyAggregate:
1856
+ pass
1857
+
1858
+ ...is equivalent to...
1859
+
1860
+ .. code-block:: python
1861
+
1862
+ class MyAggregate(Aggregate):
1863
+ pass
1864
+ """
1865
+
1866
+ def decorator(cls_: Any) -> type[Aggregate]:
1867
+ if issubclass(cls_, Aggregate):
1868
+ msg = f"{cls_.__qualname__} is already an Aggregate"
1869
+ raise TypeError(msg)
1870
+ bases = cls_.__bases__
1871
+ if bases == (object,):
1872
+ bases = (Aggregate,)
1873
+ else:
1874
+ bases += (Aggregate,)
1875
+ cls_dict = {}
1876
+ cls_dict.update(cls_.__dict__)
1877
+ cls_ = MetaAggregate(
1878
+ cls_.__qualname__,
1879
+ bases,
1880
+ cls_dict,
1881
+ created_event_name=created_event_name,
1882
+ )
1883
+ assert issubclass(cls_, Aggregate)
1884
+ return cls_
1885
+
1886
+ if cls:
1887
+ return decorator(cls)
1888
+ return decorator