eventsourcing 9.4.0a4__py3-none-any.whl → 9.4.0a6__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 +21 -27
- eventsourcing/domain.py +49 -51
- eventsourcing/interface.py +5 -2
- eventsourcing/persistence.py +29 -36
- eventsourcing/popo.py +17 -16
- eventsourcing/postgres.py +18 -17
- eventsourcing/projection.py +17 -26
- eventsourcing/sqlite.py +18 -17
- eventsourcing/system.py +49 -62
- eventsourcing/tests/application.py +3 -3
- eventsourcing/tests/persistence.py +12 -12
- eventsourcing/utils.py +8 -20
- {eventsourcing-9.4.0a4.dist-info → eventsourcing-9.4.0a6.dist-info}/METADATA +4 -6
- eventsourcing-9.4.0a6.dist-info/RECORD +26 -0
- {eventsourcing-9.4.0a4.dist-info → eventsourcing-9.4.0a6.dist-info}/WHEEL +1 -1
- eventsourcing-9.4.0a4.dist-info/RECORD +0 -26
- {eventsourcing-9.4.0a4.dist-info → eventsourcing-9.4.0a6.dist-info}/AUTHORS +0 -0
- {eventsourcing-9.4.0a4.dist-info → eventsourcing-9.4.0a6.dist-info}/LICENSE +0 -0
eventsourcing/persistence.py
CHANGED
|
@@ -4,27 +4,15 @@ import json
|
|
|
4
4
|
import uuid
|
|
5
5
|
from abc import ABC, abstractmethod
|
|
6
6
|
from collections import deque
|
|
7
|
+
from collections.abc import Iterator, Mapping, Sequence
|
|
7
8
|
from dataclasses import dataclass
|
|
8
9
|
from datetime import datetime
|
|
9
10
|
from decimal import Decimal
|
|
10
11
|
from queue import Queue
|
|
11
12
|
from threading import Condition, Event, Lock, Semaphore, Thread, Timer
|
|
12
13
|
from time import monotonic, sleep, time
|
|
13
|
-
from types import ModuleType
|
|
14
|
-
from typing import
|
|
15
|
-
TYPE_CHECKING,
|
|
16
|
-
Any,
|
|
17
|
-
Dict,
|
|
18
|
-
Generic,
|
|
19
|
-
Iterator,
|
|
20
|
-
List,
|
|
21
|
-
Mapping,
|
|
22
|
-
Sequence,
|
|
23
|
-
Type,
|
|
24
|
-
TypeVar,
|
|
25
|
-
Union,
|
|
26
|
-
cast,
|
|
27
|
-
)
|
|
14
|
+
from types import GenericAlias, ModuleType
|
|
15
|
+
from typing import TYPE_CHECKING, Any, Generic, TypeVar, Union, cast
|
|
28
16
|
from uuid import UUID
|
|
29
17
|
from warnings import warn
|
|
30
18
|
|
|
@@ -78,8 +66,8 @@ class JSONTranscoder(Transcoder):
|
|
|
78
66
|
"""
|
|
79
67
|
|
|
80
68
|
def __init__(self) -> None:
|
|
81
|
-
self.types:
|
|
82
|
-
self.names:
|
|
69
|
+
self.types: dict[type, Transcoding] = {}
|
|
70
|
+
self.names: dict[str, Transcoding] = {}
|
|
83
71
|
self.encoder = json.JSONEncoder(
|
|
84
72
|
default=self._encode_obj,
|
|
85
73
|
separators=(",", ":"),
|
|
@@ -106,7 +94,7 @@ class JSONTranscoder(Transcoder):
|
|
|
106
94
|
"""
|
|
107
95
|
return self.decoder.decode(data.decode("utf8"))
|
|
108
96
|
|
|
109
|
-
def _encode_obj(self, o: Any) ->
|
|
97
|
+
def _encode_obj(self, o: Any) -> dict[str, Any]:
|
|
110
98
|
try:
|
|
111
99
|
transcoding = self.types[type(o)]
|
|
112
100
|
except KeyError:
|
|
@@ -122,7 +110,7 @@ class JSONTranscoder(Transcoder):
|
|
|
122
110
|
"_data_": transcoding.encode(o),
|
|
123
111
|
}
|
|
124
112
|
|
|
125
|
-
def _decode_obj(self, d:
|
|
113
|
+
def _decode_obj(self, d: dict[str, Any]) -> Any:
|
|
126
114
|
if len(d) == 2:
|
|
127
115
|
try:
|
|
128
116
|
_type_ = d["_type_"]
|
|
@@ -314,7 +302,7 @@ class Mapper:
|
|
|
314
302
|
stored_state = self.cipher.decrypt(stored_state)
|
|
315
303
|
if self.compressor:
|
|
316
304
|
stored_state = self.compressor.decompress(stored_state)
|
|
317
|
-
event_state:
|
|
305
|
+
event_state: dict[str, Any] = self.transcoder.decode(stored_state)
|
|
318
306
|
event_state["originator_id"] = stored_event.originator_id
|
|
319
307
|
event_state["originator_version"] = stored_event.originator_version
|
|
320
308
|
cls = resolve_topic(stored_event.topic)
|
|
@@ -422,7 +410,7 @@ class AggregateRecorder(ABC):
|
|
|
422
410
|
|
|
423
411
|
@abstractmethod
|
|
424
412
|
def insert_events(
|
|
425
|
-
self, stored_events:
|
|
413
|
+
self, stored_events: list[StoredEvent], **kwargs: Any
|
|
426
414
|
) -> Sequence[int] | None:
|
|
427
415
|
"""
|
|
428
416
|
Writes stored events into database.
|
|
@@ -437,7 +425,7 @@ class AggregateRecorder(ABC):
|
|
|
437
425
|
lte: int | None = None,
|
|
438
426
|
desc: bool = False,
|
|
439
427
|
limit: int | None = None,
|
|
440
|
-
) ->
|
|
428
|
+
) -> list[StoredEvent]:
|
|
441
429
|
"""
|
|
442
430
|
Reads stored events from database.
|
|
443
431
|
"""
|
|
@@ -468,7 +456,7 @@ class ApplicationRecorder(AggregateRecorder):
|
|
|
468
456
|
topics: Sequence[str] = (),
|
|
469
457
|
*,
|
|
470
458
|
inclusive_of_start: bool = True,
|
|
471
|
-
) ->
|
|
459
|
+
) -> list[Notification]:
|
|
472
460
|
"""
|
|
473
461
|
Returns a list of Notification objects representing events from an
|
|
474
462
|
application sequence. If `inclusive_of_start` is True (the default),
|
|
@@ -598,7 +586,7 @@ class EventStore:
|
|
|
598
586
|
|
|
599
587
|
def put(
|
|
600
588
|
self, domain_events: Sequence[DomainEventProtocol], **kwargs: Any
|
|
601
|
-
) ->
|
|
589
|
+
) -> list[Recording]:
|
|
602
590
|
"""
|
|
603
591
|
Stores domain events in aggregate sequence.
|
|
604
592
|
"""
|
|
@@ -670,14 +658,14 @@ class InfrastructureFactory(ABC, Generic[TTrackingRecorder]):
|
|
|
670
658
|
|
|
671
659
|
@classmethod
|
|
672
660
|
def construct(
|
|
673
|
-
cls:
|
|
661
|
+
cls: type[TInfrastructureFactory], env: Environment | None = None
|
|
674
662
|
) -> TInfrastructureFactory:
|
|
675
663
|
"""
|
|
676
664
|
Constructs concrete infrastructure factory for given
|
|
677
665
|
named application. Reads and resolves persistence
|
|
678
666
|
topic from environment variable 'PERSISTENCE_MODULE'.
|
|
679
667
|
"""
|
|
680
|
-
factory_cls:
|
|
668
|
+
factory_cls: type[InfrastructureFactory[TrackingRecorder]]
|
|
681
669
|
if env is None:
|
|
682
670
|
env = Environment()
|
|
683
671
|
topic = (
|
|
@@ -696,7 +684,7 @@ class InfrastructureFactory(ABC, Generic[TTrackingRecorder]):
|
|
|
696
684
|
or "eventsourcing.popo"
|
|
697
685
|
)
|
|
698
686
|
try:
|
|
699
|
-
obj:
|
|
687
|
+
obj: type[InfrastructureFactory[TrackingRecorder]] | ModuleType = (
|
|
700
688
|
resolve_topic(topic)
|
|
701
689
|
)
|
|
702
690
|
except TopicError as e:
|
|
@@ -709,15 +697,20 @@ class InfrastructureFactory(ABC, Generic[TTrackingRecorder]):
|
|
|
709
697
|
|
|
710
698
|
if isinstance(obj, ModuleType):
|
|
711
699
|
# Find the factory in the module.
|
|
712
|
-
factory_classes:
|
|
700
|
+
factory_classes: list[type[InfrastructureFactory[TrackingRecorder]]] = []
|
|
713
701
|
for member in obj.__dict__.values():
|
|
714
702
|
if (
|
|
715
703
|
member is not InfrastructureFactory
|
|
716
704
|
and isinstance(member, type) # Look for classes...
|
|
705
|
+
and isinstance(member, type) # Look for classes...
|
|
706
|
+
and not isinstance(
|
|
707
|
+
member, GenericAlias
|
|
708
|
+
) # Issue with Python 3.9 and 3.10.
|
|
717
709
|
and issubclass(member, InfrastructureFactory) # Ignore base class.
|
|
718
710
|
and member not in factory_classes # Forgive aliases.
|
|
719
711
|
):
|
|
720
712
|
factory_classes.append(member)
|
|
713
|
+
|
|
721
714
|
if len(factory_classes) == 1:
|
|
722
715
|
factory_cls = factory_classes[0]
|
|
723
716
|
else:
|
|
@@ -747,7 +740,7 @@ class InfrastructureFactory(ABC, Generic[TTrackingRecorder]):
|
|
|
747
740
|
"""
|
|
748
741
|
transcoder_topic = self.env.get(self.TRANSCODER_TOPIC)
|
|
749
742
|
if transcoder_topic:
|
|
750
|
-
transcoder_class:
|
|
743
|
+
transcoder_class: type[Transcoder] = resolve_topic(transcoder_topic)
|
|
751
744
|
else:
|
|
752
745
|
transcoder_class = JSONTranscoder
|
|
753
746
|
return transcoder_class()
|
|
@@ -755,7 +748,7 @@ class InfrastructureFactory(ABC, Generic[TTrackingRecorder]):
|
|
|
755
748
|
def mapper(
|
|
756
749
|
self,
|
|
757
750
|
transcoder: Transcoder | None = None,
|
|
758
|
-
mapper_class:
|
|
751
|
+
mapper_class: type[Mapper] | None = None,
|
|
759
752
|
) -> Mapper:
|
|
760
753
|
"""
|
|
761
754
|
Constructs a mapper.
|
|
@@ -783,7 +776,7 @@ class InfrastructureFactory(ABC, Generic[TTrackingRecorder]):
|
|
|
783
776
|
cipher_topic = default_cipher_topic
|
|
784
777
|
|
|
785
778
|
if cipher_topic:
|
|
786
|
-
cipher_cls:
|
|
779
|
+
cipher_cls: type[Cipher] = resolve_topic(cipher_topic)
|
|
787
780
|
cipher = cipher_cls(self.env)
|
|
788
781
|
|
|
789
782
|
return cipher
|
|
@@ -796,7 +789,7 @@ class InfrastructureFactory(ABC, Generic[TTrackingRecorder]):
|
|
|
796
789
|
compressor: Compressor | None = None
|
|
797
790
|
compressor_topic = self.env.get(self.COMPRESSOR_TOPIC)
|
|
798
791
|
if compressor_topic:
|
|
799
|
-
compressor_cls:
|
|
792
|
+
compressor_cls: type[Compressor] | Compressor = resolve_topic(
|
|
800
793
|
compressor_topic
|
|
801
794
|
)
|
|
802
795
|
if isinstance(compressor_cls, type):
|
|
@@ -832,7 +825,7 @@ class InfrastructureFactory(ABC, Generic[TTrackingRecorder]):
|
|
|
832
825
|
|
|
833
826
|
@abstractmethod
|
|
834
827
|
def tracking_recorder(
|
|
835
|
-
self, tracking_recorder_class:
|
|
828
|
+
self, tracking_recorder_class: type[TTrackingRecorder] | None = None
|
|
836
829
|
) -> TTrackingRecorder:
|
|
837
830
|
"""
|
|
838
831
|
Constructs a tracking recorder.
|
|
@@ -1013,7 +1006,7 @@ class ConnectionPool(ABC, Generic[TConnection]):
|
|
|
1013
1006
|
self.max_age = max_age
|
|
1014
1007
|
self.pre_ping = pre_ping
|
|
1015
1008
|
self._pool: deque[TConnection] = deque()
|
|
1016
|
-
self._in_use:
|
|
1009
|
+
self._in_use: dict[int, TConnection] = {}
|
|
1017
1010
|
self._get_semaphore = Semaphore()
|
|
1018
1011
|
self._put_condition = Condition()
|
|
1019
1012
|
self._no_readers = Condition()
|
|
@@ -1362,9 +1355,9 @@ class ListenNotifySubscription(Subscription[TApplicationRecorder_co]):
|
|
|
1362
1355
|
) -> None:
|
|
1363
1356
|
super().__init__(recorder=recorder, gt=gt, topics=topics)
|
|
1364
1357
|
self._select_limit = 500
|
|
1365
|
-
self._notifications:
|
|
1358
|
+
self._notifications: list[Notification] = []
|
|
1366
1359
|
self._notifications_index: int = 0
|
|
1367
|
-
self._notifications_queue: Queue[
|
|
1360
|
+
self._notifications_queue: Queue[list[Notification]] = Queue(maxsize=10)
|
|
1368
1361
|
self._has_been_notified = Event()
|
|
1369
1362
|
self._thread_error: BaseException | None = None
|
|
1370
1363
|
self._pull_thread = Thread(target=self._loop_on_pull)
|
eventsourcing/popo.py
CHANGED
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import contextlib
|
|
4
4
|
from collections import defaultdict
|
|
5
5
|
from threading import Event, Lock
|
|
6
|
-
from typing import TYPE_CHECKING, Any
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
7
|
|
|
8
8
|
from eventsourcing.persistence import (
|
|
9
9
|
AggregateRecorder,
|
|
@@ -21,6 +21,7 @@ from eventsourcing.persistence import (
|
|
|
21
21
|
from eventsourcing.utils import resolve_topic, reversed_keys
|
|
22
22
|
|
|
23
23
|
if TYPE_CHECKING:
|
|
24
|
+
from collections.abc import Iterable, Sequence
|
|
24
25
|
from uuid import UUID
|
|
25
26
|
|
|
26
27
|
|
|
@@ -32,23 +33,23 @@ class POPORecorder:
|
|
|
32
33
|
class POPOAggregateRecorder(POPORecorder, AggregateRecorder):
|
|
33
34
|
def __init__(self) -> None:
|
|
34
35
|
super().__init__()
|
|
35
|
-
self._stored_events:
|
|
36
|
-
self._stored_events_index:
|
|
36
|
+
self._stored_events: list[StoredEvent] = []
|
|
37
|
+
self._stored_events_index: dict[UUID, dict[int, int]] = defaultdict(dict)
|
|
37
38
|
|
|
38
39
|
def insert_events(
|
|
39
|
-
self, stored_events:
|
|
40
|
+
self, stored_events: list[StoredEvent], **kwargs: Any
|
|
40
41
|
) -> Sequence[int] | None:
|
|
41
42
|
self._insert_events(stored_events, **kwargs)
|
|
42
43
|
return None
|
|
43
44
|
|
|
44
45
|
def _insert_events(
|
|
45
|
-
self, stored_events:
|
|
46
|
+
self, stored_events: list[StoredEvent], **kwargs: Any
|
|
46
47
|
) -> Sequence[int] | None:
|
|
47
48
|
with self._database_lock:
|
|
48
49
|
self._assert_uniqueness(stored_events, **kwargs)
|
|
49
50
|
return self._update_table(stored_events, **kwargs)
|
|
50
51
|
|
|
51
|
-
def _assert_uniqueness(self, stored_events:
|
|
52
|
+
def _assert_uniqueness(self, stored_events: list[StoredEvent], **_: Any) -> None:
|
|
52
53
|
new = set()
|
|
53
54
|
for s in stored_events:
|
|
54
55
|
# Check events don't already exist.
|
|
@@ -62,7 +63,7 @@ class POPOAggregateRecorder(POPORecorder, AggregateRecorder):
|
|
|
62
63
|
raise IntegrityError(msg)
|
|
63
64
|
|
|
64
65
|
def _update_table(
|
|
65
|
-
self, stored_events:
|
|
66
|
+
self, stored_events: list[StoredEvent], **_: Any
|
|
66
67
|
) -> Sequence[int] | None:
|
|
67
68
|
notification_ids = []
|
|
68
69
|
for s in stored_events:
|
|
@@ -81,7 +82,7 @@ class POPOAggregateRecorder(POPORecorder, AggregateRecorder):
|
|
|
81
82
|
lte: int | None = None,
|
|
82
83
|
desc: bool = False,
|
|
83
84
|
limit: int | None = None,
|
|
84
|
-
) ->
|
|
85
|
+
) -> list[StoredEvent]:
|
|
85
86
|
with self._database_lock:
|
|
86
87
|
results = []
|
|
87
88
|
|
|
@@ -103,10 +104,10 @@ class POPOAggregateRecorder(POPORecorder, AggregateRecorder):
|
|
|
103
104
|
class POPOApplicationRecorder(POPOAggregateRecorder, ApplicationRecorder):
|
|
104
105
|
def __init__(self) -> None:
|
|
105
106
|
super().__init__()
|
|
106
|
-
self._listeners:
|
|
107
|
+
self._listeners: set[Event] = set()
|
|
107
108
|
|
|
108
109
|
def insert_events(
|
|
109
|
-
self, stored_events:
|
|
110
|
+
self, stored_events: list[StoredEvent], **kwargs: Any
|
|
110
111
|
) -> Sequence[int] | None:
|
|
111
112
|
notification_ids = self._insert_events(stored_events, **kwargs)
|
|
112
113
|
self._notify_listeners()
|
|
@@ -120,7 +121,7 @@ class POPOApplicationRecorder(POPOAggregateRecorder, ApplicationRecorder):
|
|
|
120
121
|
topics: Sequence[str] = (),
|
|
121
122
|
*,
|
|
122
123
|
inclusive_of_start: bool = True,
|
|
123
|
-
) ->
|
|
124
|
+
) -> list[Notification]:
|
|
124
125
|
with self._database_lock:
|
|
125
126
|
results = []
|
|
126
127
|
if start is None:
|
|
@@ -192,8 +193,8 @@ class POPOSubscription(ListenNotifySubscription[POPOApplicationRecorder]):
|
|
|
192
193
|
class POPOTrackingRecorder(POPORecorder, TrackingRecorder):
|
|
193
194
|
def __init__(self) -> None:
|
|
194
195
|
super().__init__()
|
|
195
|
-
self._tracking_table:
|
|
196
|
-
self._max_tracking_ids:
|
|
196
|
+
self._tracking_table: dict[str, set[int]] = defaultdict(set)
|
|
197
|
+
self._max_tracking_ids: dict[str, int | None] = defaultdict(lambda: None)
|
|
197
198
|
|
|
198
199
|
def _assert_tracking_uniqueness(self, tracking: Tracking) -> None:
|
|
199
200
|
if tracking.notification_id in self._tracking_table[tracking.application_name]:
|
|
@@ -227,7 +228,7 @@ class POPOProcessRecorder(
|
|
|
227
228
|
POPOTrackingRecorder, POPOApplicationRecorder, ProcessRecorder
|
|
228
229
|
):
|
|
229
230
|
def _assert_uniqueness(
|
|
230
|
-
self, stored_events:
|
|
231
|
+
self, stored_events: list[StoredEvent], **kwargs: Any
|
|
231
232
|
) -> None:
|
|
232
233
|
super()._assert_uniqueness(stored_events, **kwargs)
|
|
233
234
|
t: Tracking | None = kwargs.get("tracking", None)
|
|
@@ -235,7 +236,7 @@ class POPOProcessRecorder(
|
|
|
235
236
|
self._assert_tracking_uniqueness(t)
|
|
236
237
|
|
|
237
238
|
def _update_table(
|
|
238
|
-
self, stored_events:
|
|
239
|
+
self, stored_events: list[StoredEvent], **kwargs: Any
|
|
239
240
|
) -> Sequence[int] | None:
|
|
240
241
|
notification_ids = super()._update_table(stored_events, **kwargs)
|
|
241
242
|
t: Tracking | None = kwargs.get("tracking", None)
|
|
@@ -252,7 +253,7 @@ class POPOFactory(InfrastructureFactory[POPOTrackingRecorder]):
|
|
|
252
253
|
return POPOApplicationRecorder()
|
|
253
254
|
|
|
254
255
|
def tracking_recorder(
|
|
255
|
-
self, tracking_recorder_class:
|
|
256
|
+
self, tracking_recorder_class: type[POPOTrackingRecorder] | None = None
|
|
256
257
|
) -> POPOTrackingRecorder:
|
|
257
258
|
if tracking_recorder_class is None:
|
|
258
259
|
tracking_recorder_topic = self.env.get(self.TRACKING_RECORDER_TOPIC)
|
eventsourcing/postgres.py
CHANGED
|
@@ -4,7 +4,7 @@ import logging
|
|
|
4
4
|
from asyncio import CancelledError
|
|
5
5
|
from contextlib import contextmanager
|
|
6
6
|
from threading import Thread
|
|
7
|
-
from typing import TYPE_CHECKING, Any, Callable
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
8
8
|
|
|
9
9
|
import psycopg
|
|
10
10
|
import psycopg.errors
|
|
@@ -37,6 +37,7 @@ from eventsourcing.persistence import (
|
|
|
37
37
|
from eventsourcing.utils import Environment, resolve_topic, retry, strtobool
|
|
38
38
|
|
|
39
39
|
if TYPE_CHECKING:
|
|
40
|
+
from collections.abc import Iterator, Sequence
|
|
40
41
|
from uuid import UUID
|
|
41
42
|
|
|
42
43
|
from typing_extensions import Self
|
|
@@ -190,7 +191,7 @@ class PostgresRecorder:
|
|
|
190
191
|
self.datastore = datastore
|
|
191
192
|
self.create_table_statements = self.construct_create_table_statements()
|
|
192
193
|
|
|
193
|
-
def construct_create_table_statements(self) ->
|
|
194
|
+
def construct_create_table_statements(self) -> list[str]:
|
|
194
195
|
return []
|
|
195
196
|
|
|
196
197
|
@staticmethod
|
|
@@ -247,11 +248,11 @@ class PostgresAggregateRecorder(PostgresRecorder, AggregateRecorder):
|
|
|
247
248
|
self.select_events_statement = (
|
|
248
249
|
f"SELECT * FROM {self.events_table_name} WHERE originator_id = %s"
|
|
249
250
|
)
|
|
250
|
-
self.lock_table_statements:
|
|
251
|
+
self.lock_table_statements: list[str] = []
|
|
251
252
|
|
|
252
253
|
@retry((InterfaceError, OperationalError), max_attempts=10, wait=0.2)
|
|
253
254
|
def insert_events(
|
|
254
|
-
self, stored_events:
|
|
255
|
+
self, stored_events: list[StoredEvent], **kwargs: Any
|
|
255
256
|
) -> Sequence[int] | None:
|
|
256
257
|
conn: Connection[DictRow]
|
|
257
258
|
exc: Exception | None = None
|
|
@@ -283,7 +284,7 @@ class PostgresAggregateRecorder(PostgresRecorder, AggregateRecorder):
|
|
|
283
284
|
def _insert_events(
|
|
284
285
|
self,
|
|
285
286
|
c: Cursor[DictRow],
|
|
286
|
-
stored_events:
|
|
287
|
+
stored_events: list[StoredEvent],
|
|
287
288
|
**_: Any,
|
|
288
289
|
) -> None:
|
|
289
290
|
pass
|
|
@@ -291,7 +292,7 @@ class PostgresAggregateRecorder(PostgresRecorder, AggregateRecorder):
|
|
|
291
292
|
def _insert_stored_events(
|
|
292
293
|
self,
|
|
293
294
|
c: Cursor[DictRow],
|
|
294
|
-
stored_events:
|
|
295
|
+
stored_events: list[StoredEvent],
|
|
295
296
|
**_: Any,
|
|
296
297
|
) -> None:
|
|
297
298
|
# Only do something if there is something to do.
|
|
@@ -324,7 +325,7 @@ class PostgresAggregateRecorder(PostgresRecorder, AggregateRecorder):
|
|
|
324
325
|
def _fetch_ids_after_insert_events(
|
|
325
326
|
self,
|
|
326
327
|
c: Cursor[DictRow],
|
|
327
|
-
stored_events:
|
|
328
|
+
stored_events: list[StoredEvent],
|
|
328
329
|
**kwargs: Any,
|
|
329
330
|
) -> Sequence[int] | None:
|
|
330
331
|
return None
|
|
@@ -338,9 +339,9 @@ class PostgresAggregateRecorder(PostgresRecorder, AggregateRecorder):
|
|
|
338
339
|
lte: int | None = None,
|
|
339
340
|
desc: bool = False,
|
|
340
341
|
limit: int | None = None,
|
|
341
|
-
) ->
|
|
342
|
+
) -> list[StoredEvent]:
|
|
342
343
|
statement = self.select_events_statement
|
|
343
|
-
params:
|
|
344
|
+
params: list[Any] = [originator_id]
|
|
344
345
|
if gt is not None:
|
|
345
346
|
params.append(gt)
|
|
346
347
|
statement += " AND originator_version > %s"
|
|
@@ -413,13 +414,13 @@ class PostgresApplicationRecorder(PostgresAggregateRecorder, ApplicationRecorder
|
|
|
413
414
|
topics: Sequence[str] = (),
|
|
414
415
|
*,
|
|
415
416
|
inclusive_of_start: bool = True,
|
|
416
|
-
) ->
|
|
417
|
+
) -> list[Notification]:
|
|
417
418
|
"""
|
|
418
419
|
Returns a list of event notifications
|
|
419
420
|
from 'start', limited by 'limit'.
|
|
420
421
|
"""
|
|
421
422
|
|
|
422
|
-
params:
|
|
423
|
+
params: list[int | str | Sequence[str]] = []
|
|
423
424
|
statement = f"SELECT * FROM {self.events_table_name}"
|
|
424
425
|
has_where = False
|
|
425
426
|
if start is not None:
|
|
@@ -508,10 +509,10 @@ class PostgresApplicationRecorder(PostgresAggregateRecorder, ApplicationRecorder
|
|
|
508
509
|
def _fetch_ids_after_insert_events(
|
|
509
510
|
self,
|
|
510
511
|
c: Cursor[DictRow],
|
|
511
|
-
stored_events:
|
|
512
|
+
stored_events: list[StoredEvent],
|
|
512
513
|
**kwargs: Any,
|
|
513
514
|
) -> Sequence[int] | None:
|
|
514
|
-
notification_ids:
|
|
515
|
+
notification_ids: list[int] = []
|
|
515
516
|
len_events = len(stored_events)
|
|
516
517
|
if len_events:
|
|
517
518
|
while c.nextset() and len(notification_ids) != len_events:
|
|
@@ -667,7 +668,7 @@ class PostgresProcessRecorder(
|
|
|
667
668
|
def _insert_events(
|
|
668
669
|
self,
|
|
669
670
|
c: Cursor[DictRow],
|
|
670
|
-
stored_events:
|
|
671
|
+
stored_events: list[StoredEvent],
|
|
671
672
|
**kwargs: Any,
|
|
672
673
|
) -> None:
|
|
673
674
|
tracking: Tracking | None = kwargs.get("tracking", None)
|
|
@@ -893,7 +894,7 @@ class PostgresFactory(InfrastructureFactory[PostgresTrackingRecorder]):
|
|
|
893
894
|
|
|
894
895
|
application_recorder_topic = self.env.get(self.APPLICATION_RECORDER_TOPIC)
|
|
895
896
|
if application_recorder_topic:
|
|
896
|
-
application_recorder_class:
|
|
897
|
+
application_recorder_class: type[PostgresApplicationRecorder] = (
|
|
897
898
|
resolve_topic(application_recorder_topic)
|
|
898
899
|
)
|
|
899
900
|
assert issubclass(application_recorder_class, PostgresApplicationRecorder)
|
|
@@ -909,7 +910,7 @@ class PostgresFactory(InfrastructureFactory[PostgresTrackingRecorder]):
|
|
|
909
910
|
return recorder
|
|
910
911
|
|
|
911
912
|
def tracking_recorder(
|
|
912
|
-
self, tracking_recorder_class:
|
|
913
|
+
self, tracking_recorder_class: type[PostgresTrackingRecorder] | None = None
|
|
913
914
|
) -> PostgresTrackingRecorder:
|
|
914
915
|
prefix = self.env.name.lower() or "notification"
|
|
915
916
|
tracking_table_name = prefix + "_tracking"
|
|
@@ -942,7 +943,7 @@ class PostgresFactory(InfrastructureFactory[PostgresTrackingRecorder]):
|
|
|
942
943
|
|
|
943
944
|
process_recorder_topic = self.env.get(self.PROCESS_RECORDER_TOPIC)
|
|
944
945
|
if process_recorder_topic:
|
|
945
|
-
process_recorder_class:
|
|
946
|
+
process_recorder_class: type[PostgresTrackingRecorder] = resolve_topic(
|
|
946
947
|
process_recorder_topic
|
|
947
948
|
)
|
|
948
949
|
assert issubclass(process_recorder_class, PostgresProcessRecorder)
|
eventsourcing/projection.py
CHANGED
|
@@ -3,19 +3,10 @@ from __future__ import annotations
|
|
|
3
3
|
import os
|
|
4
4
|
import weakref
|
|
5
5
|
from abc import ABC, abstractmethod
|
|
6
|
+
from collections.abc import Iterator, Sequence
|
|
6
7
|
from threading import Event, Thread
|
|
7
8
|
from traceback import format_exc
|
|
8
|
-
from typing import
|
|
9
|
-
TYPE_CHECKING,
|
|
10
|
-
Any,
|
|
11
|
-
Dict,
|
|
12
|
-
Generic,
|
|
13
|
-
Iterator,
|
|
14
|
-
Sequence,
|
|
15
|
-
Tuple,
|
|
16
|
-
Type,
|
|
17
|
-
TypeVar,
|
|
18
|
-
)
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Generic, TypeVar
|
|
19
10
|
from warnings import warn
|
|
20
11
|
|
|
21
12
|
from eventsourcing.application import Application
|
|
@@ -34,7 +25,7 @@ if TYPE_CHECKING:
|
|
|
34
25
|
from typing_extensions import Self
|
|
35
26
|
|
|
36
27
|
|
|
37
|
-
class ApplicationSubscription(Iterator[
|
|
28
|
+
class ApplicationSubscription(Iterator[tuple[DomainEventProtocol, Tracking]]):
|
|
38
29
|
"""
|
|
39
30
|
An iterator that yields all domain events recorded in an application
|
|
40
31
|
sequence that have notification IDs greater than a given value. The iterator
|
|
@@ -64,7 +55,7 @@ class ApplicationSubscription(Iterator[Tuple[DomainEventProtocol, Tracking]]):
|
|
|
64
55
|
def __iter__(self) -> Self:
|
|
65
56
|
return self
|
|
66
57
|
|
|
67
|
-
def __next__(self) ->
|
|
58
|
+
def __next__(self) -> tuple[DomainEventProtocol, Tracking]:
|
|
68
59
|
notification = next(self.subscription)
|
|
69
60
|
tracking = Tracking(self.name, notification.id)
|
|
70
61
|
domain_event = self.mapper.to_domain_event(notification)
|
|
@@ -109,9 +100,9 @@ class ProjectionRunner(Generic[TApplication, TTrackingRecorder]):
|
|
|
109
100
|
def __init__(
|
|
110
101
|
self,
|
|
111
102
|
*,
|
|
112
|
-
application_class:
|
|
113
|
-
projection_class:
|
|
114
|
-
tracking_recorder_class:
|
|
103
|
+
application_class: type[TApplication],
|
|
104
|
+
projection_class: type[Projection[TTrackingRecorder]],
|
|
105
|
+
tracking_recorder_class: type[TTrackingRecorder] | None = None,
|
|
115
106
|
env: EnvType | None = None,
|
|
116
107
|
):
|
|
117
108
|
self.app: TApplication = application_class(env)
|
|
@@ -134,14 +125,14 @@ class ProjectionRunner(Generic[TApplication, TTrackingRecorder]):
|
|
|
134
125
|
gt=self.tracking_recorder.max_tracking_id(self.app.name),
|
|
135
126
|
topics=self.projection.topics,
|
|
136
127
|
)
|
|
137
|
-
self.
|
|
128
|
+
self._is_stopping = Event()
|
|
138
129
|
self.thread_error: BaseException | None = None
|
|
139
130
|
self.processing_thread = Thread(
|
|
140
131
|
target=self._process_events_loop,
|
|
141
132
|
kwargs={
|
|
142
133
|
"subscription": self.subscription,
|
|
143
134
|
"projection": self.projection,
|
|
144
|
-
"
|
|
135
|
+
"is_stopping": self._is_stopping,
|
|
145
136
|
"runner": weakref.ref(self),
|
|
146
137
|
},
|
|
147
138
|
)
|
|
@@ -151,20 +142,21 @@ class ProjectionRunner(Generic[TApplication, TTrackingRecorder]):
|
|
|
151
142
|
"""
|
|
152
143
|
Constructs environment from which projection will be configured.
|
|
153
144
|
"""
|
|
154
|
-
_env:
|
|
145
|
+
_env: dict[str, str] = {}
|
|
155
146
|
_env.update(os.environ)
|
|
156
147
|
if env is not None:
|
|
157
148
|
_env.update(env)
|
|
158
149
|
return Environment(name, _env)
|
|
159
150
|
|
|
160
151
|
def stop(self) -> None:
|
|
152
|
+
self._is_stopping.set()
|
|
161
153
|
self.subscription.stop()
|
|
162
154
|
|
|
163
155
|
@staticmethod
|
|
164
156
|
def _process_events_loop(
|
|
165
157
|
subscription: ApplicationSubscription,
|
|
166
158
|
projection: Projection[TrackingRecorder],
|
|
167
|
-
|
|
159
|
+
is_stopping: Event,
|
|
168
160
|
runner: weakref.ReferenceType[ProjectionRunner[Application, TrackingRecorder]],
|
|
169
161
|
) -> None:
|
|
170
162
|
try:
|
|
@@ -184,12 +176,11 @@ class ProjectionRunner(Generic[TApplication, TTrackingRecorder]):
|
|
|
184
176
|
stacklevel=2,
|
|
185
177
|
)
|
|
186
178
|
|
|
187
|
-
|
|
179
|
+
is_stopping.set()
|
|
188
180
|
subscription.subscription.stop()
|
|
189
181
|
|
|
190
182
|
def run_forever(self, timeout: float | None = None) -> None:
|
|
191
|
-
if self.
|
|
192
|
-
assert self.thread_error is not None # for mypy
|
|
183
|
+
if self._is_stopping.wait(timeout=timeout) and self.thread_error is not None:
|
|
193
184
|
raise self.thread_error
|
|
194
185
|
|
|
195
186
|
def wait(self, notification_id: int, timeout: float = 1.0) -> None:
|
|
@@ -198,11 +189,11 @@ class ProjectionRunner(Generic[TApplication, TTrackingRecorder]):
|
|
|
198
189
|
application_name=self.subscription.name,
|
|
199
190
|
notification_id=notification_id,
|
|
200
191
|
timeout=timeout,
|
|
201
|
-
interrupt=self.
|
|
192
|
+
interrupt=self._is_stopping,
|
|
202
193
|
)
|
|
203
194
|
except WaitInterruptedError:
|
|
204
|
-
|
|
205
|
-
|
|
195
|
+
if self.thread_error is not None:
|
|
196
|
+
raise self.thread_error from None
|
|
206
197
|
|
|
207
198
|
def __enter__(self) -> Self:
|
|
208
199
|
return self
|