- timetable_builder_router: Supabase-primary slot write (POST /timetable/slots),
week_cycle support, GET /slots reads from Supabase, materialize-periods endpoint,
rebuild-neo4j endpoint, sync-lessons endpoint (Track B: TaughtLesson Neo4j nodes),
_sync_teacher_timetables_to_neo4j and _sync_taught_lessons_to_neo4j helpers
- classes_router: GET /{class_id} enriched with profiles + enrollment_requests,
GET /school/students for admin search, PATCH /enrollment-requests/{id} approve/reject
- taught_lessons_router: GET /student/lessons student week view with enrichment
- school_router: academic_periods sync, day-type management
- platform_admin_router + platform_admin: POST /admin/reset and /admin/seed endpoints
- invitations_router: teacher invite scaffolding
- reset_environment + seed_environment: idempotent dev environment scripts
- graph_tree_router: Supabase-first institute resolution
- provisioning_service: neo4j_private_db_name column support
- main.py + run/routers.py: register new routers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
173 lines
6.4 KiB
Python
173 lines
6.4 KiB
Python
"""
|
|
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))
|