feat: add Store.get_element_profiles() for wizard role candidate lookup

This commit is contained in:
pyr0ball 2026-04-14 10:50:46 -07:00
parent 1a5fb23dfd
commit 4f1570ee6f
2 changed files with 41 additions and 0 deletions

View file

@ -571,6 +571,7 @@ class Store:
max_carbs_g: float | None = None, max_carbs_g: float | None = None,
max_sodium_mg: float | None = None, max_sodium_mg: float | None = None,
excluded_ids: list[int] | None = None, excluded_ids: list[int] | None = None,
exclude_generic: bool = False,
) -> list[dict]: ) -> list[dict]:
"""Find recipes containing any of the given ingredient names. """Find recipes containing any of the given ingredient names.
Scores by match count and returns highest-scoring first. Scores by match count and returns highest-scoring first.
@ -580,6 +581,9 @@ class Store:
Nutrition filters use NULL-passthrough: rows without nutrition data Nutrition filters use NULL-passthrough: rows without nutrition data
always pass (they may be estimated or absent entirely). always pass (they may be estimated or absent entirely).
exclude_generic: when True, skips recipes marked is_generic=1.
Pass True for Level 1 ("Use What I Have") to suppress catch-all recipes.
""" """
if not ingredient_names: if not ingredient_names:
return [] return []
@ -605,6 +609,8 @@ class Store:
placeholders = ",".join("?" * len(excluded_ids)) placeholders = ",".join("?" * len(excluded_ids))
extra_clauses.append(f"r.id NOT IN ({placeholders})") extra_clauses.append(f"r.id NOT IN ({placeholders})")
extra_params.extend(excluded_ids) extra_params.extend(excluded_ids)
if exclude_generic:
extra_clauses.append("r.is_generic = 0")
where_extra = (" AND " + " AND ".join(extra_clauses)) if extra_clauses else "" where_extra = (" AND " + " AND ".join(extra_clauses)) if extra_clauses else ""
if self._fts_ready(): if self._fts_ready():
@ -680,6 +686,29 @@ class Store:
def get_recipe(self, recipe_id: int) -> dict | None: def get_recipe(self, recipe_id: int) -> dict | None:
return self._fetch_one("SELECT * FROM recipes WHERE id = ?", (recipe_id,)) return self._fetch_one("SELECT * FROM recipes WHERE id = ?", (recipe_id,))
def get_element_profiles(self, names: list[str]) -> dict[str, list[str]]:
"""Return {ingredient_name: [element_tag, ...]} for the given names.
Only names present in ingredient_profiles are returned -- missing names
are silently omitted so callers can distinguish "no profile" from "empty
elements list".
"""
if not names:
return {}
placeholders = ",".join("?" * len(names))
rows = self._fetch_all(
f"SELECT name, elements FROM ingredient_profiles WHERE name IN ({placeholders})",
tuple(names),
)
result: dict[str, list[str]] = {}
for row in rows:
try:
elements = json.loads(row["elements"]) if row["elements"] else []
except (json.JSONDecodeError, TypeError):
elements = []
result[row["name"]] = elements
return result
# ── rate limits ─────────────────────────────────────────────────────── # ── rate limits ───────────────────────────────────────────────────────
def check_and_increment_rate_limit( def check_and_increment_rate_limit(

View file

@ -42,3 +42,15 @@ def test_check_rate_limit_exceeded(store_with_recipes):
allowed, count = store_with_recipes.check_and_increment_rate_limit("leftover_mode", daily_max=5) allowed, count = store_with_recipes.check_and_increment_rate_limit("leftover_mode", daily_max=5)
assert allowed is False assert allowed is False
assert count == 5 assert count == 5
def test_get_element_profiles_returns_known_items(store_with_profiles):
profiles = store_with_profiles.get_element_profiles(["butter", "parmesan", "unknown_item"])
assert profiles["butter"] == ["Richness"]
assert "Depth" in profiles["parmesan"]
assert "unknown_item" not in profiles
def test_get_element_profiles_empty_list(store_with_profiles):
profiles = store_with_profiles.get_element_profiles([])
assert profiles == {}