python-event-sourcery 0.4.0a2__tar.gz → 0.4.0a4__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 (43) hide show
  1. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/PKG-INFO +1 -1
  2. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery/aggregate/aggregate.py +2 -1
  3. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery/aggregate/repository.py +3 -2
  4. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery/event_store/dispatcher.py +4 -4
  5. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery/event_store/event/dto.py +23 -2
  6. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery/event_store/event/registry.py +14 -7
  7. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery/event_store/event/serde.py +2 -1
  8. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery/event_store/event_store.py +84 -16
  9. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery/event_store/factory.py +7 -2
  10. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery/event_store/in_memory.py +9 -5
  11. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery/event_store/interfaces.py +7 -4
  12. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery/event_store/outbox.py +1 -1
  13. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery/event_store/subscription.py +10 -12
  14. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery/read_model/projector.py +2 -1
  15. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery_esdb/__init__.py +4 -2
  16. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery_esdb/event_store.py +5 -6
  17. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery_esdb/outbox.py +8 -7
  18. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery_esdb/stream.py +2 -2
  19. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery_esdb/subscription.py +1 -1
  20. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery_sqlalchemy/__init__.py +3 -3
  21. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery_sqlalchemy/event_store.py +6 -4
  22. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery_sqlalchemy/guid.py +7 -8
  23. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery_sqlalchemy/jsonb.py +3 -3
  24. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery_sqlalchemy/models.py +2 -2
  25. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery_sqlalchemy/outbox.py +6 -3
  26. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery_sqlalchemy/subscription.py +3 -3
  27. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/pyproject.toml +41 -11
  28. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/LICENSE +0 -0
  29. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery/__init__.py +0 -0
  30. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery/aggregate/__init__.py +0 -0
  31. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery/event_store/__init__.py +0 -0
  32. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery/event_store/event/__init__.py +0 -0
  33. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery/event_store/exceptions.py +0 -0
  34. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery/event_store/stream_id.py +0 -0
  35. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery/event_store/versioning.py +0 -0
  36. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery/py.typed +0 -0
  37. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery/read_model/__init__.py +0 -0
  38. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery/read_model/cursors_dao.py +0 -0
  39. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery_esdb/dto.py +0 -0
  40. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery_esdb/py.typed +0 -0
  41. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery_sqlalchemy/cursors_dao.py +0 -0
  42. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery_sqlalchemy/dto.py +0 -0
  43. {python_event_sourcery-0.4.0a2 → python_event_sourcery-0.4.0a4}/event_sourcery_sqlalchemy/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-event-sourcery
3
- Version: 0.4.0a2
3
+ Version: 0.4.0a4
4
4
  Summary:
5
5
  License: MIT
6
6
  Author: Your Name
@@ -1,5 +1,6 @@
1
+ from collections.abc import Iterator
1
2
  from contextlib import contextmanager
2
- from typing import ClassVar, Iterator
3
+ from typing import ClassVar
3
4
 
4
5
  from event_sourcery.event_store import Event
5
6
 
@@ -1,5 +1,6 @@
1
+ from collections.abc import Iterator
1
2
  from contextlib import contextmanager
2
- from typing import Generic, Iterator, TypeVar, cast
3
+ from typing import Generic, TypeVar, cast
3
4
 
4
5
  from event_sourcery.aggregate import Aggregate
5
6
  from event_sourcery.event_store import Event, EventStore, Metadata, StreamId, StreamUUID
@@ -48,7 +49,7 @@ class Repository(Generic[TAggregate]):
48
49
  if not events:
49
50
  return
50
51
 
