python-event-sourcery 0.4.0a18__tar.gz → 0.4.0a19__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.0a18 → python_event_sourcery-0.4.0a19}/PKG-INFO +1 -1
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery_sqlalchemy/__init__.py +60 -7
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery_sqlalchemy/cursors_dao.py +22 -19
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery_sqlalchemy/dto.py +2 -2
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery_sqlalchemy/event_store.py +45 -40
- python_event_sourcery-0.4.0a19/event_sourcery_sqlalchemy/models/__init__.py +53 -0
- python_event_sourcery-0.4.0a18/event_sourcery_sqlalchemy/models.py → python_event_sourcery-0.4.0a19/event_sourcery_sqlalchemy/models/base.py +102 -60
- python_event_sourcery-0.4.0a19/event_sourcery_sqlalchemy/models/default.py +27 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery_sqlalchemy/outbox.py +7 -6
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery_sqlalchemy/subscription.py +74 -29
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/pyproject.toml +1 -1
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/LICENSE +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery/__init__.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery/event_sourcing/__init__.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery/event_sourcing/aggregate.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery/event_sourcing/repository.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery/event_store/__init__.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery/event_store/backend.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery/event_store/dispatcher.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery/event_store/event/__init__.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery/event_store/event/_dto.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery/event_store/event/_encryption.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery/event_store/event/_registry.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery/event_store/event/_serde.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery/event_store/event_store.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery/event_store/exceptions.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery/event_store/in_memory.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery/event_store/interfaces.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery/event_store/outbox.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery/event_store/stream_id.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery/event_store/subscription.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery/event_store/tenant_id.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery/event_store/versioning.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery/py.typed +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery/read_model/__init__.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery/read_model/cursors_dao.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery/read_model/projector.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery_django/__init__.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery_django/dto.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery_django/event_store.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery_django/migrations/0001_initial.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery_django/migrations/__init__.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery_django/models.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery_django/outbox.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery_django/py.typed +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery_django/subscription.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery_kurrentdb/__init__.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery_kurrentdb/dto.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery_kurrentdb/event_store.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery_kurrentdb/outbox.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery_kurrentdb/py.typed +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery_kurrentdb/stream.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery_kurrentdb/subscription.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery_sqlalchemy/guid.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery_sqlalchemy/jsonb.py +0 -0
- {python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery_sqlalchemy/py.typed +0 -0
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
__all__ = [
|
|
2
|
+
"BaseEvent",
|
|
3
|
+
"BaseOutboxEntry",
|
|
4
|
+
"BaseProjectorCursor",
|
|
5
|
+
"BaseSnapshot",
|
|
6
|
+
"BaseStream",
|
|
2
7
|
"Config",
|
|
8
|
+
"Models",
|
|
3
9
|
"SQLAlchemyBackend",
|
|
4
10
|
"SqlAlchemyStorageStrategy",
|
|
5
11
|
"configure_models",
|
|
6
12
|
"models",
|
|
7
13
|
]
|
|
8
14
|
|
|
15
|
+
from dataclasses import dataclass
|
|
9
16
|
from datetime import timedelta
|
|
10
17
|
|
|
11
18
|
from pydantic import BaseModel, ConfigDict, PositiveInt
|
|
@@ -23,10 +30,31 @@ from event_sourcery.event_store.interfaces import (
|
|
|
23
30
|
from event_sourcery_sqlalchemy import models
|
|
24
31
|
from event_sourcery_sqlalchemy.event_store import SqlAlchemyStorageStrategy
|
|
25
32
|
from event_sourcery_sqlalchemy.models import configure_models
|
|
33
|
+
from event_sourcery_sqlalchemy.models.base import (
|
|
34
|
+
BaseEvent,
|
|
35
|
+
BaseOutboxEntry,
|
|
36
|
+
BaseProjectorCursor,
|
|
37
|
+
BaseSnapshot,
|
|
38
|
+
BaseStream,
|
|
39
|
+
)
|
|
40
|
+
from event_sourcery_sqlalchemy.models.default import (
|
|
41
|
+
DefaultEvent,
|
|
42
|
+
DefaultOutboxEntry,
|
|
43
|
+
DefaultSnapshot,
|
|
44
|
+
DefaultStream,
|
|
45
|
+
)
|
|
26
46
|
from event_sourcery_sqlalchemy.outbox import SqlAlchemyOutboxStorageStrategy
|
|
27
47
|
from event_sourcery_sqlalchemy.subscription import SqlAlchemySubscriptionStrategy
|
|
28
48
|
|
|
29
49
|
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class Models:
|
|
52
|
+
event_model: type[BaseEvent]
|
|
53
|
+
stream_model: type[BaseStream]
|
|
54
|
+
snapshot_model: type[BaseSnapshot]
|
|
55
|
+
outbox_entry_model: type[BaseOutboxEntry] = DefaultOutboxEntry
|
|
56
|
+
|
|
57
|
+
|
|
30
58
|
class Config(BaseModel):
|
|
31
59
|
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
32
60
|
|
|
@@ -35,27 +63,51 @@ class Config(BaseModel):
|
|
|
35
63
|
|
|
36
64
|
|
|
37
65
|
class SQLAlchemyBackend(TransactionalBackend):
|
|
66
|
+
UNCONFIGURED_MESSAGE = "Configure backend with `.configure(session, config)`"
|
|
67
|
+
|
|
38
68
|
def __init__(self) -> None:
|
|
39
69
|
super().__init__()
|
|
40
|
-
self[
|
|
41
|
-
|
|
42
|
-
)
|
|
43
|
-
self[
|
|
44
|
-
|
|
45
|
-
)
|
|
70
|
+
self[BaseEvent] = not_configured(self.UNCONFIGURED_MESSAGE)
|
|
71
|
+
self[BaseStream] = not_configured(self.UNCONFIGURED_MESSAGE)
|
|
72
|
+
self[BaseSnapshot] = not_configured(self.UNCONFIGURED_MESSAGE)
|
|
73
|
+
self[BaseOutboxEntry] = not_configured(self.UNCONFIGURED_MESSAGE)
|
|
74
|
+
self[Session] = not_configured(self.UNCONFIGURED_MESSAGE)
|
|
75
|
+
self[Config] = not_configured(self.UNCONFIGURED_MESSAGE)
|
|
46
76
|
self[StorageStrategy] = lambda c: SqlAlchemyStorageStrategy(
|
|
47
77
|
c[Session],
|
|
48
78
|
c[Dispatcher],
|
|
49
79
|
c.get(SqlAlchemyOutboxStorageStrategy, None),
|
|
80
|
+
c[BaseEvent],
|
|
81
|
+
c[BaseSnapshot],
|
|
82
|
+
c[BaseStream],
|
|
50
83
|
).scoped_for_tenant(c.tenant_id)
|
|
51
84
|
self[SubscriptionStrategy] = lambda c: SqlAlchemySubscriptionStrategy(
|
|
52
85
|
c[Session],
|
|
53
86
|
c[Config].gap_retry_interval,
|
|
87
|
+
c[BaseEvent],
|
|
88
|
+
c[BaseStream],
|
|
54
89
|
)
|
|
55
90
|
|
|
56
|
-
def configure(
|
|
91
|
+
def configure(
|
|
92
|
+
self,
|
|
93
|
+
session: Session,
|
|
94
|
+
config: Config | None = None,
|
|
95
|
+
custom_models: Models | None = None,
|
|
96
|
+
) -> Self:
|
|
97
|
+
if custom_models is None:
|
|
98
|
+
custom_models = Models(
|
|
99
|
+
event_model=DefaultEvent,
|
|
100
|
+
stream_model=DefaultStream,
|
|
101
|
+
snapshot_model=DefaultSnapshot,
|
|
102
|
+
outbox_entry_model=DefaultOutboxEntry,
|
|
103
|
+
)
|
|
104
|
+
|
|
57
105
|
self[Session] = session
|
|
58
106
|
self[Config] = config or Config()
|
|
107
|
+
self[BaseEvent] = lambda _: custom_models.event_model
|
|
108
|
+
self[BaseStream] = lambda _: custom_models.stream_model
|
|
109
|
+
self[BaseSnapshot] = lambda _: custom_models.snapshot_model
|
|
110
|
+
self[BaseOutboxEntry] = lambda _: custom_models.outbox_entry_model
|
|
59
111
|
return self
|
|
60
112
|
|
|
61
113
|
def with_outbox(self, filterer: OutboxFiltererStrategy = no_filter) -> Self:
|
|
@@ -65,6 +117,7 @@ class SQLAlchemyBackend(TransactionalBackend):
|
|
|
65
117
|
c[Session],
|
|
66
118
|
c[OutboxFiltererStrategy],
|
|
67
119
|
c[Config].outbox_attempts,
|
|
120
|
+
c[BaseOutboxEntry],
|
|
68
121
|
)
|
|
69
122
|
)
|
|
70
123
|
self[OutboxStorageStrategy] = lambda c: c[SqlAlchemyOutboxStorageStrategy]
|
|
@@ -5,12 +5,15 @@ from sqlalchemy.orm import Session
|
|
|
5
5
|
|
|
6
6
|
from event_sourcery.event_store import StreamId
|
|
7
7
|
from event_sourcery.read_model import CursorsDao
|
|
8
|
-
from event_sourcery_sqlalchemy.models import
|
|
8
|
+
from event_sourcery_sqlalchemy.models.base import BaseProjectorCursor
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class SqlAlchemyCursorsDao(CursorsDao):
|
|
12
|
-
def __init__(
|
|
12
|
+
def __init__(
|
|
13
|
+
self, session: Session, projector_cursor_model: type[BaseProjectorCursor]
|
|
14
|
+
) -> None:
|
|
13
15
|
self._session = session
|
|
16
|
+
self._projector_cursor_model = projector_cursor_model
|
|
14
17
|
|
|
15
18
|
def increment(self, name: str, stream_id: StreamId, version: int) -> None:
|
|
16
19
|
if version == 1:
|
|
@@ -18,7 +21,7 @@ class SqlAlchemyCursorsDao(CursorsDao):
|
|
|
18
21
|
if current_version is not None:
|
|
19
22
|
raise self.AheadOfStream(current_version=current_version)
|
|
20
23
|
|
|
21
|
-
stmt = insert(
|
|
24
|
+
stmt = insert(self._projector_cursor_model).values(
|
|
22
25
|
name=name,
|
|
23
26
|
stream_id=stream_id,
|
|
24
27
|
category=stream_id.category,
|
|
@@ -28,14 +31,14 @@ class SqlAlchemyCursorsDao(CursorsDao):
|
|
|
28
31
|
return
|
|
29
32
|
|
|
30
33
|
update_stmt = (
|
|
31
|
-
update(
|
|
34
|
+
update(self._projector_cursor_model)
|
|
32
35
|
.where(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
self._projector_cursor_model.name == name,
|
|
37
|
+
self._projector_cursor_model.stream_id == stream_id,
|
|
38
|
+
self._projector_cursor_model.category == stream_id.category,
|
|
39
|
+
self._projector_cursor_model.version == version - 1,
|
|
37
40
|
)
|
|
38
|
-
.values({
|
|
41
|
+
.values({self._projector_cursor_model.version: version})
|
|
39
42
|
)
|
|
40
43
|
result = self._session.execute(update_stmt)
|
|
41
44
|
if result.rowcount == 1:
|
|
@@ -51,15 +54,15 @@ class SqlAlchemyCursorsDao(CursorsDao):
|
|
|
51
54
|
raise self.AheadOfStream(current_version=current_version)
|
|
52
55
|
|
|
53
56
|
def _current_version(self, name: str, stream_id: StreamId) -> int | None:
|
|
54
|
-
stmt = select(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
stmt = select(self._projector_cursor_model.version).filter(
|
|
58
|
+
self._projector_cursor_model.name == name,
|
|
59
|
+
self._projector_cursor_model.stream_id == stream_id,
|
|
60
|
+
self._projector_cursor_model.category == stream_id.category,
|
|
58
61
|
)
|
|
59
62
|
return cast(int | None, self._session.execute(stmt).scalar())
|
|
60
63
|
|
|
61
64
|
def put_at(self, name: str, stream_id: StreamId, version: int) -> None:
|
|
62
|
-
stmt = insert(
|
|
65
|
+
stmt = insert(self._projector_cursor_model).values(
|
|
63
66
|
name=name,
|
|
64
67
|
stream_id=stream_id,
|
|
65
68
|
category=stream_id.category,
|
|
@@ -69,12 +72,12 @@ class SqlAlchemyCursorsDao(CursorsDao):
|
|
|
69
72
|
|
|
70
73
|
def move_to(self, name: str, stream_id: StreamId, version: int) -> None:
|
|
71
74
|
stmt = (
|
|
72
|
-
update(
|
|
75
|
+
update(self._projector_cursor_model)
|
|
73
76
|
.where(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
self._projector_cursor_model.name == name,
|
|
78
|
+
self._projector_cursor_model.stream_id == stream_id,
|
|
79
|
+
self._projector_cursor_model.category == stream_id.category,
|
|
77
80
|
)
|
|
78
|
-
.values({
|
|
81
|
+
.values({self._projector_cursor_model.version: version})
|
|
79
82
|
)
|
|
80
83
|
self._session.execute(stmt)
|
{python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery_sqlalchemy/dto.py
RENAMED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from event_sourcery.event_store import RawEvent, StreamId
|
|
2
|
-
from event_sourcery_sqlalchemy.models import
|
|
2
|
+
from event_sourcery_sqlalchemy.models.base import BaseEvent, BaseStream
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
def raw_event(from_entry:
|
|
5
|
+
def raw_event(from_entry: BaseEvent, in_stream: BaseStream) -> RawEvent:
|
|
6
6
|
return RawEvent(
|
|
7
7
|
uuid=from_entry.uuid,
|
|
8
8
|
stream_id=StreamId(
|
|
@@ -24,9 +24,7 @@ from event_sourcery.event_store.exceptions import (
|
|
|
24
24
|
)
|
|
25
25
|
from event_sourcery.event_store.interfaces import StorageStrategy
|
|
26
26
|
from event_sourcery.event_store.tenant_id import DEFAULT_TENANT, TenantId
|
|
27
|
-
from event_sourcery_sqlalchemy.models import
|
|
28
|
-
from event_sourcery_sqlalchemy.models import Snapshot as SnapshotModel
|
|
29
|
-
from event_sourcery_sqlalchemy.models import Stream as StreamModel
|
|
27
|
+
from event_sourcery_sqlalchemy.models.base import BaseEvent, BaseSnapshot, BaseStream
|
|
30
28
|
from event_sourcery_sqlalchemy.outbox import SqlAlchemyOutboxStorageStrategy
|
|
31
29
|
|
|
32
30
|
|
|
@@ -34,7 +32,10 @@ from event_sourcery_sqlalchemy.outbox import SqlAlchemyOutboxStorageStrategy
|
|
|
34
32
|
class SqlAlchemyStorageStrategy(StorageStrategy):
|
|
35
33
|
_session: Session
|
|
36
34
|
_dispatcher: Dispatcher
|
|
37
|
-
_outbox: SqlAlchemyOutboxStorageStrategy
|
|
35
|
+
_outbox: SqlAlchemyOutboxStorageStrategy
|
|
36
|
+
_event_model: type[BaseEvent]
|
|
37
|
+
_snapshot_model: type[BaseSnapshot]
|
|
38
|
+
_stream_model: type[BaseStream]
|
|
38
39
|
_tenant_id: TenantId = DEFAULT_TENANT
|
|
39
40
|
|
|
40
41
|
def fetch_events(
|
|
@@ -44,41 +45,45 @@ class SqlAlchemyStorageStrategy(StorageStrategy):
|
|
|
44
45
|
stop: int | None = None,
|
|
45
46
|
) -> list[RawEvent]:
|
|
46
47
|
events_stmt = (
|
|
47
|
-
select(
|
|
48
|
+
select(self._event_model)
|
|
48
49
|
.filter_by(stream_id=stream_id, tenant_id=self._tenant_id)
|
|
49
|
-
.order_by(
|
|
50
|
+
.order_by(self._event_model.version)
|
|
50
51
|
)
|
|
51
52
|
|
|
52
53
|
if start is not None:
|
|
53
|
-
events_stmt = events_stmt.filter(
|
|
54
|
+
events_stmt = events_stmt.filter(self._event_model.version >= start)
|
|
54
55
|
|
|
55
56
|
if stop is not None:
|
|
56
|
-
events_stmt = events_stmt.filter(
|
|
57
|
+
events_stmt = events_stmt.filter(self._event_model.version < stop)
|
|
57
58
|
|
|
58
|
-
events: Sequence[
|
|
59
|
+
events: Sequence[BaseEvent | BaseSnapshot]
|
|
59
60
|
try:
|
|
60
61
|
snapshot_stmt = (
|
|
61
|
-
select(
|
|
62
|
-
.join(
|
|
62
|
+
select(self._snapshot_model)
|
|
63
|
+
.join(self._stream_model)
|
|
63
64
|
.filter(
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
self._stream_model.stream_id == stream_id,
|
|
66
|
+
self._stream_model.tenant_id == self._tenant_id,
|
|
66
67
|
)
|
|
67
|
-
.order_by(
|
|
68
|
+
.order_by(self._snapshot_model.created_at.desc())
|
|
68
69
|
.limit(1)
|
|
69
70
|
)
|
|
70
71
|
if start is not None:
|
|
71
|
-
snapshot_stmt = snapshot_stmt.filter(
|
|
72
|
+
snapshot_stmt = snapshot_stmt.filter(
|
|
73
|
+
self._snapshot_model.version >= start
|
|
74
|
+
)
|
|
72
75
|
|
|
73
76
|
if stop is not None:
|
|
74
|
-
snapshot_stmt = snapshot_stmt.filter(
|
|
77
|
+
snapshot_stmt = snapshot_stmt.filter(
|
|
78
|
+
self._snapshot_model.version < stop
|
|
79
|
+
)
|
|
75
80
|
|
|
76
81
|
latest_snapshot = self._session.execute(snapshot_stmt).scalars().one()
|
|
77
82
|
except NoResultFound:
|
|
78
83
|
events = self._session.execute(events_stmt).scalars().all()
|
|
79
84
|
else:
|
|
80
85
|
events_stmt = events_stmt.filter(
|
|
81
|
-
|
|
86
|
+
self._event_model.version > latest_snapshot.version
|
|
82
87
|
)
|
|
83
88
|
newer_events = list(self._session.execute(events_stmt).scalars().all())
|
|
84
89
|
events = [latest_snapshot, *newer_events]
|
|
@@ -104,21 +109,21 @@ class SqlAlchemyStorageStrategy(StorageStrategy):
|
|
|
104
109
|
initial_version = versioning.initial_version
|
|
105
110
|
|
|
106
111
|
condition = (
|
|
107
|
-
(
|
|
108
|
-
& (
|
|
109
|
-
& (
|
|
112
|
+
(self._stream_model.uuid == stream_id)
|
|
113
|
+
& (self._stream_model.category == (stream_id.category or ""))
|
|
114
|
+
& (self._stream_model.tenant_id == self._tenant_id)
|
|
110
115
|
)
|
|
111
116
|
if stream_id.name:
|
|
112
117
|
condition = condition | (
|
|
113
|
-
(
|
|
114
|
-
& (
|
|
115
|
-
& (
|
|
118
|
+
(self._stream_model.name == stream_id.name)
|
|
119
|
+
& (self._stream_model.category == (stream_id.category or ""))
|
|
120
|
+
& (self._stream_model.tenant_id == self._tenant_id)
|
|
116
121
|
)
|
|
117
|
-
matching_streams_stmt = select(
|
|
122
|
+
matching_streams_stmt = select(self._stream_model).where(condition)
|
|
118
123
|
matching_streams = self._session.execute(matching_streams_stmt).scalars().all()
|
|
119
124
|
if not matching_streams:
|
|
120
125
|
ensure_stream_stmt = (
|
|
121
|
-
postgresql_insert(
|
|
126
|
+
postgresql_insert(self._stream_model)
|
|
122
127
|
.values(
|
|
123
128
|
uuid=stream_id,
|
|
124
129
|
name=stream_id.name,
|
|
@@ -134,7 +139,7 @@ class SqlAlchemyStorageStrategy(StorageStrategy):
|
|
|
134
139
|
)
|
|
135
140
|
|
|
136
141
|
if stream_id.name is not None:
|
|
137
|
-
matching_stream_with_same_name:
|
|
142
|
+
matching_stream_with_same_name: BaseStream = [
|
|
138
143
|
stream
|
|
139
144
|
for stream in matching_streams
|
|
140
145
|
if stream.name == stream_id.name
|
|
@@ -144,7 +149,7 @@ class SqlAlchemyStorageStrategy(StorageStrategy):
|
|
|
144
149
|
raise AnotherStreamWithThisNameButOtherIdExists()
|
|
145
150
|
|
|
146
151
|
stream = cast(
|
|
147
|
-
|
|
152
|
+
BaseStream,
|
|
148
153
|
first_true(
|
|
149
154
|
matching_streams, pred=lambda stream: stream.stream_id == stream_id
|
|
150
155
|
),
|
|
@@ -156,10 +161,10 @@ class SqlAlchemyStorageStrategy(StorageStrategy):
|
|
|
156
161
|
|
|
157
162
|
if versioning.expected_version and versioning is not NO_VERSIONING:
|
|
158
163
|
bump_version_stmt = (
|
|
159
|
-
update(
|
|
164
|
+
update(self._stream_model)
|
|
160
165
|
.where(
|
|
161
|
-
|
|
162
|
-
|
|
166
|
+
self._stream_model.stream_id == stream_id,
|
|
167
|
+
self._stream_model.version == versioning.expected_version,
|
|
163
168
|
)
|
|
164
169
|
.values(version=versioning.initial_version)
|
|
165
170
|
)
|
|
@@ -175,17 +180,17 @@ class SqlAlchemyStorageStrategy(StorageStrategy):
|
|
|
175
180
|
self._ensure_stream(stream_id=stream_id, versioning=versioning)
|
|
176
181
|
|
|
177
182
|
stream = cast(
|
|
178
|
-
|
|
183
|
+
BaseStream,
|
|
179
184
|
first_true(
|
|
180
185
|
self._session.info["strong_set"],
|
|
181
|
-
pred=lambda model: isinstance(model,
|
|
186
|
+
pred=lambda model: isinstance(model, self._stream_model)
|
|
182
187
|
and model.stream_id == stream_id
|
|
183
188
|
and model.tenant_id == self._tenant_id,
|
|
184
189
|
),
|
|
185
190
|
)
|
|
186
191
|
|
|
187
192
|
entries = [
|
|
188
|
-
|
|
193
|
+
self._event_model(
|
|
189
194
|
uuid=event.uuid,
|
|
190
195
|
created_at=event.created_at,
|
|
191
196
|
name=event.name,
|
|
@@ -207,7 +212,7 @@ class SqlAlchemyStorageStrategy(StorageStrategy):
|
|
|
207
212
|
self._dispatcher.dispatch(*records)
|
|
208
213
|
|
|
209
214
|
def save_snapshot(self, snapshot: RawEvent) -> None:
|
|
210
|
-
entry =
|
|
215
|
+
entry = self._snapshot_model(
|
|
211
216
|
uuid=snapshot.uuid,
|
|
212
217
|
created_at=snapshot.created_at,
|
|
213
218
|
version=snapshot.version,
|
|
@@ -216,7 +221,7 @@ class SqlAlchemyStorageStrategy(StorageStrategy):
|
|
|
216
221
|
event_context=snapshot.context,
|
|
217
222
|
)
|
|
218
223
|
stream = (
|
|
219
|
-
self._session.query(
|
|
224
|
+
self._session.query(self._stream_model)
|
|
220
225
|
.filter_by(stream_id=snapshot.stream_id)
|
|
221
226
|
.one()
|
|
222
227
|
)
|
|
@@ -224,18 +229,18 @@ class SqlAlchemyStorageStrategy(StorageStrategy):
|
|
|
224
229
|
self._session.flush()
|
|
225
230
|
|
|
226
231
|
def delete_stream(self, stream_id: StreamId) -> None:
|
|
227
|
-
delete_events_stmt = delete(
|
|
228
|
-
|
|
232
|
+
delete_events_stmt = delete(self._event_model).where(
|
|
233
|
+
self._event_model.stream_id == stream_id,
|
|
229
234
|
)
|
|
230
235
|
self._session.execute(delete_events_stmt)
|
|
231
|
-
delete_stream_stmt = delete(
|
|
232
|
-
|
|
236
|
+
delete_stream_stmt = delete(self._stream_model).where(
|
|
237
|
+
self._stream_model.stream_id == stream_id,
|
|
233
238
|
)
|
|
234
239
|
self._session.execute(delete_stream_stmt)
|
|
235
240
|
|
|
236
241
|
@property
|
|
237
242
|
def current_position(self) -> Position | None:
|
|
238
|
-
stmt = select(func.max(
|
|
243
|
+
stmt = select(func.max(self._event_model.id))
|
|
239
244
|
last_event = self._session.scalar(stmt)
|
|
240
245
|
return last_event or Position(0)
|
|
241
246
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from typing import Protocol
|
|
2
|
+
|
|
3
|
+
from sqlalchemy import MetaData
|
|
4
|
+
from sqlalchemy.orm import registry
|
|
5
|
+
from sqlalchemy.orm.clsregistry import ClsRegistryToken
|
|
6
|
+
|
|
7
|
+
from event_sourcery_sqlalchemy.models.base import (
|
|
8
|
+
BaseEvent,
|
|
9
|
+
BaseOutboxEntry,
|
|
10
|
+
BaseProjectorCursor,
|
|
11
|
+
BaseSnapshot,
|
|
12
|
+
BaseStream,
|
|
13
|
+
)
|
|
14
|
+
from event_sourcery_sqlalchemy.models.default import (
|
|
15
|
+
DefaultEvent,
|
|
16
|
+
DefaultOutboxEntry,
|
|
17
|
+
DefaultProjectorCursor,
|
|
18
|
+
DefaultSnapshot,
|
|
19
|
+
DefaultStream,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BaseProto(Protocol):
|
|
24
|
+
metadata: MetaData
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_class_registry: dict[str, type | ClsRegistryToken] = {}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def configure_models(
|
|
31
|
+
base: type[BaseProto],
|
|
32
|
+
event_model: type[BaseEvent] = DefaultEvent,
|
|
33
|
+
stream_model: type[BaseStream] = DefaultStream,
|
|
34
|
+
snapshot_model: type[BaseSnapshot] = DefaultSnapshot,
|
|
35
|
+
outbox_entry_model: type[BaseOutboxEntry] = DefaultOutboxEntry,
|
|
36
|
+
projector_cursor_model: type[BaseProjectorCursor] = DefaultProjectorCursor,
|
|
37
|
+
) -> None:
|
|
38
|
+
event_model.__set_mapping_information__(stream_model)
|
|
39
|
+
snapshot_model.__set_mapping_information__(stream_model)
|
|
40
|
+
stream_model.__set_mapping_information__(event_model, snapshot_model)
|
|
41
|
+
|
|
42
|
+
mapping_registry = registry(metadata=base.metadata, class_registry=_class_registry)
|
|
43
|
+
for model_cls in (
|
|
44
|
+
stream_model,
|
|
45
|
+
event_model,
|
|
46
|
+
snapshot_model,
|
|
47
|
+
outbox_entry_model,
|
|
48
|
+
projector_cursor_model,
|
|
49
|
+
):
|
|
50
|
+
if model_cls in _class_registry.values():
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
mapping_registry.map_declaratively(model_cls)
|
|
@@ -20,8 +20,9 @@ from sqlalchemy.orm import (
|
|
|
20
20
|
Composite,
|
|
21
21
|
InstrumentedAttribute,
|
|
22
22
|
Mapped,
|
|
23
|
+
MappedColumn,
|
|
24
|
+
declared_attr,
|
|
23
25
|
mapped_column,
|
|
24
|
-
registry,
|
|
25
26
|
relationship,
|
|
26
27
|
)
|
|
27
28
|
|
|
@@ -31,11 +32,6 @@ from event_sourcery_sqlalchemy.guid import GUID
|
|
|
31
32
|
from event_sourcery_sqlalchemy.jsonb import JSONB
|
|
32
33
|
|
|
33
34
|
|
|
34
|
-
def configure_models(base: type[Any]) -> None:
|
|
35
|
-
for model_cls in (Stream, Event, Snapshot, OutboxEntry, ProjectorCursor):
|
|
36
|
-
registry(metadata=base.metadata, class_registry={}).map_declaratively(model_cls)
|
|
37
|
-
|
|
38
|
-
|
|
39
35
|
class StreamIdComparator(Comparator[StreamId]):
|
|
40
36
|
def __init__(
|
|
41
37
|
self,
|
|
@@ -55,12 +51,17 @@ class StreamIdComparator(Comparator[StreamId]):
|
|
|
55
51
|
return and_(same_uuid, same_name, same_category) # type: ignore
|
|
56
52
|
|
|
57
53
|
|
|
58
|
-
class
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
54
|
+
class BaseStream:
|
|
55
|
+
__event_model__: type["BaseEvent"]
|
|
56
|
+
__snapshot_model__: type["BaseSnapshot"]
|
|
57
|
+
|
|
58
|
+
@declared_attr # type: ignore[arg-type]
|
|
59
|
+
@classmethod
|
|
60
|
+
def __table_args__(cls) -> tuple[Index | UniqueConstraint, ...]:
|
|
61
|
+
return (
|
|
62
|
+
UniqueConstraint("uuid", "category", "tenant_id"),
|
|
63
|
+
UniqueConstraint("name", "category", "tenant_id"),
|
|
64
|
+
)
|
|
64
65
|
|
|
65
66
|
id = mapped_column(BigInteger().with_variant(Integer(), "sqlite"), primary_key=True)
|
|
66
67
|
uuid = mapped_column(GUID(), nullable=False)
|
|
@@ -69,8 +70,22 @@ class Stream:
|
|
|
69
70
|
tenant_id = mapped_column(String(255), nullable=False)
|
|
70
71
|
version = mapped_column(BigInteger(), nullable=True)
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
@classmethod
|
|
74
|
+
def __set_mapping_information__(
|
|
75
|
+
cls, event_model: type["BaseEvent"], snapshot_model: type["BaseSnapshot"]
|
|
76
|
+
) -> None:
|
|
77
|
+
cls.__event_model__ = event_model
|
|
78
|
+
cls.__snapshot_model__ = snapshot_model
|
|
79
|
+
|
|
80
|
+
@declared_attr
|
|
81
|
+
@classmethod
|
|
82
|
+
def events(cls) -> Mapped[list["BaseEvent"]]:
|
|
83
|
+
return relationship(cls.__event_model__, back_populates="stream")
|
|
84
|
+
|
|
85
|
+
@declared_attr
|
|
86
|
+
@classmethod
|
|
87
|
+
def snapshots(cls) -> Mapped[list["BaseSnapshot"]]:
|
|
88
|
+
return relationship(cls.__snapshot_model__, back_populates="stream")
|
|
74
89
|
|
|
75
90
|
@hybrid_property
|
|
76
91
|
def stream_id(self) -> StreamId:
|
|
@@ -82,16 +97,21 @@ class Stream:
|
|
|
82
97
|
return StreamIdComparator(cls.uuid, cls.name, cls.category)
|
|
83
98
|
|
|
84
99
|
|
|
85
|
-
class
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
100
|
+
class BaseEvent:
|
|
101
|
+
__stream_model__: type["BaseStream"]
|
|
102
|
+
__tablename__: str
|
|
103
|
+
|
|
104
|
+
@declared_attr # type: ignore[arg-type]
|
|
105
|
+
@classmethod
|
|
106
|
+
def __table_args__(cls) -> tuple[Index | UniqueConstraint, ...]:
|
|
107
|
+
return (
|
|
108
|
+
Index(
|
|
109
|
+
f"ix_events_stream_id_version_{cls.__tablename__}",
|
|
110
|
+
"db_stream_id",
|
|
111
|
+
"version",
|
|
112
|
+
unique=True,
|
|
113
|
+
),
|
|
114
|
+
)
|
|
95
115
|
|
|
96
116
|
def __init__(
|
|
97
117
|
self,
|
|
@@ -109,17 +129,29 @@ class Event:
|
|
|
109
129
|
self.event_context = event_context
|
|
110
130
|
self.version = version
|
|
111
131
|
|
|
132
|
+
@classmethod
|
|
133
|
+
def __set_mapping_information__(cls, stream_model: type[BaseStream]) -> None:
|
|
134
|
+
cls.__stream_model__ = stream_model
|
|
135
|
+
|
|
136
|
+
@declared_attr
|
|
137
|
+
@classmethod
|
|
138
|
+
def _db_stream_id(cls) -> MappedColumn[Any]:
|
|
139
|
+
return mapped_column(
|
|
140
|
+
"db_stream_id",
|
|
141
|
+
BigInteger().with_variant(Integer(), "sqlite"),
|
|
142
|
+
ForeignKey(cls.__stream_model__.id),
|
|
143
|
+
nullable=False,
|
|
144
|
+
index=True,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
@declared_attr
|
|
148
|
+
@classmethod
|
|
149
|
+
def stream(cls) -> Mapped[BaseStream]:
|
|
150
|
+
return relationship(cls.__stream_model__, back_populates="events")
|
|
151
|
+
|
|
112
152
|
id = mapped_column(BigInteger().with_variant(Integer(), "sqlite"), primary_key=True)
|
|
113
153
|
version = mapped_column(Integer(), nullable=True)
|
|
114
154
|
uuid = mapped_column(GUID(), index=True, unique=True)
|
|
115
|
-
_db_stream_id = mapped_column(
|
|
116
|
-
"db_stream_id",
|
|
117
|
-
BigInteger().with_variant(Integer(), "sqlite"),
|
|
118
|
-
ForeignKey(Stream.id),
|
|
119
|
-
nullable=False,
|
|
120
|
-
index=True,
|
|
121
|
-
)
|
|
122
|
-
stream: Mapped[Stream] = relationship(Stream, back_populates="events")
|
|
123
155
|
stream_id: AssociationProxy[StreamId] = association_proxy("stream", "stream_id")
|
|
124
156
|
tenant_id: AssociationProxy[TenantId] = association_proxy("stream", "tenant_id")
|
|
125
157
|
name = mapped_column(String(200), nullable=False)
|
|
@@ -128,8 +160,8 @@ class Event:
|
|
|
128
160
|
created_at = mapped_column(DateTime(), nullable=False, index=True)
|
|
129
161
|
|
|
130
162
|
|
|
131
|
-
class
|
|
132
|
-
|
|
163
|
+
class BaseSnapshot:
|
|
164
|
+
__stream_model__: type["BaseStream"]
|
|
133
165
|
|
|
134
166
|
def __init__(
|
|
135
167
|
self,
|
|
@@ -147,16 +179,28 @@ class Snapshot:
|
|
|
147
179
|
self.event_context = event_context
|
|
148
180
|
self.version = version
|
|
149
181
|
|
|
182
|
+
@classmethod
|
|
183
|
+
def __set_mapping_information__(cls, stream_model: type[BaseStream]) -> None:
|
|
184
|
+
cls.__stream_model__ = stream_model
|
|
185
|
+
|
|
186
|
+
@declared_attr
|
|
187
|
+
@classmethod
|
|
188
|
+
def _db_stream_id(cls) -> MappedColumn[Any]:
|
|
189
|
+
return mapped_column(
|
|
190
|
+
"db_stream_id",
|
|
191
|
+
BigInteger().with_variant(Integer(), "sqlite"),
|
|
192
|
+
ForeignKey(cls.__stream_model__.id),
|
|
193
|
+
nullable=False,
|
|
194
|
+
index=True,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
@declared_attr
|
|
198
|
+
@classmethod
|
|
199
|
+
def stream(cls) -> Mapped[BaseStream]:
|
|
200
|
+
return relationship(cls.__stream_model__, back_populates="snapshots")
|
|
201
|
+
|
|
150
202
|
uuid = mapped_column(GUID, primary_key=True)
|
|
151
203
|
version = mapped_column(Integer(), nullable=False)
|
|
152
|
-
_db_stream_id = mapped_column(
|
|
153
|
-
"db_stream_id",
|
|
154
|
-
BigInteger().with_variant(Integer(), "sqlite"),
|
|
155
|
-
ForeignKey(Stream.id),
|
|
156
|
-
nullable=False,
|
|
157
|
-
index=True,
|
|
158
|
-
)
|
|
159
|
-
stream = relationship(Stream, back_populates="snapshots")
|
|
160
204
|
stream_id: AssociationProxy[StreamId] = association_proxy("stream", "stream_id")
|
|
161
205
|
name = mapped_column(String(50), nullable=False)
|
|
162
206
|
data = mapped_column(JSONB(), nullable=False)
|
|
@@ -164,9 +208,7 @@ class Snapshot:
|
|
|
164
208
|
created_at = mapped_column(DateTime(), nullable=False)
|
|
165
209
|
|
|
166
210
|
|
|
167
|
-
class
|
|
168
|
-
__tablename__ = "event_sourcery_outbox_entries"
|
|
169
|
-
|
|
211
|
+
class BaseOutboxEntry:
|
|
170
212
|
id = mapped_column(BigInteger().with_variant(Integer(), "sqlite"), primary_key=True)
|
|
171
213
|
created_at = mapped_column(DateTime(), nullable=False, index=True)
|
|
172
214
|
data = mapped_column(JSONB(), nullable=False)
|
|
@@ -175,24 +217,24 @@ class OutboxEntry:
|
|
|
175
217
|
tries_left = mapped_column(Integer(), nullable=False)
|
|
176
218
|
|
|
177
219
|
|
|
178
|
-
class
|
|
179
|
-
__tablename__
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
220
|
+
class BaseProjectorCursor:
|
|
221
|
+
__tablename__: str
|
|
222
|
+
|
|
223
|
+
@declared_attr # type: ignore[arg-type]
|
|
224
|
+
@classmethod
|
|
225
|
+
def __table_args__(cls) -> tuple[Index | UniqueConstraint, ...]:
|
|
226
|
+
return (
|
|
227
|
+
Index(
|
|
228
|
+
f"ix_name_stream_id_{cls.__tablename__}",
|
|
229
|
+
"name",
|
|
230
|
+
"stream_id",
|
|
231
|
+
"category",
|
|
232
|
+
unique=True,
|
|
233
|
+
),
|
|
234
|
+
)
|
|
189
235
|
|
|
190
236
|
id = mapped_column(BigInteger().with_variant(Integer(), "sqlite"), primary_key=True)
|
|
191
237
|
name = mapped_column(String(255), nullable=False)
|
|
192
238
|
stream_id = mapped_column(GUID(), nullable=False, index=True)
|
|
193
239
|
category = mapped_column(String(255), nullable=True)
|
|
194
240
|
version = mapped_column(BigInteger(), nullable=False)
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
Stream.events = relationship(Event, back_populates="stream")
|
|
198
|
-
Stream.snapshots = relationship(Snapshot, back_populates="stream")
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from event_sourcery_sqlalchemy.models.base import (
|
|
2
|
+
BaseEvent,
|
|
3
|
+
BaseOutboxEntry,
|
|
4
|
+
BaseProjectorCursor,
|
|
5
|
+
BaseSnapshot,
|
|
6
|
+
BaseStream,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DefaultStream(BaseStream):
|
|
11
|
+
__tablename__ = "event_sourcery_streams"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DefaultEvent(BaseEvent):
|
|
15
|
+
__tablename__ = "event_sourcery_events"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DefaultSnapshot(BaseSnapshot):
|
|
19
|
+
__tablename__ = "event_sourcery_snapshots"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DefaultOutboxEntry(BaseOutboxEntry):
|
|
23
|
+
__tablename__ = "event_sourcery_outbox_entries"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DefaultProjectorCursor(BaseProjectorCursor):
|
|
27
|
+
__tablename__ = "event_sourcery_projector_cursors"
|
|
@@ -15,7 +15,7 @@ from event_sourcery.event_store.interfaces import (
|
|
|
15
15
|
OutboxFiltererStrategy,
|
|
16
16
|
OutboxStorageStrategy,
|
|
17
17
|
)
|
|
18
|
-
from event_sourcery_sqlalchemy.models import
|
|
18
|
+
from event_sourcery_sqlalchemy.models.base import BaseOutboxEntry
|
|
19
19
|
|
|
20
20
|
logger = logging.getLogger(__name__)
|
|
21
21
|
|
|
@@ -25,6 +25,7 @@ class SqlAlchemyOutboxStorageStrategy(OutboxStorageStrategy):
|
|
|
25
25
|
_session: Session
|
|
26
26
|
_filterer: OutboxFiltererStrategy
|
|
27
27
|
_max_publish_attempts: int
|
|
28
|
+
_outbox_entry_model: type[BaseOutboxEntry]
|
|
28
29
|
|
|
29
30
|
def put_into_outbox(self, records: list[RecordedRaw]) -> None:
|
|
30
31
|
rows = []
|
|
@@ -53,15 +54,15 @@ class SqlAlchemyOutboxStorageStrategy(OutboxStorageStrategy):
|
|
|
53
54
|
if len(rows) == 0:
|
|
54
55
|
return
|
|
55
56
|
|
|
56
|
-
self._session.execute(insert(
|
|
57
|
+
self._session.execute(insert(self._outbox_entry_model), rows)
|
|
57
58
|
|
|
58
59
|
def outbox_entries(
|
|
59
60
|
self, limit: int
|
|
60
61
|
) -> Iterator[AbstractContextManager[RecordedRaw]]:
|
|
61
62
|
stmt = (
|
|
62
|
-
select(
|
|
63
|
-
.filter(
|
|
64
|
-
.order_by(
|
|
63
|
+
select(self._outbox_entry_model)
|
|
64
|
+
.filter(self._outbox_entry_model.tries_left > 0)
|
|
65
|
+
.order_by(self._outbox_entry_model.id)
|
|
65
66
|
.limit(limit)
|
|
66
67
|
.with_for_update(skip_locked=True)
|
|
67
68
|
)
|
|
@@ -71,7 +72,7 @@ class SqlAlchemyOutboxStorageStrategy(OutboxStorageStrategy):
|
|
|
71
72
|
|
|
72
73
|
@contextmanager
|
|
73
74
|
def _publish_context(
|
|
74
|
-
self, entry:
|
|
75
|
+
self, entry: BaseOutboxEntry
|
|
75
76
|
) -> Generator[RecordedRaw, None, None]:
|
|
76
77
|
raw = RawEvent(
|
|
77
78
|
uuid=UUID(entry.data["uuid"]),
|
|
@@ -9,13 +9,22 @@ from sqlalchemy.orm import Session
|
|
|
9
9
|
|
|
10
10
|
from event_sourcery.event_store import Position, RecordedRaw
|
|
11
11
|
from event_sourcery.event_store.interfaces import SubscriptionStrategy
|
|
12
|
-
from event_sourcery_sqlalchemy import dto
|
|
12
|
+
from event_sourcery_sqlalchemy import dto
|
|
13
|
+
from event_sourcery_sqlalchemy.models.base import BaseEvent, BaseStream
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
class SqlAlchemySubscriptionStrategy(SubscriptionStrategy):
|
|
16
|
-
def __init__(
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
session: Session,
|
|
20
|
+
gap_retry_interval: timedelta,
|
|
21
|
+
event_model: type[BaseEvent],
|
|
22
|
+
stream_model: type[BaseStream],
|
|
23
|
+
) -> None:
|
|
17
24
|
self._session = session
|
|
18
25
|
self._gap_retry_interval = gap_retry_interval
|
|
26
|
+
self._event_model = event_model
|
|
27
|
+
self._stream_model = stream_model
|
|
19
28
|
|
|
20
29
|
def subscribe_to_all(
|
|
21
30
|
self,
|
|
@@ -24,7 +33,9 @@ class SqlAlchemySubscriptionStrategy(SubscriptionStrategy):
|
|
|
24
33
|
timelimit: timedelta,
|
|
25
34
|
) -> Iterator[list[RecordedRaw]]:
|
|
26
35
|
return GapDetectingIterator(
|
|
27
|
-
get_batch=GetBatchToAll(
|
|
36
|
+
get_batch=GetBatchToAll(
|
|
37
|
+
self._session, batch_size, self._event_model, self._stream_model
|
|
38
|
+
),
|
|
28
39
|
gap_retry_interval=self._gap_retry_interval,
|
|
29
40
|
start_from=start_from,
|
|
30
41
|
batch_size=batch_size,
|
|
@@ -39,7 +50,13 @@ class SqlAlchemySubscriptionStrategy(SubscriptionStrategy):
|
|
|
39
50
|
category: str,
|
|
40
51
|
) -> Iterator[list[RecordedRaw]]:
|
|
41
52
|
return GapDetectingIterator(
|
|
42
|
-
get_batch=GetBatchToCategory(
|
|
53
|
+
get_batch=GetBatchToCategory(
|
|
54
|
+
self._session,
|
|
55
|
+
batch_size,
|
|
56
|
+
category,
|
|
57
|
+
self._event_model,
|
|
58
|
+
self._stream_model,
|
|
59
|
+
),
|
|
43
60
|
gap_retry_interval=self._gap_retry_interval,
|
|
44
61
|
start_from=start_from,
|
|
45
62
|
batch_size=batch_size,
|
|
@@ -54,7 +71,9 @@ class SqlAlchemySubscriptionStrategy(SubscriptionStrategy):
|
|
|
54
71
|
events: list[str],
|
|
55
72
|
) -> Iterator[list[RecordedRaw]]:
|
|
56
73
|
return GapDetectingIterator(
|
|
57
|
-
get_batch=GetBatchToEvents(
|
|
74
|
+
get_batch=GetBatchToEvents(
|
|
75
|
+
self._session, batch_size, events, self._event_model, self._stream_model
|
|
76
|
+
),
|
|
58
77
|
gap_retry_interval=self._gap_retry_interval,
|
|
59
78
|
start_from=start_from,
|
|
60
79
|
batch_size=batch_size,
|
|
@@ -63,20 +82,28 @@ class SqlAlchemySubscriptionStrategy(SubscriptionStrategy):
|
|
|
63
82
|
|
|
64
83
|
|
|
65
84
|
class GetBatch(Protocol):
|
|
66
|
-
def __call__(self, position: Position) -> list[
|
|
85
|
+
def __call__(self, position: Position) -> list[BaseEvent]: ...
|
|
67
86
|
|
|
68
87
|
|
|
69
88
|
class GetBatchToAll(GetBatch):
|
|
70
|
-
def __init__(
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
session: Session,
|
|
92
|
+
batch_size: int,
|
|
93
|
+
event_model: type[BaseEvent],
|
|
94
|
+
stream_model: type[BaseStream],
|
|
95
|
+
) -> None:
|
|
71
96
|
self._session = session
|
|
72
97
|
self._batch_size = batch_size
|
|
98
|
+
self._event_model = event_model
|
|
99
|
+
self._stream_model = stream_model
|
|
73
100
|
|
|
74
|
-
def __call__(self, position: Position) -> list[
|
|
101
|
+
def __call__(self, position: Position) -> list[BaseEvent]:
|
|
75
102
|
stmt = (
|
|
76
|
-
select(
|
|
77
|
-
.join(
|
|
78
|
-
.where(
|
|
79
|
-
.order_by(
|
|
103
|
+
select(self._event_model)
|
|
104
|
+
.join(self._stream_model)
|
|
105
|
+
.where(self._event_model.id > position)
|
|
106
|
+
.order_by(self._event_model.id)
|
|
80
107
|
.limit(self._batch_size)
|
|
81
108
|
)
|
|
82
109
|
|
|
@@ -84,18 +111,27 @@ class GetBatchToAll(GetBatch):
|
|
|
84
111
|
|
|
85
112
|
|
|
86
113
|
class GetBatchToCategory(GetBatch):
|
|
87
|
-
def __init__(
|
|
114
|
+
def __init__(
|
|
115
|
+
self,
|
|
116
|
+
session: Session,
|
|
117
|
+
batch_size: int,
|
|
118
|
+
category: str,
|
|
119
|
+
event_model: type[BaseEvent],
|
|
120
|
+
stream_model: type[BaseStream],
|
|
121
|
+
) -> None:
|
|
88
122
|
self._session = session
|
|
89
123
|
self._batch_size = batch_size
|
|
90
124
|
self._category = category
|
|
125
|
+
self._event_model = event_model
|
|
126
|
+
self._stream_model = stream_model
|
|
91
127
|
|
|
92
|
-
def __call__(self, position: Position) -> list[
|
|
128
|
+
def __call__(self, position: Position) -> list[BaseEvent]:
|
|
93
129
|
stmt = (
|
|
94
|
-
select(
|
|
95
|
-
.join(
|
|
96
|
-
.where(
|
|
97
|
-
.where(
|
|
98
|
-
.order_by(
|
|
130
|
+
select(self._event_model)
|
|
131
|
+
.join(self._stream_model)
|
|
132
|
+
.where(self._stream_model.category == self._category)
|
|
133
|
+
.where(self._event_model.id > position)
|
|
134
|
+
.order_by(self._event_model.id)
|
|
99
135
|
.limit(self._batch_size)
|
|
100
136
|
)
|
|
101
137
|
|
|
@@ -103,18 +139,27 @@ class GetBatchToCategory(GetBatch):
|
|
|
103
139
|
|
|
104
140
|
|
|
105
141
|
class GetBatchToEvents(GetBatch):
|
|
106
|
-
def __init__(
|
|
142
|
+
def __init__(
|
|
143
|
+
self,
|
|
144
|
+
session: Session,
|
|
145
|
+
batch_size: int,
|
|
146
|
+
events: list[str],
|
|
147
|
+
event_model: type[BaseEvent],
|
|
148
|
+
stream_model: type[BaseStream],
|
|
149
|
+
) -> None:
|
|
107
150
|
self._session = session
|
|
108
151
|
self._batch_size = batch_size
|
|
109
152
|
self._events = events
|
|
153
|
+
self._event_model = event_model
|
|
154
|
+
self._stream_model = stream_model
|
|
110
155
|
|
|
111
|
-
def __call__(self, position: Position) -> list[
|
|
156
|
+
def __call__(self, position: Position) -> list[BaseEvent]:
|
|
112
157
|
stmt = (
|
|
113
|
-
select(
|
|
114
|
-
.join(
|
|
115
|
-
.where(
|
|
116
|
-
.where(
|
|
117
|
-
.order_by(
|
|
158
|
+
select(self._event_model)
|
|
159
|
+
.join(self._stream_model)
|
|
160
|
+
.where(self._event_model.name.in_(self._events))
|
|
161
|
+
.where(self._event_model.id > position)
|
|
162
|
+
.order_by(self._event_model.id)
|
|
118
163
|
.limit(self._batch_size)
|
|
119
164
|
)
|
|
120
165
|
|
|
@@ -125,7 +170,7 @@ class GetBatchToEvents(GetBatch):
|
|
|
125
170
|
class Cursor:
|
|
126
171
|
position: Position
|
|
127
172
|
|
|
128
|
-
def advance(self, batch: list[
|
|
173
|
+
def advance(self, batch: list[BaseEvent]) -> None:
|
|
129
174
|
if len(batch) > 0:
|
|
130
175
|
self.position = batch[-1].id
|
|
131
176
|
|
|
@@ -159,14 +204,14 @@ class GapDetectingIterator(Iterator[list[RecordedRaw]]):
|
|
|
159
204
|
time.sleep(self._gap_retry_interval.total_seconds())
|
|
160
205
|
|
|
161
206
|
@staticmethod
|
|
162
|
-
def _is_continuous(batch: list[
|
|
207
|
+
def _is_continuous(batch: list[BaseEvent]) -> bool:
|
|
163
208
|
if len(batch) < 2:
|
|
164
209
|
return False
|
|
165
210
|
|
|
166
211
|
return cast(bool, batch[-1].id - batch[0].id + 1 == len(batch))
|
|
167
212
|
|
|
168
213
|
@staticmethod
|
|
169
|
-
def _batch_to_recorded_raw(batch: list[
|
|
214
|
+
def _batch_to_recorded_raw(batch: list[BaseEvent]) -> list[RecordedRaw]:
|
|
170
215
|
return [
|
|
171
216
|
RecordedRaw(
|
|
172
217
|
entry=dto.raw_event(event, event.stream),
|
|
File without changes
|
{python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery/__init__.py
RENAMED
|
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
|
|
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.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery_django/__init__.py
RENAMED
|
File without changes
|
{python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery_django/dto.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery_django/models.py
RENAMED
|
File without changes
|
{python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery_django/outbox.py
RENAMED
|
File without changes
|
{python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery_django/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery_kurrentdb/dto.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery_kurrentdb/outbox.py
RENAMED
|
File without changes
|
{python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery_kurrentdb/py.typed
RENAMED
|
File without changes
|
{python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery_kurrentdb/stream.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery_sqlalchemy/guid.py
RENAMED
|
File without changes
|
{python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery_sqlalchemy/jsonb.py
RENAMED
|
File without changes
|
{python_event_sourcery-0.4.0a18 → python_event_sourcery-0.4.0a19}/event_sourcery_sqlalchemy/py.typed
RENAMED
|
File without changes
|