eventsourcing 9.4.4__py3-none-any.whl → 9.4.6__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,28 @@ from typing import (
13
15
  TYPE_CHECKING,
14
16
  Any,
15
17
  Callable,
18
+ ClassVar,
16
19
  Generic,
20
+ Optional,
17
21
  Protocol,
18
22
  TypeVar,
19
23
  Union,
20
24
  cast,
25
+ get_args,
26
+ get_origin,
21
27
  overload,
22
28
  runtime_checkable,
23
29
  )
24
30
  from uuid import UUID, uuid4
25
31
  from warnings import warn
26
32
 
27
- from eventsourcing.utils import get_method_name, get_topic, resolve_topic
33
+ from eventsourcing.utils import (
34
+ TopicError,
35
+ get_method_name,
36
+ get_topic,
37
+ register_topic,
38
+ resolve_topic,
39
+ )
28
40
 
29
41
  if TYPE_CHECKING:
30
42
  from collections.abc import Iterable, Sequence
@@ -74,8 +86,12 @@ def patch_dataclasses_process_class() -> None:
74
86
  patch_dataclasses_process_class()
75
87
 
76
88
 
89
+ TAggregateID = TypeVar("TAggregateID", bound=Union[UUID, str])
90
+ TAggregateID_co = TypeVar("TAggregateID_co", bound=Union[UUID, str], covariant=True)
91
+
92
+
77
93
  @runtime_checkable
78
- class DomainEventProtocol(Protocol):
94
+ class DomainEventProtocol(Protocol[TAggregateID_co]):
79
95
  """Protocol for domain event objects.
80
96
 
81
97
  A protocol is defined to allow the event sourcing mechanisms
@@ -89,7 +105,7 @@ class DomainEventProtocol(Protocol):
89
105
  pass # pragma: no cover
90
106
 
91
107
  @property
92
- def originator_id(self) -> UUID:
108
+ def originator_id(self) -> TAggregateID_co:
93
109
  """UUID identifying an aggregate to which the event belongs."""
94
110
  raise NotImplementedError # pragma: no cover
95
111
 
@@ -99,11 +115,11 @@ class DomainEventProtocol(Protocol):
99
115
  raise NotImplementedError # pragma: no cover
100
116
 
101
117
 
102
- TDomainEvent = TypeVar("TDomainEvent", bound=DomainEventProtocol)
103
- SDomainEvent = TypeVar("SDomainEvent", bound=DomainEventProtocol)
118
+ TDomainEvent = TypeVar("TDomainEvent", bound=DomainEventProtocol[Any])
119
+ SDomainEvent = TypeVar("SDomainEvent", bound=DomainEventProtocol[Any])
104
120
 
105
121
 
106
- class MutableAggregateProtocol(Protocol):
122
+ class MutableAggregateProtocol(Protocol[TAggregateID_co]):
107
123
  """Protocol for mutable aggregate objects.
108
124
 
109
125
  A protocol is defined to allow the event sourcing mechanisms
@@ -114,7 +130,7 @@ class MutableAggregateProtocol(Protocol):
114
130
  """
115
131
 
116
132
  @property
117
- def id(self) -> UUID:
133
+ def id(self) -> TAggregateID_co:
118
134
  """Mutable aggregates have a read-only ID that is a UUID."""
119
135
  raise NotImplementedError # pragma: no cover
120
136
 
@@ -129,7 +145,7 @@ class MutableAggregateProtocol(Protocol):
129
145
  raise NotImplementedError # pragma: no cover
130
146
 
131
147
 
132
- class ImmutableAggregateProtocol(Protocol):
148
+ class ImmutableAggregateProtocol(Protocol[TAggregateID_co]):
133
149
  """Protocol for immutable aggregate objects.
134
150
 
135
151
  A protocol is defined to allow the event sourcing mechanisms
@@ -140,7 +156,7 @@ class ImmutableAggregateProtocol(Protocol):
140
156
  """
141
157
 
142
158
  @property
143
- def id(self) -> UUID:
159
+ def id(self) -> TAggregateID_co:
144
160
  """Immutable aggregates have a read-only ID that is a UUID."""
145
161
  raise NotImplementedError # pragma: no cover
146
162
 
@@ -151,13 +167,14 @@ class ImmutableAggregateProtocol(Protocol):
151
167
 
152
168
 
153
169
  MutableOrImmutableAggregate = Union[
154
- ImmutableAggregateProtocol, MutableAggregateProtocol
170
+ ImmutableAggregateProtocol[TAggregateID],
171
+ MutableAggregateProtocol[TAggregateID],
155
172
  ]
156
173
  """Type alias defining a union of mutable and immutable aggregate protocols."""
157
174
 
158
175
 
159
176
  TMutableOrImmutableAggregate = TypeVar(
160
- "TMutableOrImmutableAggregate", bound=MutableOrImmutableAggregate
177
+ "TMutableOrImmutableAggregate", bound=MutableOrImmutableAggregate[Any]
161
178
  )
162
179
  """Type variable bound by the union of mutable and immutable aggregate protocols."""
163
180
 
@@ -166,13 +183,15 @@ TMutableOrImmutableAggregate = TypeVar(
166
183
  class CollectEventsProtocol(Protocol):
167
184
  """Protocol for aggregates that support collecting pending events."""
168
185
 
169
- def collect_events(self) -> Sequence[DomainEventProtocol]:
186
+ def collect_events(self) -> Sequence[DomainEventProtocol[Any]]:
170
187
  """Returns a sequence of events."""
171
188
  raise NotImplementedError # pragma: no cover
172
189
 
173
190
 
174
191
  @runtime_checkable
175
- class CanMutateProtocol(DomainEventProtocol, Protocol[TMutableOrImmutableAggregate]):
192
+ class CanMutateProtocol(
193
+ DomainEventProtocol[Any], Protocol[TMutableOrImmutableAggregate]
194
+ ):
176
195
  """Protocol for events that have a mutate method."""
177
196
 
178
197
  def mutate(
@@ -216,20 +235,45 @@ class CanCreateTimestamp:
216
235
  return datetime_now_with_tzinfo()
217
236
 
218
237
 
219
- TAggregate = TypeVar("TAggregate", bound="BaseAggregate")
238
+ TAggregate = TypeVar("TAggregate", bound="BaseAggregate[Any]")
220
239
 
221
240
 
222
- class HasOriginatorIDVersion:
241
+ class HasOriginatorIDVersion(Generic[TAggregateID]):
223
242
  """Declares ``originator_id`` and ``originator_version`` attributes."""
224
243
 
225
- originator_id: UUID
244
+ originator_id: TAggregateID
226
245
  """UUID identifying an aggregate to which the event belongs."""
227
246
  originator_version: int
228
247
  """Integer identifying the version of the aggregate when the event occurred."""
229
248
 
249
+ originator_id_type: ClassVar[Optional[type[Union[UUID, str]]]] = None # noqa: UP007
230
250
 
231
- class CanMutateAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
232
- """Implements a :func:`~eventsourcing.domain.CanMutateAggregate.mutate`
251
+ def __init_subclass__(cls) -> None:
252
+ cls.find_originator_id_type(HasOriginatorIDVersion)
253
+ super().__init_subclass__()
254
+
255
+ @classmethod
256
+ def find_originator_id_type(cls: type, generic_cls: type) -> None:
257
+ """Store the type argument of TAggregateID on the subclass."""
258
+ if "originator_id_type" not in cls.__dict__:
259
+ for orig_base in cls.__orig_bases__: # type: ignore[attr-defined]
260
+ if "originator_id_type" in orig_base.__dict__:
261
+ cls.originator_id_type = orig_base.__dict__["originator_id_type"] # type: ignore[attr-defined]
262
+ elif get_origin(orig_base) is generic_cls:
263
+ originator_id_type = get_args(orig_base)[0]
264
+ if originator_id_type in (UUID, str):
265
+ cls.originator_id_type = originator_id_type # type: ignore[attr-defined]
266
+ break
267
+ if originator_id_type is Any:
268
+ continue
269
+ if isinstance(originator_id_type, TypeVar):
270
+ continue
271
+ msg = f"Aggregate ID type arg cannot be {originator_id_type}"
272
+ raise TypeError(msg)
273
+
274
+
275
+ class CanMutateAggregate(HasOriginatorIDVersion[TAggregateID], CanCreateTimestamp):
276
+ """Implements a :py:func:`~eventsourcing.domain.CanMutateAggregate.mutate`
233
277
  method that evolves the state of an aggregate.
234
278
  """
