eventsourcing 9.3.2__py3-none-any.whl → 9.3.4__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 (129) hide show
  1. eventsourcing/postgres.py +2 -1
  2. eventsourcing/system.py +3 -1
  3. {eventsourcing-9.3.2.dist-info → eventsourcing-9.3.4.dist-info}/METADATA +3 -8
  4. eventsourcing-9.3.4.dist-info/RECORD +24 -0
  5. eventsourcing/examples/__init__.py +0 -0
  6. eventsourcing/examples/aggregate1/__init__.py +0 -0
  7. eventsourcing/examples/aggregate1/application.py +0 -27
  8. eventsourcing/examples/aggregate1/domainmodel.py +0 -16
  9. eventsourcing/examples/aggregate1/test_application.py +0 -37
  10. eventsourcing/examples/aggregate2/__init__.py +0 -0
  11. eventsourcing/examples/aggregate2/application.py +0 -27
  12. eventsourcing/examples/aggregate2/domainmodel.py +0 -22
  13. eventsourcing/examples/aggregate2/test_application.py +0 -37
  14. eventsourcing/examples/aggregate3/__init__.py +0 -0
  15. eventsourcing/examples/aggregate3/application.py +0 -27
  16. eventsourcing/examples/aggregate3/domainmodel.py +0 -38
  17. eventsourcing/examples/aggregate3/test_application.py +0 -37
  18. eventsourcing/examples/aggregate4/__init__.py +0 -0
  19. eventsourcing/examples/aggregate4/application.py +0 -27
  20. eventsourcing/examples/aggregate4/domainmodel.py +0 -114
  21. eventsourcing/examples/aggregate4/test_application.py +0 -38
  22. eventsourcing/examples/aggregate5/__init__.py +0 -0
  23. eventsourcing/examples/aggregate5/application.py +0 -27
  24. eventsourcing/examples/aggregate5/domainmodel.py +0 -131
  25. eventsourcing/examples/aggregate5/test_application.py +0 -38
  26. eventsourcing/examples/aggregate6/__init__.py +0 -0
  27. eventsourcing/examples/aggregate6/application.py +0 -30
  28. eventsourcing/examples/aggregate6/domainmodel.py +0 -123
  29. eventsourcing/examples/aggregate6/test_application.py +0 -38
  30. eventsourcing/examples/aggregate6a/__init__.py +0 -0
  31. eventsourcing/examples/aggregate6a/application.py +0 -40
  32. eventsourcing/examples/aggregate6a/domainmodel.py +0 -149
  33. eventsourcing/examples/aggregate6a/test_application.py +0 -45
  34. eventsourcing/examples/aggregate7/__init__.py +0 -0
  35. eventsourcing/examples/aggregate7/application.py +0 -48
  36. eventsourcing/examples/aggregate7/domainmodel.py +0 -144
  37. eventsourcing/examples/aggregate7/persistence.py +0 -57
  38. eventsourcing/examples/aggregate7/test_application.py +0 -38
  39. eventsourcing/examples/aggregate7/test_compression_and_encryption.py +0 -45
  40. eventsourcing/examples/aggregate7/test_snapshotting_intervals.py +0 -67
  41. eventsourcing/examples/aggregate7a/__init__.py +0 -0
  42. eventsourcing/examples/aggregate7a/application.py +0 -56
  43. eventsourcing/examples/aggregate7a/domainmodel.py +0 -170
  44. eventsourcing/examples/aggregate7a/test_application.py +0 -46
  45. eventsourcing/examples/aggregate7a/test_compression_and_encryption.py +0 -45
  46. eventsourcing/examples/aggregate8/__init__.py +0 -0
  47. eventsourcing/examples/aggregate8/application.py +0 -47
  48. eventsourcing/examples/aggregate8/domainmodel.py +0 -65
  49. eventsourcing/examples/aggregate8/persistence.py +0 -57
  50. eventsourcing/examples/aggregate8/test_application.py +0 -37
  51. eventsourcing/examples/aggregate8/test_compression_and_encryption.py +0 -44
  52. eventsourcing/examples/aggregate8/test_snapshotting_intervals.py +0 -38
  53. eventsourcing/examples/bankaccounts/__init__.py +0 -0
  54. eventsourcing/examples/bankaccounts/application.py +0 -70
  55. eventsourcing/examples/bankaccounts/domainmodel.py +0 -56
  56. eventsourcing/examples/bankaccounts/test.py +0 -173
  57. eventsourcing/examples/cargoshipping/__init__.py +0 -0
  58. eventsourcing/examples/cargoshipping/application.py +0 -126
  59. eventsourcing/examples/cargoshipping/domainmodel.py +0 -330
  60. eventsourcing/examples/cargoshipping/interface.py +0 -143
  61. eventsourcing/examples/cargoshipping/test.py +0 -231
  62. eventsourcing/examples/contentmanagement/__init__.py +0 -0
  63. eventsourcing/examples/contentmanagement/application.py +0 -118
  64. eventsourcing/examples/contentmanagement/domainmodel.py +0 -69
  65. eventsourcing/examples/contentmanagement/test.py +0 -180
  66. eventsourcing/examples/contentmanagement/utils.py +0 -26
  67. eventsourcing/examples/contentmanagementsystem/__init__.py +0 -0
  68. eventsourcing/examples/contentmanagementsystem/application.py +0 -54
  69. eventsourcing/examples/contentmanagementsystem/postgres.py +0 -17
  70. eventsourcing/examples/contentmanagementsystem/sqlite.py +0 -17
  71. eventsourcing/examples/contentmanagementsystem/system.py +0 -14
  72. eventsourcing/examples/contentmanagementsystem/test_system.py +0 -180
  73. eventsourcing/examples/searchablecontent/__init__.py +0 -0
  74. eventsourcing/examples/searchablecontent/application.py +0 -45
  75. eventsourcing/examples/searchablecontent/persistence.py +0 -23
  76. eventsourcing/examples/searchablecontent/postgres.py +0 -118
  77. eventsourcing/examples/searchablecontent/sqlite.py +0 -136
  78. eventsourcing/examples/searchablecontent/test_application.py +0 -110
  79. eventsourcing/examples/searchablecontent/test_recorder.py +0 -68
  80. eventsourcing/examples/searchabletimestamps/__init__.py +0 -0
  81. eventsourcing/examples/searchabletimestamps/application.py +0 -32
  82. eventsourcing/examples/searchabletimestamps/persistence.py +0 -20
  83. eventsourcing/examples/searchabletimestamps/postgres.py +0 -110
  84. eventsourcing/examples/searchabletimestamps/sqlite.py +0 -99
  85. eventsourcing/examples/searchabletimestamps/test_searchabletimestamps.py +0 -94
  86. eventsourcing/examples/test_invoice.py +0 -176
  87. eventsourcing/examples/test_parking_lot.py +0 -206
  88. eventsourcing/tests/application_tests/__init__.py +0 -0
  89. eventsourcing/tests/application_tests/test_application_with_automatic_snapshotting.py +0 -55
  90. eventsourcing/tests/application_tests/test_application_with_popo.py +0 -22
  91. eventsourcing/tests/application_tests/test_application_with_postgres.py +0 -75
  92. eventsourcing/tests/application_tests/test_application_with_sqlite.py +0 -72
  93. eventsourcing/tests/application_tests/test_cache.py +0 -134
  94. eventsourcing/tests/application_tests/test_event_sourced_log.py +0 -162
  95. eventsourcing/tests/application_tests/test_notificationlog.py +0 -232
  96. eventsourcing/tests/application_tests/test_notificationlogreader.py +0 -126
  97. eventsourcing/tests/application_tests/test_processapplication.py +0 -110
  98. eventsourcing/tests/application_tests/test_processingpolicy.py +0 -109
  99. eventsourcing/tests/application_tests/test_repository.py +0 -504
  100. eventsourcing/tests/application_tests/test_snapshotting.py +0 -68
  101. eventsourcing/tests/application_tests/test_upcasting.py +0 -459
  102. eventsourcing/tests/docs_tests/__init__.py +0 -0
  103. eventsourcing/tests/docs_tests/test_docs.py +0 -293
  104. eventsourcing/tests/domain_tests/__init__.py +0 -0
  105. eventsourcing/tests/domain_tests/test_aggregate.py +0 -1200
  106. eventsourcing/tests/domain_tests/test_aggregate_decorators.py +0 -1604
  107. eventsourcing/tests/domain_tests/test_domainevent.py +0 -80
  108. eventsourcing/tests/interface_tests/__init__.py +0 -0
  109. eventsourcing/tests/interface_tests/test_remotenotificationlog.py +0 -258
  110. eventsourcing/tests/persistence_tests/__init__.py +0 -0
  111. eventsourcing/tests/persistence_tests/test_aes.py +0 -93
  112. eventsourcing/tests/persistence_tests/test_connection_pool.py +0 -722
  113. eventsourcing/tests/persistence_tests/test_eventstore.py +0 -72
  114. eventsourcing/tests/persistence_tests/test_infrastructure_factory.py +0 -21
  115. eventsourcing/tests/persistence_tests/test_mapper.py +0 -113
  116. eventsourcing/tests/persistence_tests/test_noninterleaving_notification_ids.py +0 -69
  117. eventsourcing/tests/persistence_tests/test_popo.py +0 -124
  118. eventsourcing/tests/persistence_tests/test_postgres.py +0 -1119
  119. eventsourcing/tests/persistence_tests/test_sqlite.py +0 -348
  120. eventsourcing/tests/persistence_tests/test_transcoder.py +0 -44
  121. eventsourcing/tests/system_tests/__init__.py +0 -0
  122. eventsourcing/tests/system_tests/test_runner.py +0 -935
  123. eventsourcing/tests/system_tests/test_system.py +0 -284
  124. eventsourcing/tests/utils_tests/__init__.py +0 -0
  125. eventsourcing/tests/utils_tests/test_utils.py +0 -226
  126. eventsourcing-9.3.2.dist-info/RECORD +0 -145
  127. {eventsourcing-9.3.2.dist-info → eventsourcing-9.3.4.dist-info}/AUTHORS +0 -0
  128. {eventsourcing-9.3.2.dist-info → eventsourcing-9.3.4.dist-info}/LICENSE +0 -0
  129. {eventsourcing-9.3.2.dist-info → eventsourcing-9.3.4.dist-info}/WHEEL +0 -0
