eventsourcing 9.4.3__py3-none-any.whl → 9.4.5__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.

@@ -22,14 +22,15 @@ from warnings import warn
22
22
 
23
23
  from eventsourcing.domain import (
24
24
  Aggregate,
25
+ BaseAggregate,
25
26
  CanMutateProtocol,
26
27
  CollectEventsProtocol,
27
28
  DomainEventProtocol,
28
29
  EventSourcingError,
29
30
  MutableOrImmutableAggregate,
30
31
  SDomainEvent,
31
- Snapshot,
32
32
  SnapshotProtocol,
33
+ TAggregateID,
33
34
  TDomainEvent,
34
35
  TMutableOrImmutableAggregate,
35
36
  datetime_now_with_tzinfo,
@@ -70,7 +71,7 @@ class ProgrammingError(Exception):
70
71
 
71
72
  def project_aggregate(
72
73
  aggregate: TMutableOrImmutableAggregate | None,
73
- domain_events: Iterable[DomainEventProtocol],
74
+ domain_events: Iterable[DomainEventProtocol[Any]],
74
75
  ) -> TMutableOrImmutableAggregate | None:
75
76
  """Projector function for aggregate projections, which works
76
77
  by successively calling aggregate mutator function mutate()
@@ -199,7 +200,7 @@ class LRUCache(Cache[S, T]):
199
200
  return evicted_key, evicted_value
200
201
 
201
202
 
202
- class Repository:
203
+ class Repository(Generic[TAggregateID]):
203
204
  """Reconstructs aggregates from events in an
204
205
  :class:`~eventsourcing.persistence.EventStore`,
205
206
  possibly using snapshot store to avoid replaying
@@ -210,9 +211,9 @@ class Repository:
210
211
 
211
212
  def __init__(
212
213
  self,
213
- event_store: EventStore,
214
+ event_store: EventStore[TAggregateID],
214
215
  *,
215
- snapshot_store: EventStore | None = None,
216
+ snapshot_store: EventStore[TAggregateID] | None = None,
216
217
  cache_maxsize: int | None = None,
217
218
  fastforward: bool = True,
218
219
  fastforward_skipping: bool = False,
@@ -225,11 +226,13 @@ class Repository:
225
226
  :class:`~eventsourcing.persistence.EventStore` for aggregate
226
227
  :class:`~eventsourcing.domain.Snapshot` objects).
227
228
  """
228
- self.event_store = event_store
229
- self.snapshot_store = snapshot_store
229
+ self.event_store: EventStore[TAggregateID] = event_store
230
+ self.snapshot_store: EventStore[TAggregateID] | None = snapshot_store
230
231
 
231
232
  if cache_maxsize is None:
232
- self.cache: Cache[UUID, MutableOrImmutableAggregate] | None = None
233
+ self.cache: (
234
+ Cache[TAggregateID, MutableOrImmutableAggregate[TAggregateID]] | None
235
+ ) = None
233
236
  elif cache_maxsize <= 0:
234
237
  self.cache = Cache()
235
238
  else:
@@ -240,14 +243,14 @@ class Repository:
240
243
 
241
244
  # Because fast-forwarding a cached aggregate isn't thread-safe.
242
245
  self._fastforward_locks_lock = Lock()
243
- self._fastforward_locks_cache: LRUCache[UUID, Lock] = LRUCache(
246
+ self._fastforward_locks_cache: LRUCache[TAggregateID, Lock] = LRUCache(
244
247
  maxsize=self.FASTFORWARD_LOCKS_CACHE_MAXSIZE
245
248
  )
246
- self._fastforward_locks_inuse: dict[UUID, tuple[Lock, int]] = {}
249
+ self._fastforward_locks_inuse: dict[TAggregateID, tuple[Lock, int]] = {}
247
250
 
248
251
  def get(
249
252
  self,
250
- aggregate_id: UUID,
253
+ aggregate_id: TAggregateID,
251
254
  *,
252
255
  version: int | None = None,
253
256
  projector_func: ProjectorFunction[
@@ -310,7 +313,7 @@ class Repository:
310
313
 
311
314
  def _reconstruct_aggregate(
312
315
  self,
313
- aggregate_id: UUID,
316
+ aggregate_id: TAggregateID,
314
317
  version: int | None,
315
318
  projector_func: ProjectorFunction[TMutableOrImmutableAggregate, TDomainEvent],
316
319
  ) -> TMutableOrImmutableAggregate:
@@ -350,11 +353,12 @@ class Repository:
350
353
 
351
354
  # Raise exception if "not found".
352
355
  if aggregate is None:
353
- raise AggregateNotFoundError((aggregate_id, version))
356
+ msg = f"Aggregate {aggregate_id!r} version {version!r} not found."
357
+ raise AggregateNotFoundError(msg)
354
358
  # Return the aggregate.
355
359
  return aggregate
356
360
 
357
- def _use_fastforward_lock(self, aggregate_id: UUID) -> Lock:
361
+ def _use_fastforward_lock(self, aggregate_id: TAggregateID) -> Lock:
358
362
  lock: Lock | None = None
359
363
  with self._fastforward_locks_lock:
360
364
  num_users = 0
@@ -369,7 +373,7 @@ class Repository:
369
373
  self._fastforward_locks_inuse[aggregate_id] = (lock, num_users)
370
374
  return lock
371
375
 
372
- def _disuse_fastforward_lock(self, aggregate_id: UUID) -> None:
376
+ def _disuse_fastforward_lock(self, aggregate_id: TAggregateID) -> None:
373
377
  with self._fastforward_locks_lock:
374
378
  lock_, num_users = self._fastforward_locks_inuse[aggregate_id]
375
379
  num_users -= 1
@@ -379,7 +383,7 @@ class Repository:
379
383
  else:
380
384
  self._fastforward_locks_inuse[aggregate_id] = (lock_, num_users)
381
385
 
382
- def __contains__(self, item: UUID) -> bool:
386
+ def __contains__(self, item: TAggregateID) -> bool:
383
387
  """Tests to see if an aggregate exists in the repository."""
384
388
  try:
385
389
  self.get(aggregate_id=item)
@@ -409,7 +413,7 @@ class Section:
409
413
  """
410
414
 
411
415
  id: str | None
412
- items: list[Notification]
416
+ items: Sequence[Notification]
413
417
  next_id: str | None
414
418
 
415
419
 
@@ -432,7 +436,7 @@ class NotificationLog(ABC):
432
436
  topics: Sequence[str] = (),
433
437
  *,
434
438
  inclusive_of_start: bool = True,
435
- ) -> list[Notification]:
439
+ ) -> Sequence[Notification]:
436
440
  """Returns a selection of
437
441
  :class:`~eventsourcing.persistence.Notification` objects
438
442
  from the notification log.
@@ -518,7 +522,7 @@ class LocalNotificationLog(NotificationLog):
518
522
  topics: Sequence[str] = (),
519
523
  *,
520
524
  inclusive_of_start: bool = True,
521
- ) -> list[Notification]:
525
+ ) -> Sequence[Notification]:
522
526
  """Returns a selection of
