eventsourcing 9.4.0a5__tar.gz → 9.4.0a7__tar.gz
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-9.4.0a5 → eventsourcing-9.4.0a7}/PKG-INFO +2 -4
- {eventsourcing-9.4.0a5 → eventsourcing-9.4.0a7}/eventsourcing/application.py +22 -37
- {eventsourcing-9.4.0a5 → eventsourcing-9.4.0a7}/eventsourcing/domain.py +88 -55
- {eventsourcing-9.4.0a5 → eventsourcing-9.4.0a7}/eventsourcing/interface.py +5 -2
- {eventsourcing-9.4.0a5 → eventsourcing-9.4.0a7}/eventsourcing/persistence.py +29 -36
- {eventsourcing-9.4.0a5 → eventsourcing-9.4.0a7}/eventsourcing/popo.py +17 -16
- {eventsourcing-9.4.0a5 → eventsourcing-9.4.0a7}/eventsourcing/postgres.py +18 -17
- {eventsourcing-9.4.0a5 → eventsourcing-9.4.0a7}/eventsourcing/projection.py +8 -17
- {eventsourcing-9.4.0a5 → eventsourcing-9.4.0a7}/eventsourcing/sqlite.py +18 -17
- {eventsourcing-9.4.0a5 → eventsourcing-9.4.0a7}/eventsourcing/system.py +49 -62
- {eventsourcing-9.4.0a5 → eventsourcing-9.4.0a7}/eventsourcing/tests/application.py +3 -3
- {eventsourcing-9.4.0a5 → eventsourcing-9.4.0a7}/eventsourcing/tests/persistence.py +12 -12
- {eventsourcing-9.4.0a5 → eventsourcing-9.4.0a7}/eventsourcing/utils.py +8 -20
- {eventsourcing-9.4.0a5 → eventsourcing-9.4.0a7}/pyproject.toml +7 -13
- {eventsourcing-9.4.0a5 → eventsourcing-9.4.0a7}/AUTHORS +0 -0
- {eventsourcing-9.4.0a5 → eventsourcing-9.4.0a7}/LICENSE +0 -0
- {eventsourcing-9.4.0a5 → eventsourcing-9.4.0a7}/README.md +0 -0
- {eventsourcing-9.4.0a5 → eventsourcing-9.4.0a7}/eventsourcing/__init__.py +0 -0
- {eventsourcing-9.4.0a5 → eventsourcing-9.4.0a7}/eventsourcing/cipher.py +0 -0
- {eventsourcing-9.4.0a5 → eventsourcing-9.4.0a7}/eventsourcing/compressor.py +0 -0
- {eventsourcing-9.4.0a5 → eventsourcing-9.4.0a7}/eventsourcing/cryptography.py +0 -0
- {eventsourcing-9.4.0a5 → eventsourcing-9.4.0a7}/eventsourcing/dispatch.py +0 -0
- {eventsourcing-9.4.0a5 → eventsourcing-9.4.0a7}/eventsourcing/py.typed +0 -0
- {eventsourcing-9.4.0a5 → eventsourcing-9.4.0a7}/eventsourcing/tests/__init__.py +0 -0
- {eventsourcing-9.4.0a5 → eventsourcing-9.4.0a7}/eventsourcing/tests/domain.py +0 -0
- {eventsourcing-9.4.0a5 → eventsourcing-9.4.0a7}/eventsourcing/tests/postgres_utils.py +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: eventsourcing
|
|
3
|
-
Version: 9.4.
|
|
3
|
+
Version: 9.4.0a7
|
|
4
4
|
Summary: Event sourcing in Python
|
|
5
5
|
License: BSD 3-Clause
|
|
6
6
|
Keywords: event sourcing,event store,domain driven design,domain-driven design,ddd,cqrs,cqs
|
|
7
7
|
Author: John Bywater
|
|
8
8
|
Author-email: john.bywater@appropriatesoftware.net
|
|
9
|
-
Requires-Python: >=3.
|
|
9
|
+
Requires-Python: >=3.9, !=2.7.*, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*, !=3.7.*, !=3.8.*
|
|
10
10
|
Classifier: Development Status :: 3 - Alpha
|
|
11
11
|
Classifier: Intended Audience :: Developers
|
|
12
12
|
Classifier: Intended Audience :: Education
|
|
@@ -16,7 +16,6 @@ Classifier: License :: Other/Proprietary License
|
|
|
16
16
|
Classifier: Operating System :: OS Independent
|
|
17
17
|
Classifier: Programming Language :: Python
|
|
18
18
|
Classifier: Programming Language :: Python :: 3
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.10
|
|
21
20
|
Classifier: Programming Language :: Python :: 3.11
|
|
22
21
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -26,7 +25,6 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
26
25
|
Provides-Extra: crypto
|
|
27
26
|
Provides-Extra: cryptography
|
|
28
27
|
Provides-Extra: postgres
|
|
29
|
-
Requires-Dist: backports.zoneinfo ; python_version < "3.9"
|
|
30
28
|
Requires-Dist: cryptography (>=44.0,<44.1) ; extra == "cryptography"
|
|
31
29
|
Requires-Dist: psycopg[pool] (<=3.2.99999) ; extra == "postgres"
|
|
32
30
|
Requires-Dist: pycryptodome (>=3.22,<3.23) ; extra == "crypto"
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
from abc import ABC, abstractmethod
|
|
5
|
+
from collections.abc import Iterable, Iterator, Sequence
|
|
5
6
|
from copy import deepcopy
|
|
6
7
|
from dataclasses import dataclass
|
|
7
8
|
from itertools import chain
|
|
@@ -11,22 +12,13 @@ from typing import (
|
|
|
11
12
|
Any,
|
|
12
13
|
Callable,
|
|
13
14
|
ClassVar,
|
|
14
|
-
Dict,
|
|
15
15
|
Generic,
|
|
16
|
-
Iterable,
|
|
17
|
-
Iterator,
|
|
18
|
-
List,
|
|
19
16
|
Optional,
|
|
20
|
-
Sequence,
|
|
21
|
-
Tuple,
|
|
22
|
-
Type,
|
|
23
17
|
TypeVar,
|
|
24
18
|
cast,
|
|
25
19
|
)
|
|
26
20
|
from warnings import warn
|
|
27
21
|
|
|
28
|
-
from typing_extensions import deprecated
|
|
29
|
-
|
|
30
22
|
from eventsourcing.domain import (
|
|
31
23
|
Aggregate,
|
|
32
24
|
CanMutateProtocol,
|
|
@@ -96,7 +88,7 @@ T = TypeVar("T")
|
|
|
96
88
|
|
|
97
89
|
class Cache(Generic[S, T]):
|
|
98
90
|
def __init__(self) -> None:
|
|
99
|
-
self.cache:
|
|
91
|
+
self.cache: dict[S, Any] = {}
|
|
100
92
|
|
|
101
93
|
def get(self, key: S, *, evict: bool = False) -> T:
|
|
102
94
|
if evict:
|
|
@@ -127,7 +119,7 @@ class LRUCache(Cache[S, T]):
|
|
|
127
119
|
self.maxsize = maxsize
|
|
128
120
|
self.full = False
|
|
129
121
|
self.lock = Lock() # because linkedlist updates aren't threadsafe
|
|
130
|
-
self.root:
|
|
122
|
+
self.root: list[Any] = [] # root of the circular doubly linked list
|
|
131
123
|
self.clear()
|
|
132
124
|
|
|
133
125
|
def clear(self) -> None:
|
|
@@ -253,7 +245,7 @@ class Repository:
|
|
|
253
245
|
self._fastforward_locks_cache: LRUCache[UUID, Lock] = LRUCache(
|
|
254
246
|
maxsize=self.FASTFORWARD_LOCKS_CACHE_MAXSIZE
|
|
255
247
|
)
|
|
256
|
-
self._fastforward_locks_inuse:
|
|
248
|
+
self._fastforward_locks_inuse: dict[UUID, tuple[Lock, int]] = {}
|
|
257
249
|
|
|
258
250
|
def get(
|
|
259
251
|
self,
|
|
@@ -415,12 +407,12 @@ class Section:
|
|
|
415
407
|
Constructor arguments:
|
|
416
408
|
|
|
417
409
|
:param Optional[str] id: section ID of this section e.g. "1,10"
|
|
418
|
-
:param
|
|
410
|
+
:param list[Notification] items: a list of event notifications
|
|
419
411
|
:param Optional[str] next_id: section ID of the following section
|
|
420
412
|
"""
|
|
421
413
|
|
|
422
414
|
id: str | None
|
|
423
|
-
items:
|
|
415
|
+
items: list[Notification]
|
|
424
416
|
next_id: str | None
|
|
425
417
|
|
|
426
418
|
|
|
@@ -446,7 +438,7 @@ class NotificationLog(ABC):
|
|
|
446
438
|
topics: Sequence[str] = (),
|
|
447
439
|
*,
|
|
448
440
|
inclusive_of_start: bool = True,
|
|
449
|
-
) ->
|
|
441
|
+
) -> list[Notification]:
|
|
450
442
|
"""
|
|
451
443
|
Returns a selection of
|
|
452
444
|
:class:`~eventsourcing.persistence.Notification` objects
|
|
@@ -536,7 +528,7 @@ class LocalNotificationLog(NotificationLog):
|
|
|
536
528
|
topics: Sequence[str] = (),
|
|
537
529
|
*,
|
|
538
530
|
inclusive_of_start: bool = True,
|
|
539
|
-
) ->
|
|
531
|
+
) -> list[Notification]:
|
|
540
532
|
"""
|
|
541
533
|
Returns a selection of
|
|
542
534
|
:class:`~eventsourcing.persistence.Notification` objects
|
|
@@ -573,9 +565,9 @@ class ProcessingEvent:
|
|
|
573
565
|
Initialises the process event with the given tracking object.
|
|
574
566
|
"""
|
|
575
567
|
self.tracking = tracking
|
|
576
|
-
self.events:
|
|
577
|
-
self.aggregates:
|
|
578
|
-
self.saved_kwargs:
|
|
568
|
+
self.events: list[DomainEventProtocol] = []
|
|
569
|
+
self.aggregates: dict[UUID, MutableOrImmutableAggregate] = {}
|
|
570
|
+
self.saved_kwargs: dict[Any, Any] = {}
|
|
579
571
|
|
|
580
572
|
def collect_events(
|
|
581
573
|
self,
|
|
@@ -618,15 +610,15 @@ class Application:
|
|
|
618
610
|
"""
|
|
619
611
|
|
|
620
612
|
name = "Application"
|
|
621
|
-
env: ClassVar[
|
|
613
|
+
env: ClassVar[dict[str, str]] = {}
|
|
622
614
|
is_snapshotting_enabled: bool = False
|
|
623
615
|
snapshotting_intervals: ClassVar[
|
|
624
|
-
|
|
616
|
+
dict[type[MutableOrImmutableAggregate], int] | None
|
|
625
617
|
] = None
|
|
626
618
|
snapshotting_projectors: ClassVar[
|
|
627
|
-
|
|
619
|
+
dict[type[MutableOrImmutableAggregate], ProjectorFunction[Any, Any]] | None
|
|
628
620
|
] = None
|
|
629
|
-
snapshot_class:
|
|
621
|
+
snapshot_class: type[SnapshotProtocol] = Snapshot
|
|
630
622
|
log_section_size = 10
|
|
631
623
|
notify_topics: Sequence[str] = []
|
|
632
624
|
|
|
@@ -792,7 +784,7 @@ class Application:
|
|
|
792
784
|
self,
|
|
793
785
|
*objs: MutableOrImmutableAggregate | DomainEventProtocol | None,
|
|
794
786
|
**kwargs: Any,
|
|
795
|
-
) ->
|
|
787
|
+
) -> list[Recording]:
|
|
796
788
|
"""
|
|
797
789
|
Collects pending events from given aggregates and
|
|
798
790
|
puts them in the application's event store.
|
|
@@ -805,7 +797,7 @@ class Application:
|
|
|
805
797
|
self.notify(processing_event.events) # Deprecated.
|
|
806
798
|
return recordings
|
|
807
799
|
|
|
808
|
-
def _record(self, processing_event: ProcessingEvent) ->
|
|
800
|
+
def _record(self, processing_event: ProcessingEvent) -> list[Recording]:
|
|
809
801
|
"""
|
|
810
802
|
Records given process event in the application's recorder.
|
|
811
803
|
"""
|
|
@@ -885,7 +877,7 @@ class Application:
|
|
|
885
877
|
snapshot = snapshot_class.take(aggregate)
|
|
886
878
|
self.snapshots.put([snapshot])
|
|
887
879
|
|
|
888
|
-
def notify(self, new_events:
|
|
880
|
+
def notify(self, new_events: list[DomainEventProtocol]) -> None:
|
|
889
881
|
"""
|
|
890
882
|
Deprecated.
|
|
891
883
|
|
|
@@ -895,7 +887,7 @@ class Application:
|
|
|
895
887
|
need to take action when new domain events have been saved.
|
|
896
888
|
"""
|
|
897
889
|
|
|
898
|
-
def _notify(self, recordings:
|
|
890
|
+
def _notify(self, recordings: list[Recording]) -> None:
|
|
899
891
|
"""
|
|
900
892
|
Called after new aggregate events have been saved. This
|
|
901
893
|
method on this class doesn't actually do anything,
|
|
@@ -911,14 +903,7 @@ class Application:
|
|
|
911
903
|
TApplication = TypeVar("TApplication", bound=Application)
|
|
912
904
|
|
|
913
905
|
|
|
914
|
-
|
|
915
|
-
"AggregateNotFound is deprecated, use AggregateNotFoundError instead", category=None
|
|
916
|
-
)
|
|
917
|
-
class AggregateNotFound(EventSourcingError): # noqa: N818
|
|
918
|
-
pass
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
class AggregateNotFoundError(AggregateNotFound):
|
|
906
|
+
class AggregateNotFoundError(EventSourcingError):
|
|
922
907
|
"""
|
|
923
908
|
Raised when an :class:`~eventsourcing.domain.Aggregate`
|
|
924
909
|
object is not found in a :class:`Repository`.
|
|
@@ -944,7 +929,7 @@ class EventSourcedLog(Generic[TDomainEvent]):
|
|
|
944
929
|
self,
|
|
945
930
|
events: EventStore,
|
|
946
931
|
originator_id: UUID,
|
|
947
|
-
logged_cls:
|
|
932
|
+
logged_cls: type[TDomainEvent], # TODO: Rename to 'event_class' in v10.
|
|
948
933
|
):
|
|
949
934
|
self.events = events
|
|
950
935
|
self.originator_id = originator_id
|
|
@@ -966,7 +951,7 @@ class EventSourcedLog(Generic[TDomainEvent]):
|
|
|
966
951
|
|
|
967
952
|
def _trigger_event(
|
|
968
953
|
self,
|
|
969
|
-
logged_cls:
|
|
954
|
+
logged_cls: type[T] | None,
|
|
970
955
|
next_originator_version: int | None = None,
|
|
971
956
|
**kwargs: Any,
|
|
972
957
|
) -> T:
|
|
@@ -1,23 +1,18 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import dataclasses
|
|
4
|
+
import importlib
|
|
3
5
|
import inspect
|
|
4
6
|
import os
|
|
5
|
-
from dataclasses import dataclass
|
|
6
7
|
from datetime import datetime, tzinfo
|
|
7
|
-
from functools import
|
|
8
|
+
from functools import cache
|
|
8
9
|
from types import FunctionType, WrapperDescriptorType
|
|
9
10
|
from typing import (
|
|
10
11
|
TYPE_CHECKING,
|
|
11
12
|
Any,
|
|
12
13
|
Callable,
|
|
13
|
-
Dict,
|
|
14
14
|
Generic,
|
|
15
|
-
Iterable,
|
|
16
|
-
List,
|
|
17
15
|
Protocol,
|
|
18
|
-
Sequence,
|
|
19
|
-
Tuple,
|
|
20
|
-
Type,
|
|
21
16
|
TypeVar,
|
|
22
17
|
Union,
|
|
23
18
|
cast,
|
|
@@ -29,6 +24,10 @@ from warnings import warn
|
|
|
29
24
|
|
|
30
25
|
from eventsourcing.utils import get_method_name, get_topic, resolve_topic
|
|
31
26
|
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from collections.abc import Iterable, Sequence
|
|
29
|
+
|
|
30
|
+
|
|
32
31
|
TZINFO: tzinfo = resolve_topic(os.getenv("TZINFO_TOPIC", "datetime:timezone.utc"))
|
|
33
32
|
"""
|
|
34
33
|
A Python :py:obj:`tzinfo` object that defaults to UTC (:py:obj:`timezone.utc`). Used
|
|
@@ -294,7 +293,7 @@ class CanInitAggregate(CanMutateAggregate):
|
|
|
294
293
|
assert aggregate is None
|
|
295
294
|
|
|
296
295
|
# Resolve originator topic.
|
|
297
|
-
aggregate_class:
|
|
296
|
+
aggregate_class: type[TAggregate] = resolve_topic(self.originator_topic)
|
|
298
297
|
|
|
299
298
|
# Construct an aggregate object (a "shell" of the correct object type).
|
|
300
299
|
agg = aggregate_class.__new__(aggregate_class)
|
|
@@ -333,12 +332,12 @@ class MetaDomainEvent(type):
|
|
|
333
332
|
"""
|
|
334
333
|
|
|
335
334
|
def __new__(
|
|
336
|
-
cls, name: str, bases:
|
|
337
|
-
) ->
|
|
335
|
+
cls, name: str, bases: tuple[type[TDomainEvent], ...], cls_dict: dict[str, Any]
|
|
336
|
+
) -> type[TDomainEvent]:
|
|
338
337
|
event_cls = cast(
|
|
339
|
-
|
|
338
|
+
type[TDomainEvent], super().__new__(cls, name, bases, cls_dict)
|
|
340
339
|
)
|
|
341
|
-
event_cls = dataclass(frozen=True)(event_cls)
|
|
340
|
+
event_cls = dataclasses.dataclass(frozen=True)(event_cls)
|
|
342
341
|
event_cls.__signature__ = inspect.signature(event_cls.__init__) # type: ignore
|
|
343
342
|
return event_cls
|
|
344
343
|
|
|
@@ -398,20 +397,20 @@ TLogEvent = TypeVar("TLogEvent", bound=DomainEventProtocol)
|
|
|
398
397
|
|
|
399
398
|
|
|
400
399
|
def _filter_kwargs_for_method_params(
|
|
401
|
-
kwargs:
|
|
402
|
-
) ->
|
|
400
|
+
kwargs: dict[str, Any], method: Callable[..., Any]
|
|
401
|
+
) -> dict[str, Any]:
|
|
403
402
|
names = _spec_filter_kwargs_for_method_params(method)
|
|
404
403
|
return {k: v for k, v in kwargs.items() if k in names}
|
|
405
404
|
|
|
406
405
|
|
|
407
|
-
@
|
|
406
|
+
@cache
|
|
408
407
|
def _spec_filter_kwargs_for_method_params(method: Callable[..., Any]) -> set[str]:
|
|
409
408
|
method_signature = inspect.signature(method)
|
|
410
409
|
return set(method_signature.parameters)
|
|
411
410
|
|
|
412
411
|
|
|
413
412
|
if TYPE_CHECKING:
|
|
414
|
-
EventSpecType = Union[str,
|
|
413
|
+
EventSpecType = Union[str, type[CanMutateAggregate]]
|
|
415
414
|
|
|
416
415
|
CommandMethod = Callable[..., None]
|
|
417
416
|
DecoratedObjType = Union[CommandMethod, property]
|
|
@@ -425,7 +424,7 @@ class CommandMethodDecorator:
|
|
|
425
424
|
decorated_obj: DecoratedObjType,
|
|
426
425
|
):
|
|
427
426
|
self.is_name_inferred_from_method = False
|
|
428
|
-
self.given_event_cls:
|
|
427
|
+
self.given_event_cls: type[CanMutateAggregate] | None = None
|
|
429
428
|
self.event_cls_name: str | None = None
|
|
430
429
|
self.decorated_property: property | None = None
|
|
431
430
|
self.is_property_setter = False
|
|
@@ -740,12 +739,12 @@ class BoundCommandMethodDecorator:
|
|
|
740
739
|
|
|
741
740
|
|
|
742
741
|
given_event_classes: set[type] = set()
|
|
743
|
-
decorated_methods:
|
|
744
|
-
aggregate_has_many_created_event_classes:
|
|
742
|
+
decorated_methods: dict[type, CommandMethod] = {}
|
|
743
|
+
aggregate_has_many_created_event_classes: dict[type, list[str]] = {}
|
|
745
744
|
|
|
746
745
|
|
|
747
|
-
decorated_event_classes:
|
|
748
|
-
CommandMethodDecorator,
|
|
746
|
+
decorated_event_classes: dict[
|
|
747
|
+
CommandMethodDecorator, type[MetaAggregate.DecoratedEvent]
|
|
749
748
|
] = {}
|
|
750
749
|
|
|
751
750
|
|
|
@@ -767,10 +766,10 @@ def _check_no_variable_params(method: FunctionType) -> None:
|
|
|
767
766
|
def _coerce_args_to_kwargs(
|
|
768
767
|
method: FunctionType | WrapperDescriptorType,
|
|
769
768
|
args: Iterable[Any],
|
|
770
|
-
kwargs:
|
|
769
|
+
kwargs: dict[str, Any],
|
|
771
770
|
*,
|
|
772
771
|
expects_id: bool = False,
|
|
773
|
-
) ->
|
|
772
|
+
) -> dict[str, Any]:
|
|
774
773
|
assert isinstance(method, (FunctionType, WrapperDescriptorType))
|
|
775
774
|
|
|
776
775
|
args = tuple(args)
|
|
@@ -789,14 +788,14 @@ def _coerce_args_to_kwargs(
|
|
|
789
788
|
return copy_kwargs
|
|
790
789
|
|
|
791
790
|
|
|
792
|
-
@
|
|
791
|
+
@cache
|
|
793
792
|
def _spec_coerce_args_to_kwargs(
|
|
794
793
|
method: FunctionType | WrapperDescriptorType,
|
|
795
794
|
len_args: int,
|
|
796
|
-
kwargs_keys:
|
|
795
|
+
kwargs_keys: tuple[str],
|
|
797
796
|
*,
|
|
798
797
|
expects_id: bool,
|
|
799
|
-
) ->
|
|
798
|
+
) -> tuple[tuple[tuple[int, str], ...], tuple[tuple[str, Any], ...]]:
|
|
800
799
|
method_signature = inspect.signature(method)
|
|
801
800
|
positional_names = []
|
|
802
801
|
keyword_defaults = {}
|
|
@@ -874,7 +873,7 @@ def _spec_coerce_args_to_kwargs(
|
|
|
874
873
|
return enumerated_args_names, keyword_defaults_items
|
|
875
874
|
|
|
876
875
|
|
|
877
|
-
def _raise_missing_names_type_error(missing_names:
|
|
876
|
+
def _raise_missing_names_type_error(missing_names: list[str], msg: str) -> None:
|
|
878
877
|
msg += missing_names[0]
|
|
879
878
|
if len(missing_names) == 2:
|
|
880
879
|
msg += f" and {missing_names[1]}"
|
|
@@ -888,6 +887,34 @@ _annotations_mention_id: set[MetaAggregate[Aggregate]] = set()
|
|
|
888
887
|
_init_mentions_id: set[MetaAggregate[Aggregate]] = set()
|
|
889
888
|
|
|
890
889
|
|
|
890
|
+
def _ensure_idempotent_dataclass(module_name: str) -> None:
|
|
891
|
+
module = importlib.import_module(module_name)
|
|
892
|
+
if (
|
|
893
|
+
"dataclass" in module.__dict__
|
|
894
|
+
and module.__dict__["dataclass"] == dataclasses.dataclass
|
|
895
|
+
and "__original_dataclass_func__" not in module.__dict__
|
|
896
|
+
):
|
|
897
|
+
module.__dict__["__original_dataclass_func__"] = module.__dict__["dataclass"]
|
|
898
|
+
module.__dict__["dataclass"] = _idempotent_dataclass
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
def _idempotent_dataclass(cls: type[object] | None = None, /, **kwargs: Any) -> Any:
|
|
902
|
+
|
|
903
|
+
def idempotent_wrap(cls: type[object]) -> type[object]:
|
|
904
|
+
# Avoid processing dataclass twice.
|
|
905
|
+
if "__dataclass_fields__" in cls.__dict__:
|
|
906
|
+
return cls
|
|
907
|
+
return dataclasses.dataclass(**kwargs)(cls)
|
|
908
|
+
|
|
909
|
+
# See if we're being called as @dataclass or @dataclass().
|
|
910
|
+
if cls is None:
|
|
911
|
+
# We're called with parens.
|
|
912
|
+
return idempotent_wrap
|
|
913
|
+
|
|
914
|
+
# We're called as @dataclass without parens.
|
|
915
|
+
return idempotent_wrap(cls)
|
|
916
|
+
|
|
917
|
+
|
|
891
918
|
class MetaAggregate(type, Generic[TAggregate]):
|
|
892
919
|
"""
|
|
893
920
|
Metaclass for aggregate classes.
|
|
@@ -920,12 +947,18 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
920
947
|
# Call the original method with event attribute values.
|
|
921
948
|
decorated_method(aggregate, **kwargs)
|
|
922
949
|
|
|
923
|
-
_created_event_class:
|
|
950
|
+
_created_event_class: type[CanInitAggregate]
|
|
924
951
|
|
|
925
952
|
def __new__(cls, *args: Any, **_: Any) -> MetaAggregate[Aggregate]:
|
|
926
953
|
"""
|
|
927
954
|
Configures aggregate class definition.
|
|
928
955
|
"""
|
|
956
|
+
|
|
957
|
+
# Avoid processing dataclass twice. This avoids dataclasses.Field(init=False)
|
|
958
|
+
# attributes being reduced to annotation only and then appearing in __init__
|
|
959
|
+
# method signature when class is reprocessed, and other similar problems.
|
|
960
|
+
_ensure_idempotent_dataclass(module_name=args[2]["__module__"])
|
|
961
|
+
|
|
929
962
|
try:
|
|
930
963
|
class_annotations = args[2]["__annotations__"]
|
|
931
964
|
except KeyError:
|
|
@@ -939,8 +972,8 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
939
972
|
else:
|
|
940
973
|
annotations_mention_id = True
|
|
941
974
|
aggregate_cls = type.__new__(cls, *args)
|
|
942
|
-
if class_annotations:
|
|
943
|
-
aggregate_cls = dataclass(eq=False, repr=False)(aggregate_cls)
|
|
975
|
+
if class_annotations or any(dataclasses.is_dataclass(base) for base in args[1]):
|
|
976
|
+
aggregate_cls = dataclasses.dataclass(eq=False, repr=False)(aggregate_cls)
|
|
944
977
|
if annotations_mention_id:
|
|
945
978
|
_annotations_mention_id.add(aggregate_cls)
|
|
946
979
|
return aggregate_cls
|
|
@@ -957,7 +990,7 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
957
990
|
|
|
958
991
|
# Identify or define a base event class for this aggregate.
|
|
959
992
|
base_event_name = "Event"
|
|
960
|
-
base_event_cls:
|
|
993
|
+
base_event_cls: type[CanMutateAggregate]
|
|
961
994
|
try:
|
|
962
995
|
base_event_cls = cls.__dict__[base_event_name]
|
|
963
996
|
except KeyError:
|
|
@@ -967,7 +1000,7 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
967
1000
|
setattr(cls, base_event_name, base_event_cls)
|
|
968
1001
|
|
|
969
1002
|
# Make sure all events defined on aggregate subclass the base event class.
|
|
970
|
-
created_event_classes:
|
|
1003
|
+
created_event_classes: dict[str, type[CanInitAggregate]] = {}
|
|
971
1004
|
for name, value in tuple(cls.__dict__.items()):
|
|
972
1005
|
if name == base_event_name:
|
|
973
1006
|
# Don't subclass the base event class again.
|
|
@@ -987,7 +1020,7 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
987
1020
|
created_event_classes[name] = value
|
|
988
1021
|
|
|
989
1022
|
# Disallow using both '_created_event_class' and 'created_event_name'.
|
|
990
|
-
created_event_class:
|
|
1023
|
+
created_event_class: type[CanInitAggregate] | None = cls.__dict__.get(
|
|
991
1024
|
"_created_event_class"
|
|
992
1025
|
)
|
|
993
1026
|
if created_event_class and created_event_name:
|
|
@@ -1015,7 +1048,7 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
1015
1048
|
# Does the decorator specify a "created" event class?
|
|
1016
1049
|
if init_decorator.given_event_cls:
|
|
1017
1050
|
created_event_class = cast(
|
|
1018
|
-
|
|
1051
|
+
type[CanInitAggregate], init_decorator.given_event_cls
|
|
1019
1052
|
)
|
|
1020
1053
|
# Does the decorator specify a "created" event name?
|
|
1021
1054
|
elif init_decorator.event_cls_name:
|
|
@@ -1093,11 +1126,11 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
1093
1126
|
# Define a "created" event class for this aggregate.
|
|
1094
1127
|
if issubclass(base_created_event_cls, base_event_cls):
|
|
1095
1128
|
# Don't subclass from base event class twice.
|
|
1096
|
-
bases:
|
|
1129
|
+
bases: tuple[type[CanMutateAggregate], ...] = (base_created_event_cls,)
|
|
1097
1130
|
else:
|
|
1098
1131
|
bases = (base_created_event_cls, base_event_cls)
|
|
1099
1132
|
created_event_class = cast(
|
|
1100
|
-
|
|
1133
|
+
type[CanInitAggregate],
|
|
1101
1134
|
cls._define_event_class(
|
|
1102
1135
|
created_event_name,
|
|
1103
1136
|
bases,
|
|
@@ -1160,7 +1193,7 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
1160
1193
|
|
|
1161
1194
|
# Define event class as subclass of given class.
|
|
1162
1195
|
given_subclass = cast(
|
|
1163
|
-
|
|
1196
|
+
type[CanMutateAggregate],
|
|
1164
1197
|
getattr(cls, event_decorator.given_event_cls.__name__),
|
|
1165
1198
|
)
|
|
1166
1199
|
event_cls = cls._define_event_class(
|
|
@@ -1194,7 +1227,7 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
1194
1227
|
|
|
1195
1228
|
# Remember which event class to trigger.
|
|
1196
1229
|
decorated_event_classes[event_decorator] = cast(
|
|
1197
|
-
|
|
1230
|
+
type[MetaAggregate.DecoratedEvent], event_cls
|
|
1198
1231
|
)
|
|
1199
1232
|
|
|
1200
1233
|
# Check any create_id method defined on this class is static or class method.
|
|
@@ -1208,7 +1241,7 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
1208
1241
|
raise TypeError(msg)
|
|
1209
1242
|
|
|
1210
1243
|
# Get the parameters of the create_id method that will be used by this class.
|
|
1211
|
-
cls._create_id_param_names:
|
|
1244
|
+
cls._create_id_param_names: list[str] = []
|
|
1212
1245
|
for name, param in inspect.signature(cls.create_id).parameters.items():
|
|
1213
1246
|
if param.kind in [param.KEYWORD_ONLY, param.POSITIONAL_OR_KEYWORD]:
|
|
1214
1247
|
cls._create_id_param_names.append(name)
|
|
@@ -1230,9 +1263,9 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
1230
1263
|
def _define_event_class(
|
|
1231
1264
|
cls,
|
|
1232
1265
|
name: str,
|
|
1233
|
-
bases:
|
|
1266
|
+
bases: tuple[type[CanMutateAggregate], ...],
|
|
1234
1267
|
apply_method: CommandMethod | None,
|
|
1235
|
-
) ->
|
|
1268
|
+
) -> type[CanMutateAggregate]:
|
|
1236
1269
|
# Define annotations for the event class (specs the init method).
|
|
1237
1270
|
annotations = {}
|
|
1238
1271
|
if apply_method is not None:
|
|
@@ -1256,7 +1289,7 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
1256
1289
|
}
|
|
1257
1290
|
|
|
1258
1291
|
# Create the event class object.
|
|
1259
|
-
return cast(
|
|
1292
|
+
return cast(type[CanMutateAggregate], type(name, bases, event_cls_dict))
|
|
1260
1293
|
|
|
1261
1294
|
def __call__(
|
|
1262
1295
|
cls: MetaAggregate[TAggregate], *args: Any, **kwargs: Any
|
|
@@ -1286,7 +1319,7 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
1286
1319
|
|
|
1287
1320
|
def _create(
|
|
1288
1321
|
cls: MetaAggregate[TAggregate],
|
|
1289
|
-
event_class:
|
|
1322
|
+
event_class: type[CanInitAggregate],
|
|
1290
1323
|
**kwargs: Any,
|
|
1291
1324
|
) -> TAggregate:
|
|
1292
1325
|
raise NotImplementedError # pragma: no cover
|
|
@@ -1308,8 +1341,8 @@ class Aggregate(metaclass=MetaAggregate):
|
|
|
1308
1341
|
|
|
1309
1342
|
@classmethod
|
|
1310
1343
|
def _create(
|
|
1311
|
-
cls:
|
|
1312
|
-
event_class:
|
|
1344
|
+
cls: type[TAggregate],
|
|
1345
|
+
event_class: type[CanInitAggregate],
|
|
1313
1346
|
*,
|
|
1314
1347
|
id: UUID | None = None, # noqa: A002
|
|
1315
1348
|
**kwargs: Any,
|
|
@@ -1359,7 +1392,7 @@ class Aggregate(metaclass=MetaAggregate):
|
|
|
1359
1392
|
self._version = originator_version
|
|
1360
1393
|
self._created_on = timestamp
|
|
1361
1394
|
self._modified_on = timestamp
|
|
1362
|
-
self._pending_events:
|
|
1395
|
+
self._pending_events: list[CanMutateAggregate] = []
|
|
1363
1396
|
|
|
1364
1397
|
@property
|
|
1365
1398
|
def id(self) -> UUID:
|
|
@@ -1398,7 +1431,7 @@ class Aggregate(metaclass=MetaAggregate):
|
|
|
1398
1431
|
self._modified_on = modified_on
|
|
1399
1432
|
|
|
1400
1433
|
@property
|
|
1401
|
-
def pending_events(self) ->
|
|
1434
|
+
def pending_events(self) -> list[CanMutateAggregate]:
|
|
1402
1435
|
"""
|
|
1403
1436
|
A list of pending events.
|
|
1404
1437
|
"""
|
|
@@ -1423,7 +1456,7 @@ class Aggregate(metaclass=MetaAggregate):
|
|
|
1423
1456
|
|
|
1424
1457
|
def trigger_event(
|
|
1425
1458
|
self,
|
|
1426
|
-
event_class:
|
|
1459
|
+
event_class: type[CanMutateAggregate],
|
|
1427
1460
|
**kwargs: Any,
|
|
1428
1461
|
) -> None:
|
|
1429
1462
|
"""
|
|
@@ -1480,7 +1513,7 @@ def aggregate(
|
|
|
1480
1513
|
cls: Any | None = None,
|
|
1481
1514
|
*,
|
|
1482
1515
|
created_event_name: str = "",
|
|
1483
|
-
) ->
|
|
1516
|
+
) -> type[Aggregate] | Callable[[Any], type[Aggregate]]:
|
|
1484
1517
|
"""
|
|
1485
1518
|
Converts the class that was passed in to inherit from Aggregate.
|
|
1486
1519
|
|
|
@@ -1498,7 +1531,7 @@ def aggregate(
|
|
|
1498
1531
|
pass
|
|
1499
1532
|
"""
|
|
1500
1533
|
|
|
1501
|
-
def decorator(cls_: Any) ->
|
|
1534
|
+
def decorator(cls_: Any) -> type[Aggregate]:
|
|
1502
1535
|
if issubclass(cls_, Aggregate):
|
|
1503
1536
|
msg = f"{cls_.__qualname__} is already an Aggregate"
|
|
1504
1537
|
raise TypeError(msg)
|
|
@@ -1553,7 +1586,7 @@ class VersionError(OriginatorVersionError):
|
|
|
1553
1586
|
|
|
1554
1587
|
class SnapshotProtocol(DomainEventProtocol, Protocol):
|
|
1555
1588
|
@property
|
|
1556
|
-
def state(self) ->
|
|
1589
|
+
def state(self) -> dict[str, Any]:
|
|
1557
1590
|
"""
|
|
1558
1591
|
Snapshots have a read-only 'state'.
|
|
1559
1592
|
"""
|
|
@@ -1575,7 +1608,7 @@ class CanSnapshotAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
|
|
|
1575
1608
|
|
|
1576
1609
|
@classmethod
|
|
1577
1610
|
def take(
|
|
1578
|
-
cls:
|
|
1611
|
+
cls: type[TCanSnapshotAggregate],
|
|
1579
1612
|
aggregate: MutableOrImmutableAggregate,
|
|
1580
1613
|
) -> TCanSnapshotAggregate:
|
|
1581
1614
|
"""
|
|
@@ -1601,7 +1634,7 @@ class CanSnapshotAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
|
|
|
1601
1634
|
"""
|
|
1602
1635
|
Reconstructs the snapshotted :class:`Aggregate` object.
|
|
1603
1636
|
"""
|
|
1604
|
-
cls = cast(
|
|
1637
|
+
cls = cast(type[Aggregate], resolve_topic(self.topic))
|
|
1605
1638
|
aggregate_state = dict(self.state)
|
|
1606
1639
|
from_version = aggregate_state.pop("class_version", 1)
|
|
1607
1640
|
class_version = getattr(cls, "class_version", 1)
|
|
@@ -1634,4 +1667,4 @@ class Snapshot(CanSnapshotAggregate, DomainEvent):
|
|
|
1634
1667
|
"""
|
|
1635
1668
|
|
|
1636
1669
|
topic: str
|
|
1637
|
-
state:
|
|
1670
|
+
state: dict[str, Any]
|
|
@@ -3,12 +3,15 @@ from __future__ import annotations
|
|
|
3
3
|
import json
|
|
4
4
|
from abc import ABC, abstractmethod
|
|
5
5
|
from base64 import b64decode, b64encode
|
|
6
|
-
from typing import
|
|
6
|
+
from typing import TYPE_CHECKING, Generic
|
|
7
7
|
from uuid import UUID
|
|
8
8
|
|
|
9
9
|
from eventsourcing.application import NotificationLog, Section, TApplication
|
|
10
10
|
from eventsourcing.persistence import Notification
|
|
11
11
|
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from collections.abc import Sequence
|
|
14
|
+
|
|
12
15
|
|
|
13
16
|
class NotificationLogInterface(ABC):
|
|
14
17
|
"""
|
|
@@ -142,7 +145,7 @@ class NotificationLogJSONClient(NotificationLog):
|
|
|
142
145
|
topics: Sequence[str] = (),
|
|
143
146
|
*,
|
|
144
147
|
inclusive_of_start: bool = True,
|
|
145
|
-
) ->
|
|
148
|
+
) -> list[Notification]:
|
|
146
149
|
"""
|
|
147
150
|
Returns a selection of
|
|
148
151
|
:class:`~eventsourcing.persistence.Notification` objects
|