eventsourcing 9.2.21__py3-none-any.whl → 9.3.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 +1 -1
- eventsourcing/application.py +137 -132
- eventsourcing/cipher.py +17 -12
- eventsourcing/compressor.py +2 -0
- eventsourcing/dispatch.py +30 -56
- eventsourcing/domain.py +221 -227
- eventsourcing/examples/__init__.py +0 -0
- eventsourcing/examples/aggregate1/__init__.py +0 -0
- eventsourcing/examples/aggregate1/application.py +27 -0
- eventsourcing/examples/aggregate1/domainmodel.py +16 -0
- eventsourcing/examples/aggregate1/test_application.py +37 -0
- eventsourcing/examples/aggregate2/__init__.py +0 -0
- eventsourcing/examples/aggregate2/application.py +27 -0
- eventsourcing/examples/aggregate2/domainmodel.py +22 -0
- eventsourcing/examples/aggregate2/test_application.py +37 -0
- eventsourcing/examples/aggregate3/__init__.py +0 -0
- eventsourcing/examples/aggregate3/application.py +27 -0
- eventsourcing/examples/aggregate3/domainmodel.py +38 -0
- eventsourcing/examples/aggregate3/test_application.py +37 -0
- eventsourcing/examples/aggregate4/__init__.py +0 -0
- eventsourcing/examples/aggregate4/application.py +27 -0
- eventsourcing/examples/aggregate4/domainmodel.py +114 -0
- eventsourcing/examples/aggregate4/test_application.py +38 -0
- eventsourcing/examples/aggregate5/__init__.py +0 -0
- eventsourcing/examples/aggregate5/application.py +27 -0
- eventsourcing/examples/aggregate5/domainmodel.py +131 -0
- eventsourcing/examples/aggregate5/test_application.py +38 -0
- eventsourcing/examples/aggregate6/__init__.py +0 -0
- eventsourcing/examples/aggregate6/application.py +30 -0
- eventsourcing/examples/aggregate6/domainmodel.py +123 -0
- eventsourcing/examples/aggregate6/test_application.py +38 -0
- eventsourcing/examples/aggregate6a/__init__.py +0 -0
- eventsourcing/examples/aggregate6a/application.py +40 -0
- eventsourcing/examples/aggregate6a/domainmodel.py +149 -0
- eventsourcing/examples/aggregate6a/test_application.py +45 -0
- eventsourcing/examples/aggregate7/__init__.py +0 -0
- eventsourcing/examples/aggregate7/application.py +48 -0
- eventsourcing/examples/aggregate7/domainmodel.py +144 -0
- eventsourcing/examples/aggregate7/persistence.py +57 -0
- eventsourcing/examples/aggregate7/test_application.py +38 -0
- eventsourcing/examples/aggregate7/test_compression_and_encryption.py +45 -0
- eventsourcing/examples/aggregate7/test_snapshotting_intervals.py +67 -0
- eventsourcing/examples/aggregate7a/__init__.py +0 -0
- eventsourcing/examples/aggregate7a/application.py +56 -0
- eventsourcing/examples/aggregate7a/domainmodel.py +170 -0
- eventsourcing/examples/aggregate7a/test_application.py +46 -0
- eventsourcing/examples/aggregate7a/test_compression_and_encryption.py +45 -0
- eventsourcing/examples/aggregate8/__init__.py +0 -0
- eventsourcing/examples/aggregate8/application.py +47 -0
- eventsourcing/examples/aggregate8/domainmodel.py +65 -0
- eventsourcing/examples/aggregate8/persistence.py +57 -0
- eventsourcing/examples/aggregate8/test_application.py +37 -0
- eventsourcing/examples/aggregate8/test_compression_and_encryption.py +44 -0
- eventsourcing/examples/aggregate8/test_snapshotting_intervals.py +38 -0
- eventsourcing/examples/bankaccounts/__init__.py +0 -0
- eventsourcing/examples/bankaccounts/application.py +70 -0
- eventsourcing/examples/bankaccounts/domainmodel.py +56 -0
- eventsourcing/examples/bankaccounts/test.py +173 -0
- eventsourcing/examples/cargoshipping/__init__.py +0 -0
- eventsourcing/examples/cargoshipping/application.py +126 -0
- eventsourcing/examples/cargoshipping/domainmodel.py +330 -0
- eventsourcing/examples/cargoshipping/interface.py +143 -0
- eventsourcing/examples/cargoshipping/test.py +231 -0
- eventsourcing/examples/contentmanagement/__init__.py +0 -0
- eventsourcing/examples/contentmanagement/application.py +118 -0
- eventsourcing/examples/contentmanagement/domainmodel.py +69 -0
- eventsourcing/examples/contentmanagement/test.py +180 -0
- eventsourcing/examples/contentmanagement/utils.py +26 -0
- eventsourcing/examples/contentmanagementsystem/__init__.py +0 -0
- eventsourcing/examples/contentmanagementsystem/application.py +54 -0
- eventsourcing/examples/contentmanagementsystem/postgres.py +17 -0
- eventsourcing/examples/contentmanagementsystem/sqlite.py +17 -0
- eventsourcing/examples/contentmanagementsystem/system.py +14 -0
- eventsourcing/examples/contentmanagementsystem/test_system.py +180 -0
- eventsourcing/examples/searchablecontent/__init__.py +0 -0
- eventsourcing/examples/searchablecontent/application.py +45 -0
- eventsourcing/examples/searchablecontent/persistence.py +23 -0
- eventsourcing/examples/searchablecontent/postgres.py +118 -0
- eventsourcing/examples/searchablecontent/sqlite.py +136 -0
- eventsourcing/examples/searchablecontent/test_application.py +110 -0
- eventsourcing/examples/searchablecontent/test_recorder.py +68 -0
- eventsourcing/examples/searchabletimestamps/__init__.py +0 -0
- eventsourcing/examples/searchabletimestamps/application.py +32 -0
- eventsourcing/examples/searchabletimestamps/persistence.py +20 -0
- eventsourcing/examples/searchabletimestamps/postgres.py +110 -0
- eventsourcing/examples/searchabletimestamps/sqlite.py +99 -0
- eventsourcing/examples/searchabletimestamps/test_searchabletimestamps.py +94 -0
- eventsourcing/examples/test_invoice.py +176 -0
- eventsourcing/examples/test_parking_lot.py +206 -0
- eventsourcing/interface.py +4 -2
- eventsourcing/persistence.py +88 -82
- eventsourcing/popo.py +32 -31
- eventsourcing/postgres.py +388 -593
- eventsourcing/sqlite.py +100 -102
- eventsourcing/system.py +66 -71
- eventsourcing/tests/application.py +20 -32
- eventsourcing/tests/application_tests/__init__.py +0 -0
- eventsourcing/tests/application_tests/test_application_with_automatic_snapshotting.py +55 -0
- eventsourcing/tests/application_tests/test_application_with_popo.py +22 -0
- eventsourcing/tests/application_tests/test_application_with_postgres.py +75 -0
- eventsourcing/tests/application_tests/test_application_with_sqlite.py +72 -0
- eventsourcing/tests/application_tests/test_cache.py +134 -0
- eventsourcing/tests/application_tests/test_event_sourced_log.py +162 -0
- eventsourcing/tests/application_tests/test_notificationlog.py +232 -0
- eventsourcing/tests/application_tests/test_notificationlogreader.py +126 -0
- eventsourcing/tests/application_tests/test_processapplication.py +110 -0
- eventsourcing/tests/application_tests/test_processingpolicy.py +109 -0
- eventsourcing/tests/application_tests/test_repository.py +504 -0
- eventsourcing/tests/application_tests/test_snapshotting.py +68 -0
- eventsourcing/tests/application_tests/test_upcasting.py +459 -0
- eventsourcing/tests/docs_tests/__init__.py +0 -0
- eventsourcing/tests/docs_tests/test_docs.py +293 -0
- eventsourcing/tests/domain.py +1 -1
- eventsourcing/tests/domain_tests/__init__.py +0 -0
- eventsourcing/tests/domain_tests/test_aggregate.py +1180 -0
- eventsourcing/tests/domain_tests/test_aggregate_decorators.py +1604 -0
- eventsourcing/tests/domain_tests/test_domainevent.py +80 -0
- eventsourcing/tests/interface_tests/__init__.py +0 -0
- eventsourcing/tests/interface_tests/test_remotenotificationlog.py +258 -0
- eventsourcing/tests/persistence.py +52 -50
- eventsourcing/tests/persistence_tests/__init__.py +0 -0
- eventsourcing/tests/persistence_tests/test_aes.py +93 -0
- eventsourcing/tests/persistence_tests/test_connection_pool.py +722 -0
- eventsourcing/tests/persistence_tests/test_eventstore.py +72 -0
- eventsourcing/tests/persistence_tests/test_infrastructure_factory.py +21 -0
- eventsourcing/tests/persistence_tests/test_mapper.py +113 -0
- eventsourcing/tests/persistence_tests/test_noninterleaving_notification_ids.py +69 -0
- eventsourcing/tests/persistence_tests/test_popo.py +124 -0
- eventsourcing/tests/persistence_tests/test_postgres.py +1119 -0
- eventsourcing/tests/persistence_tests/test_sqlite.py +348 -0
- eventsourcing/tests/persistence_tests/test_transcoder.py +44 -0
- eventsourcing/tests/postgres_utils.py +7 -7
- eventsourcing/tests/system_tests/__init__.py +0 -0
- eventsourcing/tests/system_tests/test_runner.py +935 -0
- eventsourcing/tests/system_tests/test_system.py +284 -0
- eventsourcing/tests/utils_tests/__init__.py +0 -0
- eventsourcing/tests/utils_tests/test_utils.py +226 -0
- eventsourcing/utils.py +49 -50
- {eventsourcing-9.2.21.dist-info → eventsourcing-9.3.0.dist-info}/METADATA +30 -33
- eventsourcing-9.3.0.dist-info/RECORD +145 -0
- {eventsourcing-9.2.21.dist-info → eventsourcing-9.3.0.dist-info}/WHEEL +1 -2
- eventsourcing-9.2.21.dist-info/RECORD +0 -25
- eventsourcing-9.2.21.dist-info/top_level.txt +0 -1
- {eventsourcing-9.2.21.dist-info → eventsourcing-9.3.0.dist-info}/AUTHORS +0 -0
- {eventsourcing-9.2.21.dist-info → eventsourcing-9.3.0.dist-info}/LICENSE +0 -0
eventsourcing/domain.py
CHANGED
|
@@ -2,34 +2,28 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
4
|
import os
|
|
5
|
-
import sys
|
|
6
5
|
from dataclasses import dataclass
|
|
7
6
|
from datetime import datetime, tzinfo
|
|
8
7
|
from functools import lru_cache
|
|
9
8
|
from types import FunctionType, WrapperDescriptorType
|
|
10
9
|
from typing import (
|
|
10
|
+
TYPE_CHECKING,
|
|
11
11
|
Any,
|
|
12
12
|
Callable,
|
|
13
13
|
Dict,
|
|
14
14
|
Generic,
|
|
15
15
|
Iterable,
|
|
16
16
|
List,
|
|
17
|
-
|
|
17
|
+
Protocol,
|
|
18
18
|
Sequence,
|
|
19
|
-
Set,
|
|
20
19
|
Tuple,
|
|
21
20
|
Type,
|
|
22
21
|
TypeVar,
|
|
23
22
|
Union,
|
|
24
23
|
cast,
|
|
25
24
|
overload,
|
|
25
|
+
runtime_checkable,
|
|
26
26
|
)
|
|
27
|
-
|
|
28
|
-
if sys.version_info >= (3, 8): # pragma: no cover
|
|
29
|
-
from typing import Protocol, runtime_checkable
|
|
30
|
-
else: # pragma: no cover
|
|
31
|
-
from typing_extensions import Protocol, runtime_checkable
|
|
32
|
-
|
|
33
27
|
from uuid import UUID, uuid4
|
|
34
28
|
|
|
35
29
|
from eventsourcing.utils import get_method_name, get_topic, resolve_topic
|
|
@@ -150,8 +144,8 @@ class CanMutateProtocol(DomainEventProtocol, Protocol[TMutableOrImmutableAggrega
|
|
|
150
144
|
"""
|
|
151
145
|
|
|
152
146
|
def mutate(
|
|
153
|
-
self, aggregate:
|
|
154
|
-
) ->
|
|
147
|
+
self, aggregate: TMutableOrImmutableAggregate | None
|
|
148
|
+
) -> TMutableOrImmutableAggregate | None:
|
|
155
149
|
"""
|
|
156
150
|
Evolves the state of the given aggregate, either by
|
|
157
151
|
returning the given aggregate instance with modified attributes
|
|
@@ -203,7 +197,7 @@ class CanMutateAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
|
|
|
203
197
|
timestamp: datetime
|
|
204
198
|
"""Timezone-aware :class:`datetime` object representing when an event occurred."""
|
|
205
199
|
|
|
206
|
-
def mutate(self, aggregate:
|
|
200
|
+
def mutate(self, aggregate: TAggregate | None) -> TAggregate | None:
|
|
207
201
|
"""
|
|
208
202
|
Validates and adjusted the attributes of the given ``aggregate`` argument. The
|
|
209
203
|
argument is typed as ``Optional`` but the value is expected to be not ``None``.
|
|
@@ -266,7 +260,7 @@ class CanInitAggregate(CanMutateAggregate):
|
|
|
266
260
|
originator_topic: str
|
|
267
261
|
"""String describing the path to an aggregate class."""
|
|
268
262
|
|
|
269
|
-
def mutate(self, aggregate:
|
|
263
|
+
def mutate(self, aggregate: TAggregate | None) -> TAggregate | None:
|
|
270
264
|
"""
|
|
271
265
|
Constructs an aggregate instance according to the attributes of an event.
|
|
272
266
|
|
|
@@ -278,31 +272,34 @@ class CanInitAggregate(CanMutateAggregate):
|
|
|
278
272
|
# Resolve originator topic.
|
|
279
273
|
aggregate_class: Type[TAggregate] = resolve_topic(self.originator_topic)
|
|
280
274
|
|
|
281
|
-
# Construct
|
|
275
|
+
# Construct an aggregate object (a "shell" of the correct object type).
|
|
282
276
|
agg = aggregate_class.__new__(aggregate_class)
|
|
283
277
|
|
|
284
|
-
#
|
|
278
|
+
# Pick out event attributes for the aggregate base class init method.
|
|
285
279
|
base_kwargs = _filter_kwargs_for_method_params(
|
|
286
280
|
self.__dict__, type(agg).__base_init__
|
|
287
281
|
)
|
|
288
282
|
|
|
289
|
-
# Call the base class init method
|
|
283
|
+
# Call the base class init method (so we don't need to always write
|
|
284
|
+
# a call to super().__init__() in every aggregate __init__() method).
|
|
290
285
|
agg.__base_init__(**base_kwargs)
|
|
291
286
|
|
|
292
|
-
#
|
|
287
|
+
# Pick out event attributes for aggregate subclass class init method.
|
|
293
288
|
init_kwargs = _filter_kwargs_for_method_params(
|
|
294
289
|
self.__dict__, type(agg).__init__
|
|
295
290
|
)
|
|
296
291
|
|
|
297
|
-
# Provide the id, if the init method expects it.
|
|
292
|
+
# Provide the aggregate id, if the aggregate subclass init method expects it.
|
|
298
293
|
if aggregate_class in _init_mentions_id:
|
|
299
294
|
init_kwargs["id"] = self.__dict__["originator_id"]
|
|
300
295
|
|
|
301
|
-
# Call the aggregate class init method.
|
|
296
|
+
# Call the aggregate subclass class init method.
|
|
302
297
|
agg.__init__(**init_kwargs) # type: ignore
|
|
303
298
|
|
|
299
|
+
# Call the event apply method (alternative to using __init__())
|
|
304
300
|
self.apply(agg)
|
|
305
301
|
|
|
302
|
+
# Return the constructed and initialised aggregate object.
|
|
306
303
|
return agg
|
|
307
304
|
|
|
308
305
|
|
|
@@ -384,36 +381,38 @@ def _filter_kwargs_for_method_params(
|
|
|
384
381
|
|
|
385
382
|
|
|
386
383
|
@lru_cache(maxsize=None)
|
|
387
|
-
def _spec_filter_kwargs_for_method_params(method: Callable[..., Any]) ->
|
|
384
|
+
def _spec_filter_kwargs_for_method_params(method: Callable[..., Any]) -> set[str]:
|
|
388
385
|
method_signature = inspect.signature(method)
|
|
389
386
|
return set(method_signature.parameters)
|
|
390
387
|
|
|
391
388
|
|
|
392
|
-
|
|
389
|
+
if TYPE_CHECKING: # pragma: nocover
|
|
390
|
+
EventSpecType = Union[str, Type[CanMutateAggregate]]
|
|
391
|
+
|
|
393
392
|
CommandMethod = Callable[..., None]
|
|
394
393
|
DecoratedObjType = Union[CommandMethod, property]
|
|
395
394
|
TDecoratedObjType = TypeVar("TDecoratedObjType", bound=DecoratedObjType)
|
|
396
|
-
InjectEventType = bool
|
|
397
395
|
|
|
398
396
|
|
|
399
397
|
class CommandMethodDecorator:
|
|
400
398
|
def __init__(
|
|
401
399
|
self,
|
|
402
|
-
event_spec:
|
|
400
|
+
event_spec: EventSpecType | None,
|
|
403
401
|
decorated_obj: DecoratedObjType,
|
|
404
402
|
):
|
|
405
403
|
self.is_name_inferred_from_method = False
|
|
406
|
-
self.given_event_cls:
|
|
407
|
-
self.event_cls_name:
|
|
408
|
-
self.decorated_property:
|
|
404
|
+
self.given_event_cls: Type[CanMutateAggregate] | None = None
|
|
405
|
+
self.event_cls_name: str | None = None
|
|
406
|
+
self.decorated_property: property | None = None
|
|
409
407
|
self.is_property_setter = False
|
|
410
|
-
self.property_setter_arg_name:
|
|
411
|
-
self.decorated_method:
|
|
408
|
+
self.property_setter_arg_name: str | None = None
|
|
409
|
+
self.decorated_method: FunctionType | WrapperDescriptorType
|
|
412
410
|
|
|
413
411
|
# Event name has been specified.
|
|
414
412
|
if isinstance(event_spec, str):
|
|
415
413
|
if event_spec == "":
|
|
416
|
-
|
|
414
|
+
msg = "Can't use empty string as name of event class"
|
|
415
|
+
raise ValueError(msg)
|
|
417
416
|
self.event_cls_name = event_spec
|
|
418
417
|
|
|
419
418
|
# Event class has been specified.
|
|
@@ -422,7 +421,8 @@ class CommandMethodDecorator:
|
|
|
422
421
|
):
|
|
423
422
|
if event_spec in given_event_classes:
|
|
424
423
|
name = event_spec.__name__
|
|
425
|
-
|
|
424
|
+
msg = f"{name} event class used in more than one decorator"
|
|
425
|
+
raise TypeError(msg)
|
|
426
426
|
self.given_event_cls = event_spec
|
|
427
427
|
given_event_classes.add(event_spec)
|
|
428
428
|
|
|
@@ -432,9 +432,8 @@ class CommandMethodDecorator:
|
|
|
432
432
|
if decorated_obj.fset is None:
|
|
433
433
|
assert decorated_obj.fget, "Property has no getter"
|
|
434
434
|
method_name = decorated_obj.fget.__name__
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
)
|
|
435
|
+
msg = f"@event can't decorate {method_name}() property getter"
|
|
436
|
+
raise TypeError(msg)
|
|
438
437
|
|
|
439
438
|
# Remember we are decorating a property.
|
|
440
439
|
self.decorated_property = decorated_obj
|
|
@@ -447,9 +446,8 @@ class CommandMethodDecorator:
|
|
|
447
446
|
# Disallow deriving event class names from property names.
|
|
448
447
|
if not self.given_event_cls and not self.event_cls_name:
|
|
449
448
|
method_name = self.decorated_method.__name__
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
)
|
|
449
|
+
msg = f"@event on {method_name}() setter requires event name or class"
|
|
450
|
+
raise TypeError(msg)
|
|
453
451
|
|
|
454
452
|
# Remember the name of the second setter arg.
|
|
455
453
|
setter_arg_names = list(inspect.signature(self.decorated_method).parameters)
|
|
@@ -472,7 +470,8 @@ class CommandMethodDecorator:
|
|
|
472
470
|
|
|
473
471
|
# Disallow decorating other types of object.
|
|
474
472
|
else:
|
|
475
|
-
|
|
473
|
+
msg = f"{decorated_obj} is not a function or property"
|
|
474
|
+
raise TypeError(msg)
|
|
476
475
|
|
|
477
476
|
# Disallow using methods with variable params to define event class.
|
|
478
477
|
if self.event_cls_name:
|
|
@@ -496,7 +495,7 @@ class CommandMethodDecorator:
|
|
|
496
495
|
@overload
|
|
497
496
|
def __get__(
|
|
498
497
|
self, instance: None, owner: MetaAggregate[Aggregate]
|
|
499
|
-
) ->
|
|
498
|
+
) -> UnboundCommandMethodDecorator | property:
|
|
500
499
|
"""
|
|
501
500
|
Descriptor protocol for getting decorated method or property on class object.
|
|
502
501
|
"""
|
|
@@ -504,16 +503,14 @@ class CommandMethodDecorator:
|
|
|
504
503
|
@overload
|
|
505
504
|
def __get__(
|
|
506
505
|
self, instance: Aggregate, owner: MetaAggregate[Aggregate]
|
|
507
|
-
) ->
|
|
506
|
+
) -> BoundCommandMethodDecorator | Any:
|
|
508
507
|
"""
|
|
509
508
|
Descriptor protocol for getting decorated method or property on instance object.
|
|
510
509
|
"""
|
|
511
510
|
|
|
512
511
|
def __get__(
|
|
513
|
-
self, instance:
|
|
514
|
-
) ->
|
|
515
|
-
BoundCommandMethodDecorator, UnboundCommandMethodDecorator, property, Any
|
|
516
|
-
]:
|
|
512
|
+
self, instance: Aggregate | None, owner: MetaAggregate[Aggregate]
|
|
513
|
+
) -> BoundCommandMethodDecorator | UnboundCommandMethodDecorator | property | Any:
|
|
517
514
|
"""
|
|
518
515
|
Descriptor protocol for getting decorated method or property.
|
|
519
516
|
"""
|
|
@@ -522,12 +519,11 @@ class CommandMethodDecorator:
|
|
|
522
519
|
return self.decorated_property.__get__(instance, owner)
|
|
523
520
|
|
|
524
521
|
# Return a "bound" command method decorator if we have an instance.
|
|
525
|
-
|
|
522
|
+
if instance:
|
|
526
523
|
return BoundCommandMethodDecorator(self, instance)
|
|
527
524
|
|
|
528
525
|
# Return an "unbound" command method decorator if we have no instance.
|
|
529
|
-
|
|
530
|
-
return UnboundCommandMethodDecorator(self)
|
|
526
|
+
return UnboundCommandMethodDecorator(self)
|
|
531
527
|
|
|
532
528
|
def __set__(self, instance: Aggregate, value: Any) -> None:
|
|
533
529
|
"""
|
|
@@ -566,8 +562,8 @@ def event(
|
|
|
566
562
|
|
|
567
563
|
|
|
568
564
|
def event(
|
|
569
|
-
arg:
|
|
570
|
-
) ->
|
|
565
|
+
arg: EventSpecType | TDecoratedObjType | None = None,
|
|
566
|
+
) -> TDecoratedObjType | Callable[[TDecoratedObjType], TDecoratedObjType]:
|
|
571
567
|
"""
|
|
572
568
|
Event-triggering decorator for aggregate command methods and property setters.
|
|
573
569
|
|
|
@@ -627,7 +623,7 @@ def event(
|
|
|
627
623
|
Callable[[TDecoratedObjType], TDecoratedObjType], command_method_decorator
|
|
628
624
|
)
|
|
629
625
|
|
|
630
|
-
|
|
626
|
+
if (
|
|
631
627
|
arg is None
|
|
632
628
|
or isinstance(arg, str)
|
|
633
629
|
or isinstance(arg, type)
|
|
@@ -646,11 +642,11 @@ def event(
|
|
|
646
642
|
|
|
647
643
|
return create_command_method_decorator
|
|
648
644
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
645
|
+
msg = (
|
|
646
|
+
f"{arg} is not a str, function, property, or subclass of "
|
|
647
|
+
f"{CanMutateAggregate.__name__}"
|
|
648
|
+
)
|
|
649
|
+
raise TypeError(msg)
|
|
654
650
|
|
|
655
651
|
|
|
656
652
|
triggers = event
|
|
@@ -676,7 +672,8 @@ class UnboundCommandMethodDecorator:
|
|
|
676
672
|
def __call__(self, *args: Any, **kwargs: Any) -> None:
|
|
677
673
|
# Expect first argument is an aggregate instance.
|
|
678
674
|
if len(args) < 1 or not isinstance(args[0], Aggregate):
|
|
679
|
-
|
|
675
|
+
msg = "Expected aggregate as first argument"
|
|
676
|
+
raise TypeError(msg)
|
|
680
677
|
aggregate: Aggregate = args[0]
|
|
681
678
|
assert isinstance(aggregate, Aggregate)
|
|
682
679
|
BoundCommandMethodDecorator(self.event_decorator, aggregate)(
|
|
@@ -716,7 +713,7 @@ class BoundCommandMethodDecorator:
|
|
|
716
713
|
self.trigger(*args, **kwargs)
|
|
717
714
|
|
|
718
715
|
|
|
719
|
-
given_event_classes:
|
|
716
|
+
given_event_classes: set[type] = set()
|
|
720
717
|
decorated_methods: Dict[type, CommandMethod] = {}
|
|
721
718
|
aggregate_has_many_created_event_classes: Dict[type, List[str]] = {}
|
|
722
719
|
|
|
@@ -729,24 +726,23 @@ decorated_event_classes: Dict[
|
|
|
729
726
|
def _check_no_variable_params(method: FunctionType) -> None:
|
|
730
727
|
for param in inspect.signature(method).parameters.values():
|
|
731
728
|
if param.kind is param.VAR_POSITIONAL:
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
# Todo: Support VAR_POSITIONAL?
|
|
729
|
+
msg = f"*{param.name} not supported by decorator on {method.__name__}()"
|
|
730
|
+
raise TypeError(msg)
|
|
731
|
+
# TODO: Support VAR_POSITIONAL?
|
|
736
732
|
# annotations["__star_args__"] = "typing.Any"
|
|
737
733
|
|
|
738
734
|
if param.kind is param.VAR_KEYWORD:
|
|
739
|
-
#
|
|
735
|
+
# TODO: Support VAR_KEYWORD?
|
|
740
736
|
# annotations["__star_kwargs__"] = "typing.Any"
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
)
|
|
737
|
+
msg = f"**{param.name} not supported by decorator on {method.__name__}()"
|
|
738
|
+
raise TypeError(msg)
|
|
744
739
|
|
|
745
740
|
|
|
746
741
|
def _coerce_args_to_kwargs(
|
|
747
|
-
method:
|
|
742
|
+
method: FunctionType | WrapperDescriptorType,
|
|
748
743
|
args: Iterable[Any],
|
|
749
744
|
kwargs: Dict[str, Any],
|
|
745
|
+
*,
|
|
750
746
|
expects_id: bool = False,
|
|
751
747
|
) -> Dict[str, Any]:
|
|
752
748
|
assert isinstance(method, (FunctionType, WrapperDescriptorType))
|
|
@@ -769,9 +765,10 @@ def _coerce_args_to_kwargs(
|
|
|
769
765
|
|
|
770
766
|
@lru_cache(maxsize=None)
|
|
771
767
|
def _spec_coerce_args_to_kwargs(
|
|
772
|
-
method:
|
|
768
|
+
method: FunctionType | WrapperDescriptorType,
|
|
773
769
|
len_args: int,
|
|
774
770
|
kwargs_keys: Tuple[str],
|
|
771
|
+
*,
|
|
775
772
|
expects_id: bool,
|
|
776
773
|
) -> Tuple[Tuple[Tuple[int, str], ...], Tuple[Tuple[str, Any], ...]]:
|
|
777
774
|
method_signature = inspect.signature(method)
|
|
@@ -800,9 +797,8 @@ def _spec_coerce_args_to_kwargs(
|
|
|
800
797
|
method_name = get_method_name(method)
|
|
801
798
|
for name in kwargs_keys:
|
|
802
799
|
if name not in required_keyword_only and name not in positional_names:
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
)
|
|
800
|
+
msg = f"{method_name}() got an unexpected keyword argument '{name}'"
|
|
801
|
+
raise TypeError(msg)
|
|
806
802
|
if len_args > len(positional_names):
|
|
807
803
|
msg = (
|
|
808
804
|
f"{method_name}() takes {len(positional_names) + 1} "
|
|
@@ -829,21 +825,18 @@ def _spec_coerce_args_to_kwargs(
|
|
|
829
825
|
if counter + 1 > len_args:
|
|
830
826
|
break
|
|
831
827
|
if name in kwargs_keys:
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
for name in required_keyword_only:
|
|
840
|
-
if name not in kwargs_keys:
|
|
841
|
-
missing_keyword_only_arguments.append(name)
|
|
828
|
+
msg = f"{method_name}() got multiple values for argument '{name}'"
|
|
829
|
+
raise TypeError(msg)
|
|
830
|
+
args_names.append(name)
|
|
831
|
+
counter += 1
|
|
832
|
+
missing_keyword_only_arguments = [
|
|
833
|
+
name for name in required_keyword_only if name not in kwargs_keys
|
|
834
|
+
]
|
|
842
835
|
if missing_keyword_only_arguments:
|
|
843
836
|
missing_names = [f"'{name}'" for name in missing_keyword_only_arguments]
|
|
844
837
|
msg = (
|
|
845
838
|
f"{method_name}() missing {len(missing_names)} "
|
|
846
|
-
|
|
839
|
+
"required keyword-only argument"
|
|
847
840
|
f"{'' if len(missing_names) == 1 else 's'}: "
|
|
848
841
|
)
|
|
849
842
|
_raise_missing_names_type_error(missing_names, msg)
|
|
@@ -865,10 +858,8 @@ def _raise_missing_names_type_error(missing_names: List[str], msg: str) -> None:
|
|
|
865
858
|
raise TypeError(msg)
|
|
866
859
|
|
|
867
860
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
_annotations_mention_id: Set[MetaAggregate[Aggregate]] = set()
|
|
871
|
-
_init_mentions_id: Set[MetaAggregate[Aggregate]] = set()
|
|
861
|
+
_annotations_mention_id: set[MetaAggregate[Aggregate]] = set()
|
|
862
|
+
_init_mentions_id: set[MetaAggregate[Aggregate]] = set()
|
|
872
863
|
|
|
873
864
|
|
|
874
865
|
class MetaAggregate(type, Generic[TAggregate]):
|
|
@@ -905,7 +896,7 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
905
896
|
|
|
906
897
|
_created_event_class: Type[CanInitAggregate]
|
|
907
898
|
|
|
908
|
-
def __new__(cls, *args: Any, **
|
|
899
|
+
def __new__(cls, *args: Any, **_: Any) -> MetaAggregate[Aggregate]:
|
|
909
900
|
"""
|
|
910
901
|
Configures aggregate class definition.
|
|
911
902
|
"""
|
|
@@ -950,6 +941,7 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
950
941
|
setattr(cls, base_event_name, base_event_cls)
|
|
951
942
|
|
|
952
943
|
# Make sure all events defined on aggregate subclass the base event class.
|
|
944
|
+
created_event_classes: Dict[str, Type[CanInitAggregate]] = {}
|
|
953
945
|
for name, value in tuple(cls.__dict__.items()):
|
|
954
946
|
if name == base_event_name:
|
|
955
947
|
# Don't subclass the base event class again.
|
|
@@ -957,36 +949,26 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
957
949
|
if name.lower() == name:
|
|
958
950
|
# Don't subclass lowercase named attributes that have classes.
|
|
959
951
|
continue
|
|
960
|
-
if
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
)
|
|
965
|
-
setattr(cls, name, sub_class)
|
|
966
|
-
|
|
967
|
-
# Identify or define the aggregate's "created" event class.
|
|
968
|
-
created_event_class: Optional[Type[CanInitAggregate]] = None
|
|
969
|
-
|
|
970
|
-
# Has the "created" event class been indicated with '_created_event_class'.
|
|
971
|
-
if "_created_event_class" in cls.__dict__:
|
|
972
|
-
created_event_class = cls.__dict__["_created_event_class"]
|
|
973
|
-
if isinstance(created_event_class, type) and issubclass(
|
|
974
|
-
created_event_class, CanInitAggregate
|
|
952
|
+
if (
|
|
953
|
+
isinstance(value, type)
|
|
954
|
+
and issubclass(value, AggregateEvent)
|
|
955
|
+
and not issubclass(value, base_event_cls)
|
|
975
956
|
):
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
raise TypeError(
|
|
982
|
-
f"{created_event_class} not subclass of {CanInitAggregate.__name__}"
|
|
983
|
-
)
|
|
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
|
|
984
962
|
|
|
985
963
|
# Disallow using both '_created_event_class' and 'created_event_name'.
|
|
964
|
+
created_event_class: Type[CanInitAggregate] | None = cls.__dict__.get(
|
|
965
|
+
"_created_event_class"
|
|
966
|
+
)
|
|
986
967
|
if created_event_class and created_event_name:
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
968
|
+
msg = "Can't use both '_created_event_class' and 'created_event_name'"
|
|
969
|
+
raise TypeError(msg)
|
|
970
|
+
|
|
971
|
+
# Identify or define the aggregate's "created" event class.
|
|
990
972
|
|
|
991
973
|
# Is the init method decorated with a CommandMethodDecorator?
|
|
992
974
|
if isinstance(cls.__dict__.get("__init__"), CommandMethodDecorator):
|
|
@@ -995,112 +977,119 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
995
977
|
# Set the original method on the class (un-decorate __init__).
|
|
996
978
|
cls.__init__ = init_decorator.decorated_method # type: ignore
|
|
997
979
|
|
|
998
|
-
# Disallow using both 'created_event_name' and
|
|
980
|
+
# Disallow using both 'created_event_name' and decorator on __init__.
|
|
999
981
|
if created_event_name:
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
# Does the decorator specify a "create" event class?
|
|
982
|
+
msg = "Can't use both 'created_event_name' and decorator on __init__"
|
|
983
|
+
raise TypeError(msg)
|
|
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)
|
|
988
|
+
|
|
989
|
+
# Does the decorator specify a "created" event class?
|
|
1009
990
|
if init_decorator.given_event_cls:
|
|
1010
|
-
created_event_class =
|
|
1011
|
-
|
|
991
|
+
created_event_class = cast(
|
|
992
|
+
Type[CanInitAggregate], init_decorator.given_event_cls
|
|
1012
993
|
)
|
|
1013
|
-
if isinstance(created_event_class, type) and issubclass(
|
|
1014
|
-
created_event_class, CanInitAggregate
|
|
1015
|
-
):
|
|
1016
|
-
assert created_event_class
|
|
1017
|
-
cls._created_event_class = created_event_class
|
|
1018
|
-
else:
|
|
1019
|
-
raise TypeError(
|
|
1020
|
-
f"{created_event_class} not subclass of "
|
|
1021
|
-
f"{CanInitAggregate.__name__}"
|
|
1022
|
-
)
|
|
1023
|
-
|
|
1024
994
|
# Does the decorator specify a "created" event name?
|
|
1025
995
|
elif init_decorator.event_cls_name:
|
|
1026
996
|
created_event_name = init_decorator.event_cls_name
|
|
1027
997
|
|
|
1028
998
|
# Disallow using decorator on __init__ without event spec.
|
|
1029
999
|
else:
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
)
|
|
1000
|
+
msg = "Decorator on __init__ has neither event name nor class"
|
|
1001
|
+
raise TypeError(msg)
|
|
1033
1002
|
|
|
1034
|
-
#
|
|
1003
|
+
# TODO: Write a test to cover this when "Created" class is explicitly defined.
|
|
1035
1004
|
# Check if init mentions ID.
|
|
1036
1005
|
for param_name in inspect.signature(cls.__init__).parameters: # type: ignore
|
|
1037
1006
|
if param_name == "id":
|
|
1038
1007
|
_init_mentions_id.add(cls)
|
|
1039
1008
|
break
|
|
1040
1009
|
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1010
|
+
if created_event_class:
|
|
1011
|
+
# Check specified "created" event class can init aggregate.
|
|
1012
|
+
if not issubclass(created_event_class, CanInitAggregate):
|
|
1013
|
+
msg = (
|
|
1014
|
+
f"{created_event_class} not subclass of {CanInitAggregate.__name__}"
|
|
1015
|
+
)
|
|
1016
|
+
raise TypeError(msg)
|
|
1017
|
+
|
|
1018
|
+
for sub_class in created_event_classes.values():
|
|
1019
|
+
if issubclass(sub_class, created_event_class):
|
|
1020
|
+
# We just subclassed the created event class, so reassign it.
|
|
1021
|
+
created_event_class = sub_class
|
|
1022
|
+
|
|
1023
|
+
# Is a "created" event class already defined that matches the name?
|
|
1024
|
+
elif created_event_name and created_event_name in created_event_classes:
|
|
1025
|
+
created_event_class = created_event_classes[created_event_name]
|
|
1026
|
+
|
|
1027
|
+
# If there is only one class defined, then use it.
|
|
1028
|
+
elif len(created_event_classes) == 1 and not created_event_name:
|
|
1029
|
+
created_event_class = next(iter(created_event_classes.values()))
|
|
1030
|
+
|
|
1031
|
+
# If there are no "created" event classes already defined, or a name is
|
|
1032
|
+
# specified that hasn't matched, then define a "created" event class.
|
|
1033
|
+
elif len(created_event_classes) == 0 or created_event_name:
|
|
1034
|
+
|
|
1035
|
+
# Decide the base classes for the new "created" event class.
|
|
1036
|
+
if created_event_name and len(created_event_classes) == 1:
|
|
1037
|
+
base_created_event_cls = next(iter(created_event_classes.values()))
|
|
1038
|
+
else:
|
|
1039
|
+
for base_cls in cls.__mro__:
|
|
1040
|
+
if base_cls is cls:
|
|
1041
|
+
continue
|
|
1042
|
+
base_created_event_cls = base_cls.__dict__.get(
|
|
1043
|
+
"_created_event_class",
|
|
1044
|
+
base_cls.__dict__.get("Created"),
|
|
1045
|
+
)
|
|
1046
|
+
if base_created_event_cls:
|
|
1047
|
+
break
|
|
1048
|
+
else: # pragma: no cover
|
|
1049
|
+
msg = "Can't decide base class for new 'created' event class"
|
|
1050
|
+
raise TypeError(msg)
|
|
1051
|
+
|
|
1052
|
+
if not created_event_name:
|
|
1053
|
+
created_event_name = base_created_event_cls.__name__
|
|
1054
|
+
|
|
1055
|
+
# Disallow init method from having variable params if
|
|
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
|
|
1061
|
+
else:
|
|
1067
1062
|
try:
|
|
1068
|
-
init_method
|
|
1069
|
-
except
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
else:
|
|
1082
|
-
bases = (cls.Created, base_event_cls)
|
|
1083
|
-
event_cls = cls._define_event_class(
|
|
1063
|
+
_check_no_variable_params(init_method)
|
|
1064
|
+
except TypeError:
|
|
1065
|
+
raise
|
|
1066
|
+
|
|
1067
|
+
# Define a "created" event class for this aggregate.
|
|
1068
|
+
if issubclass(base_created_event_cls, base_event_cls):
|
|
1069
|
+
# Don't subclass from base event class twice.
|
|
1070
|
+
bases: Tuple[Type[CanMutateAggregate], ...] = (base_created_event_cls,)
|
|
1071
|
+
else:
|
|
1072
|
+
bases = (base_created_event_cls, base_event_cls)
|
|
1073
|
+
created_event_class = cast(
|
|
1074
|
+
Type[CanInitAggregate],
|
|
1075
|
+
cls._define_event_class(
|
|
1084
1076
|
created_event_name,
|
|
1085
1077
|
bases,
|
|
1086
1078
|
init_method,
|
|
1087
|
-
)
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
# Remember which is the "created" event class.
|
|
1093
|
-
cls._created_event_class = cast(Type[AggregateCreated], event_cls)
|
|
1079
|
+
),
|
|
1080
|
+
)
|
|
1081
|
+
# Set the event class as an attribute of the aggregate class.
|
|
1082
|
+
setattr(cls, created_event_name, created_event_class)
|
|
1094
1083
|
|
|
1084
|
+
if created_event_class:
|
|
1085
|
+
cls._created_event_class = created_event_class
|
|
1086
|
+
else:
|
|
1095
1087
|
# Prepare to disallow ambiguity of choice between created event classes.
|
|
1096
|
-
|
|
1097
|
-
aggregate_has_many_created_event_classes[cls] = list(
|
|
1098
|
-
created_event_classes
|
|
1099
|
-
)
|
|
1088
|
+
aggregate_has_many_created_event_classes[cls] = list(created_event_classes)
|
|
1100
1089
|
|
|
1101
1090
|
# Prepare the subsequent event classes.
|
|
1102
1091
|
for attr_name, attr_value in tuple(cls.__dict__.items()):
|
|
1103
|
-
event_decorator:
|
|
1092
|
+
event_decorator: CommandMethodDecorator | None = None
|
|
1104
1093
|
|
|
1105
1094
|
if isinstance(attr_value, CommandMethodDecorator):
|
|
1106
1095
|
event_decorator = attr_value
|
|
@@ -1123,24 +1112,25 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
1123
1112
|
# is a decorated method.
|
|
1124
1113
|
continue
|
|
1125
1114
|
# Otherwise, it's "x = property(getx, event(setx))".
|
|
1126
|
-
|
|
1115
|
+
elif event_decorator.is_name_inferred_from_method:
|
|
1127
1116
|
# This is the "@property.setter \ @event" form. We don't want
|
|
1128
1117
|
# event class name inferred from property (not past participle).
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1118
|
+
method_name = event_decorator.decorated_method.__name__
|
|
1119
|
+
msg = (
|
|
1120
|
+
f"@event under {method_name}() property setter requires "
|
|
1121
|
+
"event class name"
|
|
1122
|
+
)
|
|
1123
|
+
raise TypeError(msg)
|
|
1135
1124
|
|
|
1136
1125
|
if event_decorator is not None:
|
|
1137
1126
|
if event_decorator.given_event_cls:
|
|
1138
1127
|
# Check this is not a "created" event class.
|
|
1139
1128
|
if issubclass(event_decorator.given_event_cls, CanInitAggregate):
|
|
1140
|
-
|
|
1129
|
+
msg = (
|
|
1141
1130
|
f"{event_decorator.given_event_cls} "
|
|
1142
1131
|
f"is subclass of {CanInitAggregate.__name__}"
|
|
1143
1132
|
)
|
|
1133
|
+
raise TypeError(msg)
|
|
1144
1134
|
|
|
1145
1135
|
# Define event class as subclass of given class.
|
|
1146
1136
|
given_subclass = cast(
|
|
@@ -1157,10 +1147,11 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
1157
1147
|
# Check event class isn't already defined.
|
|
1158
1148
|
assert event_decorator.event_cls_name
|
|
1159
1149
|
if event_decorator.event_cls_name in cls.__dict__:
|
|
1160
|
-
|
|
1150
|
+
msg = (
|
|
1161
1151
|
f"{event_decorator.event_cls_name} "
|
|
1162
1152
|
f"event already defined on {cls.__name__}"
|
|
1163
1153
|
)
|
|
1154
|
+
raise TypeError(msg)
|
|
1164
1155
|
|
|
1165
1156
|
# Define event class from signature of original method.
|
|
1166
1157
|
event_cls = cls._define_event_class(
|
|
@@ -1181,12 +1172,14 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
1181
1172
|
)
|
|
1182
1173
|
|
|
1183
1174
|
# Check any create_id method defined on this class is static or class method.
|
|
1184
|
-
if "create_id" in cls.__dict__
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
)
|
|
1175
|
+
if "create_id" in cls.__dict__ and not isinstance(
|
|
1176
|
+
cls.__dict__["create_id"], (staticmethod, classmethod)
|
|
1177
|
+
):
|
|
1178
|
+
msg = (
|
|
1179
|
+
f"{cls.create_id} is not a static or class method: "
|
|
1180
|
+
f"{type(cls.create_id)}"
|
|
1181
|
+
)
|
|
1182
|
+
raise TypeError(msg)
|
|
1190
1183
|
|
|
1191
1184
|
# Get the parameters of the create_id method that will be used by this class.
|
|
1192
1185
|
cls._create_id_param_names: List[str] = []
|
|
@@ -1212,7 +1205,7 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
1212
1205
|
cls,
|
|
1213
1206
|
name: str,
|
|
1214
1207
|
bases: Tuple[Type[CanMutateAggregate], ...],
|
|
1215
|
-
apply_method:
|
|
1208
|
+
apply_method: CommandMethod | None,
|
|
1216
1209
|
) -> Type[CanMutateAggregate]:
|
|
1217
1210
|
# Define annotations for the event class (specs the init method).
|
|
1218
1211
|
annotations = {}
|
|
@@ -1229,7 +1222,7 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
1229
1222
|
# Don't override super class annotations, unless no default on param.
|
|
1230
1223
|
if param_name not in super_annotations or param.default == param.empty:
|
|
1231
1224
|
annotations[param_name] = param.annotation or "typing.Any"
|
|
1232
|
-
event_cls_qualname = "
|
|
1225
|
+
event_cls_qualname = f"{cls.__qualname__}.{name}"
|
|
1233
1226
|
event_cls_dict = {
|
|
1234
1227
|
"__annotations__": annotations,
|
|
1235
1228
|
"__module__": cls.__module__,
|
|
@@ -1244,15 +1237,16 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
1244
1237
|
) -> TAggregate:
|
|
1245
1238
|
try:
|
|
1246
1239
|
created_event_classes = aggregate_has_many_created_event_classes[cls]
|
|
1247
|
-
|
|
1240
|
+
msg = (
|
|
1248
1241
|
"""Can't decide which of many "created" event classes to use: """
|
|
1249
1242
|
f"""'{"', '".join(created_event_classes)}'. Please use class """
|
|
1250
1243
|
"arg 'created_event_name' or @event decorator on __init__ method."
|
|
1251
1244
|
)
|
|
1245
|
+
raise TypeError(msg)
|
|
1252
1246
|
except KeyError:
|
|
1253
1247
|
pass
|
|
1254
1248
|
|
|
1255
|
-
cls_init:
|
|
1249
|
+
cls_init: FunctionType | WrapperDescriptorType = cls.__init__ # type: ignore
|
|
1256
1250
|
kwargs = _coerce_args_to_kwargs(
|
|
1257
1251
|
cls_init,
|
|
1258
1252
|
args,
|
|
@@ -1269,10 +1263,10 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
1269
1263
|
event_class: Type[CanInitAggregate],
|
|
1270
1264
|
**kwargs: Any,
|
|
1271
1265
|
) -> TAggregate:
|
|
1272
|
-
raise NotImplementedError
|
|
1266
|
+
raise NotImplementedError # pragma: no cover
|
|
1273
1267
|
|
|
1274
1268
|
@staticmethod
|
|
1275
|
-
def create_id(**
|
|
1269
|
+
def create_id(**_: Any) -> UUID:
|
|
1276
1270
|
"""
|
|
1277
1271
|
Returns a new aggregate ID.
|
|
1278
1272
|
"""
|
|
@@ -1291,7 +1285,7 @@ class Aggregate(metaclass=MetaAggregate):
|
|
|
1291
1285
|
cls: Type[TAggregate],
|
|
1292
1286
|
event_class: Type[CanInitAggregate],
|
|
1293
1287
|
*,
|
|
1294
|
-
id:
|
|
1288
|
+
id: UUID | None = None, # noqa: A002
|
|
1295
1289
|
**kwargs: Any,
|
|
1296
1290
|
) -> TAggregate:
|
|
1297
1291
|
"""
|
|
@@ -1318,7 +1312,7 @@ class Aggregate(metaclass=MetaAggregate):
|
|
|
1318
1312
|
created_event = event_class(**kwargs)
|
|
1319
1313
|
except TypeError as e:
|
|
1320
1314
|
msg = f"Unable to construct '{event_class.__name__}' event: {e}"
|
|
1321
|
-
raise TypeError(msg)
|
|
1315
|
+
raise TypeError(msg) from None
|
|
1322
1316
|
# Construct the aggregate object.
|
|
1323
1317
|
agg = cast(TAggregate, created_event.mutate(None))
|
|
1324
1318
|
|
|
@@ -1390,7 +1384,7 @@ class Aggregate(metaclass=MetaAggregate):
|
|
|
1390
1384
|
class Created(Event, AggregateCreated):
|
|
1391
1385
|
pass
|
|
1392
1386
|
|
|
1393
|
-
def __eq__(self, other:
|
|
1387
|
+
def __eq__(self, other: object) -> bool:
|
|
1394
1388
|
return type(self) is type(other) and self.__dict__ == other.__dict__
|
|
1395
1389
|
|
|
1396
1390
|
def __repr__(self) -> str:
|
|
@@ -1427,7 +1421,8 @@ class Aggregate(metaclass=MetaAggregate):
|
|
|
1427
1421
|
try:
|
|
1428
1422
|
new_event = event_class(**kwargs)
|
|
1429
1423
|
except TypeError as e:
|
|
1430
|
-
|
|
1424
|
+
msg = f"Can't construct event {event_class}: {e}"
|
|
1425
|
+
raise TypeError(msg) from None
|
|
1431
1426
|
|
|
1432
1427
|
# Mutate aggregate with domain event.
|
|
1433
1428
|
new_event.mutate(self)
|
|
@@ -1456,10 +1451,10 @@ class Aggregate(metaclass=MetaAggregate):
|
|
|
1456
1451
|
|
|
1457
1452
|
|
|
1458
1453
|
def aggregate(
|
|
1459
|
-
cls:
|
|
1454
|
+
cls: Any | None = None,
|
|
1460
1455
|
*,
|
|
1461
1456
|
created_event_name: str = "",
|
|
1462
|
-
) ->
|
|
1457
|
+
) -> Type[Aggregate] | Callable[[Any], Type[Aggregate]]:
|
|
1463
1458
|
"""
|
|
1464
1459
|
Converts the class that was passed in to inherit from Aggregate.
|
|
1465
1460
|
|
|
@@ -1479,13 +1474,14 @@ def aggregate(
|
|
|
1479
1474
|
|
|
1480
1475
|
def decorator(cls_: Any) -> Type[Aggregate]:
|
|
1481
1476
|
if issubclass(cls_, Aggregate):
|
|
1482
|
-
|
|
1477
|
+
msg = f"{cls_.__qualname__} is already an Aggregate"
|
|
1478
|
+
raise TypeError(msg)
|
|
1483
1479
|
bases = cls_.__bases__
|
|
1484
1480
|
if bases == (object,):
|
|
1485
1481
|
bases = (Aggregate,)
|
|
1486
1482
|
else:
|
|
1487
1483
|
bases += (Aggregate,)
|
|
1488
|
-
cls_dict =
|
|
1484
|
+
cls_dict = {}
|
|
1489
1485
|
cls_dict.update(cls_.__dict__)
|
|
1490
1486
|
cls_ = MetaAggregate(
|
|
1491
1487
|
cls_.__qualname__,
|
|
@@ -1498,8 +1494,7 @@ def aggregate(
|
|
|
1498
1494
|
|
|
1499
1495
|
if cls:
|
|
1500
1496
|
return decorator(cls)
|
|
1501
|
-
|
|
1502
|
-
return decorator
|
|
1497
|
+
return decorator
|
|
1503
1498
|
|
|
1504
1499
|
|
|
1505
1500
|
class OriginatorIDError(EventSourcingError):
|
|
@@ -1543,7 +1538,7 @@ class SnapshotProtocol(DomainEventProtocol, Protocol):
|
|
|
1543
1538
|
Snapshots have a read-only 'state'.
|
|
1544
1539
|
"""
|
|
1545
1540
|
|
|
1546
|
-
#
|
|
1541
|
+
# TODO: Improve on this 'Any'.
|
|
1547
1542
|
@classmethod
|
|
1548
1543
|
def take(cls: Any, aggregate: Any) -> Any:
|
|
1549
1544
|
"""
|
|
@@ -1556,7 +1551,7 @@ TCanSnapshotAggregate = TypeVar("TCanSnapshotAggregate", bound="CanSnapshotAggre
|
|
|
1556
1551
|
|
|
1557
1552
|
class CanSnapshotAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
|
|
1558
1553
|
topic: str
|
|
1559
|
-
state:
|
|
1554
|
+
state: Any
|
|
1560
1555
|
|
|
1561
1556
|
@classmethod
|
|
1562
1557
|
def take(
|
|
@@ -1574,14 +1569,13 @@ class CanSnapshotAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
|
|
|
1574
1569
|
aggregate_state.pop("_id")
|
|
1575
1570
|
aggregate_state.pop("_version")
|
|
1576
1571
|
aggregate_state.pop("_pending_events")
|
|
1577
|
-
|
|
1572
|
+
return cls( # type: ignore
|
|
1578
1573
|
originator_id=aggregate.id,
|
|
1579
1574
|
originator_version=aggregate.version,
|
|
1580
1575
|
timestamp=cls.create_timestamp(),
|
|
1581
1576
|
topic=get_topic(type(aggregate)),
|
|
1582
1577
|
state=aggregate_state,
|
|
1583
1578
|
)
|
|
1584
|
-
return snapshot
|
|
1585
1579
|
|
|
1586
1580
|
def mutate(self, _: None) -> Aggregate:
|
|
1587
1581
|
"""
|