diff --git a/api/persons.py b/api/persons.py index 15a1f2d..297a40e 100644 --- a/api/persons.py +++ b/api/persons.py @@ -6,25 +6,93 @@ import aiosqlite from fastapi import APIRouter, Depends, Query, Request import persons -from common import Page, ProblemDetails -from main import get_db # temporary during extraction +from common import Page +from main import get_db router = APIRouter(prefix="/persons", tags=["persons"]) -@router.get("", response_model=Page[persons.Person], operation_id="listPersons", summary="List persons (paginated)") +@router.get( + "", + operation_id="listPersons", + response_model=Page[persons.Person], + summary="List persons (paginated)", + responses={ + 200: { + "description": "A page of persons", + "content": { + "application/json": { + "example": { + "items": [ + { + "id": 1, + "name": "Ada Lovelace" + } + ], + "nextCursor": "2", + "prevCursor": "0", + "total": 1 + } + } + }, + } + }, +) async def list_persons( - q: Optional[str] = Query(default=None, description="Optional case-insensitive name filter (applied after page read; may be pushed to SQL in the future)."), - cursor: Optional[str] = Query(default=None, description="Opaque cursor for pagination. Pass the value returned in nextCursor to fetch the next page."), - limit: int = Query(50, ge=1, le=200, description="Maximum number of items to return (1-200)."), + q: Optional[str] = Query( + default=None, + description="Optional case-insensitive name filter (applied after page read; may be pushed to SQL in the future).", + ), + cursor: Optional[str] = Query( + default=None, + description="Opaque cursor for pagination. Pass the value returned in nextCursor to fetch the next page.", + ), + limit: int = Query( + 50, + ge=1, + le=200, + description="Maximum number of items to return (1-200).", + ), conn: aiosqlite.Connection = Depends(get_db), request: Request | None = None, ) -> Page[persons.Person]: - raise NotImplementedError("list_persons extraction pending") + # v1: DB-backed pagination + last_id = None + if cursor: + try: + last_id = int(cursor) + except ValueError: + last_id = None + + fetch_limit = limit + 1 + paged: List[persons.Person] = [] + if q: + async for p in persons.search_by_name_paged(conn, q, last_id, fetch_limit): + paged.append(p) + else: + async for p in persons.get_all_paged(conn, last_id, fetch_limit): + paged.append(p) + + has_more = len(paged) > limit + items = paged[:limit] + next_cursor = str(items[-1].id) if has_more and items else None + # Compute prevCursor via DB helper + prev_cursor: Optional[str] = None + if items: + first_id = items[0].id + prev_cursor = await persons.compute_prev_cursor(conn, first_id, limit, q) + total = await (persons.count_by_name(conn, q) if q else persons.count_all(conn)) + return Page(items=items, nextCursor=next_cursor, prevCursor=prev_cursor, total=total) -@router.post("", response_model=persons.Person, operation_id="createPerson", summary="Create a person") +@router.post( + "", + operation_id="createPerson", + summary="Create a person", +) async def create_person( person: persons.Person, conn: aiosqlite.Connection = Depends(get_db) ) -> persons.Person: - raise NotImplementedError("create_person extraction pending") + await persons.insert_person(conn, person) + await conn.commit() + return person diff --git a/main.py b/main.py index 6778ee2..997066a 100644 --- a/main.py +++ b/main.py @@ -620,92 +620,7 @@ async def unrequest_meal( return {} -@api_v1.get( - "/persons", - operation_id="listPersons", - response_model=Page[persons.Person], - tags=["persons"], - summary="List persons (paginated)", - responses={ - 200: { - "description": "A page of persons", - "content": { - "application/json": { - "example": { - "items": [ - { - "id": 1, - "name": "Ada Lovelace" - } - ], - "nextCursor": "2", - "prevCursor": "0", - "total": 1 - } - } - }, - } - }, -) -async def get_persons( - q: Optional[str] = Query( - default=None, - description="Optional case-insensitive name filter (applied after page read; may be pushed to SQL in the future).", - ), - cursor: Optional[str] = Query( - default=None, - description="Opaque cursor for pagination. Pass the value returned in nextCursor to fetch the next page.", - ), - limit: int = Query( - 50, - ge=1, - le=200, - description="Maximum number of items to return (1-200).", - ), - conn: aiosqlite.Connection = Depends(get_db), - request: Request = None, -) -> List[persons.Person] | Page[persons.Person]: - # v1: DB-backed pagination - last_id = None - if cursor: - try: - last_id = int(cursor) - except ValueError: - last_id = None - - fetch_limit = limit + 1 - paged: List[persons.Person] = [] - if q: - async for p in persons.search_by_name_paged(conn, q, last_id, fetch_limit): - paged.append(p) - else: - async for p in persons.get_all_paged(conn, last_id, fetch_limit): - paged.append(p) - - has_more = len(paged) > limit - items = paged[:limit] - next_cursor = str(items[-1].id) if has_more and items else None - # Compute prevCursor via DB helper - prev_cursor: Optional[str] = None - if items: - first_id = items[0].id - prev_cursor = await persons.compute_prev_cursor(conn, first_id, limit, q) - total = await (persons.count_by_name(conn, q) if q else persons.count_all(conn)) - return Page(items=items, nextCursor=next_cursor, prevCursor=prev_cursor, total=total) - - -@api_v1.post( - "/persons", - operation_id="createPerson", - tags=["persons"], - summary="Create a person", -) -async def create_person( - person: persons.Person, conn: aiosqlite.Connection = Depends(get_db) -) -> persons.Person: - await persons.insert_person(conn, person) - await conn.commit() - return person +from api import persons as persons_router # type: ignore class LoginBody(ApiModel): @@ -809,6 +724,7 @@ async def request_validation_exc_handler(request: Request, exc: RequestValidatio # Mount versioned API router app.include_router(api_v1, prefix="/api/v1", tags=["v1"]) app.include_router(recipes_router.router, prefix="/api/v1", tags=["v1"]) # extracted +app.include_router(persons_router.router, prefix="/api/v1", tags=["v1"]) # extracted @app.get("/healthz") diff --git a/refactor-project-strategy.md b/refactor-project-strategy.md index c72c1a9..a616796 100644 --- a/refactor-project-strategy.md +++ b/refactor-project-strategy.md @@ -33,7 +33,7 @@ Acceptance criteria - [ ] Extract routers by feature - [x] api/recipes.py - [ ] api/meals.py - - [ ] api/persons.py + - [x] api/persons.py - [ ] api/shopping.py - [ ] api/auth.py - [ ] Wire routers in main with minimal app code @@ -144,6 +144,7 @@ Note: We can adopt this structure gradually without moving DB code immediately; - 2025-10-18: Finalized router layout and health endpoint plan — Phase 0 complete - 2025-10-18: Created api package and scaffolded routers (recipes, meals, persons, shopping, auth) with placeholders - 2025-10-18: Extracted recipes routes to api/recipes.py and wired router; added /healthz +- 2025-10-18: Extracted persons routes to api/persons.py and wired router ---