eventsourcing 9.3.0b1__py3-none-any.whl → 9.3.1__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.

@@ -25,6 +25,8 @@ from typing import (
25
25
  )
26
26
  from warnings import warn
27
27
 
28
+ from typing_extensions import deprecated
29
+
28
30
  from eventsourcing.domain import (
29
31
  Aggregate,
30
32
  CanMutateProtocol,
@@ -908,7 +910,12 @@ class Application:
908
910
  TApplication = TypeVar("TApplication", bound=Application)
909
911
 
910
912
 
911
- class AggregateNotFoundError(EventSourcingError):
913
+ @deprecated("AggregateNotFound is deprecated, use AggregateNotFoundError instead")
914
+ class AggregateNotFound(EventSourcingError): # noqa: N818
915
+ pass
916
+
917
+
918
+ class AggregateNotFoundError(AggregateNotFound):
912
919
  """
913
920
  Raised when an :class:`~eventsourcing.domain.Aggregate`
914
921
  object is not found in a :class:`Repository`.
@@ -1,10 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
- import contextlib
4
- from collections import defaultdict
5
3
  from dataclasses import dataclass
6
4
  from datetime import datetime, timezone
7
- from typing import Any, ClassVar, Dict, Iterable, List, Type, TypeVar, cast
5
+ from typing import Any, Iterable, List, Type, TypeVar, cast
8
6
  from uuid import UUID, uuid4
9
7
 
10
8
  from eventsourcing.dispatch import singledispatchmethod
@@ -29,6 +27,7 @@ class Aggregate:
29
27
  id: UUID
30
28
  version: int
31
29
  created_on: datetime
30
+ _pending_events: List[DomainEvent]
32
31
 
33
32
  def __init__(self, event: DomainEvent):
34
33
  self.id = event.originator_id
@@ -47,15 +46,15 @@ class Aggregate:
47
46
  timestamp=event_class.create_timestamp(),
48
47
  )
49
48
  new_event = event_class(**kwargs)
50
- self.apply(new_event)
51
- self.pending_events.append(new_event)
49
+ self._apply(new_event)
50
+ self._pending_events.append(new_event)
52
51
 
53
52
  @singledispatchmethod
54
- def apply(self, event: DomainEvent) -> None:
53
+ def _apply(self, event: DomainEvent) -> None:
55
54
  """Applies event to aggregate."""
56
55
 
57
56
  def collect_events(self) -> List[DomainEvent]:
58
- events, self.pending_events = self.pending_events, []
57
+ events, self._pending_events = self._pending_events, []
59
58
  return events
60
59
 
61
60
  @classmethod
@@ -64,25 +63,12 @@ class Aggregate:
64
63
  _: TAggregate | None,
65
64
  events: Iterable[DomainEvent],
66
65
  ) -> TAggregate | None:
67
- aggregate = object.__new__(cls)
66
+ aggregate: TAggregate = object.__new__(cls)
67
+ aggregate._pending_events = []
68
68
  for event in events:
69
- aggregate.apply(event)
69
+ aggregate._apply(event)
70
70
  return aggregate
71
71
 
72
- @property
73
- def pending_events(self) -> List[DomainEvent]:
74
- return type(self).__pending_events[id(self)]
75
-
76
- @pending_events.setter
77
- def pending_events(self, pending_events: List[DomainEvent]) -> None:
78
- type(self).__pending_events[id(self)] = pending_events
79
-
80
- __pending_events: ClassVar[Dict[int, List[DomainEvent]]] = defaultdict(list)
81
-
82
- def __del__(self) -> None:
83
- with contextlib.suppress(KeyError):
84
- type(self).__pending_events.pop(id(self))
85
-
86
72
 
87
73
  class Dog(Aggregate):
88
74
  @dataclass(frozen=True)
@@ -102,27 +88,27 @@ class Dog(Aggregate):
102
88
  name=name,
103
89
  )
104
90
  dog = cast(Dog, cls.projector(None, [event]))
105
- dog.pending_events.append(event)
91
+ dog._pending_events.append(event)
106
92
  return dog
107
93
 
108
94
  def add_trick(self, trick: str) -> None:
109
95
  self.trigger_event(self.TrickAdded, trick=trick)
110
96
 
111
97
  @singledispatchmethod
112
- def apply(self, event: DomainEvent) -> None:
98
+ def _apply(self, event: DomainEvent) -> None:
113
99
  """Applies event to aggregate."""
114
100
 
115
- @apply.register(Registered)
101
+ @_apply.register(Registered)
116
102
  def _(self, event: Registered) -> None:
117
103
  super().__init__(event)
118
104
  self.name = event.name
119
105
  self.tricks: List[str] = []
120
106
 
121
- @apply.register(TrickAdded)
107
+ @_apply.register(TrickAdded)
122
108
  def _(self, event: TrickAdded) -> None:
123
109
  self.tricks.append(event.trick)
124
110
  self.version = event.originator_version
125
111
 
126
- @apply.register(Snapshot)
112
+ @_apply.register(Snapshot)
127
113
  def _(self, event: Snapshot) -> None:
128
114
  self.__dict__.update(event.state)
@@ -23,112 +23,119 @@ class ContentManagementSystemTestCase(TestCase):
23
23
  env: ClassVar[Dict[str, str]] = {}
24
24
 
25
25
  def test_system(self) -> None:
26
- runner = SingleThreadedRunner(system=ContentManagementSystem(), env=self.env)
27
- runner.start()
28
-
29
- content_management_app = runner.get(ContentManagementApplication)
30
- search_index_app = runner.get(SearchIndexApplication)
31
-
32
- # Set user_id context variable.
33
- user_id = uuid4()
34
- user_id_cvar.set(user_id)
35
-
36
- # Create empty pages.
37
- content_management_app.create_page(title="Animals", slug="animals")
38
- content_management_app.create_page(title="Plants", slug="plants")
39
- content_management_app.create_page(title="Minerals", slug="minerals")
40
-
41
- # Search, expect no results.
42
- self.assertEqual(0, len(search_index_app.search("cat")))
43
- self.assertEqual(0, len(search_index_app.search("rose")))
44
- self.assertEqual(0, len(search_index_app.search("calcium")))
45
-
46
- # Update the pages.
47
- content_management_app.update_body(slug="animals", body="cat")
48
- content_management_app.update_body(slug="plants", body="rose")
49
- content_management_app.update_body(slug="minerals", body="calcium")
50
-
51
- # Search for single words.
52
- page_ids = search_index_app.search("cat")
53
- self.assertEqual(1, len(page_ids))
54
- page = content_management_app.get_page_by_id(page_ids[0])
55
- self.assertEqual(page["slug"], "animals")
56
- self.assertEqual(page["body"], "cat")
57
-
58
- page_ids = search_index_app.search("rose")
59
- self.assertEqual(1, len(page_ids))
60
- page = content_management_app.get_page_by_id(page_ids[0])
61
- self.assertEqual(page["slug"], "plants")
62
- self.assertEqual(page["body"], "rose")
63
-
64
- page_ids = search_index_app.search("calcium")
65
- self.assertEqual(1, len(page_ids))
66
- page = content_management_app.get_page_by_id(page_ids[0])
67
- self.assertEqual(page["slug"], "minerals")
68
- self.assertEqual(page["body"], "calcium")
69
-
70
- self.assertEqual(len(search_index_app.search("dog")), 0)
71
- self.assertEqual(len(search_index_app.search("bluebell")), 0)
72
- self.assertEqual(len(search_index_app.search("zinc")), 0)
73
-
74
- # Update the pages again.
75
- content_management_app.update_body(slug="animals", body="cat dog zebra")
76
- content_management_app.update_body(slug="plants", body="bluebell rose jasmine")
77
- content_management_app.update_body(slug="minerals", body="iron zinc calcium")
78
-
79
- # Search for single words.
80
- page_ids = search_index_app.search("cat")
81
- self.assertEqual(1, len(page_ids))
82
- page = content_management_app.get_page_by_id(page_ids[0])
83
- self.assertEqual(page["slug"], "animals")
84
- self.assertEqual(page["body"], "cat dog zebra")
85
-
86
- page_ids = search_index_app.search("rose")
87
- self.assertEqual(1, len(page_ids))
88
- page = content_management_app.get_page_by_id(page_ids[0])
89
- self.assertEqual(page["slug"], "plants")
90
- self.assertEqual(page["body"], "bluebell rose jasmine")
91
-
92
- page_ids = search_index_app.search("calcium")
93
- self.assertEqual(1, len(page_ids))
94
- page = content_management_app.get_page_by_id(page_ids[0])
95
- self.assertEqual(page["slug"], "minerals")
96
- self.assertEqual(page["body"], "iron zinc calcium")
97
-
98
- page_ids = search_index_app.search("dog")
99
- self.assertEqual(1, len(page_ids))
100
- page = content_management_app.get_page_by_id(page_ids[0])
101
- self.assertEqual(page["slug"], "animals")
102
- self.assertEqual(page["body"], "cat dog zebra")
103
-
104
- page_ids = search_index_app.search("bluebell")
105
- self.assertEqual(1, len(page_ids))
106
- page = content_management_app.get_page_by_id(page_ids[0])
107
- self.assertEqual(page["slug"], "plants")
108
- self.assertEqual(page["body"], "bluebell rose jasmine")
109
-
110
- page_ids = search_index_app.search("zinc")
111
- self.assertEqual(1, len(page_ids))
112
- page = content_management_app.get_page_by_id(page_ids[0])
113
- self.assertEqual(page["slug"], "minerals")
114
- self.assertEqual(page["body"], "iron zinc calcium")
115
-
116
- # Search for multiple words in same page.
117
- page_ids = search_index_app.search("dog cat")
118
- self.assertEqual(1, len(page_ids))
119
- page = content_management_app.get_page_by_id(page_ids[0])
120
- self.assertEqual(page["slug"], "animals")
121
- self.assertEqual(page["body"], "cat dog zebra")
122
-
123
- # Search for multiple words in same page, expect no results.
124
- page_ids = search_index_app.search("rose zebra")
125
- self.assertEqual(0, len(page_ids))
126
-
127
- # Search for alternative words, expect two results.
128
- page_ids = search_index_app.search("rose OR zebra")
129
- pages = [content_management_app.get_page_by_id(page_id) for page_id in page_ids]
130
- self.assertEqual(2, len(pages))
131
- self.assertEqual(["animals", "plants"], sorted(p["slug"] for p in pages))
26
+ with SingleThreadedRunner(
27
+ system=ContentManagementSystem(), env=self.env
28
+ ) as runner:
29
+
30
+ content_management_app = runner.get(ContentManagementApplication)
31
+ search_index_app = runner.get(SearchIndexApplication)
32
+
33
+ # Set user_id context variable.
34
+ user_id = uuid4()
35
+ user_id_cvar.set(user_id)
36
+
37
+ # Create empty pages.
38
+ content_management_app.create_page(title="Animals", slug="animals")
39
+ content_management_app.create_page(title="Plants", slug="plants")
40
+ content_management_app.create_page(title="Minerals", slug="minerals")
41
+
42
+ # Search, expect no results.
43
+ self.assertEqual(0, len(search_index_app.search("cat")))
44
+ self.assertEqual(0, len(search_index_app.search("rose")))
45
+ self.assertEqual(0, len(search_index_app.search("calcium")))
46
+
47
+ # Update the pages.
48
+ content_management_app.update_body(slug="animals", body="cat")
49
+ content_management_app.update_body(slug="plants", body="rose")
50
+ content_management_app.update_body(slug="minerals", body="calcium")
51
+
52
+ # Search for single words.
53
+ page_ids = search_index_app.search("cat")
54
+ self.assertEqual(1, len(page_ids))
55
+ page = content_management_app.get_page_by_id(page_ids[0])
56
+ self.assertEqual(page["slug"], "animals")
57
+ self.assertEqual(page["body"], "cat")
58
+
59
+ page_ids = search_index_app.search("rose")
60
+ self.assertEqual(1, len(page_ids))
61
+ page = content_management_app.get_page_by_id(page_ids[0])
62
+ self.assertEqual(page["slug"], "plants")
63
+ self.assertEqual(page["body"], "rose")
64
+
65
+ page_ids = search_index_app.search("calcium")
66
+ self.assertEqual(1, len(page_ids))
67
+ page = content_management_app.get_page_by_id(page_ids[0])
68
+ self.assertEqual(page["slug"], "minerals")
69
+ self.assertEqual(page["body"], "calcium")
70
+
71
+ self.assertEqual(len(search_index_app.search("dog")), 0)
72
+ self.assertEqual(len(search_index_app.search("bluebell")), 0)
73
+ self.assertEqual(len(search_index_app.search("zinc")), 0)
74
+
75
+ # Update the pages again.
76
+ content_management_app.update_body(slug="animals", body="cat dog zebra")
77
+ content_management_app.update_body(
78
+ slug="plants", body="bluebell rose jasmine"
79
+ )
80
+ content_management_app.update_body(
81
+ slug="minerals", body="iron zinc calcium"
82
+ )
83
+
84
+ # Search for single words.
85
+ page_ids = search_index_app.search("cat")
86
+ self.assertEqual(1, len(page_ids))
87
+ page = content_management_app.get_page_by_id(page_ids[0])
88
+ self.assertEqual(page["slug"], "animals")
89
+ self.assertEqual(page["body"], "cat dog zebra")
90
+
91
+ page_ids = search_index_app.search("rose")
92
+ self.assertEqual(1, len(page_ids))
93
+ page = content_management_app.get_page_by_id(page_ids[0])
94
+ self.assertEqual(page["slug"], "plants")
95
+ self.assertEqual(page["body"], "bluebell rose jasmine")
96
+
97
+ page_ids = search_index_app.search("calcium")
98
+ self.assertEqual(1, len(page_ids))
99
+ page = content_management_app.get_page_by_id(page_ids[0])
100
+ self.assertEqual(page["slug"], "minerals")
101
+ self.assertEqual(page["body"], "iron zinc calcium")
102
+
103
+ page_ids = search_index_app.search("dog")
104
+ self.assertEqual(1, len(page_ids))
105
+ page = content_management_app.get_page_by_id(page_ids[0])
106
+ self.assertEqual(page["slug"], "animals")
107
+ self.assertEqual(page["body"], "cat dog zebra")
108
+
109
+ page_ids = search_index_app.search("bluebell")
110
+ self.assertEqual(1, len(page_ids))
111
+ page = content_management_app.get_page_by_id(page_ids[0])
112
+ self.assertEqual(page["slug"], "plants")
113
+ self.assertEqual(page["body"], "bluebell rose jasmine")
114
+
115
+ page_ids = search_index_app.search("zinc")
116
+ self.assertEqual(1, len(page_ids))
117
+ page = content_management_app.get_page_by_id(page_ids[0])
118
+ self.assertEqual(page["slug"], "minerals")
119
+ self.assertEqual(page["body"], "iron zinc calcium")
120
+
121
+ # Search for multiple words in same page.
122
+ page_ids = search_index_app.search("dog cat")
123
+ self.assertEqual(1, len(page_ids))
124
+ page = content_management_app.get_page_by_id(page_ids[0])
125
+ self.assertEqual(page["slug"], "animals")
126
+ self.assertEqual(page["body"], "cat dog zebra")
127
+
128
+ # Search for multiple words in same page, expect no results.
129
+ page_ids = search_index_app.search("rose zebra")
130
+ self.assertEqual(0, len(page_ids))
131
+
132
+ # Search for alternative words, expect two results.
133
+ page_ids = search_index_app.search("rose OR zebra")
134
+ pages = [
135
+ content_management_app.get_page_by_id(page_id) for page_id in page_ids
136
+ ]
137
+ self.assertEqual(2, len(pages))
138
+ self.assertEqual(["animals", "plants"], sorted(p["slug"] for p in pages))
132
139
 
133
140
 
134
141
  class TestWithSQLite(ContentManagementSystemTestCase):
@@ -157,18 +164,17 @@ class TestWithPostgres(ContentManagementSystemTestCase):
157
164
  super().tearDown()
158
165
 
159
166
  def drop_tables(self) -> None:
160
- db = PostgresDatastore(
167
+ with PostgresDatastore(
161
168
  self.env["POSTGRES_DBNAME"],
162
169
  self.env["POSTGRES_HOST"],
163
170
  self.env["POSTGRES_PORT"],
164
171
  self.env["POSTGRES_USER"],
165
172
  self.env["POSTGRES_PASSWORD"],
166
- )
167
- drop_postgres_table(db, "public.contentmanagementapplication_events")
168
- drop_postgres_table(db, "public.pages_projection_example")
169
- drop_postgres_table(db, "public.searchindexapplication_events")
170
- drop_postgres_table(db, "public.searchindexapplication_tracking")
171
- db.close()
173
+ ) as datastore:
174
+ drop_postgres_table(datastore, "public.contentmanagementapplication_events")
175
+ drop_postgres_table(datastore, "public.pages_projection_example")
176
+ drop_postgres_table(datastore, "public.searchindexapplication_events")
177
+ drop_postgres_table(datastore, "public.searchindexapplication_tracking")
172
178
 
173
179
 
174
180
  del ContentManagementSystemTestCase
@@ -96,16 +96,15 @@ class TestWithPostgres(SearchableContentApplicationTestCase):
96
96
  super().tearDown()
97
97
 
98
98
  def drop_tables(self) -> None:
99
- db = PostgresDatastore(
99
+ with PostgresDatastore(
100
100
  os.environ["POSTGRES_DBNAME"],
101
101
  os.environ["POSTGRES_HOST"],
102
102
  os.environ["POSTGRES_PORT"],
103
103
  os.environ["POSTGRES_USER"],
104
104
  os.environ["POSTGRES_PASSWORD"],
105
- )
106
- drop_postgres_table(db, "public.searchablecontentapplication_events")
107
- drop_postgres_table(db, "public.pages_projection_example")
108
- db.close()
105
+ ) as datastore:
106
+ drop_postgres_table(datastore, "public.searchablecontentapplication_events")
107
+ drop_postgres_table(datastore, "public.pages_projection_example")
109
108
 
110
109
 
111
110
  del SearchableContentApplicationTestCase
@@ -54,16 +54,15 @@ class TestWithPostgres(SearchableContentRecorderTestCase):
54
54
  super().tearDown()
55
55
 
56
56
  def drop_tables(self) -> None:
57
- db = PostgresDatastore(
57
+ with PostgresDatastore(
58
58
  os.environ["POSTGRES_DBNAME"],
59
59
  os.environ["POSTGRES_HOST"],
60
60
  os.environ["POSTGRES_PORT"],
61
61
  os.environ["POSTGRES_USER"],
62
62
  os.environ["POSTGRES_PASSWORD"],
63
- )
64
- drop_postgres_table(db, "public.searchablecontentapplication_events")
65
- drop_postgres_table(db, "public.pages_projection_example")
66
- db.close()
63
+ ) as datastore:
64
+ drop_postgres_table(datastore, "public.searchablecontentapplication_events")
65
+ drop_postgres_table(datastore, "public.pages_projection_example")
67
66
 
68
67
 
69
68
  del SearchableContentRecorderTestCase
@@ -76,16 +76,19 @@ class WithPostgreSQL(SearchableTimestampsTestCase):
76
76
  super().tearDown()
77
77
 
78
78
  def drop_tables(self) -> None:
79
- db = PostgresDatastore(
79
+ with PostgresDatastore(
80
80
  os.environ["POSTGRES_DBNAME"],
81
81
  os.environ["POSTGRES_HOST"],
82
82
  os.environ["POSTGRES_PORT"],
83
83
  os.environ["POSTGRES_USER"],
84
84
  os.environ["POSTGRES_PASSWORD"],
85
- )
86
- drop_postgres_table(db, "public.searchabletimestampsapplication_events")
87
- drop_postgres_table(db, "public.searchabletimestampsapplication_timestamps")
88
- db.close()
85
+ ) as datastore:
86
+ drop_postgres_table(
87
+ datastore, "public.searchabletimestampsapplication_events"
88
+ )
89
+ drop_postgres_table(
90
+ datastore, "public.searchabletimestampsapplication_timestamps"
91
+ )
89
92
 
90
93
 
91
94
  del SearchableTimestampsTestCase
eventsourcing/postgres.py CHANGED
@@ -2,13 +2,14 @@ from __future__ import annotations
2
2
 
3
3
  import logging
4
4
  from contextlib import contextmanager
5
- from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Sequence
5
+ from typing import TYPE_CHECKING, Any, Callable, Iterator, List, Sequence
6
6
 
7
7
  import psycopg
8
8
  import psycopg.errors
9
9
  import psycopg_pool
10
10
  from psycopg import Connection, Cursor
11
11
  from psycopg.rows import DictRow, dict_row
12
+ from typing_extensions import Self
12
13
 
13
14
  from eventsourcing.persistence import (
14
15
  AggregateRecorder,
@@ -62,11 +63,11 @@ class PostgresDatastore:
62
63
  user: str,
63
64
  password: str,
64
65
  *,
65
- connect_timeout: int = 5,
66
+ connect_timeout: int = 30,
66
67
  idle_in_transaction_session_timeout: int = 0,
67
68
  pool_size: int = 2,
68
69
  max_overflow: int = 2,
69
- pool_timeout: float = 5.0,
70
+ max_waiting: int = 0,
70
71
  conn_max_age: float = 60 * 60.0,
71
72
  pre_ping: bool = False,
72
73
  lock_timeout: int = 0,
@@ -79,7 +80,6 @@ class PostgresDatastore:
79
80
  self.pool_open_timeout = pool_open_timeout
80
81
 
81
82
  check = ConnectionPool.check_connection if pre_ping else None
82
- kwargs: Dict[str, Any] = {"check": check}
83
83
  self.pool = ConnectionPool(
84
84
  get_password_func=get_password_func,
85
85
  connection_class=Connection[DictRow],
@@ -96,9 +96,9 @@ class PostgresDatastore:
96
96
  open=False,
97
97
  configure=self.after_connect,
98
98
  timeout=connect_timeout,
99
- max_waiting=round(pool_timeout),
99
+ max_waiting=max_waiting,
100
100
  max_lifetime=conn_max_age,
101
- **kwargs, # use the 'check' argument when no longer supporting Python 3.7
101
+ check=check,
102
102
  )
103
103
  self.lock_timeout = lock_timeout
104
104
  self.schema = schema.strip()
@@ -156,6 +156,12 @@ class PostgresDatastore:
156
156
  def __del__(self) -> None:
157
157
  self.close()
158
158
 
159
+ def __enter__(self) -> Self:
160
+ return self
161
+
162
+ def __exit__(self, *args: object, **kwargs: Any) -> None:
163
+ self.close()
164
+
159
165
 
160
166
  class PostgresAggregateRecorder(AggregateRecorder):
161
167
  def __init__(
@@ -558,10 +564,10 @@ class Factory(InfrastructureFactory):
558
564
  POSTGRES_CONNECT_TIMEOUT = "POSTGRES_CONNECT_TIMEOUT"
559
565
  POSTGRES_CONN_MAX_AGE = "POSTGRES_CONN_MAX_AGE"
560
566
  POSTGRES_PRE_PING = "POSTGRES_PRE_PING"
561
- POSTGRES_POOL_TIMEOUT = "POSTGRES_POOL_TIMEOUT"
567
+ POSTGRES_MAX_WAITING = "POSTGRES_MAX_WAITING"
562
568
  POSTGRES_LOCK_TIMEOUT = "POSTGRES_LOCK_TIMEOUT"
563
569
  POSTGRES_POOL_SIZE = "POSTGRES_POOL_SIZE"
564
- POSTGRES_POOL_MAX_OVERFLOW = "POSTGRES_POOL_MAX_OVERFLOW"
570
+ POSTGRES_MAX_OVERFLOW = "POSTGRES_MAX_OVERFLOW"
565
571
  POSTGRES_IDLE_IN_TRANSACTION_SESSION_TIMEOUT = (
566
572
  "POSTGRES_IDLE_IN_TRANSACTION_SESSION_TIMEOUT"
567
573
  )
@@ -618,7 +624,7 @@ class Factory(InfrastructureFactory):
618
624
  get_password_func = resolve_topic(get_password_topic)
619
625
  password = ""
620
626
 
621
- connect_timeout = 5
627
+ connect_timeout = 30
622
628
  connect_timeout_str = self.env.get(self.POSTGRES_CONNECT_TIMEOUT)
623
629
  if connect_timeout_str:
624
630
  try:
@@ -664,30 +670,30 @@ class Factory(InfrastructureFactory):
664
670
  raise OSError(msg) from None
665
671
 
666
672
  pool_max_overflow = 10
667
- pool_max_overflow_str = self.env.get(self.POSTGRES_POOL_MAX_OVERFLOW)
673
+ pool_max_overflow_str = self.env.get(self.POSTGRES_MAX_OVERFLOW)
668
674
  if pool_max_overflow_str:
669
675
  try:
670
676
  pool_max_overflow = int(pool_max_overflow_str)
671
677
  except ValueError:
672
678
  msg = (
673
679
  "Postgres environment value for key "
674
- f"'{self.POSTGRES_POOL_MAX_OVERFLOW}' is invalid. "
680
+ f"'{self.POSTGRES_MAX_OVERFLOW}' is invalid. "
675
681
  "If set, an integer or empty string is expected: "
676
682
  f"'{pool_max_overflow_str}'"
677
683
  )
678
684
  raise OSError(msg) from None
679
685
 
680
- pool_timeout = 30.0
681
- pool_timeout_str = self.env.get(self.POSTGRES_POOL_TIMEOUT)
682
- if pool_timeout_str:
686
+ max_waiting = 0
687
+ max_waiting_str = self.env.get(self.POSTGRES_MAX_WAITING)
688
+ if max_waiting_str:
683
689
  try:
684
- pool_timeout = float(pool_timeout_str)
690
+ max_waiting = int(max_waiting_str)
685
691
  except ValueError:
686
692
  msg = (
687
693
  "Postgres environment value for key "
688
- f"'{self.POSTGRES_POOL_TIMEOUT}' is invalid. "
689
- "If set, a float or empty string is expected: "
690
- f"'{pool_timeout_str}'"
694
+ f"'{self.POSTGRES_MAX_WAITING}' is invalid. "
695
+ "If set, an integer or empty string is expected: "
696
+ f"'{max_waiting_str}'"
691
697
  )
692
698
  raise OSError(msg) from None
693
699
 
@@ -733,13 +739,16 @@ class Factory(InfrastructureFactory):
733
739
  idle_in_transaction_session_timeout=idle_in_transaction_session_timeout,
734
740
  pool_size=pool_size,
735
741
  max_overflow=pool_max_overflow,
736
- pool_timeout=pool_timeout,
742
+ max_waiting=max_waiting,
737
743
  conn_max_age=conn_max_age,
738
744
  pre_ping=pre_ping,
739
745
  lock_timeout=lock_timeout,
740
746
  schema=schema,
741
747
  )
742
748
 
749
+ def env_create_table(self) -> bool:
750
+ return strtobool(self.env.get(self.CREATE_TABLE) or "yes")
751
+
743
752
  def aggregate_recorder(self, purpose: str = "events") -> AggregateRecorder:
744
753
  prefix = self.env.name.lower() or "stored"
745
754
  events_table_name = prefix + "_" + purpose
@@ -783,9 +792,6 @@ class Factory(InfrastructureFactory):
783
792
  recorder.create_table()
784
793
  return recorder
785
794
 
786
- def env_create_table(self) -> bool:
787
- return strtobool(self.env.get(self.CREATE_TABLE) or "yes")
788
-
789
795
  def close(self) -> None:
790
796
  if hasattr(self, "datastore"):
791
797
  self.datastore.close()
eventsourcing/system.py CHANGED
@@ -8,6 +8,7 @@ from queue import Full, Queue
8
8
  from threading import Event, Lock, RLock, Thread
9
9
  from types import FrameType, ModuleType
10
10
  from typing import (
11
+ Any,
11
12
  ClassVar,
12
13
  Dict,
13
14
  Iterable,
@@ -21,6 +22,8 @@ from typing import (
21
22
  cast,
22
23
  )
23
24
 
25
+ from typing_extensions import Self
26
+
24
27
  from eventsourcing.application import (
25
28
  Application,
26
29
  NotificationLog,
@@ -525,6 +528,13 @@ class SingleThreadedRunner(Runner, RecordingEventReceiver):
525
528
  assert isinstance(app, cls)
526
529
  return app
527
530
 
531
+ def __enter__(self) -> Self:
532
+ self.start()
533
+ return self
534
+
535
+ def __exit__(self, *args: object, **kwargs: Any) -> None:
536
+ self.stop()
537
+
528
538
 
529
539
  class NewSingleThreadedRunner(Runner, RecordingEventReceiver):
530
540
  """