eventsourcing 9.4.0a8__tar.gz → 9.4.0b1__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.

Files changed (27) hide show
  1. {eventsourcing-9.4.0a8 → eventsourcing-9.4.0b1}/PKG-INFO +2 -2
  2. {eventsourcing-9.4.0a8 → eventsourcing-9.4.0b1}/eventsourcing/application.py +19 -23
  3. {eventsourcing-9.4.0a8 → eventsourcing-9.4.0b1}/eventsourcing/cipher.py +3 -1
  4. eventsourcing-9.4.0b1/eventsourcing/dispatch.py +79 -0
  5. {eventsourcing-9.4.0a8 → eventsourcing-9.4.0b1}/eventsourcing/domain.py +45 -21
  6. {eventsourcing-9.4.0a8 → eventsourcing-9.4.0b1}/eventsourcing/interface.py +1 -1
  7. {eventsourcing-9.4.0a8 → eventsourcing-9.4.0b1}/eventsourcing/persistence.py +8 -4
  8. {eventsourcing-9.4.0a8 → eventsourcing-9.4.0b1}/eventsourcing/postgres.py +136 -97
  9. {eventsourcing-9.4.0a8 → eventsourcing-9.4.0b1}/eventsourcing/projection.py +62 -9
  10. {eventsourcing-9.4.0a8 → eventsourcing-9.4.0b1}/eventsourcing/system.py +4 -7
  11. {eventsourcing-9.4.0a8 → eventsourcing-9.4.0b1}/eventsourcing/tests/domain.py +6 -6
  12. {eventsourcing-9.4.0a8 → eventsourcing-9.4.0b1}/eventsourcing/tests/persistence.py +8 -0
  13. {eventsourcing-9.4.0a8 → eventsourcing-9.4.0b1}/eventsourcing/tests/postgres_utils.py +5 -1
  14. {eventsourcing-9.4.0a8 → eventsourcing-9.4.0b1}/eventsourcing/utils.py +12 -7
  15. {eventsourcing-9.4.0a8 → eventsourcing-9.4.0b1}/pyproject.toml +13 -3
  16. eventsourcing-9.4.0a8/eventsourcing/dispatch.py +0 -38
  17. {eventsourcing-9.4.0a8 → eventsourcing-9.4.0b1}/AUTHORS +0 -0
  18. {eventsourcing-9.4.0a8 → eventsourcing-9.4.0b1}/LICENSE +0 -0
  19. {eventsourcing-9.4.0a8 → eventsourcing-9.4.0b1}/README.md +0 -0
  20. {eventsourcing-9.4.0a8 → eventsourcing-9.4.0b1}/eventsourcing/__init__.py +0 -0
  21. {eventsourcing-9.4.0a8 → eventsourcing-9.4.0b1}/eventsourcing/compressor.py +0 -0
  22. {eventsourcing-9.4.0a8 → eventsourcing-9.4.0b1}/eventsourcing/cryptography.py +0 -0
  23. {eventsourcing-9.4.0a8 → eventsourcing-9.4.0b1}/eventsourcing/popo.py +0 -0
  24. {eventsourcing-9.4.0a8 → eventsourcing-9.4.0b1}/eventsourcing/py.typed +0 -0
  25. {eventsourcing-9.4.0a8 → eventsourcing-9.4.0b1}/eventsourcing/sqlite.py +0 -0
  26. {eventsourcing-9.4.0a8 → eventsourcing-9.4.0b1}/eventsourcing/tests/__init__.py +0 -0
  27. {eventsourcing-9.4.0a8 → eventsourcing-9.4.0b1}/eventsourcing/tests/application.py +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: eventsourcing
3
- Version: 9.4.0a8
3
+ Version: 9.4.0b1
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
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
- Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Development Status :: 4 - Beta
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: Intended Audience :: Education
13
13
  Classifier: Intended Audience :: Science/Research
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import contextlib
3
4
  import os
