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/application.py +92 -68
- eventsourcing/domain.py +479 -226
- eventsourcing/interface.py +10 -2
- eventsourcing/persistence.py +121 -33
- eventsourcing/popo.py +16 -14
- eventsourcing/postgres.py +20 -20
- eventsourcing/projection.py +25 -20
- eventsourcing/sqlite.py +28 -18
- eventsourcing/system.py +130 -90
- eventsourcing/tests/application.py +14 -97
- eventsourcing/tests/persistence.py +86 -18
- eventsourcing/tests/postgres_utils.py +27 -7
- eventsourcing/utils.py +1 -1
- {eventsourcing-9.4.4.dist-info → eventsourcing-9.4.6.dist-info}/METADATA +18 -12
- eventsourcing-9.4.6.dist-info/RECORD +26 -0
- eventsourcing-9.4.4.dist-info/RECORD +0 -26
- {eventsourcing-9.4.4.dist-info → eventsourcing-9.4.6.dist-info}/AUTHORS +0 -0
- {eventsourcing-9.4.4.dist-info → eventsourcing-9.4.6.dist-info}/LICENSE +0 -0
- {eventsourcing-9.4.4.dist-info → eventsourcing-9.4.6.dist-info}/WHEEL +0 -0
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
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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,
|
|
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(
|
|
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:
|
|
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
|
-
|
|
232
|
-
|
|
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
|
|
242
|
-
argument. The argument typed as ``Optional
|
|
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
|
-
|
|
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
|
-
|
|
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"] =
|
|
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:
|
|
642
|
+
arg: type[CanMutateAggregate[Any]], /
|
|
578
643
|
) -> Callable[[TDecoratableType], TDecoratableType]:
|
|
579
|
-
"""Signature for calling ``@event`` decorator with event
|
|
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
|
-
|
|
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],
|
|
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
|
-
|
|
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 = "
|
|
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) ->
|
|
1078
|
+
def create_id(*_: Any, **__: Any) -> TAggregateID:
|
|
997
1079
|
"""Returns a new aggregate ID."""
|
|
998
|
-
|
|
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:
|
|
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
|
-
|
|
1021
|
-
|
|
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,
|
|
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) ->
|
|
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
|
|
1163
|
-
|
|
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
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
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
|
|
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
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
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"
|
|
1310
|
-
# specified that hasn't matched, then
|
|
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
|
-
|
|
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
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
1495
|
-
class Event(AggregateEvent):
|
|
1496
|
-
pass
|
|
1686
|
+
explicit_topic = cls.__dict__.get("TOPIC", None)
|
|
1497
1687
|
|
|
1498
|
-
|
|
1499
|
-
|
|
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
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
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
|
-
|
|
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) ->
|
|
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
|
-
|
|
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
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
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) ->
|
|
1807
|
+
def mutate(self, _: None) -> BaseAggregate[TAggregateID]:
|
|
1626
1808
|
"""Reconstructs the snapshotted :class:`Aggregate` object."""
|
|
1627
|
-
cls = cast(
|
|
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:
|
|
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
|