diff --git a/scripts/seed_recipes_round2.py b/scripts/seed_recipes_round2.py new file mode 100755 index 0000000..9bee9c7 --- /dev/null +++ b/scripts/seed_recipes_round2.py @@ -0,0 +1,154 @@ +#!/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", "") + 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())