python-event-sourcery 0.5.1a5__tar.gz → 0.5.2__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 (66) hide show
  1. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/PKG-INFO +1 -1
  2. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery/event_sourcing/__init__.py +2 -1
  3. python_event_sourcery-0.5.2/event_sourcery/event_sourcing/aggregate.py +123 -0
  4. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery/event_sourcing/repository.py +27 -26
  5. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/pyproject.toml +1 -1
  6. python_event_sourcery-0.5.1a5/event_sourcery/event_sourcing/aggregate.py +0 -67
  7. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/LICENSE +0 -0
  8. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/README.md +0 -0
  9. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery/__init__.py +0 -0
  10. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery/_event_store/__init__.py +0 -0
  11. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery/_event_store/backend.py +0 -0
  12. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery/_event_store/event/__init__.py +0 -0
  13. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery/_event_store/event/dto.py +0 -0
  14. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery/_event_store/event/encryption.py +0 -0
  15. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery/_event_store/event/registry.py +0 -0
  16. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery/_event_store/event/serde.py +0 -0
  17. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery/_event_store/event_store.py +0 -0
  18. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery/_event_store/in_memory.py +0 -0
  19. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery/_event_store/outbox.py +0 -0
  20. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery/_event_store/stream_id.py +0 -0
  21. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery/_event_store/subscription/__init__.py +0 -0
  22. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery/_event_store/subscription/builder.py +0 -0
  23. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery/_event_store/subscription/in_transaction.py +0 -0
  24. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery/_event_store/subscription/interfaces.py +0 -0
  25. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery/_event_store/tenant_id.py +0 -0
  26. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery/_event_store/versioning.py +0 -0
  27. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery/backend.py +0 -0
  28. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery/encryption.py +0 -0
  29. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery/event.py +0 -0
  30. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery/exceptions.py +0 -0
  31. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery/in_transaction.py +0 -0
  32. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery/interfaces.py +0 -0
  33. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery/outbox.py +0 -0
  34. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery/py.typed +0 -0
  35. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery/read_model/__init__.py +0 -0
  36. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery/read_model/cursors_dao.py +0 -0
  37. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery/read_model/projector.py +0 -0
  38. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery/subscription.py +0 -0
  39. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery_django/__init__.py +0 -0
  40. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery_django/dto.py +0 -0
  41. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery_django/event_store.py +0 -0
  42. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery_django/migrations/0001_initial.py +0 -0
  43. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery_django/migrations/__init__.py +0 -0
  44. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery_django/models.py +0 -0
  45. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery_django/outbox.py +0 -0
  46. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery_django/py.typed +0 -0
  47. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery_django/subscription.py +0 -0
  48. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery_kurrentdb/__init__.py +0 -0
  49. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery_kurrentdb/dto.py +0 -0
  50. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery_kurrentdb/event_store.py +0 -0
  51. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery_kurrentdb/outbox.py +0 -0
  52. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery_kurrentdb/py.typed +0 -0
  53. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery_kurrentdb/stream.py +0 -0
  54. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery_kurrentdb/subscription.py +0 -0
  55. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery_sqlalchemy/__init__.py +0 -0
  56. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery_sqlalchemy/cursors_dao.py +0 -0
  57. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery_sqlalchemy/dto.py +0 -0
  58. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery_sqlalchemy/event_store.py +0 -0
  59. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery_sqlalchemy/guid.py +0 -0
  60. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery_sqlalchemy/jsonb.py +0 -0
  61. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery_sqlalchemy/models/__init__.py +0 -0
  62. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery_sqlalchemy/models/base.py +0 -0
  63. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery_sqlalchemy/models/default.py +0 -0
  64. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery_sqlalchemy/outbox.py +0 -0
  65. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery_sqlalchemy/py.typed +0 -0
  66. {python_event_sourcery-0.5.1a5 → python_event_sourcery-0.5.2}/event_sourcery_sqlalchemy/subscription.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: python-event-sourcery
