"""
Quercus Client
Handles interaction with the University of Toronto's Quercus (Canvas LMS) system.
Uses the Canvas API for data retrieval.
Canvas API Documentation: https://canvas.instructure.com/doc/api/
"""
import requests
from typing import List, Dict, Any, Optional
from datetime import datetime
class QuercusClient:
"""Client for interacting with UofT Quercus (Canvas LMS)"""
def __init__(self, api_token: str):
"""
Initialize Quercus client with API token.
Args:
api_token: Canvas API access token from Quercus settings
"""
self.api_token = api_token
self.base_url = "https://q.utoronto.ca/api/v1"
self.headers = {
"Authorization": f"Bearer {api_token}",
"Content-Type": "application/json"
}
def _make_request(self, endpoint: str, params: Optional[Dict] = None) -> Any:
"""
Make a request to the Canvas API.
Args:
endpoint: API endpoint (e.g., "/courses")
params: Optional query parameters
Returns:
JSON response from API
"""
url = f"{self.base_url}{endpoint}"
try:
response = requests.get(url, headers=self.headers, params=params)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"❌ Quercus API request failed: {str(e)}")
return None
def get_courses(self) -> List[Dict[str, Any]]:
"""
Get all courses the student is enrolled in on Quercus.
Returns:
List of course dictionaries
"""
# TODO: Implement actual API call
# endpoint = "/courses"
# params = {"enrollment_state": "active"}
# courses = self._make_request(endpoint, params)
# Mock data for demonstration
mock_courses = [
{
"course_code": "CSC148H1",
"course_name": "Introduction to Computer Science",
"course_id": 123456,
"term": "Fall 2025",
"enrollment_status": "active",
"start_date": "2025-09-08",
"end_date": "2025-12-08"
},
{
"course_code": "MAT137Y1",
"course_name": "Calculus with Proofs",
"course_id": 123457,
"term": "Fall 2025 - Winter 2026",
"enrollment_status": "active",
"start_date": "2025-09-08",
"end_date": "2026-04-30"
},
{
"course_code": "PHY151H1",
"course_name": "Foundations of Physics I",
"course_id": 123458,
"term": "Fall 2025",
"enrollment_status": "active",
"start_date": "2025-09-08",
"end_date": "2025-12-08"
},
{
"course_code": "CSC165H1",
"course_name": "Mathematical Expression and Reasoning for CS",
"course_id": 123459,
"term": "Fall 2025",
"enrollment_status": "active",
"start_date": "2025-09-08",
"end_date": "2025-12-08"
},
{
"course_code": "ENG100H1",
"course_name": "Effective Writing",
"course_id": 123460,
"term": "Fall 2025",
"enrollment_status": "active",
"start_date": "2025-09-08",
"end_date": "2025-12-08"
}
]
return mock_courses
def _get_course_id_by_code(self, course_code: str) -> Optional[int]:
"""
Find the Quercus course ID for a given course code.
Args:
course_code: Course code (e.g., "CSC148H1")
Returns:
Course ID or None if not found
"""
courses = self.get_courses()
for course in courses:
if course["course_code"] == course_code:
return course["course_id"]
return None
def get_syllabus(self, course_code: str) -> Dict[str, Any]:
"""
Fetch the syllabus for a specific course.
Args:
course_code: Course code (e.g., "CSC148H1")
Returns:
Dictionary with syllabus information
"""
course_id = self._get_course_id_by_code(course_code)
if not course_id:
return {
"error": f"Course {course_code} not found on Quercus"
}
# TODO: Implement actual API call
# endpoint = f"/courses/{course_id}"
# params = {"include": ["syllabus_body"]}
# course_data = self._make_request(endpoint, params)
# Mock data for demonstration
mock_syllabi = {
"CSC148H1": {
"course_code": "CSC148H1",
"course_name": "Introduction to Computer Science",
"syllabus_text": """
# CSC148H1: Introduction to Computer Science
## Course Description
Abstract data types and data structures for implementing them. Linked lists, stacks, queues, trees. Specifications and analysis of algorithms. Sorting and searching. Design and analysis of recursive algorithms.
## Learning Objectives
By the end of this course, students will be able to:
- Design and implement abstract data types
- Analyze algorithm efficiency using Big-O notation
- Implement and use common data structures (lists, stacks, queues, trees)
- Design recursive algorithms
- Write well-documented, maintainable code
## Course Schedule
- Week 1-2: Python review, object-oriented programming
- Week 3-4: Linked lists and stacks
- Week 5-6: Queues and recursion
- Week 7: Midterm exam
- Week 8-9: Trees and binary search trees
- Week 10-11: Sorting algorithms
- Week 12: Review and final exam preparation
## Evaluation
- Assignments (40%): 4 assignments, 10% each
- Midterm (20%): Week 7
- Final Exam (40%): During exam period
## Required Materials
- Textbook: "Data Structures and Algorithms in Python" (available online)
- Python 3.9 or higher
- PyCharm or VS Code (recommended)
## Course Policies
- Late submissions: 10% penalty per day, up to 3 days
- Academic integrity: All work must be your own
- Attendance: Strongly recommended but not mandatory
""",
"syllabus_url": f"https://q.utoronto.ca/courses/{course_id}/assignments/syllabus",
"last_updated": "2025-09-01"
},
"MAT137Y1": {
"course_code": "MAT137Y1",
"course_name": "Calculus with Proofs",
"syllabus_text": """
# MAT137Y1: Calculus with Proofs
## Course Description
A conceptual approach to calculus emphasizing understanding and mathematical reasoning. Topics include limits, continuity, differentiation, integration, sequences, and series.
## Learning Objectives
- Understand and construct mathematical proofs
- Master fundamental calculus concepts
- Apply calculus to solve real-world problems
- Develop mathematical maturity and rigor
## Evaluation
- Problem Sets (30%): Weekly assignments
- Midterm 1 (15%): October
- Midterm 2 (15%): February
- Final Exam (40%): April exam period
## Office Hours
- Monday 2-3pm, BA6180
- Wednesday 3-4pm, BA6180
- Or by appointment
""",
"syllabus_url": f"https://q.utoronto.ca/courses/{course_id}/assignments/syllabus",
"last_updated": "2025-09-01"
}
}
return mock_syllabi.get(course_code, {
"course_code": course_code,
"error": "Syllabus not available for this course"
})
def get_assignments(self, course_code: str) -> Dict[str, Any]:
"""
Get all assignments for a specific course.
Args:
course_code: Course code (e.g., "CSC148H1")
Returns:
Dictionary with assignment information
"""
course_id = self._get_course_id_by_code(course_code)
if not course_id:
return {
"error": f"Course {course_code} not found on Quercus"
}
# TODO: Implement actual API call
# endpoint = f"/courses/{course_id}/assignments"
# assignments = self._make_request(endpoint)
# Mock data for demonstration
mock_assignments = {
"CSC148H1": {
"course_code": "CSC148H1",
"assignments": [
{
"name": "Assignment 1: Linked Lists",
"due_date": "2025-10-01 23:59:00",
"points_possible": 100,
"submission_status": "submitted",
"grade": 95,
"feedback": "Excellent work!"
},
{
"name": "Assignment 2: Stacks and Queues",
"due_date": "2025-10-15 23:59:00",
"points_possible": 100,
"submission_status": "submitted",
"grade": 88,
"feedback": "Good job, minor issues with edge cases"
},
{
"name": "Assignment 3: Recursion",
"due_date": "2025-11-05 23:59:00",
"points_possible": 100,
"submission_status": "not_submitted",
"grade": None,
"feedback": None
},
{
"name": "Assignment 4: Trees",
"due_date": "2025-11-20 23:59:00",
"points_possible": 100,
"submission_status": "not_submitted",
"grade": None,
"feedback": None
}
],
"total_assignments": 4,
"completed": 2,
"pending": 2,
"average_grade": 91.5
}
}
return mock_assignments.get(course_code, {
"course_code": course_code,
"assignments": [],
"total_assignments": 0,
"completed": 0,
"pending": 0
})
def get_announcements(self, course_code: str, limit: int = 5) -> Dict[str, Any]:
"""
Get recent announcements from a specific course.
Args:
course_code: Course code (e.g., "CSC148H1")
limit: Maximum number of announcements to retrieve
Returns:
Dictionary with announcement information
"""
course_id = self._get_course_id_by_code(course_code)
if not course_id:
return {
"error": f"Course {course_code} not found on Quercus"
}
# TODO: Implement actual API call
# endpoint = f"/courses/{course_id}/discussion_topics"
# params = {"only_announcements": True, "per_page": limit}
# announcements = self._make_request(endpoint, params)
# Mock data for demonstration
mock_announcements = {
"CSC148H1": {
"course_code": "CSC148H1",
"announcements": [
{
"title": "Midterm Information",
"message": "The midterm exam will be held on October 28th in BA1170. It will cover all material up to and including Week 6. Bring your TCard and a pen. No electronic devices allowed.",
"posted_at": "2025-10-15 10:00:00",
"author": "Dr. Jane Smith"
},
{
"title": "Office Hours This Week",
"message": "Due to a conference, my office hours this week will be moved to Thursday 2-4pm instead of the usual Wednesday slot.",
"posted_at": "2025-10-14 14:30:00",
"author": "Dr. Jane Smith"
},
{
"title": "Assignment 2 Extension",
"message": "Due to technical issues with the autograder, Assignment 2 deadline has been extended by 24 hours to October 16th at 11:59pm.",
"posted_at": "2025-10-13 18:00:00",
"author": "Teaching Assistant"
}
],
"total_count": 3
}
}
result = mock_announcements.get(course_code, {
"course_code": course_code,
"announcements": [],
"total_count": 0
})
# Limit the number of announcements
if "announcements" in result:
result["announcements"] = result["announcements"][:limit]
result["total_count"] = len(result["announcements"])
return result
def get_modules(self, course_code: str) -> Dict[str, Any]:
"""
Get course modules (content organization) from Quercus.
Args:
course_code: Course code (e.g., "CSC148H1")
Returns:
Dictionary with module information
"""
course_id = self._get_course_id_by_code(course_code)
if not course_id:
return {
"error": f"Course {course_code} not found on Quercus"
}
# TODO: Implement actual API call
# endpoint = f"/courses/{course_id}/modules"
# modules = self._make_request(endpoint)
# Mock data for demonstration
return {
"course_code": course_code,
"modules": [
{
"name": "Week 1: Introduction",
"items": ["Lecture slides", "Lab exercises", "Reading materials"]
},
{
"name": "Week 2: Object-Oriented Programming",
"items": ["Lecture slides", "Code examples", "Practice problems"]
}
]
}