Download utils/api.py from FinEDProj/FInFront: direct link, hf CLI and curl.
- Browser
- Download file 16.4 kB
-
https://huggingface.co/spaces/FinEDProj/FInFront/resolve/main/utils/api.py
- Command line
-
hf download hf://spaces/FinEDProj/FInFront/utils/api.py
-
curl -L -o api.py https://huggingface.co/spaces/FinEDProj/FInFront/resolve/main/utils/api.py
16.4 kB
| # 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})) | |