Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| """Local dev: optionally free API port(s), then delete all user profiles from SQLite. | |
| Does **not** delete kinks, catalog edges, or FetLife crawl tables — only user-scoped rows. | |
| Examples: | |
| python scripts/dev_reset_local.py --db data/store.db --dry-run | |
| python scripts/dev_reset_local.py --db data/store.db --port 8011 --i-am-sure | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import os | |
| import sqlite3 | |
| import subprocess | |
| import sys | |
| from pathlib import Path | |
| _DEFAULT_PORTS = (8011,) | |
| def _kill_ports(ports: list[int], *, dry_run: bool) -> None: | |
| for port in ports: | |
| if dry_run: | |
| print(f"[dry-run] would attempt to free TCP port {port}") | |
| continue | |
| # fuser is common on Linux; lsof fallback for macOS-ish environments. | |
| r = subprocess.run( | |
| ["fuser", "-k", f"{port}/tcp"], | |
| capture_output=True, | |
| text=True, | |
| ) | |
| if r.returncode == 0: | |
| print(f"freed port {port} (fuser)") | |
| continue | |
| r2 = subprocess.run(["lsof", f"-ti:{port}"], capture_output=True, text=True) | |
| pids = [int(pid) for pid in r2.stdout.split() if pid.isdigit()] | |
| for pid in pids: | |
| os.kill(pid, 9) | |
| if pids: | |
| print(f"freed port {port} (lsof)") | |
| else: | |
| print(f"note: no listener freed on port {port} (fuser/lsof)", file=sys.stderr) | |
| def _wipe_all_users(conn: sqlite3.Connection) -> None: | |
| """Delete user-linked rows then users (order safe for typical FK graphs).""" | |
| conn.executescript( | |
| """ | |
| DELETE FROM partnergroupmember; | |
| DELETE FROM partnergroup; | |
| DELETE FROM partnerlinkrequest; | |
| DELETE FROM partnerlink; | |
| DELETE FROM scenariopreference; | |
| DELETE FROM promptdismissal; | |
| DELETE FROM rolepreference; | |
| DELETE FROM playpreference; | |
| DELETE FROM userpreference; | |
| DELETE FROM user; | |
| """ | |
| ) | |
| def main() -> int: | |
| ap = argparse.ArgumentParser(description=__doc__) | |
| ap.add_argument("--db", type=Path, required=True, help="path to SQLite store") | |
| ap.add_argument( | |
| "--port", | |
| type=int, | |
| action="append", | |
| default=[], | |
| help="TCP port to try to kill (repeatable); default 8011 if none given", | |
| ) | |
| ap.add_argument("--dry-run", action="store_true", help="print actions only; no DB or port changes") | |
| ap.add_argument( | |
| "--i-am-sure", | |
| action="store_true", | |
| help="required with --dry-run false to apply destructive DB deletes", | |
| ) | |
| ap.add_argument( | |
| "--skip-port-kill", | |
| action="store_true", | |
| help="do not run fuser/lsof (use in CI / when only DB wipe is wanted)", | |
| ) | |
| args = ap.parse_args() | |
| db = args.db.resolve() | |
| if not db.is_file(): | |
| print(f"database not found: {db}", file=sys.stderr) | |
| return 1 | |
| ports = [] if args.skip_port_kill else (args.port if args.port else list(_DEFAULT_PORTS)) | |
| if args.dry_run: | |
| print(f"[dry-run] db={db}") | |
| if ports: | |
| _kill_ports(ports, dry_run=True) | |
| else: | |
| print("[dry-run] skip port kill") | |
| print("[dry-run] would DELETE all user-scoped tables then user rows (see script source)") | |
| return 0 | |
| if not args.i_am_sure: | |
| print("Refusing to modify database without --i-am-sure (use --dry-run to preview).", file=sys.stderr) | |
| return 2 | |
| if ports: | |
| _kill_ports(ports, dry_run=False) | |
| conn = sqlite3.connect(db) | |
| try: | |
| conn.execute("PRAGMA foreign_keys=OFF") | |
| _wipe_all_users(conn) | |
| conn.commit() | |
| finally: | |
| conn.execute("PRAGMA foreign_keys=ON") | |
| conn.close() | |
| print(f"wrote user wipe to {db}") | |
| return 0 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |