eventsourcing 9.3.4__py3-none-any.whl → 9.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of eventsourcing might be problematic. Click here for more details.

eventsourcing/system.py CHANGED
@@ -1,42 +1,30 @@
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
- from typing import (
11
- TYPE_CHECKING,
12
- Any,
13
- ClassVar,
14
- Dict,
15
- Iterable,
16
- Iterator,
17
- List,
18
- Optional,
19
- Sequence,
20
- Tuple,
21
- Type,
22
- Union,
23
- cast,
24
- )
10
+ from typing import TYPE_CHECKING, Any, ClassVar, Optional, Union, cast
11
+
12
+ from eventsourcing.projection import EventSourcedProjection
13
+
14
+ if TYPE_CHECKING:
15
+ from collections.abc import Iterable, Iterator, Sequence
25
16
 
26
- if TYPE_CHECKING: # pragma: nocover
27
17
  from typing_extensions import Self
28
18
 
29
19
  from eventsourcing.application import (
30
20
  Application,
31
21
  NotificationLog,
32
- ProcessingEvent,
33
- RecordingEvent,
22
+ ProgrammingError,
34
23
  Section,
35
24
  TApplication,
36
25
  )
37
- from eventsourcing.domain import DomainEventProtocol
26
+ from eventsourcing.domain import DomainEventProtocol, MutableOrImmutableAggregate
38
27
  from eventsourcing.persistence import (
39
- IntegrityError,
40
28
  Mapper,
41
29
  Notification,
42
30
  ProcessRecorder,
@@ -45,39 +33,46 @@ from eventsourcing.persistence import (
45
33
  )
46
34
  from eventsourcing.utils import EnvType, get_topic, resolve_topic
47
35
 
48
- ProcessingJob = Tuple[DomainEventProtocol, Tracking]
49
- ConvertingJob = Optional[Union[RecordingEvent, List[Notification]]]
36
+ ProcessingJob = tuple[DomainEventProtocol, Tracking]
50
37
 
51
38
 
52
- class Follower(Application):
53
- """
54
- Extends the :class:`~eventsourcing.application.Application` class
55
- by using a process recorder as its application recorder, by keeping
56
- track of the applications it is following, and pulling and processing
57
- new domain event notifications through its :func:`policy` method.
39
+ class RecordingEvent:
40
+ def __init__(
41
+ self,
42
+ application_name: str,
43
+ recordings: list[Recording],
44
+ previous_max_notification_id: int | None,
45
+ ):
46
+ self.application_name = application_name
47
+ self.recordings = recordings
48
+ self.previous_max_notification_id = previous_max_notification_id
49
+
50
+
51
+ ConvertingJob = Optional[Union[RecordingEvent, list[Notification]]]
52
+
53
+
54
+ class Follower(EventSourcedProjection):
55
+ """Extends the :class:`~eventsourcing.projection.EventSourcedProjection` class
56
+ by pulling notification objects from its notification log readers, by converting
57
+ the notification objects to domain events and tracking objects and by processing
58
+ the reconstructed domain event objects.
58
59
  """
59
60
 
60
- follow_topics: ClassVar[Sequence[str]] = []
61
61
  pull_section_size = 10
62
62
 
63
+ def __init_subclass__(cls, **kwargs: Any) -> None:
64
+ super().__init_subclass__(**kwargs)
65
+ # for backwards compatibility, set "topics" if has "follow_topics".
66
+ cls.topics = getattr(cls, "follow_topics", cls.topics)
67
+
63
68
  def __init__(self, env: EnvType | None = None) -> None:
64
69
  super().__init__(env)
65
- self.readers: Dict[str, NotificationLogReader] = {}
66
- self.mappers: Dict[str, Mapper] = {}
67
- self.recorder: ProcessRecorder
70
+ self.readers: dict[str, NotificationLogReader] = {}
71
+ self.mappers: dict[str, Mapper] = {}
68
72
  self.is_threading_enabled = False
69
- self.processing_lock = RLock()
70
-
71
- def construct_recorder(self) -> ProcessRecorder:
72
- """
73
- Constructs and returns a :class:`~eventsourcing.persistence.ProcessRecorder`
74
- for the application to use as its application recorder.
75
- """
76
- return self.factory.process_recorder()
77
73
 
78
74
  def follow(self, name: str, log: NotificationLog) -> None:
79
- """
80
- Constructs a notification log reader and a mapper for
75
+ """Constructs a notification log reader and a mapper for
81
76
  the named application, and adds them to its collections
82
77
  of readers and mappers.
83
78
  """
@@ -95,13 +90,11 @@ class Follower(Application):
95
90
  def pull_and_process(
96
91
  self, leader_name: str, start: int | None = None, stop: int | None = None
97
92
  ) -> None:
98
- """
99
- Pull and process new domain event notifications.
100
- """
93
+ """Pull and process new domain event notifications."""
101
94
  if start is None:
102
- start = self.recorder.max_tracking_id(leader_name) + 1
95
+ start = self.recorder.max_tracking_id(leader_name)
103
96
  for notifications in self.pull_notifications(
104
- leader_name, start=start, stop=stop
97
+ leader_name, start=start, stop=stop, inclusive_of_start=False
105
98
  ):
106
99
  notifications_iter = self.filter_received_notifications(notifications)
107
100
  for domain_event, tracking in self.convert_notifications(
@@ -109,29 +102,41 @@ class Follower(Application):
109
102
  ):
110
103
  self.process_event(domain_event, tracking)
111
104
 
105
+ def process_event(
106
+ self, domain_event: DomainEventProtocol, tracking: Tracking
107
+ ) -> None:
108
+ with self.processing_lock:
109
+ super().process_event(domain_event, tracking)
110
+
112
111
  def pull_notifications(
113
- self, leader_name: str, start: int, stop: int | None = None
114
- ) -> Iterator[List[Notification]]:
115
- """
116
- Pulls batches of unseen :class:`~eventsourcing.persistence.Notification`
112
+ self,
113
+ leader_name: str,
114
+ start: int | None,
115
+ stop: int | None = None,
116
+ *,
117
+ inclusive_of_start: bool = True,
118
+ ) -> Iterator[list[Notification]]:
119
+ """Pulls batches of unseen :class:`~eventsourcing.persistence.Notification`
117
120
  objects from the notification log reader of the named application.
118
121
  """
119
122
  return self.readers[leader_name].select(
120
- start=start, stop=stop, topics=self.follow_topics
123
+ start=start,
124
+ stop=stop,
125
+ topics=self.topics,
126
+ inclusive_of_start=inclusive_of_start,
121
127
  )
122
128
 
123
129
  def filter_received_notifications(
124
- self, notifications: List[Notification]
125
- ) -> List[Notification]:
126
- if self.follow_topics:
127
- return [n for n in notifications if n.topic in self.follow_topics]
130
+ self, notifications: list[Notification]
131
+ ) -> list[Notification]:
132
+ if self.topics:
133
+ return [n for n in notifications if n.topic in self.topics]
128
134
  return notifications
129
135
 
130
136
  def convert_notifications(
131
137
  self, leader_name: str, notifications: Iterable[Notification]
132
- ) -> List[ProcessingJob]:
133
- """
134
- Uses the given :class:`~eventsourcing.persistence.Mapper` to convert
138
+ ) -> list[ProcessingJob]:
139
+ """Uses the given :class:`~eventsourcing.persistence.Mapper` to convert
135
140
  each received :class:`~eventsourcing.persistence.Notification`
136
141
  object to an :class:`~eventsourcing.domain.AggregateEvent` object
137
142
  paired with a :class:`~eventsourcing.persistence.Tracking` object.
@@ -147,82 +152,17 @@ class Follower(Application):
147
152
  processing_jobs.append((domain_event, tracking))
148
153
  return processing_jobs
149
154
 
150
- # @retry(IntegrityError, max_attempts=50000, wait=0.01)
151
- def process_event(
152
- self, domain_event: DomainEventProtocol, tracking: Tracking
153
- ) -> None:
154
- """
155
- Calls :func:`~eventsourcing.system.Follower.policy` method with
156
- the given :class:`~eventsourcing.domain.AggregateEvent` and a
157
- new :class:`~eventsourcing.application.ProcessingEvent` created from
158
- the given :class:`~eventsourcing.persistence.Tracking` object.
159
-
160
- The policy will collect any new aggregate events on the process
161
- event object.
162
-
163
- After the policy method returns, the process event object will
164
- then be recorded by calling
165
- :func:`~eventsourcing.application.Application.record`, which
166
- will return new notifications.
167
-
168
- After calling
169
- :func:`~eventsourcing.application.Application.take_snapshots`,
170
- the new notifications are passed to the
171
- :func:`~eventsourcing.application.Application.notify` method.
172
- """
173
- processing_event = ProcessingEvent(tracking=tracking)
174
- with self.processing_lock:
175
- self.policy(domain_event, processing_event)
176
- try:
177
- recordings = self._record(processing_event)
178
- except IntegrityError:
179
- if self.recorder.has_tracking_id(
180
- tracking.application_name,
181
- tracking.notification_id,
182
- ):
183
- pass
184
- else:
185
- raise
186
- else:
187
- self._take_snapshots(processing_event)
188
- self.notify(processing_event.events)
189
- self._notify(recordings)
190
-
191
- @abstractmethod
192
- def policy(
193
- self,
194
- domain_event: DomainEventProtocol,
195
- processing_event: ProcessingEvent,
196
- ) -> None:
197
- """
198
- Abstract domain event processing policy method. Must be
199
- implemented by event processing applications. When
200
- processing the given domain event, event processing
201
- applications must use the :func:`~ProcessingEvent.collect_events`
202
- method of the given process event object (instead of
203
- the application's :func:`~eventsourcing.application.Application.save`
204
- method) to collect pending events from changed aggregates,
205
- so that the new domain events will be recorded atomically
206
- with tracking information about the position of the given
207
- domain event's notification.
208
- """
209
-
210
155
 
211
156
  class RecordingEventReceiver(ABC):
212
- """
213
- Abstract base class for objects that may receive recording events.
214
- """
157
+ """Abstract base class for objects that may receive recording events."""
215
158
 
216
159
  @abstractmethod
217
- def receive_recording_event(self, recording_event: RecordingEvent) -> None:
218
- """
219
- Receives a recording event.
220
- """
160
+ def receive_recording_event(self, new_recording_event: RecordingEvent) -> None:
161
+ """Receives a recording event."""
221
162
 
222
163
 
223
164
  class Leader(Application):
224
- """
225
- Extends the :class:`~eventsourcing.application.Application`
165
+ """Extends the :class:`~eventsourcing.application.Application`
226
166
  class by also being responsible for keeping track of
227
167
  followers, and prompting followers when there are new
228
168
  domain event notifications to be pulled and processed.
@@ -230,17 +170,24 @@ class Leader(Application):
230
170
 
231
171
  def __init__(self, env: EnvType | None = None) -> None:
232
172
  super().__init__(env)
233
- self.followers: List[RecordingEventReceiver] = []
173
+ self.previous_max_notification_id: int | None = None
174
+ self.followers: list[RecordingEventReceiver] = []
234
175
 
235
176
  def lead(self, follower: RecordingEventReceiver) -> None:
236
- """
237
- Adds given follower to a list of followers.
238
- """
177
+ """Adds given follower to a list of followers."""
239
178
  self.followers.append(follower)
240
179
 
241
- def _notify(self, recordings: List[Recording]) -> None:
242
- """
243
- Calls :func:`receive_recording_event` on each follower
180
+ def save(
181
+ self,
182
+ *objs: MutableOrImmutableAggregate | DomainEventProtocol | None,
183
+ **kwargs: Any,
184
+ ) -> list[Recording]:
185
+ if self.previous_max_notification_id is None:
186
+ self.previous_max_notification_id = self.recorder.max_notification_id()
187
+ return super().save(*objs, **kwargs)
188
+
189
+ def _notify(self, recordings: list[Recording]) -> None:
190
+ """Calls :func:`receive_recording_event` on each follower
244
191
  whenever new events have just been saved.
245
192
  """
