Public Access
117 lines
3.9 KiB
Python
117 lines
3.9 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any, Mapping
|
|
|
|
from app.models import MealType
|
|
|
|
|
|
_COMPLETE_MEAL_TERMS = {
|
|
"bowl", "burger", "burrito", "casserole", "chili", "curry", "fried rice",
|
|
"lasagna", "noodle", "paella", "pasta", "pizza", "quesadilla", "ramen",
|
|
"rice bowl", "risotto", "salad", "sandwich", "soup", "spaghetti", "stew",
|
|
"stir fry", "stir-fry", "taco", "tacos", "wrap",
|
|
}
|
|
|
|
_CARB_TERMS = {
|
|
"bread", "bun", "couscous", "farro", "grain", "noodle", "orzo", "pasta",
|
|
"pita", "potato", "quinoa", "rice", "tortilla",
|
|
}
|
|
|
|
_VEG_TERMS = {
|
|
"asparagus", "beans", "broccoli", "brussels", "cabbage", "carrot",
|
|
"cauliflower", "corn", "greens", "kale", "pepper", "salad", "spinach",
|
|
"vegetable", "zucchini",
|
|
}
|
|
|
|
_PROTEIN_TERMS = {
|
|
"beef", "breast", "chicken", "chop", "cod", "cutlet", "fish", "pork",
|
|
"salmon", "shrimp", "steak", "tilapia", "tofu", "turkey",
|
|
}
|
|
|
|
_PAIRINGS_BY_CUISINE = {
|
|
"asian": ("sesame broccoli", "steamed jasmine rice"),
|
|
"chinese": ("garlic green beans", "steamed jasmine rice"),
|
|
"indian": ("roasted cauliflower", "basmati rice"),
|
|
"italian": ("garlicky green beans", "orzo or crusty bread"),
|
|
"mediterranean": ("cucumber tomato salad", "warm pita or couscous"),
|
|
"mexican": ("sauteed peppers and onions", "cilantro lime rice"),
|
|
"thai": ("cucumber salad", "steamed jasmine rice"),
|
|
}
|
|
|
|
_DEFAULT_PAIRING = ("roasted broccoli", "rice pilaf or roasted potatoes")
|
|
|
|
|
|
def components_with_suggested_sides(
|
|
recipe: Any,
|
|
meal_type: MealType | str,
|
|
base_components: Mapping[str, Any] | None = None,
|
|
) -> dict[str, Any]:
|
|
components = dict(base_components or {})
|
|
suggestion = suggest_sides_for_recipe(recipe, meal_type)
|
|
if suggestion:
|
|
components["suggested_sides"] = suggestion
|
|
return components
|
|
|
|
|
|
def suggest_sides_for_recipe(recipe: Any, meal_type: MealType | str) -> dict[str, Any] | None:
|
|
meal_value = meal_type.value if isinstance(meal_type, MealType) else str(meal_type).lower()
|
|
if meal_value == MealType.BREAKFAST.value:
|
|
return None
|
|
|
|
explicit_sides = [
|
|
side.get("name")
|
|
for side in (getattr(recipe, "side_dishes", None) or [])
|
|
if isinstance(side, dict) and side.get("name")
|
|
]
|
|
if explicit_sides:
|
|
return {
|
|
"needed": True,
|
|
"items": explicit_sides[:2],
|
|
"note": "Use the recipe's recommended side dish pairing.",
|
|
}
|
|
|
|
text = _recipe_text(recipe)
|
|
if not _looks_like_simple_protein(recipe, text):
|
|
return None
|
|
|
|
vegetable, carb = _pairing_for_cuisine(getattr(recipe, "cuisine_tags", None) or [])
|
|
return {
|
|
"needed": True,
|
|
"vegetable": vegetable,
|
|
"carb": carb,
|
|
"note": "Simple protein entree; add a vegetable and carb to make it a complete meal.",
|
|
}
|
|
|
|
|
|
def _recipe_text(recipe: Any) -> str:
|
|
parts = [
|
|
getattr(recipe, "name", "") or "",
|
|
getattr(recipe, "protein_type", "") or "",
|
|
" ".join(getattr(recipe, "cuisine_tags", None) or []),
|
|
]
|
|
for ingredient in getattr(recipe, "ingredients", None) or []:
|
|
if isinstance(ingredient, dict):
|
|
parts.append(str(ingredient.get("name") or ingredient.get("ingredient") or ""))
|
|
return " ".join(parts).lower()
|
|
|
|
|
|
def _looks_like_simple_protein(recipe: Any, text: str) -> bool:
|
|
if any(term in text for term in _COMPLETE_MEAL_TERMS):
|
|
return False
|
|
has_protein = bool(getattr(recipe, "protein_type", None)) or any(
|
|
term in text for term in _PROTEIN_TERMS
|
|
)
|
|
if not has_protein:
|
|
return False
|
|
has_carb = any(term in text for term in _CARB_TERMS)
|
|
has_veg = any(term in text for term in _VEG_TERMS)
|
|
return not (has_carb and has_veg)
|
|
|
|
|
|
def _pairing_for_cuisine(tags: list[str]) -> tuple[str, str]:
|
|
lowered = {tag.lower() for tag in tags}
|
|
for key, pairing in _PAIRINGS_BY_CUISINE.items():
|
|
if key in lowered:
|
|
return pairing
|
|
return _DEFAULT_PAIRING
|