@@ -1,144 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from datetime import datetime, timezone
4
- from functools import singledispatch
5
- from typing import Any, Callable, Dict, Iterable, Optional, Tuple, TypeVar
6
- from uuid import UUID, uuid4
7
-
8
- from pydantic import BaseModel
9
-
10
- from eventsourcing.utils import get_topic
11
-
12
-
13
- class DomainEvent(BaseModel):
14
- originator_id: UUID
15
- originator_version: int
16
- timestamp: datetime
17
-
18
- class Config:
19
- frozen = True
20
-
21
-
22
- def create_timestamp() -> datetime:
23
- return datetime.now(tz=timezone.utc)
24
-
25
-
26
- class Aggregate(BaseModel):
27
- id: UUID
28
- version: int
29
- created_on: datetime
30
- modified_on: datetime
31
-
32
- class Config:
33
- frozen = True
34
-
35
-
36
- class Snapshot(DomainEvent):
37
- topic: str
38
- state: Dict[str, Any]
39
-
40
- @classmethod
41
- def take(cls, aggregate: Aggregate) -> Snapshot:
42
- return Snapshot(
43
- originator_id=aggregate.id,
44
- originator_version=aggregate.version,
45
- timestamp=create_timestamp(),
46
- topic=get_topic(type(aggregate)),
47
- state=aggregate.model_dump(),
48
- )
49
-
50
-
51
- TAggregate = TypeVar("TAggregate", bound=Aggregate)
52
- MutatorFunction = Callable[..., Optional[TAggregate]]
53
-
54
-
55
- def aggregate_projector(
56
- mutator: MutatorFunction[TAggregate],
57
- ) -> Callable[[TAggregate | None, Iterable[DomainEvent]], TAggregate | None]:
58
- def project_aggregate(
59
- aggregate: TAggregate | None, events: Iterable[DomainEvent]
60
- ) -> TAggregate | None:
61
- for event in events:
62
- aggregate = mutator(event, aggregate)
63
- return aggregate
64
-
65
- return project_aggregate
66
-
67
-
68
- class Trick(BaseModel):
69
- name: str
70
-
71
-
72
- class Dog(Aggregate):
73
- name: str
74
- tricks: Tuple[Trick, ...]
75
-
76
-
77
- class DogRegistered(DomainEvent):
78
- name: str
79
-
80
-
81
- class TrickAdded(DomainEvent):
82
- trick: Trick
83
-
84
-
85
- def register_dog(name: str) -> DomainEvent:
86
- return DogRegistered(
87
- originator_id=uuid4(),
88
- originator_version=1,
89
- timestamp=create_timestamp(),
90
- name=name,
91
- )
92
-
93
-
94
- def add_trick(dog: Dog, trick: Trick) -> DomainEvent:
95
- return TrickAdded(
96
- originator_id=dog.id,
97
- originator_version=dog.version + 1,
98
- timestamp=create_timestamp(),
99
- trick=trick,
100
- )
101
-
102
-
103
- @singledispatch
104
- def mutate_dog(_: DomainEvent, __: Dog | None) -> Dog | None:
105
- """Mutates aggregate with event."""
106
-
107
-
108
- @mutate_dog.register
109
- def _(event: DogRegistered, _: None) -> Dog:
110
- return Dog(
111
- id=event.originator_id,
112
- version=event.originator_version,
113
- created_on=event.timestamp,
114
- modified_on=event.timestamp,
115
- name=event.name,
116
- tricks=(),
117
- )
118
-
119
-
120
- @mutate_dog.register
121
- def _(event: TrickAdded, dog: Dog) -> Dog:
122
- return Dog(
123
- id=dog.id,
124
- version=event.originator_version,
125
- created_on=dog.created_on,
126
- modified_on=event.timestamp,
127
- name=dog.name,
128
- tricks=(*dog.tricks, event.trick),
129
- )
130
-
131
-
132
- @mutate_dog.register
133
- def _(event: Snapshot, _: None) -> Dog:
134
- return Dog(
135
- id=event.state["id"],
136
- version=event.state["version"],
137
- created_on=event.state["created_on"],
138
- modified_on=event.state["modified_on"],
139
- name=event.state["name"],
140
- tricks=event.state["tricks"],
141
- )
142
-
143
-
144
- project_dog = aggregate_projector(mutate_dog)
@@ -1,57 +0,0 @@
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)
@@ -1,38 +0,0 @@
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"))
@@ -1,45 +0,0 @@
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"))
@@ -1,67 +0,0 @@
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
@@ -1,56 +0,0 @@
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()
@@ -1,170 +0,0 @@
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)
@@ -1,46 +0,0 @@
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
@@ -1,45 +0,0 @@
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