#!/usr/bin/env python3 """Export canonical culinary recipes as clean Markdown knowledge documents.""" from __future__ import annotations import argparse import shutil import tempfile from pathlib import Path import yaml ROOT = Path(__file__).resolve().parent.parent CULINARY_DIR = ROOT / "culinary" DEFAULT_OUTPUT = ROOT / "generated/open-webui/recipes" def load_entities(directory: Path) -> dict[str, dict]: entities = {} for path in sorted(directory.glob("*.yaml")): data = yaml.safe_load(path.read_text(encoding="utf-8")) entity_id = data["id"] if entity_id in entities: raise ValueError(f"duplicate ID {entity_id!r} in {directory}") entities[entity_id] = data return entities def amount_text(amount: dict | None) -> str: if not amount: return "—" display = amount.get("display") if display: return display quantity = amount["quantity"] maximum = amount.get("quantity_max") value = f"{quantity:g}" if isinstance(quantity, float) else str(quantity) if maximum is not None: upper = f"{maximum:g}" if isinstance(maximum, float) else str(maximum) value = f"{value}–{upper}" return f"{value} {amount['unit_id'].replace('_', ' ')}" def render_recipe(recipe: dict, ingredients: dict[str, dict], recipes: dict[str, dict]) -> str: lines = [f"# {recipe['title']}", ""] lines.extend( [ f"Recipe ID: `{recipe['id']}`", f"Categories: {', '.join(recipe['categories']) or 'None'}", f"Tags: {', '.join(recipe['tags']) or 'None'}", f"Yield: {amount_text(recipe['yield']['amount'])}", ] ) if recipe["yield"].get("servings") is not None: lines.append(f"Servings: {recipe['yield']['servings']:g}") if recipe["yield"].get("description"): lines.append(f"Yield description: {recipe['yield']['description']}") lines.append("") for component in recipe["components"]: lines.extend([f"## {component['name']}", "", "| Item | Amount | Formula percentage |", "| --- | ---: | ---: |"]) for item in component["items"]: reference = item["reference"] if "ingredient_id" in reference: item_id = reference["ingredient_id"] label = ingredients[item_id]["name"] else: item_id = reference["recipe_id"] label = f"{recipes[item_id]['title']} (sub-recipe)" percentage = f"{item['percentage']:g}%" if "percentage" in item else "—" lines.append(f"| {label} | {amount_text(item['amount'])} | {percentage} |") if component.get("notes"): lines.extend(["", *[f"- {note}" for note in component["notes"]]]) lines.append("") lines.extend(["## Preparation", ""]) for step in sorted(recipe["steps"], key=lambda value: value["order"]): lines.append(f"{step['order']}. {step['instruction']}") if recipe.get("notes"): lines.extend(["", "## Notes", "", *[f"- {note}" for note in recipe["notes"]]]) return "\n".join(lines).rstrip() + "\n" def parse_args(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("output", nargs="?", type=Path, default=DEFAULT_OUTPUT) return parser.parse_args() def main() -> int: args = parse_args() output = args.output.resolve() if output in {Path("/"), ROOT}: raise ValueError(f"refusing unsafe output directory: {output}") ingredients = load_entities(CULINARY_DIR / "ingredients") recipes = load_entities(CULINARY_DIR / "recipes") output.parent.mkdir(parents=True, exist_ok=True) staging = Path(tempfile.mkdtemp(prefix="recipe-corpus-", dir=output.parent)) try: for recipe_id, recipe in recipes.items(): (staging / f"{recipe_id}.md").write_text( render_recipe(recipe, ingredients, recipes), encoding="utf-8" ) if output.exists(): shutil.rmtree(output) staging.replace(output) except Exception: shutil.rmtree(staging, ignore_errors=True) raise print(f"Exported {len(recipes)} canonical recipes to {output}") return 0 if __name__ == "__main__": raise SystemExit(main())