python-event-sourcery 0.4.1a1__tar.gz → 0.5.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.
- python_event_sourcery-0.5.0/PKG-INFO +152 -0
- python_event_sourcery-0.5.0/README.md +126 -0
- python_event_sourcery-0.5.0/event_sourcery/__init__.py +21 -0
- {python_event_sourcery-0.4.1a1/event_sourcery/event_store → python_event_sourcery-0.5.0/event_sourcery/_event_store}/backend.py +89 -35
- python_event_sourcery-0.4.1a1/event_sourcery/event_store/event/_dto.py → python_event_sourcery-0.5.0/event_sourcery/_event_store/event/dto.py +64 -3
- python_event_sourcery-0.4.1a1/event_sourcery/event_store/event/_encryption.py → python_event_sourcery-0.5.0/event_sourcery/_event_store/event/encryption.py +125 -8
- python_event_sourcery-0.4.1a1/event_sourcery/event_store/event/_registry.py → python_event_sourcery-0.5.0/event_sourcery/_event_store/event/registry.py +8 -7
- python_event_sourcery-0.4.1a1/event_sourcery/event_store/event/_serde.py → python_event_sourcery-0.5.0/event_sourcery/_event_store/event/serde.py +4 -4
- {python_event_sourcery-0.4.1a1/event_sourcery/event_store → python_event_sourcery-0.5.0/event_sourcery/_event_store}/event_store.py +89 -5
- {python_event_sourcery-0.4.1a1/event_sourcery/event_store → python_event_sourcery-0.5.0/event_sourcery/_event_store}/in_memory.py +60 -16
- python_event_sourcery-0.5.0/event_sourcery/_event_store/outbox.py +106 -0
- {python_event_sourcery-0.4.1a1/event_sourcery/event_store → python_event_sourcery-0.5.0/event_sourcery/_event_store}/stream_id.py +31 -3
- python_event_sourcery-0.4.1a1/event_sourcery/event_store/subscription.py → python_event_sourcery-0.5.0/event_sourcery/_event_store/subscription/builder.py +11 -33
- python_event_sourcery-0.5.0/event_sourcery/_event_store/subscription/in_transaction.py +132 -0
- python_event_sourcery-0.5.0/event_sourcery/_event_store/subscription/interfaces.py +107 -0
- python_event_sourcery-0.5.0/event_sourcery/_event_store/versioning.py +108 -0
- python_event_sourcery-0.5.0/event_sourcery/backend.py +21 -0
- python_event_sourcery-0.5.0/event_sourcery/encryption.py +17 -0
- python_event_sourcery-0.5.0/event_sourcery/event.py +26 -0
- python_event_sourcery-0.5.0/event_sourcery/event_sourcing/aggregate.py +66 -0
- {python_event_sourcery-0.4.1a1 → python_event_sourcery-0.5.0}/event_sourcery/event_sourcing/repository.py +24 -7
- {python_event_sourcery-0.4.1a1/event_sourcery/event_store → python_event_sourcery-0.5.0/event_sourcery}/exceptions.py +4 -0
- python_event_sourcery-0.5.0/event_sourcery/in_transaction.py +11 -0
- python_event_sourcery-0.5.0/event_sourcery/interfaces.py +21 -0
- python_event_sourcery-0.5.0/event_sourcery/outbox.py +11 -0
- {python_event_sourcery-0.4.1a1 → python_event_sourcery-0.5.0}/event_sourcery/read_model/cursors_dao.py +1 -1
- {python_event_sourcery-0.4.1a1 → python_event_sourcery-0.5.0}/event_sourcery/read_model/projector.py +2 -1
- python_event_sourcery-0.5.0/event_sourcery/subscription.py +15 -0
- python_event_sourcery-0.5.0/event_sourcery_django/__init__.py +97 -0
- {python_event_sourcery-0.4.1a1 → python_event_sourcery-0.5.0}/event_sourcery_django/dto.py +2 -1
- {python_event_sourcery-0.4.1a1 → python_event_sourcery-0.5.0}/event_sourcery_django/event_store.py +5 -13
- {python_event_sourcery-0.4.1a1 → python_event_sourcery-0.5.0}/event_sourcery_django/models.py +1 -1
- {python_event_sourcery-0.4.1a1 → python_event_sourcery-0.5.0}/event_sourcery_django/outbox.py +2 -2
- {python_event_sourcery-0.4.1a1 → python_event_sourcery-0.5.0}/event_sourcery_django/subscription.py +2 -2
- python_event_sourcery-0.5.0/event_sourcery_kurrentdb/__init__.py +107 -0
- {python_event_sourcery-0.4.1a1 → python_event_sourcery-0.5.0}/event_sourcery_kurrentdb/dto.py +1 -1
- {python_event_sourcery-0.4.1a1 → python_event_sourcery-0.5.0}/event_sourcery_kurrentdb/event_store.py +4 -11
- {python_event_sourcery-0.4.1a1 → python_event_sourcery-0.5.0}/event_sourcery_kurrentdb/outbox.py +2 -2
- python_event_sourcery-0.5.0/event_sourcery_kurrentdb/py.typed +0 -0
- {python_event_sourcery-0.4.1a1 → python_event_sourcery-0.5.0}/event_sourcery_kurrentdb/stream.py +2 -2
- {python_event_sourcery-0.4.1a1 → python_event_sourcery-0.5.0}/event_sourcery_kurrentdb/subscription.py +2 -2
- {python_event_sourcery-0.4.1a1 → python_event_sourcery-0.5.0}/event_sourcery_sqlalchemy/__init__.py +65 -10
- {python_event_sourcery-0.4.1a1 → python_event_sourcery-0.5.0}/event_sourcery_sqlalchemy/cursors_dao.py +1 -1
- {python_event_sourcery-0.4.1a1 → python_event_sourcery-0.5.0}/event_sourcery_sqlalchemy/dto.py +2 -1
- {python_event_sourcery-0.4.1a1 → python_event_sourcery-0.5.0}/event_sourcery_sqlalchemy/event_store.py +5 -12
- {python_event_sourcery-0.4.1a1 → python_event_sourcery-0.5.0}/event_sourcery_sqlalchemy/models/__init__.py +25 -0
- {python_event_sourcery-0.4.1a1 → python_event_sourcery-0.5.0}/event_sourcery_sqlalchemy/models/base.py +1 -2
- {python_event_sourcery-0.4.1a1 → python_event_sourcery-0.5.0}/event_sourcery_sqlalchemy/outbox.py +3 -2
- python_event_sourcery-0.5.0/event_sourcery_sqlalchemy/py.typed +0 -0
- {python_event_sourcery-0.4.1a1 → python_event_sourcery-0.5.0}/event_sourcery_sqlalchemy/subscription.py +2 -2
- {python_event_sourcery-0.4.1a1 → python_event_sourcery-0.5.0}/pyproject.toml +7 -3
- python_event_sourcery-0.4.1a1/PKG-INFO +0 -23
- python_event_sourcery-0.4.1a1/event_sourcery/event_sourcing/aggregate.py +0 -24
- python_event_sourcery-0.4.1a1/event_sourcery/event_store/__init__.py +0 -52
- python_event_sourcery-0.4.1a1/event_sourcery/event_store/dispatcher.py +0 -64
- python_event_sourcery-0.4.1a1/event_sourcery/event_store/event/__init__.py +0 -38
- python_event_sourcery-0.4.1a1/event_sourcery/event_store/interfaces.py +0 -101
- python_event_sourcery-0.4.1a1/event_sourcery/event_store/outbox.py +0 -27
- python_event_sourcery-0.4.1a1/event_sourcery/event_store/versioning.py +0 -57
- python_event_sourcery-0.4.1a1/event_sourcery_django/__init__.py +0 -61
- python_event_sourcery-0.4.1a1/event_sourcery_kurrentdb/__init__.py +0 -69
- {python_event_sourcery-0.4.1a1 → python_event_sourcery-0.5.0}/LICENSE +0 -0
- {python_event_sourcery-0.4.1a1/event_sourcery → python_event_sourcery-0.5.0/event_sourcery/_event_store}/__init__.py +0 -0
- {python_event_sourcery-0.4.1a1/event_sourcery_django/migrations → python_event_sourcery-0.5.0/event_sourcery/_event_store/event}/__init__.py +0 -0
- /python_event_sourcery-0.4.1a1/event_sourcery/py.typed → /python_event_sourcery-0.5.0/event_sourcery/_event_store/subscription/__init__.py +0 -0
- {python_event_sourcery-0.4.1a1/event_sourcery/event_store → python_event_sourcery-0.5.0/event_sourcery/_event_store}/tenant_id.py +0 -0
- {python_event_sourcery-0.4.1a1 → python_event_sourcery-0.5.0}/event_sourcery/event_sourcing/__init__.py +0 -0
- {python_event_sourcery-0.4.1a1/event_sourcery_django → python_event_sourcery-0.5.0/event_sourcery}/py.typed +0 -0
- {python_event_sourcery-0.4.1a1 → python_event_sourcery-0.5.0}/event_sourcery/read_model/__init__.py +0 -0
- {python_event_sourcery-0.4.1a1 → python_event_sourcery-0.5.0}/event_sourcery_django/migrations/0001_initial.py +0 -0
- /python_event_sourcery-0.4.1a1/event_sourcery_kurrentdb/py.typed → /python_event_sourcery-0.5.0/event_sourcery_django/migrations/__init__.py +0 -0
- {python_event_sourcery-0.4.1a1/event_sourcery_sqlalchemy → python_event_sourcery-0.5.0/event_sourcery_django}/py.typed +0 -0
- {python_event_sourcery-0.4.1a1 → python_event_sourcery-0.5.0}/event_sourcery_sqlalchemy/guid.py +0 -0
- {python_event_sourcery-0.4.1a1 → python_event_sourcery-0.5.0}/event_sourcery_sqlalchemy/jsonb.py +0 -0
- {python_event_sourcery-0.4.1a1 → python_event_sourcery-0.5.0}/event_sourcery_sqlalchemy/models/default.py +0 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: python-event-sourcery
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: A library for event-based systems in Python.
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Sebastian Buczyński
|
|
7
|
+
Author-email: nnplaya@gmail.com
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Provides-Extra: django
|
|
16
|
+
Provides-Extra: kurrentdb
|
|
17
|
+
Provides-Extra: sqlalchemy
|
|
18
|
+
Requires-Dist: SQLAlchemy (>=2.0) ; extra == "sqlalchemy"
|
|
19
|
+
Requires-Dist: django (>=4,<6) ; extra == "django"
|
|
20
|
+
Requires-Dist: kurrentdbclient (>=1.0.6) ; extra == "kurrentdb"
|
|
21
|
+
Requires-Dist: more-itertools
|
|
22
|
+
Requires-Dist: psycopg2-binary (>2,<3)
|
|
23
|
+
Requires-Dist: pydantic (>=2.0,<3)
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Python Event Sourcery
|
|
28
|
+
|
|
29
|
+
A library for event-based systems in Python. For event sourcing, CQRS, and event-driven architectures.
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
event = EventSourceryIsBorn()
|
|
33
|
+
|
|
34
|
+
event_store.append(event, stream_id=stream_id)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
_Under heavy development_
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
Documentation: [python-event-sourcery.github.io/python-event-sourcery/](https://python-event-sourcery.github.io/python-event-sourcery/)
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install python-event-sourcery
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Easy to setup
|
|
54
|
+
Just configure your `EventStore` instance (or a factory) and call `configure_models` once. Examples below.
|
|
55
|
+
|
|
56
|
+
## Versatile
|
|
57
|
+
Event Sourcery makes no assumptions about your configuration or session management. It's designed to be plugged in into what you already have, without a need to adjust anything. It can be integrated smoothly with thread-scoped SQLAlchemy sessions as well as dependency injection.
|
|
58
|
+
|
|
59
|
+
## NOT opinionated
|
|
60
|
+
Although one can easily start with a library, the latter is very customizable and DOES NOT enforce a particular style of using. Also, libraries integrated, i.e. SQLAlchemy and Pydantic are not hardcoded dependencies. They can be replaced with a finite amount of effort.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Use cases & features
|
|
65
|
+
Until full documentation is available, you can follow tests
|
|
66
|
+
|
|
67
|
+
- Event Sourcing [tests](https://github.com/Enforcer/python-event-sourcery/blob/main/tests/repository/test_aggregate_context_manager.py)
|
|
68
|
+
- Snapshots (for Event Sourcing) [tests](https://github.com/Enforcer/python-event-sourcery/blob/main/tests/event_store/test_snapshots.py)
|
|
69
|
+
- Event Store - storage for events [tests](https://github.com/Enforcer/python-event-sourcery/blob/main/tests/event_store/test_save_retrieval.py)
|
|
70
|
+
- Outbox pattern [tests](https://github.com/Enforcer/python-event-sourcery/blob/main/tests/outbox/test_outbox.py)
|
|
71
|
+
- Concurrency control with optimistic locking [tests](https://github.com/Enforcer/python-event-sourcery/blob/main/tests/event_store/test_concurrency_control.py)
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Quick start
|
|
76
|
+
|
|
77
|
+
The script is complete, it should run as-is provided FastAPI and library with dependencies are installed.
|
|
78
|
+
See examples/0-fastapi-integration
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from typing import Iterator, Literal
|
|
82
|
+
from uuid import uuid4
|
|
83
|
+
|
|
84
|
+
from fastapi import FastAPI, Depends
|
|
85
|
+
from sqlalchemy import create_engine
|
|
86
|
+
from sqlalchemy.orm import Session, as_declarative
|
|
87
|
+
|
|
88
|
+
# Import a couple of things from event_sourcery
|
|
89
|
+
from event_sourcery import EventStore # for type annotations
|
|
90
|
+
from event_sourcery import (
|
|
91
|
+
configure_models, # set-up function for library's models
|
|
92
|
+
get_event_store, # factory function to start quickly
|
|
93
|
+
Event, # base class for events
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Set up your DB and application as you would normally do
|
|
97
|
+
engine = create_engine(
|
|
98
|
+
"postgresql://event_sourcery:event_sourcery@localhost:5432/event_sourcery"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def db_session() -> Iterator[Session]:
|
|
103
|
+
session = Session(bind=engine)
|
|
104
|
+
try:
|
|
105
|
+
yield session
|
|
106
|
+
finally:
|
|
107
|
+
session.close()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@as_declarative()
|
|
111
|
+
class Base:
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
app = FastAPI(on_startup=[lambda: Base.metadata.create_all(bind=engine)])
|
|
116
|
+
|
|
117
|
+
# initialize Event Sourcery models, so they can be handled by SQLAlchemy and e.g. alembic
|
|
118
|
+
configure_models(Base)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# Set up factory for event store to be injected into views
|
|
122
|
+
def event_store(session: Session = Depends(db_session)) -> EventStore:
|
|
123
|
+
return get_event_store(session)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# Define your event(s) using base-class provided
|
|
127
|
+
class CustomerSubscribed(Event):
|
|
128
|
+
plan_id: int
|
|
129
|
+
model: Literal["monthly", "yearly"]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@app.post("/subscription")
|
|
133
|
+
def subscribe(
|
|
134
|
+
event_store: EventStore = Depends(event_store),
|
|
135
|
+
session: Session = Depends(db_session),
|
|
136
|
+
):
|
|
137
|
+
# Create an event
|
|
138
|
+
event = CustomerSubscribed(plan_id=1, model="yearly")
|
|
139
|
+
# Put it in the event store!
|
|
140
|
+
event_store.append(event, stream_id=uuid4())
|
|
141
|
+
session.commit()
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Feature requests / feedback
|
|
146
|
+
|
|
147
|
+
Anything missing? Create an issue in this repository. Let others upvote
|
|
148
|
+
|
|
149
|
+
## Inspirations
|
|
150
|
+
- [Rails Event Store](https://railseventstore.org/)
|
|
151
|
+
- [Marten](https://martendb.io/)
|
|
152
|
+
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
|
|
2
|
+
# Python Event Sourcery
|
|
3
|
+
|
|
4
|
+
A library for event-based systems in Python. For event sourcing, CQRS, and event-driven architectures.
|
|
5
|
+
|
|
6
|
+
```python
|
|
7
|
+
event = EventSourceryIsBorn()
|
|
8
|
+
|
|
9
|
+
event_store.append(event, stream_id=stream_id)
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
_Under heavy development_
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
Documentation: [python-event-sourcery.github.io/python-event-sourcery/](https://python-event-sourcery.github.io/python-event-sourcery/)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install python-event-sourcery
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Easy to setup
|
|
29
|
+
Just configure your `EventStore` instance (or a factory) and call `configure_models` once. Examples below.
|
|
30
|
+
|
|
31
|
+
## Versatile
|
|
32
|
+
Event Sourcery makes no assumptions about your configuration or session management. It's designed to be plugged in into what you already have, without a need to adjust anything. It can be integrated smoothly with thread-scoped SQLAlchemy sessions as well as dependency injection.
|
|
33
|
+
|
|
34
|
+
## NOT opinionated
|
|
35
|
+
Although one can easily start with a library, the latter is very customizable and DOES NOT enforce a particular style of using. Also, libraries integrated, i.e. SQLAlchemy and Pydantic are not hardcoded dependencies. They can be replaced with a finite amount of effort.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Use cases & features
|
|
40
|
+
Until full documentation is available, you can follow tests
|
|
41
|
+
|
|
42
|
+
- Event Sourcing [tests](https://github.com/Enforcer/python-event-sourcery/blob/main/tests/repository/test_aggregate_context_manager.py)
|
|
43
|
+
- Snapshots (for Event Sourcing) [tests](https://github.com/Enforcer/python-event-sourcery/blob/main/tests/event_store/test_snapshots.py)
|
|
44
|
+
- Event Store - storage for events [tests](https://github.com/Enforcer/python-event-sourcery/blob/main/tests/event_store/test_save_retrieval.py)
|
|
45
|
+
- Outbox pattern [tests](https://github.com/Enforcer/python-event-sourcery/blob/main/tests/outbox/test_outbox.py)
|
|
46
|
+
- Concurrency control with optimistic locking [tests](https://github.com/Enforcer/python-event-sourcery/blob/main/tests/event_store/test_concurrency_control.py)
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Quick start
|
|
51
|
+
|
|
52
|
+
The script is complete, it should run as-is provided FastAPI and library with dependencies are installed.
|
|
53
|
+
See examples/0-fastapi-integration
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from typing import Iterator, Literal
|
|
57
|
+
from uuid import uuid4
|
|
58
|
+
|
|
59
|
+
from fastapi import FastAPI, Depends
|
|
60
|
+
from sqlalchemy import create_engine
|
|
61
|
+
from sqlalchemy.orm import Session, as_declarative
|
|
62
|
+
|
|
63
|
+
# Import a couple of things from event_sourcery
|
|
64
|
+
from event_sourcery import EventStore # for type annotations
|
|
65
|
+
from event_sourcery import (
|
|
66
|
+
configure_models, # set-up function for library's models
|
|
67
|
+
get_event_store, # factory function to start quickly
|
|
68
|
+
Event, # base class for events
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Set up your DB and application as you would normally do
|
|
72
|
+
engine = create_engine(
|
|
73
|
+
"postgresql://event_sourcery:event_sourcery@localhost:5432/event_sourcery"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def db_session() -> Iterator[Session]:
|
|
78
|
+
session = Session(bind=engine)
|
|
79
|
+
try:
|
|
80
|
+
yield session
|
|
81
|
+
finally:
|
|
82
|
+
session.close()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@as_declarative()
|
|
86
|
+
class Base:
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
app = FastAPI(on_startup=[lambda: Base.metadata.create_all(bind=engine)])
|
|
91
|
+
|
|
92
|
+
# initialize Event Sourcery models, so they can be handled by SQLAlchemy and e.g. alembic
|
|
93
|
+
configure_models(Base)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# Set up factory for event store to be injected into views
|
|
97
|
+
def event_store(session: Session = Depends(db_session)) -> EventStore:
|
|
98
|
+
return get_event_store(session)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# Define your event(s) using base-class provided
|
|
102
|
+
class CustomerSubscribed(Event):
|
|
103
|
+
plan_id: int
|
|
104
|
+
model: Literal["monthly", "yearly"]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@app.post("/subscription")
|
|
108
|
+
def subscribe(
|
|
109
|
+
event_store: EventStore = Depends(event_store),
|
|
110
|
+
session: Session = Depends(db_session),
|
|
111
|
+
):
|
|
112
|
+
# Create an event
|
|
113
|
+
event = CustomerSubscribed(plan_id=1, model="yearly")
|
|
114
|
+
# Put it in the event store!
|
|
115
|
+
event_store.append(event, stream_id=uuid4())
|
|
116
|
+
session.commit()
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Feature requests / feedback
|
|
121
|
+
|
|
122
|
+
Anything missing? Create an issue in this repository. Let others upvote
|
|
123
|
+
|
|
124
|
+
## Inspirations
|
|
125
|
+
- [Rails Event Store](https://railseventstore.org/)
|
|
126
|
+
- [Marten](https://martendb.io/)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
"DEFAULT_TENANT",
|
|
3
|
+
"NO_VERSIONING",
|
|
4
|
+
"Backend",
|
|
5
|
+
"Event",
|
|
6
|
+
"EventStore",
|
|
7
|
+
"Outbox",
|
|
8
|
+
"StreamCategory",
|
|
9
|
+
"StreamId",
|
|
10
|
+
"StreamUUID",
|
|
11
|
+
"TenantId",
|
|
12
|
+
"TransactionalBackend",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
from event_sourcery._event_store.backend import Backend, TransactionalBackend
|
|
16
|
+
from event_sourcery._event_store.event.dto import Event
|
|
17
|
+
from event_sourcery._event_store.event_store import EventStore
|
|
18
|
+
from event_sourcery._event_store.outbox import Outbox
|
|
19
|
+
from event_sourcery._event_store.stream_id import StreamCategory, StreamId, StreamUUID
|
|
20
|
+
from event_sourcery._event_store.tenant_id import DEFAULT_TENANT, TenantId
|
|
21
|
+
from event_sourcery._event_store.versioning import NO_VERSIONING
|
|
@@ -1,48 +1,42 @@
|
|
|
1
|
-
from collections.abc import Callable
|
|
2
|
-
from contextlib import AbstractContextManager
|
|
1
|
+
from collections.abc import Callable
|
|
3
2
|
from functools import wraps
|
|
4
3
|
from typing import NoReturn, TypeVar, cast
|
|
5
4
|
|
|
6
5
|
from typing_extensions import Self
|
|
7
6
|
|
|
8
|
-
from event_sourcery.
|
|
9
|
-
from event_sourcery.event_store.dispatcher import Dispatcher, Listeners
|
|
10
|
-
from event_sourcery.event_store.event import (
|
|
7
|
+
from event_sourcery._event_store.event.encryption import (
|
|
11
8
|
Encryption,
|
|
12
|
-
|
|
9
|
+
EncryptionKeyStorageStrategy,
|
|
10
|
+
EncryptionStrategy,
|
|
13
11
|
NoEncryptionStrategy,
|
|
14
12
|
NoKeyStorageStrategy,
|
|
15
|
-
RawEvent,
|
|
16
|
-
RecordedRaw,
|
|
17
|
-
Serde,
|
|
18
13
|
)
|
|
19
|
-
from event_sourcery.
|
|
20
|
-
from event_sourcery.
|
|
21
|
-
|
|
22
|
-
|
|
14
|
+
from event_sourcery._event_store.event.registry import EventRegistry
|
|
15
|
+
from event_sourcery._event_store.event.serde import Serde
|
|
16
|
+
from event_sourcery._event_store.event_store import (
|
|
17
|
+
EventStore,
|
|
18
|
+
StorageStrategy,
|
|
19
|
+
)
|
|
20
|
+
from event_sourcery._event_store.outbox import (
|
|
21
|
+
NoOutboxStorageStrategy,
|
|
22
|
+
Outbox,
|
|
23
23
|
OutboxFiltererStrategy,
|
|
24
24
|
OutboxStorageStrategy,
|
|
25
|
-
|
|
25
|
+
no_filter,
|
|
26
|
+
)
|
|
27
|
+
from event_sourcery._event_store.subscription.builder import (
|
|
28
|
+
SubscriptionBuilder,
|
|
29
|
+
)
|
|
30
|
+
from event_sourcery._event_store.subscription.in_transaction import (
|
|
31
|
+
Dispatcher,
|
|
32
|
+
Listeners,
|
|
33
|
+
)
|
|
34
|
+
from event_sourcery._event_store.subscription.interfaces import (
|
|
35
|
+
PositionPhase,
|
|
26
36
|
SubscriptionStrategy,
|
|
27
37
|
)
|
|
28
|
-
from event_sourcery.
|
|
29
|
-
from event_sourcery.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class NoProviderConfigured(Exception):
|
|
33
|
-
pass
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def no_filter(entry: RawEvent) -> bool:
|
|
37
|
-
return True
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
class NoOutboxStorageStrategy(OutboxStorageStrategy):
|
|
41
|
-
def outbox_entries(
|
|
42
|
-
self, limit: int
|
|
43
|
-
) -> Iterator[AbstractContextManager[RecordedRaw]]:
|
|
44
|
-
return iter([])
|
|
45
|
-
|
|
38
|
+
from event_sourcery._event_store.tenant_id import DEFAULT_TENANT, TenantId
|
|
39
|
+
from event_sourcery.exceptions import NoProviderConfigured
|
|
46
40
|
|
|
47
41
|
T = TypeVar("T")
|
|
48
42
|
T_co = TypeVar("T_co", covariant=True)
|
|
@@ -51,6 +45,16 @@ _Provider = Callable[["_Container"], T]
|
|
|
51
45
|
|
|
52
46
|
|
|
53
47
|
def singleton(provider: _Provider[T]) -> _Provider[T]:
|
|
48
|
+
"""
|
|
49
|
+
Decorator that ensures the provider returns the same instance (singleton)
|
|
50
|
+
for every call within the container's lifecycle.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
provider (_Provider[T]): The provider function to wrap.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
_Provider[T]: A provider that always returns the same instance.
|
|
57
|
+
"""
|
|
54
58
|
result: T | None = None
|
|
55
59
|
|
|
56
60
|
@wraps(provider)
|
|
@@ -65,6 +69,13 @@ def singleton(provider: _Provider[T]) -> _Provider[T]:
|
|
|
65
69
|
|
|
66
70
|
|
|
67
71
|
def not_configured(error_message: str) -> _Provider[T]:
|
|
72
|
+
"""
|
|
73
|
+
Used as a placeholder for providers that require configuration.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
error_message (str): str. The error message to raise with the exception.
|
|
77
|
+
"""
|
|
78
|
+
|
|
68
79
|
def _raise(container: "_Container") -> NoReturn:
|
|
69
80
|
raise NoProviderConfigured(error_message) # pragma: no cover
|
|
70
81
|
|
|
@@ -97,6 +108,18 @@ class _Container:
|
|
|
97
108
|
|
|
98
109
|
|
|
99
110
|
class Backend(_Container):
|
|
111
|
+
"""
|
|
112
|
+
Dependency Injection container for Event Sourcery components.
|
|
113
|
+
|
|
114
|
+
Dict-like object resolving object instance during access by type.
|
|
115
|
+
Providers are passed setting the type as key and provider as a value.
|
|
116
|
+
|
|
117
|
+
By default, basic implementations are registered or an exception is raised
|
|
118
|
+
if backend configuration (e.g., storage strategy) is required.
|
|
119
|
+
|
|
120
|
+
Core components are exposed by properties.
|
|
121
|
+
"""
|
|
122
|
+
|
|
100
123
|
def __init__(self) -> None:
|
|
101
124
|
super().__init__()
|
|
102
125
|
self[TenantId] = DEFAULT_TENANT
|
|
@@ -129,29 +152,44 @@ class Backend(_Container):
|
|
|
129
152
|
self[SubscriptionStrategy] = not_configured(
|
|
130
153
|
"Use one of pyES backends: SQLAlchemy, Django or KurrentDB",
|
|
131
154
|
)
|
|
132
|
-
self[
|
|
155
|
+
self[PositionPhase] = lambda c: SubscriptionBuilder(
|
|
133
156
|
c[Serde],
|
|
134
157
|
c[SubscriptionStrategy],
|
|
135
158
|
)
|
|
136
159
|
|
|
137
160
|
@property
|
|
138
161
|
def event_store(self) -> EventStore:
|
|
162
|
+
"""
|
|
163
|
+
Returns the current instance of `EventStore`.
|
|
164
|
+
"""
|
|
139
165
|
return self[EventStore]
|
|
140
166
|
|
|
141
167
|
@property
|
|
142
168
|
def outbox(self) -> Outbox:
|
|
169
|
+
"""
|
|
170
|
+
Returns the current instance of `Outbox`.
|
|
171
|
+
"""
|
|
143
172
|
return self[Outbox]
|
|
144
173
|
|
|
145
174
|
@property
|
|
146
|
-
def subscriber(self) ->
|
|
147
|
-
|
|
175
|
+
def subscriber(self) -> PositionPhase:
|
|
176
|
+
"""
|
|
177
|
+
Returns the current instance of `SubscriptionBuilder` (as `PositionPhase`).
|
|
178
|
+
"""
|
|
179
|
+
return self[PositionPhase]
|
|
148
180
|
|
|
149
181
|
def in_tenant_mode(self, tenant_id: TenantId) -> Self:
|
|
182
|
+
"""
|
|
183
|
+
Returns a copy of the backend with the specified tenant ID set.
|
|
184
|
+
"""
|
|
150
185
|
in_tenant_mode = self.copy()
|
|
151
186
|
in_tenant_mode[TenantId] = tenant_id
|
|
152
187
|
return in_tenant_mode
|
|
153
188
|
|
|
154
189
|
def with_outbox(self, filterer: OutboxFiltererStrategy = no_filter) -> Self:
|
|
190
|
+
"""
|
|
191
|
+
Configure the outbox with a custom filter.
|
|
192
|
+
"""
|
|
155
193
|
raise NotImplementedError()
|
|
156
194
|
|
|
157
195
|
def with_encryption(
|
|
@@ -159,6 +197,9 @@ class Backend(_Container):
|
|
|
159
197
|
strategy: EncryptionStrategy,
|
|
160
198
|
key_storage: EncryptionKeyStorageStrategy,
|
|
161
199
|
) -> Self:
|
|
200
|
+
"""
|
|
201
|
+
Configures event encryption with the provided strategy and key storage.
|
|
202
|
+
"""
|
|
162
203
|
self[EncryptionStrategy] = strategy
|
|
163
204
|
self[EncryptionKeyStorageStrategy] = lambda c: key_storage.scoped_for_tenant(
|
|
164
205
|
c[TenantId],
|
|
@@ -167,6 +208,16 @@ class Backend(_Container):
|
|
|
167
208
|
|
|
168
209
|
|
|
169
210
|
class TransactionalBackend(Backend):
|
|
211
|
+
"""
|
|
212
|
+
Backend variant that provides transactional event handling support.
|
|
213
|
+
|
|
214
|
+
Registers additional services for managing listeners and dispatchers
|
|
215
|
+
to enable in-transaction event processing.
|
|
216
|
+
|
|
217
|
+
Properties:
|
|
218
|
+
in_transaction (Listeners): The current state of in transaction Listeners.
|
|
219
|
+
"""
|
|
220
|
+
|
|
170
221
|
def __init__(self) -> None:
|
|
171
222
|
super().__init__()
|
|
172
223
|
self[Listeners] = singleton(lambda _: Listeners())
|
|
@@ -174,4 +225,7 @@ class TransactionalBackend(Backend):
|
|
|
174
225
|
|
|
175
226
|
@property
|
|
176
227
|
def in_transaction(self) -> Listeners:
|
|
228
|
+
"""
|
|
229
|
+
Returns the current instance of `Listeners` for transactional event handling.
|
|
230
|
+
"""
|
|
177
231
|
return cast(Listeners, self[Listeners])
|
|
@@ -6,14 +6,30 @@ from uuid import UUID, uuid4
|
|
|
6
6
|
|
|
7
7
|
from pydantic import BaseModel
|
|
8
8
|
|
|
9
|
-
from event_sourcery.
|
|
10
|
-
from event_sourcery.
|
|
9
|
+
from event_sourcery._event_store.stream_id import StreamId
|
|
10
|
+
from event_sourcery._event_store.tenant_id import DEFAULT_TENANT, TenantId
|
|
11
11
|
|
|
12
12
|
DataSubject = object()
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
@dataclasses.dataclass(frozen=True)
|
|
16
16
|
class RawEvent(BaseModel):
|
|
17
|
+
"""
|
|
18
|
+
Data Transfer Object representing a raw event that will be stored.
|
|
19
|
+
|
|
20
|
+
Contains all event metadata and payload in a flat structure.
|
|
21
|
+
Used for low-level event store operations, serialization, and transport.
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
uuid (UUID): Unique identifier of the event.
|
|
25
|
+
stream_id (StreamId): Identifier of the stream to which the event belongs.
|
|
26
|
+
created_at (datetime): Timestamp when the event was created.
|
|
27
|
+
name (str): Name/type of the event.
|
|
28
|
+
data (dict[str, Any]): Event payload (fields and values).
|
|
29
|
+
context (dict[str, Any]): Event custom metadata.
|
|
30
|
+
version (int | None): Version of the event in the stream (if applicable).
|
|
31
|
+
"""
|
|
32
|
+
|
|
17
33
|
uuid: UUID
|
|
18
34
|
stream_id: StreamId
|
|
19
35
|
created_at: datetime
|
|
@@ -28,6 +44,18 @@ Position: TypeAlias = int
|
|
|
28
44
|
|
|
29
45
|
@dataclasses.dataclass(frozen=True)
|
|
30
46
|
class RecordedRaw(BaseModel):
|
|
47
|
+
"""
|
|
48
|
+
DTO representing a raw event received from the event store.
|
|
49
|
+
|
|
50
|
+
Used for low-level event store operations.
|
|
51
|
+
Wraps a RawEvent with additional metadata (position and tenant id).
|
|
52
|
+
|
|
53
|
+
Attributes:
|
|
54
|
+
entry (RawEvent): The raw event data and metadata.
|
|
55
|
+
position (Position): Position of the event in the event store.
|
|
56
|
+
tenant_id (TenantId): Tenant identifier (for multi-tenant scenarios).
|
|
57
|
+
"""
|
|
58
|
+
|
|
31
59
|
entry: RawEvent
|
|
32
60
|
position: Position
|
|
33
61
|
tenant_id: TenantId = DEFAULT_TENANT
|
|
@@ -48,16 +76,38 @@ TEvent = TypeVar("TEvent", bound=Event)
|
|
|
48
76
|
|
|
49
77
|
|
|
50
78
|
class Context(BaseModel, extra="allow"):
|
|
79
|
+
"""
|
|
80
|
+
Extensible context object for event metadata.
|
|
81
|
+
|
|
82
|
+
Designed to carry correlation and causation identifiers, and can be extended
|
|
83
|
+
with additional metadata fields as needed. This allows for flexible propagation
|
|
84
|
+
of context information throughout event processing and handling pipelines.
|
|
85
|
+
|
|
86
|
+
Attributes:
|
|
87
|
+
correlation_id (UUID | None): Identifier for correlating related events.
|
|
88
|
+
causation_id (UUID | None): Identifier for the cause of the event.
|
|
89
|
+
|
|
90
|
+
Additional fields can be added dynamically due to `extra="allow"`.
|
|
91
|
+
"""
|
|
92
|
+
|
|
51
93
|
correlation_id: UUID | None = None
|
|
52
94
|
causation_id: UUID | None = None
|
|
53
95
|
|
|
54
96
|
|
|
55
97
|
@dataclasses.dataclass()
|
|
56
98
|
class WrappedEvent(Generic[TEvent]):
|
|
57
|
-
"""
|
|
99
|
+
"""
|
|
100
|
+
Wrapper for events with all relevant metadata.
|
|
58
101
|
|
|
59
102
|
Returned from EventStore when loading events from a stream.
|
|
60
103
|
|
|
104
|
+
Attributes:
|
|
105
|
+
event (TEvent): The event instance.
|
|
106
|
+
version (int | None): Version of the event in the stream (if applicable).
|
|
107
|
+
uuid (UUID): Unique identifier of the event instance.
|
|
108
|
+
created_at (datetime): Timestamp when the event was created.
|
|
109
|
+
context (Context): Context object containing additional metadata.
|
|
110
|
+
|
|
61
111
|
Example usage:
|
|
62
112
|
```
|
|
63
113
|
class OrderCancelled(Event):
|
|
@@ -83,6 +133,17 @@ class WrappedEvent(Generic[TEvent]):
|
|
|
83
133
|
|
|
84
134
|
@dataclasses.dataclass(frozen=True)
|
|
85
135
|
class Entry:
|
|
136
|
+
"""
|
|
137
|
+
Container for a single event and its stream identifier.
|
|
138
|
+
|
|
139
|
+
Used as a base DTO for representing an event (with all metadata) and the stream
|
|
140
|
+
it belongs to. Serves as a base for more specialized event store DTOs (e.g. Recorded).
|
|
141
|
+
|
|
142
|
+
Attributes:
|
|
143
|
+
wrapped_event (WrappedEvent[Event]): The event instance with metadata.
|
|
144
|
+
stream_id (StreamId): The identifier of the stream to which the event belongs.
|
|
145
|
+
"""
|
|
146
|
+
|
|
86
147
|
wrapped_event: WrappedEvent[Event]
|
|
87
148
|
stream_id: StreamId
|
|
88
149
|
|