eventsourcing 9.4.0a7__py3-none-any.whl → 9.4.0a8__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 +3 -7
- eventsourcing/domain.py +346 -357
- eventsourcing/persistence.py +18 -24
- eventsourcing/popo.py +5 -1
- eventsourcing/postgres.py +41 -33
- eventsourcing/projection.py +22 -19
- eventsourcing/sqlite.py +5 -1
- eventsourcing/system.py +19 -11
- eventsourcing/tests/application.py +57 -49
- eventsourcing/tests/domain.py +8 -6
- eventsourcing/tests/persistence.py +162 -143
- eventsourcing/tests/postgres_utils.py +7 -8
- eventsourcing/utils.py +15 -10
- {eventsourcing-9.4.0a7.dist-info → eventsourcing-9.4.0a8.dist-info}/METADATA +1 -1
- eventsourcing-9.4.0a8.dist-info/RECORD +26 -0
- eventsourcing-9.4.0a7.dist-info/RECORD +0 -26
- {eventsourcing-9.4.0a7.dist-info → eventsourcing-9.4.0a8.dist-info}/AUTHORS +0 -0
- {eventsourcing-9.4.0a7.dist-info → eventsourcing-9.4.0a8.dist-info}/LICENSE +0 -0
- {eventsourcing-9.4.0a7.dist-info → eventsourcing-9.4.0a8.dist-info}/WHEEL +0 -0
eventsourcing/persistence.py
CHANGED
|
@@ -12,9 +12,10 @@ from queue import Queue
|
|
|
12
12
|
from threading import Condition, Event, Lock, Semaphore, Thread, Timer
|
|
13
13
|
from time import monotonic, sleep, time
|
|
14
14
|
from types import GenericAlias, ModuleType
|
|
15
|
-
from typing import TYPE_CHECKING, Any, Generic,
|
|
15
|
+
from typing import TYPE_CHECKING, Any, Generic, Union, cast
|
|
16
16
|
from uuid import UUID
|
|
17
|
-
|
|
17
|
+
|
|
18
|
+
from typing_extensions import TypeVar
|
|
18
19
|
|
|
19
20
|
from eventsourcing.domain import DomainEventProtocol, EventSourcingError
|
|
20
21
|
from eventsourcing.utils import (
|
|
@@ -284,15 +285,6 @@ class Mapper:
|
|
|
284
285
|
state=stored_state,
|
|
285
286
|
)
|
|
286
287
|
|
|
287
|
-
def from_domain_event(self, domain_event: DomainEventProtocol) -> StoredEvent:
|
|
288
|
-
warn(
|
|
289
|
-
"'from_domain_event()' is deprecated, use 'to_stored_event()' instead",
|
|
290
|
-
DeprecationWarning,
|
|
291
|
-
stacklevel=2,
|
|
292
|
-
)
|
|
293
|
-
|
|
294
|
-
return self.to_stored_event(domain_event)
|
|
295
|
-
|
|
296
288
|
def to_domain_event(self, stored_event: StoredEvent) -> DomainEventProtocol:
|
|
297
289
|
"""
|
|
298
290
|
Converts the given :class:`StoredEvent` to a domain event object.
|
|
@@ -508,16 +500,19 @@ class TrackingRecorder(Recorder, ABC):
|
|
|
508
500
|
"""
|
|
509
501
|
|
|
510
502
|
@abstractmethod
|
|
511
|
-
def has_tracking_id(
|
|
503
|
+
def has_tracking_id(
|
|
504
|
+
self, application_name: str, notification_id: int | None
|
|
505
|
+
) -> bool:
|
|
512
506
|
"""
|
|
513
507
|
Returns True if a tracking object with the given application name
|
|
514
|
-
and notification ID has been recorded,
|
|
508
|
+
and notification ID has been recorded, and True if given notification_id is
|
|
509
|
+
None, otherwise returns False.
|
|
515
510
|
"""
|
|
516
511
|
|
|
517
512
|
def wait(
|
|
518
513
|
self,
|
|
519
514
|
application_name: str,
|
|
520
|
-
notification_id: int,
|
|
515
|
+
notification_id: int | None,
|
|
521
516
|
timeout: float = 1.0,
|
|
522
517
|
interrupt: Event | None = None,
|
|
523
518
|
) -> None:
|
|
@@ -634,12 +629,10 @@ class EventStore:
|
|
|
634
629
|
)
|
|
635
630
|
|
|
636
631
|
|
|
637
|
-
|
|
638
|
-
"
|
|
632
|
+
TTrackingRecorder = TypeVar(
|
|
633
|
+
"TTrackingRecorder", bound=TrackingRecorder, default=TrackingRecorder
|
|
639
634
|
)
|
|
640
635
|
|
|
641
|
-
TTrackingRecorder = TypeVar("TTrackingRecorder", bound=TrackingRecorder)
|
|
642
|
-
|
|
643
636
|
|
|
644
637
|
class InfrastructureFactory(ABC, Generic[TTrackingRecorder]):
|
|
645
638
|
"""
|
|
@@ -658,14 +651,15 @@ class InfrastructureFactory(ABC, Generic[TTrackingRecorder]):
|
|
|
658
651
|
|
|
659
652
|
@classmethod
|
|
660
653
|
def construct(
|
|
661
|
-
cls: type[
|
|
662
|
-
|
|
654
|
+
cls: type[InfrastructureFactory[TTrackingRecorder]],
|
|
655
|
+
env: Environment | None = None,
|
|
656
|
+
) -> InfrastructureFactory[TTrackingRecorder]:
|
|
663
657
|
"""
|
|
664
658
|
Constructs concrete infrastructure factory for given
|
|
665
659
|
named application. Reads and resolves persistence
|
|
666
660
|
topic from environment variable 'PERSISTENCE_MODULE'.
|
|
667
661
|
"""
|
|
668
|
-
factory_cls: type[InfrastructureFactory[
|
|
662
|
+
factory_cls: type[InfrastructureFactory[TTrackingRecorder]]
|
|
669
663
|
if env is None:
|
|
670
664
|
env = Environment()
|
|
671
665
|
topic = (
|
|
@@ -684,7 +678,7 @@ class InfrastructureFactory(ABC, Generic[TTrackingRecorder]):
|
|
|
684
678
|
or "eventsourcing.popo"
|
|
685
679
|
)
|
|
686
680
|
try:
|
|
687
|
-
obj: type[InfrastructureFactory[
|
|
681
|
+
obj: type[InfrastructureFactory[TTrackingRecorder]] | ModuleType = (
|
|
688
682
|
resolve_topic(topic)
|
|
689
683
|
)
|
|
690
684
|
except TopicError as e:
|
|
@@ -697,7 +691,7 @@ class InfrastructureFactory(ABC, Generic[TTrackingRecorder]):
|
|
|
697
691
|
|
|
698
692
|
if isinstance(obj, ModuleType):
|
|
699
693
|
# Find the factory in the module.
|
|
700
|
-
factory_classes: list[type[InfrastructureFactory[
|
|
694
|
+
factory_classes: list[type[InfrastructureFactory[TTrackingRecorder]]] = []
|
|
701
695
|
for member in obj.__dict__.values():
|
|
702
696
|
if (
|
|
703
697
|
member is not InfrastructureFactory
|
|
@@ -724,7 +718,7 @@ class InfrastructureFactory(ABC, Generic[TTrackingRecorder]):
|
|
|
724
718
|
else:
|
|
725
719
|
msg = f"Not an infrastructure factory class or module: {topic}"
|
|
726
720
|
raise AssertionError(msg)
|
|
727
|
-
return
|
|
721
|
+
return factory_cls(env=env)
|
|
728
722
|
|
|
729
723
|
def __init__(self, env: Environment):
|
|
730
724
|
"""
|
eventsourcing/popo.py
CHANGED
|
@@ -219,7 +219,11 @@ class POPOTrackingRecorder(POPORecorder, TrackingRecorder):
|
|
|
219
219
|
with self._database_lock:
|
|
220
220
|
return self._max_tracking_ids[application_name]
|
|
221
221
|
|
|
222
|
-
def has_tracking_id(
|
|
222
|
+
def has_tracking_id(
|
|
223
|
+
self, application_name: str, notification_id: int | None
|
|
224
|
+
) -> bool:
|
|
225
|
+
if notification_id is None:
|
|
226
|
+
return True
|
|
223
227
|
with self._database_lock:
|
|
224
228
|
return notification_id in self._tracking_table[application_name]
|
|
225
229
|
|
eventsourcing/postgres.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import contextlib
|
|
3
4
|
import logging
|
|
4
5
|
from asyncio import CancelledError
|
|
5
6
|
from contextlib import contextmanager
|
|
@@ -180,6 +181,9 @@ class PostgresDatastore:
|
|
|
180
181
|
def __exit__(self, *args: object, **kwargs: Any) -> None:
|
|
181
182
|
self.close()
|
|
182
183
|
|
|
184
|
+
def __del__(self) -> None:
|
|
185
|
+
self.close()
|
|
186
|
+
|
|
183
187
|
|
|
184
188
|
class PostgresRecorder:
|
|
185
189
|
"""Base class for recorders that use PostgreSQL."""
|
|
@@ -194,9 +198,8 @@ class PostgresRecorder:
|
|
|
194
198
|
def construct_create_table_statements(self) -> list[str]:
|
|
195
199
|
return []
|
|
196
200
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
schema_prefix = schema_name + "."
|
|
201
|
+
def check_table_name_length(self, table_name: str) -> None:
|
|
202
|
+
schema_prefix = self.datastore.schema + "."
|
|
200
203
|
if table_name.startswith(schema_prefix):
|
|
201
204
|
unqualified_table_name = table_name[len(schema_prefix) :]
|
|
202
205
|
else:
|
|
@@ -219,7 +222,7 @@ class PostgresAggregateRecorder(PostgresRecorder, AggregateRecorder):
|
|
|
219
222
|
events_table_name: str = "stored_events",
|
|
220
223
|
):
|
|
221
224
|
super().__init__(datastore)
|
|
222
|
-
self.check_table_name_length(events_table_name
|
|
225
|
+
self.check_table_name_length(events_table_name)
|
|
223
226
|
self.events_table_name = events_table_name
|
|
224
227
|
# Index names can't be qualified names, but
|
|
225
228
|
# are created in the same schema as the table.
|
|
@@ -283,7 +286,7 @@ class PostgresAggregateRecorder(PostgresRecorder, AggregateRecorder):
|
|
|
283
286
|
|
|
284
287
|
def _insert_events(
|
|
285
288
|
self,
|
|
286
|
-
|
|
289
|
+
curs: Cursor[DictRow],
|
|
287
290
|
stored_events: list[StoredEvent],
|
|
288
291
|
**_: Any,
|
|
289
292
|
) -> None:
|
|
@@ -291,18 +294,18 @@ class PostgresAggregateRecorder(PostgresRecorder, AggregateRecorder):
|
|
|
291
294
|
|
|
292
295
|
def _insert_stored_events(
|
|
293
296
|
self,
|
|
294
|
-
|
|
297
|
+
curs: Cursor[DictRow],
|
|
295
298
|
stored_events: list[StoredEvent],
|
|
296
299
|
**_: Any,
|
|
297
300
|
) -> None:
|
|
298
301
|
# Only do something if there is something to do.
|
|
299
302
|
if len(stored_events) > 0:
|
|
300
|
-
self._lock_table(
|
|
303
|
+
self._lock_table(curs)
|
|
301
304
|
|
|
302
|
-
self._notify_channel(
|
|
305
|
+
self._notify_channel(curs)
|
|
303
306
|
|
|
304
307
|
# Insert events.
|
|
305
|
-
|
|
308
|
+
curs.executemany(
|
|
306
309
|
query=self.insert_events_statement,
|
|
307
310
|
params_seq=[
|
|
308
311
|
(
|
|
@@ -316,15 +319,15 @@ class PostgresAggregateRecorder(PostgresRecorder, AggregateRecorder):
|
|
|
316
319
|
returning="RETURNING" in self.insert_events_statement,
|
|
317
320
|
)
|
|
318
321
|
|
|
319
|
-
def _lock_table(self,
|
|
322
|
+
def _lock_table(self, curs: Cursor[DictRow]) -> None:
|
|
320
323
|
pass
|
|
321
324
|
|
|
322
|
-
def _notify_channel(self,
|
|
325
|
+
def _notify_channel(self, curs: Cursor[DictRow]) -> None:
|
|
323
326
|
pass
|
|
324
327
|
|
|
325
328
|
def _fetch_ids_after_insert_events(
|
|
326
329
|
self,
|
|
327
|
-
|
|
330
|
+
curs: Cursor[DictRow],
|
|
328
331
|
stored_events: list[StoredEvent],
|
|
329
332
|
**kwargs: Any,
|
|
330
333
|
) -> Sequence[int] | None:
|
|
@@ -479,7 +482,7 @@ class PostgresApplicationRecorder(PostgresAggregateRecorder, ApplicationRecorder
|
|
|
479
482
|
assert fetchone is not None
|
|
480
483
|
return fetchone["max"]
|
|
481
484
|
|
|
482
|
-
def _lock_table(self,
|
|
485
|
+
def _lock_table(self, curs: Cursor[DictRow]) -> None:
|
|
483
486
|
# Acquire "EXCLUSIVE" table lock, to serialize transactions that insert
|
|
484
487
|
# stored events, so that readers don't pass over gaps that are filled in
|
|
485
488
|
# later. We want each transaction that will be issued with notifications
|
|
@@ -501,23 +504,23 @@ class PostgresApplicationRecorder(PostgresAggregateRecorder, ApplicationRecorder
|
|
|
501
504
|
# https://stackoverflow.com/questions/45866187/guarantee-monotonicity-of
|
|
502
505
|
# -postgresql-serial-column-values-by-commit-order
|
|
503
506
|
for lock_statement in self.lock_table_statements:
|
|
504
|
-
|
|
507
|
+
curs.execute(lock_statement, prepare=True)
|
|
505
508
|
|
|
506
|
-
def _notify_channel(self,
|
|
507
|
-
|
|
509
|
+
def _notify_channel(self, curs: Cursor[DictRow]) -> None:
|
|
510
|
+
curs.execute("NOTIFY " + self.channel_name)
|
|
508
511
|
|
|
509
512
|
def _fetch_ids_after_insert_events(
|
|
510
513
|
self,
|
|
511
|
-
|
|
514
|
+
curs: Cursor[DictRow],
|
|
512
515
|
stored_events: list[StoredEvent],
|
|
513
516
|
**kwargs: Any,
|
|
514
517
|
) -> Sequence[int] | None:
|
|
515
518
|
notification_ids: list[int] = []
|
|
516
519
|
len_events = len(stored_events)
|
|
517
520
|
if len_events:
|
|
518
|
-
while
|
|
519
|
-
if
|
|
520
|
-
row =
|
|
521
|
+
while curs.nextset() and len(notification_ids) != len_events:
|
|
522
|
+
if curs.statusmessage and curs.statusmessage.startswith("INSERT"):
|
|
523
|
+
row = curs.fetchone()
|
|
521
524
|
assert row is not None
|
|
522
525
|
notification_ids.append(row["notification_id"])
|
|
523
526
|
if len(notification_ids) != len(stored_events):
|
|
@@ -579,7 +582,7 @@ class PostgresTrackingRecorder(PostgresRecorder, TrackingRecorder):
|
|
|
579
582
|
**kwargs: Any,
|
|
580
583
|
):
|
|
581
584
|
super().__init__(datastore, **kwargs)
|
|
582
|
-
self.check_table_name_length(tracking_table_name
|
|
585
|
+
self.check_table_name_length(tracking_table_name)
|
|
583
586
|
self.tracking_table_name = tracking_table_name
|
|
584
587
|
self.create_table_statements.append(
|
|
585
588
|
"CREATE TABLE IF NOT EXISTS "
|
|
@@ -605,16 +608,20 @@ class PostgresTrackingRecorder(PostgresRecorder, TrackingRecorder):
|
|
|
605
608
|
|
|
606
609
|
@retry((InterfaceError, OperationalError), max_attempts=10, wait=0.2)
|
|
607
610
|
def insert_tracking(self, tracking: Tracking) -> None:
|
|
608
|
-
|
|
609
|
-
with
|
|
611
|
+
conn: Connection[DictRow]
|
|
612
|
+
with (
|
|
613
|
+
self.datastore.get_connection() as conn,
|
|
614
|
+
conn.transaction(),
|
|
615
|
+
conn.cursor() as curs,
|
|
616
|
+
):
|
|
610
617
|
self._insert_tracking(curs, tracking)
|
|
611
618
|
|
|
612
619
|
def _insert_tracking(
|
|
613
620
|
self,
|
|
614
|
-
|
|
621
|
+
curs: Cursor[DictRow],
|
|
615
622
|
tracking: Tracking,
|
|
616
623
|
) -> None:
|
|
617
|
-
|
|
624
|
+
curs.execute(
|
|
618
625
|
query=self.insert_tracking_statement,
|
|
619
626
|
params=(
|
|
620
627
|
tracking.application_name,
|
|
@@ -636,7 +643,11 @@ class PostgresTrackingRecorder(PostgresRecorder, TrackingRecorder):
|
|
|
636
643
|
return fetchone["max"]
|
|
637
644
|
|
|
638
645
|
@retry((InterfaceError, OperationalError), max_attempts=10, wait=0.2)
|
|
639
|
-
def has_tracking_id(
|
|
646
|
+
def has_tracking_id(
|
|
647
|
+
self, application_name: str, notification_id: int | None
|
|
648
|
+
) -> bool:
|
|
649
|
+
if notification_id is None:
|
|
650
|
+
return True
|
|
640
651
|
conn: Connection[DictRow]
|
|
641
652
|
with self.datastore.get_connection() as conn, conn.cursor() as curs:
|
|
642
653
|
curs.execute(
|
|
@@ -667,14 +678,14 @@ class PostgresProcessRecorder(
|
|
|
667
678
|
|
|
668
679
|
def _insert_events(
|
|
669
680
|
self,
|
|
670
|
-
|
|
681
|
+
curs: Cursor[DictRow],
|
|
671
682
|
stored_events: list[StoredEvent],
|
|
672
683
|
**kwargs: Any,
|
|
673
684
|
) -> None:
|
|
674
685
|
tracking: Tracking | None = kwargs.get("tracking", None)
|
|
675
686
|
if tracking is not None:
|
|
676
|
-
self._insert_tracking(
|
|
677
|
-
super()._insert_events(
|
|
687
|
+
self._insert_tracking(curs, tracking=tracking)
|
|
688
|
+
super()._insert_events(curs, stored_events, **kwargs)
|
|
678
689
|
|
|
679
690
|
|
|
680
691
|
class PostgresFactory(InfrastructureFactory[PostgresTrackingRecorder]):
|
|
@@ -960,11 +971,8 @@ class PostgresFactory(InfrastructureFactory[PostgresTrackingRecorder]):
|
|
|
960
971
|
return recorder
|
|
961
972
|
|
|
962
973
|
def close(self) -> None:
|
|
963
|
-
|
|
974
|
+
with contextlib.suppress(AttributeError):
|
|
964
975
|
self.datastore.close()
|
|
965
976
|
|
|
966
|
-
def __del__(self) -> None:
|
|
967
|
-
self.close()
|
|
968
|
-
|
|
969
977
|
|
|
970
978
|
Factory = PostgresFactory
|
eventsourcing/projection.py
CHANGED
|
@@ -72,13 +72,19 @@ class Projection(ABC, Generic[TTrackingRecorder]):
|
|
|
72
72
|
name: str = ""
|
|
73
73
|
"""Name of projection, used to pick prefixed environment variables."""
|
|
74
74
|
topics: Sequence[str] = ()
|
|
75
|
-
"""
|
|
75
|
+
"""Filter events in database when subscribing to an application."""
|
|
76
76
|
|
|
77
77
|
def __init__(
|
|
78
78
|
self,
|
|
79
|
-
|
|
79
|
+
view: TTrackingRecorder,
|
|
80
80
|
):
|
|
81
|
-
|
|
81
|
+
"""Initialises a projection instance."""
|
|
82
|
+
self._view = view
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def view(self) -> TTrackingRecorder:
|
|
86
|
+
"""Materialised view of an event-sourced application."""
|
|
87
|
+
return self._view
|
|
82
88
|
|
|
83
89
|
@singledispatchmethod
|
|
84
90
|
@abstractmethod
|
|
@@ -90,9 +96,6 @@ class Projection(ABC, Generic[TTrackingRecorder]):
|
|
|
90
96
|
"""
|
|
91
97
|
|
|
92
98
|
|
|
93
|
-
TProjection = TypeVar("TProjection", bound=Projection[Any])
|
|
94
|
-
|
|
95
|
-
|
|
96
99
|
TApplication = TypeVar("TApplication", bound=Application)
|
|
97
100
|
|
|
98
101
|
|
|
@@ -101,28 +104,28 @@ class ProjectionRunner(Generic[TApplication, TTrackingRecorder]):
|
|
|
101
104
|
self,
|
|
102
105
|
*,
|
|
103
106
|
application_class: type[TApplication],
|
|
107
|
+
view_class: type[TTrackingRecorder],
|
|
104
108
|
projection_class: type[Projection[TTrackingRecorder]],
|
|
105
|
-
tracking_recorder_class: type[TTrackingRecorder] | None = None,
|
|
106
109
|
env: EnvType | None = None,
|
|
107
110
|
):
|
|
108
111
|
self.app: TApplication = application_class(env)
|
|
109
112
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
113
|
+
self.view = (
|
|
114
|
+
InfrastructureFactory[TTrackingRecorder]
|
|
115
|
+
.construct(
|
|
116
|
+
env=self._construct_env(
|
|
117
|
+
name=projection_class.name or projection_class.__name__, env=env
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
.tracking_recorder(view_class)
|
|
118
121
|
)
|
|
119
122
|
|
|
120
123
|
self.projection = projection_class(
|
|
121
|
-
|
|
124
|
+
view=self.view,
|
|
122
125
|
)
|
|
123
126
|
self.subscription = ApplicationSubscription(
|
|
124
127
|
app=self.app,
|
|
125
|
-
gt=self.
|
|
128
|
+
gt=self.view.max_tracking_id(self.app.name),
|
|
126
129
|
topics=self.projection.topics,
|
|
127
130
|
)
|
|
128
131
|
self._is_stopping = Event()
|
|
@@ -183,9 +186,9 @@ class ProjectionRunner(Generic[TApplication, TTrackingRecorder]):
|
|
|
183
186
|
if self._is_stopping.wait(timeout=timeout) and self.thread_error is not None:
|
|
184
187
|
raise self.thread_error
|
|
185
188
|
|
|
186
|
-
def wait(self, notification_id: int, timeout: float = 1.0) -> None:
|
|
189
|
+
def wait(self, notification_id: int | None, timeout: float = 1.0) -> None:
|
|
187
190
|
try:
|
|
188
|
-
self.projection.
|
|
191
|
+
self.projection.view.wait(
|
|
189
192
|
application_name=self.subscription.name,
|
|
190
193
|
notification_id=notification_id,
|
|
191
194
|
timeout=timeout,
|
eventsourcing/sqlite.py
CHANGED
|
@@ -530,7 +530,11 @@ class SQLiteTrackingRecorder(SQLiteRecorder, TrackingRecorder):
|
|
|
530
530
|
c.execute(self.select_max_tracking_id_statement, params)
|
|
531
531
|
return c.fetchone()[0]
|
|
532
532
|
|
|
533
|
-
def has_tracking_id(
|
|
533
|
+
def has_tracking_id(
|
|
534
|
+
self, application_name: str, notification_id: int | None
|
|
535
|
+
) -> bool:
|
|
536
|
+
if notification_id is None:
|
|
537
|
+
return True
|
|
534
538
|
params = [application_name, notification_id]
|
|
535
539
|
with self.datastore.transaction(commit=False) as c:
|
|
536
540
|
c.execute(self.count_tracking_id_statement, params)
|
eventsourcing/system.py
CHANGED
|
@@ -7,16 +7,18 @@ from abc import ABC, abstractmethod
|
|
|
7
7
|
from collections import defaultdict
|
|
8
8
|
from queue import Full, Queue
|
|
9
9
|
from types import FrameType, ModuleType
|
|
10
|
-
from typing import TYPE_CHECKING, Any, ClassVar, Optional, Union, cast
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Optional, Union, cast
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
13
13
|
from collections.abc import Iterable, Iterator, Sequence
|
|
14
14
|
from typing_extensions import Self
|
|
15
|
+
from eventsourcing.dispatch import singledispatchmethod
|
|
15
16
|
|
|
16
17
|
from eventsourcing.application import (
|
|
17
18
|
Application,
|
|
18
19
|
NotificationLog,
|
|
19
20
|
ProcessingEvent,
|
|
21
|
+
ProgrammingError,
|
|
20
22
|
Section,
|
|
21
23
|
TApplication,
|
|
22
24
|
)
|
|
@@ -196,8 +198,11 @@ class Follower(Application):
|
|
|
196
198
|
self.notify(processing_event.events)
|
|
197
199
|
self._notify(recordings)
|
|
198
200
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
+
policy: (
|
|
202
|
+
Callable[[DomainEventProtocol, ProcessingEvent], None] | singledispatchmethod
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
def policy( # type: ignore[no-redef]
|
|
201
206
|
self,
|
|
202
207
|
domain_event: DomainEventProtocol,
|
|
203
208
|
processing_event: ProcessingEvent,
|
|
@@ -379,7 +384,7 @@ class System:
|
|
|
379
384
|
return cls
|
|
380
385
|
|
|
381
386
|
@property
|
|
382
|
-
def topic(self) -> str
|
|
387
|
+
def topic(self) -> str:
|
|
383
388
|
"""
|
|
384
389
|
Returns a topic to the system object, if constructed as a module attribute.
|
|
385
390
|
"""
|
|
@@ -389,6 +394,9 @@ class System:
|
|
|
389
394
|
if value is self:
|
|
390
395
|
topic = module.__name__ + ":" + name
|
|
391
396
|
assert resolve_topic(topic) is self
|
|
397
|
+
if topic is None:
|
|
398
|
+
msg = "Unable to compute topic for system object: %s" % self
|
|
399
|
+
raise ProgrammingError(msg)
|
|
392
400
|
return topic
|
|
393
401
|
|
|
394
402
|
|
|
@@ -423,6 +431,13 @@ class Runner(ABC):
|
|
|
423
431
|
Returns an application instance for given application class.
|
|
424
432
|
"""
|
|
425
433
|
|
|
434
|
+
def __enter__(self) -> Self:
|
|
435
|
+
self.start()
|
|
436
|
+
return self
|
|
437
|
+
|
|
438
|
+
def __exit__(self, *args: object, **kwargs: Any) -> None:
|
|
439
|
+
self.stop()
|
|
440
|
+
|
|
426
441
|
|
|
427
442
|
class RunnerAlreadyStartedError(Exception):
|
|
428
443
|
"""
|
|
@@ -548,13 +563,6 @@ class SingleThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
548
563
|
assert isinstance(app, cls)
|
|
549
564
|
return app
|
|
550
565
|
|
|
551
|
-
def __enter__(self) -> Self:
|
|
552
|
-
self.start()
|
|
553
|
-
return self
|
|
554
|
-
|
|
555
|
-
def __exit__(self, *args: object, **kwargs: Any) -> None:
|
|
556
|
-
self.stop()
|
|
557
|
-
|
|
558
566
|
|
|
559
567
|
class NewSingleThreadedRunner(Runner, RecordingEventReceiver):
|
|
560
568
|
"""
|