From 4f1570ee6ff6c46e5bfa9d12ec2031e7b58ec216 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Tue, 14 Apr 2026 10:50:46 -0700 Subject: [PATCH] feat: add Store.get_element_profiles() for wizard role candidate lookup --- app/db/store.py | 29 +++++++++++++++++++++++++++++ tests/db/test_store_recipes.py | 12 ++++++++++++ 2 files changed, 41 insertions(+) diff --git a/app/db/store.py b/app/db/store.py index ce872ee..65fafdd 100644 --- a/app/db/store.py +++ b/app/db/store.py @@ -571,6 +571,7 @@ class Store: max_carbs_g: float | None = None, max_sodium_mg: float | None = None, excluded_ids: list[int] | None = None, + exclude_generic: bool = False, ) -> list[dict]: """Find recipes containing any of the given ingredient names. Scores by match count and returns highest-scoring first. @@ -580,6 +581,9 @@ class Store: Nutrition filters use NULL-passthrough: rows without nutrition data 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: return [] @@ -605,6 +609,8 @@ class Store: placeholders = ",".join("?" * len(excluded_ids)) extra_clauses.append(f"r.id NOT IN ({placeholders})") 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 "" if self._fts_ready(): @@ -680,6 +686,29 @@ class Store: def get_recipe(self, recipe_id: int) -> dict | None: 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 ─────────────────────────────────────────────────────── def check_and_increment_rate_limit( diff --git a/tests/db/test_store_recipes.py b/tests/db/test_store_recipes.py index 1e71446..c0cde77 100644 --- a/tests/db/test_store_recipes.py +++ b/tests/db/test_store_recipes.py @@ -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) assert allowed is False 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 == {}