eventsourcing 9.3.4__py3-none-any.whl → 9.4.0a1__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.

@@ -25,7 +25,7 @@ from typing import (
25
25
  )
26
26
  from warnings import warn
27
27
 
28
- from typing_extensions import deprecated
28
+ from typing_extensions import Self, deprecated
29
29
 
30
30
  from eventsourcing.domain import (
31
31
  Aggregate,
@@ -34,12 +34,11 @@ from eventsourcing.domain import (
34
34
  DomainEventProtocol,
35
35
  EventSourcingError,
36
36
  MutableOrImmutableAggregate,
37
- ProgrammingError,
38
37
  Snapshot,
39
38
  SnapshotProtocol,
40
39
  TDomainEvent,
41
40
  TMutableOrImmutableAggregate,
42
- create_utc_datetime_now,
41
+ datetime_now_with_tzinfo,
43
42
  )
44
43
  from eventsourcing.persistence import (
45
44
  ApplicationRecorder,
@@ -47,16 +46,18 @@ from eventsourcing.persistence import (
47
46
  DecimalAsStr,
48
47
  EventStore,
49
48
  InfrastructureFactory,
49
+ JSONTranscoder,
50
50
  Mapper,
51
51
  Notification,
52
52
  Recording,
53
53
  Tracking,
54
+ TrackingRecorder,
54
55
  Transcoder,
55
56
  UUIDAsHex,
56
57
  )
57
58
  from eventsourcing.utils import Environment, EnvType, strtobool
58
59
 
59
- if TYPE_CHECKING: # pragma: nocover
60
+ if TYPE_CHECKING: # pragma: no cover
60
61
  from uuid import UUID
61
62
 
62
63
  ProjectorFunction = Callable[
@@ -70,6 +71,10 @@ MutatorFunction = Callable[
70
71
  ]
71
72
 
72
73
 
74
+ class ProgrammingError(Exception):
75
+ pass
76
+
77
+
73
78
  def project_aggregate(
74
79
  aggregate: TMutableOrImmutableAggregate | None,
75
80
  domain_events: Iterable[DomainEventProtocol],
@@ -435,10 +440,12 @@ class NotificationLog(ABC):
435
440
  @abstractmethod
436
441
  def select(
437
442
  self,
438
- start: int,
443
+ start: int | None,
439
444
  limit: int,
440
445
  stop: int | None = None,
441
446
  topics: Sequence[str] = (),
447
+ *,
448
+ inclusive_of_start: bool = True,
442
449
  ) -> List[Notification]:
443
450
  """
444
451
  Returns a selection of
@@ -523,10 +530,12 @@ class LocalNotificationLog(NotificationLog):
523
530
 
524
531
  def select(
525
532
  self,
526
- start: int,
533
+ start: int | None,
527
534
  limit: int,
528
535
  stop: int | None = None,
529
536
  topics: Sequence[str] = (),
537
+ *,
538
+ inclusive_of_start: bool = True,
530
539
  ) -> List[Notification]:
531
540
  """
532
541
  Returns a selection of
@@ -539,7 +548,11 @@ class LocalNotificationLog(NotificationLog):
539
548
  )
540
549
  raise ValueError(msg)
541
550
  return self.recorder.select_notifications(
542
- start=start, limit=limit, stop=stop, topics=topics
551
+ start=start,
552
+ limit=limit,
553
+ stop=stop,
554
+ topics=topics,
555
+ inclusive_of_start=inclusive_of_start,
543
556
  )
544
557
 
545
558
  @staticmethod
@@ -599,18 +612,6 @@ class ProcessingEvent:
599
612
  self.collect_events(*aggregates, **kwargs)
600
613
 
601
614
 
602
- class RecordingEvent:
603
- def __init__(
604
- self,
605
- application_name: str,
606
- recordings: List[Recording],
607
- previous_max_notification_id: int | None,
608
- ):
609
- self.application_name = application_name
610
- self.recordings = recordings
611
- self.previous_max_notification_id = previous_max_notification_id
612
-
613
-
614
615
  class Application:
615
616
  """
616
617
  Base class for event-sourced applications.
@@ -659,9 +660,6 @@ class Application:
659
660
  self._repository = self.construct_repository()
660
661
  self._notification_log = self.construct_notification_log()
661
662
  self.closing = Event()
662
- self.previous_max_notification_id: int | None = (
663
- self.recorder.max_notification_id()
664
- )
665
663
 
666
664
  @property
667
665
  def repository(self) -> Repository:
@@ -700,7 +698,9 @@ class Application:
700
698
  _env.update(env)
701
699
  return Environment(name, _env)
702
700
 
703
- def construct_factory(self, env: Environment) -> InfrastructureFactory:
701
+ def construct_factory(
702
+ self, env: Environment
703
+ ) -> InfrastructureFactory[TrackingRecorder]:
704
704
  """
705
705
  Constructs an :class:`~eventsourcing.persistence.InfrastructureFactory`
706
706
  for use by the application.
@@ -720,10 +720,11 @@ class Application:
720
720
  for use by the application.
721
721
  """
722
722
  transcoder = self.factory.transcoder()
723
- self.register_transcodings(transcoder)
723
+ if isinstance(transcoder, JSONTranscoder):
724
+ self.register_transcodings(transcoder)
724
725
  return transcoder
725
726
 
726
- def register_transcodings(self, transcoder: Transcoder) -> None:
727
+ def register_transcodings(self, transcoder: JSONTranscoder) -> None:
727
728
  """
728
729
  Registers :class:`~eventsourcing.persistence.Transcoding`
729
730
  objects on given :class:`~eventsourcing.persistence.JSONTranscoder`.
@@ -902,6 +903,21 @@ class Application:
902
903
  need to take action when new domain events have been saved.
903
904
  """
904
905
 
906
+ def subscribe(self, gt: int | None = None) -> ApplicationSubscription:
907
+ """
908
+ Returns an iterator that yields all domain events recorded in an application
909
+ sequence that have notification IDs greater than a given value. The iterator
910
+ will block when all recorded domain events have been yielded, and then
911
+ continue when new events are recorded. Domain events are returned along
912
+ with tracking objects that identify the position in the application sequence.
913
+ """
914
+ return ApplicationSubscription(
915
+ name=self.name,
916
+ recorder=self.recorder,
917
+ mapper=self.mapper,
918
+ gt=gt,
919
+ )
920
+
905
921
  def close(self) -> None:
906
922
  self.closing.set()
907
923
  self.factory.close()
@@ -982,7 +998,7 @@ class EventSourcedLog(Generic[TDomainEvent]):
982
998
  return logged_cls( # type: ignore
983
999
  originator_id=self.originator_id,
984
1000
  originator_version=next_originator_version,
985
- timestamp=create_utc_datetime_now(),
1001
+ timestamp=datetime_now_with_tzinfo(),
986
1002
  **kwargs,
987
1003
  )
988
1004
 
@@ -1026,3 +1042,36 @@ class EventSourcedLog(Generic[TDomainEvent]):
1026
1042
  limit=limit,
1027
1043
  ),
1028
1044
  )
1045
+
1046
+
1047
+ class ApplicationSubscription:
1048
+ def __init__(
1049
+ self,
1050
+ name: str,
1051
+ recorder: ApplicationRecorder,
1052
+ mapper: Mapper,
1053
+ gt: int | None = None,
1054
+ ):
1055
+ self.name = name
1056
+ self.recorder = recorder
1057
+ self.mapper = mapper
1058
+ self.subscription = self.recorder.subscribe(gt=gt)
1059
+
1060
+ def __enter__(self) -> Self:
1061
+ self.subscription.__enter__()
1062
+ return self
1063
+
1064
+ def __exit__(self, *args: object, **kwargs: Any) -> None:
1065
+ self.subscription.__exit__(*args, **kwargs)
1066
+
1067
+ def __iter__(self) -> Self:
1068
+ return self
1069
+
1070
+ def __next__(self) -> Tuple[DomainEventProtocol, Tracking]:
1071
+ notification = next(self.subscription)
1072
+ tracking = Tracking(self.name, notification.id)
1073
+ domain_event = self.mapper.to_domain_event(notification)
1074
+ return domain_event, tracking
1075
+
1076
+ def __del__(self) -> None:
1077
+ self.subscription.stop()
eventsourcing/cipher.py CHANGED
@@ -10,7 +10,7 @@ from Crypto.Cipher.AES import key_size
10
10
 
11
11
  from eventsourcing.persistence import Cipher
12
12
 
13
- if TYPE_CHECKING: # pragma: nocover
13
+ if TYPE_CHECKING: # pragma: no cover
14
14
  from eventsourcing.utils import Environment
15
15
 
16
16
 
eventsourcing/domain.py CHANGED
@@ -25,10 +25,20 @@ from typing import (
25
25
  runtime_checkable,
26
26
  )
27
27
  from uuid import UUID, uuid4
28
+ from warnings import warn
28
29
 
29
30
  from eventsourcing.utils import get_method_name, get_topic, resolve_topic
30
31
 
31
32
  TZINFO: tzinfo = resolve_topic(os.getenv("TZINFO_TOPIC", "datetime:timezone.utc"))
33
+ """
34
+ A Python :py:obj:`tzinfo` object that defaults to UTC (:py:obj:`timezone.utc`). Used
35
+ as the timezone argument in :func:`~eventsourcing.domain.datetime_now_with_tzinfo`.
36
+
37
+ Set environment variable ``TZINFO_TOPIC`` to the topic of a different :py:obj:`tzinfo`
38
+ object so that all your domain model event timestamps are located in that timezone
39
+ (not recommended). It is generally recommended to locate all timestamps in the UTC
40
+ domain and convert to local timezones when presenting values in user interfaces.
41
+ """
32
42
 
33
43
 
34
44
  @runtime_checkable
@@ -153,13 +163,27 @@ class CanMutateProtocol(DomainEventProtocol, Protocol[TMutableOrImmutableAggrega
153
163
  """
154
164
 
155
165
 
156
- def create_utc_datetime_now() -> datetime:
166
+ def datetime_now_with_tzinfo() -> datetime:
157
167
  """
158
168
  Constructs a timezone-aware :class:`datetime` object for the current date and time.
169
+
170
+ Uses :py:obj:`TZINFO` as the timezone.
159
171
  """
160
172
  return datetime.now(tz=TZINFO)
161
173
 
162
174
 
175
+ def create_utc_datetime_now() -> datetime:
176
+ """
177
+ Deprected in favour of :func:`~eventsourcing.domain.datetime_now_with_tzinfo`.
178
+ """
179
+ msg = (
180
+ "'create_utc_datetime_now()' is deprecated, "
181
+ "use 'datetime_now_with_tzinfo()' instead"
182
+ )
183
+ warn(msg, DeprecationWarning, stacklevel=2)
184
+ return datetime_now_with_tzinfo()
185
+
186
+
163
187
  class CanCreateTimestamp:
164
188
  """
165
189
  Provides a create_timestamp() method to subclasses.
@@ -171,7 +195,7 @@ class CanCreateTimestamp:
171
195
  Constructs a timezone-aware :class:`datetime` object
172
196
  representing when an event occurred.
173
197
  """
174
- return create_utc_datetime_now()
198
+ return datetime_now_with_tzinfo()
175
199
 
176
200
 
177
201
  TAggregate = TypeVar("TAggregate", bound="Aggregate")
@@ -386,7 +410,7 @@ def _spec_filter_kwargs_for_method_params(method: Callable[..., Any]) -> set[str
386
410
  return set(method_signature.parameters)
387
411
 
388
412
 
389
- if TYPE_CHECKING: # pragma: nocover
413
+ if TYPE_CHECKING: # pragma: no cover
390
414
  EventSpecType = Union[str, Type[CanMutateAggregate]]
391
415
 
392
416
  CommandMethod = Callable[..., None]
@@ -668,6 +692,8 @@ class UnboundCommandMethodDecorator:
668
692
  self.__qualname__ = event_decorator.decorated_method.__qualname__
669
693
  self.__annotations__ = event_decorator.decorated_method.__annotations__
670
694
  self.__doc__ = event_decorator.decorated_method.__doc__
695
+ # self.__wrapped__ = event_decorator.decorated_method
696
+ # functools.update_wrapper(self, event_decorator.decorated_method)
671
697
 
672
698
  def __call__(self, *args: Any, **kwargs: Any) -> None:
673
699
  # Expect first argument is an aggregate instance.
@@ -25,7 +25,12 @@ class NotificationLogInterface(ABC):
25
25
 
26
26
  @abstractmethod
27
27
  def get_notifications(
28
- self, start: int, limit: int, topics: Sequence[str] = ()
28
+ self,
29
+ start: int | None,
30
+ limit: int,
31
+ topics: Sequence[str] = (),
32
+ *,
33
+ inclusive_of_start: bool = True,
29
34
  ) -> str:
30
35
  """
31
36
  Returns a serialised list of :class:`~eventsourcing.persistence.Notification`
@@ -68,10 +73,18 @@ class NotificationLogJSONService(NotificationLogInterface, Generic[TApplication]
68
73
  )
69
74
 
70
75
  def get_notifications(
71
- self, start: int, limit: int, topics: Sequence[str] = ()
76
+ self,
77
+ start: int | None,
78
+ limit: int,
79
+ topics: Sequence[str] = (),
80
+ *,
81
+ inclusive_of_start: bool = True,
72
82
  ) -> str:
73
83
  notifications = self.app.notification_log.select(
74
- start=start, limit=limit, topics=topics
84
+ start=start,
85
+ limit=limit,
86
+ topics=topics,
87
+ inclusive_of_start=inclusive_of_start,
75
88
  )
76
89
  return json.dumps(
77
90
  [
@@ -123,10 +136,12 @@ class NotificationLogJSONClient(NotificationLog):
123
136
 
124
137
  def select(
125
138
  self,
126
- start: int,
139
+ start: int | None,
127
140
  limit: int,
128
141
  _: int | None = None,
129
142
  topics: Sequence[str] = (),
143
+ *,
144
+ inclusive_of_start: bool = True,
130
145
  ) -> List[Notification]:
131
146
  """
132
147
  Returns a selection of
@@ -143,7 +158,10 @@ class NotificationLogJSONClient(NotificationLog):
143
158
  )
144
159
  for item in json.loads(
145
160
  self.interface.get_notifications(
146
- start=start, limit=limit, topics=topics
161
+ start=start,
162
+ limit=limit,
163
+ topics=topics,
164
+ inclusive_of_start=inclusive_of_start,
147
165
  )
148
166
  )
149
167
  ]