3
- Version: 0.5.1a5
3
+ Version: 0.5.2
4
4
  Summary: A library for event-based systems in Python.
5
5
  License: MIT
6
6
  Author: Sebastian Buczyński
@@ -1,7 +1,8 @@
1
1
  __all__ = [
2
2
  "Aggregate",
3
3
  "Repository",
4
+ "WrappedAggregate",
4
5
  ]
5
6
 
6
- from event_sourcery.event_sourcing.aggregate import Aggregate
7
+ from event_sourcery.event_sourcing.aggregate import Aggregate, WrappedAggregate
7
8
  from event_sourcery.event_sourcing.repository import Repository
@@ -0,0 +1,123 @@
1
+ import dataclasses
2
+ from collections.abc import Iterator
3
+ from contextlib import contextmanager
4
+ from datetime import datetime
5
+ from typing import ClassVar, Generic, TypeVar
6
+
7
+ from event_sourcery import StreamCategory, StreamId
8
+ from event_sourcery.event import Context, Event
9
+
10
+
11
+ class Aggregate:
12
+ """
13
+ Base class for event-sourced aggregates.
14
+
15
+ Aggregates encapsulate domain logic and state changes as a sequence of events.
16
+ They are responsible for applying events to mutate their state and for emitting new
17
+ events that represent business-relevant changes.
18
+
19
+ The Aggregate base class provides mechanisms for tracking unpersisted changes and
20
+ for applying events in a consistent way.
21
+
22
+ Attributes:
23
+ category (ClassVar[StreamCategory]): StreamCategory for the aggregate type (group streams).
24
+ __changes__ (list[Event]): List of yet not persisted events.
25
+ """
26
+
27
+ category: ClassVar[StreamCategory]
28
+ _changes: list[Event]
29
+
30
+ @property
31
+ def __changes__(self) -> list[Event]:
32
+ return list(getattr(self, "_changes", []))
33
+
34
+ @contextmanager
35
+ def __persisting_changes__(self) -> Iterator[Iterator[Event]]:
36
+ """
37
+ Context manager for accessing and clearing unpersisted events.
38
+
39
+ Yields an iterator over all events that have been applied but not yet persisted.
40
+ After exiting the context, the list of unpersisted events is cleared.
41
+
42
+ Returns:
43
+ Iterator[Iterator[Event]]: Iterator over unpersisted events.
44
+ """
45
+ yield iter(self.__changes__)
46
+
47
+ def __apply__(self, event: Event) -> None:
48
+ """
49
+ Applies a single event to mutate the aggregate's state.
50
+
51
+ This method must be implemented by subclasses to define how each event type
52
+ changes the aggregate's state.
53
+
54
+ Args:
55
+ event (Event): The event to apply.
56
+ """
57
+ raise NotImplementedError
58
+
59
+ def _emit(self, event: Event) -> None:
60
+ """
61
+ Applies and tracks a new event as a pending change.
62
+
63
+ Calls __apply__ to mutate the aggregate's state and appends the event to the list
64
+ of unpersisted changes.
65
+
66
+ Args:
67
+ event (Event): The event to emit and track.
68
+ """
69
+ if not hasattr(self, "_changes"):
70
+ self._changes = []
71
+ self.__apply__(event)
72
+ self._changes.append(event)
73
+
74
+
75
+ TAggregate = TypeVar("TAggregate", bound=Aggregate)
76
+ TContext = TypeVar("TContext", bound=Context)
77
+
78
+
79
+ @dataclasses.dataclass()
80
+ class WrappedAggregate(Generic[TAggregate]):
81
+ """
82
+ Wraps an aggregate instance with stream-level metadata.
83
+
84
+ Provides access to the aggregate alongside information such as version,
85
+ timestamps, and whether the aggregate is newly created. Follows the same
86
+ wrapper pattern as ``WrappedEvent``.
87
+
88
+ Attributes:
89
+ aggregate: The aggregate instance.
90
+ stream_id: The stream identity (UUID + category).
91
+ context: The context passed for this operation, if any.
92
+ created_at: Timestamp of the first event in the stream.
93
+ updated_at: Timestamp of the last event in the stream.
94
+ stored_version: Number of events persisted before this session.
95
+ """
96
+
97
+ aggregate: TAggregate
98
+ stream_id: StreamId
99
+ context: Context = dataclasses.field(default_factory=Context)
100
+ stored_version: int = 0
101
+ created_at: datetime | None = None
102
+ updated_at: datetime | None = None
103
+
104
+ @property
105
+ def version(self) -> int:
106
+ """Current version of the aggregate, including pending events."""
107
+ return self.stored_version + len(getattr(self.aggregate, "__changes__", []))
108
+
109
+ @property
110
+ def is_new(self) -> bool:
111
+ """Whether the aggregate is newly created (no events existed before)."""
112
+ return self.stored_version == 0
113
+
114
+ def get_context(self, context_type: type[TContext]) -> TContext:
115
+ """Convert the stored context to a specific context type.
116
+
117
+ Args:
118
+ context_type: The target context class to validate against.
119
+
120
+ Returns:
121
+ An instance of *context_type* populated from the stored context data.
122
+ """
123
+ return context_type.model_validate(self.context.model_dump())
@@ -5,6 +5,7 @@ from typing import Generic, TypeVar, cast
5
5
  from event_sourcery import EventStore, StreamId, StreamUUID
