Skip to content

Commit 639852a

Browse files
committed
Improved aggregate examples 6, 7 and 8.
1 parent ab733b1 commit 639852a

File tree

7 files changed

+92
-55
lines changed

7 files changed

+92
-55
lines changed

docs/topics/examples/aggregate6.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ The :class:`~examples.aggregate6.baseclasses.Aggregate` base class in this examp
2020
.. literalinclude:: ../../../examples/aggregate6/baseclasses.py
2121
:pyobject: Aggregate
2222

23+
The :class:`~examples.aggregate6.baseclasses.Snapshot` class in this example is also defined as a "frozen" Python
24+
:class:`dataclass` that extends :class:`~examples.aggregate6.baseclasses.DomainEvent`.
25+
26+
.. literalinclude:: ../../../examples/aggregate6/baseclasses.py
27+
:pyobject: Snapshot
28+
2329
A generic :class:`~examples.aggregate6.baseclasses.aggregate_projector` function is also defined, which takes
2430
a mutator function and returns a function that can reconstruct an aggregate of a particular type from an iterable
2531
of domain events.

docs/topics/examples/aggregate7.rst

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,58 +3,74 @@
33
Aggregate 7 - Pydantic and orjson
44
=================================
55

6-
This example shows another variation of the ``Dog`` aggregate class used
7-
in the tutorial and module docs.
8-
9-
Similar to the previous example, the model is expressed in a functional
10-
style. In contrast to the previous example, this example uses Pydantic
11-
to define immutable aggregate and event classes, rather than defining
12-
them as Python frozen data classes. This has implications for the
13-
persistence layer.
14-
15-
The application class in this example uses its own persistence classes
16-
``PydanticMapper`` and ``OrjsonTranscoder``. Pydantic is responsible
17-
for converting domain model objects to object types that orjson can
18-
serialise, and for reconstructing model objects from JSON objects
19-
that have been deserialised by orjson.
20-
21-
One advantage of using Pydantic here is that any custom value objects
22-
will be automatically reconstructed without needing to define the
23-
transcoding classes that would be needed when using the library's
24-
default ``JSONTranscoder``. This is demonstrated in the example below
25-
with the ``Trick`` class, which is used in both aggregate events and
26-
aggregate state, and which is reconstructed from serialised string
27-
values, representing only the name of the trick, from both recorded
28-
aggregate events and from recorded snapshots.
6+
This example shows how to use Pydantic to define immutable aggregate and event classes.
7+
8+
The main advantage of using Pydantic here is that any custom value objects
9+
used in the domain model will be automatically serialised and deserialised,
10+
without needing also to define custom :ref:`transcoding<Transcodings>` classes.
11+
12+
This is demonstrated in the example below with the :class:`~examples.aggregate7.domainmodel.Trick` class,
13+
which is used in both aggregate events and aggregate state, and which is reconstructed from serialised string
14+
values, representing only the name of the trick, from both recorded aggregate events and from recorded snapshots.
2915

3016
Pydantic mapper and orjson transcoder
3117
-------------------------------------
3218

19+
The application class in this example uses a :ref:`mapper<Mapper>` that supports Pydantic and a :ref:`transcoder<Transcoder>` that uses orjson.
20+
21+
The :class:`~examples.aggregate7.orjsonpydantic.PydanticMapper` class is a
22+
:ref:`mapper<Mapper>` that supports Pydantic. It is responsible for converting
23+
domain model objects to object types that orjson can serialise, and for
24+
reconstructing model objects from JSON objects that have been deserialised by orjson.
25+
3326
.. literalinclude:: ../../../examples/aggregate7/orjsonpydantic.py
27+
:pyobject: PydanticMapper
28+
29+
The :class:`~examples.aggregate7.orjsonpydantic.OrjsonTranscoder` class is a
30+
:ref:`transcoder<Transcoder>` that uses orjson, possibly the fastest JSON transcoder
31+
available in Python.
32+
33+
.. literalinclude:: ../../../examples/aggregate7/orjsonpydantic.py
34+
:pyobject: OrjsonTranscoder
3435

