python-event-sourcery 0.1.8__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- python_event_sourcery-0.3.0/PKG-INFO +23 -0
- python_event_sourcery-0.3.0/event_sourcery/aggregate/__init__.py +7 -0
- python_event_sourcery-0.3.0/event_sourcery/aggregate/aggregate.py +23 -0
- {python-event-sourcery-0.1.8/event_sourcery → python_event_sourcery-0.3.0/event_sourcery/aggregate}/repository.py +13 -12
- python_event_sourcery-0.3.0/event_sourcery/event_store/__init__.py +52 -0
- python_event_sourcery-0.3.0/event_sourcery/event_store/dispatcher.py +35 -0
- python_event_sourcery-0.3.0/event_sourcery/event_store/event/__init__.py +26 -0
- python_event_sourcery-0.3.0/event_sourcery/event_store/event/dto.py +62 -0
- python-event-sourcery-0.1.8/event_sourcery/event_registry.py → python_event_sourcery-0.3.0/event_sourcery/event_store/event/registry.py +9 -8
- python_event_sourcery-0.3.0/event_sourcery/event_store/event/serde.py +51 -0
- python_event_sourcery-0.3.0/event_sourcery/event_store/event_store.py +128 -0
- python_event_sourcery-0.3.0/event_sourcery/event_store/exceptions.py +45 -0
- python_event_sourcery-0.3.0/event_sourcery/event_store/factory.py +52 -0
- python_event_sourcery-0.3.0/event_sourcery/event_store/in_memory.py +344 -0
- python_event_sourcery-0.3.0/event_sourcery/event_store/interfaces.py +79 -0
- python_event_sourcery-0.3.0/event_sourcery/event_store/outbox.py +26 -0
- python_event_sourcery-0.3.0/event_sourcery/event_store/stream_id.py +54 -0
- python_event_sourcery-0.3.0/event_sourcery/event_store/subscription.py +117 -0
- {python-event-sourcery-0.1.8/event_sourcery → python_event_sourcery-0.3.0/event_sourcery/event_store}/versioning.py +1 -1
- python_event_sourcery-0.3.0/event_sourcery/read_model/__init__.py +8 -0
- python_event_sourcery-0.3.0/event_sourcery/read_model/cursors_dao.py +30 -0
- python_event_sourcery-0.3.0/event_sourcery/read_model/projector.py +59 -0
- python_event_sourcery-0.3.0/event_sourcery_esdb/__init__.py +86 -0
- python_event_sourcery-0.3.0/event_sourcery_esdb/dto.py +55 -0
- python_event_sourcery-0.3.0/event_sourcery_esdb/event_store.py +128 -0
- python_event_sourcery-0.3.0/event_sourcery_esdb/outbox.py +94 -0
- python_event_sourcery-0.3.0/event_sourcery_esdb/stream.py +65 -0
- python_event_sourcery-0.3.0/event_sourcery_esdb/subscription.py +83 -0
- python_event_sourcery-0.3.0/event_sourcery_kombu/__init__.py +155 -0
- python_event_sourcery-0.3.0/event_sourcery_sqlalchemy/__init__.py +84 -0
- python_event_sourcery-0.3.0/event_sourcery_sqlalchemy/cursors_dao.py +80 -0
- python_event_sourcery-0.3.0/event_sourcery_sqlalchemy/event_store.py +224 -0
- {python-event-sourcery-0.1.8 → python_event_sourcery-0.3.0}/event_sourcery_sqlalchemy/guid.py +5 -9
- {python-event-sourcery-0.1.8 → python_event_sourcery-0.3.0}/event_sourcery_sqlalchemy/jsonb.py +2 -6
- python_event_sourcery-0.3.0/event_sourcery_sqlalchemy/models.py +195 -0
- python_event_sourcery-0.3.0/event_sourcery_sqlalchemy/outbox.py +87 -0
- python_event_sourcery-0.3.0/event_sourcery_sqlalchemy/subscription.py +33 -0
- python_event_sourcery-0.3.0/pyproject.toml +80 -0
- python-event-sourcery-0.1.8/PKG-INFO +0 -13
- python-event-sourcery-0.1.8/event_sourcery/__init__.py +0 -60
- python-event-sourcery-0.1.8/event_sourcery/after_commit_subscriber.py +0 -10
- python-event-sourcery-0.1.8/event_sourcery/aggregate.py +0 -21
- python-event-sourcery-0.1.8/event_sourcery/dto/__init__.py +0 -3
- python-event-sourcery-0.1.8/event_sourcery/dto/raw_event_dict.py +0 -13
- python-event-sourcery-0.1.8/event_sourcery/dummy_outbox_storage_strategy.py +0 -21
- python-event-sourcery-0.1.8/event_sourcery/event_store.py +0 -200
- python-event-sourcery-0.1.8/event_sourcery/exceptions.py +0 -26
- python-event-sourcery-0.1.8/event_sourcery/interfaces/base_event.py +0 -12
- python-event-sourcery-0.1.8/event_sourcery/interfaces/event.py +0 -27
- python-event-sourcery-0.1.8/event_sourcery/interfaces/outbox_storage_strategy.py +0 -24
- python-event-sourcery-0.1.8/event_sourcery/interfaces/serde.py +0 -22
- python-event-sourcery-0.1.8/event_sourcery/interfaces/storage_strategy.py +0 -42
- python-event-sourcery-0.1.8/event_sourcery/interfaces/subscriber.py +0 -8
- python-event-sourcery-0.1.8/event_sourcery/outbox.py +0 -37
- python-event-sourcery-0.1.8/event_sourcery/types/__init__.py +0 -0
- python-event-sourcery-0.1.8/event_sourcery/types/stream_id.py +0 -3
- python-event-sourcery-0.1.8/event_sourcery_pydantic/__init__.py +0 -0
- python-event-sourcery-0.1.8/event_sourcery_pydantic/serde.py +0 -37
- python-event-sourcery-0.1.8/event_sourcery_sqlalchemy/__init__.py +0 -0
- python-event-sourcery-0.1.8/event_sourcery_sqlalchemy/models.py +0 -61
- python-event-sourcery-0.1.8/event_sourcery_sqlalchemy/sqlalchemy_event_store.py +0 -183
- python-event-sourcery-0.1.8/event_sourcery_sqlalchemy/sqlalchemy_outbox.py +0 -53
- python-event-sourcery-0.1.8/pyproject.toml +0 -52
- python-event-sourcery-0.1.8/setup.py +0 -35
- {python-event-sourcery-0.1.8 → python_event_sourcery-0.3.0}/LICENSE +0 -0
- {python-event-sourcery-0.1.8/event_sourcery/interfaces → python_event_sourcery-0.3.0/event_sourcery}/__init__.py +0 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: python-event-sourcery
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary:
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Your Name
|
|
7
|
+
Author-email: you@example.com
|
|
8
|
+
Requires-Python: >=3.10,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Provides-Extra: cloudevents
|
|
15
|
+
Provides-Extra: django
|
|
16
|
+
Provides-Extra: kombu
|
|
17
|
+
Requires-Dist: SQLAlchemy (>=2.0)
|
|
18
|
+
Requires-Dist: cloudevents ; extra == "cloudevents"
|
|
19
|
+
Requires-Dist: django (>=4,<6) ; extra == "django"
|
|
20
|
+
Requires-Dist: esdbclient (>=1.0b3)
|
|
21
|
+
Requires-Dist: kombu ; extra == "kombu"
|
|
22
|
+
Requires-Dist: more-itertools
|
|
23
|
+
Requires-Dist: pydantic (>=2.0,<3)
|
|
@@ -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
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from uuid import UUID
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class EventStoreException(Exception):
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ConcurrentStreamWriteError(EventStoreException):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AnotherStreamWithThisNameButOtherIdExists(EventStoreException):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class IllegalCategoryName(EventStoreException):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class VersioningMismatch(EventStoreException):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ExpectedVersionUsedOnVersionlessStream(VersioningMismatch):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class NoExpectedVersionGivenOnVersionedStream(VersioningMismatch):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class IncompatibleUuidAndName(EventStoreException):
|
|
35
|
+
received: UUID
|
|
36
|
+
expected: UUID
|
|
37
|
+
name: str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ClassModuleUnavailable(Exception):
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class DuplicatedEvent(Exception):
|
|
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
|