#!/usr/bin/env python3 """Backfill the ``beer_runs`` metadata collection from the existing readings. The ``beer_data`` collection stores a beer name on *each reading*, and that name drifts across a run — run 4 was entered as "Necatron-Christmas Beer", then "Nocturna the Christmas beer", then "Nocturna Christmas beer". The ``beer_runs`` collection holds one canonical name per run, which is what the active-beers page and the analysis pages use. This script populates that collection from the readings. It is **idempotent** and **non-destructive**: - It only ever inserts into ``beer_runs``. It never touches ``beer_data``. - It never overwrites a name you have already set by hand. A run whose metadata already carries a ``name`` is left alone unless ``--force`` is given. - Re-running it produces the same result. Usage ----- python scripts/backfill_run_metadata.py # dry run python scripts/backfill_run_metadata.py --apply # write it python scripts/backfill_run_metadata.py --apply --force # overwrite names The connection is taken from the same environment variables the app uses: ``MONGO_HOST``, ``MONGO_USER``, ``MONGO_PASS`` (or ``MONGO_URI``). """ from __future__ import annotations import argparse import os import sys sys.path.insert( 0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "src", "flask"), ) from brewing import Run # noqa: E402 from mongo.repository import BeerRepository # noqa: E402 def build_plan(repo: BeerRepository) -> list[dict]: """Work out what should be in ``beer_runs``, one entry per run.""" existing = repo.all_run_metadata() plan = [] for run in repo.runs(): metadata = existing.get(run.beer_run_id, {}) canonical = metadata.get("name") or run.display_name() og = metadata.get("og_points") or run.original_gravity() plan.append({ "beer_run_id": run.beer_run_id, "name": canonical, "og_points": og, "tap_number": metadata.get("tap_number"), "notes": metadata.get("notes", ""), "readings": len(run.readings), "active": run.is_active(), "existing_name": metadata.get("name"), "would_change": not metadata.get("name"), }) return plan def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--apply", action="store_true", help="write the changes (default is a dry run)") parser.add_argument("--force", action="store_true", help="overwrite names that have already been set") args = parser.parse_args() repo = BeerRepository() try: plan = build_plan(repo) finally: repo.client.close() if not plan: print("No runs found in the readings collection — nothing to do.") return 0 print(f"{'run':>5} {'name':<38} {'OG':>5} {'readings':>8} state") print("-" * 78) for entry in plan: state = "active" if entry["active"] else "finished" marker = " " if entry["would_change"] else "*" print(f"{entry['beer_run_id']:>5} {entry['name'][:38]:<38} " f"{entry['og_points'] or '-':>5} {entry['readings']:>8} {state}{marker}") changed = [e for e in plan if e["would_change"] or args.force] kept = [e for e in plan if not e["would_change"] and not args.force] print() print(f"{len(changed)} run(s) would be written, {len(kept)} kept as they are.") if kept: print("(* already has a name set by hand — use --force to overwrite)") print(f"{'APPLYING' if args.apply else 'DRY RUN — nothing written'}") print() if not args.apply: print("Re-run with --apply to write these.") return 0 repo = BeerRepository() try: for entry in changed: repo.upsert_run_metadata_partial( entry["beer_run_id"], name=entry["name"], og_points=entry["og_points"], tap_number=entry["tap_number"], notes=entry["notes"] or None, ) print(f" wrote run {entry['beer_run_id']}: {entry['name']}") finally: repo.client.close() print(f"\nDone — {len(changed)} run(s) written to the beer_runs collection.") return 0 if __name__ == "__main__": raise SystemExit(main())