Skip to content

Commit fc8e109

Browse files
committed
Added "vertical slices" example.
1 parent feafe24 commit fc8e109

File tree

29 files changed

+1296
-165
lines changed

29 files changed

+1296
-165
lines changed

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ install-packages:
3030

3131
.PHONY: poetry-update
3232
poetry-update:
33-
$(POETRY) lock
33+
$(POETRY) update
3434

3535
.PHONY: update
36-
update: poetry-update install
36+
update: poetry-update install-packages
3737

3838
.PHONY: lint
3939
lint: lint-black lint-ruff lint-isort lint-pyright lint-mypy #lint-dockerfile

docs/topics/examples.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ Example applications
4343
examples/content-management
4444
examples/searchable-timestamps
4545
examples/fts-content-management
46+
examples/shop-vertical
4647

4748
.. _Example projections:
4849

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
.. _Vertical slices example:
2+
3+
Application 6 - Vertical slices
4+
===============================
5+
6+
This example demonstrates how to do event sourcing with the "vertical slice architecture" advocated by the event
7+
modelling community.
8+
9+
Get cart items
10+
--------------
11+
12+
.. literalinclude:: ../../../examples/shopvertical/slices/get_cart_items/query.py
13+
:pyobject: CartItem
14+
15+
.. literalinclude:: ../../../examples/shopvertical/slices/get_cart_items/query.py
16+
:pyobject: GetCartItems
17+
18+
.. literalinclude:: ../../../examples/shopvertical/slices/get_cart_items/test.py
19+
:pyobject: TestGetCartItems
20+
21+
Add item to cart
22+
----------------
23+
24+
.. literalinclude:: ../../../examples/shopvertical/slices/add_item_to_cart/cmd.py
25+
:pyobject: AddItemToCart
26+
27+
.. literalinclude:: ../../../examples/shopvertical/slices/add_item_to_cart/test.py
28+
:pyobject: TestAddItemToCart
29+
30+
Remove item from cart
31+
---------------------
32+
33+
.. literalinclude:: ../../../examples/shopvertical/slices/remove_item_from_cart/cmd.py
34+
:pyobject: RemoveItemFromCart
35+
36+
.. literalinclude:: ../../../examples/shopvertical/slices/remove_item_from_cart/test.py
37+
:pyobject: TestRemoveItemFromCart
38+
39+
Clear cart
40+
----------
41+
42+
.. literalinclude:: ../../../examples/shopvertical/slices/clear_cart/cmd.py
43+
:pyobject: ClearCart
44+
45+
.. literalinclude:: ../../../examples/shopvertical/slices/clear_cart/test.py
46+
:pyobject: TestClearCart
47+
48+
Submit cart
49+
-----------
50+
51+
.. literalinclude:: ../../../examples/shopvertical/slices/submit_cart/cmd.py
52+
:pyobject: SubmitCart
53+
54+
.. literalinclude:: ../../../examples/shopvertical/slices/submit_cart/test.py
55+
:pyobject: TestSubmitCart
56+
57+
Adjust product inventory
58+
------------------------
59+
60+
.. literalinclude:: ../../../examples/shopvertical/slices/adjust_product_inventory/cmd.py
61+
:pyobject: AdjustProductInventory
62+
63+
.. literalinclude:: ../../../examples/shopvertical/slices/adjust_product_inventory/test.py
64+
:pyobject: TestAdjustProductInventory
65+
66+
Events
67+
------
68+
69+
.. literalinclude:: ../../../examples/shopvertical/events.py
70+
71+
Exceptions
72+
----------
73+
74+
.. literalinclude:: ../../../examples/shopvertical/exceptions.py
75+
76+
Common code
77+
-----------
78+
79+
.. literalinclude:: ../../../examples/shopvertical/common.py
80+
81+
Integration test
82+
----------------
83+
84+
.. literalinclude:: ../../../examples/shopvertical/test.py
85+
:pyobject: TestShop

examples/shopvertical/__init__.py

Whitespace-only changes.

