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