246
193
  super()._notify(recordings)
@@ -260,31 +207,28 @@ class Leader(Application):
260
207
 
261
208
 
262
209
  class ProcessApplication(Leader, Follower):
263
- """
264
- Base class for event processing applications
210
+ """Base class for event processing applications
265
211
  that are both "leaders" and followers".
266
212
  """
267
213
 
268
214
 
269
215
  class System:
270
- """
271
- Defines a system of applications.
272
- """
216
+ """Defines a system of applications."""
273
217
 
274
- __caller_modules: ClassVar[Dict[int, ModuleType]] = {}
218
+ __caller_modules: ClassVar[dict[int, ModuleType]] = {}
275
219
 
276
220
  def __init__(
277
221
  self,
278
- pipes: Iterable[Iterable[Type[Application]]],
222
+ pipes: Iterable[Iterable[type[Application]]],
279
223
  ):
280
224
  # Remember the caller frame's module, so that we might identify a topic.
281
- caller_frame = cast(FrameType, cast(FrameType, inspect.currentframe()).f_back)
225
+ caller_frame = cast(FrameType, inspect.currentframe()).f_back
282
226
  module = cast(ModuleType, inspect.getmodule(caller_frame))
283
- type(self).__caller_modules[id(self)] = module
227
+ type(self).__caller_modules[id(self)] = module # noqa: SLF001
284
228
 
285
229
  # Build nodes and edges.
286
- self.edges: List[Tuple[str, str]] = []
287
- classes: Dict[str, Type[Application]] = {}
230
+ self.edges: list[tuple[str, str]] = []
231
+ classes: dict[str, type[Application]] = {}
288
232
  for pipe in pipes:
289
233
  follower_cls = None
290
234
  for cls in pipe:
@@ -298,14 +242,13 @@ class System:
298
242
  if edge not in self.edges:
299
243
  self.edges.append(edge)
300
244
 
301
- self.nodes: Dict[str, str] = {}
302
- for name in classes:
303
- topic = get_topic(classes[name])
304
- self.nodes[name] = topic
245
+ self.nodes: dict[str, str] = {}
246
+ for name, cls in classes.items():
247
+ self.nodes[name] = get_topic(cls)
305
248
 
306
249
  # Identify leaders and followers.
307
- self.follows: Dict[str, List[str]] = defaultdict(list)
308
- self.leads: Dict[str, List[str]] = defaultdict(list)
250
+ self.follows: dict[str, list[str]] = defaultdict(list)
251
+ self.leads: dict[str, list[str]] = defaultdict(list)
309
252
  for edge in self.edges:
310
253
  self.leads[edge[0]].append(edge[1])
311
254
  self.follows[edge[1]].append(edge[0])
@@ -319,35 +262,37 @@ class System:
319
262
  # Check followers are followers.
320
263
  for name in self.follows:
321
264
  if not issubclass(classes[name], Follower):
322
- raise TypeError("Not a follower class: %s" % classes[name])
265
+ msg = f"Not a follower class: {classes[name]}"
266
+ raise TypeError(msg)
323
267
 
324
268
  # Check each process is a process application class.
325
269
  for name in self.processors:
326
270
  if not issubclass(classes[name], ProcessApplication):
327
- raise TypeError("Not a process application class: %s" % classes[name])
271
+ msg = f"Not a process application class: {classes[name]}"
272
+ raise TypeError(msg)
328
273
 
329
274
  @property
330
- def leaders(self) -> List[str]:
275
+ def leaders(self) -> list[str]:
331
276
  return list(self.leads.keys())
332
277
 
333
278
  @property
334
- def leaders_only(self) -> List[str]:
279
+ def leaders_only(self) -> list[str]:
335
280
  return [name for name in self.leads if name not in self.follows]
336
281
 
337
282
  @property
338
- def followers(self) -> List[str]:
283
+ def followers(self) -> list[str]:
339
284
  return list(self.follows.keys())
340
285
 
341
286
  @property
342
- def processors(self) -> List[str]:
287
+ def processors(self) -> list[str]:
343
288
  return [name for name in self.leads if name in self.follows]
344
289
 
345
- def get_app_cls(self, name: str) -> Type[Application]:
290
+ def get_app_cls(self, name: str) -> type[Application]:
346
291
  cls = resolve_topic(self.nodes[name])
347
292
  assert issubclass(cls, Application)
348
293
  return cls
349
294
 
350
- def leader_cls(self, name: str) -> Type[Leader]:
295
+ def leader_cls(self, name: str) -> type[Leader]:
351
296
  cls = self.get_app_cls(name)
352
297
  if issubclass(cls, Leader):
353
298
  return cls
@@ -355,13 +300,13 @@ class System:
355
300
  assert issubclass(cls, Leader)
356
301
  return cls
357
302
 
358
- def follower_cls(self, name: str) -> Type[Follower]:
303
+ def follower_cls(self, name: str) -> type[Follower]:
359
304
  cls = self.get_app_cls(name)
360
305
  assert issubclass(cls, Follower)
361
306
  return cls
362
307
 
363
308
  @property
364
- def topic(self) -> str | None:
309
+ def topic(self) -> str:
365
310
  """
