Files
Meal-Planner/scripts/seed_recipes_round2.py
T
admin 2695ba0232
CI / backend (pytest + alembic) (push) Canceled after 0s
CI / frontend (build) (push) Canceled after 0s
feat(meals): enforce three lower-calorie dinners with media
2026-09-01 10:11:36 -07:00

163 lines
5.6 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:
log.error(
"Retired: direct Spoonacular publication is disabled. "
"Use the staged Wikibooks import workflow."
)
return 2
def _legacy_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())