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/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 = _filter_kwargs_for_method_params(
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 = _filter_kwargs_for_method_params(self_dict, type(agg).__init__)
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 _filter_kwargs_for_method_params(
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[str, type[CanMutateAggregate[Any]]]
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: type[CanMutateAggregate[Any]] | None = None
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
- if event_spec in _given_event_classes:
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 (isinstance(arg, type) and issubclass(arg, CanMutateAggregate))
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 aggregate instance so calls to
778
- decorated command methods can be intercepted and will trigger an event.
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.aggregate = aggregate
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
- event_cls = decorator_event_classes[self.event_decorator]
800
- kwargs = _filter_kwargs_for_method_params(kwargs, event_cls)
801
- self.aggregate.trigger_event(event_cls, **kwargs)
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 DecoratorEvent(CanMutateAggregate[Any]):
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 = _decorated_funcs[type(self)]
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 = _filter_kwargs_for_method_params(self_dict, decorated_func)
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
- _given_event_classes: set[type] = set()
826
- _decorated_funcs: dict[type, CallableType] = {}
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
- decorator_event_classes: dict[CommandMethodDecorator, type[DecoratorEvent]] = {}
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
- (DecoratorEvent, given_subclass),
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
- (DecoratorEvent, base_event_cls),
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
- _decorated_funcs[event_cls] = event_decorator.decorated_func
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
- decorator_event_classes[event_decorator] = cast(
1650
- "type[DecoratorEvent]", event_cls
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.
@@ -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 TYPE_CHECKING, Any, Callable, Generic, Union, cast
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[InfrastructureFactory[TTrackingRecorder]],
679
+ cls: type[Self],
683
680
  env: Environment | None = None,
684
- ) -> InfrastructureFactory[TTrackingRecorder]:
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[InfrastructureFactory[TTrackingRecorder]]
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[InfrastructureFactory[TTrackingRecorder]] | ModuleType = (
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: list[type[InfrastructureFactory[TTrackingRecorder]]] = []
716
+ factory_classes = set[type[Self]]()
722
717
  for member in obj.__dict__.values():
723
- if (
724
- member is not InfrastructureFactory
725
- and isinstance(member, type) # Look for classes...
726
- and isinstance(member, type) # Look for classes...
727
- and not isinstance(
728
- member, GenericAlias
729
- ) # Issue with Python 3.9 and 3.10.
730
- and issubclass(member, InfrastructureFactory) # Ignore base class.
731
- and member not in factory_classes # Forgive aliases.
732
- ):
733
- factory_classes.append(member)
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[0]
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, InfrastructureFactory):
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, Lock
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 = Lock()
30
+ self._database_lock = RLock()
31
31
 
32
32
 
33
33
  class POPOAggregateRecorder(POPORecorder, AggregateRecorder):