eventsourcing 9.4.0a7__py3-none-any.whl → 9.4.0a8__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.

@@ -12,9 +12,10 @@ from queue import Queue
12
12
  from threading import Condition, Event, Lock, Semaphore, Thread, Timer
13
13
  from time import monotonic, sleep, time
14
14
  from types import GenericAlias, ModuleType
15
- from typing import TYPE_CHECKING, Any, Generic, TypeVar, Union, cast
15
+ from typing import TYPE_CHECKING, Any, Generic, Union, cast
16
16
  from uuid import UUID
17
- from warnings import warn
17
+
18
+ from typing_extensions import TypeVar
18
19
 
19
20
  from eventsourcing.domain import DomainEventProtocol, EventSourcingError
20
21
  from eventsourcing.utils import (
@@ -284,15 +285,6 @@ class Mapper:
284
285
  state=stored_state,
285
286
  )
286
287
 
287
- def from_domain_event(self, domain_event: DomainEventProtocol) -> StoredEvent:
288
- warn(
289
- "'from_domain_event()' is deprecated, use 'to_stored_event()' instead",
290
- DeprecationWarning,
291
- stacklevel=2,
292
- )
293
-
294
- return self.to_stored_event(domain_event)
295
-
296
288
  def to_domain_event(self, stored_event: StoredEvent) -> DomainEventProtocol:
297
289
  """
298
290
  Converts the given :class:`StoredEvent` to a domain event object.
@@ -508,16 +500,19 @@ class TrackingRecorder(Recorder, ABC):
508
500
  """
509
501
 
510
502
  @abstractmethod
511
- def has_tracking_id(self, application_name: str, notification_id: int) -> bool:
503
+ def has_tracking_id(
504
+ self, application_name: str, notification_id: int | None
505
+ ) -> bool:
512
506
  """
513
507
  Returns True if a tracking object with the given application name
514
- and notification ID has been recorded, otherwise returns False.
508
+ and notification ID has been recorded, and True if given notification_id is
509
+ None, otherwise returns False.
515
510
  """
516
511
 
517
512
  def wait(
518
513
  self,
519
514
  application_name: str,
520
- notification_id: int,
515
+ notification_id: int | None,
521
516
  timeout: float = 1.0,
522
517
  interrupt: Event | None = None,
523
518
  ) -> None:
@@ -634,12 +629,10 @@ class EventStore:
634
629
  )
635
630
 
636
631
 