366
311
  Returns a topic to the system object, if constructed as a module attribute.
367
312
  """
@@ -371,13 +316,14 @@ class System:
371
316
  if value is self:
372
317
  topic = module.__name__ + ":" + name
373
318
  assert resolve_topic(topic) is self
319
+ if topic is None:
320
+ msg = f"Unable to compute topic for system object: {self}"
321
+ raise ProgrammingError(msg)
374
322
  return topic
375
323
 
376
324
 
377
325
  class Runner(ABC):
378
- """
379
- Abstract base class for system runners.
380
- """
326
+ """Abstract base class for system runners."""
381
327
 
382
328
  def __init__(self, system: System, env: EnvType | None = None):
383
329
  self.system = system
@@ -386,65 +332,54 @@ class Runner(ABC):
386
332
 
387
333
  @abstractmethod
388
334
  def start(self) -> None:
389
- """
390
- Starts the runner.
391
- """
335
+ """Starts the runner."""
392
336
  if self.is_started:
393
337
  raise RunnerAlreadyStartedError
394
338
  self.is_started = True
395
339
 
396
340
  @abstractmethod
397
341
  def stop(self) -> None:
398
- """
399
- Stops the runner.
400
- """
342
+ """Stops the runner."""
401
343
 
402
344
  @abstractmethod
403
- def get(self, cls: Type[TApplication]) -> TApplication:
404
- """
405
- Returns an application instance for given application class.
406
- """
345
+ def get(self, cls: type[TApplication]) -> TApplication:
346
+ """Returns an application instance for given application class."""
347
+
348
+ def __enter__(self) -> Self:
349
+ self.start()
350
+ return self
351
+
352
+ def __exit__(self, *args: object, **kwargs: Any) -> None:
353
+ self.stop()
407
354
 
408
355
 
409
356
  class RunnerAlreadyStartedError(Exception):
410
- """
411
- Raised when runner is already started.
412
- """
357
+ """Raised when runner is already started."""
413
358
 
414
359
 
415
360
  class NotificationPullingError(Exception):
416
- """
417
- Raised when pulling notifications fails.
418
- """
361
+ """Raised when pulling notifications fails."""
419
362
 
420
363
 
421
364
  class NotificationConvertingError(Exception):
422
- """
423
- Raised when converting notifications fails.
424
- """
365
+ """Raised when converting notifications fails."""
425
366
 
426
367
 
427
368
  class EventProcessingError(Exception):
428
- """
429
- Raised when event processing fails.
430
- """
369
+ """Raised when event processing fails."""
431
370
 
432
371
 
433
372
  class SingleThreadedRunner(Runner, RecordingEventReceiver):
434
- """
435
- Runs a :class:`System` in a single thread.
436
- """
373
+ """Runs a :class:`System` in a single thread."""
437
374
 
438
375
  def __init__(self, system: System, env: EnvType | None = None):
439
- """
440
- Initialises runner with the given :class:`System`.
441
- """
376
+ """Initialises runner with the given :class:`System`."""
442
377
  super().__init__(system=system, env=env)
443
- self.apps: Dict[str, Application] = {}
444
- self._recording_events_received: List[RecordingEvent] = []
445
- self._prompted_names_lock = Lock()
378
+ self.apps: dict[str, Application] = {}
379
+ self._recording_events_received: list[RecordingEvent] = []
380
+ self._prompted_names_lock = threading.Lock()
446
381
  self._prompted_names: set[str] = set()
447
- self._processing_lock = Lock()
382
+ self._processing_lock = threading.Lock()
448
383
 
449
384
  # Construct followers.
450
385
  for name in self.system.followers:
@@ -461,34 +396,31 @@ class SingleThreadedRunner(Runner, RecordingEventReceiver):
461
396
  self.apps[name] = single
462
397
 
463
398
  def start(self) -> None:
464
- """
465
- Starts the runner. The applications mentioned in the system definition
399
+ """Starts the runner. The applications mentioned in the system definition
466
400
  are constructed. The followers are set up to follow the applications
467
401
  they are defined as following in the system definition. And the leaders
468
402
  are set up to lead the runner itself.
469
403
  """
