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:
parent
e751b828fe
commit
7741df79e9
4 changed files with 79 additions and 1 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<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.
|
||||
|
|
|
|||
1
main.py
1
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
|
||||
|
||||
|
|
|
|||
52
openapi.json
52
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": [
|
||||
|
|
|
|||
Loading…
Reference in a new issue