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
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import os
|
|
5
|
+
import threading
|
|
6
|
+
import weakref
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from collections.abc import Iterator, Sequence
|
|
9
|
+
from threading import Event, Thread
|
|
10
|
+
from traceback import format_exc
|
|
11
|
+
from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast, get_origin
|
|
12
|
+
from warnings import warn
|
|
13
|
+
|
|
14
|
+
from eventsourcing.application import Application, ProcessingEvent
|
|
15
|
+
from eventsourcing.dcb.api import DCBQuery, DCBQueryItem
|
|
16
|
+
from eventsourcing.dcb.application import DCBApplication
|
|
17
|
+
from eventsourcing.dcb.domain import Decision, Tagged
|
|
18
|
+
from eventsourcing.dispatch import singledispatchmethod
|
|
19
|
+
from eventsourcing.domain import DomainEventProtocol, TAggregateID
|
|
20
|
+
from eventsourcing.persistence import (
|
|
21
|
+
InfrastructureFactory,
|
|
22
|
+
Mapper,
|
|
23
|
+
ProcessRecorder,
|
|
24
|
+
Tracking,
|
|
25
|
+
TrackingRecorder,
|
|
26
|
+
TTrackingRecorder,
|
|
27
|
+
WaitInterruptedError,
|
|
28
|
+
)
|
|
29
|
+
from eventsourcing.utils import Environment, EnvType
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from types import TracebackType
|
|
33
|
+
|
|
34
|
+
from typing_extensions import Self
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ApplicationSubscription(
|
|
38
|
+
Iterator[tuple[DomainEventProtocol[TAggregateID], Tracking]]
|
|
39
|
+
):
|
|
40
|
+
"""An iterator that yields all domain events recorded in an application
|
|
41
|
+
sequence that have notification IDs greater than a given value. The iterator
|
|
42
|
+
will block when all recorded domain events have been yielded, and then
|
|
43
|
+
continue when new events are recorded. Domain events are returned along
|
|
44
|
+
with tracking objects that identify the position in the application sequence.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
app: Application[TAggregateID],
|
|
50
|
+
gt: int | None = None,
|
|
51
|
+
topics: Sequence[str] = (),
|
|
52
|
+
):
|
|
53
|
+
"""
|
|
54
|
+
Starts a subscription to application's recorder.
|
|
55
|
+
"""
|
|
56
|
+
self.name = app.name
|
|
57
|
+
self.recorder = app.recorder
|
|
58
|
+
self.mapper: Mapper[TAggregateID] = app.mapper
|
|
59
|
+
self.subscription = self.recorder.subscribe(gt=gt, topics=topics)
|
|
60
|
+
|
|
61
|
+
def stop(self) -> None:
|
|
62
|
+
"""Stops the subscription to the application's recorder."""
|
|
63
|
+
self.subscription.stop()
|
|
64
|
+
|
|
65
|
+
def __enter__(self) -> Self:
|
|
66
|
+
"""Calls __enter__ on the stored event subscription."""
|
|
67
|
+
self.subscription.__enter__()
|
|
68
|
+
return self
|
|
69
|
+
|
|
70
|
+
def __exit__(self, *args: object, **kwargs: Any) -> None:
|
|
71
|
+
"""Calls __exit__ on the stored event subscription."""
|
|
72
|
+
self.subscription.__exit__(*args, **kwargs)
|
|
73
|
+
|
|
74
|
+
def __iter__(self) -> Self:
|
|
75
|
+
return self
|
|
76
|
+
|
|
77
|
+
def __next__(self) -> tuple[DomainEventProtocol[TAggregateID], Tracking]:
|
|
78
|
+
"""Returns the next stored event from subscription to the application's
|
|
79
|
+
recorder. Constructs a tracking object that identifies the position of
|
|
80
|
+
the event in the application sequence. Constructs a domain event object
|
|
81
|
+
from the stored event object using the application's mapper. Returns a
|
|
82
|
+
tuple of the domain event object and the tracking object.
|
|
83
|
+
"""
|
|
84
|
+
notification = next(self.subscription)
|
|
85
|
+
tracking = Tracking(self.name, notification.id)
|
|
86
|
+
domain_event = self.mapper.to_domain_event(notification)
|
|
87
|
+
return domain_event, tracking
|
|
88
|
+
|
|
89
|
+
def __del__(self) -> None:
|
|
90
|
+
"""Stops the stored event subscription."""
|
|
91
|
+
with contextlib.suppress(AttributeError):
|
|
92
|
+
self.stop()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class DCBApplicationSubscription(Iterator[tuple[Tagged[Decision], Tracking]]):
|
|
96
|
+
"""An iterator that yields all tagged decisions recorded in an application
|
|
97
|
+
sequence that have sequence numbers greater than a given value. The iterator
|
|
98
|
+
will block when all tagged decisions have been yielded, and then
|
|
99
|
+
continue when new ones are recorded. Tagged decisions are returned along
|
|
100
|
+
with tracking objects that identify the position in the application sequence.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
def __init__(
|
|
104
|
+
self,
|
|
105
|
+
app: DCBApplication,
|
|
106
|
+
gt: int | None = None,
|
|
107
|
+
topics: Sequence[str] = (),
|
|
108
|
+
):
|
|
109
|
+
"""
|
|
110
|
+
Starts a subscription to application's recorder.
|
|
111
|
+
"""
|
|
112
|
+
self.name = app.name
|
|
113
|
+
self.recorder = app.recorder
|
|
114
|
+
self.mapper = app.mapper
|
|
115
|
+
self.subscription = self.recorder.subscribe(
|
|
116
|
+
query=DCBQuery(items=[DCBQueryItem(types=list(topics))]),
|
|
117
|
+
after=gt,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def stop(self) -> None:
|
|
121
|
+
"""Stops the subscription to the application's recorder."""
|
|
122
|
+
self.subscription.stop()
|
|
123
|
+
|
|
124
|
+
def __enter__(self) -> Self:
|
|
125
|
+
"""Calls __enter__ on the stored event subscription."""
|
|
126
|
+
self.subscription.__enter__()
|
|
127
|
+
return self
|
|
128
|
+
|
|
129
|
+
def __exit__(self, *args: object, **kwargs: Any) -> None:
|
|
130
|
+
"""Calls __exit__ on the stored event subscription."""
|
|
131
|
+
self.subscription.__exit__(*args, **kwargs)
|
|
132
|
+
|
|
133
|
+
def __iter__(self) -> Self:
|
|
134
|
+
return self
|
|
135
|
+
|
|
136
|
+
def __next__(self) -> tuple[Tagged[Decision], Tracking]:
|
|
137
|
+
"""Returns the next stored event from subscription to the application's
|
|
138
|
+
recorder. Constructs a tracking object that identifies the position of
|
|
139
|
+
the event in the application sequence. Constructs a domain event object
|
|
140
|
+
from the stored event object using the application's mapper. Returns a
|
|
141
|
+
tuple of the domain event object and the tracking object.
|
|
142
|
+
"""
|
|
143
|
+
sequenced_event = next(self.subscription)
|
|
144
|
+
tracking = Tracking(self.name, sequenced_event.position)
|
|
145
|
+
tagged_decision = self.mapper.to_domain_event(sequenced_event.event)
|
|
146
|
+
return tagged_decision, tracking
|
|
147
|
+
|
|
148
|
+
def __del__(self) -> None:
|
|
149
|
+
"""Stops the stored event subscription."""
|
|
150
|
+
with contextlib.suppress(AttributeError):
|
|
151
|
+
self.stop()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class Projection(ABC, Generic[TTrackingRecorder]):
|
|
155
|
+
name: str = ""
|
|
156
|
+
"""
|
|
157
|
+
Name of projection, used to pick prefixed environment
|
|
158
|
+
variables and define database table names.
|
|
159
|
+
"""
|
|
160
|
+
topics: tuple[str, ...] = ()
|
|
161
|
+
"""
|
|
162
|
+
Event topics, used to filter events in database when subscribing to an application.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
166
|
+
if "name" not in cls.__dict__:
|
|
167
|
+
cls.name = cls.__name__
|
|
168
|
+
|
|
169
|
+
def __init__(
|
|
170
|
+
self,
|
|
171
|
+
view: TTrackingRecorder,
|
|
172
|
+
):
|
|
173
|
+
"""Initialises the view property with the given view argument."""
|
|
174
|
+
self._view = view
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def view(self) -> TTrackingRecorder:
|
|
178
|
+
"""Materialised view of an event-sourced application."""
|
|
179
|
+
return self._view
|
|
180
|
+
|
|
181
|
+
@singledispatchmethod
|
|
182
|
+
@abstractmethod
|
|
183
|
+
def process_event(self, domain_event: Any, tracking: Tracking) -> None:
|
|
184
|
+
"""Process a domain event and track it."""
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class EventSourcedProjection(Application[TAggregateID], ABC):
|
|
188
|
+
"""Extends the :py:class:`~eventsourcing.application.Application` class
|
|
189
|
+
by using a process recorder as its application recorder, and by
|
|
190
|
+
processing domain events through its :py:func:`policy` method.
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
topics: Sequence[str] = ()
|
|
194
|
+
|
|
195
|
+
def __init__(self, env: EnvType | None = None) -> None:
|
|
196
|
+
super().__init__(env)
|
|
197
|
+
self.recorder: ProcessRecorder
|
|
198
|
+
self.processing_lock = threading.Lock()
|
|
199
|
+
|
|
200
|
+
def construct_recorder(self) -> ProcessRecorder:
|
|
201
|
+
"""Constructs and returns a :class:`~eventsourcing.persistence.ProcessRecorder`
|
|
202
|
+
for the application to use as its application recorder.
|
|
203
|
+
"""
|
|
204
|
+
return self.factory.process_recorder()
|
|
205
|
+
|
|
206
|
+
def process_event(
|
|
207
|
+
self, domain_event: DomainEventProtocol[TAggregateID], tracking: Tracking
|
|
208
|
+
) -> None:
|
|
209
|
+
"""Calls :func:`~eventsourcing.system.Follower.policy` method with the given
|
|
210
|
+
domain event and a new :class:`~eventsourcing.application.ProcessingEvent`
|
|
211
|
+
constructed with the given tracking object.
|
|
212
|
+
|
|
213
|
+
The policy method should collect any new aggregate events on the process
|
|
214
|
+
event object.
|
|
215
|
+
|
|
216
|
+
After the policy method returns, the processing event object will be recorded
|
|
217
|
+
by calling :py:func:`~eventsourcing.application.Application._record`,
|
|
218
|
+
which then returns list of :py:class:`~eventsourcing.persistence.Recording`.
|
|
219
|
+
|
|
220
|
+
After calling :func:`~eventsourcing.application.Application._take_snapshots`,
|
|
221
|
+
the recordings are passed in a call to
|
|
222
|
+
:py:func:`~eventsourcing.application.Application._notify`.
|
|
223
|
+
"""
|
|
224
|
+
processing_event = ProcessingEvent[TAggregateID](tracking=tracking)
|
|
225
|
+
self.policy(domain_event, processing_event)
|
|
226
|
+
recordings = self._record(processing_event)
|
|
227
|
+
self._take_snapshots(processing_event)
|
|
228
|
+
self.notify(processing_event.events)
|
|
229
|
+
self._notify(recordings)
|
|
230
|
+
|
|
231
|
+
@singledispatchmethod
|
|
232
|
+
def policy(
|
|
233
|
+
self,
|
|
234
|
+
domain_event: DomainEventProtocol[TAggregateID],
|
|
235
|
+
processing_event: ProcessingEvent[TAggregateID],
|
|
236
|
+
) -> None:
|
|
237
|
+
"""Abstract domain event processing policy method. Must be
|
|
238
|
+
implemented by event processing applications. When
|
|
239
|
+
processing the given domain event, event processing
|
|
240
|
+
applications must use the :func:`~ProcessingEvent.collect_events`
|
|
241
|
+
method of the given :py:class:`~ProcessingEvent` object (not
|
|
242
|
+
the application's :func:`~eventsourcing.application.Application.save`
|
|
243
|
+
method) so that the new domain events will be recorded atomically
|
|
244
|
+
and uniquely with tracking information about the position of the processed
|
|
245
|
+
event in its application sequence.
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
TApplication = TypeVar("TApplication")
|
|
250
|
+
TEventSourcedProjection = TypeVar(
|
|
251
|
+
"TEventSourcedProjection", bound=EventSourcedProjection[Any]
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class BaseProjectionRunner(Generic[TApplication]):
|
|
256
|
+
def __init__(
|
|
257
|
+
self,
|
|
258
|
+
*,
|
|
259
|
+
projection: EventSourcedProjection[Any] | Projection[Any],
|
|
260
|
+
application_class: type[TApplication],
|
|
261
|
+
tracking_recorder: TrackingRecorder,
|
|
262
|
+
topics: Sequence[str],
|
|
263
|
+
env: EnvType | None = None,
|
|
264
|
+
) -> None:
|
|
265
|
+
self._projection = projection
|
|
266
|
+
self._is_interrupted = Event()
|
|
267
|
+
self._has_called_stop = False
|
|
268
|
+
|
|
269
|
+
self._tracking_recorder = tracking_recorder
|
|
270
|
+
|
|
271
|
+
# Construct and subscribe to the application.
|
|
272
|
+
self._subscription: ApplicationSubscription[Any] | DCBApplicationSubscription
|
|
273
|
+
# Do this for pyright (with the cast to TApplication below).
|
|
274
|
+
app: Any
|
|
275
|
+
# get_origin() because issubclass doesn't work with generic alias, and
|
|
276
|
+
# then 'or' with the class in case get_origin() returns None.
|
|
277
|
+
if issubclass(get_origin(application_class) or application_class, Application):
|
|
278
|
+
# cast() because that call to issubclass() doesn't narrow the type.
|
|
279
|
+
app = cast(type[Application[Any]], application_class)(env)
|
|
280
|
+
self.app_name = app.name
|
|
281
|
+
self._subscription = ApplicationSubscription(
|
|
282
|
+
app=app,
|
|
283
|
+
gt=self._tracking_recorder.max_tracking_id(app.name),
|
|
284
|
+
topics=topics,
|
|
285
|
+
)
|
|
286
|
+
elif issubclass(application_class, DCBApplication):
|
|
287
|
+
app = application_class(env)
|
|
288
|
+
self.app = app
|
|
289
|
+
self.app_name = app.name
|
|
290
|
+
self._subscription = DCBApplicationSubscription(
|
|
291
|
+
app=app,
|
|
292
|
+
gt=self._tracking_recorder.max_tracking_id(app.name),
|
|
293
|
+
topics=topics,
|
|
294
|
+
)
|
|
295
|
+
else: # pragma: no cover
|
|
296
|
+
msg = f"Unsupported application type: {application_class}"
|
|
297
|
+
raise TypeError(msg)
|
|
298
|
+
|
|
299
|
+
self.app = cast(TApplication, app)
|
|
300
|
+
|
|
301
|
+
# Start a thread to stop the subscription when the runner is interrupted.
|
|
302
|
+
self._thread_error: BaseException | None = None
|
|
303
|
+
self._stop_thread = Thread(
|
|
304
|
+
target=self._stop_subscription_when_stopping,
|
|
305
|
+
kwargs={
|
|
306
|
+
"subscription": self._subscription,
|
|
307
|
+
"is_stopping": self._is_interrupted,
|
|
308
|
+
},
|
|
309
|
+
)
|
|
310
|
+
self._stop_thread.start()
|
|
311
|
+
|
|
312
|
+
# Start a thread to iterate over the subscription.
|
|
313
|
+
self._processing_thread = Thread(
|
|
314
|
+
target=self._process_events_loop,
|
|
315
|
+
kwargs={
|
|
316
|
+
"subscription": self._subscription,
|
|
317
|
+
"projection": self._projection,
|
|
318
|
+
"is_stopping": self._is_interrupted,
|
|
319
|
+
"runner": weakref.ref(self),
|
|
320
|
+
},
|
|
321
|
+
)
|
|
322
|
+
self._processing_thread.start()
|
|
323
|
+
|
|
324
|
+
@property
|
|
325
|
+
def is_interrupted(self) -> Event:
|
|
326
|
+
return self._is_interrupted
|
|
327
|
+
|
|
328
|
+
@staticmethod
|
|
329
|
+
def _construct_env(name: str, env: EnvType | None = None) -> Environment:
|
|
330
|
+
"""Constructs environment from which projection will be configured."""
|
|
331
|
+
_env: dict[str, str] = {}
|
|
332
|
+
_env.update(os.environ)
|
|
333
|
+
if env is not None:
|
|
334
|
+
_env.update(env)
|
|
335
|
+
return Environment(name, _env)
|
|
336
|
+
|
|
337
|
+
def stop(self) -> None:
|
|
338
|
+
"""Sets the "interrupted" event."""
|
|
339
|
+
self._has_called_stop = True
|
|
340
|
+
self._is_interrupted.set()
|
|
341
|
+
|
|
342
|
+
@staticmethod
|
|
343
|
+
def _stop_subscription_when_stopping(
|
|
344
|
+
subscription: ApplicationSubscription[TAggregateID],
|
|
345
|
+
is_stopping: Event,
|
|
346
|
+
) -> None:
|
|
347
|
+
"""Stops the application subscription, which
|
|
348
|
+
will stop the event-processing thread.
|
|
349
|
+
"""
|
|
350
|
+
try:
|
|
351
|
+
is_stopping.wait()
|
|
352
|
+
finally:
|
|
353
|
+
is_stopping.set()
|
|
354
|
+
subscription.stop()
|
|
355
|
+
|
|
356
|
+
@staticmethod
|
|
357
|
+
def _process_events_loop(
|
|
358
|
+
subscription: ApplicationSubscription[TAggregateID],
|
|
359
|
+
projection: EventSourcedProjection[Any] | Projection[Any],
|
|
360
|
+
is_stopping: Event,
|
|
361
|
+
runner: weakref.ReferenceType[
|
|
362
|
+
ProjectionRunner[Application[Any], TrackingRecorder]
|
|
363
|
+
],
|
|
364
|
+
) -> None:
|
|
365
|
+
"""Iterates over the subscription and calls process_event()."""
|
|
366
|
+
try:
|
|
367
|
+
for domain_event, tracking in subscription:
|
|
368
|
+
projection.process_event(domain_event, tracking)
|
|
369
|
+
except BaseException as e:
|
|
370
|
+
_runner = runner() # get reference from weakref
|
|
371
|
+
if _runner is not None:
|
|
372
|
+
_runner._thread_error = e # noqa: SLF001
|
|
373
|
+
else:
|
|
374
|
+
msg = "ProjectionRunner was deleted before error could be assigned:\n"
|
|
375
|
+
msg += format_exc()
|
|
376
|
+
warn(
|
|
377
|
+
msg,
|
|
378
|
+
RuntimeWarning,
|
|
379
|
+
stacklevel=2,
|
|
380
|
+
)
|
|
381
|
+
finally:
|
|
382
|
+
is_stopping.set()
|
|
383
|
+
|
|
384
|
+
def run_forever(self, timeout: float | None = None) -> None:
|
|
385
|
+
"""Blocks until timeout, or until the runner is stopped or errors. Re-raises
|
|
386
|
+
any error otherwise exits normally
|
|
387
|
+
"""
|
|
388
|
+
if (
|
|
389
|
+
self._is_interrupted.wait(timeout=timeout)
|
|
390
|
+
and self._thread_error is not None
|
|
391
|
+
):
|
|
392
|
+
error = self._thread_error
|
|
393
|
+
self._thread_error = None
|
|
394
|
+
raise error from None
|
|
395
|
+
|
|
396
|
+
def wait(self, notification_id: int | None, timeout: float = 1.0) -> None:
|
|
397
|
+
"""Blocks until timeout, or until the materialised view has recorded a tracking
|
|
398
|
+
object that is greater than or equal to the given notification ID.
|
|
399
|
+
"""
|
|
400
|
+
try:
|
|
401
|
+
self._tracking_recorder.wait(
|
|
402
|
+
application_name=self.app_name,
|
|
403
|
+
notification_id=notification_id,
|
|
404
|
+
timeout=timeout,
|
|
405
|
+
interrupt=self._is_interrupted,
|
|
406
|
+
)
|
|
407
|
+
except WaitInterruptedError as e:
|
|
408
|
+
if self._thread_error:
|
|
409
|
+
error = self._thread_error
|
|
410
|
+
self._thread_error = None
|
|
411
|
+
raise error from None
|
|
412
|
+
if self._has_called_stop:
|
|
413
|
+
return
|
|
414
|
+
raise e from None
|
|
415
|
+
|
|
416
|
+
def __enter__(self) -> Self:
|
|
417
|
+
self._subscription.__enter__()
|
|
418
|
+
return self
|
|
419
|
+
|
|
420
|
+
def __exit__(
|
|
421
|
+
self,
|
|
422
|
+
exc_type: type[BaseException] | None,
|
|
423
|
+
exc_val: BaseException | None,
|
|
424
|
+
exc_tb: TracebackType | None,
|
|
425
|
+
) -> None:
|
|
426
|
+
"""Calls stop() and waits for the event-processing thread to exit."""
|
|
427
|
+
self.stop()
|
|
428
|
+
self._stop_thread.join()
|
|
429
|
+
self._subscription.__exit__(exc_type, exc_val, exc_tb)
|
|
430
|
+
self._processing_thread.join()
|
|
431
|
+
# TODO: Improve typing of application classes and type annotation for self.app
|
|
432
|
+
self.app.close() # pyright: ignore [reportAttributeAccessIssue]
|
|
433
|
+
if self._thread_error:
|
|
434
|
+
error = self._thread_error
|
|
435
|
+
self._thread_error = None
|
|
436
|
+
raise error
|
|
437
|
+
|
|
438
|
+
def __del__(self) -> None:
|
|
439
|
+
"""Calls stop()."""
|
|
440
|
+
with contextlib.suppress(AttributeError):
|
|
441
|
+
self.stop()
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
class ProjectionRunner(
|
|
445
|
+
BaseProjectionRunner[TApplication], Generic[TApplication, TTrackingRecorder]
|
|
446
|
+
):
|
|
447
|
+
def __init__(
|
|
448
|
+
self,
|
|
449
|
+
*,
|
|
450
|
+
application_class: type[TApplication],
|
|
451
|
+
projection_class: type[Projection[TTrackingRecorder]],
|
|
452
|
+
view_class: type[TTrackingRecorder],
|
|
453
|
+
env: EnvType | None = None,
|
|
454
|
+
):
|
|
455
|
+
"""Constructs application from given application class with given environment.
|
|
456
|
+
Also constructs a materialised view from given class using an infrastructure
|
|
457
|
+
factory constructed with an environment named after the projection. Also
|
|
458
|
+
constructs a projection with the constructed materialised view object.
|
|
459
|
+
Starts a subscription to application and, in a separate event-processing
|
|
460
|
+
thread, calls projection's process_event() method for each event and tracking
|
|
461
|
+
object pair received from the subscription.
|
|
462
|
+
"""
|
|
463
|
+
# Construct the materialised view using an infrastructure factory.
|
|
464
|
+
self.view = (
|
|
465
|
+
InfrastructureFactory[TTrackingRecorder]
|
|
466
|
+
.construct(env=self._construct_env(name=projection_class.name, env=env))
|
|
467
|
+
.tracking_recorder(view_class)
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
# Construct the projection using the materialised view.
|
|
471
|
+
self.projection = projection_class(view=self.view)
|
|
472
|
+
|
|
473
|
+
super().__init__(
|
|
474
|
+
projection=self.projection,
|
|
475
|
+
application_class=application_class,
|
|
476
|
+
tracking_recorder=self.view,
|
|
477
|
+
topics=self.projection.topics,
|
|
478
|
+
env=env,
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
class EventSourcedProjectionRunner(
|
|
483
|
+
BaseProjectionRunner[TApplication], Generic[TApplication, TEventSourcedProjection]
|
|
484
|
+
):
|
|
485
|
+
def __init__(
|
|
486
|
+
self,
|
|
487
|
+
*,
|
|
488
|
+
application_class: type[TApplication],
|
|
489
|
+
projection_class: type[TEventSourcedProjection],
|
|
490
|
+
env: EnvType | None = None,
|
|
491
|
+
):
|
|
492
|
+
self.projection: TEventSourcedProjection = projection_class(
|
|
493
|
+
env=self._construct_env(name=projection_class.name, env=env)
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
super().__init__(
|
|
497
|
+
projection=self.projection,
|
|
498
|
+
application_class=application_class,
|
|
499
|
+
tracking_recorder=self.projection.recorder,
|
|
500
|
+
topics=self.projection.topics,
|
|
501
|
+
env=env,
|
|
502
|
+
)
|
eventsourcing/py.typed
ADDED
|
File without changes
|