School MCP
by 54yyyu
Verified
- school-mcp
- src
- school_mcp
"""Deadline scraper module for fetching Canvas and Gradescope assignments."""
from typing import List, Dict, Tuple, Any, Optional
from dataclasses import dataclass
from enum import Enum
import re
import pytz
import pandas as pd
from datetime import datetime, timedelta
from gradescopeapi.classes.connection import GSConnection
from canvasapi import Canvas
from .config import get_config
class Platform(Enum):
"""Platforms for assignments."""
CANVAS = "Canvas"
GRADESCOPE = "Gradescope"
@dataclass
class Assignment:
"""Data class for an assignment."""
platform: Platform
course_name: str
assignment_name: str
due_date: datetime
late_due_date: Optional[datetime] = None
points_possible: Optional[float] = None
url: Optional[str] = None
status: Optional[str] = None
class DeadlineScraper:
"""Class for scraping deadlines from Canvas and Gradescope."""
def __init__(self):
"""Initialize connections to Canvas and Gradescope."""
try:
config = get_config()
# Initialize Gradescope
self.gs_connection = GSConnection()
self.gs_connection.login(
config['gradescope_email'],
config['gradescope_password']
)
# Initialize Canvas
domain = config['canvas_domain'].replace('https://', '').replace('http://', '')
self.canvas = Canvas(f'https://{domain}', config['canvas_access_token'])
except Exception as e:
raise ValueError(f"Error initializing DeadlineScraper: {str(e)}")
def _get_current_term_info(self) -> Tuple[str, str, str]:
"""
Determine current academic term information including term code.
Returns tuple of (term_name, year, term_code)
where term_code is: 1=spring, 2=summer, 3=fall
"""
now = datetime.now()
year = str(now.year)
if 1 <= now.month <= 5: # Spring
return ('Spring', year, '1')
elif 6 <= now.month <= 8: # Summer
return ('Summer', year, '2')
else: # Fall
return ('Fall', year, '3')
def _get_current_term(self) -> Tuple[str, str]:
"""
Get current term info for Gradescope (term name and year only).
Returns tuple of (term_name, year)
"""
term, year, _ = self._get_current_term_info()
return term, year
def _is_current_term_course(self, course_name: str) -> bool:
"""
Check if a course belongs to the current term based on both standardized
course codes and text patterns.
"""
current_term, current_year, current_term_code = self._get_current_term_info()
# Try to match standardized format first (e.g., ECONUN1155_001_2024_1)
standardized_pattern = r'.*_\d{3}_(\d{4})_(\d).*'
match = re.match(standardized_pattern, course_name)
if match:
year, term_code = match.groups()
return year == current_year and term_code == current_term_code
# If no match, check text-based patterns
course_lower = course_name.lower()
term_lower = current_term.lower()
# Common term patterns in text-based course names
term_patterns = [
f"{term_lower} {current_year}", # "spring 2024"
f"{term_lower}{current_year}", # "spring2024"
f"{current_year}{term_lower}", # "2024spring"
f"{term_lower}'{current_year[2:]}", # "spring'24"
f"{term_lower} '{current_year[2:]}" # "spring '24"
]
return any(pattern in course_lower for pattern in term_patterns)
def get_gradescope_assignments(self, days_ahead: int = 14) -> List[Assignment]:
"""Get upcoming assignments from Gradescope."""
try:
# Get current courses
courses = self.gs_connection.account.get_courses()
current_term, current_year = self._get_current_term()
active_courses = []
if "student" in courses:
for course_id, course in courses["student"].items():
if course.semester == current_term and course.year == current_year:
active_courses.append({
'id': course_id,
'name': course.full_name,
'short_name': course.name
})
# Get assignments for each course
assignments_list = []
for course in active_courses:
assignments = self.gs_connection.account.get_assignments(course['id'])
for assignment in assignments:
if getattr(assignment, 'submissions_status', None) != 'Submitted':
due_date = getattr(assignment, 'due_date', None)
if due_date and due_date > datetime.now(due_date.tzinfo):
# Check if it's within the days_ahead limit
if due_date <= datetime.now(due_date.tzinfo) + timedelta(days=days_ahead):
assignments_list.append(Assignment(
platform=Platform.GRADESCOPE,
course_name=course['name'],
assignment_name=assignment.name,
due_date=due_date,
late_due_date=getattr(assignment, 'late_due_date', None),
status=getattr(assignment, 'submissions_status', 'Not submitted')
))
return assignments_list
except Exception as e:
raise ValueError(f"Error fetching Gradescope assignments: {str(e)}")
def get_canvas_assignments(self, days_ahead: int = 14) -> List[Assignment]:
"""Get upcoming assignments from Canvas."""
try:
# Get all active courses first
all_courses = list(self.canvas.get_courses(
enrollment_type='student',
enrollment_state='active'
))
# Filter for current term courses
current_courses = [
course for course in all_courses
if self._is_current_term_course(course.name)
]
upcoming_assignments = []
cutoff_date = datetime.now(pytz.UTC) + timedelta(days=days_ahead)
for course in current_courses:
try:
assignments = list(course.get_assignments())
for assignment in assignments:
due_date = getattr(assignment, 'due_at', None)
if due_date:
due_date = pd.to_datetime(due_date)
if (due_date <= cutoff_date and
due_date >= datetime.now(pytz.UTC) and
getattr(assignment, 'published', False)):
try:
submission = assignment.get_submission(
self.canvas.get_current_user().id
)
submission_status = submission.workflow_state
except Exception:
submission_status = 'No submission'
if submission_status not in ['submitted', 'graded']:
upcoming_assignments.append(Assignment(
platform=Platform.CANVAS,
course_name=course.name,
assignment_name=assignment.name,
due_date=due_date,
late_due_date=None,
points_possible=getattr(assignment, 'points_possible', None),
url=getattr(assignment, 'html_url', None),
status=submission_status
))
except Exception:
continue
return upcoming_assignments
except Exception as e:
raise ValueError(f"Error fetching Canvas assignments: {str(e)}")
def get_all_assignments(self, days_ahead: int = 14) -> List[Dict[str, Any]]:
"""Get all assignments from Canvas and Gradescope as a list of dictionaries."""
try:
# Get assignments from both platforms
gradescope_assignments = self.get_gradescope_assignments(days_ahead)
canvas_assignments = self.get_canvas_assignments(days_ahead)
# Combine assignments
all_assignments = gradescope_assignments + canvas_assignments
# Sort assignments by due date
all_assignments.sort(key=lambda x: x.due_date)
# Convert to dictionaries
assignment_dicts = []
local_tz = datetime.now().astimezone().tzinfo
for assignment in all_assignments:
# Convert due date to local timezone
due_date = assignment.due_date
if isinstance(due_date, pd.Timestamp):
if due_date.tz is None:
due_date = due_date.tz_localize('UTC')
due_date = due_date.tz_convert(local_tz)
else:
if due_date.tzinfo is None:
due_date = pytz.UTC.localize(due_date)
due_date = due_date.astimezone(local_tz)
# Format the late due date
late_due_date = None
if assignment.late_due_date:
late_date = assignment.late_due_date
if isinstance(late_date, pd.Timestamp):
if late_date.tz is None:
late_date = late_date.tz_localize('UTC')
late_date = late_date.tz_convert(local_tz)
else:
if late_date.tzinfo is None:
late_date = pytz.UTC.localize(late_date)
late_date = late_date.astimezone(local_tz)
late_due_date = late_date.strftime('%Y-%m-%d %I:%M %p %Z')
# Calculate time remaining
# Convert to UTC for time calculation
if isinstance(due_date, pd.Timestamp):
due_date_utc = due_date.tz_convert(pytz.UTC)
else:
due_date_utc = due_date.astimezone(pytz.UTC)
time_remaining = due_date_utc - datetime.now(pytz.UTC)
days_remaining = time_remaining.days
hours_remaining = time_remaining.seconds // 3600
if days_remaining > 0:
time_str = f"{days_remaining} days, {hours_remaining} hours"
else:
time_str = f"{hours_remaining} hours"
assignment_dicts.append({
'platform': assignment.platform.value,
'course_name': assignment.course_name,
'assignment_name': assignment.assignment_name,
'due_date': due_date.strftime('%Y-%m-%d %I:%M %p %Z'),
'late_due_date': late_due_date,
'points_possible': assignment.points_possible,
'url': assignment.url,
'status': assignment.status,
'time_remaining': time_str
})
return assignment_dicts
except Exception as e:
raise ValueError(f"Error fetching assignments: {str(e)}")
ID: sz7cciep9p