51
- self._event_store.publish(
52
+ self._event_store.append(
52
53
  *events,
53
54
  stream_id=stream_id,
54
55
  expected_version=old_version,
@@ -1,5 +1,5 @@
1
1
  from collections import defaultdict
2
- from typing import Callable, Type
2
+ from collections.abc import Callable
3
3
 
4
4
  from event_sourcery.event_store.event import (
5
5
  Event,
@@ -15,7 +15,7 @@ Listener = Callable[[Metadata, StreamId, Position | None], None]
15
15
 
16
16
  class Dispatcher:
17
17
  def __init__(self, serde: Serde) -> None:
18
- self._listeners: dict[Type[Event], set[Listener]] = defaultdict(set)
18
+ self._listeners: dict[type[Event], set[Listener]] = defaultdict(set)
19
19
  self._serde = serde
20
20
 
21
21
  def dispatch(self, *raws: RecordedRaw) -> None:
@@ -27,9 +27,9 @@ class Dispatcher:
27
27
  for listener in listeners:
28
28
  listener(record.metadata, record.stream_id, record.position)
29
29
 
30
- def register(self, listener: Listener, to: Type[Event]) -> None:
30
+ def register(self, listener: Listener, to: type[Event]) -> None:
31
31
  self._listeners[to].add(listener)
32
32
 
33
- def remove(self, listener: Listener, to: Type[Event]) -> None:
33
+ def remove(self, listener: Listener, to: type[Event]) -> None:
34
34
  if listener in self._listeners[to]:
35
35
  self._listeners[to].remove(listener)
@@ -1,5 +1,5 @@
1
1
  from datetime import datetime
2
- from typing import Any, ClassVar, Generic, Optional, TypeAlias, TypeVar
2
+ from typing import Any, ClassVar, Generic, TypeAlias, TypeVar
3
3
  from uuid import UUID, uuid4
4
4
 
5
5
  from pydantic import BaseModel, Field
@@ -27,6 +27,14 @@ 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
+ """
30
38
  __registry__: ClassVar = EventRegistry()
31
39
 
32
40
  def __init_subclass__(cls, **kwargs: Any) -> None:
@@ -42,8 +50,21 @@ class Context(BaseModel, extra="allow"):
42
50
 
43
51
 
44
52
  class Metadata(BaseModel, Generic[TEvent], extra="forbid"):
53
+ """Wrapper for events with all relevant metadata.
54
+
55
+ Returned from EventStore when loading events from a stream.
56
+
57
+ Example usage:
58
+ ```
59
+ class OrderCancelled(Event):
60
+ order_id: OrderId
61
+
62
+ event = OrderCancelled(order_id=OrderId("#123"))
63
+ metadata = Metadata.wrap(event, version=1)
64
+ ```
65
+ """
45
66
  event: TEvent
46
- version: Optional[int]
67
+ version: int | None
47
68
  uuid: UUID = Field(default_factory=uuid4)
48
69
  created_at: datetime = Field(default_factory=datetime.utcnow)
49
70
  context: Context = Field(default_factory=Context)
@@ -1,5 +1,5 @@
1
1
  import inspect
2
- from typing import TYPE_CHECKING, Type
2
+ from typing import TYPE_CHECKING
3
3
 
4
4
  from event_sourcery.event_store.exceptions import (
5
5
  ClassModuleUnavailable,
@@ -10,7 +10,7 @@ if TYPE_CHECKING:
10
10
  from event_sourcery.event_store.event.dto import Event
11
11
 
12
12
 
13
- def event_name(cls: Type) -> str:
13
+ def event_name(cls: type) -> str:
14
14
  event_module = inspect.getmodule(cls)
15
15
  if event_module is None: # pragma: no cover
16
16
  raise ClassModuleUnavailable
@@ -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
- self._types_to_names: dict[Type["Event"], str] = {}
23
- self._names_to_types: dict[str, Type["Event"]] = {}
28
+ self._types_to_names: dict[type[Event], str] = {}
29
+ self._names_to_types: dict[str, type[Event]] = {}
24
30
 
25
- def add(self, event: Type["Event"]) -> Type["Event"]:
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
 
@@ -34,8 +41,8 @@ class EventRegistry:
34
41
  self._names_to_types[name] = event
35
42
  return event # for use as a decorator
36
43
 
37
- def type_for_name(self, name: str) -> Type["Event"]:
44
+ def type_for_name(self, name: str) -> type["Event"]:
38
45
  return self._names_to_types[name]
39
46
 
40
- def name_for_type(self, event: Type["Event"]) -> str:
47
+ def name_for_type(self, event: type["Event"]) -> str:
41
48
  return self._types_to_names[event]
@@ -1,5 +1,6 @@
1
+ from collections.abc import Mapping
1
2
  from dataclasses import dataclass
2
- from typing import Mapping, cast
3
+ from typing import cast
3
4
 
4
5
  from event_sourcery.event_store.event.dto import (
5
6
  Event,
@@ -1,5 +1,6 @@
1
+ from collections.abc import Sequence
1
2
  from functools import singledispatchmethod
2
- from typing import Sequence, cast
3
+ from typing import cast
3
4
 
4
5
  from event_sourcery.event_store.event import Event, Metadata, Position, RawEvent, Serde
5
6
  from event_sourcery.event_store.interfaces import StorageStrategy
@@ -12,6 +13,8 @@ from event_sourcery.event_store.versioning import (
12
13
 
13
14
 
14
15
  class EventStore:
16
+ """API for working with events."""
17
+
15
18
  def __init__(self, storage_strategy: StorageStrategy, serde: Serde) -> None:
16
19
  self._storage_strategy = storage_strategy
17
20
  self._serde = serde
@@ -22,6 +25,24 @@ class EventStore:
22
25
  start: int | None = None,
23
26
  stop: int | None = None,
24
27
  ) -> Sequence[Metadata]:
28
+ """Loads events from a given stream.
29
+
30
+ Examples:
31
+ >>> load_stream(stream_id=StreamId(name="not_existing_stream"))
32
+ []
33
+ >>> load_stream(stream_id=StreamId(name="existing_stream"))
34
+ [Metadata(..., version=1), Metadata(...), Metadata(..., version=3)]
35
+ >>> load_stream(stream_id=StreamId(name="existing_stream"), start=2, stop=3)
36
+ [Metadata(..., version=2)]
37
+
38
+ Args:
39
+ stream_id: The stream identifier to load events from.
40
+ start: The stream version to start loading from (including).
41
+ stop: The stream version to stop loading at (excluding).
42
+
43
+ Returns:
44
+ A sequence of events or empty list if the stream doesn't exist.
45
+ """
25
46
  events = self._storage_strategy.fetch_events(stream_id, start=start, stop=stop)
26
47
  return self._deserialize_events(events)
27
48
 
@@ -33,9 +54,29 @@ class EventStore:
33
54
  stream_id: StreamId,
34
55
  expected_version: int | Versioning = 0,
35
56
  ) -> None:
57
+ """Appends events to a stream with a given ID.
58
+
59
+ Implements optimistic locking to ensure stream wasn't modified since last read.
60
+ To use it, pass the expected version of the stream.
61
+
62
+ Examples:
63
+ >>> append(Metadata(...), stream_id=StreamId())
64
+ None
65
+ >>> append(Metadata(...), stream_id=StreamId(), expected_version=1)
66
+ None
67
+
68
+ Args:
69
+ first: The first event to append (Metadata or Event).
70
+ *events: The rest of the events to append (same type as first argument).
71
+ stream_id: The stream identifier to append events to.
72
+ expected_version: The expected version of the stream
73
+
74
+ Returns:
75
+ None
76
+ """
36
77
  self._append(
37
78
  stream_id=stream_id,
38
- events=(first,) + events,
79
+ events=(first, *events),
39
80
  expected_version=expected_version,
40
81
  )
41
82
 
@@ -70,20 +111,6 @@ class EventStore:
70
111
  ) -> Sequence[Metadata]:
71
112
  return [Metadata.wrap(event=event, version=None) for event in events]
72
113
 
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
114
  def _append(
88
115
  self,
89
116
  stream_id: StreamId,
@@ -107,9 +134,41 @@ class EventStore:
107
134
  )
108
135
 
109
136
  def delete_stream(self, stream_id: StreamId) -> None:
137
+ """Deletes a stream with a given ID.
138
+
139
+ If a stream does not exist, this method does nothing.
140
+
141
+ Examples:
142
+ >>> delete_stream(StreamId())
143
+ None
144
+ >>> delete_stream(StreamId(name="not_existing_stream"))
145
+ None
146
+
147
+ Args:
148
+ stream_id: The stream identifier to delete.
149
+
150
+ Returns:
151
+ None
152
+ """
110
153
  self._storage_strategy.delete_stream(stream_id)
111
154
 
112
155
  def save_snapshot(self, stream_id: StreamId, snapshot: Metadata) -> None:
156
+ """Saves a snapshot of the stream.
157
+
158
+ Examples:
159
+ >>> save_snapshot(StreamId(), Metadata(...))
160
+ None
161
+ >>> save_snapshot(StreamId(name="not_existing_stream"), Metadata(...))
162
+ None
163
+
164
+ Args:
165
+ stream_id: The stream identifier to save the snapshot.
166
+ snapshot: The snapshot to save.
167
+
168
+ Returns:
169
+ None
170
+ """
171
+
113
172
  serialized = self._serde.serialize(event=snapshot, stream_id=stream_id)
114
173
  self._storage_strategy.save_snapshot(serialized)
115
174
 
@@ -125,4 +184,13 @@ class EventStore:
125
184
 
126
185
  @property
127
186
  def position(self) -> Position | None:
187
+ """Returns the current position of the event store.
188
+
189
+ Examples:
190
+ >>> position
191
+ None # nothing was saved yet
192
+ >>> position
193
+ Position(15) # Some events were saved
194
+
195
+ """
128
196
  return self._storage_strategy.current_position
@@ -1,5 +1,6 @@
1
1
  import abc
2
- from typing import ContextManager, Iterator
2
+ from collections.abc import Iterator
3
+ from contextlib import AbstractContextManager
3
4
 
4
5
  from typing_extensions import Self
5
6
 
@@ -19,7 +20,9 @@ def no_filter(entry: RawEvent) -> bool:
19
20
 
20
21
 
21
22
  class NoOutboxStorageStrategy(OutboxStorageStrategy):
22
- def outbox_entries(self, limit: int) -> Iterator[ContextManager[RecordedRaw]]:
23
+ def outbox_entries(
24
+ self, limit: int
25
+ ) -> Iterator[AbstractContextManager[RecordedRaw]]:
23
26
  return iter([])
24
27
 
25
28
 
@@ -35,6 +38,8 @@ class TransactionalBackend(Backend):
35
38
 
36
39
 
37
40
  class BackendFactory(abc.ABC):
41
+ """Abstract base class to configure EventStore."""
42
+
38
43
  @abc.abstractmethod
39
44
  def build(self) -> Backend:
40
45
  pass
@@ -1,10 +1,10 @@
1
1
  import time
2
- from contextlib import contextmanager
2
+ from collections.abc import Generator, Iterator
3
+ from contextlib import AbstractContextManager, contextmanager
3
4
  from copy import copy
4
5
  from dataclasses import dataclass, field
5
6
  from datetime import timedelta
6
7
  from operator import getitem
7
- from typing import ContextManager, Dict, Generator, Iterator
8
8
 
9
9
  from pydantic import BaseModel, ConfigDict, PositiveInt
10
10
  from typing_extensions import Self
@@ -148,7 +148,9 @@ class InMemoryOutboxStorageStrategy(OutboxStorageStrategy):
148
148
  def put_into_outbox(self, records: list[RecordedRaw]) -> None:
149
149
  self._outbox.extend([(e, 0) for e in records if self._filterer(e.entry)])
150
150
 
151
- def outbox_entries(self, limit: int) -> Iterator[ContextManager[RecordedRaw]]:
151
+ def outbox_entries(
152
+ self, limit: int
153
+ ) -> Iterator[AbstractContextManager[RecordedRaw]]:
152
154
  for record in self._outbox[:limit]:
153
155
  yield self._publish_context(*record)
154
156
 
@@ -224,7 +226,7 @@ class InMemoryStorageStrategy(StorageStrategy):
224
226
  dispatcher: Dispatcher,
225
227
  outbox_strategy: InMemoryOutboxStorageStrategy | None,
226
228
  ) -> None:
227
- self._names: Dict[str | None, str] = {}
229
+ self._names: dict[str | None, str] = {}
228
230
  self._storage = storage
229
231
  self._dispatcher = dispatcher
230
232
  self._outbox = outbox_strategy
@@ -296,9 +298,11 @@ class Config(BaseModel):
296
298
 
297
299
  @dataclass(repr=False)
298
300
  class InMemoryBackendFactory(BackendFactory):
301
+ """Lightweight in-memory backend factory for testing and development."""
302
+
299
303
  serde = Serde(Event.__registry__)
300
304
 
301
- _config: Config = Config()
305
+ _config: Config = field(default_factory=Config)
302
306
  _storage: Storage = field(default_factory=Storage)
303
307
  _outbox_strategy: InMemoryOutboxStorageStrategy | None = None
304
308
  _subscription_strategy: InMemorySubscriptionStrategy = field(init=False)
@@ -1,6 +1,8 @@
1
1
  import abc
2
+ from collections.abc import Iterator
3
+ from contextlib import AbstractContextManager
2
4
  from datetime import timedelta
3
- from typing import ContextManager, Iterator, Protocol
5
+ from typing import Protocol
4
6
 
5
7
  from event_sourcery.event_store.event import Position, RawEvent, RecordedRaw
6
8
  from event_sourcery.event_store.stream_id import StreamId
@@ -8,13 +10,14 @@ from event_sourcery.event_store.versioning import Versioning
8
10
 
9
11
 
10
12
  class OutboxFiltererStrategy(Protocol):
11
- def __call__(self, entry: RawEvent) -> bool:
12
- ...
13
+ def __call__(self, entry: RawEvent) -> bool: ...
13
14
 
14
15
 
15
16
  class OutboxStorageStrategy(abc.ABC):
16
17
  @abc.abstractmethod
17
- def outbox_entries(self, limit: int) -> Iterator[ContextManager[RecordedRaw]]:
18
+ def outbox_entries(
19
+ self, limit: int
20
+ ) -> Iterator[AbstractContextManager[RecordedRaw]]:
18
21
  pass
19
22
 
20
23
 
@@ -1,4 +1,4 @@
1
- from typing import Callable
1
+ from collections.abc import Callable
2
2
 
3
3
  from event_sourcery.event_store.event import Recorded, Serde
4
4
  from event_sourcery.event_store.interfaces import OutboxStorageStrategy
@@ -1,9 +1,10 @@
1
1
  import abc
2
2
  import sys
3
+ from collections.abc import Callable, Iterator
3
4
  from dataclasses import dataclass, field
4
5
  from datetime import timedelta
5
6
  from functools import partial
6
- from typing import Callable, Iterator, Type, TypeAlias
7
+ from typing import TypeAlias
7
8
 
8
9
  from event_sourcery.event_store.event import (
9
10
  Event,
@@ -20,32 +21,29 @@ Seconds: TypeAlias = int | float
20
21
 
21
22
  class BuildPhase(abc.ABC):
22
23
  @abc.abstractmethod
23
- def build_iter(self, timelimit: Seconds | timedelta) -> Iterator[Recorded | None]:
24
- ...
24
+ def build_iter(
25
+ self, timelimit: Seconds | timedelta
26
+ ) -> Iterator[Recorded | None]: ...
25
27
 
26
28
  @abc.abstractmethod
27
29
  def build_batch(
28
30
  self,
29
31
  size: int,
30
32
  timelimit: Seconds | timedelta,
31
- ) -> Iterator[list[Recorded]]:
32
- ...
33
+ ) -> Iterator[list[Recorded]]: ...
33
34
 
34
35
 
35
36
  class FilterPhase(BuildPhase):
36
37
  @abc.abstractmethod
37
- def to_category(self, category: Category) -> BuildPhase:
38
- ...
38
+ def to_category(self, category: Category) -> BuildPhase: ...
39
39
 
40
40
  @abc.abstractmethod
41
- def to_events(self, events: list[Type[Event]]) -> BuildPhase:
42
- ...
41
+ def to_events(self, events: list[type[Event]]) -> BuildPhase: ...
43
42
 
44
43
 
45
44
  class PositionPhase(abc.ABC):
46
45
  @abc.abstractmethod
47
- def start_from(self, position: Position) -> FilterPhase:
48
- ...
46
+ def start_from(self, position: Position) -> FilterPhase: ...
49
47
 
50
48
 
51
49
  @dataclass(repr=False)
@@ -71,7 +69,7 @@ class SubscriptionBuilder(PositionPhase, FilterPhase, BuildPhase):
71
69
  )
72
70
  return self
73
71
 
74
- def to_events(self, events: list[Type[Event]]) -> BuildPhase:
72
+ def to_events(self, events: list[type[Event]]) -> BuildPhase:
75
73
  self._build = partial(
76
74
  self._strategy.subscribe_to_events,
77
75
  start_from=self._position,
@@ -1,4 +1,5 @@
1
- from typing import Callable, TypeAlias
1
+ from collections.abc import Callable
2
+ from typing import TypeAlias
2
3
 
3
4
  from event_sourcery.event_store import Event, EventStore, Metadata, StreamId
4
5
  from event_sourcery.read_model.cursors_dao import CursorsDao
@@ -45,8 +45,10 @@ class Config(BaseModel):
45
45
  class ESDBBackendFactory(BackendFactory):
46
46
  esdb_client: EventStoreDBClient
47
47
  config: Config = field(default_factory=Config)
48
- _serde: Serde = Serde(Event.__registry__)
49
- _outbox_strategy: OutboxStorageStrategy = NoOutboxStorageStrategy()
48
+ _serde: Serde = field(default_factory=lambda: Serde(Event.__registry__))
49
+ _outbox_strategy: OutboxStorageStrategy = field(
50
+ default_factory=NoOutboxStorageStrategy
51
+ )
50
52
 
51
53
  def build(self) -> Backend:
52
54
  backend = Backend()
@@ -42,7 +42,7 @@ class ESDBStorageStrategy(StorageStrategy):
42
42
  try:
43
43
  events = [dto.raw_event(entry) for entry in entries]
44
44
  if snapshot:
45
- return [snapshot] + events
45
+ return [snapshot, *events]
46
46
  return events
47
47
  except NotFound:
48
48
  return []
@@ -63,14 +63,13 @@ class ESDBStorageStrategy(StorageStrategy):
63
63
  def insert_events(
64
64
  self, stream_id: StreamId, versioning: Versioning, events: list[RawEvent]
65
65
  ) -> None:
66
- self._ensure_stream(stream_id=stream_id, versioning=versioning)
67
- for stream_id in {e.stream_id for e in events}:
68
- stream_name = stream.Name(stream_id)
69
- stream_events = [e for e in events if e.stream_id == stream_id]
66
+ for sid in {e.stream_id for e in events}:
67
+ self._ensure_stream(stream_id=sid, versioning=versioning)
68
+ stream_name = stream.Name(sid)
69
+ stream_events = [e for e in events if e.stream_id == sid]
70
70
  self._append_events(stream_name, events=stream_events)
71
71
 
72
72
  def _append_events(self, name: stream.Name, events: list[RawEvent]) -> int:
73
- assert all(e.stream_id == name.uuid for e in events)
74
73
  return self._client.append_events(
75
74
  str(name),
76
75
  current_version=StreamState.ANY,
@@ -1,8 +1,8 @@
1
1
  import logging
2
- from contextlib import contextmanager
3
- from dataclasses import dataclass
2
+ from collections.abc import Generator, Iterator
3
+ from contextlib import AbstractContextManager, contextmanager
4
+ from dataclasses import dataclass, field
4
5
  from itertools import islice
5
- from typing import ContextManager, Generator, Iterator
6
6
 
7
7
  from esdbclient import EventStoreDBClient, RecordedEvent
8
8
  from esdbclient.exceptions import DeadlineExceeded, NotFound
@@ -25,7 +25,7 @@ class ESDBOutboxStorageStrategy(OutboxStorageStrategy):
25
25
  _outbox_name: str
26
26
  _max_publish_attempts: int
27
27
  _timeout: float | None
28
- _active_subscription: PersistentSubscription | None = None
28
+ _active_subscription: PersistentSubscription = field(init=False)
29
29
 
30
30
  def create_subscription(self) -> None:
31
31
  try:
@@ -48,14 +48,15 @@ class ESDBOutboxStorageStrategy(OutboxStorageStrategy):
48
48
  )
49
49
  yield islice(self._active_subscription, limit or 100)
50
50
  self._active_subscription.stop()
51
- self._active_subscription = None
51
+ delattr(self, "_active_subscription")
52
52
 
53
53
  @property
54
54
  def active_subscription(self) -> PersistentSubscription:
55
- assert self._active_subscription is not None
56
55
  return self._active_subscription
57
56
 
58
- def outbox_entries(self, limit: int) -> Iterator[ContextManager[RecordedRaw]]:
57
+ def outbox_entries(
58
+ self, limit: int
59
+ ) -> Iterator[AbstractContextManager[RecordedRaw]]:
59
60
  info = self._client.get_subscription_info(
60
61
  self._outbox_name,
61
62
  timeout=self._timeout,
@@ -1,6 +1,6 @@
1
1
  import sys
2
2
  from collections import UserString
3
- from typing import Tuple, cast
3
+ from typing import cast
4
4
 
5
5
  from event_sourcery.event_store import StreamId
6
6
  from event_sourcery.event_store.exceptions import IllegalCategoryName
@@ -57,7 +57,7 @@ MAX_POSITION = Position(sys.maxsize)
57
57
  def scope(
58
58
  start_version: int | None,
59
59
  stop_version: int | None,
60
- ) -> Tuple[Position | None, int]:
60
+ ) -> tuple[Position | None, int]:
61
61
  start = cast(
62
62
  Position | None, start_version and Position.from_version(start_version)
63
63
  )
@@ -1,7 +1,7 @@
1
+ from collections.abc import Callable, Iterator
1
2
  from dataclasses import dataclass
2
3
  from datetime import timedelta
3
4
  from functools import partial
4
- from typing import Callable, Iterator
5
5
 
6
6
  import esdbclient.exceptions
7
7
  from esdbclient import EventStoreDBClient, RecordedEvent
@@ -6,7 +6,7 @@ __all__ = [
6
6
  "SQLAlchemyBackendFactory",
7
7
  ]
8
8
 
9
- from dataclasses import dataclass
9
+ from dataclasses import dataclass, field
10
10
  from datetime import timedelta
11
11
 
12
12
  from pydantic import BaseModel, ConfigDict, PositiveInt
@@ -43,8 +43,8 @@ class Config(BaseModel):
43
43
  @dataclass(repr=False)
44
44
  class SQLAlchemyBackendFactory(BackendFactory):
45
45
  _session: Session
46
- _config: Config = Config()
47
- _serde: Serde = Serde(Event.__registry__)
46
+ _config: Config = field(default_factory=Config)
47
+ _serde: Serde = field(default_factory=lambda: Serde(Event.__registry__))
48
48
  _outbox_strategy: SqlAlchemyOutboxStorageStrategy | None = None
49
49
 
50
50
  def build(self) -> TransactionalBackend:
@@ -1,5 +1,6 @@
1
+ from collections.abc import Sequence
1
2
  from dataclasses import dataclass
2
- from typing import Sequence, Union, cast
3
+ from typing import cast
3
4
 
4
5
  from more_itertools import first_true
5
6
  from sqlalchemy import delete, func, select, update
@@ -51,7 +52,7 @@ class SqlAlchemyStorageStrategy(StorageStrategy):
51
52
  if stop is not None:
52
53
  events_stmt = events_stmt.filter(EventModel.version < stop)
53
54
 
54
- events: Sequence[Union[EventModel, SnapshotModel]]
55
+ events: Sequence[EventModel | SnapshotModel]
55
56
  try:
56
57
  snapshot_stmt = (
57
58
  select(SnapshotModel)
@@ -74,7 +75,7 @@ class SqlAlchemyStorageStrategy(StorageStrategy):
74
75
  EventModel.version > latest_snapshot.version
75
76
  )
76
77
  newer_events = list(self._session.execute(events_stmt).scalars().all())
77
- events = [latest_snapshot] + newer_events # type: ignore
78
+ events = [latest_snapshot, *newer_events]
78
79
 
79
80
  if not events:
80
81
  return []
@@ -185,7 +186,8 @@ class SqlAlchemyStorageStrategy(StorageStrategy):
185
186
  stream.events.extend(entries)
186
187
  self._session.flush()
187
188
  records = [
188
- RecordedRaw(entry=raw, position=db.id) for raw, db in zip(events, entries)
189
+ RecordedRaw(entry=raw, position=db.id)
190
+ for raw, db in zip(events, entries, strict=False)
189
191
  ]
190
192
  if self._outbox:
191
193
  self._outbox.put_into_outbox(records)
@@ -1,5 +1,5 @@
1
1
  import uuid
2
- from typing import Any, Optional
2
+ from typing import Any
3
3
 
4
4
  from sqlalchemy.dialects.postgresql import UUID
5
5
  from sqlalchemy.types import CHAR, TypeDecorator
@@ -24,17 +24,16 @@ class GUID(TypeDecorator):
24
24
  else:
25
25
  return dialect.type_descriptor(CHAR(32))
26
26
 
27
- def process_bind_param(self, value: Any, dialect: Any) -> Optional[str]:
27
+ def process_bind_param(self, value: Any, dialect: Any) -> str | None:
28
28
  if dialect.name == "postgresql":
29
29
  return str(value)
30
+ elif not isinstance(value, uuid.UUID): # pragma: no cover
31
+ return uuid.UUID(value).hex
30
32
  else:
31
- if not isinstance(value, uuid.UUID): # pragma: no cover
32
- return "%.32x" % uuid.UUID(value).int
33
- else:
34
- # hexstring
35
- return "%.32x" % value.int
33
+ # hexstring
34
+ return value.hex
36
35
 
37
- def process_result_value(self, value: Any, dialect: Any) -> Optional[uuid.UUID]:
36
+ def process_result_value(self, value: Any, dialect: Any) -> uuid.UUID | None:
38
37
  if isinstance(value, uuid.UUID):
39
38
  return value
40
39
  else:
@@ -1,5 +1,5 @@
1
1
  import json
2
- from typing import Any, Optional
2
+ from typing import Any
3
3
 
4
4
  from sqlalchemy.dialects.postgresql import JSONB as POSTGRES_JSONB
5
5
  from sqlalchemy.types import Text, TypeDecorator
@@ -14,13 +14,13 @@ class JSONB(TypeDecorator):
14
14
  else:
15
15
  return dialect.type_descriptor(Text())
16
16
 
17
- def process_bind_param(self, value: Any, dialect: Any) -> Optional[Any]:
17
+ def process_bind_param(self, value: Any, dialect: Any) -> Any | None:
18
18
  if dialect.name == "postgresql":
19
19
  return value
20
20
  else:
21
21
  return json.dumps(value)
22
22
 
23
- def process_result_value(self, value: Any, dialect: Any) -> Optional[Any]:
23
+ def process_result_value(self, value: Any, dialect: Any) -> Any | None:
24
24
  if dialect.name == "postgresql":
25
25
  return value
26
26
  else:
@@ -1,5 +1,5 @@
1
1
  from datetime import datetime
2
- from typing import Any, Type, cast
2
+ from typing import Any, cast
3
3
  from uuid import UUID
4
4
 
5
5
  from sqlalchemy import (
@@ -30,7 +30,7 @@ from event_sourcery_sqlalchemy.guid import GUID
30
30
  from event_sourcery_sqlalchemy.jsonb import JSONB
31
31
 
32
32
 
33
- def configure_models(base: Type) -> None:
33
+ def configure_models(base: type[Any]) -> None:
34
34
  for model_cls in (Stream, Event, Snapshot, OutboxEntry, ProjectorCursor):
35
35
  registry(metadata=base.metadata, class_registry={}).map_declaratively(model_cls)
36
36
 
@@ -1,8 +1,9 @@
1
1
  import logging
2
- from contextlib import contextmanager
2
+ from collections.abc import Generator, Iterator
3
+ from contextlib import AbstractContextManager, contextmanager
3
4
  from dataclasses import dataclass
4
5
  from datetime import datetime
5
- from typing import ContextManager, Generator, Iterator, cast
6
+ from typing import cast
6
7
  from uuid import UUID
7
8
 
8
9
  from sqlalchemy import insert, select
@@ -50,7 +51,9 @@ class SqlAlchemyOutboxStorageStrategy(OutboxStorageStrategy):
50
51
 
51
52
  self._session.execute(insert(OutboxEntry), rows)
52
53
 
53
- def outbox_entries(self, limit: int) -> Iterator[ContextManager[RecordedRaw]]:
54
+ def outbox_entries(
55
+ self, limit: int
56
+ ) -> Iterator[AbstractContextManager[RecordedRaw]]:
54
57
  stmt = (
55
58
  select(OutboxEntry)
56
59
  .filter(OutboxEntry.tries_left > 0)
@@ -1,7 +1,8 @@
1
1
  import time
2
+ from collections.abc import Iterator
2
3
  from dataclasses import dataclass
3
4
  from datetime import timedelta
4
- from typing import Iterator, Protocol, cast
5
+ from typing import Protocol, cast
5
6
 
6
7
  from sqlalchemy import select
7
8
  from sqlalchemy.orm import Session
@@ -62,8 +63,7 @@ class SqlAlchemySubscriptionStrategy(SubscriptionStrategy):
62
63
 
63
64
 
64
65
  class GetBatch(Protocol):
65
- def __call__(self, position: Position) -> list[models.Event]:
66
- ...
66
+ def __call__(self, position: Position) -> list[models.Event]: ...
67
67
 
68
68
 
69
69
  class GetBatchToAll(GetBatch):
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "python-event-sourcery"
3
- version = "0.4.0a2"
3
+ version = "0.4.0a4"
4
4
  description = ""
5
5
  authors = ["Your Name <you@example.com>"]
6
6
  license = "MIT"
@@ -19,17 +19,14 @@ esdbclient = {version = ">=1.0b3", optional = true}
19
19
  django = {version = ">=4,<6", optional = true}
20
20
 
21
21
  [tool.poetry.group.dev.dependencies]
22
- black = "*"
23
- isort = "*"
24
- flake8 = "*"
25
- bandit = "*"
22
+ ruff = "*"
26
23
  mypy = "*"
27
24
  pytest = "*"
28
25
  pytest-cov = "*"
26
+ pytest-django = {version = "*"}
29
27
  mkdocs-material = "^8.3.9"
30
28
  psycopg2-binary = "^2.9.3"
31
- pytest-django = {version = "*"}
32
- flake8-pyproject = "*"
29
+ mkdocstrings = {version = "*", extras = ["python"]}
33
30
 
34
31
  [tool.poetry.extras]
35
32
  sqlalchemy = ["SQLAlchemy"]
@@ -82,11 +79,44 @@ warn_return_any = true
82
79
  warn_unused_ignores = true
83
80
  show_error_codes = true
84
81
 
85
- [tool.flake8]
86
- max-line-length = 88
82
+ [tool.ruff]
83
+ line-length = 88
84
+ target-version = "py310"
85
+ show-fixes = true
86
+ exclude = ["**/migrations"]
87
+
88
+ [tool.ruff.lint]
89
+ select = [
90
+ "A", # flake8-builtins
91
+ "B", # flake8-bugbear
92
+ "C", # Convention
93
+ "E", # Error
94
+ "F", # Pyflakes
95
+ "G", # flake8-logging-format
96
+ "I", # isort
97
+ "N", # pep8-naming
98
+ "Q", # flake8-quotes
99
+ "S", # flake8-bandit
100
+ "W", # Warning
101
+ "PL", # Pylint
102
+ "UP", # pyupgrade
103
+ "RUF", # Ruff-specific rules
104
+ ]
105
+ ignore = [
106
+ "N818", # error-suffix-on-exception-name
107
+ "PLR2004", # magic-value-comparison
108
+ ]
109
+
110
+ [tool.ruff.lint.per-file-ignores]
111
+ "tests/*" = [
112
+ "S101", # Use of assert detected
113
+ ]
114
+ "event_sourcery_django/models.py" = [
115
+ "RUF012", # Mutable class attributes should be annotated with typing.ClassVar
116
+ ]
87
117
 
88
- [tool.isort]
89
- profile = "black"
118
+ [tool.ruff.lint.pylint]
119
+ max-args = 10
90
120
 
91
121
  [build-system]
92
122
  requires = ["poetry-core>=1.0.0"]