eventsourcing 9.4.0b2__py3-none-any.whl → 9.4.0b3__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 +211 -291
- eventsourcing/interface.py +10 -24
- eventsourcing/persistence.py +91 -212
- eventsourcing/popo.py +2 -2
- eventsourcing/postgres.py +7 -10
- eventsourcing/projection.py +17 -40
- eventsourcing/sqlite.py +4 -7
- eventsourcing/system.py +89 -156
- 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.0b3.dist-info}/METADATA +1 -1
- eventsourcing-9.4.0b3.dist-info/RECORD +26 -0
- eventsourcing-9.4.0b2.dist-info/RECORD +0 -26
- {eventsourcing-9.4.0b2.dist-info → eventsourcing-9.4.0b3.dist-info}/AUTHORS +0 -0
- {eventsourcing-9.4.0b2.dist-info → eventsourcing-9.4.0b3.dist-info}/LICENSE +0 -0
- {eventsourcing-9.4.0b2.dist-info → eventsourcing-9.4.0b3.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)
|
|
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)]
|
|
795
740
|
|
|
796
|
-
#
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
# Select event attributes mentioned in method signature.
|
|
800
|
-
kwargs = _filter_kwargs_for_method_params(self.__dict__, decorated_method)
|
|
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,7 +943,7 @@ class MetaAggregate(EventsourcingType, Generic[TAggregate], type):
|
|
|
985
943
|
}
|
|
986
944
|
|
|
987
945
|
# Create the event class object.
|
|
988
|
-
return cast(type[CanMutateAggregate], type(name, bases, event_cls_dict))
|
|
946
|
+
return cast("type[CanMutateAggregate]", type(name, bases, event_cls_dict))
|
|
989
947
|
|
|
990
948
|
def __call__(
|
|
991
949
|
cls: MetaAggregate[TAggregate], *args: Any, **kwargs: Any
|
|
@@ -1001,9 +959,8 @@ class MetaAggregate(EventsourcingType, Generic[TAggregate], type):
|
|
|
1001
959
|
)
|
|
1002
960
|
raise TypeError(msg)
|
|
1003
961
|
|
|
1004
|
-
cls_init: FunctionType | WrapperDescriptorType = cls.__init__ # type: ignore
|
|
1005
962
|
kwargs = _coerce_args_to_kwargs(
|
|
1006
|
-
|
|
963
|
+
cls.__init__, # type: ignore[misc]
|
|
1007
964
|
args,
|
|
1008
965
|
kwargs,
|
|
1009
966
|
expects_id=cls in _annotations_mention_id,
|
|
@@ -1024,17 +981,13 @@ class MetaAggregate(EventsourcingType, Generic[TAggregate], type):
|
|
|
1024
981
|
|
|
1025
982
|
|
|
1026
983
|
class BaseAggregate(metaclass=MetaAggregate):
|
|
1027
|
-
"""
|
|
1028
|
-
Base class for aggregates.
|
|
1029
|
-
"""
|
|
984
|
+
"""Base class for aggregates."""
|
|
1030
985
|
|
|
1031
986
|
INITIAL_VERSION = 1
|
|
1032
987
|
|
|
1033
988
|
@staticmethod
|
|
1034
989
|
def create_id(*_: Any, **__: Any) -> UUID:
|
|
1035
|
-
"""
|
|
1036
|
-
Returns a new aggregate ID.
|
|
1037
|
-
"""
|
|
990
|
+
"""Returns a new aggregate ID."""
|
|
1038
991
|
return uuid4()
|
|
1039
992
|
|
|
1040
993
|
@classmethod
|
|
@@ -1045,9 +998,7 @@ class BaseAggregate(metaclass=MetaAggregate):
|
|
|
1045
998
|
id: UUID | None = None, # noqa: A002
|
|
1046
999
|
**kwargs: Any,
|
|
1047
1000
|
) -> Self:
|
|
1048
|
-
"""
|
|
1049
|
-
Constructs a new aggregate object instance.
|
|
1050
|
-
"""
|
|
1001
|
+
"""Constructs a new aggregate object instance."""
|
|
1051
1002
|
# Construct the domain event with an ID and a
|
|
1052
1003
|
# version, and a topic for the aggregate class.
|
|
1053
1004
|
create_id_kwargs = {
|
|
@@ -1083,19 +1034,18 @@ class BaseAggregate(metaclass=MetaAggregate):
|
|
|
1083
1034
|
msg = f"Unable to construct '{event_class.__qualname__}' event: {e}"
|
|
1084
1035
|
raise TypeError(msg) from e
|
|
1085
1036
|
# Construct the aggregate object.
|
|
1086
|
-
agg = cast(Self, created_event.mutate(None))
|
|
1037
|
+
agg = cast("Self", created_event.mutate(None))
|
|
1087
1038
|
|
|
1088
1039
|
assert agg is not None
|
|
1089
1040
|
# Append the domain event to pending list.
|
|
1090
|
-
agg.
|
|
1041
|
+
agg.pending_events.append(created_event)
|
|
1091
1042
|
# Return the aggregate.
|
|
1092
1043
|
return agg
|
|
1093
1044
|
|
|
1094
1045
|
def __base_init__(
|
|
1095
1046
|
self, originator_id: UUID, originator_version: int, timestamp: datetime
|
|
1096
1047
|
) -> None:
|
|
1097
|
-
"""
|
|
1098
|
-
Initialises an aggregate object with an :data:`id`, a :data:`version`
|
|
1048
|
+
"""Initialises an aggregate object with an :data:`id`, a :data:`version`
|
|
1099
1049
|
number, and a :data:`timestamp`.
|
|
1100
1050
|
"""
|
|
1101
1051
|
self._id = originator_id
|
|
@@ -1106,16 +1056,12 @@ class BaseAggregate(metaclass=MetaAggregate):
|
|
|
1106
1056
|
|
|
1107
1057
|
@property
|
|
1108
1058
|
def id(self) -> UUID:
|
|
1109
|
-
"""
|
|
1110
|
-
The ID of the aggregate.
|
|
1111
|
-
"""
|
|
1059
|
+
"""The ID of the aggregate."""
|
|
1112
1060
|
return self._id
|
|
1113
1061
|
|
|
1114
1062
|
@property
|
|
1115
1063
|
def version(self) -> int:
|
|
1116
|
-
"""
|
|
1117
|
-
The version number of the aggregate.
|
|
1118
|
-
"""
|
|
1064
|
+
"""The version number of the aggregate."""
|
|
1119
1065
|
return self._version
|
|
1120
1066
|
|
|
1121
1067
|
@version.setter
|
|
@@ -1124,16 +1070,12 @@ class BaseAggregate(metaclass=MetaAggregate):
|
|
|
1124
1070
|
|
|
1125
1071
|
@property
|
|
1126
1072
|
def created_on(self) -> datetime:
|
|
1127
|
-
"""
|
|
1128
|
-
The date and time when the aggregate was created.
|
|
1129
|
-
"""
|
|
1073
|
+
"""The date and time when the aggregate was created."""
|
|
1130
1074
|
return self._created_on
|
|
1131
1075
|
|
|
1132
1076
|
@property
|
|
1133
1077
|
def modified_on(self) -> datetime:
|
|
1134
|
-
"""
|
|
1135
|
-
The date and time when the aggregate was last modified.
|
|
1136
|
-
"""
|
|
1078
|
+
"""The date and time when the aggregate was last modified."""
|
|
1137
1079
|
return self._modified_on
|
|
1138
1080
|
|
|
1139
1081
|
@modified_on.setter
|
|
@@ -1142,9 +1084,7 @@ class BaseAggregate(metaclass=MetaAggregate):
|
|
|
1142
1084
|
|
|
1143
1085
|
@property
|
|
1144
1086
|
def pending_events(self) -> list[CanMutateAggregate]:
|
|
1145
|
-
"""
|
|
1146
|
-
A list of pending events.
|
|
1147
|
-
"""
|
|
1087
|
+
"""A list of pending events."""
|
|
1148
1088
|
return self._pending_events
|
|
1149
1089
|
|
|
1150
1090
|
def trigger_event(
|
|
@@ -1152,8 +1092,7 @@ class BaseAggregate(metaclass=MetaAggregate):
|
|
|
1152
1092
|
event_class: type[CanMutateAggregate],
|
|
1153
1093
|
**kwargs: Any,
|
|
1154
1094
|
) -> None:
|
|
1155
|
-
"""
|
|
1156
|
-
Triggers domain event of given type, by creating
|
|
1095
|
+
"""Triggers domain event of given type, by creating
|
|
1157
1096
|
an event object and using it to mutate the aggregate.
|
|
1158
1097
|
"""
|
|
1159
1098
|
# Construct the domain event as the
|
|
@@ -1182,8 +1121,7 @@ class BaseAggregate(metaclass=MetaAggregate):
|
|
|
1182
1121
|
self._pending_events.append(new_event)
|
|
1183
1122
|
|
|
1184
1123
|
def collect_events(self) -> Sequence[CanMutateAggregate]:
|
|
1185
|
-
"""
|
|
1186
|
-
Collects and returns a list of pending aggregate
|
|
1124
|
+
"""Collects and returns a list of pending aggregate
|
|
1187
1125
|
:class:`AggregateEvent` objects.
|
|
1188
1126
|
"""
|
|
1189
1127
|
collected = []
|
|
@@ -1238,7 +1176,6 @@ class BaseAggregate(metaclass=MetaAggregate):
|
|
|
1238
1176
|
setattr(cls, base_event_name, base_event_cls)
|
|
1239
1177
|
|
|
1240
1178
|
# Make sure all events defined on aggregate subclass the base event class.
|
|
1241
|
-
created_event_classes: dict[str, type[CanInitAggregate]] = {}
|
|
1242
1179
|
for name, value in tuple(cls.__dict__.items()):
|
|
1243
1180
|
if name == base_event_name:
|
|
1244
1181
|
# Don't subclass the base event class again.
|
|
@@ -1253,19 +1190,30 @@ class BaseAggregate(metaclass=MetaAggregate):
|
|
|
1253
1190
|
):
|
|
1254
1191
|
sub_class = cls._define_event_class(name, (value, base_event_cls), None)
|
|
1255
1192
|
setattr(cls, name, sub_class)
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1193
|
+
created_event_classes: dict[str, type[CanInitAggregate]] = {
|
|
1194
|
+
name: value
|
|
1195
|
+
for name, value in cls.__dict__.items()
|
|
1196
|
+
if isinstance(value, type) and issubclass(value, CanInitAggregate)
|
|
1197
|
+
}
|
|
1198
|
+
# Analyse the __init__ attribute.
|
|
1199
|
+
init_attr: FunctionType | CommandMethodDecorator | None = cls.__dict__.get(
|
|
1200
|
+
"__init__"
|
|
1201
|
+
)
|
|
1202
|
+
if init_attr is None:
|
|
1203
|
+
init_decorator: CommandMethodDecorator | None = None
|
|
1204
|
+
init_method: CallableType | None = None
|
|
1205
|
+
elif isinstance(init_attr, CommandMethodDecorator):
|
|
1206
|
+
init_decorator = init_attr
|
|
1207
|
+
init_method = init_attr.decorated_func
|
|
1208
|
+
else:
|
|
1209
|
+
init_decorator = None
|
|
1210
|
+
init_method = init_attr
|
|
1259
1211
|
|
|
1260
1212
|
# Identify or define the aggregate's "created" event class.
|
|
1261
1213
|
created_event_class: type[CanInitAggregate] | None = None
|
|
1262
1214
|
|
|
1263
|
-
#
|
|
1264
|
-
if
|
|
1265
|
-
init_decorator: CommandMethodDecorator = cls.__dict__["__init__"]
|
|
1266
|
-
|
|
1267
|
-
# Set the original method on the class (un-decorate __init__).
|
|
1268
|
-
cls.__init__ = init_decorator.decorated_method # type: ignore
|
|
1215
|
+
# Does class have an init method decorated with a CommandMethodDecorator?
|
|
1216
|
+
if init_decorator:
|
|
1269
1217
|
|
|
1270
1218
|
# Disallow using both 'created_event_name' and decorator on __init__.
|
|
1271
1219
|
if created_event_name:
|
|
@@ -1275,7 +1223,7 @@ class BaseAggregate(metaclass=MetaAggregate):
|
|
|
1275
1223
|
# Does the decorator specify a "created" event class?
|
|
1276
1224
|
if init_decorator.given_event_cls:
|
|
1277
1225
|
created_event_class = cast(
|
|
1278
|
-
type[CanInitAggregate], init_decorator.given_event_cls
|
|
1226
|
+
"type[CanInitAggregate]", init_decorator.given_event_cls
|
|
1279
1227
|
)
|
|
1280
1228
|
|
|
1281
1229
|
# Does the decorator specify a "created" event name?
|
|
@@ -1287,7 +1235,6 @@ class BaseAggregate(metaclass=MetaAggregate):
|
|
|
1287
1235
|
msg = "Decorator on __init__ has neither event name nor class"
|
|
1288
1236
|
raise TypeError(msg)
|
|
1289
1237
|
|
|
1290
|
-
# TODO: Write a test to cover this when "Created" class is explicitly defined.
|
|
1291
1238
|
# Check if init mentions ID.
|
|
1292
1239
|
for param_name in inspect.signature(cls.__init__).parameters:
|
|
1293
1240
|
if param_name == "id":
|
|
@@ -1323,7 +1270,7 @@ class BaseAggregate(metaclass=MetaAggregate):
|
|
|
1323
1270
|
if created_event_name and len(created_event_classes) == 1:
|
|
1324
1271
|
base_created_event_cls = next(iter(created_event_classes.values()))
|
|
1325
1272
|
else:
|
|
1326
|
-
#
|
|
1273
|
+
# TODO: This could probably be improved.
|
|
1327
1274
|
# Look for first class in MRO that has one specified "created" class.
|
|
1328
1275
|
for base_cls in cls.__mro__:
|
|
1329
1276
|
if (
|
|
@@ -1333,7 +1280,7 @@ class BaseAggregate(metaclass=MetaAggregate):
|
|
|
1333
1280
|
base_created_event_cls = _created_event_classes[base_cls][0]
|
|
1334
1281
|
break
|
|
1335
1282
|
else: # pragma: no cover
|
|
1336
|
-
#
|
|
1283
|
+
# TODO: Write a test to cover this.
|
|
1337
1284
|
msg = (
|
|
1338
1285
|
"Can't find base aggregate class with "
|
|
1339
1286
|
"a specified 'created' event class"
|
|
@@ -1345,15 +1292,8 @@ class BaseAggregate(metaclass=MetaAggregate):
|
|
|
1345
1292
|
|
|
1346
1293
|
# Disallow init method from having variable params if
|
|
1347
1294
|
# we are using it to define a "created" event class.
|
|
1348
|
-
|
|
1349
|
-
init_method
|
|
1350
|
-
except KeyError:
|
|
1351
|
-
init_method = None
|
|
1352
|
-
else:
|
|
1353
|
-
try:
|
|
1354
|
-
_check_no_variable_params(init_method)
|
|
1355
|
-
except TypeError:
|
|
1356
|
-
raise
|
|
1295
|
+
if init_method:
|
|
1296
|
+
_raise_type_error_if_func_has_variable_params(init_method)
|
|
1357
1297
|
|
|
1358
1298
|
# Define a "created" event class for this aggregate.
|
|
1359
1299
|
if issubclass(base_created_event_cls, base_event_cls):
|
|
@@ -1362,7 +1302,7 @@ class BaseAggregate(metaclass=MetaAggregate):
|
|
|
1362
1302
|
else:
|
|
1363
1303
|
bases = (base_created_event_cls, base_event_cls)
|
|
1364
1304
|
created_event_class = cast(
|
|
1365
|
-
type[CanInitAggregate],
|
|
1305
|
+
"type[CanInitAggregate]",
|
|
1366
1306
|
cls._define_event_class(
|
|
1367
1307
|
created_event_name,
|
|
1368
1308
|
bases,
|
|
@@ -1384,20 +1324,22 @@ class BaseAggregate(metaclass=MetaAggregate):
|
|
|
1384
1324
|
|
|
1385
1325
|
if isinstance(attr_value, CommandMethodDecorator):
|
|
1386
1326
|
event_decorator = attr_value
|
|
1327
|
+
if event_decorator.decorated_func.__name__ == "__init__":
|
|
1328
|
+
continue
|
|
1387
1329
|
|
|
1388
1330
|
elif isinstance(attr_value, property) and isinstance(
|
|
1389
1331
|
attr_value.fset, CommandMethodDecorator
|
|
1390
1332
|
):
|
|
1391
1333
|
event_decorator = attr_value.fset
|
|
1392
1334
|
# Inspect the setter method.
|
|
1393
|
-
method_signature = inspect.signature(event_decorator.
|
|
1335
|
+
method_signature = inspect.signature(event_decorator.decorated_func)
|
|
1394
1336
|
assert len(method_signature.parameters) == 2
|
|
1395
1337
|
event_decorator.is_property_setter = True
|
|
1396
1338
|
event_decorator.property_setter_arg_name = list(
|
|
1397
1339
|
method_signature.parameters
|
|
1398
1340
|
)[1]
|
|
1399
|
-
if event_decorator.
|
|
1400
|
-
attr = cls.__dict__[event_decorator.
|
|
1341
|
+
if event_decorator.decorated_func.__name__ != attr_name:
|
|
1342
|
+
attr = cls.__dict__[event_decorator.decorated_func.__name__]
|
|
1401
1343
|
if isinstance(attr, CommandMethodDecorator):
|
|
1402
1344
|
# This is the "x = property(getx, setx) form" where setx
|
|
1403
1345
|
# is a decorated method.
|
|
@@ -1406,10 +1348,10 @@ class BaseAggregate(metaclass=MetaAggregate):
|
|
|
1406
1348
|
elif event_decorator.is_name_inferred_from_method:
|
|
1407
1349
|
# This is the "@property.setter \ @event" form. We don't want
|
|
1408
1350
|
# event class name inferred from property (not past participle).
|
|
1409
|
-
method_name = event_decorator.
|
|
1351
|
+
method_name = event_decorator.decorated_func.__name__
|
|
1410
1352
|
msg = (
|
|
1411
|
-
f"@event under {method_name}
|
|
1412
|
-
"event class
|
|
1353
|
+
f"@event decorator under @{method_name}.setter "
|
|
1354
|
+
"requires event name or class"
|
|
1413
1355
|
)
|
|
1414
1356
|
raise TypeError(msg)
|
|
1415
1357
|
|
|
@@ -1425,7 +1367,7 @@ class BaseAggregate(metaclass=MetaAggregate):
|
|
|
1425
1367
|
|
|
1426
1368
|
# Define event class as subclass of given class.
|
|
1427
1369
|
given_subclass = cast(
|
|
1428
|
-
type[CanMutateAggregate],
|
|
1370
|
+
"type[CanMutateAggregate]",
|
|
1429
1371
|
getattr(cls, event_decorator.given_event_cls.__name__),
|
|
1430
1372
|
)
|
|
1431
1373
|
event_cls = cls._define_event_class(
|
|
@@ -1448,18 +1390,18 @@ class BaseAggregate(metaclass=MetaAggregate):
|
|
|
1448
1390
|
event_cls = cls._define_event_class(
|
|
1449
1391
|
event_decorator.event_cls_name,
|
|
1450
1392
|
(DecoratorEvent, base_event_cls),
|
|
1451
|
-
event_decorator.
|
|
1393
|
+
event_decorator.decorated_func,
|
|
1452
1394
|
)
|
|
1453
1395
|
|
|
1454
1396
|
# Cache the decorated method for the event class to use.
|
|
1455
|
-
|
|
1397
|
+
_decorated_funcs[event_cls] = event_decorator.decorated_func
|
|
1456
1398
|
|
|
1457
1399
|
# Set the event class as an attribute of the aggregate class.
|
|
1458
1400
|
setattr(cls, event_cls.__name__, event_cls)
|
|
1459
1401
|
|
|
1460
1402
|
# Remember which event class to trigger.
|
|
1461
1403
|
decorator_event_classes[event_decorator] = cast(
|
|
1462
|
-
type[DecoratorEvent], event_cls
|
|
1404
|
+
"type[DecoratorEvent]", event_cls
|
|
1463
1405
|
)
|
|
1464
1406
|
|
|
1465
1407
|
# Check any create_id method defined on this class is static or class method.
|
|
@@ -1515,8 +1457,7 @@ def aggregate(
|
|
|
1515
1457
|
*,
|
|
1516
1458
|
created_event_name: str = "",
|
|
1517
1459
|
) -> type[Aggregate] | Callable[[Any], type[Aggregate]]:
|
|
1518
|
-
"""
|
|
1519
|
-
Converts the class that was passed in to inherit from Aggregate.
|
|
1460
|
+
"""Converts the class that was passed in to inherit from Aggregate.
|
|
1520
1461
|
|
|
1521
1462
|
.. code-block:: python
|
|
1522
1463
|
|
|
@@ -1558,8 +1499,7 @@ def aggregate(
|
|
|
1558
1499
|
|
|
1559
1500
|
|
|
1560
1501
|
class OriginatorIDError(EventSourcingError):
|
|
1561
|
-
"""
|
|
1562
|
-
Raised when a domain event can't be applied to
|
|
1502
|
+
"""Raised when a domain event can't be applied to
|
|
1563
1503
|
an aggregate due to an ID mismatch indicating
|
|
1564
1504
|
the domain event is not in the aggregate's
|
|
1565
1505
|
sequence of events.
|
|
@@ -1567,38 +1507,23 @@ class OriginatorIDError(EventSourcingError):
|
|
|
1567
1507
|
|
|
1568
1508
|
|
|
1569
1509
|
class OriginatorVersionError(EventSourcingError):
|
|
1570
|
-
"""
|
|
1571
|
-
Raised when a domain event can't be applied to
|
|
1510
|
+
"""Raised when a domain event can't be applied to
|
|
1572
1511
|
an aggregate due to version mismatch indicating
|
|
1573
1512
|
the domain event is not the next in the aggregate's
|
|
1574
1513
|
sequence of events.
|
|
1575
1514
|
"""
|
|
1576
1515
|
|
|
1577
1516
|
|
|
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
1517
|
class SnapshotProtocol(DomainEventProtocol, Protocol):
|
|
1589
1518
|
@property
|
|
1590
1519
|
def state(self) -> dict[str, Any]:
|
|
1591
|
-
"""
|
|
1592
|
-
Snapshots have a read-only 'state'.
|
|
1593
|
-
"""
|
|
1520
|
+
"""Snapshots have a read-only 'state'."""
|
|
1594
1521
|
raise NotImplementedError # pragma: no cover
|
|
1595
1522
|
|
|
1596
1523
|
# TODO: Improve on this 'Any'.
|
|
1597
1524
|
@classmethod
|
|
1598
1525
|
def take(cls: Any, aggregate: Any) -> Any:
|
|
1599
|
-
"""
|
|
1600
|
-
Snapshots have a 'take()' class method.
|
|
1601
|
-
"""
|
|
1526
|
+
"""Snapshots have a 'take()' class method."""
|
|
1602
1527
|
|
|
1603
1528
|
|
|
1604
1529
|
TCanSnapshotAggregate = TypeVar("TCanSnapshotAggregate", bound="CanSnapshotAggregate")
|
|
@@ -1620,12 +1545,10 @@ class CanSnapshotAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
|
|
|
1620
1545
|
|
|
1621
1546
|
@classmethod
|
|
1622
1547
|
def take(
|
|
1623
|
-
cls
|
|
1548
|
+
cls,
|
|
1624
1549
|
aggregate: MutableOrImmutableAggregate,
|
|
1625
|
-
) ->
|
|
1626
|
-
"""
|
|
1627
|
-
Creates a snapshot of the given :class:`Aggregate` object.
|
|
1628
|
-
"""
|
|
1550
|
+
) -> Self:
|
|
1551
|
+
"""Creates a snapshot of the given :class:`Aggregate` object."""
|
|
1629
1552
|
aggregate_state = dict(aggregate.__dict__)
|
|
1630
1553
|
class_version = getattr(type(aggregate), "class_version", 1)
|
|
1631
1554
|
if class_version > 1:
|
|
@@ -1643,10 +1566,8 @@ class CanSnapshotAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
|
|
|
1643
1566
|
)
|
|
1644
1567
|
|
|
1645
1568
|
def mutate(self, _: None) -> Aggregate:
|
|
1646
|
-
"""
|
|
1647
|
-
|
|
1648
|
-
"""
|
|
1649
|
-
cls = cast(type[Aggregate], resolve_topic(self.topic))
|
|
1569
|
+
"""Reconstructs the snapshotted :class:`Aggregate` object."""
|
|
1570
|
+
cls = cast("type[Aggregate]", resolve_topic(self.topic))
|
|
1650
1571
|
aggregate_state = dict(self.state)
|
|
1651
1572
|
from_version = aggregate_state.pop("class_version", 1)
|
|
1652
1573
|
class_version = getattr(cls, "class_version", 1)
|
|
@@ -1666,8 +1587,7 @@ class CanSnapshotAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
|
|
|
1666
1587
|
|
|
1667
1588
|
@dataclass(frozen=True)
|
|
1668
1589
|
class Snapshot(CanSnapshotAggregate, DomainEvent):
|
|
1669
|
-
"""
|
|
1670
|
-
Snapshots represent the state of an aggregate at a particular
|
|
1590
|
+
"""Snapshots represent the state of an aggregate at a particular
|
|
1671
1591
|
version.
|
|
1672
1592
|
|
|
1673
1593
|
Constructor arguments:
|