198 lines
7.3 KiB
Python
198 lines
7.3 KiB
Python
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"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|