python-event-sourcery 0.4.0a9__tar.gz → 0.4.0a11__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.0a9 → python_event_sourcery-0.4.0a11}/PKG-INFO +1 -1
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery/event_store/__init__.py +2 -0
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery/event_store/dispatcher.py +8 -2
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery/event_store/event/dto.py +3 -0
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery/event_store/event/serde.py +1 -0
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery/event_store/event_store.py +19 -0
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery/event_store/exceptions.py +4 -0
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery/event_store/in_memory.py +44 -33
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery/event_store/interfaces.py +6 -0
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery/event_store/outbox.py +1 -0
- python_event_sourcery-0.4.0a11/event_sourcery/event_store/tenant_id.py +5 -0
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery_esdb/dto.py +4 -2
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery_esdb/event_store.py +13 -6
- python_event_sourcery-0.4.0a11/event_sourcery_esdb/stream.py +60 -0
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery_esdb/subscription.py +1 -3
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery_sqlalchemy/event_store.py +22 -7
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery_sqlalchemy/models.py +5 -2
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery_sqlalchemy/outbox.py +6 -1
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery_sqlalchemy/subscription.py +1 -0
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/pyproject.toml +1 -1
- python_event_sourcery-0.4.0a9/event_sourcery_esdb/stream.py +0 -65
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/LICENSE +0 -0
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery/__init__.py +0 -0
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery/aggregate/__init__.py +0 -0
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery/aggregate/aggregate.py +0 -0
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery/aggregate/repository.py +0 -0
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery/event_store/event/__init__.py +0 -0
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery/event_store/event/registry.py +0 -0
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery/event_store/factory.py +0 -0
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery/event_store/stream_id.py +0 -0
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery/event_store/subscription.py +0 -0
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery/event_store/versioning.py +0 -0
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery/py.typed +0 -0
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery/read_model/__init__.py +0 -0
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery/read_model/cursors_dao.py +0 -0
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery/read_model/projector.py +0 -0
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery_esdb/__init__.py +0 -0
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery_esdb/outbox.py +0 -0
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery_esdb/py.typed +0 -0
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery_sqlalchemy/__init__.py +0 -0
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery_sqlalchemy/cursors_dao.py +0 -0
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery_sqlalchemy/dto.py +0 -0
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery_sqlalchemy/guid.py +0 -0
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery_sqlalchemy/jsonb.py +0 -0
- {python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery_sqlalchemy/py.typed +0 -0
|
@@ -23,6 +23,7 @@ __all__ = [
|
|
|
23
23
|
"factory",
|
|
24
24
|
"interfaces",
|
|
25
25
|
"subscription",
|
|
26
|
+
"TenantId",
|
|
26
27
|
]
|
|
27
28
|
|
|
28
29
|
from event_sourcery.event_store import exceptions, factory, interfaces, subscription
|
|
@@ -45,6 +46,7 @@ from event_sourcery.event_store.factory import (
|
|
|
45
46
|
)
|
|
46
47
|
from event_sourcery.event_store.in_memory import InMemoryBackendFactory
|
|
47
48
|
from event_sourcery.event_store.stream_id import StreamId, StreamUUID
|
|
49
|
+
from event_sourcery.event_store.tenant_id import TenantId
|
|
48
50
|
from event_sourcery.event_store.versioning import (
|
|
49
51
|
NO_VERSIONING,
|
|
50
52
|
ExplicitVersioning,
|
|
@@ -9,8 +9,9 @@ from event_sourcery.event_store.event import (
|
|
|
9
9
|
WrappedEvent,
|
|
10
10
|
)
|
|
11
11
|
from event_sourcery.event_store.stream_id import StreamId
|
|
12
|
+
from event_sourcery.event_store.tenant_id import TenantId
|
|
12
13
|
|
|
13
|
-
Listener = Callable[[WrappedEvent, StreamId, Position | None], None]
|
|
14
|
+
Listener = Callable[[WrappedEvent, StreamId, TenantId, Position | None], None]
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
class Dispatcher:
|
|
@@ -25,7 +26,12 @@ class Dispatcher:
|
|
|
25
26
|
if not isinstance(record.wrapped_event.event, event_type):
|
|
26
27
|
continue
|
|
27
28
|
for listener in listeners:
|
|
28
|
-
listener(
|
|
29
|
+
listener(
|
|
30
|
+
record.wrapped_event,
|
|
31
|
+
record.stream_id,
|
|
32
|
+
record.tenant_id,
|
|
33
|
+
record.position,
|
|
34
|
+
)
|
|
29
35
|
|
|
30
36
|
def register(self, listener: Listener, to: type[Event]) -> None:
|
|
31
37
|
self._listeners[to].add(listener)
|
|
@@ -6,6 +6,7 @@ from pydantic import BaseModel, Field
|
|
|
6
6
|
|
|
7
7
|
from event_sourcery.event_store.event.registry import EventRegistry
|
|
8
8
|
from event_sourcery.event_store.stream_id import StreamId
|
|
9
|
+
from event_sourcery.event_store.tenant_id import DEFAULT_TENANT, TenantId
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
class RawEvent(BaseModel):
|
|
@@ -24,6 +25,7 @@ Position: TypeAlias = int
|
|
|
24
25
|
class RecordedRaw(BaseModel):
|
|
25
26
|
entry: RawEvent
|
|
26
27
|
position: Position
|
|
28
|
+
tenant_id: TenantId = DEFAULT_TENANT
|
|
27
29
|
|
|
28
30
|
|
|
29
31
|
class Event(BaseModel, extra="forbid"):
|
|
@@ -83,3 +85,4 @@ class Entry(BaseModel):
|
|
|
83
85
|
|
|
84
86
|
class Recorded(Entry):
|
|
85
87
|
position: Position
|
|
88
|
+
tenant_id: TenantId = DEFAULT_TENANT
|
|
@@ -10,6 +10,7 @@ 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
|
|
13
14
|
from event_sourcery.event_store.versioning import (
|
|
14
15
|
NO_VERSIONING,
|
|
15
16
|
ExplicitVersioning,
|
|
@@ -189,3 +190,21 @@ class EventStore:
|
|
|
189
190
|
|
|
190
191
|
"""
|
|
191
192
|
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,
|
|
210
|
+
)
|
|
@@ -32,18 +32,19 @@ from event_sourcery.event_store.interfaces import (
|
|
|
32
32
|
)
|
|
33
33
|
from event_sourcery.event_store.outbox import Outbox
|
|
34
34
|
from event_sourcery.event_store.stream_id import StreamId
|
|
35
|
+
from event_sourcery.event_store.tenant_id import DEFAULT_TENANT, TenantId
|
|
35
36
|
from event_sourcery.event_store.versioning import NO_VERSIONING, Versioning
|
|
36
37
|
|
|
37
38
|
|
|
38
39
|
@dataclass
|
|
39
40
|
class Storage:
|
|
40
|
-
|
|
41
|
-
_data: dict[StreamId, list[
|
|
41
|
+
records: list[RecordedRaw] = field(default_factory=list, init=False)
|
|
42
|
+
_data: dict[StreamId, list[RecordedRaw]] = field(default_factory=dict, init=False)
|
|
42
43
|
_versions: dict[StreamId, int | None] = field(default_factory=dict, init=False)
|
|
43
44
|
|
|
44
45
|
@property
|
|
45
|
-
def current_position(self) -> int:
|
|
46
|
-
return
|
|
46
|
+
def current_position(self) -> int | None:
|
|
47
|
+
return self.records[-1].position if self.records else None
|
|
47
48
|
|
|
48
49
|
def __contains__(self, stream_id: object) -> bool:
|
|
49
50
|
return stream_id in self._data
|
|
@@ -55,19 +56,19 @@ class Storage:
|
|
|
55
56
|
else:
|
|
56
57
|
self._versions[stream_id] = 0
|
|
57
58
|
|
|
58
|
-
def append(self,
|
|
59
|
-
self.
|
|
60
|
-
for
|
|
61
|
-
stream_id =
|
|
62
|
-
self._data[stream_id].append(
|
|
63
|
-
self._versions[stream_id] =
|
|
59
|
+
def append(self, records: list[RecordedRaw]) -> None:
|
|
60
|
+
self.records.extend(records)
|
|
61
|
+
for record in records:
|
|
62
|
+
stream_id = record.entry.stream_id
|
|
63
|
+
self._data[stream_id].append(record)
|
|
64
|
+
self._versions[stream_id] = record.entry.version
|
|
64
65
|
|
|
65
|
-
def replace(self, with_snapshot:
|
|
66
|
-
stream_id = with_snapshot.stream_id
|
|
66
|
+
def replace(self, with_snapshot: RecordedRaw) -> None:
|
|
67
|
+
stream_id = with_snapshot.entry.stream_id
|
|
67
68
|
self._data[stream_id] = [with_snapshot]
|
|
68
|
-
self._versions[stream_id] = with_snapshot.version
|
|
69
|
+
self._versions[stream_id] = with_snapshot.entry.version
|
|
69
70
|
|
|
70
|
-
def read(self, stream_id: StreamId) -> list[
|
|
71
|
+
def read(self, stream_id: StreamId) -> list[RecordedRaw]:
|
|
71
72
|
return copy(self._data[stream_id])
|
|
72
73
|
|
|
73
74
|
def delete(self, stream_id: StreamId) -> None:
|
|
@@ -84,15 +85,15 @@ class InMemorySubscription(Iterator[list[RecordedRaw]]):
|
|
|
84
85
|
_batch_size: int
|
|
85
86
|
_timelimit: timedelta
|
|
86
87
|
|
|
87
|
-
def _pop_record(self) ->
|
|
88
|
-
if
|
|
88
|
+
def _pop_record(self) -> RecordedRaw | None:
|
|
89
|
+
if (self._storage.current_position or 0) <= self._current_position:
|
|
89
90
|
return None
|
|
90
|
-
record = self._storage.
|
|
91
|
+
record = self._storage.records[self._current_position]
|
|
91
92
|
self._current_position += 1
|
|
92
|
-
return record
|
|
93
|
+
return record
|
|
93
94
|
|
|
94
95
|
def __next__(self) -> list[RecordedRaw]:
|
|
95
|
-
batch: list[
|
|
96
|
+
batch: list[RecordedRaw] = []
|
|
96
97
|
|
|
97
98
|
start = time.monotonic()
|
|
98
99
|
while len(batch) < self._batch_size:
|
|
@@ -103,23 +104,19 @@ class InMemorySubscription(Iterator[list[RecordedRaw]]):
|
|
|
103
104
|
if time.monotonic() - start > self._timelimit.total_seconds():
|
|
104
105
|
break
|
|
105
106
|
|
|
106
|
-
return
|
|
107
|
-
RecordedRaw(entry=copy(record), position=position)
|
|
108
|
-
for record, position in batch
|
|
109
|
-
]
|
|
107
|
+
return batch
|
|
110
108
|
|
|
111
109
|
|
|
112
110
|
@dataclass
|
|
113
111
|
class InMemoryToCategorySubscription(InMemorySubscription):
|
|
114
112
|
_category: str
|
|
115
113
|
|
|
116
|
-
def _pop_record(self) ->
|
|
114
|
+
def _pop_record(self) -> RecordedRaw | None:
|
|
117
115
|
while True:
|
|
118
116
|
record = super()._pop_record()
|
|
119
117
|
if record is None:
|
|
120
118
|
return None
|
|
121
|
-
|
|
122
|
-
if raw.stream_id.category != self._category:
|
|
119
|
+
if record.entry.stream_id.category != self._category:
|
|
123
120
|
continue
|
|
124
121
|
return record
|
|
125
122
|
|
|
@@ -128,13 +125,12 @@ class InMemoryToCategorySubscription(InMemorySubscription):
|
|
|
128
125
|
class InMemoryToEventTypesSubscription(InMemorySubscription):
|
|
129
126
|
_types: list[str]
|
|
130
127
|
|
|
131
|
-
def _pop_record(self) ->
|
|
128
|
+
def _pop_record(self) -> RecordedRaw | None:
|
|
132
129
|
while True:
|
|
133
130
|
record = super()._pop_record()
|
|
134
131
|
if record is None:
|
|
135
132
|
return None
|
|
136
|
-
|
|
137
|
-
if raw.name not in self._types:
|
|
133
|
+
if record.entry.name not in self._types:
|
|
138
134
|
continue
|
|
139
135
|
return record
|
|
140
136
|
|
|
@@ -225,11 +221,13 @@ class InMemoryStorageStrategy(StorageStrategy):
|
|
|
225
221
|
storage: Storage,
|
|
226
222
|
dispatcher: Dispatcher,
|
|
227
223
|
outbox_strategy: InMemoryOutboxStorageStrategy | None,
|
|
224
|
+
tenant_id: TenantId = DEFAULT_TENANT,
|
|
228
225
|
) -> None:
|
|
229
226
|
self._names: dict[str | None, str] = {}
|
|
230
227
|
self._storage = storage
|
|
231
228
|
self._dispatcher = dispatcher
|
|
232
229
|
self._outbox = outbox_strategy
|
|
230
|
+
self._tenant_id = tenant_id
|
|
233
231
|
|
|
234
232
|
def fetch_events(
|
|
235
233
|
self,
|
|
@@ -243,24 +241,29 @@ class InMemoryStorageStrategy(StorageStrategy):
|
|
|
243
241
|
self._storage.read(stream_id),
|
|
244
242
|
slice(start and start - 1, stop and stop - 1),
|
|
245
243
|
)
|
|
246
|
-
return
|
|
244
|
+
return [r.entry for r in stream if r.tenant_id == self._tenant_id]
|
|
247
245
|
|
|
248
246
|
def insert_events(
|
|
249
247
|
self, stream_id: StreamId, versioning: Versioning, events: list[RawEvent]
|
|
250
248
|
) -> None:
|
|
251
249
|
position = self.current_position or 0
|
|
252
250
|
self._ensure_stream(stream_id=stream_id, versioning=versioning)
|
|
253
|
-
self._storage.append(events)
|
|
254
251
|
records = [
|
|
255
|
-
RecordedRaw(entry=raw, position=position)
|
|
252
|
+
RecordedRaw(entry=raw, position=position, tenant_id=self._tenant_id)
|
|
256
253
|
for position, raw in enumerate(events, start=position + 1)
|
|
257
254
|
]
|
|
255
|
+
self._storage.append(records)
|
|
258
256
|
if self._outbox:
|
|
259
257
|
self._outbox.put_into_outbox(records)
|
|
260
258
|
self._dispatcher.dispatch(*records)
|
|
261
259
|
|
|
262
260
|
def save_snapshot(self, snapshot: RawEvent) -> None:
|
|
263
|
-
|
|
261
|
+
record = RecordedRaw(
|
|
262
|
+
entry=snapshot,
|
|
263
|
+
position=(self.current_position or 0) + 1,
|
|
264
|
+
tenant_id=self._tenant_id,
|
|
265
|
+
)
|
|
266
|
+
self._storage.replace(with_snapshot=record)
|
|
264
267
|
|
|
265
268
|
def _ensure_stream(self, stream_id: StreamId, versioning: Versioning) -> None:
|
|
266
269
|
if stream_id not in self._storage:
|
|
@@ -289,6 +292,14 @@ class InMemoryStorageStrategy(StorageStrategy):
|
|
|
289
292
|
current_position = self._storage.current_position
|
|
290
293
|
return current_position and Position(current_position)
|
|
291
294
|
|
|
295
|
+
def scoped_for_tenant(self, tenant_id: TenantId) -> Self:
|
|
296
|
+
return type(self)(
|
|
297
|
+
storage=self._storage,
|
|
298
|
+
dispatcher=self._dispatcher,
|
|
299
|
+
outbox_strategy=self._outbox,
|
|
300
|
+
tenant_id=tenant_id,
|
|
301
|
+
)
|
|
302
|
+
|
|
292
303
|
|
|
293
304
|
class Config(BaseModel):
|
|
294
305
|
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
@@ -4,6 +4,8 @@ from contextlib import AbstractContextManager
|
|
|
4
4
|
from datetime import timedelta
|
|
5
5
|
from typing import Protocol
|
|
6
6
|
|
|
7
|
+
from typing_extensions import Self
|
|
8
|
+
|
|
7
9
|
from event_sourcery.event_store.event import Position, RawEvent, RecordedRaw
|
|
8
10
|
from event_sourcery.event_store.stream_id import StreamId
|
|
9
11
|
from event_sourcery.event_store.versioning import Versioning
|
|
@@ -80,3 +82,7 @@ class StorageStrategy(abc.ABC):
|
|
|
80
82
|
@abc.abstractmethod
|
|
81
83
|
def current_position(self) -> Position | None:
|
|
82
84
|
pass
|
|
85
|
+
|
|
86
|
+
@abc.abstractmethod
|
|
87
|
+
def scoped_for_tenant(self, tenant_id: str) -> Self:
|
|
88
|
+
pass
|
|
@@ -16,7 +16,7 @@ def raw_event(from_entry: RecordedEvent) -> RawEvent:
|
|
|
16
16
|
position = stream.Position(from_entry.stream_position)
|
|
17
17
|
return RawEvent(
|
|
18
18
|
uuid=from_entry.id,
|
|
19
|
-
stream_id=stream.Name(
|
|
19
|
+
stream_id=stream.Name.from_stream_name(from_entry.stream_name).uuid,
|
|
20
20
|
created_at=created_at,
|
|
21
21
|
version=position.as_version(),
|
|
22
22
|
name=from_entry.type,
|
|
@@ -49,7 +49,9 @@ def new_entry(from_raw: RawEvent, **metadata: Any) -> NewEvent:
|
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
def raw_record(from_entry: RecordedEvent) -> RecordedRaw:
|
|
52
|
+
stream_name = stream.Name.from_stream_name(from_entry.stream_name)
|
|
52
53
|
return RecordedRaw(
|
|
53
|
-
entry=raw_event(from_entry
|
|
54
|
+
entry=raw_event(from_entry),
|
|
54
55
|
position=Position(from_entry.commit_position or 0),
|
|
56
|
+
tenant_id=stream_name.tenant_id,
|
|
55
57
|
)
|
{python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery_esdb/event_store.py
RENAMED
|
@@ -1,18 +1,21 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
1
|
+
from dataclasses import dataclass, replace
|
|
2
2
|
from typing import cast
|
|
3
3
|
|
|
4
4
|
from esdbclient import EventStoreDBClient, StreamState
|
|
5
5
|
from esdbclient.exceptions import NotFound
|
|
6
|
+
from typing_extensions import Self
|
|
6
7
|
|
|
7
8
|
from event_sourcery.event_store import (
|
|
8
9
|
NO_VERSIONING,
|
|
9
10
|
Position,
|
|
10
11
|
RawEvent,
|
|
11
12
|
StreamId,
|
|
13
|
+
TenantId,
|
|
12
14
|
Versioning,
|
|
13
15
|
)
|
|
14
16
|
from event_sourcery.event_store.exceptions import ConcurrentStreamWriteError
|
|
15
17
|
from event_sourcery.event_store.interfaces import StorageStrategy
|
|
18
|
+
from event_sourcery.event_store.tenant_id import DEFAULT_TENANT
|
|
16
19
|
from event_sourcery_esdb import dto, stream
|
|
17
20
|
|
|
18
21
|
|
|
@@ -20,6 +23,7 @@ from event_sourcery_esdb import dto, stream
|
|
|
20
23
|
class ESDBStorageStrategy(StorageStrategy):
|
|
21
24
|
_client: EventStoreDBClient
|
|
22
25
|
_timeout: float | None
|
|
26
|
+
_tenant_id: TenantId = DEFAULT_TENANT
|
|
23
27
|
|
|
24
28
|
def fetch_events(
|
|
25
29
|
self,
|
|
@@ -28,7 +32,7 @@ class ESDBStorageStrategy(StorageStrategy):
|
|
|
28
32
|
stop: int | None = None,
|
|
29
33
|
) -> list[RawEvent]:
|
|
30
34
|
snapshot = None
|
|
31
|
-
name = stream.Name(stream_id)
|
|
35
|
+
name = stream.Name(self._tenant_id, stream_id)
|
|
32
36
|
if start is None and (snapshot := self._read_snapshot(name)) is not None:
|
|
33
37
|
start = cast(int, snapshot.version) + 1
|
|
34
38
|
|
|
@@ -65,7 +69,7 @@ class ESDBStorageStrategy(StorageStrategy):
|
|
|
65
69
|
) -> None:
|
|
66
70
|
for sid in {e.stream_id for e in events}:
|
|
67
71
|
self._ensure_stream(stream_id=sid, versioning=versioning)
|
|
68
|
-
stream_name = stream.Name(sid)
|
|
72
|
+
stream_name = stream.Name(self._tenant_id, sid)
|
|
69
73
|
stream_events = [e for e in events if e.stream_id == sid]
|
|
70
74
|
self._append_events(stream_name, events=stream_events)
|
|
71
75
|
|
|
@@ -78,7 +82,7 @@ class ESDBStorageStrategy(StorageStrategy):
|
|
|
78
82
|
)
|
|
79
83
|
|
|
80
84
|
def save_snapshot(self, snapshot: RawEvent) -> None:
|
|
81
|
-
name = stream.Name(snapshot.stream_id)
|
|
85
|
+
name = stream.Name(self._tenant_id, snapshot.stream_id)
|
|
82
86
|
stream_position = stream.Position.from_version(cast(int, snapshot.version))
|
|
83
87
|
self._client.append_events(
|
|
84
88
|
name.snapshot,
|
|
@@ -88,7 +92,7 @@ class ESDBStorageStrategy(StorageStrategy):
|
|
|
88
92
|
)
|
|
89
93
|
|
|
90
94
|
def _ensure_stream(self, stream_id: StreamId, versioning: Versioning) -> None:
|
|
91
|
-
name = stream.Name(stream_id)
|
|
95
|
+
name = stream.Name(self._tenant_id, stream_id)
|
|
92
96
|
|
|
93
97
|
if versioning is not NO_VERSIONING and versioning.expected_version:
|
|
94
98
|
expected = stream.Position.from_version(versioning.expected_version)
|
|
@@ -112,7 +116,7 @@ class ESDBStorageStrategy(StorageStrategy):
|
|
|
112
116
|
return None
|
|
113
117
|
|
|
114
118
|
def delete_stream(self, stream_id: StreamId) -> None:
|
|
115
|
-
name = stream.Name(stream_id)
|
|
119
|
+
name = stream.Name(self._tenant_id, stream_id)
|
|
116
120
|
try:
|
|
117
121
|
self._client.delete_stream(
|
|
118
122
|
str(name),
|
|
@@ -125,3 +129,6 @@ class ESDBStorageStrategy(StorageStrategy):
|
|
|
125
129
|
@property
|
|
126
130
|
def current_position(self) -> Position | None:
|
|
127
131
|
return Position(self._client.get_commit_position(timeout=self._timeout))
|
|
132
|
+
|
|
133
|
+
def scoped_for_tenant(self, tenant_id: TenantId) -> Self:
|
|
134
|
+
return replace(self, _tenant_id=tenant_id)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from collections import UserString
|
|
3
|
+
from typing import cast
|
|
4
|
+
|
|
5
|
+
from typing_extensions import Self
|
|
6
|
+
|
|
7
|
+
from event_sourcery.event_store import StreamId, TenantId
|
|
8
|
+
from event_sourcery.event_store.exceptions import IllegalCategoryName, IllegalTenantId
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Name(UserString):
|
|
12
|
+
tenant_id: TenantId
|
|
13
|
+
uuid: StreamId
|
|
14
|
+
|
|
15
|
+
def __init__(self, tenant_id: TenantId, stream_id: StreamId) -> None:
|
|
16
|
+
if "-" in tenant_id:
|
|
17
|
+
raise IllegalTenantId("ESDB can't handle tenant id with '-'")
|
|
18
|
+
self.tenant_id = tenant_id
|
|
19
|
+
self.uuid = stream_id
|
|
20
|
+
super().__init__(self._as_string(self.uuid))
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def from_stream_name(cls, stream_name: str) -> Self:
|
|
24
|
+
if stream_name.endswith("-snapshot"):
|
|
25
|
+
stream_name, _ = stream_name.rsplit("-", 1)
|
|
26
|
+
|
|
27
|
+
category, tenant_id, id_as_hex = stream_name.split("-", 2)
|
|
28
|
+
return cls(tenant_id, StreamId(from_hex=id_as_hex, category=category or None))
|
|
29
|
+
|
|
30
|
+
def _as_string(self, stream_id: StreamId) -> str:
|
|
31
|
+
if stream_id.category and "-" in stream_id.category:
|
|
32
|
+
raise IllegalCategoryName("ESDB storage can't handle category with '-'")
|
|
33
|
+
return f"{stream_id.category or ''}-{self.tenant_id}-{stream_id.hex}"
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def snapshot(self) -> str:
|
|
37
|
+
return f"{self!s}-snapshot"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Position(int):
|
|
41
|
+
@classmethod
|
|
42
|
+
def from_version(cls, version: int) -> "Position":
|
|
43
|
+
return Position(version - 1)
|
|
44
|
+
|
|
45
|
+
def as_version(self) -> int:
|
|
46
|
+
return self + 1
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
MAX_POSITION = Position(sys.maxsize)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def scope(
|
|
53
|
+
start_version: int | None,
|
|
54
|
+
stop_version: int | None,
|
|
55
|
+
) -> tuple[Position | None, int]:
|
|
56
|
+
start = cast(
|
|
57
|
+
Position | None, start_version and Position.from_version(start_version)
|
|
58
|
+
)
|
|
59
|
+
stop = stop_version and Position.from_version(stop_version) or MAX_POSITION
|
|
60
|
+
return start, stop - (start or 0)
|
{python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery_esdb/subscription.py
RENAMED
|
@@ -66,9 +66,7 @@ class ESDBSubscriptionStrategy(SubscriptionStrategy):
|
|
|
66
66
|
self._client.subscribe_to_all,
|
|
67
67
|
commit_position=start_from,
|
|
68
68
|
timeout=timelimit.total_seconds(),
|
|
69
|
-
filter_include=[
|
|
70
|
-
f"{category}-\\w+",
|
|
71
|
-
],
|
|
69
|
+
filter_include=[f"{category}-[^-]*-\\w+"],
|
|
72
70
|
filter_by_stream_name=True,
|
|
73
71
|
)
|
|
74
72
|
return self._iterator(builder, batch_size)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from collections.abc import Sequence
|
|
2
|
-
from dataclasses import dataclass
|
|
2
|
+
from dataclasses import dataclass, replace
|
|
3
3
|
from typing import cast
|
|
4
4
|
|
|
5
5
|
from more_itertools import first_true
|
|
@@ -7,6 +7,7 @@ from sqlalchemy import delete, func, select, update
|
|
|
7
7
|
from sqlalchemy.dialects.postgresql import insert as postgresql_insert
|
|
8
8
|
from sqlalchemy.exc import NoResultFound
|
|
9
9
|
from sqlalchemy.orm import Session
|
|
10
|
+
from typing_extensions import Self
|
|
10
11
|
|
|
11
12
|
from event_sourcery.event_store import (
|
|
12
13
|
NO_VERSIONING,
|
|
@@ -22,6 +23,7 @@ from event_sourcery.event_store.exceptions import (
|
|
|
22
23
|
ConcurrentStreamWriteError,
|
|
23
24
|
)
|
|
24
25
|
from event_sourcery.event_store.interfaces import StorageStrategy
|
|
26
|
+
from event_sourcery.event_store.tenant_id import DEFAULT_TENANT, TenantId
|
|
25
27
|
from event_sourcery_sqlalchemy.models import Event as EventModel
|
|
26
28
|
from event_sourcery_sqlalchemy.models import Snapshot as SnapshotModel
|
|
27
29
|
from event_sourcery_sqlalchemy.models import Stream as StreamModel
|
|
@@ -33,6 +35,7 @@ class SqlAlchemyStorageStrategy(StorageStrategy):
|
|
|
33
35
|
_session: Session
|
|
34
36
|
_dispatcher: Dispatcher
|
|
35
37
|
_outbox: SqlAlchemyOutboxStorageStrategy | None = None
|
|
38
|
+
_tenant_id: TenantId = DEFAULT_TENANT
|
|
36
39
|
|
|
37
40
|
def fetch_events(
|
|
38
41
|
self,
|
|
@@ -42,7 +45,7 @@ class SqlAlchemyStorageStrategy(StorageStrategy):
|
|
|
42
45
|
) -> list[RawEvent]:
|
|
43
46
|
events_stmt = (
|
|
44
47
|
select(EventModel)
|
|
45
|
-
.filter_by(stream_id=stream_id)
|
|
48
|
+
.filter_by(stream_id=stream_id, tenant_id=self._tenant_id)
|
|
46
49
|
.order_by(EventModel.version)
|
|
47
50
|
)
|
|
48
51
|
|
|
@@ -57,7 +60,10 @@ class SqlAlchemyStorageStrategy(StorageStrategy):
|
|
|
57
60
|
snapshot_stmt = (
|
|
58
61
|
select(SnapshotModel)
|
|
59
62
|
.join(StreamModel)
|
|
60
|
-
.filter(
|
|
63
|
+
.filter(
|
|
64
|
+
StreamModel.stream_id == stream_id,
|
|
65
|
+
StreamModel.tenant_id == self._tenant_id,
|
|
66
|
+
)
|
|
61
67
|
.order_by(SnapshotModel.created_at.desc())
|
|
62
68
|
.limit(1)
|
|
63
69
|
)
|
|
@@ -97,13 +103,16 @@ class SqlAlchemyStorageStrategy(StorageStrategy):
|
|
|
97
103
|
def _ensure_stream(self, stream_id: StreamId, versioning: Versioning) -> None:
|
|
98
104
|
initial_version = versioning.initial_version
|
|
99
105
|
|
|
100
|
-
condition = (
|
|
101
|
-
StreamModel.
|
|
106
|
+
condition = (
|
|
107
|
+
(StreamModel.uuid == stream_id)
|
|
108
|
+
& (StreamModel.category == (stream_id.category or ""))
|
|
109
|
+
& (StreamModel.tenant_id == self._tenant_id)
|
|
102
110
|
)
|
|
103
111
|
if stream_id.name:
|
|
104
112
|
condition = condition | (
|
|
105
113
|
(StreamModel.name == stream_id.name)
|
|
106
114
|
& (StreamModel.category == (stream_id.category or ""))
|
|
115
|
+
& (StreamModel.tenant_id == self._tenant_id)
|
|
107
116
|
)
|
|
108
117
|
matching_streams_stmt = select(StreamModel).where(condition)
|
|
109
118
|
matching_streams = self._session.execute(matching_streams_stmt).scalars().all()
|
|
@@ -115,6 +124,7 @@ class SqlAlchemyStorageStrategy(StorageStrategy):
|
|
|
115
124
|
name=stream_id.name,
|
|
116
125
|
category=stream_id.category or "",
|
|
117
126
|
version=initial_version,
|
|
127
|
+
tenant_id=self._tenant_id,
|
|
118
128
|
)
|
|
119
129
|
.on_conflict_do_nothing()
|
|
120
130
|
)
|
|
@@ -163,12 +173,14 @@ class SqlAlchemyStorageStrategy(StorageStrategy):
|
|
|
163
173
|
self, stream_id: StreamId, versioning: Versioning, events: list[RawEvent]
|
|
164
174
|
) -> None:
|
|
165
175
|
self._ensure_stream(stream_id=stream_id, versioning=versioning)
|
|
176
|
+
|
|
166
177
|
stream = cast(
|
|
167
178
|
StreamModel,
|
|
168
179
|
first_true(
|
|
169
180
|
self._session.info["strong_set"],
|
|
170
181
|
pred=lambda model: isinstance(model, StreamModel)
|
|
171
|
-
and model.stream_id == stream_id
|
|
182
|
+
and model.stream_id == stream_id
|
|
183
|
+
and model.tenant_id == self._tenant_id,
|
|
172
184
|
),
|
|
173
185
|
)
|
|
174
186
|
|
|
@@ -186,7 +198,7 @@ class SqlAlchemyStorageStrategy(StorageStrategy):
|
|
|
186
198
|
stream.events.extend(entries)
|
|
187
199
|
self._session.flush()
|
|
188
200
|
records = [
|
|
189
|
-
RecordedRaw(entry=raw, position=db.id)
|
|
201
|
+
RecordedRaw(entry=raw, position=db.id, tenant_id=db.tenant_id)
|
|
190
202
|
for raw, db in zip(events, entries, strict=False)
|
|
191
203
|
]
|
|
192
204
|
if self._outbox:
|
|
@@ -226,3 +238,6 @@ class SqlAlchemyStorageStrategy(StorageStrategy):
|
|
|
226
238
|
stmt = select(func.max(EventModel.id))
|
|
227
239
|
last_event = self._session.scalar(stmt)
|
|
228
240
|
return last_event or Position(0)
|
|
241
|
+
|
|
242
|
+
def scoped_for_tenant(self, tenant_id: TenantId) -> Self:
|
|
243
|
+
return replace(self, _tenant_id=tenant_id)
|
{python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery_sqlalchemy/models.py
RENAMED
|
@@ -26,6 +26,7 @@ from sqlalchemy.orm import (
|
|
|
26
26
|
)
|
|
27
27
|
|
|
28
28
|
from event_sourcery.event_store import StreamId
|
|
29
|
+
from event_sourcery.event_store.tenant_id import TenantId
|
|
29
30
|
from event_sourcery_sqlalchemy.guid import GUID
|
|
30
31
|
from event_sourcery_sqlalchemy.jsonb import JSONB
|
|
31
32
|
|
|
@@ -57,14 +58,15 @@ class StreamIdComparator(Comparator[StreamId]):
|
|
|
57
58
|
class Stream:
|
|
58
59
|
__tablename__ = "event_sourcery_streams"
|
|
59
60
|
__table_args__ = (
|
|
60
|
-
UniqueConstraint("uuid", "category"),
|
|
61
|
-
UniqueConstraint("name", "category"),
|
|
61
|
+
UniqueConstraint("uuid", "category", "tenant_id"),
|
|
62
|
+
UniqueConstraint("name", "category", "tenant_id"),
|
|
62
63
|
)
|
|
63
64
|
|
|
64
65
|
id = mapped_column(BigInteger().with_variant(Integer(), "sqlite"), primary_key=True)
|
|
65
66
|
uuid = mapped_column(GUID(), nullable=False)
|
|
66
67
|
name = mapped_column(String(255), nullable=True, default=None)
|
|
67
68
|
category = mapped_column(String(255), nullable=False, default="")
|
|
69
|
+
tenant_id = mapped_column(String(255), nullable=False)
|
|
68
70
|
version = mapped_column(BigInteger(), nullable=True)
|
|
69
71
|
|
|
70
72
|
events: Mapped[list["Event"]]
|
|
@@ -119,6 +121,7 @@ class Event:
|
|
|
119
121
|
)
|
|
120
122
|
stream: Mapped[Stream] = relationship(Stream, back_populates="events")
|
|
121
123
|
stream_id: AssociationProxy[StreamId] = association_proxy("stream", "stream_id")
|
|
124
|
+
tenant_id: AssociationProxy[TenantId] = association_proxy("stream", "tenant_id")
|
|
122
125
|
name = mapped_column(String(200), nullable=False)
|
|
123
126
|
data = mapped_column(JSONB(), nullable=False)
|
|
124
127
|
event_context = mapped_column(JSONB(), nullable=False)
|
{python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery_sqlalchemy/outbox.py
RENAMED
|
@@ -36,6 +36,7 @@ class SqlAlchemyOutboxStorageStrategy(OutboxStorageStrategy):
|
|
|
36
36
|
as_dict["created_at"] = created_at.isoformat()
|
|
37
37
|
as_dict["uuid"] = str(as_dict["uuid"])
|
|
38
38
|
as_dict["stream_id"] = str(as_dict["stream_id"])
|
|
39
|
+
as_dict["tenant_id"] = str(record.tenant_id)
|
|
39
40
|
rows.append(
|
|
40
41
|
{
|
|
41
42
|
"created_at": datetime.utcnow(),
|
|
@@ -82,7 +83,11 @@ class SqlAlchemyOutboxStorageStrategy(OutboxStorageStrategy):
|
|
|
82
83
|
context=entry.data["context"],
|
|
83
84
|
)
|
|
84
85
|
try:
|
|
85
|
-
yield RecordedRaw(
|
|
86
|
+
yield RecordedRaw(
|
|
87
|
+
entry=raw,
|
|
88
|
+
position=entry.position,
|
|
89
|
+
tenant_id=entry.data["tenant_id"],
|
|
90
|
+
)
|
|
86
91
|
except Exception:
|
|
87
92
|
logger.exception("Failed to publish message #%d", entry.id)
|
|
88
93
|
entry.tries_left -= 1
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
from collections import UserString
|
|
3
|
-
from typing import cast
|
|
4
|
-
|
|
5
|
-
from event_sourcery.event_store import StreamId
|
|
6
|
-
from event_sourcery.event_store.exceptions import IllegalCategoryName
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class Name(UserString):
|
|
10
|
-
uuid: StreamId
|
|
11
|
-
|
|
12
|
-
def __init__(
|
|
13
|
-
self,
|
|
14
|
-
stream_id: StreamId | None = None,
|
|
15
|
-
stream_name: str | None = None,
|
|
16
|
-
) -> None:
|
|
17
|
-
self.uuid = stream_id or self._get_id(from_name=(stream_name or ""))
|
|
18
|
-
super().__init__(self._as_string(self.uuid))
|
|
19
|
-
|
|
20
|
-
@staticmethod
|
|
21
|
-
def _get_id(from_name: str) -> StreamId:
|
|
22
|
-
category: str | None = None
|
|
23
|
-
if from_name.endswith("-snapshot"):
|
|
24
|
-
from_name, _ = from_name.rsplit("-", 1)
|
|
25
|
-
if "-" in from_name:
|
|
26
|
-
category, from_name = from_name.split("-", 1)
|
|
27
|
-
return StreamId(from_hex=from_name, category=category)
|
|
28
|
-
|
|
29
|
-
@staticmethod
|
|
30
|
-
def _as_string(stream_id: StreamId) -> str:
|
|
31
|
-
if stream_id.category:
|
|
32
|
-
if "-" in stream_id.category:
|
|
33
|
-
raise IllegalCategoryName("ESDB storage can't handle category with '-'")
|
|
34
|
-
return f"{stream_id.category}-{stream_id.hex}"
|
|
35
|
-
return stream_id.hex
|
|
36
|
-
|
|
37
|
-
@property
|
|
38
|
-
def snapshot(self) -> str:
|
|
39
|
-
return f"{self!s}-snapshot"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
class Position(int):
|
|
43
|
-
def __new__(cls, value: int | str) -> "Position":
|
|
44
|
-
return super().__new__(Position, value)
|
|
45
|
-
|
|
46
|
-
@classmethod
|
|
47
|
-
def from_version(cls, version: int) -> "Position":
|
|
48
|
-
return Position(version - 1)
|
|
49
|
-
|
|
50
|
-
def as_version(self) -> int:
|
|
51
|
-
return self + 1
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
MAX_POSITION = Position(sys.maxsize)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def scope(
|
|
58
|
-
start_version: int | None,
|
|
59
|
-
stop_version: int | None,
|
|
60
|
-
) -> tuple[Position | None, int]:
|
|
61
|
-
start = cast(
|
|
62
|
-
Position | None, start_version and Position.from_version(start_version)
|
|
63
|
-
)
|
|
64
|
-
stop = stop_version and Position.from_version(stop_version) or MAX_POSITION
|
|
65
|
-
return start, stop - (start or 0)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery_esdb/__init__.py
RENAMED
|
File without changes
|
{python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery_esdb/outbox.py
RENAMED
|
File without changes
|
{python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery_esdb/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery_sqlalchemy/dto.py
RENAMED
|
File without changes
|
{python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery_sqlalchemy/guid.py
RENAMED
|
File without changes
|
{python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery_sqlalchemy/jsonb.py
RENAMED
|
File without changes
|
{python_event_sourcery-0.4.0a9 → python_event_sourcery-0.4.0a11}/event_sourcery_sqlalchemy/py.typed
RENAMED
|
File without changes
|