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