470
-
471
404
  super().start()
472
405
 
473
406
  # Setup followers to follow leaders.
474
407
  for edge in self.system.edges:
475
408
  leader_name = edge[0]
476
409
  follower_name = edge[1]
477
- leader = cast(Leader, self.apps[leader_name])
478
- follower = cast(Follower, self.apps[follower_name])
410
+ leader = cast("Leader", self.apps[leader_name])
411
+ follower = cast("Follower", self.apps[follower_name])
479
412
  assert isinstance(leader, Leader)
480
413
  assert isinstance(follower, Follower)
481
414
  follower.follow(leader_name, leader.notification_log)
482
415
 
483
416
  # Setup leaders to lead this runner.
484
417
  for name in self.system.leaders:
485
- leader = cast(Leader, self.apps[name])
418
+ leader = cast("Leader", self.apps[name])
486
419
  assert isinstance(leader, Leader)
487
420
  leader.lead(self)
488
421
 
489
- def receive_recording_event(self, recording_event: RecordingEvent) -> None:
490
- """
491
- Receives recording event by appending the name of the leader
422
+ def receive_recording_event(self, new_recording_event: RecordingEvent) -> None:
423
+ """Receives recording event by appending the name of the leader
492
424
  to a list of prompted names.
493
425
 
494
426
  Then, unless this method has previously been called and not yet returned,
@@ -498,7 +430,7 @@ class SingleThreadedRunner(Runner, RecordingEventReceiver):
498
430
  continues until there are no more prompted names. In this way, a system
499
431
  of applications will process all events in a single thread.
500
432
  """
501
- leader_name = recording_event.application_name
433
+ leader_name = new_recording_event.application_name
502
434
  with self._prompted_names_lock:
503
435
  self._prompted_names.add(leader_name)
504
436
 
@@ -514,7 +446,7 @@ class SingleThreadedRunner(Runner, RecordingEventReceiver):
514
446
 
515
447
  for leader_name in prompted_names:
516
448
  for follower_name in self.system.leads[leader_name]:
517
- follower = cast(Follower, self.apps[follower_name])
449
+ follower = cast("Follower", self.apps[follower_name])
518
450
  follower.pull_and_process(leader_name)
519
451
 
520
452
  finally:
@@ -525,34 +457,23 @@ class SingleThreadedRunner(Runner, RecordingEventReceiver):
525
457
  app.close()
526
458
  self.apps.clear()
527
459
 
528
- def get(self, cls: Type[TApplication]) -> TApplication:
460
+ def get(self, cls: type[TApplication]) -> TApplication:
529
461
  app = self.apps[cls.name]
530
462
  assert isinstance(app, cls)
531
463
  return app
532
464
 
533
- def __enter__(self) -> Self:
534
- self.start()
535
- return self
536
-
537
- def __exit__(self, *args: object, **kwargs: Any) -> None:
538
- self.stop()
539
-
540
465
 
541
466
  class NewSingleThreadedRunner(Runner, RecordingEventReceiver):
542
- """
543
- Runs a :class:`System` in a single thread.
544
- """
467
+ """Runs a :class:`System` in a single thread."""
545
468
 
546
469
  def __init__(self, system: System, env: EnvType | None = None):
547
- """
548
- Initialises runner with the given :class:`System`.
549
- """
470
+ """Initialises runner with the given :class:`System`."""
550
471
  super().__init__(system=system, env=env)
551
- self.apps: Dict[str, Application] = {}
552
- self._recording_events_received: List[RecordingEvent] = []
553
- self._recording_events_received_lock = Lock()
554
- self._processing_lock = Lock()
555
- self._previous_max_notification_ids: Dict[str, int] = {}
472
+ self.apps: dict[str, Application] = {}
473
+ self._recording_events_received: list[RecordingEvent] = []
474
+ self._recording_events_received_lock = threading.Lock()
475
+ self._processing_lock = threading.Lock()
476
+ self._previous_max_notification_ids: dict[str, int] = {}
556
477
 
557
478
  # Construct followers.
558
479
  for name in self.system.followers:
@@ -569,8 +490,7 @@ class NewSingleThreadedRunner(Runner, RecordingEventReceiver):
569
490
  self.apps[name] = single
570
491
 
571
492
  def start(self) -> None:
572
- """
573
- Starts the runner.
493
+ """Starts the runner.
574
494
  The applications are constructed, and setup to lead and follow
575
495
  each other, according to the system definition.
576
496
  The followers are setup to follow the applications they follow
@@ -578,28 +498,26 @@ class NewSingleThreadedRunner(Runner, RecordingEventReceiver):
578
498
  leader), and their leaders are setup to lead the runner itself
579
499
  (send prompts).
580
500
  """
