Files
Meal-Planner/scripts/seed_recipes_round2.py
T
admin 97b84a7cd8 feat(scripts): Sprint 15 round 2 — seed_recipes_round2.py (50 gap-filling queries)
Round 1 (commit a3c89bf) imported 18 recipes before hitting
the 50-pt/day Spoonacular free-tier cap. User direction
(2026-06-07): 'please add more meals to the potential list' /
'Pull in more recipes so we have a larger sample to generate
from.'

This is a fresh-quota run with a different query list focused
on cuisines and meal types the round 1 list didn't cover:
Indian (8) + Thai (6) + Chinese regional (6) + Soups & stews
(6) + Salads (6) + Sandwiches/wraps (5) + Breakfast (5) +
German/European (4) + French (4) = 50 queries.

Same shape as scripts/seed_recipes.py: hits Spoonacular's
complexSearch directly (avoids the broken backend route and
the backend's quota counter), POSTs top hits to the backend's
/api/recipes/import. Idempotent (409 on duplicate), 1.5 sec
sleep, stops on 402.

Result: 18 imported today. 12 queries returned no hits from
Spoonacular's free-tier index (e.g. 'chana masala', 'thai
basil chicken', 'dan dan noodles' — these are absent or
premium-only). 1 query ('wedge salad') hit 402 mid-import.

DB went 49 -> 67 total recipes (37 Spoonacular + 30 manual).
LLM test (Sprint 13 endpoint, week 2026-07-20, prompt
'variety, mix of cuisines, family-friendly, no repeats'):
  {picked_count: 0, filled_count: 21, failed_count: 0}
The library now covers all 21 slots of a week (was 19/21 +
2 failed in round 1). 4 weeks of planning now has a real
library to pick from with 1.25x rotation.

Re-running scripts/seed_recipes.py (round 1) tomorrow will
add 30+ more — its query list has gaps the round 1 cap didn't
reach (American + Mediterranean cuisines).
2026-06-07 16:20:20 -07:00

155 lines
5.4 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Sprint 15 — Round 2: Seed 50 more family-friendly recipes.
User direction (2026-06-06): "please add more meals to the potential
list" / "Pull in more recipes so we have a larger sample to generate
from."
This is a follow-up to scripts/seed_recipes.py (round 1) which
imported 18 recipes before hitting the 50-pt/day free-tier cap. This
round focuses on cuisines and meal types the round 1 list didn't
cover: Indian, Thai, Chinese regional, soups/stews, salads,
sandwiches/wraps, breakfast, German/European, French.
Same idempotent behavior as round 1: 409 from /api/recipes/import
means already imported; the script logs and continues.
Cost: 50 × complexSearch (1.10 pts) + 50 × /information (1 pt) =
105 pts, ~2 days on free tier. Today: run until the cap hits.
Run on the deploy host:
ssh docker-willester
cd /home/peter/MealPlanner
set -a && source .env && set +a
python3 scripts/seed_recipes_round2.py
"""
from __future__ import annotations
import json
import logging
import os
import sys
import time
from typing import Optional
import requests
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("seed_recipes_round2")
SPOONACULAR_KEY = os.environ.get("SPOONACULAR_API_KEY", "")
BACKEND_BASE = os.environ.get("MEALPLANNER_BACKEND", "http://localhost:8082")
SEARCH_URL = "https://api.spoonacular.com/recipes/complexSearch"
IMPORT_URL = f"{BACKEND_BASE}/api/recipes/import"
SLEEP_BETWEEN_QUERIES_S = 1.5
QUERIES: list[str] = [
# Indian (8)
"chicken tikka masala", "butter chicken", "palak paneer", "chana masala",
"biryani", "dal", "samosa", "naan",
# Thai (6)
"green curry", "massaman curry", "tom yum soup", "mango sticky rice",
"papaya salad", "thai basil chicken",
# Chinese regional (6)
"mapo tofu", "hot and sour soup", "scallion pancakes", "soup dumplings",
"beef noodle soup", "dan dan noodles",
# Soups & stews (6)
"french onion soup", "clam chowder", "chicken noodle soup", "tomato soup",
"lentil soup", "butternut squash soup",
# Salads (6)
"caesar salad", "cobb salad", "nicoise salad", "wedge salad",
"pasta salad", "quinoa salad",
# Sandwiches / wraps (5)
"banh mi", "reuben sandwich", "club sandwich", "french dip", "gyro wrap",
# Breakfast (5)
"eggs benedict", "pancakes", "french toast", "omelette", "breakfast burrito",
# German / European (4)
"schnitzel", "spaetzle", "sauerbraten", "beef rouladen",
# French (4)
"coq au vin", "ratatouille", "beef bourguignon", "quiche lorraine",
]
def search_top_hit(query: str) -> Optional[dict]:
try:
resp = requests.get(
SEARCH_URL,
params={
"query": query,
"number": 1,
"addRecipeInformation": "true",
"instructionsRequired": "true",
"apiKey": SPOONACULAR_KEY,
},
timeout=20,
)
except requests.RequestException as exc:
log.warning("search request failed for %r: %s", query, exc)
return None
if resp.status_code == 402:
log.error("Spoonacular quota exhausted mid-run; stopping.")
sys.exit(1)
if resp.status_code != 200:
log.warning("search %r: HTTP %d %s", query, resp.status_code, resp.text[:200])
return None
results = resp.json().get("results", [])
return results[0] if results else None
def import_recipe(external_id: str) -> str:
try:
resp = requests.post(
IMPORT_URL,
json={"external_id": str(external_id), "external_source": "spoonacular"},
timeout=30,
)
except requests.RequestException as exc:
return f"request_error: {exc}"
if resp.status_code == 201:
body = resp.json()
return f"imported id={body.get('id')} name={body.get('name')!r}"
if resp.status_code == 409:
return "duplicate (already in library)"
if resp.status_code == 503:
return "quota_exhausted"
return f"HTTP {resp.status_code}: {resp.text[:200]}"
def main() -> int:
if not SPOONACULAR_KEY:
log.error("SPOONACULAR_API_KEY not set. export it from /home/peter/MealPlanner/.env first.")
return 2
log.info("Seeding %d recipes (round 2) via %s", len(QUERIES), BACKEND_BASE)
stats = {"imported": 0, "duplicate": 0, "no_hit": 0, "error": 0, "quota_exhausted": 0}
for i, q in enumerate(QUERIES, start=1):
hit = search_top_hit(q)
if not hit:
log.info("[%2d/%d] %-25r no hit", i, len(QUERIES), q)
stats["no_hit"] += 1
else:
ext_id = hit["id"]
name = hit.get("title", "<no title>")
result = import_recipe(ext_id)
log.info("[%2d/%d] %-25r ext=%s %s name=%r", i, len(QUERIES), q, ext_id, result, name)
if result.startswith("imported"):
stats["imported"] += 1
elif "duplicate" in result:
stats["duplicate"] += 1
elif "quota" in result:
stats["quota_exhausted"] += 1
log.error("Backend quota exhausted. Stop at %d/%d.", i, len(QUERIES))
break
elif result.startswith("HTTP") or result.startswith("request_error"):
stats["error"] += 1
time.sleep(SLEEP_BETWEEN_QUERIES_S)
log.info("DONE. stats=%s", json.dumps(stats))
return 0
if __name__ == "__main__":
sys.exit(main())