235
279
 
@@ -237,9 +281,13 @@ class CanMutateAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
237
281
  timestamp: datetime
238
282
  """Timezone-aware :class:`datetime` object representing when an event occurred."""
239
283
 
284
+ def __init_subclass__(cls) -> None:
285
+ cls.find_originator_id_type(CanMutateAggregate)
286
+ super().__init_subclass__()
287
+
240
288
  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
289
+ """Validates and adjusts the attributes of the given ``aggregate``
290
+ argument. The argument is typed as ``Optional``, but the value is
243
291
  expected to be not ``None``.
244
292
 
245
293
  Validates the ``aggregate`` argument by checking the event's
@@ -289,8 +337,11 @@ class CanMutateAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
289
337
  of an aggregate.
290
338
  """
291
339
 
340
+ def _as_dict(self) -> dict[str, Any]:
341
+ return self.__dict__
292
342
 
293
- class CanInitAggregate(CanMutateAggregate):
343
+
344
+ class CanInitAggregate(CanMutateAggregate[TAggregateID]):
294
345
  """Implements a :func:`~eventsourcing.domain.CanMutateAggregate.mutate`
295
346
  method that constructs the initial state of an aggregate.
296
347
  """
@@ -298,6 +349,10 @@ class CanInitAggregate(CanMutateAggregate):
298
349
  originator_topic: str
299
350
  """String describing the path to an aggregate class."""
300
351
 
352
+ def __init_subclass__(cls) -> None:
353
+ cls.find_originator_id_type(CanInitAggregate)
354
+ super().__init_subclass__()
355
+
301
356
  def mutate(self, aggregate: TAggregate | None) -> TAggregate | None:
302
357
  """Constructs an aggregate instance according to the attributes of an event.
303
358
 
@@ -313,8 +368,9 @@ class CanInitAggregate(CanMutateAggregate):
313
368
  agg = aggregate_class.__new__(aggregate_class)
314
369
 
315
370
  # Pick out event attributes for the aggregate base class init method.
371
+ self_dict = self._as_dict()
316
372
  base_kwargs = _filter_kwargs_for_method_params(
317
- self.__dict__, type(agg).__base_init__
373
+ self_dict, type(agg).__base_init__
318
374
  )
319
375
 
320
376
  # Call the base class init method (so we don't need to always write
@@ -322,13 +378,11 @@ class CanInitAggregate(CanMutateAggregate):
322
378
  agg.__base_init__(**base_kwargs)
323
379
 
324
380
  # 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
- )
381
+ init_kwargs = _filter_kwargs_for_method_params(self_dict, type(agg).__init__)
328
382
 
329
383
  # Provide the aggregate id, if the __init__ method expects it.
330
384
  if aggregate_class in _init_mentions_id:
331
- init_kwargs["id"] = self.__dict__["originator_id"]
385
+ init_kwargs["id"] = self_dict["originator_id"]
332
386
 
333
387
  # Call the aggregate subclass class init method.
334
388
  agg.__init__(**init_kwargs) # type: ignore[misc]
@@ -365,8 +419,17 @@ class DomainEvent(CanCreateTimestamp, metaclass=MetaDomainEvent):
365
419
  timestamp: datetime
366
420
  """Timezone-aware :class:`datetime` object representing when an event occurred."""
367
421
 
422
+ def __post_init__(self) -> None:
423
+ if not isinstance(self.originator_id, UUID):
424
+ msg = (
425
+ f"{type(self)} "
426
+ f"was initialized with a non-UUID originator_id: "
427
+ f"{self.originator_id!r}"
428
+ )
429
+ raise TypeError(msg)
430
+
368
431
 
369
- class AggregateEvent(CanMutateAggregate, DomainEvent):
432
+ class AggregateEvent(CanMutateAggregate[UUID], DomainEvent):
370
433
  """Frozen data class representing aggregate events.
371
434
 
372
435
  Subclasses represent original decisions made by domain model aggregates.
@@ -374,7 +437,7 @@ class AggregateEvent(CanMutateAggregate, DomainEvent):
374
437
 
375
438
 
376
439
  @dataclass(frozen=True)
377
- class AggregateCreated(CanInitAggregate, AggregateEvent):
440
+ class AggregateCreated(CanInitAggregate[UUID], AggregateEvent):
378
441
  """Frozen data class representing the initial creation of an aggregate."""
379
442
 
380
443
  originator_topic: str
@@ -410,7 +473,7 @@ def _spec_filter_kwargs_for_method_params(method: Callable[..., Any]) -> set[str
410
473
 
411
474
 
412
475
  if TYPE_CHECKING:
413
- EventSpecType = Union[str, type[CanMutateAggregate]]
476
+ EventSpecType = Union[str, type[CanMutateAggregate[Any]]]
414
477
 
415
478
  CallableType = Callable[..., None]
416
479
  DecoratableType = Union[CallableType, property]
@@ -422,14 +485,16 @@ class CommandMethodDecorator:
422
485
  self,
423
486
  event_spec: EventSpecType | None,
424
487
  decorated_obj: DecoratableType,
488
+ event_topic: str | None = None,
425
489
  ):
