python-event-sourcery 0.4.0a3__tar.gz → 0.4.0a5__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.0a3 → python_event_sourcery-0.4.0a5}/PKG-INFO +1 -1
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery/aggregate/repository.py +9 -3
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery/event_store/__init__.py +2 -2
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery/event_store/dispatcher.py +4 -4
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery/event_store/event/__init__.py +2 -2
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery/event_store/event/dto.py +27 -4
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery/event_store/event/registry.py +7 -0
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery/event_store/event/serde.py +5 -5
- python_event_sourcery-0.4.0a5/event_sourcery/event_store/event_store.py +202 -0
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery/event_store/factory.py +2 -0
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery/event_store/in_memory.py +2 -0
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery/event_store/outbox.py +1 -1
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery/read_model/projector.py +12 -12
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery_sqlalchemy/cursors_dao.py +1 -1
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery_sqlalchemy/event_store.py +1 -1
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/pyproject.toml +2 -1
- python_event_sourcery-0.4.0a3/event_sourcery/event_store/event_store.py +0 -129
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/LICENSE +0 -0
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery/__init__.py +0 -0
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery/aggregate/__init__.py +0 -0
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery/aggregate/aggregate.py +0 -0
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery/event_store/exceptions.py +0 -0
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery/event_store/interfaces.py +0 -0
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery/event_store/stream_id.py +0 -0
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery/event_store/subscription.py +0 -0
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery/event_store/versioning.py +0 -0
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery/py.typed +0 -0
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery/read_model/__init__.py +0 -0
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery/read_model/cursors_dao.py +0 -0
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery_esdb/__init__.py +0 -0
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery_esdb/dto.py +0 -0
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery_esdb/event_store.py +0 -0
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery_esdb/outbox.py +0 -0
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery_esdb/py.typed +0 -0
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery_esdb/stream.py +0 -0
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery_esdb/subscription.py +0 -0
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery_sqlalchemy/__init__.py +0 -0
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery_sqlalchemy/dto.py +0 -0
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery_sqlalchemy/guid.py +0 -0
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery_sqlalchemy/jsonb.py +0 -0
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery_sqlalchemy/models.py +0 -0
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery_sqlalchemy/outbox.py +0 -0
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery_sqlalchemy/py.typed +0 -0
- {python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery_sqlalchemy/subscription.py +0 -0
|
@@ -3,7 +3,13 @@ from contextlib import contextmanager
|
|
|
3
3
|
from typing import Generic, TypeVar, cast
|
|
4
4
|
|
|
5
5
|
from event_sourcery.aggregate import Aggregate
|
|
6
|
-
from event_sourcery.event_store import
|
|
6
|
+
from event_sourcery.event_store import (
|
|
7
|
+
Event,
|
|
8
|
+
EventStore,
|
|
9
|
+
StreamId,
|
|
10
|
+
StreamUUID,
|
|
11
|
+
WrappedEvent,
|
|
12
|
+
)
|
|
7
13
|
|
|
8
14
|
TAggregate = TypeVar("TAggregate", bound=Aggregate)
|
|
9
15
|
TEvent = TypeVar("TEvent", bound=Event)
|
|
@@ -42,14 +48,14 @@ class Repository(Generic[TAggregate]):
|
|
|
42
48
|
with aggregate.__persisting_changes__() as pending:
|
|
43
49
|
start_from = old_version + 1
|
|
44
50
|
events = [
|
|
45
|
-
|
|
51
|
+
WrappedEvent.wrap(event, version)
|
|
46
52
|
for version, event in enumerate(pending, start=start_from)
|
|
47
53
|
]
|
|
48
54
|
|
|
49
55
|
if not events:
|
|
50
56
|
return
|
|
51
57
|
|
|
52
|
-
self._event_store.
|
|
58
|
+
self._event_store.append(
|
|
53
59
|
*events,
|
|
54
60
|
stream_id=stream_id,
|
|
55
61
|
expected_version=old_version,
|
|
@@ -9,7 +9,7 @@ __all__ = [
|
|
|
9
9
|
"ExplicitVersioning",
|
|
10
10
|
"InMemoryBackendFactory",
|
|
11
11
|
"Listener",
|
|
12
|
-
"
|
|
12
|
+
"WrappedEvent",
|
|
13
13
|
"NO_VERSIONING",
|
|
14
14
|
"Position",
|
|
15
15
|
"RawEvent",
|
|
@@ -31,11 +31,11 @@ from event_sourcery.event_store.event import (
|
|
|
31
31
|
Entry,
|
|
32
32
|
Event,
|
|
33
33
|
EventRegistry,
|
|
34
|
-
Metadata,
|
|
35
34
|
Position,
|
|
36
35
|
RawEvent,
|
|
37
36
|
Recorded,
|
|
38
37
|
RecordedRaw,
|
|
38
|
+
WrappedEvent,
|
|
39
39
|
)
|
|
40
40
|
from event_sourcery.event_store.event_store import EventStore
|
|
41
41
|
from event_sourcery.event_store.factory import (
|
|
@@ -3,14 +3,14 @@ from collections.abc import Callable
|
|
|
3
3
|
|
|
4
4
|
from event_sourcery.event_store.event import (
|
|
5
5
|
Event,
|
|
6
|
-
Metadata,
|
|
7
6
|
Position,
|
|
8
7
|
RecordedRaw,
|
|
9
8
|
Serde,
|
|
9
|
+
WrappedEvent,
|
|
10
10
|
)
|
|
11
11
|
from event_sourcery.event_store.stream_id import StreamId
|
|
12
12
|
|
|
13
|
-
Listener = Callable[[
|
|
13
|
+
Listener = Callable[[WrappedEvent, StreamId, Position | None], None]
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class Dispatcher:
|
|
@@ -22,10 +22,10 @@ class Dispatcher:
|
|
|
22
22
|
for raw in raws:
|
|
23
23
|
record = self._serde.deserialize_record(raw)
|
|
24
24
|
for event_type, listeners in self._listeners.items():
|
|
25
|
-
if not isinstance(record.
|
|
25
|
+
if not isinstance(record.wrapped_event.event, event_type):
|
|
26
26
|
continue
|
|
27
27
|
for listener in listeners:
|
|
28
|
-
listener(record.
|
|
28
|
+
listener(record.wrapped_event, record.stream_id, record.position)
|
|
29
29
|
|
|
30
30
|
def register(self, listener: Listener, to: type[Event]) -> None:
|
|
31
31
|
self._listeners[to].add(listener)
|
|
@@ -3,7 +3,7 @@ __all__ = [
|
|
|
3
3
|
"Entry",
|
|
4
4
|
"Event",
|
|
5
5
|
"EventRegistry",
|
|
6
|
-
"
|
|
6
|
+
"WrappedEvent",
|
|
7
7
|
"Position",
|
|
8
8
|
"RawEvent",
|
|
9
9
|
"Recorded",
|
|
@@ -16,11 +16,11 @@ from event_sourcery.event_store.event.dto import (
|
|
|
16
16
|
Context,
|
|
17
17
|
Entry,
|
|
18
18
|
Event,
|
|
19
|
-
Metadata,
|
|
20
19
|
Position,
|
|
21
20
|
RawEvent,
|
|
22
21
|
Recorded,
|
|
23
22
|
RecordedRaw,
|
|
23
|
+
WrappedEvent,
|
|
24
24
|
)
|
|
25
25
|
from event_sourcery.event_store.event.registry import EventRegistry
|
|
26
26
|
from event_sourcery.event_store.event.serde import Serde
|
|
@@ -27,6 +27,15 @@ class RecordedRaw(BaseModel):
|
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
class Event(BaseModel, extra="forbid"):
|
|
30
|
+
"""Base class for all events.
|
|
31
|
+
|
|
32
|
+
Example usage:
|
|
33
|
+
```
|
|
34
|
+
class OrderCancelled(Event):
|
|
35
|
+
order_id: OrderId
|
|
36
|
+
```
|
|
37
|
+
"""
|
|
38
|
+
|
|
30
39
|
__registry__: ClassVar = EventRegistry()
|
|
31
40
|
|
|
32
41
|
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
@@ -41,7 +50,21 @@ class Context(BaseModel, extra="allow"):
|
|
|
41
50
|
causation_id: UUID | None = None
|
|
42
51
|
|
|
43
52
|
|
|
44
|
-
class
|
|
53
|
+
class WrappedEvent(BaseModel, Generic[TEvent], extra="forbid"):
|
|
54
|
+
"""Wrapper for events with all relevant metadata.
|
|
55
|
+
|
|
56
|
+
Returned from EventStore when loading events from a stream.
|
|
57
|
+
|
|
58
|
+
Example usage:
|
|
59
|
+
```
|
|
60
|
+
class OrderCancelled(Event):
|
|
61
|
+
order_id: OrderId
|
|
62
|
+
|
|
63
|
+
event = OrderCancelled(order_id=OrderId("#123"))
|
|
64
|
+
wrapped_event = WrappedEvent.wrap(event, version=1)
|
|
65
|
+
```
|
|
66
|
+
"""
|
|
67
|
+
|
|
45
68
|
event: TEvent
|
|
46
69
|
version: int | None
|
|
47
70
|
uuid: UUID = Field(default_factory=uuid4)
|
|
@@ -49,12 +72,12 @@ class Metadata(BaseModel, Generic[TEvent], extra="forbid"):
|
|
|
49
72
|
context: Context = Field(default_factory=Context)
|
|
50
73
|
|
|
51
74
|
@classmethod
|
|
52
|
-
def wrap(cls, event: TEvent, version: int | None) -> "
|
|
53
|
-
return
|
|
75
|
+
def wrap(cls, event: TEvent, version: int | None) -> "WrappedEvent[TEvent]":
|
|
76
|
+
return WrappedEvent[TEvent](event=event, version=version)
|
|
54
77
|
|
|
55
78
|
|
|
56
79
|
class Entry(BaseModel):
|
|
57
|
-
|
|
80
|
+
wrapped_event: WrappedEvent
|
|
58
81
|
stream_id: StreamId
|
|
59
82
|
|
|
60
83
|
|
|
@@ -18,11 +18,18 @@ def event_name(cls: type) -> str:
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class EventRegistry:
|
|
21
|
+
"""Keeps mappings between event types and their names.
|
|
22
|
+
|
|
23
|
+
Normally, there is no need to use it directly. If one needs to have multiple
|
|
24
|
+
registries or wants more granular control, they can pass an instance
|
|
25
|
+
of EventRegistry to BackendFactory."""
|
|
26
|
+
|
|
21
27
|
def __init__(self) -> None:
|
|
22
28
|
self._types_to_names: dict[type[Event], str] = {}
|
|
23
29
|
self._names_to_types: dict[str, type[Event]] = {}
|
|
24
30
|
|
|
25
31
|
def add(self, event: type["Event"]) -> type["Event"]:
|
|
32
|
+
"""Add event subclass to the registry."""
|
|
26
33
|
if event in self._types_to_names:
|
|
27
34
|
raise DuplicatedEvent(f"Duplicated Event detected! {event}")
|
|
28
35
|
|
|
@@ -4,10 +4,10 @@ from typing import cast
|
|
|
4
4
|
|
|
5
5
|
from event_sourcery.event_store.event.dto import (
|
|
6
6
|
Event,
|
|
7
|
-
Metadata,
|
|
8
7
|
RawEvent,
|
|
9
8
|
Recorded,
|
|
10
9
|
RecordedRaw,
|
|
10
|
+
WrappedEvent,
|
|
11
11
|
)
|
|
12
12
|
from event_sourcery.event_store.event.registry import EventRegistry
|
|
13
13
|
from event_sourcery.event_store.stream_id import StreamId
|
|
@@ -17,27 +17,27 @@ from event_sourcery.event_store.stream_id import StreamId
|
|
|
17
17
|
class Serde:
|
|
18
18
|
registry: EventRegistry
|
|
19
19
|
|
|
20
|
-
def deserialize(self, event: RawEvent) ->
|
|
20
|
+
def deserialize(self, event: RawEvent) -> WrappedEvent:
|
|
21
21
|
event_as_dict = dict(event)
|
|
22
22
|
del event_as_dict["stream_id"]
|
|
23
23
|
del event_as_dict["name"]
|
|
24
24
|
data = cast(Mapping, event_as_dict.pop("data"))
|
|
25
25
|
event_type = self.registry.type_for_name(event.name)
|
|
26
|
-
return
|
|
26
|
+
return WrappedEvent[event_type]( # type: ignore
|
|
27
27
|
**event_as_dict,
|
|
28
28
|
event=event_type(**data),
|
|
29
29
|
)
|
|
30
30
|
|
|
31
31
|
def deserialize_record(self, record: RecordedRaw) -> Recorded:
|
|
32
32
|
return Recorded(
|
|
33
|
-
|
|
33
|
+
wrapped_event=self.deserialize(record.entry),
|
|
34
34
|
stream_id=record.entry.stream_id,
|
|
35
35
|
position=record.position,
|
|
36
36
|
)
|
|
37
37
|
|
|
38
38
|
def serialize(
|
|
39
39
|
self,
|
|
40
|
-
event:
|
|
40
|
+
event: WrappedEvent,
|
|
41
41
|
stream_id: StreamId,
|
|
42
42
|
) -> RawEvent:
|
|
43
43
|
model = cast(Event, event.event)
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from functools import singledispatchmethod
|
|
3
|
+
from typing import cast
|
|
4
|
+
|
|
5
|
+
from event_sourcery.event_store.event import (
|
|
6
|
+
Event,
|
|
7
|
+
Position,
|
|
8
|
+
RawEvent,
|
|
9
|
+
Serde,
|
|
10
|
+
WrappedEvent,
|
|
11
|
+
)
|
|
12
|
+
from event_sourcery.event_store.interfaces import StorageStrategy
|
|
13
|
+
from event_sourcery.event_store.stream_id import StreamId
|
|
14
|
+
from event_sourcery.event_store.versioning import (
|
|
15
|
+
NO_VERSIONING,
|
|
16
|
+
ExplicitVersioning,
|
|
17
|
+
Versioning,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EventStore:
|
|
22
|
+
"""API for working with events."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, storage_strategy: StorageStrategy, serde: Serde) -> None:
|
|
25
|
+
self._storage_strategy = storage_strategy
|
|
26
|
+
self._serde = serde
|
|
27
|
+
|
|
28
|
+
def load_stream(
|
|
29
|
+
self,
|
|
30
|
+
stream_id: StreamId,
|
|
31
|
+
start: int | None = None,
|
|
32
|
+
stop: int | None = None,
|
|
33
|
+
) -> Sequence[WrappedEvent]:
|
|
34
|
+
"""Loads events from a given stream.
|
|
35
|
+
|
|
36
|
+
Examples:
|
|
37
|
+
>>> load_stream(stream_id=StreamId(name="not_existing_stream"))
|
|
38
|
+
[]
|
|
39
|
+
>>> load_stream(stream_id=StreamId(name="existing_stream"))
|
|
40
|
+
[WrappedEvent(..., version=1), ..., WrappedEvent(..., version=3)]
|
|
41
|
+
>>> load_stream(stream_id=StreamId(name="existing_stream"), start=2, stop=3)
|
|
42
|
+
[WrappedEvent(..., version=2)]
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
stream_id: The stream identifier to load events from.
|
|
46
|
+
start: The stream version to start loading from (including).
|
|
47
|
+
stop: The stream version to stop loading at (excluding).
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
A sequence of events or empty list if the stream doesn't exist.
|
|
51
|
+
"""
|
|
52
|
+
events = self._storage_strategy.fetch_events(stream_id, start=start, stop=stop)
|
|
53
|
+
return self._deserialize_events(events)
|
|
54
|
+
|
|
55
|
+
@singledispatchmethod
|
|
56
|
+
def append(
|
|
57
|
+
self,
|
|
58
|
+
first: WrappedEvent,
|
|
59
|
+
*events: WrappedEvent,
|
|
60
|
+
stream_id: StreamId,
|
|
61
|
+
expected_version: int | Versioning = 0,
|
|
62
|
+
) -> None:
|
|
63
|
+
"""Appends events to a stream with a given ID.
|
|
64
|
+
|
|
65
|
+
Implements optimistic locking to ensure stream wasn't modified since last read.
|
|
66
|
+
To use it, pass the expected version of the stream.
|
|
67
|
+
|
|
68
|
+
Examples:
|
|
69
|
+
>>> append(WrappedEvent(...), stream_id=StreamId())
|
|
70
|
+
None
|
|
71
|
+
>>> append(WrappedEvent(...), stream_id=StreamId(), expected_version=1)
|
|
72
|
+
None
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
first: The first event to append (WrappedEvent or Event).
|
|
76
|
+
*events: The rest of the events to append (same type as first argument).
|
|
77
|
+
stream_id: The stream identifier to append events to.
|
|
78
|
+
expected_version: The expected version of the stream
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
None
|
|
82
|
+
"""
|
|
83
|
+
self._append(
|
|
84
|
+
stream_id=stream_id,
|
|
85
|
+
events=(first, *events),
|
|
86
|
+
expected_version=expected_version,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
@append.register
|
|
90
|
+
def _append_events(
|
|
91
|
+
self,
|
|
92
|
+
*events: Event,
|
|
93
|
+
stream_id: StreamId,
|
|
94
|
+
expected_version: int | Versioning = 0,
|
|
95
|
+
) -> None:
|
|
96
|
+
wrapped_events = self._wrap_events(expected_version, events)
|
|
97
|
+
self.append(
|
|
98
|
+
*wrapped_events,
|
|
99
|
+
stream_id=stream_id,
|
|
100
|
+
expected_version=expected_version,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
@singledispatchmethod
|
|
104
|
+
def _wrap_events(
|
|
105
|
+
self,
|
|
106
|
+
expected_version: int,
|
|
107
|
+
events: Sequence[Event],
|
|
108
|
+
) -> Sequence[WrappedEvent]:
|
|
109
|
+
return [
|
|
110
|
+
WrappedEvent.wrap(event=event, version=version)
|
|
111
|
+
for version, event in enumerate(events, start=expected_version + 1)
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
@_wrap_events.register
|
|
115
|
+
def _wrap_events_versioning(
|
|
116
|
+
self, expected_version: Versioning, events: Sequence[Event]
|
|
117
|
+
) -> Sequence[WrappedEvent]:
|
|
118
|
+
return [WrappedEvent.wrap(event=event, version=None) for event in events]
|
|
119
|
+
|
|
120
|
+
def _append(
|
|
121
|
+
self,
|
|
122
|
+
stream_id: StreamId,
|
|
123
|
+
events: Sequence[WrappedEvent],
|
|
124
|
+
expected_version: int | Versioning,
|
|
125
|
+
) -> None:
|
|
126
|
+
new_version = events[-1].version
|
|
127
|
+
versioning: Versioning
|
|
128
|
+
if expected_version is not NO_VERSIONING:
|
|
129
|
+
versioning = ExplicitVersioning(
|
|
130
|
+
expected_version=cast(int, expected_version),
|
|
131
|
+
initial_version=cast(int, new_version),
|
|
132
|
+
)
|
|
133
|
+
else:
|
|
134
|
+
versioning = NO_VERSIONING
|
|
135
|
+
|
|
136
|
+
self._storage_strategy.insert_events(
|
|
137
|
+
stream_id=stream_id,
|
|
138
|
+
versioning=versioning,
|
|
139
|
+
events=self._serialize_events(events, stream_id),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def delete_stream(self, stream_id: StreamId) -> None:
|
|
143
|
+
"""Deletes a stream with a given ID.
|
|
144
|
+
|
|
145
|
+
If a stream does not exist, this method does nothing.
|
|
146
|
+
|
|
147
|
+
Examples:
|
|
148
|
+
>>> delete_stream(StreamId())
|
|
149
|
+
None
|
|
150
|
+
>>> delete_stream(StreamId(name="not_existing_stream"))
|
|
151
|
+
None
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
stream_id: The stream identifier to delete.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
None
|
|
158
|
+
"""
|
|
159
|
+
self._storage_strategy.delete_stream(stream_id)
|
|
160
|
+
|
|
161
|
+
def save_snapshot(self, stream_id: StreamId, snapshot: WrappedEvent) -> None:
|
|
162
|
+
"""Saves a snapshot of the stream.
|
|
163
|
+
|
|
164
|
+
Examples:
|
|
165
|
+
>>> save_snapshot(StreamId(), WrappedEvent(...))
|
|
166
|
+
None
|
|
167
|
+
>>> save_snapshot(StreamId(name="not_existing_stream"), WrappedEvent(...))
|
|
168
|
+
None
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
stream_id: The stream identifier to save the snapshot.
|
|
172
|
+
snapshot: The snapshot to save.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
None
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
serialized = self._serde.serialize(event=snapshot, stream_id=stream_id)
|
|
179
|
+
self._storage_strategy.save_snapshot(serialized)
|
|
180
|
+
|
|
181
|
+
def _deserialize_events(self, events: list[RawEvent]) -> list[WrappedEvent]:
|
|
182
|
+
return [self._serde.deserialize(e) for e in events]
|
|
183
|
+
|
|
184
|
+
def _serialize_events(
|
|
185
|
+
self,
|
|
186
|
+
events: Sequence[WrappedEvent],
|
|
187
|
+
stream_id: StreamId,
|
|
188
|
+
) -> list[RawEvent]:
|
|
189
|
+
return [self._serde.serialize(event=e, stream_id=stream_id) for e in events]
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def position(self) -> Position | None:
|
|
193
|
+
"""Returns the current position of the event store.
|
|
194
|
+
|
|
195
|
+
Examples:
|
|
196
|
+
>>> position
|
|
197
|
+
None # nothing was saved yet
|
|
198
|
+
>>> position
|
|
199
|
+
Position(15) # Some events were saved
|
|
200
|
+
|
|
201
|
+
"""
|
|
202
|
+
return self._storage_strategy.current_position
|
|
@@ -298,6 +298,8 @@ class Config(BaseModel):
|
|
|
298
298
|
|
|
299
299
|
@dataclass(repr=False)
|
|
300
300
|
class InMemoryBackendFactory(BackendFactory):
|
|
301
|
+
"""Lightweight in-memory backend factory for testing and development."""
|
|
302
|
+
|
|
301
303
|
serde = Serde(Event.__registry__)
|
|
302
304
|
|
|
303
305
|
_config: Config = field(default_factory=Config)
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
from collections.abc import Callable
|
|
2
2
|
from typing import TypeAlias
|
|
3
3
|
|
|
4
|
-
from event_sourcery.event_store import Event, EventStore,
|
|
4
|
+
from event_sourcery.event_store import Event, EventStore, StreamId, WrappedEvent
|
|
5
5
|
from event_sourcery.read_model.cursors_dao import CursorsDao
|
|
6
6
|
|
|
7
|
-
ReadModel: TypeAlias = Callable[[
|
|
7
|
+
ReadModel: TypeAlias = Callable[[WrappedEvent, StreamId], None]
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class Projector:
|
|
@@ -23,38 +23,38 @@ class Projector:
|
|
|
23
23
|
self._cursors_dao = cursors_dao
|
|
24
24
|
self._read_model = read_model
|
|
25
25
|
|
|
26
|
-
def project(self,
|
|
27
|
-
if
|
|
26
|
+
def project(self, wrapped_event: WrappedEvent[Event], stream_id: StreamId) -> None:
|
|
27
|
+
if wrapped_event.version is None:
|
|
28
28
|
raise self.CantProjectUnversionedEvent
|
|
29
29
|
|
|
30
30
|
try:
|
|
31
31
|
self._cursors_dao.increment(
|
|
32
|
-
name=self._name, stream_id=stream_id, version=
|
|
32
|
+
name=self._name, stream_id=stream_id, version=wrapped_event.version
|
|
33
33
|
)
|
|
34
|
-
self._read_model(
|
|
34
|
+
self._read_model(wrapped_event, stream_id)
|
|
35
35
|
except self._cursors_dao.StreamNotTracked:
|
|
36
36
|
self._cursors_dao.put_at(
|
|
37
|
-
name=self._name, stream_id=stream_id, version=
|
|
37
|
+
name=self._name, stream_id=stream_id, version=wrapped_event.version
|
|
38
38
|
)
|
|
39
39
|
missed_events = self._event_store.load_stream(
|
|
40
|
-
stream_id=stream_id, stop=
|
|
40
|
+
stream_id=stream_id, stop=wrapped_event.version
|
|
41
41
|
)
|
|
42
42
|
for event in missed_events:
|
|
43
43
|
self._read_model(event, stream_id)
|
|
44
44
|
|
|
45
|
-
self._read_model(
|
|
45
|
+
self._read_model(wrapped_event, stream_id)
|
|
46
46
|
except self._cursors_dao.BehindStream as exc:
|
|
47
47
|
self._cursors_dao.move_to(
|
|
48
|
-
name=self._name, stream_id=stream_id, version=
|
|
48
|
+
name=self._name, stream_id=stream_id, version=wrapped_event.version
|
|
49
49
|
)
|
|
50
50
|
missed_events = self._event_store.load_stream(
|
|
51
51
|
stream_id=stream_id,
|
|
52
52
|
start=exc.current_version + 1,
|
|
53
|
-
stop=
|
|
53
|
+
stop=wrapped_event.version,
|
|
54
54
|
)
|
|
55
55
|
for event in missed_events:
|
|
56
56
|
self._read_model(event, stream_id)
|
|
57
57
|
|
|
58
|
-
self._read_model(
|
|
58
|
+
self._read_model(wrapped_event, stream_id)
|
|
59
59
|
except self._cursors_dao.AheadOfStream:
|
|
60
60
|
return
|
|
@@ -38,7 +38,7 @@ class SqlAlchemyCursorsDao(CursorsDao):
|
|
|
38
38
|
.values({ProjectorCursor.version: version})
|
|
39
39
|
)
|
|
40
40
|
result = self._session.execute(update_stmt)
|
|
41
|
-
if result.rowcount == 1:
|
|
41
|
+
if result.rowcount == 1:
|
|
42
42
|
return
|
|
43
43
|
else:
|
|
44
44
|
current_version = self._current_version(name, stream_id)
|
|
@@ -155,7 +155,7 @@ class SqlAlchemyStorageStrategy(StorageStrategy):
|
|
|
155
155
|
)
|
|
156
156
|
result = self._session.execute(bump_version_stmt)
|
|
157
157
|
|
|
158
|
-
if result.rowcount != 1:
|
|
158
|
+
if result.rowcount != 1:
|
|
159
159
|
# optimistic lock failed
|
|
160
160
|
raise ConcurrentStreamWriteError
|
|
161
161
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "python-event-sourcery"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.0a5"
|
|
4
4
|
description = ""
|
|
5
5
|
authors = ["Your Name <you@example.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -26,6 +26,7 @@ pytest-cov = "*"
|
|
|
26
26
|
pytest-django = {version = "*"}
|
|
27
27
|
mkdocs-material = "^8.3.9"
|
|
28
28
|
psycopg2-binary = "^2.9.3"
|
|
29
|
+
mkdocstrings = {version = "*", extras = ["python"]}
|
|
29
30
|
|
|
30
31
|
[tool.poetry.extras]
|
|
31
32
|
sqlalchemy = ["SQLAlchemy"]
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
from collections.abc import Sequence
|
|
2
|
-
from functools import singledispatchmethod
|
|
3
|
-
from typing import cast
|
|
4
|
-
|
|
5
|
-
from event_sourcery.event_store.event import Event, Metadata, Position, RawEvent, Serde
|
|
6
|
-
from event_sourcery.event_store.interfaces import StorageStrategy
|
|
7
|
-
from event_sourcery.event_store.stream_id import StreamId
|
|
8
|
-
from event_sourcery.event_store.versioning import (
|
|
9
|
-
NO_VERSIONING,
|
|
10
|
-
ExplicitVersioning,
|
|
11
|
-
Versioning,
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class EventStore:
|
|
16
|
-
def __init__(self, storage_strategy: StorageStrategy, serde: Serde) -> None:
|
|
17
|
-
self._storage_strategy = storage_strategy
|
|
18
|
-
self._serde = serde
|
|
19
|
-
|
|
20
|
-
def load_stream(
|
|
21
|
-
self,
|
|
22
|
-
stream_id: StreamId,
|
|
23
|
-
start: int | None = None,
|
|
24
|
-
stop: int | None = None,
|
|
25
|
-
) -> Sequence[Metadata]:
|
|
26
|
-
events = self._storage_strategy.fetch_events(stream_id, start=start, stop=stop)
|
|
27
|
-
return self._deserialize_events(events)
|
|
28
|
-
|
|
29
|
-
@singledispatchmethod
|
|
30
|
-
def append(
|
|
31
|
-
self,
|
|
32
|
-
first: Metadata,
|
|
33
|
-
*events: Metadata,
|
|
34
|
-
stream_id: StreamId,
|
|
35
|
-
expected_version: int | Versioning = 0,
|
|
36
|
-
) -> None:
|
|
37
|
-
self._append(
|
|
38
|
-
stream_id=stream_id,
|
|
39
|
-
events=(first, *events),
|
|
40
|
-
expected_version=expected_version,
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
@append.register
|
|
44
|
-
def _append_events(
|
|
45
|
-
self,
|
|
46
|
-
*events: Event,
|
|
47
|
-
stream_id: StreamId,
|
|
48
|
-
expected_version: int | Versioning = 0,
|
|
49
|
-
) -> None:
|
|
50
|
-
wrapped_events = self._wrap_events(expected_version, events)
|
|
51
|
-
self.append(
|
|
52
|
-
*wrapped_events,
|
|
53
|
-
stream_id=stream_id,
|
|
54
|
-
expected_version=expected_version,
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
@singledispatchmethod
|
|
58
|
-
def _wrap_events(
|
|
59
|
-
self,
|
|
60
|
-
expected_version: int,
|
|
61
|
-
events: Sequence[Event],
|
|
62
|
-
) -> Sequence[Metadata]:
|
|
63
|
-
return [
|
|
64
|
-
Metadata.wrap(event=event, version=version)
|
|
65
|
-
for version, event in enumerate(events, start=expected_version + 1)
|
|
66
|
-
]
|
|
67
|
-
|
|
68
|
-
@_wrap_events.register
|
|
69
|
-
def _wrap_events_versioning(
|
|
70
|
-
self, expected_version: Versioning, events: Sequence[Event]
|
|
71
|
-
) -> Sequence[Metadata]:
|
|
72
|
-
return [Metadata.wrap(event=event, version=None) for event in events]
|
|
73
|
-
|
|
74
|
-
@singledispatchmethod
|
|
75
|
-
def publish(
|
|
76
|
-
self,
|
|
77
|
-
first: Metadata,
|
|
78
|
-
*events: Metadata,
|
|
79
|
-
stream_id: StreamId,
|
|
80
|
-
expected_version: int | Versioning = 0,
|
|
81
|
-
) -> None:
|
|
82
|
-
self._append(
|
|
83
|
-
stream_id=stream_id,
|
|
84
|
-
events=(first, *events),
|
|
85
|
-
expected_version=expected_version,
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
def _append(
|
|
89
|
-
self,
|
|
90
|
-
stream_id: StreamId,
|
|
91
|
-
events: Sequence[Metadata],
|
|
92
|
-
expected_version: int | Versioning,
|
|
93
|
-
) -> None:
|
|
94
|
-
new_version = events[-1].version
|
|
95
|
-
versioning: Versioning
|
|
96
|
-
if expected_version is not NO_VERSIONING:
|
|
97
|
-
versioning = ExplicitVersioning(
|
|
98
|
-
expected_version=cast(int, expected_version),
|
|
99
|
-
initial_version=cast(int, new_version),
|
|
100
|
-
)
|
|
101
|
-
else:
|
|
102
|
-
versioning = NO_VERSIONING
|
|
103
|
-
|
|
104
|
-
self._storage_strategy.insert_events(
|
|
105
|
-
stream_id=stream_id,
|
|
106
|
-
versioning=versioning,
|
|
107
|
-
events=self._serialize_events(events, stream_id),
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
def delete_stream(self, stream_id: StreamId) -> None:
|
|
111
|
-
self._storage_strategy.delete_stream(stream_id)
|
|
112
|
-
|
|
113
|
-
def save_snapshot(self, stream_id: StreamId, snapshot: Metadata) -> None:
|
|
114
|
-
serialized = self._serde.serialize(event=snapshot, stream_id=stream_id)
|
|
115
|
-
self._storage_strategy.save_snapshot(serialized)
|
|
116
|
-
|
|
117
|
-
def _deserialize_events(self, events: list[RawEvent]) -> list[Metadata]:
|
|
118
|
-
return [self._serde.deserialize(e) for e in events]
|
|
119
|
-
|
|
120
|
-
def _serialize_events(
|
|
121
|
-
self,
|
|
122
|
-
events: Sequence[Metadata],
|
|
123
|
-
stream_id: StreamId,
|
|
124
|
-
) -> list[RawEvent]:
|
|
125
|
-
return [self._serde.serialize(event=e, stream_id=stream_id) for e in events]
|
|
126
|
-
|
|
127
|
-
@property
|
|
128
|
-
def position(self) -> Position | None:
|
|
129
|
-
return self._storage_strategy.current_position
|
|
File without changes
|
|
File without changes
|
{python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery/aggregate/__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
|
{python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery_esdb/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery_esdb/event_store.py
RENAMED
|
File without changes
|
{python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery_esdb/outbox.py
RENAMED
|
File without changes
|
{python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery_esdb/py.typed
RENAMED
|
File without changes
|
{python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery_esdb/stream.py
RENAMED
|
File without changes
|
{python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery_esdb/subscription.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery_sqlalchemy/dto.py
RENAMED
|
File without changes
|
{python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery_sqlalchemy/guid.py
RENAMED
|
File without changes
|
{python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery_sqlalchemy/jsonb.py
RENAMED
|
File without changes
|
{python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery_sqlalchemy/models.py
RENAMED
|
File without changes
|
{python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery_sqlalchemy/outbox.py
RENAMED
|
File without changes
|
{python_event_sourcery-0.4.0a3 → python_event_sourcery-0.4.0a5}/event_sourcery_sqlalchemy/py.typed
RENAMED
|
File without changes
|
|
File without changes
|