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.
This commit is contained in:
146
standards/mcp/templates/app/interfaces/_common.py
Normal file
146
standards/mcp/templates/app/interfaces/_common.py
Normal file
@@ -0,0 +1,146 @@
|
||||
"""# 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")
|
||||
@@ -2,41 +2,89 @@
|
||||
|
||||
Item route handlers — names must match openapi.yaml operationIds.
|
||||
"""
|
||||
from fastapi import Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi import Request, Response
|
||||
|
||||
from domain.item.models import Item
|
||||
from interfaces._common import delete_or_404, dump, paginate, read_or_404, svc
|
||||
|
||||
|
||||
def svc(request: Request):
|
||||
"""Resolve the item CRUD service from app state."""
|
||||
return request.app.state.db_resources["item_svc"]
|
||||
async def list_items(request: Request, limit: int = 20, offset: int = 0) -> dict:
|
||||
"""List items (paginated). ``limit=0`` returns all remaining items.
|
||||
|
||||
Args:
|
||||
request (Request): Incoming request.
|
||||
limit (int): Page size. Defaults to 20.
|
||||
offset (int): Items to skip. Defaults to 0.
|
||||
|
||||
Returns:
|
||||
dict: ``{"total": int, "items": list[Item]}``.
|
||||
"""
|
||||
docs = await svc(request, "item").search({})
|
||||
return paginate([dump(d) for d in docs], limit, offset)
|
||||
|
||||
|
||||
async def create_item(request: Request, body: dict):
|
||||
"""Create an item from the request body."""
|
||||
data = body.model_dump() if hasattr(body, "model_dump") else dict(body)
|
||||
doc = await svc(request).create(Item(**data))
|
||||
return JSONResponse(status_code=201, content=doc.model_dump(mode="json", by_alias=True))
|
||||
async def create_item(request: Request, body: dict, response: Response) -> dict:
|
||||
"""Create an item.
|
||||
|
||||
Args:
|
||||
request (Request): Incoming request.
|
||||
body (dict): Item fields (``id`` ignored).
|
||||
response (Response): Response object; set to 201 on success.
|
||||
|
||||
Returns:
|
||||
dict: The created item.
|
||||
"""
|
||||
response.status_code = 201
|
||||
body.pop("id", None)
|
||||
doc = await svc(request, "item").create(body)
|
||||
return dump(doc)
|
||||
|
||||
|
||||
async def get_item(request: Request, id: str):
|
||||
"""Fetch one item by id."""
|
||||
doc = await svc(request).read(id)
|
||||
if doc is None:
|
||||
return JSONResponse(status_code=404, content={"detail": "item not found"})
|
||||
return doc.model_dump(mode="json", by_alias=True)
|
||||
async def get_item(request: Request, id: int) -> dict:
|
||||
"""Fetch one item by id.
|
||||
|
||||
Args:
|
||||
request (Request): Incoming request.
|
||||
id (int): Item identifier.
|
||||
|
||||
Returns:
|
||||
dict: The item document.
|
||||
|
||||
Raises:
|
||||
HTTPException: 404 when the item does not exist.
|
||||
"""
|
||||
return dump(await read_or_404(svc(request, "item"), id, "item"))
|
||||
|
||||
|
||||
async def list_items(request: Request):
|
||||
"""List all items."""
|
||||
docs = await svc(request).search({})
|
||||
return [d.model_dump(mode="json", by_alias=True) for d in docs]
|
||||
async def update_item(request: Request, id: int, body: dict) -> dict:
|
||||
"""Replace an item by id.
|
||||
|
||||
Args:
|
||||
request (Request): Incoming request.
|
||||
id (int): Item identifier.
|
||||
body (dict): Full replacement payload.
|
||||
|
||||
Returns:
|
||||
dict: The updated item.
|
||||
|
||||
Raises:
|
||||
HTTPException: 404 when the item does not exist.
|
||||
"""
|
||||
await read_or_404(svc(request, "item"), id, "item")
|
||||
body.pop("id", None)
|
||||
doc = await svc(request, "item").update(id, body)
|
||||
return dump(doc)
|
||||
|
||||
|
||||
async def delete_item(request: Request, id: str):
|
||||
"""Delete one item by id."""
|
||||
ok = await svc(request).delete(id)
|
||||
if not ok:
|
||||
return JSONResponse(status_code=404, content={"detail": "item not found"})
|
||||
return {"deleted": True}
|
||||
async def delete_item(request: Request, id: int, response: Response) -> None:
|
||||
"""Delete an item by id.
|
||||
|
||||
Args:
|
||||
request (Request): Incoming request.
|
||||
id (int): Item identifier.
|
||||
response (Response): Response object; set to 204 on success.
|
||||
|
||||
Raises:
|
||||
HTTPException: 404 when the item does not exist.
|
||||
"""
|
||||
await delete_or_404(svc(request, "item"), id, "item")
|
||||
response.status_code = 204
|
||||
|
||||
@@ -2,33 +2,89 @@
|
||||
|
||||
Job route handlers — names must match openapi.yaml operationIds.
|
||||
"""
|
||||
from fastapi import Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi import Request, Response
|
||||
|
||||
from domain.job.models import Job
|
||||
from interfaces._common import delete_or_404, dump, paginate, read_or_404, svc
|
||||
|
||||
|
||||
def svc(request: Request):
|
||||
"""Resolve the job CRUD service from app state."""
|
||||
return request.app.state.db_resources["job_svc"]
|
||||
async def list_jobs(request: Request, limit: int = 20, offset: int = 0) -> dict:
|
||||
"""List jobs (paginated). ``limit=0`` returns all remaining items.
|
||||
|
||||
Args:
|
||||
request (Request): Incoming request.
|
||||
limit (int): Page size. Defaults to 20.
|
||||
offset (int): Items to skip. Defaults to 0.
|
||||
|
||||
Returns:
|
||||
dict: ``{"total": int, "items": list[Job]}``.
|
||||
"""
|
||||
docs = await svc(request, "job").search({})
|
||||
return paginate([dump(d) for d in docs], limit, offset)
|
||||
|
||||
|
||||
async def create_job(request: Request, body: dict):
|
||||
"""Create a job from the request body."""
|
||||
data = body.model_dump() if hasattr(body, "model_dump") else dict(body)
|
||||
doc = await svc(request).create(Job(**data))
|
||||
return JSONResponse(status_code=201, content=doc.model_dump(mode="json", by_alias=True))
|
||||
async def create_job(request: Request, body: dict, response: Response) -> dict:
|
||||
"""Create a job.
|
||||
|
||||
Args:
|
||||
request (Request): Incoming request.
|
||||
body (dict): Job fields (``id`` ignored).
|
||||
response (Response): Response object; set to 201 on success.
|
||||
|
||||
Returns:
|
||||
dict: The created job.
|
||||
"""
|
||||
response.status_code = 201
|
||||
body.pop("id", None)
|
||||
doc = await svc(request, "job").create(body)
|
||||
return dump(doc)
|
||||
|
||||
|
||||
async def get_job(request: Request, id: str):
|
||||
"""Fetch one job by id."""
|
||||
doc = await svc(request).read(id)
|
||||
if doc is None:
|
||||
return JSONResponse(status_code=404, content={"detail": "job not found"})
|
||||
return doc.model_dump(mode="json", by_alias=True)
|
||||
async def get_job(request: Request, id: int) -> dict:
|
||||
"""Fetch one job by id.
|
||||
|
||||
Args:
|
||||
request (Request): Incoming request.
|
||||
id (int): Job identifier.
|
||||
|
||||
Returns:
|
||||
dict: The job document.
|
||||
|
||||
Raises:
|
||||
HTTPException: 404 when the job does not exist.
|
||||
"""
|
||||
return dump(await read_or_404(svc(request, "job"), id, "job"))
|
||||
|
||||
|
||||
async def list_jobs(request: Request):
|
||||
"""List all jobs."""
|
||||
docs = await svc(request).search({})
|
||||
return [d.model_dump(mode="json", by_alias=True) for d in docs]
|
||||
async def update_job(request: Request, id: int, body: dict) -> dict:
|
||||
"""Replace a job by id.
|
||||
|
||||
Args:
|
||||
request (Request): Incoming request.
|
||||
id (int): Job identifier.
|
||||
body (dict): Full replacement payload.
|
||||
|
||||
Returns:
|
||||
dict: The updated job.
|
||||
|
||||
Raises:
|
||||
HTTPException: 404 when the job does not exist.
|
||||
"""
|
||||
await read_or_404(svc(request, "job"), id, "job")
|
||||
body.pop("id", None)
|
||||
doc = await svc(request, "job").update(id, body)
|
||||
return dump(doc)
|
||||
|
||||
|
||||
async def delete_job(request: Request, id: int, response: Response) -> None:
|
||||
"""Delete a job by id.
|
||||
|
||||
Args:
|
||||
request (Request): Incoming request.
|
||||
id (int): Job identifier.
|
||||
response (Response): Response object; set to 204 on success.
|
||||
|
||||
Raises:
|
||||
HTTPException: 404 when the job does not exist.
|
||||
"""
|
||||
await delete_or_404(svc(request, "job"), id, "job")
|
||||
response.status_code = 204
|
||||
|
||||
@@ -6,4 +6,5 @@ re-exports every route handler from the subpackages into one flat namespace.
|
||||
"""
|
||||
# ruff: noqa: F403
|
||||
from interfaces.item.routes import *
|
||||
from interfaces.job.routes import *
|
||||
from interfaces.job.routes import *
|
||||
from interfaces.system.routes import *
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
"""# Summary
|
||||
|
||||
System interfaces."""
|
||||
13
standards/mcp/templates/app/interfaces/system/routes.py
Normal file
13
standards/mcp/templates/app/interfaces/system/routes.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""# Summary
|
||||
|
||||
System route handlers — health probe and other non-resource endpoints.
|
||||
"""
|
||||
|
||||
|
||||
async def health() -> dict:
|
||||
"""Liveness probe — public, unauthenticated.
|
||||
|
||||
Returns:
|
||||
dict: ``{"status": "ok", "service": "app-template"}``.
|
||||
"""
|
||||
return {"status": "ok", "service": "app-template"}
|
||||
Reference in New Issue
Block a user