#!/usr/bin/env python3 """Wipe all PocketBase rows for one site. Run BEFORE re-importing The Hills ext 12 from Buildsite for the year. Destructive — requires explicit --yes flag. Backs up every row's full record to /tmp before deletion so a mid-clear crash still leaves something to inspect. PB batch DELETE returns 404 on these collections, so this script parallelizes single-record deletes via threads. Usage: python3 scripts/clear-site-rows.py --yes """ import json import sys import urllib.error import urllib.request from concurrent.futures import ThreadPoolExecutor, as_completed from pathlib import Path from pb_env import http, PB, superuser_token if len(sys.argv) < 3 or sys.argv[2] != "--yes": sys.exit("REFUSING: pass --yes to confirm destructive wipe") SITE_ID = sys.argv[1] BACKUP_DIR = Path(f"/tmp/pb-clear-{SITE_ID}") BACKUP_DIR.mkdir(exist_ok=True) TOKEN = superuser_token() def list_all(collection: str, site_filter: str) -> list[dict]: items: list[dict] = [] page = 1 while True: url = ( f"/api/collections/{collection}/records" f"?filter={site_filter}&fields=*&perPage=500&page={page}" ) code, body = http("GET", url, token=TOKEN) if code != 200: sys.exit(f"list {collection} page {page} failed: {code} {body}") page_items = body.get("items", []) items.extend(page_items) if len(page_items) < 500: break page += 1 return items def delete_one(collection: str, rid: str) -> tuple[str, int]: req = urllib.request.Request( f"{PB}/api/collections/{collection}/records/{rid}", method="DELETE", headers={"Authorization": TOKEN}, ) try: with urllib.request.urlopen(req) as r: return rid, r.status except urllib.error.HTTPError as e: return rid, e.code def parallel_delete(collection: str, ids: list[str], workers: int = 16) -> tuple[int, list]: deleted = 0 errors: list = [] with ThreadPoolExecutor(max_workers=workers) as ex: futures = {ex.submit(delete_one, collection, rid): rid for rid in ids} for fut in as_completed(futures): rid, code = fut.result() if code == 204: deleted += 1 else: errors.append({"id": rid, "code": code}) return deleted, errors def wipe(collection: str) -> None: filter_q = f"site='{SITE_ID}'" print(f" listing {collection} for site {SITE_ID}...") rows = list_all(collection, filter_q) print(f" found {len(rows)} rows") if not rows: return backup_path = BACKUP_DIR / f"{collection}.json" backup_path.write_text(json.dumps(rows, indent=2, default=str)) print(f" backed up to {backup_path}") ids = [r["id"] for r in rows] deleted, errors = parallel_delete(collection, ids) print(f" deleted {deleted}/{len(ids)} rows") if errors: err_path = BACKUP_DIR / f"{collection}-delete-errors.json" err_path.write_text(json.dumps(errors, indent=2)) print(f" ERRORS written to {err_path}") sys.exit(1) print(f"=== CLEAR SITE {SITE_ID} ===") print(f"backup dir: {BACKUP_DIR}") # fuel_logs reference vehicles — delete children first. wipe("fuel_logs") wipe("vehicles") print("=== verifying ===") for coll in ("fuel_logs", "vehicles"): code, body = http( "GET", f"/api/collections/{coll}/records?filter=site='{SITE_ID}'&fields=id&perPage=1", token=TOKEN, ) if code != 200: sys.exit(f"verify {coll} failed: {code} {body}") remaining = body.get("totalItems", 0) print(f" {coll}: {remaining} remaining") if remaining != 0: sys.exit(f"!! {coll} still has {remaining} rows for site {SITE_ID}") print("=== DONE ===")