637
- TInfrastructureFactory = TypeVar(
638
- "TInfrastructureFactory", bound="InfrastructureFactory[Any]"
632
+ TTrackingRecorder = TypeVar(
633
+ "TTrackingRecorder", bound=TrackingRecorder, default=TrackingRecorder
639
634
  )
640
635
 
641
- TTrackingRecorder = TypeVar("TTrackingRecorder", bound=TrackingRecorder)
642
-
643
636
 
644
637
  class InfrastructureFactory(ABC, Generic[TTrackingRecorder]):
645
638
  """
@@ -658,14 +651,15 @@ class InfrastructureFactory(ABC, Generic[TTrackingRecorder]):
658
651
 
659
652
  @classmethod
660
653
  def construct(
661
- cls: type[TInfrastructureFactory], env: Environment | None = None
662
- ) -> TInfrastructureFactory:
654
+ cls: type[InfrastructureFactory[TTrackingRecorder]],
655
+ env: Environment | None = None,
656
+ ) -> InfrastructureFactory[TTrackingRecorder]:
663
657
  """
664
658
  Constructs concrete infrastructure factory for given
665
659
  named application. Reads and resolves persistence
666
660
  topic from environment variable 'PERSISTENCE_MODULE'.
667
661
  """
668
- factory_cls: type[InfrastructureFactory[TrackingRecorder]]
662
+ factory_cls: type[InfrastructureFactory[TTrackingRecorder]]
669
663
  if env is None:
670
664
  env = Environment()
671
665
  topic = (
@@ -684,7 +678,7 @@ class InfrastructureFactory(ABC, Generic[TTrackingRecorder]):
684
678
  or "eventsourcing.popo"
685
679
  )
686
680
  try:
687
- obj: type[InfrastructureFactory[TrackingRecorder]] | ModuleType = (
681
+ obj: type[InfrastructureFactory[TTrackingRecorder]] | ModuleType = (
688
682
  resolve_topic(topic)
689
683
  )
690
684
  except TopicError as e:
@@ -697,7 +691,7 @@ class InfrastructureFactory(ABC, Generic[TTrackingRecorder]):
697
691
 
698
692
  if isinstance(obj, ModuleType):
699
693
  # Find the factory in the module.
700
- factory_classes: list[type[InfrastructureFactory[TrackingRecorder]]] = []
694
+ factory_classes: list[type[InfrastructureFactory[TTrackingRecorder]]] = []
701
695
  for member in obj.__dict__.values():
702
696
  if (
703
697
  member is not InfrastructureFactory
@@ -724,7 +718,7 @@ class InfrastructureFactory(ABC, Generic[TTrackingRecorder]):
724
718
  else:
725
719
  msg = f"Not an infrastructure factory class or module: {topic}"
726
720
  raise AssertionError(msg)
727
- return cast(TInfrastructureFactory, factory_cls(env=env))
721
+ return factory_cls(env=env)
728
722
 
729
723
  def __init__(self, env: Environment):
730
724
  """
eventsourcing/popo.py CHANGED
@@ -219,7 +219,11 @@ class POPOTrackingRecorder(POPORecorder, TrackingRecorder):
219
219
  with self._database_lock:
220
220
  return self._max_tracking_ids[application_name]
221
221
 
222
- def has_tracking_id(self, application_name: str, notification_id: int) -> bool:
222
+ def has_tracking_id(
223
+ self, application_name: str, notification_id: int | None
224
+ ) -> bool:
225
+ if notification_id is None:
226
+ return True
223
227
  with self._database_lock:
224
228
  return notification_id in self._tracking_table[application_name]
225
229
 
eventsourcing/postgres.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import contextlib
3
4
  import logging
4
5
  from asyncio import CancelledError
5
6
  from contextlib import contextmanager
@@ -180,6 +181,9 @@ class PostgresDatastore:
180
181
  def __exit__(self, *args: object, **kwargs: Any) -> None:
181
182
  self.close()
182
183
 
184
+ def __del__(self) -> None:
185
+ self.close()
186
+
183
187
 
184
188
  class PostgresRecorder:
185
189
  """Base class for recorders that use PostgreSQL."""
@@ -194,9 +198,8 @@ class PostgresRecorder:
194
198
  def construct_create_table_statements(self) -> list[str]:
195
199
  return []
196
200
 
197
- @staticmethod
198
- def check_table_name_length(table_name: str, schema_name: str) -> None:
199
- schema_prefix = schema_name + "."
201
+ def check_table_name_length(self, table_name: str) -> None:
202
+ schema_prefix = self.datastore.schema + "."
200
203
  if table_name.startswith(schema_prefix):
201
204
  unqualified_table_name = table_name[len(schema_prefix) :]
202
205
  else:
@@ -219,7 +222,7 @@ class PostgresAggregateRecorder(PostgresRecorder, AggregateRecorder):
219
222
  events_table_name: str = "stored_events",
220
223
  ):
221
224
  super().__init__(datastore)
222
- self.check_table_name_length(events_table_name, datastore.schema)
225
+ self.check_table_name_length(events_table_name)
223
226
  self.events_table_name = events_table_name
224
227
  # Index names can't be qualified names, but
225
228
  # are created in the same schema as the table.
@@ -283,7 +286,7 @@ class PostgresAggregateRecorder(PostgresRecorder, AggregateRecorder):
283
286
 
284
287
  def _insert_events(
285
288
  self,
286
- c: Cursor[DictRow],
289
+ curs: Cursor[DictRow],
287
290
  stored_events: list[StoredEvent],
288
291
  **_: Any,
289
292
  ) -> None:
@@ -291,18 +294,18 @@ class PostgresAggregateRecorder(PostgresRecorder, AggregateRecorder):
291
294
 
292
295
  def _insert_stored_events(
293
296
  self,
294
- c: Cursor[DictRow],
297
+ curs: Cursor[DictRow],
295
298
  stored_events: list[StoredEvent],
296
299
  **_: Any,
297
300
  ) -> None:
298
301
  # Only do something if there is something to do.
299
302
  if len(stored_events) > 0:
300
- self._lock_table(c)
303
+ self._lock_table(curs)
301
304
 
302
- self._notify_channel(c)
305
+ self._notify_channel(curs)
303
306
 
304
307
  # Insert events.
