- 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
173 lines
5.2 KiB
Python
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")
|