from __future__ import annotations from typing import Literal from pydantic import BaseModel, ConfigDict, Field, model_validator class WarningItem(BaseModel): model_config = ConfigDict(extra="forbid") severity: Literal["info", "warning", "blocking"] code: str message: str class IngredientSpec(BaseModel): model_config = ConfigDict(extra="forbid") food_name: str = Field(min_length=1, max_length=200) preferred_food_id: int | None = Field(default=None, ge=1) create_food: bool = False food_plural_name: str | None = Field(default=None, max_length=200) amount: float = Field(ge=0) amount_max: float | None = Field(default=None, ge=0) unit_name: str | None = Field(default=None, max_length=80) preferred_unit_id: int | None = Field(default=None, ge=1) create_unit: bool = False unit_plural_name: str | None = Field(default=None, max_length=80) note: str = Field(default="", max_length=500) no_amount: bool = False original_text: str = Field(default="", max_length=1000) @model_validator(mode="after") def validate_values(self) -> "IngredientSpec": if self.amount_max is not None and self.amount_max < self.amount: raise ValueError("amount_max darf nicht kleiner als amount sein.") if self.no_amount and self.amount != 0: raise ValueError("Bei no_amount=true muss amount 0 sein.") if self.create_food and self.preferred_food_id is not None: raise ValueError( "create_food und preferred_food_id dürfen nicht gleichzeitig gesetzt sein." ) if self.create_unit and self.preferred_unit_id is not None: raise ValueError( "create_unit und preferred_unit_id dürfen nicht gleichzeitig gesetzt sein." ) if self.create_unit and not self.unit_name: raise ValueError("create_unit benötigt unit_name.") return self class StepSpec(BaseModel): model_config = ConfigDict(extra="forbid") name: str = Field(min_length=1, max_length=200) instruction: str = Field(min_length=1, max_length=12000) time: int = Field(default=0, ge=0, le=10080) ingredients: list[IngredientSpec] class RecipeSpec(BaseModel): model_config = ConfigDict(extra="forbid") schema_version: Literal[1] = 1 name: str = Field(min_length=1, max_length=250) description: str = Field(default="", max_length=2000) source_url: str = Field(min_length=1, max_length=2000) image_url: str | None = Field(default=None, max_length=2000) servings: float = Field(default=1, gt=0, le=10000) servings_text: str = Field(default="", max_length=100) working_time: int = Field(default=0, ge=0, le=10080) waiting_time: int = Field(default=0, ge=0, le=10080) keywords: list[str] = Field(default_factory=list, max_length=30) confidence: Literal["high", "medium", "low"] warnings: list[WarningItem] = Field(default_factory=list, max_length=50) steps: list[StepSpec] = Field(min_length=1, max_length=50) class AnalyzeRequest(BaseModel): model_config = ConfigDict(extra="forbid") url: str = Field(min_length=3, max_length=2000) class RecipeRequest(BaseModel): model_config = ConfigDict(extra="forbid") recipe: RecipeSpec class ImportRequest(BaseModel): model_config = ConfigDict(extra="forbid") recipe: RecipeSpec import_image: bool = True force_duplicate: bool = False RECIPE_JSON_SCHEMA = { "type": "object", "additionalProperties": False, "required": [ "schema_version", "name", "description", "source_url", "image_url", "servings", "servings_text", "working_time", "waiting_time", "keywords", "confidence", "warnings", "steps", ], "properties": { "schema_version": {"type": "integer", "enum": [1]}, "name": {"type": "string"}, "description": {"type": "string"}, "source_url": {"type": "string"}, "image_url": {"type": ["string", "null"]}, "servings": {"type": "number"}, "servings_text": {"type": "string"}, "working_time": {"type": "integer"}, "waiting_time": {"type": "integer"}, "keywords": {"type": "array", "items": {"type": "string"}}, "confidence": {"type": "string", "enum": ["high", "medium", "low"]}, "warnings": { "type": "array", "items": { "type": "object", "additionalProperties": False, "required": ["severity", "code", "message"], "properties": { "severity": { "type": "string", "enum": ["info", "warning", "blocking"], }, "code": {"type": "string"}, "message": {"type": "string"}, }, }, }, "steps": { "type": "array", "minItems": 1, "items": { "type": "object", "additionalProperties": False, "required": ["name", "instruction", "time", "ingredients"], "properties": { "name": {"type": "string"}, "instruction": {"type": "string"}, "time": {"type": "integer"}, "ingredients": { "type": "array", "items": { "type": "object", "additionalProperties": False, "required": [ "food_name", "preferred_food_id", "create_food", "food_plural_name", "amount", "amount_max", "unit_name", "preferred_unit_id", "create_unit", "unit_plural_name", "note", "no_amount", "original_text", ], "properties": { "food_name": {"type": "string"}, "preferred_food_id": {"type": ["integer", "null"]}, "create_food": {"type": "boolean"}, "food_plural_name": {"type": ["string", "null"]}, "amount": {"type": "number"}, "amount_max": {"type": ["number", "null"]}, "unit_name": {"type": ["string", "null"]}, "preferred_unit_id": {"type": ["integer", "null"]}, "create_unit": {"type": "boolean"}, "unit_plural_name": {"type": ["string", "null"]}, "note": {"type": "string"}, "no_amount": {"type": "boolean"}, "original_text": {"type": "string"}, }, }, }, }, }, }, }, }