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.

Files changed (144) hide show
  1. eventsourcing/__init__.py +1 -1
  2. eventsourcing/application.py +116 -135
  3. eventsourcing/cipher.py +15 -12
  4. eventsourcing/dispatch.py +31 -91
  5. eventsourcing/domain.py +220 -226
  6. eventsourcing/examples/__init__.py +0 -0
  7. eventsourcing/examples/aggregate1/__init__.py +0 -0
  8. eventsourcing/examples/aggregate1/application.py +27 -0
  9. eventsourcing/examples/aggregate1/domainmodel.py +16 -0
  10. eventsourcing/examples/aggregate1/test_application.py +37 -0
  11. eventsourcing/examples/aggregate2/__init__.py +0 -0
  12. eventsourcing/examples/aggregate2/application.py +27 -0
  13. eventsourcing/examples/aggregate2/domainmodel.py +22 -0
  14. eventsourcing/examples/aggregate2/test_application.py +37 -0
  15. eventsourcing/examples/aggregate3/__init__.py +0 -0
  16. eventsourcing/examples/aggregate3/application.py +27 -0
  17. eventsourcing/examples/aggregate3/domainmodel.py +38 -0
  18. eventsourcing/examples/aggregate3/test_application.py +37 -0
  19. eventsourcing/examples/aggregate4/__init__.py +0 -0
  20. eventsourcing/examples/aggregate4/application.py +27 -0
  21. eventsourcing/examples/aggregate4/domainmodel.py +114 -0
  22. eventsourcing/examples/aggregate4/test_application.py +38 -0
  23. eventsourcing/examples/aggregate5/__init__.py +0 -0
  24. eventsourcing/examples/aggregate5/application.py +27 -0
  25. eventsourcing/examples/aggregate5/domainmodel.py +131 -0
  26. eventsourcing/examples/aggregate5/test_application.py +38 -0
  27. eventsourcing/examples/aggregate6/__init__.py +0 -0
  28. eventsourcing/examples/aggregate6/application.py +30 -0
  29. eventsourcing/examples/aggregate6/domainmodel.py +123 -0
  30. eventsourcing/examples/aggregate6/test_application.py +38 -0
  31. eventsourcing/examples/aggregate6a/__init__.py +0 -0
  32. eventsourcing/examples/aggregate6a/application.py +40 -0
  33. eventsourcing/examples/aggregate6a/domainmodel.py +149 -0
  34. eventsourcing/examples/aggregate6a/test_application.py +45 -0
  35. eventsourcing/examples/aggregate7/__init__.py +0 -0
  36. eventsourcing/examples/aggregate7/application.py +48 -0
  37. eventsourcing/examples/aggregate7/domainmodel.py +144 -0
  38. eventsourcing/examples/aggregate7/persistence.py +57 -0
  39. eventsourcing/examples/aggregate7/test_application.py +38 -0
  40. eventsourcing/examples/aggregate7/test_compression_and_encryption.py +45 -0
  41. eventsourcing/examples/aggregate7/test_snapshotting_intervals.py +67 -0
  42. eventsourcing/examples/aggregate7a/__init__.py +0 -0
  43. eventsourcing/examples/aggregate7a/application.py +56 -0
  44. eventsourcing/examples/aggregate7a/domainmodel.py +170 -0
  45. eventsourcing/examples/aggregate7a/test_application.py +46 -0
  46. eventsourcing/examples/aggregate7a/test_compression_and_encryption.py +45 -0
  47. eventsourcing/examples/aggregate8/__init__.py +0 -0
  48. eventsourcing/examples/aggregate8/application.py +47 -0
  49. eventsourcing/examples/aggregate8/domainmodel.py +65 -0
  50. eventsourcing/examples/aggregate8/persistence.py +57 -0
  51. eventsourcing/examples/aggregate8/test_application.py +37 -0
  52. eventsourcing/examples/aggregate8/test_compression_and_encryption.py +44 -0
  53. eventsourcing/examples/aggregate8/test_snapshotting_intervals.py +38 -0
  54. eventsourcing/examples/bankaccounts/__init__.py +0 -0
  55. eventsourcing/examples/bankaccounts/application.py +70 -0
  56. eventsourcing/examples/bankaccounts/domainmodel.py +56 -0
  57. eventsourcing/examples/bankaccounts/test.py +173 -0
  58. eventsourcing/examples/cargoshipping/__init__.py +0 -0
  59. eventsourcing/examples/cargoshipping/application.py +126 -0
  60. eventsourcing/examples/cargoshipping/domainmodel.py +330 -0
  61. eventsourcing/examples/cargoshipping/interface.py +143 -0
  62. eventsourcing/examples/cargoshipping/test.py +231 -0
  63. eventsourcing/examples/contentmanagement/__init__.py +0 -0
  64. eventsourcing/examples/contentmanagement/application.py +118 -0
  65. eventsourcing/examples/contentmanagement/domainmodel.py +69 -0
  66. eventsourcing/examples/contentmanagement/test.py +180 -0
  67. eventsourcing/examples/contentmanagement/utils.py +26 -0
  68. eventsourcing/examples/contentmanagementsystem/__init__.py +0 -0
  69. eventsourcing/examples/contentmanagementsystem/application.py +54 -0
  70. eventsourcing/examples/contentmanagementsystem/postgres.py +17 -0
  71. eventsourcing/examples/contentmanagementsystem/sqlite.py +17 -0
  72. eventsourcing/examples/contentmanagementsystem/system.py +14 -0
  73. eventsourcing/examples/contentmanagementsystem/test_system.py +180 -0
  74. eventsourcing/examples/searchablecontent/__init__.py +0 -0
  75. eventsourcing/examples/searchablecontent/application.py +45 -0
  76. eventsourcing/examples/searchablecontent/persistence.py +23 -0
  77. eventsourcing/examples/searchablecontent/postgres.py +118 -0
  78. eventsourcing/examples/searchablecontent/sqlite.py +136 -0
  79. eventsourcing/examples/searchablecontent/test_application.py +110 -0
  80. eventsourcing/examples/searchablecontent/test_recorder.py +68 -0
  81. eventsourcing/examples/searchabletimestamps/__init__.py +0 -0
  82. eventsourcing/examples/searchabletimestamps/application.py +32 -0
  83. eventsourcing/examples/searchabletimestamps/persistence.py +20 -0
  84. eventsourcing/examples/searchabletimestamps/postgres.py +110 -0
  85. eventsourcing/examples/searchabletimestamps/sqlite.py +99 -0
  86. eventsourcing/examples/searchabletimestamps/test_searchabletimestamps.py +94 -0
  87. eventsourcing/examples/test_invoice.py +176 -0
  88. eventsourcing/examples/test_parking_lot.py +206 -0
  89. eventsourcing/interface.py +2 -2
  90. eventsourcing/persistence.py +85 -81
  91. eventsourcing/popo.py +30 -31
  92. eventsourcing/postgres.py +379 -590
  93. eventsourcing/sqlite.py +91 -99
  94. eventsourcing/system.py +52 -57
  95. eventsourcing/tests/application.py +20 -32
  96. eventsourcing/tests/application_tests/__init__.py +0 -0
  97. eventsourcing/tests/application_tests/test_application_with_automatic_snapshotting.py +55 -0
  98. eventsourcing/tests/application_tests/test_application_with_popo.py +22 -0
  99. eventsourcing/tests/application_tests/test_application_with_postgres.py +75 -0
  100. eventsourcing/tests/application_tests/test_application_with_sqlite.py +72 -0
  101. eventsourcing/tests/application_tests/test_cache.py +134 -0
  102. eventsourcing/tests/application_tests/test_event_sourced_log.py +162 -0
  103. eventsourcing/tests/application_tests/test_notificationlog.py +232 -0
  104. eventsourcing/tests/application_tests/test_notificationlogreader.py +126 -0
  105. eventsourcing/tests/application_tests/test_processapplication.py +110 -0
  106. eventsourcing/tests/application_tests/test_processingpolicy.py +109 -0
  107. eventsourcing/tests/application_tests/test_repository.py +504 -0
  108. eventsourcing/tests/application_tests/test_snapshotting.py +68 -0
  109. eventsourcing/tests/application_tests/test_upcasting.py +459 -0
  110. eventsourcing/tests/docs_tests/__init__.py +0 -0
  111. eventsourcing/tests/docs_tests/test_docs.py +293 -0
  112. eventsourcing/tests/domain.py +1 -1
  113. eventsourcing/tests/domain_tests/__init__.py +0 -0
  114. eventsourcing/tests/domain_tests/test_aggregate.py +1180 -0
  115. eventsourcing/tests/domain_tests/test_aggregate_decorators.py +1604 -0
  116. eventsourcing/tests/domain_tests/test_domainevent.py +80 -0
  117. eventsourcing/tests/interface_tests/__init__.py +0 -0
  118. eventsourcing/tests/interface_tests/test_remotenotificationlog.py +258 -0
  119. eventsourcing/tests/persistence.py +52 -50
  120. eventsourcing/tests/persistence_tests/__init__.py +0 -0
  121. eventsourcing/tests/persistence_tests/test_aes.py +93 -0
  122. eventsourcing/tests/persistence_tests/test_connection_pool.py +722 -0
  123. eventsourcing/tests/persistence_tests/test_eventstore.py +72 -0
  124. eventsourcing/tests/persistence_tests/test_infrastructure_factory.py +21 -0
  125. eventsourcing/tests/persistence_tests/test_mapper.py +113 -0
  126. eventsourcing/tests/persistence_tests/test_noninterleaving_notification_ids.py +69 -0
  127. eventsourcing/tests/persistence_tests/test_popo.py +124 -0
  128. eventsourcing/tests/persistence_tests/test_postgres.py +1119 -0
  129. eventsourcing/tests/persistence_tests/test_sqlite.py +348 -0
  130. eventsourcing/tests/persistence_tests/test_transcoder.py +44 -0
  131. eventsourcing/tests/postgres_utils.py +7 -7
  132. eventsourcing/tests/system_tests/__init__.py +0 -0
  133. eventsourcing/tests/system_tests/test_runner.py +935 -0
  134. eventsourcing/tests/system_tests/test_system.py +284 -0
  135. eventsourcing/tests/utils_tests/__init__.py +0 -0
  136. eventsourcing/tests/utils_tests/test_utils.py +226 -0
  137. eventsourcing/utils.py +47 -50
  138. {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0.dist-info}/METADATA +29 -79
  139. eventsourcing-9.3.0.dist-info/RECORD +145 -0
  140. {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0.dist-info}/WHEEL +1 -2
  141. eventsourcing-9.2.22.dist-info/RECORD +0 -25
  142. eventsourcing-9.2.22.dist-info/top_level.txt +0 -1
  143. {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0.dist-info}/AUTHORS +0 -0
  144. {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
- # For backwards compatibility of import statements...
25
- from eventsourcing.application import ProcessEvent # noqa: F401
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: Optional[EnvType] = None) -> None:
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: Optional[int] = None, stop: Optional[int] = None
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: Optional[int] = None
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
- else:
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: Optional[EnvType] = None) -> None:
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]] = list()
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.keys() if name not in self.follows]
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.keys() if name in self.follows]
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
- else:
353
- cls = type(
354
- cls.name,
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) -> Optional[str]:
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: Optional[str] = None
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: Optional[EnvType] = None):
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 RunnerAlreadyStarted()
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 RunnerAlreadyStarted(Exception):
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: Optional[EnvType] = None):
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: Set[str] = set()
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: Optional[EnvType] = None):
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
- leader_name
670
- ] = recording_event.recordings[-1].notification.id
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: Optional[EnvType] = None):
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: Optional[float] = None) -> bool:
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: Optional[Exception] = None
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: Optional[EnvType] = None,
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, "Queue[Optional[List[ProcessingJob]]]"] = {}
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[Optional[List[ProcessingJob]]] = 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: Optional[float] = None) -> bool:
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[Optional[RecordingEvent]] = Queue(maxsize=100)
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: Optional[Exception] = None
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[Optional[List[ProcessingJob]]],
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: Optional[Exception] = None
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[Optional[List[ProcessingJob]]],
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: Optional[Exception] = None
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 = "{},{}".format(start, start + self.section_size - 1)
1215
+ section_id = f"{start},{start + self.section_size - 1}"
1216
1216
  while True:
1217
1217
  section: Section = self.notification_log[section_id]
1218
- for item in section.items:
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: Optional[int] = None, topics: Sequence[str] = ()
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(BankAccounts, self).register_transcodings(transcoder)
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 AggregateNotFound:
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
- "Found 0 infrastructure factory classes in "
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
- "Not an infrastructure factory class or module: "
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
- "Can't take snapshot without snapshots store. Please "
323
- "set environment variable IS_SNAPSHOTTING_ENABLED to "
324
- "a true value (e.g. 'y'), or set 'is_snapshotting_enabled' "
325
- "on application class, or set 'snapshotting_intervals' on "
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