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.

Files changed (144) hide show
  1. eventsourcing/__init__.py +1 -1
  2. eventsourcing/application.py +106 -135
  3. eventsourcing/cipher.py +15 -12
  4. eventsourcing/dispatch.py +31 -91
  5. eventsourcing/domain.py +138 -143
  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 +128 -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 +174 -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 +111 -0
  80. eventsourcing/examples/searchablecontent/test_recorder.py +69 -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 +91 -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 +361 -578
  93. eventsourcing/sqlite.py +91 -99
  94. eventsourcing/system.py +42 -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 +1159 -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 +49 -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 +1121 -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 +287 -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.0a1.dist-info}/METADATA +28 -80
  139. eventsourcing-9.3.0a1.dist-info/RECORD +144 -0
  140. {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0a1.dist-info}/WHEEL +1 -2
  141. eventsourcing-9.2.22.dist-info/AUTHORS +0 -10
  142. eventsourcing-9.2.22.dist-info/RECORD +0 -25
  143. eventsourcing-9.2.22.dist-info/top_level.txt +0 -1
  144. {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: Optional[EnvType] = None) -> None:
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: Optional[int] = None, stop: Optional[int] = None
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: Optional[int] = None
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
- else:
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: Optional[EnvType] = None) -> None:
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]] = list()
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.keys() if name not in self.follows]
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.keys() if name in self.follows]
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
- else:
353
- cls = type(
354
- cls.name,
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) -> Optional[str]:
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: Optional[str] = None
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: Optional[EnvType] = None):
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 RunnerAlreadyStarted()
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 RunnerAlreadyStarted(Exception):
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: Optional[EnvType] = None):
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: Set[str] = set()
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: Optional[EnvType] = None):
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
- leader_name
670
- ] = recording_event.recordings[-1].notification.id
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: Optional[EnvType] = None):
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: Optional[float] = None) -> bool:
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: Optional[Exception] = None
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: Optional[EnvType] = None,
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, "Queue[Optional[List[ProcessingJob]]]"] = {}
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[Optional[List[ProcessingJob]]] = 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: Optional[float] = None) -> bool:
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[Optional[RecordingEvent]] = Queue(maxsize=100)
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: Optional[Exception] = None
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[Optional[List[ProcessingJob]]],
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: Optional[Exception] = None
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[Optional[List[ProcessingJob]]],
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: Optional[Exception] = None
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 = "{},{}".format(start, start + self.section_size - 1)
1205
+ section_id = f"{start},{start + self.section_size - 1}"
1216
1206
  while True:
1217
1207
  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
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: Optional[int] = None, topics: Sequence[str] = ()
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(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
@@ -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