Download app/progress.py from wzender/mathly: direct link, hf CLI and curl.
- Browser
- Download file 3.03 kB
-
https://huggingface.co/spaces/wzender/mathly/resolve/main/app/progress.py
- Command line
-
hf download hf://spaces/wzender/mathly/app/progress.py
-
curl -L -o progress.py https://huggingface.co/spaces/wzender/mathly/resolve/main/app/progress.py
3.03 kB
| """The student's path progress, kept with the account so it follows them to every device. | |
| The page keeps its progress (P in static/math.js) on the device and sends a copy here after each change. A device | |
| that opens the app reads this copy and merges it with its own (the merge rules are in math.js), so work done on | |
| either device is kept. The server only stores the object: one row per account in the Supabase table `math_progress`, | |
| read and written with the server's secret key. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import logging | |
| from datetime import datetime, timezone | |
| from fastapi import Depends, HTTPException, Request | |
| from fastapi.responses import JSONResponse | |
| from .auth import AuthUnavailable | |
| log = logging.getLogger("mathly.progress") | |
| MAX_BYTES = 1_000_000 | |
| MIGRATION = "supabase/migrations/20261103200000_math_progress.sql" | |
| TABLE = "/rest/v1/math_progress" | |
| def load_progress(auth, user_id: str) -> dict: | |
| rows = auth._admin(f"{TABLE}?select=data&user_id=eq.{user_id}") or [] | |
| return rows[0]["data"] if rows and isinstance(rows[0].get("data"), dict) else {} | |
| def save_progress(auth, user_id: str, data: dict) -> None: | |
| auth._admin(f"{TABLE}?on_conflict=user_id", "POST", | |
| {"user_id": user_id, "data": data, "updated_at": datetime.now(timezone.utc).isoformat()}, | |
| prefer="resolution=merge-duplicates,return=minimal") | |
| def install_progress_routes(app, signed_in): | |
| """GET and PUT /api/math/progress: the signed-in student's progress.""" | |
| def account(user=Depends(signed_in)) -> str: | |
| if not user or not user.get("id"): | |
| raise HTTPException(401, "sign_in_required") | |
| return user["id"] | |
| def unavailable(exc: AuthUnavailable) -> HTTPException: | |
| if "answered 404" in str(exc): | |
| log.warning("the math_progress table is missing; apply %s", MIGRATION) | |
| else: | |
| log.error("progress not synced: %s", exc) | |
| return HTTPException(503, "progress_unavailable") | |
| def get_progress(user: str = Depends(account)): | |
| try: | |
| data = load_progress(app.state.auth, user) | |
| except AuthUnavailable as exc: | |
| raise unavailable(exc) from exc | |
| return JSONResponse({"progress": data}, headers={"Cache-Control": "no-store"}) | |
| async def put_progress(request: Request, user: str = Depends(account)): | |
| raw = await request.body() | |
| if len(raw) > MAX_BYTES: | |
| raise HTTPException(413, "progress_too_large") | |
| try: | |
| data = json.loads(raw).get("progress") | |
| except (ValueError, AttributeError) as exc: | |
| raise HTTPException(400, "send {\"progress\": {...}}") from exc | |
| if not isinstance(data, dict): | |
| raise HTTPException(400, "send {\"progress\": {...}}") | |
| try: | |
| save_progress(app.state.auth, user, data) | |
| except AuthUnavailable as exc: | |
| raise unavailable(exc) from exc | |
| return {"ok": True} | |