eventsourcing 9.2.22__py3-none-any.whl → 9.3.0a1__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 +106 -135
- eventsourcing/cipher.py +15 -12
- eventsourcing/dispatch.py +31 -91
- eventsourcing/domain.py +138 -143
- 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 +128 -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 +174 -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 +111 -0
- eventsourcing/examples/searchablecontent/test_recorder.py +69 -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 +91 -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 +361 -578
- eventsourcing/sqlite.py +91 -99
- eventsourcing/system.py +42 -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 +1159 -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 +49 -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 +1121 -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 +287 -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.0a1.dist-info}/METADATA +28 -80
- eventsourcing-9.3.0a1.dist-info/RECORD +144 -0
- {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0a1.dist-info}/WHEEL +1 -2
- eventsourcing-9.2.22.dist-info/AUTHORS +0 -10
- 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.0a1.dist-info}/LICENSE +0 -0
eventsourcing/system.py
CHANGED
|
@@ -8,21 +8,19 @@ 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
|
+
ClassVar,
|
|
11
12
|
Dict,
|
|
12
13
|
Iterable,
|
|
13
14
|
Iterator,
|
|
14
15
|
List,
|
|
15
16
|
Optional,
|
|
16
17
|
Sequence,
|
|
17
|
-
Set,
|
|
18
18
|
Tuple,
|
|
19
19
|
Type,
|
|
20
20
|
Union,
|
|
21
21
|
cast,
|
|
22
22
|
)
|
|
23
23
|
|
|
24
|
-
# For backwards compatibility of import statements...
|
|
25
|
-
from eventsourcing.application import ProcessEvent # noqa: F401
|
|
26
24
|
from eventsourcing.application import (
|
|
27
25
|
Application,
|
|
28
26
|
NotificationLog,
|
|
@@ -54,10 +52,10 @@ class Follower(Application):
|
|
|
54
52
|
new domain event notifications through its :func:`policy` method.
|
|
55
53
|
"""
|
|
56
54
|
|
|
57
|
-
follow_topics: Sequence[str] = []
|
|
55
|
+
follow_topics: ClassVar[Sequence[str]] = []
|
|
58
56
|
pull_section_size = 10
|
|
59
57
|
|
|
60
|
-
def __init__(self, env:
|
|
58
|
+
def __init__(self, env: EnvType | None = None) -> None:
|
|
61
59
|
super().__init__(env)
|
|
62
60
|
self.readers: Dict[str, NotificationLogReader] = {}
|
|
63
61
|
self.mappers: Dict[str, Mapper] = {}
|
|
@@ -90,7 +88,7 @@ class Follower(Application):
|
|
|
90
88
|
|
|
91
89
|
# @retry(IntegrityError, max_attempts=100)
|
|
92
90
|
def pull_and_process(
|
|
93
|
-
self, leader_name: str, start:
|
|
91
|
+
self, leader_name: str, start: int | None = None, stop: int | None = None
|
|
94
92
|
) -> None:
|
|
95
93
|
"""
|
|
96
94
|
Pull and process new domain event notifications.
|
|
@@ -107,7 +105,7 @@ class Follower(Application):
|
|
|
107
105
|
self.process_event(domain_event, tracking)
|
|
108
106
|
|
|
109
107
|
def pull_notifications(
|
|
110
|
-
self, leader_name: str, start: int, stop:
|
|
108
|
+
self, leader_name: str, start: int, stop: int | None = None
|
|
111
109
|
) -> Iterator[List[Notification]]:
|
|
112
110
|
"""
|
|
113
111
|
Pulls batches of unseen :class:`~eventsourcing.persistence.Notification`
|
|
@@ -122,8 +120,7 @@ class Follower(Application):
|
|
|
122
120
|
) -> List[Notification]:
|
|
123
121
|
if self.follow_topics:
|
|
124
122
|
return [n for n in notifications if n.topic in self.follow_topics]
|
|
125
|
-
|
|
126
|
-
return notifications
|
|
123
|
+
return notifications
|
|
127
124
|
|
|
128
125
|
def convert_notifications(
|
|
129
126
|
self, leader_name: str, notifications: Iterable[Notification]
|
|
@@ -226,7 +223,7 @@ class Leader(Application):
|
|
|
226
223
|
domain event notifications to be pulled and processed.
|
|
227
224
|
"""
|
|
228
225
|
|
|
229
|
-
def __init__(self, env:
|
|
226
|
+
def __init__(self, env: EnvType | None = None) -> None:
|
|
230
227
|
super().__init__(env)
|
|
231
228
|
self.followers: List[RecordingEventReceiver] = []
|
|
232
229
|
|
|
@@ -269,7 +266,7 @@ class System:
|
|
|
269
266
|
Defines a system of applications.
|
|
270
267
|
"""
|
|
271
268
|
|
|
272
|
-
__caller_modules: Dict[int, ModuleType] = {}
|
|
269
|
+
__caller_modules: ClassVar[Dict[int, ModuleType]] = {}
|
|
273
270
|
|
|
274
271
|
def __init__(
|
|
275
272
|
self,
|
|
@@ -281,7 +278,7 @@ class System:
|
|
|
281
278
|
type(self).__caller_modules[id(self)] = module
|
|
282
279
|
|
|
283
280
|
# Build nodes and edges.
|
|
284
|
-
self.edges: List[Tuple[str, str]] =
|
|
281
|
+
self.edges: List[Tuple[str, str]] = []
|
|
285
282
|
classes: Dict[str, Type[Application]] = {}
|
|
286
283
|
for pipe in pipes:
|
|
287
284
|
follower_cls = None
|
|
@@ -330,7 +327,7 @@ class System:
|
|
|
330
327
|
|
|
331
328
|
@property
|
|
332
329
|
def leaders_only(self) -> List[str]:
|
|
333
|
-
return [name for name in self.leads
|
|
330
|
+
return [name for name in self.leads if name not in self.follows]
|
|
334
331
|
|
|
335
332
|
@property
|
|
336
333
|
def followers(self) -> List[str]:
|
|
@@ -338,7 +335,7 @@ class System:
|
|
|
338
335
|
|
|
339
336
|
@property
|
|
340
337
|
def processors(self) -> List[str]:
|
|
341
|
-
return [name for name in self.leads
|
|
338
|
+
return [name for name in self.leads if name in self.follows]
|
|
342
339
|
|
|
343
340
|
def get_app_cls(self, name: str) -> Type[Application]:
|
|
344
341
|
cls = resolve_topic(self.nodes[name])
|
|
@@ -349,14 +346,9 @@ class System:
|
|
|
349
346
|
cls = self.get_app_cls(name)
|
|
350
347
|
if issubclass(cls, Leader):
|
|
351
348
|
return cls
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
(Leader, cls),
|
|
356
|
-
{},
|
|
357
|
-
)
|
|
358
|
-
assert issubclass(cls, Leader)
|
|
359
|
-
return cls
|
|
349
|
+
cls = type(cls.name, (Leader, cls), {})
|
|
350
|
+
assert issubclass(cls, Leader)
|
|
351
|
+
return cls
|
|
360
352
|
|
|
361
353
|
def follower_cls(self, name: str) -> Type[Follower]:
|
|
362
354
|
cls = self.get_app_cls(name)
|
|
@@ -364,11 +356,11 @@ class System:
|
|
|
364
356
|
return cls
|
|
365
357
|
|
|
366
358
|
@property
|
|
367
|
-
def topic(self) ->
|
|
359
|
+
def topic(self) -> str | None:
|
|
368
360
|
"""
|
|
369
361
|
Returns a topic to the system object, if constructed as a module attribute.
|
|
370
362
|
"""
|
|
371
|
-
topic:
|
|
363
|
+
topic: str | None = None
|
|
372
364
|
module = System.__caller_modules[id(self)]
|
|
373
365
|
for name, value in module.__dict__.items():
|
|
374
366
|
if value is self:
|
|
@@ -382,7 +374,7 @@ class Runner(ABC):
|
|
|
382
374
|
Abstract base class for system runners.
|
|
383
375
|
"""
|
|
384
376
|
|
|
385
|
-
def __init__(self, system: System, env:
|
|
377
|
+
def __init__(self, system: System, env: EnvType | None = None):
|
|
386
378
|
self.system = system
|
|
387
379
|
self.env = env
|
|
388
380
|
self.is_started = False
|
|
@@ -393,7 +385,7 @@ class Runner(ABC):
|
|
|
393
385
|
Starts the runner.
|
|
394
386
|
"""
|
|
395
387
|
if self.is_started:
|
|
396
|
-
raise
|
|
388
|
+
raise RunnerAlreadyStartedError
|
|
397
389
|
self.is_started = True
|
|
398
390
|
|
|
399
391
|
@abstractmethod
|
|
@@ -409,7 +401,7 @@ class Runner(ABC):
|
|
|
409
401
|
"""
|
|
410
402
|
|
|
411
403
|
|
|
412
|
-
class
|
|
404
|
+
class RunnerAlreadyStartedError(Exception):
|
|
413
405
|
"""
|
|
414
406
|
Raised when runner is already started.
|
|
415
407
|
"""
|
|
@@ -438,7 +430,7 @@ class SingleThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
438
430
|
Runs a :class:`System` in a single thread.
|
|
439
431
|
"""
|
|
440
432
|
|
|
441
|
-
def __init__(self, system: System, env:
|
|
433
|
+
def __init__(self, system: System, env: EnvType | None = None):
|
|
442
434
|
"""
|
|
443
435
|
Initialises runner with the given :class:`System`.
|
|
444
436
|
"""
|
|
@@ -446,7 +438,7 @@ class SingleThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
446
438
|
self.apps: Dict[str, Application] = {}
|
|
447
439
|
self._recording_events_received: List[RecordingEvent] = []
|
|
448
440
|
self._prompted_names_lock = Lock()
|
|
449
|
-
self._prompted_names:
|
|
441
|
+
self._prompted_names: set[str] = set()
|
|
450
442
|
self._processing_lock = Lock()
|
|
451
443
|
|
|
452
444
|
# Construct followers.
|
|
@@ -539,7 +531,7 @@ class NewSingleThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
539
531
|
Runs a :class:`System` in a single thread.
|
|
540
532
|
"""
|
|
541
533
|
|
|
542
|
-
def __init__(self, system: System, env:
|
|
534
|
+
def __init__(self, system: System, env: EnvType | None = None):
|
|
543
535
|
"""
|
|
544
536
|
Initialises runner with the given :class:`System`.
|
|
545
537
|
"""
|
|
@@ -665,9 +657,9 @@ class NewSingleThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
665
657
|
),
|
|
666
658
|
)
|
|
667
659
|
|
|
668
|
-
self._previous_max_notification_ids[
|
|
669
|
-
|
|
670
|
-
|
|
660
|
+
self._previous_max_notification_ids[leader_name] = (
|
|
661
|
+
recording_event.recordings[-1].notification.id
|
|
662
|
+
)
|
|
671
663
|
|
|
672
664
|
finally:
|
|
673
665
|
self._processing_lock.release()
|
|
@@ -689,7 +681,7 @@ class MultiThreadedRunner(Runner):
|
|
|
689
681
|
for each :class:`Follower` in the system definition.
|
|
690
682
|
"""
|
|
691
683
|
|
|
692
|
-
def __init__(self, system: System, env:
|
|
684
|
+
def __init__(self, system: System, env: EnvType | None = None):
|
|
693
685
|
"""
|
|
694
686
|
Initialises runner with the given :class:`System`.
|
|
695
687
|
"""
|
|
@@ -753,7 +745,7 @@ class MultiThreadedRunner(Runner):
|
|
|
753
745
|
thread = self.threads[follower.name]
|
|
754
746
|
leader.lead(thread)
|
|
755
747
|
|
|
756
|
-
def watch_for_errors(self, timeout:
|
|
748
|
+
def watch_for_errors(self, timeout: float | None = None) -> bool:
|
|
757
749
|
if self.has_errored.wait(timeout=timeout):
|
|
758
750
|
self.stop()
|
|
759
751
|
return self.has_errored.is_set()
|
|
@@ -794,7 +786,7 @@ class MultiThreadedRunnerThread(RecordingEventReceiver, Thread):
|
|
|
794
786
|
super().__init__(daemon=True)
|
|
795
787
|
self.follower = follower
|
|
796
788
|
self.has_errored = has_errored
|
|
797
|
-
self.error:
|
|
789
|
+
self.error: Exception | None = None
|
|
798
790
|
self.is_stopping = Event()
|
|
799
791
|
self.has_started = Event()
|
|
800
792
|
self.is_prompted = Event()
|
|
@@ -852,7 +844,7 @@ class NewMultiThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
852
844
|
def __init__(
|
|
853
845
|
self,
|
|
854
846
|
system: System,
|
|
855
|
-
env:
|
|
847
|
+
env: EnvType | None = None,
|
|
856
848
|
):
|
|
857
849
|
"""
|
|
858
850
|
Initialises runner with the given :class:`System`.
|
|
@@ -860,10 +852,8 @@ class NewMultiThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
860
852
|
super().__init__(system=system, env=env)
|
|
861
853
|
self.apps: Dict[str, Application] = {}
|
|
862
854
|
self.pulling_threads: Dict[str, List[PullingThread]] = {}
|
|
863
|
-
self.processing_queues: Dict[str,
|
|
864
|
-
self.all_threads: List[
|
|
865
|
-
Union[PullingThread, ConvertingThread, ProcessingThread]
|
|
866
|
-
] = []
|
|
855
|
+
self.processing_queues: Dict[str, Queue[List[ProcessingJob] | None]] = {}
|
|
856
|
+
self.all_threads: List[PullingThread | ConvertingThread | ProcessingThread] = []
|
|
867
857
|
self.has_errored = Event()
|
|
868
858
|
|
|
869
859
|
# Construct followers.
|
|
@@ -902,7 +892,7 @@ class NewMultiThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
902
892
|
# Start the processing threads.
|
|
903
893
|
for follower_name in self.system.followers:
|
|
904
894
|
follower = cast(Follower, self.apps[follower_name])
|
|
905
|
-
processing_queue: Queue[
|
|
895
|
+
processing_queue: Queue[List[ProcessingJob] | None] = Queue(
|
|
906
896
|
maxsize=self.QUEUE_MAX_SIZE
|
|
907
897
|
)
|
|
908
898
|
self.processing_queues[follower_name] = processing_queue
|
|
@@ -959,7 +949,7 @@ class NewMultiThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
959
949
|
assert isinstance(leader, Leader)
|
|
960
950
|
leader.lead(self)
|
|
961
951
|
|
|
962
|
-
def watch_for_errors(self, timeout:
|
|
952
|
+
def watch_for_errors(self, timeout: float | None = None) -> bool:
|
|
963
953
|
if self.has_errored.wait(timeout=timeout):
|
|
964
954
|
self.stop()
|
|
965
955
|
return self.has_errored.is_set()
|
|
@@ -1004,12 +994,12 @@ class PullingThread(Thread):
|
|
|
1004
994
|
):
|
|
1005
995
|
super().__init__(daemon=True)
|
|
1006
996
|
self.overflow_event = Event()
|
|
1007
|
-
self.recording_event_queue: Queue[
|
|
997
|
+
self.recording_event_queue: Queue[RecordingEvent | None] = Queue(maxsize=100)
|
|
1008
998
|
self.converting_queue = converting_queue
|
|
1009
999
|
self.receive_lock = Lock()
|
|
1010
1000
|
self.follower = follower
|
|
1011
1001
|
self.leader_name = leader_name
|
|
1012
|
-
self.error:
|
|
1002
|
+
self.error: Exception | None = None
|
|
1013
1003
|
self.has_errored = has_errored
|
|
1014
1004
|
self.is_stopping = Event()
|
|
1015
1005
|
self.has_started = Event()
|
|
@@ -1075,7 +1065,7 @@ class ConvertingThread(Thread):
|
|
|
1075
1065
|
def __init__(
|
|
1076
1066
|
self,
|
|
1077
1067
|
converting_queue: Queue[ConvertingJob],
|
|
1078
|
-
processing_queue: Queue[
|
|
1068
|
+
processing_queue: Queue[List[ProcessingJob] | None],
|
|
1079
1069
|
follower: Follower,
|
|
1080
1070
|
leader_name: str,
|
|
1081
1071
|
has_errored: Event,
|
|
@@ -1085,7 +1075,7 @@ class ConvertingThread(Thread):
|
|
|
1085
1075
|
self.processing_queue = processing_queue
|
|
1086
1076
|
self.follower = follower
|
|
1087
1077
|
self.leader_name = leader_name
|
|
1088
|
-
self.error:
|
|
1078
|
+
self.error: Exception | None = None
|
|
1089
1079
|
self.has_errored = has_errored
|
|
1090
1080
|
self.is_stopping = Event()
|
|
1091
1081
|
self.has_started = Event()
|
|
@@ -1145,14 +1135,14 @@ class ProcessingThread(Thread):
|
|
|
1145
1135
|
|
|
1146
1136
|
def __init__(
|
|
1147
1137
|
self,
|
|
1148
|
-
processing_queue: Queue[
|
|
1138
|
+
processing_queue: Queue[List[ProcessingJob] | None],
|
|
1149
1139
|
follower: Follower,
|
|
1150
1140
|
has_errored: Event,
|
|
1151
1141
|
):
|
|
1152
1142
|
super().__init__(daemon=True)
|
|
1153
1143
|
self.processing_queue = processing_queue
|
|
1154
1144
|
self.follower = follower
|
|
1155
|
-
self.error:
|
|
1145
|
+
self.error: Exception | None = None
|
|
1156
1146
|
self.has_errored = has_errored
|
|
1157
1147
|
self.is_stopping = Event()
|
|
1158
1148
|
self.has_started = Event()
|
|
@@ -1212,22 +1202,17 @@ class NotificationLogReader:
|
|
|
1212
1202
|
event notifications in the notification log from the start position
|
|
1213
1203
|
have been yielded.
|
|
1214
1204
|
"""
|
|
1215
|
-
section_id = "{},{
|
|
1205
|
+
section_id = f"{start},{start + self.section_size - 1}"
|
|
1216
1206
|
while True:
|
|
1217
1207
|
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
|
|
1208
|
+
yield from section.items
|
|
1224
1209
|
if section.next_id is None:
|
|
1225
1210
|
break
|
|
1226
1211
|
else:
|
|
1227
1212
|
section_id = section.next_id
|
|
1228
1213
|
|
|
1229
1214
|
def select(
|
|
1230
|
-
self, *, start: int, stop:
|
|
1215
|
+
self, *, start: int, stop: int | None = None, topics: Sequence[str] = ()
|
|
1231
1216
|
) -> Iterator[List[Notification]]:
|
|
1232
1217
|
"""
|
|
1233
1218
|
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
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from unittest import TestCase
|
|
3
|
+
|
|
4
|
+
from eventsourcing.tests.application import (
|
|
5
|
+
TIMEIT_FACTOR,
|
|
6
|
+
ApplicationTestCase,
|
|
7
|
+
ExampleApplicationTestCase,
|
|
8
|
+
)
|
|
9
|
+
from eventsourcing.tests.persistence import tmpfile_uris
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class WithSQLiteFile(TestCase):
|
|
13
|
+
timeit_number = 30 * TIMEIT_FACTOR
|
|
14
|
+
expected_factory_topic = "eventsourcing.sqlite:Factory"
|
|
15
|
+
|
|
16
|
+
def setUp(self) -> None:
|
|
17
|
+
super().setUp()
|
|
18
|
+
self.uris = tmpfile_uris()
|
|
19
|
+
# self.db_uri = next(self.uris)
|
|
20
|
+
|
|
21
|
+
os.environ["PERSISTENCE_MODULE"] = "eventsourcing.sqlite"
|
|
22
|
+
os.environ["CREATE_TABLE"] = "y"
|
|
23
|
+
os.environ["SQLITE_DBNAME"] = next(self.uris)
|
|
24
|
+
|
|
25
|
+
def tearDown(self) -> None:
|
|
26
|
+
del os.environ["PERSISTENCE_MODULE"]
|
|
27
|
+
del os.environ["CREATE_TABLE"]
|
|
28
|
+
del os.environ["SQLITE_DBNAME"]
|
|
29
|
+
super().tearDown()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class WithSQLiteInMemory(TestCase):
|
|
33
|
+
timeit_number = 30 * TIMEIT_FACTOR
|
|
34
|
+
expected_factory_topic = "eventsourcing.sqlite:Factory"
|
|
35
|
+
|
|
36
|
+
def setUp(self) -> None:
|
|
37
|
+
super().setUp()
|
|
38
|
+
os.environ["PERSISTENCE_MODULE"] = "eventsourcing.sqlite"
|
|
39
|
+
os.environ["CREATE_TABLE"] = "y"
|
|
40
|
+
os.environ["SQLITE_DBNAME"] = "file:memory:?mode=memory&cache=shared"
|
|
41
|
+
|
|
42
|
+
def tearDown(self) -> None:
|
|
43
|
+
del os.environ["PERSISTENCE_MODULE"]
|
|
44
|
+
del os.environ["CREATE_TABLE"]
|
|
45
|
+
del os.environ["SQLITE_DBNAME"]
|
|
46
|
+
super().tearDown()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TestApplicationWithSQLiteFile(ApplicationTestCase, WithSQLiteFile):
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# class TestApplicationWithSQLiteInMemory(TestApplication, WithSQLiteInMemory):
|
|
54
|
+
# pass
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TestExampleApplicationWithSQLiteFile(ExampleApplicationTestCase, WithSQLiteFile):
|
|
58
|
+
timeit_number = 30 * TIMEIT_FACTOR
|
|
59
|
+
expected_factory_topic = "eventsourcing.sqlite:Factory"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class TestExampleApplicationWithSQLiteInMemory(
|
|
63
|
+
ExampleApplicationTestCase, WithSQLiteInMemory
|
|
64
|
+
):
|
|
65
|
+
timeit_number = 30 * TIMEIT_FACTOR
|
|
66
|
+
expected_factory_topic = "eventsourcing.sqlite:Factory"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
del ApplicationTestCase
|
|
70
|
+
del ExampleApplicationTestCase
|
|
71
|
+
del WithSQLiteFile
|
|
72
|
+
del WithSQLiteInMemory
|