Skip to content

Commit 6c953a7

Browse files
committed
Updated to work with eventsourcing 9.4.0 and kurrentdbclient 1.0.
1 parent 3e5dc69 commit 6c953a7

File tree

13 files changed

+784
-486
lines changed

13 files changed

+784
-486
lines changed

.github/workflows/github-actions.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
strategy:
1212
fail-fast: false
1313
matrix:
14-
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
14+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
1515
eventstore-image-tag: [ "21.10.9-buster-slim", "22.10.3-buster-slim", "23.10.0-bookworm-slim" ]
1616
env:
1717
EVENTSTORE_IMAGE_TAG: ${{ matrix.eventstore-image-tag }}

Makefile

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,33 @@
11
.EXPORT_ALL_VARIABLES:
22

3-
POETRY ?= poetry
4-
POETRY_INSTALLER_URL ?= https://install.python-poetry.org
5-
POETRY_VERSION=1.5.1
6-
73
# EVENTSTORE_DOCKER_IMAGE ?= eventstore/eventstore:23.10.0-bookworm-slim
84
EVENTSTORE_DOCKER_IMAGE ?= docker.eventstore.com/eventstore/eventstoredb-ee:24.10.0-x64-8.0-bookworm-slim
95

6+
PYTHONUNBUFFERED=1
7+
8+
POETRY_VERSION=2.1.2
9+
POETRY ?= poetry@$(POETRY_VERSION)
1010

1111
.PHONY: install-poetry
1212
install-poetry:
13-
curl -sSL $(POETRY_INSTALLER_URL) | python3
13+
@pipx install --suffix="@$(POETRY_VERSION)" "poetry==$(POETRY_VERSION)"
1414
$(POETRY) --version
1515

16+
.PHONY: install
17+
install:
18+
$(POETRY) sync -vv $(opts)
19+
1620
.PHONY: install-packages
1721
install-packages:
18-
$(POETRY) --version
19-
$(POETRY) install --no-root -vv $(opts)
22+
$(POETRY) sync --no-root -vv $(opts)
23+
24+
.PHONY: update-lockfile
25+
update-lockfile:
26+
$(POETRY) lock
27+
28+
.PHONY: update-packages
29+
update-packages: update-lockfile install-packages
2030

21-
.PHONY: install
22-
install:
23-
$(POETRY) --version
24-
$(POETRY) install -vv $(opts)
2531

2632
.PHONY: install-pre-commit-hooks
2733
install-pre-commit-hooks:
@@ -78,7 +84,8 @@ fmt: fmt-black fmt-isort
7884

7985
.PHONY: test
8086
test:
81-
$(POETRY) run python -m pytest -v $(opts) $(call tests,.)
87+
$(POETRY) run coverage run -m unittest discover . -v
88+
$(POETRY) run coverage report --fail-under=100 --show-missing
8289

8390
.PHONY: build
8491
build:

README.md

Lines changed: 28 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,10 @@ class DogDetails(TypedDict):
7575
tricks: Tuple[str, ...]
7676
```
7777

78-
Configure the application to use EventStoreDB by setting the application environment
79-
variable `PERSISTENCE_MODULE` to `'eventsourcing_eventstoredb'`. You can do this
80-
in actual environment variables, by passing in an `env` argument when constructing
81-
the application object, or by setting `env` on the application class.
78+
Configure the `TrainingSchool` application to use EventStoreDB by setting the
79+
environment variable `PERSISTENCE_MODULE` to `'eventsourcing_eventstoredb'`. You
80+
can do this in actual environment variables, or by passing in an `env` argument when
81+
constructing the application object, or by setting `env` on the application class.
8282

8383
```python
8484
import os
@@ -142,7 +142,7 @@ assert dog_details['name'] == 'Fido'
142142
assert dog_details['tricks'] == ('roll over', 'play dead')
143143
```
144144

145-
## Projections
145+
## Eventually-consistent materialised views
146146

147147
To project the state of an event-sourced application "write model" into a
148148
materialised view "read model", first define an interface for the materialised view
@@ -155,7 +155,7 @@ application
155155
from abc import abstractmethod
156156
from eventsourcing.persistence import Tracking, TrackingRecorder
157157

158-
class CountRecorderInterface(TrackingRecorder):
158+
class MaterialisedViewInterface(TrackingRecorder):
159159
@abstractmethod
160160
def incr_dog_counter(self, tracking: Tracking) -> None:
161161
pass
@@ -173,14 +173,14 @@ class CountRecorderInterface(TrackingRecorder):
173173
pass
174174
```
175175