4
5
  from abc import ABC, abstractmethod
5
6
  from collections.abc import Iterable, Iterator, Sequence
@@ -26,6 +27,7 @@ from eventsourcing.domain import (
26
27
  DomainEventProtocol,
27
28
  EventSourcingError,
28
29
  MutableOrImmutableAggregate,
30
+ SDomainEvent,
29
31
  Snapshot,
30
32
  SnapshotProtocol,
31
33
  TDomainEvent,
@@ -352,19 +354,18 @@ class Repository:
352
354
  return aggregate
353
355
 
354
356
  def _use_fastforward_lock(self, aggregate_id: UUID) -> Lock:
357
+ lock: Lock | None = None
355
358
  with self._fastforward_locks_lock:
356
- try:
359
+ num_users = 0
360
+ with contextlib.suppress(KeyError):
357
361
  lock, num_users = self._fastforward_locks_inuse[aggregate_id]
358
- except KeyError:
359
- try:
362
+ if lock is None:
363
+ with contextlib.suppress(KeyError):
360
364
  lock = self._fastforward_locks_cache.get(aggregate_id, evict=True)
361
- except KeyError:
362
- lock = Lock()
363
- finally:
364
- num_users = 0
365
- finally:
366
- num_users += 1
367
- self._fastforward_locks_inuse[aggregate_id] = (lock, num_users)
365
+ if lock is None:
366
+ lock = Lock()
367
+ num_users += 1
368
+ self._fastforward_locks_inuse[aggregate_id] = (lock, num_users)
368
369
  return lock
369
370
 
370
371
  def _disuse_fastforward_lock(self, aggregate_id: UUID) -> None:
@@ -610,12 +611,10 @@ class Application:
610
611
  name = "Application"
611
612
  env: ClassVar[dict[str, str]] = {}
612
613
  is_snapshotting_enabled: bool = False
613
- snapshotting_intervals: ClassVar[
614
- dict[type[MutableOrImmutableAggregate], int] | None
615
- ] = None
614
+ snapshotting_intervals: ClassVar[dict[type[MutableOrImmutableAggregate], int]] = {}
616
615
  snapshotting_projectors: ClassVar[
617
- dict[type[MutableOrImmutableAggregate], ProjectorFunction[Any, Any]] | None
618
- ] = None
616
+ dict[type[MutableOrImmutableAggregate], ProjectorFunction[Any, Any]]
617
+ ] = {}
619
618
  snapshot_class: type[SnapshotProtocol] = Snapshot
620
619
  log_section_size = 10
621
620
  notify_topics: Sequence[str] = []
@@ -817,12 +816,9 @@ class Application:
817
816
  continue
818
817
  interval = self.snapshotting_intervals.get(type(aggregate))
819
818
  if interval is not None and event.originator_version % interval == 0:
820
- if (
821
- self.snapshotting_projectors
822
- and type(aggregate) in self.snapshotting_projectors
823
- ):
819
+ try:
824
820
  projector_func = self.snapshotting_projectors[type(aggregate)]
825
- else:
821
+ except KeyError:
826
822
  projector_func = project_aggregate
827
823
  if projector_func is project_aggregate and not isinstance(
828
824
  event, CanMutateProtocol
@@ -947,10 +943,10 @@ class EventSourcedLog(Generic[TDomainEvent]):
947
943
 
948
944
  def _trigger_event(
949
945
  self,
950
- logged_cls: type[T] | None,
946
+ logged_cls: type[SDomainEvent],
951
947
  next_originator_version: int | None = None,
952
948
  **kwargs: Any,
953
- ) -> T:
949
+ ) -> SDomainEvent:
954
950
  """
955
951
  Constructs and returns a new log event.
956
952
  """
@@ -961,7 +957,7 @@ class EventSourcedLog(Generic[TDomainEvent]):
961
957
  else:
962
958
  next_originator_version = last_logged.originator_version + 1
963
959
 
964
- return logged_cls( # type: ignore
960
+ return logged_cls(
965
961
  originator_id=self.originator_id,
966
962
  originator_version=next_originator_version,
967
963
  timestamp=datetime_now_with_tzinfo(),
@@ -5,7 +5,9 @@ from base64 import b64decode, b64encode
5
5
  from typing import TYPE_CHECKING
6
6
 
7
7
  from Crypto.Cipher import AES
8
- from Crypto.Cipher._mode_gcm import GcmMode
8
+ from Crypto.Cipher._mode_gcm import (
9
+ GcmMode, # pyright: ignore [reportPrivateImportUsage]
10
+ )
9
11
  from Crypto.Cipher.AES import key_size
10
12
 
11
13
  from eventsourcing.persistence import Cipher
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ import functools
4
+ from typing import TYPE_CHECKING, Any, Callable, Generic, TypeVar, cast, overload
5
+
6
+ _T = TypeVar("_T")
7
+ _S = TypeVar("_S")
8
+
9
+ if TYPE_CHECKING:
10
+
11
+ class _singledispatchmethod(functools.singledispatchmethod[_T]): # noqa: N801
12
+ pass
13
+
14
+ else:
15
+
16
+ class _singledispatchmethod( # noqa: N801
17
+ functools.singledispatchmethod, Generic[_T]
18
+ ):
19
+ pass
20
+
21
+
22
+ class singledispatchmethod(_singledispatchmethod[_T]): # noqa: N801
23
+ def __init__(self, func: Callable[..., _T]) -> None:
24
+ super().__init__(func)
25
+ self.deferred_registrations: list[
26
+ tuple[type[Any] | Callable[..., _T], Callable[..., _T] | None]
27
+ ] = []
28
+
29
+ @overload
30
+ def register(
31
+ self, cls: type[Any], method: None = None
32
+ ) -> Callable[[Callable[..., _T]], Callable[..., _T]]: ... # pragma: no cover
33
+ @overload
34
+ def register(
35
+ self, cls: Callable[..., _T], method: None = None
36
+ ) -> Callable[..., _T]: ... # pragma: no cover
37
+
38
+ @overload
39
+ def register(
40
+ self, cls: type[Any], method: Callable[..., _T]
41
+ ) -> Callable[..., _T]: ... # pragma: no cover
42
+
43
+ def register(
44
+ self,
45
+ cls: type[Any] | Callable[..., _T],
46
+ method: Callable[..., _T] | None = None,
47
+ ) -> Callable[[Callable[..., _T]], Callable[..., _T]] | Callable[..., _T]:
48
+ """generic_method.register(cls, func) -> func
49
+
50
+ Registers a new implementation for the given *cls* on a *generic_method*.
51
+ """
52
+ if isinstance(cls, (classmethod, staticmethod)):
53
+ first_annotation = {}
54
+ for k, v in cls.__func__.__annotations__.items():
55
+ first_annotation[k] = v
56
+ break
57
+ cls.__annotations__ = first_annotation
58
+
59
+ # for globals in typing.get_type_hints() in Python 3.8 and 3.9
60
+ if not hasattr(cls, "__wrapped__"):
61
+ cls.__dict__["__wrapped__"] = cls.__func__
62
+ # cls.__wrapped__ = cls.__func__
63
+
64
+ try:
65
+ return self.dispatcher.register(cast(type[Any], cls), func=method)
66
+ except NameError:
67
+ self.deferred_registrations.append(
68
+ (cls, method) # pyright: ignore [reportArgumentType]
69
+ )
70
+ # TODO: Fix this....
71
+ return method or cls # pyright: ignore [reportReturnType]
72
+
73
+ def __get__(self, obj: _S, cls: type[_S] | None = None) -> Callable[..., _T]:
74
+ for registered_cls, registered_method in self.deferred_registrations:
75
+ self.dispatcher.register(
76
+ cast(type[Any], registered_cls), func=registered_method
77
+ )
78
+ self.deferred_registrations = []
79
+ return super().__get__(obj, cls=cls)
@@ -88,20 +88,26 @@ class DomainEventProtocol(Protocol):
88
88
  kinds of domain event classes, such as Pydantic classes.
89
89
  """
90
90
 
91
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
92
+ pass # pragma: no cover
93
+
91
94
  @property
92
95
  def originator_id(self) -> UUID:
93
96
  """
94
97
  UUID identifying an aggregate to which the event belongs.
95
98
  """
99
+ raise NotImplementedError # pragma: no cover
96
100
 
97
101
  @property
98
102
  def originator_version(self) -> int:
99
103
  """
100
104
  Integer identifying the version of the aggregate when the event occurred.
101
105
  """
106
+ raise NotImplementedError # pragma: no cover
102
107
 
103
108
 
104
109
  TDomainEvent = TypeVar("TDomainEvent", bound=DomainEventProtocol)
110
+ SDomainEvent = TypeVar("SDomainEvent", bound=DomainEventProtocol)
105
111
 
106
112
 
107
113
  class MutableAggregateProtocol(Protocol):
@@ -120,18 +126,21 @@ class MutableAggregateProtocol(Protocol):
120
126
  """
121
127
  Mutable aggregates have a read-only ID that is a UUID.
122
128
  """
129
+ raise NotImplementedError # pragma: no cover
123
130
 
124
131
  @property
125
132
  def version(self) -> int:
126
133
  """
127
134
  Mutable aggregates have a read-write version that is an int.
128
135
  """
136
+ raise NotImplementedError # pragma: no cover
129
137
 
130
138
  @version.setter
131
139
  def version(self, value: int) -> None:
132
140
  """
133
141
  Mutable aggregates have a read-write version that is an int.
134
142
  """
143
+ raise NotImplementedError # pragma: no cover
135
144
 
136
145
 
137
146
  class ImmutableAggregateProtocol(Protocol):
@@ -150,12 +159,14 @@ class ImmutableAggregateProtocol(Protocol):
150
159
  """
151
160
  Immutable aggregates have a read-only ID that is a UUID.
152
161
  """
162
+ raise NotImplementedError # pragma: no cover
153
163
 
154
164
  @property
155
165
  def version(self) -> int:
156
166
  """
157
167
  Immutable aggregates have a read-only version that is an int.
158
168
  """
169
+ raise NotImplementedError # pragma: no cover
159
170
 
160
171
 
161
172
  MutableOrImmutableAggregate = Union[
@@ -180,6 +191,7 @@ class CollectEventsProtocol(Protocol):
180
191
  """
181
192
  Returns a sequence of events.
182
193
  """
194
+ raise NotImplementedError # pragma: no cover
183
195
 
184
196
 
185
197
  @runtime_checkable
@@ -233,7 +245,7 @@ class CanCreateTimestamp:
233
245
  return datetime_now_with_tzinfo()
234
246
 
235
247
 
236
- TAggregate = TypeVar("TAggregate", bound="Aggregate")
248
+ TAggregate = TypeVar("TAggregate", bound="BaseAggregate")
237
249
 
238
250
 
239
251
  class HasOriginatorIDVersion:
@@ -300,7 +312,7 @@ class CanMutateAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
300
312
  # Return the mutated aggregate.
301
313
  return aggregate
302
314
 
303
- def apply(self, aggregate: Aggregate) -> None:
315
+ def apply(self, aggregate: Any) -> None:
304
316
  """
305
317
  Applies the domain event to its aggregate.
306
318
 
@@ -934,9 +946,9 @@ def _raise_missing_names_type_error(missing_names: list[str], msg: str) -> None:
934
946
  raise TypeError(msg)
935
947
 
936
948
 
937
- _annotations_mention_id: set[type[Aggregate]] = set()
938
- _init_mentions_id: set[type[Aggregate]] = set()
939
- _create_id_param_names: dict[type[Aggregate], list[str]] = defaultdict(list)
949
+ _annotations_mention_id: set[type[BaseAggregate]] = set()
950
+ _init_mentions_id: set[type[BaseAggregate]] = set()
951
+ _create_id_param_names: dict[type[BaseAggregate], list[str]] = defaultdict(list)
940
952
 
941
953
 
942
954
  class MetaAggregate(EventsourcingType, Generic[TAggregate], type):
@@ -1011,19 +1023,13 @@ class MetaAggregate(EventsourcingType, Generic[TAggregate], type):
1011
1023
  _created_event_class: type[CanInitAggregate]
1012
1024
 
1013
1025
 
1014
- class Aggregate(metaclass=MetaAggregate):
1026
+ class BaseAggregate(metaclass=MetaAggregate):
1015
1027
  """
1016
1028
  Base class for aggregates.
1017
1029
  """
1018
1030
 
1019
1031
  INITIAL_VERSION = 1
1020
1032
 
1021
- class Event(AggregateEvent):
1022
- pass
1023
-
1024
- class Created(Event, AggregateCreated):
1025
- pass
1026
-
1027
1033
  @staticmethod
1028
1034
  def create_id(*_: Any, **__: Any) -> UUID:
1029
1035
  """
@@ -1081,7 +1087,7 @@ class Aggregate(metaclass=MetaAggregate):
1081
1087
 
1082
1088
  assert agg is not None
1083
1089
  # Append the domain event to pending list.
1084
- agg.pending_events.append(created_event)
1090
+ agg._pending_events.append(created_event)
1085
1091
  # Return the aggregate.
1086
1092
  return agg
1087
1093
 
@@ -1197,7 +1203,7 @@ class Aggregate(metaclass=MetaAggregate):
1197
1203
  return f"{type(self).__name__}({', '.join(attrs)})"
1198
1204
 
1199
1205
  def __init_subclass__(
1200
- cls: type[Aggregate], *, created_event_name: str | None = None
1206
+ cls: type[BaseAggregate], *, created_event_name: str | None = None
1201
1207
  ) -> None:
1202
1208
  """
1203
1209
  Initialises aggregate subclass by defining __init__ method and event classes.
@@ -1211,8 +1217,10 @@ class Aggregate(metaclass=MetaAggregate):
1211
1217
  except KeyError:
1212
1218
  pass
1213
1219
 
1214
- if class_annotations or any(
1215
- dataclasses.is_dataclass(base) for base in cls.__bases__
1220
+ if (
1221
+ class_annotations
1222
+ or cls in _annotations_mention_id
1223
+ or any(dataclasses.is_dataclass(base) for base in cls.__bases__)
1216
1224
  ):
1217
1225
  dataclasses.dataclass(eq=False, repr=False)(cls)
1218
1226
 
@@ -1223,7 +1231,9 @@ class Aggregate(metaclass=MetaAggregate):
1223
1231
  base_event_cls = cls.__dict__[base_event_name]
1224
1232
  except KeyError:
1225
1233
  base_event_cls = cls._define_event_class(
1226
- base_event_name, (cls.Event,), None
1234
+ name=base_event_name,
1235
+ bases=(getattr(cls, base_event_name, AggregateEvent),),
1236
+ apply_method=None,
1227
1237
  )
1228
1238
  setattr(cls, base_event_name, base_event_cls)
1229
1239
 
@@ -1482,9 +1492,12 @@ class Aggregate(metaclass=MetaAggregate):
1482
1492
  setattr(cls, name, sub_class)
1483
1493
 
1484
1494
 
1485
- # Special case for the Aggregate class because
1486
- # it's not processed by Aggregate.__init_subclass__.
1487
- _created_event_classes[Aggregate] = [Aggregate.Created]
1495
+ class Aggregate(BaseAggregate):
1496
+ class Event(AggregateEvent):
1497
+ pass
1498
+
1499
+ class Created(Event, AggregateCreated):
1500
+ pass
1488
1501
 
1489
1502
 
1490
1503
  @overload
@@ -1578,6 +1591,7 @@ class SnapshotProtocol(DomainEventProtocol, Protocol):
1578
1591
  """
1579
1592
  Snapshots have a read-only 'state'.
1580
1593
  """
1594
+ raise NotImplementedError # pragma: no cover
1581
1595
 
1582
1596
  # TODO: Improve on this 'Any'.
1583
1597
  @classmethod
@@ -1594,6 +1608,16 @@ class CanSnapshotAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
1594
1608
  topic: str
1595
1609
  state: Any
1596
1610
 
1611
+ def __init__(
1612
+ self,
1613
+ originator_id: UUID,
1614
+ originator_version: int,
1615
+ timestamp: datetime,
1616
+ topic: str,
1617
+ state: Any,
1618
+ ) -> None:
1619
+ raise NotImplementedError # pragma: no cover
1620
+
1597
1621
  @classmethod
1598
1622
  def take(
1599
1623
  cls: type[TCanSnapshotAggregate],
@@ -1610,7 +1634,7 @@ class CanSnapshotAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
1610
1634
  aggregate_state.pop("_id")
1611
1635
  aggregate_state.pop("_version")
1612
1636
  aggregate_state.pop("_pending_events")
1613
- return cls( # type: ignore
1637
+ return cls(
1614
1638
  originator_id=aggregate.id,
1615
1639
  originator_version=aggregate.version,
1616
1640
  timestamp=cls.create_timestamp(),
@@ -141,7 +141,7 @@ class NotificationLogJSONClient(NotificationLog):
141
141
  self,
142
142
  start: int | None,
143
143
  limit: int,
144
- _: int | None = None,
144
+ stop: int | None = None,
145
145
  topics: Sequence[str] = (),
146
146
  *,
147
147
  inclusive_of_start: bool = True,
@@ -517,10 +517,10 @@ class TrackingRecorder(Recorder, ABC):
517
517
  interrupt: Event | None = None,
518
518
  ) -> None:
519
519
  """
520
- Block until a tracking object with the given application name and
521
- notification ID has been recorded.
520
+ Block until a tracking object with the given application name and a
521
+ notification ID greater than equal to the given value has been recorded.
522
522
 
523
- Polls has_tracking_id() with exponential backoff until the timeout
523
+ Polls max_tracking_id() with exponential backoff until the timeout
524
524
  is reached, or until the optional interrupt event is set.
525
525
 
526
526
  The timeout argument should be a floating point number specifying a
@@ -534,7 +534,10 @@ class TrackingRecorder(Recorder, ABC):
534
534
  deadline = monotonic() + timeout
535
535
  delay_ms = 1.0
536
536
  while True:
537
- if self.has_tracking_id(application_name, notification_id):
537
+ max_tracking_id = self.max_tracking_id(application_name)
538
+ if notification_id is None or (
539
+ max_tracking_id is not None and max_tracking_id >= notification_id
540
+ ):
538
541
  break
539
542
  if interrupt:
540
543
  if interrupt.wait(timeout=delay_ms / 1000):
@@ -751,6 +754,7 @@ class InfrastructureFactory(ABC, Generic[TTrackingRecorder]):
751
754
  mapper_topic = self.env.get(self.MAPPER_TOPIC)
752
755
  mapper_class = resolve_topic(mapper_topic) if mapper_topic else Mapper
753
756
 
757
+ assert isinstance(mapper_class, type) and issubclass(mapper_class, Mapper)
754
758
  return mapper_class(
755
759
  transcoder=transcoder or self.transcoder(),
756
760
  cipher=self.cipher(),