Spaces:
Running
Running
Download main.py from simats0/ARMS-Result: direct link, hf CLI and curl.
- Browser
- Download file 29.4 kB
-
https://huggingface.co/spaces/simats0/ARMS-Result/resolve/main/main.py
- Command line
-
hf download hf://spaces/simats0/ARMS-Result/main.py
-
curl -L -o main.py https://huggingface.co/spaces/simats0/ARMS-Result/resolve/main/main.py
29.4 kB
| from fastapi import FastAPI, Form, HTTPException, Request, Response, BackgroundTasks | |
| from fastapi.staticfiles import StaticFiles | |
| from fastapi.responses import JSONResponse, FileResponse | |
| import httpx | |
| from bs4 import BeautifulSoup | |
| import asyncio | |
| import urllib3 | |
| import logging | |
| import json | |
| import os | |
| import re | |
| from cachetools import TTLCache | |
| # Caches for high concurrency | |
| month_year_cache = TTLCache(maxsize=1, ttl=3600) # 1 hour | |
| course_by_month_cache = TTLCache(maxsize=2000, ttl=2592000) # 30 days | |
| faculty_view_cache = TTLCache(maxsize=30000, ttl=2592000) # 30 days | |
| user_marks_cache = TTLCache(maxsize=10000, ttl=300) # 5 minutes | |
| # Suppress the insecure request warnings caused by verify=False | |
| urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) | |
| # Set up simple logging | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| # File path for cache | |
| PERSISTENT_CACHE_FILE = os.path.join(os.path.dirname(__file__), "persistent_cache.json") | |
| def load_persistent_cache(): | |
| if os.path.exists(PERSISTENT_CACHE_FILE): | |
| try: | |
| with open(PERSISTENT_CACHE_FILE, "r", encoding="utf-8") as f: | |
| data = json.load(f) | |
| # Load faculty views | |
| fv_loaded = 0 | |
| for k, v in data.get("faculty_view", {}).items(): | |
| key = int(k) if k.isdigit() else k | |
| faculty_view_cache[key] = v | |
| fv_loaded += 1 | |
| # Load course by month | |
| cbm_loaded = 0 | |
| for k, v in data.get("course_by_month", {}).items(): | |
| course_by_month_cache[k] = v | |
| cbm_loaded += 1 | |
| logger.info(f"Loaded persistent cache: {fv_loaded} faculty views, {cbm_loaded} course-by-months") | |
| except Exception as e: | |
| logger.error(f"Failed to load persistent cache: {e}") | |
| def save_persistent_cache(): | |
| try: | |
| data = { | |
| "faculty_view": {str(k): v for k, v in faculty_view_cache.items()}, | |
| "course_by_month": {str(k): v for k, v in course_by_month_cache.items()} | |
| } | |
| with open(PERSISTENT_CACHE_FILE, "w", encoding="utf-8") as f: | |
| json.dump(data, f, ensure_ascii=False) | |
| logger.info(f"Saved persistent cache: {len(faculty_view_cache)} faculty views, {len(course_by_month_cache)} course-by-months") | |
| except Exception as e: | |
| logger.error(f"Failed to save persistent cache: {e}") | |
| # Initialize FastAPI App | |
| app = FastAPI(title="SIMATS Result Hub") | |
| async def startup_event(): | |
| load_persistent_cache() | |
| # Serve static files | |
| app.mount("/static", StaticFiles(directory="static"), name="static") | |
| async def index(): | |
| return FileResponse('static/index.html') | |
| async def style(): | |
| return FileResponse('static/style.css') | |
| async def script(): | |
| return FileResponse('static/script.js') | |
| async def favicon(): | |
| return Response(status_code=204) | |
| async def debug_cache(): | |
| # Convert cache to a dict for inspection | |
| return {str(k): v for k, v in user_marks_cache.items()} | |
| async def login(username: str = Form(...), password: str = Form(...)): | |
| url = "https://arms.sse.saveetha.com/Login.aspx" | |
| headers = { | |
| "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8", | |
| "accept-language": "en-US,en;q=0.5", | |
| "cache-control": "no-cache", | |
| "content-type": "application/x-www-form-urlencoded", | |
| "origin": "https://arms.sse.saveetha.com", | |
| "pragma": "no-cache", | |
| "referer": "https://arms.sse.saveetha.com/Login.aspx", | |
| "upgrade-insecure-requests": "1", | |
| "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36" | |
| } | |
| try: | |
| # We use httpx.AsyncClient with follow_redirects=False for the POST | |
| # and verify=False to ignore SSL warnings. | |
| async with httpx.AsyncClient(verify=False, headers=headers) as client: | |
| # Step 1: GET request | |
| response_get = await client.get(url, timeout=15.0) | |
| response_get.raise_for_status() | |
| # Parse HTML | |
| soup = BeautifulSoup(response_get.text, 'html.parser') | |
| viewstate = soup.find(id="__VIEWSTATE") | |
| viewstategenerator = soup.find(id="__VIEWSTATEGENERATOR") | |
| eventvalidation = soup.find(id="__EVENTVALIDATION") | |
| if not viewstate or not eventvalidation: | |
| return JSONResponse(status_code=500, content={"detail": "Could not find ASP.NET hidden fields on the login page."}) | |
| payload = { | |
| "__VIEWSTATE": viewstate.get("value", ""), | |
| "__VIEWSTATEGENERATOR": viewstategenerator.get("value", "") if viewstategenerator else "", | |
| "__EVENTVALIDATION": eventvalidation.get("value", ""), | |
| "txtusername": username, | |
| "txtpassword": password, | |
| "btnlogin": "Login" | |
| } | |
| # Use the cookies from the GET request | |
| cookies = response_get.cookies | |
| # Step 2: POST request | |
| response_post = await client.post(url, data=payload, cookies=cookies, follow_redirects=False, timeout=15.0) | |
| # Extract the ASP.NET_SessionId cookie from the POST response or the client's current cookie jar | |
| session_id = client.cookies.get("ASP.NET_SessionId", domain="arms.sse.saveetha.com") or response_post.cookies.get("ASP.NET_SessionId") | |
| if response_post.status_code == 302: | |
| return JSONResponse(content={"success": True, "ASP.NET_SessionId": session_id, "message": "Login successful!"}) | |
| if response_post.status_code == 200: | |
| if "Invalid Username" in response_post.text or "Invalid" in response_post.text: | |
| return JSONResponse(status_code=401, content={"detail": "Invalid Username or Password"}) | |
| return JSONResponse(content={"success": True, "ASP.NET_SessionId": session_id, "message": "Login appears successful!"}) | |
| return JSONResponse(status_code=401, content={"detail": "Login failed or unexpected status code."}) | |
| except httpx.RequestError as e: | |
| return JSONResponse(status_code=502, content={"detail": f"API request failed: {str(e)}"}) | |
| except Exception as e: | |
| return JSONResponse(status_code=500, content={"detail": f"An error occurred: {str(e)}"}) | |
| async def fetch_api3(client: httpx.AsyncClient, month_id: str): | |
| """Fetch subjects for a given month.""" | |
| if month_id in course_by_month_cache: | |
| return month_id, course_by_month_cache[month_id] | |
| url = f"https://arms.sse.saveetha.com/Handler/Controller.ashx?Page=CoursebyMonth&Mode=PublishCoursebyMonthNew&Monthyear={month_id}" | |
| resp = await client.get(url, timeout=15.0) | |
| data = resp.json().get("Table", []) | |
| course_by_month_cache[month_id] = data | |
| return month_id, data | |
| async def process_course(client: httpx.AsyncClient, course_info: dict, subject_ids: list, username: str): | |
| """Fetch marks for a specific course/subject_ids.""" | |
| try: | |
| user_clean = str(username).strip().lower() | |
| for subject_id in subject_ids: | |
| # API 4 (Cached) | |
| if subject_id in faculty_view_cache: | |
| api4_data = faculty_view_cache[subject_id] | |
| else: | |
| api4_url = f"https://arms.sse.saveetha.com/Handler/Controller.ashx?Page=ResultView&Mode=NewResultViewFaculty&Coursename={subject_id}" | |
| resp4 = await client.get(api4_url, timeout=15.0) | |
| api4_data = resp4.json().get("Table", []) | |
| faculty_view_cache[subject_id] = api4_data | |
| view_id = None | |
| for student in api4_data: | |
| reg_clean = str(student.get("RegNo", "")).strip().lower() | |
| # Alias check for SSETSCS054 -> SSETSSH239 | |
| target_users = [user_clean] | |
| if user_clean == "ssetscs054": | |
| target_users.append("ssetssh239") | |
| match_found = False | |
| for target in target_users: | |
| if reg_clean == target or reg_clean.startswith(target) or target.startswith(reg_clean): | |
| match_found = True | |
| break | |
| if match_found: | |
| view_id = student.get("ViewId") | |
| break | |
| if view_id: | |
| # API 5 | |
| api5_url = f"https://arms.sse.saveetha.com/Handler/Controller.ashx?Page=ViewMarks&Mode=MarkSplitbyId&Id={subject_id}&Id2={view_id}" | |
| resp5 = await client.get(api5_url, timeout=15.0) | |
| api5_data = resp5.json().get("Table", []) | |
| return {"course": course_info, "marks": api5_data, "error": None} | |
| return {"course": course_info, "marks": None, "error": f"View ID not found (searched across {len(subject_ids)} subject sections)"} | |
| except Exception as e: | |
| return {"course": course_info, "marks": None, "error": str(e)} | |
| async def fetch_marks(username: str = Form(...), session_id: str = Form(...)): | |
| cache_key = f"{username}_{session_id}" | |
| if cache_key in user_marks_cache: | |
| return JSONResponse(content={"success": True, "data": user_marks_cache[cache_key]}) | |
| headers = { | |
| "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" | |
| } | |
| cookies = {"ASP.NET_SessionId": session_id} | |
| try: | |
| # Increase connection limits for high concurrency | |
| limits = httpx.Limits(max_connections=2000, max_keepalive_connections=500) | |
| # Use a single httpx AsyncClient session for all requests | |
| async with httpx.AsyncClient(verify=False, headers=headers, cookies=cookies, limits=limits) as client: | |
| # API 1 | |
| api1_url = "https://arms.sse.saveetha.com/Handler/Student.ashx?Page=CourseEnroll&Mode=GetResult&Id=0" | |
| resp1 = await client.get(api1_url, timeout=15.0) | |
| courses = resp1.json().get("Table", []) | |
| # API 2 (Cached) | |
| if "global" in month_year_cache: | |
| month_years = month_year_cache["global"] | |
| else: | |
| api2_url = "https://arms.sse.saveetha.com/Handler/Controller.ashx?Page=MonthYear&Mode=MonthYearNew" | |
| resp2 = await client.get(api2_url, timeout=15.0) | |
| month_years = resp2.json().get("Table", []) | |
| month_year_cache["global"] = month_years | |
| month_map = {item["Value"]: item["Id"] for item in month_years} | |
| unique_months = set() | |
| for c in courses: | |
| mv = c.get("MonthYearValue") | |
| if mv in month_map: | |
| unique_months.add(month_map[mv]) | |
| # Fetch API 3 for all unique months concurrently | |
| api3_tasks = [fetch_api3(client, mid) for mid in unique_months] | |
| api3_results = await asyncio.gather(*api3_tasks, return_exceptions=True) | |
| api3_cache = {} | |
| for res in api3_results: | |
| if isinstance(res, tuple): | |
| mid, data = res | |
| api3_cache[mid] = data | |
| else: | |
| logger.error(f"Error fetching API 3: {res}") | |
| # Build course to subject mapping | |
| course_subject_map = [] | |
| for c in courses: | |
| mv = c.get("MonthYearValue") | |
| if mv not in month_map: continue | |
| month_id = month_map[mv] | |
| subject_ids = [] | |
| for sub in api3_cache.get(month_id, []): | |
| if sub["SubjectCode"] == c["CourseCode"]: | |
| subject_ids.append(sub["SubjectId"]) | |
| if subject_ids: | |
| course_subject_map.append((c, subject_ids)) | |
| # Fetch all marks concurrently | |
| marks_tasks = [process_course(client, c, sids, username) for c, sids in course_subject_map] | |
| final_data = await asyncio.gather(*marks_tasks, return_exceptions=True) | |
| # Filter out exceptions from gather and ensure valid dict structure | |
| processed_data = [] | |
| for item in final_data: | |
| if isinstance(item, Exception): | |
| logger.error(f"Error processing course: {item}") | |
| continue | |
| processed_data.append(item) | |
| # Sort data by Course Name for consistency | |
| processed_data.sort(key=lambda x: x["course"].get("CourseName", "")) | |
| user_marks_cache[cache_key] = processed_data | |
| return JSONResponse(content={"success": True, "data": processed_data}) | |
| except Exception as e: | |
| logger.exception("Failed to fetch detailed marks") | |
| error_msg = str(e) | |
| if "Expecting value" in error_msg: | |
| return JSONResponse(status_code=401, content={"detail": "Your credentials are invalid"}) | |
| return JSONResponse(status_code=500, content={"detail": f"Failed to fetch detailed marks: {error_msg}"}) | |
| # Cache for search results per register number (avoids re-searching) | |
| search_result_cache = TTLCache(maxsize=5000, ttl=600) # 10 minutes | |
| async def search_student(register_no: str = Form(...), session_id: str = Form(...), background_tasks: BackgroundTasks = None): | |
| """Search for a student's results by register number using regulation and month filtering.""" | |
| import time as _time | |
| register_clean = register_no.strip().lower() | |
| # ===== FAST PATH: Return cached search result instantly ===== | |
| if register_clean in search_result_cache: | |
| logger.info(f"Search cache HIT for {register_clean}") | |
| return JSONResponse(content={"success": True, "data": search_result_cache[register_clean]}) | |
| headers = { | |
| "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" | |
| } | |
| cookies = {"ASP.NET_SessionId": session_id} | |
| TOTAL_TIME_BUDGET = 55 # seconds — stop Phase 2 scanning after this | |
| start_time = _time.monotonic() | |
| try: | |
| limits = httpx.Limits(max_connections=500, max_keepalive_connections=200) | |
| async with httpx.AsyncClient(verify=False, headers=headers, cookies=cookies, limits=limits) as client: | |
| # Step 1: Get all month/years (cached, instant) | |
| if "global" in month_year_cache: | |
| month_years = month_year_cache["global"] | |
| else: | |
| api2_url = "https://arms.sse.saveetha.com/Handler/Controller.ashx?Page=MonthYear&Mode=MonthYearNew" | |
| resp2 = await client.get(api2_url, timeout=15.0) | |
| month_years = resp2.json().get("Table", []) | |
| month_year_cache["global"] = month_years | |
| # Extract student admission year | |
| admission_year = 2015 | |
| try: | |
| digits = "".join(c for c in register_clean if c.isdigit()) | |
| if len(digits) >= 6: | |
| y1 = int(digits[4:6]) | |
| if 15 <= y1 <= 26: | |
| admission_year = 2000 + y1 | |
| elif len(digits) >= 2: | |
| y2 = int(digits[:2]) | |
| if 15 <= y2 <= 26: | |
| admission_year = 2000 + y2 | |
| except Exception: | |
| pass | |
| logger.info(f"Student inferred admission year: {admission_year}") | |
| # Filter months (only search years >= admission_year - 1 and <= admission_year + 6) | |
| filtered_month_ids = [] | |
| for item in month_years: | |
| mid = item.get("Id") | |
| val = item.get("Value", "") | |
| if not mid: | |
| continue | |
| try: | |
| m = re.search(r'\b(20\d{2})\b', val) | |
| if m: | |
| year = int(m.group(1)) | |
| if admission_year - 1 <= year <= admission_year + 6: | |
| filtered_month_ids.append(mid) | |
| else: | |
| filtered_month_ids.append(mid) | |
| except Exception: | |
| filtered_month_ids.append(mid) | |
| logger.info(f"Filtered to {len(filtered_month_ids)} out of {len(month_years)} months (year range: {admission_year-1} - {admission_year+6})") | |
| # Step 2: Fetch subjects for filtered months | |
| api3_tasks = [fetch_api3(client, mid) for mid in filtered_month_ids] | |
| api3_results = await asyncio.gather(*api3_tasks, return_exceptions=True) | |
| # Build subject map and filter by regulation year | |
| subject_map = {} | |
| for res in api3_results: | |
| if isinstance(res, tuple): | |
| _, subjects = res | |
| for sub in subjects: | |
| sid = sub.get("SubjectId") | |
| scode = sub.get("SubjectCode", "") | |
| if sid: | |
| # Filter out subjects that belong to regulations in the future | |
| is_valid = True | |
| try: | |
| sm = re.match(r'^\s*(\d{2})', scode) | |
| if sm: | |
| sub_year = 2000 + int(sm.group(1)) | |
| if sub_year > admission_year: | |
| is_valid = False | |
| except Exception: | |
| pass | |
| if is_valid: | |
| subject_map[sid] = sub | |
| if not subject_map: | |
| return JSONResponse(content={"success": True, "data": []}) | |
| logger.info(f"Scanning {len(subject_map)} regulation-matched subjects for {register_clean}") | |
| # ===== Phase 1: Scan cached faculty_view data (no network) ===== | |
| cached_subject_ids = set() | |
| found_hits = [] # (subject_id, student_record) | |
| uncached_subject_ids = [] | |
| for sid in subject_map: | |
| if sid in faculty_view_cache: | |
| cached_subject_ids.add(sid) | |
| for student in faculty_view_cache[sid]: | |
| reg = str(student.get("RegNo", "")).strip().lower() | |
| target_users = [register_clean] | |
| if register_clean == "ssetscs054": | |
| target_users.append("ssetssh239") | |
| match_found = False | |
| for target in target_users: | |
| if reg == target or reg.startswith(target) or target.startswith(reg): | |
| match_found = True | |
| break | |
| if match_found: | |
| found_hits.append((sid, student)) | |
| break | |
| else: | |
| uncached_subject_ids.append(sid) | |
| logger.info(f"Phase 1 cache: {len(cached_subject_ids)} cached, {len(found_hits)} hits found, {len(uncached_subject_ids)} uncached") | |
| # ===== Phase 2: Fetch uncached subjects concurrently with time limit ===== | |
| semaphore = asyncio.Semaphore(250) # Increased concurrency to maximize throughput | |
| async def check_uncached_subject(subject_id): | |
| async with semaphore: | |
| try: | |
| api4_url = f"https://arms.sse.saveetha.com/Handler/Controller.ashx?Page=ResultView&Mode=NewResultViewFaculty&Coursename={subject_id}" | |
| resp4 = await client.get(api4_url, timeout=10.0) | |
| api4_data = resp4.json().get("Table", []) | |
| faculty_view_cache[subject_id] = api4_data | |
| for student in api4_data: | |
| reg = str(student.get("RegNo", "")).strip().lower() | |
| target_users = [register_clean] | |
| if register_clean == "ssetscs054": | |
| target_users.append("ssetssh239") | |
| for target in target_users: | |
| if reg == target or reg.startswith(target) or target.startswith(reg): | |
| return (subject_id, student) | |
| return None | |
| except Exception as e: | |
| return None | |
| if uncached_subject_ids: | |
| total_uncached = len(uncached_subject_ids) | |
| phase2_tasks = [asyncio.create_task(check_uncached_subject(sid)) for sid in uncached_subject_ids] | |
| elapsed = _time.monotonic() - start_time | |
| remaining_budget = max(TOTAL_TIME_BUDGET - elapsed, 5.0) | |
| try: | |
| # Run all tasks concurrently, bound by semaphore and global timeout | |
| done, pending = await asyncio.wait( | |
| phase2_tasks, | |
| timeout=remaining_budget, | |
| return_when=asyncio.ALL_COMPLETED | |
| ) | |
| for task in done: | |
| try: | |
| r = task.result() | |
| if r: | |
| found_hits.append(r) | |
| except Exception as e: | |
| logger.error(f"Error in Phase 2 task result: {e}") | |
| if pending: | |
| logger.warning(f"Phase 2 timeout: {len(pending)}/{total_uncached} subjects not scanned.") | |
| for p in pending: | |
| p.cancel() | |
| logger.info(f"Phase 2 complete: scanned {len(done)}/{total_uncached} uncached, elapsed {_time.monotonic() - start_time:.1f}s") | |
| except Exception as e: | |
| logger.error(f"Phase 2 wait error: {e}") | |
| logger.info(f"Total hits found: {len(found_hits)}") | |
| if not found_hits: | |
| search_result_cache[register_clean] = [] | |
| return JSONResponse(content={"success": True, "data": []}) | |
| # ===== Phase 3: Fetch detailed marks for all hits in parallel ===== | |
| async def fetch_mark_details(subject_id, student): | |
| view_id = student.get("ViewId") | |
| if not view_id: | |
| # Return course info even without marks | |
| sub = subject_map.get(subject_id, {}) | |
| return { | |
| "course": { | |
| "CourseCode": sub.get("SubjectCode", ""), | |
| "CourseName": sub.get("SubjectName", sub.get("SubjectCode", "Unknown")), | |
| "MonthYearValue": sub.get("MonthYearValue", ""), | |
| "FinalGrade": student.get("Grade", ""), | |
| "Credit": student.get("Credit", 1) | |
| }, | |
| "marks": [], | |
| "error": None | |
| } | |
| try: | |
| async with semaphore: | |
| api5_url = f"https://arms.sse.saveetha.com/Handler/Controller.ashx?Page=ViewMarks&Mode=MarkSplitbyId&Id={subject_id}&Id2={view_id}" | |
| resp5 = await client.get(api5_url, timeout=10.0) | |
| api5_data = resp5.json().get("Table", []) | |
| sub = subject_map.get(subject_id, {}) | |
| course_info = { | |
| "CourseCode": sub.get("SubjectCode", ""), | |
| "CourseName": sub.get("SubjectName", sub.get("SubjectCode", "Unknown")), | |
| "MonthYearValue": sub.get("MonthYearValue", ""), | |
| "FinalGrade": student.get("Grade", ""), | |
| "Credit": student.get("Credit", 1) | |
| } | |
| return {"course": course_info, "marks": api5_data, "error": None} | |
| except Exception as e: | |
| logger.error(f"Error fetching marks for {subject_id}: {e}") | |
| # Return course info even if marks fail | |
| sub = subject_map.get(subject_id, {}) | |
| return { | |
| "course": { | |
| "CourseCode": sub.get("SubjectCode", ""), | |
| "CourseName": sub.get("SubjectName", sub.get("SubjectCode", "Unknown")), | |
| "MonthYearValue": sub.get("MonthYearValue", ""), | |
| "FinalGrade": student.get("Grade", ""), | |
| "Credit": student.get("Credit", 1) | |
| }, | |
| "marks": [], | |
| "error": None | |
| } | |
| # Phase 3 also gets a time limit | |
| try: | |
| marks_tasks = [fetch_mark_details(sid, stu) for sid, stu in found_hits] | |
| marks_results = await asyncio.wait_for( | |
| asyncio.gather(*marks_tasks, return_exceptions=True), | |
| timeout=30.0 | |
| ) | |
| except asyncio.TimeoutError: | |
| logger.warning("Phase 3 (marks fetch) timed out after 30s") | |
| marks_results = [] | |
| processed_data = [] | |
| for r in marks_results: | |
| if r and not isinstance(r, Exception): | |
| processed_data.append(r) | |
| # If marks fetch timed out but we have hits, still return course info | |
| if not processed_data and found_hits: | |
| for sid, stu in found_hits: | |
| sub = subject_map.get(sid, {}) | |
| processed_data.append({ | |
| "course": { | |
| "CourseCode": sub.get("SubjectCode", ""), | |
| "CourseName": sub.get("SubjectName", sub.get("SubjectCode", "Unknown")), | |
| "MonthYearValue": sub.get("MonthYearValue", ""), | |
| "FinalGrade": stu.get("Grade", ""), | |
| "Credit": stu.get("Credit", 1) | |
| }, | |
| "marks": [], | |
| "error": None | |
| }) | |
| processed_data.sort(key=lambda x: x["course"].get("CourseName", "")) | |
| # Cache the result for this student | |
| search_result_cache[register_clean] = processed_data | |
| # Save cache updates to disk in background | |
| if background_tasks: | |
| background_tasks.add_task(save_persistent_cache) | |
| total_elapsed = _time.monotonic() - start_time | |
| logger.info(f"Search complete for {register_clean}: {len(processed_data)} courses in {total_elapsed:.1f}s") | |
| return JSONResponse(content={"success": True, "data": processed_data}) | |
| except Exception as e: | |
| logger.exception("Failed to search student") | |
| error_msg = str(e) | |
| if "Expecting value" in error_msg: | |
| return JSONResponse(status_code=401, content={"detail": "Session expired. Please log in again."}) | |
| return JSONResponse(status_code=500, content={"detail": f"Search failed: {error_msg}"}) | |
| if __name__ == "__main__": | |
| import uvicorn | |
| # Enable running via `python main.py` directly for convenience | |
| uvicorn.run("main:app", host="0.0.0.0", port=8080, reload=True) | |