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.
- eventsourcing/application.py +75 -26
- eventsourcing/cipher.py +1 -1
- eventsourcing/domain.py +29 -3
- eventsourcing/interface.py +23 -5
- eventsourcing/persistence.py +292 -71
- eventsourcing/popo.py +113 -32
- eventsourcing/postgres.py +265 -103
- eventsourcing/projection.py +157 -0
- eventsourcing/sqlite.py +143 -36
- eventsourcing/system.py +89 -44
- eventsourcing/tests/application.py +48 -12
- eventsourcing/tests/persistence.py +304 -75
- eventsourcing/utils.py +1 -1
- {eventsourcing-9.3.4.dist-info → eventsourcing-9.4.0a1.dist-info}/LICENSE +1 -1
- {eventsourcing-9.3.4.dist-info → eventsourcing-9.4.0a1.dist-info}/METADATA +2 -2
- eventsourcing-9.4.0a1.dist-info/RECORD +25 -0
- eventsourcing-9.3.4.dist-info/RECORD +0 -24
- {eventsourcing-9.3.4.dist-info → eventsourcing-9.4.0a1.dist-info}/AUTHORS +0 -0
- {eventsourcing-9.3.4.dist-info → eventsourcing-9.4.0a1.dist-info}/WHEEL +0 -0
eventsourcing/application.py
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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,
|
|
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(
|
|
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
|
-
|
|
723
|
+
if isinstance(transcoder, JSONTranscoder):
|
|
724
|
+
self.register_transcodings(transcoder)
|
|
724
725
|
return transcoder
|
|
725
726
|
|
|
726
|
-
def register_transcodings(self, transcoder:
|
|
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=
|
|
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
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
|
|
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
|
|
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:
|
|
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.
|
eventsourcing/interface.py
CHANGED
|
@@ -25,7 +25,12 @@ class NotificationLogInterface(ABC):
|
|
|
25
25
|
|
|
26
26
|
@abstractmethod
|
|
27
27
|
def get_notifications(
|
|
28
|
-
self,
|
|
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,
|
|
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,
|
|
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,
|
|
161
|
+
start=start,
|
|
162
|
+
limit=limit,
|
|
163
|
+
topics=topics,
|
|
164
|
+
inclusive_of_start=inclusive_of_start,
|
|
147
165
|
)
|
|
148
166
|
)
|
|
149
167
|
]
|