Persons api extraction
This commit is contained in:
parent
edfc341b4b
commit
2bbed57313
3 changed files with 81 additions and 96 deletions
|
|
@ -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
88
main.py
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue