eventsourcing 9.4.6__py3-none-any.whl → 9.5.0a0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of eventsourcing might be problematic. Click here for more details.
- eventsourcing/application.py +15 -2
- eventsourcing/dcb/__init__.py +0 -0
- eventsourcing/dcb/api.py +65 -0
- eventsourcing/dcb/application.py +116 -0
- eventsourcing/dcb/domain.py +381 -0
- eventsourcing/dcb/persistence.py +146 -0
- eventsourcing/dcb/popo.py +95 -0
- eventsourcing/dcb/postgres_tt.py +643 -0
- eventsourcing/domain.py +89 -29
- eventsourcing/persistence.py +20 -25
- eventsourcing/popo.py +2 -2
- eventsourcing/postgres.py +355 -132
- eventsourcing/sqlite.py +25 -3
- eventsourcing/tests/application.py +5 -1
- eventsourcing/tests/persistence.py +53 -80
- eventsourcing/tests/postgres_utils.py +59 -1
- eventsourcing/utils.py +7 -3
- {eventsourcing-9.4.6.dist-info → eventsourcing-9.5.0a0.dist-info}/METADATA +2 -2
- eventsourcing-9.5.0a0.dist-info/RECORD +33 -0
- eventsourcing-9.4.6.dist-info/RECORD +0 -26
- {eventsourcing-9.4.6.dist-info → eventsourcing-9.5.0a0.dist-info}/AUTHORS +0 -0
- {eventsourcing-9.4.6.dist-info → eventsourcing-9.5.0a0.dist-info}/LICENSE +0 -0
- {eventsourcing-9.4.6.dist-info → eventsourcing-9.5.0a0.dist-info}/WHEEL +0 -0
eventsourcing/domain.py
CHANGED
|
@@ -32,6 +32,7 @@ from warnings import warn
|
|
|
32
32
|
|
|
33
33
|
from eventsourcing.utils import (
|
|
34
34
|
TopicError,
|
|
35
|
+
construct_topic,
|
|
35
36
|
get_method_name,
|
|
36
37
|
get_topic,
|
|
37
38
|
register_topic,
|
|
@@ -369,7 +370,7 @@ class CanInitAggregate(CanMutateAggregate[TAggregateID]):
|
|
|
369
370
|
|
|
370
371
|
# Pick out event attributes for the aggregate base class init method.
|
|
371
372
|
self_dict = self._as_dict()
|
|
372
|
-
base_kwargs =
|
|
373
|
+
base_kwargs = filter_kwargs_for_method_params(
|
|
373
374
|
self_dict, type(agg).__base_init__
|
|
374
375
|
)
|
|
375
376
|
|
|
@@ -378,7 +379,7 @@ class CanInitAggregate(CanMutateAggregate[TAggregateID]):
|
|
|
378
379
|
agg.__base_init__(**base_kwargs)
|
|
379
380
|
|
|
380
381
|
# Pick out event attributes for aggregate subclass class init method.
|
|
381
|
-
init_kwargs =
|
|
382
|
+
init_kwargs = filter_kwargs_for_method_params(self_dict, type(agg).__init__)
|
|
382
383
|
|
|
383
384
|
# Provide the aggregate id, if the __init__ method expects it.
|
|
384
385
|
if aggregate_class in _init_mentions_id:
|
|
@@ -459,7 +460,7 @@ class LogEvent(DomainEvent):
|
|
|
459
460
|
"""
|
|
460
461
|
|
|
461
462
|
|
|
462
|
-
def
|
|
463
|
+
def filter_kwargs_for_method_params(
|
|
463
464
|
kwargs: dict[str, Any], method: Callable[..., Any]
|
|
464
465
|
) -> dict[str, Any]:
|
|
465
466
|
names = _spec_filter_kwargs_for_method_params(method)
|
|
@@ -472,8 +473,14 @@ def _spec_filter_kwargs_for_method_params(method: Callable[..., Any]) -> set[str
|
|
|
472
473
|
return set(method_signature.parameters)
|
|
473
474
|
|
|
474
475
|
|
|
476
|
+
class AbstractDCBEvent:
|
|
477
|
+
pass
|
|
478
|
+
|
|
479
|
+
|
|
475
480
|
if TYPE_CHECKING:
|
|
476
|
-
EventSpecType = Union[
|
|
481
|
+
EventSpecType = Union[ # noqa: PYI055
|
|
482
|
+
str, type[CanMutateAggregate[Any]], type[AbstractDCBEvent]
|
|
483
|
+
]
|
|
477
484
|
|
|
478
485
|
CallableType = Callable[..., None]
|
|
479
486
|
DecoratableType = Union[CallableType, property]
|
|
@@ -488,7 +495,9 @@ class CommandMethodDecorator:
|
|
|
488
495
|
event_topic: str | None = None,
|
|
489
496
|
):
|
|
490
497
|
self.is_name_inferred_from_method = False
|
|
491
|
-
self.given_event_cls:
|
|
498
|
+
self.given_event_cls: (
|
|
499
|
+
type[CanMutateAggregate[Any] | AbstractDCBEvent] | None
|
|
500
|
+
) = None
|
|
492
501
|
self.event_cls_name: str | None = None
|
|
493
502
|
self.decorated_property: property | None = None
|
|
494
503
|
self.is_property_setter = False
|
|
@@ -505,9 +514,13 @@ class CommandMethodDecorator:
|
|
|
505
514
|
|
|
506
515
|
# Event class has been specified.
|
|
507
516
|
elif isinstance(event_spec, type) and issubclass(
|
|
508
|
-
event_spec, CanMutateAggregate
|
|
517
|
+
event_spec, (CanMutateAggregate, AbstractDCBEvent)
|
|
509
518
|
):
|
|
510
|
-
|
|
519
|
+
# Guard against associating more than one method body with any given class.
|
|
520
|
+
if (
|
|
521
|
+
issubclass(event_spec, CanMutateAggregate)
|
|
522
|
+
and event_spec in _given_event_classes
|
|
523
|
+
):
|
|
511
524
|
name = event_spec.__name__
|
|
512
525
|
msg = f"{name} event class used in more than one decorator"
|
|
513
526
|
raise TypeError(msg)
|
|
@@ -551,6 +564,10 @@ class CommandMethodDecorator:
|
|
|
551
564
|
# Remember the decorated obj as the decorated method.
|
|
552
565
|
self.decorated_func = decorated_obj
|
|
553
566
|
|
|
567
|
+
if self.decorated_func.__name__ == "_":
|
|
568
|
+
underscore_method_decorators.append(
|
|
569
|
+
(construct_topic(self.decorated_func), self)
|
|
570
|
+
)
|
|
554
571
|
# If necessary, derive an event class name from the method.
|
|
555
572
|
if not self.given_event_cls and not self.event_cls_name:
|
|
556
573
|
original_method_name = self.decorated_func.__name__
|
|
@@ -608,6 +625,10 @@ class CommandMethodDecorator:
|
|
|
608
625
|
self, instance: BaseAggregate[Any] | None, owner: type[BaseAggregate[Any]]
|
|
609
626
|
) -> BoundCommandMethodDecorator | UnboundCommandMethodDecorator | property | Any:
|
|
610
627
|
"""Descriptor protocol for getting decorated method or property."""
|
|
628
|
+
if self.decorated_func.__name__ == "_":
|
|
629
|
+
msg = "Underscore 'non-command' methods cannot be used to trigger events."
|
|
630
|
+
raise ProgrammingError(msg)
|
|
631
|
+
|
|
611
632
|
# If we are decorating a property, then delegate to the property's __get__.
|
|
612
633
|
if self.decorated_property:
|
|
613
634
|
return self.decorated_property.__get__(instance, owner)
|
|
@@ -620,6 +641,12 @@ class CommandMethodDecorator:
|
|
|
620
641
|
if instance:
|
|
621
642
|
return BoundCommandMethodDecorator(self, instance)
|
|
622
643
|
|
|
644
|
+
if "SPHINX_BUILD" in os.environ: # pragma: no cover
|
|
645
|
+
# Sphinx hack: use the original function when sphinx is running so that the
|
|
646
|
+
# documentation ends up with the correct function signatures.
|
|
647
|
+
# See 'SPHINX_BUILD' in conf.py.
|
|
648
|
+
return self.decorated_func
|
|
649
|
+
|
|
623
650
|
# Return an "unbound" command method decorator if we have no instance.
|
|
624
651
|
return UnboundCommandMethodDecorator(self)
|
|
625
652
|
|
|
@@ -639,7 +666,7 @@ def event(arg: TDecoratableType, /) -> TDecoratableType:
|
|
|
639
666
|
|
|
640
667
|
@overload
|
|
641
668
|
def event(
|
|
642
|
-
arg: type[CanMutateAggregate[Any]], /
|
|
669
|
+
arg: type[CanMutateAggregate[Any] | AbstractDCBEvent], /
|
|
643
670
|
) -> Callable[[TDecoratableType], TDecoratableType]:
|
|
644
671
|
"""Signature for calling ``@event`` decorator with event class."""
|
|
645
672
|
|
|
@@ -720,7 +747,10 @@ def event(
|
|
|
720
747
|
if (
|
|
721
748
|
arg is None
|
|
722
749
|
or isinstance(arg, str)
|
|
723
|
-
or (
|
|
750
|
+
or (
|
|
751
|
+
isinstance(arg, type)
|
|
752
|
+
and issubclass(arg, (CanMutateAggregate, AbstractDCBEvent))
|
|
753
|
+
)
|
|
724
754
|
):
|
|
725
755
|
event_spec = arg
|
|
726
756
|
|
|
@@ -773,14 +803,22 @@ class UnboundCommandMethodDecorator:
|
|
|
773
803
|
)
|
|
774
804
|
|
|
775
805
|
|
|
806
|
+
class CanTriggerEvent(Protocol):
|
|
807
|
+
def trigger_event(
|
|
808
|
+
self,
|
|
809
|
+
event_class: type[Any],
|
|
810
|
+
**kwargs: Any,
|
|
811
|
+
) -> None:
|
|
812
|
+
pass # pragma: no cover
|
|
813
|
+
|
|
814
|
+
|
|
776
815
|
class BoundCommandMethodDecorator:
|
|
777
|
-
"""Binds a CommandMethodDecorator with an
|
|
778
|
-
decorated command methods can be intercepted and
|
|
816
|
+
"""Binds a CommandMethodDecorator with an object instance that can trigger
|
|
817
|
+
events, so that calls to decorated command methods can be intercepted and
|
|
818
|
+
will trigger a "decorated func caller" event.
|
|
779
819
|
"""
|
|
780
820
|
|
|
781
|
-
def __init__(
|
|
782
|
-
self, event_decorator: CommandMethodDecorator, aggregate: BaseAggregate[Any]
|
|
783
|
-
):
|
|
821
|
+
def __init__(self, event_decorator: CommandMethodDecorator, obj: CanTriggerEvent):
|
|
784
822
|
""":param CommandMethodDecorator event_decorator:
|
|
785
823
|
:param Aggregate aggregate:
|
|
786
824
|
"""
|
|
@@ -790,29 +828,41 @@ class BoundCommandMethodDecorator:
|
|
|
790
828
|
self.__qualname__ = event_decorator.decorated_func.__qualname__
|
|
791
829
|
self.__annotations__ = event_decorator.decorated_func.__annotations__
|
|
792
830
|
self.__doc__ = event_decorator.decorated_func.__doc__
|
|
793
|
-
self.
|
|
831
|
+
self.obj = obj
|
|
794
832
|
|
|
795
833
|
def trigger(self, *args: Any, **kwargs: Any) -> None:
|
|
796
834
|
kwargs = _coerce_args_to_kwargs(
|
|
797
835
|
self.event_decorator.decorated_func, args, kwargs
|
|
798
836
|
)
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
837
|
+
try:
|
|
838
|
+
event_cls = decorated_func_callers[self.event_decorator]
|
|
839
|
+
except KeyError as e: # pragma: no cover
|
|
840
|
+
msg = (
|
|
841
|
+
f"Event class not registered for event decorator on "
|
|
842
|
+
f"{self.event_decorator.decorated_func.__qualname__}"
|
|
843
|
+
)
|
|
844
|
+
raise KeyError(msg) from e
|
|
845
|
+
kwargs = filter_kwargs_for_method_params(kwargs, event_cls)
|
|
846
|
+
assert issubclass(event_cls, AbstractDecoratedFuncCaller), event_cls
|
|
847
|
+
self.obj.trigger_event(event_cls, **kwargs)
|
|
802
848
|
|
|
803
849
|
def __call__(self, *args: Any, **kwargs: Any) -> None:
|
|
804
850
|
self.trigger(*args, **kwargs)
|
|
805
851
|
|
|
806
852
|
|
|
807
|
-
class
|
|
853
|
+
class AbstractDecoratedFuncCaller:
|
|
854
|
+
pass
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
class DecoratedFuncCaller(CanMutateAggregate[Any], AbstractDecoratedFuncCaller):
|
|
808
858
|
def apply(self, aggregate: BaseAggregate[Any]) -> None:
|
|
809
859
|
"""Applies event to aggregate by calling method decorated by @event."""
|
|
810
860
|
# Identify the function that was decorated.
|
|
811
|
-
decorated_func =
|
|
861
|
+
decorated_func = decorated_funcs[type(self)]
|
|
812
862
|
|
|
813
863
|
# Select event attributes mentioned in function signature.
|
|
814
864
|
self_dict = self._as_dict()
|
|
815
|
-
kwargs =
|
|
865
|
+
kwargs = filter_kwargs_for_method_params(self_dict, decorated_func)
|
|
816
866
|
|
|
817
867
|
# Call the original method with event attribute values.
|
|
818
868
|
decorated_method = decorated_func.__get__(aggregate, type(aggregate))
|
|
@@ -822,12 +872,22 @@ class DecoratorEvent(CanMutateAggregate[Any]):
|
|
|
822
872
|
super().apply(aggregate)
|
|
823
873
|
|
|
824
874
|
|
|
825
|
-
|
|
826
|
-
|
|
875
|
+
# This helps enforce single usage of original event classes in decorators.
|
|
876
|
+
_given_event_classes = set[type]()
|
|
877
|
+
|
|
878
|
+
# This keeps track of the "created" event classes for an aggregate.
|
|
827
879
|
_created_event_classes: dict[type, list[type[CanInitAggregate[Any]]]] = {}
|
|
828
880
|
|
|
881
|
+
# This remembers which event class to trigger when a decorated method is called.
|
|
882
|
+
decorated_func_callers: dict[
|
|
883
|
+
CommandMethodDecorator, type[AbstractDecoratedFuncCaller]
|
|
884
|
+
] = {}
|
|
885
|
+
|
|
886
|
+
# This remembers which decorated func a decorated func caller should call.
|
|
887
|
+
decorated_funcs: dict[type, CallableType] = {}
|
|
829
888
|
|
|
830
|
-
|
|
889
|
+
# This keeps track of decorated "non-command" projection-only methods called "_".
|
|
890
|
+
underscore_method_decorators: list[tuple[str, CommandMethodDecorator]] = []
|
|
831
891
|
|
|
832
892
|
|
|
833
893
|
def _raise_type_error_if_func_has_variable_params(method: CallableType) -> None:
|
|
@@ -1613,7 +1673,7 @@ class BaseAggregate(Generic[TAggregateID], metaclass=MetaAggregate):
|
|
|
1613
1673
|
# the subclassing of events above? Maybe do this first?
|
|
1614
1674
|
event_cls = cls._define_event_class(
|
|
1615
1675
|
event_decorator.given_event_cls.__name__,
|
|
1616
|
-
(
|
|
1676
|
+
(DecoratedFuncCaller, given_subclass),
|
|
1617
1677
|
None,
|
|
1618
1678
|
)
|
|
1619
1679
|
|
|
@@ -1634,20 +1694,20 @@ class BaseAggregate(Generic[TAggregateID], metaclass=MetaAggregate):
|
|
|
1634
1694
|
# Define event class from signature of original method.
|
|
1635
1695
|
event_cls = cls._define_event_class(
|
|
1636
1696
|
event_decorator.event_cls_name,
|
|
1637
|
-
(
|
|
1697
|
+
(DecoratedFuncCaller, base_event_cls),
|
|
1638
1698
|
event_decorator.decorated_func,
|
|
1639
1699
|
event_topic=event_decorator.event_topic,
|
|
1640
1700
|
)
|
|
1641
1701
|
|
|
1642
1702
|
# Cache the decorated method for the event class to use.
|
|
1643
|
-
|
|
1703
|
+
decorated_funcs[event_cls] = event_decorator.decorated_func
|
|
1644
1704
|
|
|
1645
1705
|
# Set the event class as an attribute of the aggregate class.
|
|
1646
1706
|
setattr(cls, event_cls.__name__, event_cls)
|
|
1647
1707
|
|
|
1648
1708
|
# Remember which event class to trigger.
|
|
1649
|
-
|
|
1650
|
-
|
|
1709
|
+
decorated_func_callers[event_decorator] = cast(
|
|
1710
|
+
type[DecoratedFuncCaller], event_cls
|
|
1651
1711
|
)
|
|
1652
1712
|
|
|
1653
1713
|
# Check any create_id() method defined on this class is static or class method.
|
eventsourcing/persistence.py
CHANGED
|
@@ -13,10 +13,10 @@ from queue import Queue
|
|
|
13
13
|
from threading import Condition, Event, Lock, Semaphore, Thread, Timer
|
|
14
14
|
from time import monotonic, sleep, time
|
|
15
15
|
from types import GenericAlias, ModuleType
|
|
16
|
-
from typing import
|
|
16
|
+
from typing import Any, Callable, Generic, Union, cast
|
|
17
17
|
from uuid import UUID
|
|
18
18
|
|
|
19
|
-
from typing_extensions import TypeVar
|
|
19
|
+
from typing_extensions import Self, TypeVar
|
|
20
20
|
|
|
21
21
|
from eventsourcing.domain import (
|
|
22
22
|
DomainEventProtocol,
|
|
@@ -33,9 +33,6 @@ from eventsourcing.utils import (
|
|
|
33
33
|
strtobool,
|
|
34
34
|
)
|
|
35
35
|
|
|
36
|
-
if TYPE_CHECKING:
|
|
37
|
-
from typing_extensions import Self
|
|
38
|
-
|
|
39
36
|
|
|
40
37
|
class Transcoding(ABC):
|
|
41
38
|
"""Abstract base class for custom transcodings."""
|
|
@@ -679,14 +676,14 @@ class InfrastructureFactory(ABC, Generic[TTrackingRecorder]):
|
|
|
679
676
|
|
|
680
677
|
@classmethod
|
|
681
678
|
def construct(
|
|
682
|
-
cls: type[
|
|
679
|
+
cls: type[Self],
|
|
683
680
|
env: Environment | None = None,
|
|
684
|
-
) ->
|
|
681
|
+
) -> Self:
|
|
685
682
|
"""Constructs concrete infrastructure factory for given
|
|
686
683
|
named application. Reads and resolves persistence
|
|
687
684
|
topic from environment variable 'PERSISTENCE_MODULE'.
|
|
688
685
|
"""
|
|
689
|
-
factory_cls: type[
|
|
686
|
+
factory_cls: type[Self]
|
|
690
687
|
if env is None:
|
|
691
688
|
env = Environment()
|
|
692
689
|
topic = (
|
|
@@ -705,9 +702,7 @@ class InfrastructureFactory(ABC, Generic[TTrackingRecorder]):
|
|
|
705
702
|
or "eventsourcing.popo"
|
|
706
703
|
)
|
|
707
704
|
try:
|
|
708
|
-
obj: type[
|
|
709
|
-
resolve_topic(topic)
|
|
710
|
-
)
|
|
705
|
+
obj: type[Self] | ModuleType = resolve_topic(topic)
|
|
711
706
|
except TopicError as e:
|
|
712
707
|
msg = (
|
|
713
708
|
"Failed to resolve persistence module topic: "
|
|
@@ -718,29 +713,29 @@ class InfrastructureFactory(ABC, Generic[TTrackingRecorder]):
|
|
|
718
713
|
|
|
719
714
|
if isinstance(obj, ModuleType):
|
|
720
715
|
# Find the factory in the module.
|
|
721
|
-
factory_classes
|
|
716
|
+
factory_classes = set[type[Self]]()
|
|
722
717
|
for member in obj.__dict__.values():
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
718
|
+
# Look for classes...
|
|
719
|
+
if not isinstance(member, type):
|
|
720
|
+
continue
|
|
721
|
+
# Issue with Python 3.9 and 3.10.
|
|
722
|
+
if isinstance(member, GenericAlias):
|
|
723
|
+
continue # pragma: no cover (for Python > 3.10 only)
|
|
724
|
+
if not issubclass(member, cls):
|
|
725
|
+
continue
|
|
726
|
+
if getattr(member, "__parameters__", None):
|
|
727
|
+
continue
|
|
728
|
+
factory_classes.add(member)
|
|
734
729
|
|
|
735
730
|
if len(factory_classes) == 1:
|
|
736
|
-
factory_cls = factory_classes
|
|
731
|
+
factory_cls = next(iter(factory_classes))
|
|
737
732
|
else:
|
|
738
733
|
msg = (
|
|
739
734
|
f"Found {len(factory_classes)} infrastructure factory classes in"
|
|
740
735
|
f" '{topic}', expected 1."
|
|
741
736
|
)
|
|
742
737
|
raise InfrastructureFactoryError(msg)
|
|
743
|
-
elif isinstance(obj, type) and issubclass(obj,
|
|
738
|
+
elif isinstance(obj, type) and issubclass(obj, cls):
|
|
744
739
|
factory_cls = obj
|
|
745
740
|
else:
|
|
746
741
|
msg = (
|
eventsourcing/popo.py
CHANGED
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import contextlib
|
|
4
4
|
from collections import defaultdict
|
|
5
|
-
from threading import Event,
|
|
5
|
+
from threading import Event, RLock
|
|
6
6
|
from typing import TYPE_CHECKING, Any
|
|
7
7
|
|
|
8
8
|
from eventsourcing.persistence import (
|
|
@@ -27,7 +27,7 @@ if TYPE_CHECKING:
|
|
|
27
27
|
|
|
28
28
|
class POPORecorder:
|
|
29
29
|
def __init__(self) -> None:
|
|
30
|
-
self._database_lock =
|
|
30
|
+
self._database_lock = RLock()
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
class POPOAggregateRecorder(POPORecorder, AggregateRecorder):
|