python-event-sourcery 0.4.0a16__tar.gz → 0.4.0a18__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.4.0a16 → python_event_sourcery-0.4.0a18}/PKG-INFO +3 -3
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery/event_store/__init__.py +5 -10
- python_event_sourcery-0.4.0a18/event_sourcery/event_store/backend.py +174 -0
- python_event_sourcery-0.4.0a18/event_sourcery/event_store/dispatcher.py +64 -0
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery/event_store/event/__init__.py +7 -1
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery/event_store/event/_encryption.py +4 -10
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery/event_store/event/_registry.py +3 -1
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery/event_store/event/_serde.py +2 -9
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery/event_store/event_store.py +0 -19
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery/event_store/in_memory.py +33 -86
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery/event_store/interfaces.py +2 -1
- python_event_sourcery-0.4.0a18/event_sourcery_django/__init__.py +61 -0
- python_event_sourcery-0.4.0a18/event_sourcery_kurrentdb/__init__.py +69 -0
- {python_event_sourcery-0.4.0a16/event_sourcery_esdb → python_event_sourcery-0.4.0a18/event_sourcery_kurrentdb}/dto.py +2 -2
- {python_event_sourcery-0.4.0a16/event_sourcery_esdb → python_event_sourcery-0.4.0a18/event_sourcery_kurrentdb}/event_store.py +9 -9
- {python_event_sourcery-0.4.0a16/event_sourcery_esdb → python_event_sourcery-0.4.0a18/event_sourcery_kurrentdb}/outbox.py +8 -8
- {python_event_sourcery-0.4.0a16/event_sourcery_esdb → python_event_sourcery-0.4.0a18/event_sourcery_kurrentdb}/stream.py +4 -2
- {python_event_sourcery-0.4.0a16/event_sourcery_esdb → python_event_sourcery-0.4.0a18/event_sourcery_kurrentdb}/subscription.py +6 -6
- python_event_sourcery-0.4.0a18/event_sourcery_sqlalchemy/__init__.py +71 -0
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/pyproject.toml +5 -5
- python_event_sourcery-0.4.0a16/event_sourcery/event_store/dispatcher.py +0 -41
- python_event_sourcery-0.4.0a16/event_sourcery/event_store/factory.py +0 -67
- python_event_sourcery-0.4.0a16/event_sourcery_django/__init__.py +0 -97
- python_event_sourcery-0.4.0a16/event_sourcery_esdb/__init__.py +0 -105
- python_event_sourcery-0.4.0a16/event_sourcery_sqlalchemy/__init__.py +0 -107
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/LICENSE +0 -0
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery/__init__.py +0 -0
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery/event_sourcing/__init__.py +0 -0
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery/event_sourcing/aggregate.py +0 -0
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery/event_sourcing/repository.py +0 -0
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery/event_store/event/_dto.py +0 -0
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery/event_store/exceptions.py +0 -0
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery/event_store/outbox.py +0 -0
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery/event_store/stream_id.py +0 -0
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery/event_store/subscription.py +0 -0
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery/event_store/tenant_id.py +0 -0
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery/event_store/versioning.py +0 -0
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery/py.typed +0 -0
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery/read_model/__init__.py +0 -0
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery/read_model/cursors_dao.py +0 -0
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery/read_model/projector.py +0 -0
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery_django/dto.py +0 -0
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery_django/event_store.py +0 -0
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery_django/migrations/0001_initial.py +0 -0
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery_django/migrations/__init__.py +0 -0
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery_django/models.py +0 -0
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery_django/outbox.py +0 -0
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery_django/py.typed +0 -0
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery_django/subscription.py +0 -0
- {python_event_sourcery-0.4.0a16/event_sourcery_esdb → python_event_sourcery-0.4.0a18/event_sourcery_kurrentdb}/py.typed +0 -0
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery_sqlalchemy/cursors_dao.py +0 -0
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery_sqlalchemy/dto.py +0 -0
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery_sqlalchemy/event_store.py +0 -0
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery_sqlalchemy/guid.py +0 -0
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery_sqlalchemy/jsonb.py +0 -0
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery_sqlalchemy/models.py +0 -0
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery_sqlalchemy/outbox.py +0 -0
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery_sqlalchemy/py.typed +0 -0
- {python_event_sourcery-0.4.0a16 → python_event_sourcery-0.4.0a18}/event_sourcery_sqlalchemy/subscription.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: python-event-sourcery
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.0a18
|
|
4
4
|
Summary:
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: Your Name
|
|
@@ -13,11 +13,11 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
15
|
Provides-Extra: django
|
|
16
|
-
Provides-Extra:
|
|
16
|
+
Provides-Extra: kurrentdb
|
|
17
17
|
Provides-Extra: sqlalchemy
|
|
18
18
|
Requires-Dist: SQLAlchemy (>=2.0) ; extra == "sqlalchemy"
|
|
19
19
|
Requires-Dist: django (>=4,<6) ; extra == "django"
|
|
20
|
-
Requires-Dist:
|
|
20
|
+
Requires-Dist: kurrentdbclient (>=1.0.6) ; extra == "kurrentdb"
|
|
21
21
|
Requires-Dist: more-itertools
|
|
22
22
|
Requires-Dist: psycopg2-binary (>2,<3)
|
|
23
23
|
Requires-Dist: pydantic (>=2.0,<3)
|
|
@@ -2,7 +2,6 @@ __all__ = [
|
|
|
2
2
|
"DEFAULT_TENANT",
|
|
3
3
|
"NO_VERSIONING",
|
|
4
4
|
"Backend",
|
|
5
|
-
"BackendFactory",
|
|
6
5
|
"Context",
|
|
7
6
|
"Dispatcher",
|
|
8
7
|
"Entry",
|
|
@@ -10,7 +9,7 @@ __all__ = [
|
|
|
10
9
|
"EventRegistry",
|
|
11
10
|
"EventStore",
|
|
12
11
|
"ExplicitVersioning",
|
|
13
|
-
"
|
|
12
|
+
"InMemoryBackend",
|
|
14
13
|
"Listener",
|
|
15
14
|
"Position",
|
|
16
15
|
"RawEvent",
|
|
@@ -22,13 +21,14 @@ __all__ = [
|
|
|
22
21
|
"TransactionalBackend",
|
|
23
22
|
"Versioning",
|
|
24
23
|
"WrappedEvent",
|
|
24
|
+
"backend",
|
|
25
25
|
"exceptions",
|
|
26
|
-
"factory",
|
|
27
26
|
"interfaces",
|
|
28
27
|
"subscription",
|
|
29
28
|
]
|
|
30
29
|
|
|
31
|
-
from event_sourcery.event_store import
|
|
30
|
+
from event_sourcery.event_store import backend, exceptions, interfaces, subscription
|
|
31
|
+
from event_sourcery.event_store.backend import Backend, TransactionalBackend
|
|
32
32
|
from event_sourcery.event_store.dispatcher import Dispatcher, Listener
|
|
33
33
|
from event_sourcery.event_store.event import (
|
|
34
34
|
Context,
|
|
@@ -42,12 +42,7 @@ from event_sourcery.event_store.event import (
|
|
|
42
42
|
WrappedEvent,
|
|
43
43
|
)
|
|
44
44
|
from event_sourcery.event_store.event_store import EventStore
|
|
45
|
-
from event_sourcery.event_store.
|
|
46
|
-
Backend,
|
|
47
|
-
BackendFactory,
|
|
48
|
-
TransactionalBackend,
|
|
49
|
-
)
|
|
50
|
-
from event_sourcery.event_store.in_memory import InMemoryBackendFactory
|
|
45
|
+
from event_sourcery.event_store.in_memory import InMemoryBackend
|
|
51
46
|
from event_sourcery.event_store.stream_id import StreamId, StreamUUID
|
|
52
47
|
from event_sourcery.event_store.tenant_id import DEFAULT_TENANT, TenantId
|
|
53
48
|
from event_sourcery.event_store.versioning import (
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
from collections import UserDict
|
|
3
|
+
from collections.abc import Iterator
|
|
4
|
+
from contextlib import AbstractContextManager
|
|
5
|
+
from functools import wraps
|
|
6
|
+
from typing import NoReturn, Protocol, TypeVar, cast
|
|
7
|
+
|
|
8
|
+
from typing_extensions import Self
|
|
9
|
+
|
|
10
|
+
from event_sourcery.event_store import subscription
|
|
11
|
+
from event_sourcery.event_store.dispatcher import Dispatcher, Listeners
|
|
12
|
+
from event_sourcery.event_store.event import (
|
|
13
|
+
Encryption,
|
|
14
|
+
EventRegistry,
|
|
15
|
+
NoEncryptionStrategy,
|
|
16
|
+
NoKeyStorageStrategy,
|
|
17
|
+
RawEvent,
|
|
18
|
+
RecordedRaw,
|
|
19
|
+
Serde,
|
|
20
|
+
)
|
|
21
|
+
from event_sourcery.event_store.event_store import EventStore
|
|
22
|
+
from event_sourcery.event_store.interfaces import (
|
|
23
|
+
EncryptionKeyStorageStrategy,
|
|
24
|
+
EncryptionStrategy,
|
|
25
|
+
OutboxFiltererStrategy,
|
|
26
|
+
OutboxStorageStrategy,
|
|
27
|
+
StorageStrategy,
|
|
28
|
+
SubscriptionStrategy,
|
|
29
|
+
)
|
|
30
|
+
from event_sourcery.event_store.outbox import Outbox
|
|
31
|
+
from event_sourcery.event_store.tenant_id import DEFAULT_TENANT, TenantId
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class NoProviderConfigured(Exception):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def no_filter(entry: RawEvent) -> bool:
|
|
39
|
+
return True
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class NoOutboxStorageStrategy(OutboxStorageStrategy):
|
|
43
|
+
def outbox_entries(
|
|
44
|
+
self, limit: int
|
|
45
|
+
) -> Iterator[AbstractContextManager[RecordedRaw]]:
|
|
46
|
+
return iter([])
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
T_co = TypeVar("T_co", covariant=True)
|
|
50
|
+
T = TypeVar("T")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class _Provider(Protocol[T_co]):
|
|
54
|
+
def __call__(self, container: "_Container") -> T_co: ...
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def singleton(provider: _Provider[T]) -> _Provider[T]:
|
|
58
|
+
result: T | None = None
|
|
59
|
+
|
|
60
|
+
@wraps(provider)
|
|
61
|
+
def _wrapper(container: "_Container") -> T:
|
|
62
|
+
nonlocal result
|
|
63
|
+
if result is not None:
|
|
64
|
+
return result
|
|
65
|
+
result = provider(container)
|
|
66
|
+
return result
|
|
67
|
+
|
|
68
|
+
return _wrapper
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def not_configured(error_message: str) -> _Provider[T]:
|
|
72
|
+
def _raise(container: "_Container") -> NoReturn:
|
|
73
|
+
raise NoProviderConfigured(error_message) # pragma: no cover
|
|
74
|
+
|
|
75
|
+
return _raise
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class _Container(UserDict[type[T], _Provider[T] | T]):
|
|
79
|
+
def __getitem__(self, _type: type[T]) -> T:
|
|
80
|
+
provider = cast(_Provider[T], self.data[_type])
|
|
81
|
+
return provider(self)
|
|
82
|
+
|
|
83
|
+
def __setitem__(self, _type: type[T], value: T | _Provider[T]) -> None:
|
|
84
|
+
self.data[_type] = (
|
|
85
|
+
cast(_Provider[T], lambda _: value) if isinstance(value, _type) else value
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def copy(self) -> Self:
|
|
89
|
+
new: Self = self.__class__()
|
|
90
|
+
new.data.update(self.data.copy())
|
|
91
|
+
return new
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class Backend(_Container):
|
|
95
|
+
tenant_id: TenantId
|
|
96
|
+
|
|
97
|
+
def __init__(self) -> None:
|
|
98
|
+
super().__init__()
|
|
99
|
+
self.tenant_id = DEFAULT_TENANT
|
|
100
|
+
self[EventRegistry] = lambda _: EventRegistry()
|
|
101
|
+
self[EncryptionStrategy] = lambda _: NoEncryptionStrategy()
|
|
102
|
+
self[EncryptionKeyStorageStrategy] = (
|
|
103
|
+
lambda c: NoKeyStorageStrategy().scoped_for_tenant(c.tenant_id)
|
|
104
|
+
)
|
|
105
|
+
self[Encryption] = lambda c: Encryption(
|
|
106
|
+
registry=c[EventRegistry],
|
|
107
|
+
strategy=c[EncryptionStrategy],
|
|
108
|
+
key_storage=c[EncryptionKeyStorageStrategy],
|
|
109
|
+
)
|
|
110
|
+
self[Serde] = lambda c: Serde(
|
|
111
|
+
registry=c[EventRegistry],
|
|
112
|
+
encryption=c[Encryption],
|
|
113
|
+
)
|
|
114
|
+
self[StorageStrategy] = not_configured(
|
|
115
|
+
"Use one of pyES backends: SQLAlchemy, Django or KurrentDB",
|
|
116
|
+
)
|
|
117
|
+
self[EventStore] = lambda c: EventStore(
|
|
118
|
+
storage_strategy=c[StorageStrategy],
|
|
119
|
+
serde=c[Serde],
|
|
120
|
+
)
|
|
121
|
+
self[Outbox] = lambda c: Outbox(
|
|
122
|
+
strategy=c[OutboxStorageStrategy],
|
|
123
|
+
serde=c[Serde],
|
|
124
|
+
)
|
|
125
|
+
self[OutboxStorageStrategy] = lambda _: NoOutboxStorageStrategy()
|
|
126
|
+
self[SubscriptionStrategy] = not_configured(
|
|
127
|
+
"Use one of pyES backends: SQLAlchemy, Django or KurrentDB",
|
|
128
|
+
)
|
|
129
|
+
self[subscription.PositionPhase] = lambda c: subscription.SubscriptionBuilder(
|
|
130
|
+
c[Serde],
|
|
131
|
+
c[SubscriptionStrategy],
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def event_store(self) -> EventStore:
|
|
136
|
+
return cast(EventStore, self[EventStore])
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def outbox(self) -> Outbox:
|
|
140
|
+
return cast(Outbox, self[Outbox])
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def subscriber(self) -> subscription.PositionPhase:
|
|
144
|
+
return cast(subscription.PositionPhase, self[subscription.PositionPhase])
|
|
145
|
+
|
|
146
|
+
def in_tenant_mode(self, tenant_id: TenantId) -> Self:
|
|
147
|
+
in_tenant_mode = self.copy()
|
|
148
|
+
in_tenant_mode.tenant_id = tenant_id
|
|
149
|
+
return in_tenant_mode
|
|
150
|
+
|
|
151
|
+
@abc.abstractmethod
|
|
152
|
+
def with_outbox(self, filterer: OutboxFiltererStrategy = no_filter) -> Self: ...
|
|
153
|
+
|
|
154
|
+
def with_encryption(
|
|
155
|
+
self,
|
|
156
|
+
strategy: EncryptionStrategy,
|
|
157
|
+
key_storage: EncryptionKeyStorageStrategy,
|
|
158
|
+
) -> Self:
|
|
159
|
+
self[EncryptionStrategy] = strategy
|
|
160
|
+
self[EncryptionKeyStorageStrategy] = lambda c: key_storage.scoped_for_tenant(
|
|
161
|
+
c.tenant_id
|
|
162
|
+
)
|
|
163
|
+
return self
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class TransactionalBackend(Backend, abc.ABC):
|
|
167
|
+
def __init__(self) -> None:
|
|
168
|
+
super().__init__()
|
|
169
|
+
self[Listeners] = singleton(lambda _: Listeners())
|
|
170
|
+
self[Dispatcher] = lambda c: Dispatcher(c[Serde], c[Listeners])
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def in_transaction(self) -> Listeners:
|
|
174
|
+
return cast(Listeners, self[Listeners])
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from collections.abc import Iterator
|
|
2
|
+
from itertools import chain
|
|
3
|
+
from typing import Protocol
|
|
4
|
+
|
|
5
|
+
from event_sourcery.event_store.event import (
|
|
6
|
+
Event,
|
|
7
|
+
Position,
|
|
8
|
+
RecordedRaw,
|
|
9
|
+
Serde,
|
|
10
|
+
WrappedEvent,
|
|
11
|
+
)
|
|
12
|
+
from event_sourcery.event_store.stream_id import StreamId
|
|
13
|
+
from event_sourcery.event_store.tenant_id import TenantId
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Listener(Protocol):
|
|
17
|
+
def __call__(
|
|
18
|
+
self,
|
|
19
|
+
wrapped_event: WrappedEvent,
|
|
20
|
+
stream_id: StreamId,
|
|
21
|
+
tenant_id: TenantId,
|
|
22
|
+
position: Position,
|
|
23
|
+
) -> None: ...
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Listeners:
|
|
27
|
+
def __init__(self) -> None:
|
|
28
|
+
self._listeners: dict[type[Event], set[Listener]] = {}
|
|
29
|
+
|
|
30
|
+
def __getitem__(self, event_type: type[Event]) -> Iterator[Listener]:
|
|
31
|
+
return chain(
|
|
32
|
+
*(
|
|
33
|
+
listeners
|
|
34
|
+
for registered_to, listeners in self._listeners.items()
|
|
35
|
+
if issubclass(event_type, registered_to)
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def register(self, listener: Listener, to: type[Event]) -> None:
|
|
40
|
+
if to not in self._listeners:
|
|
41
|
+
self._listeners[to] = set()
|
|
42
|
+
self._listeners[to].add(listener)
|
|
43
|
+
|
|
44
|
+
def remove(self, listener: Listener, to: type[Event]) -> None:
|
|
45
|
+
if to in self._listeners and listener in self._listeners[to]:
|
|
46
|
+
self._listeners[to].remove(listener)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Dispatcher:
|
|
50
|
+
def __init__(self, serde: Serde, listeners: Listeners) -> None:
|
|
51
|
+
self._serde = serde
|
|
52
|
+
self._listeners = listeners
|
|
53
|
+
|
|
54
|
+
def dispatch(self, *raws: RecordedRaw) -> None:
|
|
55
|
+
for raw in raws:
|
|
56
|
+
record = self._serde.deserialize_record(raw)
|
|
57
|
+
event_type = record.wrapped_event.event.__class__
|
|
58
|
+
for listener in self._listeners[event_type]:
|
|
59
|
+
listener(
|
|
60
|
+
record.wrapped_event,
|
|
61
|
+
record.stream_id,
|
|
62
|
+
record.tenant_id,
|
|
63
|
+
record.position,
|
|
64
|
+
)
|
|
@@ -6,6 +6,8 @@ __all__ = [
|
|
|
6
6
|
"Entry",
|
|
7
7
|
"Event",
|
|
8
8
|
"EventRegistry",
|
|
9
|
+
"NoEncryptionStrategy",
|
|
10
|
+
"NoKeyStorageStrategy",
|
|
9
11
|
"Position",
|
|
10
12
|
"RawEvent",
|
|
11
13
|
"Recorded",
|
|
@@ -27,6 +29,10 @@ from event_sourcery.event_store.event._dto import (
|
|
|
27
29
|
RecordedRaw,
|
|
28
30
|
WrappedEvent,
|
|
29
31
|
)
|
|
30
|
-
from event_sourcery.event_store.event._encryption import
|
|
32
|
+
from event_sourcery.event_store.event._encryption import (
|
|
33
|
+
Encryption,
|
|
34
|
+
NoEncryptionStrategy,
|
|
35
|
+
NoKeyStorageStrategy,
|
|
36
|
+
)
|
|
31
37
|
from event_sourcery.event_store.event._registry import EventRegistry
|
|
32
38
|
from event_sourcery.event_store.event._serde import Serde
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
from collections import UserDict
|
|
3
|
-
from dataclasses import dataclass
|
|
3
|
+
from dataclasses import dataclass
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
6
|
from pydantic import BaseModel
|
|
@@ -68,11 +68,9 @@ class Encryption:
|
|
|
68
68
|
- Processing event encryption/decryption with field annotations
|
|
69
69
|
"""
|
|
70
70
|
|
|
71
|
-
registry: EventRegistry
|
|
72
|
-
strategy: EncryptionStrategy
|
|
73
|
-
key_storage: EncryptionKeyStorageStrategy
|
|
74
|
-
default_factory=NoKeyStorageStrategy
|
|
75
|
-
)
|
|
71
|
+
registry: EventRegistry
|
|
72
|
+
strategy: EncryptionStrategy
|
|
73
|
+
key_storage: EncryptionKeyStorageStrategy
|
|
76
74
|
|
|
77
75
|
def encrypt(self, event: BaseModel, stream_id: StreamId) -> dict[str, Any]:
|
|
78
76
|
event_type = type(event)
|
|
@@ -111,7 +109,6 @@ class Encryption:
|
|
|
111
109
|
if key is None:
|
|
112
110
|
return mask_value
|
|
113
111
|
decrypted = self.strategy.decrypt(value, key)
|
|
114
|
-
|
|
115
112
|
try:
|
|
116
113
|
return json.loads(decrypted)
|
|
117
114
|
except (json.JSONDecodeError, TypeError):
|
|
@@ -130,6 +127,3 @@ class Encryption:
|
|
|
130
127
|
|
|
131
128
|
def shred(self, subject_id: str) -> None:
|
|
132
129
|
self.key_storage.delete(subject_id)
|
|
133
|
-
|
|
134
|
-
def scoped_for_tenant(self, tenant_id: TenantId) -> Self:
|
|
135
|
-
return replace(self, key_storage=self.key_storage.scoped_for_tenant(tenant_id))
|
|
@@ -67,7 +67,9 @@ class EventRegistry:
|
|
|
67
67
|
|
|
68
68
|
def _register_defined_events(self) -> None:
|
|
69
69
|
for event_type in Event.__subclasses__():
|
|
70
|
-
event_type not in self._types_to_names
|
|
70
|
+
not_registered_type = event_type not in self._types_to_names
|
|
71
|
+
not_registered_name = event_name(event_type) not in self._names_to_types
|
|
72
|
+
not_registered_type and not_registered_name and self.add(event_type)
|
|
71
73
|
|
|
72
74
|
def add(self, event: type[TEvent]) -> type[TEvent]:
|
|
73
75
|
"""Add event subclass to the registry."""
|
|
@@ -12,20 +12,16 @@ from event_sourcery.event_store.event._dto import (
|
|
|
12
12
|
from event_sourcery.event_store.event._encryption import Encryption
|
|
13
13
|
from event_sourcery.event_store.event._registry import EventRegistry
|
|
14
14
|
from event_sourcery.event_store.stream_id import StreamId
|
|
15
|
-
from event_sourcery.event_store.tenant_id import TenantId
|
|
16
15
|
|
|
17
16
|
|
|
18
17
|
class Serde:
|
|
19
|
-
registry: EventRegistry
|
|
20
|
-
encryption: Encryption
|
|
21
|
-
|
|
22
18
|
def __init__(
|
|
23
19
|
self,
|
|
24
20
|
registry: EventRegistry,
|
|
25
|
-
encryption: Encryption
|
|
21
|
+
encryption: Encryption,
|
|
26
22
|
) -> None:
|
|
27
23
|
self.registry = registry
|
|
28
|
-
self.encryption = encryption
|
|
24
|
+
self.encryption = encryption
|
|
29
25
|
|
|
30
26
|
def deserialize(self, event: RawEvent) -> WrappedEvent:
|
|
31
27
|
event_as_dict = dataclasses.asdict(event)
|
|
@@ -77,6 +73,3 @@ class Serde:
|
|
|
77
73
|
self, events: Sequence[WrappedEvent], stream_id: StreamId
|
|
78
74
|
) -> list[RawEvent]:
|
|
79
75
|
return [self.serialize(event, stream_id) for event in events]
|
|
80
|
-
|
|
81
|
-
def scoped_for_tenant(self, tenant_id: TenantId) -> "Serde":
|
|
82
|
-
return Serde(self.registry, self.encryption.scoped_for_tenant(tenant_id))
|
|
@@ -10,7 +10,6 @@ from event_sourcery.event_store.event import (
|
|
|
10
10
|
)
|
|
11
11
|
from event_sourcery.event_store.interfaces import StorageStrategy
|
|
12
12
|
from event_sourcery.event_store.stream_id import StreamId
|
|
13
|
-
from event_sourcery.event_store.tenant_id import DEFAULT_TENANT, TenantId
|
|
14
13
|
from event_sourcery.event_store.versioning import (
|
|
15
14
|
NO_VERSIONING,
|
|
16
15
|
ExplicitVersioning,
|
|
@@ -190,21 +189,3 @@ class EventStore:
|
|
|
190
189
|
|
|
191
190
|
"""
|
|
192
191
|
return self._storage_strategy.current_position
|
|
193
|
-
|
|
194
|
-
def scoped_for_tenant(self, tenant_id: TenantId = DEFAULT_TENANT) -> "EventStore":
|
|
195
|
-
"""Factory method to create a new event store instance scoped to a tenant.
|
|
196
|
-
|
|
197
|
-
Examples:
|
|
198
|
-
>>> event_store.scoped_for_tenant("tenant_1")
|
|
199
|
-
<EventStore ...>
|
|
200
|
-
|
|
201
|
-
Args:
|
|
202
|
-
tenant_id: The tenant identifier to work with.
|
|
203
|
-
|
|
204
|
-
Returns:
|
|
205
|
-
An event store instance scoped to the tenant.
|
|
206
|
-
"""
|
|
207
|
-
return EventStore(
|
|
208
|
-
storage_strategy=self._storage_strategy.scoped_for_tenant(tenant_id),
|
|
209
|
-
serde=self._serde.scoped_for_tenant(tenant_id),
|
|
210
|
-
)
|
|
@@ -2,42 +2,33 @@ import time
|
|
|
2
2
|
from collections.abc import Generator, Iterator
|
|
3
3
|
from contextlib import AbstractContextManager, contextmanager
|
|
4
4
|
from copy import copy
|
|
5
|
-
from dataclasses import dataclass, field
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
6
|
from datetime import timedelta
|
|
7
7
|
from operator import getitem
|
|
8
8
|
|
|
9
9
|
from pydantic import BaseModel, ConfigDict, PositiveInt
|
|
10
10
|
from typing_extensions import Self
|
|
11
11
|
|
|
12
|
-
from event_sourcery.event_store import
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
from event_sourcery.event_store import Dispatcher
|
|
13
|
+
from event_sourcery.event_store.backend import (
|
|
14
|
+
TransactionalBackend,
|
|
15
|
+
no_filter,
|
|
16
|
+
not_configured,
|
|
17
|
+
singleton,
|
|
17
18
|
)
|
|
18
19
|
from event_sourcery.event_store.event import (
|
|
19
|
-
Encryption,
|
|
20
20
|
Position,
|
|
21
21
|
RawEvent,
|
|
22
22
|
RecordedRaw,
|
|
23
|
-
Serde,
|
|
24
23
|
)
|
|
25
24
|
from event_sourcery.event_store.exceptions import ConcurrentStreamWriteError
|
|
26
|
-
from event_sourcery.event_store.factory import (
|
|
27
|
-
BackendFactory,
|
|
28
|
-
NoOutboxStorageStrategy,
|
|
29
|
-
TransactionalBackend,
|
|
30
|
-
no_filter,
|
|
31
|
-
)
|
|
32
25
|
from event_sourcery.event_store.interfaces import (
|
|
33
26
|
EncryptionKeyStorageStrategy,
|
|
34
|
-
EncryptionStrategy,
|
|
35
27
|
OutboxFiltererStrategy,
|
|
36
28
|
OutboxStorageStrategy,
|
|
37
29
|
StorageStrategy,
|
|
38
30
|
SubscriptionStrategy,
|
|
39
31
|
)
|
|
40
|
-
from event_sourcery.event_store.outbox import Outbox
|
|
41
32
|
from event_sourcery.event_store.stream_id import StreamId
|
|
42
33
|
from event_sourcery.event_store.tenant_id import DEFAULT_TENANT, TenantId
|
|
43
34
|
from event_sourcery.event_store.versioning import NO_VERSIONING, Versioning
|
|
@@ -228,13 +219,12 @@ class InMemoryStorageStrategy(StorageStrategy):
|
|
|
228
219
|
storage: Storage,
|
|
229
220
|
dispatcher: Dispatcher,
|
|
230
221
|
outbox_strategy: InMemoryOutboxStorageStrategy | None,
|
|
231
|
-
tenant_id: TenantId = DEFAULT_TENANT,
|
|
232
222
|
) -> None:
|
|
233
223
|
self._names: dict[str | None, str] = {}
|
|
234
224
|
self._storage = storage
|
|
235
225
|
self._dispatcher = dispatcher
|
|
236
226
|
self._outbox = outbox_strategy
|
|
237
|
-
self._tenant_id =
|
|
227
|
+
self._tenant_id = DEFAULT_TENANT
|
|
238
228
|
|
|
239
229
|
def fetch_events(
|
|
240
230
|
self,
|
|
@@ -300,85 +290,42 @@ class InMemoryStorageStrategy(StorageStrategy):
|
|
|
300
290
|
return current_position and Position(current_position)
|
|
301
291
|
|
|
302
292
|
def scoped_for_tenant(self, tenant_id: TenantId) -> Self:
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
dispatcher=self._dispatcher,
|
|
306
|
-
outbox_strategy=self._outbox,
|
|
307
|
-
tenant_id=tenant_id,
|
|
308
|
-
)
|
|
293
|
+
self._tenant_id = tenant_id
|
|
294
|
+
return self
|
|
309
295
|
|
|
310
296
|
|
|
311
297
|
class Config(BaseModel):
|
|
312
298
|
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
313
|
-
|
|
314
299
|
outbox_attempts: PositiveInt = 3
|
|
315
300
|
|
|
316
301
|
|
|
317
|
-
|
|
318
|
-
class InMemoryBackendFactory(BackendFactory):
|
|
302
|
+
class InMemoryBackend(TransactionalBackend):
|
|
319
303
|
"""Lightweight in-memory backend factory for testing and development."""
|
|
320
304
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
backend.in_transaction = Dispatcher(backend.serde)
|
|
335
|
-
backend.event_store = EventStore(
|
|
336
|
-
InMemoryStorageStrategy(
|
|
337
|
-
self._storage,
|
|
338
|
-
backend.in_transaction,
|
|
339
|
-
self._outbox_strategy,
|
|
340
|
-
),
|
|
341
|
-
backend.serde,
|
|
342
|
-
)
|
|
343
|
-
backend.outbox = Outbox(
|
|
344
|
-
self._outbox_strategy or NoOutboxStorageStrategy(),
|
|
345
|
-
backend.serde,
|
|
346
|
-
)
|
|
347
|
-
backend.subscriber = subscription.SubscriptionBuilder(
|
|
348
|
-
_serde=backend.serde,
|
|
349
|
-
_strategy=self._subscription_strategy,
|
|
350
|
-
)
|
|
351
|
-
return backend
|
|
352
|
-
|
|
353
|
-
def with_event_registry(self, event_registry: EventRegistry) -> Self:
|
|
354
|
-
self.serde = Serde(event_registry)
|
|
305
|
+
def __init__(self) -> None:
|
|
306
|
+
super().__init__()
|
|
307
|
+
self[Config] = not_configured("Configure backend with `.configure(config)`")
|
|
308
|
+
self[Storage] = Storage()
|
|
309
|
+
self[StorageStrategy] = lambda c: InMemoryStorageStrategy(
|
|
310
|
+
c[Storage],
|
|
311
|
+
c[Dispatcher],
|
|
312
|
+
outbox_strategy=c.get(InMemoryOutboxStorageStrategy),
|
|
313
|
+
).scoped_for_tenant(c.tenant_id)
|
|
314
|
+
self[SubscriptionStrategy] = lambda c: InMemorySubscriptionStrategy(c[Storage])
|
|
315
|
+
|
|
316
|
+
def configure(self, config: Config | None = None) -> Self:
|
|
317
|
+
self[Config] = config or Config()
|
|
355
318
|
return self
|
|
356
319
|
|
|
357
320
|
def with_outbox(self, filterer: OutboxFiltererStrategy = no_filter) -> Self:
|
|
358
|
-
self
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
def without_outbox(self, filterer: OutboxFiltererStrategy = no_filter) -> Self:
|
|
365
|
-
self._outbox_strategy = None
|
|
366
|
-
return self
|
|
367
|
-
|
|
368
|
-
def with_encryption(
|
|
369
|
-
self,
|
|
370
|
-
strategy: EncryptionStrategy,
|
|
371
|
-
key_storage: EncryptionKeyStorageStrategy,
|
|
372
|
-
) -> Self:
|
|
373
|
-
registry = self.serde.registry
|
|
374
|
-
self.serde = Serde(
|
|
375
|
-
registry,
|
|
376
|
-
encryption=Encryption(
|
|
377
|
-
registry=registry,
|
|
378
|
-
strategy=strategy,
|
|
379
|
-
key_storage=key_storage,
|
|
380
|
-
),
|
|
321
|
+
self[OutboxFiltererStrategy] = filterer
|
|
322
|
+
self[InMemoryOutboxStorageStrategy] = singleton(
|
|
323
|
+
lambda c: InMemoryOutboxStorageStrategy(
|
|
324
|
+
c[OutboxFiltererStrategy],
|
|
325
|
+
c[Config].outbox_attempts,
|
|
326
|
+
)
|
|
381
327
|
)
|
|
328
|
+
self[OutboxStorageStrategy] = lambda c: c[InMemoryOutboxStorageStrategy]
|
|
382
329
|
return self
|
|
383
330
|
|
|
384
331
|
|
|
@@ -396,5 +343,5 @@ class InMemoryKeyStorage(EncryptionKeyStorageStrategy):
|
|
|
396
343
|
def delete(self, subject_id: str) -> None:
|
|
397
344
|
self._keys.pop((self._tenant_id, subject_id), None)
|
|
398
345
|
|
|
399
|
-
def scoped_for_tenant(self, tenant_id: TenantId) ->
|
|
400
|
-
return
|
|
346
|
+
def scoped_for_tenant(self, tenant_id: TenantId) -> "InMemoryKeyStorage":
|
|
347
|
+
return InMemoryKeyStorage(self._keys, tenant_id)
|
|
@@ -2,7 +2,7 @@ import abc
|
|
|
2
2
|
from collections.abc import Iterator
|
|
3
3
|
from contextlib import AbstractContextManager
|
|
4
4
|
from datetime import timedelta
|
|
5
|
-
from typing import Any, Protocol
|
|
5
|
+
from typing import Any, Protocol, runtime_checkable
|
|
6
6
|
|
|
7
7
|
from typing_extensions import Self
|
|
8
8
|
|
|
@@ -12,6 +12,7 @@ from event_sourcery.event_store.tenant_id import TenantId
|
|
|
12
12
|
from event_sourcery.event_store.versioning import Versioning
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
@runtime_checkable
|
|
15
16
|
class OutboxFiltererStrategy(Protocol):
|
|
16
17
|
def __call__(self, entry: RawEvent) -> bool: ...
|
|
17
18
|
|