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.
- eventsourcing/postgres.py +2 -1
- eventsourcing/system.py +3 -1
- {eventsourcing-9.3.2.dist-info → eventsourcing-9.3.4.dist-info}/METADATA +3 -8
- eventsourcing-9.3.4.dist-info/RECORD +24 -0
- eventsourcing/examples/__init__.py +0 -0
- eventsourcing/examples/aggregate1/__init__.py +0 -0
- eventsourcing/examples/aggregate1/application.py +0 -27
- eventsourcing/examples/aggregate1/domainmodel.py +0 -16
- eventsourcing/examples/aggregate1/test_application.py +0 -37
- eventsourcing/examples/aggregate2/__init__.py +0 -0
- eventsourcing/examples/aggregate2/application.py +0 -27
- eventsourcing/examples/aggregate2/domainmodel.py +0 -22
- eventsourcing/examples/aggregate2/test_application.py +0 -37
- eventsourcing/examples/aggregate3/__init__.py +0 -0
- eventsourcing/examples/aggregate3/application.py +0 -27
- eventsourcing/examples/aggregate3/domainmodel.py +0 -38
- eventsourcing/examples/aggregate3/test_application.py +0 -37
- eventsourcing/examples/aggregate4/__init__.py +0 -0
- eventsourcing/examples/aggregate4/application.py +0 -27
- eventsourcing/examples/aggregate4/domainmodel.py +0 -114
- eventsourcing/examples/aggregate4/test_application.py +0 -38
- eventsourcing/examples/aggregate5/__init__.py +0 -0
- eventsourcing/examples/aggregate5/application.py +0 -27
- eventsourcing/examples/aggregate5/domainmodel.py +0 -131
- eventsourcing/examples/aggregate5/test_application.py +0 -38
- eventsourcing/examples/aggregate6/__init__.py +0 -0
- eventsourcing/examples/aggregate6/application.py +0 -30
- eventsourcing/examples/aggregate6/domainmodel.py +0 -123
- eventsourcing/examples/aggregate6/test_application.py +0 -38
- eventsourcing/examples/aggregate6a/__init__.py +0 -0
- eventsourcing/examples/aggregate6a/application.py +0 -40
- eventsourcing/examples/aggregate6a/domainmodel.py +0 -149
- eventsourcing/examples/aggregate6a/test_application.py +0 -45
- eventsourcing/examples/aggregate7/__init__.py +0 -0
- eventsourcing/examples/aggregate7/application.py +0 -48
- eventsourcing/examples/aggregate7/domainmodel.py +0 -144
- eventsourcing/examples/aggregate7/persistence.py +0 -57
- eventsourcing/examples/aggregate7/test_application.py +0 -38
- eventsourcing/examples/aggregate7/test_compression_and_encryption.py +0 -45
- eventsourcing/examples/aggregate7/test_snapshotting_intervals.py +0 -67
- eventsourcing/examples/aggregate7a/__init__.py +0 -0
- eventsourcing/examples/aggregate7a/application.py +0 -56
- eventsourcing/examples/aggregate7a/domainmodel.py +0 -170
- eventsourcing/examples/aggregate7a/test_application.py +0 -46
- eventsourcing/examples/aggregate7a/test_compression_and_encryption.py +0 -45
- eventsourcing/examples/aggregate8/__init__.py +0 -0
- eventsourcing/examples/aggregate8/application.py +0 -47
- eventsourcing/examples/aggregate8/domainmodel.py +0 -65
- eventsourcing/examples/aggregate8/persistence.py +0 -57
- eventsourcing/examples/aggregate8/test_application.py +0 -37
- eventsourcing/examples/aggregate8/test_compression_and_encryption.py +0 -44
- eventsourcing/examples/aggregate8/test_snapshotting_intervals.py +0 -38
- eventsourcing/examples/bankaccounts/__init__.py +0 -0
- eventsourcing/examples/bankaccounts/application.py +0 -70
- eventsourcing/examples/bankaccounts/domainmodel.py +0 -56
- eventsourcing/examples/bankaccounts/test.py +0 -173
- eventsourcing/examples/cargoshipping/__init__.py +0 -0
- eventsourcing/examples/cargoshipping/application.py +0 -126
- eventsourcing/examples/cargoshipping/domainmodel.py +0 -330
- eventsourcing/examples/cargoshipping/interface.py +0 -143
- eventsourcing/examples/cargoshipping/test.py +0 -231
- eventsourcing/examples/contentmanagement/__init__.py +0 -0
- eventsourcing/examples/contentmanagement/application.py +0 -118
- eventsourcing/examples/contentmanagement/domainmodel.py +0 -69
- eventsourcing/examples/contentmanagement/test.py +0 -180
- eventsourcing/examples/contentmanagement/utils.py +0 -26
- eventsourcing/examples/contentmanagementsystem/__init__.py +0 -0
- eventsourcing/examples/contentmanagementsystem/application.py +0 -54
- eventsourcing/examples/contentmanagementsystem/postgres.py +0 -17
- eventsourcing/examples/contentmanagementsystem/sqlite.py +0 -17
- eventsourcing/examples/contentmanagementsystem/system.py +0 -14
- eventsourcing/examples/contentmanagementsystem/test_system.py +0 -180
- eventsourcing/examples/searchablecontent/__init__.py +0 -0
- eventsourcing/examples/searchablecontent/application.py +0 -45
- eventsourcing/examples/searchablecontent/persistence.py +0 -23
- eventsourcing/examples/searchablecontent/postgres.py +0 -118
- eventsourcing/examples/searchablecontent/sqlite.py +0 -136
- eventsourcing/examples/searchablecontent/test_application.py +0 -110
- eventsourcing/examples/searchablecontent/test_recorder.py +0 -68
- eventsourcing/examples/searchabletimestamps/__init__.py +0 -0
- eventsourcing/examples/searchabletimestamps/application.py +0 -32
- eventsourcing/examples/searchabletimestamps/persistence.py +0 -20
- eventsourcing/examples/searchabletimestamps/postgres.py +0 -110
- eventsourcing/examples/searchabletimestamps/sqlite.py +0 -99
- eventsourcing/examples/searchabletimestamps/test_searchabletimestamps.py +0 -94
- eventsourcing/examples/test_invoice.py +0 -176
- eventsourcing/examples/test_parking_lot.py +0 -206
- eventsourcing/tests/application_tests/__init__.py +0 -0
- eventsourcing/tests/application_tests/test_application_with_automatic_snapshotting.py +0 -55
- eventsourcing/tests/application_tests/test_application_with_popo.py +0 -22
- eventsourcing/tests/application_tests/test_application_with_postgres.py +0 -75
- eventsourcing/tests/application_tests/test_application_with_sqlite.py +0 -72
- eventsourcing/tests/application_tests/test_cache.py +0 -134
- eventsourcing/tests/application_tests/test_event_sourced_log.py +0 -162
- eventsourcing/tests/application_tests/test_notificationlog.py +0 -232
- eventsourcing/tests/application_tests/test_notificationlogreader.py +0 -126
- eventsourcing/tests/application_tests/test_processapplication.py +0 -110
- eventsourcing/tests/application_tests/test_processingpolicy.py +0 -109
- eventsourcing/tests/application_tests/test_repository.py +0 -504
- eventsourcing/tests/application_tests/test_snapshotting.py +0 -68
- eventsourcing/tests/application_tests/test_upcasting.py +0 -459
- eventsourcing/tests/docs_tests/__init__.py +0 -0
- eventsourcing/tests/docs_tests/test_docs.py +0 -293
- eventsourcing/tests/domain_tests/__init__.py +0 -0
- eventsourcing/tests/domain_tests/test_aggregate.py +0 -1200
- eventsourcing/tests/domain_tests/test_aggregate_decorators.py +0 -1604
- eventsourcing/tests/domain_tests/test_domainevent.py +0 -80
- eventsourcing/tests/interface_tests/__init__.py +0 -0
- eventsourcing/tests/interface_tests/test_remotenotificationlog.py +0 -258
- eventsourcing/tests/persistence_tests/__init__.py +0 -0
- eventsourcing/tests/persistence_tests/test_aes.py +0 -93
- eventsourcing/tests/persistence_tests/test_connection_pool.py +0 -722
- eventsourcing/tests/persistence_tests/test_eventstore.py +0 -72
- eventsourcing/tests/persistence_tests/test_infrastructure_factory.py +0 -21
- eventsourcing/tests/persistence_tests/test_mapper.py +0 -113
- eventsourcing/tests/persistence_tests/test_noninterleaving_notification_ids.py +0 -69
- eventsourcing/tests/persistence_tests/test_popo.py +0 -124
- eventsourcing/tests/persistence_tests/test_postgres.py +0 -1119
- eventsourcing/tests/persistence_tests/test_sqlite.py +0 -348
- eventsourcing/tests/persistence_tests/test_transcoder.py +0 -44
- eventsourcing/tests/system_tests/__init__.py +0 -0
- eventsourcing/tests/system_tests/test_runner.py +0 -935
- eventsourcing/tests/system_tests/test_system.py +0 -284
- eventsourcing/tests/utils_tests/__init__.py +0 -0
- eventsourcing/tests/utils_tests/test_utils.py +0 -226
- eventsourcing-9.3.2.dist-info/RECORD +0 -145
- {eventsourcing-9.3.2.dist-info → eventsourcing-9.3.4.dist-info}/AUTHORS +0 -0
- {eventsourcing-9.3.2.dist-info → eventsourcing-9.3.4.dist-info}/LICENSE +0 -0
- {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
|