@@ -6,9 +6,11 @@ package when you are ready.
66# Event Sourcing in Python with KurrentDB
77
88This is an extension package for the Python [ eventsourcing] ( https://github.com/pyeventsourcing/eventsourcing ) library
9- that provides a persistence module for [ KurrentDB] ( https://www.kurrent.io ) .
9+ that provides a persistence module for [ KurrentDB and EventStoreDB ] ( https://www.kurrent.io ) .
1010It uses the [ kurrentdbclient] ( https://github.com/pyeventsourcing/kurrentdbclient )
11- package to communicate with KurrentDB via the gRPC interface.
11+ package to communicate with KurrentDB via the gRPC interface. It is tested with
12+ KurrentDB 25.0 and three previous LTS versions of EventStoreDB (24.10, 23.10, and 22.10)
13+ across Python versions 3.9 to 3.13.
1214
1315## Installation
1416
@@ -29,8 +31,8 @@ to be `0`, so you must set `INITIAL_VERSION` on your aggregate classes to `0`.
2931``` python
3032from __future__ import annotations
3133
32- from uuid import uuid5, NAMESPACE_URL
33- from typing import List, TypedDict, Tuple
34+ from typing import TypedDict
35+ from uuid import NAMESPACE_URL , UUID , uuid5
3436
3537from eventsourcing.application import Application
3638from eventsourcing.domain import Aggregate, event
@@ -50,65 +52,60 @@ class TrainingSchool(Application):
5052
5153 def get_dog_details (self , name : str ) -> DogDetails:
5254 dog = self ._get_dog(name)
53- return {' name' : dog.name, ' tricks' : tuple (dog.tricks)}
55+ return {" name" : dog.name, " tricks" : tuple (dog.tricks)}
5456
5557 def _get_dog (self , name : str ) -> Dog:
5658 return self .repository.get(Dog.create_id(name))
5759
5860
59-
6061class Dog (Aggregate ):
6162 INITIAL_VERSION = 0 # for KurrentDB
6263
6364 @ staticmethod
64- def create_id (name : str ):
65+ def create_id (name : str ) -> UUID :
6566 return uuid5(NAMESPACE_URL , f " /dogs/ { name} " )
6667
67- @event (' Registered' )
68- def __init__ (self , name ) :
68+ @event (" Registered" )
69+ def __init__ (self , name : str ) -> None :
6970 self .name = name
70- self .tricks: List [str ] = []
71+ self .tricks: list [str ] = []
7172
72- @event (' TrickAdded' )
73- def add_trick (self , trick ) :
73+ @event (" TrickAdded" )
74+ def add_trick (self , trick : str ) -> None :
7475 self .tricks.append(trick)
7576
7677
7778class DogDetails (TypedDict ):
7879 name: str
79- tricks: Tuple [str , ... ]
80+ tricks: tuple [str , ... ]
8081```
8182
82- Configure the ` TrainingSchool ` application to use KurrentDB by setting
83- the environment variable ` PERSISTENCE_MODULE ` to ` 'eventsourcing_kurrentdb' ` . You
84- can do this in actual environment variables, or by passing in an ` env ` argument when
85- constructing the application object, or by setting ` env ` on the application class.
83+ Configure the ` TrainingSchool ` application to use KurrentDB with environment variables.
84+ You can configure an application with environment variables by setting them in the
85+ operating system environment, or by using the application constructor argument ` env ` ,
86+ or by setting the application class attribute ` env ` .
87+
88+ Set ` PERSISTENCE_MODULE ` to ` 'eventsourcing_kurrentdb' ` . Also set ` KURRENTDB_URI ` to a
89+ KurrentDB connection string URI. This value will be used as the ` uri ` argument when
90+ the ` KurrentDBClient ` class is constructed by this package.
8691
8792``` python
8893import os
8994
90- os.environ[' TRAININGSCHOOL_PERSISTENCE_MODULE' ] = ' eventsourcing_kurrentdb'
91- ```
92-
93- Also set environment variable ` KURRENTDB_URI ` to a KurrentDB connection
94- string URI. This value will be used as the ` uri ` argument when the ` KurrentDBClient `
95- class is constructed by this package.
96-
97- ``` python
98- os.environ[' KURRENTDB_URI' ] = ' esdb://localhost:2113?Tls=false'
95+ os.environ[" TRAININGSCHOOL_PERSISTENCE_MODULE" ] = " eventsourcing_kurrentdb"
96+ os.environ[" KURRENTDB_URI" ] = " esdb://localhost:2113?Tls=false"
9997```
10098
101- If you are connecting to a "secure" KurrentDB server, unless
99+ If you are connecting to a "secure" KurrentDB server, and if
102100the root certificate of the certificate authority used to generate the
103- server's certificate is installed locally, then also set environment
101+ server's certificate is not installed locally, then also set environment
104102variable ` KURRENTDB_ROOT_CERTIFICATES ` to an SSL/TLS certificate
105103suitable for making a secure gRPC connection to the KurrentDB server(s).
106104This value will be used as the ` root_certificates ` argument when the
107105` KurrentDBClient ` class is constructed by this package.
108106
109-
110107``` python
111- os.environ[' KURRENTDB_ROOT_CERTIFICATES' ] = ' <PEM encoded SSL/TLS root certificates>'
108+ os.environ[" KURRENTDB_ROOT_CERTIFICATES" ] = " <PEM encoded SSL/TLS root certificates>"
112109```
113110
114111Please refer to the [ kurrentdbclient] ( https://github.com/pyeventsourcing/kurrentdbclient )
@@ -117,7 +114,7 @@ server, and the "kdb" and "kdb+discover" KurrentDB connection string
117114URI schemes, and how to obtain a suitable SSL/TLS certificate for use
118115in the client when connecting to a "secure" KurrentDB server.
119116
120- Construct the application.
117+ After configuring environment variables, construct the application.
121118
122119``` python
123120training_school = TrainingSchool()
@@ -126,24 +123,24 @@ training_school = TrainingSchool()
126123Call application methods from tests and user interfaces.
127124
128125``` python
129- training_school.register(' Fido' )
130- training_school.add_trick(' Fido' , ' roll over' )
131- training_school.add_trick(' Fido' , ' play dead' )
132- dog_details = training_school.get_dog_details(' Fido' )
133- assert dog_details[' name' ] == ' Fido'
134- assert dog_details[' tricks' ] == (' roll over' , ' play dead' )
126+ training_school.register(" Fido" )
127+ training_school.add_trick(" Fido" , " roll over" )
128+ training_school.add_trick(" Fido" , " play dead" )
129+ dog_details = training_school.get_dog_details(" Fido" )
130+ assert dog_details[" name" ] == " Fido"
131+ assert dog_details[" tricks" ] == (" roll over" , " play dead" )
135132```
136133
137- To see the events have been saved, we can reconstruct the application
134+ To see the events have been saved in KurrentDB , we can reconstruct the application
138135and get Fido's details again.
139136
140137``` python
141138training_school = TrainingSchool()
142139
143- dog_details = training_school.get_dog_details(' Fido' )
140+ dog_details = training_school.get_dog_details(" Fido" )
144141
145- assert dog_details[' name' ] == ' Fido'
146- assert dog_details[' tricks' ] == (' roll over' , ' play dead' )
142+ assert dog_details[" name" ] == " Fido"
143+ assert dog_details[" tricks" ] == (" roll over" , " play dead" )
147144```
148145
149146## Eventually-consistent materialised views
@@ -157,8 +154,10 @@ application
157154
158155``` python
159156from abc import abstractmethod
157+
160158from eventsourcing.persistence import Tracking, TrackingRecorder
161159
160+
162161class MaterialisedViewInterface (TrackingRecorder ):
163162 @abstractmethod
164163 def incr_dog_counter (self , tracking : Tracking) -> None :
@@ -184,6 +183,7 @@ The example below counts dogs and tricks in memory, using "plain old Python obje
184183``` python
185184from eventsourcing.popo import POPOTrackingRecorder
186185
186+
187187class InMemoryMaterialiseView (POPOTrackingRecorder , MaterialisedViewInterface ):
188188 def __init__ (self ):
189189 super ().__init__ ()
@@ -216,8 +216,8 @@ by calling `incr_dog_counter()` on the materialised view. The `Dog.TrickAdded` e
216216are processed by calling ` incr_trick_counter() ` .
217217
218218``` python
219- from eventsourcing.domain import DomainEventProtocol
220219from eventsourcing.dispatch import singledispatchmethod
220+ from eventsourcing.domain import DomainEventProtocol
221221from eventsourcing.projection import Projection
222222from eventsourcing.utils import get_topic
223223
@@ -274,7 +274,7 @@ with ProjectionRunner(
274274 trick_count = materialised_view.get_trick_counter()
275275
276276 # Record another event in "write model".
277- notification_id = training_school.add_trick(' Fido' , ' sit and stay' )
277+ notification_id = training_school.add_trick(" Fido" , " sit and stay" )
278278
279279 # Wait for the new event to be processed.
280280 materialised_view.wait(
@@ -287,7 +287,7 @@ with ProjectionRunner(
287287 assert trick_count + 1 == materialised_view.get_trick_counter()
288288
289289 # Write another event.
290- notification_id = training_school.add_trick(' Fido' , ' jump hoop' )
290+ notification_id = training_school.add_trick(" Fido" , " jump hoop" )
291291
292292 # Wait for the new event to be processed.
293293 materialised_view.wait(
0 commit comments