eventsourcing 9.2.22__py3-none-any.whl → 9.3.0a1__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.

Potentially problematic release.


This version of eventsourcing might be problematic. Click here for more details.

Files changed (144) hide show
  1. eventsourcing/__init__.py +1 -1
  2. eventsourcing/application.py +106 -135
  3. eventsourcing/cipher.py +15 -12
  4. eventsourcing/dispatch.py +31 -91
  5. eventsourcing/domain.py +138 -143
  6. eventsourcing/examples/__init__.py +0 -0
  7. eventsourcing/examples/aggregate1/__init__.py +0 -0
  8. eventsourcing/examples/aggregate1/application.py +27 -0
  9. eventsourcing/examples/aggregate1/domainmodel.py +16 -0
  10. eventsourcing/examples/aggregate1/test_application.py +37 -0
  11. eventsourcing/examples/aggregate2/__init__.py +0 -0
  12. eventsourcing/examples/aggregate2/application.py +27 -0
  13. eventsourcing/examples/aggregate2/domainmodel.py +22 -0
  14. eventsourcing/examples/aggregate2/test_application.py +37 -0
  15. eventsourcing/examples/aggregate3/__init__.py +0 -0
  16. eventsourcing/examples/aggregate3/application.py +27 -0
  17. eventsourcing/examples/aggregate3/domainmodel.py +38 -0
  18. eventsourcing/examples/aggregate3/test_application.py +37 -0
  19. eventsourcing/examples/aggregate4/__init__.py +0 -0
  20. eventsourcing/examples/aggregate4/application.py +27 -0
  21. eventsourcing/examples/aggregate4/domainmodel.py +128 -0
  22. eventsourcing/examples/aggregate4/test_application.py +38 -0
  23. eventsourcing/examples/aggregate5/__init__.py +0 -0
  24. eventsourcing/examples/aggregate5/application.py +27 -0
  25. eventsourcing/examples/aggregate5/domainmodel.py +131 -0
  26. eventsourcing/examples/aggregate5/test_application.py +38 -0
  27. eventsourcing/examples/aggregate6/__init__.py +0 -0
  28. eventsourcing/examples/aggregate6/application.py +30 -0
  29. eventsourcing/examples/aggregate6/domainmodel.py +123 -0
  30. eventsourcing/examples/aggregate6/test_application.py +38 -0
  31. eventsourcing/examples/aggregate6a/__init__.py +0 -0
  32. eventsourcing/examples/aggregate6a/application.py +40 -0
  33. eventsourcing/examples/aggregate6a/domainmodel.py +149 -0
  34. eventsourcing/examples/aggregate6a/test_application.py +45 -0
  35. eventsourcing/examples/aggregate7/__init__.py +0 -0
  36. eventsourcing/examples/aggregate7/application.py +48 -0
  37. eventsourcing/examples/aggregate7/domainmodel.py +144 -0
  38. eventsourcing/examples/aggregate7/persistence.py +57 -0
  39. eventsourcing/examples/aggregate7/test_application.py +38 -0
  40. eventsourcing/examples/aggregate7/test_compression_and_encryption.py +45 -0
  41. eventsourcing/examples/aggregate7/test_snapshotting_intervals.py +67 -0
  42. eventsourcing/examples/aggregate7a/__init__.py +0 -0
  43. eventsourcing/examples/aggregate7a/application.py +56 -0
  44. eventsourcing/examples/aggregate7a/domainmodel.py +170 -0
  45. eventsourcing/examples/aggregate7a/test_application.py +46 -0
  46. eventsourcing/examples/aggregate7a/test_compression_and_encryption.py +45 -0
  47. eventsourcing/examples/aggregate8/__init__.py +0 -0
  48. eventsourcing/examples/aggregate8/application.py +47 -0
  49. eventsourcing/examples/aggregate8/domainmodel.py +65 -0
  50. eventsourcing/examples/aggregate8/persistence.py +57 -0
  51. eventsourcing/examples/aggregate8/test_application.py +37 -0
  52. eventsourcing/examples/aggregate8/test_compression_and_encryption.py +44 -0
  53. eventsourcing/examples/aggregate8/test_snapshotting_intervals.py +38 -0
  54. eventsourcing/examples/bankaccounts/__init__.py +0 -0
  55. eventsourcing/examples/bankaccounts/application.py +70 -0
  56. eventsourcing/examples/bankaccounts/domainmodel.py +56 -0
  57. eventsourcing/examples/bankaccounts/test.py +173 -0
  58. eventsourcing/examples/cargoshipping/__init__.py +0 -0
  59. eventsourcing/examples/cargoshipping/application.py +126 -0
  60. eventsourcing/examples/cargoshipping/domainmodel.py +330 -0
  61. eventsourcing/examples/cargoshipping/interface.py +143 -0
  62. eventsourcing/examples/cargoshipping/test.py +231 -0
  63. eventsourcing/examples/contentmanagement/__init__.py +0 -0
  64. eventsourcing/examples/contentmanagement/application.py +118 -0
  65. eventsourcing/examples/contentmanagement/domainmodel.py +69 -0
  66. eventsourcing/examples/contentmanagement/test.py +180 -0
  67. eventsourcing/examples/contentmanagement/utils.py +26 -0
  68. eventsourcing/examples/contentmanagementsystem/__init__.py +0 -0
  69. eventsourcing/examples/contentmanagementsystem/application.py +54 -0
  70. eventsourcing/examples/contentmanagementsystem/postgres.py +17 -0
  71. eventsourcing/examples/contentmanagementsystem/sqlite.py +17 -0
  72. eventsourcing/examples/contentmanagementsystem/system.py +14 -0
  73. eventsourcing/examples/contentmanagementsystem/test_system.py +174 -0
  74. eventsourcing/examples/searchablecontent/__init__.py +0 -0
  75. eventsourcing/examples/searchablecontent/application.py +45 -0
  76. eventsourcing/examples/searchablecontent/persistence.py +23 -0
  77. eventsourcing/examples/searchablecontent/postgres.py +118 -0
  78. eventsourcing/examples/searchablecontent/sqlite.py +136 -0
  79. eventsourcing/examples/searchablecontent/test_application.py +111 -0
  80. eventsourcing/examples/searchablecontent/test_recorder.py +69 -0
  81. eventsourcing/examples/searchabletimestamps/__init__.py +0 -0
  82. eventsourcing/examples/searchabletimestamps/application.py +32 -0
  83. eventsourcing/examples/searchabletimestamps/persistence.py +20 -0
  84. eventsourcing/examples/searchabletimestamps/postgres.py +110 -0
  85. eventsourcing/examples/searchabletimestamps/sqlite.py +99 -0
  86. eventsourcing/examples/searchabletimestamps/test_searchabletimestamps.py +91 -0
  87. eventsourcing/examples/test_invoice.py +176 -0
  88. eventsourcing/examples/test_parking_lot.py +206 -0
  89. eventsourcing/interface.py +2 -2
  90. eventsourcing/persistence.py +85 -81
  91. eventsourcing/popo.py +30 -31
  92. eventsourcing/postgres.py +361 -578
  93. eventsourcing/sqlite.py +91 -99
  94. eventsourcing/system.py +42 -57
  95. eventsourcing/tests/application.py +20 -32
  96. eventsourcing/tests/application_tests/__init__.py +0 -0
  97. eventsourcing/tests/application_tests/test_application_with_automatic_snapshotting.py +55 -0
  98. eventsourcing/tests/application_tests/test_application_with_popo.py +22 -0
  99. eventsourcing/tests/application_tests/test_application_with_postgres.py +75 -0
  100. eventsourcing/tests/application_tests/test_application_with_sqlite.py +72 -0
  101. eventsourcing/tests/application_tests/test_cache.py +134 -0
  102. eventsourcing/tests/application_tests/test_event_sourced_log.py +162 -0
  103. eventsourcing/tests/application_tests/test_notificationlog.py +232 -0
  104. eventsourcing/tests/application_tests/test_notificationlogreader.py +126 -0
  105. eventsourcing/tests/application_tests/test_processapplication.py +110 -0
  106. eventsourcing/tests/application_tests/test_processingpolicy.py +109 -0
  107. eventsourcing/tests/application_tests/test_repository.py +504 -0
  108. eventsourcing/tests/application_tests/test_snapshotting.py +68 -0
  109. eventsourcing/tests/application_tests/test_upcasting.py +459 -0
  110. eventsourcing/tests/docs_tests/__init__.py +0 -0
  111. eventsourcing/tests/docs_tests/test_docs.py +293 -0
  112. eventsourcing/tests/domain.py +1 -1
  113. eventsourcing/tests/domain_tests/__init__.py +0 -0
  114. eventsourcing/tests/domain_tests/test_aggregate.py +1159 -0
  115. eventsourcing/tests/domain_tests/test_aggregate_decorators.py +1604 -0
  116. eventsourcing/tests/domain_tests/test_domainevent.py +80 -0
  117. eventsourcing/tests/interface_tests/__init__.py +0 -0
  118. eventsourcing/tests/interface_tests/test_remotenotificationlog.py +258 -0
  119. eventsourcing/tests/persistence.py +49 -50
  120. eventsourcing/tests/persistence_tests/__init__.py +0 -0
  121. eventsourcing/tests/persistence_tests/test_aes.py +93 -0
  122. eventsourcing/tests/persistence_tests/test_connection_pool.py +722 -0
  123. eventsourcing/tests/persistence_tests/test_eventstore.py +72 -0
  124. eventsourcing/tests/persistence_tests/test_infrastructure_factory.py +21 -0
  125. eventsourcing/tests/persistence_tests/test_mapper.py +113 -0
  126. eventsourcing/tests/persistence_tests/test_noninterleaving_notification_ids.py +69 -0
  127. eventsourcing/tests/persistence_tests/test_popo.py +124 -0
  128. eventsourcing/tests/persistence_tests/test_postgres.py +1121 -0
  129. eventsourcing/tests/persistence_tests/test_sqlite.py +348 -0
  130. eventsourcing/tests/persistence_tests/test_transcoder.py +44 -0
  131. eventsourcing/tests/postgres_utils.py +7 -7
  132. eventsourcing/tests/system_tests/__init__.py +0 -0
  133. eventsourcing/tests/system_tests/test_runner.py +935 -0
  134. eventsourcing/tests/system_tests/test_system.py +287 -0
  135. eventsourcing/tests/utils_tests/__init__.py +0 -0
  136. eventsourcing/tests/utils_tests/test_utils.py +226 -0
  137. eventsourcing/utils.py +47 -50
  138. {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0a1.dist-info}/METADATA +28 -80
  139. eventsourcing-9.3.0a1.dist-info/RECORD +144 -0
  140. {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0a1.dist-info}/WHEEL +1 -2
  141. eventsourcing-9.2.22.dist-info/AUTHORS +0 -10
  142. eventsourcing-9.2.22.dist-info/RECORD +0 -25
  143. eventsourcing-9.2.22.dist-info/top_level.txt +0 -1
  144. {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0a1.dist-info}/LICENSE +0 -0
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+ from unittest import TestCase
4
+
5
+ from eventsourcing.examples.aggregate7.application import DogSchool
6
+ from eventsourcing.examples.aggregate7.domainmodel import project_dog
7
+
8
+
9
+ class TestDogSchool(TestCase):
10
+ def test_dog_school(self) -> None:
11
+ # Construct application object.
12
+ school = DogSchool()
13
+
14
+ # Evolve application state.
15
+ dog_id = school.register_dog("Fido")
16
+ school.add_trick(dog_id, "roll over")
17
+ school.add_trick(dog_id, "play dead")
18
+
19
+ # Query application state.
20
+ dog = school.get_dog(dog_id)
21
+ self.assertEqual(dog["name"], "Fido")
22
+ self.assertEqual(dog["tricks"], ("roll over", "play dead"))
23
+
24
+ # Select notifications.
25
+ notifications = school.notification_log.select(start=1, limit=10)
26
+ assert len(notifications) == 3
27
+
28
+ # Take snapshot.
29
+ school.take_snapshot(dog_id, version=3, projector_func=project_dog)
30
+ dog = school.get_dog(dog_id)
31
+ self.assertEqual(dog["name"], "Fido")
32
+ self.assertEqual(dog["tricks"], ("roll over", "play dead"))
33
+
34
+ # Continue with snapshotted aggregate.
35
+ school.add_trick(dog_id, "fetch ball")
36
+ dog = school.get_dog(dog_id)
37
+ self.assertEqual(dog["name"], "Fido")
38
+ self.assertEqual(dog["tricks"], ("roll over", "play dead", "fetch ball"))
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ from unittest import TestCase
4
+
5
+ from eventsourcing.cipher import AESCipher
6
+ from eventsourcing.examples.aggregate7.application import DogSchool
7
+ from eventsourcing.examples.aggregate7.domainmodel import project_dog
8
+
9
+
10
+ class TestDogSchool(TestCase):
11
+ def test_dog_school(self) -> None:
12
+ # Construct application object.
13
+ school = DogSchool(
14
+ env={
15
+ "COMPRESSOR_TOPIC": "eventsourcing.compressor:ZlibCompressor",
16
+ "CIPHER_TOPIC": "eventsourcing.cipher:AESCipher",
17
+ "CIPHER_KEY": AESCipher.create_key(num_bytes=32),
18
+ }
19
+ )
20
+
21
+ # Evolve application state.
22
+ dog_id = school.register_dog("Fido")
23
+ school.add_trick(dog_id, "roll over")
24
+ school.add_trick(dog_id, "play dead")
25
+
26
+ # Query application state.
27
+ dog = school.get_dog(dog_id)
28
+ assert dog["name"] == "Fido"
29
+ self.assertEqual(dog["tricks"], ("roll over", "play dead"))
30
+
31
+ # Select notifications.
32
+ notifications = school.notification_log.select(start=1, limit=10)
33
+ assert len(notifications) == 3
34
+
35
+ # Take snapshot.
36
+ school.take_snapshot(dog_id, version=3, projector_func=project_dog)
37
+ dog = school.get_dog(dog_id)
38
+ assert dog["name"] == "Fido"
39
+ self.assertEqual(dog["tricks"], ("roll over", "play dead"))
40
+
41
+ # Continue with snapshotted aggregate.
42
+ school.add_trick(dog_id, "fetch ball")
43
+ dog = school.get_dog(dog_id)
44
+ assert dog["name"] == "Fido"
45
+ self.assertEqual(dog["tricks"], ("roll over", "play dead", "fetch ball"))
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, ClassVar, Dict, Type, cast
4
+ from unittest import TestCase
5
+
6
+ from eventsourcing.domain import MutableOrImmutableAggregate, ProgrammingError
7
+ from eventsourcing.examples.aggregate7.application import DogSchool
8
+ from eventsourcing.examples.aggregate7.domainmodel import (
9
+ Dog,
10
+ Trick,
11
+ add_trick,
12
+ project_dog,
13
+ register_dog,
14
+ )
15
+
16
+ if TYPE_CHECKING: # pragma: nocover
17
+ from uuid import UUID
18
+
19
+
20
+ class SubDogSchool(DogSchool):
21
+ snapshotting_intervals: ClassVar[
22
+ Dict[Type[MutableOrImmutableAggregate], int] | None
23
+ ] = {Dog: 1}
24
+
25
+ def register_dog(self, name: str) -> UUID:
26
+ event = register_dog(name)
27
+ dog = project_dog(None, [event])
28
+ self.save(dog, event)
29
+ return event.originator_id
30
+
31
+ def add_trick(self, dog_id: UUID, trick: str) -> None:
32
+ dog = self.repository.get(dog_id, projector_func=project_dog)
33
+ event = add_trick(dog, Trick(name=trick))
34
+ dog = cast(Dog, project_dog(dog, [event]))
35
+ self.save(dog, event)
36
+
37
+
38
+ class TestDogSchool(TestCase):
39
+ def test_dog_school(self) -> None:
40
+ # Construct application object.
41
+ school = SubDogSchool()
42
+
43
+ # Check error when snapshotting_projectors not set.
44
+ with self.assertRaises(ProgrammingError) as cm:
45
+ school.register_dog("Fido")
46
+
47
+ self.assertIn("Cannot take snapshot", cm.exception.args[0])
48
+
49
+ # Set snapshotting_projectors.
50
+ SubDogSchool.snapshotting_projectors = {Dog: project_dog}
51
+
52
+ # Check snapshotting when snapshotting_projectors is set.
53
+ dog_id = school.register_dog("Fido")
54
+
55
+ assert school.snapshots is not None
56
+ self.assertEqual(1, len(list(school.snapshots.get(dog_id))))
57
+
58
+ school.add_trick(dog_id, "roll over")
59
+ self.assertEqual(2, len(list(school.snapshots.get(dog_id))))
60
+
61
+ school.add_trick(dog_id, "play dead")
62
+ self.assertEqual(3, len(list(school.snapshots.get(dog_id))))
63
+
64
+ # Query application state.
65
+ dog = school.get_dog(dog_id)
66
+ self.assertEqual(dog["name"], "Fido")
67
+ self.assertEqual(dog["tricks"], ("roll over", "play dead"))
File without changes
@@ -0,0 +1,56 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, ClassVar, Dict, Type
4
+
5
+ from eventsourcing.application import Application, ProjectorFunction
6
+ from eventsourcing.examples.aggregate7.persistence import (
7
+ OrjsonTranscoder,
8
+ PydanticMapper,
9
+ )
10
+ from eventsourcing.examples.aggregate7a.domainmodel import (
11
+ Dog,
12
+ Snapshot,
13
+ add_trick,
14
+ project_dog,
15
+ register_dog,
16
+ )
17
+
18
+ if TYPE_CHECKING: # pragma: nocover
19
+ from uuid import UUID
20
+
21
+ from eventsourcing.domain import MutableOrImmutableAggregate
22
+ from eventsourcing.persistence import Mapper, Transcoder
23
+
24
+
25
+ class DogSchool(Application):
26
+ is_snapshotting_enabled = True
27
+ snapshot_class = Snapshot
28
+ snapshotting_intervals: ClassVar[
29
+ Dict[Type[MutableOrImmutableAggregate], int] | None
30
+ ] = {Dog: 5}
31
+ snapshotting_projectors: ClassVar[
32
+ Dict[Type[MutableOrImmutableAggregate], ProjectorFunction[Any, Any]] | None
33
+ ] = {Dog: project_dog}
34
+
35
+ def register_dog(self, name: str) -> UUID:
36
+ dog = register_dog(name)
37
+ self.save(dog)
38
+ return dog.id
39
+
40
+ def add_trick(self, dog_id: UUID, trick: str) -> None:
41
+ dog = self.repository.get(dog_id, projector_func=project_dog)
42
+ dog = add_trick(dog, trick)
43
+ self.save(dog)
44
+
45
+ def get_dog(self, dog_id: UUID) -> Dict[str, Any]:
46
+ dog = self.repository.get(dog_id, projector_func=project_dog)
47
+ return {"name": dog.name, "tricks": tuple([t.name for t in dog.tricks])}
48
+
49
+ def construct_mapper(self) -> Mapper:
50
+ return self.factory.mapper(
51
+ transcoder=self.construct_transcoder(),
52
+ mapper_class=PydanticMapper,
53
+ )
54
+
55
+ def construct_transcoder(self) -> Transcoder:
56
+ return OrjsonTranscoder()
@@ -0,0 +1,170 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ from collections import defaultdict
5
+ from datetime import datetime, timezone
6
+ from functools import singledispatch
7
+ from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, TypeVar
8
+ from uuid import UUID, uuid4
9
+
10
+ from pydantic import BaseModel
11
+
12
+ from eventsourcing.utils import get_topic
13
+
14
+
15
+ class DomainEvent(BaseModel):
16
+ originator_id: UUID
17
+ originator_version: int
18
+ timestamp: datetime
19
+
20
+ class Config:
21
+ frozen = True
22
+
23
+
24
+ def create_timestamp() -> datetime:
25
+ return datetime.now(tz=timezone.utc)
26
+
27
+
28
+ class Aggregate(BaseModel):
29
+ id: UUID
30
+ version: int
31
+ created_on: datetime
32
+ modified_on: datetime
33
+
34
+ class Config:
35
+ frozen = True
36
+
37
+ def hold_event(self, event: DomainEvent) -> None:
38
+ all_pending_events[id(self)].append(event)
39
+
40
+ def collect_events(self) -> List[DomainEvent]:
41
+ try:
42
+ return all_pending_events.pop(id(self))
43
+ except KeyError: # pragma: no cover
44
+ return []
45
+
46
+ def __del__(self) -> None:
47
+ with contextlib.suppress(KeyError):
48
+ all_pending_events.pop(id(self))
49
+
50
+
51
+ class Snapshot(DomainEvent):
52
+ topic: str
53
+ state: Dict[str, Any]
54
+
55
+ @classmethod
56
+ def take(cls, aggregate: Aggregate) -> Snapshot:
57
+ return Snapshot(
58
+ originator_id=aggregate.id,
59
+ originator_version=aggregate.version,
60
+ timestamp=create_timestamp(),
61
+ topic=get_topic(type(aggregate)),
62
+ state=aggregate.model_dump(),
63
+ )
64
+
65
+
66
+ TAggregate = TypeVar("TAggregate", bound=Aggregate)
67
+ MutatorFunction = Callable[..., Optional[TAggregate]]
68
+
69
+
70
+ def aggregate_projector(
71
+ mutator: MutatorFunction[TAggregate],
72
+ ) -> Callable[[TAggregate | None, Iterable[DomainEvent]], TAggregate | None]:
73
+ def project_aggregate(
74
+ aggregate: TAggregate | None, events: Iterable[DomainEvent]
75
+ ) -> TAggregate | None:
76
+ for event in events:
77
+ aggregate = mutator(event, aggregate)
78
+ return aggregate
79
+
80
+ return project_aggregate
81
+
82
+
83
+ class Trick(BaseModel):
84
+ name: str
85
+
86
+
87
+ all_pending_events: Dict[int, List[DomainEvent]] = defaultdict(list)
88
+
89
+
90
+ class Dog(Aggregate):
91
+ name: str
92
+ tricks: Tuple[Trick, ...]
93
+
94
+
95
+ class DogRegistered(DomainEvent):
96
+ name: str
97
+
98
+
99
+ class TrickAdded(DomainEvent):
100
+ trick: Trick
101
+
102
+
103
+ def register_dog(name: str) -> Dog:
104
+ event = DogRegistered(
105
+ originator_id=uuid4(),
106
+ originator_version=1,
107
+ timestamp=create_timestamp(),
108
+ name=name,
109
+ )
110
+ dog = mutate_dog(event, None)
111
+ assert isinstance(dog, Dog)
112
+ dog.hold_event(event)
113
+ return dog
114
+
115
+
116
+ def add_trick(dog: Dog, trick: str) -> Dog:
117
+ event = TrickAdded(
118
+ originator_id=dog.id,
119
+ originator_version=dog.version + 1,
120
+ timestamp=create_timestamp(),
121
+ trick=Trick(name=trick),
122
+ )
123
+ dog_ = mutate_dog(event, dog)
124
+ assert isinstance(dog_, Dog)
125
+ dog_.hold_event(event)
126
+ return dog_
127
+
128
+
129
+ @singledispatch
130
+ def mutate_dog(_: DomainEvent, __: Dog | None) -> Dog | None:
131
+ """Mutates aggregate with event."""
132
+
133
+
134
+ @mutate_dog.register
135
+ def _(event: DogRegistered, _: None) -> Dog:
136
+ return Dog(
137
+ id=event.originator_id,
138
+ version=event.originator_version,
139
+ created_on=event.timestamp,
140
+ modified_on=event.timestamp,
141
+ name=event.name,
142
+ tricks=(),
143
+ )
144
+
145
+
146
+ @mutate_dog.register
147
+ def _(event: TrickAdded, dog: Dog) -> Dog:
148
+ return Dog(
149
+ id=dog.id,
150
+ version=event.originator_version,
151
+ created_on=dog.created_on,
152
+ modified_on=event.timestamp,
153
+ name=dog.name,
154
+ tricks=(*dog.tricks, event.trick),
155
+ )
156
+
157
+
158
+ @mutate_dog.register
159
+ def _(event: Snapshot, _: None) -> Dog:
160
+ return Dog(
161
+ id=event.state["id"],
162
+ version=event.state["version"],
163
+ created_on=event.state["created_on"],
164
+ modified_on=event.state["modified_on"],
165
+ name=event.state["name"],
166
+ tricks=event.state["tricks"],
167
+ )
168
+
169
+
170
+ project_dog = aggregate_projector(mutate_dog)
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ from unittest import TestCase
4
+
5
+ from eventsourcing.examples.aggregate7a.application import DogSchool
6
+ from eventsourcing.examples.aggregate7a.domainmodel import project_dog
7
+
8
+
9
+ class TestDogSchool(TestCase):
10
+ def test_dog_school(self) -> None:
11
+ # Construct application object.
12
+ school = DogSchool()
13
+
14
+ # Evolve application state.
15
+ dog_id = school.register_dog("Fido")
16
+ school.add_trick(dog_id, "roll over")
17
+ school.add_trick(dog_id, "play dead")
18
+
19
+ # Query application state.
20
+ dog = school.get_dog(dog_id)
21
+ self.assertEqual(dog["name"], "Fido")
22
+ self.assertEqual(dog["tricks"], ("roll over", "play dead"))
23
+
24
+ # Select notifications.
25
+ notifications = school.notification_log.select(start=1, limit=10)
26
+ assert len(notifications) == 3
27
+
28
+ # Take snapshot.
29
+ assert school.snapshots is not None
30
+ assert len(list(school.snapshots.get(dog_id))) == 0
31
+ school.take_snapshot(dog_id, version=3, projector_func=project_dog)
32
+ assert len(list(school.snapshots.get(dog_id))) == 1
33
+ dog = school.get_dog(dog_id)
34
+ self.assertEqual(dog["name"], "Fido")
35
+ self.assertEqual(dog["tricks"], ("roll over", "play dead"))
36
+
37
+ # Continue with snapshotted aggregate.
38
+ school.add_trick(dog_id, "fetch ball")
39
+ dog = school.get_dog(dog_id)
40
+ self.assertEqual(dog["name"], "Fido")
41
+ self.assertEqual(dog["tricks"], ("roll over", "play dead", "fetch ball"))
42
+
43
+ # Auto-snapshotting at version 5.
44
+ assert len(list(school.snapshots.get(dog_id))) == 1
45
+ school.add_trick(dog_id, "jump hoop")
46
+ assert len(list(school.snapshots.get(dog_id))) == 2
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ from unittest import TestCase
4
+
5
+ from eventsourcing.cipher import AESCipher
6
+ from eventsourcing.examples.aggregate7a.application import DogSchool
7
+ from eventsourcing.examples.aggregate7a.domainmodel import project_dog
8
+
9
+
10
+ class TestDogSchool(TestCase):
11
+ def test_dog_school(self) -> None:
12
+ # Construct application object.
13
+ school = DogSchool(
14
+ env={
15
+ "COMPRESSOR_TOPIC": "eventsourcing.compressor:ZlibCompressor",
16
+ "CIPHER_TOPIC": "eventsourcing.cipher:AESCipher",
17
+ "CIPHER_KEY": AESCipher.create_key(num_bytes=32),
18
+ }
19
+ )
20
+
21
+ # Evolve application state.
22
+ dog_id = school.register_dog("Fido")
23
+ school.add_trick(dog_id, "roll over")
24
+ school.add_trick(dog_id, "play dead")
25
+
26
+ # Query application state.
27
+ dog = school.get_dog(dog_id)
28
+ assert dog["name"] == "Fido"
29
+ self.assertEqual(dog["tricks"], ("roll over", "play dead"))
30
+
31
+ # Select notifications.
32
+ notifications = school.notification_log.select(start=1, limit=10)
33
+ assert len(notifications) == 3
34
+
35
+ # Take snapshot.
36
+ school.take_snapshot(dog_id, version=3, projector_func=project_dog)
37
+ dog = school.get_dog(dog_id)
38
+ assert dog["name"] == "Fido"
39
+ self.assertEqual(dog["tricks"], ("roll over", "play dead"))
40
+
41
+ # Continue with snapshotted aggregate.
42
+ school.add_trick(dog_id, "fetch ball")
43
+ dog = school.get_dog(dog_id)
44
+ assert dog["name"] == "Fido"
45
+ self.assertEqual(dog["tricks"], ("roll over", "play dead", "fetch ball"))
File without changes
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, Dict
4
+
5
+ from eventsourcing.application import Application
6
+ from eventsourcing.examples.aggregate8.domainmodel import Dog, Trick
7
+ from eventsourcing.examples.aggregate8.persistence import (
8
+ OrjsonTranscoder,
9
+ PydanticMapper,
10
+ )
11
+
12
+ if TYPE_CHECKING: # pragma: nocover
13
+ from uuid import UUID
14
+
15
+ from eventsourcing.persistence import Mapper, Transcoder
16
+
17
+
18
+ class DogSchool(Application):
19
+ is_snapshotting_enabled = True
20
+
21
+ def register_dog(self, name: str) -> UUID:
22
+ dog = Dog(name)
23
+ self.save(dog)
24
+ return dog.id
25
+
26
+ def add_trick(self, dog_id: UUID, trick: str) -> None:
27
+ dog: Dog = self.repository.get(dog_id)
28
+ dog.add_trick(Trick(name=trick))
29
+ self.save(dog)
30
+
31
+ def get_dog(self, dog_id: UUID) -> Dict[str, Any]:
32
+ dog: Dog = self.repository.get(dog_id)
33
+ return {
34
+ "name": dog.name,
35
+ "tricks": tuple([t.name for t in dog.tricks]),
36
+ "created_on": dog.created_on,
37
+ "modified_on": dog.modified_on,
38
+ }
39
+
40
+ def construct_mapper(self) -> Mapper:
41
+ return self.factory.mapper(
42
+ transcoder=self.construct_transcoder(),
43
+ mapper_class=PydanticMapper,
44
+ )
45
+
46
+ def construct_transcoder(self) -> Transcoder:
47
+ return OrjsonTranscoder()
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime # noqa: TCH003
4
+ from typing import List
5
+ from uuid import UUID # noqa: TCH003
6
+
7
+ from pydantic import BaseModel, Extra
8
+
9
+ from eventsourcing.domain import (
10
+ Aggregate as BaseAggregate,
11
+ CanInitAggregate,
12
+ CanMutateAggregate,
13
+ CanSnapshotAggregate,
14
+ event,
15
+ )
16
+
17
+
18
+ class DomainEvent(BaseModel):
19
+ originator_id: UUID
20
+ originator_version: int
21
+ timestamp: datetime
22
+
23
+ class Config:
24
+ frozen = True
25
+
26
+
27
+ class Aggregate(BaseAggregate):
28
+ class Event(DomainEvent, CanMutateAggregate):
29
+ pass
30
+
31
+ class Created(Event, CanInitAggregate):
32
+ originator_topic: str
33
+
34
+
35
+ class SnapshotState(BaseModel):
36
+ class Config:
37
+ extra = Extra.allow
38
+
39
+
40
+ class AggregateSnapshot(DomainEvent, CanSnapshotAggregate):
41
+ topic: str
42
+ state: SnapshotState
43
+
44
+
45
+ class Trick(BaseModel):
46
+ name: str
47
+
48
+
49
+ class DogState(SnapshotState):
50
+ name: str
51
+ tricks: List[Trick]
52
+
53
+
54
+ class Dog(Aggregate):
55
+ class Snapshot(AggregateSnapshot):
56
+ state: DogState
57
+
58
+ @event("Registered")
59
+ def __init__(self, name: str) -> None:
60
+ self.name = name
61
+ self.tricks: List[Trick] = []
62
+
63
+ @event("TrickAdded")
64
+ def add_trick(self, trick: Trick) -> None:
65
+ self.tricks.append(trick)
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, Dict, cast
4
+
5
+ import orjson
6
+ from pydantic import BaseModel
7
+
8
+ from eventsourcing.persistence import (
9
+ Mapper,
10
+ ProgrammingError,
11
+ StoredEvent,
12
+ Transcoder,
13
+ Transcoding,
14
+ )
15
+ from eventsourcing.utils import get_topic, resolve_topic
16
+
17
+ if TYPE_CHECKING: # pragma: nocover
18
+ from eventsourcing.domain import DomainEventProtocol
19
+
20
+
21
+ class PydanticMapper(Mapper):
22
+ def to_stored_event(self, domain_event: DomainEventProtocol) -> StoredEvent:
23
+ topic = get_topic(domain_event.__class__)
24
+ event_state = cast(BaseModel, domain_event).model_dump()
25
+ stored_state = self.transcoder.encode(event_state)
26
+ if self.compressor:
27
+ stored_state = self.compressor.compress(stored_state)
28
+ if self.cipher:
29
+ stored_state = self.cipher.encrypt(stored_state)
30
+ return StoredEvent(
31
+ originator_id=domain_event.originator_id,
32
+ originator_version=domain_event.originator_version,
33
+ topic=topic,
34
+ state=stored_state,
35
+ )
36
+
37
+ def to_domain_event(self, stored: StoredEvent) -> DomainEventProtocol:
38
+ stored_state = stored.state
39
+ if self.cipher:
40
+ stored_state = self.cipher.decrypt(stored_state)
41
+ if self.compressor:
42
+ stored_state = self.compressor.decompress(stored_state)
43
+ event_state: Dict[str, Any] = self.transcoder.decode(stored_state)
44
+ cls = resolve_topic(stored.topic)
45
+ return cls(**event_state)
46
+
47
+
48
+ class OrjsonTranscoder(Transcoder):
49
+ def encode(self, obj: Any) -> bytes:
50
+ return orjson.dumps(obj)
51
+
52
+ def decode(self, data: bytes) -> Any:
53
+ return orjson.loads(data)
54
+
55
+ def register(self, _: Transcoding) -> None: # pragma: no cover
56
+ msg = "Please use Pydantic BaseModel"
57
+ raise ProgrammingError(msg)
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ from unittest import TestCase
4
+
5
+ from eventsourcing.examples.aggregate8.application import DogSchool
6
+
7
+
8
+ class TestDogSchool(TestCase):
9
+ def test_dog_school(self) -> None:
10
+ # Construct application object.
11
+ school = DogSchool()
12
+
13
+ # Evolve application state.
14
+ dog_id = school.register_dog("Fido")
15
+ school.add_trick(dog_id, "roll over")
16
+ school.add_trick(dog_id, "play dead")
17
+
18
+ # Query application state.
19
+ dog = school.get_dog(dog_id)
20
+ self.assertEqual(dog["name"], "Fido")
21
+ self.assertEqual(dog["tricks"], ("roll over", "play dead"))
22
+
23
+ # Select notifications.
24
+ notifications = school.notification_log.select(start=1, limit=10)
25
+ assert len(notifications) == 3
26
+
27
+ # Take snapshot.
28
+ school.take_snapshot(dog_id, version=3)
29
+ dog = school.get_dog(dog_id)
30
+ self.assertEqual(dog["name"], "Fido")
31
+ self.assertEqual(dog["tricks"], ("roll over", "play dead"))
32
+
33
+ # Continue with snapshotted aggregate.
34
+ school.add_trick(dog_id, "fetch ball")
35
+ dog = school.get_dog(dog_id)
36
+ self.assertEqual(dog["name"], "Fido")
37
+ self.assertEqual(dog["tricks"], ("roll over", "play dead", "fetch ball"))