python-event-sourcery 0.4.0a17__tar.gz → 0.4.0a19__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 (61) hide show
  1. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/PKG-INFO +1 -1
  2. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery/event_store/__init__.py +5 -10
  3. python_event_sourcery-0.4.0a19/event_sourcery/event_store/backend.py +174 -0
  4. python_event_sourcery-0.4.0a19/event_sourcery/event_store/dispatcher.py +64 -0
  5. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery/event_store/event/__init__.py +7 -1
  6. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery/event_store/event/_encryption.py +4 -10
  7. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery/event_store/event/_serde.py +2 -9
  8. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery/event_store/event_store.py +0 -19
  9. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery/event_store/in_memory.py +33 -86
  10. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery/event_store/interfaces.py +2 -1
  11. python_event_sourcery-0.4.0a19/event_sourcery_django/__init__.py +61 -0
  12. python_event_sourcery-0.4.0a19/event_sourcery_kurrentdb/__init__.py +69 -0
  13. python_event_sourcery-0.4.0a19/event_sourcery_sqlalchemy/__init__.py +124 -0
  14. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery_sqlalchemy/cursors_dao.py +22 -19
  15. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery_sqlalchemy/dto.py +2 -2
  16. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery_sqlalchemy/event_store.py +45 -40
  17. python_event_sourcery-0.4.0a19/event_sourcery_sqlalchemy/models/__init__.py +53 -0
  18. python_event_sourcery-0.4.0a17/event_sourcery_sqlalchemy/models.py → python_event_sourcery-0.4.0a19/event_sourcery_sqlalchemy/models/base.py +102 -60
  19. python_event_sourcery-0.4.0a19/event_sourcery_sqlalchemy/models/default.py +27 -0
  20. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery_sqlalchemy/outbox.py +7 -6
  21. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery_sqlalchemy/subscription.py +74 -29
  22. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/pyproject.toml +1 -1
  23. python_event_sourcery-0.4.0a17/event_sourcery/event_store/dispatcher.py +0 -41
  24. python_event_sourcery-0.4.0a17/event_sourcery/event_store/factory.py +0 -67
  25. python_event_sourcery-0.4.0a17/event_sourcery_django/__init__.py +0 -97
  26. python_event_sourcery-0.4.0a17/event_sourcery_kurrentdb/__init__.py +0 -105
  27. python_event_sourcery-0.4.0a17/event_sourcery_sqlalchemy/__init__.py +0 -107
  28. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/LICENSE +0 -0
  29. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery/__init__.py +0 -0
  30. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery/event_sourcing/__init__.py +0 -0
  31. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery/event_sourcing/aggregate.py +0 -0
  32. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery/event_sourcing/repository.py +0 -0
  33. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery/event_store/event/_dto.py +0 -0
  34. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery/event_store/event/_registry.py +0 -0
  35. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery/event_store/exceptions.py +0 -0
  36. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery/event_store/outbox.py +0 -0
  37. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery/event_store/stream_id.py +0 -0
  38. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery/event_store/subscription.py +0 -0
  39. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery/event_store/tenant_id.py +0 -0
  40. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery/event_store/versioning.py +0 -0
  41. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery/py.typed +0 -0
  42. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery/read_model/__init__.py +0 -0
  43. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery/read_model/cursors_dao.py +0 -0
  44. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery/read_model/projector.py +0 -0
  45. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery_django/dto.py +0 -0
  46. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery_django/event_store.py +0 -0
  47. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery_django/migrations/0001_initial.py +0 -0
  48. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery_django/migrations/__init__.py +0 -0
  49. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery_django/models.py +0 -0
  50. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery_django/outbox.py +0 -0
  51. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery_django/py.typed +0 -0
  52. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery_django/subscription.py +0 -0
  53. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery_kurrentdb/dto.py +0 -0
  54. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery_kurrentdb/event_store.py +0 -0
  55. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery_kurrentdb/outbox.py +0 -0
  56. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery_kurrentdb/py.typed +0 -0
  57. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery_kurrentdb/stream.py +0 -0
  58. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery_kurrentdb/subscription.py +0 -0
  59. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery_sqlalchemy/guid.py +0 -0
  60. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery_sqlalchemy/jsonb.py +0 -0
  61. {python_event_sourcery-0.4.0a17 → python_event_sourcery-0.4.0a19}/event_sourcery_sqlalchemy/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: python-event-sourcery