3536

3637
Pydantic model for immutable aggregate
3738
--------------------------------------
3839

40+
The code below shows how to define base classes for immutable aggregates that use Pydantic.
41+
3942
.. literalinclude:: ../../../examples/aggregate7/immutablemodel.py
4043

4144

4245
Domain model
4346
------------
4447

48+
The code below shows how to define an immutable aggregate in a functional style, using the Pydantic module for immutable aggregates.
49+
4550
.. literalinclude:: ../../../examples/aggregate7/domainmodel.py
4651

4752

4853
Application
4954
-----------
5055

56+
The :class:`~examples.aggregate7.application.DogSchool` application in this example uses the library's
57+
:class:`~eventsourcing.application.Application` class. It must receive the new events that are returned
58+
by the aggregate command methods, and pass them to its :func:`~eventsourcing.application.Application.save`
59+
method. The aggregate projector function must also be supplied when reconstructing an aggregate from the
60+
repository, and when taking snapshots.
61+
5162
.. literalinclude:: ../../../examples/aggregate7/application.py
63+
:pyobject: DogSchool
5264

5365

5466
Test case
5567
---------
5668

69+
The :class:`~examples.aggregate7.test_application.TestDogSchool` test case shows how the
70+
:class:`~examples.aggregate7.application.DogSchool` application can be used.
71+
5772
.. literalinclude:: ../../../examples/aggregate7/test_application.py
73+
:pyobject: TestDogSchool
5874

5975

6076
Code reference

docs/topics/examples/aggregate8.rst

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,52 +3,50 @@
33
Aggregate 8 - Pydantic with declarative syntax
44
==============================================
55

6-
This example shows another variation of the ``Dog`` aggregate class used
7-
in the tutorial and module docs.
8-
9-
Similar to :doc:`example 1 </topics/examples/aggregate1>`, the aggregate is expressed
10-
using the library's declarative syntax. And similar to :doc:`example 7 </topics/examples/aggregate7>`,
11-
the model events are defined using Pydantic.
12-
13-
The application class in this example uses the persistence classes ``PydanticMapper``
14-
and ``OrjsonTranscoder`` from :doc:`example 7 </topics/examples/aggregate7>`. Pydantic
15-
is responsible for converting domain model objects to object types that orjson can serialise,
16-
and for reconstructing model objects from JSON objects that have been deserialised by orjson.
17-
The application class also uses the custom ``Snapshot`` class, which also is defined as a
18-
Pydantic model.
19-
20-
One advantage of using Pydantic here is that any custom value objects
21-
will be automatically reconstructed without needing to define the
22-
transcoding classes that would be needed when using the library's
23-
default ``JSONTranscoder``. This is demonstrated in the example below
24-
with the ``Trick`` class, which is used in both aggregate events and
25-
aggregate state, and which is reconstructed from serialised string
26-
values, representing only the name of the trick, from both recorded
27-
aggregate events and from recorded snapshots.
6+
This example shows how to use Pydantic with the library's declarative syntax.
287

8+
Similar to :doc:`example 1 </topics/examples/aggregate1>`, aggregates are expressed
9+
using the library's declarative syntax. This is the most concise way of defining an
10+
event-sourced aggregate.
11+
12+
Similar to :doc:`example 7 </topics/examples/aggregate7>`, domain event and custom value objects
13+
are defined using Pydantic. The main advantage of using Pydantic here is that any custom value objects
14+
used in the domain model will be automatically serialised and deserialised, without needing also to
15+
define custom :ref:`transcoding<Transcodings>` classes.
2916

3017
Pydantic model for mutable aggregate
3118
------------------------------------
3219

20+
The code below shows how to define base classes for mutable aggregates that use Pydantic.
21+
3322
.. literalinclude:: ../../../examples/aggregate8/mutablemodel.py
3423

3524

3625
Domain model
3726
------------
3827

28+
The code below shows how to define an aggregate using the Pydantic and the library's declarative syntax.
29+
3930
.. literalinclude:: ../../../examples/aggregate8/domainmodel.py
4031

4132

