eventsourcing 9.5.0b3__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.
@@ -0,0 +1,131 @@
1
+ import os
2
+
3
+ import psycopg
4
+ from psycopg.sql import SQL, Identifier
5
+
6
+ from eventsourcing.dcb.postgres_tt import (
7
+ DB_FUNCTION_NAME_DCB_CONDITIONAL_APPEND_TT,
8
+ DB_FUNCTION_NAME_DCB_UNCONDITIONAL_APPEND_TT,
9
+ DB_TYPE_NAME_DCB_EVENT_TT,
10
+ DB_TYPE_NAME_DCB_QUERY_ITEM_TT,
11
+ )
12
+ from eventsourcing.postgres import PostgresDatastore
13
+ from examples.dcb_enrolment_with_basic_objects.postgres_ts import (
14
+ PG_FUNCTION_NAME_DCB_CHECK_APPEND_CONDITION_TS,
15
+ PG_FUNCTION_NAME_DCB_INSERT_EVENTS_TS,
16
+ PG_FUNCTION_NAME_DCB_SELECT_EVENTS_TS,
17
+ PG_PROCEDURE_NAME_DCB_APPEND_EVENTS_TS,
18
+ PG_TYPE_NAME_DCB_EVENT_TS,
19
+ )
20
+
21
+
22
+ def pg_close_all_connections(
23
+ name: str = "eventsourcing",
24
+ host: str = "127.0.0.1",
25
+ port: str = "5432",
26
+ user: str = "postgres",
27
+ password: str = "postgres", # noqa: S107
28
+ ) -> None:
29
+ try:
30
+ # For local development... probably.
31
+ pg_conn = psycopg.connect(
32
+ dbname=name,
33
+ host=host,
34
+ port=port,
35
+ )
36
+ except psycopg.Error:
37
+ # For GitHub actions.
38
+ """CREATE ROLE postgres LOGIN SUPERUSER PASSWORD 'postgres';"""
39
+ pg_conn = psycopg.connect(
40
+ dbname=name,
41
+ host=host,
42
+ port=port,
43
+ user=user,
44
+ password=password,
45
+ )
46
+ close_all_connections = """
47
+ SELECT
48
+ pg_terminate_backend(pid)
49
+ FROM
50
+ pg_stat_activity
51
+ WHERE
52
+ -- don't kill my own connection!
53
+ pid <> pg_backend_pid();
54
+
55
+ """
56
+ pg_conn_cursor = pg_conn.cursor()
57
+ pg_conn_cursor.execute(close_all_connections)
58
+
59
+
60
+ def drop_tables() -> None:
61
+
62
+ for schema in ["public", "myschema"]:
63
+ datastore = PostgresDatastore(
64
+ dbname=os.environ.get("POSTGRES_DBNAME", "eventsourcing"),
65
+ host=os.environ.get("POSTGRES_HOST", "127.0.0.1"),
66
+ port=os.environ.get("POSTGRES_PORT", "5432"),
67
+ user=os.environ.get("POSTGRES_USER", "eventsourcing"),
68
+ password=os.environ.get("POSTGRES_PASSWORD", "eventsourcing"),
69
+ schema=schema,
70
+ )
71
+ with datastore.transaction(commit=True) as curs:
72
+ select_table_names = SQL(
73
+ "SELECT table_name FROM information_schema.tables "
74
+ "WHERE table_schema = %s"
75
+ )
76
+ fetchall = curs.execute(select_table_names, (datastore.schema,)).fetchall()
77
+ for row in fetchall:
78
+ table_name = row["table_name"]
79
+ # print(f"Dropping table '{table_name}' in schema '{schema}'")
80
+ statement = SQL("DROP TABLE IF EXISTS {0}.{1} CASCADE").format(
81
+ Identifier(datastore.schema), Identifier(table_name)
82
+ )
83
+ curs.execute(statement, prepare=False)
84
+ # print(f"Dropped table '{table_name}' in schema '{schema}'")
85
+
86
+ # Also drop composite types.
87
+ composite_types = [
88
+ "stored_event_uuid",
89
+ "stored_event_text",
90
+ PG_TYPE_NAME_DCB_EVENT_TS,
91
+ DB_TYPE_NAME_DCB_EVENT_TT,
92
+ DB_TYPE_NAME_DCB_QUERY_ITEM_TT,
93
+ ]
94
+ for name in composite_types:
95
+ statement = SQL("DROP TYPE IF EXISTS {schema}.{name} CASCADE").format(
96
+ schema=Identifier(datastore.schema),
97
+ name=Identifier(name),
98
+ )
99
+ curs.execute(statement, prepare=False)
100
+
101
+ # Also drop functions.
102
+ functions = [
103
+ "es_insert_events_uuid",
104
+ "es_insert_events_text",
105
+ PG_FUNCTION_NAME_DCB_INSERT_EVENTS_TS,
106
+ PG_FUNCTION_NAME_DCB_SELECT_EVENTS_TS,
107
+ PG_FUNCTION_NAME_DCB_CHECK_APPEND_CONDITION_TS,
108
+ DB_FUNCTION_NAME_DCB_UNCONDITIONAL_APPEND_TT,
109
+ DB_FUNCTION_NAME_DCB_CONDITIONAL_APPEND_TT,
110
+ ]
111
+ for name in functions:
112
+ statement = SQL(
113
+ "DROP FUNCTION IF EXISTS {schema}.{name} CASCADE"
114
+ ).format(
115
+ schema=Identifier(datastore.schema),
116
+ name=Identifier(name),
117
+ )
118
+ curs.execute(statement, prepare=False)
119
+
120
+ # Also drop procedures.
121
+ procedures = [
122
+ PG_PROCEDURE_NAME_DCB_APPEND_EVENTS_TS,
123
+ ]
124
+ for name in procedures:
125
+ statement = SQL(
126
+ "DROP PROCEDURE IF EXISTS {schema}.{name} CASCADE"
127
+ ).format(
128
+ schema=Identifier(datastore.schema),
129
+ name=Identifier(name),
130
+ )
131
+ curs.execute(statement, prepare=False)
eventsourcing/utils.py ADDED
@@ -0,0 +1,257 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib
4
+ from collections.abc import Callable, Iterator, Mapping
5
+ from functools import wraps
6
+ from inspect import isfunction
7
+ from random import random
8
+ from threading import Lock
9
+ from time import sleep
10
+ from types import ModuleType
11
+ from typing import TYPE_CHECKING, Any, TypeVar, no_type_check, overload
12
+
13
+ if TYPE_CHECKING:
14
+ from types import FunctionType, WrapperDescriptorType
15
+
16
+
17
+ class TopicError(Exception):
18
+ """Raised when topic doesn't resolve."""
19
+
20
+
21
+ SupportsTopic = type | Callable[..., Any] | ModuleType
22
+
23
+ _type_cache: dict[SupportsTopic, str] = {}
24
+ _topic_cache: dict[str, SupportsTopic] = {}
25
+ _topic_cache_lock = Lock()
26
+
27
+
28
+ def get_topic(obj: SupportsTopic, /) -> str:
29
+ """Returns a "topic string" that locates the given class
30
+ in its module. The string is formed by joining the
31
+ module name and the class qualname separated by the
32
+ colon character.
33
+ """
34
+ try:
35
+ return _type_cache[obj]
36
+ except KeyError:
37
+ topic = construct_topic(obj)
38
+ register_topic(topic, obj)
39
+ _type_cache[obj] = topic
40
+ return topic
41
+
42
+
43
+ def construct_topic(obj: SupportsTopic, /) -> str:
44
+ return getattr(obj, "TOPIC", f"{obj.__module__}:{obj.__qualname__}")
45
+
46
+
47
+ def resolve_topic(topic: str) -> Any:
48
+ """Returns an object located by the given topic.
49
+
50
+ This function can be (is) used to locate domain
51
+ event classes and aggregate classes from the
52
+ topics in stored events and snapshots. It can
53
+ also be used to locate compression modules,
54
+ timezone objects, etc.
55
+ """
56
+ try:
57
+ obj = _topic_cache[topic]
58
+ except KeyError:
59
+ module_name, _, attr_name = topic.partition(":")
60
+
61
+ attr_name_parts = attr_name.split(".")
62
+ for i in range(len(attr_name_parts) - 1, 0, -1):
63
+ part_name = ".".join(attr_name_parts[:i])
64
+ try:
65
+ obj = _topic_cache[f"{module_name}:{part_name}"]
66
+ except KeyError:
67
+ continue
68
+ else:
69
+ attr_name = ".".join(attr_name_parts[i:])
70
+ break
71
+
72
+ else:
73
+ try:
74
+ obj = _topic_cache[module_name]
75
+ except KeyError:
76
+ module_name_parts = module_name.split(".")
77
+ for i in range(len(module_name_parts) - 1, 0, -1):
78
+ part_name = ".".join(module_name_parts[:i])
79
+ try:
80
+ obj = _topic_cache[f"{part_name}"]
81
+ except KeyError:
82
+ continue
83
+ else:
84
+ module_name = ".".join([obj.__name__, *module_name_parts[i:]])
85
+ break
86
+ try:
87
+ obj = importlib.import_module(module_name)
88
+ except ImportError as e:
89
+ msg = f"Failed to resolve topic '{topic}': {e}"
90
+ raise TopicError(msg) from e
91
+ if attr_name:
92
+ try:
93
+ for attr_name_part in attr_name.split("."):
94
+ obj = getattr(obj, attr_name_part)
95
+ except AttributeError as e:
96
+ msg = f"Failed to resolve topic '{topic}': {e}"
97
+ raise TopicError(msg) from e
98
+ register_topic(topic, obj)
99
+ return obj
100
+
101
+
102
+ def register_topic(topic: str, obj: SupportsTopic) -> None:
103
+ """Registers a topic with an object, so the object will be
104
+ returned whenever the topic is resolved.
105
+
106
+ This function can be used to cache the topic of a class, so
107
+ that the topic can be resolved faster. It can also be used to
108
+ register old topics for objects that have been renamed or moved,
109
+ so that old topics will resolve to the renamed or moved object.
110
+ """
111
+ with _topic_cache_lock:
112
+ try:
113
+ cached_obj = _topic_cache[topic]
114
+ except KeyError:
115
+ _topic_cache[topic] = obj
116
+ else:
117
+ if cached_obj != obj:
118
+ msg = (
119
+ f"Refusing to cache {obj} (oid {id(obj)}): {cached_obj} (oid "
120
+ f"{id(cached_obj)}) is already registered for topic '{topic}'"
121
+ )
122
+ raise TopicError(msg)
123
+
124
+
125
+ def clear_topic_cache() -> None:
126
+ _topic_cache.clear()
127
+
128
+
129
+ def retry(
130
+ exc: type[Exception] | tuple[type[Exception], ...] = Exception,
131
+ max_attempts: int = 1,
132
+ wait: float = 0,
133
+ stall: float = 0,
134
+ ) -> Callable[[Any], Any]:
135
+ """Retry decorator.
136
+
137
+ :param exc: List of exceptions that will cause the call to be retried if raised.
138
+ :param max_attempts: Maximum number of attempts to try.
139
+ :param wait: Amount of time to wait before retrying after an exception.
140
+ :param stall: Amount of time to wait before the first attempt.
141
+ :return: Returns the value returned by decorated function.
142
+ """
143
+
144
+ @no_type_check
145
+ def _retry(func: Callable) -> Callable:
146
+ @wraps(func)
147
+ def retry_decorator(*args: Any, **kwargs: Any) -> Any:
148
+ if stall:
149
+ sleep(stall)
150
+ attempts = 0
151
+ while True:
152
+ try:
153
+ return func(*args, **kwargs)
154
+ except exc: # noqa: PERF203
155
+ attempts += 1
156
+ if max_attempts is None or attempts < max_attempts:
157
+ sleep(wait * (1 + 0.1 * (random() - 0.5))) # noqa: S311
158
+ else:
159
+ # Max retries exceeded.
160
+ raise
161
+
162
+ return retry_decorator
163
+
164
+ # If using decorator in bare form, the decorated
165
+ # function is the first arg, so check 'exc'.
166
+ if isfunction(exc):
167
+ # Remember the given function.
168
+ _func = exc
169
+ # Set 'exc' to a sensible exception class for _retry().
170
+ exc = Exception
171
+ # Wrap and return.
172
+ return _retry(func=_func)
173
+ # Check decorator args, and return _retry,
174
+ # to be called with the decorated function.
175
+ if isinstance(exc, (list, tuple)):
176
+ for _exc in exc:
177
+ if not (isinstance(_exc, type) and issubclass(_exc, Exception)):
178
+ msg = f"not an exception class: {_exc}"
179
+ raise TypeError(msg)
180
+ elif not (isinstance(exc, type) and issubclass(exc, Exception)):
181
+ msg = f"not an exception class: {exc}"
182
+ raise TypeError(msg)
183
+ if not isinstance(max_attempts, int):
184
+ msg = f"'max_attempts' must be an int: {max_attempts}"
185
+ raise TypeError(msg)
186
+ if not isinstance(wait, (float, int)):
187
+ msg = f"'wait' must be a float: {max_attempts}"
188
+ raise TypeError(msg)
189
+ if not isinstance(stall, (float, int)):
190
+ msg = f"'stall' must be a float: {max_attempts}"
191
+ raise TypeError(msg)
192
+ return _retry
193
+
194
+
195
+ def strtobool(val: str) -> bool:
196
+ """Convert a string representation of truth to True or False.
197
+
198
+ True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values
199
+ are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if
200
+ 'val' is anything else.
201
+ """
202
+ if not isinstance(val, str):
203
+ msg = f"{val} is not a str"
204
+ raise TypeError(msg)
205
+ val = val.lower()
206
+ if val in ("y", "yes", "t", "true", "on", "1"):
207
+ return True
208
+ if val in ("n", "no", "f", "false", "off", "0"):
209
+ return False
210
+ msg = f"invalid truth value {val!r}"
211
+ raise ValueError(msg)
212
+
213
+
214
+ def reversed_keys(d: dict[Any, Any]) -> Iterator[Any]:
215
+ return reversed(d.keys())
216
+
217
+
218
+ # TODO: Inline this now.
219
+ def get_method_name(
220
+ method: Callable[..., Any] | FunctionType | WrapperDescriptorType,
221
+ ) -> str:
222
+ return method.__qualname__
223
+
224
+
225
+ EnvType = Mapping[str, str]
226
+ T = TypeVar("T")
227
+
228
+
229
+ class Environment(dict[str, str]):
230
+ def __init__(self, name: str = "", env: EnvType | None = None):
231
+ super().__init__(env or {})
232
+ self.name = name
233
+
234
+ @overload # type: ignore[override]
235
+ def get(self, __key: str, /) -> str | None: ... # pragma: no cover
236
+
237
+ @overload
238
+ def get(self, __key: str, /, __default: str) -> str: ... # pragma: no cover
239
+
240
+ @overload
241
+ def get(self, __key: str, /, __default: T) -> str | T: ... # pragma: no cover
242
+
243
+ def get( # pyright: ignore [reportIncompatibleMethodOverride]
244
+ self, __key: str, /, __default: str | T | None = None
245
+ ) -> str | T | None:
246
+ for _key in self.create_keys(__key):
247
+ value = super().get(_key, None)
248
+ if value is not None:
249
+ return value
250
+ return __default
251
+
252
+ def create_keys(self, key: str) -> list[str]:
253
+ keys = []
254
+ if self.name:
255
+ keys.append(self.name.upper() + "_" + key)
256
+ keys.append(key)
257
+ return keys
@@ -0,0 +1,253 @@
1
+ Metadata-Version: 2.4
2
+ Name: eventsourcing
3
+ Version: 9.5.0b3
4
+ Summary: Event sourcing in Python
5
+ License-Expression: BSD-3-Clause
6
+ License-File: AUTHORS
7
+ License-File: LICENSE
8
+ Keywords: event sourcing,event store,domain driven design,domain-driven design,ddd,cqrs,cqs
9
+ Author: John Bywater
10
+ Author-email: john.bywater@appropriatesoftware.net
11
+ Requires-Python: >=3.10.0
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Education
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: License :: OSI Approved :: BSD License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Programming Language :: Python
25
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
26
+ Provides-Extra: crypto
27
+ Provides-Extra: cryptography
28
+ Provides-Extra: postgres
29
+ Requires-Dist: cryptography (>=44.0) ; extra == "cryptography"
30
+ Requires-Dist: psycopg[pool] (>=3.2) ; extra == "postgres"
31
+ Requires-Dist: pycryptodome (>=3.22) ; extra == "crypto"
32
+ Requires-Dist: typing_extensions
33
+ Project-URL: Documentation, https://eventsourcing.readthedocs.io/
34
+ Project-URL: Homepage, https://github.com/pyeventsourcing/eventsourcing
35
+ Project-URL: Repository, https://github.com/pyeventsourcing/eventsourcing
36
+ Description-Content-Type: text/markdown
37
+
38
+ [![Build Status](https://github.com/pyeventsourcing/eventsourcing/actions/workflows/runtests.yaml/badge.svg?branch=9.5)](https://github.com/pyeventsourcing/eventsourcing)
39
+ [![Coverage Status](https://coveralls.io/repos/github/pyeventsourcing/eventsourcing/badge.svg?branch=main)](https://coveralls.io/github/pyeventsourcing/eventsourcing?branch=main)
40
+ [![Documentation Status](https://readthedocs.org/projects/eventsourcing/badge/?version=stable)](https://eventsourcing.readthedocs.io/en/stable/)
41
+ [![Latest Release](https://badge.fury.io/py/eventsourcing.svg)](https://pypi.org/project/eventsourcing/)
42
+ [![Downloads](https://static.pepy.tech/personalized-badge/eventsourcing?period=total&units=international_system&left_color=grey&right_color=brightgreen&left_text=downloads)](https://pypistats.org/packages/eventsourcing)
43
+ [![Code Style: Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
44
+
45
+
46
+ # Event Sourcing in Python
47
+
48
+ This project is a comprehensive Python library for implementing event sourcing, a design pattern where all
49
+ changes to application state are stored as a sequence of events. This library provides a solid foundation
50
+ for building event-sourced applications in Python, with a focus on reliability, performance, and developer
51
+ experience. Please [read the docs](https://eventsourcing.readthedocs.io/). See also [extension projects](https://github.com/pyeventsourcing).
52
+
53
+ *"totally amazing and a pleasure to use"*
54
+
55
+ *"very clean and intuitive"*
56
+
57
+ *"a huge help and time saver"*
58
+
59
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/pyeventsourcing/eventsourcing)
60
+
61
+
62
+ ## Installation
63
+
64
+ Use pip to install the [stable distribution](https://pypi.org/project/eventsourcing/)
65
+ from the Python Package Index.
66
+
67
+ $ pip install eventsourcing
68
+
69
+ Please note, it is recommended to install Python
70
+ packages into a Python virtual environment.
71
+
72
+
73
+ ## Synopsis
74
+
75
+ Define aggregates with the `Aggregate` class and the `@event` decorator.
76
+
77
+ ```python
78
+ from eventsourcing.domain import Aggregate, event
79
+
80
+ class Dog(Aggregate):
81
+ @event('Registered')
82
+ def __init__(self, name: str) -> None:
83
+ self.name = name
84
+ self.tricks: list[str] = []
85
+
86
+ @event('TrickAdded')
87
+ def add_trick(self, trick: str) -> None:
88
+ self.tricks.append(trick)
89
+ ```
90
+
91
+ Define application objects with the `Application` class.
92
+
93
+ ```python
94
+ from typing import Any
95
+ from uuid import UUID
96
+
97
+ from eventsourcing.application import Application
98
+
99
+
100
+ class DogSchool(Application[UUID]):
101
+ def register_dog(self, name: str) -> UUID:
102
+ dog = Dog(name)
103
+ self.save(dog)
104
+ return dog.id
105
+
106
+ def add_trick(self, dog_id: UUID, trick: str) -> None:
107
+ dog: Dog = self.repository.get(dog_id)
108
+ dog.add_trick(trick)
109
+ self.save(dog)
110
+
111
+ def get_dog(self, dog_id: UUID) -> dict[str, Any]:
112
+ dog: Dog = self.repository.get(dog_id)
113
+ return {'name': dog.name, 'tricks': tuple(dog.tricks)}
114
+ ```
115
+
116
+ Write a test.
117
+
118
+ ```python
119
+ def test_dog_school() -> None:
120
+ # Construct application object.
121
+ school = DogSchool()
122
+
123
+ # Evolve application state.
124
+ dog_id = school.register_dog('Fido')
125
+ school.add_trick(dog_id, 'roll over')
126
+ school.add_trick(dog_id, 'play dead')
127
+
128
+ # Query application state.
129
+ dog = school.get_dog(dog_id)
130
+ assert dog['name'] == 'Fido'
131
+ assert dog['tricks'] == ('roll over', 'play dead')
132
+
133
+ # Select notifications.
134
+ notifications = school.notification_log.select(start=1, limit=10)
135
+ assert len(notifications) == 3
136
+ ```
137
+
138
+ Run the test with the default persistence module. Events are stored
139
+ in memory using Python objects.
140
+
141
+ ```python
142
+ test_dog_school()
143
+ ```
144
+
145
+ Configure the application to run with an SQLite database. Other persistence modules are available.
146
+
147
+ ```python
148
+ import os
149
+
150
+ os.environ["PERSISTENCE_MODULE"] = 'eventsourcing.sqlite'
151
+ os.environ["SQLITE_DBNAME"] = ':memory:'
152
+ ```
153
+
154
+ Run the test with SQLite.
155
+
156
+ ```python
157
+ test_dog_school()
158
+ ```
159
+
160
+ See the [documentation](https://eventsourcing.readthedocs.io/) for more information.
161
+
162
+
163
+ ## Features
164
+
165
+ **Flexible event store** — flexible persistence of domain events. Combines
166
+ an event mapper and an event recorder in ways that can be easily extended.
167
+ Mapper uses a transcoder that can be easily substituted or extended to support
168
+ custom model object types. Recorders supporting different databases can be easily
169
+ substituted and configured with environment variables.
170
+
171
+ **Domain models and applications** — base classes for event-sourced domain models
172
+ and applications. Suggests how to structure an event-sourced application. This
173
+ library supports event-sourced aggregates and dynamic consistency boundaries.
174
+
175
+ **Application-level encryption and compression** — encrypts and decrypts events inside the
176
+ application. This means data will be encrypted in transit across a network ("on the wire")
177
+ and at disk level including backups ("at rest"), which is a legal requirement in some
178
+ jurisdictions when dealing with personally identifiable information (PII) for example
179
+ the EU's GDPR. Compression reduces the size of stored domain events and snapshots, usually
180
+ by around 25% to 50% of the original size. Compression reduces the size of data
181
+ in the database and decreases transit time across a network.
182
+
183
+ **Snapshotting** — reduces access-time for aggregates with many domain events.
184
+
185
+ **Versioning** - allows domain model changes to be introduced after an application
186
+ has been deployed. Both domain events and aggregate classes can be versioned.
187
+ The recorded state of an older version can be upcast to be compatible with a new
188
+ version. Stored events and snapshots are upcast from older versions
189
+ to new versions before the event or aggregate object is reconstructed.
190
+
191
+ **Optimistic concurrency control** — ensures a distributed or horizontally scaled
192
+ application doesn't become inconsistent due to concurrent method execution. Leverages
193
+ optimistic concurrency controls in adapted database management systems.
194
+
195
+ **Notifications and projections** — reliable propagation of application
196
+ events with pull-based notifications allows the application state to be
197
+ projected accurately into replicas, indexes, view models, and other applications.
198
+ Supports materialised views and CQRS.
199
+
200
+ **Event-driven systems** — reliable event processing. Event-driven systems
201
+ can be defined independently of particular persistence infrastructure and mode of
202
+ running.
203
+
204
+ **Detailed documentation** — documentation provides general overview, introduction
205
+ of concepts, explanation of usage, and detailed descriptions of library classes.
206
+ All code is annotated with type hints.
207
+
208
+ **Worked examples** — includes examples showing how to develop aggregates, applications
209
+ and systems.
210
+
211
+
212
+
213
+ ## Extensions
214
+
215
+ The GitHub organisation
216
+ [Event Sourcing in Python](https://github.com/pyeventsourcing)
217
+ hosts extension projects for the Python eventsourcing library.
218
+ There are projects that adapt popular ORMs such as
219
+ [Django](https://github.com/pyeventsourcing/eventsourcing-django#readme)
220
+ and [SQLAlchemy](https://github.com/pyeventsourcing/eventsourcing-sqlalchemy#readme).
221
+ There are projects that adapt specialist event stores such as
222
+ [Axon Server](https://github.com/pyeventsourcing/eventsourcing-axonserver#readme) and
223
+ [KurrentDB](https://github.com/pyeventsourcing/eventsourcing-kurrentdb#readme).
224
+ There are projects that support popular NoSQL databases such as
225
+ [DynamoDB](https://github.com/pyeventsourcing/eventsourcing-dynamodb#readme).
226
+ There are also projects that provide examples of using the
227
+ library with web frameworks such as
228
+ [FastAPI](https://github.com/pyeventsourcing/example-fastapi#readme)
229
+ and [Flask](https://github.com/pyeventsourcing/example-flask#readme),
230
+ and for serving applications and running systems with efficient
231
+ inter-process communication technologies like [gRPC](https://github.com/pyeventsourcing/eventsourcing-grpc#readme).
232
+ And there are examples of event-sourced applications and systems
233
+ of event-sourced applications, such as the
234
+ [Paxos system](https://github.com/pyeventsourcing/example-paxos#readme),
235
+ which is used as the basis for a
236
+ [replicated state machine](https://github.com/pyeventsourcing/example-paxos/tree/master/replicatedstatemachine),
237
+ which is used as the basis for a
238
+ [distributed key-value store](https://github.com/pyeventsourcing/example-paxos/tree/master/keyvaluestore).
239
+
240
+ ## Project
241
+
242
+ This project is [hosted on GitHub](https://github.com/pyeventsourcing/eventsourcing).
243
+
244
+ Please register questions, requests and
245
+ [issues on GitHub](https://github.com/pyeventsourcing/eventsourcing/issues),
246
+ or post in the project's Slack channel.
247
+
248
+ There is a [Slack channel](https://join.slack.com/t/eventsourcinginpython/shared_invite/zt-3hogb36o-LCvKd4Rz8JMALoLSl_pQ8g)
249
+ for this project, which you are [welcome to join](https://join.slack.com/t/eventsourcinginpython/shared_invite/zt-3hogb36o-LCvKd4Rz8JMALoLSl_pQ8g).
250
+
251
+ Please refer to the [documentation](https://eventsourcing.readthedocs.io/) for installation and usage guides.
252
+
253
+
@@ -0,0 +1,35 @@
1
+ eventsourcing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ eventsourcing/application.py,sha256=OjzmDs0SbNRRUVs4Um2qmfiD6jD0QA-jZx66c7tixu4,37321
3
+ eventsourcing/cipher.py,sha256=ulTBtX5K9ejRAkdUaUbdIaj4H7anYwDOi7JxOolj2uo,3295
4
+ eventsourcing/compressor.py,sha256=qEYWvsUXFLyhKgfuv-HGNJ6VF4sRw4z0IxbNW9ukOfc,385
5
+ eventsourcing/cryptography.py,sha256=aFZLlJxxSb5seVbh94-T8FA_RIGOe-VFu5SJrbOnwUU,2969
6
+ eventsourcing/dcb/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ eventsourcing/dcb/api.py,sha256=2h9KzUi1NWZjq7y2DHz_wRYUIv7NzibyJdsd2iTk5P8,4153
8
+ eventsourcing/dcb/application.py,sha256=-2CG1ph1nHRGbwrlXnNikz4898YVCmkXS8gi5yqIn4Y,5089
9
+ eventsourcing/dcb/domain.py,sha256=YrXAnDfQSrtraq8Je0QNM5rtnb8teon3iJFZJCJmZgA,12250
10
+ eventsourcing/dcb/msgpack.py,sha256=a6AZgou_TvlSSzUqIS5bYCSFxPyOrP24FJhevrib4C0,1097
11
+ eventsourcing/dcb/persistence.py,sha256=EScPVFi0xPtbxwykyeXOx1NA0KgUYOmel-z0BHhCvEg,6116
12
+ eventsourcing/dcb/popo.py,sha256=4mXl2m2gbs6uLjgLRGmBh0YzAP_FwDmZ11-NkLhXSjE,5678
13
+ eventsourcing/dcb/postgres_tt.py,sha256=pmAQKLydTFQTi6QIc_Frqko9SjoTCgZvs68WufKHB-Y,23036
14
+ eventsourcing/dcb/tests.py,sha256=QGBwWHWpzeeUHZHjzpBXZrJ0EBRX4ldRJ8oe7Llt_pc,24698
15
+ eventsourcing/dispatch.py,sha256=A36Oj0zs28lk2E08p2M2QMEkj4PUECAs60OeEQziTf4,2828
16
+ eventsourcing/domain.py,sha256=WuGpjXhDYH1l2yuJgZYQ1lmzrz7LAeraGW3VkkVy2iw,75999
17
+ eventsourcing/interface.py,sha256=K7tAJjriOJa_XB9-wptJR9VTb5sHlBpqrz3BGUXxI4A,5387
18
+ eventsourcing/persistence.py,sha256=PKLBVnXASLgOcRd5vKMsiOpnkmeHDmXHwPNSSLRQCcI,50057
19
+ eventsourcing/popo.py,sha256=fBPLn6_49kDbfqtFvVPozYfPQfeSl4VhBP38klCUL_A,9203
20
+ eventsourcing/postgres.py,sha256=7NjIrrP5g46zfbeP-zB4vyxFqcWgQ6pHaNPix8cUB74,56277
21
+ eventsourcing/projection.py,sha256=SFek7kw2op0Zg3bchZV9xSRQT-OnssT2TFN8TgmRSSU,18718
22
+ eventsourcing/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
+ eventsourcing/sqlite.py,sha256=xvgu9euGYalgMoKurCuzBWySEMi3Ga4rGzXmm0XGep8,27949
24
+ eventsourcing/system.py,sha256=_jAYtB7gkDx3Kdm0we2AJJHQR5JLPYtA7loOrS7mDHY,45907
25
+ eventsourcing/tests/__init__.py,sha256=FtOyuj-L-oSisYeByTIrnUw-XzsctSbq76XmjPy5fMc,102
26
+ eventsourcing/tests/application.py,sha256=pE2tYfuykbV4Q6WW1U-gi_YgyW2485NGLXkemaH46Do,18072
27
+ eventsourcing/tests/domain.py,sha256=yN-F6gMRumeX6nIXIcZGxAR3RrUslzmEMM8JksnkI8Q,3227
28
+ eventsourcing/tests/persistence.py,sha256=jsS2JUo5U8gG5POdWGfuTDiQzhd8OoroWpt8QzpID1o,62942
29
+ eventsourcing/tests/postgres_utils.py,sha256=hKW7xd3yzOHl7W5Q4-1YAecoLRS7QaYr8zBOrUFWfVA,4690
30
+ eventsourcing/utils.py,sha256=tR4qVXHvgq0fJuS_1x_ULahdePrQPvg8wSEtmEVaRy0,8586
31
+ eventsourcing-9.5.0b3.dist-info/METADATA,sha256=AhS14onZ5jHiWf-l7DtdLdKNj0NnOlDXMvYQ4tnLbGM,10564
32
+ eventsourcing-9.5.0b3.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
33
+ eventsourcing-9.5.0b3.dist-info/licenses/AUTHORS,sha256=8aHOM4UbNZcKlD-cHpFRcM6RWyCqtwtxRev6DeUgVRs,137
34
+ eventsourcing-9.5.0b3.dist-info/licenses/LICENSE,sha256=CQEQzcZO8AWXL5i3hIo4yVKrYjh2FBz6hCM7kpXWpw4,1512
35
+ eventsourcing-9.5.0b3.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.2.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any