diff --git a/backend/main.py b/backend/main.py index f827449..b15c237 100644 --- a/backend/main.py +++ b/backend/main.py @@ -405,6 +405,35 @@ def get_courses_batch(request: Request, codes: List[str]): finally: conn.close() +@app.get("/courses/{course_code}/instructors") +@limiter.limit("60/minute") +def get_course_instructors(request: Request, course_code: str): + code = course_code.upper().replace("-", " ") + if not COURSE_CODE_RE.match(code): + raise HTTPException(status_code=400, detail="Invalid course code") + conn = get_connection() + try: + cur = conn.cursor() + cur.execute(""" + SELECT DISTINCT term, instructor_name + FROM course_instructors + WHERE course_code = %s + ORDER BY term, instructor_name + """, (code,)) + rows = cur.fetchall() + cur.close() + # Group by term + terms: dict = {} + for term, name in rows: + terms.setdefault(term, []).append(name) + return [{"term": t, "instructors": names} for t, names in terms.items()] + except Exception as e: + logger.error("GET /courses/%s/instructors failed: %s", course_code, e, exc_info=True) + raise HTTPException(status_code=500, detail="Internal server error") + finally: + conn.close() + + if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/backend/timetable_scraper.py b/backend/timetable_scraper.py new file mode 100644 index 0000000..83779c0 --- /dev/null +++ b/backend/timetable_scraper.py @@ -0,0 +1,192 @@ +""" +Scrapes Carleton Central timetable for instructor names per course per term. +Populates the course_instructors table. + +Usage: python3 timetable_scraper.py +""" + +import os, re, time, requests +from psycopg2.extras import execute_values +from db import get_connection +from dotenv import load_dotenv + +load_dotenv() + +BASE_URL = "https://central.carleton.ca/prod" +TERMS = [ + ("202630", "Fall 2026"), + ("202710", "Winter 2027"), +] + +# All undergrad subject codes we care about (pulled from the search page) +def get_subject_codes(session: requests.Session, term_code: str, session_id: str) -> list[str]: + resp = session.post(f"{BASE_URL}/bwysched.p_search_fields", data={ + "wsea_code": "EXT", + "term_code": term_code, + "session_id": session_id, + }) + return re.findall(r'