3
- Version: 0.4.0a17
3
+ Version: 0.4.0a19
4
4
  Summary:
5
5
  License: MIT
6
6
  Author: Your Name
@@ -2,7 +2,6 @@ __all__ = [
2
2
  "DEFAULT_TENANT",
3
3
  "NO_VERSIONING",
4
4
  "Backend",
5
- "BackendFactory",
6
5
  "Context",
7
6
  "Dispatcher",
8
7
  "Entry",
@@ -10,7 +9,7 @@ __all__ = [
10
9
  "EventRegistry",
11
10
  "EventStore",
12
11
  "ExplicitVersioning",
13
- "InMemoryBackendFactory",
12
+ "InMemoryBackend",
14
13
  "Listener",
15
14
  "Position",
16
15
  "RawEvent",
@@ -22,13 +21,14 @@ __all__ = [
22
21
  "TransactionalBackend",
23
22
  "Versioning",
24
23
  "WrappedEvent",
24
+ "backend",
25
25
  "exceptions",
26
- "factory",
27
26
  "interfaces",
28
27
  "subscription",
29
28
  ]
30
29
 
31
- from event_sourcery.event_store import exceptions, factory, interfaces, subscription
30
+ from event_sourcery.event_store import backend, exceptions, interfaces, subscription
31
+ from event_sourcery.event_store.backend import Backend, TransactionalBackend
32
32
  from event_sourcery.event_store.dispatcher import Dispatcher, Listener
33
33
  from event_sourcery.event_store.event import (
34
34
  Context,
@@ -42,12 +42,7 @@ from event_sourcery.event_store.event import (
42
42
  WrappedEvent,
43
43
  )
44
44
  from event_sourcery.event_store.event_store import EventStore
45
- from event_sourcery.event_store.factory import (
46
- Backend,
47
- BackendFactory,
48
- TransactionalBackend,
49
- )
50
- from event_sourcery.event_store.in_memory import InMemoryBackendFactory
45
+ from event_sourcery.event_store.in_memory import InMemoryBackend
51
46
  from event_sourcery.event_store.stream_id import StreamId, StreamUUID
52
47
  from event_sourcery.event_store.tenant_id import DEFAULT_TENANT, TenantId
53
48
  from event_sourcery.event_store.versioning import (
@@ -0,0 +1,174 @@
1
+ import abc
2
+ from collections import UserDict
3
+ from collections.abc import Iterator
4
+ from contextlib import AbstractContextManager
5
+ from functools import wraps
6
+ from typing import NoReturn, Protocol, TypeVar, cast
7
+
8
+ from typing_extensions import Self
9
+
10
+ from event_sourcery.event_store import subscription
11
+ from event_sourcery.event_store.dispatcher import Dispatcher, Listeners
12
+ from event_sourcery.event_store.event import (
13
+ Encryption,
14
+ EventRegistry,
15
+ NoEncryptionStrategy,
16
+ NoKeyStorageStrategy,
17
+ RawEvent,
18
+ RecordedRaw,
19
+ Serde,
20
+ )
21
+ from event_sourcery.event_store.event_store import EventStore
22
+ from event_sourcery.event_store.interfaces import (
23
+ EncryptionKeyStorageStrategy,
24
+ EncryptionStrategy,
25
+ OutboxFiltererStrategy,
26
+ OutboxStorageStrategy,
27
+ StorageStrategy,
28
+ SubscriptionStrategy,
29
+ )
30
+ from event_sourcery.event_store.outbox import Outbox
31
+ from event_sourcery.event_store.tenant_id import DEFAULT_TENANT, TenantId
32
+
33
+
34
+ class NoProviderConfigured(Exception):
35
+ pass
36
+
37
+
38
+ def no_filter(entry: RawEvent) -> bool:
39
+ return True
40
+
41
+
42
+ class NoOutboxStorageStrategy(OutboxStorageStrategy):
43
+ def outbox_entries(
44
+ self, limit: int
45
+ ) -> Iterator[AbstractContextManager[RecordedRaw]]:
46
+ return iter([])
47
+
48
+
49
+ T_co = TypeVar("T_co", covariant=True)
50
+ T = TypeVar("T")
51
+
52
+
53
+ class _Provider(Protocol[T_co]):
54
+ def __call__(self, container: "_Container") -> T_co: ...
55
+
56
+
57
+ def singleton(provider: _Provider[T]) -> _Provider[T]:
58
+ result: T | None = None
59
+
60
+ @wraps(provider)
61
+ def _wrapper(container: "_Container") -> T:
62
+ nonlocal result
63
+ if result is not None:
64
+ return result
65
+ result = provider(container)
66
+ return result
67
+
68
+ return _wrapper
69
+
70
+
71
+ def not_configured(error_message: str) -> _Provider[T]:
72
+ def _raise(container: "_Container") -> NoReturn:
73
+ raise NoProviderConfigured(error_message) # pragma: no cover
74
+
75
+ return _raise
76
+
77
+
78
+ class _Container(UserDict[type[T], _Provider[T] | T]):
79
+ def __getitem__(self, _type: type[T]) -> T:
80
+ provider = cast(_Provider[T], self.data[_type])
81
+ return provider(self)
82
+
83
+ def __setitem__(self, _type: type[T], value: T | _Provider[T]) -> None:
84
+ self.data[_type] = (
85
+ cast(_Provider[T], lambda _: value) if isinstance(value, _type) else value
86
+ )
87
+
88
+ def copy(self) -> Self:
89
+ new: Self = self.__class__()
90
+ new.data.update(self.data.copy())
91
+ return new
92
+
93
+
94
+ class Backend(_Container):
95
+ tenant_id: TenantId
96
+
97
+ def __init__(self) -> None:
98
+ super().__init__()
99
+ self.tenant_id = DEFAULT_TENANT
100
+ self[EventRegistry] = lambda _: EventRegistry()
101
+ self[EncryptionStrategy] = lambda _: NoEncryptionStrategy()
102
+ self[EncryptionKeyStorageStrategy] = (
103
+ lambda c: NoKeyStorageStrategy().scoped_for_tenant(c.tenant_id)
104
+ )
105
+ self[Encryption] = lambda c: Encryption(
106
+ registry=c[EventRegistry],
107
+ strategy=c[EncryptionStrategy],
108
+ key_storage=c[EncryptionKeyStorageStrategy],
109
+ )
110
+ self[Serde] = lambda c: Serde(
111
+ registry=c[EventRegistry],
112
+ encryption=c[Encryption],
113
+ )
114
+ self[StorageStrategy] = not_configured(
115
+ "Use one of pyES backends: SQLAlchemy, Django or KurrentDB",
116
+ )
117
+ self[EventStore] = lambda c: EventStore(
118
+ storage_strategy=c[StorageStrategy],
119
+ serde=c[Serde],
120
+ )
121
+ self[Outbox] = lambda c: Outbox(
122
+ strategy=c[OutboxStorageStrategy],
123
+ serde=c[Serde],
124
+ )
125
+ self[OutboxStorageStrategy] = lambda _: NoOutboxStorageStrategy()
126
+ self[SubscriptionStrategy] = not_configured(
127
+ "Use one of pyES backends: SQLAlchemy, Django or KurrentDB",
128
+ )
129
+ self[subscription.PositionPhase] = lambda c: subscription.SubscriptionBuilder(
130
+ c[Serde],
131
+ c[SubscriptionStrategy],
132
+ )
133
+
134
+ @property
135
+ def event_store(self) -> EventStore:
136
+ return cast(EventStore, self[EventStore])
137
+
138
+ @property
139
+ def outbox(self) -> Outbox:
140
+ return cast(Outbox, self[Outbox])
141
+
142
+ @property
143
+ def subscriber(self) -> subscription.PositionPhase:
144
+ return cast(subscription.PositionPhase, self[subscription.PositionPhase])
145
+
146
+ def in_tenant_mode(self, tenant_id: TenantId) -> Self:
147
+ in_tenant_mode = self.copy()
148
+ in_tenant_mode.tenant_id = tenant_id
149
+ return in_tenant_mode
150
+
151
+ @abc.abstractmethod
152
+ def with_outbox(self, filterer: OutboxFiltererStrategy = no_filter) -> Self: ...
153
+
154
+ def with_encryption(
155
+ self,
156
+ strategy: EncryptionStrategy,
157
+ key_storage: EncryptionKeyStorageStrategy,
158
+ ) -> Self:
159
+ self[EncryptionStrategy] = strategy
160
+ self[EncryptionKeyStorageStrategy] = lambda c: key_storage.scoped_for_tenant(
161
+ c.tenant_id
162
+ )
163
+ return self
164
+
165
+
166
+ class TransactionalBackend(Backend, abc.ABC):
167
+ def __init__(self) -> None:
168
+ super().__init__()
169
+ self[Listeners] = singleton(lambda _: Listeners())
170
+ self[Dispatcher] = lambda c: Dispatcher(c[Serde], c[Listeners])
171
+
172
+ @property
173
+ def in_transaction(self) -> Listeners:
174
+ return cast(Listeners, self[Listeners])
@@ -0,0 +1,64 @@
1
+ from collections.abc import Iterator
2
+ from itertools import chain
3
+ from typing import Protocol
4
+
5
+ from event_sourcery.event_store.event import (
6
+ Event,
7
+ Position,
8
+ RecordedRaw,
9
+ Serde,
10
+ WrappedEvent,
11
+ )
12
+ from event_sourcery.event_store.stream_id import StreamId
13
+ from event_sourcery.event_store.tenant_id import TenantId
14
+
15
+
16
+ class Listener(Protocol):
17
+ def __call__(
18
+ self,
19
+ wrapped_event: WrappedEvent,
20
+ stream_id: StreamId,
21
+ tenant_id: TenantId,
22
+ position: Position,
23
+ ) -> None: ...
24
+
25
+
26
+ class Listeners:
27
+ def __init__(self) -> None:
28
+ self._listeners: dict[type[Event], set[Listener]] = {}
29
+
30
+ def __getitem__(self, event_type: type[Event]) -> Iterator[Listener]:
31
+ return chain(
32
+ *(
33
+ listeners
34
+ for registered_to, listeners in self._listeners.items()
35
+ if issubclass(event_type, registered_to)
36
+ )
37
+ )
38
+
39
+ def register(self, listener: Listener, to: type[Event]) -> None:
40
+ if to not in self._listeners:
41
+ self._listeners[to] = set()
42
+ self._listeners[to].add(listener)
43
+
44
+ def remove(self, listener: Listener, to: type[Event]) -> None:
45
+ if to in self._listeners and listener in self._listeners[to]:
46
+ self._listeners[to].remove(listener)
47
+
48
+
49
+ class Dispatcher:
50
+ def __init__(self, serde: Serde, listeners: Listeners) -> None:
51
+ self._serde = serde
52
+ self._listeners = listeners
53
+
54
+ def dispatch(self, *raws: RecordedRaw) -> None:
55
+ for raw in raws:
56
+ record = self._serde.deserialize_record(raw)
57
+ event_type = record.wrapped_event.event.__class__
58
+ for listener in self._listeners[event_type]:
59
+ listener(
60
+ record.wrapped_event,
61
+ record.stream_id,
62
+ record.tenant_id,
63
+ record.position,
64
+ )
@@ -6,6 +6,8 @@ __all__ = [
6
6
  "Entry",
7
7
  "Event",
8
8
  "EventRegistry",
9
+ "NoEncryptionStrategy",
10
+ "NoKeyStorageStrategy",
9
11
  "Position",
10
12
  "RawEvent",
11
13
  "Recorded",
@@ -27,6 +29,10 @@ from event_sourcery.event_store.event._dto import (
27
29
  RecordedRaw,
28
30
  WrappedEvent,
29
31
  )
30
- from event_sourcery.event_store.event._encryption import Encryption
32
+ from event_sourcery.event_store.event._encryption import (
33
+ Encryption,
34
+ NoEncryptionStrategy,
35
+ NoKeyStorageStrategy,
36
+ )
31
37
  from event_sourcery.event_store.event._registry import EventRegistry
32
38
  from event_sourcery.event_store.event._serde import Serde
@@ -1,6 +1,6 @@
1
1
  import json
2
2
  from collections import UserDict
3
- from dataclasses import dataclass, field, replace
3
+ from dataclasses import dataclass
4
4
  from typing import Any
5
5
 
6
6
  from pydantic import BaseModel
@@ -68,11 +68,9 @@ class Encryption:
68
68
  - Processing event encryption/decryption with field annotations
69
69
  """
70
70
 
71
- registry: EventRegistry = field(default_factory=EventRegistry)
72
- strategy: EncryptionStrategy = field(default_factory=NoEncryptionStrategy)
73
- key_storage: EncryptionKeyStorageStrategy = field(
74
- default_factory=NoKeyStorageStrategy
75
- )
71
+ registry: EventRegistry
72
+ strategy: EncryptionStrategy
73
+ key_storage: EncryptionKeyStorageStrategy
76
74
 
77
75
  def encrypt(self, event: BaseModel, stream_id: StreamId) -> dict[str, Any]:
78
76
  event_type = type(event)
@@ -111,7 +109,6 @@ class Encryption:
111
109
  if key is None:
112
110
  return mask_value
113
111
  decrypted = self.strategy.decrypt(value, key)
114
-
115
112
  try:
116
113
  return json.loads(decrypted)
117
114
  except (json.JSONDecodeError, TypeError):
@@ -130,6 +127,3 @@ class Encryption:
130
127
 
131
128
  def shred(self, subject_id: str) -> None:
132
129
  self.key_storage.delete(subject_id)
133
-
134
- def scoped_for_tenant(self, tenant_id: TenantId) -> Self:
135
- return replace(self, key_storage=self.key_storage.scoped_for_tenant(tenant_id))
@@ -12,20 +12,16 @@ from event_sourcery.event_store.event._dto import (
12
12
  from event_sourcery.event_store.event._encryption import Encryption
13
13
  from event_sourcery.event_store.event._registry import EventRegistry
14
14
  from event_sourcery.event_store.stream_id import StreamId
15
- from event_sourcery.event_store.tenant_id import TenantId
16
15
 
17
16
 
18
17
  class Serde:
19
- registry: EventRegistry
20
- encryption: Encryption
21
-
22
18
  def __init__(
23
19
  self,
24
20
  registry: EventRegistry,
25
- encryption: Encryption | None = None,
21
+ encryption: Encryption,
26
22
  ) -> None:
27
23
  self.registry = registry
28
- self.encryption = encryption or Encryption(registry)
24
+ self.encryption = encryption
29
25
 
30
26
  def deserialize(self, event: RawEvent) -> WrappedEvent:
31
27
  event_as_dict = dataclasses.asdict(event)
@@ -77,6 +73,3 @@ class Serde:
77
73
  self, events: Sequence[WrappedEvent], stream_id: StreamId
78
74
  ) -> list[RawEvent]:
79
75
  return [self.serialize(event, stream_id) for event in events]
80
-
81
- def scoped_for_tenant(self, tenant_id: TenantId) -> "Serde":
82
- return Serde(self.registry, self.encryption.scoped_for_tenant(tenant_id))
@@ -10,7 +10,6 @@ from event_sourcery.event_store.event import (
10
10
  )
11
11
  from event_sourcery.event_store.interfaces import StorageStrategy
12
12
  from event_sourcery.event_store.stream_id import StreamId
13
- from event_sourcery.event_store.tenant_id import DEFAULT_TENANT, TenantId
14
13
  from event_sourcery.event_store.versioning import (
15
14
  NO_VERSIONING,
16
15
  ExplicitVersioning,
@@ -190,21 +189,3 @@ class EventStore:
190
189
 
191
190
  """
192
191
  return self._storage_strategy.current_position
193
-
194
- def scoped_for_tenant(self, tenant_id: TenantId = DEFAULT_TENANT) -> "EventStore":
195
- """Factory method to create a new event store instance scoped to a tenant.
196
-
197
- Examples:
198
- >>> event_store.scoped_for_tenant("tenant_1")
199
- <EventStore ...>
200
-
201
- Args:
202
- tenant_id: The tenant identifier to work with.
203
-
204
- Returns:
205
- An event store instance scoped to the tenant.
206
- """
207
- return EventStore(
208
- storage_strategy=self._storage_strategy.scoped_for_tenant(tenant_id),
209
- serde=self._serde.scoped_for_tenant(tenant_id),
210
- )
@@ -2,42 +2,33 @@ import time
2
2
  from collections.abc import Generator, Iterator
3
3
  from contextlib import AbstractContextManager, contextmanager
4
4
  from copy import copy
5
- from dataclasses import dataclass, field, replace
5
+ from dataclasses import dataclass, field
6
6
  from datetime import timedelta
7
7
  from operator import getitem
8
8
 
9
9
  from pydantic import BaseModel, ConfigDict, PositiveInt
10
10
  from typing_extensions import Self
11
11
 
12
- from event_sourcery.event_store import (
13
- Dispatcher,
14
- EventRegistry,
15
- EventStore,
16
- subscription,
12
+ from event_sourcery.event_store import Dispatcher
13
+ from event_sourcery.event_store.backend import (
14
+ TransactionalBackend,
15
+ no_filter,
16
+ not_configured,
17
+ singleton,
17
18
  )
18
19
  from event_sourcery.event_store.event import (
19
- Encryption,
20
20
  Position,
21
21
  RawEvent,
22
22
  RecordedRaw,
23
- Serde,
24
23
  )
25
24
  from event_sourcery.event_store.exceptions import ConcurrentStreamWriteError
26
- from event_sourcery.event_store.factory import (
27
- BackendFactory,
28
- NoOutboxStorageStrategy,
29
- TransactionalBackend,
30
- no_filter,
31
- )
32
25
  from event_sourcery.event_store.interfaces import (
33
26
  EncryptionKeyStorageStrategy,
34
- EncryptionStrategy,
35
27
  OutboxFiltererStrategy,
36
28
  OutboxStorageStrategy,
37
29
  StorageStrategy,
38
30
  SubscriptionStrategy,
39
31
  )
40
- from event_sourcery.event_store.outbox import Outbox
41
32
  from event_sourcery.event_store.stream_id import StreamId
42
33
  from event_sourcery.event_store.tenant_id import DEFAULT_TENANT, TenantId
43
34
  from event_sourcery.event_store.versioning import NO_VERSIONING, Versioning
@@ -228,13 +219,12 @@ class InMemoryStorageStrategy(StorageStrategy):
228
219
  storage: Storage,
229
220
  dispatcher: Dispatcher,
230
221
  outbox_strategy: InMemoryOutboxStorageStrategy | None,
231
- tenant_id: TenantId = DEFAULT_TENANT,
232
222
  ) -> None:
233
223
  self._names: dict[str | None, str] = {}
234
224
  self._storage = storage
235
225
  self._dispatcher = dispatcher
236
226
  self._outbox = outbox_strategy
237
- self._tenant_id = tenant_id
227
+ self._tenant_id = DEFAULT_TENANT
238
228
 
239
229
  def fetch_events(
240
230
  self,
@@ -300,85 +290,42 @@ class InMemoryStorageStrategy(StorageStrategy):
300
290
  return current_position and Position(current_position)
301
291
 
302
292
  def scoped_for_tenant(self, tenant_id: TenantId) -> Self:
303
- return type(self)(
304
- storage=self._storage,
305
- dispatcher=self._dispatcher,
306
- outbox_strategy=self._outbox,
307
- tenant_id=tenant_id,
308
- )
293
+ self._tenant_id = tenant_id
294
+ return self
309
295
 
310
296
 
311
297
  class Config(BaseModel):
312
298
  model_config = ConfigDict(extra="forbid", frozen=True)
313
-
314
299
  outbox_attempts: PositiveInt = 3
315
300
 
316
301
 
317
- @dataclass(repr=False)
318
- class InMemoryBackendFactory(BackendFactory):
302
+ class InMemoryBackend(TransactionalBackend):
319
303
  """Lightweight in-memory backend factory for testing and development."""
320
304
 
321
- serde = Serde(EventRegistry())
322
-
323
- _config: Config = field(default_factory=Config)
324
- _storage: Storage = field(default_factory=Storage)
325
- _outbox_strategy: InMemoryOutboxStorageStrategy | None = None
326
- _subscription_strategy: InMemorySubscriptionStrategy = field(init=False)
327
-
328
- def __post_init__(self) -> None:
329
- self._subscription_strategy = InMemorySubscriptionStrategy(self._storage)
330
-
331
- def build(self) -> TransactionalBackend:
332
- backend = TransactionalBackend()
333
- backend.serde = self.serde
334
- backend.in_transaction = Dispatcher(backend.serde)
335
- backend.event_store = EventStore(
336
- InMemoryStorageStrategy(
337
- self._storage,
338
- backend.in_transaction,
339
- self._outbox_strategy,
340
- ),
341
- backend.serde,
342
- )
343
- backend.outbox = Outbox(
344
- self._outbox_strategy or NoOutboxStorageStrategy(),
345
- backend.serde,
346
- )
347
- backend.subscriber = subscription.SubscriptionBuilder(
348
- _serde=backend.serde,
349
- _strategy=self._subscription_strategy,
350
- )
351
- return backend
352
-
353
- def with_event_registry(self, event_registry: EventRegistry) -> Self:
354
- self.serde = Serde(event_registry)
305
+ def __init__(self) -> None:
306
+ super().__init__()
307
+ self[Config] = not_configured("Configure backend with `.configure(config)`")
308
+ self[Storage] = Storage()
309
+ self[StorageStrategy] = lambda c: InMemoryStorageStrategy(
310
+ c[Storage],
311
+ c[Dispatcher],
312
+ outbox_strategy=c.get(InMemoryOutboxStorageStrategy),
313
+ ).scoped_for_tenant(c.tenant_id)
314
+ self[SubscriptionStrategy] = lambda c: InMemorySubscriptionStrategy(c[Storage])
315
+
316
+ def configure(self, config: Config | None = None) -> Self:
317
+ self[Config] = config or Config()
355
318
  return self
356
319
 
357
320
  def with_outbox(self, filterer: OutboxFiltererStrategy = no_filter) -> Self:
358
- self._outbox_strategy = InMemoryOutboxStorageStrategy(
359
- filterer,
360
- self._config.outbox_attempts,
361
- )
362
- return self
363
-
364
- def without_outbox(self, filterer: OutboxFiltererStrategy = no_filter) -> Self:
365
- self._outbox_strategy = None
366
- return self
367
-
368
- def with_encryption(
369
- self,
370
- strategy: EncryptionStrategy,
371
- key_storage: EncryptionKeyStorageStrategy,
372
- ) -> Self:
373
- registry = self.serde.registry
374
- self.serde = Serde(
375
- registry,
376
- encryption=Encryption(
377
- registry=registry,
378
- strategy=strategy,
379
- key_storage=key_storage,
380
- ),
321
+ self[OutboxFiltererStrategy] = filterer
322
+ self[InMemoryOutboxStorageStrategy] = singleton(
323
+ lambda c: InMemoryOutboxStorageStrategy(
324
+ c[OutboxFiltererStrategy],
325
+ c[Config].outbox_attempts,
326
+ )
381
327
  )
328
+ self[OutboxStorageStrategy] = lambda c: c[InMemoryOutboxStorageStrategy]
382
329
  return self
383
330
 
384
331
 
@@ -396,5 +343,5 @@ class InMemoryKeyStorage(EncryptionKeyStorageStrategy):
396
343
  def delete(self, subject_id: str) -> None:
397
344
  self._keys.pop((self._tenant_id, subject_id), None)
398
345
 
399
- def scoped_for_tenant(self, tenant_id: TenantId) -> Self:
400
- return replace(self, _tenant_id=tenant_id)
346
+ def scoped_for_tenant(self, tenant_id: TenantId) -> "InMemoryKeyStorage":
347
+ return InMemoryKeyStorage(self._keys, tenant_id)
@@ -2,7 +2,7 @@ import abc
2
2
  from collections.abc import Iterator
3
3
  from contextlib import AbstractContextManager
4
4
  from datetime import timedelta
5
- from typing import Any, Protocol
5
+ from typing import Any, Protocol, runtime_checkable
6
6
 
7
7
  from typing_extensions import Self
8
8
 
@@ -12,6 +12,7 @@ from event_sourcery.event_store.tenant_id import TenantId
12
12
  from event_sourcery.event_store.versioning import Versioning
13
13
 
14
14
 
15
+ @runtime_checkable
15
16
  class OutboxFiltererStrategy(Protocol):
16
17
  def __call__(self, entry: RawEvent) -> bool: ...
17
18
 
@@ -0,0 +1,61 @@
1
+ __all__ = [
2
+ "Config",
3
+ "DjangoBackend",
4
+ ]
5
+
6
+ from datetime import timedelta
7
+
8
+ from pydantic import BaseModel, ConfigDict, PositiveInt
9
+ from typing_extensions import Self
10
+
11
+ from event_sourcery.event_store import Dispatcher
12
+ from event_sourcery.event_store.backend import (
13
+ TransactionalBackend,
14
+ no_filter,
15
+ not_configured,
16
+ )
17
+ from event_sourcery.event_store.interfaces import (
18
+ OutboxFiltererStrategy,
19
+ OutboxStorageStrategy,
20
+ StorageStrategy,
21
+ SubscriptionStrategy,
22
+ )
23
+
24
+
25
+ class Config(BaseModel):
26
+ model_config = ConfigDict(extra="forbid", frozen=True)
27
+
28
+ outbox_attempts: PositiveInt = 3
29
+ gap_retry_interval: timedelta = timedelta(seconds=0.5)
30
+
31
+
32
+ class DjangoBackend(TransactionalBackend):
33
+ def __init__(self) -> None:
34
+ from event_sourcery_django.event_store import DjangoStorageStrategy
35
+ from event_sourcery_django.outbox import DjangoOutboxStorageStrategy
36
+ from event_sourcery_django.subscription import DjangoSubscriptionStrategy
37
+
38
+ super().__init__()
39
+ self[Config] = not_configured("Configure backend with `.configure(config)`")
40
+ self[StorageStrategy] = lambda c: DjangoStorageStrategy(
41
+ c[Dispatcher],
42
+ c.get(DjangoOutboxStorageStrategy, None),
43
+ ).scoped_for_tenant(c.tenant_id)
44
+ self[SubscriptionStrategy] = lambda c: DjangoSubscriptionStrategy(
45
+ gap_retry_interval=c[Config].gap_retry_interval
46
+ )
47
+
48
+ def configure(self, config: Config | None = None) -> Self:
49
+ self[Config] = config or Config()
50
+ return self
51
+
52
+ def with_outbox(self, filterer: OutboxFiltererStrategy = no_filter) -> Self:
53
+ from event_sourcery_django.outbox import DjangoOutboxStorageStrategy
54
+
55
+ self[OutboxFiltererStrategy] = filterer
56
+ self[DjangoOutboxStorageStrategy] = lambda c: DjangoOutboxStorageStrategy(
57
+ c[OutboxFiltererStrategy],
58
+ c[Config].outbox_attempts,
59
+ )
60
+ self[OutboxStorageStrategy] = lambda c: c[DjangoOutboxStorageStrategy]
61
+ return self