Skip to content

Commit

Permalink
Improved aggregate examples 7 and 8 by introducing a custom type of v…
Browse files Browse the repository at this point in the history
…alue object (the Trick class) and using this in aggregate events and aggregate state, and having this type reconstructed from JSON by Pydantic when reconstructing aggregate state from both recorded aggregate events and snapshots which simply contain string values representing the name of the trick.
  • Loading branch information
johnbywater committed Oct 14, 2023
1 parent 0741f00 commit e750724
Show file tree
Hide file tree
Showing 16 changed files with 119 additions and 46 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ create_postgres_db:
.PHONY: updatetools
updatetools:
pip install -U pip
pip install -U black mypy flake8 flake8-bugbear isort
pip install -U black mypy flake8 flake8-bugbear isort orjson python-coveralls coverage

.PHONY: docs
docs:
Expand Down
3 changes: 2 additions & 1 deletion docs/topics/examples/aggregate7.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ that have been deserialised by orjson.
One advantage of using Pydantic here is that any custom value objects
will be automatically reconstructed without needing to define the
transcoding classes that would be needed when using the library's
default ``JSONTranscoder``.
default ``JSONTranscoder``. This is demonstrated with the ``Trick``
class.


Domain model
Expand Down
3 changes: 2 additions & 1 deletion docs/topics/examples/aggregate8.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ Pydantic model.
One advantage of using Pydantic here is that any custom value objects
will be automatically reconstructed without needing to define the
transcoding classes that would be needed when using the library's
default ``JSONTranscoder``.
default ``JSONTranscoder``. This is demonstrated with the ``Trick``
class.


