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