581
-
582
501
  super().start()
583
502
 
584
503
  # Setup followers to follow leaders.
585
504
  for edge in self.system.edges:
586
505
  leader_name = edge[0]
587
506
  follower_name = edge[1]
588
- leader = cast(Leader, self.apps[leader_name])
589
- follower = cast(Follower, self.apps[follower_name])
507
+ leader = cast("Leader", self.apps[leader_name])
508
+ follower = cast("Follower", self.apps[follower_name])
590
509
  assert isinstance(leader, Leader)
591
510
  assert isinstance(follower, Follower)
592
511
  follower.follow(leader_name, leader.notification_log)
593
512
 
594
513
  # Setup leaders to notify followers.
595
514
  for name in self.system.leaders:
596
- leader = cast(Leader, self.apps[name])
515
+ leader = cast("Leader", self.apps[name])
597
516
  assert isinstance(leader, Leader)
598
517
  leader.lead(self)
599
518
 
600
- def receive_recording_event(self, recording_event: RecordingEvent) -> None:
601
- """
602
- Receives recording event by appending it to list of received recording
519
+ def receive_recording_event(self, new_recording_event: RecordingEvent) -> None:
520
+ """Receives recording event by appending it to list of received recording
603
521
  events.
604
522
 
605
523
  Unless this method has previously been called and not yet returned, it
@@ -607,19 +525,19 @@ class NewSingleThreadedRunner(Runner, RecordingEventReceiver):
607
525
  events, until there are none remaining.
608
526
  """
609
527
  with self._recording_events_received_lock:
610
- self._recording_events_received.append(recording_event)
528
+ self._recording_events_received.append(new_recording_event)
611
529
 
612
530
  if self._processing_lock.acquire(blocking=False):
613
531
  try:
614
532
  while True:
615
533
  with self._recording_events_received_lock:
616
- recording_events_received = self._recording_events_received
534
+ recording_events = self._recording_events_received
617
535
  self._recording_events_received = []
618
536
 
619
- if not recording_events_received:
537
+ if not recording_events:
620
538
  break
621
539
 
622
- for recording_event in recording_events_received:
540
+ for recording_event in recording_events:
623
541
  leader_name = recording_event.application_name
624
542
  previous_max_notification_id = (
625
543
  self._previous_max_notification_ids.get(leader_name, 0)
@@ -642,9 +560,7 @@ class NewSingleThreadedRunner(Runner, RecordingEventReceiver):
642
560
  for follower_name in self.system.leads[leader_name]:
643
561
  follower = self.apps[follower_name]
644
562
  assert isinstance(follower, Follower)
645
- start = (
646
- follower.recorder.max_tracking_id(leader_name) + 1
647
- )
563
+ start = follower.recorder.max_tracking_id(leader_name)
648
564
  stop = recording_event.recordings[0].notification.id - 1
649
565
  follower.pull_and_process(
650
566
  leader_name=leader_name,
@@ -656,9 +572,9 @@ class NewSingleThreadedRunner(Runner, RecordingEventReceiver):
656
572
  follower = self.apps[follower_name]
657
573
  assert isinstance(follower, Follower)
658
574
  if (
659
- follower.follow_topics
575
+ follower.topics
660
576
  and recording.notification.topic
661
- not in follower.follow_topics
577
+ not in follower.topics
662
578
  ):
663
579
  continue
664
580
  follower.process_event(
@@ -681,26 +597,23 @@ class NewSingleThreadedRunner(Runner, RecordingEventReceiver):
681
597
  app.close()
682
598
  self.apps.clear()
683
599
 
684
- def get(self, cls: Type[TApplication]) -> TApplication:
600
+ def get(self, cls: type[TApplication]) -> TApplication:
685
601
  app = self.apps[cls.name]
686
602
  assert isinstance(app, cls)
687
603
  return app
688
604
 
689
605
 
690
606
  class MultiThreadedRunner(Runner):
691
- """
692
- Runs a :class:`System` with one :class:`MultiThreadedRunnerThread`
607
+ """Runs a :class:`System` with one :class:`MultiThreadedRunnerThread`
693
608
  for each :class:`Follower` in the system definition.
694
609
  """
695
610
 
696
611
  def __init__(self, system: System, env: EnvType | None = None):
697
- """
698
- Initialises runner with the given :class:`System`.
699
- """
612
+ """Initialises runner with the given :class:`System`."""
700
613
  super().__init__(system=system, env=env)
701
- self.apps: Dict[str, Application] = {}
702
- self.threads: Dict[str, MultiThreadedRunnerThread] = {}
703
- self.has_errored = Event()
614
+ self.apps: dict[str, Application] = {}
615
+ self.threads: dict[str, MultiThreadedRunnerThread] = {}
616
+ self.has_errored = threading.Event()
704
617
 
705
618
  # Construct followers.
706
619
  for follower_name in self.system.followers:
@@ -722,8 +635,7 @@ class MultiThreadedRunner(Runner):
722
635
  self.apps[name] = single
723
636
 
724
637
  def start(self) -> None:
725
- """
726
- Starts the runner.
638
+ """Starts the runner.
727
639
  A multi-threaded runner thread is started for each
728
640
  'follower' application in the system, and constructs
729
641
  an instance of each non-follower leader application in
@@ -736,7 +648,7 @@ class MultiThreadedRunner(Runner):
736
648
 
737
649
  # Construct followers.
738
650
  for follower_name in self.system.followers:
739
- follower = cast(Follower, self.apps[follower_name])
651
+ follower = cast("Follower", self.apps[follower_name])
740
652
 
741
653
  thread = MultiThreadedRunnerThread(
742
654
  follower=follower,
@@ -751,8 +663,8 @@ class MultiThreadedRunner(Runner):
751
663
 
752
664
  # Lead and follow.
753
665
  for edge in self.system.edges:
754
- leader = cast(Leader, self.apps[edge[0]])
755
- follower = cast(Follower, self.apps[edge[1]])
666
+ leader = cast("Leader", self.apps[edge[0]])
667
+ follower = cast("Follower", self.apps[edge[1]])
756
668
  follower.follow(leader.name, leader.notification_log)
757
669
  thread = self.threads[follower.name]
758
670
  leader.lead(thread)
@@ -778,37 +690,35 @@ class MultiThreadedRunner(Runner):
778
690
  if thread.error:
779
691
  raise thread.error
780
692
 
781
- def get(self, cls: Type[TApplication]) -> TApplication:
693
+ def get(self, cls: type[TApplication]) -> TApplication:
782
694
  app = self.apps[cls.name]
783
695
  assert isinstance(app, cls)
784
696
  return app
785
697
 
786
698
 
787
- class MultiThreadedRunnerThread(RecordingEventReceiver, Thread):
788
- """
789
- Runs one :class:`~eventsourcing.system.Follower` application in
699
+ class MultiThreadedRunnerThread(RecordingEventReceiver, threading.Thread):
700
+ """Runs one :class:`~eventsourcing.system.Follower` application in
790
701
  a :class:`~eventsourcing.system.MultiThreadedRunner`.
791
702
  """
792
703
 
793
704
  def __init__(
794
705
  self,
795
706
  follower: Follower,
796
- has_errored: Event,
707
+ has_errored: threading.Event,
797
708
  ):
798
709
  super().__init__(daemon=True)
799
710
  self.follower = follower
800
711
  self.has_errored = has_errored
801
712
  self.error: Exception | None = None
802
- self.is_stopping = Event()
803
- self.has_started = Event()
804
- self.is_prompted = Event()
805
- self.prompted_names: List[str] = []
806
- self.prompted_names_lock = Lock()
807
- self.is_running = Event()
713
+ self.is_stopping = threading.Event()
714
+ self.has_started = threading.Event()
715
+ self.is_prompted = threading.Event()
716
+ self.prompted_names: list[str] = []
717
+ self.prompted_names_lock = threading.Lock()
718
+ self.is_running = threading.Event()
808
719
 
809
720
  def run(self) -> None:
810
- """
811
- Loops forever until stopped. The loop blocks on waiting
721
+ """Loops forever until stopped. The loop blocks on waiting
812
722
  for the 'is_prompted' event to be set, then calls
813
723
  :func:`~Follower.pull_and_process` method for each
814
724
  prompted name.
@@ -830,12 +740,11 @@ class MultiThreadedRunnerThread(RecordingEventReceiver, Thread):
830
740
  self.error.__cause__ = e
831
741
  self.has_errored.set()
832
742
 
833
- def receive_recording_event(self, recording_event: RecordingEvent) -> None:
834
- """
835
- Receives prompt by appending name of
743
+ def receive_recording_event(self, new_recording_event: RecordingEvent) -> None:
744
+ """Receives prompt by appending name of
836
745
  leader to list of prompted names.
837
746
  """
838
- leader_name = recording_event.application_name
747
+ leader_name = new_recording_event.application_name
839
748
  with self.prompted_names_lock:
840
749
  if leader_name not in self.prompted_names:
841
750
  self.prompted_names.append(leader_name)
@@ -847,9 +756,7 @@ class MultiThreadedRunnerThread(RecordingEventReceiver, Thread):
847
756
 
848
757
 
849
758
  class NewMultiThreadedRunner(Runner, RecordingEventReceiver):
850
- """
851
- Runs a :class:`System` with multiple threads in a new way.
852
- """
759
+ """Runs a :class:`System` with multiple threads in a new way."""
853
760
 
854
761
  QUEUE_MAX_SIZE: int = 0
855
762
 
@@ -858,15 +765,13 @@ class NewMultiThreadedRunner(Runner, RecordingEventReceiver):
858
765
  system: System,
859
766
  env: EnvType | None = None,
860
767
  ):
861
- """
862
- Initialises runner with the given :class:`System`.
863
- """
768
+ """Initialises runner with the given :class:`System`."""
864
769
  super().__init__(system=system, env=env)
865
- self.apps: Dict[str, Application] = {}
866
- self.pulling_threads: Dict[str, List[PullingThread]] = {}
867
- self.processing_queues: Dict[str, Queue[List[ProcessingJob] | None]] = {}
868
- self.all_threads: List[PullingThread | ConvertingThread | ProcessingThread] = []
869
- self.has_errored = Event()
770
+ self.apps: dict[str, Application] = {}
771
+ self.pulling_threads: dict[str, list[PullingThread]] = {}
772
+ self.processing_queues: dict[str, Queue[list[ProcessingJob] | None]] = {}
773
+ self.all_threads: list[PullingThread | ConvertingThread | ProcessingThread] = []
774
+ self.has_errored = threading.Event()
870
775
 
871
776
  # Construct followers.
872
777
  for follower_name in self.system.followers:
@@ -888,8 +793,7 @@ class NewMultiThreadedRunner(Runner, RecordingEventReceiver):
888
793
  self.apps[name] = single
889
794
 
890
795
  def start(self) -> None:
891
- """
892
- Starts the runner.
796
+ """Starts the runner.
893
797
 
