diff --git a/api/auth.py b/api/auth.py index 402f3ba..42cb365 100644 --- a/api/auth.py +++ b/api/auth.py @@ -7,7 +7,7 @@ from fastapi.encoders import jsonable_encoder import persons 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"]) diff --git a/api/deps.py b/api/deps.py new file mode 100644 index 0000000..103d616 --- /dev/null +++ b/api/deps.py @@ -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", + ) diff --git a/api/meals.py b/api/meals.py index a141627..f675442 100644 --- a/api/meals.py +++ b/api/meals.py @@ -10,7 +10,7 @@ import meals import persons import shopping 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 import datetime from typing import Dict diff --git a/api/recipes.py b/api/recipes.py index d9fa957..8f7aad0 100644 --- a/api/recipes.py +++ b/api/recipes.py @@ -10,7 +10,7 @@ import recipes import persons from common import Page, ProblemDetails, ApiModel 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"]) diff --git a/api/shopping.py b/api/shopping.py index d89f23d..c6b0965 100644 --- a/api/shopping.py +++ b/api/shopping.py @@ -11,7 +11,7 @@ import persons import recipes import shopping 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"]) diff --git a/main.py b/main.py index b8a2c37..3c137b9 100644 --- a/main.py +++ b/main.py @@ -18,6 +18,8 @@ import shopping from fastapi.routing import APIRoute from common import ProblemDetails, Page, ApiModel +from settings import settings +from api.deps import get_db, cookie_person, error_response class CamelCaseRoute(APIRoute): @@ -29,35 +31,9 @@ class CamelCaseRoute(APIRoute): app = FastAPI(title="Doof API", version="1.0.0", description="Doof Backend API") 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 -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", - ) +# get_db, cookie_person, and error_response are imported from api.deps # OpenAPI reusable responses for ProblemDetails diff --git a/refactor-project-strategy.md b/refactor-project-strategy.md index 17fc9a2..0b04bd1 100644 --- a/refactor-project-strategy.md +++ b/refactor-project-strategy.md @@ -32,14 +32,15 @@ Acceptance criteria ## Phase 1 — API structure and lifecycle - [ ] Extract routers by feature - [x] api/recipes.py - - [ ] api/meals.py - [x] api/meals.py - [x] api/persons.py - [x] api/shopping.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 - [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 - 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: 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 meals, shopping, and auth routes; created api/deps and switched DB path to settings --- ## Next actions -Begin Phase 1 work in small steps: - - Create api package and extract the first router (e.g., recipes) without logic changes - - Wire the router in main.py and run tests - - Prepare lifespan handler for dev reverse proxy, but keep behavior identical +- Phase 1: Move dev reverse proxy into a lifespan handler (startup/shutdown) and close AsyncClient cleanly +- Phase 2: Add OpenAPI cookie security scheme and normalize 201 Created + Location +- Phase 3: Plan DB PRAGMAs and indexes; add transaction scoping per request +- Phase 5: Add fixtures for DB/auth and tests for health + 201 Location ### Health endpoint plan (Phase 1 target) - Path: GET /healthz