176-
The `CountRecorderInterface` can be implemented to use a concrete database.
176+
The `MaterialisedViewInterface` can be implemented to use a concrete database.
177177

178178
The example below counts dogs and tricks in memory, using "plain old Python objects".
179179

180180
```python
181181
from eventsourcing.popo import POPOTrackingRecorder
182182

183-
class POPOCountRecorder(POPOTrackingRecorder, CountRecorderInterface):
183+
class InMemoryMaterialiseView(POPOTrackingRecorder, MaterialisedViewInterface):
184184
def __init__(self):
185185
super().__init__()
186186
self._dog_counter = 0
@@ -203,15 +203,11 @@ class POPOCountRecorder(POPOTrackingRecorder, CountRecorderInterface):
203203
return self._trick_counter
204204
```
205205

206-
After defining the materialised view interface, define how events will be processed
207-
using the `Projection` class from the `eventsourcing` library.
206+
Define how events will be processed using the `Projection` class from the `eventsourcing` library.
208207

209208
The example below processes `Dog` events. The `Dog.Registered` events are processed
210-
by calling `incr_dog_counter()`. The `Dog.TrickAdded` events are processed by calling
211-
`incr_trick_counter()`.
212-
213-
Setting the event topics on the `CountProjection` class is not necessary, but speeds
214-
event processing by filtering events in the application's database.
209+
by calling `incr_dog_counter()` on the materialised view. The `Dog.TrickAdded` events
210+
are processed by calling `incr_trick_counter()`.
215211

216212
```python
217213
from eventsourcing.domain import DomainEventProtocol
@@ -220,36 +216,26 @@ from eventsourcing.projection import Projection
220216
from eventsourcing.utils import get_topic
221217

222218

223-
class CountProjection(Projection[CountRecorderInterface]):
219+
class CountProjection(Projection[MaterialisedViewInterface]):
224220
topics = (
225221
get_topic(Dog.Registered),
226222
get_topic(Dog.TrickAdded),
227223
)
228224

229-
def __init__(
230-
self,
231-
tracking_recorder: CountRecorderInterface,
232-
):
233-
assert isinstance(tracking_recorder, CountRecorderInterface), type(
234-
tracking_recorder
235-
)
236-
super().__init__(tracking_recorder)
237-
238225
@singledispatchmethod
239226
def process_event(self, event: DomainEventProtocol, tracking: Tracking) -> None:
227+
pass
240228

241229
@process_event.register
242-
def aggregate_created(self, event: Dog.Registered, tracking: Tracking) -> None:
243-
self.tracking_recorder.incr_dog_counter(tracking)
230+
def dog_registered(self, event: Dog.Registered, tracking: Tracking) -> None:
231+
self.view.incr_dog_counter(tracking)
244232

245233
@process_event.register
246-
def aggregate_event(self, event: Dog.TrickAdded, tracking: Tracking) -> None:
247-
self.tracking_recorder.incr_trick_counter(tracking)
234+
def trick_added(self, event: Dog.TrickAdded, tracking: Tracking) -> None:
235+
self.view.incr_trick_counter(tracking)
248236
```
249237

250-
Run the projection with the `ProjectionRunner` class from the `eventsourcing` library,
251-
by calling it with an application class, a projection class, and a concrete tracking
252-
recorder class.
238+
Run the projection with the `ProjectionRunner` class from the `eventsourcing` library.
253239

254240
The example below shows that when the projection is run, the materialised view is updated
255241
by processing the event of the upstream event-sourced `TrainingSchool` application. It
@@ -264,17 +250,17 @@ from eventsourcing.projection import ProjectionRunner
264250
with ProjectionRunner(
265251
application_class=TrainingSchool,
266252
projection_class=CountProjection,
267-
tracking_recorder_class=POPOCountRecorder,
253+
view_class=InMemoryMaterialiseView,
268254
) as runner:
269255