6
6
  from event_sourcery.event import Context, Event, WrappedEvent
7
7
  from event_sourcery.event_sourcing import Aggregate
8
+ from event_sourcery.event_sourcing.aggregate import WrappedAggregate
8
9
 
9
10
  TAggregate = TypeVar("TAggregate", bound=Aggregate)
10
11
  TEvent = TypeVar("TEvent", bound=Event)
@@ -28,13 +29,13 @@ class Repository(Generic[TAggregate]):
28
29
  uuid: StreamUUID,
29
30
  aggregate: TAggregate,
30
31
  context: Context | None = None,
31
- ) -> Iterator[TAggregate]:
32
+ ) -> Iterator[WrappedAggregate[TAggregate]]:
32
33
  """
33
34
  Context manager for loading an aggregate instance.
34
35
 
35
36
  Loads the aggregate's event stream, replays events to reconstruct its state,
36
- yields the aggregate for use, and persists any new events emitted during the
37
- context.
37
+ yields a ``WrappedAggregate`` containing the aggregate and stream metadata,
38
+ and persists any new events emitted during the context.
38
39
 
39
40
  Args:
40
41
  uuid (StreamUUID): The unique identifier of the aggregate's stream.
@@ -42,33 +43,33 @@ class Repository(Generic[TAggregate]):
42
43
  context (Context | None): Optional context to attach to all emitted events.
43
44
 
44
45
  Yields:
45
- TAggregate: The loaded and ready-to-use aggregate instance.
46
+ WrappedAggregate[TAggregate]: The aggregate wrapped with stream metadata.
46
47
  """
47
48
  stream_id = StreamId(uuid=uuid, name=uuid.name, category=aggregate.category)
48
- old_version = self._load(stream_id, aggregate)
49
- yield aggregate
50
- self._save(aggregate, old_version, stream_id, context)
51
49
 
52
- def _load(self, stream_id: StreamId, aggregate: TAggregate) -> int:
53
- stream = self._event_store.load_stream(stream_id)
54
- last_version = 0
55
- for envelope in stream:
56
- aggregate.__apply__(envelope.event)
57
- last_version = cast(int, envelope.version)
50
+ wrapped = WrappedAggregate(
51
+ aggregate=aggregate,
52
+ stream_id=stream_id,
53
+ context=context or Context(),
54
+ )
55
+ self._load(wrapped)
56
+ yield wrapped
57
+ self._save(wrapped)
58
58
 
