eventsourcing 9.4.0b1__py3-none-any.whl → 9.4.0b3__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/popo.py CHANGED
@@ -235,7 +235,7 @@ class POPOProcessRecorder(
235
235
  self, stored_events: list[StoredEvent], **kwargs: Any
236
236
  ) -> None:
237
237
  super()._assert_uniqueness(stored_events, **kwargs)
238
- t: Tracking | None = kwargs.get("tracking", None)
238
+ t: Tracking | None = kwargs.get("tracking")
239
239
  if t:
240
240
  self._assert_tracking_uniqueness(t)
241
241
 
@@ -243,7 +243,7 @@ class POPOProcessRecorder(
243
243
  self, stored_events: list[StoredEvent], **kwargs: Any
244
244
  ) -> Sequence[int] | None:
245
245
  notification_ids = super()._update_table(stored_events, **kwargs)
246
- t: Tracking | None = kwargs.get("tracking", None)
246
+ t: Tracking | None = kwargs.get("tracking")
247
247
  if t:
248
248
  self._insert_tracking(t)
249
249
  return notification_ids
eventsourcing/postgres.py CHANGED
@@ -75,7 +75,7 @@ class ConnectionPool(psycopg_pool.ConnectionPool[Any]):
75
75
 
76
76
 
77
77
  class PostgresDatastore:
78
- def __init__(
78
+ def __init__( # noqa: PLR0913
79
79
  self,
80
80
  dbname: str,
81
81
  host: str,
@@ -440,11 +440,9 @@ class PostgresApplicationRecorder(PostgresAggregateRecorder, ApplicationRecorder
440
440
  *,
441
441
  inclusive_of_start: bool = True,
442
442
  ) -> list[Notification]:
443
- """
444
- Returns a list of event notifications
443
+ """Returns a list of event notifications
445
444
  from 'start', limited by 'limit'.
446
445
  """
447
-
448
446
  params: list[int | str | Sequence[str]] = []
449
447
  statement = SQL("SELECT * FROM {0}.{1}").format(
450
448
  Identifier(self.datastore.schema),
@@ -501,9 +499,7 @@ class PostgresApplicationRecorder(PostgresAggregateRecorder, ApplicationRecorder
501
499
 
502
500
  @retry((InterfaceError, OperationalError), max_attempts=10, wait=0.2)
503
501
  def max_notification_id(self) -> int | None:
504
- """
505
- Returns the maximum notification ID.
506
- """
502
+ """Returns the maximum notification ID."""
507
503
  with self.datastore.get_connection() as conn, conn.cursor() as curs:
508
504
  curs.execute(self.max_notification_id_statement)
509
505
  fetchone = curs.fetchone()
@@ -553,7 +549,7 @@ class PostgresApplicationRecorder(PostgresAggregateRecorder, ApplicationRecorder
553
549
  notification_ids.append(row["notification_id"])
554
550
  if len(notification_ids) != len(stored_events):
555
551
  msg = "Couldn't get all notification IDs "
556
- msg += f"(got {len(notification_ids)}, expected {len(stored_events)}"
552
+ msg += f"(got {len(notification_ids)}, expected {len(stored_events)})"
557
553
  raise ProgrammingError(msg)
558
554
  return notification_ids
559
555
 
@@ -730,7 +726,7 @@ class PostgresProcessRecorder(
730
726
  stored_events: list[StoredEvent],
731
727
  **kwargs: Any,
732
728
  ) -> None:
733
- tracking: Tracking | None = kwargs.get("tracking", None)
729
+ tracking: Tracking | None = kwargs.get("tracking")
734
730
  if tracking is not None:
735
731
  self._insert_tracking(curs, tracking=tracking)
736
732
  super()._insert_events(curs, stored_events, **kwargs)
@@ -974,7 +970,8 @@ class PostgresFactory(InfrastructureFactory[PostgresTrackingRecorder]):
974
970
  tracking_recorder_class = resolve_topic(tracking_recorder_topic)
975
971
  else:
976
972
  tracking_recorder_class = cast(
977
- type[TPostgresTrackingRecorder], type(self).tracking_recorder_class
973
+ "type[TPostgresTrackingRecorder]",
974
+ type(self).tracking_recorder_class,
978
975
  )
979
976
  assert tracking_recorder_class is not None
980
977
  assert issubclass(tracking_recorder_class, PostgresTrackingRecorder)
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import contextlib
3
4
  import os
4
5
  import weakref
5
6
  from abc import ABC, abstractmethod
@@ -22,12 +23,13 @@ from eventsourcing.persistence import (
22
23
  from eventsourcing.utils import Environment, EnvType
23
24
 
24
25
  if TYPE_CHECKING:
26
+ from types import TracebackType
27
+
25
28
  from typing_extensions import Self
26
29
 
27
30
 
28
31
  class ApplicationSubscription(Iterator[tuple[DomainEventProtocol, Tracking]]):
29
- """
30
- An iterator that yields all domain events recorded in an application
32
+ """An iterator that yields all domain events recorded in an application
31
33
  sequence that have notification IDs greater than a given value. The iterator
32
34
  will block when all recorded domain events have been yielded, and then
33
35
  continue when new events are recorded. Domain events are returned along
@@ -49,30 +51,23 @@ class ApplicationSubscription(Iterator[tuple[DomainEventProtocol, Tracking]]):
49
51
  self.subscription = self.recorder.subscribe(gt=gt, topics=topics)
50
52
 
51
53
  def stop(self) -> None:
52
- """
53
- Stops the stored event subscription.
54
- """
54
+ """Stops the stored event subscription."""
55
55
  self.subscription.stop()
56
56
 
57
57
  def __enter__(self) -> Self:
58
- """
59
- Calls __enter__ on the stored event subscription.
60
- """
58
+ """Calls __enter__ on the stored event subscription."""
61
59
  self.subscription.__enter__()
62
60
  return self
63
61
 
64
62
  def __exit__(self, *args: object, **kwargs: Any) -> None:
65
- """
66
- Calls __exit__ on the stored event subscription.
67
- """
63
+ """Calls __exit__ on the stored event subscription."""
68
64
  self.subscription.__exit__(*args, **kwargs)
69
65
 
70
66
  def __iter__(self) -> Self:
71
67
  return self
72
68
 
73
69
  def __next__(self) -> tuple[DomainEventProtocol, Tracking]:
74
- """
75
- Returns the next stored event from the stored event subscription.
70
+ """Returns the next stored event from the stored event subscription.
76
71
  Constructs a tracking object that identifies the position of
77
72
  the event in the application sequence, and reconstructs a domain
78
73
  event object from the stored event object.
@@ -83,9 +78,7 @@ class ApplicationSubscription(Iterator[tuple[DomainEventProtocol, Tracking]]):
83
78
  return domain_event, tracking
84
79
 
85
80
  def __del__(self) -> None:
86
- """
87
- Stops the stored event subscription.
88
- """
81
+ """Stops the stored event subscription."""
89
82
  self.stop()
90
83
 
91
84
 
@@ -117,9 +110,7 @@ class Projection(ABC, Generic[TTrackingRecorder]):
117
110
  def process_event(
118
111
  self, domain_event: DomainEventProtocol, tracking: Tracking
119
112
  ) -> None:
120
- """
121
- Process a domain event and track it.
122
- """
113
+ """Process a domain event and track it."""
123
114
 
124
115
 
125
116
  TApplication = TypeVar("TApplication", bound=Application)
@@ -134,8 +125,7 @@ class ProjectionRunner(Generic[TApplication, TTrackingRecorder]):
134
125
  view_class: type[TTrackingRecorder],
135
126
  env: EnvType | None = None,
136
127
  ):
137
- """
138
- Constructs application from given application class with given environment.
128
+ """Constructs application from given application class with given environment.
139
129
  Also constructs a materialised view from given class using an infrastructure
140
130
  factory constructed with an environment named after the projection. Also
141
131
  constructs a projection with the constructed materialised view object.
@@ -143,7 +133,8 @@ class ProjectionRunner(Generic[TApplication, TTrackingRecorder]):
143
133
  thread, calls projection's process_event() method for each event and tracking
144
134
  object pair received from the subscription.
145
135
  """
146
- self._is_stopping = Event()
136
+ self._is_interrupted = Event()
137
+ self._has_called_stop = False
147
138
 
148
139
  self.app: TApplication = application_class(env)
149
140
 
@@ -165,22 +156,32 @@ class ProjectionRunner(Generic[TApplication, TTrackingRecorder]):
165
156
  gt=self.view.max_tracking_id(self.app.name),
166
157
  topics=self.projection.topics,
167
158
  )
168
- self.thread_error: BaseException | None = None
169
- self.processing_thread = Thread(
159
+ self._thread_error: BaseException | None = None
160
+ self._stop_thread = Thread(
161
+ target=self._stop_subscription_when_stopping,
162
+ kwargs={
163
+ "subscription": self.subscription,
164
+ "is_stopping": self._is_interrupted,
165
+ },
166
+ )
167
+ self._stop_thread.start()
168
+ self._processing_thread = Thread(
170
169
  target=self._process_events_loop,
171
170
  kwargs={
172
171
  "subscription": self.subscription,
173
172
  "projection": self.projection,
174
- "is_stopping": self._is_stopping,
173
+ "is_stopping": self._is_interrupted,
175
174
  "runner": weakref.ref(self),
176
175
  },
177
176
  )
178
- self.processing_thread.start()
177
+ self._processing_thread.start()
178
+
179
+ @property
180
+ def is_interrupted(self) -> Event:
181
+ return self._is_interrupted
179
182
 
180
183
  def _construct_env(self, name: str, env: EnvType | None = None) -> Environment:
181
- """
182
- Constructs environment from which projection will be configured.
183
- """
184
+ """Constructs environment from which projection will be configured."""
184
185
  _env: dict[str, str] = {}
185
186
  _env.update(os.environ)
186
187
  if env is not None:
@@ -188,11 +189,23 @@ class ProjectionRunner(Generic[TApplication, TTrackingRecorder]):
188
189
  return Environment(name, _env)
189
190
 
190
191
  def stop(self) -> None:
192
+ """Sets the "interrupted" event."""
193
+ self._has_called_stop = True
194
+ self._is_interrupted.set()
195
+
196
+ @staticmethod
197
+ def _stop_subscription_when_stopping(
198
+ subscription: ApplicationSubscription,
199
+ is_stopping: Event,
200
+ ) -> None:
201
+ """Stops the application subscription, which
202
+ will stop the event-processing thread.
191
203
  """
192
- Stops the application subscription, which will stop the event-processing thread.
193
- """
194
- self._is_stopping.set()
195
- self.subscription.stop()
204
+ try:
205
+ is_stopping.wait()
206
+ finally:
207
+ is_stopping.set()
208
+ subscription.stop()
196
209
 
197
210
  @staticmethod
198
211
  def _process_events_loop(
@@ -208,7 +221,7 @@ class ProjectionRunner(Generic[TApplication, TTrackingRecorder]):
208
221
  except BaseException as e:
209
222
  _runner = runner() # get reference from weakref
210
223
  if _runner is not None:
211
- _runner.thread_error = e
224
+ _runner._thread_error = e # noqa: SLF001
212
225
  else:
213
226
  msg = "ProjectionRunner was deleted before error could be assigned:\n"
214
227
  msg += format_exc()
@@ -217,21 +230,23 @@ class ProjectionRunner(Generic[TApplication, TTrackingRecorder]):
217
230
  RuntimeWarning,
218
231
  stacklevel=2,
219
232
  )
220
-
233
+ finally:
221
234
  is_stopping.set()
222
- subscription.stop()
223
235
 
224
236
  def run_forever(self, timeout: float | None = None) -> None:
225
- """
226
- Blocks until timeout, or until the runner is stopped or errors. Re-raises
237
+ """Blocks until timeout, or until the runner is stopped or errors. Re-raises
227
238
  any error otherwise exits normally
228
239
  """
229
- if self._is_stopping.wait(timeout=timeout) and self.thread_error is not None:
230
- raise self.thread_error
240
+ if (
241
+ self._is_interrupted.wait(timeout=timeout)
242
+ and self._thread_error is not None
243
+ ):
244
+ error = self._thread_error
245
+ self._thread_error = None
246
+ raise error
231
247
 
232
248
  def wait(self, notification_id: int | None, timeout: float = 1.0) -> None:
233
- """
234
- Blocks until timeout, or until the materialised view has recorded a tracking
249
+ """Blocks until timeout, or until the materialised view has recorded a tracking
235
250
  object that is greater than or equal to the given notification ID.
236
251
  """
237
252
  try:
@@ -239,24 +254,36 @@ class ProjectionRunner(Generic[TApplication, TTrackingRecorder]):
239
254
  application_name=self.subscription.name,
240
255
  notification_id=notification_id,
241
256
  timeout=timeout,
242
- interrupt=self._is_stopping,
257
+ interrupt=self._is_interrupted,
243
258
  )
244
259
  except WaitInterruptedError:
245
- if self.thread_error is not None:
246
- raise self.thread_error from None
260
+ if self._thread_error:
261
+ error = self._thread_error
262
+ self._thread_error = None
263
+ raise error from None
264
+ if self._has_called_stop:
265
+ return
266
+ raise
247
267
 
248
268
  def __enter__(self) -> Self:
249
269
  return self
250
270
 
251
- def __exit__(self, *args: object, **kwargs: Any) -> None:
252
- """
253
- Calls stop() and waits for the event-processing thread to exit.
254
- """
271
+ def __exit__(
272
+ self,
273
+ exc_type: type[BaseException] | None,
274
+ exc_val: BaseException | None,
275
+ exc_tb: TracebackType | None,
276
+ ) -> None:
277
+ """Calls stop() and waits for the event-processing thread to exit."""
255
278
  self.stop()
256
- self.processing_thread.join()
279
+ self._stop_thread.join()
280
+ self._processing_thread.join()
281
+ if self._thread_error:
282
+ error = self._thread_error
283
+ self._thread_error = None
284
+ raise error
257
285
 
258
286
  def __del__(self) -> None:
259
- """
260
- Calls stop().
261
- """
262
- self.stop()
287
+ """Calls stop()."""
288
+ with contextlib.suppress(AttributeError):
289
+ self.stop()
eventsourcing/sqlite.py CHANGED
@@ -412,8 +412,7 @@ class SQLiteApplicationRecorder(
412
412
  *,
413
413
  inclusive_of_start: bool = True,
414
414
  ) -> list[Notification]:
415
- """
416
- Returns a list of event notifications
415
+ """Returns a list of event notifications
417
416
  from 'start', limited by 'limit'.
418
417
  """
419
418
  params: list[int | str] = []
@@ -443,7 +442,7 @@ class SQLiteApplicationRecorder(
443
442
  else:
444
443
  statement += "AND "
445
444
  params += list(topics)
446
- statement += "topic IN (%s) " % ",".join("?" * len(topics))
445
+ statement += f"topic IN ({','.join('?' * len(topics))}) "
447
446
 
448
447
  params.append(limit)
449
448
  statement += "ORDER BY rowid LIMIT ?"
@@ -462,9 +461,7 @@ class SQLiteApplicationRecorder(
462
461
  ]
463
462
 
464
463
  def max_notification_id(self) -> int:
465
- """
466
- Returns the maximum notification ID.
467
- """
464
+ """Returns the maximum notification ID."""
468
465
  with self.datastore.transaction(commit=False) as c:
469
466
  return self._max_notification_id(c)
470
467
 
@@ -561,7 +558,7 @@ class SQLiteProcessRecorder(
561
558
  **kwargs: Any,
562
559
  ) -> Sequence[int] | None:
563
560
  returning = super()._insert_events(c, stored_events, **kwargs)
564
- tracking: Tracking | None = kwargs.get("tracking", None)
561
+ tracking: Tracking | None = kwargs.get("tracking")
565
562
  if tracking is not None:
566
563
  self._insert_tracking(c, tracking)
567
564
  return returning