munch-ease-backend/scripts/manual_parse_recipes.py

134 lines
4.8 KiB
Python
Raw Normal View History

2025-11-04 07:31:13 +00:00
"""
Manual harness to exercise recipes.parse_recipe against specific public URLs.
This WILL NOT run in CI or with `make test` run it explicitly:
make install # first time
python -m scripts.manual_parse_recipes
Notes:
- Creates/bootstraps a local SQLite DB (./data/doof.sqlite) for lookups.
- Does not persist recipes; it just prints parsed results.
- Network access required to fetch pages.
"""
import asyncio
from types import SimpleNamespace
from typing import Any, Dict, List
from db import connect, create
from recipes import parse_recipe
from scripts.recipe_urls import URLS
import os
# This harness streams results as they complete and keeps per-URL logs grouped.
def _fmt(v) -> str:
if v is None:
return "-"
return str(v)
def _print_recipe(r) -> None:
print("\n=== Parsed Recipe ===")
print(f"name: {_fmt(getattr(r, 'name', None))}")
print(f"serves: {_fmt(getattr(r, 'serves', None))}")
print(f"link: {_fmt(getattr(r, 'link', None))}")
imgs = getattr(r, "image_urls", []) or []
if imgs:
print(f"images[0]: {imgs[0]}")
ings = getattr(r, "ingredients", []) or []
print(f"ingredients: {len(ings)}")
for ing in ings[:10]:
# Each is an Ingredient model with line/name/quantity/unit
n = getattr(ing, "name", "")
q = getattr(ing, "quantity", "")
u = getattr(ing, "unit", "")
line = getattr(ing, "line", "")
print(f" - {n} ({q} {u}) :: {line}")
async def _parse_one(conn, url: str, dump_dir: str | None) -> Dict[str, Any]:
# Minimal created_by object the parser expects (id, display_name)
created_by = SimpleNamespace(id=1, display_name="Manual Tester")
logs: List[str] = []
def _log(msg: str):
# Collect per-URL logs; we'll print them later in a grouped section
logs.append(msg)
try:
r = await parse_recipe(conn, created_by, url, log=_log, dump_dir=dump_dir)
except Exception as e:
return {"url": url, "recipe": None, "logs": logs, "error": str(e)}
return {"url": url, "recipe": r, "logs": logs, "error": None}
async def main() -> None:
conn = await connect()
# Ensure schema exists for product matching in ingredient parsing
await create(conn)
try:
dump_dir = os.environ.get("SCRAPER_DUMP_DIR") or None
concurrency = min(8, len(URLS))
sem = asyncio.Semaphore(concurrency)
# Consume all available slots and stagger release initially to avoid bursts
for _ in range(concurrency):
await sem.acquire()
# Release all slots with slight delays to avoid thundering herd
for i in range(concurrency):
asyncio.get_event_loop().call_later(i * 0.5, sem.release)
async def _worker(u: str):
async with sem:
return await _parse_one(conn, u, dump_dir)
start_time = asyncio.get_event_loop().time()
tasks = [asyncio.create_task(_worker(url)) for url in URLS]
# Stream results as they arrive; print per-URL blocks to avoid jumbled output
totals = {"total": 0, "parsed": 0, "errors": 0, "no_recipe": 0}
for task in asyncio.as_completed(tasks):
res = await task
url = res["url"]
totals["total"] += 1
print("\n==============================")
print(f"Result: {url}")
print("==============================")
if res.get("error"):
totals["errors"] += 1
print(f"ERROR: failed to parse {url}:")
print(f" {res['error']}")
r = res.get("recipe")
if r is None and not res.get("error"):
totals["no_recipe"] += 1
print(f"WARN: no recipe data found at {url}")
if r is not None:
totals["parsed"] += 1
_print_recipe(r)
# Logs
logs: List[str] = res.get("logs") or []
# Print logs for failures only to reduce noise; clip to a reasonable limit
if (r is None or res.get("error")) and logs:
clip = logs[:200]
print("-- logs --")
for line in clip:
print(f" dbg: {line}")
if len(logs) > len(clip):
print(f" .. ({len(logs) - len(clip)} more lines clipped) ..")
print("\n=== Summary ===")
print(f"Total URLs processed: {totals['total']}")
print(f"Successfully parsed: {totals['parsed']}")
print(f"Errors: {totals['errors']}")
print(f"No recipe found: {totals['no_recipe']}")
print(f"Time elapsed: {asyncio.get_event_loop().time() - start_time:.2f} seconds")
print(f"Remaining URLs: {len(URLS) - totals['total']}")
finally:
await conn.close()
if __name__ == "__main__":
asyncio.run(main())