eventsourcing 9.4.0b4__py3-none-any.whl → 9.4.1__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/postgres.py +4 -0
- eventsourcing/projection.py +182 -44
- eventsourcing/sqlite.py +1 -0
- eventsourcing/system.py +28 -86
- eventsourcing/tests/persistence.py +2 -2
- {eventsourcing-9.4.0b4.dist-info → eventsourcing-9.4.1.dist-info}/METADATA +12 -12
- {eventsourcing-9.4.0b4.dist-info → eventsourcing-9.4.1.dist-info}/RECORD +10 -10
- {eventsourcing-9.4.0b4.dist-info → eventsourcing-9.4.1.dist-info}/AUTHORS +0 -0
- {eventsourcing-9.4.0b4.dist-info → eventsourcing-9.4.1.dist-info}/LICENSE +0 -0
- {eventsourcing-9.4.0b4.dist-info → eventsourcing-9.4.1.dist-info}/WHEEL +0 -0
eventsourcing/postgres.py
CHANGED
|
@@ -766,6 +766,10 @@ class PostgresFactory(InfrastructureFactory[PostgresTrackingRecorder]):
|
|
|
766
766
|
"in environment with key "
|
|
767
767
|
f"'{self.POSTGRES_DBNAME}'"
|
|
768
768
|
)
|
|
769
|
+
# TODO: Indicate both keys here, also for other environment variables.
|
|
770
|
+
# ) + " or ".join(
|
|
771
|
+
# [f"'{key}'" for key in self.env.create_keys(self.POSTGRES_DBNAME)]
|
|
772
|
+
# )
|
|
769
773
|
raise OSError(msg)
|
|
770
774
|
|
|
771
775
|
host = self.env.get(self.POSTGRES_HOST)
|
eventsourcing/projection.py
CHANGED
|
@@ -2,19 +2,22 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import contextlib
|
|
4
4
|
import os
|
|
5
|
+
import threading
|
|
5
6
|
import weakref
|
|
6
7
|
from abc import ABC, abstractmethod
|
|
7
8
|
from collections.abc import Iterator, Sequence
|
|
8
9
|
from threading import Event, Thread
|
|
9
10
|
from traceback import format_exc
|
|
10
|
-
from typing import TYPE_CHECKING, Any, Generic, TypeVar
|
|
11
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypeVar
|
|
11
12
|
from warnings import warn
|
|
12
13
|
|
|
13
|
-
from eventsourcing.application import Application
|
|
14
|
+
from eventsourcing.application import Application, ProcessingEvent
|
|
14
15
|
from eventsourcing.dispatch import singledispatchmethod
|
|
15
16
|
from eventsourcing.domain import DomainEventProtocol
|
|
16
17
|
from eventsourcing.persistence import (
|
|
17
18
|
InfrastructureFactory,
|
|
19
|
+
IntegrityError,
|
|
20
|
+
ProcessRecorder,
|
|
18
21
|
Tracking,
|
|
19
22
|
TrackingRecorder,
|
|
20
23
|
TTrackingRecorder,
|
|
@@ -43,7 +46,7 @@ class ApplicationSubscription(Iterator[tuple[DomainEventProtocol, Tracking]]):
|
|
|
43
46
|
topics: Sequence[str] = (),
|
|
44
47
|
):
|
|
45
48
|
"""
|
|
46
|
-
Starts subscription to application's
|
|
49
|
+
Starts a subscription to application's recorder.
|
|
47
50
|
"""
|
|
48
51
|
self.name = app.name
|
|
49
52
|
self.recorder = app.recorder
|
|
@@ -51,7 +54,7 @@ class ApplicationSubscription(Iterator[tuple[DomainEventProtocol, Tracking]]):
|
|
|
51
54
|
self.subscription = self.recorder.subscribe(gt=gt, topics=topics)
|
|
52
55
|
|
|
53
56
|
def stop(self) -> None:
|
|
54
|
-
"""Stops the
|
|
57
|
+
"""Stops the subscription to the application's recorder."""
|
|
55
58
|
self.subscription.stop()
|
|
56
59
|
|
|
57
60
|
def __enter__(self) -> Self:
|
|
@@ -67,10 +70,11 @@ class ApplicationSubscription(Iterator[tuple[DomainEventProtocol, Tracking]]):
|
|
|
67
70
|
return self
|
|
68
71
|
|
|
69
72
|
def __next__(self) -> tuple[DomainEventProtocol, Tracking]:
|
|
70
|
-
"""Returns the next stored event from
|
|
71
|
-
Constructs a tracking object that identifies the position of
|
|
72
|
-
the event in the application sequence
|
|
73
|
-
|
|
73
|
+
"""Returns the next stored event from subscription to the application's
|
|
74
|
+
recorder. Constructs a tracking object that identifies the position of
|
|
75
|
+
the event in the application sequence. Constructs a domain event object
|
|
76
|
+
from the stored event object using the application's mapper. Returns a
|
|
77
|
+
tuple of the domain event object and the tracking object.
|
|
74
78
|
"""
|
|
75
79
|
notification = next(self.subscription)
|
|
76
80
|
tracking = Tracking(self.name, notification.id)
|
|
@@ -91,14 +95,18 @@ class Projection(ABC, Generic[TTrackingRecorder]):
|
|
|
91
95
|
"""
|
|
92
96
|
topics: tuple[str, ...] = ()
|
|
93
97
|
"""
|
|
94
|
-
|
|
98
|
+
Event topics, used to filter events in database when subscribing to an application.
|
|
95
99
|
"""
|
|
96
100
|
|
|
101
|
+
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
102
|
+
if "name" not in cls.__dict__:
|
|
103
|
+
cls.name = cls.__name__
|
|
104
|
+
|
|
97
105
|
def __init__(
|
|
98
106
|
self,
|
|
99
107
|
view: TTrackingRecorder,
|
|
100
108
|
):
|
|
101
|
-
"""Initialises
|
|
109
|
+
"""Initialises the view property with the given view argument."""
|
|
102
110
|
self._view = view
|
|
103
111
|
|
|
104
112
|
@property
|
|
@@ -114,63 +122,130 @@ class Projection(ABC, Generic[TTrackingRecorder]):
|
|
|
114
122
|
"""Process a domain event and track it."""
|
|
115
123
|
|
|
116
124
|
|
|
125
|
+
class EventSourcedProjection(Application, ABC):
|
|
126
|
+
"""Extends the :py:class:`~eventsourcing.application.Application` class
|
|
127
|
+
by using a process recorder as its application recorder, and by
|
|
128
|
+
processing domain events through its :py:func:`policy` method.
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
topics: ClassVar[Sequence[str]] = ()
|
|
132
|
+
|
|
133
|
+
def __init__(self, env: EnvType | None = None) -> None:
|
|
134
|
+
super().__init__(env)
|
|
135
|
+
self.recorder: ProcessRecorder
|
|
136
|
+
self.processing_lock = threading.Lock()
|
|
137
|
+
|
|
138
|
+
def construct_recorder(self) -> ProcessRecorder:
|
|
139
|
+
"""Constructs and returns a :class:`~eventsourcing.persistence.ProcessRecorder`
|
|
140
|
+
for the application to use as its application recorder.
|
|
141
|
+
"""
|
|
142
|
+
return self.factory.process_recorder()
|
|
143
|
+
|
|
144
|
+
def process_event(
|
|
145
|
+
self, domain_event: DomainEventProtocol, tracking: Tracking
|
|
146
|
+
) -> None:
|
|
147
|
+
"""Calls :func:`~eventsourcing.system.Follower.policy` method with
|
|
148
|
+
the given :class:`~eventsourcing.domain.AggregateEvent` and a
|
|
149
|
+
new :class:`~eventsourcing.application.ProcessingEvent` created from
|
|
150
|
+
the given :class:`~eventsourcing.persistence.Tracking` object.
|
|
151
|
+
|
|
152
|
+
The policy will collect any new aggregate events on the process
|
|
153
|
+
event object.
|
|
154
|
+
|
|
155
|
+
After the policy method returns, the process event object will
|
|
156
|
+
then be recorded by calling
|
|
157
|
+
:func:`~eventsourcing.application.Application.record`, which
|
|
158
|
+
will return new notifications.
|
|
159
|
+
|
|
160
|
+
After calling
|
|
161
|
+
:func:`~eventsourcing.application.Application.take_snapshots`,
|
|
162
|
+
the new notifications are passed to the
|
|
163
|
+
:func:`~eventsourcing.application.Application.notify` method.
|
|
164
|
+
"""
|
|
165
|
+
processing_event = ProcessingEvent(tracking=tracking)
|
|
166
|
+
self.policy(domain_event, processing_event)
|
|
167
|
+
try:
|
|
168
|
+
recordings = self._record(processing_event)
|
|
169
|
+
except IntegrityError:
|
|
170
|
+
if self.recorder.has_tracking_id(
|
|
171
|
+
tracking.application_name,
|
|
172
|
+
tracking.notification_id,
|
|
173
|
+
):
|
|
174
|
+
pass
|
|
175
|
+
else:
|
|
176
|
+
raise
|
|
177
|
+
else:
|
|
178
|
+
self._take_snapshots(processing_event)
|
|
179
|
+
self.notify(processing_event.events)
|
|
180
|
+
self._notify(recordings)
|
|
181
|
+
|
|
182
|
+
@singledispatchmethod
|
|
183
|
+
def policy(
|
|
184
|
+
self,
|
|
185
|
+
domain_event: DomainEventProtocol,
|
|
186
|
+
processing_event: ProcessingEvent,
|
|
187
|
+
) -> None:
|
|
188
|
+
"""Abstract domain event processing policy method. Must be
|
|
189
|
+
implemented by event processing applications. When
|
|
190
|
+
processing the given domain event, event processing
|
|
191
|
+
applications must use the :func:`~ProcessingEvent.collect_events`
|
|
192
|
+
method of the given :py:class:`~ProcessingEvent` object (not
|
|
193
|
+
the application's :func:`~eventsourcing.application.Application.save`
|
|
194
|
+
method) so that the new domain events will be recorded atomically
|
|
195
|
+
and uniquely with tracking information about the position of the processed
|
|
196
|
+
event in its application sequence.
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
|
|
117
200
|
TApplication = TypeVar("TApplication", bound=Application)
|
|
201
|
+
TEventSourcedProjection = TypeVar(
|
|
202
|
+
"TEventSourcedProjection", bound=EventSourcedProjection
|
|
203
|
+
)
|
|
118
204
|
|
|
119
205
|
|
|
120
|
-
class
|
|
206
|
+
class BaseProjectionRunner(Generic[TApplication]):
|
|
121
207
|
def __init__(
|
|
122
208
|
self,
|
|
123
209
|
*,
|
|
210
|
+
projection: EventSourcedProjection | Projection[Any],
|
|
124
211
|
application_class: type[TApplication],
|
|
125
|
-
|
|
126
|
-
|
|
212
|
+
tracking_recorder: TrackingRecorder,
|
|
213
|
+
topics: Sequence[str],
|
|
127
214
|
env: EnvType | None = None,
|
|
128
|
-
):
|
|
129
|
-
|
|
130
|
-
Also constructs a materialised view from given class using an infrastructure
|
|
131
|
-
factory constructed with an environment named after the projection. Also
|
|
132
|
-
constructs a projection with the constructed materialised view object.
|
|
133
|
-
Starts a subscription to application and, in a separate event-processing
|
|
134
|
-
thread, calls projection's process_event() method for each event and tracking
|
|
135
|
-
object pair received from the subscription.
|
|
136
|
-
"""
|
|
215
|
+
) -> None:
|
|
216
|
+
self._projection = projection
|
|
137
217
|
self._is_interrupted = Event()
|
|
138
218
|
self._has_called_stop = False
|
|
139
219
|
|
|
220
|
+
# Construct the application.
|
|
140
221
|
self.app: TApplication = application_class(env)
|
|
141
222
|
|
|
142
|
-
self.
|
|
143
|
-
InfrastructureFactory[TTrackingRecorder]
|
|
144
|
-
.construct(
|
|
145
|
-
env=self._construct_env(
|
|
146
|
-
name=projection_class.name or projection_class.__name__, env=env
|
|
147
|
-
)
|
|
148
|
-
)
|
|
149
|
-
.tracking_recorder(view_class)
|
|
150
|
-
)
|
|
223
|
+
self._tracking_recorder = tracking_recorder
|
|
151
224
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
)
|
|
155
|
-
self.subscription = ApplicationSubscription(
|
|
225
|
+
# Subscribe to the application.
|
|
226
|
+
self._subscription = ApplicationSubscription(
|
|
156
227
|
app=self.app,
|
|
157
|
-
gt=self.
|
|
158
|
-
topics=
|
|
228
|
+
gt=self._tracking_recorder.max_tracking_id(self.app.name),
|
|
229
|
+
topics=topics,
|
|
159
230
|
)
|
|
231
|
+
|
|
232
|
+
# Start a thread to stop the subscription when the runner is interrupted.
|
|
160
233
|
self._thread_error: BaseException | None = None
|
|
161
234
|
self._stop_thread = Thread(
|
|
162
235
|
target=self._stop_subscription_when_stopping,
|
|
163
236
|
kwargs={
|
|
164
|
-
"subscription": self.
|
|
237
|
+
"subscription": self._subscription,
|
|
165
238
|
"is_stopping": self._is_interrupted,
|
|
166
239
|
},
|
|
167
240
|
)
|
|
168
241
|
self._stop_thread.start()
|
|
242
|
+
|
|
243
|
+
# Start a thread to iterate over the subscription.
|
|
169
244
|
self._processing_thread = Thread(
|
|
170
245
|
target=self._process_events_loop,
|
|
171
246
|
kwargs={
|
|
172
|
-
"subscription": self.
|
|
173
|
-
"projection": self.
|
|
247
|
+
"subscription": self._subscription,
|
|
248
|
+
"projection": self._projection,
|
|
174
249
|
"is_stopping": self._is_interrupted,
|
|
175
250
|
"runner": weakref.ref(self),
|
|
176
251
|
},
|
|
@@ -181,7 +256,8 @@ class ProjectionRunner(Generic[TApplication, TTrackingRecorder]):
|
|
|
181
256
|
def is_interrupted(self) -> Event:
|
|
182
257
|
return self._is_interrupted
|
|
183
258
|
|
|
184
|
-
|
|
259
|
+
@staticmethod
|
|
260
|
+
def _construct_env(name: str, env: EnvType | None = None) -> Environment:
|
|
185
261
|
"""Constructs environment from which projection will be configured."""
|
|
186
262
|
_env: dict[str, str] = {}
|
|
187
263
|
_env.update(os.environ)
|
|
@@ -211,10 +287,11 @@ class ProjectionRunner(Generic[TApplication, TTrackingRecorder]):
|
|
|
211
287
|
@staticmethod
|
|
212
288
|
def _process_events_loop(
|
|
213
289
|
subscription: ApplicationSubscription,
|
|
214
|
-
projection: Projection[
|
|
290
|
+
projection: EventSourcedProjection | Projection[Any],
|
|
215
291
|
is_stopping: Event,
|
|
216
292
|
runner: weakref.ReferenceType[ProjectionRunner[Application, TrackingRecorder]],
|
|
217
293
|
) -> None:
|
|
294
|
+
"""Iterates over the subscription and calls process_event()."""
|
|
218
295
|
try:
|
|
219
296
|
with subscription:
|
|
220
297
|
for domain_event, tracking in subscription:
|
|
@@ -251,8 +328,8 @@ class ProjectionRunner(Generic[TApplication, TTrackingRecorder]):
|
|
|
251
328
|
object that is greater than or equal to the given notification ID.
|
|
252
329
|
"""
|
|
253
330
|
try:
|
|
254
|
-
self.
|
|
255
|
-
application_name=self.
|
|
331
|
+
self._tracking_recorder.wait(
|
|
332
|
+
application_name=self.app.name,
|
|
256
333
|
notification_id=notification_id,
|
|
257
334
|
timeout=timeout,
|
|
258
335
|
interrupt=self._is_interrupted,
|
|
@@ -288,3 +365,64 @@ class ProjectionRunner(Generic[TApplication, TTrackingRecorder]):
|
|
|
288
365
|
"""Calls stop()."""
|
|
289
366
|
with contextlib.suppress(AttributeError):
|
|
290
367
|
self.stop()
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
class ProjectionRunner(
|
|
371
|
+
BaseProjectionRunner[TApplication], Generic[TApplication, TTrackingRecorder]
|
|
372
|
+
):
|
|
373
|
+
def __init__(
|
|
374
|
+
self,
|
|
375
|
+
*,
|
|
376
|
+
application_class: type[TApplication],
|
|
377
|
+
projection_class: type[Projection[TTrackingRecorder]],
|
|
378
|
+
view_class: type[TTrackingRecorder],
|
|
379
|
+
env: EnvType | None = None,
|
|
380
|
+
):
|
|
381
|
+
"""Constructs application from given application class with given environment.
|
|
382
|
+
Also constructs a materialised view from given class using an infrastructure
|
|
383
|
+
factory constructed with an environment named after the projection. Also
|
|
384
|
+
constructs a projection with the constructed materialised view object.
|
|
385
|
+
Starts a subscription to application and, in a separate event-processing
|
|
386
|
+
thread, calls projection's process_event() method for each event and tracking
|
|
387
|
+
object pair received from the subscription.
|
|
388
|
+
"""
|
|
389
|
+
# Construct the materialised view using an infrastructure factory.
|
|
390
|
+
self.view = (
|
|
391
|
+
InfrastructureFactory[TTrackingRecorder]
|
|
392
|
+
.construct(env=self._construct_env(name=projection_class.name, env=env))
|
|
393
|
+
.tracking_recorder(view_class)
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
# Construct the projection using the materialised view.
|
|
397
|
+
self.projection = projection_class(view=self.view)
|
|
398
|
+
|
|
399
|
+
super().__init__(
|
|
400
|
+
projection=self.projection,
|
|
401
|
+
application_class=application_class,
|
|
402
|
+
tracking_recorder=self.view,
|
|
403
|
+
topics=self.projection.topics,
|
|
404
|
+
env=env,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
class EventSourcedProjectionRunner(
|
|
409
|
+
BaseProjectionRunner[TApplication], Generic[TApplication, TEventSourcedProjection]
|
|
410
|
+
):
|
|
411
|
+
def __init__(
|
|
412
|
+
self,
|
|
413
|
+
*,
|
|
414
|
+
application_class: type[TApplication],
|
|
415
|
+
projection_class: type[TEventSourcedProjection],
|
|
416
|
+
env: EnvType | None = None,
|
|
417
|
+
):
|
|
418
|
+
self.projection: TEventSourcedProjection = projection_class(
|
|
419
|
+
env=self._construct_env(name=projection_class.name, env=env)
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
super().__init__(
|
|
423
|
+
projection=self.projection,
|
|
424
|
+
application_class=application_class,
|
|
425
|
+
tracking_recorder=self.projection.recorder,
|
|
426
|
+
topics=self.projection.topics,
|
|
427
|
+
env=env,
|
|
428
|
+
)
|
eventsourcing/sqlite.py
CHANGED
|
@@ -472,6 +472,7 @@ class SQLiteApplicationRecorder(
|
|
|
472
472
|
def subscribe(
|
|
473
473
|
self, gt: int | None = None, topics: Sequence[str] = ()
|
|
474
474
|
) -> Subscription[ApplicationRecorder]:
|
|
475
|
+
"""This method is not implemented on this class."""
|
|
475
476
|
msg = f"The {type(self).__qualname__} recorder does not support subscriptions"
|
|
476
477
|
raise NotImplementedError(msg)
|
|
477
478
|
|
eventsourcing/system.py
CHANGED
|
@@ -6,26 +6,25 @@ import traceback
|
|
|
6
6
|
from abc import ABC, abstractmethod
|
|
7
7
|
from collections import defaultdict
|
|
8
8
|
from queue import Full, Queue
|
|
9
|
+
from types import FrameType, ModuleType
|
|
9
10
|
from typing import TYPE_CHECKING, Any, ClassVar, Optional, Union, cast
|
|
10
11
|
|
|
12
|
+
from eventsourcing.projection import EventSourcedProjection
|
|
13
|
+
|
|
11
14
|
if TYPE_CHECKING:
|
|
12
15
|
from collections.abc import Iterable, Iterator, Sequence
|
|
13
|
-
from types import FrameType, ModuleType
|
|
14
16
|
|
|
15
17
|
from typing_extensions import Self
|
|
16
18
|
|
|
17
19
|
from eventsourcing.application import (
|
|
18
20
|
Application,
|
|
19
21
|
NotificationLog,
|
|
20
|
-
ProcessingEvent,
|
|
21
22
|
ProgrammingError,
|
|
22
23
|
Section,
|
|
23
24
|
TApplication,
|
|
24
25
|
)
|
|
25
|
-
from eventsourcing.dispatch import singledispatchmethod
|
|
26
26
|
from eventsourcing.domain import DomainEventProtocol, MutableOrImmutableAggregate
|
|
27
27
|
from eventsourcing.persistence import (
|
|
28
|
-
IntegrityError,
|
|
29
28
|
Mapper,
|
|
30
29
|
Notification,
|
|
31
30
|
ProcessRecorder,
|
|
@@ -52,29 +51,25 @@ class RecordingEvent:
|
|
|
52
51
|
ConvertingJob = Optional[Union[RecordingEvent, list[Notification]]]
|
|
53
52
|
|
|
54
53
|
|
|
55
|
-
class Follower(
|
|
56
|
-
"""Extends the :class:`~eventsourcing.
|
|
57
|
-
by
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
class Follower(EventSourcedProjection):
|
|
55
|
+
"""Extends the :class:`~eventsourcing.projection.EventSourcedProjection` class
|
|
56
|
+
by pulling notification objects from its notification log readers, by converting
|
|
57
|
+
the notification objects to domain events and tracking objects and by processing
|
|
58
|
+
the reconstructed domain event objects.
|
|
60
59
|
"""
|
|
61
60
|
|
|
62
|
-
follow_topics: ClassVar[Sequence[str]] = []
|
|
63
61
|
pull_section_size = 10
|
|
64
62
|
|
|
63
|
+
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
64
|
+
super().__init_subclass__(**kwargs)
|
|
65
|
+
# for backwards compatibility, set "topics" if has "follow_topics".
|
|
66
|
+
cls.topics = getattr(cls, "follow_topics", cls.topics)
|
|
67
|
+
|
|
65
68
|
def __init__(self, env: EnvType | None = None) -> None:
|
|
66
69
|
super().__init__(env)
|
|
67
70
|
self.readers: dict[str, NotificationLogReader] = {}
|
|
68
71
|
self.mappers: dict[str, Mapper] = {}
|
|
69
|
-
self.recorder: ProcessRecorder
|
|
70
72
|
self.is_threading_enabled = False
|
|
71
|
-
self.processing_lock = threading.Lock()
|
|
72
|
-
|
|
73
|
-
def construct_recorder(self) -> ProcessRecorder:
|
|
74
|
-
"""Constructs and returns a :class:`~eventsourcing.persistence.ProcessRecorder`
|
|
75
|
-
for the application to use as its application recorder.
|
|
76
|
-
"""
|
|
77
|
-
return self.factory.process_recorder()
|
|
78
73
|
|
|
79
74
|
def follow(self, name: str, log: NotificationLog) -> None:
|
|
80
75
|
"""Constructs a notification log reader and a mapper for
|
|
@@ -107,6 +102,12 @@ class Follower(Application):
|
|
|
107
102
|
):
|
|
108
103
|
self.process_event(domain_event, tracking)
|
|
109
104
|
|
|
105
|
+
def process_event(
|
|
106
|
+
self, domain_event: DomainEventProtocol, tracking: Tracking
|
|
107
|
+
) -> None:
|
|
108
|
+
with self.processing_lock:
|
|
109
|
+
super().process_event(domain_event, tracking)
|
|
110
|
+
|
|
110
111
|
def pull_notifications(
|
|
111
112
|
self,
|
|
112
113
|
leader_name: str,
|
|
@@ -121,15 +122,15 @@ class Follower(Application):
|
|
|
121
122
|
return self.readers[leader_name].select(
|
|
122
123
|
start=start,
|
|
123
124
|
stop=stop,
|
|
124
|
-
topics=self.
|
|
125
|
+
topics=self.topics,
|
|
125
126
|
inclusive_of_start=inclusive_of_start,
|
|
126
127
|
)
|
|
127
128
|
|
|
128
129
|
def filter_received_notifications(
|
|
129
130
|
self, notifications: list[Notification]
|
|
130
131
|
) -> list[Notification]:
|
|
131
|
-
if self.
|
|
132
|
-
return [n for n in notifications if n.topic in self.
|
|
132
|
+
if self.topics:
|
|
133
|
+
return [n for n in notifications if n.topic in self.topics]
|
|
133
134
|
return notifications
|
|
134
135
|
|
|
135
136
|
def convert_notifications(
|
|
@@ -151,64 +152,6 @@ class Follower(Application):
|
|
|
151
152
|
processing_jobs.append((domain_event, tracking))
|
|
152
153
|
return processing_jobs
|
|
153
154
|
|
|
154
|
-
# @retry(IntegrityError, max_attempts=50000, wait=0.01)
|
|
155
|
-
def process_event(
|
|
156
|
-
self, domain_event: DomainEventProtocol, tracking: Tracking
|
|
157
|
-
) -> None:
|
|
158
|
-
"""Calls :func:`~eventsourcing.system.Follower.policy` method with
|
|
159
|
-
the given :class:`~eventsourcing.domain.AggregateEvent` and a
|
|
160
|
-
new :class:`~eventsourcing.application.ProcessingEvent` created from
|
|
161
|
-
the given :class:`~eventsourcing.persistence.Tracking` object.
|
|
162
|
-
|
|
163
|
-
The policy will collect any new aggregate events on the process
|
|
164
|
-
event object.
|
|
165
|
-
|
|
166
|
-
After the policy method returns, the process event object will
|
|
167
|
-
then be recorded by calling
|
|
168
|
-
:func:`~eventsourcing.application.Application.record`, which
|
|
169
|
-
will return new notifications.
|
|
170
|
-
|
|
171
|
-
After calling
|
|
172
|
-
:func:`~eventsourcing.application.Application.take_snapshots`,
|
|
173
|
-
the new notifications are passed to the
|
|
174
|
-
:func:`~eventsourcing.application.Application.notify` method.
|
|
175
|
-
"""
|
|
176
|
-
processing_event = ProcessingEvent(tracking=tracking)
|
|
177
|
-
with self.processing_lock:
|
|
178
|
-
self.policy(domain_event, processing_event)
|
|
179
|
-
try:
|
|
180
|
-
recordings = self._record(processing_event)
|
|
181
|
-
except IntegrityError:
|
|
182
|
-
if self.recorder.has_tracking_id(
|
|
183
|
-
tracking.application_name,
|
|
184
|
-
tracking.notification_id,
|
|
185
|
-
):
|
|
186
|
-
pass
|
|
187
|
-
else:
|
|
188
|
-
raise
|
|
189
|
-
else:
|
|
190
|
-
self._take_snapshots(processing_event)
|
|
191
|
-
self.notify(processing_event.events)
|
|
192
|
-
self._notify(recordings)
|
|
193
|
-
|
|
194
|
-
@singledispatchmethod
|
|
195
|
-
def policy(
|
|
196
|
-
self,
|
|
197
|
-
domain_event: DomainEventProtocol,
|
|
198
|
-
processing_event: ProcessingEvent,
|
|
199
|
-
) -> None:
|
|
200
|
-
"""Abstract domain event processing policy method. Must be
|
|
201
|
-
implemented by event processing applications. When
|
|
202
|
-
processing the given domain event, event processing
|
|
203
|
-
applications must use the :func:`~ProcessingEvent.collect_events`
|
|
204
|
-
method of the given process event object (instead of
|
|
205
|
-
the application's :func:`~eventsourcing.application.Application.save`
|
|
206
|
-
method) to collect pending events from changed aggregates,
|
|
207
|
-
so that the new domain events will be recorded atomically
|
|
208
|
-
with tracking information about the position of the given
|
|
209
|
-
domain event's notification.
|
|
210
|
-
"""
|
|
211
|
-
|
|
212
155
|
|
|
213
156
|
class RecordingEventReceiver(ABC):
|
|
214
157
|
"""Abstract base class for objects that may receive recording events."""
|
|
@@ -279,8 +222,8 @@ class System:
|
|
|
279
222
|
pipes: Iterable[Iterable[type[Application]]],
|
|
280
223
|
):
|
|
281
224
|
# Remember the caller frame's module, so that we might identify a topic.
|
|
282
|
-
caller_frame = cast(
|
|
283
|
-
module = cast(
|
|
225
|
+
caller_frame = cast(FrameType, inspect.currentframe()).f_back
|
|
226
|
+
module = cast(ModuleType, inspect.getmodule(caller_frame))
|
|
284
227
|
type(self).__caller_modules[id(self)] = module # noqa: SLF001
|
|
285
228
|
|
|
286
229
|
# Build nodes and edges.
|
|
@@ -629,9 +572,9 @@ class NewSingleThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
629
572
|
follower = self.apps[follower_name]
|
|
630
573
|
assert isinstance(follower, Follower)
|
|
631
574
|
if (
|
|
632
|
-
follower.
|
|
575
|
+
follower.topics
|
|
633
576
|
and recording.notification.topic
|
|
634
|
-
not in follower.
|
|
577
|
+
not in follower.topics
|
|
635
578
|
):
|
|
636
579
|
continue
|
|
637
580
|
follower.process_event(
|
|
@@ -1076,9 +1019,8 @@ class ConvertingThread(threading.Thread):
|
|
|
1076
1019
|
recording_event = recording_event_or_notifications
|
|
1077
1020
|
for recording in recording_event.recordings:
|
|
1078
1021
|
if (
|
|
1079
|
-
self.follower.
|
|
1080
|
-
and recording.notification.topic
|
|
1081
|
-
not in self.follower.follow_topics
|
|
1022
|
+
self.follower.topics
|
|
1023
|
+
and recording.notification.topic not in self.follower.topics
|
|
1082
1024
|
):
|
|
1083
1025
|
continue
|
|
1084
1026
|
tracking = Tracking(
|
|
@@ -450,8 +450,8 @@ class ApplicationRecorderTestCase(TestCase, ABC, Generic[_TApplicationRecorder])
|
|
|
450
450
|
num_writers = 10
|
|
451
451
|
num_writes_per_writer = 100
|
|
452
452
|
num_events_per_write = 100
|
|
453
|
-
reader_sleep = 0.
|
|
454
|
-
writer_sleep = 0.
|
|
453
|
+
reader_sleep = 0.0001
|
|
454
|
+
writer_sleep = 0.0001
|
|
455
455
|
|
|
456
456
|
def insert_events() -> None:
|
|
457
457
|
thread_id = get_ident()
|
|
@@ -1,33 +1,32 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: eventsourcing
|
|
3
|
-
Version: 9.4.
|
|
3
|
+
Version: 9.4.1
|
|
4
4
|
Summary: Event sourcing in Python
|
|
5
|
-
License: BSD
|
|
5
|
+
License: BSD-3-Clause
|
|
6
6
|
Keywords: event sourcing,event store,domain driven design,domain-driven design,ddd,cqrs,cqs
|
|
7
7
|
Author: John Bywater
|
|
8
8
|
Author-email: john.bywater@appropriatesoftware.net
|
|
9
|
-
Requires-Python: >=3.9
|
|
10
|
-
Classifier: Development Status ::
|
|
9
|
+
Requires-Python: >=3.9.2
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
11
|
Classifier: Intended Audience :: Developers
|
|
12
12
|
Classifier: Intended Audience :: Education
|
|
13
13
|
Classifier: Intended Audience :: Science/Research
|
|
14
14
|
Classifier: License :: OSI Approved :: BSD License
|
|
15
|
-
Classifier: License :: Other/Proprietary License
|
|
16
15
|
Classifier: Operating System :: OS Independent
|
|
17
|
-
Classifier: Programming Language :: Python
|
|
18
16
|
Classifier: Programming Language :: Python :: 3
|
|
19
17
|
Classifier: Programming Language :: Python :: 3.10
|
|
20
18
|
Classifier: Programming Language :: Python :: 3.11
|
|
21
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
22
20
|
Classifier: Programming Language :: Python :: 3.13
|
|
23
21
|
Classifier: Programming Language :: Python :: 3.9
|
|
22
|
+
Classifier: Programming Language :: Python
|
|
24
23
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
24
|
Provides-Extra: crypto
|
|
26
25
|
Provides-Extra: cryptography
|
|
27
26
|
Provides-Extra: postgres
|
|
28
|
-
Requires-Dist: cryptography (>=44.0,<
|
|
29
|
-
Requires-Dist: psycopg[pool] (
|
|
30
|
-
Requires-Dist: pycryptodome (>=3.22,<
|
|
27
|
+
Requires-Dist: cryptography (>=44.0,<45.0) ; extra == "cryptography"
|
|
28
|
+
Requires-Dist: psycopg[pool] (>=3.2,<3.3) ; extra == "postgres"
|
|
29
|
+
Requires-Dist: pycryptodome (>=3.22,<4.0) ; extra == "crypto"
|
|
31
30
|
Requires-Dist: typing_extensions
|
|
32
31
|
Project-URL: Homepage, https://github.com/pyeventsourcing/eventsourcing
|
|
33
32
|
Project-URL: Repository, https://github.com/pyeventsourcing/eventsourcing
|
|
@@ -43,7 +42,10 @@ Description-Content-Type: text/markdown
|
|
|
43
42
|
|
|
44
43
|
# Event Sourcing in Python
|
|
45
44
|
|
|
46
|
-
|
|
45
|
+
This project is a comprehensive Python library for implementing event sourcing, a design pattern where all
|
|
46
|
+
changes to application state are stored as a sequence of events. This library provides a solid foundation
|
|
47
|
+
for building event-sourced applications in Python, with a focus on reliability, performance, and developer
|
|
48
|
+
experience. Please [read the docs](https://eventsourcing.readthedocs.io/). See also [extension projects](https://github.com/pyeventsourcing).
|
|
47
49
|
|
|
48
50
|
*"totally amazing and a pleasure to use"*
|
|
49
51
|
|
|
@@ -51,8 +53,6 @@ A library for event sourcing in Python.
|
|
|
51
53
|
|
|
52
54
|
*"a huge help and time saver"*
|
|
53
55
|
|
|
54
|
-
Please [read the docs](https://eventsourcing.readthedocs.io/). See also [extension projects](https://github.com/pyeventsourcing).
|
|
55
|
-
|
|
56
56
|
|
|
57
57
|
## Installation
|
|
58
58
|
|
|
@@ -8,19 +8,19 @@ eventsourcing/domain.py,sha256=bIELG5g8O4KhBBJMa9A0YeUe9EIuHul4LvCvGBuUhls,62962
|
|
|
8
8
|
eventsourcing/interface.py,sha256=-VLoqcd9a0PXpD_Bv0LjCiG21xLREG6tXK6phgtShOw,5035
|
|
9
9
|
eventsourcing/persistence.py,sha256=y_1o3LNi9tkOTqkvjgsGF4un4XPXEgxzt0Iwhk7UzEI,46340
|
|
10
10
|
eventsourcing/popo.py,sha256=xZD6mig7bVwAoHe-UdraXvuu2iL5a8b2b41cEcBHlBU,9642
|
|
11
|
-
eventsourcing/postgres.py,sha256=
|
|
12
|
-
eventsourcing/projection.py,sha256
|
|
11
|
+
eventsourcing/postgres.py,sha256=LGEsfAN_cUOL6xX0NT4gxVcu2Btma95WLqtYcEzdPSA,37757
|
|
12
|
+
eventsourcing/projection.py,sha256=2qkagy2o1sts22LqndYZzsDV7AJK28oGkPrFH1ECnYM,15069
|
|
13
13
|
eventsourcing/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
-
eventsourcing/sqlite.py,sha256=
|
|
15
|
-
eventsourcing/system.py,sha256=
|
|
14
|
+
eventsourcing/sqlite.py,sha256=1DRQgDE1S7lz7Mnz9QH6WZ4luMjI9tof1YpH5qrK9u8,22076
|
|
15
|
+
eventsourcing/system.py,sha256=WCfuSc45A9A1fFO7zpDum_ddh4pU7x-vEcpVZ_ycAyE,44358
|
|
16
16
|
eventsourcing/tests/__init__.py,sha256=FtOyuj-L-oSisYeByTIrnUw-XzsctSbq76XmjPy5fMc,102
|
|
17
17
|
eventsourcing/tests/application.py,sha256=pAn9Cugp_1rjOtj_nruGhh7PxdrQWibDlrnOAlOKXwo,20614
|
|
18
18
|
eventsourcing/tests/domain.py,sha256=yN-F6gMRumeX6nIXIcZGxAR3RrUslzmEMM8JksnkI8Q,3227
|
|
19
|
-
eventsourcing/tests/persistence.py,sha256=
|
|
19
|
+
eventsourcing/tests/persistence.py,sha256=jy_aMQwRGKQKohw8Ji7oBf1aFIdcHLHiSrHxH6yHvzw,58654
|
|
20
20
|
eventsourcing/tests/postgres_utils.py,sha256=0ywklGp6cXZ5PmV8ANVkwSHsZZCl5zTmOk7iG-RmrCE,1548
|
|
21
21
|
eventsourcing/utils.py,sha256=pOnczXzaE5q7UbQbPmgcpWaP660fsmfiDJs6Gmo8QCM,8558
|
|
22
|
-
eventsourcing-9.4.
|
|
23
|
-
eventsourcing-9.4.
|
|
24
|
-
eventsourcing-9.4.
|
|
25
|
-
eventsourcing-9.4.
|
|
26
|
-
eventsourcing-9.4.
|
|
22
|
+
eventsourcing-9.4.1.dist-info/AUTHORS,sha256=8aHOM4UbNZcKlD-cHpFRcM6RWyCqtwtxRev6DeUgVRs,137
|
|
23
|
+
eventsourcing-9.4.1.dist-info/LICENSE,sha256=CQEQzcZO8AWXL5i3hIo4yVKrYjh2FBz6hCM7kpXWpw4,1512
|
|
24
|
+
eventsourcing-9.4.1.dist-info/METADATA,sha256=c_uEtz47qhAkUkxw8RE5xfPV5K4GufPon70MObdblXw,9959
|
|
25
|
+
eventsourcing-9.4.1.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
|
|
26
|
+
eventsourcing-9.4.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|