eventsourcing 9.2.21__py3-none-any.whl → 9.3.0__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.
- eventsourcing/__init__.py +1 -1
- eventsourcing/application.py +137 -132
- eventsourcing/cipher.py +17 -12
- eventsourcing/compressor.py +2 -0
- eventsourcing/dispatch.py +30 -56
- eventsourcing/domain.py +221 -227
- eventsourcing/examples/__init__.py +0 -0
- eventsourcing/examples/aggregate1/__init__.py +0 -0
- eventsourcing/examples/aggregate1/application.py +27 -0
- eventsourcing/examples/aggregate1/domainmodel.py +16 -0
- eventsourcing/examples/aggregate1/test_application.py +37 -0
- eventsourcing/examples/aggregate2/__init__.py +0 -0
- eventsourcing/examples/aggregate2/application.py +27 -0
- eventsourcing/examples/aggregate2/domainmodel.py +22 -0
- eventsourcing/examples/aggregate2/test_application.py +37 -0
- eventsourcing/examples/aggregate3/__init__.py +0 -0
- eventsourcing/examples/aggregate3/application.py +27 -0
- eventsourcing/examples/aggregate3/domainmodel.py +38 -0
- eventsourcing/examples/aggregate3/test_application.py +37 -0
- eventsourcing/examples/aggregate4/__init__.py +0 -0
- eventsourcing/examples/aggregate4/application.py +27 -0
- eventsourcing/examples/aggregate4/domainmodel.py +114 -0
- eventsourcing/examples/aggregate4/test_application.py +38 -0
- eventsourcing/examples/aggregate5/__init__.py +0 -0
- eventsourcing/examples/aggregate5/application.py +27 -0
- eventsourcing/examples/aggregate5/domainmodel.py +131 -0
- eventsourcing/examples/aggregate5/test_application.py +38 -0
- eventsourcing/examples/aggregate6/__init__.py +0 -0
- eventsourcing/examples/aggregate6/application.py +30 -0
- eventsourcing/examples/aggregate6/domainmodel.py +123 -0
- eventsourcing/examples/aggregate6/test_application.py +38 -0
- eventsourcing/examples/aggregate6a/__init__.py +0 -0
- eventsourcing/examples/aggregate6a/application.py +40 -0
- eventsourcing/examples/aggregate6a/domainmodel.py +149 -0
- eventsourcing/examples/aggregate6a/test_application.py +45 -0
- eventsourcing/examples/aggregate7/__init__.py +0 -0
- eventsourcing/examples/aggregate7/application.py +48 -0
- eventsourcing/examples/aggregate7/domainmodel.py +144 -0
- eventsourcing/examples/aggregate7/persistence.py +57 -0
- eventsourcing/examples/aggregate7/test_application.py +38 -0
- eventsourcing/examples/aggregate7/test_compression_and_encryption.py +45 -0
- eventsourcing/examples/aggregate7/test_snapshotting_intervals.py +67 -0
- eventsourcing/examples/aggregate7a/__init__.py +0 -0
- eventsourcing/examples/aggregate7a/application.py +56 -0
- eventsourcing/examples/aggregate7a/domainmodel.py +170 -0
- eventsourcing/examples/aggregate7a/test_application.py +46 -0
- eventsourcing/examples/aggregate7a/test_compression_and_encryption.py +45 -0
- eventsourcing/examples/aggregate8/__init__.py +0 -0
- eventsourcing/examples/aggregate8/application.py +47 -0
- eventsourcing/examples/aggregate8/domainmodel.py +65 -0
- eventsourcing/examples/aggregate8/persistence.py +57 -0
- eventsourcing/examples/aggregate8/test_application.py +37 -0
- eventsourcing/examples/aggregate8/test_compression_and_encryption.py +44 -0
- eventsourcing/examples/aggregate8/test_snapshotting_intervals.py +38 -0
- eventsourcing/examples/bankaccounts/__init__.py +0 -0
- eventsourcing/examples/bankaccounts/application.py +70 -0
- eventsourcing/examples/bankaccounts/domainmodel.py +56 -0
- eventsourcing/examples/bankaccounts/test.py +173 -0
- eventsourcing/examples/cargoshipping/__init__.py +0 -0
- eventsourcing/examples/cargoshipping/application.py +126 -0
- eventsourcing/examples/cargoshipping/domainmodel.py +330 -0
- eventsourcing/examples/cargoshipping/interface.py +143 -0
- eventsourcing/examples/cargoshipping/test.py +231 -0
- eventsourcing/examples/contentmanagement/__init__.py +0 -0
- eventsourcing/examples/contentmanagement/application.py +118 -0
- eventsourcing/examples/contentmanagement/domainmodel.py +69 -0
- eventsourcing/examples/contentmanagement/test.py +180 -0
- eventsourcing/examples/contentmanagement/utils.py +26 -0
- eventsourcing/examples/contentmanagementsystem/__init__.py +0 -0
- eventsourcing/examples/contentmanagementsystem/application.py +54 -0
- eventsourcing/examples/contentmanagementsystem/postgres.py +17 -0
- eventsourcing/examples/contentmanagementsystem/sqlite.py +17 -0
- eventsourcing/examples/contentmanagementsystem/system.py +14 -0
- eventsourcing/examples/contentmanagementsystem/test_system.py +180 -0
- eventsourcing/examples/searchablecontent/__init__.py +0 -0
- eventsourcing/examples/searchablecontent/application.py +45 -0
- eventsourcing/examples/searchablecontent/persistence.py +23 -0
- eventsourcing/examples/searchablecontent/postgres.py +118 -0
- eventsourcing/examples/searchablecontent/sqlite.py +136 -0
- eventsourcing/examples/searchablecontent/test_application.py +110 -0
- eventsourcing/examples/searchablecontent/test_recorder.py +68 -0
- eventsourcing/examples/searchabletimestamps/__init__.py +0 -0
- eventsourcing/examples/searchabletimestamps/application.py +32 -0
- eventsourcing/examples/searchabletimestamps/persistence.py +20 -0
- eventsourcing/examples/searchabletimestamps/postgres.py +110 -0
- eventsourcing/examples/searchabletimestamps/sqlite.py +99 -0
- eventsourcing/examples/searchabletimestamps/test_searchabletimestamps.py +94 -0
- eventsourcing/examples/test_invoice.py +176 -0
- eventsourcing/examples/test_parking_lot.py +206 -0
- eventsourcing/interface.py +4 -2
- eventsourcing/persistence.py +88 -82
- eventsourcing/popo.py +32 -31
- eventsourcing/postgres.py +388 -593
- eventsourcing/sqlite.py +100 -102
- eventsourcing/system.py +66 -71
- eventsourcing/tests/application.py +20 -32
- eventsourcing/tests/application_tests/__init__.py +0 -0
- eventsourcing/tests/application_tests/test_application_with_automatic_snapshotting.py +55 -0
- eventsourcing/tests/application_tests/test_application_with_popo.py +22 -0
- eventsourcing/tests/application_tests/test_application_with_postgres.py +75 -0
- eventsourcing/tests/application_tests/test_application_with_sqlite.py +72 -0
- eventsourcing/tests/application_tests/test_cache.py +134 -0
- eventsourcing/tests/application_tests/test_event_sourced_log.py +162 -0
- eventsourcing/tests/application_tests/test_notificationlog.py +232 -0
- eventsourcing/tests/application_tests/test_notificationlogreader.py +126 -0
- eventsourcing/tests/application_tests/test_processapplication.py +110 -0
- eventsourcing/tests/application_tests/test_processingpolicy.py +109 -0
- eventsourcing/tests/application_tests/test_repository.py +504 -0
- eventsourcing/tests/application_tests/test_snapshotting.py +68 -0
- eventsourcing/tests/application_tests/test_upcasting.py +459 -0
- eventsourcing/tests/docs_tests/__init__.py +0 -0
- eventsourcing/tests/docs_tests/test_docs.py +293 -0
- eventsourcing/tests/domain.py +1 -1
- eventsourcing/tests/domain_tests/__init__.py +0 -0
- eventsourcing/tests/domain_tests/test_aggregate.py +1180 -0
- eventsourcing/tests/domain_tests/test_aggregate_decorators.py +1604 -0
- eventsourcing/tests/domain_tests/test_domainevent.py +80 -0
- eventsourcing/tests/interface_tests/__init__.py +0 -0
- eventsourcing/tests/interface_tests/test_remotenotificationlog.py +258 -0
- eventsourcing/tests/persistence.py +52 -50
- eventsourcing/tests/persistence_tests/__init__.py +0 -0
- eventsourcing/tests/persistence_tests/test_aes.py +93 -0
- eventsourcing/tests/persistence_tests/test_connection_pool.py +722 -0
- eventsourcing/tests/persistence_tests/test_eventstore.py +72 -0
- eventsourcing/tests/persistence_tests/test_infrastructure_factory.py +21 -0
- eventsourcing/tests/persistence_tests/test_mapper.py +113 -0
- eventsourcing/tests/persistence_tests/test_noninterleaving_notification_ids.py +69 -0
- eventsourcing/tests/persistence_tests/test_popo.py +124 -0
- eventsourcing/tests/persistence_tests/test_postgres.py +1119 -0
- eventsourcing/tests/persistence_tests/test_sqlite.py +348 -0
- eventsourcing/tests/persistence_tests/test_transcoder.py +44 -0
- eventsourcing/tests/postgres_utils.py +7 -7
- eventsourcing/tests/system_tests/__init__.py +0 -0
- eventsourcing/tests/system_tests/test_runner.py +935 -0
- eventsourcing/tests/system_tests/test_system.py +284 -0
- eventsourcing/tests/utils_tests/__init__.py +0 -0
- eventsourcing/tests/utils_tests/test_utils.py +226 -0
- eventsourcing/utils.py +49 -50
- {eventsourcing-9.2.21.dist-info → eventsourcing-9.3.0.dist-info}/METADATA +30 -33
- eventsourcing-9.3.0.dist-info/RECORD +145 -0
- {eventsourcing-9.2.21.dist-info → eventsourcing-9.3.0.dist-info}/WHEEL +1 -2
- eventsourcing-9.2.21.dist-info/RECORD +0 -25
- eventsourcing-9.2.21.dist-info/top_level.txt +0 -1
- {eventsourcing-9.2.21.dist-info → eventsourcing-9.3.0.dist-info}/AUTHORS +0 -0
- {eventsourcing-9.2.21.dist-info → eventsourcing-9.3.0.dist-info}/LICENSE +0 -0
|
@@ -0,0 +1,30 @@
|
|
|
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.aggregate6.domainmodel import (
|
|
7
|
+
add_trick,
|
|
8
|
+
project_dog,
|
|
9
|
+
register_dog,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING: # pragma: nocover
|
|
13
|
+
from uuid import UUID
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DogSchool(Application):
|
|
17
|
+
is_snapshotting_enabled = True
|
|
18
|
+
|
|
19
|
+
def register_dog(self, name: str) -> UUID:
|
|
20
|
+
event = register_dog(name)
|
|
21
|
+
self.save(event)
|
|
22
|
+
return event.originator_id
|
|
23
|
+
|
|
24
|
+
def add_trick(self, dog_id: UUID, trick: str) -> None:
|
|
25
|
+
dog = self.repository.get(dog_id, projector_func=project_dog)
|
|
26
|
+
self.save(add_trick(dog, trick))
|
|
27
|
+
|
|
28
|
+
def get_dog(self, dog_id: UUID) -> Dict[str, Any]:
|
|
29
|
+
dog = self.repository.get(dog_id, projector_func=project_dog)
|
|
30
|
+
return {"name": dog.name, "tricks": dog.tricks}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from functools import singledispatch
|
|
6
|
+
from typing import Callable, Iterable, Optional, Tuple, TypeVar
|
|
7
|
+
from uuid import UUID, uuid4
|
|
8
|
+
|
|
9
|
+
from eventsourcing.domain import Snapshot
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class DomainEvent:
|
|
14
|
+
originator_id: UUID
|
|
15
|
+
originator_version: int
|
|
16
|
+
timestamp: datetime
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def create_timestamp() -> datetime:
|
|
20
|
+
return datetime.now(tz=timezone.utc)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class Aggregate:
|
|
25
|
+
id: UUID
|
|
26
|
+
version: int
|
|
27
|
+
created_on: datetime
|
|
28
|
+
modified_on: datetime
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
TAggregate = TypeVar("TAggregate", bound=Aggregate)
|
|
32
|
+
MutatorFunction = Callable[..., Optional[TAggregate]]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def aggregate_projector(
|
|
36
|
+
mutator: MutatorFunction[TAggregate],
|
|
37
|
+
) -> Callable[[TAggregate | None, Iterable[DomainEvent]], TAggregate | None]:
|
|
38
|
+
def project_aggregate(
|
|
39
|
+
aggregate: TAggregate | None, events: Iterable[DomainEvent]
|
|
40
|
+
) -> TAggregate | None:
|
|
41
|
+
for event in events:
|
|
42
|
+
aggregate = mutator(event, aggregate)
|
|
43
|
+
return aggregate
|
|
44
|
+
|
|
45
|
+
return project_aggregate
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class Dog(Aggregate):
|
|
50
|
+
name: str
|
|
51
|
+
tricks: Tuple[str, ...]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class DogRegistered(DomainEvent):
|
|
56
|
+
name: str
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class TrickAdded(DomainEvent):
|
|
61
|
+
trick: str
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def register_dog(name: str) -> DomainEvent:
|
|
65
|
+
return DogRegistered(
|
|
66
|
+
originator_id=uuid4(),
|
|
67
|
+
originator_version=1,
|
|
68
|
+
timestamp=create_timestamp(),
|
|
69
|
+
name=name,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def add_trick(dog: Dog, trick: str) -> DomainEvent:
|
|
74
|
+
return TrickAdded(
|
|
75
|
+
originator_id=dog.id,
|
|
76
|
+
originator_version=dog.version + 1,
|
|
77
|
+
timestamp=create_timestamp(),
|
|
78
|
+
trick=trick,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@singledispatch
|
|
83
|
+
def mutate_dog(_: DomainEvent | Snapshot, __: Dog | None) -> Dog | None:
|
|
84
|
+
"""Mutates aggregate with event."""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@mutate_dog.register
|
|
88
|
+
def _(event: DogRegistered, _: None) -> Dog:
|
|
89
|
+
return Dog(
|
|
90
|
+
id=event.originator_id,
|
|
91
|
+
version=event.originator_version,
|
|
92
|
+
created_on=event.timestamp,
|
|
93
|
+
modified_on=event.timestamp,
|
|
94
|
+
name=event.name,
|
|
95
|
+
tricks=(),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@mutate_dog.register
|
|
100
|
+
def _(event: TrickAdded, dog: Dog) -> Dog:
|
|
101
|
+
return Dog(
|
|
102
|
+
id=dog.id,
|
|
103
|
+
version=event.originator_version,
|
|
104
|
+
created_on=dog.created_on,
|
|
105
|
+
modified_on=event.timestamp,
|
|
106
|
+
name=dog.name,
|
|
107
|
+
tricks=(*dog.tricks, event.trick),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@mutate_dog.register
|
|
112
|
+
def _(event: Snapshot, _: None) -> Dog:
|
|
113
|
+
return Dog(
|
|
114
|
+
id=event.state["id"],
|
|
115
|
+
version=event.state["version"],
|
|
116
|
+
created_on=event.state["created_on"],
|
|
117
|
+
modified_on=event.state["modified_on"],
|
|
118
|
+
name=event.state["name"],
|
|
119
|
+
tricks=tuple(event.state["tricks"]), # comes back from JSON as a list
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
project_dog = aggregate_projector(mutate_dog)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from unittest import TestCase
|
|
4
|
+
|
|
5
|
+
from eventsourcing.examples.aggregate6.application import DogSchool
|
|
6
|
+
from eventsourcing.examples.aggregate6.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
|
+
assert dog["name"] == "Fido"
|
|
22
|
+
assert 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
|
+
assert dog["name"] == "Fido"
|
|
32
|
+
assert 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
|
+
assert dog["name"] == "Fido"
|
|
38
|
+
assert dog["tricks"] == ("roll over", "play dead", "fetch ball")
|
|
File without changes
|
|
@@ -0,0 +1,40 @@
|
|
|
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.aggregate6a.domainmodel import (
|
|
7
|
+
Dog,
|
|
8
|
+
add_trick,
|
|
9
|
+
project_dog,
|
|
10
|
+
register_dog,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING: # pragma: nocover
|
|
14
|
+
from uuid import UUID
|
|
15
|
+
|
|
16
|
+
from eventsourcing.domain import MutableOrImmutableAggregate
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DogSchool(Application):
|
|
20
|
+
is_snapshotting_enabled = True
|
|
21
|
+
snapshotting_intervals: ClassVar[
|
|
22
|
+
Dict[Type[MutableOrImmutableAggregate], int] | None
|
|
23
|
+
] = {Dog: 5}
|
|
24
|
+
snapshotting_projectors: ClassVar[
|
|
25
|
+
Dict[Type[MutableOrImmutableAggregate], ProjectorFunction[Any, Any]] | None
|
|
26
|
+
] = {Dog: project_dog}
|
|
27
|
+
|
|
28
|
+
def register_dog(self, name: str) -> UUID:
|
|
29
|
+
dog = register_dog(name)
|
|
30
|
+
self.save(dog)
|
|
31
|
+
return dog.id
|
|
32
|
+
|
|
33
|
+
def add_trick(self, dog_id: UUID, trick: str) -> None:
|
|
34
|
+
dog = self.repository.get(dog_id, projector_func=project_dog)
|
|
35
|
+
dog = add_trick(dog, trick)
|
|
36
|
+
self.save(dog)
|
|
37
|
+
|
|
38
|
+
def get_dog(self, dog_id: UUID) -> Dict[str, Any]:
|
|
39
|
+
dog = self.repository.get(dog_id, projector_func=project_dog)
|
|
40
|
+
return {"name": dog.name, "tricks": dog.tricks}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from functools import singledispatch
|
|
8
|
+
from typing import Callable, Dict, Iterable, List, Optional, Tuple, TypeVar
|
|
9
|
+
from uuid import UUID, uuid4
|
|
10
|
+
|
|
11
|
+
from eventsourcing.domain import Snapshot
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class DomainEvent:
|
|
16
|
+
originator_id: UUID
|
|
17
|
+
originator_version: int
|
|
18
|
+
timestamp: datetime
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def create_timestamp() -> datetime:
|
|
22
|
+
return datetime.now(tz=timezone.utc)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class Aggregate:
|
|
27
|
+
id: UUID
|
|
28
|
+
version: int
|
|
29
|
+
created_on: datetime
|
|
30
|
+
modified_on: datetime
|
|
31
|
+
|
|
32
|
+
def hold_event(self, event: DomainEvent) -> None:
|
|
33
|
+
all_pending_events[id(self)].append(event)
|
|
34
|
+
|
|
35
|
+
def collect_events(self) -> List[DomainEvent]:
|
|
36
|
+
try:
|
|
37
|
+
return all_pending_events.pop(id(self))
|
|
38
|
+
except KeyError: # pragma: no cover
|
|
39
|
+
return []
|
|
40
|
+
|
|
41
|
+
def __del__(self) -> None:
|
|
42
|
+
with contextlib.suppress(KeyError):
|
|
43
|
+
all_pending_events.pop(id(self))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
TAggregate = TypeVar("TAggregate", bound=Aggregate)
|
|
47
|
+
MutatorFunction = Callable[..., Optional[TAggregate]]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def aggregate_projector(
|
|
51
|
+
mutator: MutatorFunction[TAggregate],
|
|
52
|
+
) -> Callable[[TAggregate | None, Iterable[DomainEvent]], TAggregate | None]:
|
|
53
|
+
def project_aggregate(
|
|
54
|
+
aggregate: TAggregate | None, events: Iterable[DomainEvent]
|
|
55
|
+
) -> TAggregate | None:
|
|
56
|
+
for event in events:
|
|
57
|
+
aggregate = mutator(event, aggregate)
|
|
58
|
+
return aggregate
|
|
59
|
+
|
|
60
|
+
return project_aggregate
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
all_pending_events: Dict[int, List[DomainEvent]] = defaultdict(list)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class Dog(Aggregate):
|
|
68
|
+
name: str
|
|
69
|
+
tricks: Tuple[str, ...]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True)
|
|
73
|
+
class DogRegistered(DomainEvent):
|
|
74
|
+
name: str
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True)
|
|
78
|
+
class TrickAdded(DomainEvent):
|
|
79
|
+
trick: str
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def register_dog(name: str) -> Dog:
|
|
83
|
+
event = DogRegistered(
|
|
84
|
+
originator_id=uuid4(),
|
|
85
|
+
originator_version=1,
|
|
86
|
+
timestamp=create_timestamp(),
|
|
87
|
+
name=name,
|
|
88
|
+
)
|
|
89
|
+
dog = mutate_dog(event, None)
|
|
90
|
+
assert isinstance(dog, Dog)
|
|
91
|
+
dog.hold_event(event)
|
|
92
|
+
return dog
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def add_trick(dog: Dog, trick: str) -> Dog:
|
|
96
|
+
event = TrickAdded(
|
|
97
|
+
originator_id=dog.id,
|
|
98
|
+
originator_version=dog.version + 1,
|
|
99
|
+
timestamp=create_timestamp(),
|
|
100
|
+
trick=trick,
|
|
101
|
+
)
|
|
102
|
+
dog_ = mutate_dog(event, dog)
|
|
103
|
+
assert isinstance(dog_, Dog)
|
|
104
|
+
dog_.hold_event(event)
|
|
105
|
+
return dog_
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@singledispatch
|
|
109
|
+
def mutate_dog(_: DomainEvent | Snapshot, __: Dog | None) -> Dog | None:
|
|
110
|
+
"""Mutates aggregate with event."""
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@mutate_dog.register
|
|
114
|
+
def _(event: DogRegistered, _: None) -> Dog:
|
|
115
|
+
return Dog(
|
|
116
|
+
id=event.originator_id,
|
|
117
|
+
version=event.originator_version,
|
|
118
|
+
created_on=event.timestamp,
|
|
119
|
+
modified_on=event.timestamp,
|
|
120
|
+
name=event.name,
|
|
121
|
+
tricks=(),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@mutate_dog.register
|
|
126
|
+
def _(event: TrickAdded, dog: Dog) -> Dog:
|
|
127
|
+
return Dog(
|
|
128
|
+
id=dog.id,
|
|
129
|
+
version=event.originator_version,
|
|
130
|
+
created_on=dog.created_on,
|
|
131
|
+
modified_on=event.timestamp,
|
|
132
|
+
name=dog.name,
|
|
133
|
+
tricks=(*dog.tricks, event.trick),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@mutate_dog.register
|
|
138
|
+
def _(event: Snapshot, _: None) -> Dog:
|
|
139
|
+
return Dog(
|
|
140
|
+
id=event.state["id"],
|
|
141
|
+
version=event.state["version"],
|
|
142
|
+
created_on=event.state["created_on"],
|
|
143
|
+
modified_on=event.state["modified_on"],
|
|
144
|
+
name=event.state["name"],
|
|
145
|
+
tricks=tuple(event.state["tricks"]), # comes back from JSON as a list
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
project_dog = aggregate_projector(mutate_dog)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from unittest import TestCase
|
|
4
|
+
|
|
5
|
+
from eventsourcing.examples.aggregate6a.application import DogSchool
|
|
6
|
+
from eventsourcing.examples.aggregate6a.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
|
+
assert dog["name"] == "Fido"
|
|
22
|
+
assert 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
|
+
assert dog["name"] == "Fido"
|
|
35
|
+
assert 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
|
+
assert dog["name"] == "Fido"
|
|
41
|
+
assert dog["tricks"] == ("roll over", "play dead", "fetch ball")
|
|
42
|
+
|
|
43
|
+
assert len(list(school.snapshots.get(dog_id))) == 1
|
|
44
|
+
school.add_trick(dog_id, "jump hoop") # Version 5, autosnapshot.
|
|
45
|
+
assert len(list(school.snapshots.get(dog_id))) == 2
|
|
File without changes
|
|
@@ -0,0 +1,48 @@
|
|
|
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.aggregate7.domainmodel import (
|
|
7
|
+
Snapshot,
|
|
8
|
+
Trick,
|
|
9
|
+
add_trick,
|
|
10
|
+
project_dog,
|
|
11
|
+
register_dog,
|
|
12
|
+
)
|
|
13
|
+
from eventsourcing.examples.aggregate7.persistence import (
|
|
14
|
+
OrjsonTranscoder,
|
|
15
|
+
PydanticMapper,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING: # pragma: nocover
|
|
19
|
+
from uuid import UUID
|
|
20
|
+
|
|
21
|
+
from eventsourcing.persistence import Mapper, Transcoder
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DogSchool(Application):
|
|
25
|
+
is_snapshotting_enabled = True
|
|
26
|
+
snapshot_class = Snapshot
|
|
27
|
+
|
|
28
|
+
def register_dog(self, name: str) -> UUID:
|
|
29
|
+
event = register_dog(name)
|
|
30
|
+
self.save(event)
|
|
31
|
+
return event.originator_id
|
|
32
|
+
|
|
33
|
+
def add_trick(self, dog_id: UUID, trick: str) -> None:
|
|
34
|
+
dog = self.repository.get(dog_id, projector_func=project_dog)
|
|
35
|
+
self.save(add_trick(dog, Trick(name=trick)))
|
|
36
|
+
|
|
37
|
+
def get_dog(self, dog_id: UUID) -> Dict[str, Any]:
|
|
38
|
+
dog = self.repository.get(dog_id, projector_func=project_dog)
|
|
39
|
+
return {"name": dog.name, "tricks": tuple([t.name for t in dog.tricks])}
|
|
40
|
+
|
|
41
|
+
def construct_mapper(self) -> Mapper:
|
|
42
|
+
return self.factory.mapper(
|
|
43
|
+
transcoder=self.construct_transcoder(),
|
|
44
|
+
mapper_class=PydanticMapper,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def construct_transcoder(self) -> Transcoder:
|
|
48
|
+
return OrjsonTranscoder()
|
|
@@ -0,0 +1,144 @@
|
|
|
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)
|
|
@@ -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)
|