eventsourcing 9.3.5__py3-none-any.whl → 9.4.0a1__py3-none-any.whl

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

Potentially problematic release.


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

@@ -0,0 +1,157 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import weakref
5
+ from abc import ABC, abstractmethod
6
+ from threading import Event, Thread
7
+ from traceback import format_exc
8
+ from typing import TYPE_CHECKING, Any, Dict, Generic, Type, TypeVar
9
+ from warnings import warn
10
+
11
+ from eventsourcing.application import Application
12
+ from eventsourcing.dispatch import singledispatchmethod
13
+ from eventsourcing.persistence import (
14
+ InfrastructureFactory,
15
+ Tracking,
16
+ TrackingRecorder,
17
+ TTrackingRecorder,
18
+ WaitInterruptedError,
19
+ )
20
+ from eventsourcing.utils import Environment, EnvType
21
+
22
+ if TYPE_CHECKING: # pragma: no cover
23
+ from typing_extensions import Self
24
+
25
+ from eventsourcing.application import ApplicationSubscription
26
+ from eventsourcing.domain import DomainEventProtocol
27
+
28
+
29
+ class Projection(ABC, Generic[TTrackingRecorder]):
30
+ name: str = ""
31
+
32
+ def __init__(
33
+ self,
34
+ tracking_recorder: TTrackingRecorder,
35
+ ):
36
+ self.tracking_recorder = tracking_recorder
37
+
38
+ @singledispatchmethod
39
+ @abstractmethod
40
+ def process_event(
41
+ self, domain_event: DomainEventProtocol, tracking: Tracking
42
+ ) -> None:
43
+ """
44
+ Process a domain event and track it.
45
+ """
46
+
47
+
48
+ TProjection = TypeVar("TProjection", bound=Projection[Any])
49
+ TApplication = TypeVar("TApplication", bound=Application)
50
+
51
+
52
+ class ProjectionRunner(Generic[TApplication, TTrackingRecorder]):
53
+ def __init__(
54
+ self,
55
+ *,
56
+ application_class: Type[TApplication],
57
+ projection_class: Type[Projection[TTrackingRecorder]],
58
+ tracking_recorder_class: Type[TTrackingRecorder] | None = None,
59
+ env: EnvType | None = None,
60
+ ):
61
+ self.app: TApplication = application_class(env)
62
+
63
+ projection_environment = self._construct_env(
64
+ name=projection_class.name or projection_class.__name__, env=env
65
+ )
66
+ self.projection_factory: InfrastructureFactory[TTrackingRecorder] = (
67
+ InfrastructureFactory.construct(env=projection_environment)
68
+ )
69
+ self.tracking_recorder: TTrackingRecorder = (
70
+ self.projection_factory.tracking_recorder(tracking_recorder_class)
71
+ )
72
+
73
+ self.subscription = self.app.subscribe(
74
+ gt=self.tracking_recorder.max_tracking_id(self.app.name)
75
+ )
76
+ self.projection = projection_class(
77
+ tracking_recorder=self.tracking_recorder,
78
+ )
79
+ self._has_error = Event()
80
+ self.thread_error: BaseException | None = None
81
+ self.processing_thread = Thread(
82
+ target=self._process_events_loop,
83
+ kwargs={
84
+ "subscription": self.subscription,
85
+ "projection": self.projection,
86
+ "has_error": self._has_error,
87
+ "runner": weakref.ref(self),
88
+ },
89
+ )
90
+ self.processing_thread.start()
91
+
92
+ def _construct_env(self, name: str, env: EnvType | None = None) -> Environment:
93
+ """
94
+ Constructs environment from which projection will be configured.
95
+ """
96
+ _env: Dict[str, str] = {}
97
+ _env.update(os.environ)
98
+ if env is not None:
99
+ _env.update(env)
100
+ return Environment(name, _env)
101
+
102
+ def stop(self) -> None:
103
+ self.subscription.subscription.stop()
104
+
105
+ @staticmethod
106
+ def _process_events_loop(
107
+ subscription: ApplicationSubscription,
108
+ projection: Projection[TrackingRecorder],
109
+ has_error: Event,
110
+ runner: weakref.ReferenceType[ProjectionRunner[Application, TrackingRecorder]],
111
+ ) -> None:
112
+ try:
113
+ with subscription:
114
+ for domain_event, tracking in subscription:
115
+ projection.process_event(domain_event, tracking)
116
+ except BaseException as e:
117
+ _runner = runner() # get reference from weakref
118
+ if _runner is not None:
119
+ _runner.thread_error = e
120
+ else:
121
+ msg = "ProjectionRunner was deleted before error could be assigned:\n"
122
+ msg += format_exc()
123
+ warn(
124
+ msg,
125
+ RuntimeWarning,
126
+ stacklevel=2,
127
+ )
128
+
129
+ has_error.set()
130
+ subscription.subscription.stop()
131
+
132
+ def run_forever(self, timeout: float | None = None) -> None:
133
+ if self._has_error.wait(timeout=timeout):
134
+ assert self.thread_error is not None # for mypy
135
+ raise self.thread_error
136
+
137
+ def wait(self, notification_id: int, timeout: float = 1.0) -> None:
138
+ try:
139
+ self.projection.tracking_recorder.wait(
140
+ application_name=self.subscription.name,
141
+ notification_id=notification_id,
142
+ timeout=timeout,
143
+ interrupt=self._has_error,
144
+ )
145
+ except WaitInterruptedError:
146
+ assert self.thread_error is not None # for mypy
147
+ raise self.thread_error from None
148
+
149
+ def __enter__(self) -> Self:
150
+ return self
151
+
152
+ def __exit__(self, *args: object, **kwargs: Any) -> None:
153
+ self.stop()
154
+ self.processing_thread.join()
155
+
156
+ def __del__(self) -> None:
157
+ self.stop()
eventsourcing/sqlite.py CHANGED
@@ -23,12 +23,15 @@ from eventsourcing.persistence import (
23
23
  PersistenceError,
24
24
  ProcessRecorder,
25
25
  ProgrammingError,
26
+ Recorder,
26
27
  StoredEvent,
28
+ Subscription,
27
29
  Tracking,
30
+ TrackingRecorder,
28
31
  )
