Phase 7: serve the vet sample from the template tools and docs

- build.py: _TEMPLATE_COMPONENTS rewritten around the vet sample (53 kinds —
  per-resource slices, jobs, worked appointment_reminder family, tests);
  seed tokens dropped from the manifest ('sample: vet' instead)
- registry.py: get_app_component(kind) no longer renames item/example seeds;
  prompts and tool docs updated to 'copy a slice and adapt'
- templates/README.md, 01_build_recipe, 40_ui_admin, wiki/03 updated off the
  old seed scheme; repo inventory/config now exclude vet (docs move under
  standards; react/vet itself is deleted in a later phase)
- regenerated MCP bundle, vendored templates, and site output
This commit is contained in:
2026-10-20 14:58:17 +05:30
parent 56aa1322fb
commit 0cb35d10a6
107 changed files with 3292 additions and 937 deletions

View File

@@ -0,0 +1,3 @@
"""# Summary
Appointments interfaces."""

View File

@@ -0,0 +1,135 @@
"""# Summary
Appointments route handlers — names must match openapi.yaml operationIds.
Notes:
``pet``, ``vet`` and ``treatment`` are foreign keys expanded to full
objects on read and normalized from ids on write. The list endpoint
supports date-prefix and pet/vet id filters.
"""
from fastapi import Request, Response
from interfaces._common import (
delete_or_404,
dump,
expand_ref,
paginate,
read_or_404,
svc,
)
async def list_appointments(
request: Request,
limit: int = 20,
offset: int = 0,
date: str | None = None,
vet: int | None = None,
pet: int | None = None,
) -> dict:
"""List appointments (paginated, filterable by date/vet/pet).
Args:
request (Request): Incoming request.
limit (int): Page size. Defaults to 20.
offset (int): Items to skip. Defaults to 0.
date (str | None): ISO date prefix to match (e.g. ``2025-06-01``).
vet (int | None): Filter by embedded vet id.
pet (int | None): Filter by embedded pet id.
Returns:
dict: ``{"total": int, "items": list[Appointment]}``.
"""
docs = await svc(request, "appointment").search({})
items = []
for d in docs:
row = dump(d)
if date and not str(row.get("date", "")).startswith(date):
continue
if vet is not None and (row.get("vet") or {}).get("id") != vet:
continue
if pet is not None and (row.get("pet") or {}).get("id") != pet:
continue
items.append(row)
return paginate(items, limit, offset)
async def create_appointment(request: Request, body: dict, response: Response) -> dict:
"""Create an appointment; FK fields may be bare ids or expanded objects.
Args:
request (Request): Incoming request.
body (dict): Appointment fields (``id`` ignored; FKs normalized).
response (Response): Response object; set to 201 on success.
Returns:
dict: The created appointment with FKs expanded.
Raises:
HTTPException: 422 when an FK reference cannot be resolved.
"""
response.status_code = 201
body.pop("id", None)
body["pet"] = await expand_ref(svc(request, "pet"), body.get("pet"), "pet")
body["vet"] = await expand_ref(svc(request, "vet"), body.get("vet"), "vet")
body["treatment"] = await expand_ref(
svc(request, "treatment"), body.get("treatment"), "treatment"
)
doc = await svc(request, "appointment").create(body)
return dump(doc)
async def get_appointment(request: Request, id: int) -> dict:
"""Fetch one appointment by id.
Args:
request (Request): Incoming request.
id (int): Appointment identifier.
Returns:
dict: The appointment document.
Raises:
HTTPException: 404 when the appointment does not exist.
"""
return dump(await read_or_404(svc(request, "appointment"), id, "appointment"))
async def update_appointment(request: Request, id: int, body: dict) -> dict:
"""Replace an appointment by id; FK fields may be ids or objects.
Args:
request (Request): Incoming request.
id (int): Appointment identifier.
body (dict): Full replacement payload.
Returns:
dict: The updated appointment with FKs expanded.
Raises:
HTTPException: 404 when the appointment does not exist; 422 on bad FKs.
"""
await read_or_404(svc(request, "appointment"), id, "appointment")
body.pop("id", None)
body["pet"] = await expand_ref(svc(request, "pet"), body.get("pet"), "pet")
body["vet"] = await expand_ref(svc(request, "vet"), body.get("vet"), "vet")
body["treatment"] = await expand_ref(
svc(request, "treatment"), body.get("treatment"), "treatment"
)
doc = await svc(request, "appointment").update(id, body)
return dump(doc)
async def delete_appointment(request: Request, id: int, response: Response) -> None:
"""Delete an appointment by id.
Args:
request (Request): Incoming request.
id (int): Appointment identifier.
response (Response): Response object; set to 204 on success.
Raises:
HTTPException: 404 when the appointment does not exist.
"""
await delete_or_404(svc(request, "appointment"), id, "appointment")
response.status_code = 204

