- [x] Wire routers in main with minimal app code

- [x] Create api/deps module for get_db, cookie_person, and error_response
- [x] Use settings.py (DOOF_DB) for DB path in main and deps
This commit is contained in:
jableader 2025-10-18 17:14:03 +11:00
parent 53b07343d1
commit f51c90f922
7 changed files with 57 additions and 38 deletions

View file

@ -7,7 +7,7 @@ from fastapi.encoders import jsonable_encoder
import persons import persons
from common import ProblemDetails, ApiModel from common import ProblemDetails, ApiModel
from main import get_db, cookie_person, error_response from api.deps import get_db, cookie_person, error_response
router = APIRouter(prefix="/auth", tags=["auth"]) router = APIRouter(prefix="/auth", tags=["auth"])

41
api/deps.py Normal file
View file

@ -0,0 +1,41 @@
from __future__ import annotations
from typing import Annotated, Optional, AsyncGenerator
import aiosqlite
from fastapi import Cookie, Depends, Request
from fastapi.responses import JSONResponse
import db
import persons
from common import ProblemDetails
from settings import settings
# Dependency to create SQLite connection
async def get_db() -> AsyncGenerator[aiosqlite.Connection, None]:
sql_db = await db.connect(settings.database_path)
try:
yield sql_db
finally:
await sql_db.close()
async def cookie_person(
user_id: Annotated[int, Cookie(alias="user_id")], conn: aiosqlite.Connection = Depends(get_db)
) -> Optional[persons.Person]:
return await persons.get_by_id(conn, user_id)
def error_response(request: Optional[Request], status_code: int, message: str) -> JSONResponse:
body = ProblemDetails(
title=message,
status=status_code,
type=f"https://httpstatuses.com/{status_code}",
instance=str(request.url) if request else None,
)
return JSONResponse(
content=body.model_dump(by_alias=True),
status_code=status_code,
media_type="application/problem+json",
)

View file

@ -10,7 +10,7 @@ import meals
import persons import persons
import shopping import shopping
from common import ProblemDetails from common import ProblemDetails
from main import get_db, cookie_person, error_response # temporary imports during extraction from api.deps import get_db, cookie_person, error_response
from common import ApiModel, Field from common import ApiModel, Field
import datetime import datetime
from typing import Dict from typing import Dict

View file

@ -10,7 +10,7 @@ import recipes
import persons import persons
from common import Page, ProblemDetails, ApiModel from common import Page, ProblemDetails, ApiModel
from pydantic import Field from pydantic import Field
from main import get_db, cookie_person, error_response from api.deps import get_db, cookie_person, error_response
router = APIRouter(prefix="/recipes", tags=["recipes"]) router = APIRouter(prefix="/recipes", tags=["recipes"])

View file

@ -11,7 +11,7 @@ import persons
import recipes import recipes
import shopping import shopping
from common import ProblemDetails from common import ProblemDetails
from main import get_db, cookie_person, error_response # temporary during extraction from api.deps import get_db, cookie_person, error_response
router = APIRouter(prefix="/shopping", tags=["shopping"]) router = APIRouter(prefix="/shopping", tags=["shopping"])

32
main.py
View file

@ -18,6 +18,8 @@ import shopping
from fastapi.routing import APIRoute from fastapi.routing import APIRoute
from common import ProblemDetails, Page, ApiModel from common import ProblemDetails, Page, ApiModel
from settings import settings
from api.deps import get_db, cookie_person, error_response
class CamelCaseRoute(APIRoute): class CamelCaseRoute(APIRoute):
@ -29,35 +31,9 @@ class CamelCaseRoute(APIRoute):
app = FastAPI(title="Doof API", version="1.0.0", description="Doof Backend API") app = FastAPI(title="Doof API", version="1.0.0", description="Doof Backend API")
api_v1 = APIRouter(route_class=CamelCaseRoute) api_v1 = APIRouter(route_class=CamelCaseRoute)
DATABASE_PATH = os.environ.get("DOOF_DB", "./data/doof.sqlite") DATABASE_PATH = settings.database_path
# Dependency to create SQLite connection # get_db, cookie_person, and error_response are imported from api.deps
async def get_db():
sql_db = await db.connect(DATABASE_PATH)
try:
yield sql_db
finally:
await sql_db.close()
async def cookie_person(
user_id: Annotated[int, Cookie(alias="user_id")], conn: aiosqlite.Connection = Depends(get_db)
) -> Optional[persons.Person]:
return await persons.get_by_id(conn, user_id)
def error_response(request: Optional[Request], status_code: int, message: str) -> JSONResponse:
body = ProblemDetails(
title=message,
status=status_code,
type=f"https://httpstatuses.com/{status_code}",
instance=str(request.url) if request else None,
)
return JSONResponse(
content=body.model_dump(by_alias=True),
status_code=status_code,
media_type="application/problem+json",
)
# OpenAPI reusable responses for ProblemDetails # OpenAPI reusable responses for ProblemDetails

View file

@ -32,14 +32,15 @@ Acceptance criteria
## Phase 1 — API structure and lifecycle ## Phase 1 — API structure and lifecycle
- [ ] Extract routers by feature - [ ] Extract routers by feature
- [x] api/recipes.py - [x] api/recipes.py
- [ ] api/meals.py
- [x] api/meals.py - [x] api/meals.py
- [x] api/persons.py - [x] api/persons.py
- [x] api/shopping.py - [x] api/shopping.py
- [x] api/auth.py - [x] api/auth.py
- [ ] Wire routers in main with minimal app code - [x] Wire routers in main with minimal app code
- [ ] Move dev reverse proxy setup into a lifespan handler and ensure httpx client is closed - [ ] Move dev reverse proxy setup into a lifespan handler and ensure httpx client is closed
- [x] Add /healthz endpoint (simple JSON: {"status": "ok"}) - [x] Add /healthz endpoint (simple JSON: {"status": "ok"})
- [x] Create api/deps module for get_db, cookie_person, and error_response
- [x] Use settings.py (DOOF_DB) for DB path in main and deps
Acceptance criteria Acceptance criteria
- main.py primarily wires app, routers, settings, and lifespan - main.py primarily wires app, routers, settings, and lifespan
@ -146,14 +147,15 @@ Note: We can adopt this structure gradually without moving DB code immediately;
- 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 - 2025-10-18: Extracted persons routes to api/persons.py and wired router
- 2025-10-18: Extracted meals, shopping, and auth routes; created api/deps and switched DB path to settings
--- ---
## Next actions ## Next actions
Begin Phase 1 work in small steps: - Phase 1: Move dev reverse proxy into a lifespan handler (startup/shutdown) and close AsyncClient cleanly
- Create api package and extract the first router (e.g., recipes) without logic changes - Phase 2: Add OpenAPI cookie security scheme and normalize 201 Created + Location
- Wire the router in main.py and run tests - Phase 3: Plan DB PRAGMAs and indexes; add transaction scoping per request
- Prepare lifespan handler for dev reverse proxy, but keep behavior identical - Phase 5: Add fixtures for DB/auth and tests for health + 201 Location
### Health endpoint plan (Phase 1 target) ### Health endpoint plan (Phase 1 target)
- Path: GET /healthz - Path: GET /healthz