Files
docs/standards/mcp/templates/app/interfaces/_common.py
Vishesh 'ironeagle' Bangotra 406affcfcf feat: renumber lib reference into 4-pillar anatomy
Renumber the 15 reference docs into pillar groups (recipe -> anatomy):
- 01 Build Recipe (entry), 02 Composition Root
- domains: 10 Layering Rules
- contract: 20 Spec-First Routing, 21 CRUD Template, 22 Auth Integration
- interfaces/infra: 30 Infra Adapters
- UI: 40 UI: Auto-Rendered Admin & Custom Pages (new; @aetoskia packages,
  client template, x- catalog via react-openapi MCP module)
- pipelines: 50 Pipeline Scaffold, 51 Worker & Progress, 52 Human-in-the-Loop
- delivery/meta: 90 Drone Pipelines, 91 Versioning & Changelog,
  92 Genericity Matrix, 93 Library Registry, 94 Repository Inventory

MCP module names are unchanged (NN_ prefix stripped); all cross-references
(wiki, templates, hooks, registry) updated; lib index rewritten pillar-first;
build recipe bootstraps the client template. Bundle + HTML sites regenerated.
2026-10-19 17:42:55 +05:30

147 lines
4.5 KiB
Python

"""# Summary
Shared interface helpers — service resolution, serialization, pagination and
foreign-key expansion used by every route module.
---
FK fields on the wire may arrive either as a bare integer id (from an
``x-fk`` picker) or as an already-expanded object. The wire contract always
*returns* fully expanded objects, so create/update handlers normalize the
input through :func:`expand_ref` before persisting.
"""
from fastapi import HTTPException, Request
def svc(request: Request, name: str):
"""Resolve a CRUD service from app state by resource name.
Args:
request (Request): Incoming request whose app.state carries resources.
name (str): Resource key suffix, e.g. ``"parent"`` → ``parent_svc``.
Returns:
Any: The named CRUD service.
"""
return request.app.state.db_resources[f"{name}_svc"]
def dump(doc) -> dict:
"""Serialize a document to a JSON-ready mapping with an integer id.
Args:
doc (Any): Pydantic document to serialize.
Returns:
dict: JSON-mode mapping (``by_alias`` so ``_id`` maps back to ``id``).
"""
return doc.model_dump(mode="json", by_alias=True)
def paginate(items: list, limit: int, offset: int) -> dict:
"""Slice ``items`` into the ``{total, items}`` pagination envelope.
A ``limit`` of 0 means "all remaining items" (used by react-openapi for
FK option lists and counts).
Args:
items (list): Full result list.
limit (int): Page size; ``0`` returns everything after ``offset``.
offset (int): Number of items to skip.
Returns:
dict: ``{"total": int, "items": list}``.
"""
offset = max(offset, 0)
page = items[offset:] if limit == 0 else items[offset : offset + limit]
return {"total": len(items), "items": page}
async def expand_ref(svc_obj, value, label: str) -> dict | None:
"""Normalize a foreign-key reference into a fully expanded object.
Args:
svc_obj (Any): Target resource CRUD service used for lookups.
value (dict | int | None): Bare id, ``{"id": N}`` stub, or full object.
label (str): Human label used in 422 error messages.
Returns:
dict | None: Expanded object, or ``None`` when ``value`` is ``None``.
Raises:
HTTPException: 422 when the reference cannot be resolved to a document.
"""
if value is None:
return None
if isinstance(value, dict):
if set(value.keys()) == {"id"}:
value = value["id"]
else:
return value
if isinstance(value, int):
doc = await svc_obj.read(value)
if doc is None:
raise HTTPException(status_code=422, detail=f"{label} {value} not found")
return dump(doc)
raise HTTPException(status_code=422, detail=f"invalid {label} reference")
async def expand_ref_list(svc_obj, value, label: str) -> list[dict]:
"""Normalize a list foreign-key reference into expanded objects.
Args:
svc_obj (Any): Target resource CRUD service used for lookups.
value (list | None): List of bare ids, stubs, or full objects.
label (str): Human label used in 422 error messages.
Returns:
list[dict]: Expanded objects.
Raises:
HTTPException: 422 when any reference cannot be resolved.
"""
if value is None:
return []
if not isinstance(value, list):
raise HTTPException(status_code=422, detail=f"invalid {label} list")
results = []
for entry in value:
results.append(await expand_ref(svc_obj, entry, label))
return results
async def read_or_404(svc_obj, id: int, label: str):
"""Read a document by id or raise a 404 HTTPException.
Args:
svc_obj (Any): CRUD service to read from.
id (int): Document identifier.
label (str): Human label used in the 404 detail message.
Returns:
Any: The found document.
Raises:
HTTPException: 404 when no document matches ``id``.
"""
doc = await svc_obj.read(id)
if doc is None:
raise HTTPException(status_code=404, detail=f"{label} not found")
return doc
async def delete_or_404(svc_obj, id: int, label: str) -> None:
"""Delete a document by id or raise a 404 HTTPException.
Args:
svc_obj (Any): CRUD service to delete from.
id (int): Document identifier.
label (str): Human label used in the 404 detail message.
Raises:
HTTPException: 404 when no document matches ``id``.
"""
ok = await svc_obj.delete(id)
if not ok:
raise HTTPException(status_code=404, detail=f"{label} not found")