Added the missing ingredient parsing API to reach feature parity, updated the spec, and validated with full checks: build PASS, lint/typecheck PASS, tests PASS.

This commit is contained in:
jableader 2025-11-01 22:58:44 +11:00
parent e751b828fe
commit 7741df79e9
4 changed files with 79 additions and 1 deletions

View file

@ -12,6 +12,8 @@ from common import Page, ProblemDetails, ApiModel, Field
from api.dtos import MemberRef
router = APIRouter(prefix="/households/{householdSlug}/recipes", tags=["recipes"])
# Public, stateless recipes utilities
public = APIRouter(prefix="/recipes", tags=["recipes"])
class RecipeOut(ApiModel):
@ -230,3 +232,14 @@ async def parse_from_url(
if not data:
return error_response(None, 404, "Recipe data not found at URL")
return data
@public.get("/ingredients/parse", response_model=List[ingredients_mod.Ingredient])
async def parse_ingredients(
ingredients: List[str] = Query(..., description="Array of ingredients to parse"),
conn: aiosqlite.Connection = Depends(get_db),
):
# Stateless NLP parsing; attempt to match existing products for convenience
parsed = [ingredients_mod.parse_ingredient_from_nlp(s) for s in ingredients]
matched = await ingredients_mod.match_existing_products(conn, parsed)
return matched

View file

@ -6,6 +6,7 @@
- POST `/api/v1/households/{householdSlug}/shopping/current/ingredients` requests an adhoc ingredient scoped to household+user; duplicates deduped per user per household.
- DELETE `/api/v1/households/{householdSlug}/shopping/current/ingredients` removes an adhoc ingredient request for the current user in this household; idempotent.
- POST `/api/v1/households/{householdSlug}/recipes/parse-from-url` returns structured JSONLD recipe data for a URL (stateless; household auth enforced).
- GET `/api/v1/recipes/ingredients/parse?ingredients=...&ingredients=...` parses raw ingredient lines into Ingredient DTOs (stateless, public); attempts best-effort product matching.
- Comprehensive v2 coverage exists for scoping, purchases, request/unrequest, meals CRUD/consumed, and OpenAPI security. PASS.
# Backend Specification: Household Multi-Tenancy (v2)
@ -43,7 +44,7 @@ Special-case 401: Removed. v1 cookie-based auth and routes have been retired in
- GET `/api/v1/recipes``Page<Recipe>`; loads ingredients per page.
- GET `/api/v1/recipes/{id}` → full recipe (ingredients + createdBy).
- POST `/api/v1/households/{householdSlug}/recipes/parse-from-url` (auth required) → scrape/parse a recipe URL; 404 if not found.
- GET `/api/v1/recipes/ingredients/parse?ingredients=...&ingredients=...` → parse raw ingredient lines (no auth); matches existing products.
- GET `/api/v1/recipes/ingredients/parse?ingredients=...&ingredients=...` → parse raw ingredient lines (no auth); matches existing products. Implemented in v2 as public utility under the same path.
- POST `/api/v1/recipes` (auth required) → validate (≥1 ingredient), perform versioning (hide base if id≥0), set createdById, insert ingredients; sets `Location` header.
- DELETE `/api/v1/recipes/{id}` (auth required) → soft-delete (hide) recipe.
@ -198,6 +199,17 @@ Route surface lockdown:
- Creates indices `idx_<table>_household_id` for all above tables.
- Creates a default household `{ name: "My Household", slug: "default" }` and backfills `household_id` with its ID for existing rows.
- Ports `Person` rows to `User` (email derived as `<name>@example.com`) and creates `HouseholdMember` links (role `admin`).
### Feature parity checklist (OpenAPI diffs vs master)
Completed:
- Auth endpoints (`/api/v1/auth/*`) migrated to JWT with tokens/refresh cookie.
- Household-scoped recipes/meals/shopping endpoints in place.
- `POST /api/v1/households/{householdSlug}/recipes/parse-from-url` implemented.
- `GET /api/v1/recipes/ingredients/parse` ported as a public stateless NLP parser returning `Ingredient[]`.
Outstanding (tracked):
- None identified blocking parity for shopping/recipes needed by the frontend as of 2025-11-01. Re-check if any v1 product scrape/create endpoint needs re-exposure; current frontend uses household flows and parsing utilities.
- **Bootstrap Update**: Modify `db.py` so that a fresh database bootstrap (`db.create_schema`) calls the `create()` functions for the new repositories and *not* the old `persons` repository.
- ✅ **Indices/Constraints**: Enforced unique `Household.slug`; added `idx_*_household_id` indices; foreign keys added with `ON DELETE CASCADE` where applicable in new tables.
- ✅ **Acceptance (initial)**: Added `tests/test_migration_households.py` covering: new tables exist, `household_id` columns exist, default household created, and data porting from `Person` to `User` and `HouseholdMember`. Full test suite passes.

View file

@ -139,6 +139,7 @@ def create_app() -> FastAPI:
except Exception:
pass
app.include_router(recipes_router.router, prefix="/api/v1", tags=["recipes"]) # canonical
app.include_router(recipes_router.public, prefix="/api/v1", tags=["recipes"]) # public utils
app.include_router(meals_router.router, prefix="/api/v1", tags=["meals"]) # canonical
app.include_router(shopping_router.router, prefix="/api/v1", tags=["shopping"]) # canonical

View file

@ -763,6 +763,58 @@
]
}
},
"/api/v1/recipes/ingredients/parse": {
"get": {
"tags": [
"recipes",
"recipes"
],
"summary": "Parse Ingredients",
"operationId": "parse_ingredients_api_v1_recipes_ingredients_parse_get",
"parameters": [
{
"name": "ingredients",
"in": "query",
"required": true,
"schema": {
"type": "array",
"items": {
"type": "string"
},
"description": "Array of ingredients to parse",
"title": "Ingredients"
},
"description": "Array of ingredients to parse"
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Ingredient"
},
"title": "Response Parse Ingredients Api V1 Recipes Ingredients Parse Get"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/v1/households/{householdSlug}/meals/upcoming": {
"get": {
"tags": [