eventsourcing 9.3.4__py3-none-any.whl → 9.4.0__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,23 +1,20 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import dataclasses
4
+ import importlib
3
5
  import inspect
4
6
  import os
7
+ from collections import defaultdict
5
8
  from dataclasses import dataclass
6
9
  from datetime import datetime, tzinfo
7
- from functools import lru_cache
10
+ from functools import cache
8
11
  from types import FunctionType, WrapperDescriptorType
9
12
  from typing import (
10
13
  TYPE_CHECKING,
11
14
  Any,
12
15
  Callable,
13
- Dict,
14
16
  Generic,
15
- Iterable,
16
- List,
17
17
  Protocol,
18
- Sequence,
19
- Tuple,
20
- Type,
21
18
  TypeVar,
22
19
  Union,
23
20
  cast,
@@ -25,16 +22,61 @@ from typing import (
25
22
  runtime_checkable,
26
23
  )
27
24
  from uuid import UUID, uuid4
25
+ from warnings import warn
28
26
 
29
27
  from eventsourcing.utils import get_method_name, get_topic, resolve_topic
30
28
 
29
+ if TYPE_CHECKING:
30
+ from collections.abc import Iterable, Sequence
31
+
32
+ from typing_extensions import Self
33
+
34
+
31
35
  TZINFO: tzinfo = resolve_topic(os.getenv("TZINFO_TOPIC", "datetime:timezone.utc"))
36
+ """
37
+ A Python :py:obj:`tzinfo` object that defaults to UTC (:py:obj:`timezone.utc`). Used
38
+ as the timezone argument in :func:`~eventsourcing.domain.datetime_now_with_tzinfo`.
39
+
40
+ Set environment variable ``TZINFO_TOPIC`` to the topic of a different :py:obj:`tzinfo`
41
+ object so that all your domain model event timestamps are located in that timezone
42
+ (not recommended). It is generally recommended to locate all timestamps in the UTC
43
+ domain and convert to local timezones when presenting values in user interfaces.
44
+ """
45
+
46
+
47
+ class EventsourcingType(type):
48
+ """Base type for event sourcing domain model types (aggregates and events)."""
49
+
50
+
51
+ _T = TypeVar("_T")
52
+
53
+
54
+ def patch_dataclasses_process_class() -> None:
55
+ dataclasses_module = importlib.import_module("dataclasses")
56
+ original_process_class_func = dataclasses_module.__dict__["_process_class"]
57
+
58
+ def _patched_dataclasses_process_class(
59
+ cls: type[_T], *args: Any, **kwargs: Any
60
+ ) -> type[_T]:
61
+ # Avoid processing aggregate and event dataclasses twice,
62
+ # because doing so screws up non-init and default fields.
63
+ if (
64
+ cls
65
+ and isinstance(cls, EventsourcingType)
66
+ and "__dataclass_fields__" in cls.__dict__
67
+ ):
68
+ return cls
69
+ return original_process_class_func(cls, *args, **kwargs)
70
+
71
+ dataclasses_module.__dict__["_process_class"] = _patched_dataclasses_process_class
72
+
73
+
74
+ patch_dataclasses_process_class()
32
75
 
33
76
 
34
77
  @runtime_checkable
35
78
  class DomainEventProtocol(Protocol):
36
- """
37
- Protocol for domain event objects.
79
+ """Protocol for domain event objects.
38
80
 
39
81
  A protocol is defined to allow the event sourcing mechanisms
40
82
  to work with different kinds of domain event classes. Whilst
@@ -43,25 +85,26 @@ class DomainEventProtocol(Protocol):
43
85
  kinds of domain event classes, such as Pydantic classes.
44
86
  """
45
87
 
88
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
89
+ pass # pragma: no cover
90
+
46
91
  @property
47
92
  def originator_id(self) -> UUID:
48
- """
49
- UUID identifying an aggregate to which the event belongs.
50
- """
93
+ """UUID identifying an aggregate to which the event belongs."""
94
+ raise NotImplementedError # pragma: no cover
51
95
 
52
96
  @property
53
97
  def originator_version(self) -> int:
54
- """
55
- Integer identifying the version of the aggregate when the event occurred.
56
- """
98
+ """Integer identifying the version of the aggregate when the event occurred."""
99
+ raise NotImplementedError # pragma: no cover
57
100
 
58
101
 
59
102
  TDomainEvent = TypeVar("TDomainEvent", bound=DomainEventProtocol)
103
+ SDomainEvent = TypeVar("SDomainEvent", bound=DomainEventProtocol)
60
104
 
61
105
 
62
106
  class MutableAggregateProtocol(Protocol):
63
- """
64
- Protocol for mutable aggregate objects.
107
+ """Protocol for mutable aggregate objects.
65
108
 
66
109
  A protocol is defined to allow the event sourcing mechanisms
67
110
  to work with different kinds of aggregate classes. Whilst
@@ -72,26 +115,22 @@ class MutableAggregateProtocol(Protocol):
72
115
 
73
116
  @property
74
117
  def id(self) -> UUID:
75
- """
76
- Mutable aggregates have a read-only ID that is a UUID.
77
- """
118
+ """Mutable aggregates have a read-only ID that is a UUID."""
119
+ raise NotImplementedError # pragma: no cover
78
120
 
79
121
  @property
80
122
  def version(self) -> int:
81
- """
82
- Mutable aggregates have a read-write version that is an int.
83
- """
123
+ """Mutable aggregates have a read-write version that is an int."""
124
+ raise NotImplementedError # pragma: no cover
84
125
 
85
126
  @version.setter
86
127
  def version(self, value: int) -> None:
87
- """
88
- Mutable aggregates have a read-write version that is an int.
89
- """
128
+ """Mutable aggregates have a read-write version that is an int."""
129
+ raise NotImplementedError # pragma: no cover
90
130
 
91
131
 
92
132
  class ImmutableAggregateProtocol(Protocol):
93
- """
94
- Protocol for immutable aggregate objects.
133
+ """Protocol for immutable aggregate objects.
95
134
 
96
135
  A protocol is defined to allow the event sourcing mechanisms
97
136
  to work with different kinds of aggregate classes. Whilst
@@ -102,15 +141,13 @@ class ImmutableAggregateProtocol(Protocol):
102
141
 
103
142
  @property
104
143
  def id(self) -> UUID:
105
- """
106
- Immutable aggregates have a read-only ID that is a UUID.
107
- """
144
+ """Immutable aggregates have a read-only ID that is a UUID."""
145
+ raise NotImplementedError # pragma: no cover
108
146
 
109
147
  @property
110
148
  def version(self) -> int:
111
- """
112
- Immutable aggregates have a read-only version that is an int.
113
- """
149
+ """Immutable aggregates have a read-only version that is an int."""
150
+ raise NotImplementedError # pragma: no cover
114
151
 
115
152
 
116
153
  MutableOrImmutableAggregate = Union[
@@ -127,21 +164,16 @@ TMutableOrImmutableAggregate = TypeVar(
127
164
 
128
165
  @runtime_checkable
129
166
  class CollectEventsProtocol(Protocol):
130
- """
131
- Protocol for aggregates that support collecting pending events.
132
- """
167
+ """Protocol for aggregates that support collecting pending events."""
133
168
 
134
169
  def collect_events(self) -> Sequence[DomainEventProtocol]:
135
- """
136
- Returns a sequence of events.
137
- """
170
+ """Returns a sequence of events."""
171
+ raise NotImplementedError # pragma: no cover
138
172
 
139
173
 
140
174
  @runtime_checkable
141
175
  class CanMutateProtocol(DomainEventProtocol, Protocol[TMutableOrImmutableAggregate]):
142
- """
143
- Protocol for events that have a mutate method.
144
- """
176
+ """Protocol for events that have a mutate method."""
145
177
 
146
178
  def mutate(
147
179
  self, aggregate: TMutableOrImmutableAggregate | None
@@ -153,34 +185,42 @@ class CanMutateProtocol(DomainEventProtocol, Protocol[TMutableOrImmutableAggrega
153
185
  """
154
186
 
155
187
 
156
- def create_utc_datetime_now() -> datetime:
188
+ def datetime_now_with_tzinfo() -> datetime:
157
189
  """
158
- Constructs a timezone-aware :class:`datetime` object for the current date and time.
190
+ Constructs a timezone-aware :class:`datetime`
191
+ object for the current date and time.
192
+
193
+ Uses :py:obj:`TZINFO` as the timezone.
159
194
  """
160
195
  return datetime.now(tz=TZINFO)
161
196
 
162
197
 
198
+ def create_utc_datetime_now() -> datetime:
199
+ """Deprected in favour of :func:`~eventsourcing.domain.datetime_now_with_tzinfo`."""
200
+ msg = (
201
+ "'create_utc_datetime_now()' is deprecated, "
202
+ "use 'datetime_now_with_tzinfo()' instead"
203
+ )
204
+ warn(msg, DeprecationWarning, stacklevel=2)
205
+ return datetime_now_with_tzinfo()
206
+
207
+
163
208
  class CanCreateTimestamp:
164
- """
165
- Provides a create_timestamp() method to subclasses.
166
- """
209
+ """Provides a create_timestamp() method to subclasses."""
167
210
 
168
211
  @staticmethod
169
212
  def create_timestamp() -> datetime:
170
- """
171
- Constructs a timezone-aware :class:`datetime` object
213
+ """Constructs a timezone-aware :class:`datetime` object
172
214
  representing when an event occurred.
173
215
  """
174
- return create_utc_datetime_now()
216
+ return datetime_now_with_tzinfo()
175
217
 
176
218
 
177
- TAggregate = TypeVar("TAggregate", bound="Aggregate")
219
+ TAggregate = TypeVar("TAggregate", bound="BaseAggregate")
178
220
 
179
221
 
180
222
  class HasOriginatorIDVersion:
181
- """
182
- Declares ``originator_id`` and ``originator_version`` attributes.
183
- """
223
+ """Declares ``originator_id`` and ``originator_version`` attributes."""
184
224
 
185
225
  originator_id: UUID
186
226
  """UUID identifying an aggregate to which the event belongs."""
@@ -189,18 +229,18 @@ class HasOriginatorIDVersion:
189
229
 
190
230
 
191
231
  class CanMutateAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
192
- """
193
- Implements a :func:`~eventsourcing.domain.CanMutateAggregate.mutate`
232
+ """Implements a :func:`~eventsourcing.domain.CanMutateAggregate.mutate`
194
233
  method that evolves the state of an aggregate.
195
234
  """
196
235
 
236
+ # TODO: Move this to a HasTimestamp? Why is it here??
197
237
  timestamp: datetime
198
238
  """Timezone-aware :class:`datetime` object representing when an event occurred."""
199
239
 
200
240
  def mutate(self, aggregate: TAggregate | None) -> TAggregate | None:
201
- """
202
- Validates and adjusted the attributes of the given ``aggregate`` argument. The
203
- argument is typed as ``Optional`` but the value is expected to be not ``None``.
241
+ """Validates and adjustes the attributes of the given ``aggregate`
242
+ argument. The argument typed as ``Optional`` but the value is
243
+ expected to be not ``None``.
204
244
 
205
245
  Validates the ``aggregate`` argument by checking the event's
206
246
  :py:attr:`~eventsourcing.domain.HasOriginatorIDVersion.originator_id` equals the
@@ -240,9 +280,8 @@ class CanMutateAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
240
280
  # Return the mutated aggregate.
241
281
  return aggregate
242
282
 
243
- def apply(self, aggregate: Aggregate) -> None:
244
- """
245
- Applies the domain event to its aggregate.
283
+ def apply(self, aggregate: Any) -> None:
284
+ """Applies the domain event to its aggregate.
246
285
 
247
286
  This method does nothing but exist to be
248
287
  overridden as a convenient way for users
@@ -252,8 +291,7 @@ class CanMutateAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
252
291
 
253
292
 
254
293
  class CanInitAggregate(CanMutateAggregate):
255
- """
256
- Implements a :func:`~eventsourcing.domain.CanMutateAggregate.mutate`
294
+ """Implements a :func:`~eventsourcing.domain.CanMutateAggregate.mutate`
257
295
  method that constructs the initial state of an aggregate.
258
296
  """
259
297
 
@@ -261,8 +299,7 @@ class CanInitAggregate(CanMutateAggregate):
261
299
  """String describing the path to an aggregate class."""
262
300
 
263
301
  def mutate(self, aggregate: TAggregate | None) -> TAggregate | None:
264
- """
265
- Constructs an aggregate instance according to the attributes of an event.
302
+ """Constructs an aggregate instance according to the attributes of an event.
266
303
 
267
304
  The ``aggregate`` argument is typed as an optional argument, but the
268
305
  value is expected to be ``None``.
@@ -270,7 +307,7 @@ class CanInitAggregate(CanMutateAggregate):
270
307
  assert aggregate is None
271
308
 
272
309
  # Resolve originator topic.
273
- aggregate_class: Type[TAggregate] = resolve_topic(self.originator_topic)
310
+ aggregate_class: type[TAggregate] = resolve_topic(self.originator_topic)
274
311
 
275
312
  # Construct an aggregate object (a "shell" of the correct object type).
276
313
  agg = aggregate_class.__new__(aggregate_class)
@@ -289,12 +326,12 @@ class CanInitAggregate(CanMutateAggregate):
289
326
  self.__dict__, type(agg).__init__
290
327
  )
291
328
 
292
- # Provide the aggregate id, if the aggregate subclass init method expects it.
329
+ # Provide the aggregate id, if the __init__ method expects it.
293
330
  if aggregate_class in _init_mentions_id:
294
331
  init_kwargs["id"] = self.__dict__["originator_id"]
295
332
 
296
333
  # Call the aggregate subclass class init method.
297
- agg.__init__(**init_kwargs) # type: ignore
334
+ agg.__init__(**init_kwargs) # type: ignore[misc]
298
335
 
299
336
  # Call the event apply method (alternative to using __init__())
300
337
  self.apply(agg)
@@ -303,26 +340,23 @@ class CanInitAggregate(CanMutateAggregate):
303
340
  return agg
304
341
 
305
342
 
306
- class MetaDomainEvent(type):
307
- """
308
- Metaclass which ensures all domain event classes are frozen dataclasses.
309
- """
343
+ class MetaDomainEvent(EventsourcingType):
344
+ """Metaclass which ensures all domain event classes are frozen dataclasses."""
310
345
 
311
346
  def __new__(
312
- cls, name: str, bases: Tuple[Type[TDomainEvent], ...], cls_dict: Dict[str, Any]
313
- ) -> Type[TDomainEvent]:
347
+ cls, name: str, bases: tuple[type[TDomainEvent], ...], cls_dict: dict[str, Any]
348
+ ) -> type[TDomainEvent]:
314
349
  event_cls = cast(
315
- Type[TDomainEvent], super().__new__(cls, name, bases, cls_dict)
350
+ "type[TDomainEvent]", super().__new__(cls, name, bases, cls_dict)
316
351
  )
317
- event_cls = dataclass(frozen=True)(event_cls)
318
- event_cls.__signature__ = inspect.signature(event_cls.__init__) # type: ignore
352
+ event_cls = dataclasses.dataclass(frozen=True)(event_cls)
353
+ event_cls.__signature__ = inspect.signature(event_cls.__init__) # type: ignore[attr-defined]
319
354
  return event_cls
320
355
 
321
356
 
357
+ @dataclass(frozen=True)
322
358
  class DomainEvent(CanCreateTimestamp, metaclass=MetaDomainEvent):
323
- """
324
- Frozen data class representing domain model events.
325
- """
359
+ """Frozen data class representing domain model events."""
326
360
 
327
361
  originator_id: UUID
328
362
  """UUID identifying an aggregate to which the event belongs."""
@@ -333,80 +367,69 @@ class DomainEvent(CanCreateTimestamp, metaclass=MetaDomainEvent):
333
367
 
334
368
 
335
369
  class AggregateEvent(CanMutateAggregate, DomainEvent):
336
- """
337
- Frozen data class representing aggregate events.
370
+ """Frozen data class representing aggregate events.
338
371
 
339
372
  Subclasses represent original decisions made by domain model aggregates.
340
373
  """
341
374
 
342
375
 
376
+ @dataclass(frozen=True)
343
377
  class AggregateCreated(CanInitAggregate, AggregateEvent):
344
- """
345
- Frozen data class representing the initial creation of an aggregate.
346
- """
378
+ """Frozen data class representing the initial creation of an aggregate."""
347
379
 
348
380
  originator_topic: str
349
381
  """String describing the path to an aggregate class."""
350
382
 
351
383
 
352
384
  class EventSourcingError(Exception):
353
- """
354
- Base exception class.
355
- """
385
+ """Base exception class."""
356
386
 
357
387
 
358
388
  class ProgrammingError(EventSourcingError):
359
- """
360
- Exception class for domain model programming errors.
361
- """
389
+ """Exception class for domain model programming errors."""
362
390
 
363
391
 
364
392
  class LogEvent(DomainEvent):
365
- """
366
- Deprecated: Inherit from DomainEvent instead.
393
+ """Deprecated: Inherit from DomainEvent instead.
367
394
 
368
395
  Base class for the events of event-sourced logs.
369
396
  """
370
397
 
371
398
 
372
- # Deprecated: Use TDomainEvent instead.
373
- TLogEvent = TypeVar("TLogEvent", bound=DomainEventProtocol)
374
-
375
-
376
399
  def _filter_kwargs_for_method_params(
377
- kwargs: Dict[str, Any], method: Callable[..., Any]
378
- ) -> Dict[str, Any]:
400
+ kwargs: dict[str, Any], method: Callable[..., Any]
401
+ ) -> dict[str, Any]:
379
402
  names = _spec_filter_kwargs_for_method_params(method)
380
403
  return {k: v for k, v in kwargs.items() if k in names}
381
404
 
382
405
 
383
- @lru_cache(maxsize=None)
406
+ @cache
384
407
  def _spec_filter_kwargs_for_method_params(method: Callable[..., Any]) -> set[str]:
385
408
  method_signature = inspect.signature(method)
386
409
  return set(method_signature.parameters)
387
410
 
388
411
 
389
- if TYPE_CHECKING: # pragma: nocover
390
- EventSpecType = Union[str, Type[CanMutateAggregate]]
412
+ if TYPE_CHECKING:
413
+ EventSpecType = Union[str, type[CanMutateAggregate]]
391
414
 
392
- CommandMethod = Callable[..., None]
393
- DecoratedObjType = Union[CommandMethod, property]
394
- TDecoratedObjType = TypeVar("TDecoratedObjType", bound=DecoratedObjType)
415
+ CallableType = Callable[..., None]
416
+ DecoratableType = Union[CallableType, property]
417
+ TDecoratableType = TypeVar("TDecoratableType", bound=DecoratableType)
395
418
 
396
419
 
397
420
  class CommandMethodDecorator:
398
421
  def __init__(
399
422
  self,
400
423
  event_spec: EventSpecType | None,
401
- decorated_obj: DecoratedObjType,
424
+ decorated_obj: DecoratableType,
402
425
  ):
403
426
  self.is_name_inferred_from_method = False
404
- self.given_event_cls: Type[CanMutateAggregate] | None = None
427
+ self.given_event_cls: type[CanMutateAggregate] | None = None
405
428
  self.event_cls_name: str | None = None
406
429
  self.decorated_property: property | None = None
407
430
  self.is_property_setter = False
408
431
  self.property_setter_arg_name: str | None = None
409
- self.decorated_method: FunctionType | WrapperDescriptorType
432
+ self.decorated_func: CallableType
410
433
 
411
434
  # Event name has been specified.
412
435
  if isinstance(event_spec, str):
@@ -419,12 +442,12 @@ class CommandMethodDecorator:
419
442
  elif isinstance(event_spec, type) and issubclass(
420
443
  event_spec, CanMutateAggregate
421
444
  ):
422
- if event_spec in given_event_classes:
445
+ if event_spec in _given_event_classes:
423
446
  name = event_spec.__name__
424
447
  msg = f"{name} event class used in more than one decorator"
425
448
  raise TypeError(msg)
426
449
  self.given_event_cls = event_spec
427
- given_event_classes.add(event_spec)
450
+ _given_event_classes.add(event_spec)
428
451
 
429
452
  # Process a decorated property.
430
453
  if isinstance(decorated_obj, property):
@@ -438,30 +461,34 @@ class CommandMethodDecorator:
438
461
  # Remember we are decorating a property.
439
462
  self.decorated_property = decorated_obj
440
463
 
441
- # Remember the decorated method as the "setter" of the property.
442
- self.decorated_method = cast(FunctionType, decorated_obj.fset)
443
-
444
- assert isinstance(self.decorated_method, FunctionType)
464
+ # TODO: Disallow unusual property setters in more detail.
465
+ assert isinstance(decorated_obj.fset, FunctionType)
445
466
 
446
467
  # Disallow deriving event class names from property names.
447
468
  if not self.given_event_cls and not self.event_cls_name:
448
- method_name = self.decorated_method.__name__
449
- msg = f"@event on {method_name}() setter requires event name or class"
469
+ method_name = decorated_obj.fset.__name__
470
+ msg = (
471
+ f"@event decorator on @{method_name}.setter "
472
+ f"requires event name or class"
473
+ )
450
474
  raise TypeError(msg)
451
475
 
476
+ # Remember property "setter" as the decorated function.
477
+ self.decorated_func = decorated_obj.fset
478
+
452
479
  # Remember the name of the second setter arg.
453
- setter_arg_names = list(inspect.signature(self.decorated_method).parameters)
480
+ setter_arg_names = list(inspect.signature(self.decorated_func).parameters)
454
481
  assert len(setter_arg_names) == 2
455
482
  self.property_setter_arg_name = setter_arg_names[1]
456
483
 
457
- # Process a decorated method.
484
+ # Process a decorated function.
458
485
  elif isinstance(decorated_obj, FunctionType):
459
- # Remember the decorated method as the decorated object.
460
- self.decorated_method = decorated_obj
486
+ # Remember the decorated obj as the decorated method.
487
+ self.decorated_func = decorated_obj
461
488
 
462
489
  # If necessary, derive an event class name from the method.
463
490
  if not self.given_event_cls and not self.event_cls_name:
464
- original_method_name = self.decorated_method.__name__
491
+ original_method_name = self.decorated_func.__name__
465
492
  if original_method_name != "__init__":
466
493
  self.is_name_inferred_from_method = True
467
494
  self.event_cls_name = "".join(
@@ -475,7 +502,11 @@ class CommandMethodDecorator:
475
502
 
476
503
  # Disallow using methods with variable params to define event class.
477
504
  if self.event_cls_name:
478
- _check_no_variable_params(self.decorated_method)
505
+ _raise_type_error_if_func_has_variable_params(self.decorated_func)
506
+
507
+ # Disallow using methods with positional only params to define event class.
508
+ if self.event_cls_name:
509
+ _raise_type_error_if_func_has_positional_only_params(self.decorated_func)
479
510
 
480
511
  def __call__(self, *args: Any, **kwargs: Any) -> None:
481
512
  # Initialised decorator was called directly, presumably by
@@ -494,7 +525,7 @@ class CommandMethodDecorator:
494
525
 
495
526
  @overload
496
527
  def __get__(
497
- self, instance: None, owner: MetaAggregate[Aggregate]
528
+ self, instance: None, owner: type[BaseAggregate]
498
529
  ) -> UnboundCommandMethodDecorator | property:
499
530
  """
500
531
  Descriptor protocol for getting decorated method or property on class object.
@@ -502,22 +533,24 @@ class CommandMethodDecorator:
502
533
 
503
534
  @overload
504
535
  def __get__(
505
- self, instance: Aggregate, owner: MetaAggregate[Aggregate]
536
+ self, instance: BaseAggregate, owner: type[BaseAggregate]
506
537
  ) -> BoundCommandMethodDecorator | Any:
507
538
  """
508
539
  Descriptor protocol for getting decorated method or property on instance object.
509
540
  """
510
541
 
511
542
  def __get__(
512
- self, instance: Aggregate | None, owner: MetaAggregate[Aggregate]
543
+ self, instance: BaseAggregate | None, owner: type[BaseAggregate]
513
544
  ) -> BoundCommandMethodDecorator | UnboundCommandMethodDecorator | property | Any:
514
- """
515
- Descriptor protocol for getting decorated method or property.
516
- """
545
+ """Descriptor protocol for getting decorated method or property."""
517
546
  # If we are decorating a property, then delegate to the property's __get__.
518
547
  if self.decorated_property:
519
548
  return self.decorated_property.__get__(instance, owner)
520
549
 
550
+ # If we are decorating an __init__ method, then delegate to the __init__ method.
551
+ if self.decorated_func.__name__ == "__init__":
552
+ return self.decorated_func.__get__(instance, owner)
553
+
521
554
  # Return a "bound" command method decorator if we have an instance.
522
555
  if instance:
523
556
  return BoundCommandMethodDecorator(self, instance)
@@ -525,10 +558,8 @@ class CommandMethodDecorator:
525
558
  # Return an "unbound" command method decorator if we have no instance.
526
559
  return UnboundCommandMethodDecorator(self)
527
560
 
528
- def __set__(self, instance: Aggregate, value: Any) -> None:
529
- """
530
- Descriptor protocol for assigning to decorated property.
531
- """
561
+ def __set__(self, instance: BaseAggregate, value: Any) -> None:
562
+ """Descriptor protocol for assigning to decorated property."""
532
563
  # Set decorated property indirectly by triggering an event.
533
564
  assert self.property_setter_arg_name
534
565
  b = BoundCommandMethodDecorator(self, instance)
@@ -537,35 +568,28 @@ class CommandMethodDecorator:
537
568
 
538
569
 
539
570
  @overload
540
- def event(arg: TDecoratedObjType) -> TDecoratedObjType:
541
- """
542
- Signature for calling ``@event`` decorator with decorated method.
543
- """
571
+ def event(arg: TDecoratableType) -> TDecoratableType:
572
+ """Signature for calling ``@event`` decorator with decorated method."""
544
573
 
545
574
 
546
575
  @overload
547
576
  def event(
548
577
  arg: EventSpecType,
549
- ) -> Callable[[TDecoratedObjType], TDecoratedObjType]:
550
- """
551
- Signature for calling ``@event`` decorator with event specification.
552
- """
578
+ ) -> Callable[[TDecoratableType], TDecoratableType]:
579
+ """Signature for calling ``@event`` decorator with event specification."""
553
580
 
554
581
 
555
582
  @overload
556
583
  def event(
557
584
  arg: None = None,
558
- ) -> Callable[[TDecoratedObjType], TDecoratedObjType]:
559
- """
560
- Signature for calling ``@event`` decorator without event specification.
561
- """
585
+ ) -> Callable[[TDecoratableType], TDecoratableType]:
586
+ """Signature for calling ``@event`` decorator without event specification."""
562
587
 
563
588
 
564
589
  def event(
565
- arg: EventSpecType | TDecoratedObjType | None = None,
566
- ) -> TDecoratedObjType | Callable[[TDecoratedObjType], TDecoratedObjType]:
567
- """
568
- Event-triggering decorator for aggregate command methods and property setters.
590
+ arg: EventSpecType | TDecoratableType | None = None,
591
+ ) -> TDecoratableType | Callable[[TDecoratableType], TDecoratableType]:
592
+ """Event-triggering decorator for aggregate command methods and property setters.
569
593
 
570
594
  Can be used to decorate an aggregate method or property setter so that an
571
595
  event will be triggered when the method is called or the property is set.
@@ -620,25 +644,24 @@ def event(
620
644
  decorated_obj=arg,
621
645
  )
622
646
  return cast(
623
- Callable[[TDecoratedObjType], TDecoratedObjType], command_method_decorator
647
+ "Callable[[TDecoratableType], TDecoratableType]", command_method_decorator
624
648
  )
625
649
 
626
650
  if (
627
651
  arg is None
628
652
  or isinstance(arg, str)
629
- or isinstance(arg, type)
630
- and issubclass(arg, CanMutateAggregate)
653
+ or (isinstance(arg, type) and issubclass(arg, CanMutateAggregate))
631
654
  ):
632
655
  event_spec = arg
633
656
 
634
657
  def create_command_method_decorator(
635
- decorated_obj: TDecoratedObjType,
636
- ) -> TDecoratedObjType:
658
+ decorated_obj: TDecoratableType,
659
+ ) -> TDecoratableType:
637
660
  command_method_decorator = CommandMethodDecorator(
638
661
  event_spec=event_spec,
639
662
  decorated_obj=decorated_obj,
640
663
  )
641
- return cast(TDecoratedObjType, command_method_decorator)
664
+ return cast("TDecoratableType", command_method_decorator)
642
665
 
643
666
  return create_command_method_decorator
644
667
 
@@ -653,21 +676,18 @@ triggers = event
653
676
 
654
677
 
655
678
  class UnboundCommandMethodDecorator:
656
- """
657
- Wraps a CommandMethodDecorator instance when accessed on an aggregate class.
658
- """
679
+ """Wraps a CommandMethodDecorator instance when accessed on an aggregate class."""
659
680
 
660
681
  def __init__(self, event_decorator: CommandMethodDecorator):
661
- """
662
-
663
- :param CommandMethodDecorator event_decorator:
664
- """
682
+ """:param CommandMethodDecorator event_decorator:"""
665
683
  self.event_decorator = event_decorator
666
- self.__module__ = event_decorator.decorated_method.__module__
667
- self.__name__ = event_decorator.decorated_method.__name__
668
- self.__qualname__ = event_decorator.decorated_method.__qualname__
669
- self.__annotations__ = event_decorator.decorated_method.__annotations__
670
- self.__doc__ = event_decorator.decorated_method.__doc__
684
+ self.__module__ = event_decorator.decorated_func.__module__
685
+ self.__name__ = event_decorator.decorated_func.__name__
686
+ self.__qualname__ = event_decorator.decorated_func.__qualname__
687
+ self.__annotations__ = event_decorator.decorated_func.__annotations__
688
+ self.__doc__ = event_decorator.decorated_func.__doc__
689
+ # self.__wrapped__ = event_decorator.decorated_method
690
+ # functools.update_wrapper(self, event_decorator.decorated_method)
671
691
 
672
692
  def __call__(self, *args: Any, **kwargs: Any) -> None:
673
693
  # Expect first argument is an aggregate instance.
@@ -682,30 +702,29 @@ class UnboundCommandMethodDecorator:
682
702
 
683
703
 
684
704
  class BoundCommandMethodDecorator:
685
- """
686
- Binds a CommandMethodDecorator with an aggregate instance so calls to
705
+ """Binds a CommandMethodDecorator with an aggregate instance so calls to
687
706
  decorated command methods can be intercepted and will trigger an event.
688
707
  """
689
708
 
690
- def __init__(self, event_decorator: CommandMethodDecorator, aggregate: Aggregate):
691
- """
692
-
693
- :param CommandMethodDecorator event_decorator:
709
+ def __init__(
710
+ self, event_decorator: CommandMethodDecorator, aggregate: BaseAggregate
711
+ ):
712
+ """:param CommandMethodDecorator event_decorator:
694
713
  :param Aggregate aggregate:
695
714
  """
696
715
  self.event_decorator = event_decorator
697
- self.__module__ = event_decorator.decorated_method.__module__
698
- self.__name__ = event_decorator.decorated_method.__name__
699
- self.__qualname__ = event_decorator.decorated_method.__qualname__
700
- self.__annotations__ = event_decorator.decorated_method.__annotations__
701
- self.__doc__ = event_decorator.decorated_method.__doc__
716
+ self.__module__ = event_decorator.decorated_func.__module__
717
+ self.__name__ = event_decorator.decorated_func.__name__
718
+ self.__qualname__ = event_decorator.decorated_func.__qualname__
719
+ self.__annotations__ = event_decorator.decorated_func.__annotations__
720
+ self.__doc__ = event_decorator.decorated_func.__doc__
702
721
  self.aggregate = aggregate
703
722
 
704
723
  def trigger(self, *args: Any, **kwargs: Any) -> None:
705
724
  kwargs = _coerce_args_to_kwargs(
706
- self.event_decorator.decorated_method, args, kwargs
725
+ self.event_decorator.decorated_func, args, kwargs
707
726
  )
708
- event_cls = decorated_event_classes[self.event_decorator]
727
+ event_cls = decorator_event_classes[self.event_decorator]
709
728
  kwargs = _filter_kwargs_for_method_params(kwargs, event_cls)
710
729
  self.aggregate.trigger_event(event_cls, **kwargs)
711
730
 
@@ -713,17 +732,32 @@ class BoundCommandMethodDecorator:
713
732
  self.trigger(*args, **kwargs)
714
733
 
715
734
 
716
- given_event_classes: set[type] = set()
717
- decorated_methods: Dict[type, CommandMethod] = {}
718
- aggregate_has_many_created_event_classes: Dict[type, List[str]] = {}
735
+ class DecoratorEvent(CanMutateAggregate):
736
+ def apply(self, aggregate: BaseAggregate) -> None:
737
+ """Applies event to aggregate by calling method decorated by @event."""
738
+ # Identify the function that was decorated.
739
+ decorated_func = _decorated_funcs[type(self)]
740
+
741
+ # Select event attributes mentioned in function signature.
742
+ kwargs = _filter_kwargs_for_method_params(self.__dict__, decorated_func)
743
+
744
+ # Call the original method with event attribute values.
745
+ decorated_method = decorated_func.__get__(aggregate, type(aggregate))
746
+ decorated_method(**kwargs)
719
747
 
748
+ # Call super method, just in case any base classes need it.
749
+ super().apply(aggregate)
720
750
 
721
- decorated_event_classes: Dict[
722
- CommandMethodDecorator, Type[MetaAggregate.DecoratedEvent]
723
- ] = {}
724
751
 
752
+ _given_event_classes: set[type] = set()
753
+ _decorated_funcs: dict[type, CallableType] = {}
754
+ _created_event_classes: dict[type, list[type[CanInitAggregate]]] = {}
725
755
 
726
- def _check_no_variable_params(method: FunctionType) -> None:
756
+
757
+ decorator_event_classes: dict[CommandMethodDecorator, type[DecoratorEvent]] = {}
758
+
759
+
760
+ def _raise_type_error_if_func_has_variable_params(method: CallableType) -> None:
727
761
  for param in inspect.signature(method).parameters.values():
728
762
  if param.kind is param.VAR_POSITIONAL:
729
763
  msg = f"*{param.name} not supported by decorator on {method.__name__}()"
@@ -738,14 +772,32 @@ def _check_no_variable_params(method: FunctionType) -> None:
738
772
  raise TypeError(msg)
739
773
 
740
774
 
775
+ def _raise_type_error_if_func_has_positional_only_params(method: CallableType) -> None:
776
+ # TODO: Support POSITIONAL_ONLY?
777
+ positional_only_params = []
778
+ for param in inspect.signature(method).parameters.values():
779
+ if param.name == "self":
780
+ continue
781
+ if param.kind is param.POSITIONAL_ONLY:
782
+ positional_only_params.append(param.name)
783
+
784
+ if positional_only_params:
785
+ msg = (
786
+ f"positional only args arg not supported by @event decorator on "
787
+ f"{method.__name__}(): {', '.join(positional_only_params)}"
788
+ )
789
+ raise TypeError(msg)
790
+
791
+
741
792
  def _coerce_args_to_kwargs(
742
- method: FunctionType | WrapperDescriptorType,
793
+ method: CallableType,
743
794
  args: Iterable[Any],
744
- kwargs: Dict[str, Any],
795
+ kwargs: dict[str, Any],
745
796
  *,
746
797
  expects_id: bool = False,
747
- ) -> Dict[str, Any]:
748
- assert isinstance(method, (FunctionType, WrapperDescriptorType))
798
+ ) -> dict[str, Any]:
799
+ # __init__ methods are WrapperDescriptorType, other method are FunctionType.
800
+ assert isinstance(method, (FunctionType, WrapperDescriptorType)), method
749
801
 
750
802
  args = tuple(args)
751
803
  enumerated_args_names, keyword_defaults_items = _spec_coerce_args_to_kwargs(
@@ -756,21 +808,19 @@ def _coerce_args_to_kwargs(
756
808
  )
757
809
 
758
810
  copy_kwargs = dict(kwargs)
759
- for i, name in enumerated_args_names:
760
- copy_kwargs[name] = args[i]
761
- for name, value in keyword_defaults_items:
762
- copy_kwargs[name] = value
811
+ copy_kwargs.update({name: args[i] for i, name in enumerated_args_names})
812
+ copy_kwargs.update(keyword_defaults_items)
763
813
  return copy_kwargs
764
814
 
765
815
 
766
- @lru_cache(maxsize=None)
816
+ @cache
767
817
  def _spec_coerce_args_to_kwargs(
768
- method: FunctionType | WrapperDescriptorType,
818
+ method: CallableType,
769
819
  len_args: int,
770
- kwargs_keys: Tuple[str],
820
+ kwargs_keys: tuple[str],
771
821
  *,
772
822
  expects_id: bool,
773
- ) -> Tuple[Tuple[Tuple[int, str], ...], Tuple[Tuple[str, Any], ...]]:
823
+ ) -> tuple[tuple[tuple[int, str], ...], tuple[tuple[str, Any], ...]]:
774
824
  method_signature = inspect.signature(method)
775
825
  positional_names = []
776
826
  keyword_defaults = {}
@@ -819,16 +869,14 @@ def _spec_coerce_args_to_kwargs(
819
869
  f"argument{'' if num_missing == 1 else 's'}: "
820
870
  )
821
871
  _raise_missing_names_type_error(missing_names, msg)
822
- counter = 0
823
872
  args_names = []
824
- for name in positional_names:
873
+ for counter, name in enumerate(positional_names):
825
874
  if counter + 1 > len_args:
826
875
  break
827
876
  if name in kwargs_keys:
828
877
  msg = f"{method_name}() got multiple values for argument '{name}'"
829
878
  raise TypeError(msg)
830
879
  args_names.append(name)
831
- counter += 1
832
880
  missing_keyword_only_arguments = [
833
881
  name for name in required_keyword_only if name not in kwargs_keys
834
882
  ]
@@ -848,7 +896,7 @@ def _spec_coerce_args_to_kwargs(
848
896
  return enumerated_args_names, keyword_defaults_items
849
897
 
850
898
 
851
- def _raise_missing_names_type_error(missing_names: List[str], msg: str) -> None:
899
+ def _raise_missing_names_type_error(missing_names: list[str], msg: str) -> None:
852
900
  msg += missing_names[0]
853
901
  if len(missing_names) == 2:
854
902
  msg += f" and {missing_names[1]}"
@@ -858,255 +906,494 @@ def _raise_missing_names_type_error(missing_names: List[str], msg: str) -> None:
858
906
  raise TypeError(msg)
859
907
 
860
908
 
861
- _annotations_mention_id: set[MetaAggregate[Aggregate]] = set()
862
- _init_mentions_id: set[MetaAggregate[Aggregate]] = set()
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)
863
912
 
864
913
 
865
- class MetaAggregate(type, Generic[TAggregate]):
866
- """
867
- Metaclass for aggregate classes.
914
+ class MetaAggregate(EventsourcingType, Generic[TAggregate], type):
915
+ """Metaclass for aggregate classes."""
868
916
 
869
- Initialises aggregate classes by defining event classes.
870
- """
917
+ def _define_event_class(
918
+ cls,
919
+ name: str,
920
+ bases: tuple[type[CanMutateAggregate], ...],
921
+ apply_method: CallableType | None,
922
+ ) -> type[CanMutateAggregate]:
923
+ # Define annotations for the event class (specs the init method).
924
+ annotations = {}
925
+ if apply_method is not None:
926
+ method_signature = inspect.signature(apply_method)
927
+ supers = {
928
+ s for b in bases for s in b.__mro__ if hasattr(s, "__annotations__")
929
+ }
930
+ super_annotations = {a for s in supers for a in s.__annotations__}
931
+ for param_name, param in list(method_signature.parameters.items())[1:]:
932
+ # Don't define 'id' on a "created" class.
933
+ if param_name == "id" and apply_method.__name__ == "__init__":
934
+ continue
935
+ # Don't override super class annotations, unless no default on param.
936
+ if param_name not in super_annotations or param.default == param.empty:
937
+ annotations[param_name] = param.annotation or "typing.Any"
938
+ event_cls_qualname = f"{cls.__qualname__}.{name}"
939
+ event_cls_dict = {
940
+ "__annotations__": annotations,
941
+ "__module__": cls.__module__,
942
+ "__qualname__": event_cls_qualname,
943
+ }
871
944
 
872
- INITIAL_VERSION = 1
945
+ # Create the event class object.
946
+ _new_class = type(name, bases, event_cls_dict)
947
+ return cast("type[CanMutateAggregate]", _new_class)
873
948
 
874
- class Event(AggregateEvent):
875
- pass
949
+ def __call__(
950
+ cls: MetaAggregate[TAggregate], *args: Any, **kwargs: Any
951
+ ) -> TAggregate:
952
+ if cls is BaseAggregate:
953
+ msg = "BaseAggregate class cannot be instantiated directly"
954
+ raise TypeError(msg)
955
+ created_event_classes = _created_event_classes[cls]
956
+ # Here, unlike when calling _create(), we don't have a given event class,
957
+ # so we need to check that there is one "created" event class to use here.
958
+ # We don't check this in __init_subclass__ to allow for alternatives that
959
+ # can be selected by developers by calling _create(event_class=...).
960
+ if len(created_event_classes) > 1:
961
+ msg = (
962
+ f"{cls.__qualname__} can't decide which of many "
963
+ '"created" event classes to use: '
964
+ f"""'{"', '".join(c.__name__ for c in created_event_classes)}'. """
965
+ "Please use class arg 'created_event_name' or"
966
+ " @event decorator on __init__ method."
967
+ )
968
+ raise TypeError(msg)
876
969
 
877
- class Created(Event, AggregateCreated):
878
- pass
970
+ kwargs = _coerce_args_to_kwargs(
971
+ cls.__init__, # type: ignore[misc]
972
+ args,
973
+ kwargs,
974
+ expects_id=cls in _annotations_mention_id,
975
+ )
976
+ return cls._create(
977
+ event_class=created_event_classes[0],
978
+ **kwargs,
979
+ )
879
980
 
880
- class DecoratedEvent(CanMutateAggregate):
881
- def apply(self, aggregate: Aggregate) -> None:
882
- """
883
- Applies event to aggregate by calling method decorated by @event.
884
- """
885
- # Call super method, just in case any base classes need it.
886
- super().apply(aggregate)
981
+ def _create(
982
+ cls: MetaAggregate[TAggregate],
983
+ event_class: type[CanInitAggregate],
984
+ **kwargs: Any,
985
+ ) -> TAggregate:
986
+ # Just define method signature for the __call__() method.
987
+ raise NotImplementedError # pragma: no cover
887
988
 
888
- # Identify the method that was decorated.
889
- decorated_method = decorated_methods[type(self)]
890
989
 
891
- # Select event attributes mentioned in method signature.
892
- kwargs = _filter_kwargs_for_method_params(self.__dict__, decorated_method)
990
+ class BaseAggregate(metaclass=MetaAggregate):
991
+ """Base class for aggregates."""
893
992
 
894
- # Call the original method with event attribute values.
895
- decorated_method(aggregate, **kwargs)
993
+ INITIAL_VERSION = 1
896
994
 
897
- _created_event_class: Type[CanInitAggregate]
995
+ @staticmethod
996
+ def create_id(*_: Any, **__: Any) -> UUID:
997
+ """Returns a new aggregate ID."""
998
+ return uuid4()
898
999
 
899
- def __new__(cls, *args: Any, **_: Any) -> MetaAggregate[Aggregate]:
900
- """
901
- Configures aggregate class definition.
902
- """
903
- try:
904
- class_annotations = args[2]["__annotations__"]
905
- except KeyError:
906
- class_annotations = None
907
- annotations_mention_id = False
1000
+ @classmethod
1001
+ def _create(
1002
+ cls: type[Self],
1003
+ event_class: type[CanInitAggregate],
1004
+ *,
1005
+ id: UUID | None = None, # noqa: A002
1006
+ **kwargs: Any,
1007
+ ) -> Self:
1008
+ """Constructs a new aggregate object instance."""
1009
+ # Construct the domain event with an ID and a
1010
+ # version, and a topic for the aggregate class.
1011
+ create_id_kwargs = {
1012
+ k: v for k, v in kwargs.items() if k in _create_id_param_names[cls]
1013
+ }
1014
+ if id is not None:
1015
+ originator_id = id
1016
+ if not isinstance(originator_id, UUID):
1017
+ msg = f"Given id was not a UUID: {originator_id}"
1018
+ raise TypeError(msg)
908
1019
  else:
909
- try:
910
- class_annotations.pop("id")
911
- except KeyError:
912
- annotations_mention_id = False
913
- else:
914
- annotations_mention_id = True
915
- aggregate_cls = type.__new__(cls, *args)
916
- if class_annotations:
917
- aggregate_cls = dataclass(eq=False, repr=False)(aggregate_cls)
918
- if annotations_mention_id:
919
- _annotations_mention_id.add(aggregate_cls)
920
- return aggregate_cls
1020
+ originator_id = cls.create_id(**create_id_kwargs)
1021
+ if not isinstance(originator_id, UUID):
1022
+ msg = (
1023
+ f"{cls.create_id.__module__}.{cls.create_id.__qualname__}"
1024
+ f" did not return UUID, it returned: {originator_id}"
1025
+ )
1026
+ raise TypeError(msg)
921
1027
 
922
- def __init__(
923
- cls: MetaAggregate[Aggregate],
924
- *args: Any,
925
- created_event_name: str = "",
926
- ) -> None:
927
- """
928
- Initialises aggregate class by completing the definition of its event classes.
929
- """
930
- super().__init__(*args)
1028
+ # Impose the required common "created" event attribute values.
1029
+ kwargs = kwargs.copy()
1030
+ kwargs.update(
1031
+ originator_topic=get_topic(cls),
1032
+ originator_id=originator_id,
1033
+ originator_version=cls.INITIAL_VERSION,
1034
+ )
1035
+ if kwargs.get("timestamp") is None:
1036
+ kwargs["timestamp"] = event_class.create_timestamp()
931
1037
 
932
- # Identify or define a base event class for this aggregate.
933
- base_event_name = "Event"
934
- base_event_cls: Type[CanMutateAggregate]
935
1038
  try:
936
- base_event_cls = cls.__dict__[base_event_name]
937
- except KeyError:
938
- base_event_cls = cls._define_event_class(
939
- base_event_name, (cls.Event,), None
940
- )
941
- setattr(cls, base_event_name, base_event_cls)
1039
+ created_event = event_class(**kwargs)
1040
+ except TypeError as e:
1041
+ msg = f"Unable to construct '{event_class.__qualname__}' event: {e}"
1042
+ raise TypeError(msg) from e
1043
+ # Construct the aggregate object.
1044
+ agg = cast("Self", created_event.mutate(None))
942
1045
 
943
- # Make sure all events defined on aggregate subclass the base event class.
944
- created_event_classes: Dict[str, Type[CanInitAggregate]] = {}
945
- for name, value in tuple(cls.__dict__.items()):
946
- if name == base_event_name:
947
- # Don't subclass the base event class again.
948
- continue
949
- if name.lower() == name:
950
- # Don't subclass lowercase named attributes that have classes.
951
- continue
952
- if (
953
- isinstance(value, type)
954
- and issubclass(value, AggregateEvent)
955
- and not issubclass(value, base_event_cls)
956
- ):
957
- sub_class = cls._define_event_class(name, (value, base_event_cls), None)
958
- setattr(cls, name, sub_class)
959
- for name, value in tuple(cls.__dict__.items()):
960
- if isinstance(value, type) and issubclass(value, CanInitAggregate):
961
- created_event_classes[name] = value
1046
+ assert agg is not None
1047
+ # Append the domain event to pending list.
1048
+ agg.pending_events.append(created_event)
1049
+ # Return the aggregate.
1050
+ return agg
962
1051
 
963
- # Disallow using both '_created_event_class' and 'created_event_name'.
964
- created_event_class: Type[CanInitAggregate] | None = cls.__dict__.get(
965
- "_created_event_class"
966
- )
967
- if created_event_class and created_event_name:
968
- msg = "Can't use both '_created_event_class' and 'created_event_name'"
969
- raise TypeError(msg)
1052
+ def __base_init__(
1053
+ self, originator_id: UUID, originator_version: int, timestamp: datetime
1054
+ ) -> None:
1055
+ """Initialises an aggregate object with an :data:`id`, a :data:`version`
1056
+ number, and a :data:`timestamp`.
1057
+ """
1058
+ self._id = originator_id
1059
+ self._version = originator_version
1060
+ self._created_on = timestamp
1061
+ self._modified_on = timestamp
1062
+ self._pending_events: list[CanMutateAggregate] = []
970
1063
 
971
- # Identify or define the aggregate's "created" event class.
1064
+ @property
1065
+ def id(self) -> UUID:
1066
+ """The ID of the aggregate."""
1067
+ return self._id
972
1068
 
973
- # Is the init method decorated with a CommandMethodDecorator?
974
- if isinstance(cls.__dict__.get("__init__"), CommandMethodDecorator):
975
- init_decorator: CommandMethodDecorator = cls.__dict__["__init__"]
1069
+ @property
1070
+ def version(self) -> int:
1071
+ """The version number of the aggregate."""
1072
+ return self._version
976
1073
 
977
- # Set the original method on the class (un-decorate __init__).
978
- cls.__init__ = init_decorator.decorated_method # type: ignore
1074
+ @version.setter
1075
+ def version(self, version: int) -> None:
1076
+ self._version = version
979
1077
 
980
- # Disallow using both 'created_event_name' and decorator on __init__.
981
- if created_event_name:
982
- msg = "Can't use both 'created_event_name' and decorator on __init__"
983
- raise TypeError(msg)
984
- # Disallow using both '_created_event_class' and decorator on __init__.
985
- if created_event_class:
986
- msg = "Can't use both '_created_event_class' and decorator on __init__"
987
- raise TypeError(msg)
1078
+ @property
1079
+ def created_on(self) -> datetime:
1080
+ """The date and time when the aggregate was created."""
1081
+ return self._created_on
988
1082
 
989
- # Does the decorator specify a "created" event class?
990
- if init_decorator.given_event_cls:
991
- created_event_class = cast(
992
- Type[CanInitAggregate], init_decorator.given_event_cls
993
- )
994
- # Does the decorator specify a "created" event name?
995
- elif init_decorator.event_cls_name:
996
- created_event_name = init_decorator.event_cls_name
1083
+ @property
1084
+ def modified_on(self) -> datetime:
1085
+ """The date and time when the aggregate was last modified."""
1086
+ return self._modified_on
997
1087
 
998
- # Disallow using decorator on __init__ without event spec.
999
- else:
1000
- msg = "Decorator on __init__ has neither event name nor class"
1001
- raise TypeError(msg)
1088
+ @modified_on.setter
1089
+ def modified_on(self, modified_on: datetime) -> None:
1090
+ self._modified_on = modified_on
1091
+
1092
+ @property
1093
+ def pending_events(self) -> list[CanMutateAggregate]:
1094
+ """A list of pending events."""
1095
+ return self._pending_events
1096
+
1097
+ def trigger_event(
1098
+ self,
1099
+ event_class: type[CanMutateAggregate],
1100
+ **kwargs: Any,
1101
+ ) -> None:
1102
+ """Triggers domain event of given type, by creating
1103
+ an event object and using it to mutate the aggregate.
1104
+ """
1105
+ # Construct the domain event as the
1106
+ # next in the aggregate's sequence.
1107
+ # Use counting to generate the sequence.
1108
+ next_version = self.version + 1
1109
+
1110
+ # Impose the required common domain event attribute values.
1111
+ kwargs = kwargs.copy()
1112
+ kwargs.update(
1113
+ originator_id=self.id,
1114
+ originator_version=next_version,
1115
+ )
1116
+ if kwargs.get("timestamp") is None:
1117
+ kwargs["timestamp"] = event_class.create_timestamp()
1118
+
1119
+ try:
1120
+ new_event = event_class(**kwargs)
1121
+ except TypeError as e:
1122
+ msg = f"Can't construct event {event_class}: {e}"
1123
+ raise TypeError(msg) from None
1124
+
1125
+ # Mutate aggregate with domain event.
1126
+ new_event.mutate(self)
1127
+ # Append the domain event to pending list.
1128
+ self._pending_events.append(new_event)
1129
+
1130
+ def collect_events(self) -> Sequence[CanMutateAggregate]:
1131
+ """Collects and returns a list of pending aggregate
1132
+ :class:`AggregateEvent` objects.
1133
+ """
1134
+ collected = []
1135
+ while self._pending_events:
1136
+ collected.append(self._pending_events.pop(0))
1137
+ return collected
1138
+
1139
+ def __eq__(self, other: object) -> bool:
1140
+ return type(self) is type(other) and self.__dict__ == other.__dict__
1141
+
1142
+ def __repr__(self) -> str:
1143
+ attrs = [
1144
+ f"{k.lstrip('_')}={v!r}"
1145
+ for k, v in self.__dict__.items()
1146
+ if k != "_pending_events"
1147
+ ]
1148
+ return f"{type(self).__name__}({', '.join(attrs)})"
1149
+
1150
+ def __init_subclass__(
1151
+ cls: type[BaseAggregate], *, created_event_name: str = ""
1152
+ ) -> None:
1153
+ """
1154
+ Initialises aggregate subclass by defining __init__ method and event classes.
1155
+ """
1156
+ super().__init_subclass__()
1157
+
1158
+ # Ensure we aren't defining another instance of the same class,
1159
+ # because annotations can get confused when using singledispatchmethod
1160
+ # during class definition e.g. on an aggregate projector function.
1161
+ _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"
1164
+ raise ProgrammingError(msg)
1002
1165
 
1003
- # TODO: Write a test to cover this when "Created" class is explicitly defined.
1004
- # Check if init mentions ID.
1005
- for param_name in inspect.signature(cls.__init__).parameters: # type: ignore
1166
+ # Get the class annotations.
1167
+ class_annotations = cls.__dict__.get("__annotations__", {})
1168
+ try:
1169
+ class_annotations.pop("id")
1170
+ _annotations_mention_id.add(cls)
1171
+ except KeyError:
1172
+ pass
1173
+
1174
+ if "id" in cls.__dict__:
1175
+ msg = f"Setting attribute 'id' on class '{cls.__name__}' is not allowed"
1176
+ raise ProgrammingError(msg)
1177
+
1178
+ # Process the class as a dataclass, if there are annotations.
1179
+ if (
1180
+ class_annotations
1181
+ or cls in _annotations_mention_id
1182
+ or any(dataclasses.is_dataclass(base) for base in cls.__bases__)
1183
+ ):
1184
+ dataclasses.dataclass(eq=False, repr=False)(cls)
1185
+
1186
+ # Remember if __init__ mentions ID.
1187
+ for param_name in inspect.signature(cls.__init__).parameters:
1006
1188
  if param_name == "id":
1007
1189
  _init_mentions_id.add(cls)
1008
1190
  break
1009
1191
 
1010
- if created_event_class:
1011
- # Check specified "created" event class can init aggregate.
1012
- if not issubclass(created_event_class, CanInitAggregate):
1013
- msg = (
1014
- f"{created_event_class} not subclass of {CanInitAggregate.__name__}"
1015
- )
1016
- raise TypeError(msg)
1192
+ # Analyse __init__ attribute, to get __init__ method and @event decorator.
1193
+ init_attr: FunctionType | CommandMethodDecorator | None = cls.__dict__.get(
1194
+ "__init__"
1195
+ )
1196
+ if init_attr is None:
1197
+ # No method, no decorator.
1198
+ init_method: CallableType | None = None
1199
+ init_decorator: CommandMethodDecorator | None = None
1200
+ elif isinstance(init_attr, CommandMethodDecorator):
1201
+ # Method decorated with @event.
1202
+ init_method = init_attr.decorated_func
1203
+ init_decorator = init_attr
1204
+ else:
1205
+ # Undecorated __init__ method.
1206
+ init_decorator = None
1207
+ init_method = init_attr
1017
1208
 
1018
- for sub_class in created_event_classes.values():
1019
- if issubclass(sub_class, created_event_class):
1020
- # We just subclassed the created event class, so reassign it.
1021
- created_event_class = sub_class
1209
+ # Identify or define a base event class for this aggregate.
1210
+ base_event_name = "Event"
1211
+ base_event_cls: type[CanMutateAggregate]
1212
+ try:
1213
+ base_event_cls = cls.__dict__[base_event_name]
1214
+ 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)
1022
1221
 
1023
- # Is a "created" event class already defined that matches the name?
1024
- elif created_event_name and created_event_name in created_event_classes:
1025
- created_event_class = created_event_classes[created_event_name]
1222
+ # Ensure all events defined on this class are subclasses of base event class.
1223
+ created_event_classes: dict[str, type[CanInitAggregate]] = {}
1224
+ for name, value in tuple(cls.__dict__.items()):
1225
+ if name == base_event_name:
1226
+ # Don't subclass the base event class again.
1227
+ continue
1228
+ if name.lower() == name:
1229
+ # Don't subclass lowercase named attributes.
1230
+ 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
1026
1239
 
1027
- # If there is only one class defined, then use it.
1028
- elif len(created_event_classes) == 1 and not created_event_name:
1029
- created_event_class = next(iter(created_event_classes.values()))
1240
+ # Remember all "created" event classes defined on this class.
1241
+ if issubclass(event_class, CanInitAggregate):
1242
+ created_event_classes[name] = event_class
1030
1243
 
1031
- # If there are no "created" event classes already defined, or a name is
1032
- # specified that hasn't matched, then define a "created" event class.
1033
- elif len(created_event_classes) == 0 or created_event_name:
1244
+ # Identify or define the aggregate's "created" event class.
1245
+ created_event_class: type[CanInitAggregate] | None = None
1034
1246
 
1035
- # Decide the base classes for the new "created" event class.
1036
- if created_event_name and len(created_event_classes) == 1:
1037
- base_created_event_cls = next(iter(created_event_classes.values()))
1038
- else:
1039
- for base_cls in cls.__mro__:
1040
- if base_cls is cls:
1041
- continue
1042
- base_created_event_cls = base_cls.__dict__.get(
1043
- "_created_event_class",
1044
- base_cls.__dict__.get("Created"),
1247
+ # Analyse __init__ method decorator.
1248
+ if init_decorator:
1249
+
1250
+ # Does the decorator specify an event class?
1251
+ if init_decorator.given_event_cls:
1252
+
1253
+ # Disallow conflicts between 'created_event_name' and given class.
1254
+ if (
1255
+ created_event_name
1256
+ and created_event_name != init_decorator.given_event_cls.__name__
1257
+ ):
1258
+ msg = (
1259
+ "Given 'created_event_name' conflicts "
1260
+ "with decorator on __init__"
1045
1261
  )
1046
- if base_created_event_cls:
1262
+ raise TypeError(msg)
1263
+
1264
+ # Check given event class can init aggregate.
1265
+ if not issubclass(init_decorator.given_event_cls, CanInitAggregate):
1266
+ msg = (
1267
+ f"class '{init_decorator.given_event_cls.__name__}' "
1268
+ f'not a "created" event class'
1269
+ )
1270
+ raise TypeError(msg)
1271
+
1272
+ # Have we already subclassed the given event class?
1273
+ for sub_class in created_event_classes.values():
1274
+ if issubclass(sub_class, init_decorator.given_event_cls):
1275
+ created_event_class = sub_class
1047
1276
  break
1048
- else: # pragma: no cover
1049
- msg = "Can't decide base class for new 'created' event class"
1277
+ else:
1278
+ created_event_class = init_decorator.given_event_cls
1279
+
1280
+ # Does the decorator specify an event name?
1281
+ elif init_decorator.event_cls_name:
1282
+ # Disallow conflicts between 'created_event_name' and given name.
1283
+ if (
1284
+ created_event_name
1285
+ and created_event_name != init_decorator.event_cls_name
1286
+ ):
1287
+ msg = (
1288
+ "Given 'created_event_name' conflicts "
1289
+ "with decorator on __init__"
1290
+ )
1050
1291
  raise TypeError(msg)
1051
1292
 
1052
- if not created_event_name:
1053
- created_event_name = base_created_event_cls.__name__
1293
+ created_event_name = init_decorator.event_cls_name
1054
1294
 
1055
- # Disallow init method from having variable params if
1056
- # we are using it to define a "created" event class.
1057
- try:
1058
- init_method = cls.__dict__["__init__"]
1059
- except KeyError:
1060
- init_method = None
1295
+ # Disallow using decorator on __init__ without event name or class.
1061
1296
  else:
1297
+ msg = "@event decorator on __init__ has neither event name nor class"
1298
+ raise TypeError(msg)
1299
+
1300
+ # Do we need to define a created event class?
1301
+ if not created_event_class:
1302
+ # If we have a "created" event class that matches the name, then use it.
1303
+ if created_event_name in created_event_classes:
1304
+ created_event_class = created_event_classes[created_event_name]
1305
+ # Otherwise, if we have no name and only one class defined, then use it.
1306
+ elif not created_event_name and len(created_event_classes) == 1:
1307
+ created_event_class = next(iter(created_event_classes.values()))
1308
+
1309
+ # Otherwise, if there are no "created" events, or a name is
1310
+ # specified that hasn't matched, then define a "created" event class.
1311
+ elif len(created_event_classes) == 0 or created_event_name:
1312
+ # Decide the base "created" event class.
1313
+
1062
1314
  try:
1063
- _check_no_variable_params(init_method)
1064
- except TypeError:
1065
- raise
1315
+ # 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:
1321
+ # Look for base class with one nominated "created" event.
1322
+ for base_cls in cls.__mro__:
1323
+ if (
1324
+ base_cls in _created_event_classes
1325
+ and len(_created_event_classes[base_cls]) == 1
1326
+ ):
1327
+ base_created_event_cls = _created_event_classes[base_cls][0]
1328
+ 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)
1066
1343
 
1067
- # Define a "created" event class for this aggregate.
1068
- if issubclass(base_created_event_cls, base_event_cls):
1069
1344
  # Don't subclass from base event class twice.
1070
- bases: Tuple[Type[CanMutateAggregate], ...] = (base_created_event_cls,)
1071
- else:
1072
- bases = (base_created_event_cls, base_event_cls)
1073
- created_event_class = cast(
1074
- Type[CanInitAggregate],
1075
- cls._define_event_class(
1076
- created_event_name,
1077
- bases,
1078
- init_method,
1079
- ),
1080
- )
1081
- # Set the event class as an attribute of the aggregate class.
1082
- setattr(cls, created_event_name, created_event_class)
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
+
1365
+ assert created_event_class or len(created_event_classes) > 1
1083
1366
 
1084
1367
  if created_event_class:
1085
- cls._created_event_class = created_event_class
1368
+ _created_event_classes[cls] = [created_event_class]
1086
1369
  else:
1087
1370
  # Prepare to disallow ambiguity of choice between created event classes.
1088
- aggregate_has_many_created_event_classes[cls] = list(created_event_classes)
1371
+ _created_event_classes[cls] = list(created_event_classes.values())
1089
1372
 
1090
- # Prepare the subsequent event classes.
1373
+ # Find and analyse any @event decorators.
1091
1374
  for attr_name, attr_value in tuple(cls.__dict__.items()):
1092
1375
  event_decorator: CommandMethodDecorator | None = None
1093
1376
 
1094
- if isinstance(attr_value, CommandMethodDecorator):
1095
- event_decorator = attr_value
1377
+ # Ignore a decorator on the __init__ method.
1378
+ if isinstance(attr_value, CommandMethodDecorator) and (
1379
+ attr_value.decorated_func.__name__ == "__init__"
1380
+ ):
1381
+ continue
1096
1382
 
1097
- elif isinstance(attr_value, property) and isinstance(
1383
+ # Handle @property.setter decorator on top of @event decorator.
1384
+ if isinstance(attr_value, property) and isinstance(
1098
1385
  attr_value.fset, CommandMethodDecorator
1099
1386
  ):
1100
1387
  event_decorator = attr_value.fset
1101
1388
  # Inspect the setter method.
1102
- method_signature = inspect.signature(event_decorator.decorated_method)
1389
+ method_signature = inspect.signature(event_decorator.decorated_func)
1103
1390
  assert len(method_signature.parameters) == 2
1104
1391
  event_decorator.is_property_setter = True
1105
1392
  event_decorator.property_setter_arg_name = list(
1106
1393
  method_signature.parameters
1107
1394
  )[1]
1108
- if event_decorator.decorated_method.__name__ != attr_name:
1109
- attr = cls.__dict__[event_decorator.decorated_method.__name__]
1395
+ if event_decorator.decorated_func.__name__ != attr_name:
1396
+ attr = cls.__dict__[event_decorator.decorated_func.__name__]
1110
1397
  if isinstance(attr, CommandMethodDecorator):
1111
1398
  # This is the "x = property(getx, setx) form" where setx
1112
1399
  # is a decorated method.
@@ -1115,13 +1402,16 @@ class MetaAggregate(type, Generic[TAggregate]):
1115
1402
  elif event_decorator.is_name_inferred_from_method:
1116
1403
  # This is the "@property.setter \ @event" form. We don't want
1117
1404
  # event class name inferred from property (not past participle).
1118
- method_name = event_decorator.decorated_method.__name__
1405
+ method_name = event_decorator.decorated_func.__name__
1119
1406
  msg = (
1120
- f"@event under {method_name}() property setter requires "
1121
- "event class name"
1407
+ f"@event decorator under @{method_name}.setter "
1408
+ "requires event name or class"
1122
1409
  )
1123
1410
  raise TypeError(msg)
1124
1411
 
1412
+ elif isinstance(attr_value, CommandMethodDecorator):
1413
+ event_decorator = attr_value
1414
+
1125
1415
  if event_decorator is not None:
1126
1416
  if event_decorator.given_event_cls:
1127
1417
  # Check this is not a "created" event class.
@@ -1134,12 +1424,12 @@ class MetaAggregate(type, Generic[TAggregate]):
1134
1424
 
1135
1425
  # Define event class as subclass of given class.
1136
1426
  given_subclass = cast(
1137
- Type[CanMutateAggregate],
1427
+ "type[CanMutateAggregate]",
1138
1428
  getattr(cls, event_decorator.given_event_cls.__name__),
1139
1429
  )
1140
1430
  event_cls = cls._define_event_class(
1141
1431
  event_decorator.given_event_cls.__name__,
1142
- (cls.DecoratedEvent, given_subclass),
1432
+ (DecoratorEvent, given_subclass),
1143
1433
  None,
1144
1434
  )
1145
1435
 
@@ -1156,22 +1446,22 @@ class MetaAggregate(type, Generic[TAggregate]):
1156
1446
  # Define event class from signature of original method.
1157
1447
  event_cls = cls._define_event_class(
1158
1448
  event_decorator.event_cls_name,
1159
- (cls.DecoratedEvent, base_event_cls),
1160
- event_decorator.decorated_method,
1449
+ (DecoratorEvent, base_event_cls),
1450
+ event_decorator.decorated_func,
1161
1451
  )
1162
1452
 
1163
1453
  # Cache the decorated method for the event class to use.
1164
- decorated_methods[event_cls] = event_decorator.decorated_method
1454
+ _decorated_funcs[event_cls] = event_decorator.decorated_func
1165
1455
 
1166
1456
  # Set the event class as an attribute of the aggregate class.
1167
1457
  setattr(cls, event_cls.__name__, event_cls)
1168
1458
 
1169
1459
  # Remember which event class to trigger.
1170
- decorated_event_classes[event_decorator] = cast(
1171
- Type[MetaAggregate.DecoratedEvent], event_cls
1460
+ decorator_event_classes[event_decorator] = cast(
1461
+ "type[DecoratorEvent]", event_cls
1172
1462
  )
1173
1463
 
1174
- # Check any create_id method defined on this class is static or class method.
1464
+ # Check any create_id() method defined on this class is static or class method.
1175
1465
  if "create_id" in cls.__dict__ and not isinstance(
1176
1466
  cls.__dict__["create_id"], (staticmethod, classmethod)
1177
1467
  ):
@@ -1182,13 +1472,12 @@ class MetaAggregate(type, Generic[TAggregate]):
1182
1472
  raise TypeError(msg)
1183
1473
 
1184
1474
  # Get the parameters of the create_id method that will be used by this class.
1185
- cls._create_id_param_names: List[str] = []
1186
1475
  for name, param in inspect.signature(cls.create_id).parameters.items():
1187
1476
  if param.kind in [param.KEYWORD_ONLY, param.POSITIONAL_OR_KEYWORD]:
1188
- cls._create_id_param_names.append(name)
1477
+ _create_id_param_names[cls].append(name)
1189
1478
 
1190
- # Define event classes for all events on bases.
1191
- for aggregate_base_class in args[1]:
1479
+ # Define event classes for all events on all bases if not defined on this class.
1480
+ for aggregate_base_class in cls.__bases__:
1192
1481
  for name, value in aggregate_base_class.__dict__.items():
1193
1482
  if (
1194
1483
  isinstance(value, type)
@@ -1196,267 +1485,36 @@ class MetaAggregate(type, Generic[TAggregate]):
1196
1485
  and name not in cls.__dict__
1197
1486
  and name.lower() != name
1198
1487
  ):
1199
- sub_class = cls._define_event_class(
1488
+ event_class = cls._define_event_class(
1200
1489
  name, (base_event_cls, value), None
1201
1490
  )
1202
- setattr(cls, name, sub_class)
1491
+ setattr(cls, name, event_class)
1203
1492
 
1204
- def _define_event_class(
1205
- cls,
1206
- name: str,
1207
- bases: Tuple[Type[CanMutateAggregate], ...],
1208
- apply_method: CommandMethod | None,
1209
- ) -> Type[CanMutateAggregate]:
1210
- # Define annotations for the event class (specs the init method).
1211
- annotations = {}
1212
- if apply_method is not None:
1213
- method_signature = inspect.signature(apply_method)
1214
- supers = {
1215
- s for b in bases for s in b.__mro__ if hasattr(s, "__annotations__")
1216
- }
1217
- super_annotations = {a for s in supers for a in s.__annotations__}
1218
- for param_name, param in list(method_signature.parameters.items())[1:]:
1219
- # Don't define 'id' on a "created" class.
1220
- if param_name == "id" and apply_method.__name__ == "__init__":
1221
- continue
1222
- # Don't override super class annotations, unless no default on param.
1223
- if param_name not in super_annotations or param.default == param.empty:
1224
- annotations[param_name] = param.annotation or "typing.Any"
1225
- event_cls_qualname = f"{cls.__qualname__}.{name}"
1226
- event_cls_dict = {
1227
- "__annotations__": annotations,
1228
- "__module__": cls.__module__,
1229
- "__qualname__": event_cls_qualname,
1230
- }
1231
-
1232
- # Create the event class object.
1233
- return cast(Type[CanMutateAggregate], type(name, bases, event_cls_dict))
1234
-
1235
- def __call__(
1236
- cls: MetaAggregate[TAggregate], *args: Any, **kwargs: Any
1237
- ) -> TAggregate:
1238
- try:
1239
- created_event_classes = aggregate_has_many_created_event_classes[cls]
1240
- msg = (
1241
- """Can't decide which of many "created" event classes to use: """
1242
- f"""'{"', '".join(created_event_classes)}'. Please use class """
1243
- "arg 'created_event_name' or @event decorator on __init__ method."
1244
- )
1245
- raise TypeError(msg)
1246
- except KeyError:
1247
- pass
1248
-
1249
- cls_init: FunctionType | WrapperDescriptorType = cls.__init__ # type: ignore
1250
- kwargs = _coerce_args_to_kwargs(
1251
- cls_init,
1252
- args,
1253
- kwargs,
1254
- expects_id=cls in _annotations_mention_id,
1255
- )
1256
- return cls._create(
1257
- event_class=cls._created_event_class,
1258
- **kwargs,
1259
- )
1260
-
1261
- def _create(
1262
- cls: MetaAggregate[TAggregate],
1263
- event_class: Type[CanInitAggregate],
1264
- **kwargs: Any,
1265
- ) -> TAggregate:
1266
- raise NotImplementedError # pragma: no cover
1267
-
1268
- @staticmethod
1269
- def create_id(**_: Any) -> UUID:
1270
- """
1271
- Returns a new aggregate ID.
1272
- """
1273
- return uuid4()
1274
-
1275
-
1276
- class Aggregate(metaclass=MetaAggregate):
1277
- """
1278
- Base class for aggregate roots.
1279
-
1280
- .. automethod:: _create
1281
- """
1282
-
1283
- @classmethod
1284
- def _create(
1285
- cls: Type[TAggregate],
1286
- event_class: Type[CanInitAggregate],
1287
- *,
1288
- id: UUID | None = None, # noqa: A002
1289
- **kwargs: Any,
1290
- ) -> TAggregate:
1291
- """
1292
- Constructs a new aggregate object instance.
1293
- """
1294
- # Construct the domain event with an ID and a
1295
- # version, and a topic for the aggregate class.
1296
- create_id_kwargs = {
1297
- k: v for k, v in kwargs.items() if k in cls._create_id_param_names
1298
- }
1299
- originator_id = id or cls.create_id(**create_id_kwargs)
1300
-
1301
- # Impose the required common "created" event attribute values.
1302
- kwargs = kwargs.copy()
1303
- kwargs.update(
1304
- originator_topic=get_topic(cls),
1305
- originator_id=originator_id,
1306
- originator_version=cls.INITIAL_VERSION,
1307
- )
1308
- if kwargs.get("timestamp") is None:
1309
- kwargs["timestamp"] = event_class.create_timestamp()
1310
-
1311
- try:
1312
- created_event = event_class(**kwargs)
1313
- except TypeError as e:
1314
- msg = f"Unable to construct '{event_class.__name__}' event: {e}"
1315
- raise TypeError(msg) from None
1316
- # Construct the aggregate object.
1317
- agg = cast(TAggregate, created_event.mutate(None))
1318
-
1319
- assert agg is not None
1320
- # Append the domain event to pending list.
1321
- agg.pending_events.append(created_event)
1322
- # Return the aggregate.
1323
- return agg
1324
-
1325
- def __base_init__(
1326
- self, originator_id: UUID, originator_version: int, timestamp: datetime
1327
- ) -> None:
1328
- """
1329
- Initialises an aggregate object with an :data:`id`, a :data:`version`
1330
- number, and a :data:`timestamp`.
1331
- """
1332
- self._id = originator_id
1333
- self._version = originator_version
1334
- self._created_on = timestamp
1335
- self._modified_on = timestamp
1336
- self._pending_events: List[CanMutateAggregate] = []
1337
-
1338
- @property
1339
- def id(self) -> UUID:
1340
- """
1341
- The ID of the aggregate.
1342
- """
1343
- return self._id
1344
-
1345
- @property
1346
- def version(self) -> int:
1347
- """
1348
- The version number of the aggregate.
1349
- """
1350
- return self._version
1351
-
1352
- @version.setter
1353
- def version(self, version: int) -> None:
1354
- self._version = version
1355
-
1356
- @property
1357
- def created_on(self) -> datetime:
1358
- """
1359
- The date and time when the aggregate was created.
1360
- """
1361
- return self._created_on
1362
-
1363
- @property
1364
- def modified_on(self) -> datetime:
1365
- """
1366
- The date and time when the aggregate was last modified.
1367
- """
1368
- return self._modified_on
1369
-
1370
- @modified_on.setter
1371
- def modified_on(self, modified_on: datetime) -> None:
1372
- self._modified_on = modified_on
1373
-
1374
- @property
1375
- def pending_events(self) -> List[CanMutateAggregate]:
1376
- """
1377
- A list of pending events.
1378
- """
1379
- return self._pending_events
1380
1493
 
1494
+ class Aggregate(BaseAggregate):
1381
1495
  class Event(AggregateEvent):
1382
1496
  pass
1383
1497
 
1384
1498
  class Created(Event, AggregateCreated):
1385
1499
  pass
1386
1500
 
1387
- def __eq__(self, other: object) -> bool:
1388
- return type(self) is type(other) and self.__dict__ == other.__dict__
1389
-
1390
- def __repr__(self) -> str:
1391
- attrs = [
1392
- f"{k.lstrip('_')}={v!r}"
1393
- for k, v in self.__dict__.items()
1394
- if k != "_pending_events"
1395
- ]
1396
- return f"{type(self).__name__}({', '.join(attrs)})"
1397
-
1398
- def trigger_event(
1399
- self,
1400
- event_class: Type[CanMutateAggregate],
1401
- **kwargs: Any,
1402
- ) -> None:
1403
- """
1404
- Triggers domain event of given type, by creating
1405
- an event object and using it to mutate the aggregate.
1406
- """
1407
- # Construct the domain event as the
1408
- # next in the aggregate's sequence.
1409
- # Use counting to generate the sequence.
1410
- next_version = self.version + 1
1411
-
1412
- # Impose the required common domain event attribute values.
1413
- kwargs = kwargs.copy()
1414
- kwargs.update(
1415
- originator_id=self.id,
1416
- originator_version=next_version,
1417
- )
1418
- if kwargs.get("timestamp") is None:
1419
- kwargs["timestamp"] = event_class.create_timestamp()
1420
1501
 
1421
- try:
1422
- new_event = event_class(**kwargs)
1423
- except TypeError as e:
1424
- msg = f"Can't construct event {event_class}: {e}"
1425
- raise TypeError(msg) from None
1426
-
1427
- # Mutate aggregate with domain event.
1428
- new_event.mutate(self)
1429
- # Append the domain event to pending list.
1430
- self._pending_events.append(new_event)
1431
-
1432
- def collect_events(self) -> Sequence[CanMutateAggregate]:
1433
- """
1434
- Collects and returns a list of pending aggregate
1435
- :class:`AggregateEvent` objects.
1436
- """
1437
- collected = []
1438
- while self._pending_events:
1439
- collected.append(self._pending_events.pop(0))
1440
- return collected
1502
+ @overload
1503
+ def aggregate(*, created_event_name: str) -> Callable[[Any], type[Aggregate]]:
1504
+ pass # pragma: no cover
1441
1505
 
1442
1506
 
1443
- # @overload
1444
- # def aggregate(*, created_event_name: str) -> Callable[[Any], Type[Aggregate]]:
1445
- # ...
1446
- #
1447
- #
1448
- # @overload
1449
- # def aggregate(cls: Any) -> Type[Aggregate]:
1450
- # ...
1507
+ @overload
1508
+ def aggregate(cls: Any) -> type[Aggregate]:
1509
+ pass # pragma: no cover
1451
1510
 
1452
1511
 
1453
1512
  def aggregate(
1454
1513
  cls: Any | None = None,
1455
1514
  *,
1456
1515
  created_event_name: str = "",
1457
- ) -> Type[Aggregate] | Callable[[Any], Type[Aggregate]]:
1458
- """
1459
- Converts the class that was passed in to inherit from Aggregate.
1516
+ ) -> type[Aggregate] | Callable[[Any], type[Aggregate]]:
1517
+ """Converts the class that was passed in to inherit from Aggregate.
1460
1518
 
1461
1519
  .. code-block:: python
1462
1520
 
@@ -1472,7 +1530,7 @@ def aggregate(
1472
1530
  pass
1473
1531
  """
1474
1532
 
1475
- def decorator(cls_: Any) -> Type[Aggregate]:
1533
+ def decorator(cls_: Any) -> type[Aggregate]:
1476
1534
  if issubclass(cls_, Aggregate):
1477
1535
  msg = f"{cls_.__qualname__} is already an Aggregate"
1478
1536
  raise TypeError(msg)
@@ -1498,8 +1556,7 @@ def aggregate(
1498
1556
 
1499
1557
 
1500
1558
  class OriginatorIDError(EventSourcingError):
1501
- """
1502
- Raised when a domain event can't be applied to
1559
+ """Raised when a domain event can't be applied to
1503
1560
  an aggregate due to an ID mismatch indicating
1504
1561
  the domain event is not in the aggregate's
1505
1562
  sequence of events.
@@ -1507,43 +1564,23 @@ class OriginatorIDError(EventSourcingError):
1507
1564
 
1508
1565
 
1509
1566
  class OriginatorVersionError(EventSourcingError):
1510
- """
1511
- Raised when a domain event can't be applied to
1567
+ """Raised when a domain event can't be applied to
1512
1568
  an aggregate due to version mismatch indicating
1513
1569
  the domain event is not the next in the aggregate's
1514
1570
  sequence of events.
1515
1571
  """
1516
1572
 
1517
1573
 
1518
- class VersionError(OriginatorVersionError):
1519
- """
1520
- Old name for 'OriginatorVersionError'.
1521
-
1522
- This class exists to maintain backwards-compatibility
1523
- but will be removed in a future version Please use
1524
- 'OriginatorVersionError' instead.
1525
- """
1526
-
1527
-
1528
1574
  class SnapshotProtocol(DomainEventProtocol, Protocol):
1529
1575
  @property
1530
- def topic(self) -> str:
1531
- """
1532
- Snapshots have a read-only 'topic'.
1533
- """
1534
-
1535
- @property
1536
- def state(self) -> Dict[str, Any]:
1537
- """
1538
- Snapshots have a read-only 'state'.
1539
- """
1576
+ def state(self) -> dict[str, Any]:
1577
+ """Snapshots have a read-only 'state'."""
1578
+ raise NotImplementedError # pragma: no cover
1540
1579
 
1541
1580
  # TODO: Improve on this 'Any'.
1542
1581
  @classmethod
1543
1582
  def take(cls: Any, aggregate: Any) -> Any:
1544
- """
1545
- Snapshots have a 'take()' class method.
1546
- """
1583
+ """Snapshots have a 'take()' class method."""
1547
1584
 
1548
1585
 
1549
1586
  TCanSnapshotAggregate = TypeVar("TCanSnapshotAggregate", bound="CanSnapshotAggregate")
@@ -1553,14 +1590,22 @@ class CanSnapshotAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
1553
1590
  topic: str
1554
1591
  state: Any
1555
1592
 
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
1602
+
1556
1603
  @classmethod
1557
1604
  def take(
1558
- cls: Type[TCanSnapshotAggregate],
1605
+ cls,
1559
1606
  aggregate: MutableOrImmutableAggregate,
1560
- ) -> TCanSnapshotAggregate:
1561
- """
1562
- Creates a snapshot of the given :class:`Aggregate` object.
1563
- """
1607
+ ) -> Self:
1608
+ """Creates a snapshot of the given :class:`Aggregate` object."""
1564
1609
  aggregate_state = dict(aggregate.__dict__)
1565
1610
  class_version = getattr(type(aggregate), "class_version", 1)
1566
1611
  if class_version > 1:
@@ -1569,7 +1614,7 @@ class CanSnapshotAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
1569
1614
  aggregate_state.pop("_id")
1570
1615
  aggregate_state.pop("_version")
1571
1616
  aggregate_state.pop("_pending_events")
1572
- return cls( # type: ignore
1617
+ return cls(
1573
1618
  originator_id=aggregate.id,
1574
1619
  originator_version=aggregate.version,
1575
1620
  timestamp=cls.create_timestamp(),
@@ -1578,10 +1623,8 @@ class CanSnapshotAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
1578
1623
  )
1579
1624
 
1580
1625
  def mutate(self, _: None) -> Aggregate:
1581
- """
1582
- Reconstructs the snapshotted :class:`Aggregate` object.
1583
- """
1584
- cls = cast(Type[Aggregate], resolve_topic(self.topic))
1626
+ """Reconstructs the snapshotted :class:`Aggregate` object."""
1627
+ cls = cast("type[Aggregate]", resolve_topic(self.topic))
1585
1628
  aggregate_state = dict(self.state)
1586
1629
  from_version = aggregate_state.pop("class_version", 1)
1587
1630
  class_version = getattr(cls, "class_version", 1)
@@ -1599,9 +1642,9 @@ class CanSnapshotAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
1599
1642
  return aggregate
1600
1643
 
1601
1644
 
1645
+ @dataclass(frozen=True)
1602
1646
  class Snapshot(CanSnapshotAggregate, DomainEvent):
1603
- """
1604
- Snapshots represent the state of an aggregate at a particular
1647
+ """Snapshots represent the state of an aggregate at a particular
1605
1648
  version.
1606
1649
 
1607
1650
  Constructor arguments:
@@ -1614,4 +1657,4 @@ class Snapshot(CanSnapshotAggregate, DomainEvent):
1614
1657
  """
1615
1658
 
1616
1659
  topic: str
1617
- state: Dict[str, Any]
1660
+ state: dict[str, Any]