Download src/api/v1/progress.py from False45/math-api: direct link, hf CLI and curl.
- Browser
- Download file 11.2 kB
-
https://huggingface.co/spaces/False45/math-api/resolve/main/src/api/v1/progress.py
- Command line
-
hf download hf://spaces/False45/math-api/src/api/v1/progress.py
-
curl -L -o progress.py https://huggingface.co/spaces/False45/math-api/resolve/main/src/api/v1/progress.py
11.2 kB
| """ | |
| Progress Tracking API — /api/v1/progress endpoints. | |
| Handles daily streak calculations and total problems solved per user. | |
| Backed by Firestore. | |
| """ | |
| import logging | |
| from datetime import datetime, timezone | |
| from typing import Dict, List | |
| from pydantic import BaseModel | |
| from fastapi import APIRouter, Depends, HTTPException | |
| from backend.src.api.middleware.auth import verify_firebase_token | |
| from backend.src.services.firebase_service import get_firestore_client | |
| logger = logging.getLogger("math_assistant.api.progress") | |
| router = APIRouter(prefix="/api/v1/progress", tags=["progress"]) | |
| class ProgressResponse(BaseModel): | |
| streak: int | |
| total_solved: int | |
| last_solved_date: str | |
| activity_map: Dict[str, int] = {} | |
| weak_topics: List[str] = [] | |
| accuracy: int = 100 | |
| _MEMORY_STORE: Dict[str, dict] = {} | |
| def get_memory_stats(uid: str) -> dict: | |
| if uid not in _MEMORY_STORE: | |
| _MEMORY_STORE[uid] = { | |
| "streak": 0, | |
| "total_solved": 0, | |
| "last_solved_date": "", | |
| "activity_map": {}, | |
| "weak_topics": [], | |
| "accuracy": 100, | |
| "correct_answers": 0, | |
| "total_answers": 0 | |
| } | |
| return _MEMORY_STORE[uid] | |
| async def get_progress(uid: str = Depends(verify_firebase_token)): | |
| """Get the user's current progress and streak.""" | |
| db = get_firestore_client() | |
| if not db: | |
| # Fallback if Firebase is disabled | |
| stats = get_memory_stats(uid) | |
| return ProgressResponse(**{k: v for k, v in stats.items() if k in ProgressResponse.__fields__}) | |
| try: | |
| doc_ref = db.collection("users").document(uid).collection("profile").document("stats") | |
| doc = doc_ref.get() | |
| if doc.exists: | |
| data = doc.to_dict() | |
| return ProgressResponse( | |
| streak=data.get("streak", 0), | |
| total_solved=data.get("total_solved", 0), | |
| last_solved_date=data.get("last_solved_date", ""), | |
| activity_map=data.get("activity_map", {}), | |
| weak_topics=data.get("weak_topics", []), | |
| accuracy=data.get("accuracy", 100) | |
| ) | |
| else: | |
| return ProgressResponse(streak=0, total_solved=0, last_solved_date="") | |
| except Exception as e: | |
| logger.error(f"Error getting progress for {uid}: {e}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def increment_progress(uid: str = Depends(verify_firebase_token)): | |
| """Increment the user's total solved count and update their streak.""" | |
| db = get_firestore_client() | |
| today_str = datetime.now(timezone.utc).date().isoformat() | |
| if not db: | |
| stats = get_memory_stats(uid) | |
| last_date = stats.get("last_solved_date", "") | |
| streak = stats.get("streak", 0) | |
| total = stats.get("total_solved", 0) + 1 | |
| activity_map = stats.get("activity_map", {}) | |
| activity_map[today_str] = activity_map.get(today_str, 0) + 1 | |
| if last_date != today_str: | |
| try: | |
| if last_date: | |
| delta = (datetime.fromisoformat(today_str).date() - datetime.fromisoformat(last_date).date()).days | |
| streak = (streak + 1) if delta == 1 else 1 | |
| else: | |
| streak = 1 | |
| except ValueError: | |
| streak = 1 | |
| stats.update({ | |
| "streak": streak, | |
| "total_solved": total, | |
| "last_solved_date": today_str, | |
| "activity_map": activity_map | |
| }) | |
| return ProgressResponse(**{k: v for k, v in stats.items() if k in ProgressResponse.__fields__}) | |
| try: | |
| doc_ref = db.collection("users").document(uid).collection("profile").document("stats") | |
| today_str = datetime.now(timezone.utc).date().isoformat() | |
| # We need a transaction to safely update the streak | |
| def update_in_transaction(transaction, ref, today): | |
| snapshot = ref.get(transaction=transaction) | |
| if not snapshot.exists: | |
| data = { | |
| "streak": 1, | |
| "total_solved": 1, | |
| "last_solved_date": today, | |
| "activity_map": {today: 1}, | |
| "weak_topics": [], | |
| "accuracy": 100, | |
| "correct_answers": 1, | |
| "total_answers": 1 | |
| } | |
| transaction.set(ref, data) | |
| return data | |
| current_data = snapshot.to_dict() | |
| last_date = current_data.get("last_solved_date", "") | |
| streak = current_data.get("streak", 0) | |
| total = current_data.get("total_solved", 0) + 1 | |
| activity_map = current_data.get("activity_map", {}) | |
| activity_map[today] = activity_map.get(today, 0) + 1 | |
| if last_date == today: | |
| # Already solved a problem today, just increment total | |
| pass | |
| else: | |
| # Need to check if it was yesterday | |
| try: | |
| last_date_obj = datetime.fromisoformat(last_date).date() | |
| today_obj = datetime.fromisoformat(today).date() | |
| delta = (today_obj - last_date_obj).days | |
| if delta == 1: | |
| streak += 1 | |
| else: | |
| streak = 1 | |
| except ValueError: | |
| streak = 1 | |
| data = { | |
| "streak": streak, | |
| "total_solved": total, | |
| "last_solved_date": today, | |
| "activity_map": activity_map, | |
| "weak_topics": current_data.get("weak_topics", []), | |
| "accuracy": current_data.get("accuracy", 100) | |
| } | |
| transaction.update(ref, data) | |
| return data | |
| new_stats = update_in_transaction(db.transaction(), doc_ref, today_str) | |
| return ProgressResponse(**new_stats) | |
| except Exception as e: | |
| logger.error(f"Error incrementing progress for {uid}: {e}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| class WeaknessRequest(BaseModel): | |
| topic: str | |
| async def add_weakness(req: WeaknessRequest, uid: str = Depends(verify_firebase_token)): | |
| """Add a topic to the user's weak topics list.""" | |
| db = get_firestore_client() | |
| if not db: | |
| stats = get_memory_stats(uid) | |
| topics = stats.get("weak_topics", []) | |
| if req.topic not in topics: | |
| topics.append(req.topic) | |
| if len(topics) > 5: | |
| topics = topics[-5:] | |
| stats["weak_topics"] = topics | |
| return ProgressResponse(**{k: v for k, v in stats.items() if k in ProgressResponse.__fields__}) | |
| doc_ref = db.collection("users").document(uid).collection("profile").document("stats") | |
| def update_weakness(transaction, ref): | |
| snapshot = ref.get(transaction=transaction) | |
| if not snapshot.exists: | |
| data = {"streak": 0, "total_solved": 0, "last_solved_date": "", "weak_topics": [req.topic], "accuracy": 100} | |
| transaction.set(ref, data) | |
| return data | |
| data = snapshot.to_dict() | |
| topics = data.get("weak_topics", []) | |
| if req.topic not in topics: | |
| topics.append(req.topic) | |
| # keep only last 5 | |
| if len(topics) > 5: | |
| topics = topics[-5:] | |
| update_data = {"weak_topics": topics} | |
| transaction.update(ref, update_data) | |
| data.update(update_data) | |
| return data | |
| try: | |
| new_stats = update_weakness(db.transaction(), doc_ref) | |
| return ProgressResponse( | |
| streak=new_stats.get("streak", 0), | |
| total_solved=new_stats.get("total_solved", 0), | |
| last_solved_date=new_stats.get("last_solved_date", ""), | |
| activity_map=new_stats.get("activity_map", {}), | |
| weak_topics=new_stats.get("weak_topics", []), | |
| accuracy=new_stats.get("accuracy", 100) | |
| ) | |
| except Exception as e: | |
| logger.error(f"Error updating weakness: {e}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| class AccuracyRequest(BaseModel): | |
| correct: bool | |
| async def update_accuracy(req: AccuracyRequest, uid: str = Depends(verify_firebase_token)): | |
| """Update user's accuracy based on a correct/incorrect answer.""" | |
| db = get_firestore_client() | |
| if not db: | |
| stats = get_memory_stats(uid) | |
| correct_ans = stats.get("correct_answers", 0) + (1 if req.correct else 0) | |
| total_ans = stats.get("total_answers", 0) + 1 | |
| acc = int((correct_ans / total_ans) * 100) | |
| stats.update({ | |
| "correct_answers": correct_ans, | |
| "total_answers": total_ans, | |
| "accuracy": acc | |
| }) | |
| return ProgressResponse(**{k: v for k, v in stats.items() if k in ProgressResponse.__fields__}) | |
| doc_ref = db.collection("users").document(uid).collection("profile").document("stats") | |
| def update_acc(transaction, ref): | |
| snapshot = ref.get(transaction=transaction) | |
| if not snapshot.exists: | |
| data = { | |
| "streak": 0, "total_solved": 0, "last_solved_date": "", | |
| "correct_answers": 1 if req.correct else 0, | |
| "total_answers": 1, | |
| "accuracy": 100 if req.correct else 0 | |
| } | |
| transaction.set(ref, data) | |
| return data | |
| data = snapshot.to_dict() | |
| correct_ans = data.get("correct_answers", 0) + (1 if req.correct else 0) | |
| total_ans = data.get("total_answers", 0) + 1 | |
| acc = int((correct_ans / total_ans) * 100) | |
| update_data = { | |
| "correct_answers": correct_ans, | |
| "total_answers": total_ans, | |
| "accuracy": acc | |
| } | |
| transaction.update(ref, update_data) | |
| data.update(update_data) | |
| return data | |
| try: | |
| new_stats = update_acc(db.transaction(), doc_ref) | |
| return ProgressResponse( | |
| streak=new_stats.get("streak", 0), | |
| total_solved=new_stats.get("total_solved", 0), | |
| last_solved_date=new_stats.get("last_solved_date", ""), | |
| activity_map=new_stats.get("activity_map", {}), | |
| weak_topics=new_stats.get("weak_topics", []), | |
| accuracy=new_stats.get("accuracy", 100) | |
| ) | |
| except Exception as e: | |
| logger.error(f"Error updating accuracy: {e}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| def from_firestore_transaction(db): | |
| """Helper decorator for Firestore transactions.""" | |
| from firebase_admin import firestore | |
| return firestore.transactional | |