eventsourcing 9.5.0b3__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.
- eventsourcing/__init__.py +0 -0
- eventsourcing/application.py +998 -0
- eventsourcing/cipher.py +107 -0
- eventsourcing/compressor.py +15 -0
- eventsourcing/cryptography.py +91 -0
- eventsourcing/dcb/__init__.py +0 -0
- eventsourcing/dcb/api.py +144 -0
- eventsourcing/dcb/application.py +159 -0
- eventsourcing/dcb/domain.py +369 -0
- eventsourcing/dcb/msgpack.py +38 -0
- eventsourcing/dcb/persistence.py +193 -0
- eventsourcing/dcb/popo.py +178 -0
- eventsourcing/dcb/postgres_tt.py +704 -0
- eventsourcing/dcb/tests.py +608 -0
- eventsourcing/dispatch.py +80 -0
- eventsourcing/domain.py +1964 -0
- eventsourcing/interface.py +164 -0
- eventsourcing/persistence.py +1429 -0
- eventsourcing/popo.py +267 -0
- eventsourcing/postgres.py +1441 -0
- eventsourcing/projection.py +502 -0
- eventsourcing/py.typed +0 -0
- eventsourcing/sqlite.py +816 -0
- eventsourcing/system.py +1203 -0
- eventsourcing/tests/__init__.py +3 -0
- eventsourcing/tests/application.py +483 -0
- eventsourcing/tests/domain.py +105 -0
- eventsourcing/tests/persistence.py +1744 -0
- eventsourcing/tests/postgres_utils.py +131 -0
- eventsourcing/utils.py +257 -0
- eventsourcing-9.5.0b3.dist-info/METADATA +253 -0
- eventsourcing-9.5.0b3.dist-info/RECORD +35 -0
- eventsourcing-9.5.0b3.dist-info/WHEEL +4 -0
- eventsourcing-9.5.0b3.dist-info/licenses/AUTHORS +10 -0
- eventsourcing-9.5.0b3.dist-info/licenses/LICENSE +29 -0
eventsourcing/popo.py
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from threading import Event, RLock
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
from eventsourcing.persistence import (
|
|
9
|
+
AggregateRecorder,
|
|
10
|
+
ApplicationRecorder,
|
|
11
|
+
InfrastructureFactory,
|
|
12
|
+
IntegrityError,
|
|
13
|
+
ListenNotifySubscription,
|
|
14
|
+
Notification,
|
|
15
|
+
ProcessRecorder,
|
|
16
|
+
StoredEvent,
|
|
17
|
+
Subscription,
|
|
18
|
+
Tracking,
|
|
19
|
+
TrackingRecorder,
|
|
20
|
+
)
|
|
21
|
+
from eventsourcing.utils import resolve_topic, reversed_keys
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from collections.abc import Iterable, Sequence
|
|
25
|
+
from uuid import UUID
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class POPORecorder:
|
|
29
|
+
def __init__(self) -> None:
|
|
30
|
+
self._database_lock = RLock()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class POPOAggregateRecorder(POPORecorder, AggregateRecorder):
|
|
34
|
+
def __init__(self) -> None:
|
|
35
|
+
super().__init__()
|
|
36
|
+
self._stored_events: list[StoredEvent] = []
|
|
37
|
+
self._stored_events_index: dict[str, dict[int, int]] = defaultdict(dict)
|
|
38
|
+
|
|
39
|
+
def insert_events(
|
|
40
|
+
self, stored_events: Sequence[StoredEvent], **kwargs: Any
|
|
41
|
+
) -> Sequence[int] | None:
|
|
42
|
+
self._insert_events(stored_events, **kwargs)
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
def _insert_events(
|
|
46
|
+
self, stored_events: Sequence[StoredEvent], **kwargs: Any
|
|
47
|
+
) -> Sequence[int] | None:
|
|
48
|
+
with self._database_lock:
|
|
49
|
+
self._assert_uniqueness(stored_events, **kwargs)
|
|
50
|
+
return self._update_table(stored_events, **kwargs)
|
|
51
|
+
|
|
52
|
+
def _assert_uniqueness(
|
|
53
|
+
self, stored_events: Sequence[StoredEvent], **_: Any
|
|
54
|
+
) -> None:
|
|
55
|
+
new = set()
|
|
56
|
+
for s in stored_events:
|
|
57
|
+
# Check events don't already exist.
|
|
58
|
+
if s.originator_version in self._stored_events_index[str(s.originator_id)]:
|
|
59
|
+
msg = f"Stored event already recorded: {s}"
|
|
60
|
+
raise IntegrityError(msg)
|
|
61
|
+
new.add((s.originator_id, s.originator_version))
|
|
62
|
+
# Check new events are unique.
|
|
63
|
+
if len(new) < len(stored_events):
|
|
64
|
+
msg = f"Stored events are not unique: {stored_events}"
|
|
65
|
+
raise IntegrityError(msg)
|
|
66
|
+
|
|
67
|
+
def _update_table(
|
|
68
|
+
self, stored_events: Sequence[StoredEvent], **_: Any
|
|
69
|
+
) -> Sequence[int] | None:
|
|
70
|
+
notification_ids = []
|
|
71
|
+
for s in stored_events:
|
|
72
|
+
self._stored_events.append(s)
|
|
73
|
+
self._stored_events_index[str(s.originator_id)][s.originator_version] = (
|
|
74
|
+
len(self._stored_events) - 1
|
|
75
|
+
)
|
|
76
|
+
notification_ids.append(len(self._stored_events))
|
|
77
|
+
return notification_ids
|
|
78
|
+
|
|
79
|
+
def select_events(
|
|
80
|
+
self,
|
|
81
|
+
originator_id: UUID | str,
|
|
82
|
+
*,
|
|
83
|
+
gt: int | None = None,
|
|
84
|
+
lte: int | None = None,
|
|
85
|
+
desc: bool = False,
|
|
86
|
+
limit: int | None = None,
|
|
87
|
+
) -> Sequence[StoredEvent]:
|
|
88
|
+
with self._database_lock:
|
|
89
|
+
results = []
|
|
90
|
+
|
|
91
|
+
index = self._stored_events_index[str(originator_id)]
|
|
92
|
+
positions: Iterable[int]
|
|
93
|
+
positions = reversed_keys(index) if desc else index.keys()
|
|
94
|
+
for p in positions:
|
|
95
|
+
if gt is not None and not p > gt:
|
|
96
|
+
continue
|
|
97
|
+
if lte is not None and not p <= lte:
|
|
98
|
+
continue
|
|
99
|
+
s = self._stored_events[index[p]]
|
|
100
|
+
results.append(s)
|
|
101
|
+
if len(results) == limit:
|
|
102
|
+
break
|
|
103
|
+
return results
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class POPOApplicationRecorder(POPOAggregateRecorder, ApplicationRecorder):
|
|
107
|
+
def __init__(self) -> None:
|
|
108
|
+
super().__init__()
|
|
109
|
+
self._listeners: set[Event] = set()
|
|
110
|
+
|
|
111
|
+
def insert_events(
|
|
112
|
+
self, stored_events: Sequence[StoredEvent], **kwargs: Any
|
|
113
|
+
) -> Sequence[int] | None:
|
|
114
|
+
notification_ids = self._insert_events(stored_events, **kwargs)
|
|
115
|
+
self._notify_listeners()
|
|
116
|
+
return notification_ids
|
|
117
|
+
|
|
118
|
+
def select_notifications(
|
|
119
|
+
self,
|
|
120
|
+
start: int | None,
|
|
121
|
+
limit: int,
|
|
122
|
+
stop: int | None = None,
|
|
123
|
+
topics: Sequence[str] = (),
|
|
124
|
+
*,
|
|
125
|
+
inclusive_of_start: bool = True,
|
|
126
|
+
) -> Sequence[Notification]:
|
|
127
|
+
with self._database_lock:
|
|
128
|
+
results = []
|
|
129
|
+
if start is None:
|
|
130
|
+
start = 1
|
|
131
|
+
inclusive_of_start = True
|
|
132
|
+
if not inclusive_of_start:
|
|
133
|
+
start += 1
|
|
134
|
+
start = max(start, 1) # Don't use negative indexes!
|
|
135
|
+
i = start - 1 # Zero-based indexing.
|
|
136
|
+
while True:
|
|
137
|
+
if stop is not None and i > stop - 1:
|
|
138
|
+
break
|
|
139
|
+
try:
|
|
140
|
+
s = self._stored_events[i]
|
|
141
|
+
except IndexError:
|
|
142
|
+
break
|
|
143
|
+
i += 1
|
|
144
|
+
if topics and s.topic not in topics:
|
|
145
|
+
continue
|
|
146
|
+
n = Notification(
|
|
147
|
+
id=i,
|
|
148
|
+
originator_id=s.originator_id,
|
|
149
|
+
originator_version=s.originator_version,
|
|
150
|
+
topic=s.topic,
|
|
151
|
+
state=s.state,
|
|
152
|
+
)
|
|
153
|
+
results.append(n)
|
|
154
|
+
if len(results) == limit:
|
|
155
|
+
break
|
|
156
|
+
return results
|
|
157
|
+
|
|
158
|
+
def max_notification_id(self) -> int | None:
|
|
159
|
+
with self._database_lock:
|
|
160
|
+
return len(self._stored_events) or None
|
|
161
|
+
|
|
162
|
+
def subscribe(
|
|
163
|
+
self, gt: int | None = None, topics: Sequence[str] = ()
|
|
164
|
+
) -> Subscription[ApplicationRecorder]:
|
|
165
|
+
return POPOSubscription(recorder=self, gt=gt, topics=topics)
|
|
166
|
+
|
|
167
|
+
def listen(self, event: Event) -> None:
|
|
168
|
+
self._listeners.add(event)
|
|
169
|
+
|
|
170
|
+
def unlisten(self, event: Event) -> None:
|
|
171
|
+
with contextlib.suppress(KeyError):
|
|
172
|
+
self._listeners.remove(event)
|
|
173
|
+
|
|
174
|
+
def _notify_listeners(self) -> None:
|
|
175
|
+
for listener in self._listeners:
|
|
176
|
+
listener.set()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class POPOSubscription(ListenNotifySubscription[POPOApplicationRecorder]):
|
|
180
|
+
def __init__(
|
|
181
|
+
self,
|
|
182
|
+
recorder: POPOApplicationRecorder,
|
|
183
|
+
gt: int | None = None,
|
|
184
|
+
topics: Sequence[str] = (),
|
|
185
|
+
) -> None:
|
|
186
|
+
assert isinstance(recorder, POPOApplicationRecorder)
|
|
187
|
+
super().__init__(recorder=recorder, gt=gt, topics=topics)
|
|
188
|
+
self._recorder.listen(self._has_been_notified)
|
|
189
|
+
|
|
190
|
+
def stop(self) -> None:
|
|
191
|
+
super().stop()
|
|
192
|
+
self._recorder.unlisten(self._has_been_notified)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class POPOTrackingRecorder(POPORecorder, TrackingRecorder):
|
|
196
|
+
def __init__(self) -> None:
|
|
197
|
+
super().__init__()
|
|
198
|
+
self._max_tracking_ids: dict[str, int | None] = defaultdict(lambda: None)
|
|
199
|
+
|
|
200
|
+
def _assert_tracking_uniqueness(self, tracking: Tracking) -> None:
|
|
201
|
+
max_tracking_id = self._max_tracking_ids[tracking.application_name]
|
|
202
|
+
if max_tracking_id is not None and max_tracking_id >= tracking.notification_id:
|
|
203
|
+
msg = (
|
|
204
|
+
f"Tracking notification ID {tracking.notification_id} "
|
|
205
|
+
f"not greater than current max tracking ID {max_tracking_id}"
|
|
206
|
+
)
|
|
207
|
+
raise IntegrityError(msg)
|
|
208
|
+
|
|
209
|
+
def insert_tracking(self, tracking: Tracking) -> None:
|
|
210
|
+
with self._database_lock:
|
|
211
|
+
self._assert_tracking_uniqueness(tracking)
|
|
212
|
+
self._insert_tracking(tracking)
|
|
213
|
+
|
|
214
|
+
def _insert_tracking(self, tracking: Tracking) -> None:
|
|
215
|
+
self._max_tracking_ids[tracking.application_name] = tracking.notification_id
|
|
216
|
+
|
|
217
|
+
def max_tracking_id(self, application_name: str) -> int | None:
|
|
218
|
+
with self._database_lock:
|
|
219
|
+
return self._max_tracking_ids[application_name]
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class POPOProcessRecorder(
|
|
223
|
+
POPOTrackingRecorder, POPOApplicationRecorder, ProcessRecorder
|
|
224
|
+
):
|
|
225
|
+
def _assert_uniqueness(
|
|
226
|
+
self, stored_events: Sequence[StoredEvent], **kwargs: Any
|
|
227
|
+
) -> None:
|
|
228
|
+
super()._assert_uniqueness(stored_events, **kwargs)
|
|
229
|
+
t: Tracking | None = kwargs.get("tracking")
|
|
230
|
+
if t:
|
|
231
|
+
self._assert_tracking_uniqueness(t)
|
|
232
|
+
|
|
233
|
+
def _update_table(
|
|
234
|
+
self, stored_events: Sequence[StoredEvent], **kwargs: Any
|
|
235
|
+
) -> Sequence[int] | None:
|
|
236
|
+
notification_ids = super()._update_table(stored_events, **kwargs)
|
|
237
|
+
t: Tracking | None = kwargs.get("tracking")
|
|
238
|
+
if t:
|
|
239
|
+
self._insert_tracking(t)
|
|
240
|
+
return notification_ids
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class POPOFactory(InfrastructureFactory[POPOTrackingRecorder]):
|
|
244
|
+
def aggregate_recorder(self, purpose: str = "events") -> AggregateRecorder:
|
|
245
|
+
return POPOAggregateRecorder()
|
|
246
|
+
|
|
247
|
+
def application_recorder(self) -> ApplicationRecorder:
|
|
248
|
+
return POPOApplicationRecorder()
|
|
249
|
+
|
|
250
|
+
def tracking_recorder(
|
|
251
|
+
self, tracking_recorder_class: type[POPOTrackingRecorder] | None = None
|
|
252
|
+
) -> POPOTrackingRecorder:
|
|
253
|
+
if tracking_recorder_class is None:
|
|
254
|
+
tracking_recorder_topic = self.env.get(self.TRACKING_RECORDER_TOPIC)
|
|
255
|
+
if tracking_recorder_topic:
|
|
256
|
+
tracking_recorder_class = resolve_topic(tracking_recorder_topic)
|
|
257
|
+
else:
|
|
258
|
+
tracking_recorder_class = POPOTrackingRecorder
|
|
259
|
+
assert tracking_recorder_class is not None
|
|
260
|
+
assert issubclass(tracking_recorder_class, POPOTrackingRecorder)
|
|
261
|
+
return tracking_recorder_class()
|
|
262
|
+
|
|
263
|
+
def process_recorder(self) -> ProcessRecorder:
|
|
264
|
+
return POPOProcessRecorder()
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
Factory = POPOFactory
|