Files
docs/standards/mcp/templates/app/interfaces/pets/routes.py
Vishesh 'ironeagle' Bangotra 0cb35d10a6 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
2026-10-20 14:58:17 +05:30

173 lines
5.2 KiB
Python

"""# 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")