From 4a3b0dc9c722be7e912f6e854d0751395054d705 Mon Sep 17 00:00:00 2001 From: UserPlayer1234 Date: Fri, 29 May 2026 16:20:45 -0700 Subject: [PATCH 1/3] feat: locally caches changes made to Gradescope --- .../classes/_helpers/_assignment_helpers.py | 13 +- src/gradescopeapi/classes/account.py | 58 +- src/gradescopeapi/classes/assignments.py | 645 +++++++++--------- 3 files changed, 389 insertions(+), 327 deletions(-) diff --git a/src/gradescopeapi/classes/_helpers/_assignment_helpers.py b/src/gradescopeapi/classes/_helpers/_assignment_helpers.py index c134e5a..9647825 100644 --- a/src/gradescopeapi/classes/_helpers/_assignment_helpers.py +++ b/src/gradescopeapi/classes/_helpers/_assignment_helpers.py @@ -3,6 +3,8 @@ import dateutil.parser import requests +from bs4 import BeautifulSoup + from gradescopeapi import DEFAULT_GRADESCOPE_BASE_URL from gradescopeapi.classes.assignments import Assignment, Deadlines @@ -32,7 +34,7 @@ def check_page_auth(session, endpoint): return submissions_resp -def get_assignments_instructor_view(coursepage_soup): +def get_assignments_instructor_view(coursepage_soup: BeautifulSoup): assignments_list = [] sections_dict = {} element_with_props = coursepage_soup.find( @@ -102,6 +104,7 @@ def get_assignments_instructor_view(coursepage_soup): dateutil.parser.parse(late_due_date) if late_due_date else late_due_date ) + course_id = assignment["url"].split("/")[-3] assignment_id = assignment["url"].split("/")[-1] # Check if assignment has section management enabled @@ -110,6 +113,7 @@ def get_assignments_instructor_view(coursepage_soup): sections = sections_dict[assignment_id] assignment_obj = Assignment( + course_id=course_id, assignment_id=assignment_id, name=assignment["title"], deadlines=Deadlines( @@ -128,7 +132,11 @@ def get_assignments_instructor_view(coursepage_soup): return assignments_list -def get_assignments_student_view(coursepage_soup): +def get_assignments_student_view(coursepage_soup: BeautifulSoup): + # Extract course ID + course_id_header = coursepage_soup.find("div", class_="courseHeader--courseID") + course_id = course_id_header.text + # parse into list of lists: Assignments[row_elements[]] assignment_table = [] for assignment_row in coursepage_soup.find_all("tr", role="row")[ @@ -196,6 +204,7 @@ def get_assignments_student_view(coursepage_soup): # Store the extracted information in a dictionary assignment_obj = Assignment( + course_id=course_id, assignment_id=assignment_id, name=name, deadlines=Deadlines( diff --git a/src/gradescopeapi/classes/account.py b/src/gradescopeapi/classes/account.py index c9d529b..9c9c2fb 100644 --- a/src/gradescopeapi/classes/account.py +++ b/src/gradescopeapi/classes/account.py @@ -29,6 +29,9 @@ def __init__( ): self.session = session self.gradescope_base_url = gradescope_base_url + self.courses: dict[str, dict[str, Course]] = {} + self.assignments: list[Assignment] = [] + self.users: list[Member] = [] def get_courses(self) -> dict[str, dict[str, Course]]: """ @@ -54,6 +57,10 @@ def get_courses(self) -> dict[str, dict[str, Course]]: RuntimeError: If request to account page fails. """ + # check if courses were already retrieved + if self.courses != {}: + return self.courses + endpoint = f"{self.gradescope_base_url}/account" # get main page @@ -67,7 +74,8 @@ def get_courses(self) -> dict[str, dict[str, Course]]: soup = BeautifulSoup(response.text, "html.parser") # see if user is solely a student or instructor - return get_courses_info(soup) + self.courses = get_courses_info(soup) + return self.courses def get_course_users(self, course_id: str) -> list[Member]: """ @@ -79,15 +87,19 @@ def get_course_users(self, course_id: str) -> list[Member]: "One or more invalid parameters": if course_id is null or empty value "You must be logged in to access this page.": if no user is logged in """ + + # check that course_id is valid (not empty) + if not course_id: + raise Exception("Invalid Course ID") + + # check if users were already retrieved + if self.users != []: + return self.users membership_endpoint = ( f"{self.gradescope_base_url}/courses/{course_id}/memberships" ) - # check that course_id is valid (not empty) - if not course_id: - raise Exception("Invalid Course ID") - session = self.session try: @@ -98,6 +110,8 @@ def get_course_users(self, course_id: str) -> list[Member]: # get all users in the course users = get_course_members(membership_soup, course_id) + self.users = users + return users except Exception: return None @@ -118,6 +132,10 @@ def get_assignments(self, course_id: str) -> list[Assignment]: raise Exception("Invalid Course ID") session = self.session + # check if assignments were already retrieved + if self.assignments != []: + return self.assignments + # scrape page try: # this endpoint is only available if the user is a staff of the course @@ -137,8 +155,38 @@ def get_assignments(self, course_id: str) -> list[Assignment]: if not assignment_info_list: assignment_info_list = get_assignments_student_view(coursepage_soup) + self.assignments = assignment_info_list + return assignment_info_list + def get_assignment(self, course_id: str, assignment_id: str) -> Assignment: + """ + Get detailed information on a single assignment for a course + Returns: + Assignment: An Assignment object + Raises: + Exceptions: + "One or more invalid parameters": if course_id or assignment_id is null or empty value + "You are not authorized to access this page.": if logged in user is unable to access submissions + "You must be logged in to access this page.": if no user is logged in + """ + # check that course_id is valid (not empty) + if not course_id: + raise Exception("Invalid Course ID") + session = self.session + + # check if assignments were not retrieved + if self.assignments == []: + self.get_assignments(course_id=course_id) + + assignment: Assignment = next( + ( + assignment for assignment in self.assignments if assignment.assignment_id == assignment_id + ) + ) + + return assignment + def get_assignment_submissions( self, course_id: str, assignment_id: str ) -> dict[str, list[str]]: diff --git a/src/gradescopeapi/classes/assignments.py b/src/gradescopeapi/classes/assignments.py index fd43f7d..560e856 100644 --- a/src/gradescopeapi/classes/assignments.py +++ b/src/gradescopeapi/classes/assignments.py @@ -30,6 +30,7 @@ class Deadlines: @dataclass class Assignment: + course_id: str assignment_id: str name: str deadlines: Deadlines @@ -38,335 +39,339 @@ class Assignment: max_grade: str sections: dict[str, Deadlines] - -def update_assignment_date( - session: requests.Session, - course_id: str, - assignment_id: str, - release_date: datetime.datetime, - due_date: datetime.datetime, - late_due_date: datetime.datetime | None = None, - gradescope_base_url: str = DEFAULT_GRADESCOPE_BASE_URL, -) -> bool: - """Update the dates of an assignment on Gradescope. - - Args: - session (requests.Session): The session object for making HTTP requests. - course_id (str): The ID of the course. - assignment_id (str): The ID of the assignment. - release_date (datetime.datetime): The release date of the assignment. - due_date (datetime.datetime): The due date of the assignment. - late_due_date (datetime.datetime | None, optional): The late due date of the assignment. Defaults to None. - - Requirements: - release_date <= due_date <= late_due_date - - Notes: - The timezone for dates used in Gradescope is specific to an institution. For example, for NYU, the timezone is America/New_York. - For datetime objects passed to this function, the timezone should be set to the institution's timezone. - - Raises: - HTTPError: If session does not have access to configure assignment. - ValueError: If the release_date or due_date are not provided. - ValueError: If the dates are not in order. - - Returns: - bool: True if the assignment dates were successfully updated, False otherwise. - """ - - GS_EDIT_ASSIGNMENT_ENDPOINT = ( - f"{gradescope_base_url}/courses/{course_id}/assignments/{assignment_id}/edit" - ) - GS_POST_ASSIGNMENT_ENDPOINT = ( - f"{gradescope_base_url}/courses/{course_id}/assignments/{assignment_id}" - ) - - # Check release and due date - if release_date is None or due_date is None: - raise ValueError("A release date and due date must be provided") - - # Check if date requirements are met (in order) - dates = [ - date for date in [release_date, due_date, late_due_date] if date is not None - ] - if dates != sorted(dates): - raise ValueError( - "Dates must be in order: release_date <= due_date <= late_due_date" + def update_assignment_date( + self, + session: requests.Session, + release_date: datetime.datetime, + due_date: datetime.datetime, + late_due_date: datetime.datetime | None = None, + gradescope_base_url: str = DEFAULT_GRADESCOPE_BASE_URL, + ) -> bool: + """Update the dates of an assignment on Gradescope. + Saves deadline under self.deadlines attribute as a Deadlines object. + + Args: + session (requests.Session): The session object for making HTTP requests. + release_date (datetime.datetime): The release date of the assignment. + due_date (datetime.datetime): The due date of the assignment. + late_due_date (datetime.datetime | None, optional): The late due date of the assignment. Defaults to None. + + Requirements: + release_date <= due_date <= late_due_date + + Notes: + The timezone for dates used in Gradescope is specific to an institution. For example, for NYU, the timezone is America/New_York. + For datetime objects passed to this function, the timezone should be set to the institution's timezone. + + Raises: + HTTPError: If session does not have access to configure assignment. + ValueError: If the release_date or due_date are not provided. + ValueError: If the dates are not in order. + + Returns: + bool: True if the assignment dates were successfully updated, False otherwise. + """ + + GS_EDIT_ASSIGNMENT_ENDPOINT = ( + f"{gradescope_base_url}/courses/{self.course_id}/assignments/{self.assignment_id}/edit" + ) + GS_POST_ASSIGNMENT_ENDPOINT = ( + f"{gradescope_base_url}/courses/{self.course_id}/assignments/{self.assignment_id}" ) - # Get auth token - response = session.get(GS_EDIT_ASSIGNMENT_ENDPOINT) - response.raise_for_status() - soup = BeautifulSoup(response.text, "html.parser") - auth_token = soup.select_one('input[name="authenticity_token"]')["value"] - - # Setup multipart form data - multipart = MultipartEncoder( - fields={ - "utf8": "✓", - "_method": "patch", - "authenticity_token": auth_token, - "assignment[release_date_string]": ( - release_date.strftime("%Y-%m-%dT%H:%M") if release_date else "" - ), - "assignment[due_date_string]": ( - due_date.strftime("%Y-%m-%dT%H:%M") if due_date else "" - ), - "assignment[allow_late_submissions]": "1" if late_due_date else "0", - "assignment[hard_due_date_string]": ( - late_due_date.strftime("%Y-%m-%dT%H:%M") if late_due_date else "" - ), - "commit": "Save", - } - ) - headers = { - "Content-Type": multipart.content_type, - "Referer": GS_EDIT_ASSIGNMENT_ENDPOINT, - } - - response = session.post( - GS_POST_ASSIGNMENT_ENDPOINT, data=multipart, headers=headers - ) - response.raise_for_status() - - return response.status_code == 200 - - -def update_assignment_title( - session: requests.Session, - course_id: str, - assignment_id: str, - assignment_name: str, - gradescope_base_url: str = DEFAULT_GRADESCOPE_BASE_URL, -) -> bool: - """Update the dates of an assignment on Gradescope. - - Args: - session (requests.Session): The session object for making HTTP requests. - course_id (str): The ID of the course. - assignment_id (str): The ID of the assignment. - assignment_name (str): The name of the assignment to update to. - - Notes: - Assignment name cannot be all whitespace - - Raises if session does not have access to configure assignment. - - Returns: - bool: True if the assignment dates were successfully updated, False otherwise. - """ - GS_EDIT_ASSIGNMENT_ENDPOINT = ( - f"{gradescope_base_url}/courses/{course_id}/assignments/{assignment_id}/edit" - ) - GS_POST_ASSIGNMENT_ENDPOINT = ( - f"{gradescope_base_url}/courses/{course_id}/assignments/{assignment_id}" - ) - - # Get auth token - response = session.get(GS_EDIT_ASSIGNMENT_ENDPOINT) - response.raise_for_status() - soup = BeautifulSoup(response.text, "html.parser") - auth_token = soup.select_one('input[name="authenticity_token"]')["value"] - - # Setup multipart form data - multipart = MultipartEncoder( - fields={ - "utf8": "✓", - "_method": "patch", - "authenticity_token": auth_token, - "assignment[title]": assignment_name, - "commit": "Save", + # Check release and due date + if release_date is None or due_date is None: + raise ValueError("A release date and due date must be provided") + + # Check if date requirements are met (in order) + dates = [ + date for date in [release_date, due_date, late_due_date] if date is not None + ] + if dates != sorted(dates): + raise ValueError( + "Dates must be in order: release_date <= due_date <= late_due_date" + ) + + # Get auth token + response = session.get(GS_EDIT_ASSIGNMENT_ENDPOINT) + response.raise_for_status() + soup = BeautifulSoup(response.text, "html.parser") + auth_token = soup.select_one('input[name="authenticity_token"]')["value"] + + # Setup multipart form data + multipart = MultipartEncoder( + fields={ + "utf8": "✓", + "_method": "patch", + "authenticity_token": auth_token, + "assignment[release_date_string]": ( + release_date.strftime("%Y-%m-%dT%H:%M") if release_date else "" + ), + "assignment[due_date_string]": ( + due_date.strftime("%Y-%m-%dT%H:%M") if due_date else "" + ), + "assignment[allow_late_submissions]": "1" if late_due_date else "0", + "assignment[hard_due_date_string]": ( + late_due_date.strftime("%Y-%m-%dT%H:%M") if late_due_date else "" + ), + "commit": "Save", + } + ) + headers = { + "Content-Type": multipart.content_type, + "Referer": GS_EDIT_ASSIGNMENT_ENDPOINT, } - ) - headers = { - "Content-Type": multipart.content_type, - "Referer": GS_EDIT_ASSIGNMENT_ENDPOINT, - } - - response = session.post( - GS_POST_ASSIGNMENT_ENDPOINT, data=multipart, headers=headers - ) - response.raise_for_status() - - soup = BeautifulSoup(response.content, "html.parser") - error = soup.select_one(".form--requiredFieldStar.error") - if error is not None: - if error.parent is not None and error.parent.text.startswith("Title"): - raise InvalidTitleName(f"Assignment title '{assignment_name}' is invalid") + + response = session.post( + GS_POST_ASSIGNMENT_ENDPOINT, data=multipart, headers=headers + ) + response.raise_for_status() + + # Check response status and update deadline locally + if response.status_code == 200: + self.deadlines = Deadlines( + release_date=release_date, due_date=due_date, late_due_date=late_due_date + ) + return True else: - raise AssignmentUpdateError( - "Unknown error occurred trying to update assignment title" - ) + return False + + + def update_assignment_title( + self, + session: requests.Session, + assignment_name: str, + gradescope_base_url: str = DEFAULT_GRADESCOPE_BASE_URL, + ) -> bool: + """Update the dates of an assignment on Gradescope. + + Args: + session (requests.Session): The session object for making HTTP requests. + assignment_name (str): The name of the assignment to update to. + + Notes: + Assignment name cannot be all whitespace + + Raises if session does not have access to configure assignment. + + Returns: + bool: True if the assignment dates were successfully updated, False otherwise. + """ + GS_EDIT_ASSIGNMENT_ENDPOINT = ( + f"{gradescope_base_url}/courses/{self.course_id}/assignments/{self.assignment_id}/edit" + ) + GS_POST_ASSIGNMENT_ENDPOINT = ( + f"{gradescope_base_url}/courses/{self.course_id}/assignments/{self.assignment_id}" + ) - return response.status_code == 200 - - -def update_autograder_image_name( - session: requests.Session, - course_id: str, - assignment_id: str, - image_name: str, - gradescope_base_url: str = DEFAULT_GRADESCOPE_BASE_URL, -) -> bool: - """Update the Docker Hub image name of an assignment on Gradescope. - - Args: - session (requests.Session): The session object for making HTTP requests. - course_id (str): The ID of the course. - assignment_id (str): The ID of the assignment. - image_name (str): The Docker Hub Image Name (user-handle/repo:tag) - - Notes: - In most cases Gradescope does not validate that the image_name provided exists on Docker Hub. Garbage - values may still successfully return OK. You should test your autograder after updating the image name - to ensure it works as expected. - - Example image name: 'gradescope/autograder-base:ubuntu-22.04' - from https://hub.docker.com/layers/gradescope/autograder-base/ubuntu-22.04 - - Raises if session does not have access to configure autograder or if assignment does not have an autograder. - - Returns: - bool: True if the image name was successfully updated, False otherwise. - """ - GS_EDIT_AUTOGRADER_ASSIGNMENT_ENDPOINT = f"{gradescope_base_url}/courses/{course_id}/assignments/{assignment_id}/configure_autograder" - GS_POST_ASSIGNMENT_ENDPOINT = ( - f"{gradescope_base_url}/courses/{course_id}/assignments/{assignment_id}" - ) - - # Get auth token - response = session.get(GS_EDIT_AUTOGRADER_ASSIGNMENT_ENDPOINT) - response.raise_for_status() - soup = BeautifulSoup(response.text, "html.parser") - auth_token = soup.select_one('input[name="authenticity_token"]')["value"] - - # Setup multipart form data - multipart = MultipartEncoder( - fields={ - "utf8": "✓", - "_method": "patch", - "authenticity_token": auth_token, - "source_page": "configure_autograder", - "assignment[image_name]": image_name, + # Get auth token + response = session.get(GS_EDIT_ASSIGNMENT_ENDPOINT) + response.raise_for_status() + soup = BeautifulSoup(response.text, "html.parser") + auth_token = soup.select_one('input[name="authenticity_token"]')["value"] + + # Setup multipart form data + multipart = MultipartEncoder( + fields={ + "utf8": "✓", + "_method": "patch", + "authenticity_token": auth_token, + "assignment[title]": assignment_name, + "commit": "Save", + } + ) + headers = { + "Content-Type": multipart.content_type, + "Referer": GS_EDIT_ASSIGNMENT_ENDPOINT, } - ) - headers = { - "Content-Type": multipart.content_type, - "Referer": GS_EDIT_AUTOGRADER_ASSIGNMENT_ENDPOINT, - } - - response = session.post( - GS_POST_ASSIGNMENT_ENDPOINT, data=multipart, headers=headers - ) - response.raise_for_status() - - soup = BeautifulSoup(response.content, "html.parser") - return response.status_code == 200 and not soup.find( - string="Docker image not found in your current course!" - ) - - -def update_assignment_date_by_sections( - session: requests.Session, - course_id: str, - assignment_id: str, - sections: list[str], - visibility: bool, - release_date: datetime.datetime | None = None, - due_date: datetime.datetime | None = None, - late_due_date: datetime.datetime | None = None, - gradescope_base_url: str = DEFAULT_GRADESCOPE_BASE_URL, -) -> bool: - """Update the dates of an assignment for a specific section on Gradescope. - - Args: - session (requests.Session): The session object for making HTTP requests. - course_id (str): The ID of the course. - assignment_id (str): The ID of the assignment. - sections (list[str]): The list of section names. - visibility (bool): Whether the assignment is visible to the section. - release_date (datetime.datetime): The release date of the assignment. - due_date (datetime.datetime): The due date of the assignment. - late_due_date (datetime.datetime | None, optional): The late due date of the assignment. Defaults to None. - - Requirements: - release_date <= due_date <= late_due_date - - Notes: - The timezone for dates used in Gradescope is specific to an institution. For example, for NYU, the timezone is America/New_York. - For datetime objects passed to this function, the timezone should be set to the institution's timezone. - - Raises: - HTTPError: If session does not have access to configure assignment. - ValueError: If the dates are not in order. - - Returns: - bool: True if the assignment dates were successfully updated, False otherwise. - """ - - GS_EDIT_ASSIGNMENT_ENDPOINT = f"{gradescope_base_url}/courses/{course_id}/assignments/{assignment_id}/edit#section_management" - GS_POST_ASSIGNMENT_ENDPOINT = ( - f"{gradescope_base_url}/courses/{course_id}/assignments/{assignment_id}" - ) - - # Check if date requirements are met (in order) - dates = [ - date for date in [release_date, due_date, late_due_date] if date is not None - ] - if dates != sorted(dates): - raise ValueError( - "Dates must be in order: release_date <= due_date <= late_due_date" + + response = session.post( + GS_POST_ASSIGNMENT_ENDPOINT, data=multipart, headers=headers + ) + response.raise_for_status() + + soup = BeautifulSoup(response.content, "html.parser") + error = soup.select_one(".form--requiredFieldStar.error") + if error is not None: + if error.parent is not None and error.parent.text.startswith("Title"): + raise InvalidTitleName(f"Assignment title '{assignment_name}' is invalid") + else: + raise AssignmentUpdateError( + "Unknown error occurred trying to update assignment title" + ) + + return response.status_code == 200 + + + def update_autograder_image_name( + self, + session: requests.Session, + image_name: str, + gradescope_base_url: str = DEFAULT_GRADESCOPE_BASE_URL, + ) -> bool: + """Update the Docker Hub image name of an assignment on Gradescope. + + Args: + session (requests.Session): The session object for making HTTP requests. + image_name (str): The Docker Hub Image Name (user-handle/repo:tag) + + Notes: + In most cases Gradescope does not validate that the image_name provided exists on Docker Hub. Garbage + values may still successfully return OK. You should test your autograder after updating the image name + to ensure it works as expected. + + Example image name: 'gradescope/autograder-base:ubuntu-22.04' + from https://hub.docker.com/layers/gradescope/autograder-base/ubuntu-22.04 + + Raises if session does not have access to configure autograder or if assignment does not have an autograder. + + Returns: + bool: True if the image name was successfully updated, False otherwise. + """ + GS_EDIT_AUTOGRADER_ASSIGNMENT_ENDPOINT = f"{gradescope_base_url}/courses/{self.course_id}/assignments/{self.assignment_id}/configure_autograder" + GS_POST_ASSIGNMENT_ENDPOINT = ( + f"{gradescope_base_url}/courses/{self.course_id}/assignments/{self.assignment_id}" ) - # Get auth token - response = session.get(GS_EDIT_ASSIGNMENT_ENDPOINT) - response.raise_for_status() - soup = BeautifulSoup(response.text, "html.parser") - auth_token = soup.select_one('input[name="authenticity_token"]')["value"] - - # Format sections for update - sections_edits = {} - for section in sections: - sections_edits[section] = { - "visible": visibility, + # Get auth token + response = session.get(GS_EDIT_AUTOGRADER_ASSIGNMENT_ENDPOINT) + response.raise_for_status() + soup = BeautifulSoup(response.text, "html.parser") + auth_token = soup.select_one('input[name="authenticity_token"]')["value"] + + # Setup multipart form data + multipart = MultipartEncoder( + fields={ + "utf8": "✓", + "_method": "patch", + "authenticity_token": auth_token, + "source_page": "configure_autograder", + "assignment[image_name]": image_name, + } + ) + headers = { + "Content-Type": multipart.content_type, + "Referer": GS_EDIT_AUTOGRADER_ASSIGNMENT_ENDPOINT, } - # Adding dates only if they exist - if release_date: - sections_edits[section]["release_date"] = release_date.strftime( - "%Y-%m-%dT%H:%M" - ) - # Ensures the courses page updates with correct release date - sections_edits[section]["release_date_type"] = "absolute" - if due_date: - sections_edits[section]["due_date"] = due_date.strftime("%Y-%m-%dT%H:%M") - # Ensures the courses page updates with correct due date - sections_edits[section]["due_date_type"] = "absolute" - if late_due_date: - sections_edits[section]["hard_due_date"] = late_due_date.strftime( - "%Y-%m-%dT%H:%M" + response = session.post( + GS_POST_ASSIGNMENT_ENDPOINT, data=multipart, headers=headers + ) + response.raise_for_status() + + soup = BeautifulSoup(response.content, "html.parser") + return response.status_code == 200 and not soup.find( + string="Docker image not found in your current course!" + ) + + + def update_assignment_date_by_sections( + self, + session: requests.Session, + sections: list[str], + visibility: bool, + release_date: datetime.datetime | None = None, + due_date: datetime.datetime | None = None, + late_due_date: datetime.datetime | None = None, + gradescope_base_url: str = DEFAULT_GRADESCOPE_BASE_URL, + ) -> bool: + """Update the dates of an assignment for a specific section on Gradescope. + Saves section deadlines under self.sections attribute as a dictionary of section names mapped to Deadlines. + + Args: + session (requests.Session): The session object for making HTTP requests. + sections (list[str]): The list of section names. + visibility (bool): Whether the assignment is visible to the section. + release_date (datetime.datetime | None, optional): The release date of the assignment. Defaults to None. + due_date (datetime.datetime | None, optional): The due date of the assignment. Defaults to None. + late_due_date (datetime.datetime | None, optional): The late due date of the assignment. Defaults to None. + + Requirements: + release_date <= due_date <= late_due_date + + Notes: + The timezone for dates used in Gradescope is specific to an institution. For example, for NYU, the timezone is America/New_York. + For datetime objects passed to this function, the timezone should be set to the institution's timezone. + + Raises: + HTTPError: If session does not have access to configure assignment. + ValueError: If the dates are not in order. + + Returns: + bool: True if the assignment dates were successfully updated, False otherwise. + """ + + GS_EDIT_ASSIGNMENT_ENDPOINT = f"{gradescope_base_url}/courses/{self.course_id}/assignments/{self.assignment_id}/edit#section_management" + GS_POST_ASSIGNMENT_ENDPOINT = ( + f"{gradescope_base_url}/courses/{self.course_id}/assignments/{self.assignment_id}" + ) + + # Check if date requirements are met (in order) + dates = [ + date for date in [release_date, due_date, late_due_date] if date is not None + ] + if dates != sorted(dates): + raise ValueError( + "Dates must be in order: release_date <= due_date <= late_due_date" ) - # Ensures the courses page updates with correct late due date - sections_edits[section]["hard_due_date_type"] = "absolute" - - # Setup multipart form data - multipart = MultipartEncoder( - fields={ - "utf8": "✓", - "_method": "patch", - "authenticity_token": auth_token, - "assignment[section_overrides]": json.dumps(sections_edits), - "commit": "Save", + + # Get auth token + response = session.get(GS_EDIT_ASSIGNMENT_ENDPOINT) + response.raise_for_status() + soup = BeautifulSoup(response.text, "html.parser") + auth_token = soup.select_one('input[name="authenticity_token"]')["value"] + + # Format sections for update + sections_edits = {} + for section in sections: + sections_edits[section] = { + "visible": visibility, + } + + # Adding dates only if they exist + if release_date: + sections_edits[section]["release_date"] = release_date.strftime( + "%Y-%m-%dT%H:%M" + ) + # Ensures the courses page updates with correct release date + sections_edits[section]["release_date_type"] = "absolute" + if due_date: + sections_edits[section]["due_date"] = due_date.strftime("%Y-%m-%dT%H:%M") + # Ensures the courses page updates with correct due date + sections_edits[section]["due_date_type"] = "absolute" + if late_due_date: + sections_edits[section]["hard_due_date"] = late_due_date.strftime( + "%Y-%m-%dT%H:%M" + ) + # Ensures the courses page updates with correct late due date + sections_edits[section]["hard_due_date_type"] = "absolute" + + # Setup multipart form data + multipart = MultipartEncoder( + fields={ + "utf8": "✓", + "_method": "patch", + "authenticity_token": auth_token, + "assignment[section_overrides]": json.dumps(sections_edits), + "commit": "Save", + } + ) + headers = { + "Content-Type": multipart.content_type, + "Referer": GS_EDIT_ASSIGNMENT_ENDPOINT, } - ) - headers = { - "Content-Type": multipart.content_type, - "Referer": GS_EDIT_ASSIGNMENT_ENDPOINT, - } - - response = session.post( - GS_POST_ASSIGNMENT_ENDPOINT, data=multipart, headers=headers - ) - response.raise_for_status() - - return response.status_code == 200 + + response = session.post( + GS_POST_ASSIGNMENT_ENDPOINT, data=multipart, headers=headers + ) + response.raise_for_status() + + # Check response status and update section deadlines locally + if response.status_code == 200: + for section in sections: + self.sections[section] = Deadlines( + release_date=release_date, due_date=due_date, late_due_date=late_due_date, visibility=visibility + ) + return True + else: + return False From 80ebd1a2592ca523204ecce6f500521982df84f7 Mon Sep 17 00:00:00 2001 From: UserPlayer1234 Date: Fri, 29 May 2026 16:21:05 -0700 Subject: [PATCH 2/3] fix: updated tests --- tests/test_edit_assignment.py | 184 +++++++++++++++++++--------------- tests/test_graders.py | 10 +- tests/test_sections.py | 46 ++++----- tests/test_upload.py | 8 +- 4 files changed, 128 insertions(+), 120 deletions(-) diff --git a/tests/test_edit_assignment.py b/tests/test_edit_assignment.py index b2d0ecb..94e67d6 100644 --- a/tests/test_edit_assignment.py +++ b/tests/test_edit_assignment.py @@ -2,72 +2,94 @@ from datetime import datetime, timedelta -from gradescopeapi.classes.assignments import ( - update_assignment_date, - update_assignment_title, - update_autograder_image_name, - InvalidTitleName, -) +from gradescopeapi.classes.connection import GSConnection +from gradescopeapi.classes.assignments import Assignment, Deadlines, InvalidTitleName + import requests import uuid -def test_valid_change_assignment(create_session): - """Test valid extension for a student.""" - # create test session - test_session = create_session("instructor") +def test_valid_change_assignment(create_connection): + """Test valid assignment change.""" + # create test connection + test_connection: GSConnection = create_connection("instructor") course_id = "1302606" assignment_id = "8043535" + + test_assignment = test_connection.account.get_assignment(course_id, assignment_id) + release_date = datetime(2026, 1, 1) due_date = release_date + timedelta(days=1) late_due_date = due_date + timedelta(days=1) - result = update_assignment_date( - test_session, - course_id, - assignment_id, + result = test_assignment.update_assignment_date( + test_connection.session, release_date, due_date, late_due_date, ) - assert result + assert result, "Failed to update assignment" -def test_boundary_date_assignment(create_session): + release_date = datetime(2026, 1, 2) + due_date = release_date + timedelta(days=1) + late_due_date = due_date + timedelta(days=1) + + result = test_assignment.update_assignment_date( + test_connection.session, + release_date, + due_date, + late_due_date, + ) + + assert result, "Failed to update assignment" + + assert test_assignment.deadlines == Deadlines( + release_date, due_date, late_due_date + ), "Assignment object deadlines not updated locally" + + +def test_boundary_date_assignment(create_connection): """Test updating assignment with boundary date values.""" - test_session = create_session("instructor") + # create test connection + test_connection: GSConnection = create_connection("instructor") course_id = "1302606" assignment_id = "8043535" + + test_assignment = test_connection.account.get_assignment(course_id, assignment_id) + boundary_date = datetime(1900, 1, 1) # Very old date - result = update_assignment_date( - test_session, - course_id, - assignment_id, + result = test_assignment.update_assignment_date( + test_connection.session, boundary_date, boundary_date, boundary_date, ) + assert result, "Failed to update assignment with boundary dates" -def test_update_assignment_date_invalid_session(create_session): +def test_update_assignment_date_invalid_session(create_connection): """Test updating assignment with student session.""" - test_session = create_session("student") + test_connection: GSConnection = create_connection("instructor") course_id = "1302606" assignment_id = "8043535" + + test_assignment = test_connection.account.get_assignment(course_id, assignment_id) + release_date = datetime(2026, 1, 1) due_date = release_date + timedelta(days=1) late_due_date = due_date + timedelta(days=1) + student_connection = create_connection("student") + try: - update_assignment_date( - test_session, - course_id, - assignment_id, + test_assignment.update_assignment_date( + student_connection.session, release_date, due_date, late_due_date, @@ -77,56 +99,55 @@ def test_update_assignment_date_invalid_session(create_session): assert e.response.status_code == 401 # HTTP 401 Not Authorized -@pytest.mark.skip(reason="Not using autograder") -def test_autograder_valid_image_name(create_session): +def test_autograder_valid_image_name(create_connection): """Test updating assignment with valid image name.""" - test_session = create_session("instructor") + test_connection: GSConnection = create_connection("instructor") - course_id = "753413" - assignment_id = "7193007" + course_id = "1302606" + assignment_id = "8079664" image_name = "gradescope/autograder-base:ubuntu-22.04" - result = update_autograder_image_name( - test_session, - course_id, - assignment_id, + test_assignment = test_connection.account.get_assignment(course_id, assignment_id) + + result = test_assignment.update_autograder_image_name( + test_connection.session, image_name, ) assert result, "Failed to update autograder image name" -@pytest.mark.skip(reason="Not using autograder") -def test_autograder_invalid_image_name(create_session): +def test_autograder_invalid_image_name(create_connection): """Test updating assignment with invalid image name.""" - test_session = create_session("instructor") + test_connection: GSConnection = create_connection("instructor") - course_id = "753413" - assignment_id = "7193007" + course_id = "1302606" + assignment_id = "8079664" image_name = "gradescope/autograders:us-prod-docker_image-123456" - result = update_autograder_image_name( - test_session, - course_id, - assignment_id, + test_assignment = test_connection.account.get_assignment(course_id, assignment_id) + + result = test_assignment.update_autograder_image_name( + test_connection.session, image_name, ) assert not result, "Incorrectly updated to invalid autograder image name" -@pytest.mark.skip(reason="Not using autograder") -def test_autograder_invalid_session(create_session): +def test_autograder_invalid_session(create_connection): """Test updating assignment with student session.""" - test_session = create_session("student") + test_connection: GSConnection = create_connection("instructor") - course_id = "753413" - assignment_id = "7193007" + course_id = "1302606" + assignment_id = "8079664" image_name = "gradescope/autograder-base:ubuntu-22.04" + test_assignment = test_connection.account.get_assignment(course_id, assignment_id) + + student_connection: GSConnection = create_connection("student") + try: - update_autograder_image_name( - test_session, - course_id, - assignment_id, + test_assignment.update_autograder_image_name( + student_connection.session, image_name, ) assert False, "Incorrectly updated assignment with invalid session" @@ -134,20 +155,19 @@ def test_autograder_invalid_session(create_session): assert e.response.status_code == 401 # HTTP 401 Not Authorized -@pytest.mark.skip(reason="Not using autograder") -def test_autograder_invalid_assignment_type(create_session): +def test_autograder_invalid_assignment_type(create_connection): """Test updating assignment with invalid assignment type.""" - test_session = create_session("instructor") + test_connection: GSConnection = create_connection("instructor") - course_id = "753413" - assignment_id = "7205866" + course_id = "1302606" + assignment_id = "8043535" image_name = "gradescope/autograder-base:ubuntu-22.04" + test_assignment = test_connection.account.get_assignment(course_id, assignment_id) + try: - update_autograder_image_name( - test_session, - course_id, - assignment_id, + test_assignment.update_autograder_image_name( + test_connection.session, image_name, ) assert False, "Incorrectly updated assignment with invalid assignment" @@ -155,36 +175,36 @@ def test_autograder_invalid_assignment_type(create_session): assert e.response.status_code == 404 # HTTP 404 Not Found -def test_update_assignment_title_valid_random_title(create_session): +def test_update_assignment_title_valid_random_title(create_connection): """Test updating assignment with random name.""" - test_session = create_session("instructor") + test_connection: GSConnection = create_connection("instructor") course_id = "1302606" assignment_id = "8043535" new_assignment_name = f"Test Rename - {uuid.uuid4()}" - result = update_assignment_title( - test_session, - course_id, - assignment_id, + test_assignment = test_connection.account.get_assignment(course_id, assignment_id) + + result = test_assignment.update_assignment_title( + test_connection.session, new_assignment_name, ) assert result, "Failed to update assignment name" -def test_update_assignment_title_invalid_title_whitespace(create_session): +def test_update_assignment_title_invalid_title_whitespace(create_connection): """Test updating assignment with invalid name containing only whitespace.""" - test_session = create_session("instructor") + test_connection: GSConnection = create_connection("instructor") course_id = "1302606" assignment_id = "8043535" new_assignment_name = " " # whitespace only not allowed + test_assignment = test_connection.account.get_assignment(course_id, assignment_id) + try: - update_assignment_title( - test_session, - course_id, - assignment_id, + test_assignment.update_assignment_title( + test_connection.session, new_assignment_name, ) assert False, "Incorrectly updated to invalid assignment name" @@ -192,19 +212,21 @@ def test_update_assignment_title_invalid_title_whitespace(create_session): pass -def test_update_assignment_title_invalid_session(create_session): +def test_update_assignment_title_invalid_session(create_connection): """Test updating assignment with student session.""" - test_session = create_session("student") + test_connection: GSConnection = create_connection("instructor") course_id = "1302606" assignment_id = "8043535" new_assignment_name = f"Test Rename - {uuid.uuid4()}" + test_assignment = test_connection.account.get_assignment(course_id, assignment_id) + + student_connection: GSConnection = create_connection("student") + try: - update_assignment_title( - test_session, - course_id, - assignment_id, + test_assignment.update_assignment_title( + student_connection.session, new_assignment_name, ) assert False, "Incorrectly updated assignment title with invalid session" diff --git a/tests/test_graders.py b/tests/test_graders.py index d696917..020a512 100644 --- a/tests/test_graders.py +++ b/tests/test_graders.py @@ -3,29 +3,27 @@ from gradescopeapi.classes.account import Account -@pytest.mark.skip(reason="Not testing graders") def test_get_assignment_graders_non_empty(create_session): """Test getting graders for a question that has been graded.""" # create test session test_session = create_session("instructor") account = Account(test_session) - course_id = "753413" - question_id = "49653137" + course_id = "1302606" + question_id = "70001340" graders = account.get_assignment_graders(course_id, question_id) assert len(graders) > 0, "Should have at least 1 grader" -@pytest.mark.skip(reason="Not testing graders") def test_get_assignment_graders_empty(create_session): """Test getting graders for a question that has not been graded.""" # create test session test_session = create_session("instructor") account = Account(test_session) - course_id = "753413" - question_id = "49653136" + course_id = "1302606" + question_id = "70505553" graders = account.get_assignment_graders(course_id, question_id) assert len(graders) == 0, "Should not have any graders" diff --git a/tests/test_sections.py b/tests/test_sections.py index 52d3e30..3c89c20 100644 --- a/tests/test_sections.py +++ b/tests/test_sections.py @@ -2,11 +2,7 @@ from gradescopeapi.classes.account import Account -from gradescopeapi.classes.assignments import ( - update_assignment_date, - update_assignment_date_by_sections, - Deadlines, -) +from gradescopeapi.classes.assignments import Assignment, Deadlines def test_get_sections(create_session): @@ -30,9 +26,13 @@ def test_update_assignment_date_by_sections(create_session): # create account with test session test_session = create_session("instructor") account = Account(test_session) + course_id = "1302606" assignment_id = "8043535" + # retrieve assignment + assignment = account.get_assignment(course_id, assignment_id) + # retrieve sections sections_objects = account.get_sections(course_id) section_names = ["Section1"] @@ -42,10 +42,8 @@ def test_update_assignment_date_by_sections(create_session): og_due_date = og_release_date + timedelta(days=1) og_late_due_date = og_due_date + timedelta(days=1) - result = update_assignment_date( + result = assignment.update_assignment_date( session=test_session, - course_id=course_id, - assignment_id=assignment_id, release_date=og_release_date, due_date=og_due_date, late_due_date=og_late_due_date, @@ -58,10 +56,8 @@ def test_update_assignment_date_by_sections(create_session): sec_due_date = sec_release_date + timedelta(days=1) sec_late_due_date = sec_due_date + timedelta(days=1) - result = update_assignment_date_by_sections( + result = assignment.update_assignment_date_by_sections( session=test_session, - course_id=course_id, - assignment_id=assignment_id, sections=section_names, visibility=True, release_date=sec_release_date, @@ -94,7 +90,7 @@ def test_update_assignment_date_by_sections(create_session): if section_obj.section_name == "Section1" ) ) - section_deadline = assignment.sections.get(section_obj.section_id) + section_deadline = assignment.sections.get(section_obj.section_name) # check section deadline was changed assert section_deadline == Deadlines( @@ -107,9 +103,13 @@ def test_update_assignment_date_by_multiple_sections(create_session): # create account with test session test_session = create_session("instructor") account = Account(test_session) + course_id = "1302606" assignment_id = "8043535" + # retrieve assignment + assignment = account.get_assignment(course_id, assignment_id) + # retrieve sections sections_objects = account.get_sections(course_id) section_one = ["Section1"] @@ -120,10 +120,8 @@ def test_update_assignment_date_by_multiple_sections(create_session): og_due_date = og_release_date + timedelta(days=1) og_late_due_date = og_due_date + timedelta(days=1) - result = update_assignment_date( + result = assignment.update_assignment_date( session=test_session, - course_id=course_id, - assignment_id=assignment_id, release_date=og_release_date, due_date=og_due_date, late_due_date=og_late_due_date, @@ -136,10 +134,8 @@ def test_update_assignment_date_by_multiple_sections(create_session): sec1_due_date = sec1_release_date + timedelta(days=1) sec1_late_due_date = sec1_due_date + timedelta(days=1) - result = update_assignment_date_by_sections( + result = assignment.update_assignment_date_by_sections( session=test_session, - course_id=course_id, - assignment_id=assignment_id, sections=section_one, visibility=True, release_date=sec1_release_date, @@ -154,10 +150,8 @@ def test_update_assignment_date_by_multiple_sections(create_session): sec2_due_date = sec2_release_date + timedelta(days=1) sec2_late_due_date = sec2_due_date + timedelta(days=1) - result = update_assignment_date_by_sections( + result = assignment.update_assignment_date_by_sections( session=test_session, - course_id=course_id, - assignment_id=assignment_id, sections=section_two, visibility=True, release_date=sec2_release_date, @@ -190,7 +184,7 @@ def test_update_assignment_date_by_multiple_sections(create_session): if section_obj.section_name == "Section1" ) ) - section_one_deadline = assignment.sections.get(section_one_obj.section_id) + section_one_deadline = assignment.sections.get(section_one_obj.section_name) # check section one deadline was changed assert section_one_deadline == Deadlines( @@ -205,7 +199,7 @@ def test_update_assignment_date_by_multiple_sections(create_session): if section_obj.section_name == "Section2" ) ) - section_two_deadline = assignment.sections.get(section_two_obj.section_id) + section_two_deadline = assignment.sections.get(section_two_obj.section_name) # check section two deadline was changed assert section_two_deadline == Deadlines( @@ -215,10 +209,8 @@ def test_update_assignment_date_by_multiple_sections(create_session): # update section one and two to have section two deadline sections = ["Section1", "Section2"] - result = update_assignment_date_by_sections( + result = assignment.update_assignment_date_by_sections( session=test_session, - course_id=course_id, - assignment_id=assignment_id, sections=sections, visibility=True, release_date=sec2_release_date, @@ -242,7 +234,7 @@ def test_update_assignment_date_by_multiple_sections(create_session): if assignment.assignment_id == assignment_id ) ) - section_one_deadline = updated_assignment.sections.get(section_one_obj.section_id) + section_one_deadline = updated_assignment.sections.get(section_one_obj.section_name) # check section one and two deadlines are the same assert ( diff --git a/tests/test_upload.py b/tests/test_upload.py index 0e23b52..1fd40ee 100644 --- a/tests/test_upload.py +++ b/tests/test_upload.py @@ -15,7 +15,6 @@ GRADESCOPE_CI_INSTRUCTOR_PASSWORD = os.getenv("GRADESCOPE_CI_INSTRUCTOR_PASSWORD") -@pytest.mark.skip(reason="Not testing file uploads") def new_session(account_type="student"): """Creates and returns a session for testing""" connection = GSConnection() @@ -35,13 +34,12 @@ def new_session(account_type="student"): return connection.session -@pytest.mark.skip(reason="Not testing file uploads") def test_valid_upload(): # create test session test_session = new_session("student") course_id = "1302606" - assignment_id = "8043535" + assignment_id = "8079664" with ( open("tests/upload_files/text_file.txt", "rb") as text_file, @@ -61,7 +59,6 @@ def test_valid_upload(): assert submission_link is not None -@pytest.mark.skip(reason="Not testing file uploads") def test_invalid_upload(): # create test session test_session = new_session("student") @@ -86,11 +83,10 @@ def test_invalid_upload(): assert submission_link is None -@pytest.mark.skip(reason="Not testing file uploads") def test_upload_with_no_files(): test_session = new_session("student") course_id = "1302606" - assignment_id = "8043535" + assignment_id = "8079664" # No files are passed submission_link = upload_assignment(test_session, course_id, assignment_id) assert submission_link is None, "Should handle missing files gracefully" From 01a87133c4fa3f264f2e85c4b7076c061c84807c Mon Sep 17 00:00:00 2001 From: UserPlayer1234 Date: Fri, 29 May 2026 16:25:09 -0700 Subject: [PATCH 3/3] format: lint + format changes --- src/gradescopeapi/classes/account.py | 7 ++-- src/gradescopeapi/classes/assignments.py | 50 +++++++++++------------- tests/test_edit_assignment.py | 6 +-- tests/test_graders.py | 2 - tests/test_sections.py | 4 +- tests/test_upload.py | 1 - 6 files changed, 30 insertions(+), 40 deletions(-) diff --git a/src/gradescopeapi/classes/account.py b/src/gradescopeapi/classes/account.py index 9c9c2fb..2fcd412 100644 --- a/src/gradescopeapi/classes/account.py +++ b/src/gradescopeapi/classes/account.py @@ -87,7 +87,7 @@ def get_course_users(self, course_id: str) -> list[Member]: "One or more invalid parameters": if course_id is null or empty value "You must be logged in to access this page.": if no user is logged in """ - + # check that course_id is valid (not empty) if not course_id: raise Exception("Invalid Course ID") @@ -173,7 +173,6 @@ def get_assignment(self, course_id: str, assignment_id: str) -> Assignment: # check that course_id is valid (not empty) if not course_id: raise Exception("Invalid Course ID") - session = self.session # check if assignments were not retrieved if self.assignments == []: @@ -181,7 +180,9 @@ def get_assignment(self, course_id: str, assignment_id: str) -> Assignment: assignment: Assignment = next( ( - assignment for assignment in self.assignments if assignment.assignment_id == assignment_id + assignment + for assignment in self.assignments + if assignment.assignment_id == assignment_id ) ) diff --git a/src/gradescopeapi/classes/assignments.py b/src/gradescopeapi/classes/assignments.py index 560e856..98bcb81 100644 --- a/src/gradescopeapi/classes/assignments.py +++ b/src/gradescopeapi/classes/assignments.py @@ -47,7 +47,7 @@ def update_assignment_date( late_due_date: datetime.datetime | None = None, gradescope_base_url: str = DEFAULT_GRADESCOPE_BASE_URL, ) -> bool: - """Update the dates of an assignment on Gradescope. + """Update the dates of an assignment on Gradescope. Saves deadline under self.deadlines attribute as a Deadlines object. Args: @@ -72,12 +72,8 @@ def update_assignment_date( bool: True if the assignment dates were successfully updated, False otherwise. """ - GS_EDIT_ASSIGNMENT_ENDPOINT = ( - f"{gradescope_base_url}/courses/{self.course_id}/assignments/{self.assignment_id}/edit" - ) - GS_POST_ASSIGNMENT_ENDPOINT = ( - f"{gradescope_base_url}/courses/{self.course_id}/assignments/{self.assignment_id}" - ) + GS_EDIT_ASSIGNMENT_ENDPOINT = f"{gradescope_base_url}/courses/{self.course_id}/assignments/{self.assignment_id}/edit" + GS_POST_ASSIGNMENT_ENDPOINT = f"{gradescope_base_url}/courses/{self.course_id}/assignments/{self.assignment_id}" # Check release and due date if release_date is None or due_date is None: @@ -130,13 +126,14 @@ def update_assignment_date( # Check response status and update deadline locally if response.status_code == 200: self.deadlines = Deadlines( - release_date=release_date, due_date=due_date, late_due_date=late_due_date - ) + release_date=release_date, + due_date=due_date, + late_due_date=late_due_date, + ) return True else: return False - def update_assignment_title( self, session: requests.Session, @@ -157,12 +154,8 @@ def update_assignment_title( Returns: bool: True if the assignment dates were successfully updated, False otherwise. """ - GS_EDIT_ASSIGNMENT_ENDPOINT = ( - f"{gradescope_base_url}/courses/{self.course_id}/assignments/{self.assignment_id}/edit" - ) - GS_POST_ASSIGNMENT_ENDPOINT = ( - f"{gradescope_base_url}/courses/{self.course_id}/assignments/{self.assignment_id}" - ) + GS_EDIT_ASSIGNMENT_ENDPOINT = f"{gradescope_base_url}/courses/{self.course_id}/assignments/{self.assignment_id}/edit" + GS_POST_ASSIGNMENT_ENDPOINT = f"{gradescope_base_url}/courses/{self.course_id}/assignments/{self.assignment_id}" # Get auth token response = session.get(GS_EDIT_ASSIGNMENT_ENDPOINT) @@ -194,7 +187,9 @@ def update_assignment_title( error = soup.select_one(".form--requiredFieldStar.error") if error is not None: if error.parent is not None and error.parent.text.startswith("Title"): - raise InvalidTitleName(f"Assignment title '{assignment_name}' is invalid") + raise InvalidTitleName( + f"Assignment title '{assignment_name}' is invalid" + ) else: raise AssignmentUpdateError( "Unknown error occurred trying to update assignment title" @@ -202,7 +197,6 @@ def update_assignment_title( return response.status_code == 200 - def update_autograder_image_name( self, session: requests.Session, @@ -229,9 +223,7 @@ def update_autograder_image_name( bool: True if the image name was successfully updated, False otherwise. """ GS_EDIT_AUTOGRADER_ASSIGNMENT_ENDPOINT = f"{gradescope_base_url}/courses/{self.course_id}/assignments/{self.assignment_id}/configure_autograder" - GS_POST_ASSIGNMENT_ENDPOINT = ( - f"{gradescope_base_url}/courses/{self.course_id}/assignments/{self.assignment_id}" - ) + GS_POST_ASSIGNMENT_ENDPOINT = f"{gradescope_base_url}/courses/{self.course_id}/assignments/{self.assignment_id}" # Get auth token response = session.get(GS_EDIT_AUTOGRADER_ASSIGNMENT_ENDPOINT) @@ -264,7 +256,6 @@ def update_autograder_image_name( string="Docker image not found in your current course!" ) - def update_assignment_date_by_sections( self, session: requests.Session, @@ -275,7 +266,7 @@ def update_assignment_date_by_sections( late_due_date: datetime.datetime | None = None, gradescope_base_url: str = DEFAULT_GRADESCOPE_BASE_URL, ) -> bool: - """Update the dates of an assignment for a specific section on Gradescope. + """Update the dates of an assignment for a specific section on Gradescope. Saves section deadlines under self.sections attribute as a dictionary of section names mapped to Deadlines. Args: @@ -302,9 +293,7 @@ def update_assignment_date_by_sections( """ GS_EDIT_ASSIGNMENT_ENDPOINT = f"{gradescope_base_url}/courses/{self.course_id}/assignments/{self.assignment_id}/edit#section_management" - GS_POST_ASSIGNMENT_ENDPOINT = ( - f"{gradescope_base_url}/courses/{self.course_id}/assignments/{self.assignment_id}" - ) + GS_POST_ASSIGNMENT_ENDPOINT = f"{gradescope_base_url}/courses/{self.course_id}/assignments/{self.assignment_id}" # Check if date requirements are met (in order) dates = [ @@ -336,7 +325,9 @@ def update_assignment_date_by_sections( # Ensures the courses page updates with correct release date sections_edits[section]["release_date_type"] = "absolute" if due_date: - sections_edits[section]["due_date"] = due_date.strftime("%Y-%m-%dT%H:%M") + sections_edits[section]["due_date"] = due_date.strftime( + "%Y-%m-%dT%H:%M" + ) # Ensures the courses page updates with correct due date sections_edits[section]["due_date_type"] = "absolute" if late_due_date: @@ -370,7 +361,10 @@ def update_assignment_date_by_sections( if response.status_code == 200: for section in sections: self.sections[section] = Deadlines( - release_date=release_date, due_date=due_date, late_due_date=late_due_date, visibility=visibility + release_date=release_date, + due_date=due_date, + late_due_date=late_due_date, + visibility=visibility, ) return True else: diff --git a/tests/test_edit_assignment.py b/tests/test_edit_assignment.py index 94e67d6..d6e7fea 100644 --- a/tests/test_edit_assignment.py +++ b/tests/test_edit_assignment.py @@ -1,9 +1,7 @@ -import pytest - from datetime import datetime, timedelta from gradescopeapi.classes.connection import GSConnection -from gradescopeapi.classes.assignments import Assignment, Deadlines, InvalidTitleName +from gradescopeapi.classes.assignments import Deadlines, InvalidTitleName import requests import uuid @@ -47,7 +45,7 @@ def test_valid_change_assignment(create_connection): assert test_assignment.deadlines == Deadlines( release_date, due_date, late_due_date - ), "Assignment object deadlines not updated locally" + ), "Assignment object deadlines not updated locally" def test_boundary_date_assignment(create_connection): diff --git a/tests/test_graders.py b/tests/test_graders.py index 020a512..87a5622 100644 --- a/tests/test_graders.py +++ b/tests/test_graders.py @@ -1,5 +1,3 @@ -import pytest - from gradescopeapi.classes.account import Account diff --git a/tests/test_sections.py b/tests/test_sections.py index 3c89c20..c53920a 100644 --- a/tests/test_sections.py +++ b/tests/test_sections.py @@ -2,7 +2,7 @@ from gradescopeapi.classes.account import Account -from gradescopeapi.classes.assignments import Assignment, Deadlines +from gradescopeapi.classes.assignments import Deadlines def test_get_sections(create_session): @@ -103,7 +103,7 @@ def test_update_assignment_date_by_multiple_sections(create_session): # create account with test session test_session = create_session("instructor") account = Account(test_session) - + course_id = "1302606" assignment_id = "8043535" diff --git a/tests/test_upload.py b/tests/test_upload.py index 1fd40ee..1c4cfb8 100644 --- a/tests/test_upload.py +++ b/tests/test_upload.py @@ -1,5 +1,4 @@ import os -import pytest from dotenv import load_dotenv