#!/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())