Persons api extraction

This commit is contained in:
jableader 2025-10-18 17:00:01 +11:00
parent edfc341b4b
commit 2bbed57313
3 changed files with 81 additions and 96 deletions

View file

@ -6,25 +6,93 @@ import aiosqlite
from fastapi import APIRouter, Depends, Query, Request from fastapi import APIRouter, Depends, Query, Request
import persons import persons
from common import Page, ProblemDetails from common import Page
from main import get_db # temporary during extraction from main import get_db
router = APIRouter(prefix="/persons", tags=["persons"]) 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( 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)."), q: Optional[str] = Query(
cursor: Optional[str] = Query(default=None, description="Opaque cursor for pagination. Pass the value returned in nextCursor to fetch the next page."), default=None,
limit: int = Query(50, ge=1, le=200, description="Maximum number of items to return (1-200)."), 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), conn: aiosqlite.Connection = Depends(get_db),
request: Request | None = None, request: Request | None = None,
) -> Page[persons.Person]: ) -> 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( async def create_person(
person: persons.Person, conn: aiosqlite.Connection = Depends(get_db) person: persons.Person, conn: aiosqlite.Connection = Depends(get_db)
) -> persons.Person: ) -> persons.Person:
raise NotImplementedError("create_person extraction pending") await persons.insert_person(conn, person)
await conn.commit()
return person

88
main.py
View file

@ -620,92 +620,7 @@ async def unrequest_meal(
return {} return {}
@api_v1.get( from api import persons as persons_router # type: ignore
"/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
class LoginBody(ApiModel): class LoginBody(ApiModel):
@ -809,6 +724,7 @@ async def request_validation_exc_handler(request: Request, exc: RequestValidatio
# Mount versioned API router # Mount versioned API router
app.include_router(api_v1, prefix="/api/v1", tags=["v1"]) 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(recipes_router.router, prefix="/api/v1", tags=["v1"]) # extracted
app.include_router(persons_router.router, prefix="/api/v1", tags=["v1"]) # extracted
@app.get("/healthz") @app.get("/healthz")

View file

@ -33,7 +33,7 @@ Acceptance criteria
- [ ] Extract routers by feature - [ ] Extract routers by feature
- [x] api/recipes.py - [x] api/recipes.py
- [ ] api/meals.py - [ ] api/meals.py
- [ ] api/persons.py - [x] api/persons.py
- [ ] api/shopping.py - [ ] api/shopping.py
- [ ] api/auth.py - [ ] api/auth.py
- [ ] Wire routers in main with minimal app code - [ ] 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: 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: 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 recipes routes to api/recipes.py and wired router; added /healthz
- 2025-10-18: Extracted persons routes to api/persons.py and wired router
--- ---