eventsourcing 9.3.4__py3-none-any.whl → 9.4.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/system.py CHANGED
@@ -1,11 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import inspect
4
+ import threading
4
5
  import traceback
5
6
  from abc import ABC, abstractmethod
6
7
  from collections import defaultdict
7
8
  from queue import Full, Queue
8
- from threading import Event, Lock, RLock, Thread
9
9
  from types import FrameType, ModuleType
10
10
  from typing import (
11
11
  TYPE_CHECKING,
@@ -23,18 +23,17 @@ from typing import (
23
23
  cast,
24
24
  )
25
25
 
26
- if TYPE_CHECKING: # pragma: nocover
26
+ if TYPE_CHECKING: # pragma: no cover
27
27
  from typing_extensions import Self
28
28
 
29
29
  from eventsourcing.application import (
30
30
  Application,
31
31
  NotificationLog,
32
32
  ProcessingEvent,
33
- RecordingEvent,
34
33
  Section,
35
34
  TApplication,
36
35
  )
37
- from eventsourcing.domain import DomainEventProtocol
36
+ from eventsourcing.domain import DomainEventProtocol, MutableOrImmutableAggregate
38
37
  from eventsourcing.persistence import (
39
38
  IntegrityError,
40
39
  Mapper,
@@ -46,6 +45,20 @@ from eventsourcing.persistence import (
46
45
  from eventsourcing.utils import EnvType, get_topic, resolve_topic
47
46
 
48
47
  ProcessingJob = Tuple[DomainEventProtocol, Tracking]
48
+
49
+
50
+ class RecordingEvent:
51
+ def __init__(
52
+ self,
53
+ application_name: str,
54
+ recordings: List[Recording],
55
+ previous_max_notification_id: int | None,
56
+ ):
57
+ self.application_name = application_name
58
+ self.recordings = recordings
59
+ self.previous_max_notification_id = previous_max_notification_id
60
+
61
+
49
62
  ConvertingJob = Optional[Union[RecordingEvent, List[Notification]]]
50
63
 
51
64
 
@@ -66,7 +79,7 @@ class Follower(Application):
66
79
  self.mappers: Dict[str, Mapper] = {}
67
80
  self.recorder: ProcessRecorder
68
81
  self.is_threading_enabled = False
69
- self.processing_lock = RLock()
82
+ self.processing_lock = threading.Lock()
70
83
 
71
84
  def construct_recorder(self) -> ProcessRecorder:
72
85
  """
@@ -99,9 +112,9 @@ class Follower(Application):
99
112
  Pull and process new domain event notifications.
100
113
  """
101
114
  if start is None:
102
- start = self.recorder.max_tracking_id(leader_name) + 1
115
+ start = self.recorder.max_tracking_id(leader_name)
103
116
  for notifications in self.pull_notifications(
104
- leader_name, start=start, stop=stop
117
+ leader_name, start=start, stop=stop, inclusive_of_start=False
105
118
  ):
106
119
  notifications_iter = self.filter_received_notifications(notifications)
107
120
  for domain_event, tracking in self.convert_notifications(
@@ -110,14 +123,22 @@ class Follower(Application):
110
123
  self.process_event(domain_event, tracking)
111
124
 
112
125
  def pull_notifications(
113
- self, leader_name: str, start: int, stop: int | None = None
126
+ self,
127
+ leader_name: str,
128
+ start: int | None,
129
+ stop: int | None = None,
130
+ *,
131
+ inclusive_of_start: bool = True,
114
132
  ) -> Iterator[List[Notification]]:
115
133
  """
116
134
  Pulls batches of unseen :class:`~eventsourcing.persistence.Notification`
117
135
  objects from the notification log reader of the named application.
118
136
  """
119
137
  return self.readers[leader_name].select(
120
- start=start, stop=stop, topics=self.follow_topics
138
+ start=start,
139
+ stop=stop,
140
+ topics=self.follow_topics,
141
+ inclusive_of_start=inclusive_of_start,
121
142
  )
122
143
 
123
144
  def filter_received_notifications(
@@ -230,6 +251,7 @@ class Leader(Application):
230
251
 
231
252
  def __init__(self, env: EnvType | None = None) -> None:
232
253
  super().__init__(env)
254
+ self.previous_max_notification_id: int | None = None
233
255
  self.followers: List[RecordingEventReceiver] = []
234
256
 
235
257
  def lead(self, follower: RecordingEventReceiver) -> None:
@@ -238,6 +260,15 @@ class Leader(Application):
238
260
  """
239
261
  self.followers.append(follower)
240
262
 
263
+ def save(
264
+ self,
265
+ *objs: MutableOrImmutableAggregate | DomainEventProtocol | None,
266
+ **kwargs: Any,
267
+ ) -> List[Recording]:
268
+ if self.previous_max_notification_id is None:
269
+ self.previous_max_notification_id = self.recorder.max_notification_id()
270
+ return super().save(*objs, **kwargs)
271
+
241
272
  def _notify(self, recordings: List[Recording]) -> None:
242
273
  """
243
274
  Calls :func:`receive_recording_event` on each follower
@@ -442,9 +473,9 @@ class SingleThreadedRunner(Runner, RecordingEventReceiver):
442
473
  super().__init__(system=system, env=env)
443
474
  self.apps: Dict[str, Application] = {}
444
475
  self._recording_events_received: List[RecordingEvent] = []
445
- self._prompted_names_lock = Lock()
476
+ self._prompted_names_lock = threading.Lock()
446
477
  self._prompted_names: set[str] = set()
447
- self._processing_lock = Lock()
478
+ self._processing_lock = threading.Lock()
448
479
 
449
480
  # Construct followers.
450
481
  for name in self.system.followers:
@@ -550,8 +581,8 @@ class NewSingleThreadedRunner(Runner, RecordingEventReceiver):
550
581
  super().__init__(system=system, env=env)
551
582
  self.apps: Dict[str, Application] = {}
552
583
  self._recording_events_received: List[RecordingEvent] = []
553
- self._recording_events_received_lock = Lock()
554
- self._processing_lock = Lock()
584
+ self._recording_events_received_lock = threading.Lock()
585
+ self._processing_lock = threading.Lock()
555
586
  self._previous_max_notification_ids: Dict[str, int] = {}
556
587
 
557
588
  # Construct followers.
@@ -642,9 +673,7 @@ class NewSingleThreadedRunner(Runner, RecordingEventReceiver):
642
673
  for follower_name in self.system.leads[leader_name]:
643
674
  follower = self.apps[follower_name]
644
675
  assert isinstance(follower, Follower)
645
- start = (
646
- follower.recorder.max_tracking_id(leader_name) + 1
647
- )
676
+ start = follower.recorder.max_tracking_id(leader_name)
648
677
  stop = recording_event.recordings[0].notification.id - 1
649
678
  follower.pull_and_process(
650
679
  leader_name=leader_name,
@@ -700,7 +729,7 @@ class MultiThreadedRunner(Runner):
700
729
  super().__init__(system=system, env=env)
701
730
  self.apps: Dict[str, Application] = {}
702
731
  self.threads: Dict[str, MultiThreadedRunnerThread] = {}
703
- self.has_errored = Event()
732
+ self.has_errored = threading.Event()
704
733
 
705
734
  # Construct followers.
706
735
  for follower_name in self.system.followers:
@@ -784,7 +813,7 @@ class MultiThreadedRunner(Runner):
784
813
  return app
785
814
 
786
815
 
787
- class MultiThreadedRunnerThread(RecordingEventReceiver, Thread):
816
+ class MultiThreadedRunnerThread(RecordingEventReceiver, threading.Thread):
788
817
  """
789
818
  Runs one :class:`~eventsourcing.system.Follower` application in
790
819
  a :class:`~eventsourcing.system.MultiThreadedRunner`.
@@ -793,18 +822,18 @@ class MultiThreadedRunnerThread(RecordingEventReceiver, Thread):
793
822
  def __init__(
794
823
  self,
795
824
  follower: Follower,
796
- has_errored: Event,
825
+ has_errored: threading.Event,
797
826
  ):
798
827
  super().__init__(daemon=True)
799
828
  self.follower = follower
800
829
  self.has_errored = has_errored
801
830
  self.error: Exception | None = None
802
- self.is_stopping = Event()
803
- self.has_started = Event()
804
- self.is_prompted = Event()
831
+ self.is_stopping = threading.Event()
832
+ self.has_started = threading.Event()
833
+ self.is_prompted = threading.Event()
805
834
  self.prompted_names: List[str] = []
806
- self.prompted_names_lock = Lock()
807
- self.is_running = Event()
835
+ self.prompted_names_lock = threading.Lock()
836
+ self.is_running = threading.Event()
808
837
 
809
838
  def run(self) -> None:
810
839
  """
@@ -866,7 +895,7 @@ class NewMultiThreadedRunner(Runner, RecordingEventReceiver):
866
895
  self.pulling_threads: Dict[str, List[PullingThread]] = {}
867
896
  self.processing_queues: Dict[str, Queue[List[ProcessingJob] | None]] = {}
868
897
  self.all_threads: List[PullingThread | ConvertingThread | ProcessingThread] = []
869
- self.has_errored = Event()
898
+ self.has_errored = threading.Event()
870
899
 
871
900
  # Construct followers.
872
901
  for follower_name in self.system.followers:
@@ -991,7 +1020,7 @@ class NewMultiThreadedRunner(Runner, RecordingEventReceiver):
991
1020
  pulling_thread.receive_recording_event(recording_event)
992
1021
 
993
1022
 
994
- class PullingThread(Thread):
1023
+ class PullingThread(threading.Thread):
995
1024
  """
996
1025
  Receives or pulls notifications from the given leader, and
997
1026
  puts them on a queue for conversion into processing jobs.
@@ -1002,19 +1031,19 @@ class PullingThread(Thread):
1002
1031
  converting_queue: Queue[ConvertingJob],
1003
1032
  follower: Follower,
1004
1033
  leader_name: str,
1005
- has_errored: Event,
1034
+ has_errored: threading.Event,
1006
1035
  ):
1007
1036
  super().__init__(daemon=True)
1008
- self.overflow_event = Event()
1037
+ self.overflow_event = threading.Event()
1009
1038
  self.recording_event_queue: Queue[RecordingEvent | None] = Queue(maxsize=100)
1010
1039
  self.converting_queue = converting_queue
1011
- self.receive_lock = Lock()
1040
+ self.receive_lock = threading.Lock()
1012
1041
  self.follower = follower
1013
1042
  self.leader_name = leader_name
1014
1043
  self.error: Exception | None = None
1015
1044
  self.has_errored = has_errored
1016
- self.is_stopping = Event()
1017
- self.has_started = Event()
1045
+ self.is_stopping = threading.Event()
1046
+ self.has_started = threading.Event()
1018
1047
  self.mapper = self.follower.mappers[self.leader_name]
1019
1048
  self.previous_max_notification_id = self.follower.recorder.max_tracking_id(
1020
1049
  application_name=self.leader_name
@@ -1031,6 +1060,7 @@ class PullingThread(Thread):
1031
1060
  # Ignore recording event if already seen a subsequent.
1032
1061
  if (
1033
1062
  recording_event.previous_max_notification_id is not None
1063
+ and self.previous_max_notification_id is not None
1034
1064
  and recording_event.previous_max_notification_id
1035
1065
  < self.previous_max_notification_id
1036
1066
  ):
@@ -1039,13 +1069,17 @@ class PullingThread(Thread):
1039
1069
  # Catch up if there is a gap in sequence of recording events.
1040
1070
  if (
1041
1071
  recording_event.previous_max_notification_id is None
1072
+ or self.previous_max_notification_id is None
1042
1073
  or recording_event.previous_max_notification_id
1043
1074
  > self.previous_max_notification_id
1044
1075
  ):
1045
- start = self.previous_max_notification_id + 1
1076
+ start = self.previous_max_notification_id
1046
1077
  stop = recording_event.recordings[0].notification.id - 1
1047
1078
  for notifications in self.follower.pull_notifications(
1048
- self.leader_name, start=start, stop=stop
1079
+ self.leader_name,
1080
+ start=start,
1081
+ stop=stop,
1082
+ inclusive_of_start=False,
1049
1083
  ):
1050
1084
  self.converting_queue.put(notifications)
1051
1085
  self.previous_max_notification_id = notifications[-1].id
@@ -1069,7 +1103,7 @@ class PullingThread(Thread):
1069
1103
  self.recording_event_queue.put(None)
1070
1104
 
1071
1105
 
1072
- class ConvertingThread(Thread):
1106
+ class ConvertingThread(threading.Thread):
1073
1107
  """
1074
1108
  Converts notifications into processing jobs.
1075
1109
  """
@@ -1080,7 +1114,7 @@ class ConvertingThread(Thread):
1080
1114
  processing_queue: Queue[List[ProcessingJob] | None],
1081
1115
  follower: Follower,
1082
1116
  leader_name: str,
1083
- has_errored: Event,
1117
+ has_errored: threading.Event,
1084
1118
  ):
1085
1119
  super().__init__(daemon=True)
1086
1120
  self.converting_queue = converting_queue
@@ -1089,8 +1123,8 @@ class ConvertingThread(Thread):
1089
1123
  self.leader_name = leader_name
1090
1124
  self.error: Exception | None = None
1091
1125
  self.has_errored = has_errored
1092
- self.is_stopping = Event()
1093
- self.has_started = Event()
1126
+ self.is_stopping = threading.Event()
1127
+ self.has_started = threading.Event()
1094
1128
  self.mapper = self.follower.mappers[self.leader_name]
1095
1129
 
1096
1130
  def run(self) -> None:
@@ -1139,7 +1173,7 @@ class ConvertingThread(Thread):
1139
1173
  self.converting_queue.put(None)
1140
1174
 
1141
1175
 
1142
- class ProcessingThread(Thread):
1176
+ class ProcessingThread(threading.Thread):
1143
1177
  """
1144
1178
  A processing thread gets events from a processing queue, and
1145
1179
  calls the application's process_event() method.
@@ -1149,15 +1183,15 @@ class ProcessingThread(Thread):
1149
1183
  self,
1150
1184
  processing_queue: Queue[List[ProcessingJob] | None],
1151
1185
  follower: Follower,
1152
- has_errored: Event,
1186
+ has_errored: threading.Event,
1153
1187
  ):
1154
1188
  super().__init__(daemon=True)
1155
1189
  self.processing_queue = processing_queue
1156
1190
  self.follower = follower
1157
1191
  self.error: Exception | None = None
1158
1192
  self.has_errored = has_errored
1159
- self.is_stopping = Event()
1160
- self.has_started = Event()
1193
+ self.is_stopping = threading.Event()
1194
+ self.has_started = threading.Event()
1161
1195
 
1162
1196
  def run(self) -> None:
1163
1197
  self.has_started.set()
@@ -1224,7 +1258,12 @@ class NotificationLogReader:
1224
1258
  section_id = section.next_id
1225
1259
 
1226
1260
  def select(
1227
- self, *, start: int, stop: int | None = None, topics: Sequence[str] = ()
1261
+ self,
1262
+ *,
1263
+ start: int | None,
1264
+ stop: int | None = None,
1265
+ topics: Sequence[str] = (),
1266
+ inclusive_of_start: bool = True,
1228
1267
  ) -> Iterator[List[Notification]]:
1229
1268
  """
1230
1269
  Returns a generator that yields lists of event notifications
@@ -1240,12 +1279,18 @@ class NotificationLogReader:
1240
1279
  """
1241
1280
  while True:
1242
1281
  notifications = self.notification_log.select(
1243
- start=start, stop=stop, limit=self.section_size, topics=topics
1282
+ start=start,
1283
+ stop=stop,
1284
+ limit=self.section_size,
1285
+ topics=topics,
1286
+ inclusive_of_start=inclusive_of_start,
1244
1287
  )
1245
1288
  # Stop if zero notifications.
1246
1289
  if len(notifications) == 0:
1247
1290
  break
1248
1291
 
1249
1292
  # Otherwise, yield and continue.
1293
+ start = notifications[-1].id
1294
+ if inclusive_of_start:
1295
+ start += 1
1250
1296
  yield notifications
1251
- start = notifications[-1].id + 1
@@ -19,7 +19,8 @@ from eventsourcing.domain import Aggregate
19
19
  from eventsourcing.persistence import (
20
20
  InfrastructureFactory,
21
21
  IntegrityError,
22
- Transcoder,
22
+ JSONTranscoder,
23
+ Tracking,
23
24
  Transcoding,
24
25
  )
25
26
  from eventsourcing.tests.domain import BankAccount, EmailAddress
@@ -36,7 +37,6 @@ class ExampleApplicationTestCase(TestCase):
36
37
 
37
38
  def test_example_application(self):
38
39
  app = BankAccounts(env={"IS_SNAPSHOTTING_ENABLED": "y"})
39
- max_notification_id = app.recorder.max_notification_id()
40
40
 
41
41
  self.assertEqual(get_topic(type(app.factory)), self.expected_factory_topic)
42
42
 
@@ -75,9 +75,7 @@ class ExampleApplicationTestCase(TestCase):
75
75
  )
76
76
 
77
77
  sleep(1) # Added to make eventsourcing-axon tests work, perhaps not necessary.
78
- section = app.notification_log[
79
- f"{max_notification_id + 1},{max_notification_id + 10}"
80
- ]
78
+ section = app.notification_log["1,10"]
81
79
  self.assertEqual(len(section.items), 4)
82
80
 
83
81
  # Take snapshot (specify version).
@@ -198,7 +196,7 @@ class EmailAddressAsStr(Transcoding):
198
196
  class BankAccounts(Application):
199
197
  is_snapshotting_enabled = True
200
198
 
201
- def register_transcodings(self, transcoder: Transcoder) -> None:
199
+ def register_transcodings(self, transcoder: JSONTranscoder) -> None:
202
200
  super().register_transcodings(transcoder)
203
201
  transcoder.register(EmailAddressAsStr())
204
202
 
@@ -291,20 +289,18 @@ class ApplicationTestCase(TestCase):
291
289
  recordings = app.save(None)
292
290
  self.assertEqual(recordings, [])
293
291
 
294
- max_id = app.recorder.max_notification_id()
295
-
296
292
  recordings = app.save(Aggregate())
297
293
  self.assertEqual(len(recordings), 1)
298
- self.assertEqual(recordings[0].notification.id, 1 + max_id)
294
+ self.assertEqual(recordings[0].notification.id, 1)
299
295
 
300
296
  recordings = app.save(Aggregate())
301
297
  self.assertEqual(len(recordings), 1)
302
- self.assertEqual(recordings[0].notification.id, 2 + max_id)
298
+ self.assertEqual(recordings[0].notification.id, 2)
303
299
 
304
300
  recordings = app.save(Aggregate(), Aggregate())
305
301
  self.assertEqual(len(recordings), 2)
306
- self.assertEqual(recordings[0].notification.id, 3 + max_id)
307
- self.assertEqual(recordings[1].notification.id, 4 + max_id)
302
+ self.assertEqual(recordings[0].notification.id, 3)
303
+ self.assertEqual(recordings[1].notification.id, 4)
308
304
 
309
305
  def test_take_snapshot_raises_assertion_error_if_snapshotting_not_enabled(self):
310
306
  app = Application()
@@ -494,3 +490,43 @@ class ApplicationTestCase(TestCase):
494
490
  self.assertEqual(
495
491
  "'log' is deprecated, use 'notifications' instead", w[-1].message.args[0]
496
492
  )
493
+
494
+ def test_catchup_subscription(self):
495
+ app = Application()
496
+
497
+ max_notification_id = app.recorder.max_notification_id()
498
+
499
+ aggregate = Aggregate()
500
+ aggregate.trigger_event(Aggregate.Event)
501
+ aggregate.trigger_event(Aggregate.Event)
502
+ aggregate.trigger_event(Aggregate.Event)
503
+ app.save(aggregate)
504
+
505
+ subscription = app.subscribe(gt=max_notification_id)
506
+
507
+ # Catch up.
508
+ for domain_event, tracking in subscription:
509
+ self.assertIsInstance(domain_event, Aggregate.Event)
510
+ self.assertIsInstance(tracking, Tracking)
511
+ self.assertEqual(tracking.application_name, app.name)
512
+ if max_notification_id is not None:
513
+ self.assertGreater(tracking.notification_id, max_notification_id)
514
+ if tracking.notification_id == app.recorder.max_notification_id():
515
+ break
516
+
517
+ max_notification_id = app.recorder.max_notification_id()
518
+
519
+ aggregate.trigger_event(Aggregate.Event)
520
+ aggregate.trigger_event(Aggregate.Event)
521
+ aggregate.trigger_event(Aggregate.Event)
522
+ app.save(aggregate)
523
+
524
+ # Continue.
525
+ for domain_event, tracking in subscription:
526
+ self.assertIsInstance(domain_event, Aggregate.Event)
527
+ self.assertIsInstance(tracking, Tracking)
528
+ self.assertEqual(tracking.application_name, app.name)
529
+ if max_notification_id is not None:
530
+ self.assertGreater(tracking.notification_id, max_notification_id)
531
+ if tracking.notification_id == app.recorder.max_notification_id():
532
+ break