Domain isolation: typed contracts, service registry, and composable wiring
Add typed service contracts (Protocols + frozen DTOs) in shared/contracts/ for cross-domain communication. Each domain exposes a service interface (BlogService, CalendarService, MarketService, CartService) backed by SQL implementations in shared/services/. A singleton registry with has() guards enables composable startup — apps register their own domain service and stubs for absent domains. Absorbs glue layer: navigation, relationships, event handlers (login, container, order) now live in shared/ with has()-guarded service calls. Factory gains domain_services_fn parameter for per-app service registration. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
31
contracts/__init__.py
Normal file
31
contracts/__init__.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""Typed contracts (DTOs + Protocols) for cross-domain service interfaces."""
|
||||
|
||||
from .dtos import (
|
||||
PostDTO,
|
||||
CalendarDTO,
|
||||
CalendarEntryDTO,
|
||||
MarketPlaceDTO,
|
||||
ProductDTO,
|
||||
CartItemDTO,
|
||||
CartSummaryDTO,
|
||||
)
|
||||
from .protocols import (
|
||||
BlogService,
|
||||
CalendarService,
|
||||
MarketService,
|
||||
CartService,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"PostDTO",
|
||||
"CalendarDTO",
|
||||
"CalendarEntryDTO",
|
||||
"MarketPlaceDTO",
|
||||
"ProductDTO",
|
||||
"CartItemDTO",
|
||||
"CartSummaryDTO",
|
||||
"BlogService",
|
||||
"CalendarService",
|
||||
"MarketService",
|
||||
"CartService",
|
||||
]
|
||||
113
contracts/dtos.py
Normal file
113
contracts/dtos.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""Frozen dataclasses for cross-domain data transfer.
|
||||
|
||||
These are the *only* shapes that cross domain boundaries. Consumers never
|
||||
see ORM model instances from another domain — only these DTOs.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Blog domain
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PostDTO:
|
||||
id: int
|
||||
slug: str
|
||||
title: str
|
||||
status: str
|
||||
visibility: str
|
||||
is_page: bool = False
|
||||
feature_image: str | None = None
|
||||
html: str | None = None
|
||||
excerpt: str | None = None
|
||||
custom_excerpt: str | None = None
|
||||
published_at: datetime | None = None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Calendar / Events domain
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CalendarDTO:
|
||||
id: int
|
||||
container_type: str
|
||||
container_id: int
|
||||
name: str
|
||||
slug: str
|
||||
description: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CalendarEntryDTO:
|
||||
id: int
|
||||
calendar_id: int
|
||||
name: str
|
||||
start_at: datetime
|
||||
state: str
|
||||
cost: Decimal
|
||||
end_at: datetime | None = None
|
||||
user_id: int | None = None
|
||||
session_id: str | None = None
|
||||
order_id: int | None = None
|
||||
slot_id: int | None = None
|
||||
ticket_price: Decimal | None = None
|
||||
ticket_count: int | None = None
|
||||
calendar_name: str | None = None
|
||||
calendar_slug: str | None = None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Market domain
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class MarketPlaceDTO:
|
||||
id: int
|
||||
container_type: str
|
||||
container_id: int
|
||||
name: str
|
||||
slug: str
|
||||
description: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ProductDTO:
|
||||
id: int
|
||||
slug: str
|
||||
title: str | None = None
|
||||
image: str | None = None
|
||||
description_short: str | None = None
|
||||
rrp: Decimal | None = None
|
||||
regular_price: Decimal | None = None
|
||||
special_price: Decimal | None = None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Cart domain
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CartItemDTO:
|
||||
id: int
|
||||
product_id: int
|
||||
quantity: int
|
||||
product_title: str | None = None
|
||||
product_slug: str | None = None
|
||||
product_image: str | None = None
|
||||
unit_price: Decimal | None = None
|
||||
market_place_id: int | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CartSummaryDTO:
|
||||
count: int = 0
|
||||
total: Decimal = Decimal("0")
|
||||
calendar_count: int = 0
|
||||
calendar_total: Decimal = Decimal("0")
|
||||
items: list[CartItemDTO] = field(default_factory=list)
|
||||
95
contracts/protocols.py
Normal file
95
contracts/protocols.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""Protocol classes defining each domain's service interface.
|
||||
|
||||
All cross-domain callers program against these Protocols. Concrete
|
||||
implementations (Sql*Service) and no-op stubs both satisfy them.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Protocol, runtime_checkable
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from .dtos import (
|
||||
PostDTO,
|
||||
CalendarDTO,
|
||||
CalendarEntryDTO,
|
||||
MarketPlaceDTO,
|
||||
ProductDTO,
|
||||
CartItemDTO,
|
||||
CartSummaryDTO,
|
||||
)
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class BlogService(Protocol):
|
||||
async def get_post_by_slug(self, session: AsyncSession, slug: str) -> PostDTO | None: ...
|
||||
async def get_post_by_id(self, session: AsyncSession, id: int) -> PostDTO | None: ...
|
||||
async def get_posts_by_ids(self, session: AsyncSession, ids: list[int]) -> list[PostDTO]: ...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class CalendarService(Protocol):
|
||||
async def calendars_for_container(
|
||||
self, session: AsyncSession, container_type: str, container_id: int,
|
||||
) -> list[CalendarDTO]: ...
|
||||
|
||||
async def pending_entries(
|
||||
self, session: AsyncSession, *, user_id: int | None, session_id: str | None,
|
||||
) -> list[CalendarEntryDTO]: ...
|
||||
|
||||
async def entries_for_page(
|
||||
self, session: AsyncSession, page_id: int, *, user_id: int | None, session_id: str | None,
|
||||
) -> list[CalendarEntryDTO]: ...
|
||||
|
||||
async def entry_by_id(self, session: AsyncSession, entry_id: int) -> CalendarEntryDTO | None: ...
|
||||
|
||||
async def associated_entries(
|
||||
self, session: AsyncSession, content_type: str, content_id: int, page: int,
|
||||
) -> tuple[list[CalendarEntryDTO], bool]: ...
|
||||
|
||||
async def toggle_entry_post(
|
||||
self, session: AsyncSession, entry_id: int, content_type: str, content_id: int,
|
||||
) -> bool: ...
|
||||
|
||||
async def adopt_entries_for_user(
|
||||
self, session: AsyncSession, user_id: int, session_id: str,
|
||||
) -> None: ...
|
||||
|
||||
async def claim_entries_for_order(
|
||||
self, session: AsyncSession, order_id: int, user_id: int | None,
|
||||
session_id: str | None, page_post_id: int | None,
|
||||
) -> None: ...
|
||||
|
||||
async def confirm_entries_for_order(
|
||||
self, session: AsyncSession, order_id: int, user_id: int | None,
|
||||
session_id: str | None,
|
||||
) -> None: ...
|
||||
|
||||
async def get_entries_for_order(
|
||||
self, session: AsyncSession, order_id: int,
|
||||
) -> list[CalendarEntryDTO]: ...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class MarketService(Protocol):
|
||||
async def marketplaces_for_container(
|
||||
self, session: AsyncSession, container_type: str, container_id: int,
|
||||
) -> list[MarketPlaceDTO]: ...
|
||||
|
||||
async def product_by_id(self, session: AsyncSession, product_id: int) -> ProductDTO | None: ...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class CartService(Protocol):
|
||||
async def cart_summary(
|
||||
self, session: AsyncSession, *, user_id: int | None, session_id: str | None,
|
||||
page_slug: str | None = None,
|
||||
) -> CartSummaryDTO: ...
|
||||
|
||||
async def cart_items(
|
||||
self, session: AsyncSession, *, user_id: int | None, session_id: str | None,
|
||||
) -> list[CartItemDTO]: ...
|
||||
|
||||
async def adopt_cart_for_user(
|
||||
self, session: AsyncSession, user_id: int, session_id: str,
|
||||
) -> None: ...
|
||||
Reference in New Issue
Block a user