- pyproject.toml: add [tool.ruff] config suppressing E402/W293 globally, F841/E741/E702 in tests, E741 in scripts - inventory.py: split semicolon import (E702) - recipes.py: fix logger -> log (F821 undefined name) - shopping.py: rename l -> lnk in list comprehension (E741) - format_conversion.py: noqa F841 on CUDA flag (used as future hook) - backfill_keywords.py: rename done -> _done (F841) - ingest_purplecarrot.py: drop == True comparison (E712) - Auto-fix: I001 import sorting, F401 unused imports across all files
73 lines
3.3 KiB
Python
73 lines
3.3 KiB
Python
"""Pydantic schemas for the recipe scanner (kiwi#9).
|
|
|
|
Scan input → photo(s).
|
|
Scan output → ScannedRecipeResponse (for review + editing before save).
|
|
Save input → ScannedRecipeSaveRequest.
|
|
User recipe output → UserRecipeResponse (after save).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
# ── Ingredient in a scanned recipe ────────────────────────────────────────────
|
|
|
|
class ScannedIngredientSchema(BaseModel):
|
|
"""One ingredient line extracted from a recipe photo."""
|
|
name: str # normalized generic name ("ranch dressing")
|
|
qty: str | None = None # quantity as string, preserving fractions ("1/2", "¼")
|
|
unit: str | None = None # unit of measure; null for countable items
|
|
raw: str | None = None # verbatim original line from the image
|
|
in_pantry: bool = False # True if this ingredient matches something in the pantry
|
|
|
|
|
|
# ── Scan response (returned immediately, not persisted) ───────────────────────
|
|
|
|
class ScannedRecipeResponse(BaseModel):
|
|
"""Structured recipe extracted from photo(s). Returned for user review before save."""
|
|
title: str | None = None
|
|
subtitle: str | None = None # e.g. "with Broccoli & Ranch Dressing"
|
|
servings: str | None = None # kept as string: "2", "4-6", "serves 8"
|
|
cook_time: str | None = None # kept as string: "25 min", "1 hour"
|
|
source_note: str | None = None # e.g. "Purple Carrot", "Betty Crocker"
|
|
ingredients: list[ScannedIngredientSchema] = Field(default_factory=list)
|
|
steps: list[str] = Field(default_factory=list)
|
|
notes: str | None = None
|
|
tags: list[str] = Field(default_factory=list)
|
|
pantry_match_pct: int = 0 # 0-100: percentage of ingredients found in pantry
|
|
confidence: str = "medium" # "high" | "medium" | "low"
|
|
warnings: list[str] = Field(default_factory=list)
|
|
|
|
|
|
# ── Save request ──────────────────────────────────────────────────────────────
|
|
|
|
class ScannedRecipeSaveRequest(BaseModel):
|
|
"""User-reviewed (possibly edited) recipe data to persist as a user recipe."""
|
|
title: str
|
|
subtitle: str | None = None
|
|
servings: str | None = None
|
|
cook_time: str | None = None
|
|
source_note: str | None = None
|
|
ingredients: list[ScannedIngredientSchema]
|
|
steps: list[str]
|
|
notes: str | None = None
|
|
tags: list[str] = Field(default_factory=list)
|
|
source: str = "scan" # "scan" | "manual"
|
|
|
|
|
|
# ── User recipe (persisted) ───────────────────────────────────────────────────
|
|
|
|
class UserRecipeResponse(BaseModel):
|
|
"""A user-created or user-scanned recipe stored in user_recipes table."""
|
|
id: int
|
|
title: str
|
|
subtitle: str | None = None
|
|
servings: str | None = None
|
|
cook_time: str | None = None
|
|
source_note: str | None = None
|
|
ingredients: list[ScannedIngredientSchema]
|
|
steps: list[str]
|
|
notes: str | None = None
|
|
tags: list[str] = Field(default_factory=list)
|
|
source: str
|
|
pantry_match_pct: int | None = None
|
|
created_at: str
|