305
- c.executemany(
308
+ curs.executemany(
306
309
  query=self.insert_events_statement,
307
310
  params_seq=[
308
311
  (
@@ -316,15 +319,15 @@ class PostgresAggregateRecorder(PostgresRecorder, AggregateRecorder):
316
319
  returning="RETURNING" in self.insert_events_statement,
317
320
  )
318
321
 
319
- def _lock_table(self, c: Cursor[DictRow]) -> None:
322
+ def _lock_table(self, curs: Cursor[DictRow]) -> None:
320
323
  pass
321
324
 
322
- def _notify_channel(self, c: Cursor[DictRow]) -> None:
325
+ def _notify_channel(self, curs: Cursor[DictRow]) -> None:
323
326
  pass
324
327
 
325
328
  def _fetch_ids_after_insert_events(
326
329
  self,
327
- c: Cursor[DictRow],
330
+ curs: Cursor[DictRow],
328
331
  stored_events: list[StoredEvent],
329
332
  **kwargs: Any,
330
333
  ) -> Sequence[int] | None:
@@ -479,7 +482,7 @@ class PostgresApplicationRecorder(PostgresAggregateRecorder, ApplicationRecorder
479
482
  assert fetchone is not None
480
483
  return fetchone["max"]
481
484
 
482
- def _lock_table(self, c: Cursor[DictRow]) -> None:
485
+ def _lock_table(self, curs: Cursor[DictRow]) -> None:
483
486
  # Acquire "EXCLUSIVE" table lock, to serialize transactions that insert
484
487
  # stored events, so that readers don't pass over gaps that are filled in
485
488
  # later. We want each transaction that will be issued with notifications
@@ -501,23 +504,23 @@ class PostgresApplicationRecorder(PostgresAggregateRecorder, ApplicationRecorder
501
504
  # https://stackoverflow.com/questions/45866187/guarantee-monotonicity-of
502
505
  # -postgresql-serial-column-values-by-commit-order
503
506
  for lock_statement in self.lock_table_statements:
504
- c.execute(lock_statement, prepare=True)
507
+ curs.execute(lock_statement, prepare=True)
505
508
 
506
- def _notify_channel(self, c: Cursor[DictRow]) -> None:
507
- c.execute("NOTIFY " + self.channel_name)
509
+ def _notify_channel(self, curs: Cursor[DictRow]) -> None:
510
+ curs.execute("NOTIFY " + self.channel_name)
508
511
 
509
512
  def _fetch_ids_after_insert_events(
510
513
  self,
511
- c: Cursor[DictRow],
514
+ curs: Cursor[DictRow],
512
515
  stored_events: list[StoredEvent],
513
516
  **kwargs: Any,
514
517
  ) -> Sequence[int] | None:
515
518
  notification_ids: list[int] = []
516
519
  len_events = len(stored_events)
517
520
  if len_events:
518
- while c.nextset() and len(notification_ids) != len_events:
519
- if c.statusmessage and c.statusmessage.startswith("INSERT"):
520
- row = c.fetchone()
521
+ while curs.nextset() and len(notification_ids) != len_events:
522
+ if curs.statusmessage and curs.statusmessage.startswith("INSERT"):
523
+ row = curs.fetchone()
521
524
  assert row is not None
522
525
  notification_ids.append(row["notification_id"])
523
526
  if len(notification_ids) != len(stored_events):
@@ -579,7 +582,7 @@ class PostgresTrackingRecorder(PostgresRecorder, TrackingRecorder):
579
582
  **kwargs: Any,
580
583
  ):
581
584
  super().__init__(datastore, **kwargs)
582
- self.check_table_name_length(tracking_table_name, datastore.schema)
585
+ self.check_table_name_length(tracking_table_name)
583
586
  self.tracking_table_name = tracking_table_name
584
587
  self.create_table_statements.append(
585
588
  "CREATE TABLE IF NOT EXISTS "
@@ -605,16 +608,20 @@ class PostgresTrackingRecorder(PostgresRecorder, TrackingRecorder):
605
608
 
606
609
  @retry((InterfaceError, OperationalError), max_attempts=10, wait=0.2)
607
610
  def insert_tracking(self, tracking: Tracking) -> None:
608
- c: Connection[DictRow]
609
- with self.datastore.get_connection() as c, c.transaction(), c.cursor() as curs:
611
+ conn: Connection[DictRow]
612
+ with (
613
+ self.datastore.get_connection() as conn,
614
+ conn.transaction(),
615
+ conn.cursor() as curs,
616
+ ):
610
617
  self._insert_tracking(curs, tracking)
611
618
 
612
619
  def _insert_tracking(
613
620
  self,
614
- c: Cursor[DictRow],
621
+ curs: Cursor[DictRow],
615
622
  tracking: Tracking,
616
623
  ) -> None:
617
- c.execute(
624
+ curs.execute(
618
625
  query=self.insert_tracking_statement,
619
626
  params=(
620
627
  tracking.application_name,
@@ -636,7 +643,11 @@ class PostgresTrackingRecorder(PostgresRecorder, TrackingRecorder):
636
643
  return fetchone["max"]
637
644
 
638
645
  @retry((InterfaceError, OperationalError), max_attempts=10, wait=0.2)
639
- def has_tracking_id(self, application_name: str, notification_id: int) -> bool:
646
+ def has_tracking_id(
647
+ self, application_name: str, notification_id: int | None
648
+ ) -> bool:
649
+ if notification_id is None:
650
+ return True
640
651
  conn: Connection[DictRow]
641
652
  with self.datastore.get_connection() as conn, conn.cursor() as curs:
642
653
  curs.execute(
@@ -667,14 +678,14 @@ class PostgresProcessRecorder(
667
678
 
668
679
  def _insert_events(
669
680
  self,
670
- c: Cursor[DictRow],
681
+ curs: Cursor[DictRow],
671
682
  stored_events: list[StoredEvent],
672
683
  **kwargs: Any,
673
684
  ) -> None:
674
685
  tracking: Tracking | None = kwargs.get("tracking", None)
675
686
  if tracking is not None:
676
- self._insert_tracking(c, tracking=tracking)
677
- super()._insert_events(c, stored_events, **kwargs)
687
+ self._insert_tracking(curs, tracking=tracking)
688
+ super()._insert_events(curs, stored_events, **kwargs)
678
689
 
679
690
 
680
691
  class PostgresFactory(InfrastructureFactory[PostgresTrackingRecorder]):
@@ -960,11 +971,8 @@ class PostgresFactory(InfrastructureFactory[PostgresTrackingRecorder]):
960
971
  return recorder
961
972
 
962
973
  def close(self) -> None:
963
- if hasattr(self, "datastore"):
974
+ with contextlib.suppress(AttributeError):
964
975
  self.datastore.close()
965
976
 
966
- def __del__(self) -> None:
967
- self.close()
968
-
969
977
 
970
978
  Factory = PostgresFactory
@@ -72,13 +72,19 @@ class Projection(ABC, Generic[TTrackingRecorder]):
72
72
  name: str = ""
73
73
  """Name of projection, used to pick prefixed environment variables."""
74
74
  topics: Sequence[str] = ()
75
- """Event topics, used to filter events in database."""
75
+ """Filter events in database when subscribing to an application."""
76
76
 
77
77
  def __init__(
78
78
  self,
79
- tracking_recorder: TTrackingRecorder,
79
+ view: TTrackingRecorder,
80
80
  ):
81
- self.tracking_recorder = tracking_recorder
81
+ """Initialises a projection instance."""
82
+ self._view = view
83
+
84
+ @property
85
+ def view(self) -> TTrackingRecorder:
86
+ """Materialised view of an event-sourced application."""
87
+ return self._view
82
88
 
83
89
  @singledispatchmethod
84
90
  @abstractmethod
@@ -90,9 +96,6 @@ class Projection(ABC, Generic[TTrackingRecorder]):
90
96
  """
91
97
 
92
98
 
93
- TProjection = TypeVar("TProjection", bound=Projection[Any])
94
-
95
-
96
99
  TApplication = TypeVar("TApplication", bound=Application)
97
100
 
98
101
 
@@ -101,28 +104,28 @@ class ProjectionRunner(Generic[TApplication, TTrackingRecorder]):
101
104
  self,
102
105
  *,
103
106
  application_class: type[TApplication],
107
+ view_class: type[TTrackingRecorder],
104
108
  projection_class: type[Projection[TTrackingRecorder]],
105
- tracking_recorder_class: type[TTrackingRecorder] | None = None,
106
109
  env: EnvType | None = None,
107
110
  ):
108
111
  self.app: TApplication = application_class(env)
109
112
 
110
- projection_environment = self._construct_env(
111
- name=projection_class.name or projection_class.__name__, env=env
112
- )
113
- self.projection_factory: InfrastructureFactory[TTrackingRecorder] = (
114
- InfrastructureFactory.construct(env=projection_environment)
115
- )
116
- self.tracking_recorder: TTrackingRecorder = (
117
- self.projection_factory.tracking_recorder(tracking_recorder_class)
113
+ self.view = (
114
+ InfrastructureFactory[TTrackingRecorder]
115
+ .construct(
116
+ env=self._construct_env(
117
+ name=projection_class.name or projection_class.__name__, env=env
118
+ )
119
+ )
120
+ .tracking_recorder(view_class)
118
121
  )
119
122
 
120
123
  self.projection = projection_class(
121
- tracking_recorder=self.tracking_recorder,
124
+ view=self.view,
122
125
  )
123
126
  self.subscription = ApplicationSubscription(
124
127
  app=self.app,
125
- gt=self.tracking_recorder.max_tracking_id(self.app.name),
128
+ gt=self.view.max_tracking_id(self.app.name),
126
129
  topics=self.projection.topics,
127
130
  )
128
131
  self._is_stopping = Event()
@@ -183,9 +186,9 @@ class ProjectionRunner(Generic[TApplication, TTrackingRecorder]):
183
186
  if self._is_stopping.wait(timeout=timeout) and self.thread_error is not None:
184
187
  raise self.thread_error
185
188
 
186
- def wait(self, notification_id: int, timeout: float = 1.0) -> None:
189
+ def wait(self, notification_id: int | None, timeout: float = 1.0) -> None:
187
190
  try:
188
- self.projection.tracking_recorder.wait(
191
+ self.projection.view.wait(
189
192
  application_name=self.subscription.name,
190
193
  notification_id=notification_id,
191
194
  timeout=timeout,
eventsourcing/sqlite.py CHANGED
@@ -530,7 +530,11 @@ class SQLiteTrackingRecorder(SQLiteRecorder, TrackingRecorder):
530
530
  c.execute(self.select_max_tracking_id_statement, params)
531
531
  return c.fetchone()[0]
532
532
 
533
- def has_tracking_id(self, application_name: str, notification_id: int) -> bool:
533
+ def has_tracking_id(
534
+ self, application_name: str, notification_id: int | None
535
+ ) -> bool:
536
+ if notification_id is None:
537
+ return True
534
538
  params = [application_name, notification_id]
535
539
  with self.datastore.transaction(commit=False) as c:
536
540
  c.execute(self.count_tracking_id_statement, params)
eventsourcing/system.py CHANGED
@@ -7,16 +7,18 @@ from abc import ABC, abstractmethod
7
7
  from collections import defaultdict
8
8
  from queue import Full, Queue
9
9
  from types import FrameType, ModuleType
10
- from typing import TYPE_CHECKING, Any, ClassVar, Optional, Union, cast
10
+ from typing import TYPE_CHECKING, Any, Callable, ClassVar, Optional, Union, cast
11
11
 
12
12
  if TYPE_CHECKING:
13
13
  from collections.abc import Iterable, Iterator, Sequence
14
14
  from typing_extensions import Self
15
+ from eventsourcing.dispatch import singledispatchmethod
15
16
 
16
17
  from eventsourcing.application import (
17
18
  Application,
18
19
  NotificationLog,
19
20
  ProcessingEvent,
21
+ ProgrammingError,
20
22
  Section,
21
23
  TApplication,
22
24
  )
@@ -196,8 +198,11 @@ class Follower(Application):
196
198
  self.notify(processing_event.events)
197
199
  self._notify(recordings)
198
200
 
199
- @abstractmethod
200
- def policy(
201
+ policy: (
202
+ Callable[[DomainEventProtocol, ProcessingEvent], None] | singledispatchmethod
203
+ )
204
+
205
+ def policy( # type: ignore[no-redef]
201
206
  self,
202
207
  domain_event: DomainEventProtocol,
203
208
  processing_event: ProcessingEvent,
@@ -379,7 +384,7 @@ class System:
379
384
  return cls
380
385
 
381
386
  @property
382
- def topic(self) -> str | None:
387
+ def topic(self) -> str:
383
388
  """
384
389
  Returns a topic to the system object, if constructed as a module attribute.
385
390
  """
@@ -389,6 +394,9 @@ class System:
389
394
  if value is self:
390
395
  topic = module.__name__ + ":" + name
391
396
  assert resolve_topic(topic) is self
397
+ if topic is None:
398
+ msg = "Unable to compute topic for system object: %s" % self
399
+ raise ProgrammingError(msg)
392
400
  return topic
393
401
 
394
402
 
@@ -423,6 +431,13 @@ class Runner(ABC):
423
431
  Returns an application instance for given application class.
424
432
  """
425
433
 
434
+ def __enter__(self) -> Self:
435
+ self.start()
436
+ return self
437
+
438
+ def __exit__(self, *args: object, **kwargs: Any) -> None:
439
+ self.stop()
440
+
426
441
 
427
442
  class RunnerAlreadyStartedError(Exception):
428
443
  """
@@ -548,13 +563,6 @@ class SingleThreadedRunner(Runner, RecordingEventReceiver):
548
563
  assert isinstance(app, cls)
549
564
  return app
550
565
 
551
- def __enter__(self) -> Self:
552
- self.start()
553
- return self
554
-
555
- def __exit__(self, *args: object, **kwargs: Any) -> None:
556
- self.stop()
557
-
558
566
 
559
567
  class NewSingleThreadedRunner(Runner, RecordingEventReceiver):
560
568
  """