examples/shopvertical/common.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from __future__ import annotations
2+
3+
from abc import ABC, abstractmethod
4+
from typing import TYPE_CHECKING, cast
5+
6+
from eventsourcing.application import Application
7+
from examples.shopvertical.events import DomainEvent
8+
9+
if TYPE_CHECKING:
10+
from uuid import UUID
11+
12+
13+
class Command(ABC):
14+
@abstractmethod
15+
def handle(self, events: tuple[DomainEvent, ...]) -> tuple[DomainEvent, ...]:
16+
pass # pragma: no cover
17+
18+
@abstractmethod
19+
def execute(self) -> int | None:
20+
pass # pragma: no cover
21+
22+
23+
_event_store = Application().events
24+
25+
26+
def get_events(originator_id: UUID) -> tuple[DomainEvent, ...]:
27+
return cast(tuple[DomainEvent, ...], tuple(_event_store.get(originator_id)))
28+
29+
30+
def put_events(events: tuple[DomainEvent, ...]) -> int | None:
31+
recordings = _event_store.put(events)
32+
return recordings[-1].notification.id if recordings else None

examples/shopvertical/events.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from __future__ import annotations
2+
3+
from decimal import Decimal
4+
from uuid import UUID
5+
6+
from pydantic import BaseModel, ConfigDict
7+
8+
9+
class DomainEvent(BaseModel, frozen=True):
10+
model_config = ConfigDict(extra="forbid")
11+
12+
originator_id: UUID
13+
originator_version: int
14+
15+
16+
class AddedItemToCart(DomainEvent, frozen=True):
17+
product_id: UUID
18+
description: str
19+
price: Decimal
20+
name: str
21+
22+
23+
class RemovedItemFromCart(DomainEvent, frozen=True):
24+
product_id: UUID
25+
26+
27+
class ClearedCart(DomainEvent, frozen=True):
28+
pass
29+
30+
31+
class SubmittedCart(DomainEvent, frozen=True):
32+
pass
33+
34+
35+
class AdjustedProductInventory(DomainEvent, frozen=True):
36+
adjustment: int
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
class CartFullError(Exception):
2+
pass
3+
4+
5+
class ProductNotInCartError(Exception):
6+
pass
7+
8+
9+
class InsufficientInventoryError(Exception):
10+
pass

examples/shopvertical/slices/__init__.py

Whitespace-only changes.

examples/shopvertical/slices/add_item_to_cart/__init__.py

Whitespace-only changes.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass
4+
from typing import TYPE_CHECKING
5+
6+
from examples.shopvertical.common import Command, get_events, put_events
7+
from examples.shopvertical.events import (
8+
AddedItemToCart,
9+
ClearedCart,
10+
DomainEvent,
11+
RemovedItemFromCart,
12+
)
13+
from examples.shopvertical.exceptions import CartFullError
14+
15+
if TYPE_CHECKING:
16+
from decimal import Decimal
17+
from uuid import UUID
18+
19+
20+
@dataclass(frozen=True)
21+
class AddItemToCart(Command):
22+
cart_id: UUID
23+
product_id: UUID
24+
description: str
25+
price: Decimal
26+
name: str
27+
28+
def handle(self, events: tuple[DomainEvent, ...]) -> tuple[DomainEvent, ...]:
29+
product_ids = []
30+
for event in events:
31+
if isinstance(event, AddedItemToCart):
32+
product_ids.append(event.product_id)
33+
elif isinstance(event, RemovedItemFromCart):
34+
product_ids.remove(event.product_id)
35+
elif isinstance(event, ClearedCart):
36+
product_ids.clear()
37+
38+
if len(product_ids) >= 3:
39+
raise CartFullError
40+
41+
return (
42+
AddedItemToCart(
43+
originator_id=self.cart_id,
44+
originator_version=len(events) + 1,
45+
product_id=self.product_id,
46+
description=self.description,
47+
price=self.price,
48+
name=self.name,
49+
),
50+
)
51+
52+
def execute(self) -> int | None:
53+
return put_events(self.handle(get_events(self.cart_id)))

0 commit comments

Comments
 (0)