diff --git a/pittapi/cal.py b/pittapi/cal.py index f04081a..955c26b 100644 --- a/pittapi/cal.py +++ b/pittapi/cal.py @@ -29,11 +29,21 @@ class Event(NamedTuple): meta: str -ACADEMIC_CALENDAR_URL: str = "https://25livepub.collegenet.com/calendars/pitt-academic-calendar.json" -GRADES_CALENDAR_URL: str = "https://25livepub.collegenet.com/calendars/pitt-grades-calendar.json" -ENROLLMENT_CALENDAR_URL: str = "https://25livepub.collegenet.com/calendars/pitt-enrollment-calendar.json" -COURSE_CALENDAR_URL: str = "https://25livepub.collegenet.com/calendars/pitt-courseclass-calendar.json" -GRADUATION_CALENDAR_URL: str = "https://25livepub.collegenet.com/calendars/pitt-graduation-calendar.json" +ACADEMIC_CALENDAR_URL: str = ( + "https://25livepub.collegenet.com/calendars/pitt-academic-calendar.json" +) +GRADES_CALENDAR_URL: str = ( + "https://25livepub.collegenet.com/calendars/pitt-grades-calendar.json" +) +ENROLLMENT_CALENDAR_URL: str = ( + "https://25livepub.collegenet.com/calendars/pitt-enrollment-calendar.json" +) +COURSE_CALENDAR_URL: str = ( + "https://25livepub.collegenet.com/calendars/pitt-courseclass-calendar.json" +) +GRADUATION_CALENDAR_URL: str = ( + "https://25livepub.collegenet.com/calendars/pitt-graduation-calendar.json" +) def _fetch_calendar_events(url: str) -> list[Event]: diff --git a/pittapi/course.py b/pittapi/course.py index 5e34969..740e42d 100644 --- a/pittapi/course.py +++ b/pittapi/course.py @@ -151,7 +151,9 @@ def get_subject_courses(subject: str) -> Subject: return Subject(subject_code=subject, courses=courses) -def get_course_details(term: str | int, subject: str, course: str | int) -> CourseDetails: +def get_course_details( + term: str | int, subject: str, course: str | int +) -> CourseDetails: term = _validate_term(term) subject = _validate_subject(subject) course = _validate_course(course) @@ -165,7 +167,11 @@ def get_course_details(term: str | int, subject: str, course: str | int) -> Cour credit_range = (json_response["units_minimum"], json_response["units_maximum"]) requisites = None - if "offerings" in json_response and len(json_response["offerings"]) != 0 and "req_group" in json_response["offerings"][0]: + if ( + "offerings" in json_response + and len(json_response["offerings"]) != 0 + and "req_group" in json_response["offerings"][0] + ): requisites = json_response["offerings"][0]["req_group"] components = None @@ -199,9 +205,13 @@ def get_course_details(term: str | int, subject: str, course: str | int) -> Cour status = section["enrl_stat_descr"] instructors = None - if len(section["instructors"]) != 0 and section["instructors"][0] != "To be Announced": + if ( + len(section["instructors"]) != 0 + and section["instructors"][0] != "To be Announced" + ): instructors = [ - Instructor(name=instructor["name"], email=instructor["email"]) for instructor in section["instructors"] + Instructor(name=instructor["name"], email=instructor["email"]) + for instructor in section["instructors"] ] meetings = None @@ -272,7 +282,9 @@ def get_section_details(term: str | int, class_number: str | int) -> Section: date_range = meeting["date_range"].split(" - ") instructors = None - if len(meeting["instructors"]) != 0 and meeting["instructors"][0]["name"] not in ["To be Announced", "-"]: + if len(meeting["instructors"]) != 0 and meeting["instructors"][0][ + "name" + ] not in ["To be Announced", "-"]: instructors = [] for instructor in meeting["instructors"]: name = instructor["name"] @@ -333,7 +345,9 @@ def _validate_term(term: str | int) -> str: """Validates that the term entered follows the pattern that Pitt does for term codes.""" if VALID_TERMS.match(str(term)): return str(term) - raise ValueError("Term entered isn't a valid Pitt term, must match regex " + TERM_REGEX) + raise ValueError( + "Term entered isn't a valid Pitt term, must match regex " + TERM_REGEX + ) def _validate_subject(subject: str) -> str: @@ -378,14 +392,18 @@ def _get_course_info(course_id: str) -> JSON: def _get_course_sections(course_id: str, term: str) -> JSON: - response: JSON = requests.get(COURSE_SECTIONS_API.format(id=course_id, term=term)).json() + response: JSON = requests.get( + COURSE_SECTIONS_API.format(id=course_id, term=term) + ).json() if len(response["sections"]) == 0: raise ValueError("Invalid course ID; course with that ID does not exist") return response def _get_section_details(term: str | int, section_id: str | int) -> JSON: - response: JSON = requests.get(SECTION_DETAILS_API.format(term=term, id=section_id)).json() + response: JSON = requests.get( + SECTION_DETAILS_API.format(term=term, id=section_id) + ).json() if "error" in response: raise ValueError("Invalid section ID; section with that ID does not exist") return response diff --git a/pittapi/dining.py b/pittapi/dining.py index 760a849..50378c1 100644 --- a/pittapi/dining.py +++ b/pittapi/dining.py @@ -80,7 +80,9 @@ def get_locations() -> dict[str, JSON]: return dining_locations -def get_location_hours(location_name: str | None = None, date: datetime | None = None) -> dict[str, list[dict[str, int]]]: +def get_location_hours( + location_name: str | None = None, date: datetime | None = None +) -> dict[str, list[dict[str, int]]]: """Returns dictionary containing Opening and Closing times of locations open on date. - Ex:{'The Eatery': [{'start_hour': 7, 'start_minutes': 0, 'end_hour': 0, 'end_minutes': 0}]} - if location_name is None, returns times for all locations @@ -108,19 +110,28 @@ def get_location_hours(location_name: str | None = None, date: datetime | None = if location_name is None: hours = { - location["name"]: day["hours"] for location in locations for day in location["week"] if day["date"] == date_str + location["name"]: day["hours"] + for location in locations + for day in location["week"] + if day["date"] == date_str } return hours for location in locations: if location["name"].upper() == location_name: - hours = {location["name"]: day["hours"] for day in location["week"] if day["date"] == date_str} + hours = { + location["name"]: day["hours"] + for day in location["week"] + if day["date"] == date_str + } return hours return {} -def get_location_menu(location: str, date: datetime | None = None, period_name: str | None = None) -> JSON: +def get_location_menu( + location: str, date: datetime | None = None, period_name: str | None = None +) -> JSON: """Returns menu data for given dining location on given day/period - period_name used for locations with different serving periods(i.e. 'Breakfast','Lunch','Dinner','Late Night') - None -> Returns menu for first(or only) period at location @@ -153,7 +164,9 @@ def get_location_menu(location: str, date: datetime | None = None, period_name: period_id = period["id"] menu_resp = requests.get( - MENU_URL.format(location_id=location_id, period_id=period_id, date_str=date_str), + MENU_URL.format( + location_id=location_id, period_id=period_id, date_str=date_str + ), headers=REQUEST_HEADERS, ) menu: JSON = menu_resp.json()["menu"] diff --git a/pittapi/gym.py b/pittapi/gym.py index fe57597..3629dd3 100644 --- a/pittapi/gym.py +++ b/pittapi/gym.py @@ -56,7 +56,12 @@ def from_text(cls, text: str) -> Gym: except ValueError: percentage = 0 - return cls(name=name, last_updated=date_time, current_count=count, percent_full=percentage) + return cls( + name=name, + last_updated=date_time, + current_count=count, + percent_full=percentage, + ) def get_all_gyms_info() -> list[Gym]: @@ -80,6 +85,11 @@ def get_gym_info(gym_name: str) -> Gym | None: info = get_all_gyms_info() if gym_name in GYM_NAMES: for gym in info: - if gym.name == gym_name and gym.last_updated and gym.current_count and gym.percent_full: + if ( + gym.name == gym_name + and gym.last_updated + and gym.current_count + and gym.percent_full + ): return gym return None diff --git a/pittapi/lab.py b/pittapi/lab.py index 043bff1..b72bf4d 100644 --- a/pittapi/lab.py +++ b/pittapi/lab.py @@ -73,7 +73,9 @@ def get_one_lab_data(lab_name: str) -> Lab: if lab_name not in AVAIL_LAB_ID_MAP.keys(): # Dicts are guaranteed to preserve insertion order as of Python 3.7, # so the list of valid options will always be printed in the same order - raise ValueError(f"Invalid lab name: {lab_name}. Valid options: {', '.join(AVAIL_LAB_ID_MAP.keys())}") + raise ValueError( + f"Invalid lab name: {lab_name}. Valid options: {', '.join(AVAIL_LAB_ID_MAP.keys())}" + ) req = requests.get( PITT_BASE_URL + AVAIL_LAB_ID_MAP[lab_name] + "/status.json?noredir=1", @@ -81,9 +83,13 @@ def get_one_lab_data(lab_name: str) -> Lab: ) if req.status_code == 404: - raise LabAPIError("The Lab ID was invalid. Please open a GitHub issue so we can resolve this.") + raise LabAPIError( + "The Lab ID was invalid. Please open a GitHub issue so we can resolve this." + ) elif req.status_code != 200: - raise LabAPIError(f"An unexpected error occurred while fetching lab data: {req.text}") + raise LabAPIError( + f"An unexpected error occurred while fetching lab data: {req.text}" + ) else: lab_data = req.json() @@ -113,7 +119,9 @@ def get_one_lab_data(lab_name: str) -> Lab: elif up == 3: out_of_service_computers += 1 else: - raise LabAPIError(f"Unknown 'up' value for {computer_info['addr']} in {name}: {up}") + raise LabAPIError( + f"Unknown 'up' value for {computer_info['addr']} in {name}: {up}" + ) return Lab( name, diff --git a/pittapi/laundry.py b/pittapi/laundry.py index 42fd0ea..b24bc0a 100644 --- a/pittapi/laundry.py +++ b/pittapi/laundry.py @@ -90,7 +90,9 @@ def _parse_laundry_object_json(json: JSON) -> list[LaundryMachine]: machine1_name = json["appliance_desc"] machine1_num_match = NUMBER_REGEX.search(machine1_name) if not machine1_num_match: - raise ValueError(f"Found a combo machine with an invalid machine name: {machine1_name}") + raise ValueError( + f"Found a combo machine with an invalid machine name: {machine1_name}" + ) machine1_num = int(machine1_num_match.group(0)) machine1_type = "washer" if machine1_num % 2 == 0 else "dryer" machine1_id = json["appliance_desc_key"] @@ -101,7 +103,9 @@ def _parse_laundry_object_json(json: JSON) -> list[LaundryMachine]: machine2_name = json["appliance_desc2"] machine2_num_match = NUMBER_REGEX.search(machine2_name) if not machine2_num_match: - raise ValueError(f"Found a combo machine with an invalid machine name: {machine2_name}") + raise ValueError( + f"Found a combo machine with an invalid machine name: {machine2_name}" + ) machine2_num = int(machine2_num_match.group(0)) machine2_type = "washer" if machine2_num % 2 == 0 else "dryer" machine2_id = json["appliance_desc_key2"] @@ -111,10 +115,18 @@ def _parse_laundry_object_json(json: JSON) -> list[LaundryMachine]: return [ LaundryMachine( - name=machine1_name, id=machine1_id, status=machine1_status, type=machine1_type, time_left=time_left1 + name=machine1_name, + id=machine1_id, + status=machine1_status, + type=machine1_type, + time_left=time_left1, ), LaundryMachine( - name=machine2_name, id=machine2_id, status=machine2_status, type=machine2_type, time_left=time_left2 + name=machine2_name, + id=machine2_id, + status=machine2_status, + type=machine2_type, + time_left=time_left2, ), ] elif json["type"] in ("washFL", "dry"): # Only washers/only dryers @@ -125,10 +137,18 @@ def _parse_laundry_object_json(json: JSON) -> list[LaundryMachine]: unavailable = machine_status in ("Out of service", "Offline") time_left = None if unavailable else json["time_remaining"] machines = [ - LaundryMachine(name=machine_name, id=machine_id, status=machine_status, type=machine_type, time_left=time_left) + LaundryMachine( + name=machine_name, + id=machine_id, + status=machine_status, + type=machine_type, + time_left=time_left, + ) ] - if "type2" in json: # Double machine (two washers/two dryers), add second component separately + if ( + "type2" in json + ): # Double machine (two washers/two dryers), add second component separately machine_type = "washer" if json["type2"] == "washFL" else "dryer" machine_name = json["appliance_desc2"] machine_id = json["appliance_desc_key2"] @@ -136,7 +156,13 @@ def _parse_laundry_object_json(json: JSON) -> list[LaundryMachine]: unavailable = machine_status in ("Out of service", "Offline") time_left = None if unavailable else json["time_remaining2"] machines.append( - LaundryMachine(name=machine_name, id=machine_id, status=machine_status, type=machine_type, time_left=time_left) + LaundryMachine( + name=machine_name, + id=machine_id, + status=machine_status, + type=machine_type, + time_left=time_left, + ) ) return machines diff --git a/pittapi/library.py b/pittapi/library.py index 001c0c9..570619f 100644 --- a/pittapi/library.py +++ b/pittapi/library.py @@ -126,12 +126,16 @@ def _extract_documents(documents: list[dict[str, Any]]) -> list[dict[str, Any]]: return new_docs -def _extract_facets(facet_fields: list[dict[str, Any]]) -> dict[str, list[dict[str, Any]]]: +def _extract_facets( + facet_fields: list[dict[str, Any]] +) -> dict[str, list[dict[str, Any]]]: facets: dict[str, list[dict[str, Any]]] = {} for facet in facet_fields: facets[facet["display_name"]] = [] for count in facet["counts"]: - facets[facet["display_name"]].append({"value": count["value"], "count": count["count"]}) + facets[facet["display_name"]].append( + {"value": count["value"], "count": count["count"]} + ) return facets diff --git a/pittapi/news.py b/pittapi/news.py index ae1462c..e3c7cc7 100644 --- a/pittapi/news.py +++ b/pittapi/news.py @@ -31,7 +31,12 @@ ) PITT_BASE_URL = "https://www.pitt.edu" -Category = Literal["features-articles", "accolades-honors", "ones-to-watch", "announcements-and-updates"] +Category = Literal[ + "features-articles", + "accolades-honors", + "ones-to-watch", + "announcements-and-updates", +] Topic = Literal[ "university-news", "health-and-wellness", @@ -84,7 +89,12 @@ def from_html(cls, article_html: Element) -> Article: article_description = article_subheading.text.strip() article_tags = [tag.text.strip() for tag in article_tags_list] - return cls(title=article_title, description=article_description, url=article_url, tags=article_tags) + return cls( + title=article_title, + description=article_description, + url=article_url, + tags=article_tags, + ) def _get_page_articles( @@ -98,10 +108,16 @@ def _get_page_articles( page_num_str = str(page_num) if page_num else "" response: HTMLResponse = sess.get( NEWS_BY_CATEGORY_URL.format( - category=category, topic_id=TOPIC_ID_MAP[topic], year=year_str, query=query, page_num=page_num_str + category=category, + topic_id=TOPIC_ID_MAP[topic], + year=year_str, + query=query, + page_num=page_num_str, ) ) - main_content: Element = response.html.xpath("/html/body/div/main/div/section", first=True) + main_content: Element = response.html.xpath( + "/html/body/div/main/div/section", first=True + ) news_cards: list[Element] = main_content.find("div.news-card") page_articles = [Article.from_html(news_card) for news_card in news_cards] return page_articles diff --git a/pittapi/shuttle.py b/pittapi/shuttle.py index 6d906ea..0e14645 100644 --- a/pittapi/shuttle.py +++ b/pittapi/shuttle.py @@ -24,9 +24,15 @@ JSON = dict[str, Any] API_KEY = "8882812681" -VEHICLE_POINTS_URL = "http://www.pittshuttle.com/Services/JSONPRelay.svc/GetMapVehiclePoints" -ARRIVAL_TIMES_URL = "http://www.pittshuttle.com/Services/JSONPRelay.svc/GetRouteStopArrivals" -STOP_ESTIMATES_URL = "http://www.pittshuttle.com/Services/JSONPRelay.svc/GetVehicleRouteStopEstimates" +VEHICLE_POINTS_URL = ( + "http://www.pittshuttle.com/Services/JSONPRelay.svc/GetMapVehiclePoints" +) +ARRIVAL_TIMES_URL = ( + "http://www.pittshuttle.com/Services/JSONPRelay.svc/GetRouteStopArrivals" +) +STOP_ESTIMATES_URL = ( + "http://www.pittshuttle.com/Services/JSONPRelay.svc/GetVehicleRouteStopEstimates" +) ROUTES_URL = "http://www.pittshuttle.com/Services/JSONPRelay.svc/GetRoutesForMap" sess = requests.session() diff --git a/pittapi/sports.py b/pittapi/sports.py index e70f8a7..01c0a16 100644 --- a/pittapi/sports.py +++ b/pittapi/sports.py @@ -19,7 +19,9 @@ import requests -FOOTBALL_URL = "http://site.api.espn.com/apis/site/v2/sports/football/college-football/teams/pitt" +FOOTBALL_URL = ( + "http://site.api.espn.com/apis/site/v2/sports/football/college-football/teams/pitt" +) MENS_BASKETBALL_URL = "http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/teams/pittsburgh" @@ -47,7 +49,10 @@ def get_next_mens_basketball_game() -> dict: status = None if next_game["competitions"][0]["status"]["type"]["name"] == "STATUS_FINAL": status = "GAME_COMPLETE" - elif next_game["competitions"][0]["status"]["type"]["name"] == "STATUS_IN_PROGRESS": + elif ( + next_game["competitions"][0]["status"]["type"]["name"] + == "STATUS_IN_PROGRESS" + ): status = "IN_PROGRESS" if next_game["competitions"][0]["competitors"][0]["id"] == 221: opponent = next_game["competitions"][0]["competitors"][0] @@ -105,7 +110,10 @@ def get_next_football_game() -> dict: status = None if next_game["competitions"][0]["status"]["type"]["name"] == "STATUS_FINAL": status = "GAME_COMPLETE" - elif next_game["competitions"][0]["status"]["type"]["name"] == "STATUS_IN_PROGRESS": + elif ( + next_game["competitions"][0]["status"]["type"]["name"] + == "STATUS_IN_PROGRESS" + ): status = "IN_PROGRESS" if next_game["competitions"][0]["competitors"][0]["id"] == 221: opponent = next_game["competitions"][0]["competitors"][1] diff --git a/pittapi/textbook.py b/pittapi/textbook.py index 197314a..b4479ea 100644 --- a/pittapi/textbook.py +++ b/pittapi/textbook.py @@ -16,7 +16,9 @@ COURSES_URL = BASE_URL + "compare/courses/?id={dept_id}&term_id={term_id}" BOOKS_URL = BASE_URL + "compare/books?id={section_id}" -CURRENT_TERM_ID = 78104 # Term ID for fall 2024, TODO: figure out how this ID is generated +CURRENT_TERM_ID = ( + 78104 # Term ID for fall 2024, TODO: figure out how this ID is generated +) MAX_REQUEST_ATTEMPTS = 3 sess = HTMLSession() @@ -44,7 +46,9 @@ def __post_init__(self) -> None: self.course_num = "0" * (4 - len(self.course_num)) + self.course_num if self.instructor: self.instructor = self.instructor.upper() - if self.section_num and (len(self.section_num) != 4 or not self.section_num.isdigit()): + if self.section_num and ( + len(self.section_num) != 4 or not self.section_num.isdigit() + ): raise ValueError("Invalid section number") @@ -73,9 +77,13 @@ def _update_headers() -> None: base_response: HTMLResponse = sess.get(BASE_URL) if base_response.status_code == 200: break - warnings.warn(f"Attempt {i + 1} to connect to textbook site failed, trying again") + warnings.warn( + f"Attempt {i + 1} to connect to textbook site failed, trying again" + ) if base_response.status_code != 200: # Request failed too many times - raise ConnectionError(f"Failed to connect to textbook site after {MAX_REQUEST_ATTEMPTS} attempts") + raise ConnectionError( + f"Failed to connect to textbook site after {MAX_REQUEST_ATTEMPTS} attempts" + ) elements = base_response.html.find("meta") assert isinstance(elements, list) @@ -85,7 +93,9 @@ def _update_headers() -> None: global request_headers request_headers = {"X-CSRF-Token": csrf_token} return - raise ConnectionError("Unable to find valid request credentials, cannot connect to textbook site") + raise ConnectionError( + "Unable to find valid request credentials, cannot connect to textbook site" + ) def _update_subject_map() -> None: @@ -93,20 +103,28 @@ def _update_subject_map() -> None: _update_headers() for i in range(MAX_REQUEST_ATTEMPTS): - subject_response = sess.get(SUBJECTS_URL.format(term_id=CURRENT_TERM_ID), headers=request_headers) + subject_response = sess.get( + SUBJECTS_URL.format(term_id=CURRENT_TERM_ID), headers=request_headers + ) if subject_response.status_code == 200: break - warnings.warn(f"Attempt {i + 1} to retrieve list of subjects failed, trying again") + warnings.warn( + f"Attempt {i + 1} to retrieve list of subjects failed, trying again" + ) _update_headers() # Try again with new CSRF token if subject_response.status_code != 200: # Request failed too many times - raise ConnectionError(f"Failed to retrieve list of subjects after {MAX_REQUEST_ATTEMPTS} attempts") + raise ConnectionError( + f"Failed to retrieve list of subjects after {MAX_REQUEST_ATTEMPTS} attempts" + ) subject_json: list[dict[str, str]] = subject_response.json() global subject_map subject_map = {entry["name"]: entry["id"] for entry in subject_json} -def _find_section_from_json(sections: list[dict[str, str]], instructor: str | None, section_num: str | None) -> str: +def _find_section_from_json( + sections: list[dict[str, str]], instructor: str | None, section_num: str | None +) -> str: if section_num: for section in sections: if section["name"] == section_num: @@ -137,7 +155,10 @@ def _get_textbooks_for_ids(ids: list[str]) -> list[Textbook]: if not request_headers: _update_headers() - responses = grequests.imap(grequests.get(BOOKS_URL.format(section_id=id), headers=request_headers) for id in ids) + responses = grequests.imap( + grequests.get(BOOKS_URL.format(section_id=id), headers=request_headers) + for id in ids + ) books = [] for response in responses: for book_json in response.json(): @@ -150,11 +171,17 @@ def _get_textbooks_for_ids(ids: list[str]) -> list[Textbook]: def _get_textbooks_from_json( - course_json: list[dict[str, Any]], subject: str, course_num: str, instructor: str | None, section_num: str | None + course_json: list[dict[str, Any]], + subject: str, + course_num: str, + instructor: str | None, + section_num: str | None, ) -> list[Textbook]: for course in course_json: if course["id"] == subject + course_num: - section_id = _find_section_from_json(course["sections"], instructor, section_num) + section_id = _find_section_from_json( + course["sections"], instructor, section_num + ) return _get_textbooks_for_ids([section_id]) raise LookupError(f"{subject} {course_num} is not a valid course") @@ -168,14 +195,21 @@ def get_textbooks_for_course(course: CourseInfo) -> list[Textbook]: for i in range(MAX_REQUEST_ATTEMPTS): course_response = sess.get( - COURSES_URL.format(dept_id=subject_map[course.subject], term_id=CURRENT_TERM_ID), headers=request_headers + COURSES_URL.format( + dept_id=subject_map[course.subject], term_id=CURRENT_TERM_ID + ), + headers=request_headers, ) if course_response.status_code == 200: break - warnings.warn(f"Attempt {i} to retrieve list of {course.subject} courses failed, trying again") + warnings.warn( + f"Attempt {i} to retrieve list of {course.subject} courses failed, trying again" + ) _update_headers() # Try again with new CSRF token if course_response.status_code != 200: # Request failed too many times - raise ConnectionError(f"Failed to retrieve list of {course.subject} courses from textbook site") + raise ConnectionError( + f"Failed to retrieve list of {course.subject} courses from textbook site" + ) return _get_textbooks_from_json( course_json=course_response.json(), @@ -199,14 +233,21 @@ def get_textbooks_for_courses(courses_info: list[CourseInfo]) -> list[Textbook]: for subject in subjects: for i in range(MAX_REQUEST_ATTEMPTS): course_response = sess.get( - COURSES_URL.format(dept_id=subject_map[subject], term_id=CURRENT_TERM_ID), headers=request_headers + COURSES_URL.format( + dept_id=subject_map[subject], term_id=CURRENT_TERM_ID + ), + headers=request_headers, ) if course_response.status_code == 200: break - warnings.warn(f"Attempt {i} to retrieve list {subject} courses failed, trying again") + warnings.warn( + f"Attempt {i} to retrieve list {subject} courses failed, trying again" + ) _update_headers() # Try again with new CSRF token if course_response.status_code != 200: # Request failed too many times - raise ConnectionError(f"Failed to retrieve list of {subject} courses from textbook site") + raise ConnectionError( + f"Failed to retrieve list of {subject} courses from textbook site" + ) courses_for_subjects[subject] = course_response.json() diff --git a/tests/course_test.py b/tests/course_test.py index d96ef99..6cc1993 100644 --- a/tests/course_test.py +++ b/tests/course_test.py @@ -22,7 +22,16 @@ from pittapi import course -from pittapi.course import Attribute, Course, CourseDetails, Instructor, Meeting, Section, SectionDetails, Subject +from pittapi.course import ( + Attribute, + Course, + CourseDetails, + Instructor, + Meeting, + Section, + SectionDetails, + Subject, +) from tests.mocks.course_mocks import ( mocked_subject_data, @@ -37,7 +46,9 @@ class CourseTest(unittest.TestCase): def setUp(self): course._get_subjects = MagicMock(return_value=mocked_subject_data) - course._get_section_details = MagicMock(return_value=mocked_section_details_data) + course._get_section_details = MagicMock( + return_value=mocked_section_details_data + ) def test_validate_term(self): # If convert to string @@ -87,7 +98,9 @@ def test_get_subject_courses(self): self.assertTrue(isinstance(test_course, Course)) def test_get_subject_courses_invalid(self): - course._get_subject_courses = MagicMock(return_value=mocked_courses_data_invalid) + course._get_subject_courses = MagicMock( + return_value=mocked_courses_data_invalid + ) self.assertRaises(ValueError, course.get_subject_courses, "nonsense") course._get_subject_courses.assert_not_called() @@ -95,7 +108,9 @@ def test_get_subject_courses_invalid(self): def test_get_course_details(self): course._get_course_id = MagicMock(return_value="105611") course._get_course_info = MagicMock(return_value=mocked_course_info_data) - course._get_course_sections = MagicMock(return_value=mocked_course_sections_data) + course._get_course_sections = MagicMock( + return_value=mocked_course_sections_data + ) course_sections = course.get_course_details("2231", "CS", "0007") @@ -112,7 +127,9 @@ def test_get_course_details(self): test_attribute = course_sections.attributes[0] self.assertTrue(isinstance(test_attribute, Attribute)) self.assertEqual(test_attribute.attribute, "DSGE") - self.assertEqual(test_attribute.attribute_description, "*DSAS General Ed. Requirements") + self.assertEqual( + test_attribute.attribute_description, "*DSAS General Ed. Requirements" + ) self.assertEqual(test_attribute.value, "ALG") self.assertEqual(test_attribute.value_description, "Algebra") test_section = course_sections.sections[0] @@ -143,7 +160,9 @@ def test_get_course_details(self): self.assertEqual(test_instructor.name, "Robert Fishel") def test_get_section_details(self): - course._get_section_details = MagicMock(return_value=mocked_section_details_data) + course._get_section_details = MagicMock( + return_value=mocked_section_details_data + ) section_details = course.get_section_details("2231", "27815") diff --git a/tests/dining_test.py b/tests/dining_test.py index 80b9dbf..98d4283 100644 --- a/tests/dining_test.py +++ b/tests/dining_test.py @@ -75,7 +75,9 @@ def test_get_location_menu(self): ) responses.add( responses.GET, - dining.PERIODS_URL.format(location_id="610b1f78e82971147c9f8ba5", date_str="24-04-12"), + dining.PERIODS_URL.format( + location_id="610b1f78e82971147c9f8ba5", date_str="24-04-12" + ), json=self.dining_menu_data, status=200, ) @@ -89,5 +91,7 @@ def test_get_location_menu(self): json=self.dining_menu_data, status=200, ) - locations = dining.get_location_menu("The Eatery", datetime.datetime(2024, 4, 12), "Breakfast") + locations = dining.get_location_menu( + "The Eatery", datetime.datetime(2024, 4, 12), "Breakfast" + ) self.assertIsInstance(locations, dict) diff --git a/tests/gym_test.py b/tests/gym_test.py index ac96630..406e892 100644 --- a/tests/gym_test.py +++ b/tests/gym_test.py @@ -15,7 +15,12 @@ def test_fetch_gym_info(self): gym_info = gym.get_all_gyms_info() expected_info = [ - gym.Gym(name="Baierl Rec Center", last_updated="07/09/2024 09:05 AM", current_count=100, percent_full=50), + gym.Gym( + name="Baierl Rec Center", + last_updated="07/09/2024 09:05 AM", + current_count=100, + percent_full=50, + ), gym.Gym( name="Bellefield Hall: Fitness Center & Weight Room", last_updated="07/09/2024 09:05 AM", @@ -23,16 +28,36 @@ def test_fetch_gym_info(self): percent_full=0, ), gym.Gym(name="Bellefield Hall: Court & Dance Studio"), - gym.Gym(name="Trees Hall: Fitness Center", last_updated="07/09/2024 09:05 AM", current_count=70, percent_full=58), - gym.Gym(name="Trees Hall: Courts", last_updated="07/09/2024 09:05 AM", current_count=20, percent_full=33), + gym.Gym( + name="Trees Hall: Fitness Center", + last_updated="07/09/2024 09:05 AM", + current_count=70, + percent_full=58, + ), + gym.Gym( + name="Trees Hall: Courts", + last_updated="07/09/2024 09:05 AM", + current_count=20, + percent_full=33, + ), gym.Gym( name="Trees Hall: Racquetball Courts & Multipurpose Room", last_updated="07/09/2024 09:05 AM", current_count=10, percent_full=25, ), - gym.Gym(name="William Pitt Union", last_updated="07/09/2024 09:05 AM", current_count=25, percent_full=25), - gym.Gym(name="Pitt Sports Dome", last_updated="07/09/2024 09:05 AM", current_count=15, percent_full=20), + gym.Gym( + name="William Pitt Union", + last_updated="07/09/2024 09:05 AM", + current_count=25, + percent_full=25, + ), + gym.Gym( + name="Pitt Sports Dome", + last_updated="07/09/2024 09:05 AM", + current_count=15, + percent_full=20, + ), ] self.assertEqual(gym_info, expected_info) @@ -43,7 +68,10 @@ def test_get_gym_info(self): gym_info = gym.get_gym_info("Baierl Rec Center") expected_info = gym.Gym( - name="Baierl Rec Center", last_updated="07/09/2024 09:05 AM", current_count=100, percent_full=50 + name="Baierl Rec Center", + last_updated="07/09/2024 09:05 AM", + current_count=100, + percent_full=50, ) self.assertEqual(gym_info, expected_info) diff --git a/tests/laundry_test.py b/tests/laundry_test.py index 4f25b02..cb24d26 100644 --- a/tests/laundry_test.py +++ b/tests/laundry_test.py @@ -49,7 +49,13 @@ def test_get_building_status_holland(self): status = laundry.get_building_status(test_building) self.assertEqual( status, - BuildingStatus(building=test_building, free_washers=0, free_dryers=15, total_washers=14, total_dryers=21), + BuildingStatus( + building=test_building, + free_washers=0, + free_dryers=15, + total_washers=14, + total_dryers=21, + ), ) @responses.activate @@ -64,7 +70,10 @@ def test_get_laundry_machine_statuses_holland(self): machines = laundry.get_laundry_machine_statuses(test_building) self.assertEqual(len(machines), 35) for machine in machines: - if machine.status in ("Available", "Idle", "Ext. Cycle") or "remaining" in machine.status: + if ( + machine.status in ("Available", "Idle", "Ext. Cycle") + or "remaining" in machine.status + ): self.assertIsNotNone(machine.time_left) elif machine.status in ("Out of service", "Offline"): self.assertIsNone(machine.time_left) @@ -83,7 +92,13 @@ def test_get_building_status_towers(self): status = laundry.get_building_status(test_building) self.assertEqual( status, - BuildingStatus(building=test_building, free_washers=1, free_dryers=1, total_washers=54, total_dryers=55), + BuildingStatus( + building=test_building, + free_washers=1, + free_dryers=1, + total_washers=54, + total_dryers=55, + ), ) @responses.activate @@ -98,7 +113,10 @@ def test_get_laundry_machine_statuses_towers(self): machines = laundry.get_laundry_machine_statuses(test_building) self.assertEqual(len(machines), 109) for machine in machines: - if machine.status in ("Available", "Idle", "Ext. Cycle") or "remaining" in machine.status: + if ( + machine.status in ("Available", "Idle", "Ext. Cycle") + or "remaining" in machine.status + ): self.assertIsNotNone(machine.time_left) elif machine.status in ("Out of service", "Offline"): self.assertIsNone(machine.time_left) diff --git a/tests/library_test.py b/tests/library_test.py index b0cf888..c45c9da 100644 --- a/tests/library_test.py +++ b/tests/library_test.py @@ -62,7 +62,9 @@ def test_hillman_total_reserved(self): json=self.hillman_query, status=200, ) - self.assertEqual(library.hillman_total_reserved(), {"Total Hillman Reservations": 4}) + self.assertEqual( + library.hillman_total_reserved(), {"Total Hillman Reservations": 4} + ) @responses.activate def test_reserved_hillman_times(self): diff --git a/tests/news_test.py b/tests/news_test.py index 203b684..4255dce 100644 --- a/tests/news_test.py +++ b/tests/news_test.py @@ -30,13 +30,21 @@ class NewsTest(unittest.TestCase): def __init__(self, *args, **kwargs): unittest.TestCase.__init__(self, *args, **kwargs) - with (SAMPLE_PATH / "news_university_news_features_articles_page_0.html").open() as f: + with ( + SAMPLE_PATH / "news_university_news_features_articles_page_0.html" + ).open() as f: self.university_news_features_articles_page_0 = f.read() - with (SAMPLE_PATH / "news_university_news_features_articles_page_1.html").open() as f: + with ( + SAMPLE_PATH / "news_university_news_features_articles_page_1.html" + ).open() as f: self.university_news_features_articles_page_1 = f.read() - with (SAMPLE_PATH / "news_university_news_features_articles_fulbright.html").open() as f: + with ( + SAMPLE_PATH / "news_university_news_features_articles_fulbright.html" + ).open() as f: self.university_news_features_articles_fulbright = f.read() - with (SAMPLE_PATH / "news_university_news_features_articles_2020.html").open() as f: + with ( + SAMPLE_PATH / "news_university_news_features_articles_2020.html" + ).open() as f: self.university_news_features_articles_2020 = f.read() @responses.activate @@ -82,7 +90,9 @@ def test_get_articles_by_topic_query(self): body=self.university_news_features_articles_fulbright, ) - university_news_articles = news.get_articles_by_topic("university-news", query=query) + university_news_articles = news.get_articles_by_topic( + "university-news", query=query + ) self.assertEqual(len(university_news_articles), 3) self.assertEqual( @@ -92,7 +102,12 @@ def test_get_articles_by_topic_query(self): description="The Fulbright U.S. Scholar Program offers faculty the opportunity " "to teach and conduct research abroad.", url="https://www.pitt.edu/pittwire/features-articles/faculty-fulbright-scholars-2024", - tags=["University News", "Innovation and Research", "Global", "Faculty"], + tags=[ + "University News", + "Innovation and Research", + "Global", + "Faculty", + ], ), ) self.assertEqual( @@ -122,7 +137,9 @@ def test_get_articles_by_topic_year(self): body=self.university_news_features_articles_2020, ) - university_news_articles = news.get_articles_by_topic("university-news", year=year) + university_news_articles = news.get_articles_by_topic( + "university-news", year=year + ) self.assertEqual(len(university_news_articles), 5) self.assertEqual( @@ -159,7 +176,9 @@ def test_get_articles_by_topic_less_than_one_page(self): body=self.university_news_features_articles_page_0, ) - university_news_articles = news.get_articles_by_topic("university-news", max_num_results=num_results) + university_news_articles = news.get_articles_by_topic( + "university-news", max_num_results=num_results + ) self.assertEqual(len(university_news_articles), num_results) self.assertEqual( @@ -199,7 +218,9 @@ def test_get_articles_by_topic_multiple_pages(self): body=self.university_news_features_articles_page_1, ) - university_news_articles = news.get_articles_by_topic("university-news", max_num_results=num_results) + university_news_articles = news.get_articles_by_topic( + "university-news", max_num_results=num_results + ) self.assertEqual(len(university_news_articles), num_results) self.assertEqual( diff --git a/tests/people_test.py b/tests/people_test.py index f34ac84..5f6ef64 100644 --- a/tests/people_test.py +++ b/tests/people_test.py @@ -40,7 +40,12 @@ def __init__(self, *args, **kwargs): @responses.activate def test_people_get_person(self): - responses.add(responses.POST, people.PEOPLE_SEARCH_URL, body=self.ramirez_test_data, status=200) + responses.add( + responses.POST, + people.PEOPLE_SEARCH_URL, + body=self.ramirez_test_data, + status=200, + ) ans = people.get_person("John C Ramirez") self.assertIsInstance(ans, list) self.assertTrue(ans[0]["name"] == "Ramirez, John C") @@ -48,14 +53,24 @@ def test_people_get_person(self): @responses.activate def test_people_get_person_too_many(self): - responses.add(responses.POST, people.PEOPLE_SEARCH_URL, body=self.too_many_test_data, status=200) + responses.add( + responses.POST, + people.PEOPLE_SEARCH_URL, + body=self.too_many_test_data, + status=200, + ) ans = people.get_person("Smith") self.assertIsInstance(ans, list) self.assertEqual(ans, [{"ERROR": "Too many people matched your criteria."}]) @responses.activate def test_people_get_person_none(self): - responses.add(responses.POST, people.PEOPLE_SEARCH_URL, body=self.none_found_test_data, status=200) + responses.add( + responses.POST, + people.PEOPLE_SEARCH_URL, + body=self.none_found_test_data, + status=200, + ) ans = people.get_person("Lebron Iverson James Jordan Kobe") self.assertIsInstance(ans, list) self.assertEqual(ans, [{"ERROR": "No one found."}]) diff --git a/tests/sports_test.py b/tests/sports_test.py index 979f822..a4c5115 100644 --- a/tests/sports_test.py +++ b/tests/sports_test.py @@ -141,7 +141,9 @@ def setUp(self): "standingSummary": "1st in ACC - Coastal", } } - sports._get_mens_basketball_data = MagicMock(return_value=mocked_basketball_data) + sports._get_mens_basketball_data = MagicMock( + return_value=mocked_basketball_data + ) sports._get_football_data = MagicMock(return_value=mocked_football_data) def test_get_mens_basketball_record(self): @@ -151,7 +153,9 @@ def test_get_mens_basketball_record_offseason(self): offseason_data = {"team": {"id": "221", "record": {}}} sports._get_mens_basketball_data = MagicMock(return_value=offseason_data) - self.assertEqual("There's no record right now.", sports.get_mens_basketball_record()) + self.assertEqual( + "There's no record right now.", sports.get_mens_basketball_record() + ) def test_get_football_record(self): self.assertEqual("10-2", sports.get_football_record()) diff --git a/tests/textbook_test.py b/tests/textbook_test.py index 88b95a7..d1340e7 100644 --- a/tests/textbook_test.py +++ b/tests/textbook_test.py @@ -62,7 +62,9 @@ def tearDown(self): responses.reset() def mock_base_site_success(self): - responses.add(responses.GET, "https://pitt.verbacompare.com/", body=self.html_text) + responses.add( + responses.GET, "https://pitt.verbacompare.com/", body=self.html_text + ) def mock_base_site_failure(self): responses.add(responses.GET, "https://pitt.verbacompare.com/", status=400) @@ -76,7 +78,9 @@ def mock_subject_map_success(self): def mock_subject_map_failure(self): responses.add( - responses.GET, f"https://pitt.verbacompare.com/compare/departments/?term={textbook.CURRENT_TERM_ID}", status=400 + responses.GET, + f"https://pitt.verbacompare.com/compare/departments/?term={textbook.CURRENT_TERM_ID}", + status=400, ) def mock_cs_courses_success(self): @@ -101,7 +105,11 @@ def mock_cs_0441_garrison_books_success(self): ) def mock_cs_0441_garrison_books_none(self): - responses.add(responses.GET, f"https://pitt.verbacompare.com/compare/books?id={CS_0441_GARRISON_SECTION_ID}", json=[]) + responses.add( + responses.GET, + f"https://pitt.verbacompare.com/compare/books?id={CS_0441_GARRISON_SECTION_ID}", + json=[], + ) def mock_math_courses_success(self): responses.add( @@ -127,7 +135,12 @@ def mock_math_0430_pan_books_success(self): def test_course_info(self): self.mock_base_site_success() self.mock_subject_map_success() - subject, course_num, instructor, section_num = "CS", "0441", "GARRISON III", "1245" + subject, course_num, instructor, section_num = ( + "CS", + "0441", + "GARRISON III", + "1245", + ) course = textbook.CourseInfo(subject, course_num, instructor, section_num) @@ -139,7 +152,12 @@ def test_course_info(self): def test_course_info_convert_input(self): self.mock_base_site_success() self.mock_subject_map_success() - subject, course_num, instructor, section_num = "cs", "441", "garrison iii", "1245" + subject, course_num, instructor, section_num = ( + "cs", + "441", + "garrison iii", + "1245", + ) course = textbook.CourseInfo(subject, course_num, instructor, section_num) @@ -163,40 +181,99 @@ def test_course_info_missing_instructor_and_section_num(self): def test_course_info_invalid_subject(self): self.mock_base_site_success() self.mock_subject_map_success() - subject, course_num, instructor, section_num = "fake_subject", "0441", "GARRISON III", "1245" + subject, course_num, instructor, section_num = ( + "fake_subject", + "0441", + "GARRISON III", + "1245", + ) - self.assertRaises(LookupError, textbook.CourseInfo, subject, course_num, instructor, section_num) + self.assertRaises( + LookupError, + textbook.CourseInfo, + subject, + course_num, + instructor, + section_num, + ) def test_course_info_invalid_course_num(self): self.mock_base_site_success() self.mock_subject_map_success() - subject, course_num, instructor, section_num = "cs", "abc", "GARRISON III", "1245" + subject, course_num, instructor, section_num = ( + "cs", + "abc", + "GARRISON III", + "1245", + ) - self.assertRaises(ValueError, textbook.CourseInfo, subject, course_num, instructor, section_num) + self.assertRaises( + ValueError, + textbook.CourseInfo, + subject, + course_num, + instructor, + section_num, + ) course_num = "44111" - self.assertRaises(ValueError, textbook.CourseInfo, subject, course_num, instructor, section_num) + self.assertRaises( + ValueError, + textbook.CourseInfo, + subject, + course_num, + instructor, + section_num, + ) def test_course_info_invalid_section_num(self): self.mock_base_site_success() self.mock_subject_map_success() - subject, course_num, instructor, section_num = "cs", "0441", "GARRISON III", "12456" + subject, course_num, instructor, section_num = ( + "cs", + "0441", + "GARRISON III", + "12456", + ) - self.assertRaises(ValueError, textbook.CourseInfo, subject, course_num, instructor, section_num) + self.assertRaises( + ValueError, + textbook.CourseInfo, + subject, + course_num, + instructor, + section_num, + ) @mark.filterwarnings("ignore:Attempt") @responses.activate def test_course_info_failing_header_requests(self): self.mock_base_site_failure() - self.assertRaises(ConnectionError, textbook.CourseInfo, "CS", "0441", instructor="GARRISON III") + self.assertRaises( + ConnectionError, + textbook.CourseInfo, + "CS", + "0441", + instructor="GARRISON III", + ) @responses.activate def test_course_info_no_headers(self): - responses.add(responses.GET, "https://pitt.verbacompare.com/", body="") + responses.add( + responses.GET, + "https://pitt.verbacompare.com/", + body="", + ) - self.assertRaises(ConnectionError, textbook.CourseInfo, "CS", "0441", instructor="GARRISON III") + self.assertRaises( + ConnectionError, + textbook.CourseInfo, + "CS", + "0441", + instructor="GARRISON III", + ) @mark.filterwarnings("ignore:Attempt") @responses.activate @@ -204,7 +281,13 @@ def test_course_info_failing_subject_map_requests(self): self.mock_base_site_success() self.mock_subject_map_failure() - self.assertRaises(ConnectionError, textbook.CourseInfo, "CS", "0441", instructor="GARRISON III") + self.assertRaises( + ConnectionError, + textbook.CourseInfo, + "CS", + "0441", + instructor="GARRISON III", + ) def test_textbook_from_json(self): self.assertEqual(len(self.cs_0441_textbook_data), 1) @@ -216,7 +299,10 @@ def test_textbook_from_json(self): self.assertEqual(textbook_info.author, "Redshelf Ia") self.assertIsNone(textbook_info.edition) self.assertEqual(textbook_info.isbn, "BSZWEWZWMZYJ") - self.assertEqual(textbook_info.citation, "Ia Canvas Content by Redshelf Ia. (ISBN: BSZWEWZWMZYJ).") + self.assertEqual( + textbook_info.citation, + "Ia Canvas Content by Redshelf Ia. (ISBN: BSZWEWZWMZYJ).", + ) def test_textbook_from_json_all_empty(self): emptied_data: dict[str, Any] = self.cs_0441_textbook_data[0].copy() @@ -248,7 +334,10 @@ def test_get_textbooks_for_course_section_num(self): self.assertEqual(textbooks[0].author, "Redshelf Ia") self.assertIsNone(textbooks[0].edition) self.assertEqual(textbooks[0].isbn, "BSZWEWZWMZYJ") - self.assertEqual(textbooks[0].citation, "Ia Canvas Content by Redshelf Ia. (ISBN: BSZWEWZWMZYJ).") + self.assertEqual( + textbooks[0].citation, + "Ia Canvas Content by Redshelf Ia. (ISBN: BSZWEWZWMZYJ).", + ) @responses.activate def test_get_textbooks_for_course_invalid_section_num(self): @@ -278,7 +367,10 @@ def test_get_textbooks_for_course_instructor(self): self.assertEqual(textbooks[0].author, "Redshelf Ia") self.assertIsNone(textbooks[0].edition) self.assertEqual(textbooks[0].isbn, "BSZWEWZWMZYJ") - self.assertEqual(textbooks[0].citation, "Ia Canvas Content by Redshelf Ia. (ISBN: BSZWEWZWMZYJ).") + self.assertEqual( + textbooks[0].citation, + "Ia Canvas Content by Redshelf Ia. (ISBN: BSZWEWZWMZYJ).", + ) @responses.activate def test_get_textbooks_for_course_invalid_instructor(self): @@ -364,7 +456,9 @@ def test_get_textbooks_for_course_textbook_no_info(self): self.mock_subject_map_success() self.mock_cs_courses_success() responses.add( - responses.GET, f"https://pitt.verbacompare.com/compare/books?id={CS_0441_GARRISON_SECTION_ID}", json=[emptied_data] + responses.GET, + f"https://pitt.verbacompare.com/compare/books?id={CS_0441_GARRISON_SECTION_ID}", + json=[emptied_data], ) course = textbook.CourseInfo("CS", "0441", instructor="GARRISON III") @@ -405,7 +499,10 @@ def test_get_textbooks_for_courses(self): self.assertEqual(textbooks[1].author, "Redshelf Ia") self.assertIsNone(textbooks[1].edition) self.assertEqual(textbooks[1].isbn, "BSZWEWZWMZYJ") - self.assertEqual(textbooks[1].citation, "Ia Canvas Content by Redshelf Ia. (ISBN: BSZWEWZWMZYJ).") + self.assertEqual( + textbooks[1].citation, + "Ia Canvas Content by Redshelf Ia. (ISBN: BSZWEWZWMZYJ).", + ) @mark.filterwarnings("ignore:Attempt") @responses.activate