#!/usr/bin/env python3 """Upload a built site model and make it the site's active model. python3 scripts/site-model/upload.py --site --label "Bushveld Joy Road — RD LO Rev 1" /tmp/site-model-hills add --dry-run to only print the key report. Creates a site_models row (status active) with the five JSON files, archives the previous active model, and prints progress keys recorded on the site that the new model no longer has. """ import argparse import json import sys import uuid import urllib.request from datetime import datetime, timezone from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) sys.path.insert(0, str(Path(__file__).resolve().parent)) from pb_env import PB, http, superuser_token # noqa: E402 from model_keys import model_keys # noqa: E402 FILES = ["bushveld", "services", "earthworks", "schedules", "surface"] def multipart(fields, files): boundary = uuid.uuid4().hex out = b"" for name, value in fields.items(): out += f'--{boundary}\r\nContent-Disposition: form-data; name="{name}"\r\n\r\n{value}\r\n'.encode() for field, filename, data, ctype in files: out += f'--{boundary}\r\nContent-Disposition: form-data; name="{field}"; filename="{filename}"\r\nContent-Type: {ctype}\r\n\r\n'.encode() out += data + b"\r\n" out += f"--{boundary}--\r\n".encode() return out, f"multipart/form-data; boundary={boundary}" def orphaned(existing_keys, new_keys): return sorted(existing_keys - new_keys) def existing_progress_keys(token, site): keys, page = set(), 1 while True: code, res = http("GET", f"/api/collections/site_progress_events/records?page={page}&perPage=500&fields=key&filter=" + urllib.request.quote(f"site='{site}'"), token=token) if code != 200: raise RuntimeError(f"Reading progress failed: HTTP {code}") keys.update(r["key"] for r in res["items"]) if page >= res["totalPages"]: return keys page += 1 def main(): ap = argparse.ArgumentParser() ap.add_argument("--site", required=True) ap.add_argument("--label", required=True) ap.add_argument("--dry-run", action="store_true") ap.add_argument("folder") args = ap.parse_args() folder = Path(args.folder) missing = [f for f in FILES if not (folder / f"{f}.json").exists()] if missing: sys.exit(f"Missing model files: {missing}") token = superuser_token() new_keys = set(model_keys(folder)) gone = orphaned(existing_progress_keys(token, args.site), new_keys) print(f"{len(new_keys)} progress items in the new model") if gone: print(f"WARNING: {len(gone)} recorded progress keys are not in the new model:") for k in gone[:50]: print(" ", k) if args.dry_run: return road = json.loads((folder / "bushveld.json").read_text()) sources = {"folder": str(folder)} if (folder / "sources.json").exists(): sources["manifest"] = json.loads((folder / "sources.json").read_text()) stats = {"asphalt_m2": round(sum(a["area"] for a in road["areas"] if a["kind"] == "asphalt"), 1), "sections": len(road["sections"])} body, ctype = multipart( {"site": args.site, "label": args.label, "status": "active", "built_at": datetime.now(timezone.utc).isoformat(), "sources": json.dumps(sources), "stats": json.dumps(stats)}, [("files", f"{f}.json", (folder / f"{f}.json").read_bytes(), "application/json") for f in FILES]) req = urllib.request.Request(f"{PB}/api/collections/site_models/records", data=body, method="POST") req.add_header("Content-Type", ctype); req.add_header("Authorization", token) with urllib.request.urlopen(req) as r: created = json.loads(r.read().decode()) print(f"Created model {created['id']} ({args.label}) — active") code, res = http("GET", "/api/collections/site_models/records?perPage=200&filter=" + urllib.request.quote(f"site='{args.site}' && status='active' && id!='{created['id']}'"), token=token) for old in res.get("items", []): http("PATCH", f"/api/collections/site_models/records/{old['id']}", {"status": "archived"}, token) print(f"Archived previous model {old['id']}") if __name__ == "__main__": main()