Files
nicholas 816b28e179
Build & Deploy Formulation / Build & Push Image (push) Successful in 15s
Build & Deploy Formulation / deploy (push) Successful in 12s
build and deploy Formulation application
Consolidate the application, culinary data model, read-only runtime, and registry-backed deployment.
2026-08-13 18:08:25 -05:00

117 lines
4.2 KiB
Python
Executable File
Raw Permalink 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
"""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())