59
- return last_version
59
+ def _load(self, wrapped: WrappedAggregate[TAggregate]) -> None:
60
+ stream = self._event_store.load_stream(wrapped.stream_id)
61
+ for envelope in stream:
62
+ wrapped.aggregate.__apply__(envelope.event)
63
+ wrapped.stored_version = cast(int, envelope.version)
64
+ if wrapped.created_at is None:
65
+ wrapped.created_at = envelope.created_at
66
+ wrapped.updated_at = envelope.created_at
60
67
 
61
- def _save(
62
- self,
63
- aggregate: TAggregate,
64
- old_version: int,
65
- stream_id: StreamId,
66
- context: Context | None = None,
67
- ) -> None:
68
- with aggregate.__persisting_changes__() as pending:
69
- start_from = old_version + 1
68
+ def _save(self, wrapped: WrappedAggregate[TAggregate]) -> None:
69
+ with wrapped.aggregate.__persisting_changes__() as pending:
70
+ start_from = wrapped.stored_version + 1
70
71
  events = [
71
- WrappedEvent.wrap(event, version, context=context)
72
+ WrappedEvent.wrap(event, version, context=wrapped.context)
72
73
  for version, event in enumerate(pending, start=start_from)
73
74
  ]
74
75
 
@@ -77,6 +78,6 @@ class Repository(Generic[TAggregate]):
77
78
 
78
79
  self._event_store.append(
79
80
  *events,
80
- stream_id=stream_id,
81
- expected_version=old_version,
81
+ stream_id=wrapped.stream_id,
82
+ expected_version=wrapped.stored_version,
82
83
  )
@@ -3,7 +3,7 @@ name = "python-event-sourcery"
3
3
  requires-python = ">=3.10"
4
4
 
5
5
  [tool.poetry]
6
- version = "0.5.1a5"
6
+ version = "0.5.2"
7
7
  description = "A library for event-based systems in Python."
8
8
  readme = "README.md"
9
9
  authors = [
@@ -1,67 +0,0 @@
1
- from collections.abc import Iterator
2
- from contextlib import contextmanager
3
- from typing import ClassVar
4
-
5
- from event_sourcery import StreamCategory
6
- from event_sourcery.event import Event
7
-
8
-
9
- class Aggregate:
10
- """
11
- Base class for event-sourced aggregates.
12
-
13
- Aggregates encapsulate domain logic and state changes as a sequence of events.
14
- They are responsible for applying events to mutate their state and for emitting new
15
- events that represent business-relevant changes.
16
-
17
- The Aggregate base class provides mechanisms for tracking unpersisted changes and
18
- for applying events in a consistent way.
19
-
20
- Attributes:
21
- category (ClassVar[StreamCategory]): StreamCategory for the aggregate type (group streams).
22
- _changes (list[Event]): List of yet not persisted events.
23
- """
24
-
25
- category: ClassVar[StreamCategory]
26
- _changes: list[Event]
27
-
28
- @contextmanager
29
- def __persisting_changes__(self) -> Iterator[Iterator[Event]]:
30
- """
31
- Context manager for accessing and clearing unpersisted events.
32
-
33
- Yields an iterator over all events that have been applied but not yet persisted.
34
- After exiting the context, the list of unpersisted events is cleared.
35
-
36
- Returns:
37
- Iterator[Iterator[Event]]: Iterator over unpersisted events.
38
- """
39
- yield iter(getattr(self, "_changes", []))
40
- self._changes = []
41
-
42
- def __apply__(self, event: Event) -> None:
43
- """
44
- Applies a single event to mutate the aggregate's state.
45
-
46
- This method must be implemented by subclasses to define how each event type
47
- changes the aggregate's state.
48
-
49
- Args:
50
- event (Event): The event to apply.
51
- """
52
- raise NotImplementedError
53
-
54
- def _emit(self, event: Event) -> None:
55
- """
56
- Applies and tracks a new event as a pending change.
57
-
58
- Calls __apply__ to mutate the aggregate's state and appends the event to the list
59
- of unpersisted changes.
60
-
61
- Args:
62
- event (Event): The event to emit and track.
63
- """
64
- if not hasattr(self, "_changes"):
65
- self._changes = []
66
- self.__apply__(event)
67
- self._changes.append(event)