270256
# Get "read model" instance from runner, because
271257
# state of materialised view is stored in memory.
272-
materialised_view = runner.projection.tracking_recorder
258+
materialised_view = runner.projection.view
273259

274260
# Wait for the existing events to be processed.
275261
materialised_view.wait(
276-
training_school.name,
277-
training_school.recorder.max_notification_id(),
262+
application_name=training_school.name,
263+
notification_id=training_school.recorder.max_notification_id(),
278264
)
279265

280266
# Query the "read model".
@@ -284,10 +270,10 @@ with ProjectionRunner(
284270
# Record another event in "write model".
285271
notification_id = training_school.add_trick('Fido', 'sit and stay')
286272

287-
# Wait for the new event to be processed by the projection.
273+
# Wait for the new event to be processed.
288274
materialised_view.wait(
289-
training_school.name,
290-
notification_id,
275+
application_name=training_school.name,
276+
notification_id=notification_id,
291277
)
292278

293279
# Expect one trick more, same number of dogs.
@@ -308,10 +294,9 @@ with ProjectionRunner(
308294
assert trick_count + 2 == materialised_view.get_trick_counter()
309295
```
310296

311-
To implement a materialised view that uses PostgreSQL, please use the
312-
`PostgresTrackingRecorder` class from the `eventsourcing` library (see
313-
the library docs for more information about projecting the state of
314-
an event-sourced application into PostgreSQL).
297+
See the Python `eventsourcing` package documentation for more information about
298+
projecting the state of an event-sourced application into materialised views
299+
that use a durable database such as SQLite and PostgreSQL.
315300

316301
## More information
317302

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# -*- coding: utf-8 -*-
2-
from .factory import Factory
2+
from .factory import EventStoreDBFactory
33

4-
__all__ = ["Factory"]
4+
__all__ = ["EventStoreDBFactory"]

eventsourcing_eventstoredb/factory.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
)
2020

2121

22-
class Factory(InfrastructureFactory[TrackingRecorder]):
22+
class EventStoreDBFactory(InfrastructureFactory[TrackingRecorder]):
2323
"""
2424
Infrastructure factory for EventStoreDB infrastructure.
2525
"""
@@ -54,10 +54,10 @@ def application_recorder(self) -> ApplicationRecorder:
5454
def tracking_recorder(
5555
self, tracking_recorder_class: Type[TrackingRecorder] | None = None
5656
) -> TrackingRecorder:
57-
raise NotImplementedError()
57+
raise NotImplementedError
5858

5959
def process_recorder(self) -> ProcessRecorder:
60-
raise NotImplementedError()
60+
raise NotImplementedError
6161

6262
def __del__(self) -> None:
6363
if hasattr(self, "client"):

eventsourcing_eventstoredb/recorders.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ def _insert_events(
121121
current_version=current_version,
122122
events=new_events,
123123
)
124-
except kurrentdbclient.exceptions.WrongCurrentVersion as e:
124+
except kurrentdbclient.exceptions.WrongCurrentVersionError as e:
125125
raise IntegrityError(e) from e
126126
except Exception as e:
127127
raise PersistenceError(e) from e
@@ -212,7 +212,7 @@ def select_events( # noqa: C901
212212
state=ev.data,
213213
)
214214
stored_events.append(se)
215-
except kurrentdbclient.exceptions.NotFound:
215+
except kurrentdbclient.exceptions.NotFoundError:
216216
return []
217217

218218
return stored_events
@@ -317,7 +317,7 @@ def __next__(self) -> Notification:
317317
while not self._has_been_stopped:
318318
try:
319319
recorded_event = next(self._esdb_subscription)
320-
except kurrentdbclient.exceptions.ConsumerTooSlow: # pragma: no cover
320+
except kurrentdbclient.exceptions.ConsumerTooSlowError: # pragma: no cover
321321
# Sometimes the database drops the connection just after starting.
322322
self._esdb_subscription = self._recorder.client.subscribe_to_all(
323323
commit_position=self._last_notification_id,

mypy.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[mypy]
2-
python_version = 3.8
2+
python_version = 3.9
33
files = eventsourcing_eventstoredb,tests
44

55
check_untyped_defs = True

0 commit comments

Comments
 (0)