eventsourcing 9.2.22__py3-none-any.whl → 9.3.0__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/__init__.py +1 -1
- eventsourcing/application.py +116 -135
- eventsourcing/cipher.py +15 -12
- eventsourcing/dispatch.py +31 -91
- eventsourcing/domain.py +220 -226
- eventsourcing/examples/__init__.py +0 -0
- eventsourcing/examples/aggregate1/__init__.py +0 -0
- eventsourcing/examples/aggregate1/application.py +27 -0
- eventsourcing/examples/aggregate1/domainmodel.py +16 -0
- eventsourcing/examples/aggregate1/test_application.py +37 -0
- eventsourcing/examples/aggregate2/__init__.py +0 -0
- eventsourcing/examples/aggregate2/application.py +27 -0
- eventsourcing/examples/aggregate2/domainmodel.py +22 -0
- eventsourcing/examples/aggregate2/test_application.py +37 -0
- eventsourcing/examples/aggregate3/__init__.py +0 -0
- eventsourcing/examples/aggregate3/application.py +27 -0
- eventsourcing/examples/aggregate3/domainmodel.py +38 -0
- eventsourcing/examples/aggregate3/test_application.py +37 -0
- eventsourcing/examples/aggregate4/__init__.py +0 -0
- eventsourcing/examples/aggregate4/application.py +27 -0
- eventsourcing/examples/aggregate4/domainmodel.py +114 -0
- eventsourcing/examples/aggregate4/test_application.py +38 -0
- eventsourcing/examples/aggregate5/__init__.py +0 -0
- eventsourcing/examples/aggregate5/application.py +27 -0
- eventsourcing/examples/aggregate5/domainmodel.py +131 -0
- eventsourcing/examples/aggregate5/test_application.py +38 -0
- eventsourcing/examples/aggregate6/__init__.py +0 -0
- eventsourcing/examples/aggregate6/application.py +30 -0
- eventsourcing/examples/aggregate6/domainmodel.py +123 -0
- eventsourcing/examples/aggregate6/test_application.py +38 -0
- eventsourcing/examples/aggregate6a/__init__.py +0 -0
- eventsourcing/examples/aggregate6a/application.py +40 -0
- eventsourcing/examples/aggregate6a/domainmodel.py +149 -0
- eventsourcing/examples/aggregate6a/test_application.py +45 -0
- eventsourcing/examples/aggregate7/__init__.py +0 -0
- eventsourcing/examples/aggregate7/application.py +48 -0
- eventsourcing/examples/aggregate7/domainmodel.py +144 -0
- eventsourcing/examples/aggregate7/persistence.py +57 -0
- eventsourcing/examples/aggregate7/test_application.py +38 -0
- eventsourcing/examples/aggregate7/test_compression_and_encryption.py +45 -0
- eventsourcing/examples/aggregate7/test_snapshotting_intervals.py +67 -0
- eventsourcing/examples/aggregate7a/__init__.py +0 -0
- eventsourcing/examples/aggregate7a/application.py +56 -0
- eventsourcing/examples/aggregate7a/domainmodel.py +170 -0
- eventsourcing/examples/aggregate7a/test_application.py +46 -0
- eventsourcing/examples/aggregate7a/test_compression_and_encryption.py +45 -0
- eventsourcing/examples/aggregate8/__init__.py +0 -0
- eventsourcing/examples/aggregate8/application.py +47 -0
- eventsourcing/examples/aggregate8/domainmodel.py +65 -0
- eventsourcing/examples/aggregate8/persistence.py +57 -0
- eventsourcing/examples/aggregate8/test_application.py +37 -0
- eventsourcing/examples/aggregate8/test_compression_and_encryption.py +44 -0
- eventsourcing/examples/aggregate8/test_snapshotting_intervals.py +38 -0
- eventsourcing/examples/bankaccounts/__init__.py +0 -0
- eventsourcing/examples/bankaccounts/application.py +70 -0
- eventsourcing/examples/bankaccounts/domainmodel.py +56 -0
- eventsourcing/examples/bankaccounts/test.py +173 -0
- eventsourcing/examples/cargoshipping/__init__.py +0 -0
- eventsourcing/examples/cargoshipping/application.py +126 -0
- eventsourcing/examples/cargoshipping/domainmodel.py +330 -0
- eventsourcing/examples/cargoshipping/interface.py +143 -0
- eventsourcing/examples/cargoshipping/test.py +231 -0
- eventsourcing/examples/contentmanagement/__init__.py +0 -0
- eventsourcing/examples/contentmanagement/application.py +118 -0
- eventsourcing/examples/contentmanagement/domainmodel.py +69 -0
- eventsourcing/examples/contentmanagement/test.py +180 -0
- eventsourcing/examples/contentmanagement/utils.py +26 -0
- eventsourcing/examples/contentmanagementsystem/__init__.py +0 -0
- eventsourcing/examples/contentmanagementsystem/application.py +54 -0
- eventsourcing/examples/contentmanagementsystem/postgres.py +17 -0
- eventsourcing/examples/contentmanagementsystem/sqlite.py +17 -0
- eventsourcing/examples/contentmanagementsystem/system.py +14 -0
- eventsourcing/examples/contentmanagementsystem/test_system.py +180 -0
- eventsourcing/examples/searchablecontent/__init__.py +0 -0
- eventsourcing/examples/searchablecontent/application.py +45 -0
- eventsourcing/examples/searchablecontent/persistence.py +23 -0
- eventsourcing/examples/searchablecontent/postgres.py +118 -0
- eventsourcing/examples/searchablecontent/sqlite.py +136 -0
- eventsourcing/examples/searchablecontent/test_application.py +110 -0
- eventsourcing/examples/searchablecontent/test_recorder.py +68 -0
- eventsourcing/examples/searchabletimestamps/__init__.py +0 -0
- eventsourcing/examples/searchabletimestamps/application.py +32 -0
- eventsourcing/examples/searchabletimestamps/persistence.py +20 -0
- eventsourcing/examples/searchabletimestamps/postgres.py +110 -0
- eventsourcing/examples/searchabletimestamps/sqlite.py +99 -0
- eventsourcing/examples/searchabletimestamps/test_searchabletimestamps.py +94 -0
- eventsourcing/examples/test_invoice.py +176 -0
- eventsourcing/examples/test_parking_lot.py +206 -0
- eventsourcing/interface.py +2 -2
- eventsourcing/persistence.py +85 -81
- eventsourcing/popo.py +30 -31
- eventsourcing/postgres.py +379 -590
- eventsourcing/sqlite.py +91 -99
- eventsourcing/system.py +52 -57
- eventsourcing/tests/application.py +20 -32
- eventsourcing/tests/application_tests/__init__.py +0 -0
- eventsourcing/tests/application_tests/test_application_with_automatic_snapshotting.py +55 -0
- eventsourcing/tests/application_tests/test_application_with_popo.py +22 -0
- eventsourcing/tests/application_tests/test_application_with_postgres.py +75 -0
- eventsourcing/tests/application_tests/test_application_with_sqlite.py +72 -0
- eventsourcing/tests/application_tests/test_cache.py +134 -0
- eventsourcing/tests/application_tests/test_event_sourced_log.py +162 -0
- eventsourcing/tests/application_tests/test_notificationlog.py +232 -0
- eventsourcing/tests/application_tests/test_notificationlogreader.py +126 -0
- eventsourcing/tests/application_tests/test_processapplication.py +110 -0
- eventsourcing/tests/application_tests/test_processingpolicy.py +109 -0
- eventsourcing/tests/application_tests/test_repository.py +504 -0
- eventsourcing/tests/application_tests/test_snapshotting.py +68 -0
- eventsourcing/tests/application_tests/test_upcasting.py +459 -0
- eventsourcing/tests/docs_tests/__init__.py +0 -0
- eventsourcing/tests/docs_tests/test_docs.py +293 -0
- eventsourcing/tests/domain.py +1 -1
- eventsourcing/tests/domain_tests/__init__.py +0 -0
- eventsourcing/tests/domain_tests/test_aggregate.py +1180 -0
- eventsourcing/tests/domain_tests/test_aggregate_decorators.py +1604 -0
- eventsourcing/tests/domain_tests/test_domainevent.py +80 -0
- eventsourcing/tests/interface_tests/__init__.py +0 -0
- eventsourcing/tests/interface_tests/test_remotenotificationlog.py +258 -0
- eventsourcing/tests/persistence.py +52 -50
- eventsourcing/tests/persistence_tests/__init__.py +0 -0
- eventsourcing/tests/persistence_tests/test_aes.py +93 -0
- eventsourcing/tests/persistence_tests/test_connection_pool.py +722 -0
- eventsourcing/tests/persistence_tests/test_eventstore.py +72 -0
- eventsourcing/tests/persistence_tests/test_infrastructure_factory.py +21 -0
- eventsourcing/tests/persistence_tests/test_mapper.py +113 -0
- eventsourcing/tests/persistence_tests/test_noninterleaving_notification_ids.py +69 -0
- eventsourcing/tests/persistence_tests/test_popo.py +124 -0
- eventsourcing/tests/persistence_tests/test_postgres.py +1119 -0
- eventsourcing/tests/persistence_tests/test_sqlite.py +348 -0
- eventsourcing/tests/persistence_tests/test_transcoder.py +44 -0
- eventsourcing/tests/postgres_utils.py +7 -7
- eventsourcing/tests/system_tests/__init__.py +0 -0
- eventsourcing/tests/system_tests/test_runner.py +935 -0
- eventsourcing/tests/system_tests/test_system.py +284 -0
- eventsourcing/tests/utils_tests/__init__.py +0 -0
- eventsourcing/tests/utils_tests/test_utils.py +226 -0
- eventsourcing/utils.py +47 -50
- {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0.dist-info}/METADATA +29 -79
- eventsourcing-9.3.0.dist-info/RECORD +145 -0
- {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0.dist-info}/WHEEL +1 -2
- eventsourcing-9.2.22.dist-info/RECORD +0 -25
- eventsourcing-9.2.22.dist-info/top_level.txt +0 -1
- {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0.dist-info}/AUTHORS +0 -0
- {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0.dist-info}/LICENSE +0 -0
eventsourcing/system.py
CHANGED
|
@@ -8,21 +8,22 @@ from queue import Full, Queue
|
|
|
8
8
|
from threading import Event, Lock, RLock, Thread
|
|
9
9
|
from types import FrameType, ModuleType
|
|
10
10
|
from typing import (
|
|
11
|
+
Any,
|
|
12
|
+
ClassVar,
|
|
11
13
|
Dict,
|
|
12
14
|
Iterable,
|
|
13
15
|
Iterator,
|
|
14
16
|
List,
|
|
15
17
|
Optional,
|
|
16
18
|
Sequence,
|
|
17
|
-
Set,
|
|
18
19
|
Tuple,
|
|
19
20
|
Type,
|
|
20
21
|
Union,
|
|
21
22
|
cast,
|
|
22
23
|
)
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
from typing_extensions import Self
|
|
26
|
+
|
|
26
27
|
from eventsourcing.application import (
|
|
27
28
|
Application,
|
|
28
29
|
NotificationLog,
|
|
@@ -54,10 +55,10 @@ class Follower(Application):
|
|
|
54
55
|
new domain event notifications through its :func:`policy` method.
|
|
55
56
|
"""
|
|
56
57
|
|
|
57
|
-
follow_topics: Sequence[str] = []
|
|
58
|
+
follow_topics: ClassVar[Sequence[str]] = []
|
|
58
59
|
pull_section_size = 10
|
|
59
60
|
|
|
60
|
-
def __init__(self, env:
|
|
61
|
+
def __init__(self, env: EnvType | None = None) -> None:
|
|
61
62
|
super().__init__(env)
|
|
62
63
|
self.readers: Dict[str, NotificationLogReader] = {}
|
|
63
64
|
self.mappers: Dict[str, Mapper] = {}
|
|
@@ -90,7 +91,7 @@ class Follower(Application):
|
|
|
90
91
|
|
|
91
92
|
# @retry(IntegrityError, max_attempts=100)
|
|
92
93
|
def pull_and_process(
|
|
93
|
-
self, leader_name: str, start:
|
|
94
|
+
self, leader_name: str, start: int | None = None, stop: int | None = None
|
|
94
95
|
) -> None:
|
|
95
96
|
"""
|
|
96
97
|
Pull and process new domain event notifications.
|
|
@@ -107,7 +108,7 @@ class Follower(Application):
|
|
|
107
108
|
self.process_event(domain_event, tracking)
|
|
108
109
|
|
|
109
110
|
def pull_notifications(
|
|
110
|
-
self, leader_name: str, start: int, stop:
|
|
111
|
+
self, leader_name: str, start: int, stop: int | None = None
|
|
111
112
|
) -> Iterator[List[Notification]]:
|
|
112
113
|
"""
|
|
113
114
|
Pulls batches of unseen :class:`~eventsourcing.persistence.Notification`
|
|
@@ -122,8 +123,7 @@ class Follower(Application):
|
|
|
122
123
|
) -> List[Notification]:
|
|
123
124
|
if self.follow_topics:
|
|
124
125
|
return [n for n in notifications if n.topic in self.follow_topics]
|
|
125
|
-
|
|
126
|
-
return notifications
|
|
126
|
+
return notifications
|
|
127
127
|
|
|
128
128
|
def convert_notifications(
|
|
129
129
|
self, leader_name: str, notifications: Iterable[Notification]
|
|
@@ -226,7 +226,7 @@ class Leader(Application):
|
|
|
226
226
|
domain event notifications to be pulled and processed.
|
|
227
227
|
"""
|
|
228
228
|
|
|
229
|
-
def __init__(self, env:
|
|
229
|
+
def __init__(self, env: EnvType | None = None) -> None:
|
|
230
230
|
super().__init__(env)
|
|
231
231
|
self.followers: List[RecordingEventReceiver] = []
|
|
232
232
|
|
|
@@ -269,7 +269,7 @@ class System:
|
|
|
269
269
|
Defines a system of applications.
|
|
270
270
|
"""
|
|
271
271
|
|
|
272
|
-
__caller_modules: Dict[int, ModuleType] = {}
|
|
272
|
+
__caller_modules: ClassVar[Dict[int, ModuleType]] = {}
|
|
273
273
|
|
|
274
274
|
def __init__(
|
|
275
275
|
self,
|
|
@@ -281,7 +281,7 @@ class System:
|
|
|
281
281
|
type(self).__caller_modules[id(self)] = module
|
|
282
282
|
|
|
283
283
|
# Build nodes and edges.
|
|
284
|
-
self.edges: List[Tuple[str, str]] =
|
|
284
|
+
self.edges: List[Tuple[str, str]] = []
|
|
285
285
|
classes: Dict[str, Type[Application]] = {}
|
|
286
286
|
for pipe in pipes:
|
|
287
287
|
follower_cls = None
|
|
@@ -330,7 +330,7 @@ class System:
|
|
|
330
330
|
|
|
331
331
|
@property
|
|
332
332
|
def leaders_only(self) -> List[str]:
|
|
333
|
-
return [name for name in self.leads
|
|
333
|
+
return [name for name in self.leads if name not in self.follows]
|
|
334
334
|
|
|
335
335
|
@property
|
|
336
336
|
def followers(self) -> List[str]:
|
|
@@ -338,7 +338,7 @@ class System:
|
|
|
338
338
|
|
|
339
339
|
@property
|
|
340
340
|
def processors(self) -> List[str]:
|
|
341
|
-
return [name for name in self.leads
|
|
341
|
+
return [name for name in self.leads if name in self.follows]
|
|
342
342
|
|
|
343
343
|
def get_app_cls(self, name: str) -> Type[Application]:
|
|
344
344
|
cls = resolve_topic(self.nodes[name])
|
|
@@ -349,14 +349,9 @@ class System:
|
|
|
349
349
|
cls = self.get_app_cls(name)
|
|
350
350
|
if issubclass(cls, Leader):
|
|
351
351
|
return cls
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
(Leader, cls),
|
|
356
|
-
{},
|
|
357
|
-
)
|
|
358
|
-
assert issubclass(cls, Leader)
|
|
359
|
-
return cls
|
|
352
|
+
cls = type(cls.name, (Leader, cls), {})
|
|
353
|
+
assert issubclass(cls, Leader)
|
|
354
|
+
return cls
|
|
360
355
|
|
|
361
356
|
def follower_cls(self, name: str) -> Type[Follower]:
|
|
362
357
|
cls = self.get_app_cls(name)
|
|
@@ -364,11 +359,11 @@ class System:
|
|
|
364
359
|
return cls
|
|
365
360
|
|
|
366
361
|
@property
|
|
367
|
-
def topic(self) ->
|
|
362
|
+
def topic(self) -> str | None:
|
|
368
363
|
"""
|
|
369
364
|
Returns a topic to the system object, if constructed as a module attribute.
|
|
370
365
|
"""
|
|
371
|
-
topic:
|
|
366
|
+
topic: str | None = None
|
|
372
367
|
module = System.__caller_modules[id(self)]
|
|
373
368
|
for name, value in module.__dict__.items():
|
|
374
369
|
if value is self:
|
|
@@ -382,7 +377,7 @@ class Runner(ABC):
|
|
|
382
377
|
Abstract base class for system runners.
|
|
383
378
|
"""
|
|
384
379
|
|
|
385
|
-
def __init__(self, system: System, env:
|
|
380
|
+
def __init__(self, system: System, env: EnvType | None = None):
|
|
386
381
|
self.system = system
|
|
387
382
|
self.env = env
|
|
388
383
|
self.is_started = False
|
|
@@ -393,7 +388,7 @@ class Runner(ABC):
|
|
|
393
388
|
Starts the runner.
|
|
394
389
|
"""
|
|
395
390
|
if self.is_started:
|
|
396
|
-
raise
|
|
391
|
+
raise RunnerAlreadyStartedError
|
|
397
392
|
self.is_started = True
|
|
398
393
|
|
|
399
394
|
@abstractmethod
|
|
@@ -409,7 +404,7 @@ class Runner(ABC):
|
|
|
409
404
|
"""
|
|
410
405
|
|
|
411
406
|
|
|
412
|
-
class
|
|
407
|
+
class RunnerAlreadyStartedError(Exception):
|
|
413
408
|
"""
|
|
414
409
|
Raised when runner is already started.
|
|
415
410
|
"""
|
|
@@ -438,7 +433,7 @@ class SingleThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
438
433
|
Runs a :class:`System` in a single thread.
|
|
439
434
|
"""
|
|
440
435
|
|
|
441
|
-
def __init__(self, system: System, env:
|
|
436
|
+
def __init__(self, system: System, env: EnvType | None = None):
|
|
442
437
|
"""
|
|
443
438
|
Initialises runner with the given :class:`System`.
|
|
444
439
|
"""
|
|
@@ -446,7 +441,7 @@ class SingleThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
446
441
|
self.apps: Dict[str, Application] = {}
|
|
447
442
|
self._recording_events_received: List[RecordingEvent] = []
|
|
448
443
|
self._prompted_names_lock = Lock()
|
|
449
|
-
self._prompted_names:
|
|
444
|
+
self._prompted_names: set[str] = set()
|
|
450
445
|
self._processing_lock = Lock()
|
|
451
446
|
|
|
452
447
|
# Construct followers.
|
|
@@ -533,13 +528,20 @@ class SingleThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
533
528
|
assert isinstance(app, cls)
|
|
534
529
|
return app
|
|
535
530
|
|
|
531
|
+
def __enter__(self) -> Self:
|
|
532
|
+
self.start()
|
|
533
|
+
return self
|
|
534
|
+
|
|
535
|
+
def __exit__(self, *args: object, **kwargs: Any) -> None:
|
|
536
|
+
self.stop()
|
|
537
|
+
|
|
536
538
|
|
|
537
539
|
class NewSingleThreadedRunner(Runner, RecordingEventReceiver):
|
|
538
540
|
"""
|
|
539
541
|
Runs a :class:`System` in a single thread.
|
|
540
542
|
"""
|
|
541
543
|
|
|
542
|
-
def __init__(self, system: System, env:
|
|
544
|
+
def __init__(self, system: System, env: EnvType | None = None):
|
|
543
545
|
"""
|
|
544
546
|
Initialises runner with the given :class:`System`.
|
|
545
547
|
"""
|
|
@@ -665,9 +667,9 @@ class NewSingleThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
665
667
|
),
|
|
666
668
|
)
|
|
667
669
|
|
|
668
|
-
self._previous_max_notification_ids[
|
|
669
|
-
|
|
670
|
-
|
|
670
|
+
self._previous_max_notification_ids[leader_name] = (
|
|
671
|
+
recording_event.recordings[-1].notification.id
|
|
672
|
+
)
|
|
671
673
|
|
|
672
674
|
finally:
|
|
673
675
|
self._processing_lock.release()
|
|
@@ -689,7 +691,7 @@ class MultiThreadedRunner(Runner):
|
|
|
689
691
|
for each :class:`Follower` in the system definition.
|
|
690
692
|
"""
|
|
691
693
|
|
|
692
|
-
def __init__(self, system: System, env:
|
|
694
|
+
def __init__(self, system: System, env: EnvType | None = None):
|
|
693
695
|
"""
|
|
694
696
|
Initialises runner with the given :class:`System`.
|
|
695
697
|
"""
|
|
@@ -753,7 +755,7 @@ class MultiThreadedRunner(Runner):
|
|
|
753
755
|
thread = self.threads[follower.name]
|
|
754
756
|
leader.lead(thread)
|
|
755
757
|
|
|
756
|
-
def watch_for_errors(self, timeout:
|
|
758
|
+
def watch_for_errors(self, timeout: float | None = None) -> bool:
|
|
757
759
|
if self.has_errored.wait(timeout=timeout):
|
|
758
760
|
self.stop()
|
|
759
761
|
return self.has_errored.is_set()
|
|
@@ -794,7 +796,7 @@ class MultiThreadedRunnerThread(RecordingEventReceiver, Thread):
|
|
|
794
796
|
super().__init__(daemon=True)
|
|
795
797
|
self.follower = follower
|
|
796
798
|
self.has_errored = has_errored
|
|
797
|
-
self.error:
|
|
799
|
+
self.error: Exception | None = None
|
|
798
800
|
self.is_stopping = Event()
|
|
799
801
|
self.has_started = Event()
|
|
800
802
|
self.is_prompted = Event()
|
|
@@ -852,7 +854,7 @@ class NewMultiThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
852
854
|
def __init__(
|
|
853
855
|
self,
|
|
854
856
|
system: System,
|
|
855
|
-
env:
|
|
857
|
+
env: EnvType | None = None,
|
|
856
858
|
):
|
|
857
859
|
"""
|
|
858
860
|
Initialises runner with the given :class:`System`.
|
|
@@ -860,10 +862,8 @@ class NewMultiThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
860
862
|
super().__init__(system=system, env=env)
|
|
861
863
|
self.apps: Dict[str, Application] = {}
|
|
862
864
|
self.pulling_threads: Dict[str, List[PullingThread]] = {}
|
|
863
|
-
self.processing_queues: Dict[str,
|
|
864
|
-
self.all_threads: List[
|
|
865
|
-
Union[PullingThread, ConvertingThread, ProcessingThread]
|
|
866
|
-
] = []
|
|
865
|
+
self.processing_queues: Dict[str, Queue[List[ProcessingJob] | None]] = {}
|
|
866
|
+
self.all_threads: List[PullingThread | ConvertingThread | ProcessingThread] = []
|
|
867
867
|
self.has_errored = Event()
|
|
868
868
|
|
|
869
869
|
# Construct followers.
|
|
@@ -902,7 +902,7 @@ class NewMultiThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
902
902
|
# Start the processing threads.
|
|
903
903
|
for follower_name in self.system.followers:
|
|
904
904
|
follower = cast(Follower, self.apps[follower_name])
|
|
905
|
-
processing_queue: Queue[
|
|
905
|
+
processing_queue: Queue[List[ProcessingJob] | None] = Queue(
|
|
906
906
|
maxsize=self.QUEUE_MAX_SIZE
|
|
907
907
|
)
|
|
908
908
|
self.processing_queues[follower_name] = processing_queue
|
|
@@ -959,7 +959,7 @@ class NewMultiThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
959
959
|
assert isinstance(leader, Leader)
|
|
960
960
|
leader.lead(self)
|
|
961
961
|
|
|
962
|
-
def watch_for_errors(self, timeout:
|
|
962
|
+
def watch_for_errors(self, timeout: float | None = None) -> bool:
|
|
963
963
|
if self.has_errored.wait(timeout=timeout):
|
|
964
964
|
self.stop()
|
|
965
965
|
return self.has_errored.is_set()
|
|
@@ -1004,12 +1004,12 @@ class PullingThread(Thread):
|
|
|
1004
1004
|
):
|
|
1005
1005
|
super().__init__(daemon=True)
|
|
1006
1006
|
self.overflow_event = Event()
|
|
1007
|
-
self.recording_event_queue: Queue[
|
|
1007
|
+
self.recording_event_queue: Queue[RecordingEvent | None] = Queue(maxsize=100)
|
|
1008
1008
|
self.converting_queue = converting_queue
|
|
1009
1009
|
self.receive_lock = Lock()
|
|
1010
1010
|
self.follower = follower
|
|
1011
1011
|
self.leader_name = leader_name
|
|
1012
|
-
self.error:
|
|
1012
|
+
self.error: Exception | None = None
|
|
1013
1013
|
self.has_errored = has_errored
|
|
1014
1014
|
self.is_stopping = Event()
|
|
1015
1015
|
self.has_started = Event()
|
|
@@ -1075,7 +1075,7 @@ class ConvertingThread(Thread):
|
|
|
1075
1075
|
def __init__(
|
|
1076
1076
|
self,
|
|
1077
1077
|
converting_queue: Queue[ConvertingJob],
|
|
1078
|
-
processing_queue: Queue[
|
|
1078
|
+
processing_queue: Queue[List[ProcessingJob] | None],
|
|
1079
1079
|
follower: Follower,
|
|
1080
1080
|
leader_name: str,
|
|
1081
1081
|
has_errored: Event,
|
|
@@ -1085,7 +1085,7 @@ class ConvertingThread(Thread):
|
|
|
1085
1085
|
self.processing_queue = processing_queue
|
|
1086
1086
|
self.follower = follower
|
|
1087
1087
|
self.leader_name = leader_name
|
|
1088
|
-
self.error:
|
|
1088
|
+
self.error: Exception | None = None
|
|
1089
1089
|
self.has_errored = has_errored
|
|
1090
1090
|
self.is_stopping = Event()
|
|
1091
1091
|
self.has_started = Event()
|
|
@@ -1145,14 +1145,14 @@ class ProcessingThread(Thread):
|
|
|
1145
1145
|
|
|
1146
1146
|
def __init__(
|
|
1147
1147
|
self,
|
|
1148
|
-
processing_queue: Queue[
|
|
1148
|
+
processing_queue: Queue[List[ProcessingJob] | None],
|
|
1149
1149
|
follower: Follower,
|
|
1150
1150
|
has_errored: Event,
|
|
1151
1151
|
):
|
|
1152
1152
|
super().__init__(daemon=True)
|
|
1153
1153
|
self.processing_queue = processing_queue
|
|
1154
1154
|
self.follower = follower
|
|
1155
|
-
self.error:
|
|
1155
|
+
self.error: Exception | None = None
|
|
1156
1156
|
self.has_errored = has_errored
|
|
1157
1157
|
self.is_stopping = Event()
|
|
1158
1158
|
self.has_started = Event()
|
|
@@ -1212,22 +1212,17 @@ class NotificationLogReader:
|
|
|
1212
1212
|
event notifications in the notification log from the start position
|
|
1213
1213
|
have been yielded.
|
|
1214
1214
|
"""
|
|
1215
|
-
section_id = "{},{
|
|
1215
|
+
section_id = f"{start},{start + self.section_size - 1}"
|
|
1216
1216
|
while True:
|
|
1217
1217
|
section: Section = self.notification_log[section_id]
|
|
1218
|
-
|
|
1219
|
-
# Todo: Reintroduce if supporting
|
|
1220
|
-
# sections with regular alignment?
|
|
1221
|
-
# if item.id < start:
|
|
1222
|
-
# continue
|
|
1223
|
-
yield item
|
|
1218
|
+
yield from section.items
|
|
1224
1219
|
if section.next_id is None:
|
|
1225
1220
|
break
|
|
1226
1221
|
else:
|
|
1227
1222
|
section_id = section.next_id
|
|
1228
1223
|
|
|
1229
1224
|
def select(
|
|
1230
|
-
self, *, start: int, stop:
|
|
1225
|
+
self, *, start: int, stop: int | None = None, topics: Sequence[str] = ()
|
|
1231
1226
|
) -> Iterator[List[Notification]]:
|
|
1232
1227
|
"""
|
|
1233
1228
|
Returns a generator that yields lists of event notifications
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import os
|
|
2
4
|
import sys
|
|
3
5
|
import traceback
|
|
@@ -8,15 +10,11 @@ from decimal import Decimal
|
|
|
8
10
|
from threading import Event, get_ident
|
|
9
11
|
from time import sleep
|
|
10
12
|
from timeit import timeit
|
|
13
|
+
from typing import ClassVar, Dict, Type
|
|
11
14
|
from unittest import TestCase
|
|
12
15
|
from uuid import UUID, uuid4
|
|
13
16
|
|
|
14
|
-
from eventsourcing.application import
|
|
15
|
-
AggregateNotFound,
|
|
16
|
-
Application,
|
|
17
|
-
ProcessEvent,
|
|
18
|
-
ProcessingEvent,
|
|
19
|
-
)
|
|
17
|
+
from eventsourcing.application import AggregateNotFoundError, Application
|
|
20
18
|
from eventsourcing.domain import Aggregate
|
|
21
19
|
from eventsourcing.persistence import (
|
|
22
20
|
InfrastructureFactory,
|
|
@@ -31,9 +29,9 @@ TIMEIT_FACTOR = int(os.environ.get("TEST_TIMEIT_FACTOR", default=10))
|
|
|
31
29
|
|
|
32
30
|
|
|
33
31
|
class ExampleApplicationTestCase(TestCase):
|
|
34
|
-
timeit_number = TIMEIT_FACTOR
|
|
35
|
-
started_ats = {}
|
|
36
|
-
counts = {}
|
|
32
|
+
timeit_number: ClassVar[int] = TIMEIT_FACTOR
|
|
33
|
+
started_ats: ClassVar[Dict[Type[TestCase], datetime]] = {}
|
|
34
|
+
counts: ClassVar[Dict[Type[TestCase], int]] = {}
|
|
37
35
|
expected_factory_topic: str
|
|
38
36
|
|
|
39
37
|
def test_example_application(self):
|
|
@@ -137,7 +135,7 @@ class ExampleApplicationTestCase(TestCase):
|
|
|
137
135
|
def test__get_performance_without_snapshotting_enabled(self):
|
|
138
136
|
self._test_get_performance(is_snapshotting_enabled=False)
|
|
139
137
|
|
|
140
|
-
def _test_get_performance(self, is_snapshotting_enabled: bool):
|
|
138
|
+
def _test_get_performance(self, *, is_snapshotting_enabled: bool):
|
|
141
139
|
app = BankAccounts(
|
|
142
140
|
env={"IS_SNAPSHOTTING_ENABLED": "y" if is_snapshotting_enabled else "n"}
|
|
143
141
|
)
|
|
@@ -201,7 +199,7 @@ class BankAccounts(Application):
|
|
|
201
199
|
is_snapshotting_enabled = True
|
|
202
200
|
|
|
203
201
|
def register_transcodings(self, transcoder: Transcoder) -> None:
|
|
204
|
-
super(
|
|
202
|
+
super().register_transcodings(transcoder)
|
|
205
203
|
transcoder.register(EmailAddressAsStr())
|
|
206
204
|
|
|
207
205
|
def open_account(self, full_name, email_address):
|
|
@@ -224,8 +222,8 @@ class BankAccounts(Application):
|
|
|
224
222
|
def get_account(self, account_id: UUID) -> BankAccount:
|
|
225
223
|
try:
|
|
226
224
|
aggregate = self.repository.get(account_id)
|
|
227
|
-
except
|
|
228
|
-
raise self.AccountNotFoundError(account_id)
|
|
225
|
+
except AggregateNotFoundError:
|
|
226
|
+
raise self.AccountNotFoundError(account_id) from None
|
|
229
227
|
else:
|
|
230
228
|
assert isinstance(aggregate, BankAccount)
|
|
231
229
|
return aggregate
|
|
@@ -270,10 +268,8 @@ class ApplicationTestCase(TestCase):
|
|
|
270
268
|
Application(env={"PERSISTENCE_MODULE": "eventsourcing.application"})
|
|
271
269
|
self.assertEqual(
|
|
272
270
|
cm.exception.args[0],
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
"'eventsourcing.application', expected 1."
|
|
276
|
-
),
|
|
271
|
+
"Found 0 infrastructure factory classes in "
|
|
272
|
+
"'eventsourcing.application', expected 1.",
|
|
277
273
|
)
|
|
278
274
|
|
|
279
275
|
with self.assertRaises(AssertionError) as cm:
|
|
@@ -282,10 +278,8 @@ class ApplicationTestCase(TestCase):
|
|
|
282
278
|
)
|
|
283
279
|
self.assertEqual(
|
|
284
280
|
cm.exception.args[0],
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
"eventsourcing.application:Application"
|
|
288
|
-
),
|
|
281
|
+
"Not an infrastructure factory class or module: "
|
|
282
|
+
"eventsourcing.application:Application",
|
|
289
283
|
)
|
|
290
284
|
|
|
291
285
|
def test_save_returns_recording_event(self):
|
|
@@ -318,13 +312,11 @@ class ApplicationTestCase(TestCase):
|
|
|
318
312
|
app.take_snapshot(uuid4())
|
|
319
313
|
self.assertEqual(
|
|
320
314
|
cm.exception.args[0],
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
"application class."
|
|
327
|
-
),
|
|
315
|
+
"Can't take snapshot without snapshots store. Please "
|
|
316
|
+
"set environment variable IS_SNAPSHOTTING_ENABLED to "
|
|
317
|
+
"a true value (e.g. 'y'), or set 'is_snapshotting_enabled' "
|
|
318
|
+
"on application class, or set 'snapshotting_intervals' on "
|
|
319
|
+
"application class.",
|
|
328
320
|
)
|
|
329
321
|
|
|
330
322
|
def test_application_with_cached_aggregates_and_fastforward(self):
|
|
@@ -502,7 +494,3 @@ class ApplicationTestCase(TestCase):
|
|
|
502
494
|
self.assertEqual(
|
|
503
495
|
"'log' is deprecated, use 'notifications' instead", w[-1].message.args[0]
|
|
504
496
|
)
|
|
505
|
-
|
|
506
|
-
def test_process_event_class(self):
|
|
507
|
-
# Check the old 'ProcessEvent' class still works.
|
|
508
|
-
self.assertTrue(issubclass(ProcessEvent, ProcessingEvent))
|
|
File without changes
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from typing import ClassVar, Dict, Type
|
|
5
|
+
from unittest import TestCase
|
|
6
|
+
|
|
7
|
+
from eventsourcing.domain import Aggregate, MutableOrImmutableAggregate
|
|
8
|
+
from eventsourcing.tests.application import BankAccounts
|
|
9
|
+
from eventsourcing.tests.domain import BankAccount
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BankAccountsWithAutomaticSnapshotting(BankAccounts):
|
|
13
|
+
is_snapshotting_enabled = False
|
|
14
|
+
snapshotting_intervals: ClassVar[
|
|
15
|
+
Dict[Type[MutableOrImmutableAggregate], int] | None
|
|
16
|
+
] = {BankAccount: 5}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestApplicationWithAutomaticSnapshotting(TestCase):
|
|
20
|
+
def test(self):
|
|
21
|
+
app = BankAccountsWithAutomaticSnapshotting()
|
|
22
|
+
|
|
23
|
+
# Check snapshotting is enabled by setting snapshotting_intervals only.
|
|
24
|
+
self.assertTrue(app.snapshots)
|
|
25
|
+
|
|
26
|
+
# Open an account.
|
|
27
|
+
account_id = app.open_account("Alice", "alice@example.com")
|
|
28
|
+
|
|
29
|
+
# Check there are no snapshots.
|
|
30
|
+
snapshots = list(app.snapshots.get(account_id))
|
|
31
|
+
self.assertEqual(len(snapshots), 0)
|
|
32
|
+
|
|
33
|
+
# Trigger twelve more events.
|
|
34
|
+
for _ in range(12):
|
|
35
|
+
app.credit_account(account_id, Decimal("10.00"))
|
|
36
|
+
|
|
37
|
+
# Check the account is at version 13.
|
|
38
|
+
account = app.get_account(account_id)
|
|
39
|
+
self.assertEqual(account.version, 13)
|
|
40
|
+
|
|
41
|
+
# Check snapshots have been taken at regular intervals.
|
|
42
|
+
snapshots = list(app.snapshots.get(account_id))
|
|
43
|
+
self.assertEqual(len(snapshots), 2)
|
|
44
|
+
self.assertEqual(snapshots[0].originator_version, 5)
|
|
45
|
+
self.assertEqual(snapshots[1].originator_version, 10)
|
|
46
|
+
|
|
47
|
+
# Check another type of aggregate is not snapshotted.
|
|
48
|
+
aggregate = Aggregate()
|
|
49
|
+
for _ in range(10):
|
|
50
|
+
aggregate.trigger_event(Aggregate.Event)
|
|
51
|
+
app.save(aggregate)
|
|
52
|
+
|
|
53
|
+
# Check snapshots have not been taken at regular intervals.
|
|
54
|
+
snapshots = list(app.snapshots.get(aggregate.id))
|
|
55
|
+
self.assertEqual(len(snapshots), 0)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from eventsourcing.tests.application import (
|
|
2
|
+
TIMEIT_FACTOR,
|
|
3
|
+
ApplicationTestCase,
|
|
4
|
+
ExampleApplicationTestCase,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestApplicationWithPOPO(ApplicationTestCase):
|
|
9
|
+
def test_application_fastforward_skipping_during_contention(self):
|
|
10
|
+
self.skipTest("POPO is too fast for this test to work")
|
|
11
|
+
|
|
12
|
+
def test_application_fastforward_blocking_during_contention(self):
|
|
13
|
+
self.skipTest("POPO is too fast for this test to make sense")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TestExampleApplicationWithPOPO(ExampleApplicationTestCase):
|
|
17
|
+
timeit_number = 100 * TIMEIT_FACTOR
|
|
18
|
+
expected_factory_topic = "eventsourcing.popo:Factory"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
del ApplicationTestCase
|
|
22
|
+
del ExampleApplicationTestCase
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from unittest import TestCase
|
|
3
|
+
|
|
4
|
+
from eventsourcing.postgres import PostgresDatastore
|
|
5
|
+
from eventsourcing.tests.application import (
|
|
6
|
+
TIMEIT_FACTOR,
|
|
7
|
+
ApplicationTestCase,
|
|
8
|
+
ExampleApplicationTestCase,
|
|
9
|
+
)
|
|
10
|
+
from eventsourcing.tests.postgres_utils import drop_postgres_table
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class WithPostgres(TestCase):
|
|
14
|
+
timeit_number = 5 * TIMEIT_FACTOR
|
|
15
|
+
expected_factory_topic = "eventsourcing.postgres:Factory"
|
|
16
|
+
|
|
17
|
+
def setUp(self) -> None:
|
|
18
|
+
super().setUp()
|
|
19
|
+
|
|
20
|
+
os.environ["PERSISTENCE_MODULE"] = "eventsourcing.postgres"
|
|
21
|
+
os.environ["CREATE_TABLE"] = "y"
|
|
22
|
+
os.environ["POSTGRES_DBNAME"] = "eventsourcing"
|
|
23
|
+
os.environ["POSTGRES_HOST"] = "127.0.0.1"
|
|
24
|
+
os.environ["POSTGRES_PORT"] = "5432"
|
|
25
|
+
os.environ["POSTGRES_USER"] = "eventsourcing"
|
|
26
|
+
os.environ["POSTGRES_PASSWORD"] = "eventsourcing" # noqa: S105
|
|
27
|
+
os.environ["POSTGRES_SCHEMA"] = "public"
|
|
28
|
+
|
|
29
|
+
db = PostgresDatastore(
|
|
30
|
+
os.getenv("POSTGRES_DBNAME"),
|
|
31
|
+
os.getenv("POSTGRES_HOST"),
|
|
32
|
+
os.getenv("POSTGRES_PORT"),
|
|
33
|
+
os.getenv("POSTGRES_USER"),
|
|
34
|
+
os.getenv("POSTGRES_PASSWORD"),
|
|
35
|
+
)
|
|
36
|
+
drop_postgres_table(db, "public.bankaccounts_events")
|
|
37
|
+
drop_postgres_table(db, "public.bankaccounts_snapshots")
|
|
38
|
+
db.close()
|
|
39
|
+
|
|
40
|
+
def tearDown(self) -> None:
|
|
41
|
+
db = PostgresDatastore(
|
|
42
|
+
os.getenv("POSTGRES_DBNAME"),
|
|
43
|
+
os.getenv("POSTGRES_HOST"),
|
|
44
|
+
os.getenv("POSTGRES_PORT"),
|
|
45
|
+
os.getenv("POSTGRES_USER"),
|
|
46
|
+
os.getenv("POSTGRES_PASSWORD"),
|
|
47
|
+
)
|
|
48
|
+
drop_postgres_table(db, "public.bankaccounts_events")
|
|
49
|
+
drop_postgres_table(db, "public.bankaccounts_snapshots")
|
|
50
|
+
|
|
51
|
+
del os.environ["PERSISTENCE_MODULE"]
|
|
52
|
+
del os.environ["CREATE_TABLE"]
|
|
53
|
+
del os.environ["POSTGRES_DBNAME"]
|
|
54
|
+
del os.environ["POSTGRES_HOST"]
|
|
55
|
+
del os.environ["POSTGRES_PORT"]
|
|
56
|
+
del os.environ["POSTGRES_USER"]
|
|
57
|
+
del os.environ["POSTGRES_PASSWORD"]
|
|
58
|
+
del os.environ["POSTGRES_SCHEMA"]
|
|
59
|
+
db.close()
|
|
60
|
+
|
|
61
|
+
super().tearDown()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class TestApplicationWithPostgres(ApplicationTestCase, WithPostgres):
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class TestExampleApplicationWithPostgres(ExampleApplicationTestCase, WithPostgres):
|
|
69
|
+
timeit_number = 5 * TIMEIT_FACTOR
|
|
70
|
+
expected_factory_topic = "eventsourcing.postgres:Factory"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
del ApplicationTestCase
|
|
74
|
+
del ExampleApplicationTestCase
|
|
75
|
+
del WithPostgres
|