426
490
  self.is_name_inferred_from_method = False
427
- self.given_event_cls: type[CanMutateAggregate] | None = None
491
+ self.given_event_cls: type[CanMutateAggregate[Any]] | None = None
428
492
  self.event_cls_name: str | None = None
429
493
  self.decorated_property: property | None = None
430
494
  self.is_property_setter = False
431
495
  self.property_setter_arg_name: str | None = None
432
496
  self.decorated_func: CallableType
497
+ self.event_topic = event_topic
433
498
 
434
499
  # Event name has been specified.
435
500
  if isinstance(event_spec, str):
@@ -525,7 +590,7 @@ class CommandMethodDecorator:
525
590
 
526
591
  @overload
527
592
  def __get__(
528
- self, instance: None, owner: type[BaseAggregate]
593
+ self, instance: None, owner: type[BaseAggregate[Any]]
529
594
  ) -> UnboundCommandMethodDecorator | property:
530
595
  """
531
596
  Descriptor protocol for getting decorated method or property on class object.
@@ -533,14 +598,14 @@ class CommandMethodDecorator:
533
598
 
534
599
  @overload
535
600
  def __get__(
536
- self, instance: BaseAggregate, owner: type[BaseAggregate]
601
+ self, instance: BaseAggregate[Any], owner: type[BaseAggregate[Any]]
537
602
  ) -> BoundCommandMethodDecorator | Any:
538
603
  """
539
604
  Descriptor protocol for getting decorated method or property on instance object.
540
605
  """
541
606
 
542
607
  def __get__(
543
- self, instance: BaseAggregate | None, owner: type[BaseAggregate]
608
+ self, instance: BaseAggregate[Any] | None, owner: type[BaseAggregate[Any]]
544
609
  ) -> BoundCommandMethodDecorator | UnboundCommandMethodDecorator | property | Any:
545
610
  """Descriptor protocol for getting decorated method or property."""
546
611
  # If we are decorating a property, then delegate to the property's __get__.
@@ -558,7 +623,7 @@ class CommandMethodDecorator:
558
623
  # Return an "unbound" command method decorator if we have no instance.
559
624
  return UnboundCommandMethodDecorator(self)
560
625
 
561
- def __set__(self, instance: BaseAggregate, value: Any) -> None:
626
+ def __set__(self, instance: BaseAggregate[Any], value: Any) -> None:
562
627
  """Descriptor protocol for assigning to decorated property."""
563
628
  # Set decorated property indirectly by triggering an event.
564
629
  assert self.property_setter_arg_name
@@ -568,26 +633,31 @@ class CommandMethodDecorator:
568
633
 
569
634
 
570
635
  @overload
571
- def event(arg: TDecoratableType) -> TDecoratableType:
636
+ def event(arg: TDecoratableType, /) -> TDecoratableType:
572
637
  """Signature for calling ``@event`` decorator with decorated method."""
573
638
 
574
639
 
575
640
  @overload
576
641
  def event(
577
- arg: EventSpecType,
642
+ arg: type[CanMutateAggregate[Any]], /
578
643
  ) -> Callable[[TDecoratableType], TDecoratableType]:
579
- """Signature for calling ``@event`` decorator with event specification."""
644
+ """Signature for calling ``@event`` decorator with event class."""
580
645
 
581
646
 
582
647
  @overload
583
648
  def event(
584
- arg: None = None,
649
+ arg: str, /, *, topic: str | None = None
585
650
  ) -> Callable[[TDecoratableType], TDecoratableType]:
651
+ """Signature for calling ``@event`` decorator with event name."""
652
+
653
+
654
+ @overload
655
+ def event(arg: None = None, /) -> Callable[[TDecoratableType], TDecoratableType]:
586
656
  """Signature for calling ``@event`` decorator without event specification."""
587
657
 
588
658
 
589
659
  def event(
590
- arg: EventSpecType | TDecoratableType | None = None,
660
+ arg: EventSpecType | TDecoratableType | None = None, /, *, topic: str | None = None
591
661
  ) -> TDecoratableType | Callable[[TDecoratableType], TDecoratableType]:
592
662
  """Event-triggering decorator for aggregate command methods and property setters.
593
663
 
@@ -660,6 +730,7 @@ def event(
660
730
  command_method_decorator = CommandMethodDecorator(
661
731
  event_spec=event_spec,
662
732
  decorated_obj=decorated_obj,
733
+ event_topic=topic,
663
734
  )
664
735
  return cast("TDecoratableType", command_method_decorator)
665
736
 
@@ -690,6 +761,7 @@ class UnboundCommandMethodDecorator:
690
761
  # functools.update_wrapper(self, event_decorator.decorated_method)
691
762
 
692
763
  def __call__(self, *args: Any, **kwargs: Any) -> None:
764
+ # TODO: Review this, because other subclasses of BaseAggregate might too....
693
765
  # Expect first argument is an aggregate instance.
694
766
  if len(args) < 1 or not isinstance(args[0], Aggregate):
695
767
  msg = "Expected aggregate as first argument"
@@ -707,7 +779,7 @@ class BoundCommandMethodDecorator:
707
779
  """
708
780
 
709
781
  def __init__(
710
- self, event_decorator: CommandMethodDecorator, aggregate: BaseAggregate
782
+ self, event_decorator: CommandMethodDecorator, aggregate: BaseAggregate[Any]
711
783
  ):
712
784
  """:param CommandMethodDecorator event_decorator:
713
785
  :param Aggregate aggregate:
@@ -732,14 +804,15 @@ class BoundCommandMethodDecorator:
732
804
  self.trigger(*args, **kwargs)
733
805
 
734
806
 
735
- class DecoratorEvent(CanMutateAggregate):
736
- def apply(self, aggregate: BaseAggregate) -> None:
807
+ class DecoratorEvent(CanMutateAggregate[Any]):
808
+ def apply(self, aggregate: BaseAggregate[Any]) -> None:
737
809
  """Applies event to aggregate by calling method decorated by @event."""
738
810
  # Identify the function that was decorated.
739
811
  decorated_func = _decorated_funcs[type(self)]
740
812
 
741
813
  # Select event attributes mentioned in function signature.
742
- kwargs = _filter_kwargs_for_method_params(self.__dict__, decorated_func)
814
+ self_dict = self._as_dict()
815
+ kwargs = _filter_kwargs_for_method_params(self_dict, decorated_func)
743
816
 
744
817
  # Call the original method with event attribute values.
745
818
  decorated_method = decorated_func.__get__(aggregate, type(aggregate))
@@ -751,7 +824,7 @@ class DecoratorEvent(CanMutateAggregate):
751
824
 
752
825
  _given_event_classes: set[type] = set()
753
826
  _decorated_funcs: dict[type, CallableType] = {}
754
- _created_event_classes: dict[type, list[type[CanInitAggregate]]] = {}
827
+ _created_event_classes: dict[type, list[type[CanInitAggregate[Any]]]] = {}
755
828
 
756
829
 
757
830
  decorator_event_classes: dict[CommandMethodDecorator, type[DecoratorEvent]] = {}
@@ -906,20 +979,23 @@ def _raise_missing_names_type_error(missing_names: list[str], msg: str) -> None:
906
979
  raise TypeError(msg)
907
980
 
908
981
 
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)
982
+ _annotations_mention_id: set[type[BaseAggregate[Any]]] = set()
983
+ _init_mentions_id: set[type[BaseAggregate[Any]]] = set()
984
+ _create_id_param_names: dict[type[BaseAggregate[Any]], list[str]] = defaultdict(list)
985
+
986
+ ENVVAR_DISABLE_REDEFINITION_CHECK = "EVENTSOURCING_DISABLE_REDEFINITION_CHECK"
912
987
 