523
527
  :class:`~eventsourcing.persistence.Notification` objects
524
528
  from the notification log.
@@ -541,7 +545,7 @@ class LocalNotificationLog(NotificationLog):
541
545
  return f"{first_id},{last_id}"
542
546
 
543
547
 
544
- class ProcessingEvent:
548
+ class ProcessingEvent(Generic[TAggregateID]):
545
549
  """Keeps together a :class:`~eventsourcing.persistence.Tracking`
546
550
  object, which represents the position of a domain event notification
547
551
  in the notification log of a particular application, and the
@@ -551,13 +555,17 @@ class ProcessingEvent:
551
555
  def __init__(self, tracking: Tracking | None = None):
552
556
  """Initialises the process event with the given tracking object."""
553
557
  self.tracking = tracking
554
- self.events: list[DomainEventProtocol] = []
555
- self.aggregates: dict[UUID, MutableOrImmutableAggregate] = {}
558
+ self.events: list[DomainEventProtocol[TAggregateID]] = []
559
+ self.aggregates: dict[
560
+ TAggregateID, MutableOrImmutableAggregate[TAggregateID]
561
+ ] = {}
556
562
  self.saved_kwargs: dict[Any, Any] = {}
557
563
 
558
564
  def collect_events(
559
565
  self,
560
- *objs: MutableOrImmutableAggregate | DomainEventProtocol | None,
566
+ *objs: MutableOrImmutableAggregate[TAggregateID]
567
+ | DomainEventProtocol[TAggregateID]
568
+ | None,
561
569
  **kwargs: Any,
562
570
  ) -> None:
563
571
  """Collects pending domain events from the given aggregate."""
@@ -576,7 +584,9 @@ class ProcessingEvent:
576
584
 
577
585
  def save(
578
586
  self,
579
- *aggregates: MutableOrImmutableAggregate | DomainEventProtocol | None,
587
+ *aggregates: MutableOrImmutableAggregate[TAggregateID]
588
+ | DomainEventProtocol[TAggregateID]
589
+ | None,
580
590
  **kwargs: Any,
581
591
  ) -> None:
582
592
  warn(
@@ -588,17 +598,22 @@ class ProcessingEvent:
588
598
  self.collect_events(*aggregates, **kwargs)
589
599
 
590
600
 
591
- class Application:
601
+ class Application(Generic[TAggregateID]):
592
602
  """Base class for event-sourced applications."""
593
603
 
594
604
  name = "Application"
595
605
  env: ClassVar[dict[str, str]] = {}
596
606
  is_snapshotting_enabled: bool = False
597
- snapshotting_intervals: ClassVar[dict[type[MutableOrImmutableAggregate], int]] = {}
607
+ snapshotting_intervals: ClassVar[
608
+ dict[type[MutableOrImmutableAggregate[Any]], int]
609
+ ] = {}
598
610
  snapshotting_projectors: ClassVar[
599
- dict[type[MutableOrImmutableAggregate], ProjectorFunction[Any, Any]]
611
+ dict[
612
+ type[MutableOrImmutableAggregate[Any]],
613
+ ProjectorFunction[Any, Any],
614
+ ]
600
615
  ] = {}
601
- snapshot_class: type[SnapshotProtocol] = Snapshot
616
+ snapshot_class: type[SnapshotProtocol[TAggregateID]] | None = None
602
617
  log_section_size = 10
603
618
  notify_topics: Sequence[str] = []
604
619
 
@@ -622,18 +637,18 @@ class Application:
622
637
  """