4233
Application
4334
-----------
4435

36+
The :class:`~examples.aggregate8.application.DogSchool` application in this example uses the library's
37+
:class:`~eventsourcing.application.Application` class. It also uses the
38+
:class:`~examples.aggregate7.orjsonpydantic.PydanticMapper` and
39+
:class:`~examples.aggregate7.orjsonpydantic.OrjsonTranscoder` classes
40+
from :doc:`example 7 </topics/examples/aggregate7>`.
4541

4642
.. literalinclude:: ../../../examples/aggregate8/application.py
4743

4844

4945
Test case
5046
---------
5147

48+
The :class:`~examples.aggregate7.test_application.TestDogSchool` test case shows how the
49+
:class:`~examples.aggregate7.application.DogSchool` application can be used.
5250

5351
.. literalinclude:: ../../../examples/aggregate8/test_application.py
5452

eventsourcing/domain.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1552,12 +1552,6 @@ class VersionError(OriginatorVersionError):
15521552

15531553

15541554
class SnapshotProtocol(DomainEventProtocol, Protocol):
1555-
@property
1556-
def topic(self) -> str:
1557-
"""
1558-
Snapshots have a read-only 'topic'.
1559-
"""
1560-
15611555
@property
15621556
def state(self) -> Dict[str, Any]:
15631557
"""

examples/aggregate6/application.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import TYPE_CHECKING, Any, Dict
44

55
from eventsourcing.application import Application
6+
from examples.aggregate6.baseclasses import Snapshot
67
from examples.aggregate6.domainmodel import add_trick, project_dog, register_dog
78

89
if TYPE_CHECKING:
@@ -11,6 +12,7 @@
1112

1213
class DogSchool(Application):
1314
is_snapshotting_enabled = True
15+
snapshot_class = Snapshot
1416

1517
def register_dog(self, name: str) -> UUID:
1618
event = register_dog(name)

examples/aggregate6/baseclasses.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
from __future__ import annotations
22

33
from dataclasses import dataclass
4-
from typing import TYPE_CHECKING, Callable, Iterable, Optional, TypeVar
4+
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, Optional, TypeVar
5+
6+
from eventsourcing.domain import datetime_now_with_tzinfo
57

68
if TYPE_CHECKING:
79
from datetime import datetime
@@ -23,6 +25,20 @@ class Aggregate:
2325
modified_on: datetime
2426

2527

28+
@dataclass(frozen=True)
29+
class Snapshot(DomainEvent):
30+
state: Dict[str, Any]
31+
32+
@classmethod
33+
def take(cls, aggregate: Aggregate) -> Snapshot:
34+
return Snapshot(
35+
originator_id=aggregate.id,
36+
originator_version=aggregate.version,
37+
timestamp=datetime_now_with_tzinfo(),
38+
state=aggregate.__dict__,
39+
)
40+
41+
2642
TAggregate = TypeVar("TAggregate", bound=Aggregate)
2743
MutatorFunction = Callable[..., Optional[TAggregate]]
2844

examples/aggregate6/domainmodel.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,13 @@
55
from typing import Tuple
66
from uuid import uuid4
77

8-
from eventsourcing.domain import Snapshot, datetime_now_with_tzinfo
9-
from examples.aggregate6.baseclasses import Aggregate, DomainEvent, aggregate_projector
8+
from eventsourcing.domain import datetime_now_with_tzinfo
9+
from examples.aggregate6.baseclasses import (
10+
Aggregate,
11+
DomainEvent,
12+
Snapshot,
13+
aggregate_projector,
14+
)
1015

1116

1217
@dataclass(frozen=True)
@@ -44,7 +49,7 @@ def add_trick(dog: Dog, trick: str) -> DomainEvent:
4449

4550

4651
@singledispatch
47-
def mutate_dog(_: DomainEvent | Snapshot, __: Dog | None) -> Dog | None:
52+
def mutate_dog(_: DomainEvent, __: Dog | None) -> Dog | None:
4853
"""Mutates aggregate with event."""
4954

5055

0 commit comments

Comments
 (0)