eventsourcing 9.4.0b1__py3-none-any.whl → 9.4.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.
Potentially problematic release.
This version of eventsourcing might be problematic. Click here for more details.
- eventsourcing/application.py +53 -104
- eventsourcing/cipher.py +3 -8
- eventsourcing/compressor.py +2 -6
- eventsourcing/cryptography.py +3 -8
- eventsourcing/dispatch.py +2 -2
- eventsourcing/domain.py +211 -291
- eventsourcing/interface.py +10 -24
- eventsourcing/persistence.py +122 -219
- eventsourcing/popo.py +2 -2
- eventsourcing/postgres.py +7 -10
- eventsourcing/projection.py +81 -54
- eventsourcing/sqlite.py +4 -7
- eventsourcing/system.py +89 -156
- eventsourcing/tests/application.py +137 -74
- eventsourcing/tests/domain.py +14 -34
- eventsourcing/tests/persistence.py +21 -18
- eventsourcing/utils.py +11 -17
- {eventsourcing-9.4.0b1.dist-info → eventsourcing-9.4.0b3.dist-info}/METADATA +1 -1
- eventsourcing-9.4.0b3.dist-info/RECORD +26 -0
- eventsourcing-9.4.0b1.dist-info/RECORD +0 -26
- {eventsourcing-9.4.0b1.dist-info → eventsourcing-9.4.0b3.dist-info}/AUTHORS +0 -0
- {eventsourcing-9.4.0b1.dist-info → eventsourcing-9.4.0b3.dist-info}/LICENSE +0 -0
- {eventsourcing-9.4.0b1.dist-info → eventsourcing-9.4.0b3.dist-info}/WHEEL +0 -0
eventsourcing/popo.py
CHANGED
|
@@ -235,7 +235,7 @@ class POPOProcessRecorder(
|
|
|
235
235
|
self, stored_events: list[StoredEvent], **kwargs: Any
|
|
236
236
|
) -> None:
|
|
237
237
|
super()._assert_uniqueness(stored_events, **kwargs)
|
|
238
|
-
t: Tracking | None = kwargs.get("tracking"
|
|
238
|
+
t: Tracking | None = kwargs.get("tracking")
|
|
239
239
|
if t:
|
|
240
240
|
self._assert_tracking_uniqueness(t)
|
|
241
241
|
|
|
@@ -243,7 +243,7 @@ class POPOProcessRecorder(
|
|
|
243
243
|
self, stored_events: list[StoredEvent], **kwargs: Any
|
|
244
244
|
) -> Sequence[int] | None:
|
|
245
245
|
notification_ids = super()._update_table(stored_events, **kwargs)
|
|
246
|
-
t: Tracking | None = kwargs.get("tracking"
|
|
246
|
+
t: Tracking | None = kwargs.get("tracking")
|
|
247
247
|
if t:
|
|
248
248
|
self._insert_tracking(t)
|
|
249
249
|
return notification_ids
|
eventsourcing/postgres.py
CHANGED
|
@@ -75,7 +75,7 @@ class ConnectionPool(psycopg_pool.ConnectionPool[Any]):
|
|
|
75
75
|
|
|
76
76
|
|
|
77
77
|
class PostgresDatastore:
|
|
78
|
-
def __init__(
|
|
78
|
+
def __init__( # noqa: PLR0913
|
|
79
79
|
self,
|
|
80
80
|
dbname: str,
|
|
81
81
|
host: str,
|
|
@@ -440,11 +440,9 @@ class PostgresApplicationRecorder(PostgresAggregateRecorder, ApplicationRecorder
|
|
|
440
440
|
*,
|
|
441
441
|
inclusive_of_start: bool = True,
|
|
442
442
|
) -> list[Notification]:
|
|
443
|
-
"""
|
|
444
|
-
Returns a list of event notifications
|
|
443
|
+
"""Returns a list of event notifications
|
|
445
444
|
from 'start', limited by 'limit'.
|
|
446
445
|
"""
|
|
447
|
-
|
|
448
446
|
params: list[int | str | Sequence[str]] = []
|
|
449
447
|
statement = SQL("SELECT * FROM {0}.{1}").format(
|
|
450
448
|
Identifier(self.datastore.schema),
|
|
@@ -501,9 +499,7 @@ class PostgresApplicationRecorder(PostgresAggregateRecorder, ApplicationRecorder
|
|
|
501
499
|
|
|
502
500
|
@retry((InterfaceError, OperationalError), max_attempts=10, wait=0.2)
|
|
503
501
|
def max_notification_id(self) -> int | None:
|
|
504
|
-
"""
|
|
505
|
-
Returns the maximum notification ID.
|
|
506
|
-
"""
|
|
502
|
+
"""Returns the maximum notification ID."""
|
|
507
503
|
with self.datastore.get_connection() as conn, conn.cursor() as curs:
|
|
508
504
|
curs.execute(self.max_notification_id_statement)
|
|
509
505
|
fetchone = curs.fetchone()
|
|
@@ -553,7 +549,7 @@ class PostgresApplicationRecorder(PostgresAggregateRecorder, ApplicationRecorder
|
|
|
553
549
|
notification_ids.append(row["notification_id"])
|
|
554
550
|
if len(notification_ids) != len(stored_events):
|
|
555
551
|
msg = "Couldn't get all notification IDs "
|
|
556
|
-
msg += f"(got {len(notification_ids)}, expected {len(stored_events)}"
|
|
552
|
+
msg += f"(got {len(notification_ids)}, expected {len(stored_events)})"
|
|
557
553
|
raise ProgrammingError(msg)
|
|
558
554
|
return notification_ids
|
|
559
555
|
|
|
@@ -730,7 +726,7 @@ class PostgresProcessRecorder(
|
|
|
730
726
|
stored_events: list[StoredEvent],
|
|
731
727
|
**kwargs: Any,
|
|
732
728
|
) -> None:
|
|
733
|
-
tracking: Tracking | None = kwargs.get("tracking"
|
|
729
|
+
tracking: Tracking | None = kwargs.get("tracking")
|
|
734
730
|
if tracking is not None:
|
|
735
731
|
self._insert_tracking(curs, tracking=tracking)
|
|
736
732
|
super()._insert_events(curs, stored_events, **kwargs)
|
|
@@ -974,7 +970,8 @@ class PostgresFactory(InfrastructureFactory[PostgresTrackingRecorder]):
|
|
|
974
970
|
tracking_recorder_class = resolve_topic(tracking_recorder_topic)
|
|
975
971
|
else:
|
|
976
972
|
tracking_recorder_class = cast(
|
|
977
|
-
type[TPostgresTrackingRecorder],
|
|
973
|
+
"type[TPostgresTrackingRecorder]",
|
|
974
|
+
type(self).tracking_recorder_class,
|
|
978
975
|
)
|
|
979
976
|
assert tracking_recorder_class is not None
|
|
980
977
|
assert issubclass(tracking_recorder_class, PostgresTrackingRecorder)
|
eventsourcing/projection.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import contextlib
|
|
3
4
|
import os
|
|
4
5
|
import weakref
|
|
5
6
|
from abc import ABC, abstractmethod
|
|
@@ -22,12 +23,13 @@ from eventsourcing.persistence import (
|
|
|
22
23
|
from eventsourcing.utils import Environment, EnvType
|
|
23
24
|
|
|
24
25
|
if TYPE_CHECKING:
|
|
26
|
+
from types import TracebackType
|
|
27
|
+
|
|
25
28
|
from typing_extensions import Self
|
|
26
29
|
|
|
27
30
|
|
|
28
31
|
class ApplicationSubscription(Iterator[tuple[DomainEventProtocol, Tracking]]):
|
|
29
|
-
"""
|
|
30
|
-
An iterator that yields all domain events recorded in an application
|
|
32
|
+
"""An iterator that yields all domain events recorded in an application
|
|
31
33
|
sequence that have notification IDs greater than a given value. The iterator
|
|
32
34
|
will block when all recorded domain events have been yielded, and then
|
|
33
35
|
continue when new events are recorded. Domain events are returned along
|
|
@@ -49,30 +51,23 @@ class ApplicationSubscription(Iterator[tuple[DomainEventProtocol, Tracking]]):
|
|
|
49
51
|
self.subscription = self.recorder.subscribe(gt=gt, topics=topics)
|
|
50
52
|
|
|
51
53
|
def stop(self) -> None:
|
|
52
|
-
"""
|
|
53
|
-
Stops the stored event subscription.
|
|
54
|
-
"""
|
|
54
|
+
"""Stops the stored event subscription."""
|
|
55
55
|
self.subscription.stop()
|
|
56
56
|
|
|
57
57
|
def __enter__(self) -> Self:
|
|
58
|
-
"""
|
|
59
|
-
Calls __enter__ on the stored event subscription.
|
|
60
|
-
"""
|
|
58
|
+
"""Calls __enter__ on the stored event subscription."""
|
|
61
59
|
self.subscription.__enter__()
|
|
62
60
|
return self
|
|
63
61
|
|
|
64
62
|
def __exit__(self, *args: object, **kwargs: Any) -> None:
|
|
65
|
-
"""
|
|
66
|
-
Calls __exit__ on the stored event subscription.
|
|
67
|
-
"""
|
|
63
|
+
"""Calls __exit__ on the stored event subscription."""
|
|
68
64
|
self.subscription.__exit__(*args, **kwargs)
|
|
69
65
|
|
|
70
66
|
def __iter__(self) -> Self:
|
|
71
67
|
return self
|
|
72
68
|
|
|
73
69
|
def __next__(self) -> tuple[DomainEventProtocol, Tracking]:
|
|
74
|
-
"""
|
|
75
|
-
Returns the next stored event from the stored event subscription.
|
|
70
|
+
"""Returns the next stored event from the stored event subscription.
|
|
76
71
|
Constructs a tracking object that identifies the position of
|
|
77
72
|
the event in the application sequence, and reconstructs a domain
|
|
78
73
|
event object from the stored event object.
|
|
@@ -83,9 +78,7 @@ class ApplicationSubscription(Iterator[tuple[DomainEventProtocol, Tracking]]):
|
|
|
83
78
|
return domain_event, tracking
|
|
84
79
|
|
|
85
80
|
def __del__(self) -> None:
|
|
86
|
-
"""
|
|
87
|
-
Stops the stored event subscription.
|
|
88
|
-
"""
|
|
81
|
+
"""Stops the stored event subscription."""
|
|
89
82
|
self.stop()
|
|
90
83
|
|
|
91
84
|
|
|
@@ -117,9 +110,7 @@ class Projection(ABC, Generic[TTrackingRecorder]):
|
|
|
117
110
|
def process_event(
|
|
118
111
|
self, domain_event: DomainEventProtocol, tracking: Tracking
|
|
119
112
|
) -> None:
|
|
120
|
-
"""
|
|
121
|
-
Process a domain event and track it.
|
|
122
|
-
"""
|
|
113
|
+
"""Process a domain event and track it."""
|
|
123
114
|
|
|
124
115
|
|
|
125
116
|
TApplication = TypeVar("TApplication", bound=Application)
|
|
@@ -134,8 +125,7 @@ class ProjectionRunner(Generic[TApplication, TTrackingRecorder]):
|
|
|
134
125
|
view_class: type[TTrackingRecorder],
|
|
135
126
|
env: EnvType | None = None,
|
|
136
127
|
):
|
|
137
|
-
"""
|
|
138
|
-
Constructs application from given application class with given environment.
|
|
128
|
+
"""Constructs application from given application class with given environment.
|
|
139
129
|
Also constructs a materialised view from given class using an infrastructure
|
|
140
130
|
factory constructed with an environment named after the projection. Also
|
|
141
131
|
constructs a projection with the constructed materialised view object.
|
|
@@ -143,7 +133,8 @@ class ProjectionRunner(Generic[TApplication, TTrackingRecorder]):
|
|
|
143
133
|
thread, calls projection's process_event() method for each event and tracking
|
|
144
134
|
object pair received from the subscription.
|
|
145
135
|
"""
|
|
146
|
-
self.
|
|
136
|
+
self._is_interrupted = Event()
|
|
137
|
+
self._has_called_stop = False
|
|
147
138
|
|
|
148
139
|
self.app: TApplication = application_class(env)
|
|
149
140
|
|
|
@@ -165,22 +156,32 @@ class ProjectionRunner(Generic[TApplication, TTrackingRecorder]):
|
|
|
165
156
|
gt=self.view.max_tracking_id(self.app.name),
|
|
166
157
|
topics=self.projection.topics,
|
|
167
158
|
)
|
|
168
|
-
self.
|
|
169
|
-
self.
|
|
159
|
+
self._thread_error: BaseException | None = None
|
|
160
|
+
self._stop_thread = Thread(
|
|
161
|
+
target=self._stop_subscription_when_stopping,
|
|
162
|
+
kwargs={
|
|
163
|
+
"subscription": self.subscription,
|
|
164
|
+
"is_stopping": self._is_interrupted,
|
|
165
|
+
},
|
|
166
|
+
)
|
|
167
|
+
self._stop_thread.start()
|
|
168
|
+
self._processing_thread = Thread(
|
|
170
169
|
target=self._process_events_loop,
|
|
171
170
|
kwargs={
|
|
172
171
|
"subscription": self.subscription,
|
|
173
172
|
"projection": self.projection,
|
|
174
|
-
"is_stopping": self.
|
|
173
|
+
"is_stopping": self._is_interrupted,
|
|
175
174
|
"runner": weakref.ref(self),
|
|
176
175
|
},
|
|
177
176
|
)
|
|
178
|
-
self.
|
|
177
|
+
self._processing_thread.start()
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def is_interrupted(self) -> Event:
|
|
181
|
+
return self._is_interrupted
|
|
179
182
|
|
|
180
183
|
def _construct_env(self, name: str, env: EnvType | None = None) -> Environment:
|
|
181
|
-
"""
|
|
182
|
-
Constructs environment from which projection will be configured.
|
|
183
|
-
"""
|
|
184
|
+
"""Constructs environment from which projection will be configured."""
|
|
184
185
|
_env: dict[str, str] = {}
|
|
185
186
|
_env.update(os.environ)
|
|
186
187
|
if env is not None:
|
|
@@ -188,11 +189,23 @@ class ProjectionRunner(Generic[TApplication, TTrackingRecorder]):
|
|
|
188
189
|
return Environment(name, _env)
|
|
189
190
|
|
|
190
191
|
def stop(self) -> None:
|
|
192
|
+
"""Sets the "interrupted" event."""
|
|
193
|
+
self._has_called_stop = True
|
|
194
|
+
self._is_interrupted.set()
|
|
195
|
+
|
|
196
|
+
@staticmethod
|
|
197
|
+
def _stop_subscription_when_stopping(
|
|
198
|
+
subscription: ApplicationSubscription,
|
|
199
|
+
is_stopping: Event,
|
|
200
|
+
) -> None:
|
|
201
|
+
"""Stops the application subscription, which
|
|
202
|
+
will stop the event-processing thread.
|
|
191
203
|
"""
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
204
|
+
try:
|
|
205
|
+
is_stopping.wait()
|
|
206
|
+
finally:
|
|
207
|
+
is_stopping.set()
|
|
208
|
+
subscription.stop()
|
|
196
209
|
|
|
197
210
|
@staticmethod
|
|
198
211
|
def _process_events_loop(
|
|
@@ -208,7 +221,7 @@ class ProjectionRunner(Generic[TApplication, TTrackingRecorder]):
|
|
|
208
221
|
except BaseException as e:
|
|
209
222
|
_runner = runner() # get reference from weakref
|
|
210
223
|
if _runner is not None:
|
|
211
|
-
_runner.
|
|
224
|
+
_runner._thread_error = e # noqa: SLF001
|
|
212
225
|
else:
|
|
213
226
|
msg = "ProjectionRunner was deleted before error could be assigned:\n"
|
|
214
227
|
msg += format_exc()
|
|
@@ -217,21 +230,23 @@ class ProjectionRunner(Generic[TApplication, TTrackingRecorder]):
|
|
|
217
230
|
RuntimeWarning,
|
|
218
231
|
stacklevel=2,
|
|
219
232
|
)
|
|
220
|
-
|
|
233
|
+
finally:
|
|
221
234
|
is_stopping.set()
|
|
222
|
-
subscription.stop()
|
|
223
235
|
|
|
224
236
|
def run_forever(self, timeout: float | None = None) -> None:
|
|
225
|
-
"""
|
|
226
|
-
Blocks until timeout, or until the runner is stopped or errors. Re-raises
|
|
237
|
+
"""Blocks until timeout, or until the runner is stopped or errors. Re-raises
|
|
227
238
|
any error otherwise exits normally
|
|
228
239
|
"""
|
|
229
|
-
if
|
|
230
|
-
|
|
240
|
+
if (
|
|
241
|
+
self._is_interrupted.wait(timeout=timeout)
|
|
242
|
+
and self._thread_error is not None
|
|
243
|
+
):
|
|
244
|
+
error = self._thread_error
|
|
245
|
+
self._thread_error = None
|
|
246
|
+
raise error
|
|
231
247
|
|
|
232
248
|
def wait(self, notification_id: int | None, timeout: float = 1.0) -> None:
|
|
233
|
-
"""
|
|
234
|
-
Blocks until timeout, or until the materialised view has recorded a tracking
|
|
249
|
+
"""Blocks until timeout, or until the materialised view has recorded a tracking
|
|
235
250
|
object that is greater than or equal to the given notification ID.
|
|
236
251
|
"""
|
|
237
252
|
try:
|
|
@@ -239,24 +254,36 @@ class ProjectionRunner(Generic[TApplication, TTrackingRecorder]):
|
|
|
239
254
|
application_name=self.subscription.name,
|
|
240
255
|
notification_id=notification_id,
|
|
241
256
|
timeout=timeout,
|
|
242
|
-
interrupt=self.
|
|
257
|
+
interrupt=self._is_interrupted,
|
|
243
258
|
)
|
|
244
259
|
except WaitInterruptedError:
|
|
245
|
-
if self.
|
|
246
|
-
|
|
260
|
+
if self._thread_error:
|
|
261
|
+
error = self._thread_error
|
|
262
|
+
self._thread_error = None
|
|
263
|
+
raise error from None
|
|
264
|
+
if self._has_called_stop:
|
|
265
|
+
return
|
|
266
|
+
raise
|
|
247
267
|
|
|
248
268
|
def __enter__(self) -> Self:
|
|
249
269
|
return self
|
|
250
270
|
|
|
251
|
-
def __exit__(
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
271
|
+
def __exit__(
|
|
272
|
+
self,
|
|
273
|
+
exc_type: type[BaseException] | None,
|
|
274
|
+
exc_val: BaseException | None,
|
|
275
|
+
exc_tb: TracebackType | None,
|
|
276
|
+
) -> None:
|
|
277
|
+
"""Calls stop() and waits for the event-processing thread to exit."""
|
|
255
278
|
self.stop()
|
|
256
|
-
self.
|
|
279
|
+
self._stop_thread.join()
|
|
280
|
+
self._processing_thread.join()
|
|
281
|
+
if self._thread_error:
|
|
282
|
+
error = self._thread_error
|
|
283
|
+
self._thread_error = None
|
|
284
|
+
raise error
|
|
257
285
|
|
|
258
286
|
def __del__(self) -> None:
|
|
259
|
-
"""
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
self.stop()
|
|
287
|
+
"""Calls stop()."""
|
|
288
|
+
with contextlib.suppress(AttributeError):
|
|
289
|
+
self.stop()
|
eventsourcing/sqlite.py
CHANGED
|
@@ -412,8 +412,7 @@ class SQLiteApplicationRecorder(
|
|
|
412
412
|
*,
|
|
413
413
|
inclusive_of_start: bool = True,
|
|
414
414
|
) -> list[Notification]:
|
|
415
|
-
"""
|
|
416
|
-
Returns a list of event notifications
|
|
415
|
+
"""Returns a list of event notifications
|
|
417
416
|
from 'start', limited by 'limit'.
|
|
418
417
|
"""
|
|
419
418
|
params: list[int | str] = []
|
|
@@ -443,7 +442,7 @@ class SQLiteApplicationRecorder(
|
|
|
443
442
|
else:
|
|
444
443
|
statement += "AND "
|
|
445
444
|
params += list(topics)
|
|
446
|
-
statement += "topic IN (
|
|
445
|
+
statement += f"topic IN ({','.join('?' * len(topics))}) "
|
|
447
446
|
|
|
448
447
|
params.append(limit)
|
|
449
448
|
statement += "ORDER BY rowid LIMIT ?"
|
|
@@ -462,9 +461,7 @@ class SQLiteApplicationRecorder(
|
|
|
462
461
|
]
|
|
463
462
|
|
|
464
463
|
def max_notification_id(self) -> int:
|
|
465
|
-
"""
|
|
466
|
-
Returns the maximum notification ID.
|
|
467
|
-
"""
|
|
464
|
+
"""Returns the maximum notification ID."""
|
|
468
465
|
with self.datastore.transaction(commit=False) as c:
|
|
469
466
|
return self._max_notification_id(c)
|
|
470
467
|
|
|
@@ -561,7 +558,7 @@ class SQLiteProcessRecorder(
|
|
|
561
558
|
**kwargs: Any,
|
|
562
559
|
) -> Sequence[int] | None:
|
|
563
560
|
returning = super()._insert_events(c, stored_events, **kwargs)
|
|
564
|
-
tracking: Tracking | None = kwargs.get("tracking"
|
|
561
|
+
tracking: Tracking | None = kwargs.get("tracking")
|
|
565
562
|
if tracking is not None:
|
|
566
563
|
self._insert_tracking(c, tracking)
|
|
567
564
|
return returning
|