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/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