eventsourcing-kurrentdb 1.2.0__tar.gz

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.
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2022, John Bywater
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,437 @@
1
+ Metadata-Version: 2.3
2
+ Name: eventsourcing-kurrentdb
3
+ Version: 1.2.0
4
+ Summary: Python package for eventsourcing with KurrentDB
5
+ License: BSD-3-Clause
6
+ Author: John Bywater
7
+ Author-email: john.bywater@appropriatesoftware.net
8
+ Requires-Python: >=3.9.2
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: BSD License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Requires-Dist: eventsourcing (>=9.4.2,<10.0)
21
+ Requires-Dist: kurrentdbclient (>=1.0.2,<2.0)
22
+ Project-URL: Homepage, https://github.com/pyeventsourcing/eventsourcing-kurrentdb
23
+ Project-URL: Repository, https://github.com/pyeventsourcing/eventsourcing-kurrentdb
24
+ Description-Content-Type: text/markdown
25
+
26
+ Please note: following the rebranding of EventStoreDB to KurrentDB, this package is
27
+ the rebranding of [`eventsourcing-eventstoredb`](https://pypi.org/project/eventsourcing-eventstoredb). Please
28
+ migrate your code to use the [`eventsourcing-kurrentdb`](https://pypi.org/project/eventsourcing-kurrentdb)
29
+ package when you are ready.
30
+
31
+ # Event Sourcing in Python with KurrentDB
32
+
33
+ This is an extension package for the Python [eventsourcing](https://github.com/pyeventsourcing/eventsourcing) library
34
+ that provides a persistence module for [KurrentDB](https://www.kurrent.io).
35
+ It uses the [kurrentdbclient](https://github.com/pyeventsourcing/kurrentdbclient)
36
+ package to communicate with KurrentDB via the gRPC interface.
37
+
38
+ ## Installation
39
+
40
+ Use pip to install the [stable distribution](https://pypi.org/project/eventsourcing-kurrentdb/)
41
+ from the Python Package Index.
42
+
43
+ $ pip install eventsourcing-kurrentdb
44
+
45
+ Please note, it is recommended to install Python packages into a Python virtual environment.
46
+
47
+ ## Getting started
48
+
49
+ Define aggregates and applications in the usual way. Please note, "streams"
50
+ in KurrentDB are constrained to start from position `0`, and this
51
+ package expects the `originator_version` of the first event in an aggregate sequence
52
+ to be `0`, so you must set `INITIAL_VERSION` on your aggregate classes to `0`.
53
+
54
+ ```python
55
+ from __future__ import annotations
56
+
57
+ from uuid import uuid5, NAMESPACE_URL
58
+ from typing import List, TypedDict, Tuple
59
+
60
+ from eventsourcing.application import Application
61
+ from eventsourcing.domain import Aggregate, event
62
+
63
+
64
+ class TrainingSchool(Application):
65
+ def register(self, name: str) -> int:
66
+ dog = Dog(name)
67
+ recordings = self.save(dog)
68
+ return recordings[-1].notification.id
69
+
70
+ def add_trick(self, name: str, trick: str) -> int:
71
+ dog = self._get_dog(name)
72
+ dog.add_trick(trick)
73
+ recordings = self.save(dog)
74
+ return recordings[-1].notification.id
75
+
76
+ def get_dog_details(self, name: str) -> DogDetails:
77
+ dog = self._get_dog(name)
78
+ return {'name': dog.name, 'tricks': tuple(dog.tricks)}
79
+
80
+ def _get_dog(self, name: str) -> Dog:
81
+ return self.repository.get(Dog.create_id(name))
82
+
83
+
84
+
85
+ class Dog(Aggregate):
86
+ INITIAL_VERSION = 0 # for KurrentDB
87
+
88
+ @staticmethod
89
+ def create_id(name: str):
90
+ return uuid5(NAMESPACE_URL, f"/dogs/{name}")
91
+
92
+ @event('Registered')
93
+ def __init__(self, name):
94
+ self.name = name
95
+ self.tricks: List[str] = []
96
+
97
+ @event('TrickAdded')
98
+ def add_trick(self, trick):
99
+ self.tricks.append(trick)
100
+
101
+
102
+ class DogDetails(TypedDict):
103
+ name: str
104
+ tricks: Tuple[str, ...]
105
+ ```
106
+
107
+ Configure the `TrainingSchool` application to use KurrentDB by setting
108
+ the environment variable `PERSISTENCE_MODULE` to `'eventsourcing_kurrentdb'`. You
109
+ can do this in actual environment variables, or by passing in an `env` argument when
110
+ constructing the application object, or by setting `env` on the application class.
111
+
112
+ ```python
113
+ import os
114
+
115
+ os.environ['TRAININGSCHOOL_PERSISTENCE_MODULE'] = 'eventsourcing_kurrentdb'
116
+ ```
117
+
118
+ Also set environment variable `KURRENTDB_URI` to an KurrentDB connection
119
+ string URI. This value will be used as the `uri` argument when the `KurrentDBClient`
120
+ class is constructed by this package.
121
+
122
+ ```python
123
+ os.environ['KURRENTDB_URI'] = 'esdb://localhost:2113?Tls=false'
124
+ ```
125
+
126
+ If you are connecting to a "secure" KurrentDB server, unless
127
+ the root certificate of the certificate authority used to generate the
128
+ server's certificate is installed locally, then also set environment
129
+ variable `KURRENTDB_ROOT_CERTIFICATES` to an SSL/TLS certificate
130
+ suitable for making a secure gRPC connection to the KurrentDB server(s).
131
+ This value will be used as the `root_certificates` argument when the
132
+ `KurrentDBClient` class is constructed by this package.
133
+
134
+
135
+ ```python
136
+ os.environ['KURRENTDB_ROOT_CERTIFICATES'] = '<PEM encoded SSL/TLS root certificates>'
137
+ ```
138
+
139
+ Please refer to the [kurrentdbclient](https://github.com/pyeventsourcing/kurrentdbclient)
140
+ documentation for details about starting a "secure" or "insecure" KurrentDB
141
+ server, and the "kdb" and "kdb+discover" KurrentDB connection string
142
+ URI schemes, and how to obtain a suitable SSL/TLS certificate for use
143
+ in the client when connecting to a "secure" KurrentDB server.
144
+
145
+ Construct the application.
146
+
147
+ ```python
148
+ training_school = TrainingSchool()
149
+ ```
150
+
151
+ Call application methods from tests and user interfaces.
152
+
153
+ ```python
154
+ training_school.register('Fido')
155
+ training_school.add_trick('Fido', 'roll over')
156
+ training_school.add_trick('Fido', 'play dead')
157
+ dog_details = training_school.get_dog_details('Fido')
158
+ assert dog_details['name'] == 'Fido'
159
+ assert dog_details['tricks'] == ('roll over', 'play dead')
160
+ ```
161
+
162
+ To see the events have been saved, we can reconstruct the application
163
+ and get Fido's details again.
164
+
165
+ ```python
166
+ training_school = TrainingSchool()
167
+
168
+ dog_details = training_school.get_dog_details('Fido')
169
+
170
+ assert dog_details['name'] == 'Fido'
171
+ assert dog_details['tricks'] == ('roll over', 'play dead')
172
+ ```
173
+
174
+ ## Eventually-consistent materialised views
175
+
176
+ To project the state of an event-sourced application "write model" into a
177
+ materialised view "read model", first define an interface for the materialised view
178
+ using the `TrackingRecorder` class from the `eventsourcing` library.
179
+
180
+ The example below defines methods to count dogs and tricks for the `TrainingSchool`
181
+ application
182
+
183
+ ```python
184
+ from abc import abstractmethod
185
+ from eventsourcing.persistence import Tracking, TrackingRecorder
186
+
187
+ class MaterialisedViewInterface(TrackingRecorder):
188
+ @abstractmethod
189
+ def incr_dog_counter(self, tracking: Tracking) -> None:
190
+ pass
191
+
192
+ @abstractmethod
193
+ def incr_trick_counter(self, tracking: Tracking) -> None:
194
+ pass
195
+
196
+ @abstractmethod
197
+ def get_dog_counter(self) -> int:
198
+ pass
199
+
200
+ @abstractmethod
201
+ def get_trick_counter(self) -> int:
202
+ pass
203
+ ```
204
+
205
+ The `MaterialisedViewInterface` can be implemented as a concrete view class using a durable database such as PostgreSQL.
206
+
207
+ The example below counts dogs and tricks in memory, using "plain old Python objects".
208
+
209
+ ```python
210
+ from eventsourcing.popo import POPOTrackingRecorder
211
+
212
+ class InMemoryMaterialiseView(POPOTrackingRecorder, MaterialisedViewInterface):
213
+ def __init__(self):
214
+ super().__init__()
215
+ self._dog_counter = 0
216
+ self._trick_counter = 0
217
+
218
+ def incr_dog_counter(self, tracking: Tracking) -> None:
219
+ with self._database_lock:
220
+ self._assert_tracking_uniqueness(tracking)
221
+ self._insert_tracking(tracking)
222
+ self._dog_counter += 1
223
+
224
+ def incr_trick_counter(self, tracking: Tracking) -> None:
225
+ with self._database_lock:
226
+ self._assert_tracking_uniqueness(tracking)
227
+ self._insert_tracking(tracking)
228
+ self._trick_counter += 1
229
+
230
+ def get_dog_counter(self) -> int:
231
+ return self._dog_counter
232
+
233
+ def get_trick_counter(self) -> int:
234
+ return self._trick_counter
235
+ ```
236
+
237
+ Define how events will be processed using the `Projection` class from the `eventsourcing` library.
238
+
239
+ The example below processes `Dog` events. The `Dog.Registered` events are processed
240
+ by calling `incr_dog_counter()` on the materialised view. The `Dog.TrickAdded` events
241
+ are processed by calling `incr_trick_counter()`.
242
+
243
+ ```python
244
+ from eventsourcing.domain import DomainEventProtocol
245
+ from eventsourcing.dispatch import singledispatchmethod
246
+ from eventsourcing.projection import Projection
247
+ from eventsourcing.utils import get_topic
248
+
249
+
250
+ class CountProjection(Projection[MaterialisedViewInterface]):
251
+ topics = (
252
+ get_topic(Dog.Registered),
253
+ get_topic(Dog.TrickAdded),
254
+ )
255
+
256
+ @singledispatchmethod
257
+ def process_event(self, event: DomainEventProtocol, tracking: Tracking) -> None:
258
+ pass
259
+
260
+ @process_event.register
261
+ def dog_registered(self, event: Dog.Registered, tracking: Tracking) -> None:
262
+ self.view.incr_dog_counter(tracking)
263
+
264
+ @process_event.register
265
+ def trick_added(self, event: Dog.TrickAdded, tracking: Tracking) -> None:
266
+ self.view.incr_trick_counter(tracking)
267
+ ```
268
+
269
+ Run the projection with the `ProjectionRunner` class from the `eventsourcing` library.
270
+
271
+ The example below shows that when the projection is run, the materialised view is updated
272
+ by processing the event of the upstream event-sourced `TrainingSchool` application. It
273
+ also shows that when tricks are subsequently added to the application's aggregates,
274
+ events continue to be processed, such that the trick counter is incremented in the
275
+ downstream materialised view "read model".
276
+
277
+ ```python
278
+ import os
279
+ from eventsourcing.projection import ProjectionRunner
280
+
281
+ with ProjectionRunner(
282
+ application_class=TrainingSchool,
283
+ projection_class=CountProjection,
284
+ view_class=InMemoryMaterialiseView,
285
+ ) as runner:
286
+
287
+ # Get "read model" instance from runner, because
288
+ # state of materialised view is stored in memory.
289
+ materialised_view = runner.projection.view
290
+
291
+ # Wait for the existing events to be processed.
292
+ materialised_view.wait(
293
+ application_name=training_school.name,
294
+ notification_id=training_school.recorder.max_notification_id(),
295
+ )
296
+
297
+ # Query the "read model".
298
+ dog_count = materialised_view.get_dog_counter()
299
+ trick_count = materialised_view.get_trick_counter()
300
+
301
+ # Record another event in "write model".
302
+ notification_id = training_school.add_trick('Fido', 'sit and stay')
303
+
304
+ # Wait for the new event to be processed.
305
+ materialised_view.wait(
306
+ application_name=training_school.name,
307
+ notification_id=notification_id,
308
+ )
309
+
310
+ # Expect one trick more, same number of dogs.
311
+ assert dog_count == materialised_view.get_dog_counter()
312
+ assert trick_count + 1 == materialised_view.get_trick_counter()
313
+
314
+ # Write another event.
315
+ notification_id = training_school.add_trick('Fido', 'jump hoop')
316
+
317
+ # Wait for the new event to be processed.
318
+ materialised_view.wait(
319
+ training_school.name,
320
+ notification_id,
321
+ )
322
+
323
+ # Expect two tricks more, same number of dogs.
324
+ assert dog_count == materialised_view.get_dog_counter()
325
+ assert trick_count + 2 == materialised_view.get_trick_counter()
326
+ ```
327
+
328
+ See the Python `eventsourcing` package documentation for more information about
329
+ projecting the state of an event-sourced application into materialised views
330
+ that use a durable database such as SQLite and PostgreSQL.
331
+
332
+ ## More information
333
+
334
+ For more information, please refer to the Python
335
+ [eventsourcing](https://github.com/pyeventsourcing/eventsourcing) library, the
336
+ Python [kurrentdbclient](https://github.com/pyeventsourcing/kurrentdbclient) package,
337
+ and the [KurrentDB](https://www.kurrent.io) website.
338
+
339
+ ## Contributors
340
+
341
+ ### Install Poetry
342
+
343
+ The first thing is to check you have Poetry installed.
344
+
345
+ $ poetry --version
346
+
347
+ If you don't, then please [install Poetry](https://python-poetry.org/docs/#installing-with-the-official-installer).
348
+
349
+ It will help to make sure Poetry's bin directory is in your `PATH` environment variable.
350
+
351
+ But in any case, make sure you know the path to the `poetry` executable. The Poetry
352
+ installer tells you where it has been installed, and how to configure your shell.
353
+
354
+ Please refer to the [Poetry docs](https://python-poetry.org/docs/) for guidance on
355
+ using Poetry.
356
+
357
+ ### Setup for PyCharm users
358
+
359
+ You can easily obtain the project files using PyCharm (menu "Git > Clone...").
360
+ PyCharm will then usually prompt you to open the project.
361
+
362
+ Open the project in a new window. PyCharm will then usually prompt you to create
363
+ a new virtual environment.
364
+
365
+ Create a new Poetry virtual environment for the project. If PyCharm doesn't already
366
+ know where your `poetry` executable is, then set the path to your `poetry` executable
367
+ in the "New Poetry Environment" form input field labelled "Poetry executable". In the
368
+ "New Poetry Environment" form, you will also have the opportunity to select which
369
+ Python executable will be used by the virtual environment.
370
+
371
+ PyCharm will then create a new Poetry virtual environment for your project, using
372
+ a particular version of Python, and also install into this virtual environment the
373
+ project's package dependencies according to the `pyproject.toml` file, or the
374
+ `poetry.lock` file if that exists in the project files.
375
+
376
+ You can add different Poetry environments for different Python versions, and switch
377
+ between them using the "Python Interpreter" settings of PyCharm. If you want to use
378
+ a version of Python that isn't installed, either use your favourite package manager,
379
+ or install Python by downloading an installer for recent versions of Python directly
380
+ from the [Python website](https://www.python.org/downloads/).
381
+
382
+ Once project dependencies have been installed, you should be able to run tests
383
+ from within PyCharm (right-click on the `tests` folder and select the 'Run' option).
384
+
385
+ You should also be able to open a terminal window in PyCharm, and run the project's
386
+ Makefile commands from the command line (see below).
387
+
388
+ ### Setup from command line
389
+
390
+ Obtain the project files, using Git or suitable alternative.
391
+
392
+ In a terminal application, change your current working directory
393
+ to the root folder of the project files. There should be a Makefile
394
+ in this folder.
395
+
396
+ Use the Makefile to create a new Poetry virtual environment for the
397
+ project and install the project's package dependencies into it,
398
+ using the following command.
399
+
400
+ $ make install
401
+
402
+ Please note, if you create the virtual environment in this way, and then try to
403
+ open the project in PyCharm and configure the project to use this virtual
404
+ environment as an "Existing Poetry Environment", PyCharm sometimes has some
405
+ issues (don't know why) which might be problematic. If you encounter such
406
+ issues, you can resolve these issues by deleting the virtual environment
407
+ and creating the Poetry virtual environment using PyCharm (see above).
408
+
409
+ ### Project Makefile commands
410
+
411
+ You can start KurrentDB using the following command.
412
+
413
+ $ make start-kurrentdb
414
+
415
+ You can run tests using the following command (needs KurrentDB to be running).
416
+
417
+ $ make test
418
+
419
+ You can stop KurrentDB using the following command.
420
+
421
+ $ make stop-kurrentdb
422
+
423
+ You can check the formatting of the code using the following command.
424
+
425
+ $ make lint
426
+
427
+ You can reformat the code using the following command.
428
+
429
+ $ make fmt
430
+
431
+ Tests belong in `./tests`. Code-under-test belongs in `./eventsourcing_kurrentdb`.
432
+
433
+ Edit package dependencies in `pyproject.toml`. Update `poetry.lock` and installed packages
434
+ using the following command.
435
+
436
+ $ make update
437
+