FInFront / utils /api.py
lanna_lalala;-
trying to fix prefix
95ec11d
# utils/api.py
import os
import json
import logging
import requests
from urllib3.util.retry import Retry
from requests.adapters import HTTPAdapter
# ---- Setup ----
BACKEND = (os.getenv("BACKEND_URL") or "").strip().rstrip("/")
if not BACKEND:
raise RuntimeError("BACKEND_URL is not set in Space secrets.")
TOKEN = (os.getenv("BACKEND_TOKEN") or os.getenv("HF_TOKEN") or "").strip()
DEFAULT_TIMEOUT = int(os.getenv("BACKEND_TIMEOUT", "120"))
_session = requests.Session()
retry = Retry(
total=3,
connect=3,
read=3,
backoff_factor=0.5,
status_forcelist=(429, 500, 502, 503, 504),
allowed_methods=frozenset(["GET", "POST", "PUT", "PATCH", "DELETE"]),
)
_session.mount("https://", HTTPAdapter(max_retries=retry))
_session.mount("http://", HTTPAdapter(max_retries=retry))
_session.headers.update({
"Accept": "application/json, */*;q=0.1",
"User-Agent": "FInFront/1.0 (+spaces)",
})
if TOKEN:
_session.headers["Authorization"] = f"Bearer {TOKEN}"
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# --- Optional API prefix (e.g., "/api")
API_PREFIX_ENV = (os.getenv("BACKEND_API_PREFIX") or "").strip().rstrip("/")
def _prefixes():
return ["/" + API_PREFIX_ENV.strip("/"), ""] if API_PREFIX_ENV else [""]
# ---- Core helpers ----
def _json_or_raise(resp: requests.Response):
ctype = (resp.headers.get("content-type") or "").lower()
if "application/json" in ctype:
return resp.json()
try:
return resp.json()
except Exception:
snippet = (resp.text or "")[:300]
raise RuntimeError(f"Expected JSON but got {ctype or 'unknown'}:\n{snippet}")
def _req(method: str, path: str, **kw):
if not path.startswith("/"):
path = "/" + path
for pref in _prefixes():
url = f"{BACKEND}{pref}{path}"
try:
r = _session.request(method, url, timeout=180, **kw)
r.raise_for_status()
return r
except requests.HTTPError as e:
status = getattr(e.response, "status_code", "?")
# try next prefix on 404
if status == 404:
continue
# ---------- friendlier auth errors ----------
if status in (401, 403):
# Normal bad credentials on the login endpoint
if path.endswith("/auth/login"):
raise RuntimeError("Incorrect email or password.") from e
# Generic auth issue elsewhere (expired session, etc.)
raise RuntimeError("Authentication failed. Please sign in again.") from e
# -------------------------------------------
# keep a tiny body for other errors
body = ""
try:
body = e.response.text[:500]
except Exception:
pass
raise RuntimeError(f"{method} {url} failed [{status}]: {body}") from e
except requests.RequestException:
# try next prefix
continue
raise RuntimeError(f"No matching endpoint for {method} {path} with prefixes {list(_prefixes())}")
# ---- Public helpers ----
def health():
try:
return _json_or_raise(_req("GET", "/health"))
except Exception:
try:
_req("GET", "/")
return {"ok": True}
except Exception:
return {"ok": False}
# ---------- Auth ----------
def login(email: str, password: str):
return _json_or_raise(_req("POST", "/auth/login", json={"email": email, "password": password}))
def signup_student(name: str, email: str, password: str, level_label: str, country_label: str):
payload = {
"name": name, "email": email, "password": password,
"level_label": level_label, "country_label": country_label
}
return _json_or_raise(_req("POST", "/auth/signup/student", json=payload))
def signup_teacher(title: str, name: str, email: str, password: str):
payload = {"title": title, "name": name, "email": email, "password": password}
return _json_or_raise(_req("POST", "/auth/signup/teacher", json=payload))
# ---------- Classes ----------
def create_class(teacher_id: int, name: str):
return _json_or_raise(_req("POST", f"/teachers/{teacher_id}/classes", json={"name": name}))
def list_classes_by_teacher(teacher_id: int):
return _json_or_raise(_req("GET", f"/teachers/{teacher_id}/classes"))
def join_class_by_code(student_id: int, code: str):
d = _json_or_raise(_req("POST", f"/students/{student_id}/classes/join", json={"code": code}))
return d.get("class_id", d)
def leave_class(student_id: int, class_id: int):
_json_or_raise(_req("POST", f"/classes/{class_id}/leave", json={"student_id": student_id}))
return True
def list_students_in_class(class_id: int):
return _json_or_raise(_req("GET", f"/classes/{class_id}/students"))
def class_content_counts(class_id: int):
return _json_or_raise(_req("GET", f"/classes/{class_id}/content_counts"))
def list_class_assignments(class_id: int):
return _json_or_raise(_req("GET", f"/classes/{class_id}/assignments"))
def class_analytics(class_id: int):
return _json_or_raise(_req("GET", f"/classes/{class_id}/analytics"))
def class_student_metrics(class_id: int):
return _json_or_raise(_req("GET", f"/classes/{class_id}/students/metrics"))
def class_weekly_activity(class_id: int):
return _json_or_raise(_req("GET", f"/classes/{class_id}/activity/weekly"))
def class_progress_overview(class_id: int):
return _json_or_raise(_req("GET", f"/classes/{class_id}/progress"))
def class_recent_activity(class_id: int, limit=6, days=30):
return _json_or_raise(_req("GET", f"/classes/{class_id}/activity/recent",
params={"limit": limit, "days": days}))
def student_class_progress(student_id: int, class_id: int):
return _json_or_raise(_req("GET", f"/classes/{class_id}/students/{student_id}/progress"))
def student_assignments_for_class(student_id: int, class_id: int):
return _json_or_raise(_req("GET", f"/classes/{class_id}/students/{student_id}/assignments"))
# ---------- Lessons ----------
def list_lessons_by_teacher(teacher_id: int):
return _json_or_raise(_req("GET", f"/teachers/{teacher_id}/lessons"))
def create_lesson(teacher_id: int, title: str, description: str,
subject: str, level: str, sections: list[dict]):
payload = {
"title": title, "description": description,
"subject": subject, "level": level, "sections": sections,
}
d = _json_or_raise(_req("POST", f"/teachers/{teacher_id}/lessons", json=payload))
return d.get("lesson_id", d.get("id", d))
def get_lesson(lesson_id: int):
return _json_or_raise(_req("GET", f"/lessons/{lesson_id}"))
def update_lesson(lesson_id: int, teacher_id: int, title: str, description: str,
subject: str, level: str, sections: list[dict]):
d = _json_or_raise(_req("PUT", f"/lessons/{lesson_id}", json={
"teacher_id": teacher_id, "title": title, "description": description,
"subject": subject, "level": level, "sections": sections
}))
return bool(d.get("ok", True))
def delete_lesson(lesson_id: int, teacher_id: int):
d = _json_or_raise(_req("DELETE", f"/lessons/{lesson_id}", json={"teacher_id": teacher_id}))
return bool(d.get("ok", True)), d.get("message", "")
def list_assigned_students_for_lesson(lesson_id: int):
return _json_or_raise(_req("GET", f"/lessons/{lesson_id}/assignees"))
# ---------- Quizzes ----------
def list_quizzes_by_teacher(teacher_id: int):
return _json_or_raise(_req("GET", f"/teachers/{teacher_id}/quizzes"))
def get_quiz(quiz_id: int):
return _json_or_raise(_req("GET", f"/quizzes/{quiz_id}"))
def create_quiz(lesson_id: int, title: str, items: list[dict], settings: dict):
d = _json_or_raise(_req("POST", "/quizzes", json={
"lesson_id": lesson_id, "title": title, "items": items, "settings": settings
}))
return d.get("quiz_id", d.get("id", d))
def update_quiz(quiz_id: int, teacher_id: int, title: str, items: list[dict], settings: dict):
d = _json_or_raise(_req("PUT", f"/quizzes/{quiz_id}", json={
"teacher_id": teacher_id, "title": title, "items": items, "settings": settings
}))
return bool(d.get("ok", True))
def delete_quiz(quiz_id: int, teacher_id: int):
d = _json_or_raise(_req("DELETE", f"/quizzes/{quiz_id}", json={"teacher_id": teacher_id}))
return bool(d.get("ok", True)), d.get("message", "")
def generate_quiz_from_text(content: str, n_questions: int = 5, subject: str = "finance", level: str = "beginner"):
return _json_or_raise(_req("POST", "/quiz/generate", json={
"content": content, "n_questions": n_questions, "subject": subject, "level": level
}))
def generate_quiz(*, lesson_id: int | None, level_slug: str | None, lesson_title: str | None):
payload = {
"lesson_id": int(lesson_id) if lesson_id is not None else None,
"level_slug": level_slug,
"lesson_title": lesson_title,
}
resp = _req("POST", "/quiz/auto", json=payload)
result = _json_or_raise(resp)
if isinstance(result, dict) and "items" in result:
return result["items"]
return result if isinstance(result, list) else []
def submit_quiz(*, student_id: int, lesson_id: int, level_slug: str,
user_answers: list[dict], original_quiz: list[dict], assignment_id: int | None = None):
payload = {
"student_id": student_id,
"lesson_id": lesson_id,
"level_slug": level_slug,
"user_answers": user_answers,
"original_quiz": original_quiz,
"assignment_id": assignment_id,
}
return _json_or_raise(_req("POST", "/quiz/grade", json=payload))
# ---------- Tutor explain ----------
def tutor_explain(lesson_id: int, level_slug: str, wrong: list[dict]):
payload = {"lesson_id": lesson_id, "level_slug": level_slug, "wrong": wrong}
return _json_or_raise(_req("POST", "/tutor/explain", json=payload, timeout=60))
# ---------- Student dashboard ----------
def user_stats(student_id: int):
return _json_or_raise(_req("GET", f"/students/{student_id}/stats"))
def list_assignments_for_student(student_id: int):
return _json_or_raise(_req("GET", f"/students/{student_id}/assignments"))
def student_quiz_average(student_id: int):
d = _json_or_raise(_req("GET", f"/students/{student_id}/quiz_avg"))
# backend returns {"avg_pct": ...}
for k in ("avg_pct", "avg", "average", "score_pct", "score", "value"):
if k in d:
try:
return int(round(float(str(d[k]).strip().rstrip("%"))))
except Exception:
break
return 0
def recent_lessons_for_student(student_id: int, limit: int = 5):
return _json_or_raise(_req("GET", f"/students/{student_id}/recent", params={"limit": limit}))
def list_classes_for_student(student_id: int):
return _json_or_raise(_req("GET", f"/students/{student_id}/classes"))
def level_from_xp(xp: int):
return _json_or_raise(_req("GET", "/level-from-xp", params={"xp": xp}))["level"]
# ---------- Games ----------
def record_money_match_play(user_id: int, target: int, total: int,
elapsed_ms: int, matched: bool, gained_xp: int | None = None):
payload = {
"user_id": user_id,
"target": target,
"total": total,
"elapsed_ms": elapsed_ms,
"matched": bool(matched),
"gained_xp": gained_xp,
}
return _json_or_raise(_req("POST", "/games/money_match/record", json=payload))
def record_budget_builder_play(user_id: int, weekly_allowance: int,
allocations: list[dict], gained_xp: int | None = None):
payload = {
"user_id": user_id,
"weekly_allowance": weekly_allowance,
"allocations": allocations,
"gained_xp": gained_xp,
}
return _json_or_raise(_req("POST", "/games/budget_builder/record", json=payload))
def record_debt_dilemma_play(user_id: int, *, level: int, round_no: int,
wallet: int, health: int, happiness: int, credit_score: int,
event_json: dict, outcome: str, gained_xp: int | None = None):
payload = {
"user_id": user_id,
"level": level,
"round_no": round_no,
"wallet": wallet,
"health": health,
"happiness": happiness,
"credit_score": credit_score,
"event_json": event_json,
"outcome": outcome,
"gained_xp": gained_xp,
}
return _json_or_raise(_req("POST", "/games/debt_dilemma/record", json=payload))
def record_profit_puzzler_play(user_id: int, scenario_id: str, title: str,
units: int, price: int, cost: int,
user_answer: float, actual_profit: float, is_correct: bool,
gained_xp: int | None = None):
payload = {
"user_id": user_id, "scenario_id": scenario_id, "title": title,
"units": units, "price": price, "cost": cost,
"user_answer": user_answer, "actual_profit": actual_profit,
"is_correct": bool(is_correct), "gained_xp": gained_xp,
}
return _json_or_raise(_req("POST", "/games/profit_puzzler/record", json=payload))
# ---------- RAG passthrough ----------
def retrieve(query: str, lesson_id: int, level_slug: str = "beginner", k: int = 3) -> str:
try:
d = _json_or_raise(_req("GET", "/retrieve",
params={"query": query, "lesson_id": lesson_id, "level_slug": level_slug, "k": k}))
return d.get("context", "")
except Exception as e:
return f"(retrieval failed: {e})"
def chat_ai(query: str, lesson_id: int, level_slug: str, history=None) -> str:
payload = {
"query": query,
"lesson_id": int(lesson_id) if lesson_id is not None else 0,
"level_slug": level_slug or "beginner",
"history": history or [],
}
try:
url = f"{BACKEND}/chat"
with requests.Session() as s:
s.headers.update(_session.headers)
r = s.post(url, json=payload, timeout=DEFAULT_TIMEOUT) # no retry adapter
r.raise_for_status()
return r.json().get("answer", "")
except Exception as e:
return f"(chat failed: {e})"
# ---------- Direct requests (no prefix) ----------
def signup_student(name: str, email: str, password: str, level_label: str, country_label: str):
payload = {
"name": name,
"email": email,
"password": password,
"level_label": level_label, # match your backend param names
"country_label": country_label,
}
try:
url = f"{BACKEND}/auth/signup/student"
r = _session.post(url, json=payload, timeout=DEFAULT_TIMEOUT)
r.raise_for_status()
return r.json()
except Exception as e:
raise RuntimeError(f"Signup failed: {e}")
def signup_teacher(title: str, name: str, email: str, password: str):
payload = {
"title": title,
"name": name,
"email": email,
"password": password,
}
try:
url = f"{BACKEND}/auth/signup/teacher"
r = _session.post(url, json=payload, timeout=DEFAULT_TIMEOUT)
r.raise_for_status()
return r.json()
except Exception as e:
raise RuntimeError(f"Signup failed: {e}")
def login(email: str, password: str):
payload = {
"email": email,
"password": password,
}
try:
url = f"{BACKEND}/auth/login"
r = _session.post(url, json=payload, timeout=DEFAULT_TIMEOUT)
r.raise_for_status()
return r.json()
except Exception as e:
raise RuntimeError(f"Login failed: {e}")
# ---------- LangGraph-backed ----------
def fetch_lesson_content(lesson: str, module: str, topic: str):
d = _json_or_raise(_req("POST", "/lesson", json={"lesson": lesson, "module": module, "topic": topic}))
return d.get("lesson_content", "")
def submit_lesson_quiz(lesson: str, module: str, topic: str, responses: dict):
return _json_or_raise(_req("POST", "/lesson-quiz",
json={"lesson": lesson, "module": module, "topic": topic, "responses": responses}))
def submit_practice_quiz(lesson: str, responses: dict):
return _json_or_raise(_req("POST", "/practice-quiz", json={"lesson": lesson, "responses": responses}))