913
988
 
914
- class MetaAggregate(EventsourcingType, Generic[TAggregate], type):
989
+ class MetaAggregate(EventsourcingType, Generic[TAggregate], ABCMeta):
915
990
  """Metaclass for aggregate classes."""
916
991
 
917
992
  def _define_event_class(
918
993
  cls,
919
994
  name: str,
920
- bases: tuple[type[CanMutateAggregate], ...],
995
+ bases: tuple[type[CanMutateAggregate[Any]], ...],
921
996
  apply_method: CallableType | None,
922
- ) -> type[CanMutateAggregate]:
997
+ event_topic: str | None = None,
998
+ ) -> type[CanMutateAggregate[Any]]:
923
999
  # Define annotations for the event class (specs the init method).
924
1000
  annotations = {}
925
1001
  if apply_method is not None:
@@ -941,22 +1017,28 @@ class MetaAggregate(EventsourcingType, Generic[TAggregate], type):
941
1017
  "__module__": cls.__module__,
942
1018
  "__qualname__": event_cls_qualname,
943
1019
  }
1020
+ if event_topic:
1021
+ event_cls_dict["TOPIC"] = event_topic
944
1022
 
945
1023
  # Create the event class object.
946
1024
  _new_class = type(name, bases, event_cls_dict)
947
- return cast("type[CanMutateAggregate]", _new_class)
1025
+ return cast("type[CanMutateAggregate[Any]]", _new_class)
948
1026
 
949
1027
  def __call__(
950
1028
  cls: MetaAggregate[TAggregate], *args: Any, **kwargs: Any
951
1029
  ) -> TAggregate:
952
1030
  if cls is BaseAggregate:
953
- msg = "BaseAggregate class cannot be instantiated directly"
1031
+ msg = "Please define or use subclasses of BaseAggregate."
954
1032
  raise TypeError(msg)
955
1033
  created_event_classes = _created_event_classes[cls]
956
1034
  # Here, unlike when calling _create(), we don't have a given event class,
957
1035
  # so we need to check that there is one "created" event class to use here.
958
1036
  # We don't check this in __init_subclass__ to allow for alternatives that
959
1037
  # can be selected by developers by calling _create(event_class=...).
1038
+ if len(created_event_classes) == 0:
1039
+ msg = f"No \"created\" event classes defined on class '{cls.__name__}'."
1040
+ raise TypeError(msg)
1041
+
960
1042
  if len(created_event_classes) > 1:
961
1043
  msg = (
962
1044
  f"{cls.__qualname__} can't decide which of many "
@@ -980,32 +1062,35 @@ class MetaAggregate(EventsourcingType, Generic[TAggregate], type):
980
1062
 
981
1063
  def _create(
982
1064
  cls: MetaAggregate[TAggregate],
983
- event_class: type[CanInitAggregate],
1065
+ event_class: type[CanInitAggregate[Any]],
984
1066
  **kwargs: Any,
985
1067
  ) -> TAggregate:
986
1068
  # Just define method signature for the __call__() method.
987
1069
  raise NotImplementedError # pragma: no cover
988
1070
 
989
1071
 
990
- class BaseAggregate(metaclass=MetaAggregate):
1072
+ class BaseAggregate(Generic[TAggregateID], metaclass=MetaAggregate):
991
1073
  """Base class for aggregates."""
992
1074
 
993
1075
  INITIAL_VERSION: int = 1
994
1076
 
995
1077
  @staticmethod
996
- def create_id(*_: Any, **__: Any) -> UUID:
1078
+ def create_id(*_: Any, **__: Any) -> TAggregateID:
997
1079
  """Returns a new aggregate ID."""
998
- return uuid4()
1080
+ raise NotImplementedError
999
1081
 
1000
1082
  @classmethod
1001
1083
  def _create(
1002
1084
  cls: type[Self],
1003
- event_class: type[CanInitAggregate],
1085
+ event_class: type[CanInitAggregate[TAggregateID]],
1004
1086
  *,
1005
- id: UUID | None = None, # noqa: A002
1087
+ id: TAggregateID | None = None, # noqa: A002
1006
1088
  **kwargs: Any,
1007
1089
  ) -> Self:
1008
1090
  """Constructs a new aggregate object instance."""
1091
+ if getattr(cls, "TOPIC", None):
1092
+ _check_explicit_topic_is_registered(event_class)
1093
+
1009
1094
  # Construct the domain event with an ID and a
1010
1095
  # version, and a topic for the aggregate class.
1011
1096
  create_id_kwargs = {
@@ -1013,15 +1098,20 @@ class BaseAggregate(metaclass=MetaAggregate):
1013
1098
  }
1014
1099
  if id is not None:
1015
1100
  originator_id = id
1016
- if not isinstance(originator_id, UUID):
1017
- msg = f"Given id was not a UUID: {originator_id}"
1101
+ if not isinstance(originator_id, (UUID, str)):
1102
+ msg = f"Given id was not a UUID or str: {originator_id!r}"
1018
1103
  raise TypeError(msg)
1019
1104
  else:
1020
- originator_id = cls.create_id(**create_id_kwargs)
1021
- if not isinstance(originator_id, UUID):
1105
+ try:
1106
+ originator_id = cls.create_id(**create_id_kwargs)
1107
+ except NotImplementedError as e:
1108
+ msg = f"Please pass an 'id' arg or define a create_id() method on {cls}"
1109
+ raise NotImplementedError(msg) from e
1110
+
1111
+ if not isinstance(originator_id, (UUID, str)):
1022
1112
  msg = (
1023
1113
  f"{cls.create_id.__module__}.{cls.create_id.__qualname__}"
1024
- f" did not return UUID, it returned: {originator_id}"
1114
+ f" did not return UUID or str, it returned: {originator_id!r}"
1025
1115
  )
1026
1116
  raise TypeError(msg)
1027
1117
 
@@ -1050,19 +1140,22 @@ class BaseAggregate(metaclass=MetaAggregate):
1050
1140
  return agg
1051
1141
 
1052
1142
  def __base_init__(
1053
- self, originator_id: UUID, originator_version: int, timestamp: datetime
1143
+ self,
1144
+ originator_id: Any,
1145
+ originator_version: int,
1146
+ timestamp: datetime,
1054
1147
  ) -> None:
1055
1148
  """Initialises an aggregate object with an :data:`id`, a :data:`version`
1056
1149
  number, and a :data:`timestamp`.
1057
1150
  """
1058
- self._id = originator_id
1151
+ self._id: TAggregateID = originator_id
1059
1152
  self._version = originator_version
1060
1153
  self._created_on = timestamp
1061
1154
  self._modified_on = timestamp
1062
- self._pending_events: list[CanMutateAggregate] = []
1155
+ self._pending_events: list[CanMutateAggregate[TAggregateID]] = []
1063
1156
 
1064
1157
  @property
1065
- def id(self) -> UUID:
1158
+ def id(self) -> TAggregateID:
1066
1159
  """The ID of the aggregate."""
1067
1160
  return self._id
1068
1161
 
@@ -1090,18 +1183,24 @@ class BaseAggregate(metaclass=MetaAggregate):
1090
1183
  self._modified_on = modified_on
1091
1184
 
1092
1185
  @property
1093
- def pending_events(self) -> list[CanMutateAggregate]:
1186
+ def pending_events(self) -> list[CanMutateAggregate[TAggregateID]]:
1094
1187
  """A list of pending events."""
1095
1188
  return self._pending_events
1096
1189
 
1097
1190
  def trigger_event(
1098
1191
  self,
1099
- event_class: type[CanMutateAggregate],
1192
+ event_class: type[CanMutateAggregate[TAggregateID]],
1100
1193
  **kwargs: Any,
1101
1194
  ) -> None:
1102
1195
  """Triggers domain event of given type, by creating
1103
1196
  an event object and using it to mutate the aggregate.
1104
1197
  """
1198
+ if getattr(type(self), "TOPIC", None):
1199
+ if event_class.__name__ == "Event":
1200
+ msg = "Triggering base 'Event' class is prohibited."
1201
+ raise ProgrammingError(msg)
1202
+ _check_explicit_topic_is_registered(event_class)
1203
+
1105
1204
  # Construct the domain event as the
1106
1205
  # next in the aggregate's sequence.
1107
1206
  # Use counting to generate the sequence.
@@ -1127,7 +1226,7 @@ class BaseAggregate(metaclass=MetaAggregate):
1127
1226
  # Append the domain event to pending list.
1128
1227
  self._pending_events.append(new_event)
1129
1228
 
1130
- def collect_events(self) -> Sequence[CanMutateAggregate]:
1229
+ def collect_events(self) -> Sequence[CanMutateAggregate[TAggregateID]]:
1131
1230
  """Collects and returns a list of pending aggregate
1132
1231
  :class:`AggregateEvent` objects.
1133
1232
  """
@@ -1148,7 +1247,7 @@ class BaseAggregate(metaclass=MetaAggregate):
1148
1247
  return f"{type(self).__name__}({', '.join(attrs)})"
1149
1248
 
1150
1249
  def __init_subclass__(
1151
- cls: type[BaseAggregate], *, created_event_name: str = ""
1250
+ cls: type[BaseAggregate[TAggregateID]], *, created_event_name: str = ""
1152
1251
  ) -> None:
1153
1252
  """
1154
1253
  Initialises aggregate subclass by defining __init__ method and event classes.
@@ -1159,8 +1258,14 @@ class BaseAggregate(metaclass=MetaAggregate):
1159
1258
  # because annotations can get confused when using singledispatchmethod
1160
1259
  # during class definition e.g. on an aggregate projector function.
1161
1260
  _module = importlib.import_module(cls.__module__)
1162
- if cls.__name__ in _module.__dict__:
1163
- msg = f"Name '{cls.__name__}' already defined in '{cls.__module__}' module"
1261
+ if (
1262
+ cls.__name__ in _module.__dict__
1263
+ and ENVVAR_DISABLE_REDEFINITION_CHECK not in os.environ
1264
+ ):
1265
+ msg = (
1266
+ f"Name '{cls.__name__}' of {cls} already defined in "
1267
+ f"'{cls.__module__}' module: {_module.__dict__[cls.__name__]}"
1268
+ )
1164
1269
  raise ProgrammingError(msg)
1165
1270
 
1166
1271
  # Get the class annotations.
@@ -1208,41 +1313,105 @@ class BaseAggregate(metaclass=MetaAggregate):
1208
1313
 
1209
1314
  # Identify or define a base event class for this aggregate.
1210
1315
  base_event_name = "Event"
1211
- base_event_cls: type[CanMutateAggregate]
1316
+ base_event_cls: type[CanMutateAggregate[TAggregateID]] | None = None
1317
+ msg = f"Base event class 'Event' not defined on {cls} or ancestors"
1318
+ base_event_class_not_defined_error = TypeError(msg)
1319
+
1212
1320
  try:
1213
1321
  base_event_cls = cls.__dict__[base_event_name]
1214
1322
  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)
1323
+ try:
1324
+ super_base_event_cls = getattr(cls, base_event_name)
1325
+ except AttributeError:
1326
+ pass
1327
+ else:
1328
+ base_event_cls = cls._define_event_class(
1329
+ name=base_event_name,
1330
+ bases=(super_base_event_cls,),
1331
+ apply_method=None,
1332
+ )
1333
+ setattr(cls, base_event_name, base_event_cls)
1334
+
1335
+ # Remember which events have been redefined, to preserve apparent hierarchy,
1336
+ # in a mapping from the original class to the redefined class.
1337
+ redefined_event_classes: dict[
1338
+ type[CanMutateAggregate[TAggregateID]],
1339
+ type[CanMutateAggregate[TAggregateID]],
1340
+ ] = {}
1341
+
1342
+ # Remember any "created" event classes that are discovered.
1343
+ created_event_classes: dict[str, type[CanInitAggregate[TAggregateID]]] = {}
1344
+
1345
+ # TODO: Review decorator processing below to see if subclassing can be improved.
1346
+ # - basically, look at the decorators first, build a plan for defining events
1221
1347
 
1222
- # Ensure all events defined on this class are subclasses of base event class.
1223
- created_event_classes: dict[str, type[CanInitAggregate]] = {}
1348
+ # Ensure events defined on this class are subclasses of the base event class.
1224
1349
  for name, value in tuple(cls.__dict__.items()):
1350
+ # Don't subclass the base event class again.
1225
1351
  if name == base_event_name:
1226
- # Don't subclass the base event class again.
1227
1352
  continue
1353
+
1354
+ # Don't subclass lowercase named attributes.
1228
1355
  if name.lower() == name:
1229
- # Don't subclass lowercase named attributes.
1230
1356
  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
1357
 
1240
- # Remember all "created" event classes defined on this class.
1241
- if issubclass(event_class, CanInitAggregate):
1242
- created_event_classes[name] = event_class
1358
+ # Don't subclass if not "CanMutateAggregate".
1359
+ if not isinstance(value, type) or not issubclass(value, CanMutateAggregate):
1360
+ continue
1361
+
1362
+ # # Don't subclass generic classes (we don't have a type argument).
1363
+ # # TODO: Maybe also prohibit triggering such things?
1364
+ # if value.__dict__.get("__parameters__", ()):
1365
+ # continue
1366
+
1367
+ # Check we have a base event class.
1368
+ if base_event_cls is None:
1369
+ raise base_event_class_not_defined_error
1370
+
1371
+ # Redefine events that aren't already subclass of the base event class.
1372
+ if not issubclass(value, base_event_cls):
1373
+ # Identify base classes that were redefined, to preserve hierarchy.
1374
+ redefined_bases = []
1375
+ for base in value.__bases__:
1376
+ if base in redefined_event_classes:
1377
+ redefined_bases.append(redefined_event_classes[base])
1378
+ elif "__pydantic_generic_metadata__" in base.__dict__:
1379
+ pydantic_metadata = base.__dict__[
1380
+ "__pydantic_generic_metadata__"
1381
+ ]
1382
+ for i, key in enumerate(pydantic_metadata):
1383
+ if key == "origin":
1384
+ origin = base.__bases__[i]
1385
+ if origin in redefined_event_classes:
1386
+ redefined_bases.append(
1387
+ redefined_event_classes[origin]
1388
+ )
1389
+
1390
+ # Decide base classes of redefined event class: it must be
1391
+ # a subclass of the original class, all redefined classes that
1392
+ # were in its bases, and the aggregate's base event class.
1393
+ event_class_bases = (
1394
+ value,
1395
+ *redefined_bases,
1396
+ base_event_cls,
1397
+ )
1398
+
1399
+ # Define event class.
1400
+ event_class = cls._define_event_class(name, event_class_bases, None)
1401
+ setattr(cls, name, event_class)
1402
+
1403
+ # Remember which events have been redefined.
1404
+ redefined_event_classes[value] = event_class
1405
+ else:
1406
+ event_class = value
1407
+
1408
+ # Remember all "created" event classes defined on this class.
1409
+ if issubclass(event_class, CanInitAggregate):
1410
+ created_event_classes[name] = event_class
1243
1411
 
1244
1412
  # Identify or define the aggregate's "created" event class.
1245
- created_event_class: type[CanInitAggregate] | None = None
1413
+ created_event_class: type[CanInitAggregate[TAggregateID]] | None = None
1414
+ created_event_topic: str | None = None
1246
1415
 
1247
1416
  # Analyse __init__ method decorator.
1248
1417
  if init_decorator:
@@ -1279,6 +1448,7 @@ class BaseAggregate(metaclass=MetaAggregate):
1279
1448
 
1280
1449
  # Does the decorator specify an event name?
1281
1450
  elif init_decorator.event_cls_name:
1451
+ created_event_topic = init_decorator.event_topic
1282
1452
  # Disallow conflicts between 'created_event_name' and given name.
1283
1453
  if (
1284
1454
  created_event_name
@@ -1306,18 +1476,24 @@ class BaseAggregate(metaclass=MetaAggregate):
1306
1476
  elif not created_event_name and len(created_event_classes) == 1:
1307
1477
  created_event_class = next(iter(created_event_classes.values()))
1308
1478
 
1309
- # Otherwise, if there are no "created" events, or a name is
1310
- # specified that hasn't matched, then define a "created" event class.
1479
+ # Otherwise, if there are no "created" event classes, or a name
1480
+ # is specified that hasn't matched, then try to define one.
1311
1481
  elif len(created_event_classes) == 0 or created_event_name:
1312
1482
  # Decide the base "created" event class.
1313
1483
 
1314
- try:
1484
+ base_created_event_cls: type[CanInitAggregate[TAggregateID]] | None = (
1485
+ None
1486
+ )
1487
+
1488
+ if created_event_name:
1315
1489
  # 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:
1490
+ with contextlib.suppress(AttributeError):
1491
+ base_created_event_cls = cast(
1492
+ type[CanInitAggregate[TAggregateID]],
1493
+ getattr(cls, created_event_name),
1494
+ )
1495
+
1496
+ if base_created_event_cls is None:
1321
1497
  # Look for base class with one nominated "created" event.
1322
1498
  for base_cls in cls.__mro__:
1323
1499
  if (
@@ -1326,48 +1502,54 @@ class BaseAggregate(metaclass=MetaAggregate):
1326
1502
  ):
1327
1503
  base_created_event_cls = _created_event_classes[base_cls][0]
1328
1504
  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
1505
 
1365
- assert created_event_class or len(created_event_classes) > 1
1506
+ if base_created_event_cls:
1507
+ if not created_event_name:
1508
+ created_event_name = base_created_event_cls.__name__
1509
+
1510
+ # Disallow init method from having variable params, because
1511
+ # we are using it to define a "created" event class.
1512
+ if init_method:
1513
+ _raise_type_error_if_func_has_variable_params(init_method)
1514
+
1515
+ # Sanity check: we have a base event class.
1516
+ assert base_event_cls is not None
1517
+ # Sanity check: the base created event class is a class.
1518
+ assert isinstance(
1519
+ base_created_event_cls, type
1520
+ ), base_created_event_cls
1521
+ # Sanity check: base created event not subclass of base event class.
1522
+ assert not issubclass(
1523
+ base_created_event_cls, base_event_cls
1524
+ ), base_created_event_cls
1525
+
1526
+ # Define "created" event class.
1527
+ assert created_event_name
1528
+ assert issubclass(base_created_event_cls, CanInitAggregate)
1529
+ created_event_class_bases = (base_created_event_cls, base_event_cls)
1530
+ created_event_class = cast(
1531
+ type[CanInitAggregate[TAggregateID]],
1532
+ cls._define_event_class(
1533
+ created_event_name,
1534
+ created_event_class_bases,
1535
+ init_method,
1536
+ event_topic=created_event_topic,
1537
+ ),
1538
+ )
1539
+ # Set the event class as an attribute of the aggregate class.
1540
+ setattr(cls, created_event_name, created_event_class)
1541
+
1542
+ elif created_event_name:
1543
+ msg = (
1544
+ 'Can\'t defined "created" event class '
1545
+ f"for name '{created_event_name}'"
1546
+ )
1547
+ raise TypeError(msg)
1366
1548
 
1367
1549
  if created_event_class:
1368
1550
  _created_event_classes[cls] = [created_event_class]
1369
1551
  else:
1370
- # Prepare to disallow ambiguity of choice between created event classes.
1552
+ # Prepare to disallow any ambiguity of choice between created event classes.
1371
1553
  _created_event_classes[cls] = list(created_event_classes.values())
1372
1554
 
1373
1555
  # Find and analyse any @event decorators.
@@ -1424,9 +1606,11 @@ class BaseAggregate(metaclass=MetaAggregate):
1424
1606
 
1425
1607
  # Define event class as subclass of given class.
1426
1608
  given_subclass = cast(
1427
- "type[CanMutateAggregate]",
1609
+ type[CanMutateAggregate[TAggregateID]],
1428
1610
  getattr(cls, event_decorator.given_event_cls.__name__),
1429
1611
  )
1612
+ # TODO: Check if this subclassing means we can avoid some of
1613
+ # the subclassing of events above? Maybe do this first?
1430
1614
  event_cls = cls._define_event_class(
1431
1615
  event_decorator.given_event_cls.__name__,
1432
1616
  (DecoratorEvent, given_subclass),
@@ -1443,11 +1627,16 @@ class BaseAggregate(metaclass=MetaAggregate):
1443
1627
  )
1444
1628
  raise TypeError(msg)
1445
1629
 
1630
+ # Check we have a base event class.
1631
+ if base_event_cls is None:
1632
+ raise base_event_class_not_defined_error
1633
+
1446
1634
  # Define event class from signature of original method.
1447
1635
  event_cls = cls._define_event_class(
1448
1636
  event_decorator.event_cls_name,
1449
1637
  (DecoratorEvent, base_event_cls),
1450
1638
  event_decorator.decorated_func,
1639
+ event_topic=event_decorator.event_topic,
1451
1640
  )
1452
1641
 
1453
1642
  # Cache the decorated method for the event class to use.
@@ -1481,78 +1670,70 @@ class BaseAggregate(metaclass=MetaAggregate):
1481
1670
  for name, value in aggregate_base_class.__dict__.items():
1482
1671
  if (
1483
1672
  isinstance(value, type)
1484
- and issubclass(value, AggregateEvent)
1673
+ and issubclass(value, CanMutateAggregate)
1485
1674
  and name not in cls.__dict__
1486
1675
  and name.lower() != name
1487
1676
  ):
1677
+ # Sanity check: we have a base event class.
1678
+ assert base_event_cls is not None
1488
1679
  event_class = cls._define_event_class(
1489
1680
  name, (base_event_cls, value), None
1490
1681
  )
1491
1682
  setattr(cls, name, event_class)
1492
1683
 
1684
+ if getattr(cls, "TOPIC", None):
1493
1685
 
1494
- class Aggregate(BaseAggregate):
1495
- class Event(AggregateEvent):
1496
- pass
1686
+ explicit_topic = cls.__dict__.get("TOPIC", None)
1497
1687
 
1498
- class Created(Event, AggregateCreated):
1499
- pass
1688
+ if not explicit_topic:
1689
+ msg = f"Explicit topic not defined on {cls}"
1690
+ raise ProgrammingError(msg)
1500
1691
 
1692
+ try:
1693
+ register_topic(explicit_topic, cls)
1694
+ except TopicError:
1695
+ msg = (
1696
+ f"Explicit topic '{explicit_topic}' of {cls} "
1697
+ f"already registered for {resolve_topic(explicit_topic)}"
1698
+ )
1699
+ raise ProgrammingError(msg) from None
1501
1700
 
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
1524
-
1525
- ...is equivalent to...
1526
-
1527
- .. code-block:: python
1528
-
1529
- class MyAggregate(Aggregate):
1530
- pass
1531
- """
1532
-
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,
1701
+ for name, obj in cls.__dict__.items():
1702
+ if (
1703
+ isinstance(obj, type)
1704
+ and issubclass(obj, CanMutateAggregate)
1705
+ and name != "Event"
1706
+ ):
1707
+ explicit_topic = getattr(obj, "TOPIC", None)
1708
+ if not explicit_topic:
1709
+ msg = f"Explicit topic not defined on {obj}"
1710
+ raise ProgrammingError(msg)
1711
+ try:
1712
+ register_topic(explicit_topic, obj)
1713
+ except TopicError:
1714
+ msg = (
1715
+ f"Explicit topic '{explicit_topic}' of {obj} "
1716
+ f"already registered for {resolve_topic(explicit_topic)}"
1717
+ )
1718
+ raise ProgrammingError(msg) from None
1719
+
1720
+
1721
+ def _check_explicit_topic_is_registered(event_class: type[object]) -> None:
1722
+ explicit_topic = getattr(event_class, "TOPIC", None)
1723
+ if not explicit_topic:
1724
+ msg = f"Explicit topic not defined on {event_class}"
1725
+ raise ProgrammingError(msg)
1726
+ try:
1727
+ resolved_obj = resolve_topic(explicit_topic)
1728
+ except TopicError:
1729
+ msg = f"Explicit topic '{explicit_topic}' on {event_class} is not registered"
1730
+ raise ProgrammingError(msg) from None
1731
+ if resolved_obj is not event_class:
1732
+ msg = (
1733
+ f"Explicit topic '{explicit_topic}' on {event_class} "
1734
+ f"already registered for {resolved_obj}"
1549
1735
  )
1550
- assert issubclass(cls_, Aggregate)
1551
- return cls_
1552
-
1553
- if cls:
1554
- return decorator(cls)
1555
- return decorator
1736
+ raise ProgrammingError(msg) from None
1556
1737
 
1557
1738
 
1558
1739
  class OriginatorIDError(EventSourcingError):
@@ -1571,9 +1752,9 @@ class OriginatorVersionError(EventSourcingError):
1571
1752
  """
1572
1753
 
1573
1754
 
1574
- class SnapshotProtocol(DomainEventProtocol, Protocol):
1755
+ class SnapshotProtocol(DomainEventProtocol[TAggregateID_co], Protocol):
1575
1756
  @property
1576
- def state(self) -> dict[str, Any]:
1757
+ def state(self) -> Any:
1577
1758
  """Snapshots have a read-only 'state'."""
1578
1759
  raise NotImplementedError # pragma: no cover
1579
1760
 
@@ -1583,27 +1764,28 @@ class SnapshotProtocol(DomainEventProtocol, Protocol):
1583
1764
  """Snapshots have a 'take()' class method."""
1584
1765
 
1585
1766
 
1586
- TCanSnapshotAggregate = TypeVar("TCanSnapshotAggregate", bound="CanSnapshotAggregate")
1587
-
1588
-
1589
- class CanSnapshotAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
1767
+ class CanSnapshotAggregate(HasOriginatorIDVersion[TAggregateID], CanCreateTimestamp):
1590
1768
  topic: str
1591
1769
  state: Any
1592
1770
 
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
1771
+ def __init_subclass__(cls) -> None:
1772
+ cls.find_originator_id_type(CanSnapshotAggregate)
1773
+ super().__init_subclass__()
1774
+
1775
+ # def __init__(
1776
+ # self,
1777
+ # originator_id: UUID,
1778
+ # originator_version: int,
1779
+ # timestamp: datetime,
1780
+ # topic: str,
1781
+ # state: Any,
1782
+ # ) -> None:
1783
+ # raise NotImplementedError # pragma: no cover
1602
1784
 
1603
1785
  @classmethod
1604
1786
  def take(
1605
1787
  cls,
1606
- aggregate: MutableOrImmutableAggregate,
1788
+ aggregate: MutableOrImmutableAggregate[TAggregateID],
1607
1789
  ) -> Self:
1608
1790
  """Creates a snapshot of the given :class:`Aggregate` object."""
1609
1791
  aggregate_state = dict(aggregate.__dict__)
@@ -1615,16 +1797,16 @@ class CanSnapshotAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
1615
1797
  aggregate_state.pop("_version")
1616
1798
  aggregate_state.pop("_pending_events")
1617
1799
  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,
1800
+ originator_id=aggregate.id, # type: ignore[call-arg]
1801
+ originator_version=aggregate.version, # pyright: ignore[reportCallIssue]
1802
+ timestamp=cls.create_timestamp(), # pyright: ignore[reportCallIssue]
1803
+ topic=get_topic(type(aggregate)), # pyright: ignore[reportCallIssue]
1804
+ state=aggregate_state, # pyright: ignore[reportCallIssue]
1623
1805
  )
1624
1806
 
1625
- def mutate(self, _: None) -> Aggregate:
1807
+ def mutate(self, _: None) -> BaseAggregate[TAggregateID]:
1626
1808
  """Reconstructs the snapshotted :class:`Aggregate` object."""
1627
- cls = cast("type[Aggregate]", resolve_topic(self.topic))
1809
+ cls = cast(type[BaseAggregate[TAggregateID]], resolve_topic(self.topic))
1628
1810
  aggregate_state = dict(self.state)
1629
1811
  from_version = aggregate_state.pop("class_version", 1)
1630
1812
  class_version = getattr(cls, "class_version", 1)
@@ -1643,7 +1825,7 @@ class CanSnapshotAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
1643
1825
 
1644
1826
 
1645
1827
  @dataclass(frozen=True)
1646
- class Snapshot(CanSnapshotAggregate, DomainEvent):
1828
+ class Snapshot(CanSnapshotAggregate[UUID], DomainEvent):
1647
1829
  """Snapshots represent the state of an aggregate at a particular
1648
1830
  version.
1649
1831
 
@@ -1653,8 +1835,79 @@ class Snapshot(CanSnapshotAggregate, DomainEvent):
1653
1835
  :param int originator_version: version of originating aggregate.
1654
1836
  :param datetime timestamp: date-time of the event
1655
1837
  :param str topic: string that includes a class and its module
1656
- :param dict state: version of originating aggregate.
1838
+ :param dict state: state of originating aggregate.
1657
1839
  """
1658
1840
 
1659
1841
  topic: str
1660
1842
  state: dict[str, Any]
1843
+
1844
+
1845
+ class Aggregate(BaseAggregate[UUID]):
1846
+ @staticmethod
1847
+ def create_id(*_: Any, **__: Any) -> UUID:
1848
+ """Returns a new aggregate ID."""
1849
+ return uuid4()
1850
+
1851
+ class Event(AggregateEvent):
1852
+ pass
1853
+
1854
+ class Created(Event, AggregateCreated):
1855
+ pass
1856
+
1857
+ Snapshot = Snapshot
1858
+
1859
+
1860
+ @overload
1861
+ def aggregate(*, created_event_name: str) -> Callable[[Any], type[Aggregate]]:
1862
+ pass # pragma: no cover
1863
+
1864
+
1865
+ @overload
1866
+ def aggregate(cls: Any) -> type[Aggregate]:
1867
+ pass # pragma: no cover
1868
+
1869
+
1870
+ def aggregate(
1871
+ cls: Any | None = None,
1872
+ *,
1873
+ created_event_name: str = "",
1874
+ ) -> type[Aggregate] | Callable[[Any], type[Aggregate]]:
1875
+ """Converts the class that was passed in to inherit from Aggregate.
1876
+
1877
+ .. code-block:: python
1878
+
1879
+ @aggregate
1880
+ class MyAggregate:
1881
+ pass
1882
+
1883
+ ...is equivalent to...
1884
+
1885
+ .. code-block:: python
1886
+
1887
+ class MyAggregate(Aggregate):
1888
+ pass
1889
+ """
1890
+
1891
+ def decorator(cls_: Any) -> type[Aggregate]:
1892
+ if issubclass(cls_, Aggregate):
1893
+ msg = f"{cls_.__qualname__} is already an Aggregate"
1894
+ raise TypeError(msg)
1895
+ bases = cls_.__bases__
1896
+ if bases == (object,):
1897
+ bases = (Aggregate,)
1898
+ else:
1899
+ bases += (Aggregate,)
1900
+ cls_dict = {}
1901
+ cls_dict.update(cls_.__dict__)
1902
+ cls_ = MetaAggregate(
1903
+ cls_.__qualname__,
1904
+ bases,
1905
+ cls_dict,
1906
+ created_event_name=created_event_name,
1907
+ )
1908
+ assert issubclass(cls_, Aggregate)
1909
+ return cls_
1910
+
1911
+ if cls:
1912
+ return decorator(cls)
1913
+ return decorator