eventsourcing 9.5.0b3__py3-none-any.whl

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.
@@ -0,0 +1,369 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, ABCMeta, abstractmethod
4
+ from dataclasses import dataclass
5
+ from typing import TYPE_CHECKING, Any, Generic, ParamSpec, cast
6
+ from uuid import uuid4
7
+
8
+ from typing_extensions import Self, TypeVar
9
+
10
+ from eventsourcing.domain import (
11
+ AbstractDecision,
12
+ CallableType,
13
+ ProgrammingError,
14
+ all_func_decorators,
15
+ decorated_func_callers,
16
+ filter_kwargs_for_method_params,
17
+ )
18
+ from eventsourcing.utils import construct_topic, get_topic, resolve_topic
19
+
20
+ if TYPE_CHECKING:
21
+ from collections.abc import Callable, Sequence
22
+
23
+ _enduring_object_init_classes: dict[type[Any], type[InitialDecision]] = {}
24
+
25
+
26
+ class Decision(AbstractDecision):
27
+ def as_dict(self) -> dict[str, Any]:
28
+ return self.__dict__.copy()
29
+
30
+ def mutate(self, obj: TPerspective | None) -> TPerspective | None:
31
+ assert obj is not None
32
+
33
+ # Identify the function that was decorated.
34
+ try:
35
+ decorated_func = decorated_funcs[(type(obj), type(self))]
36
+ except KeyError:
37
+ pass
38
+ else:
39
+ # Select event attributes mentioned in function signature.
40
+ self_dict = self.as_dict()
41
+ kwargs = filter_kwargs_for_method_params(self_dict, decorated_func)
42
+
43
+ # Call the original method with event attribute values.
44
+ decorated_method = decorated_func.__get__(obj, type(obj))
45
+ try:
46
+ decorated_method(**kwargs)
47
+ except TypeError as e: # pragma: no cover
48
+ # TODO: Write a test that does this...
49
+ msg = (
50
+ f"Failed to apply {type(self).__qualname__} to "
51
+ f"{type(obj).__qualname__} with kwargs {kwargs}: {e}"
52
+ )
53
+ raise TypeError(msg) from e
54
+
55
+ self.apply(obj)
56
+ return obj
57
+
58
+ def apply(self, obj: Any) -> None:
59
+ pass
60
+
61
+
62
+ class InitialDecision(Decision):
63
+ originator_topic: str
64
+
65
+ def mutate(self, obj: TPerspective | None) -> TPerspective | None:
66
+ # Identify the function that was decorated.
67
+ if obj is not None:
68
+ try:
69
+ decorated_func = decorated_funcs[(type(obj), type(self))]
70
+ except KeyError: # pragma: no cover
71
+ pass
72
+ else:
73
+ # Select event attributes mentioned in function signature.
74
+ self_dict = self.as_dict()
75
+ kwargs = filter_kwargs_for_method_params(self_dict, decorated_func)
76
+
77
+ # Call the original method with event attribute values.
78
+ decorated_method = decorated_func.__get__(obj, type(obj))
79
+ try:
80
+ decorated_method(**kwargs)
81
+ except TypeError as e: # pragma: no cover
82
+ # TODO: Write a test that does this...
83
+ msg = (
84
+ f"Failed to apply {type(self).__qualname__} to "
85
+ f"{type(obj).__qualname__} with kwargs {kwargs}: {e}"
86
+ )
87
+ raise TypeError(msg) from e
88
+ return obj
89
+
90
+ kwargs = self.as_dict()
91
+ originator_type = resolve_topic(kwargs.pop("originator_topic"))
92
+ if issubclass(originator_type, EnduringObject):
93
+ enduring_object_id = kwargs.pop(self.id_attr_name(originator_type))
94
+ try:
95
+ enduring_object = type.__call__(originator_type, **kwargs)
96
+ except TypeError as e: # pragma: no cover
97
+ msg = (
98
+ f"{type(self).__qualname__} cannot __init__ "
99
+ f"{originator_type.__qualname__} "
100
+ f"with kwargs {kwargs}: {e}"
101
+ )
102
+ raise TypeError(msg) from e
103
+ enduring_object.id = enduring_object_id
104
+ enduring_object.__post_init__()
105
+ return enduring_object
106
+ msg = f"Originator type not subclass of EnduringObject: {originator_type}"
107
+ raise TypeError(msg)
108
+
109
+ @classmethod
110
+ def id_attr_name(cls, enduring_object_class: type[EnduringObject[Any, TID]]) -> TID:
111
+ return cast(TID, f"{enduring_object_class.__name__.lower()}_id")
112
+
113
+
114
+ TDecision = TypeVar("TDecision", bound=Decision)
115
+ """
116
+ A type variable representing any subclass of :class:`Decision`.
117
+ """
118
+
119
+
120
+ class Tagged(Generic[TDecision]):
121
+ def __init__(self, tags: list[str], decision: TDecision) -> None:
122
+ self.tags = tags
123
+ self.decision = decision
124
+
125
+
126
+ T = TypeVar("T")
127
+ P = ParamSpec("P")
128
+
129
+
130
+ class MetaPerspective(ABCMeta):
131
+ pass
132
+
133
+
134
+ class Perspective(ABC, Generic[TDecision], metaclass=MetaPerspective):
135
+ last_known_position: int | None
136
+ new_decisions: list[Tagged[TDecision]]
137
+
138
+ def __new__(cls, *_: Any, **__: Any) -> Self:
139
+ self = super().__new__(cls)
140
+ self.last_known_position = None
141
+ self.new_decisions = []
142
+ return self
143
+
144
+ @abstractmethod
145
+ def consistency_boundary(self) -> Selector | Sequence[Selector]:
146
+ raise NotImplementedError # pragma: no cover
147
+
148
+ def trigger_event(
149
+ self,
150
+ decision_cls: Callable[P, TDecision],
151
+ tags: Sequence[str] = (),
152
+ *args: P.args,
153
+ **kwargs: P.kwargs,
154
+ ) -> None:
155
+ """
156
+ Constructs new tagged decision and appends to list of uncommitted events.
157
+ """
158
+ tagged = Tagged[TDecision](
159
+ tags=list(tags),
160
+ decision=decision_cls(*args, **kwargs),
161
+ )
162
+ tagged.decision.mutate(self)
163
+ self.new_decisions.append(tagged)
164
+
165
+ def collect_events(self) -> Sequence[Tagged[TDecision]]:
166
+ """
167
+ Drains list of triggered events.
168
+ """
169
+ collected, self.new_decisions = self.new_decisions, []
170
+ return collected
171
+
172
+
173
+ TPerspective = TypeVar("TPerspective", bound=Perspective[Any])
174
+
175
+
176
+ decorated_funcs: dict[tuple[MetaPerspective, type[Decision]], CallableType] = {}
177
+
178
+
179
+ class MetaSupportsEventDecorator(MetaPerspective):
180
+ def __init__(
181
+ cls, name: str, bases: tuple[type, ...], namespace: dict[str, Any]
182
+ ) -> None:
183
+ super().__init__(name, bases, namespace)
184
+
185
+ topic_prefix = construct_topic(cls) + "."
186
+
187
+ cls.projected_types: list[type[Decision]] = []
188
+
189
+ # Find the event decorators on this class.
190
+ func_decorators = [
191
+ decorator
192
+ for decorator in all_func_decorators
193
+ if construct_topic(decorator.decorated_func).startswith(topic_prefix)
194
+ ]
195
+
196
+ for decorator in func_decorators:
197
+ given = decorator.given_event_cls
198
+
199
+ # Keep things simple by only supporting given classes (not names).
200
+ assert given is not None, "Event class not given"
201
+ # TODO: Maybe support event name strings, maybe not....
202
+
203
+ # Make sure given event class is a Decision subclass.
204
+ assert issubclass(given, Decision)
205
+
206
+ if (
207
+ issubclass(given, InitialDecision)
208
+ and decorator.decorated_func.__name__ == "__init__"
209
+ ):
210
+ _enduring_object_init_classes[cls] = given
211
+ # If command method, remember which event class to trigger.
212
+ elif not construct_topic(decorator.decorated_func).endswith("._"):
213
+ decorated_func_callers[decorator] = given
214
+
215
+ # Remember which decorated func to call.
216
+ decorated_funcs[(cls, given)] = decorator.decorated_func
217
+
218
+ cls.projected_types.append(given)
219
+
220
+
221
+ class MetaEnduringObject(MetaSupportsEventDecorator):
222
+ def __init__(
223
+ cls, name: str, bases: tuple[type, ...], namespace: dict[str, Any]
224
+ ) -> None:
225
+ super().__init__(name, bases, namespace)
226
+ # Find and remember the "InitialDecision" class.
227
+ for item in cls.__dict__.values():
228
+ if isinstance(item, type) and issubclass(item, InitialDecision):
229
+ _enduring_object_init_classes[cls] = item
230
+ break
231
+
232
+ def __call__(cls: type[T], **kwargs: Any) -> T:
233
+ # TODO: For convenience, make this error out in the same way
234
+ # as it would if the arguments didn't match the __init__
235
+ # method and __init__was called directly, and verify the
236
+ # event's __init__ is valid when initialising the class,
237
+ # just like we do for event-sourced aggregates.
238
+
239
+ assert issubclass(cls, EnduringObject)
240
+ try:
241
+ init_enduring_object_class = _enduring_object_init_classes[cls]
242
+ except KeyError:
243
+ msg = (
244
+ f"Enduring object class {cls.__name__} has no "
245
+ f"InitialDecision class. Please define a subclass of "
246
+ f"InitialDecision as a nested class on {cls.__name__}."
247
+ )
248
+ raise ProgrammingError(msg) from None
249
+
250
+ return cls._create(
251
+ decision_cls=init_enduring_object_class,
252
+ **kwargs,
253
+ )
254
+
255
+
256
+ TID = TypeVar("TID", bound=str, default=str)
257
+
258
+
259
+ class EnduringObject(
260
+ Perspective[TDecision], Generic[TDecision, TID], metaclass=MetaEnduringObject
261
+ ):
262
+ id: TID
263
+
264
+ @classmethod
265
+ def _create(
266
+ cls: type[Self], decision_cls: type[InitialDecision], **kwargs: Any
267
+ ) -> Self:
268
+ enduring_object_id = cls._create_id()
269
+ id_attr_name = decision_cls.id_attr_name(cls)
270
+ assert id_attr_name not in kwargs
271
+ assert "originator_topic" not in kwargs
272
+ assert "tags" not in kwargs
273
+ initial_kwargs: dict[str, Any] = {
274
+ id_attr_name: enduring_object_id,
275
+ "originator_topic": get_topic(cls),
276
+ }
277
+ initial_kwargs.update(kwargs)
278
+ try:
279
+
280
+ tagged = Tagged[TDecision](
281
+ tags=[enduring_object_id],
282
+ decision=cast(type[TDecision], decision_cls)(**initial_kwargs),
283
+ )
284
+ except TypeError as e:
285
+ msg = (
286
+ f"Unable to construct {decision_cls.__qualname__} event "
287
+ f"with kwargs {initial_kwargs}: {e}"
288
+ )
289
+ raise TypeError(msg) from e
290
+ self = cast(Self, tagged.decision.mutate(None))
291
+ assert self is not None
292
+ self.new_decisions.append(tagged)
293
+ return self
294
+
295
+ @classmethod
296
+ def _create_id(cls) -> TID:
297
+ return cast(TID, f"{cls.__name__.lower()}-{uuid4()}")
298
+
299
+ def __post_init__(self) -> None:
300
+ pass
301
+
302
+ def consistency_boundary(self) -> list[Selector]:
303
+ return [Selector(tags=[self.id])]
304
+
305
+ def trigger_event(
306
+ self,
307
+ decision_cls: Callable[P, TDecision],
308
+ tags: Sequence[str] = (),
309
+ *args: P.args,
310
+ **kwargs: P.kwargs,
311
+ ) -> None:
312
+ tags = [self.id, *tags]
313
+ super().trigger_event(decision_cls, tags, *args, **kwargs)
314
+
315
+
316
+ class Group(Perspective[TDecision]):
317
+ _enduring_objects: list[EnduringObject[TDecision]]
318
+
319
+ def __new__(cls, *args: Any, **kwargs: Any) -> Self:
320
+ self = super().__new__(cls, *args, **kwargs)
321
+ self._enduring_objects = [a for a in args if isinstance(a, EnduringObject)]
322
+ return self
323
+
324
+ def consistency_boundary(self) -> list[Selector]:
325
+ return [
326
+ Selector(tags=cb.tags)
327
+ for cbs in [o.consistency_boundary() for o in self._enduring_objects]
328
+ for cb in cbs
329
+ ]
330
+
331
+ def trigger_event(
332
+ self,
333
+ decision_cls: Callable[P, TDecision],
334
+ tags: Sequence[str] = (),
335
+ *args: P.args,
336
+ **kwargs: P.kwargs,
337
+ ) -> None:
338
+ objs = self._enduring_objects
339
+ tags = [o.id for o in objs] + list(tags)
340
+ tagged = Tagged[TDecision](
341
+ tags=tags,
342
+ decision=decision_cls(*args, **kwargs),
343
+ )
344
+ for o in objs:
345
+ tagged.decision.mutate(o)
346
+ self.new_decisions.append(tagged)
347
+
348
+
349
+ @dataclass
350
+ class Selector:
351
+ types: Sequence[type[Decision]] = ()
352
+ tags: Sequence[str] = ()
353
+
354
+
355
+ class MetaSlice(MetaSupportsEventDecorator):
356
+ def __init__(
357
+ cls, name: str, bases: tuple[type, ...], namespace: dict[str, Any]
358
+ ) -> None:
359
+ super().__init__(name, bases, namespace)
360
+ cls.do_projection = len(cls.projected_types) != 0
361
+
362
+
363
+ class Slice(Perspective[TDecision], metaclass=MetaSlice):
364
+ def execute(self) -> None:
365
+ pass
366
+
367
+
368
+ TSlice = TypeVar("TSlice", bound=Slice[Any])
369
+ TGroup = TypeVar("TGroup", bound=Group[Any])
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, TypeVar
4
+
5
+ import msgspec
6
+
7
+ from eventsourcing.dcb import api, domain, persistence
8
+ from eventsourcing.utils import get_topic, resolve_topic
9
+
10
+
11
+ class Decision(msgspec.Struct, domain.Decision):
12
+ def as_dict(self) -> dict[str, Any]:
13
+ return {key: getattr(self, key) for key in self.__struct_fields__}
14
+
15
+
16
+ TDecision = TypeVar("TDecision", bound=Decision)
17
+
18
+
19
+ class MessagePackMapper(persistence.DCBMapper[Decision]):
20
+ def to_dcb_event(self, event: domain.Tagged[TDecision]) -> api.DCBEvent:
21
+ return api.DCBEvent(
22
+ type=get_topic(type(event.decision)),
23
+ data=msgspec.msgpack.encode(event.decision),
24
+ tags=event.tags,
25
+ )
26
+
27
+ def to_domain_event(self, event: api.DCBEvent) -> domain.Tagged[Decision]:
28
+ return domain.Tagged(
29
+ tags=event.tags,
30
+ decision=msgspec.msgpack.decode(
31
+ event.data,
32
+ type=resolve_topic(event.type),
33
+ ),
34
+ )
35
+
36
+
37
+ class InitialDecision(Decision, domain.InitialDecision):
38
+ originator_topic: str
@@ -0,0 +1,193 @@
1
+ from __future__ import annotations
2
+
3
+ import threading
4
+ from abc import ABC, abstractmethod
5
+ from collections.abc import Iterator
6
+ from queue import Queue
7
+ from typing import TYPE_CHECKING, Any, Generic
8
+
9
+ from eventsourcing.dcb.api import (
10
+ DCBAppendCondition,
11
+ DCBEvent,
12
+ DCBQuery,
13
+ DCBQueryItem,
14
+ DCBReadResponse,
15
+ DCBRecorder,
16
+ DCBSequencedEvent,
17
+ DCBSubscription,
18
+ TDCBRecorder_co,
19
+ )
20
+ from eventsourcing.dcb.domain import (
21
+ Selector,
22
+ Tagged,
23
+ TDecision,
24
+ )
25
+ from eventsourcing.persistence import BaseInfrastructureFactory, TTrackingRecorder
26
+ from eventsourcing.utils import get_topic
27
+
28
+ if TYPE_CHECKING:
29
+ from collections.abc import Sequence
30
+
31
+
32
+ class DCBMapper(ABC, Generic[TDecision]):
33
+ @abstractmethod
34
+ def to_dcb_event(self, event: Tagged[TDecision]) -> DCBEvent:
35
+ raise NotImplementedError # pragma: no cover
36
+
37
+ @abstractmethod
38
+ def to_domain_event(self, event: DCBEvent) -> Tagged[TDecision]:
39
+ raise NotImplementedError # pragma: no cover
40
+
41
+
42
+ class DCBEventStore(Generic[TDecision]):
43
+ def __init__(self, mapper: DCBMapper[TDecision], recorder: DCBRecorder):
44
+ self.mapper = mapper
45
+ self.recorder = recorder
46
+
47
+ def append(
48
+ self,
49
+ events: Sequence[Tagged[TDecision]],
50
+ cb: Selector | Sequence[Selector] | None = None,
51
+ after: int | None = None,
52
+ ) -> int:
53
+ if len(events) == 0:
54
+ return 0
55
+ condition = (
56
+ None
57
+ if not cb and not after
58
+ else DCBAppendCondition(
59
+ fail_if_events_match=self._cb_to_dcb_query(cb),
60
+ after=after,
61
+ )
62
+ )
63
+ return self.recorder.append(
64
+ events=[self.mapper.to_dcb_event(e) for e in events],
65
+ condition=condition,
66
+ )
67
+
68
+ def read(
69
+ self,
70
+ cb: Selector | Sequence[Selector] | None = None,
71
+ *,
72
+ after: int | None = None,
73
+ ) -> DCBEventStoreReadResponse[TDecision]:
74
+ query = self._cb_to_dcb_query(cb)
75
+ read_response = self.recorder.read(
76
+ query=query,
77
+ after=after,
78
+ )
79
+ return DCBEventStoreReadResponse(read_response, self.mapper)
80
+
81
+ @staticmethod
82
+ def _cb_to_dcb_query(
83
+ cb: Selector | Sequence[Selector] | None = None,
84
+ ) -> DCBQuery:
85
+ cb = [cb] if isinstance(cb, Selector) else cb or []
86
+ return DCBQuery(
87
+ items=[
88
+ DCBQueryItem(
89
+ types=[get_topic(t) for t in s.types],
90
+ tags=list(s.tags),
91
+ )
92
+ for s in cb
93
+ ]
94
+ )
95
+
96
+
97
+ class DCBEventStoreReadResponse(Iterator[Tagged[TDecision]]):
98
+ def __init__(
99
+ self, dcb_read_response: DCBReadResponse, mapper: DCBMapper[TDecision]
100
+ ):
101
+ self._dcb_read_response = dcb_read_response
102
+ self._mapper = mapper
103
+
104
+ @property
105
+ def head(self) -> int | None:
106
+ return self._dcb_read_response.head
107
+
108
+ def __next__(self) -> Tagged[TDecision]:
109
+ dcb_sequenced_event = self._dcb_read_response.__next__()
110
+ return self._mapper.to_domain_event(dcb_sequenced_event.event)
111
+
112
+
113
+ class NotFoundError(Exception):
114
+ pass
115
+
116
+
117
+ class DCBInfrastructureFactory(BaseInfrastructureFactory[TTrackingRecorder], ABC):
118
+ @abstractmethod
119
+ def dcb_recorder(self) -> DCBRecorder:
120
+ pass # pragma: no cover
121
+
122
+
123
+ class DCBListenNotifySubscription(DCBSubscription[TDCBRecorder_co]):
124
+ def __init__(
125
+ self,
126
+ recorder: TDCBRecorder_co,
127
+ query: DCBQuery | None = None,
128
+ after: int | None = None,
129
+ ) -> None:
130
+ super().__init__(recorder=recorder, query=query, after=after)
131
+ self.select_limit = 500
132
+ self._events: Sequence[DCBSequencedEvent] = []
133
+ self._events_index: int = 0
134
+ self._events_queue: Queue[Sequence[DCBSequencedEvent]] = Queue(maxsize=10)
135
+ self._has_been_notified = threading.Event()
136
+ self._thread_error: BaseException | None = None
137
+ self._pull_thread = threading.Thread(target=self._loop_on_pull)
138
+ self._pull_thread.start()
139
+
140
+ def __exit__(self, *args: object, **kwargs: Any) -> None:
141
+ super().__exit__(*args, **kwargs)
142
+ self._pull_thread.join()
143
+
144
+ def stop(self) -> None:
145
+ """Stops the subscription."""
146
+ super().stop()
147
+ self._events_queue.put([])
148
+ self._has_been_notified.set()
149
+
150
+ def __next__(self) -> DCBSequencedEvent:
151
+ # If necessary, get a new list of events from the recorder.
152
+ if self._events_index == len(self._events) and not self._has_been_stopped:
153
+ self._events = self._events_queue.get()
154
+ self._events_index = 0
155
+
156
+ # Stop the iteration if necessary, maybe raise thread error.
157
+ if self._has_been_stopped or not self._events:
158
+ if self._thread_error is not None:
159
+ raise self._thread_error
160
+ raise StopIteration
161
+
162
+ # Return a notification from previously obtained list.
163
+ notification = self._events[self._events_index]
164
+ self._events_index += 1
165
+ return notification
166
+
167
+ def _loop_on_pull(self) -> None:
168
+ try:
169
+ self._pull() # Already recorded events.
170
+ while not self._has_been_stopped:
171
+ self._has_been_notified.wait()
172
+ self._pull() # Newly recorded events.
173
+ except BaseException as e: # pragma: no cover
174
+ if self._thread_error is None:
175
+ self._thread_error = e
176
+ self.stop()
177
+
178
+ def _pull(self) -> None:
179
+ while not self._has_been_stopped:
180
+ self._has_been_notified.clear()
181
+ events = list(
182
+ self._recorder.read(
183
+ query=self._query,
184
+ after=self._last_position,
185
+ limit=self.select_limit,
186
+ )
187
+ )
188
+ if len(events) > 0:
189
+ # print("Putting", len(events), "events into queue")
190
+ self._events_queue.put(events)
191
+ self._last_position = events[-1].position
192
+ if len(events) < self.select_limit:
193
+ break