894
798
  A multi-threaded runner thread is started for each
895
799
  'follower' application in the system, and constructs
@@ -903,8 +807,8 @@ class NewMultiThreadedRunner(Runner, RecordingEventReceiver):
903
807
 
904
808
  # Start the processing threads.
905
809
  for follower_name in self.system.followers:
906
- follower = cast(Follower, self.apps[follower_name])
907
- processing_queue: Queue[List[ProcessingJob] | None] = Queue(
810
+ follower = cast("Follower", self.apps[follower_name])
811
+ processing_queue: Queue[list[ProcessingJob] | None] = Queue(
908
812
  maxsize=self.QUEUE_MAX_SIZE
909
813
  )
910
814
  self.processing_queues[follower_name] = processing_queue
@@ -919,9 +823,9 @@ class NewMultiThreadedRunner(Runner, RecordingEventReceiver):
919
823
  for edge in self.system.edges:
920
824
  # Set up follower to pull notifications from leader.
921
825
  leader_name = edge[0]
922
- leader = cast(Leader, self.apps[leader_name])
826
+ leader = cast("Leader", self.apps[leader_name])
923
827
  follower_name = edge[1]
924
- follower = cast(Follower, self.apps[follower_name])
828
+ follower = cast("Follower", self.apps[follower_name])
925
829
  follower.follow(leader.name, leader.notification_log)
926
830
 
927
831
  # Create converting queue.
@@ -957,7 +861,7 @@ class NewMultiThreadedRunner(Runner, RecordingEventReceiver):
957
861
 
958
862
  # Subscribe for notifications from leaders.
959
863
  for leader_name in self.system.leaders:
960
- leader = cast(Leader, self.apps[leader_name])
864
+ leader = cast("Leader", self.apps[leader_name])
961
865
  assert isinstance(leader, Leader)
962
866
  leader.lead(self)
963
867
 
@@ -981,19 +885,20 @@ class NewMultiThreadedRunner(Runner, RecordingEventReceiver):
981
885
  if thread.error:
982
886
  raise thread.error
983
887
 
984
- def get(self, cls: Type[TApplication]) -> TApplication:
888
+ def get(self, cls: type[TApplication]) -> TApplication:
985
889
  app = self.apps[cls.name]
986
890
  assert isinstance(app, cls)
987
891
  return app
988
892
 
989
- def receive_recording_event(self, recording_event: RecordingEvent) -> None:
990
- for pulling_thread in self.pulling_threads[recording_event.application_name]:
991
- pulling_thread.receive_recording_event(recording_event)
893
+ def receive_recording_event(self, new_recording_event: RecordingEvent) -> None:
894
+ for pulling_thread in self.pulling_threads[
895
+ new_recording_event.application_name
896
+ ]:
897
+ pulling_thread.receive_recording_event(new_recording_event)
992
898
 
993
899
 
994
- class PullingThread(Thread):
995
- """
996
- Receives or pulls notifications from the given leader, and
900
+ class PullingThread(threading.Thread):
901
+ """Receives or pulls notifications from the given leader, and
997
902
  puts them on a queue for conversion into processing jobs.
998
903
  """
999
904
 
@@ -1002,19 +907,19 @@ class PullingThread(Thread):
1002
907
  converting_queue: Queue[ConvertingJob],
1003
908
  follower: Follower,
1004
909
  leader_name: str,
1005
- has_errored: Event,
910
+ has_errored: threading.Event,
1006
911
  ):
1007
912
  super().__init__(daemon=True)
1008
- self.overflow_event = Event()
913
+ self.overflow_event = threading.Event()
1009
914
  self.recording_event_queue: Queue[RecordingEvent | None] = Queue(maxsize=100)
1010
915
  self.converting_queue = converting_queue
1011
- self.receive_lock = Lock()
916
+ self.receive_lock = threading.Lock()
1012
917
  self.follower = follower
1013
918
  self.leader_name = leader_name
1014
919
  self.error: Exception | None = None
1015
920
  self.has_errored = has_errored
1016
- self.is_stopping = Event()
1017
- self.has_started = Event()
921
+ self.is_stopping = threading.Event()
922
+ self.has_started = threading.Event()
1018
923
  self.mapper = self.follower.mappers[self.leader_name]
1019
924
  self.previous_max_notification_id = self.follower.recorder.max_tracking_id(
1020
925
  application_name=self.leader_name
@@ -1031,6 +936,7 @@ class PullingThread(Thread):
1031
936
  # Ignore recording event if already seen a subsequent.
1032
937
  if (
1033
938
  recording_event.previous_max_notification_id is not None
939
+ and self.previous_max_notification_id is not None
1034
940
  and recording_event.previous_max_notification_id
1035
941
  < self.previous_max_notification_id
1036
942
  ):
@@ -1039,13 +945,17 @@ class PullingThread(Thread):
1039
945
  # Catch up if there is a gap in sequence of recording events.
1040
946
  if (
1041
947
  recording_event.previous_max_notification_id is None
948
+ or self.previous_max_notification_id is None
1042
949
  or recording_event.previous_max_notification_id
1043
950
  > self.previous_max_notification_id
1044
951
  ):
1045
- start = self.previous_max_notification_id + 1
952
+ start = self.previous_max_notification_id
1046
953
  stop = recording_event.recordings[0].notification.id - 1
1047
954
  for notifications in self.follower.pull_notifications(
1048
- self.leader_name, start=start, stop=stop
955
+ self.leader_name,
956
+ start=start,
957
+ stop=stop,
958
+ inclusive_of_start=False,
1049
959
  ):
1050
960
  self.converting_queue.put(notifications)
1051
961
  self.previous_max_notification_id = notifications[-1].id
@@ -1069,18 +979,16 @@ class PullingThread(Thread):
1069
979
  self.recording_event_queue.put(None)
1070
980
 
1071
981
 
1072
- class ConvertingThread(Thread):
1073
- """
1074
- Converts notifications into processing jobs.
1075
- """
982
+ class ConvertingThread(threading.Thread):
983
+ """Converts notifications into processing jobs."""
1076
984
 
1077
985
  def __init__(
1078
986
  self,
1079
987
  converting_queue: Queue[ConvertingJob],
1080
- processing_queue: Queue[List[ProcessingJob] | None],
988
+ processing_queue: Queue[list[ProcessingJob] | None],
1081
989
  follower: Follower,
1082
990
  leader_name: str,
1083
- has_errored: Event,
991
+ has_errored: threading.Event,
1084
992
  ):
1085
993
  super().__init__(daemon=True)
1086
994
  self.converting_queue = converting_queue
@@ -1089,8 +997,8 @@ class ConvertingThread(Thread):
1089
997
  self.leader_name = leader_name
1090
998
  self.error: Exception | None = None
1091
999
  self.has_errored = has_errored
1092
- self.is_stopping = Event()
1093
- self.has_started = Event()
1000
+ self.is_stopping = threading.Event()
1001
+ self.has_started = threading.Event()
1094
1002
  self.mapper = self.follower.mappers[self.leader_name]
1095
1003
 
1096
1004
  def run(self) -> None:
@@ -1111,9 +1019,8 @@ class ConvertingThread(Thread):
1111
1019
  recording_event = recording_event_or_notifications
1112
1020
  for recording in recording_event.recordings:
1113
1021
  if (
1114
- self.follower.follow_topics
1115
- and recording.notification.topic
1116
- not in self.follower.follow_topics
1022
+ self.follower.topics
1023
+ and recording.notification.topic not in self.follower.topics
1117
1024
  ):
1118
1025
  continue
1119
1026
  tracking = Tracking(
@@ -1129,7 +1036,7 @@ class ConvertingThread(Thread):
1129
1036
  if processing_jobs:
1130
1037
  self.processing_queue.put(processing_jobs)
1131
1038
  except Exception as e:
1132
- print(traceback.format_exc())
1039
+ print(traceback.format_exc()) # noqa: T201
1133
1040
  self.error = NotificationConvertingError(str(e))
1134
1041
  self.error.__cause__ = e
1135
1042
  self.has_errored.set()
@@ -1139,25 +1046,24 @@ class ConvertingThread(Thread):
1139
1046
  self.converting_queue.put(None)
1140
1047
 
1141
1048
 
1142
- class ProcessingThread(Thread):
1143
- """
1144
- A processing thread gets events from a processing queue, and
1049
+ class ProcessingThread(threading.Thread):
1050
+ """A processing thread gets events from a processing queue, and
1145
1051
  calls the application's process_event() method.
1146
1052
  """
1147
1053
 
1148
1054
  def __init__(
1149
1055
  self,
1150
- processing_queue: Queue[List[ProcessingJob] | None],
1056
+ processing_queue: Queue[list[ProcessingJob] | None],
1151
1057
  follower: Follower,
1152
- has_errored: Event,
1058
+ has_errored: threading.Event,
1153
1059
  ):
1154
1060
  super().__init__(daemon=True)
1155
1061
  self.processing_queue = processing_queue
1156
1062
  self.follower = follower
1157
1063
  self.error: Exception | None = None
1158
1064
  self.has_errored = has_errored
1159
- self.is_stopping = Event()
1160
- self.has_started = Event()
1065
+ self.is_stopping = threading.Event()
1066
+ self.has_started = threading.Event()
1161
1067
 
1162
1068
  def run(self) -> None:
1163
1069
  self.has_started.set()
@@ -1180,9 +1086,7 @@ class ProcessingThread(Thread):
1180
1086
 
1181
1087
 
1182
1088
  class NotificationLogReader:
1183
- """
1184
- Reads domain event notifications from a notification log.
1185
- """
1089
+ """Reads domain event notifications from a notification log."""
1186
1090
 
1187
1091
  DEFAULT_SECTION_SIZE = 10
1188
1092
 
@@ -1191,8 +1095,7 @@ class NotificationLogReader:
1191
1095
  notification_log: NotificationLog,
1192
1096
  section_size: int = DEFAULT_SECTION_SIZE,
1193
1097
  ):
