python-event-sourcery 0.2.0__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- event_sourcery/__init__.py +0 -68
- event_sourcery/aggregate/__init__.py +7 -0
- event_sourcery/aggregate/aggregate.py +23 -0
- event_sourcery/{repository.py → aggregate/repository.py} +13 -12
- event_sourcery/event_store/__init__.py +52 -0
- event_sourcery/event_store/dispatcher.py +35 -0
- event_sourcery/event_store/event/__init__.py +26 -0
- event_sourcery/event_store/event/dto.py +62 -0
- event_sourcery/{event_registry.py → event_store/event/registry.py} +9 -8
- event_sourcery/event_store/event/serde.py +51 -0
- event_sourcery/event_store/event_store.py +128 -0
- event_sourcery/{exceptions.py → event_store/exceptions.py} +18 -7
- event_sourcery/event_store/factory.py +52 -0
- event_sourcery/event_store/in_memory.py +344 -0
- event_sourcery/event_store/interfaces.py +79 -0
- event_sourcery/event_store/outbox.py +26 -0
- event_sourcery/event_store/stream_id.py +54 -0
- event_sourcery/event_store/subscription.py +117 -0
- event_sourcery/{versioning.py → event_store/versioning.py} +1 -1
- event_sourcery/read_model/__init__.py +8 -0
- event_sourcery/{interfaces → read_model}/cursors_dao.py +1 -1
- event_sourcery/{projector.py → read_model/projector.py} +2 -4
- event_sourcery_esdb/__init__.py +86 -0
- event_sourcery_esdb/dto.py +55 -0
- event_sourcery_esdb/event_store.py +128 -0
- event_sourcery_esdb/outbox.py +94 -0
- event_sourcery_esdb/stream.py +65 -0
- event_sourcery_esdb/subscription.py +83 -0
- event_sourcery_kombu/__init__.py +38 -9
- event_sourcery_sqlalchemy/__init__.py +84 -0
- event_sourcery_sqlalchemy/{sqlalchemy_cursors_dao.py → cursors_dao.py} +12 -7
- event_sourcery_sqlalchemy/event_store.py +224 -0
- event_sourcery_sqlalchemy/guid.py +4 -9
- event_sourcery_sqlalchemy/jsonb.py +2 -6
- event_sourcery_sqlalchemy/models.py +126 -11
- event_sourcery_sqlalchemy/outbox.py +87 -0
- event_sourcery_sqlalchemy/subscription.py +33 -0
- python_event_sourcery-0.3.0.dist-info/METADATA +23 -0
- python_event_sourcery-0.3.0.dist-info/RECORD +41 -0
- {python_event_sourcery-0.2.0.dist-info → python_event_sourcery-0.3.0.dist-info}/WHEEL +1 -1
- event_sourcery/after_commit_subscriber.py +0 -10
- event_sourcery/aggregate.py +0 -21
- event_sourcery/dto/__init__.py +0 -3
- event_sourcery/dto/raw_event_dict.py +0 -13
- event_sourcery/dummy_outbox_storage_strategy.py +0 -26
- event_sourcery/event_store.py +0 -246
- event_sourcery/interfaces/__init__.py +0 -0
- event_sourcery/interfaces/base_event.py +0 -12
- event_sourcery/interfaces/event.py +0 -27
- event_sourcery/interfaces/outbox_storage_strategy.py +0 -29
- event_sourcery/interfaces/serde.py +0 -22
- event_sourcery/interfaces/storage_strategy.py +0 -49
- event_sourcery/interfaces/subscriber.py +0 -8
- event_sourcery/outbox.py +0 -42
- event_sourcery/subscription.py +0 -20
- event_sourcery/types/__init__.py +0 -0
- event_sourcery/types/stream_id.py +0 -4
- event_sourcery_pydantic/__init__.py +0 -0
- event_sourcery_pydantic/serde.py +0 -37
- event_sourcery_sqlalchemy/sqlalchemy_event_store.py +0 -220
- event_sourcery_sqlalchemy/sqlalchemy_outbox.py +0 -59
- python_event_sourcery-0.2.0.dist-info/METADATA +0 -15
- python_event_sourcery-0.2.0.dist-info/RECORD +0 -38
- {python_event_sourcery-0.2.0.dist-info → python_event_sourcery-0.3.0.dist-info}/LICENSE +0 -0
event_sourcery/__init__.py
CHANGED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
__all__ = [
|
|
2
|
-
"configure_models",
|
|
3
|
-
"get_event_store",
|
|
4
|
-
"Event",
|
|
5
|
-
"EventStore",
|
|
6
|
-
"get_outbox",
|
|
7
|
-
"Repository",
|
|
8
|
-
"Aggregate",
|
|
9
|
-
"Metadata",
|
|
10
|
-
"Context",
|
|
11
|
-
"NO_VERSIONING",
|
|
12
|
-
"Projector",
|
|
13
|
-
"Subscription",
|
|
14
|
-
]
|
|
15
|
-
|
|
16
|
-
from typing import Callable, Type
|
|
17
|
-
|
|
18
|
-
from sqlalchemy.orm import Session
|
|
19
|
-
|
|
20
|
-
from event_sourcery.aggregate import Aggregate
|
|
21
|
-
from event_sourcery.dummy_outbox_storage_strategy import DummyOutboxStorageStrategy
|
|
22
|
-
from event_sourcery.event_store import EventStore
|
|
23
|
-
from event_sourcery.interfaces.base_event import Event
|
|
24
|
-
from event_sourcery.interfaces.event import Context, Metadata
|
|
25
|
-
from event_sourcery.interfaces.event import TEvent as EventProtocol
|
|
26
|
-
from event_sourcery.interfaces.outbox_storage_strategy import OutboxStorageStrategy
|
|
27
|
-
from event_sourcery.interfaces.subscriber import Subscriber
|
|
28
|
-
from event_sourcery.outbox import Outbox
|
|
29
|
-
from event_sourcery.projector import Projector
|
|
30
|
-
from event_sourcery.repository import Repository
|
|
31
|
-
from event_sourcery.subscription import Subscription
|
|
32
|
-
from event_sourcery.types.stream_id import StreamName
|
|
33
|
-
from event_sourcery.versioning import NO_VERSIONING
|
|
34
|
-
from event_sourcery_pydantic.serde import PydanticSerde
|
|
35
|
-
from event_sourcery_sqlalchemy.models import configure_models
|
|
36
|
-
from event_sourcery_sqlalchemy.sqlalchemy_event_store import SqlAlchemyStorageStrategy
|
|
37
|
-
from event_sourcery_sqlalchemy.sqlalchemy_outbox import SqlAlchemyOutboxStorageStrategy
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def get_event_store(
|
|
41
|
-
session: Session,
|
|
42
|
-
subscriptions: dict[Type[EventProtocol], list[Subscriber]] | None = None,
|
|
43
|
-
with_outbox: bool = True,
|
|
44
|
-
) -> EventStore:
|
|
45
|
-
outbox_storage: OutboxStorageStrategy
|
|
46
|
-
if with_outbox:
|
|
47
|
-
outbox_storage = SqlAlchemyOutboxStorageStrategy(session)
|
|
48
|
-
else:
|
|
49
|
-
outbox_storage = DummyOutboxStorageStrategy()
|
|
50
|
-
|
|
51
|
-
return EventStore(
|
|
52
|
-
serde=PydanticSerde(),
|
|
53
|
-
storage_strategy=SqlAlchemyStorageStrategy(session),
|
|
54
|
-
outbox_storage_strategy=outbox_storage,
|
|
55
|
-
event_base_class=Event,
|
|
56
|
-
subscriptions=subscriptions,
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def get_outbox(
|
|
61
|
-
session: Session, publisher: Callable[[Metadata, StreamName | None], None]
|
|
62
|
-
) -> Outbox:
|
|
63
|
-
return Outbox(
|
|
64
|
-
serde=PydanticSerde(),
|
|
65
|
-
storage_strategy=SqlAlchemyOutboxStorageStrategy(session),
|
|
66
|
-
event_base_class=Event,
|
|
67
|
-
publisher=publisher,
|
|
68
|
-
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from contextlib import contextmanager
|
|
2
|
+
from typing import ClassVar, Iterator
|
|
3
|
+
|
|
4
|
+
from event_sourcery.event_store import Event
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Aggregate:
|
|
8
|
+
category: ClassVar[str]
|
|
9
|
+
_changes: list[Event]
|
|
10
|
+
|
|
11
|
+
@contextmanager
|
|
12
|
+
def __persisting_changes__(self) -> Iterator[Iterator[Event]]:
|
|
13
|
+
yield iter(getattr(self, "_changes", []))
|
|
14
|
+
self._changes = []
|
|
15
|
+
|
|
16
|
+
def __apply__(self, event: Event) -> None:
|
|
17
|
+
raise NotImplementedError
|
|
18
|
+
|
|
19
|
+
def _emit(self, event: Event) -> None:
|
|
20
|
+
if not hasattr(self, "_changes"):
|
|
21
|
+
self._changes = []
|
|
22
|
+
self.__apply__(event)
|
|
23
|
+
self._changes.append(event)
|
|
@@ -2,14 +2,9 @@ from contextlib import contextmanager
|
|
|
2
2
|
from typing import Generic, Iterator, TypeVar, cast
|
|
3
3
|
|
|
4
4
|
from event_sourcery.aggregate import Aggregate
|
|
5
|
-
from event_sourcery.event_store import EventStore
|
|
6
|
-
from event_sourcery.interfaces.base_event import Event
|
|
7
|
-
from event_sourcery.interfaces.event import Metadata
|
|
8
|
-
from event_sourcery.types.stream_id import StreamId
|
|
5
|
+
from event_sourcery.event_store import Event, EventStore, Metadata, StreamId, StreamUUID
|
|
9
6
|
|
|
10
7
|
TAggregate = TypeVar("TAggregate", bound=Aggregate)
|
|
11
|
-
|
|
12
|
-
|
|
13
8
|
TEvent = TypeVar("TEvent", bound=Event)
|
|
14
9
|
|
|
15
10
|
|
|
@@ -20,9 +15,10 @@ class Repository(Generic[TAggregate]):
|
|
|
20
15
|
@contextmanager
|
|
21
16
|
def aggregate(
|
|
22
17
|
self,
|
|
23
|
-
|
|
18
|
+
uuid: StreamUUID,
|
|
24
19
|
aggregate: TAggregate,
|
|
25
20
|
) -> Iterator[TAggregate]:
|
|
21
|
+
stream_id = StreamId(uuid=uuid, name=uuid.name, category=aggregate.category)
|
|
26
22
|
old_version = self._load(stream_id, aggregate)
|
|
27
23
|
yield aggregate
|
|
28
24
|
self._save(aggregate, old_version, stream_id)
|
|
@@ -42,13 +38,18 @@ class Repository(Generic[TAggregate]):
|
|
|
42
38
|
old_version: int,
|
|
43
39
|
stream_id: StreamId,
|
|
44
40
|
) -> None:
|
|
45
|
-
with aggregate.__persisting_changes__() as
|
|
41
|
+
with aggregate.__persisting_changes__() as pending:
|
|
46
42
|
start_from = old_version + 1
|
|
43
|
+
events = [
|
|
44
|
+
Metadata.wrap(event, version)
|
|
45
|
+
for version, event in enumerate(pending, start=start_from)
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
if not events:
|
|
49
|
+
return
|
|
50
|
+
|
|
47
51
|
self._event_store.publish(
|
|
48
|
-
*
|
|
49
|
-
Metadata.wrap(event, version)
|
|
50
|
-
for version, event in enumerate(events, start=start_from)
|
|
51
|
-
],
|
|
52
|
+
*events,
|
|
52
53
|
stream_id=stream_id,
|
|
53
54
|
expected_version=old_version,
|
|
54
55
|
)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
"Backend",
|
|
3
|
+
"BackendFactory",
|
|
4
|
+
"Dispatcher",
|
|
5
|
+
"Entry",
|
|
6
|
+
"Event",
|
|
7
|
+
"EventRegistry",
|
|
8
|
+
"EventStore",
|
|
9
|
+
"ExplicitVersioning",
|
|
10
|
+
"InMemoryBackendFactory",
|
|
11
|
+
"Listener",
|
|
12
|
+
"Metadata",
|
|
13
|
+
"NO_VERSIONING",
|
|
14
|
+
"Position",
|
|
15
|
+
"RawEvent",
|
|
16
|
+
"Recorded",
|
|
17
|
+
"RecordedRaw",
|
|
18
|
+
"StreamId",
|
|
19
|
+
"StreamUUID",
|
|
20
|
+
"TransactionalBackend",
|
|
21
|
+
"Versioning",
|
|
22
|
+
"exceptions",
|
|
23
|
+
"factory",
|
|
24
|
+
"interfaces",
|
|
25
|
+
"subscription",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
from event_sourcery.event_store import exceptions, factory, interfaces, subscription
|
|
29
|
+
from event_sourcery.event_store.dispatcher import Dispatcher, Listener
|
|
30
|
+
from event_sourcery.event_store.event import (
|
|
31
|
+
Entry,
|
|
32
|
+
Event,
|
|
33
|
+
EventRegistry,
|
|
34
|
+
Metadata,
|
|
35
|
+
Position,
|
|
36
|
+
RawEvent,
|
|
37
|
+
Recorded,
|
|
38
|
+
RecordedRaw,
|
|
39
|
+
)
|
|
40
|
+
from event_sourcery.event_store.event_store import EventStore
|
|
41
|
+
from event_sourcery.event_store.factory import (
|
|
42
|
+
Backend,
|
|
43
|
+
BackendFactory,
|
|
44
|
+
TransactionalBackend,
|
|
45
|
+
)
|
|
46
|
+
from event_sourcery.event_store.in_memory import InMemoryBackendFactory
|
|
47
|
+
from event_sourcery.event_store.stream_id import StreamId, StreamUUID
|
|
48
|
+
from event_sourcery.event_store.versioning import (
|
|
49
|
+
NO_VERSIONING,
|
|
50
|
+
ExplicitVersioning,
|
|
51
|
+
Versioning,
|
|
52
|
+
)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from typing import Callable, Type
|
|
3
|
+
|
|
4
|
+
from event_sourcery.event_store.event import (
|
|
5
|
+
Event,
|
|
6
|
+
Metadata,
|
|
7
|
+
Position,
|
|
8
|
+
RecordedRaw,
|
|
9
|
+
Serde,
|
|
10
|
+
)
|
|
11
|
+
from event_sourcery.event_store.stream_id import StreamId
|
|
12
|
+
|
|
13
|
+
Listener = Callable[[Metadata, StreamId, Position | None], None]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Dispatcher:
|
|
17
|
+
def __init__(self, serde: Serde) -> None:
|
|
18
|
+
self._listeners: dict[Type[Event], set[Listener]] = defaultdict(set)
|
|
19
|
+
self._serde = serde
|
|
20
|
+
|
|
21
|
+
def dispatch(self, *raws: RecordedRaw) -> None:
|
|
22
|
+
for raw in raws:
|
|
23
|
+
record = self._serde.deserialize_record(raw)
|
|
24
|
+
for event_type, listeners in self._listeners.items():
|
|
25
|
+
if not isinstance(record.metadata.event, event_type):
|
|
26
|
+
continue
|
|
27
|
+
for listener in listeners:
|
|
28
|
+
listener(record.metadata, record.stream_id, record.position)
|
|
29
|
+
|
|
30
|
+
def register(self, listener: Listener, to: Type[Event]) -> None:
|
|
31
|
+
self._listeners[to].add(listener)
|
|
32
|
+
|
|
33
|
+
def remove(self, listener: Listener, to: Type[Event]) -> None:
|
|
34
|
+
if listener in self._listeners[to]:
|
|
35
|
+
self._listeners[to].remove(listener)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
"Context",
|
|
3
|
+
"Entry",
|
|
4
|
+
"Event",
|
|
5
|
+
"EventRegistry",
|
|
6
|
+
"Metadata",
|
|
7
|
+
"Position",
|
|
8
|
+
"RawEvent",
|
|
9
|
+
"Recorded",
|
|
10
|
+
"RecordedRaw",
|
|
11
|
+
"Serde",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
from event_sourcery.event_store.event.dto import (
|
|
16
|
+
Context,
|
|
17
|
+
Entry,
|
|
18
|
+
Event,
|
|
19
|
+
Metadata,
|
|
20
|
+
Position,
|
|
21
|
+
RawEvent,
|
|
22
|
+
Recorded,
|
|
23
|
+
RecordedRaw,
|
|
24
|
+
)
|
|
25
|
+
from event_sourcery.event_store.event.registry import EventRegistry
|
|
26
|
+
from event_sourcery.event_store.event.serde import Serde
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Any, ClassVar, Generic, Optional, TypeAlias, TypeVar
|
|
3
|
+
from uuid import UUID, uuid4
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from event_sourcery.event_store.event.registry import EventRegistry
|
|
8
|
+
from event_sourcery.event_store.stream_id import StreamId
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RawEvent(BaseModel):
|
|
12
|
+
uuid: UUID
|
|
13
|
+
stream_id: StreamId
|
|
14
|
+
created_at: datetime
|
|
15
|
+
version: int | None = None
|
|
16
|
+
name: str
|
|
17
|
+
data: dict
|
|
18
|
+
context: dict
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
Position: TypeAlias = int
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RecordedRaw(BaseModel):
|
|
25
|
+
entry: RawEvent
|
|
26
|
+
position: Position
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Event(BaseModel, extra="forbid"):
|
|
30
|
+
__registry__: ClassVar = EventRegistry()
|
|
31
|
+
|
|
32
|
+
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
33
|
+
cls.__registry__.add(cls)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
TEvent = TypeVar("TEvent", bound=Event)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Context(BaseModel, extra="allow"):
|
|
40
|
+
correlation_id: UUID | None = None
|
|
41
|
+
causation_id: UUID | None = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Metadata(BaseModel, Generic[TEvent], extra="forbid"):
|
|
45
|
+
event: TEvent
|
|
46
|
+
version: Optional[int]
|
|
47
|
+
uuid: UUID = Field(default_factory=uuid4)
|
|
48
|
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
49
|
+
context: Context = Field(default_factory=Context)
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def wrap(cls, event: TEvent, version: int | None) -> "Metadata[TEvent]":
|
|
53
|
+
return Metadata[TEvent](event=event, version=version)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Entry(BaseModel):
|
|
57
|
+
metadata: Metadata
|
|
58
|
+
stream_id: StreamId
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Recorded(Entry):
|
|
62
|
+
position: Position
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
from typing import TYPE_CHECKING, Type
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
from event_sourcery.event_store.exceptions import (
|
|
5
|
+
ClassModuleUnavailable,
|
|
6
|
+
DuplicatedEvent,
|
|
7
|
+
)
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from event_sourcery.event_store.event.dto import Event
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
def event_name(cls: Type) -> str:
|
|
13
14
|
event_module = inspect.getmodule(cls)
|
|
14
|
-
if event_module is None:
|
|
15
|
+
if event_module is None: # pragma: no cover
|
|
15
16
|
raise ClassModuleUnavailable
|
|
16
17
|
return f"{event_module.__name__}.{cls.__qualname__}"
|
|
17
18
|
|
|
@@ -23,11 +24,11 @@ class EventRegistry:
|
|
|
23
24
|
|
|
24
25
|
def add(self, event: Type["Event"]) -> Type["Event"]:
|
|
25
26
|
if event in self._types_to_names:
|
|
26
|
-
raise
|
|
27
|
+
raise DuplicatedEvent(f"Duplicated Event detected! {event}")
|
|
27
28
|
|
|
28
29
|
name = event_name(event)
|
|
29
30
|
if name in self._names_to_types:
|
|
30
|
-
raise
|
|
31
|
+
raise DuplicatedEvent(f"Duplicated Event name detected! {name}")
|
|
31
32
|
|
|
32
33
|
self._types_to_names[event] = name
|
|
33
34
|
self._names_to_types[name] = event
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Mapping, cast
|
|
3
|
+
|
|
4
|
+
from event_sourcery.event_store.event.dto import (
|
|
5
|
+
Event,
|
|
6
|
+
Metadata,
|
|
7
|
+
RawEvent,
|
|
8
|
+
Recorded,
|
|
9
|
+
RecordedRaw,
|
|
10
|
+
)
|
|
11
|
+
from event_sourcery.event_store.event.registry import EventRegistry
|
|
12
|
+
from event_sourcery.event_store.stream_id import StreamId
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(repr=False, frozen=True)
|
|
16
|
+
class Serde:
|
|
17
|
+
registry: EventRegistry
|
|
18
|
+
|
|
19
|
+
def deserialize(self, event: RawEvent) -> Metadata:
|
|
20
|
+
event_as_dict = dict(event)
|
|
21
|
+
del event_as_dict["stream_id"]
|
|
22
|
+
del event_as_dict["name"]
|
|
23
|
+
data = cast(Mapping, event_as_dict.pop("data"))
|
|
24
|
+
event_type = self.registry.type_for_name(event.name)
|
|
25
|
+
return Metadata[event_type]( # type: ignore
|
|
26
|
+
**event_as_dict,
|
|
27
|
+
event=event_type(**data),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def deserialize_record(self, record: RecordedRaw) -> Recorded:
|
|
31
|
+
return Recorded(
|
|
32
|
+
metadata=self.deserialize(record.entry),
|
|
33
|
+
stream_id=record.entry.stream_id,
|
|
34
|
+
position=record.position,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def serialize(
|
|
38
|
+
self,
|
|
39
|
+
event: Metadata,
|
|
40
|
+
stream_id: StreamId,
|
|
41
|
+
) -> RawEvent:
|
|
42
|
+
model = cast(Event, event.event)
|
|
43
|
+
return RawEvent(
|
|
44
|
+
uuid=event.uuid,
|
|
45
|
+
stream_id=stream_id,
|
|
46
|
+
created_at=event.created_at,
|
|
47
|
+
version=event.version,
|
|
48
|
+
name=self.registry.name_for_type(type(event.event)),
|
|
49
|
+
data=model.model_dump(mode="json"),
|
|
50
|
+
context=event.context.model_dump(mode="json"),
|
|
51
|
+
)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from functools import singledispatchmethod
|
|
2
|
+
from typing import Sequence, cast
|
|
3
|
+
|
|
4
|
+
from event_sourcery.event_store.event import Event, Metadata, Position, RawEvent, Serde
|
|
5
|
+
from event_sourcery.event_store.interfaces import StorageStrategy
|
|
6
|
+
from event_sourcery.event_store.stream_id import StreamId
|
|
7
|
+
from event_sourcery.event_store.versioning import (
|
|
8
|
+
NO_VERSIONING,
|
|
9
|
+
ExplicitVersioning,
|
|
10
|
+
Versioning,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EventStore:
|
|
15
|
+
def __init__(self, storage_strategy: StorageStrategy, serde: Serde) -> None:
|
|
16
|
+
self._storage_strategy = storage_strategy
|
|
17
|
+
self._serde = serde
|
|
18
|
+
|
|
19
|
+
def load_stream(
|
|
20
|
+
self,
|
|
21
|
+
stream_id: StreamId,
|
|
22
|
+
start: int | None = None,
|
|
23
|
+
stop: int | None = None,
|
|
24
|
+
) -> Sequence[Metadata]:
|
|
25
|
+
events = self._storage_strategy.fetch_events(stream_id, start=start, stop=stop)
|
|
26
|
+
return self._deserialize_events(events)
|
|
27
|
+
|
|
28
|
+
@singledispatchmethod
|
|
29
|
+
def append(
|
|
30
|
+
self,
|
|
31
|
+
first: Metadata,
|
|
32
|
+
*events: Metadata,
|
|
33
|
+
stream_id: StreamId,
|
|
34
|
+
expected_version: int | Versioning = 0,
|
|
35
|
+
) -> None:
|
|
36
|
+
self._append(
|
|
37
|
+
stream_id=stream_id,
|
|
38
|
+
events=(first,) + events,
|
|
39
|
+
expected_version=expected_version,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
@append.register
|
|
43
|
+
def _append_events(
|
|
44
|
+
self,
|
|
45
|
+
*events: Event,
|
|
46
|
+
stream_id: StreamId,
|
|
47
|
+
expected_version: int | Versioning = 0,
|
|
48
|
+
) -> None:
|
|
49
|
+
wrapped_events = self._wrap_events(expected_version, events)
|
|
50
|
+
self.append(
|
|
51
|
+
*wrapped_events,
|
|
52
|
+
stream_id=stream_id,
|
|
53
|
+
expected_version=expected_version,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
@singledispatchmethod
|
|
57
|
+
def _wrap_events(
|
|
58
|
+
self,
|
|
59
|
+
expected_version: int,
|
|
60
|
+
events: Sequence[Event],
|
|
61
|
+
) -> Sequence[Metadata]:
|
|
62
|
+
return [
|
|
63
|
+
Metadata.wrap(event=event, version=version)
|
|
64
|
+
for version, event in enumerate(events, start=expected_version + 1)
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
@_wrap_events.register
|
|
68
|
+
def _wrap_events_versioning(
|
|
69
|
+
self, expected_version: Versioning, events: Sequence[Event]
|
|
70
|
+
) -> Sequence[Metadata]:
|
|
71
|
+
return [Metadata.wrap(event=event, version=None) for event in events]
|
|
72
|
+
|
|
73
|
+
@singledispatchmethod
|
|
74
|
+
def publish(
|
|
75
|
+
self,
|
|
76
|
+
first: Metadata,
|
|
77
|
+
*events: Metadata,
|
|
78
|
+
stream_id: StreamId,
|
|
79
|
+
expected_version: int | Versioning = 0,
|
|
80
|
+
) -> None:
|
|
81
|
+
self._append(
|
|
82
|
+
stream_id=stream_id,
|
|
83
|
+
events=(first,) + events,
|
|
84
|
+
expected_version=expected_version,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def _append(
|
|
88
|
+
self,
|
|
89
|
+
stream_id: StreamId,
|
|
90
|
+
events: Sequence[Metadata],
|
|
91
|
+
expected_version: int | Versioning,
|
|
92
|
+
) -> None:
|
|
93
|
+
new_version = events[-1].version
|
|
94
|
+
versioning: Versioning
|
|
95
|
+
if expected_version is not NO_VERSIONING:
|
|
96
|
+
versioning = ExplicitVersioning(
|
|
97
|
+
expected_version=cast(int, expected_version),
|
|
98
|
+
initial_version=cast(int, new_version),
|
|
99
|
+
)
|
|
100
|
+
else:
|
|
101
|
+
versioning = NO_VERSIONING
|
|
102
|
+
|
|
103
|
+
self._storage_strategy.insert_events(
|
|
104
|
+
stream_id=stream_id,
|
|
105
|
+
versioning=versioning,
|
|
106
|
+
events=self._serialize_events(events, stream_id),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def delete_stream(self, stream_id: StreamId) -> None:
|
|
110
|
+
self._storage_strategy.delete_stream(stream_id)
|
|
111
|
+
|
|
112
|
+
def save_snapshot(self, stream_id: StreamId, snapshot: Metadata) -> None:
|
|
113
|
+
serialized = self._serde.serialize(event=snapshot, stream_id=stream_id)
|
|
114
|
+
self._storage_strategy.save_snapshot(serialized)
|
|
115
|
+
|
|
116
|
+
def _deserialize_events(self, events: list[RawEvent]) -> list[Metadata]:
|
|
117
|
+
return [self._serde.deserialize(e) for e in events]
|
|
118
|
+
|
|
119
|
+
def _serialize_events(
|
|
120
|
+
self,
|
|
121
|
+
events: Sequence[Metadata],
|
|
122
|
+
stream_id: StreamId,
|
|
123
|
+
) -> list[RawEvent]:
|
|
124
|
+
return [self._serde.serialize(event=e, stream_id=stream_id) for e in events]
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def position(self) -> Position | None:
|
|
128
|
+
return self._storage_strategy.current_position
|
|
@@ -1,16 +1,20 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from uuid import UUID
|
|
3
|
+
|
|
4
|
+
|
|
1
5
|
class EventStoreException(Exception):
|
|
2
6
|
pass
|
|
3
7
|
|
|
4
8
|
|
|
5
|
-
class
|
|
9
|
+
class ConcurrentStreamWriteError(EventStoreException):
|
|
6
10
|
pass
|
|
7
11
|
|
|
8
12
|
|
|
9
|
-
class
|
|
13
|
+
class AnotherStreamWithThisNameButOtherIdExists(EventStoreException):
|
|
10
14
|
pass
|
|
11
15
|
|
|
12
16
|
|
|
13
|
-
class
|
|
17
|
+
class IllegalCategoryName(EventStoreException):
|
|
14
18
|
pass
|
|
15
19
|
|
|
16
20
|
|
|
@@ -18,17 +22,24 @@ class VersioningMismatch(EventStoreException):
|
|
|
18
22
|
pass
|
|
19
23
|
|
|
20
24
|
|
|
21
|
-
class
|
|
25
|
+
class ExpectedVersionUsedOnVersionlessStream(VersioningMismatch):
|
|
22
26
|
pass
|
|
23
27
|
|
|
24
28
|
|
|
25
|
-
class
|
|
29
|
+
class NoExpectedVersionGivenOnVersionedStream(VersioningMismatch):
|
|
26
30
|
pass
|
|
27
31
|
|
|
28
32
|
|
|
29
|
-
|
|
33
|
+
@dataclass
|
|
34
|
+
class IncompatibleUuidAndName(EventStoreException):
|
|
35
|
+
received: UUID
|
|
36
|
+
expected: UUID
|
|
37
|
+
name: str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ClassModuleUnavailable(Exception):
|
|
30
41
|
pass
|
|
31
42
|
|
|
32
43
|
|
|
33
|
-
class
|
|
44
|
+
class DuplicatedEvent(Exception):
|
|
34
45
|
pass
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
from typing import ContextManager, Iterator
|
|
3
|
+
|
|
4
|
+
from typing_extensions import Self
|
|
5
|
+
|
|
6
|
+
from event_sourcery.event_store import subscription
|
|
7
|
+
from event_sourcery.event_store.dispatcher import Dispatcher
|
|
8
|
+
from event_sourcery.event_store.event import EventRegistry, RawEvent, RecordedRaw, Serde
|
|
9
|
+
from event_sourcery.event_store.event_store import EventStore
|
|
10
|
+
from event_sourcery.event_store.interfaces import (
|
|
11
|
+
OutboxFiltererStrategy,
|
|
12
|
+
OutboxStorageStrategy,
|
|
13
|
+
)
|
|
14
|
+
from event_sourcery.event_store.outbox import Outbox
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def no_filter(entry: RawEvent) -> bool:
|
|
18
|
+
return True
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class NoOutboxStorageStrategy(OutboxStorageStrategy):
|
|
22
|
+
def outbox_entries(self, limit: int) -> Iterator[ContextManager[RecordedRaw]]:
|
|
23
|
+
return iter([])
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Backend:
|
|
27
|
+
serde: Serde
|
|
28
|
+
event_store: EventStore
|
|
29
|
+
outbox: Outbox
|
|
30
|
+
subscriber: subscription.PositionPhase
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TransactionalBackend(Backend):
|
|
34
|
+
in_transaction: Dispatcher
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class BackendFactory(abc.ABC):
|
|
38
|
+
@abc.abstractmethod
|
|
39
|
+
def build(self) -> Backend:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
@abc.abstractmethod
|
|
43
|
+
def with_event_registry(self, event_registry: EventRegistry) -> Self:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
@abc.abstractmethod
|
|
47
|
+
def with_outbox(self, filterer: OutboxFiltererStrategy = no_filter) -> Self:
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
@abc.abstractmethod
|
|
51
|
+
def without_outbox(self, filterer: OutboxFiltererStrategy = no_filter) -> Self:
|
|
52
|
+
pass
|