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.
- eventsourcing/application.py +75 -11
- 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 +64 -42
- eventsourcing/tests/application.py +48 -12
- eventsourcing/tests/persistence.py +304 -75
- eventsourcing/utils.py +1 -1
- {eventsourcing-9.3.5.dist-info → eventsourcing-9.4.0a1.dist-info}/LICENSE +1 -1
- {eventsourcing-9.3.5.dist-info → eventsourcing-9.4.0a1.dist-info}/METADATA +2 -2
- eventsourcing-9.4.0a1.dist-info/RECORD +25 -0
- eventsourcing-9.3.5.dist-info/RECORD +0 -24
- {eventsourcing-9.3.5.dist-info → eventsourcing-9.4.0a1.dist-info}/AUTHORS +0 -0
- {eventsourcing-9.3.5.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
|
|
@@ -685,7 +698,9 @@ class Application:
|
|
|
685
698
|
_env.update(env)
|
|
686
699
|
return Environment(name, _env)
|
|
687
700
|
|
|
688
|
-
def construct_factory(
|
|
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
|
-
|
|
723
|
+
if isinstance(transcoder, JSONTranscoder):
|
|
724
|
+
self.register_transcodings(transcoder)
|
|
709
725
|
return transcoder
|
|
710
726
|
|
|
711
|
-
def register_transcodings(self, transcoder:
|
|
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=
|
|
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
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
|
]
|