python-event-sourcery 0.2.0__tar.gz → 0.3.1a0__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.
Files changed (65) hide show
  1. python_event_sourcery-0.3.1a0/PKG-INFO +22 -0
  2. python_event_sourcery-0.3.1a0/event_sourcery/aggregate/__init__.py +7 -0
  3. python_event_sourcery-0.3.1a0/event_sourcery/aggregate/aggregate.py +23 -0
  4. {python-event-sourcery-0.2.0/event_sourcery → python_event_sourcery-0.3.1a0/event_sourcery/aggregate}/repository.py +13 -12
  5. python_event_sourcery-0.3.1a0/event_sourcery/event_store/__init__.py +52 -0
  6. python_event_sourcery-0.3.1a0/event_sourcery/event_store/dispatcher.py +35 -0
  7. python_event_sourcery-0.3.1a0/event_sourcery/event_store/event/__init__.py +26 -0
  8. python_event_sourcery-0.3.1a0/event_sourcery/event_store/event/dto.py +62 -0
  9. python-event-sourcery-0.2.0/event_sourcery/event_registry.py → python_event_sourcery-0.3.1a0/event_sourcery/event_store/event/registry.py +9 -8
  10. python_event_sourcery-0.3.1a0/event_sourcery/event_store/event/serde.py +51 -0
  11. python_event_sourcery-0.3.1a0/event_sourcery/event_store/event_store.py +128 -0
  12. {python-event-sourcery-0.2.0/event_sourcery → python_event_sourcery-0.3.1a0/event_sourcery/event_store}/exceptions.py +18 -7
  13. python_event_sourcery-0.3.1a0/event_sourcery/event_store/factory.py +52 -0
  14. python_event_sourcery-0.3.1a0/event_sourcery/event_store/in_memory.py +344 -0
  15. python_event_sourcery-0.3.1a0/event_sourcery/event_store/interfaces.py +79 -0
  16. python_event_sourcery-0.3.1a0/event_sourcery/event_store/outbox.py +26 -0
  17. python_event_sourcery-0.3.1a0/event_sourcery/event_store/stream_id.py +54 -0
  18. python_event_sourcery-0.3.1a0/event_sourcery/event_store/subscription.py +117 -0
  19. {python-event-sourcery-0.2.0/event_sourcery → python_event_sourcery-0.3.1a0/event_sourcery/event_store}/versioning.py +1 -1
  20. python_event_sourcery-0.3.1a0/event_sourcery/read_model/__init__.py +8 -0
  21. {python-event-sourcery-0.2.0/event_sourcery/interfaces → python_event_sourcery-0.3.1a0/event_sourcery/read_model}/cursors_dao.py +1 -1
  22. {python-event-sourcery-0.2.0/event_sourcery → python_event_sourcery-0.3.1a0/event_sourcery/read_model}/projector.py +2 -4
  23. python_event_sourcery-0.3.1a0/event_sourcery_esdb/__init__.py +86 -0
  24. python_event_sourcery-0.3.1a0/event_sourcery_esdb/dto.py +55 -0
  25. python_event_sourcery-0.3.1a0/event_sourcery_esdb/event_store.py +128 -0
  26. python_event_sourcery-0.3.1a0/event_sourcery_esdb/outbox.py +94 -0
  27. python_event_sourcery-0.3.1a0/event_sourcery_esdb/stream.py +65 -0
  28. python_event_sourcery-0.3.1a0/event_sourcery_esdb/subscription.py +83 -0
  29. {python-event-sourcery-0.2.0 → python_event_sourcery-0.3.1a0}/event_sourcery_kombu/__init__.py +38 -9
  30. python_event_sourcery-0.3.1a0/event_sourcery_sqlalchemy/__init__.py +84 -0
  31. python-event-sourcery-0.2.0/event_sourcery_sqlalchemy/sqlalchemy_cursors_dao.py → python_event_sourcery-0.3.1a0/event_sourcery_sqlalchemy/cursors_dao.py +12 -7
  32. python_event_sourcery-0.3.1a0/event_sourcery_sqlalchemy/event_store.py +224 -0
  33. {python-event-sourcery-0.2.0 → python_event_sourcery-0.3.1a0}/event_sourcery_sqlalchemy/guid.py +4 -9
  34. {python-event-sourcery-0.2.0 → python_event_sourcery-0.3.1a0}/event_sourcery_sqlalchemy/jsonb.py +2 -6
  35. python_event_sourcery-0.3.1a0/event_sourcery_sqlalchemy/models.py +195 -0
  36. python_event_sourcery-0.3.1a0/event_sourcery_sqlalchemy/outbox.py +87 -0
  37. python_event_sourcery-0.3.1a0/event_sourcery_sqlalchemy/subscription.py +33 -0
  38. {python-event-sourcery-0.2.0 → python_event_sourcery-0.3.1a0}/pyproject.toml +29 -7
  39. python-event-sourcery-0.2.0/PKG-INFO +0 -15
  40. python-event-sourcery-0.2.0/event_sourcery/__init__.py +0 -68
  41. python-event-sourcery-0.2.0/event_sourcery/after_commit_subscriber.py +0 -10
  42. python-event-sourcery-0.2.0/event_sourcery/aggregate.py +0 -21
  43. python-event-sourcery-0.2.0/event_sourcery/dto/__init__.py +0 -3
  44. python-event-sourcery-0.2.0/event_sourcery/dto/raw_event_dict.py +0 -13
  45. python-event-sourcery-0.2.0/event_sourcery/dummy_outbox_storage_strategy.py +0 -26
  46. python-event-sourcery-0.2.0/event_sourcery/event_store.py +0 -246
  47. python-event-sourcery-0.2.0/event_sourcery/interfaces/base_event.py +0 -12
  48. python-event-sourcery-0.2.0/event_sourcery/interfaces/event.py +0 -27
  49. python-event-sourcery-0.2.0/event_sourcery/interfaces/outbox_storage_strategy.py +0 -29
  50. python-event-sourcery-0.2.0/event_sourcery/interfaces/serde.py +0 -22
  51. python-event-sourcery-0.2.0/event_sourcery/interfaces/storage_strategy.py +0 -49
  52. python-event-sourcery-0.2.0/event_sourcery/interfaces/subscriber.py +0 -8
  53. python-event-sourcery-0.2.0/event_sourcery/outbox.py +0 -42
  54. python-event-sourcery-0.2.0/event_sourcery/subscription.py +0 -20
  55. python-event-sourcery-0.2.0/event_sourcery/types/__init__.py +0 -0
  56. python-event-sourcery-0.2.0/event_sourcery/types/stream_id.py +0 -4
  57. python-event-sourcery-0.2.0/event_sourcery_pydantic/__init__.py +0 -0
  58. python-event-sourcery-0.2.0/event_sourcery_pydantic/serde.py +0 -37
  59. python-event-sourcery-0.2.0/event_sourcery_sqlalchemy/__init__.py +0 -0
  60. python-event-sourcery-0.2.0/event_sourcery_sqlalchemy/models.py +0 -80
  61. python-event-sourcery-0.2.0/event_sourcery_sqlalchemy/sqlalchemy_event_store.py +0 -220
  62. python-event-sourcery-0.2.0/event_sourcery_sqlalchemy/sqlalchemy_outbox.py +0 -59
  63. python-event-sourcery-0.2.0/setup.py +0 -40
  64. {python-event-sourcery-0.2.0 → python_event_sourcery-0.3.1a0}/LICENSE +0 -0
  65. {python-event-sourcery-0.2.0/event_sourcery/interfaces → python_event_sourcery-0.3.1a0/event_sourcery}/__init__.py +0 -0
