eventsourcing 9.3.5__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
@@ -685,7 +698,9 @@ class Application:
685
698
  _env.update(env)
686
699
  return Environment(name, _env)
687
700
 
688
- def construct_factory(self, env: Environment) -> InfrastructureFactory:
701
+ def construct_factory(
702
+ self, env: Environment
703
+ ) -> InfrastructureFactory[TrackingRecorder]:
689
704
  """
690
705
  Constructs an :class:`~eventsourcing.persistence.InfrastructureFactory`
691
706
  for use by the application.
@@ -705,10 +720,11 @@ class Application:
705
720
  for use by the application.
706
721
  """
707
722
  transcoder = self.factory.transcoder()
708
- self.register_transcodings(transcoder)
723
+ if isinstance(transcoder, JSONTranscoder):
724
+ self.register_transcodings(transcoder)
709
725
  return transcoder
710
726
 
711
- def register_transcodings(self, transcoder: Transcoder) -> None:
727
+ def register_transcodings(self, transcoder: JSONTranscoder) -> None:
712
728
  """
713
729
  Registers :class:`~eventsourcing.persistence.Transcoding`
714
730
  objects on given :class:`~eventsourcing.persistence.JSONTranscoder`.
@@ -887,6 +903,21 @@ class Application:
887
903
  need to take action when new domain events have been saved.
888
904
  """
889
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
+
890
921
  def close(self) -> None:
891
922
  self.closing.set()
892
923
  self.factory.close()
@@ -967,7 +998,7 @@ class EventSourcedLog(Generic[TDomainEvent]):
967
998
  return logged_cls( # type: ignore
968
999
  originator_id=self.originator_id,
969
1000
  originator_version=next_originator_version,
970
- timestamp=create_utc_datetime_now(),
1001
+ timestamp=datetime_now_with_tzinfo(),
971
1002
  **kwargs,
972
1003
  )
973
1004
 
@@ -1011,3 +1042,36 @@ class EventSourcedLog(Generic[TDomainEvent]):
1011
1042
  limit=limit,
1012
1043
  ),
1013
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
  ]