View File

@@ -1,90 +0,0 @@
"""# Summary
Item route handlers — names must match openapi.yaml operationIds.
"""
from fastapi import Request, Response
from interfaces._common import delete_or_404, dump, paginate, read_or_404, 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, 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: 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 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: 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

View File

@@ -1,25 +1,29 @@
"""# Summary
Job route handlers — names must match openapi.yaml operationIds.
Jobs route handlers — names must match openapi.yaml operationIds.
"""
from fastapi import Request, Response
from interfaces._common import delete_or_404, dump, paginate, read_or_404, svc
async def list_jobs(request: Request, limit: int = 20, offset: int = 0) -> dict:
"""List jobs (paginated). ``limit=0`` returns all remaining items.
async def list_jobs(request: Request, limit: int = 20, offset: int = 0, status: str | None = None) -> dict:
"""List jobs (paginated, optionally filtered by status).
Args:
request (Request): Incoming request.
limit (int): Page size. Defaults to 20.
offset (int): Items to skip. Defaults to 0.
status (str | None): Filter by status (pending/processing/done/failed).
Returns:
dict: ``{"total": int, "items": list[Job]}``.
"""
docs = await svc(request, "job").search({})
return paginate([dump(d) for d in docs], limit, offset)
items = [dump(d) for d in docs]
if status:
items = [row for row in items if row.get("status") == status]
return paginate(items, limit, offset)
async def create_job(request: Request, body: dict, response: Response) -> dict:

View File

@@ -5,6 +5,10 @@ OpenAPI-first router resolves each operation to a handler by name, so this modul
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.appointments.routes import *
from interfaces.jobs.routes import *
from interfaces.parents.routes import *
from interfaces.pets.routes import *
from interfaces.system.routes import *
from interfaces.treatments.routes import *
from interfaces.vets.routes import *

View File

@@ -0,0 +1,3 @@
"""# Summary
Parents interfaces."""

View File

@@ -0,0 +1,90 @@
"""# Summary
Parents route handlers — names must match openapi.yaml operationIds.
"""
from fastapi import Request, Response
from interfaces._common import delete_or_404, dump, paginate, read_or_404, svc
async def list_parents(request: Request, limit: int = 20, offset: int = 0) -> dict:
"""List parents (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[Parent]}``.
"""
docs = await svc(request, "parent").search({})
return paginate([dump(d) for d in docs], limit, offset)
async def create_parent(request: Request, body: dict, response: Response) -> dict:
"""Create a parent.
Args:
request (Request): Incoming request.
body (dict): Parent fields (``id`` ignored).
response (Response): Response object; set to 201 on success.
Returns:
dict: The created parent.
"""
response.status_code = 201
body.pop("id", None)
doc = await svc(request, "parent").create(body)
return dump(doc)
async def get_parent(request: Request, id: int) -> dict:
"""Fetch one parent by id.
Args:
request (Request): Incoming request.
id (int): Parent identifier.
Returns:
dict: The parent document.
Raises:
HTTPException: 404 when the parent does not exist.
"""
return dump(await read_or_404(svc(request, "parent"), id, "parent"))
async def update_parent(request: Request, id: int, body: dict) -> dict:
"""Replace a parent by id.
Args:
request (Request): Incoming request.
id (int): Parent identifier.
body (dict): Full replacement payload.
Returns:
dict: The updated parent.
Raises:
HTTPException: 404 when the parent does not exist.
"""
await read_or_404(svc(request, "parent"), id, "parent")
body.pop("id", None)
doc = await svc(request, "parent").update(id, body)
return dump(doc)
async def delete_parent(request: Request, id: int, response: Response) -> None:
"""Delete a parent by id.
Args:
request (Request): Incoming request.
id (int): Parent identifier.
response (Response): Response object; set to 204 on success.
Raises:
HTTPException: 404 when the parent does not exist.
"""
await delete_or_404(svc(request, "parent"), id, "parent")
response.status_code = 204

View File

@@ -0,0 +1,3 @@
"""# Summary
Pets interfaces."""

View File

@@ -0,0 +1,172 @@
"""# Summary
Pets route handlers — names must match openapi.yaml operationIds.
Notes:
``parents`` is a list foreign key expanded to full objects on read and
normalized from ids on write. ``upload_pet_photo`` stores the raw bytes
under ``settings.uploads_dir``; ``stream_actions`` returns an SSE stream
scoped to the pet's species.
"""
import os
from collections.abc import AsyncIterator
from fastapi import Request, Response, UploadFile
from fastapi.responses import StreamingResponse
from config import settings
from infra.adapters import actions_stream
from interfaces._common import (
delete_or_404,
dump,
expand_ref_list,
paginate,
read_or_404,
svc,
)
async def list_pets(request: Request, limit: int = 20, offset: int = 0) -> dict:
"""List pets (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[Pet]}``.
"""
docs = await svc(request, "pet").search({})
return paginate([dump(d) for d in docs], limit, offset)
async def create_pet(request: Request, body: dict, response: Response) -> dict:
"""Create a pet; parents may be bare ids or expanded objects.
Args:
request (Request): Incoming request.
body (dict): Pet fields (``id`` ignored; ``parents`` normalized).
response (Response): Response object; set to 201 on success.
Returns:
dict: The created pet with parents expanded.
Raises:
HTTPException: 422 when a parent reference cannot be resolved.
"""
response.status_code = 201
body.pop("id", None)
body["parents"] = await expand_ref_list(
svc(request, "parent"), body.get("parents"), "parent"
)
doc = await svc(request, "pet").create(body)
return dump(doc)
async def get_pet(request: Request, id: int) -> dict:
"""Fetch one pet by id.
Args:
request (Request): Incoming request.
id (int): Pet identifier.
Returns:
dict: The pet document.
Raises:
HTTPException: 404 when the pet does not exist.
"""
return dump(await read_or_404(svc(request, "pet"), id, "pet"))
async def update_pet(request: Request, id: int, body: dict) -> dict:
"""Replace a pet by id; parents may be bare ids or expanded objects.
Args:
request (Request): Incoming request.
id (int): Pet identifier.
body (dict): Full replacement payload.
Returns:
dict: The updated pet with parents expanded.
Raises:
HTTPException: 404 when the pet does not exist; 422 on a bad parent ref.
"""
await read_or_404(svc(request, "pet"), id, "pet")
body.pop("id", None)
body["parents"] = await expand_ref_list(
svc(request, "parent"), body.get("parents"), "parent"
)
doc = await svc(request, "pet").update(id, body)
return dump(doc)
async def delete_pet(request: Request, id: int, response: Response) -> None:
"""Delete a pet by id.
Args:
request (Request): Incoming request.
id (int): Pet identifier.
response (Response): Response object; set to 204 on success.
Raises:
HTTPException: 404 when the pet does not exist.
"""
await delete_or_404(svc(request, "pet"), id, "pet")
response.status_code = 204
async def upload_pet_photo(request: Request, id: int, file: UploadFile) -> dict:
"""Store an uploaded photo for a pet and remember its path.
Args:
request (Request): Incoming request.
id (int): Pet identifier.
file (UploadFile): Multipart upload carrying the photo bytes.
Returns:
dict: ``{"id": int, "status": "photo_uploaded", "path": str}``.
Raises:
HTTPException: 404 when the pet does not exist.
"""
pet = await read_or_404(svc(request, "pet"), id, "pet")
os.makedirs(settings.uploads_dir, exist_ok=True)
safe_name = os.path.basename(file.filename or "photo")
path = os.path.join(settings.uploads_dir, f"{id}_{safe_name}")
content = await file.read()
with open(path, "wb") as fh:
fh.write(content)
await svc(request, "pet").update(id, {"photo": path, "parents": pet.parents})
return {"id": id, "status": "photo_uploaded", "path": path}
async def stream_actions(request: Request, id: int) -> StreamingResponse:
"""Stream random, species-appropriate pet actions over SSE.
Args:
request (Request): Incoming request.
id (int): Pet identifier whose species scopes the stream.
Returns:
StreamingResponse: ``text/event-stream`` emitting ``data: {action}``.
Raises:
HTTPException: 404 when the pet does not exist.
"""
pet = await read_or_404(svc(request, "pet"), id, "pet")
async def event_gen() -> AsyncIterator[str]:
queue = await actions_stream.subscribe(id, pet.species)
try:
while True:
if await request.is_disconnected():
break
data = await queue.get()
yield f"data: {data}\n\n"
finally:
actions_stream.unsubscribe(id, queue)
return StreamingResponse(event_gen(), media_type="text/event-stream")

View File

@@ -8,6 +8,6 @@ async def health() -> dict:
"""Liveness probe — public, unauthenticated.
Returns:
dict: ``{"status": "ok", "service": "app-template"}``.
dict: ``{"status": "ok", "service": "vet-server"}``.
"""
return {"status": "ok", "service": "app-template"}
return {"status": "ok", "service": "vet-server"}

View File

@@ -0,0 +1,3 @@
"""# Summary
Treatments interfaces."""

View File

@@ -0,0 +1,92 @@
"""# Summary
Treatments route handlers — names must match openapi.yaml operationIds.
Notes:
``list_treatments`` returns a plain array (the catalogue is small and
unpaginated by contract).
"""
from fastapi import Request, Response
from interfaces._common import delete_or_404, dump, read_or_404, svc
async def list_treatments(request: Request) -> list[dict]:
"""List treatments (catalogue, unpaginated).
Args:
request (Request): Incoming request.
Returns:
list[dict]: All treatment documents.
"""
docs = await svc(request, "treatment").search({})
return [dump(d) for d in docs]
async def create_treatment(request: Request, body: dict, response: Response) -> dict:
"""Add a treatment (admin only).
Args:
request (Request): Incoming request.
body (dict): Treatment fields (``id`` ignored).
response (Response): Response object; set to 201 on success.
Returns:
dict: The created treatment.
"""
response.status_code = 201
body.pop("id", None)
doc = await svc(request, "treatment").create(body)
return dump(doc)
async def get_treatment(request: Request, id: int) -> dict:
"""Fetch one treatment by id.
Args:
request (Request): Incoming request.
id (int): Treatment identifier.
Returns:
dict: The treatment document.
Raises:
HTTPException: 404 when the treatment does not exist.
"""
return dump(await read_or_404(svc(request, "treatment"), id, "treatment"))
async def update_treatment(request: Request, id: int, body: dict) -> dict:
"""Replace a treatment by id.
Args:
request (Request): Incoming request.
id (int): Treatment identifier.
body (dict): Full replacement payload.
Returns:
dict: The updated treatment.
Raises:
HTTPException: 404 when the treatment does not exist.
"""
await read_or_404(svc(request, "treatment"), id, "treatment")
body.pop("id", None)
doc = await svc(request, "treatment").update(id, body)
return dump(doc)
async def delete_treatment(request: Request, id: int, response: Response) -> None:
"""Delete a treatment by id.
Args:
request (Request): Incoming request.
id (int): Treatment identifier.
response (Response): Response object; set to 204 on success.
Raises:
HTTPException: 404 when the treatment does not exist.
"""
await delete_or_404(svc(request, "treatment"), id, "treatment")
response.status_code = 204

View File

@@ -0,0 +1,3 @@
"""# Summary
Vets interfaces."""

View File

@@ -0,0 +1,90 @@
"""# Summary
Vets route handlers — names must match openapi.yaml operationIds.
"""
from fastapi import Request, Response
from interfaces._common import delete_or_404, dump, paginate, read_or_404, svc
async def list_vets(request: Request, limit: int = 20, offset: int = 0) -> dict:
"""List vets (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[Vet]}``.
"""
docs = await svc(request, "vet").search({})
return paginate([dump(d) for d in docs], limit, offset)
async def create_vet(request: Request, body: dict, response: Response) -> dict:
"""Create a vet.
Args:
request (Request): Incoming request.
body (dict): Vet fields (``id`` ignored).
response (Response): Response object; set to 201 on success.
Returns:
dict: The created vet.
"""
response.status_code = 201
body.pop("id", None)
doc = await svc(request, "vet").create(body)
return dump(doc)
async def get_vet(request: Request, id: int) -> dict:
"""Fetch one vet by id.
Args:
request (Request): Incoming request.
id (int): Vet identifier.
Returns:
dict: The vet document.
Raises:
HTTPException: 404 when the vet does not exist.
"""
return dump(await read_or_404(svc(request, "vet"), id, "vet"))
async def update_vet(request: Request, id: int, body: dict) -> dict:
"""Replace a vet by id.
Args:
request (Request): Incoming request.
id (int): Vet identifier.
body (dict): Full replacement payload.
Returns:
dict: The updated vet.
Raises:
HTTPException: 404 when the vet does not exist.
"""
await read_or_404(svc(request, "vet"), id, "vet")
body.pop("id", None)
doc = await svc(request, "vet").update(id, body)
return dump(doc)
async def delete_vet(request: Request, id: int, response: Response) -> None:
"""Delete a vet by id.
Args:
request (Request): Incoming request.
id (int): Vet identifier.
response (Response): Response object; set to 204 on success.
Raises:
HTTPException: 404 when the vet does not exist.
"""
await delete_or_404(svc(request, "vet"), id, "vet")
response.status_code = 204