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