29
- from eventsourcing.utils import Environment, strtobool
32
+ from eventsourcing.utils import Environment, resolve_topic, strtobool
30
33
 
31
- if TYPE_CHECKING: # pragma: nocover
34
+ if TYPE_CHECKING: # pragma: no cover
32
35
  from types import TracebackType
33
36
 
34
37
  SQLITE3_DEFAULT_LOCK_TIMEOUT = 5
@@ -242,16 +245,32 @@ class SQLiteDatastore:
242
245
  self.close()
243
246
 
244
247
 
245
- class SQLiteAggregateRecorder(AggregateRecorder):
248
+ class SQLiteRecorder(Recorder):
246
249
  def __init__(
247
250
  self,
248
251
  datastore: SQLiteDatastore,
249
- events_table_name: str = "stored_events",
250
252
  ):
251
253
  assert isinstance(datastore, SQLiteDatastore)
252
254
  self.datastore = datastore
253
- self.events_table_name = events_table_name
254
255
  self.create_table_statements = self.construct_create_table_statements()
256
+
257
+ def construct_create_table_statements(self) -> List[str]:
258
+ return []
259
+
260
+ def create_table(self) -> None:
261
+ with self.datastore.transaction(commit=True) as c:
262
+ for statement in self.create_table_statements:
263
+ c.execute(statement)
264
+
265
+
266
+ class SQLiteAggregateRecorder(SQLiteRecorder, AggregateRecorder):
267
+ def __init__(
268
+ self,
269
+ datastore: SQLiteDatastore,
270
+ events_table_name: str = "stored_events",
271
+ ):
272
+ self.events_table_name = events_table_name
273
+ super().__init__(datastore)
255
274
  self.insert_events_statement = (
256
275
  f"INSERT INTO {self.events_table_name} VALUES (?,?,?,?)"
257
276
  )
@@ -260,7 +279,8 @@ class SQLiteAggregateRecorder(AggregateRecorder):
260
279
  )
261
280
 
262
281
  def construct_create_table_statements(self) -> List[str]:
263
- statement = (
282
+ statements = super().construct_create_table_statements()
283
+ statements.append(
264
284
  "CREATE TABLE IF NOT EXISTS "
265
285
  f"{self.events_table_name} ("
266
286
  "originator_id TEXT, "
@@ -271,12 +291,7 @@ class SQLiteAggregateRecorder(AggregateRecorder):
271
291
  "(originator_id, originator_version)) "
272
292
  "WITHOUT ROWID"
273
293
  )
274
- return [statement]
275
-
276
- def create_table(self) -> None:
277
- with self.datastore.transaction(commit=True) as c:
278
- for statement in self.create_table_statements:
279
- c.execute(statement)
294
+ return statements
280
295
 
281
296
  def insert_events(
282
297
  self, stored_events: List[StoredEvent], **kwargs: Any
@@ -389,25 +404,45 @@ class SQLiteApplicationRecorder(
389
404
 
390
405
  def select_notifications(
391
406
  self,
392
- start: int,
407
+ start: int | None,
393
408
  limit: int,
394
409
  stop: int | None = None,
395
410
  topics: Sequence[str] = (),
411
+ *,
412
+ inclusive_of_start: bool = True,
396
413
  ) -> List[Notification]:
397
414
  """
398
415
  Returns a list of event notifications
399
416
  from 'start', limited by 'limit'.
400
417
  """
401
- params: List[int | str] = [start]
402
- statement = f"SELECT rowid, * FROM {self.events_table_name} WHERE rowid>=? "
418
+ params: List[int | str] = []
419
+ statement = f"SELECT rowid, * FROM {self.events_table_name} "
420
+ has_where = False
421
+ if start is not None:
422
+ has_where = True
423
+ statement += "WHERE "
424
+ params.append(start)
425
+ if inclusive_of_start:
426
+ statement += "rowid>=? "
427
+ else:
428
+ statement += "rowid>? "
403
429
 
404
430
  if stop is not None:
431
+ if not has_where:
432
+ has_where = True
433
+ statement += "WHERE "
434
+ else:
435
+ statement += "AND "
405
436
  params.append(stop)
406
- statement += "AND rowid<=? "
437
+ statement += "rowid<=? "
407
438
 
408
439
  if topics:
440
+ if not has_where:
441
+ statement += "WHERE "
442
+ else:
443
+ statement += "AND "
409
444
  params += list(topics)
410
- statement += "AND topic IN (%s) " % ",".join("?" * len(topics))
445
+ statement += "topic IN (%s) " % ",".join("?" * len(topics))
411
446
 
412
447
  params.append(limit)
413
448
  statement += "ORDER BY rowid LIMIT ?"
@@ -434,19 +469,20 @@ class SQLiteApplicationRecorder(
434
469
 
435
470
  def _max_notification_id(self, c: SQLiteCursor) -> int:
436
471
  c.execute(self.select_max_notification_id_statement)
437
- return c.fetchone()[0] or 0
472
+ return c.fetchone()[0]
438
473
 
474
+ def subscribe(self, gt: int | None = None) -> Subscription[ApplicationRecorder]:
475
+ msg = f"The {type(self).__qualname__} recorder does not support subscriptions"
476
+ raise NotImplementedError(msg)
439
477
 
440
- class SQLiteProcessRecorder(
441
- SQLiteApplicationRecorder,
442
- ProcessRecorder,
443
- ):
478
+
479
+ class SQLiteTrackingRecorder(SQLiteRecorder, TrackingRecorder):
444
480
  def __init__(
445
481
  self,
446
482
  datastore: SQLiteDatastore,
447
- events_table_name: str = "stored_events",
483
+ **kwargs: Any,
448
484
  ):
449
- super().__init__(datastore, events_table_name)
485
+ super().__init__(datastore, **kwargs)
450
486
  self.insert_tracking_statement = "INSERT INTO tracking VALUES (?,?)"
451
487
  self.select_max_tracking_id_statement = (
452
488
  "SELECT MAX(notification_id) FROM tracking WHERE application_name=?"
@@ -468,11 +504,28 @@ class SQLiteProcessRecorder(
468
504
  )
469
505
  return statements
470
506
 
471
- def max_tracking_id(self, application_name: str) -> int:
507
+ def insert_tracking(self, tracking: Tracking) -> None:
508
+ with self.datastore.transaction(commit=True) as c:
509
+ self._insert_tracking(c, tracking)
510
+
511
+ def _insert_tracking(
512
+ self,
513
+ c: SQLiteCursor,
514
+ tracking: Tracking,
515
+ ) -> None:
516
+ c.execute(
517
+ self.insert_tracking_statement,
518
+ (
519
+ tracking.application_name,
520
+ tracking.notification_id,
521
+ ),
522
+ )
523
+
524
+ def max_tracking_id(self, application_name: str) -> int | None:
472
525
  params = [application_name]
473
526
  with self.datastore.transaction(commit=False) as c:
474
527
  c.execute(self.select_max_tracking_id_statement, params)
475
- return c.fetchone()[0] or 0
528
+ return c.fetchone()[0]
476
529
 
477
530
  def has_tracking_id(self, application_name: str, notification_id: int) -> bool:
478
531
  params = [application_name, notification_id]
@@ -480,6 +533,20 @@ class SQLiteProcessRecorder(
480
533
  c.execute(self.count_tracking_id_statement, params)
481
534
  return bool(c.fetchone()[0])
482
535
 
536
+
537
+ class SQLiteProcessRecorder(
538
+ SQLiteTrackingRecorder,
539
+ SQLiteApplicationRecorder,
540
+ ProcessRecorder,
541
+ ):
542
+ def __init__(
543
+ self,
544
+ datastore: SQLiteDatastore,
545
+ *,
546
+ events_table_name: str = "stored_events",
547
+ ):
548
+ super().__init__(datastore, events_table_name=events_table_name)
549
+
483
550
  def _insert_events(
484
551
  self,
485
552
  c: SQLiteCursor,
@@ -489,23 +556,18 @@ class SQLiteProcessRecorder(
489
556
  returning = super()._insert_events(c, stored_events, **kwargs)
490
557
  tracking: Tracking | None = kwargs.get("tracking", None)
491
558
  if tracking is not None:
492
- c.execute(
493
- self.insert_tracking_statement,
494
- (
495
- tracking.application_name,
496
- tracking.notification_id,
497
- ),
498
- )
559
+ self._insert_tracking(c, tracking)
499
560
  return returning
500
561
 
501
562
 
502
- class Factory(InfrastructureFactory):
563
+ class SQLiteFactory(InfrastructureFactory[SQLiteTrackingRecorder]):
503
564
  SQLITE_DBNAME = "SQLITE_DBNAME"
504
565
  SQLITE_LOCK_TIMEOUT = "SQLITE_LOCK_TIMEOUT"
505
566
  CREATE_TABLE = "CREATE_TABLE"
506
567
 
507
568
  aggregate_recorder_class = SQLiteAggregateRecorder
508
569
  application_recorder_class = SQLiteApplicationRecorder
570
+ tracking_recorder_class = SQLiteTrackingRecorder
509
571
  process_recorder_class = SQLiteProcessRecorder
510
572
 
511
573
  def __init__(self, env: Environment):
@@ -549,13 +611,55 @@ class Factory(InfrastructureFactory):
549
611
  return recorder
550
612
 
551
613
  def application_recorder(self) -> ApplicationRecorder:
552
- recorder = self.application_recorder_class(datastore=self.datastore)
614
+ application_recorder_topic = self.env.get(self.APPLICATION_RECORDER_TOPIC)
615
+
616
+ if application_recorder_topic:
617
+ application_recorder_class: Type[SQLiteApplicationRecorder] = resolve_topic(
618
+ application_recorder_topic
619
+ )
620
+ assert issubclass(application_recorder_class, SQLiteApplicationRecorder)
621
+ else:
622
+ application_recorder_class = self.application_recorder_class
623
+
624
+ recorder = application_recorder_class(datastore=self.datastore)
625
+
626
+ if self.env_create_table():
627
+ recorder.create_table()
628
+ return recorder
629
+
630
+ def tracking_recorder(
631
+ self, tracking_recorder_class: Type[SQLiteTrackingRecorder] | None = None
632
+ ) -> SQLiteTrackingRecorder:
633
+ if tracking_recorder_class is None:
634
+ tracking_recorder_topic = self.env.get(self.TRACKING_RECORDER_TOPIC)
635
+
636
+ if tracking_recorder_topic:
637
+ tracking_recorder_class = resolve_topic(tracking_recorder_topic)
638
+ else:
639
+ tracking_recorder_class = self.tracking_recorder_class
640
+
641
+ assert tracking_recorder_class is not None
642
+ assert issubclass(tracking_recorder_class, SQLiteTrackingRecorder)
643
+
644
+ recorder = tracking_recorder_class(datastore=self.datastore)
645
+
553
646
  if self.env_create_table():
554
647
  recorder.create_table()
555
648
  return recorder
556
649
 
557
650
  def process_recorder(self) -> ProcessRecorder:
558
- recorder = self.process_recorder_class(datastore=self.datastore)
651
+ process_recorder_topic = self.env.get(self.PROCESS_RECORDER_TOPIC)
652
+
653
+ if process_recorder_topic:
654
+ process_recorder_class: Type[SQLiteProcessRecorder] = resolve_topic(
655
+ process_recorder_topic
656
+ )
657
+ assert issubclass(process_recorder_class, SQLiteProcessRecorder)
658
+ else:
659
+ process_recorder_class = self.process_recorder_class
660
+
661
+ recorder = process_recorder_class(datastore=self.datastore)
662
+
559
663
  if self.env_create_table():
560
664
  recorder.create_table()
561
665
  return recorder
@@ -566,3 +670,6 @@ class Factory(InfrastructureFactory):
566
670
 
567
671
  def close(self) -> None:
568
672
  self.datastore.close()
673
+
674
+
675
+ Factory = SQLiteFactory