@@ -0,0 +1,22 @@
1
+ Metadata-Version: 2.1
2
+ Name: python-event-sourcery
3
+ Version: 0.3.1a0
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
+ Provides-Extra: cloudevents
14
+ Provides-Extra: django
15
+ Provides-Extra: kombu
16
+ Requires-Dist: SQLAlchemy (>=2.0)
17
+ Requires-Dist: cloudevents ; extra == "cloudevents"
18
+ Requires-Dist: django (>=4,<6) ; extra == "django"
19
+ Requires-Dist: esdbclient (>=1.0b3)
20
+ Requires-Dist: kombu ; extra == "kombu"
21
+ Requires-Dist: more-itertools
22
+ Requires-Dist: pydantic (>=2.0,<3)
@@ -0,0 +1,7 @@
1
+ __all__ = [
2
+ "Aggregate",
3
+ "Repository",
4
+ ]
5
+
6
+ from event_sourcery.aggregate.aggregate import Aggregate
7
+ from event_sourcery.aggregate.repository import Repository
@@ -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
- stream_id: StreamId,
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 events:
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
- if TYPE_CHECKING:
5
- from event_sourcery.interfaces.base_event import Event
6
-
4
+ from event_sourcery.event_store.exceptions import (
5
+ ClassModuleUnavailable,
6
+ DuplicatedEvent,
7
+ )
7
8
 
8
- class ClassModuleUnavailable(Exception):
9
- pass
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 Exception(f"Duplicated Event detected! {event}")
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 Exception(f"Duplicated Event name detected! {name}")
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 NoEventsToAppend(EventStoreException):
9
+ class ConcurrentStreamWriteError(EventStoreException):
6
10
  pass
7
11
 
8
12
 
9
- class ConcurrentStreamWriteError(EventStoreException):
13
+ class AnotherStreamWithThisNameButOtherIdExists(EventStoreException):
10
14
  pass
11
15
 
12
16
 
13
- class Misconfiguration(EventStoreException):
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 EitherStreamIdOrStreamNameIsRequired(EventStoreException):
25
+ class ExpectedVersionUsedOnVersionlessStream(VersioningMismatch):
22
26
  pass
23
27
 
24
28
 
25
- class AnotherStreamWithThisNameButOtherIdExists(EventStoreException):
29
+ class NoExpectedVersionGivenOnVersionedStream(VersioningMismatch):
26
30
  pass
27
31
 
28
32
 
29
- class ExpectedVersionUsedOnVersionlessStream(VersioningMismatch):
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 NoExpectedVersionGivenOnVersionedStream(VersioningMismatch):
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