Domain model
Expand Down
5 changes: 4 additions & 1 deletion eventsourcing/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -884,7 +884,10 @@ def take_snapshot(
aggregate = self.repository.get(
aggregate_id, version=version, projector_func=projector_func
)
snapshot = type(self).snapshot_class.take(aggregate)
snapshot_class = getattr(
type(aggregate), "Snapshot", type(self).snapshot_class
)
snapshot = snapshot_class.take(aggregate)
self.snapshots.put([snapshot])

def notify(self, new_events: List[DomainEventProtocol]) -> None:
Expand Down
2 changes: 1 addition & 1 deletion eventsourcing/domain.py
Original file line number Diff line number Diff line change
Expand Up @@ -1556,7 +1556,7 @@ def take(cls: Any, aggregate: Any) -> Any:

class CanSnapshotAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
topic: str
state: Dict[str, Any]
state: Any

@classmethod
def take(
Expand Down
3 changes: 2 additions & 1 deletion eventsourcing/examples/aggregate7/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from eventsourcing.application import Application
from eventsourcing.examples.aggregate7.domainmodel import (
Snapshot,
Trick,
add_trick,
project_dog,
register_dog,
Expand All @@ -26,7 +27,7 @@ def register_dog(self, name: str) -> UUID:

def add_trick(self, dog_id: UUID, trick: str) -> None:
dog = self.repository.get(dog_id, projector_func=project_dog)
self.save(add_trick(dog, trick))
self.save(add_trick(dog, Trick(name=trick)))

def get_dog(self, dog_id: UUID) -> Dict[str, Any]:
dog = self.repository.get(dog_id, projector_func=project_dog)
Expand Down
10 changes: 7 additions & 3 deletions eventsourcing/examples/aggregate7/domainmodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,21 @@ def project_aggregate(
return project_aggregate


class Trick(BaseModel):
name: str


class Dog(Aggregate):
name: str
tricks: Tuple[str, ...]
tricks: Tuple[Trick, ...]


class DogRegistered(DomainEvent):
name: str


class TrickAdded(DomainEvent):
trick: str
trick: Trick


def register_dog(name: str) -> DomainEvent:
Expand All @@ -90,7 +94,7 @@ def register_dog(name: str) -> DomainEvent:
)


def add_trick(dog: Dog, trick: str) -> DomainEvent:
def add_trick(dog: Dog, trick: Trick) -> DomainEvent:
return TrickAdded(
originator_id=dog.id,
originator_version=dog.version + 1,
Expand Down
23 changes: 16 additions & 7 deletions eventsourcing/examples/aggregate7/test_application.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from typing import Tuple
from unittest import TestCase

from eventsourcing.examples.aggregate7.application import DogSchool
from eventsourcing.examples.aggregate7.domainmodel import project_dog
from eventsourcing.examples.aggregate7.domainmodel import Trick, project_dog


class TestDogSchool(TestCase):
Expand All @@ -16,8 +17,8 @@ def test_dog_school(self) -> None:

# Query application state.
dog = school.get_dog(dog_id)
assert dog["name"] == "Fido"
assert dog["tricks"] == ("roll over", "play dead")
self.assertEqual(dog["name"], "Fido")
self.assertEqualTricks(dog["tricks"], ("roll over", "play dead"))

# Select notifications.
notifications = school.notification_log.select(start=1, limit=10)
Expand All @@ -26,11 +27,19 @@ def test_dog_school(self) -> None:
# Take snapshot.
school.take_snapshot(dog_id, version=3, projector_func=project_dog)
dog = school.get_dog(dog_id)
assert dog["name"] == "Fido"
assert dog["tricks"] == ("roll over", "play dead")
self.assertEqual(dog["name"], "Fido")
self.assertEqualTricks(dog["tricks"], ("roll over", "play dead"))

# Continue with snapshotted aggregate.
school.add_trick(dog_id, "fetch ball")
dog = school.get_dog(dog_id)
assert dog["name"] == "Fido"
assert dog["tricks"] == ("roll over", "play dead", "fetch ball")
self.assertEqual(dog["name"], "Fido")
self.assertEqualTricks(dog["tricks"], ("roll over", "play dead", "fetch ball"))

def assertEqualTricks(
self, actual: Tuple[Trick, ...], expected: Tuple[str, ...]
) -> None:
self.assertEqual(len(actual), len(expected))
for i, trick in enumerate(actual):
self.assertIsInstance(trick, Trick)
self.assertEqual(trick.name, expected[i])
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from typing import Tuple
from unittest import TestCase

from eventsourcing.cipher import AESCipher
from eventsourcing.examples.aggregate7.application import DogSchool
from eventsourcing.examples.aggregate7.domainmodel import project_dog
from eventsourcing.examples.aggregate7.domainmodel import Trick, project_dog


class TestDogSchool(TestCase):
Expand All @@ -24,7 +25,7 @@ def test_dog_school(self) -> None:
# Query application state.
dog = school.get_dog(dog_id)
assert dog["name"] == "Fido"
assert dog["tricks"] == ("roll over", "play dead")
self.assertEqualTricks(dog["tricks"], ("roll over", "play dead"))

# Select notifications.
notifications = school.notification_log.select(start=1, limit=10)
Expand All @@ -34,10 +35,18 @@ def test_dog_school(self) -> None:
school.take_snapshot(dog_id, version=3, projector_func=project_dog)
dog = school.get_dog(dog_id)
assert dog["name"] == "Fido"
assert dog["tricks"] == ("roll over", "play dead")
self.assertEqualTricks(dog["tricks"], ("roll over", "play dead"))

# Continue with snapshotted aggregate.
school.add_trick(dog_id, "fetch ball")
dog = school.get_dog(dog_id)
assert dog["name"] == "Fido"
assert dog["tricks"] == ("roll over", "play dead", "fetch ball")
self.assertEqualTricks(dog["tricks"], ("roll over", "play dead", "fetch ball"))

def assertEqualTricks(
self, actual: Tuple[Trick, ...], expected: Tuple[str, ...]
) -> None:
self.assertEqual(len(actual), len(expected))
for i, trick in enumerate(actual):
self.assertIsInstance(trick, Trick)
self.assertEqual(trick.name, expected[i])
17 changes: 13 additions & 4 deletions eventsourcing/examples/aggregate7/test_snapshotting_intervals.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from typing import cast
from typing import Tuple, cast
from unittest import TestCase
from uuid import UUID

from eventsourcing.domain import ProgrammingError
from eventsourcing.examples.aggregate7.application import DogSchool
from eventsourcing.examples.aggregate7.domainmodel import (
Dog,
Trick,
add_trick,
project_dog,
register_dog,
Expand All @@ -23,7 +24,7 @@ def register_dog(self, name: str) -> UUID:

def add_trick(self, dog_id: UUID, trick: str) -> None:
dog = self.repository.get(dog_id, projector_func=project_dog)
event = add_trick(dog, trick)
event = add_trick(dog, Trick(name=trick))
dog = cast(Dog, project_dog(dog, [event]))
self.save(dog, event)

Expand Down Expand Up @@ -58,5 +59,13 @@ def test_dog_school(self) -> None:

# Query application state.
dog = school.get_dog(dog_id)
assert dog["name"] == "Fido"
assert dog["tricks"] == ("roll over", "play dead")
self.assertEqual(dog["name"], "Fido")
self.assertEqualTricks(dog["tricks"], ("roll over", "play dead"))

def assertEqualTricks(
self, actual: Tuple[Trick, ...], expected: Tuple[str, ...]
) -> None:
self.assertEqual(len(actual), len(expected))
for i, trick in enumerate(actual):
self.assertIsInstance(trick, Trick)
self.assertEqual(trick.name, expected[i])
12 changes: 8 additions & 4 deletions eventsourcing/examples/aggregate8/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from uuid import UUID

from eventsourcing.application import Application
from eventsourcing.examples.aggregate8.domainmodel import Dog, Snapshot
from eventsourcing.examples.aggregate8.domainmodel import Dog, Trick
from eventsourcing.examples.aggregate8.persistence import (
OrjsonTranscoder,
PydanticMapper,
Expand All @@ -12,7 +12,6 @@

class DogSchool(Application):
is_snapshotting_enabled = True
snapshot_class = Snapshot

def register_dog(self, name: str) -> UUID:
dog = Dog(name)
Expand All @@ -21,12 +20,17 @@ def register_dog(self, name: str) -> UUID:

def add_trick(self, dog_id: UUID, trick: str) -> None:
dog: Dog = self.repository.get(dog_id)
dog.add_trick(trick)
dog.add_trick(Trick(name=trick))
self.save(dog)

def get_dog(self, dog_id: UUID) -> Dict[str, Any]:
dog: Dog = self.repository.get(dog_id)
return {"name": dog.name, "tricks": tuple(dog.tricks)}
return {
"name": dog.name,
"tricks": tuple(dog.tricks),
"created_on": dog.created_on,
"modified_on": dog.modified_on,
}

def construct_mapper(self) -> Mapper:
return self.factory.mapper(
Expand Down
29 changes: 23 additions & 6 deletions eventsourcing/examples/aggregate8/domainmodel.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from datetime import datetime
from typing import Any, Dict, List
from typing import List
from uuid import UUID

from pydantic import BaseModel
from pydantic import BaseModel, Extra

from eventsourcing.domain import (
Aggregate as BaseAggregate,
Expand Down Expand Up @@ -30,17 +30,34 @@ class Created(Event, CanInitAggregate):
originator_topic: str


class Snapshot(DomainEvent, CanSnapshotAggregate):
class SnapshotState(BaseModel):
class Config:
extra = Extra.allow


class AggregateSnapshot(DomainEvent, CanSnapshotAggregate):
topic: str
state: Dict[str, Any]
state: SnapshotState


class Trick(BaseModel):
name: str


class DogState(SnapshotState):
name: str
tricks: List[Trick]


class Dog(Aggregate):
class Snapshot(AggregateSnapshot):
state: DogState

@event("Registered")
def __init__(self, name: str) -> None:
self.name = name
self.tricks: List[str] = []
self.tricks: List[Trick] = []

@event("TrickAdded")
def add_trick(self, trick: str) -> None:
def add_trick(self, trick: Trick) -> None:
self.tricks.append(trick)
22 changes: 16 additions & 6 deletions eventsourcing/examples/aggregate8/test_application.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from typing import Tuple
from unittest import TestCase

from eventsourcing.examples.aggregate8.application import DogSchool
from eventsourcing.examples.aggregate8.domainmodel import Trick


class TestDogSchool(TestCase):
Expand All @@ -15,8 +17,8 @@ def test_dog_school(self) -> None:

# Query application state.
dog = school.get_dog(dog_id)
assert dog["name"] == "Fido"
assert dog["tricks"] == ("roll over", "play dead")
self.assertEqual(dog["name"], "Fido")
self.assertEqualTricks(dog["tricks"], ("roll over", "play dead"))

# Select notifications.
notifications = school.notification_log.select(start=1, limit=10)
Expand All @@ -25,11 +27,19 @@ def test_dog_school(self) -> None:
# Take snapshot.
school.take_snapshot(dog_id, version=3)
dog = school.get_dog(dog_id)
assert dog["name"] == "Fido"
assert dog["tricks"] == ("roll over", "play dead")
self.assertEqual(dog["name"], "Fido")
self.assertEqualTricks(dog["tricks"], ("roll over", "play dead"))

# Continue with snapshotted aggregate.
school.add_trick(dog_id, "fetch ball")
dog = school.get_dog(dog_id)
assert dog["name"] == "Fido"
assert dog["tricks"] == ("roll over", "play dead", "fetch ball")
self.assertEqual(dog["name"], "Fido")
self.assertEqualTricks(dog["tricks"], ("roll over", "play dead", "fetch ball"))

def assertEqualTricks(
self, actual: Tuple[Trick, ...], expected: Tuple[str, ...]
) -> None:
self.assertEqual(len(actual), len(expected))
for i, trick in enumerate(actual):
self.assertIsInstance(trick, Trick)
self.assertEqual(trick.name, expected[i])
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from eventsourcing.cipher import AESCipher
from eventsourcing.examples.aggregate8.application import DogSchool
from eventsourcing.examples.aggregate8.domainmodel import Trick


class TestDogSchool(TestCase):
Expand All @@ -23,7 +24,7 @@ def test_dog_school(self) -> None:
# Query application state.
dog = school.get_dog(dog_id)
assert dog["name"] == "Fido"
assert dog["tricks"] == ("roll over", "play dead")
assert dog["tricks"] == (Trick(name="roll over"), Trick(name="play dead"))

# Select notifications.
notifications = school.notification_log.select(start=1, limit=10)
Expand All @@ -33,10 +34,14 @@ def test_dog_school(self) -> None:
school.take_snapshot(dog_id, version=3)
dog = school.get_dog(dog_id)
assert dog["name"] == "Fido"
assert dog["tricks"] == ("roll over", "play dead")
assert dog["tricks"] == (Trick(name="roll over"), Trick(name="play dead"))

# Continue with snapshotted aggregate.
school.add_trick(dog_id, "fetch ball")
dog = school.get_dog(dog_id)
assert dog["name"] == "Fido"
assert dog["tricks"] == ("roll over", "play dead", "fetch ball")
assert dog["tricks"] == (
Trick(name="roll over"),
Trick(name="play dead"),
Trick(name="fetch ball"),
)
Loading

0 comments on commit e750724

Please sign in to comment.