623
638
  self.env = self.construct_env(self.name, env) # type: ignore[misc]
624
639
  self.factory = self.construct_factory(self.env)
625
- self.mapper = self.construct_mapper()
640
+ self.mapper: Mapper[TAggregateID] = self.construct_mapper()
626
641
  self.recorder = self.construct_recorder()
627
- self.events = self.construct_event_store()
628
- self.snapshots: EventStore | None = None
642
+ self.events: EventStore[TAggregateID] = self.construct_event_store()
643
+ self.snapshots: EventStore[TAggregateID] | None = None
629
644
  if self.factory.is_snapshotting_enabled():
630
645
  self.snapshots = self.construct_snapshot_store()
631
- self._repository = self.construct_repository()
646
+ self._repository: Repository[TAggregateID] = self.construct_repository()
632
647
  self._notification_log = self.construct_notification_log()
633
648
  self.closing = Event()
634
649
 
635
650
  @property
636
- def repository(self) -> Repository:
651
+ def repository(self) -> Repository[TAggregateID]:
637
652
  """An application's repository reconstructs aggregates from stored events."""
638
653
  return self._repository
639
654
 
@@ -670,7 +685,7 @@ class Application:
670
685
  """
671
686
  return InfrastructureFactory.construct(env)
672
687
 
673
- def construct_mapper(self) -> Mapper:
688
+ def construct_mapper(self) -> Mapper[TAggregateID]:
674
689
  """Constructs a :class:`~eventsourcing.persistence.Mapper`
675
690
  for use by the application.
676
691
  """
@@ -699,7 +714,7 @@ class Application:
699
714
  """
700
715
  return self.factory.application_recorder()
701
716
 
