Scaffolding
This commit is contained in:
parent
1b76ea44b6
commit
3d7afa3765
8 changed files with 316 additions and 0 deletions
2
api/__init__.py
Normal file
2
api/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# API package for FastAPI routers.
|
||||
# Routers will be split by feature: recipes, meals, persons, shopping, auth.
|
||||
25
api/auth.py
Normal file
25
api/auth.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import aiosqlite
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
|
||||
import persons
|
||||
from common import ProblemDetails
|
||||
from main import get_db, cookie_person # temporary during extraction
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["auth"])
|
||||
|
||||
|
||||
@router.post("/login", operation_id="login", summary="Login and set user_id cookie",
|
||||
responses={404: {"model": ProblemDetails, "description": "Person not found", "content": {"application/problem+json": {}}}})
|
||||
async def login(
|
||||
data: Any, conn: aiosqlite.Connection = Depends(get_db), request: Request | None = None
|
||||
) -> persons.Person:
|
||||
raise NotImplementedError("login extraction pending")
|
||||
|
||||
|
||||
@router.post("/refresh", operation_id="refresh", summary="Refresh current user from cookie")
|
||||
async def current_user(user: persons.Person = Depends(cookie_person)) -> persons.Person:
|
||||
raise NotImplementedError("current_user extraction pending")
|
||||
77
api/meals.py
Normal file
77
api/meals.py
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import List, Optional
|
||||
import datetime
|
||||
|
||||
import aiosqlite
|
||||
from fastapi import APIRouter, Depends, Query, Request
|
||||
|
||||
import meals
|
||||
import persons
|
||||
import shopping
|
||||
from common import ProblemDetails
|
||||
from main import get_db, cookie_person # temporary imports during extraction
|
||||
|
||||
router = APIRouter(prefix="/meals", tags=["meals"])
|
||||
|
||||
|
||||
@router.get("/upcoming", operation_id="getUpcomingMeals", summary="List upcoming meals in a date range")
|
||||
async def get_upcoming_meals(
|
||||
date_from: datetime.datetime = Query(..., alias="from"),
|
||||
to: datetime.datetime = Query(...),
|
||||
conn: aiosqlite.Connection = Depends(get_db),
|
||||
) -> List[meals.Meal]:
|
||||
raise NotImplementedError("get_upcoming_meals extraction pending")
|
||||
|
||||
|
||||
@router.get("/{meal_id}", response_model=meals.Meal, operation_id="getMeal", summary="Get a meal by id",
|
||||
responses={404: {"model": ProblemDetails, "description": "Meal not found", "content": {"application/problem+json": {}}}})
|
||||
async def get_meal(
|
||||
meal_id: int, conn: aiosqlite.Connection = Depends(get_db), request: Request | None = None
|
||||
) -> meals.Meal:
|
||||
raise NotImplementedError("get_meal extraction pending")
|
||||
|
||||
|
||||
@router.post("", response_model=meals.Meal, operation_id="createMeal", summary="Create a new meal",
|
||||
responses={400: {"model": ProblemDetails, "description": "Validation error", "content": {"application/problem+json": {}}}})
|
||||
async def create_meal(
|
||||
meal: meals.Meal, conn: aiosqlite.Connection = Depends(get_db), request: Request | None = None
|
||||
) -> meals.Meal:
|
||||
raise NotImplementedError("create_meal extraction pending")
|
||||
|
||||
|
||||
@router.put("/{meal_id}", response_model=meals.Meal, operation_id="updateMeal", summary="Update an existing meal",
|
||||
responses={
|
||||
400: {"model": ProblemDetails, "description": "Validation error", "content": {"application/problem+json": {}}},
|
||||
404: {"model": ProblemDetails, "description": "Meal not found", "content": {"application/problem+json": {}}},
|
||||
})
|
||||
async def update_meal(
|
||||
meal_id: int, meal: meals.Meal, conn: aiosqlite.Connection = Depends(get_db), request: Request | None = None
|
||||
) -> meals.Meal:
|
||||
raise NotImplementedError("update_meal extraction pending")
|
||||
|
||||
|
||||
@router.post("/{meal_id}/consumed", response_model=meals.Meal, operation_id="markMealConsumed", summary="Mark a meal as consumed",
|
||||
responses={
|
||||
400: {"model": ProblemDetails, "description": "Validation error", "content": {"application/problem+json": {}}},
|
||||
404: {"model": ProblemDetails, "description": "Meal not found", "content": {"application/problem+json": {}}},
|
||||
})
|
||||
async def mark_consumed(
|
||||
meal_id: int,
|
||||
consumed_date: Optional[datetime.datetime] = None,
|
||||
conn: aiosqlite.Connection = Depends(get_db),
|
||||
person: persons.Person = Depends(cookie_person),
|
||||
request: Request | None = None,
|
||||
) -> meals.Meal:
|
||||
raise NotImplementedError("mark_consumed extraction pending")
|
||||
|
||||
|
||||
@router.delete("/{meal_id}", response_model=meals.Meal, operation_id="deleteMeal", summary="Delete a meal",
|
||||
responses={404: {"model": ProblemDetails, "description": "Meal not found", "content": {"application/problem+json": {}}}})
|
||||
async def delete_meal(
|
||||
meal_id: int,
|
||||
conn: aiosqlite.Connection = Depends(get_db),
|
||||
person: persons.Person = Depends(cookie_person),
|
||||
request: Request | None = None,
|
||||
) -> meals.Meal:
|
||||
raise NotImplementedError("delete_meal extraction pending")
|
||||
30
api/persons.py
Normal file
30
api/persons.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
import aiosqlite
|
||||
from fastapi import APIRouter, Depends, Query, Request
|
||||
|
||||
import persons
|
||||
from common import Page, ProblemDetails
|
||||
from main import get_db # temporary during extraction
|
||||
|
||||
router = APIRouter(prefix="/persons", tags=["persons"])
|
||||
|
||||
|
||||
@router.get("", response_model=Page[persons.Person], operation_id="listPersons", summary="List persons (paginated)")
|
||||
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)."),
|
||||
conn: aiosqlite.Connection = Depends(get_db),
|
||||
request: Request | None = None,
|
||||
) -> Page[persons.Person]:
|
||||
raise NotImplementedError("list_persons extraction pending")
|
||||
|
||||
|
||||
@router.post("", response_model=persons.Person, 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")
|
||||
77
api/recipes.py
Normal file
77
api/recipes.py
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
import aiosqlite
|
||||
from fastapi import APIRouter, Depends, Query, Request
|
||||
|
||||
import ingredients
|
||||
import recipes
|
||||
import persons
|
||||
from common import Page, ProblemDetails
|
||||
from main import get_db, cookie_person, error_response # temporary imports during extraction
|
||||
|
||||
router = APIRouter(prefix="/recipes", tags=["recipes"])
|
||||
|
||||
|
||||
@router.get("", response_model=Page[recipes.Recipe], operation_id="listRecipes", summary="List recipes (paginated)")
|
||||
async def list_recipes(
|
||||
q: Optional[str] = Query(default=None, description="Optional case-insensitive name filter (matches recipe name with SQL LIKE)."),
|
||||
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[recipes.Recipe]:
|
||||
# Placeholder: implementation will be moved from main.get_recipes in a later step.
|
||||
raise NotImplementedError("list_recipes extraction pending")
|
||||
|
||||
|
||||
@router.get("/{recipe_id}", response_model=recipes.Recipe, operation_id="getRecipe", summary="Get a single recipe",
|
||||
responses={404: {"model": ProblemDetails, "description": "Recipe not found", "content": {"application/problem+json": {}}}})
|
||||
async def get_recipe(
|
||||
recipe_id: int, conn: aiosqlite.Connection = Depends(get_db), request: Request | None = None
|
||||
) -> recipes.Recipe:
|
||||
# Placeholder: implementation will be moved from main.get_recipe in a later step.
|
||||
raise NotImplementedError("get_recipe extraction pending")
|
||||
|
||||
|
||||
@router.post("", response_model=recipes.Recipe, operation_id="createRecipe", summary="Create a new recipe (versioning semantics applied)",
|
||||
responses={400: {"model": ProblemDetails, "description": "Validation error", "content": {"application/problem+json": {}}}})
|
||||
async def create_recipe(
|
||||
recipe: recipes.Recipe,
|
||||
conn: aiosqlite.Connection = Depends(get_db),
|
||||
user: persons.Person = Depends(cookie_person),
|
||||
request: Request | None = None,
|
||||
) -> recipes.Recipe:
|
||||
# Placeholder: implementation will be moved from main.create_recipe in a later step.
|
||||
raise NotImplementedError("create_recipe extraction pending")
|
||||
|
||||
|
||||
@router.delete("/{recipe_id}", response_model=recipes.Recipe, operation_id="deleteRecipe", summary="Soft-delete (hide) a recipe",
|
||||
responses={404: {"model": ProblemDetails, "description": "Recipe not found", "content": {"application/problem+json": {}}}})
|
||||
async def delete_recipe(
|
||||
recipe_id: int,
|
||||
conn: aiosqlite.Connection = Depends(get_db),
|
||||
user: persons.Person = Depends(cookie_person),
|
||||
request: Request | None = None,
|
||||
) -> recipes.Recipe:
|
||||
# Placeholder: implementation will be moved from main.delete_recipe in a later step.
|
||||
raise NotImplementedError("delete_recipe extraction pending")
|
||||
|
||||
|
||||
@router.get("/parse", response_model=None, operation_id="parseRecipe", summary="Parse a recipe from a URL",
|
||||
responses={400: {"model": ProblemDetails, "description": "Recipe not found", "content": {"application/problem+json": {}}}})
|
||||
async def parse_recipe_handler(
|
||||
url: str, conn: aiosqlite.Connection = Depends(get_db), person=Depends(cookie_person), request: Request | None = None
|
||||
) -> recipes.Recipe:
|
||||
# Placeholder: implementation will be moved from main.parse_recipe_handler in a later step.
|
||||
raise NotImplementedError("parse_recipe_handler extraction pending")
|
||||
|
||||
|
||||
@router.get("/ingredients/parse", operation_id="parseIngredients", summary="Parse raw ingredient lines")
|
||||
async def parse_ingredients(
|
||||
lines: List[str] = Query(alias="ingredients", title="Array of ingredients to parse"),
|
||||
conn: aiosqlite.Connection = Depends(get_db),
|
||||
) -> List[ingredients.Ingredient]:
|
||||
# Placeholder: implementation will be moved from main.parse_ingredients in a later step.
|
||||
raise NotImplementedError("parse_ingredients extraction pending")
|
||||
78
api/shopping.py
Normal file
78
api/shopping.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Dict, List
|
||||
|
||||
import aiosqlite
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
|
||||
import ingredients
|
||||
import meals
|
||||
import persons
|
||||
import recipes
|
||||
import shopping
|
||||
from common import ProblemDetails
|
||||
from main import get_db, cookie_person # temporary during extraction
|
||||
|
||||
router = APIRouter(prefix="/shopping", tags=["shopping"])
|
||||
|
||||
|
||||
@router.get("/current", operation_id="getCurrentShoppingList", summary="Get the current aggregated shopping list")
|
||||
async def get_current_shopping_list(
|
||||
conn: aiosqlite.Connection = Depends(get_db),
|
||||
):
|
||||
raise NotImplementedError("get_current_shopping_list extraction pending")
|
||||
|
||||
|
||||
@router.get("/{list_id}", operation_id="getShoppingList", summary="Get a purchased shopping list by id",
|
||||
responses={404: {"model": ProblemDetails, "description": "Shopping list not found", "content": {"application/problem+json": {}}}})
|
||||
async def get_shopping_list(
|
||||
list_id: int, conn: aiosqlite.Connection = Depends(get_db), request: Request | None = None
|
||||
):
|
||||
raise NotImplementedError("get_shopping_list extraction pending")
|
||||
|
||||
|
||||
@router.post("", operation_id="purchaseIngredients", summary="Purchase ingredients for a shopping list")
|
||||
async def purchase_ingredients(
|
||||
shopping_list: shopping.ShoppingList,
|
||||
conn: aiosqlite.Connection = Depends(get_db),
|
||||
person: persons.Person = Depends(cookie_person),
|
||||
):
|
||||
raise NotImplementedError("purchase_ingredients extraction pending")
|
||||
|
||||
|
||||
@router.get("/current/me/ingredients", operation_id="getMyShoppingList", summary="Get my outstanding ingredient requests")
|
||||
async def get_my_shopping_list(
|
||||
conn: aiosqlite.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person)
|
||||
) -> List[ingredients.Ingredient]:
|
||||
raise NotImplementedError("get_my_shopping_list extraction pending")
|
||||
|
||||
|
||||
@router.post("/current/me/ingredients", operation_id="syncMyShoppingList", summary="Sync my outstanding ingredient requests")
|
||||
async def sync_my_shopping_list(
|
||||
requests: List[ingredients.Ingredient],
|
||||
conn: aiosqlite.Connection = Depends(get_db),
|
||||
person: persons.Person = Depends(cookie_person),
|
||||
) -> List[ingredients.Ingredient]:
|
||||
raise NotImplementedError("sync_my_shopping_list extraction pending")
|
||||
|
||||
|
||||
@router.post("/current/meals/{meal_id}", operation_id="requestMeal", summary="Request a meal for shopping",
|
||||
responses={404: {"model": ProblemDetails, "description": "Meal not found", "content": {"application/problem+json": {}}}})
|
||||
async def request_meal(
|
||||
meal_id: int,
|
||||
conn: aiosqlite.Connection = Depends(get_db),
|
||||
person: persons.Person = Depends(cookie_person),
|
||||
request: Request | None = None,
|
||||
):
|
||||
raise NotImplementedError("request_meal extraction pending")
|
||||
|
||||
|
||||
@router.delete("/current/meals/{meal_id}", operation_id="unrequestMeal", summary="Remove a meal request",
|
||||
responses={404: {"model": ProblemDetails, "description": "Meal not found", "content": {"application/problem+json": {}}}})
|
||||
async def unrequest_meal(
|
||||
meal_id: int,
|
||||
conn: aiosqlite.Connection = Depends(get_db),
|
||||
person: persons.Person = Depends(cookie_person),
|
||||
request: Request | None = None,
|
||||
):
|
||||
raise NotImplementedError("unrequest_meal extraction pending")
|
||||
|
|
@ -142,6 +142,7 @@ Note: We can adopt this structure gradually without moving DB code immediately;
|
|||
## Progress log
|
||||
- 2025-10-18: Created strategy document and added settings.py (not yet wired)
|
||||
- 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
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
26
settings.py
Normal file
26
settings.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
"""
|
||||
Centralized runtime settings for the Doof backend.
|
||||
|
||||
No external dependencies; reads from environment only so it can be imported
|
||||
anywhere (including tests) without side effects.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import os
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Settings:
|
||||
# Database
|
||||
database_path: str = os.environ.get("DOOF_DB", "./data/doof.sqlite")
|
||||
|
||||
# Environment flags
|
||||
prod: bool = os.environ.get("DOOF_PROD", "false").lower() in {"1", "true", "yes"}
|
||||
|
||||
# Frontend dev server for reverse proxy in non-prod
|
||||
frontend_dev_url: str = os.environ.get("FRONTEND_DEV_URL", "http://localhost:8080/")
|
||||
|
||||
|
||||
# A module-level singleton for convenience imports
|
||||
settings = Settings()
|
||||
Loading…
Reference in a new issue