diff --git a/api/recipes.py b/api/recipes.py index 7fb6f91..4675934 100644 --- a/api/recipes.py +++ b/api/recipes.py @@ -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 diff --git a/backend-spec.md b/backend-spec.md index af8b0bd..33671bf 100644 --- a/backend-spec.md +++ b/backend-spec.md @@ -6,6 +6,7 @@ - POST `/api/v1/households/{householdSlug}/shopping/current/ingredients` requests an ad‑hoc ingredient scoped to household+user; duplicates deduped per user per household. - DELETE `/api/v1/households/{householdSlug}/shopping/current/ingredients` removes an ad‑hoc ingredient request for the current user in this household; idempotent. - POST `/api/v1/households/{householdSlug}/recipes/parse-from-url` returns structured JSON‑LD 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`; 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__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 `@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. diff --git a/main.py b/main.py index 4a95e7f..e9a6eb0 100644 --- a/main.py +++ b/main.py @@ -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 diff --git a/openapi.json b/openapi.json index 3236934..4b1131e 100644 --- a/openapi.json +++ b/openapi.json @@ -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": [