702
- def construct_event_store(self) -> EventStore:
717
+ def construct_event_store(self) -> EventStore[TAggregateID]:
703
718
  """Constructs an :class:`~eventsourcing.persistence.EventStore`
704
719
  for use by the application to store and retrieve aggregate
705
720
  :class:`~eventsourcing.domain.AggregateEvent` objects.
@@ -709,8 +724,8 @@ class Application:
709
724
  recorder=self.recorder,
710
725
  )
711
726
 
712
- def construct_snapshot_store(self) -> EventStore:
713
- """Constructs an :class:`~eventsourcing.persistence.EventStore`
727
+ def construct_snapshot_store(self) -> EventStore[TAggregateID]:
728
+ """Constructs an :py:class:`~eventsourcing.persistence.EventStore`
714
729
  for use by the application to store and retrieve aggregate
715
730
  :class:`~eventsourcing.domain.Snapshot` objects.
716
731
  """
@@ -720,8 +735,8 @@ class Application:
720
735
  recorder=recorder,
721
736
  )
722
737
 
723
- def construct_repository(self) -> Repository:
724
- """Constructs a :class:`Repository` for use by the application."""
738
+ def construct_repository(self) -> Repository[TAggregateID]:
739
+ """Constructs a :py:class:`Repository` for use by the application."""
725
740
  cache_maxsize_envvar = self.env.get(self.AGGREGATE_CACHE_MAXSIZE)
726
741
  cache_maxsize = int(cache_maxsize_envvar) if cache_maxsize_envvar else None
727
742
  return Repository(
@@ -743,13 +758,15 @@ class Application:
743
758
 
744
759
  def save(
745
760
  self,
746
- *objs: MutableOrImmutableAggregate | DomainEventProtocol | None,
761
+ *objs: MutableOrImmutableAggregate[TAggregateID]
762
+ | DomainEventProtocol[TAggregateID]
763
+ | None,
747
764
  **kwargs: Any,
748
- ) -> list[Recording]:
765
+ ) -> list[Recording[TAggregateID]]:
749
766
  """Collects pending events from given aggregates and
750
767
  puts them in the application's event store.
751
768
  """
752
- processing_event = ProcessingEvent()
769
+ processing_event: ProcessingEvent[TAggregateID] = ProcessingEvent()
753
770
  processing_event.collect_events(*objs, **kwargs)
754
771
  recordings = self._record(processing_event)
755
772
  self._take_snapshots(processing_event)
@@ -757,7 +774,9 @@ class Application:
757
774
  self.notify(processing_event.events) # Deprecated.
758
775
  return recordings
759
776
 
760
- def _record(self, processing_event: ProcessingEvent) -> list[Recording]:
777
+ def _record(
778
+ self, processing_event: ProcessingEvent[TAggregateID]
779
+ ) -> list[Recording[TAggregateID]]:
761
780
  """Records given process event in the application's recorder."""
