#!/usr/bin/env python3 """Export true/false retrieval rows from Supabase. Writes a compact RAG CSV for the custom-topic-quiz-generator backend. The API embeds and searches ``Question: ...\nAnswer: True/False`` while preserving source, category, and difficulty metadata for filtering. The Edge Function/database still perform final live validation before serving questions to the app. """ from __future__ import annotations import csv import os import sys import tempfile from pathlib import Path from typing import Dict, Iterable, List import requests SOURCE_FIELDS = ( "id", "question", "answer", "correct_answer", "source_id", "category_id", "difficulty", "is_deleted", ) OUTPUT_FIELDS = ( "id", "question", "answer", "source_id", "category_id", "difficulty", "is_deleted", ) PAGE_SIZE = 1000 def require_env(name: str) -> str: value = os.getenv(name, "").strip() if not value: raise RuntimeError(f"Missing required environment variable: {name}") return value def fetch_rows(base_url: str, service_key: str) -> Iterable[Dict[str, object]]: endpoint = f"{base_url.rstrip('/')}/rest/v1/true_false_questions" offset = 0 headers = { "apikey": service_key, "Authorization": f"Bearer {service_key}", "Accept": "application/json", "Range-Unit": "items", } while True: response = requests.get( endpoint, headers={ **headers, "Range": f"{offset}-{offset + PAGE_SIZE - 1}", }, params={ "select": ",".join(SOURCE_FIELDS), "is_deleted": "eq.false", "order": "id.asc", }, timeout=60, ) response.raise_for_status() rows = response.json() if not isinstance(rows, list): raise RuntimeError("Supabase returned a non-list response.") for row in rows: if isinstance(row, dict): yield row if len(rows) < PAGE_SIZE: break offset += PAGE_SIZE def _positive_int(value: object, field: str, row_number: int) -> int: try: parsed = int(str(value).strip()) except (TypeError, ValueError) as exc: raise RuntimeError( f"Row {row_number}: invalid {field} {value!r}." ) from exc if parsed <= 0: raise RuntimeError(f"Row {row_number}: {field} must be positive.") return parsed def _answer_text(row: Dict[str, object], row_number: int) -> str: raw = str(row.get("answer") or row.get("correct_answer") or "").strip().lower() if raw in {"true", "t", "1", "yes", "y"}: return "True" if raw in {"false", "f", "0", "2", "no", "n"}: return "False" raise RuntimeError( f"Row {row_number}: answer/correct_answer must resolve to true or false." ) def prepare_rows(rows: Iterable[Dict[str, object]]) -> List[Dict[str, object]]: prepared: List[Dict[str, object]] = [] seen = set() for row_number, row in enumerate(rows, start=2): question_id = _positive_int(row.get("id"), "id", row_number) if question_id in seen: raise RuntimeError(f"Row {row_number}: duplicate id {question_id}.") question = str(row.get("question") or "").strip() if not question: raise RuntimeError(f"Row {row_number}: blank question.") if row.get("is_deleted") is True: raise RuntimeError( f"Row {row_number}: deleted true/false question {question_id} was exported." ) prepared.append( { "id": question_id, "question": question, "answer": _answer_text(row, row_number), "source_id": row.get("source_id"), "category_id": row.get("category_id"), "difficulty": row.get("difficulty"), "is_deleted": False, } ) seen.add(question_id) if not prepared: raise RuntimeError("The export contains no active true/false questions.") return prepared def write_atomic(rows: List[Dict[str, object]], output: Path) -> None: output.parent.mkdir(parents=True, exist_ok=True) with tempfile.NamedTemporaryFile( "w", encoding="utf-8", newline="", dir=output.parent, prefix=f".{output.name}.", suffix=".tmp", delete=False, ) as handle: temp_path = Path(handle.name) writer = csv.DictWriter( handle, fieldnames=OUTPUT_FIELDS, extrasaction="ignore", ) writer.writeheader() writer.writerows(rows) os.replace(temp_path, output) def main() -> int: try: base_url = require_env("QUIZ_EXPORT_SUPABASE_URL") service_key = require_env("QUIZ_EXPORT_SERVICE_ROLE_KEY") output = Path( os.getenv( "TRUE_FALSE_EXPORT_OUTPUT", "sources/custom-topic-quiz-generator/true_false_questions.csv", ) ).resolve() rows = prepare_rows(fetch_rows(base_url, service_key)) write_atomic(rows, output) print(f"Exported {len(rows)} active true/false rows to {output}") return 0 except Exception as error: print(f"Export failed: {error}", file=sys.stderr) return 1 if __name__ == "__main__": raise SystemExit(main())