Decouple all cross-app service calls to HTTP endpoints
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m0s

Replace every direct cross-app services.* call with HTTP-based
communication: call_action() for writes, fetch_data() for reads.
Each app now registers only its own domain service.

Infrastructure:
- shared/infrastructure/actions.py — POST client for /internal/actions/
- shared/infrastructure/data_client.py — GET client for /internal/data/
- shared/contracts/dtos.py — dto_to_dict/dto_from_dict serialization

Action endpoints (writes):
- events: 8 handlers (ticket adjust, claim/confirm, toggle, adopt)
- market: 2 handlers (create/soft-delete marketplace)
- cart: 1 handler (adopt cart for user)

Data endpoints (reads):
- blog: 4 (post-by-slug/id, posts-by-ids, search-posts)
- events: 10 (pending entries/tickets, entries/tickets for page/order,
  entry-ids, associated-entries, calendars, visible-entries-for-period)
- market: 1 (marketplaces-for-container)
- cart: 1 (cart-summary)

Service registration cleanup:
- blog→blog+federation, events→calendar+federation,
  market→market+federation, cart→cart only,
  federation→federation only, account→nothing
- Stubs reduced to minimal StubFederationService

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
giles
2026-02-25 03:01:38 +00:00
parent 5dafbdbda9
commit 3b707ec8a0
55 changed files with 1210 additions and 581 deletions

View File

@@ -5,11 +5,74 @@ see ORM model instances from another domain — only these DTOs.
"""
from __future__ import annotations
import dataclasses
import typing
from dataclasses import dataclass, field
from datetime import datetime
from decimal import Decimal
# ---------------------------------------------------------------------------
# Serialization helpers for JSON transport over internal data endpoints
# ---------------------------------------------------------------------------
def _serialize_value(v):
"""Convert a single value to a JSON-safe type."""
if isinstance(v, datetime):
return v.isoformat()
if isinstance(v, Decimal):
return str(v)
if isinstance(v, set):
return list(v)
if dataclasses.is_dataclass(v) and not isinstance(v, type):
return dto_to_dict(v)
if isinstance(v, list):
return [_serialize_value(item) for item in v]
return v
def dto_to_dict(obj) -> dict:
"""Convert a frozen DTO dataclass to a JSON-serialisable dict."""
return {k: _serialize_value(v) for k, v in dataclasses.asdict(obj).items()}
def _unwrap_optional(hint):
"""Unwrap Optional[X] / X | None to return X."""
args = getattr(hint, "__args__", ())
if args:
real = [a for a in args if a is not type(None)]
if real:
return real[0]
return hint
def dto_from_dict(cls, data: dict):
"""Construct a DTO from a dict, coercing dates and Decimals.
Uses ``typing.get_type_hints()`` to resolve forward-ref annotations
(from ``from __future__ import annotations``).
"""
if not data:
return None
try:
hints = typing.get_type_hints(cls)
except Exception:
hints = {}
kwargs = {}
for f in dataclasses.fields(cls):
if f.name not in data:
continue
val = data[f.name]
if val is not None and f.name in hints:
hint = _unwrap_optional(hints[f.name])
if hint is datetime and isinstance(val, str):
val = datetime.fromisoformat(val)
elif hint is Decimal:
val = Decimal(str(val))
kwargs[f.name] = val
return cls(**kwargs)
# ---------------------------------------------------------------------------
# Blog domain
# ---------------------------------------------------------------------------