762
781
  recordings = self.events.put(
763
782
  processing_event.events,
@@ -769,7 +788,7 @@ class Application:
769
788
  self.repository.cache.put(aggregate_id, aggregate)
770
789
  return recordings
771
790
 
772
- def _take_snapshots(self, processing_event: ProcessingEvent) -> None:
791
+ def _take_snapshots(self, processing_event: ProcessingEvent[TAggregateID]) -> None:
773
792
  # Take snapshots using IDs and types.
774
793
  if self.snapshots and self.snapshotting_intervals:
775
794
  for event in processing_event.events:
@@ -782,22 +801,21 @@ class Application:
782
801
  try:
783
802
  projector_func = self.snapshotting_projectors[type(aggregate)]
784
803
  except KeyError:
804
+ if not isinstance(event, CanMutateProtocol):
805
+ msg = (
806
+ f"Cannot take snapshot for {type(aggregate)} with "
807
+ "default project_aggregate() function, because its "
808
+ f"domain event {type(event)} does not implement "
809
+ "the 'can mutate' protocol (see CanMutateProtocol)."
810
+ f" Please define application class {type(self)}"
811
+ " with class variable 'snapshotting_projectors', "
812
+ f"to be a dict that has {type(aggregate)} as a key "
813
+ "with the aggregate projector function for "
814
+ f"{type(aggregate)} as the value for that key."
815
+ )
816
+ raise ProgrammingError(msg) from None
817
+
785
818
  projector_func = project_aggregate
786
- if projector_func is project_aggregate and not isinstance(
787
- event, CanMutateProtocol
788
- ):
789
- msg = (
790
- f"Cannot take snapshot for {type(aggregate)} with "
791
- "default project_aggregate() function, because its "
792
- f"domain event {type(event)} does not implement "
793
- "the 'can mutate' protocol (see CanMutateProtocol)."
794
- f" Please define application class {type(self)}"
795
- " with class variable 'snapshotting_projectors', "
796
- f"to be a dict that has {type(aggregate)} as a key "
797
- "with the aggregate projector function for "
798
- f"{type(aggregate)} as the value for that key."
799
- )
800
- raise ProgrammingError(msg)
801
819
  self.take_snapshot(
802
820
  aggregate_id=event.originator_id,
803
821
  version=event.originator_version,
@@ -806,11 +824,9 @@ class Application:
806
824
 
807
825
  def take_snapshot(
808
826
  self,
809
- aggregate_id: UUID,
827
+ aggregate_id: TAggregateID,
810
828
  version: int | None = None,
811
- projector_func: ProjectorFunction[
812
- TMutableOrImmutableAggregate, TDomainEvent
813
- ] = project_aggregate,
829
+ projector_func: ProjectorFunction[Any, Any] = project_aggregate,
814
830
  ) -> None:
815
831
  """Takes a snapshot of the recorded state of the aggregate,
816
832
  and puts the snapshot in the snapshot store.
@@ -824,14 +840,22 @@ class Application:
824
840
  "application class."
825
841
  )
826
842
  raise AssertionError(msg)
827
- aggregate = self.repository.get(
843
+ aggregate: BaseAggregate[UUID | str] = self.repository.get(
828
844
  aggregate_id, version=version, projector_func=projector_func
829
845
  )
830
846
  snapshot_class = getattr(type(aggregate), "Snapshot", type(self).snapshot_class)
847
+ if snapshot_class is None:
848
+ msg = (
849
+ "Neither application nor aggregate have a snapshot class. "
850
+ f"Please either define a nested 'Snapshot' class on {type(aggregate)} "
851
+ f"or set class attribute 'snapshot_class' on {type(self)}."
852
+ )
853
+ raise AssertionError(msg)
854
+
831
855
  snapshot = snapshot_class.take(aggregate)
832
856
  self.snapshots.put([snapshot])
833
857
 
834
- def notify(self, new_events: list[DomainEventProtocol]) -> None:
858
+ def notify(self, new_events: list[DomainEventProtocol[TAggregateID]]) -> None:
835
859
  """Deprecated.
836
860
 
837
861
  Called after new aggregate events have been saved. This
@@ -840,7 +864,7 @@ class Application:
840
864
  need to take action when new domain events have been saved.
841
865
  """
842
866
 
843
- def _notify(self, recordings: list[Recording]) -> None:
867
+ def _notify(self, recordings: list[Recording[TAggregateID]]) -> None:
844
868
  """Called after new aggregate events have been saved. This
845
869
  method on this class doesn't actually do anything,
846
870
  but this method may be implemented by subclasses that
@@ -852,7 +876,7 @@ class Application:
852
876
  self.factory.close()
853
877
 
854
878
 
855
- TApplication = TypeVar("TApplication", bound=Application)
879
+ TApplication = TypeVar("TApplication", bound=Application[Any])
856
880
 
857
881
 
858
882
  class AggregateNotFoundError(EventSourcingError):
@@ -877,7 +901,7 @@ class EventSourcedLog(Generic[TDomainEvent]):
877
901
 
878
902
  def __init__(
879
903
  self,
880
- events: EventStore,
904
+ events: EventStore[Any],
881
905
  originator_id: UUID,
882
906
  logged_cls: type[TDomainEvent], # TODO: Rename to 'event_class' in v10.
883
907
  ):
eventsourcing/dispatch.py CHANGED
@@ -63,7 +63,7 @@ class singledispatchmethod(_singledispatchmethod[_T]): # noqa: N801
63
63
 
64
64
  try:
65
65
  return self.dispatcher.register(cast("type[Any]", cls), func=method)
66
- except NameError:
66
+ except (NameError, TypeError): # NameError <= Py3.13, TypeError >= Py3.14
67
67
  self.deferred_registrations.append(
68
68
  (cls, method) # pyright: ignore [reportArgumentType]
69
69
  )