1194
- """
1195
- Initialises a reader with the given notification log,
1098
+ """Initialises a reader with the given notification log,
1196
1099
  and optionally a section size integer which determines
1197
1100
  the requested number of domain event notifications in
1198
1101
  each section retrieved from the notification log.
@@ -1201,8 +1104,7 @@ class NotificationLogReader:
1201
1104
  self.section_size = section_size
1202
1105
 
1203
1106
  def read(self, *, start: int) -> Iterator[Notification]:
1204
- """
1205
- Returns a generator that yields event notifications
1107
+ """Returns a generator that yields event notifications
1206
1108
  from the reader's notification log, starting from
1207
1109
  given start position (a notification ID).
1208
1110
 
@@ -1224,10 +1126,14 @@ class NotificationLogReader:
1224
1126
  section_id = section.next_id
1225
1127
 
1226
1128
  def select(
1227
- self, *, start: int, stop: int | None = None, topics: Sequence[str] = ()
1228
- ) -> Iterator[List[Notification]]:
1229
- """
1230
- Returns a generator that yields lists of event notifications
1129
+ self,
1130
+ *,
1131
+ start: int | None,
1132
+ stop: int | None = None,
1133
+ topics: Sequence[str] = (),
1134
+ inclusive_of_start: bool = True,
1135
+ ) -> Iterator[list[Notification]]:
1136
+ """Returns a generator that yields lists of event notifications
1231
1137
  from the reader's notification log, starting from given start
1232
1138
  position (a notification ID).
1233
1139
 
@@ -1240,12 +1146,18 @@ class NotificationLogReader:
1240
1146
  """
1241
1147
  while True:
1242
1148
  notifications = self.notification_log.select(
1243
- start=start, stop=stop, limit=self.section_size, topics=topics
1149
+ start=start,
1150
+ stop=stop,
1151
+ limit=self.section_size,
1152
+ topics=topics,
1153
+ inclusive_of_start=inclusive_of_start,
1244
1154
  )
1245
1155
  # Stop if zero notifications.
1246
1156
  if len(notifications) == 0:
1247
1157
  break
1248
1158
 
1249
1159
  # Otherwise, yield and continue.
1160
+ start = notifications[-1].id
1161
+ if inclusive_of_start:
1162
+ start += 1
1250
1163
  yield notifications
1251
- start = notifications[-1].id + 1