""" reset_environment.py — DESTRUCTIVE wipe of all non-permanent data. Clears: - Neo4j: drops cc.users.*, classroomcopilot; wipes cc.institutes.* content - Supabase: deletes all test auth users + profiles + memberships - Supabase: detaches kcar from any school Safe invariants (never touched): - gaisdata Neo4j DB - system / neo4j Neo4j DBs - kcar auth account and admin_profiles entry - institutes rows (schools themselves are kept, just de-seeded) Run from inside the ccapi container: python3 -c "from run.initialization.reset_environment import reset; reset()" """ import os import time import requests from typing import List, Dict, Any from modules.logger_tool import initialise_logger from modules.database.services.neo4j_service import Neo4jService import modules.database.tools.neo4j_driver_tools as dt logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), "default", True) KCAR_ID = "d9e1d1a9-04c4-4611-bb05-57babf4a9a28" KCAR_EMAIL = "kcar@kevlarai.com" # Databases to fully DROP (content + structure) DBS_TO_DROP = [ "classroomcopilot", "cc.users", ] # Institute DBs — wipe content only (keep the DB, re-provision in seed) INSTITUTE_DB_PREFIXES = ["cc.institutes."] # Supabase connection details (direct REST, no SDK needed for admin auth ops) def _sb_headers(): url = os.environ["SUPABASE_URL"] key = os.environ["SERVICE_ROLE_KEY"] return url, {"apikey": key, "Authorization": f"Bearer {key}", "Content-Type": "application/json"} def _neo4j_drop_all_matching(pattern: str) -> List[str]: """Drop every Neo4j database whose name starts with pattern.""" dropped = [] with dt.get_session(database="system") as s: all_dbs = [r["name"] for r in s.run("SHOW DATABASES YIELD name RETURN name")] targets = [db for db in all_dbs if db.startswith(pattern)] for db in targets: logger.info(f" DROP DATABASE `{db}`") with dt.get_session(database="system") as s: s.run(f"DROP DATABASE `{db}` IF EXISTS") dropped.append(db) return dropped def _neo4j_wipe_institute_dbs() -> List[str]: """MATCH (n) DETACH DELETE on every cc.institutes.* database.""" wiped = [] with dt.get_session(database="system") as s: all_dbs = [r["name"] for r in s.run("SHOW DATABASES YIELD name RETURN name")] targets = [db for db in all_dbs if any(db.startswith(p) for p in INSTITUTE_DB_PREFIXES) and not db.endswith(".curriculum")] for db in targets: logger.info(f" Wipe cc.institutes DB: {db}") with dt.get_session(database=db) as s: s.run("MATCH (n) DETACH DELETE n") wiped.append(db) # Also wipe curriculum DBs with dt.get_session(database="system") as s: all_dbs = [r["name"] for r in s.run("SHOW DATABASES YIELD name RETURN name")] curriculum_dbs = [db for db in all_dbs if db.endswith(".curriculum")] for db in curriculum_dbs: logger.info(f" Wipe curriculum DB: {db}") with dt.get_session(database=db) as s: s.run("MATCH (n) DETACH DELETE n") wiped.append(db) return wiped def _supabase_list_auth_users(url: str, headers: dict) -> List[Dict]: r = requests.get(f"{url}/auth/v1/admin/users", headers=headers, params={"per_page": 200}) r.raise_for_status() return r.json().get("users", []) def _supabase_delete_auth_user(url: str, headers: dict, uid: str): r = requests.delete(f"{url}/auth/v1/admin/users/{uid}", headers=headers) if r.status_code not in (200, 204): logger.warning(f" Delete auth user {uid}: {r.status_code} {r.text[:100]}") def reset() -> Dict[str, Any]: logger.info("=" * 60) logger.info("RESET ENVIRONMENT — destructive wipe starting") logger.info("=" * 60) results: Dict[str, Any] = {} # ── 1. Neo4j: drop cc.users.* and classroomcopilot ─────────────────────── logger.info("\n[Neo4j] Dropping cc.users.* databases...") dropped = _neo4j_drop_all_matching("cc.users") logger.info(f" Dropped: {dropped}") logger.info("[Neo4j] Dropping classroomcopilot...") with dt.get_session(database="system") as s: s.run("DROP DATABASE `classroomcopilot` IF EXISTS") logger.info(" Done") # ── 2. Neo4j: wipe institute DB content ────────────────────────────────── logger.info("[Neo4j] Wiping cc.institutes.* content...") wiped = _neo4j_wipe_institute_dbs() logger.info(f" Wiped: {wiped}") results["neo4j"] = {"dropped": dropped, "wiped": wiped} # ── 3. Supabase: detach kcar from school ────────────────────────────────── logger.info("\n[Supabase] Detaching kcar from school...") url, headers = _sb_headers() requests.patch( f"{url}/rest/v1/profiles", headers={**headers, "Prefer": "return=minimal"}, params={"id": f"eq.{KCAR_ID}"}, json={"school_id": None}, ) requests.delete( f"{url}/rest/v1/institute_memberships", headers=headers, params={"profile_id": f"eq.{KCAR_ID}"}, ) logger.info(" kcar detached") # ── 4. Supabase: delete all test users except kcar ──────────────────────── logger.info("[Supabase] Deleting test auth users...") all_users = _supabase_list_auth_users(url, headers) deleted_emails = [] for u in all_users: if u["email"] == KCAR_EMAIL: continue _supabase_delete_auth_user(url, headers, u["id"]) deleted_emails.append(u["email"]) time.sleep(0.1) logger.info(f" Deleted {len(deleted_emails)} auth users") # profiles + memberships cascade via FK on auth.users deletion (Supabase handles it) # but clean up explicitly to be safe requests.delete( f"{url}/rest/v1/profiles", headers=headers, params={"id": f"neq.{KCAR_ID}"}, ) requests.delete( f"{url}/rest/v1/institute_memberships", headers=headers, params={"profile_id": f"neq.{KCAR_ID}"}, ) results["supabase"] = {"deleted_users": deleted_emails} logger.info("\n" + "=" * 60) logger.info("RESET COMPLETE") logger.info("=" * 60) return results if __name__ == "__main__": import json print(json.dumps(reset(), indent=2, default=str))