From 6d6b1ef9f402c43fdcf01e6de495a7d563ca504f Mon Sep 17 00:00:00 2001 From: Tianyi Zheng Date: Sat, 17 Aug 2024 22:24:02 -0700 Subject: [PATCH] Completely rewrite `textbook` module and its unit tests (#198) * Completely rewrite textbook module and its unit tests As discussed in the CSC Discord server, textbook.py has been found to be broken. Parts of the underlying textbook API had changed, invalidating most of the existing code. For instance, one major change to the underlying API is that the API's request URLs now require CSRF tokens, which I believe weren't required before. Major breaking changes like this weren't caught earlier because textbook_test.py mostly didn't validate any function outputs, only their return types, and thus the module's unit tests merely gave the illusion of correctness. I rewrote the module along with its unit tests to adjust for the changed textbook API, while taking the liberty to make the code more readable and robust based on discussions in the CSC Discord server. This includes better error handling, input validation, automatic retrieval of CSRF tokens, and much more comprehensive unit tests. * Remove slots=True from @dataclass annotation for Python 3.9 compatibility --- pittapi/textbook.py | 580 +++--- tests/samples/textbook_base_page.html | 238 +++ tests/samples/textbook_courses_CS.json | 1595 ++++++++++++++++- tests/samples/textbook_courses_MATH.json | 870 +++++++++ tests/samples/textbook_courses_STAT.json | 1 - tests/samples/textbook_subjects.json | 674 +++++++ .../textbook_textbooks_CS_0441_garrison.json | 69 + .../textbook_textbooks_MATH_0430_pan.json | 104 ++ tests/textbook_test.py | 484 +++-- 9 files changed, 4112 insertions(+), 503 deletions(-) create mode 100644 tests/samples/textbook_base_page.html create mode 100644 tests/samples/textbook_courses_MATH.json delete mode 100644 tests/samples/textbook_courses_STAT.json create mode 100644 tests/samples/textbook_subjects.json create mode 100644 tests/samples/textbook_textbooks_CS_0441_garrison.json create mode 100644 tests/samples/textbook_textbooks_MATH_0430_pan.json diff --git a/pittapi/textbook.py b/pittapi/textbook.py index d608b92..197314a 100644 --- a/pittapi/textbook.py +++ b/pittapi/textbook.py @@ -1,377 +1,225 @@ -import grequests -import requests - -from typing import Any, Callable, Generator - -BASE_URL = "http://pitt.verbacompare.com/" - -CODES = [ - "ADMJ", - "ADMPS", - "AFRCNA", - "AFROTC", - "ANTH", - "ARABIC", - "ARTSC", - "ASL", - "ASTRON", - "ATHLTR", - "BACC", - "BCHS", - "BECN", - "BFIN", - "BHRM", - "BIND", - "BIOENG", - "BIOETH", - "BIOINF", - "BIOSC", - "BIOST", - "BMIS", - "BMKT", - "BOAH", - "BORG", - "BQOM", - "BSEO", - "BSPP", - "BUS", - "BUSACC", - "BUSADM", - "BUSBIS", - "BUSECN", - "BUSENV", - "BUSERV", - "BUSFIN", - "BUSHRM", - "BUSMKT", - "BUSORG", - "BUSQOM", - "BUSSCM", - "BUSSPP", - "CDACCT", - "CDENT", - "CEE", - "CGS", - "CHE", - "CHEM", - "CHIN", - "CLASS", - "CLRES", - "CLST", - "CMMUSIC", - "CMPBIO", - "COE", - "COEA", - "COEE", - "COMMRC", - "CS", - "CSD", - "DENHYG", - "DENT", - "DIASCI", - "DSANE", - "EAS", - "ECE", - "ECON", - "EDUC", - "ELI", - "EM", - "ENDOD", - "ENGCMP", - "ENGFLM", - "ENGLIT", - "ENGR", - "ENGSCI", - "ENGWRT", - "ENRES", - "EOH", - "EPIDEM", - "FACDEV", - "FILMG", - "FILMST", - "FP", - "FR", - "FTADMA", - "FTDA", - "FTDB", - "FTDC", - "FTDR", - "GEOL", - "GER", - "GERON", - "GREEK", - "GREEKM", - "GSWS", - "HAA", - "HIM", - "HINDI", - "HIST", - "HONORS", - "HPA", - "HPM", - "HPS", - "HRS", - "HUGEN", - "IDM", - "IE", - "IL", - "IMB", - "INFSCI", - "INTBP", - "IRISH", - "ISB", - "ISSP", - "ITAL", - "JPNSE", - "JS", - "KOREAN", - "LATIN", - "LAW", - "LCTL", - "LDRSHP", - "LEGLST", - "LING", - "LIS", - "LSAP", - "MATH", - "ME", - "MED", - "MEDEDU", - "MEMS", - "MILS", - "MOLBPH", - "MSCBIO", - "MSCBMP", - "MSCMP", - "MSE", - "MSIMM", - "MSMBPH", - "MSMGDB", - "MSMPHL", - "MSMVM", - "MSNBIO", - "MUSIC", - "NEURO", - "NPHS", - "NROSCI", - "NUR", - "NURCNS", - "NURNM", - "NURNP", - "NURSAN", - "NURSP", - "NUTR", - "ODO", - "OLLI", - "ORBIOL", - "ORSUR", - "OT", - "PAS", - "PEDC", - "PEDENT", - "PERIO", - "PERS", - "PETE", - "PHARM", - "PHIL", - "PHYS", - "PIA", - "POLISH", - "PORT", - "PROSTH", - "PS", - "PSY", - "PSYC", - "PSYED", - "PT", - "PUBHLT", - "PUBSRV", - "REHSCI", - "REL", - "RELGST", - "RESTD", - "RUSS", - "SA", - "SERCRO", - "SLAV", - "SLOVAK", - "SOC", - "SOCWRK", - "SPAN", - "STAT", - "SWAHIL", - "SWBEH", - "SWCOSA", - "SWE", - "SWGEN", - "SWINT", - "SWRES", - "SWWEL", - "TELCOM", - "THEA", - "TURKSH", - "UKRAIN", - "URBNST", - "VIET", -] -KEYS = ["isbn", "citation", "title", "edition", "author"] -QUERIES = { - "courses": "compare/courses/?id={}&term_id={}", - "books": "compare/books?id={}", -} -LOOKUP_ERRORS = { - 1: "section {1}.", - 2: "instructor {2}.", - 3: "section {1} or instructor {2}.", -} - - -def _construct_query(query: str, *args) -> str: - """Constructs query based on which one is requested - and fills the query in with the given arguments - """ - return QUERIES[query].format(*args) - - -def _validate_term(term: str) -> str: - """Validates term is a string and check if it is valid.""" - if len(term) == 4 and term.isdigit(): - return term - raise ValueError("Invalid term") - - -def _validate_course(course: str) -> str: - """Validates course is a four digit number, - otherwise adds zero(s) to create four digit number or, - raises an exception. - """ - if len(course) > 4 or not course.isdigit(): - raise ValueError("Invalid course number") - elif len(course) == 4: - return course - return "0" * (4 - len(course)) + course - - -def _filter_dictionary(d: dict[Any, Any], keys: list[Any]) -> dict[Any, Any]: - """Creates new dictionary from selecting certain - key value pairs from another dictionary - """ - return dict((k, d[k]) for k in keys if k in d) - - -def _find_item(id_key, data_key, error_item) -> Callable[[dict[Any, Any], Any], Any]: - """Finds a dictionary in a list based on its id key, and - returns a piece of data from the dictionary based on a data key. - """ - - def find(data, value): - for item in data: - if item[id_key] == value: - return item[data_key] - raise LookupError("Can't find {} {}.".format(error_item, str(value))) - - return find +from __future__ import annotations - -_find_sections = _find_item("id", "sections", "course") -_find_course_id_by_instructor = _find_item("instructor", "id", "instructor") -_find_course_id_by_section = _find_item("name", "id", "section") +# grequests must be imported before all other libraries, especially requests, +# because grequests uses gevent, which in turn uses monkey-patching to implement concurrency +import grequests +import warnings + +from dataclasses import dataclass +from requests import ConnectionError +from requests_html import HTMLResponse, HTMLSession +from typing import Any, NamedTuple + +BASE_URL = "https://pitt.verbacompare.com/" + +SUBJECTS_URL = BASE_URL + "compare/departments/?term={term_id}" +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 +MAX_REQUEST_ATTEMPTS = 3 + +sess = HTMLSession() +request_headers: dict[str, str] | None = None +subject_map: dict[str, str] | None = None + + +@dataclass # No dataclass slots because they're not supported in Python 3.9 +class CourseInfo: + subject: str + course_num: str + instructor: str | None = None + section_num: str | None = None + + def __post_init__(self) -> None: + if not subject_map: + _update_subject_map() + assert subject_map + + self.subject = self.subject.upper() + if self.subject not in subject_map: + raise LookupError(f"{self.subject} is not a valid subject") + if len(self.course_num) > 4 or not self.course_num.isdigit(): + raise ValueError("Invalid course number") + 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()): + raise ValueError("Invalid section number") + + +class Textbook(NamedTuple): + title: str | None + author: str | None + edition: str | None + isbn: str | None + citation: str | None + + @classmethod + def from_json(cls, json: dict[str, Any]) -> Textbook | None: + parsed_textbook = cls( + title=json.get("title"), + author=json.get("author"), + edition=json.get("edition"), + isbn=json.get("isbn"), + citation=json.get("citation"), + ) + # If all fields are None, then don't bother returning a Textbook object + return parsed_textbook if any(field for field in parsed_textbook) else None + + +def _update_headers() -> None: + for i in range(MAX_REQUEST_ATTEMPTS): + 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") + 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") + + elements = base_response.html.find("meta") + assert isinstance(elements, list) + for element in elements: + if element.attrs.get("name") == "csrf-token": + csrf_token: str = element.attrs["content"] + global request_headers + request_headers = {"X-CSRF-Token": csrf_token} + return + raise ConnectionError("Unable to find valid request credentials, cannot connect to textbook site") + + +def _update_subject_map() -> None: + if not request_headers: + _update_headers() + + for i in range(MAX_REQUEST_ATTEMPTS): + 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") + _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") + + 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: + if section_num: + for section in sections: + if section["name"] == section_num: + return section["id"] + raise LookupError(f"No section found with given {section_num=}") + if instructor: + for section in sections: + if section["instructor"] == instructor: + return section["id"] + raise LookupError(f"No section found with given {instructor=}") + + # Not enough info provided, so try to deduce the correct section: + # - If there's only 1 section of the course, then the sole section must be the correct one + # - If all sections of the course are taught by the same instructor, then we can assume that all sections will have the + # same textbook, meaning that the exact section doesn't matter + instructors = {section["instructor"] for section in sections} + if len(sections) == 1 or len(instructors) == 1: + return sections[0]["id"] + raise LookupError( + "Cannot determine section ID from given arguments, please provide the instructor's name and/or the section number" + ) -def _extract_id(response, course: str, instructor: str, section: str) -> str: - """Gathers sections from departments and finds course id by - instructor name or section number. - """ - sections = _find_sections(response.json(), course) - error = 0 - try: - if section is not None: - return _find_course_id_by_section(sections, section) - except LookupError: - error += 1 - try: - if instructor is not None: - return _find_course_id_by_instructor(sections, instructor.upper()) - except LookupError: - error += 2 - raise LookupError("Unable to find course by " + LOOKUP_ERRORS[error].format(section, instructor)) - - -def _extract_books(ids: list[str]) -> list[dict[str, str]]: +def _get_textbooks_for_ids(ids: list[str]) -> list[Textbook]: """Fetches a course's textbook information and returns a list of textbooks for the given course. """ - responses = grequests.imap([grequests.get(BASE_URL + _construct_query("books", section_id)) for section_id in ids]) - books = [_filter_dictionary(book, KEYS) for response in responses for book in response.json()] - return books - - -# Meant to force a return of None instead of raising a KeyError -# when using a nonexistent key -class DefaultDict(dict): - def __missing__(self, key): - return None - - -def _fetch_course( - courses: list[dict[str, str]], departments: dict[str, str] -) -> Generator[tuple[str, str, str, str], None, None]: - """Generator for fetching a courses information in order""" - for course in courses: - course = DefaultDict(course) - yield ( - departments[course["department"]], - course["department"] + _validate_course(course["course"]), - course["instructor"], - course["section"], + if not request_headers: + _update_headers() + + 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(): + book = Textbook.from_json(book_json) + if book: + books.append(book) + else: + warnings.warn(f"No textbook info found for {response}") + return [book for book in books if book] # Drop all None values + + +def _get_textbooks_from_json( + 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) + return _get_textbooks_for_ids([section_id]) + raise LookupError(f"{subject} {course_num} is not a valid course") + + +def get_textbooks_for_course(course: CourseInfo) -> list[Textbook]: + if not request_headers: + _update_headers() + if not subject_map: + _update_subject_map() + assert subject_map + + 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 ) + if course_response.status_code == 200: + break + 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") + + return _get_textbooks_from_json( + course_json=course_response.json(), + subject=course.subject, + course_num=course.course_num, + instructor=course.instructor, + section_num=course.section_num, + ) -def _get_department_number(department_code: str) -> int: - """Temporary solution to finding a department. - There will be a new method to getting department information - at a later time. - """ - department_number = CODES.index(department_code) + 22399 - if department_number > 22462: - department_number += 2 # between codes DSANE and EAS 2 id numbers are skipped. - if department_number > 22580: - department_number += 1 # between codes PUBSRV and REHSCI 1 id number is skipped. - return department_number - - -def get_textbooks(term: str, courses: list[dict[str, str]]) -> list[dict[str, str]]: - """Retrieves textbooks for multiple courses in the same term.""" - departments = {course["department"] for course in courses} - responses = grequests.map( - [ - grequests.get( - BASE_URL + _construct_query("courses", _get_department_number(department), _validate_term(term)), - timeout=10, +def get_textbooks_for_courses(courses_info: list[CourseInfo]) -> list[Textbook]: + if not request_headers: + _update_headers() + if not subject_map: + _update_subject_map() + assert subject_map + + # Precompute list of unique subjects to avoid unnecessary API requests + subjects = {course_info.subject for course_info in courses_info} + courses_for_subjects: dict[str, list[dict[str, Any]]] = {} + 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 + ) + if course_response.status_code == 200: + break + 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") + + courses_for_subjects[subject] = course_response.json() + + textbooks = [] + for course_info in courses_info: + course_json = courses_for_subjects[course_info.subject] + textbooks.extend( + _get_textbooks_from_json( + course_json=course_json, + subject=course_info.subject, + course_num=course_info.course_num, + instructor=course_info.instructor, + section_num=course_info.section_num, ) - for department in departments - ] - ) - section_ids = [ - _extract_id(*course) - for course in _fetch_course( - courses, - dict( - zip( - sorted(departments), - sorted(responses, key=lambda resp: resp.json()[0]["name"]), - ) - ), ) - ] - return _extract_books(section_ids) - - -def get_textbook(term: str, department: str, course: str, instructor: str = None, section: str = None) -> list[dict[str, str]]: - """Retrieves textbooks for a given course.""" - has_section_or_instructor = (instructor is not None) or (section is not None) - if not has_section_or_instructor: - raise TypeError("get_textbook() is missing a instructor or section argument") - response = requests.get(BASE_URL + _construct_query("courses", _get_department_number(department), _validate_term(term))) - section_id = _extract_id(response, department + _validate_course(course), instructor, section) - return _extract_books([section_id]) + return textbooks diff --git a/tests/samples/textbook_base_page.html b/tests/samples/textbook_base_page.html new file mode 100644 index 0000000..7c342b8 --- /dev/null +++ b/tests/samples/textbook_base_page.html @@ -0,0 +1,238 @@ + + + + + + + + +Compare Textbook Prices Online | University Store on Fifth + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + +
+
+
No Courses Selected!
+
+
+
+ +
+
+ + + + + + + + + + + + diff --git a/tests/samples/textbook_courses_CS.json b/tests/samples/textbook_courses_CS.json index 0ddfbc2..c19c409 100644 --- a/tests/samples/textbook_courses_CS.json +++ b/tests/samples/textbook_courses_CS.json @@ -1 +1,1594 @@ -[{"name":"CS 0004","id":"CS0004","sections":[{"id":"2096252","name":"1070","instructor":"DEVINE"},{"id":"2096253","name":"7010","instructor":"COSTANTINO"}]},{"name":"CS 0007","id":"CS0007","sections":[{"id":"2096254","name":"1050","instructor":"DELANEY"},{"id":"2096255","name":"1060","instructor":null},{"id":"2096256","name":"1090","instructor":null},{"id":"2096257","name":"1100","instructor":"DELANEY"},{"id":"2096258","name":"1105","instructor":null},{"id":"2096259","name":"1110","instructor":null},{"id":"2096260","name":"1200","instructor":"FILIPSKI"},{"id":"2096261","name":"1210","instructor":null},{"id":"2096262","name":"1220","instructor":null},{"id":"2096263","name":"1250","instructor":"FILIPSKI"},{"id":"2096264","name":"1255","instructor":null},{"id":"2096265","name":"1260","instructor":null},{"id":"2096266","name":"1300","instructor":"KREBS"},{"id":"2096267","name":"1310","instructor":"KREBS"},{"id":"2096268","name":"1320","instructor":"KREBS"}]},{"name":"CS 0008","id":"CS0008","sections":[{"id":"2096269","name":"1020","instructor":"MOHAMED KHATTAB"},{"id":"2096270","name":"1030","instructor":null},{"id":"2096271","name":"1040","instructor":null},{"id":"2096272","name":"1230","instructor":"NOVACKY"},{"id":"2096273","name":"1235","instructor":null},{"id":"2096274","name":"1240","instructor":null},{"id":"2096275","name":"1300","instructor":"PRODONOVICH"},{"id":"2096276","name":"1310","instructor":null},{"id":"2096277","name":"1320","instructor":null}]},{"name":"CS 0090","id":"CS0090","sections":[{"id":"2096278","name":"1040","instructor":"DEVINE"}]},{"name":"CS 0134","id":"CS0134","sections":[{"id":"2096279","name":"1060","instructor":"CHANG"},{"id":"2096280","name":"1200","instructor":"BILLINGSLEY"}]},{"name":"CS 0334","id":"CS0334","sections":[{"id":"2096281","name":"1020","instructor":"DENARDIS"}]},{"name":"CS 0401","id":"CS0401","sections":[{"id":"2096282","name":"1030","instructor":"HOFFMAN"},{"id":"2096283","name":"1035","instructor":null},{"id":"2096284","name":"1040","instructor":null},{"id":"2096285","name":"1045","instructor":"HOFFMAN"},{"id":"2096286","name":"1060","instructor":"RAMIREZ"},{"id":"2096287","name":"1070","instructor":null},{"id":"2096288","name":"1080","instructor":null},{"id":"2096289","name":"1090","instructor":null},{"id":"2096290","name":"1100","instructor":"RAMIREZ"},{"id":"2096291","name":"1105","instructor":null},{"id":"2096292","name":"1110","instructor":null},{"id":"2096293","name":"1115","instructor":null},{"id":"2096294","name":"1200","instructor":"HOFFMAN"},{"id":"2096295","name":"1210","instructor":null},{"id":"2096296","name":"1220","instructor":"HOFFMAN"},{"id":"2096297","name":"1300","instructor":"DICK"},{"id":"2096298","name":"1310","instructor":null},{"id":"2096299","name":"1315","instructor":null},{"id":"2096300","name":"1320","instructor":"HOFFMAN"},{"id":"2096301","name":"1325","instructor":null},{"id":"2096302","name":"1330","instructor":null}]},{"name":"CS 0441","id":"CS0441","sections":[{"id":"2096303","name":"1070","instructor":"BONIDIE"},{"id":"2096304","name":"1075","instructor":null},{"id":"2096305","name":"1080","instructor":null},{"id":"2096306","name":"1085","instructor":null},{"id":"2096307","name":"1090","instructor":"BONIDIE"},{"id":"2096308","name":"1095","instructor":null},{"id":"2096309","name":"1100","instructor":null},{"id":"2096310","name":"1101","instructor":null},{"id":"2096311","name":"1110","instructor":"GARRISON III"},{"id":"2096312","name":"1120","instructor":null},{"id":"2096313","name":"1130","instructor":null},{"id":"2096314","name":"1135","instructor":null},{"id":"2096315","name":"1200","instructor":"GARRISON III"},{"id":"2096316","name":"1210","instructor":null},{"id":"2096317","name":"1220","instructor":null},{"id":"2198583","name":"1230","instructor":"GARRISON III"}]},{"name":"CS 0445","id":"CS0445","sections":[{"id":"2096318","name":"1030","instructor":"GARRISON III"},{"id":"2096319","name":"1035","instructor":null},{"id":"2096320","name":"1040","instructor":null},{"id":"2096321","name":"1045","instructor":null},{"id":"2096322","name":"1100","instructor":"GARRISON III"},{"id":"2096323","name":"1105","instructor":null},{"id":"2096324","name":"1110","instructor":null},{"id":"2096325","name":"1115","instructor":null},{"id":"2096326","name":"1200","instructor":"MOHAMED KHATTAB"},{"id":"2096327","name":"1205","instructor":null},{"id":"2096328","name":"1210","instructor":null},{"id":"2096329","name":"1220","instructor":null},{"id":"2106366","name":"1240","instructor":"JAMES"},{"id":"2106367","name":"1242","instructor":null},{"id":"2106368","name":"1244","instructor":null}]},{"name":"CS 0447","id":"CS0447","sections":[{"id":"2096330","name":"1070","instructor":"KOSIYATRAKUL"},{"id":"2096331","name":"1080","instructor":null},{"id":"2096332","name":"1090","instructor":null},{"id":"2096333","name":"1095","instructor":null},{"id":"2096334","name":"1120","instructor":"BILLINGSLEY"},{"id":"2096335","name":"1130","instructor":null},{"id":"2096336","name":"1140","instructor":null},{"id":"2096337","name":"1145","instructor":null},{"id":"2096338","name":"1200","instructor":"ZHANG"},{"id":"2096339","name":"1205","instructor":null},{"id":"2096340","name":"1210","instructor":null},{"id":"2198983","name":"1215","instructor":"KOSIYATRAKUL"}]},{"name":"CS 0449","id":"CS0449","sections":[{"id":"2096341","name":"1070","instructor":"AHN"},{"id":"2096342","name":"1080","instructor":null},{"id":"2096343","name":"1090","instructor":null},{"id":"2096344","name":"1095","instructor":null},{"id":"2096345","name":"1110","instructor":"AHN"},{"id":"2096346","name":"1120","instructor":null},{"id":"2096347","name":"1125","instructor":null},{"id":"2096348","name":"1130","instructor":null},{"id":"2096349","name":"1135","instructor":"MISURDA"},{"id":"2096350","name":"1140","instructor":null},{"id":"2096351","name":"1145","instructor":null}]},{"name":"CS 0590","id":"CS0590","sections":[{"id":"2096352","name":"1020","instructor":"QUIRIN"},{"id":"2096353","name":"1040","instructor":"BRANDON"}]},{"name":"CS 0699","id":"CS0699","sections":[{"id":"2096354","name":"1350","instructor":"FRANOLICH"}]},{"name":"CS 1501","id":"CS1501","sections":[{"id":"2096355","name":"1040","instructor":"FARNAN"},{"id":"2096356","name":"1050","instructor":null},{"id":"2096357","name":"1055","instructor":null},{"id":"2096358","name":"1060","instructor":"FARNAN"},{"id":"2096359","name":"1065","instructor":null},{"id":"2096360","name":"1075","instructor":null},{"id":"2096361","name":"1150","instructor":"FARNAN"},{"id":"2096362","name":"1170","instructor":"FARNAN"},{"id":"2096363","name":"1172","instructor":null},{"id":"2096364","name":"1174","instructor":null},{"id":"2096365","name":"1190","instructor":null},{"id":"2096366","name":"1195","instructor":null},{"id":"2096367","name":"1200","instructor":"WEHAR"},{"id":"2096368","name":"1205","instructor":null},{"id":"2096369","name":"1210","instructor":null},{"id":"2096370","name":"1250","instructor":"WEHAR"},{"id":"2096371","name":"1255","instructor":null},{"id":"2096372","name":"1260","instructor":null},{"id":"2096373","name":"1270","instructor":"FARNAN"},{"id":"2096374","name":"1272","instructor":null},{"id":"2096375","name":"1275","instructor":null},{"id":"2096376","name":"1280","instructor":"FARNAN"},{"id":"2096377","name":"1282","instructor":null},{"id":"2096378","name":"1285","instructor":null},{"id":"2096379","name":"1300","instructor":"FARNAN"},{"id":"2096380","name":"1305","instructor":"FARNAN"},{"id":"2096381","name":"1310","instructor":"ZHANG"},{"id":"2096382","name":"1315","instructor":"ZHANG"},{"id":"2096383","name":"1320","instructor":"ZHANG"},{"id":"2096384","name":"1330","instructor":"ZHANG"},{"id":"2096385","name":"1400","instructor":"WEHAR"},{"id":"2096386","name":"1405","instructor":null},{"id":"2096387","name":"1406","instructor":null},{"id":"2096388","name":"1410","instructor":"WEHAR"},{"id":"2096389","name":"1415","instructor":null},{"id":"2096390","name":"1420","instructor":null},{"id":"2198744","name":"ALL SECTIONS","instructor":"ALL INSTRUCTORS"}]},{"name":"CS 1502","id":"CS1502","sections":[{"id":"2096391","name":"1070","instructor":"MOHAMED KHATTAB"},{"id":"2096392","name":"1075","instructor":null},{"id":"2096393","name":"1080","instructor":null},{"id":"2096394","name":"1085","instructor":null},{"id":"2096395","name":"1200","instructor":"KOSIYATRAKUL"},{"id":"2096396","name":"1210","instructor":null},{"id":"2096397","name":"1220","instructor":null}]},{"name":"CS 1510","id":"CS1510","sections":[{"id":"2096398","name":"1040","instructor":"PRUHS"},{"id":"2096399","name":"1050","instructor":null}]},{"name":"CS 1520","id":"CS1520","sections":[{"id":"2096400","name":"1100","instructor":"BOWYTZ"},{"id":"2096401","name":"1105","instructor":null},{"id":"2096402","name":"1110","instructor":null},{"id":"2106419","name":"1050","instructor":null},{"id":"2106420","name":"1055","instructor":null},{"id":"2106421","name":"1060","instructor":null}]},{"name":"CS 1530","id":"CS1530","sections":[{"id":"2096403","name":"1070","instructor":"LABOON"},{"id":"2096404","name":"1080","instructor":"LABOON"}]},{"name":"CS 1541","id":"CS1541","sections":[{"id":"2096405","name":"1070","instructor":"MELHEM"}]},{"name":"CS 1550","id":"CS1550","sections":[{"id":"2096406","name":"1040","instructor":"MISURDA"},{"id":"2096407","name":"1050","instructor":null},{"id":"2096408","name":"1060","instructor":null},{"id":"2096409","name":"1080","instructor":"MISURDA"},{"id":"2096410","name":"1085","instructor":null},{"id":"2096411","name":"1090","instructor":null}]},{"name":"CS 1555","id":"CS1555","sections":[{"id":"2096412","name":"1100","instructor":"CHRYSANTHIS"},{"id":"2096413","name":"1110","instructor":null},{"id":"2096414","name":"1120","instructor":null}]},{"name":"CS 1566","id":"CS1566","sections":[{"id":"2096415","name":"1100","instructor":"KOSIYATRAKUL"},{"id":"2096416","name":"1105","instructor":null},{"id":"2096417","name":"1110","instructor":null}]},{"name":"CS 1571","id":"CS1571","sections":[{"id":"2096418","name":"1070","instructor":"LITMAN"}]},{"name":"CS 1621","id":"CS1621","sections":[{"id":"2096419","name":"1270","instructor":"ZHANG"}]},{"name":"CS 1622","id":"CS1622","sections":[{"id":"2096420","name":"1010","instructor":"MISURDA"}]},{"name":"CS 1632","id":"CS1632","sections":[{"id":"2096421","name":"1270","instructor":"LABOON"}]},{"name":"CS 1640","id":"CS1640","sections":[{"id":"2096422","name":"1050","instructor":"LABOON"}]},{"name":"CS 1645","id":"CS1645","sections":[{"id":"2096423","name":"1020","instructor":"BIGRIGG"}]},{"name":"CS 1652","id":"CS1652","sections":[{"id":"2096424","name":"1300","instructor":"LANGE"}]},{"name":"CS 1653","id":"CS1653","sections":[{"id":"2096425","name":"1140","instructor":"LEE"}]},{"name":"CS 1656","id":"CS1656","sections":[{"id":"2096426","name":"1130","instructor":"LABRINIDIS"},{"id":"2096427","name":"1140","instructor":null},{"id":"2096428","name":"1145","instructor":null}]},{"name":"CS 1675","id":"CS1675","sections":[{"id":"2096429","name":"1160","instructor":"HAUSKRECHT"}]},{"name":"CS 1699","id":"CS1699","sections":[{"id":"2096430","name":"1280","instructor":"ZNATI"}]},{"name":"CS 1900","id":"CS1900","sections":[{"id":"2096431","name":"1010","instructor":"LABOON"}]},{"name":"CS 1902","id":"CS1902","sections":[{"id":"2096432","name":"1050","instructor":"RAMIREZ"}]},{"name":"CS 1950","id":"CS1950","sections":[{"id":"2096433","name":"1010","instructor":"LABOON"},{"id":"2096434","name":"1020","instructor":"PRUHS"}]},{"name":"CS 1980","id":"CS1980","sections":[{"id":"2096435","name":"1200","instructor":"LABOON"}]},{"name":"CS 1981","id":"CS1981","sections":[{"id":"2096436","name":"1050","instructor":"LABOON"}]},{"name":"CS 2000","id":"CS2000","sections":[{"id":"2096437","name":"1010","instructor":"CHANG"},{"id":"2096438","name":"1020","instructor":null},{"id":"2096439","name":"1030","instructor":"CHILDERS"},{"id":"2096440","name":"1040","instructor":"AHN"},{"id":"2096441","name":"1050","instructor":"CHRYSANTHIS"},{"id":"2096442","name":"1060","instructor":null},{"id":"2096443","name":"1070","instructor":"HAUSKRECHT"},{"id":"2096444","name":"1080","instructor":"HWA"},{"id":"2096445","name":"1090","instructor":"LABRINIDIS"},{"id":"2096446","name":"1100","instructor":"LEE"},{"id":"2096447","name":"1110","instructor":"LITMAN"},{"id":"2096448","name":"1120","instructor":"KOVASHKA"},{"id":"2096449","name":"1130","instructor":"MELHEM"},{"id":"2096450","name":"1140","instructor":"MOSSE"},{"id":"2096451","name":"1150","instructor":"PRUHS"},{"id":"2096452","name":"1160","instructor":"WIEBE"},{"id":"2096453","name":"1170","instructor":"ZHANG"},{"id":"2096454","name":"1180","instructor":"ZNATI"},{"id":"2096455","name":"1190","instructor":"WANG"},{"id":"2096456","name":"1200","instructor":"LANGE"}]},{"name":"CS 2001","id":"CS2001","sections":[{"id":"2096457","name":"1040","instructor":"ZNATI"}]},{"name":"CS 2003","id":"CS2003","sections":[{"id":"2096458","name":"1060","instructor":"LEE"}]},{"name":"CS 2012","id":"CS2012","sections":[{"id":"2096459","name":"1040","instructor":"PRUHS"},{"id":"2096460","name":"1050","instructor":null}]},{"name":"CS 2045","id":"CS2045","sections":[{"id":"2096461","name":"1020","instructor":"BIGRIGG"}]},{"name":"CS 2053","id":"CS2053","sections":[{"id":"2132958","name":"1140","instructor":"LEE"}]},{"name":"CS 2055","id":"CS2055","sections":[{"id":"2096462","name":"1100","instructor":"CHRYSANTHIS"},{"id":"2096463","name":"1110","instructor":null},{"id":"2096464","name":"1120","instructor":null}]},{"name":"CS 2310","id":"CS2310","sections":[{"id":"2096465","name":"1020","instructor":"CHANG"}]},{"name":"CS 2410","id":"CS2410","sections":[{"id":"2096466","name":"1040","instructor":"CHILDERS"}]},{"name":"CS 2510","id":"CS2510","sections":[{"id":"2096467","name":"1040","instructor":"LANGE"}]},{"name":"CS 2610","id":"CS2610","sections":[{"id":"2096468","name":"1140","instructor":"WANG"}]},{"name":"CS 2710","id":"CS2710","sections":[{"id":"2096469","name":"1050","instructor":"HAUSKRECHT"}]},{"name":"CS 2900","id":"CS2900","sections":[{"id":"2096470","name":"1010","instructor":"CHANG"},{"id":"2096471","name":"1020","instructor":null},{"id":"2096472","name":"1030","instructor":"CHILDERS"},{"id":"2096473","name":"1040","instructor":"AHN"},{"id":"2096474","name":"1050","instructor":"CHRYSANTHIS"},{"id":"2096475","name":"1060","instructor":null},{"id":"2096476","name":"1070","instructor":"HAUSKRECHT"},{"id":"2096477","name":"1080","instructor":"HWA"},{"id":"2096478","name":"1090","instructor":"LABRINIDIS"},{"id":"2096479","name":"1100","instructor":"LEE"},{"id":"2096480","name":"1110","instructor":"LITMAN"},{"id":"2096481","name":"1120","instructor":"KOVASHKA"},{"id":"2096482","name":"1130","instructor":"MELHEM"},{"id":"2096483","name":"1140","instructor":"MOSSE"},{"id":"2096484","name":"1150","instructor":"PRUHS"},{"id":"2096485","name":"1160","instructor":"WIEBE"},{"id":"2096486","name":"1170","instructor":"ZHANG"},{"id":"2096487","name":"1180","instructor":"ZNATI"},{"id":"2096488","name":"1190","instructor":"LANGE"},{"id":"2096489","name":"1200","instructor":"WANG"}]},{"name":"CS 2910","id":"CS2910","sections":[{"id":"2096490","name":"1010","instructor":"CHANG"},{"id":"2096491","name":"1020","instructor":null},{"id":"2096492","name":"1030","instructor":"CHILDERS"},{"id":"2096493","name":"1040","instructor":"AHN"},{"id":"2096494","name":"1050","instructor":"CHRYSANTHIS"},{"id":"2096495","name":"1060","instructor":null},{"id":"2096496","name":"1070","instructor":"HAUSKRECHT"},{"id":"2096497","name":"1080","instructor":"HWA"},{"id":"2096498","name":"1090","instructor":"LABRINIDIS"},{"id":"2096499","name":"1100","instructor":"LEE"},{"id":"2096500","name":"1110","instructor":"LITMAN"},{"id":"2096501","name":"1120","instructor":"KOVASHKA"},{"id":"2096502","name":"1130","instructor":"MELHEM"},{"id":"2096503","name":"1140","instructor":"MOSSE"},{"id":"2096504","name":"1150","instructor":"PRUHS"},{"id":"2096505","name":"1160","instructor":"WIEBE"},{"id":"2096506","name":"1170","instructor":"ZHANG"},{"id":"2096507","name":"1180","instructor":"ZNATI"},{"id":"2096508","name":"1190","instructor":"WANG"},{"id":"2096509","name":"1200","instructor":"LANGE"}]},{"name":"CS 2990","id":"CS2990","sections":[{"id":"2096510","name":"1010","instructor":"CHANG"},{"id":"2096511","name":"1015","instructor":"WIEBE"},{"id":"2096512","name":"1020","instructor":null},{"id":"2096513","name":"1030","instructor":"CHILDERS"},{"id":"2096514","name":"1040","instructor":"AHN"},{"id":"2096515","name":"1050","instructor":"CHRYSANTHIS"},{"id":"2096516","name":"1060","instructor":null},{"id":"2096517","name":"1070","instructor":"HAUSKRECHT"},{"id":"2096518","name":"1080","instructor":"HWA"},{"id":"2096519","name":"1090","instructor":"LABRINIDIS"},{"id":"2096520","name":"1100","instructor":"LEE"},{"id":"2096521","name":"1110","instructor":"LITMAN"},{"id":"2096522","name":"1120","instructor":"KOVASHKA"},{"id":"2096523","name":"1130","instructor":"MELHEM"},{"id":"2096524","name":"1140","instructor":"MOSSE"},{"id":"2096525","name":"1150","instructor":"PRUHS"},{"id":"2096526","name":"1160","instructor":"ZHANG"},{"id":"2096527","name":"1170","instructor":"ZNATI"},{"id":"2096528","name":"1180","instructor":"WANG"},{"id":"2096529","name":"1190","instructor":"LANGE"},{"id":"2096530","name":"1200","instructor":null}]},{"name":"CS 3000","id":"CS3000","sections":[{"id":"2096531","name":"1010","instructor":"CHANG"},{"id":"2096532","name":"1020","instructor":null},{"id":"2096533","name":"1030","instructor":"CHILDERS"},{"id":"2096534","name":"1040","instructor":"AHN"},{"id":"2096535","name":"1050","instructor":"CHRYSANTHIS"},{"id":"2096536","name":"1060","instructor":null},{"id":"2096537","name":"1070","instructor":"HAUSKRECHT"},{"id":"2096538","name":"1080","instructor":"HWA"},{"id":"2096539","name":"1090","instructor":"LABRINIDIS"},{"id":"2096540","name":"1100","instructor":"LEE"},{"id":"2096541","name":"1110","instructor":"LITMAN"},{"id":"2096542","name":"1120","instructor":"KOVASHKA"},{"id":"2096543","name":"1130","instructor":"MELHEM"},{"id":"2096544","name":"1140","instructor":"MOSSE"},{"id":"2096545","name":"1150","instructor":"PRUHS"},{"id":"2096546","name":"1160","instructor":"WIEBE"},{"id":"2096547","name":"1170","instructor":"ZHANG"},{"id":"2096548","name":"1180","instructor":"ZNATI"},{"id":"2096549","name":"1190","instructor":"LANGE"},{"id":"2096550","name":"1200","instructor":"WANG"}]},{"name":"CS 3580","id":"CS3580","sections":[{"id":"2096551","name":"1010","instructor":"MELHEM"}]},{"name":"CS 3900","id":"CS3900","sections":[{"id":"2096552","name":"1010","instructor":"CHANG"},{"id":"2096553","name":"1020","instructor":null},{"id":"2096554","name":"1030","instructor":"CHILDERS"},{"id":"2096555","name":"1040","instructor":"AHN"},{"id":"2096556","name":"1050","instructor":"CHRYSANTHIS"},{"id":"2096557","name":"1060","instructor":null},{"id":"2096558","name":"1070","instructor":"HAUSKRECHT"},{"id":"2096559","name":"1080","instructor":"HWA"},{"id":"2096560","name":"1090","instructor":"LABRINIDIS"},{"id":"2096561","name":"1100","instructor":"LEE"},{"id":"2096562","name":"1110","instructor":"LITMAN"},{"id":"2096563","name":"1120","instructor":"KOVASHKA"},{"id":"2096564","name":"1130","instructor":"MELHEM"},{"id":"2096565","name":"1140","instructor":"MOSSE"},{"id":"2096566","name":"1150","instructor":"PRUHS"},{"id":"2096567","name":"1160","instructor":"WIEBE"},{"id":"2096568","name":"1170","instructor":"ZHANG"},{"id":"2096569","name":"1180","instructor":"ZNATI"},{"id":"2096570","name":"1190","instructor":"LANGE"},{"id":"2096571","name":"1200","instructor":"WANG"}]}] +[ + { + "name": "CS 0007", + "id": "CS0007", + "sections": [ + { + "id": "4558013", + "name": "1050", + "instructor": "HOFFMAN" + }, + { + "id": "4558014", + "name": "1061", + "instructor": "DEVINE" + }, + { + "id": "4558015", + "name": "1100", + "instructor": "BONIDIE" + }, + { + "id": "4558016", + "name": "1200", + "instructor": "DEVINE" + }, + { + "id": "4558017", + "name": "1250", + "instructor": "HOFFMAN" + }, + { + "id": "4558018", + "name": "1300", + "instructor": "XU" + } + ] + }, + { + "name": "CS 0011", + "id": "CS0011", + "sections": [ + { + "id": "4558019", + "name": "1000", + "instructor": "DE LIMA BARBOSA" + }, + { + "id": "4558020", + "name": "1010", + "instructor": "HEDGES" + }, + { + "id": "4558021", + "name": "1035", + "instructor": "SHARMA" + } + ] + }, + { + "name": "CS 0012", + "id": "CS0012", + "sections": [ + { + "id": "4558022", + "name": "1005", + "instructor": "DE LIMA BARBOSA" + }, + { + "id": "4558023", + "name": "1010", + "instructor": "DENARDIS" + }, + { + "id": "4558024", + "name": "8020", + "instructor": "YURASITS" + } + ] + }, + { + "name": "CS 0134", + "id": "CS0134", + "sections": [ + { + "id": "4558025", + "name": "1060", + "instructor": "DENARDIS" + }, + { + "id": "4558026", + "name": "8085", + "instructor": "LAYE" + }, + { + "id": "4558027", + "name": "8155", + "instructor": "SOLTER" + } + ] + }, + { + "name": "CS 0207", + "id": "CS0207", + "sections": [ + { + "id": "4558028", + "name": "1100", + "instructor": "RAMIREZ" + } + ] + }, + { + "name": "CS 0441", + "id": "CS0441", + "sections": [ + { + "id": "4558029", + "name": "1090", + "instructor": "BONIDIE" + }, + { + "id": "4558030", + "name": "1230", + "instructor": "FARNAN" + }, + { + "id": "4558031", + "name": "1245", + "instructor": "GARRISON III" + }, + { + "id": "4558032", + "name": "1250", + "instructor": "MURRUGARRA LLERENA" + }, + { + "id": "4637596", + "name": "1150", + "instructor": "MURRUGARRA LLERENA" + } + ] + }, + { + "name": "CS 0445", + "id": "CS0445", + "sections": [ + { + "id": "4558033", + "name": "1100", + "instructor": "BARSKY" + }, + { + "id": "4558034", + "name": "1200", + "instructor": "MOHAMED KHATTAB" + }, + { + "id": "4558035", + "name": "1400", + "instructor": "ELLIS" + }, + { + "id": "4637597", + "name": "1240", + "instructor": "JAMES" + } + ] + }, + { + "name": "CS 0447", + "id": "CS0447", + "sections": [ + { + "id": "4558036", + "name": "1070", + "instructor": "BILLINGSLEY" + }, + { + "id": "4558037", + "name": "1200", + "instructor": "MOHAMED KHATTAB" + }, + { + "id": "4637598", + "name": "1215", + "instructor": "BILLINGSLEY" + }, + { + "id": "4637599", + "name": "1400", + "instructor": "BILLINGSLEY" + } + ] + }, + { + "name": "CS 0449", + "id": "CS0449", + "sections": [ + { + "id": "4558038", + "name": "1110", + "instructor": "NUNES QUARESMA DE OL" + }, + { + "id": "4558039", + "name": "1135", + "instructor": "NUNES QUARESMA DE OL" + }, + { + "id": "4558040", + "name": "1150", + "instructor": "NUNES QUARESMA DE OL" + } + ] + }, + { + "name": "CS 0590", + "id": "CS0590", + "sections": [ + { + "id": "4558041", + "name": "1020", + "instructor": "QUIRIN" + }, + { + "id": "4558042", + "name": "1090", + "instructor": "QUIRIN" + }, + { + "id": "4558043", + "name": "1300", + "instructor": "QUIRIN" + } + ] + }, + { + "name": "CS 1501", + "id": "CS1501", + "sections": [ + { + "id": "4558044", + "name": "1060", + "instructor": "FARNAN" + }, + { + "id": "4558045", + "name": "1065", + "instructor": "MOHAMED KHATTAB" + }, + { + "id": "4558046", + "name": "1150", + "instructor": "MOHAMED KHATTAB" + }, + { + "id": "4558047", + "name": "1200", + "instructor": "FARNAN" + } + ] + }, + { + "name": "CS 1502", + "id": "CS1502", + "sections": [ + { + "id": "4558048", + "name": "1070", + "instructor": "KOSIYATRAKUL" + }, + { + "id": "4558049", + "name": "1200", + "instructor": "KOSIYATRAKUL" + }, + { + "id": "4558050", + "name": "1210", + "instructor": "KOSIYATRAKUL" + } + ] + }, + { + "name": "CS 1503", + "id": "CS1503", + "sections": [ + { + "id": "4558051", + "name": "1200", + "instructor": "BARSKY" + }, + { + "id": "4558052", + "name": "1250", + "instructor": "BARSKY" + } + ] + }, + { + "name": "CS 1510", + "id": "CS1510", + "sections": [ + { + "id": "4558053", + "name": "1040", + "instructor": "PRUHS" + }, + { + "id": "4558054", + "name": "1050", + "instructor": "PRUHS" + } + ] + }, + { + "name": "CS 1520", + "id": "CS1520", + "sections": [ + { + "id": "4558055", + "name": "1050", + "instructor": "FERREIRA" + }, + { + "id": "4558056", + "name": "1100", + "instructor": "FERREIRA" + } + ] + }, + { + "name": "CS 1530", + "id": "CS1530", + "sections": [ + { + "id": "4558057", + "name": "1070", + "instructor": "VON FRANKENBERG UND" + } + ] + }, + { + "name": "CS 1550", + "id": "CS1550", + "sections": [ + { + "id": "4558058", + "name": "1040", + "instructor": "MISURDA" + }, + { + "id": "4558059", + "name": "1080", + "instructor": "MISURDA" + }, + { + "id": "4558060", + "name": "1095", + "instructor": "MISURDA" + } + ] + }, + { + "name": "CS 1555", + "id": "CS1555", + "sections": [ + { + "id": "4558061", + "name": "1100", + "instructor": "NIXON" + } + ] + }, + { + "name": "CS 1566", + "id": "CS1566", + "sections": [ + { + "id": "4558062", + "name": "1100", + "instructor": "KOSIYATRAKUL" + } + ] + }, + { + "name": "CS 1571", + "id": "CS1571", + "sections": [ + { + "id": "4637600", + "name": "1010", + "instructor": "SINGH" + } + ] + }, + { + "name": "CS 1622", + "id": "CS1622", + "sections": [ + { + "id": "4558063", + "name": "1200", + "instructor": "AHN" + } + ] + }, + { + "name": "CS 1632", + "id": "CS1632", + "sections": [ + { + "id": "4558064", + "name": "1080", + "instructor": "AHN" + }, + { + "id": "4558065", + "name": "1270", + "instructor": "AHN" + } + ] + }, + { + "name": "CS 1635", + "id": "CS1635", + "sections": [ + { + "id": "4558066", + "name": "1010", + "instructor": "BIEHL" + } + ] + }, + { + "name": "CS 1640", + "id": "CS1640", + "sections": [ + { + "id": "4637601", + "name": "1050", + "instructor": "JORDAN" + } + ] + }, + { + "name": "CS 1645", + "id": "CS1645", + "sections": [ + { + "id": "4599605", + "name": "1010", + "instructor": "TANG" + } + ] + }, + { + "name": "CS 1651", + "id": "CS1651", + "sections": [ + { + "id": "4558067", + "name": "1200", + "instructor": "NUNES QUARESMA DE OL" + } + ] + }, + { + "name": "CS 1653", + "id": "CS1653", + "sections": [ + { + "id": "4558068", + "name": "1140", + "instructor": "GARRISON III" + } + ] + }, + { + "name": "CS 1656", + "id": "CS1656", + "sections": [ + { + "id": "4558069", + "name": "1130", + "instructor": "JIA" + }, + { + "id": "4558070", + "name": "1200", + "instructor": "YU" + } + ] + }, + { + "name": "CS 1660", + "id": "CS1660", + "sections": [ + { + "id": "4558071", + "name": "1300", + "instructor": "MAHONEY" + } + ] + }, + { + "name": "CS 1666", + "id": "CS1666", + "sections": [ + { + "id": "4558072", + "name": "1040", + "instructor": "FARNAN" + } + ] + }, + { + "name": "CS 1674", + "id": "CS1674", + "sections": [ + { + "id": "4649455", + "name": "1010", + "instructor": "MURRUGARRA LLERENA" + } + ] + }, + { + "name": "CS 1675", + "id": "CS1675", + "sections": [ + { + "id": "4558073", + "name": "1100", + "instructor": "SKEBA" + } + ] + }, + { + "name": "CS 1678", + "id": "CS1678", + "sections": [ + { + "id": "4639988", + "name": "1000", + "instructor": "JORDAN" + } + ] + }, + { + "name": "CS 1684", + "id": "CS1684", + "sections": [ + { + "id": "4647043", + "name": "1010", + "instructor": "LIU" + } + ] + }, + { + "name": "CS 1699", + "id": "CS1699", + "sections": [ + { + "id": "4637602", + "name": "1200", + "instructor": "SHI" + } + ] + }, + { + "name": "CS 1900", + "id": "CS1900", + "sections": [ + { + "id": "4558074", + "name": "1010", + "instructor": "AHN" + }, + { + "id": "4558075", + "name": "1015", + "instructor": "AHN" + } + ] + }, + { + "name": "CS 1901", + "id": "CS1901", + "sections": [ + { + "id": "4558076", + "name": "1300", + "instructor": "RAMIREZ" + } + ] + }, + { + "name": "CS 1906", + "id": "CS1906", + "sections": [ + { + "id": "4558077", + "name": "1050", + "instructor": "RAMIREZ" + } + ] + }, + { + "name": "CS 1950", + "id": "CS1950", + "sections": [ + { + "id": "4558078", + "name": "1010", + "instructor": "AHN" + }, + { + "id": "4558079", + "name": "1015", + "instructor": "AHN" + } + ] + }, + { + "name": "CS 1951", + "id": "CS1951", + "sections": [ + { + "id": "4558080", + "name": "1300", + "instructor": "RAMIREZ" + } + ] + }, + { + "name": "CS 1980", + "id": "CS1980", + "sections": [ + { + "id": "4637603", + "name": "1200", + "instructor": "JORDAN" + } + ] + }, + { + "name": "CS 2000", + "id": "CS2000", + "sections": [ + { + "id": "4558081", + "name": "1010", + "instructor": "WALKER" + }, + { + "id": "4558082", + "name": "1030", + "instructor": "CHILDERS" + }, + { + "id": "4558083", + "name": "1040", + "instructor": "CHRYSANTHIS" + }, + { + "id": "4558084", + "name": "1050", + "instructor": "HAUSKRECHT" + }, + { + "id": "4558085", + "name": "1060", + "instructor": "HWA" + }, + { + "id": "4558086", + "name": "1070", + "instructor": "KOVASHKA" + }, + { + "id": "4558087", + "name": "1080", + "instructor": "LABRINIDIS" + }, + { + "id": "4558088", + "name": "1090", + "instructor": "LANGE" + }, + { + "id": "4558089", + "name": "1100", + "instructor": "LEE" + }, + { + "id": "4558090", + "name": "1110", + "instructor": "LITMAN" + }, + { + "id": "4558091", + "name": "1130", + "instructor": "MOSSE" + }, + { + "id": "4558092", + "name": "1140", + "instructor": "PRUHS" + }, + { + "id": "4558093", + "name": "1170", + "instructor": "ZHANG" + }, + { + "id": "4558094", + "name": "1200", + "instructor": "SHANGGUAN" + }, + { + "id": "4558095", + "name": "1210", + "instructor": "BIEHL" + }, + { + "id": "4599606", + "name": "1020", + "instructor": "LI" + }, + { + "id": "4637604", + "name": "1220", + "instructor": "AHN" + }, + { + "id": "4637605", + "name": "1230", + "instructor": "JIA" + } + ] + }, + { + "name": "CS 2001", + "id": "CS2001", + "sections": [ + { + "id": "4558096", + "name": "1040", + "instructor": "BIEHL" + } + ] + }, + { + "name": "CS 2003", + "id": "CS2003", + "sections": [ + { + "id": "4558097", + "name": "1060", + "instructor": "BIEHL" + } + ] + }, + { + "name": "CS 2012", + "id": "CS2012", + "sections": [ + { + "id": "4558098", + "name": "1040", + "instructor": "PRUHS" + }, + { + "id": "4558099", + "name": "1050", + "instructor": "PRUHS" + } + ] + }, + { + "name": "CS 2035", + "id": "CS2035", + "sections": [ + { + "id": "4558100", + "name": "1200", + "instructor": "BIEHL" + } + ] + }, + { + "name": "CS 2045", + "id": "CS2045", + "sections": [ + { + "id": "4599607", + "name": "1010", + "instructor": "TANG" + } + ] + }, + { + "name": "CS 2051", + "id": "CS2051", + "sections": [ + { + "id": "4558101", + "name": "1200", + "instructor": "NUNES QUARESMA DE OL" + } + ] + }, + { + "name": "CS 2053", + "id": "CS2053", + "sections": [ + { + "id": "4558102", + "name": "1140", + "instructor": "GARRISON III" + } + ] + }, + { + "name": "CS 2055", + "id": "CS2055", + "sections": [ + { + "id": "4558103", + "name": "1100", + "instructor": "NIXON" + } + ] + }, + { + "name": "CS 2056", + "id": "CS2056", + "sections": [ + { + "id": "4558104", + "name": "1050", + "instructor": "JIA" + }, + { + "id": "4558105", + "name": "1100", + "instructor": "JIA" + }, + { + "id": "4558106", + "name": "1200", + "instructor": "YU" + } + ] + }, + { + "name": "CS 2060", + "id": "CS2060", + "sections": [ + { + "id": "4558107", + "name": "1300", + "instructor": "MAHONEY" + } + ] + }, + { + "name": "CS 2074", + "id": "CS2074", + "sections": [ + { + "id": "4649456", + "name": "1010", + "instructor": "MURRUGARRA LLERENA" + } + ] + }, + { + "name": "CS 2075", + "id": "CS2075", + "sections": [ + { + "id": "4558108", + "name": "1100", + "instructor": "SKEBA" + } + ] + }, + { + "name": "CS 2078", + "id": "CS2078", + "sections": [ + { + "id": "4639989", + "name": "1000", + "instructor": "JORDAN" + } + ] + }, + { + "name": "CS 2084", + "id": "CS2084", + "sections": [ + { + "id": "4647044", + "name": "1010", + "instructor": "LIU" + } + ] + }, + { + "name": "CS 2210", + "id": "CS2210", + "sections": [ + { + "id": "4558109", + "name": "1000", + "instructor": "ZHANG" + } + ] + }, + { + "name": "CS 2520", + "id": "CS2520", + "sections": [ + { + "id": "4558110", + "name": "1000", + "instructor": "SHANGGUAN" + } + ] + }, + { + "name": "CS 2637", + "id": "CS2637", + "sections": [ + { + "id": "4558111", + "name": "1010", + "instructor": "GAUTAM" + } + ] + }, + { + "name": "CS 2710", + "id": "CS2710", + "sections": [ + { + "id": "4637606", + "name": "1050", + "instructor": "SKEBA" + } + ] + }, + { + "name": "CS 2731", + "id": "CS2731", + "sections": [ + { + "id": "4558112", + "name": "1210", + "instructor": "YODER" + } + ] + }, + { + "name": "CS 2756", + "id": "CS2756", + "sections": [ + { + "id": "4637607", + "name": "1010", + "instructor": "JIA" + } + ] + }, + { + "name": "CS 2900", + "id": "CS2900", + "sections": [ + { + "id": "4558113", + "name": "1000", + "instructor": "ZHANG" + }, + { + "id": "4558114", + "name": "1020", + "instructor": "BABAY" + }, + { + "id": "4558115", + "name": "1030", + "instructor": "BIEHL" + }, + { + "id": "4558116", + "name": "1050", + "instructor": "CHILDERS" + }, + { + "id": "4558117", + "name": "1060", + "instructor": "CHRYSANTHIS" + }, + { + "id": "4558118", + "name": "1070", + "instructor": "COHEN" + }, + { + "id": "4558119", + "name": "1080", + "instructor": "HAUSKRECHT" + }, + { + "id": "4558120", + "name": "1100", + "instructor": "SHANGGUAN" + }, + { + "id": "4558121", + "name": "1110", + "instructor": "JIA" + }, + { + "id": "4558122", + "name": "1120", + "instructor": "KOVASHKA" + }, + { + "id": "4558123", + "name": "1130", + "instructor": "LABRINIDIS" + }, + { + "id": "4558124", + "name": "1170", + "instructor": "LEE" + }, + { + "id": "4558125", + "name": "1180", + "instructor": "LEE" + }, + { + "id": "4558126", + "name": "1200", + "instructor": "LITMAN" + }, + { + "id": "4558127", + "name": "1210", + "instructor": "MOSSE" + }, + { + "id": "4558128", + "name": "1220", + "instructor": "PRUHS" + }, + { + "id": "4558129", + "name": "1230", + "instructor": "TANG" + }, + { + "id": "4558130", + "name": "1240", + "instructor": "ZHANG" + }, + { + "id": "4599608", + "name": "1010", + "instructor": "LI" + }, + { + "id": "4599609", + "name": "1040", + "instructor": "SHI" + }, + { + "id": "4637608", + "name": "1090", + "instructor": "AHN" + }, + { + "id": "4637609", + "name": "1140", + "instructor": "GAUTAM" + } + ] + }, + { + "name": "CS 2905", + "id": "CS2905", + "sections": [ + { + "id": "4558131", + "name": "1300", + "instructor": "BIEHL" + } + ] + }, + { + "name": "CS 2910", + "id": "CS2910", + "sections": [ + { + "id": "4558132", + "name": "1000", + "instructor": "ZHANG" + }, + { + "id": "4558133", + "name": "1020", + "instructor": "BABAY" + }, + { + "id": "4558134", + "name": "1030", + "instructor": "BIEHL" + }, + { + "id": "4558135", + "name": "1050", + "instructor": "CHILDERS" + }, + { + "id": "4558136", + "name": "1060", + "instructor": "CHRYSANTHIS" + }, + { + "id": "4558137", + "name": "1070", + "instructor": "COHEN" + }, + { + "id": "4558138", + "name": "1080", + "instructor": "HAUSKRECHT" + }, + { + "id": "4558139", + "name": "1100", + "instructor": "SHANGGUAN" + }, + { + "id": "4558140", + "name": "1110", + "instructor": "JIA" + }, + { + "id": "4558141", + "name": "1120", + "instructor": "KOVASHKA" + }, + { + "id": "4558142", + "name": "1130", + "instructor": "LABRINIDIS" + }, + { + "id": "4558143", + "name": "1170", + "instructor": "LEE" + }, + { + "id": "4558144", + "name": "1180", + "instructor": "LEE" + }, + { + "id": "4558145", + "name": "1200", + "instructor": "LITMAN" + }, + { + "id": "4558146", + "name": "1210", + "instructor": "MOSSE" + }, + { + "id": "4558147", + "name": "1220", + "instructor": "PRUHS" + }, + { + "id": "4558148", + "name": "1230", + "instructor": "TANG" + }, + { + "id": "4558149", + "name": "1240", + "instructor": "ZHANG" + }, + { + "id": "4599610", + "name": "1010", + "instructor": "LI" + }, + { + "id": "4637610", + "name": "1040", + "instructor": "NUNES QUARESMA DE OL" + } + ] + }, + { + "name": "CS 2990", + "id": "CS2990", + "sections": [ + { + "id": "4558150", + "name": "1000", + "instructor": "ZHANG" + }, + { + "id": "4558151", + "name": "1010", + "instructor": "GAUTAM" + }, + { + "id": "4558152", + "name": "1020", + "instructor": "BABAY" + }, + { + "id": "4558153", + "name": "1030", + "instructor": "BIEHL" + }, + { + "id": "4558154", + "name": "1040", + "instructor": "WALKER" + }, + { + "id": "4558155", + "name": "1050", + "instructor": "CHILDERS" + }, + { + "id": "4558156", + "name": "1060", + "instructor": "CHRYSANTHIS" + }, + { + "id": "4558157", + "name": "1070", + "instructor": "COHEN" + }, + { + "id": "4558158", + "name": "1080", + "instructor": "HAUSKRECHT" + }, + { + "id": "4558159", + "name": "1100", + "instructor": "SHANGGUAN" + }, + { + "id": "4558160", + "name": "1110", + "instructor": "JIA" + }, + { + "id": "4558161", + "name": "1120", + "instructor": "KOVASHKA" + }, + { + "id": "4558162", + "name": "1130", + "instructor": "LABRINIDIS" + }, + { + "id": "4558163", + "name": "1170", + "instructor": "LEE" + }, + { + "id": "4558164", + "name": "1180", + "instructor": "LEE" + }, + { + "id": "4558165", + "name": "1200", + "instructor": "LITMAN" + }, + { + "id": "4558166", + "name": "1210", + "instructor": "MOSSE" + }, + { + "id": "4558167", + "name": "1220", + "instructor": "PRUHS" + }, + { + "id": "4558168", + "name": "1230", + "instructor": "TANG" + }, + { + "id": "4558169", + "name": "1240", + "instructor": "ZHANG" + }, + { + "id": "4599611", + "name": "1090", + "instructor": "LI" + }, + { + "id": "4637611", + "name": "1140", + "instructor": "NUNES QUARESMA DE OL" + } + ] + }, + { + "name": "CS 3000", + "id": "CS3000", + "sections": [ + { + "id": "4558170", + "name": "1000", + "instructor": "WALKER" + }, + { + "id": "4558171", + "name": "1020", + "instructor": "BABAY" + }, + { + "id": "4558172", + "name": "1030", + "instructor": "BIEHL" + }, + { + "id": "4558173", + "name": "1050", + "instructor": "CHILDERS" + }, + { + "id": "4558174", + "name": "1060", + "instructor": "CHRYSANTHIS" + }, + { + "id": "4558175", + "name": "1070", + "instructor": "COHEN" + }, + { + "id": "4558176", + "name": "1080", + "instructor": "HAUSKRECHT" + }, + { + "id": "4558177", + "name": "1100", + "instructor": "SHANGGUAN" + }, + { + "id": "4558178", + "name": "1110", + "instructor": "JIA" + }, + { + "id": "4558179", + "name": "1120", + "instructor": "KOVASHKA" + }, + { + "id": "4558180", + "name": "1130", + "instructor": "LABRINIDIS" + }, + { + "id": "4558181", + "name": "1170", + "instructor": "LEE" + }, + { + "id": "4558182", + "name": "1180", + "instructor": "LEE" + }, + { + "id": "4558183", + "name": "1200", + "instructor": "LITMAN" + }, + { + "id": "4558184", + "name": "1210", + "instructor": "MOSSE" + }, + { + "id": "4558185", + "name": "1220", + "instructor": "PRUHS" + }, + { + "id": "4558186", + "name": "1230", + "instructor": "TANG" + }, + { + "id": "4558187", + "name": "1240", + "instructor": "ZHANG" + }, + { + "id": "4599612", + "name": "1010", + "instructor": "LI" + }, + { + "id": "4637612", + "name": "1140", + "instructor": "NUNES QUARESMA DE OL" + } + ] + }, + { + "name": "CS 3550", + "id": "CS3550", + "sections": [ + { + "id": "4558188", + "name": "1200", + "instructor": "LABRINIDIS" + } + ] + }, + { + "name": "CS 3551", + "id": "CS3551", + "sections": [ + { + "id": "4558189", + "name": "1100", + "instructor": "BABAY" + } + ] + }, + { + "name": "CS 3750", + "id": "CS3750", + "sections": [ + { + "id": "4558190", + "name": "1100", + "instructor": "JIA" + } + ] + }, + { + "name": "CS 3800", + "id": "CS3800", + "sections": [ + { + "id": "4639990", + "name": "1000", + "instructor": "LIU" + } + ] + }, + { + "name": "CS 3900", + "id": "CS3900", + "sections": [ + { + "id": "4558191", + "name": "1000", + "instructor": "ZHANG" + }, + { + "id": "4558192", + "name": "1020", + "instructor": "BABAY" + }, + { + "id": "4558193", + "name": "1030", + "instructor": "BIEHL" + }, + { + "id": "4558194", + "name": "1040", + "instructor": "WALKER" + }, + { + "id": "4558195", + "name": "1050", + "instructor": "CHILDERS" + }, + { + "id": "4558196", + "name": "1060", + "instructor": "CHRYSANTHIS" + }, + { + "id": "4558197", + "name": "1070", + "instructor": "COHEN" + }, + { + "id": "4558198", + "name": "1080", + "instructor": "HAUSKRECHT" + }, + { + "id": "4558199", + "name": "1090", + "instructor": "NUNES QUARESMA DE OL" + }, + { + "id": "4558200", + "name": "1100", + "instructor": "SHANGGUAN" + }, + { + "id": "4558201", + "name": "1110", + "instructor": "JIA" + }, + { + "id": "4558202", + "name": "1120", + "instructor": "KOVASHKA" + }, + { + "id": "4558203", + "name": "1130", + "instructor": "LABRINIDIS" + }, + { + "id": "4558204", + "name": "1140", + "instructor": "LANGE" + }, + { + "id": "4558205", + "name": "1170", + "instructor": "LEE" + }, + { + "id": "4558206", + "name": "1180", + "instructor": "LEE" + }, + { + "id": "4558207", + "name": "1200", + "instructor": "LITMAN" + }, + { + "id": "4558208", + "name": "1210", + "instructor": "MOSSE" + }, + { + "id": "4558209", + "name": "1220", + "instructor": "PRUHS" + }, + { + "id": "4558210", + "name": "1230", + "instructor": "TANG" + }, + { + "id": "4558211", + "name": "1240", + "instructor": "ZHANG" + }, + { + "id": "4599613", + "name": "1010", + "instructor": "LI" + } + ] + } +] diff --git a/tests/samples/textbook_courses_MATH.json b/tests/samples/textbook_courses_MATH.json new file mode 100644 index 0000000..08cbacf --- /dev/null +++ b/tests/samples/textbook_courses_MATH.json @@ -0,0 +1,870 @@ +[ + { + "name": "MATH 0010", + "id": "MATH0010", + "sections": [ + { + "id": "4650882", + "name": "1070", + "instructor": null + } + ] + }, + { + "name": "MATH 0025", + "id": "MATH0025", + "sections": [ + { + "id": "4561831", + "name": "7510", + "instructor": "DEMATEO" + }, + { + "id": "4631046", + "name": "1100", + "instructor": "VARGSON" + } + ] + }, + { + "name": "MATH 0031", + "id": "MATH0031", + "sections": [ + { + "id": "4561832", + "name": "7510", + "instructor": "O'MARA" + }, + { + "id": "4631047", + "name": "1010", + "instructor": "VARGSON" + }, + { + "id": "4631048", + "name": "1030", + "instructor": "ISLAM" + }, + { + "id": "4631049", + "name": "1070", + "instructor": "ISLAM" + }, + { + "id": "4650927", + "name": "1050", + "instructor": null + }, + { + "id": "4650929", + "name": "1110", + "instructor": null + }, + { + "id": "4650930", + "name": "1130", + "instructor": null + }, + { + "id": "4650931", + "name": "1240", + "instructor": null + } + ] + }, + { + "name": "MATH 0120", + "id": "MATH0120", + "sections": [ + { + "id": "4631050", + "name": "1060", + "instructor": "GILTON" + }, + { + "id": "4631051", + "name": "1096", + "instructor": "POPESCU" + }, + { + "id": "4631052", + "name": "1110", + "instructor": "EVEREST" + }, + { + "id": "4631053", + "name": "1250", + "instructor": "GILTON" + } + ] + }, + { + "name": "MATH 0125", + "id": "MATH0125", + "sections": [ + { + "id": "4631054", + "name": "1040", + "instructor": "VARGSON" + } + ] + }, + { + "name": "MATH 0200", + "id": "MATH0200", + "sections": [ + { + "id": "4631055", + "name": "1100", + "instructor": "HOCKENSMITH" + }, + { + "id": "4631056", + "name": "1200", + "instructor": "DEBLOIS" + }, + { + "id": "4631057", + "name": "1300", + "instructor": "BAO" + } + ] + }, + { + "name": "MATH 0220", + "id": "MATH0220", + "sections": [ + { + "id": "4631058", + "name": "1025", + "instructor": "TROFIMOV" + }, + { + "id": "4631059", + "name": "1060", + "instructor": "XIONG" + }, + { + "id": "4631060", + "name": "1080", + "instructor": "YUST" + }, + { + "id": "4631061", + "name": "1100", + "instructor": "HOCKENSMITH" + }, + { + "id": "4631062", + "name": "1116", + "instructor": "WANG" + }, + { + "id": "4631063", + "name": "1120", + "instructor": "XIONG" + }, + { + "id": "4631064", + "name": "1191", + "instructor": "KAVEH" + }, + { + "id": "4631065", + "name": "1195", + "instructor": "MORGAN" + }, + { + "id": "4631066", + "name": "1210", + "instructor": "YUST" + }, + { + "id": "4631067", + "name": "1230", + "instructor": "SPARLING" + }, + { + "id": "4631068", + "name": "1239", + "instructor": "MORGAN" + }, + { + "id": "4631069", + "name": "1250", + "instructor": "KAVEH" + }, + { + "id": "4631070", + "name": "1300", + "instructor": "WANG" + }, + { + "id": "4631071", + "name": "1350", + "instructor": "FEDOROV" + }, + { + "id": "4631072", + "name": "1500", + "instructor": "YAO" + } + ] + }, + { + "name": "MATH 0230", + "id": "MATH0230", + "sections": [ + { + "id": "4631073", + "name": "1010", + "instructor": "TROFIMOV" + }, + { + "id": "4631074", + "name": "1015", + "instructor": "EVEREST" + }, + { + "id": "4631075", + "name": "1036", + "instructor": "EVEREST" + }, + { + "id": "4631076", + "name": "1040", + "instructor": "WHEELER" + }, + { + "id": "4631077", + "name": "1050", + "instructor": "POPESCU" + }, + { + "id": "4631078", + "name": "1060", + "instructor": "POPESCU" + } + ] + }, + { + "name": "MATH 0235", + "id": "MATH0235", + "sections": [ + { + "id": "4634384", + "name": "1040", + "instructor": "WHEELER" + } + ] + }, + { + "name": "MATH 0240", + "id": "MATH0240", + "sections": [ + { + "id": "4631079", + "name": "1015", + "instructor": "STREIPERT" + }, + { + "id": "4631080", + "name": "1105", + "instructor": "HAJLASZ" + }, + { + "id": "4631081", + "name": "1160", + "instructor": "HAJLASZ" + }, + { + "id": "4631082", + "name": "1300", + "instructor": "SPARLING" + }, + { + "id": "4631083", + "name": "1400", + "instructor": "POGGI CEVALLOS" + }, + { + "id": "4631084", + "name": "1420", + "instructor": "PAN" + } + ] + }, + { + "name": "MATH 0280", + "id": "MATH0280", + "sections": [ + { + "id": "4631085", + "name": "1030", + "instructor": "CHEN" + }, + { + "id": "4631086", + "name": "1041", + "instructor": "GARTSIDE" + }, + { + "id": "4631087", + "name": "1045", + "instructor": "GARTSIDE" + }, + { + "id": "4631088", + "name": "1200", + "instructor": "WOJTOWYTSCH" + } + ] + }, + { + "name": "MATH 0290", + "id": "MATH0290", + "sections": [ + { + "id": "4631089", + "name": "1040", + "instructor": "BAO" + }, + { + "id": "4631090", + "name": "1050", + "instructor": "STREIPERT" + }, + { + "id": "4631091", + "name": "1090", + "instructor": "TRENCHEA" + }, + { + "id": "4631092", + "name": "1100", + "instructor": "BAO" + } + ] + }, + { + "name": "MATH 0413", + "id": "MATH0413", + "sections": [ + { + "id": "4631093", + "name": "1030", + "instructor": "LAYTON" + }, + { + "id": "4631094", + "name": "1060", + "instructor": "HALES" + }, + { + "id": "4631095", + "name": "1100", + "instructor": "KAVEH" + } + ] + }, + { + "name": "MATH 0420", + "id": "MATH0420", + "sections": [ + { + "id": "4631096", + "name": "1020", + "instructor": "CHEN" + } + ] + }, + { + "name": "MATH 0430", + "id": "MATH0430", + "sections": [ + { + "id": "4631097", + "name": "1070", + "instructor": "PAN" + } + ] + }, + { + "name": "MATH 0470", + "id": "MATH0470", + "sections": [ + { + "id": "4631098", + "name": "1120", + "instructor": "XIONG" + } + ] + }, + { + "name": "MATH 0480", + "id": "MATH0480", + "sections": [ + { + "id": "4631099", + "name": "1000", + "instructor": "ION" + }, + { + "id": "4631100", + "name": "1200", + "instructor": "MOAKHER" + } + ] + }, + { + "name": "MATH 1010", + "id": "MATH1010", + "sections": [ + { + "id": "4631101", + "name": "1010", + "instructor": "LEWICKA" + } + ] + }, + { + "name": "MATH 1050", + "id": "MATH1050", + "sections": [ + { + "id": "4631102", + "name": "1040", + "instructor": "CONSTANTINE" + } + ] + }, + { + "name": "MATH 1070", + "id": "MATH1070", + "sections": [ + { + "id": "4631103", + "name": "1040", + "instructor": "LAYTON" + }, + { + "id": "4631104", + "name": "1050", + "instructor": "YOTOV" + } + ] + }, + { + "name": "MATH 1101", + "id": "MATH1101", + "sections": [ + { + "id": "4631105", + "name": "1030", + "instructor": "WHEELER" + }, + { + "id": "4631106", + "name": "1300", + "instructor": "CHOU" + } + ] + }, + { + "name": "MATH 1119", + "id": "MATH1119", + "sections": [ + { + "id": "4631107", + "name": "1010", + "instructor": "YUST" + } + ] + }, + { + "name": "MATH 1122", + "id": "MATH1122", + "sections": [ + { + "id": "4631108", + "name": "1000", + "instructor": "CHADAM" + } + ] + }, + { + "name": "MATH 1126", + "id": "MATH1126", + "sections": [ + { + "id": "4631109", + "name": "1010", + "instructor": "CHADAM" + } + ] + }, + { + "name": "MATH 1180", + "id": "MATH1180", + "sections": [ + { + "id": "4631110", + "name": "1030", + "instructor": "SWIGON" + }, + { + "id": "4631111", + "name": "1050", + "instructor": "CHEN" + } + ] + }, + { + "name": "MATH 1185", + "id": "MATH1185", + "sections": [ + { + "id": "4634385", + "name": "1040", + "instructor": "GILTON" + } + ] + }, + { + "name": "MATH 1270", + "id": "MATH1270", + "sections": [ + { + "id": "4631112", + "name": "1040", + "instructor": "SWIGON" + }, + { + "id": "4631113", + "name": "1050", + "instructor": "VAINCHTEIN" + } + ] + }, + { + "name": "MATH 1290", + "id": "MATH1290", + "sections": [ + { + "id": "4631114", + "name": "1100", + "instructor": "DEBLOIS" + } + ] + }, + { + "name": "MATH 1360", + "id": "MATH1360", + "sections": [ + { + "id": "4631115", + "name": "1040", + "instructor": "ERMENTROUT" + } + ] + }, + { + "name": "MATH 1470", + "id": "MATH1470", + "sections": [ + { + "id": "4631116", + "name": "1070", + "instructor": "CHEN" + } + ] + }, + { + "name": "MATH 1530", + "id": "MATH1530", + "sections": [ + { + "id": "4590464", + "name": "1060", + "instructor": "HAJLASZ" + }, + { + "id": "4631117", + "name": "1230", + "instructor": "SCHIKORRA" + } + ] + }, + { + "name": "MATH 1550", + "id": "MATH1550", + "sections": [ + { + "id": "4631118", + "name": "1040", + "instructor": "LENNARD" + } + ] + }, + { + "name": "MATH 1900", + "id": "MATH1900", + "sections": [ + { + "id": "4561833", + "name": "1100", + "instructor": "DEBLOIS" + } + ] + }, + { + "name": "MATH 1902", + "id": "MATH1902", + "sections": [ + { + "id": "4561834", + "name": "1030", + "instructor": "HALES" + }, + { + "id": "4561835", + "name": "1040", + "instructor": "SCHIKORRA" + }, + { + "id": "4561836", + "name": "1050", + "instructor": "LAYTON" + }, + { + "id": "4561837", + "name": "1060", + "instructor": "GILTON" + }, + { + "id": "4561838", + "name": "1070", + "instructor": "DEBLOIS" + }, + { + "id": "4561839", + "name": "1080", + "instructor": "CONSTANTINE" + } + ] + }, + { + "name": "MATH 2000", + "id": "MATH2000", + "sections": [ + { + "id": "4561840", + "name": "1010", + "instructor": "NEILAN" + } + ] + }, + { + "name": "MATH 2060", + "id": "MATH2060", + "sections": [ + { + "id": "4590465", + "name": "1200", + "instructor": "CONSTANTINE" + } + ] + }, + { + "name": "MATH 2070", + "id": "MATH2070", + "sections": [ + { + "id": "4590466", + "name": "1040", + "instructor": "NEILAN" + }, + { + "id": "4631119", + "name": "1030", + "instructor": "NEILAN" + } + ] + }, + { + "name": "MATH 2090", + "id": "MATH2090", + "sections": [ + { + "id": "4590467", + "name": "1100", + "instructor": "TRENCHEA" + } + ] + }, + { + "name": "MATH 2301", + "id": "MATH2301", + "sections": [ + { + "id": "4590468", + "name": "1040", + "instructor": "LENNARD" + } + ] + }, + { + "name": "MATH 2303", + "id": "MATH2303", + "sections": [ + { + "id": "4590469", + "name": "1060", + "instructor": "LEWICKA" + } + ] + }, + { + "name": "MATH 2370", + "id": "MATH2370", + "sections": [ + { + "id": "4590470", + "name": "1040", + "instructor": "WOJTOWYTSCH" + } + ] + }, + { + "name": "MATH 2500", + "id": "MATH2500", + "sections": [ + { + "id": "4590471", + "name": "1100", + "instructor": "WANG ERICKSON" + } + ] + }, + { + "name": "MATH 2505", + "id": "MATH2505", + "sections": [ + { + "id": "4590472", + "name": "1200", + "instructor": "ION" + } + ] + }, + { + "name": "MATH 2603", + "id": "MATH2603", + "sections": [ + { + "id": "4590473", + "name": "1200", + "instructor": "YOTOV" + } + ] + }, + { + "name": "MATH 2800", + "id": "MATH2800", + "sections": [ + { + "id": "4590474", + "name": "1100", + "instructor": "FEDOROV" + } + ] + }, + { + "name": "MATH 2900", + "id": "MATH2900", + "sections": [ + { + "id": "4590475", + "name": "1000", + "instructor": "VAINCHTEIN" + } + ] + }, + { + "name": "MATH 2920", + "id": "MATH2920", + "sections": [ + { + "id": "4590476", + "name": "1070", + "instructor": "RUBIN" + } + ] + }, + { + "name": "MATH 2990", + "id": "MATH2990", + "sections": [ + { + "id": "4561841", + "name": "1010", + "instructor": "NEILAN" + } + ] + }, + { + "name": "MATH 3000", + "id": "MATH3000", + "sections": [ + { + "id": "4561842", + "name": "1010", + "instructor": "NEILAN" + } + ] + }, + { + "name": "MATH 3225", + "id": "MATH3225", + "sections": [ + { + "id": "4590477", + "name": "1010", + "instructor": "YAO" + } + ] + }, + { + "name": "MATH 3900", + "id": "MATH3900", + "sections": [ + { + "id": "4561843", + "name": "1010", + "instructor": "NEILAN" + } + ] + }, + { + "name": "MATH 3902", + "id": "MATH3902", + "sections": [ + { + "id": "4561844", + "name": "1010", + "instructor": "NEILAN" + } + ] + }, + { + "name": "MATH 3923", + "id": "MATH3923", + "sections": [ + { + "id": "4585743", + "name": "1200", + "instructor": "GALDI" + } + ] + } +] diff --git a/tests/samples/textbook_courses_STAT.json b/tests/samples/textbook_courses_STAT.json deleted file mode 100644 index 1e5c3e4..0000000 --- a/tests/samples/textbook_courses_STAT.json +++ /dev/null @@ -1 +0,0 @@ -[{"name":"STAT 0200","id":"STAT0200","sections":[{"id":"2105836","name":"1030","instructor":"QUESEN"},{"id":"2105837","name":"1035","instructor":null},{"id":"2105838","name":"1040","instructor":null},{"id":"2105839","name":"1045","instructor":null},{"id":"2105840","name":"1050","instructor":null},{"id":"2105841","name":"1055","instructor":"STRASSER"},{"id":"2105842","name":"1060","instructor":null},{"id":"2105843","name":"1065","instructor":null},{"id":"2105844","name":"1070","instructor":null},{"id":"2105845","name":"1075","instructor":null},{"id":"2105846","name":"1080","instructor":"STRASSER"},{"id":"2105847","name":"1085","instructor":null},{"id":"2105848","name":"1090","instructor":null},{"id":"2105849","name":"1095","instructor":null},{"id":"2105850","name":"1100","instructor":null},{"id":"2105851","name":"1105","instructor":"LUTZ"},{"id":"2105852","name":"1110","instructor":null},{"id":"2105853","name":"1115","instructor":null},{"id":"2105854","name":"1120","instructor":null},{"id":"2105855","name":"1125","instructor":null},{"id":"2105856","name":"1130","instructor":"PERRY"},{"id":"2105857","name":"1135","instructor":null},{"id":"2105858","name":"1140","instructor":null},{"id":"2105859","name":"1145","instructor":null},{"id":"2105860","name":"1150","instructor":null},{"id":"2105861","name":"7000","instructor":"HARMON"},{"id":"2105862","name":"7010","instructor":"KIM"},{"id":"2105863","name":"7020","instructor":"KIM"},{"id":"2105864","name":"7030","instructor":"KIM"},{"id":"2105865","name":"7510","instructor":"HARMON"}]},{"name":"STAT 0800","id":"STAT0800","sections":[{"id":"2105866","name":"1030","instructor":"NOROSKI"}]},{"name":"STAT 1000","id":"STAT1000","sections":[{"id":"2105867","name":"1040","instructor":"PFENNING"},{"id":"2105868","name":"1050","instructor":null},{"id":"2105869","name":"1060","instructor":null},{"id":"2105870","name":"1070","instructor":null},{"id":"2105871","name":"1080","instructor":null},{"id":"2105872","name":"1100","instructor":"WANG"},{"id":"2105873","name":"1110","instructor":null},{"id":"2105874","name":"1120","instructor":null},{"id":"2105875","name":"1130","instructor":null},{"id":"2105876","name":"1140","instructor":null},{"id":"2105877","name":"1200","instructor":"PFENNING"},{"id":"2105878","name":"1210","instructor":null},{"id":"2105879","name":"1260","instructor":"KENKEL"},{"id":"2105880","name":"1270","instructor":null},{"id":"2105881","name":"1280","instructor":null},{"id":"2105882","name":"1290","instructor":null},{"id":"2105883","name":"1300","instructor":null},{"id":"2105884","name":"1320","instructor":"YI"},{"id":"2105885","name":"1330","instructor":null},{"id":"2105886","name":"1340","instructor":null},{"id":"2105887","name":"1350","instructor":null},{"id":"2105888","name":"1360","instructor":null},{"id":"2105889","name":"1370","instructor":"WANG"},{"id":"2105890","name":"1380","instructor":null},{"id":"2105891","name":"1390","instructor":null},{"id":"2105892","name":"1400","instructor":null},{"id":"2105893","name":"1410","instructor":null},{"id":"2105894","name":"1420","instructor":"NELSON"},{"id":"2105895","name":"1425","instructor":null},{"id":"2105896","name":"1430","instructor":null},{"id":"2105897","name":"1435","instructor":null},{"id":"2105898","name":"1440","instructor":null}]},{"name":"STAT 1100","id":"STAT1100","sections":[{"id":"2105899","name":"1010","instructor":"NELSON"},{"id":"2105900","name":"1015","instructor":null},{"id":"2105901","name":"1020","instructor":null},{"id":"2105902","name":"1025","instructor":null},{"id":"2105903","name":"1030","instructor":null},{"id":"2105904","name":"1035","instructor":"NELSON"},{"id":"2105905","name":"1040","instructor":null},{"id":"2105906","name":"1045","instructor":null},{"id":"2105907","name":"1050","instructor":null},{"id":"2105908","name":"1055","instructor":null},{"id":"2105909","name":"1060","instructor":"NELSON"},{"id":"2105910","name":"1065","instructor":null},{"id":"2105911","name":"1070","instructor":null},{"id":"2105912","name":"1075","instructor":null},{"id":"2105913","name":"1080","instructor":null},{"id":"2105914","name":"1085","instructor":"BILONICK"},{"id":"2105915","name":"1090","instructor":null},{"id":"2105916","name":"1095","instructor":null},{"id":"2105917","name":"1100","instructor":null},{"id":"2105918","name":"1105","instructor":null},{"id":"2105919","name":"7510","instructor":"CHIAPPETTA"}]},{"name":"STAT 1151","id":"STAT1151","sections":[{"id":"2105920","name":"1055","instructor":"BODENSCHATZ"},{"id":"2105921","name":"1060","instructor":"BODENSCHATZ"}]},{"name":"STAT 1211","id":"STAT1211","sections":[{"id":"2105922","name":"1010","instructor":"BILONICK"}]},{"name":"STAT 1291","id":"STAT1291","sections":[{"id":"2105923","name":"1010","instructor":"JUNG"},{"id":"2105924","name":"1015","instructor":"JUNG"},{"id":"2105925","name":"1020","instructor":"JUNG"}]},{"name":"STAT 1321","id":"STAT1321","sections":[{"id":"2105926","name":"1060","instructor":"STOFFER"}]},{"name":"STAT 1631","id":"STAT1631","sections":[{"id":"2105927","name":"1030","instructor":"REN"}]},{"name":"STAT 1900","id":"STAT1900","sections":[{"id":"2105928","name":"1010","instructor":"IYENGAR"},{"id":"2105929","name":"1020","instructor":"CHENG"},{"id":"2105930","name":"1025","instructor":"SAMPSON"},{"id":"2105931","name":"1035","instructor":null},{"id":"2105932","name":"1040","instructor":"STOFFER"},{"id":"2105933","name":"1045","instructor":"JUNG"},{"id":"2105934","name":"1050","instructor":"CHEN"},{"id":"2105935","name":"1060","instructor":"REN"},{"id":"2105936","name":"1070","instructor":"MENTCH"}]},{"name":"STAT 1902","id":"STAT1902","sections":[{"id":"2105937","name":"1015","instructor":"IYENGAR"},{"id":"2105938","name":"1030","instructor":"CHENG"},{"id":"2105939","name":"1035","instructor":"SAMPSON"},{"id":"2105940","name":"1045","instructor":"MENTCH"},{"id":"2105941","name":"1050","instructor":"STOFFER"},{"id":"2105942","name":"1055","instructor":"JUNG"},{"id":"2105943","name":"1060","instructor":"CHEN"},{"id":"2105944","name":"1070","instructor":"REN"},{"id":"2105945","name":"1080","instructor":"MENTCH"}]},{"name":"STAT 2001","id":"STAT2001","sections":[{"id":"2105946","name":"1010","instructor":"IYENGAR"},{"id":"2105947","name":"1025","instructor":"CHENG"},{"id":"2105948","name":"1030","instructor":"SAMPSON"},{"id":"2105949","name":"1040","instructor":null},{"id":"2105950","name":"1045","instructor":"STOFFER"},{"id":"2105951","name":"1050","instructor":"JUNG"},{"id":"2105952","name":"1055","instructor":"CHEN"},{"id":"2105953","name":"1060","instructor":"REN"},{"id":"2105954","name":"1070","instructor":"MENTCH"}]},{"name":"STAT 2020","id":"STAT2020","sections":[{"id":"2105955","name":"1010","instructor":"PFENNING"}]},{"name":"STAT 2131","id":"STAT2131","sections":[{"id":"2105956","name":"1030","instructor":"CHEN"}]},{"name":"STAT 2210","id":"STAT2210","sections":[{"id":"2105957","name":"1010","instructor":"BILONICK"}]},{"name":"STAT 2320","id":"STAT2320","sections":[{"id":"2105958","name":"1060","instructor":"STOFFER"}]},{"name":"STAT 2321","id":"STAT2321","sections":[{"id":"2105959","name":"1210","instructor":"STOFFER"}]},{"name":"STAT 2381","id":"STAT2381","sections":[{"id":"2105960","name":"1030","instructor":"SAMPSON"}]},{"name":"STAT 2391","id":"STAT2391","sections":[{"id":"2105961","name":"1010","instructor":"IYENGAR"}]},{"name":"STAT 2392","id":"STAT2392","sections":[{"id":"2105962","name":"1010","instructor":null}]},{"name":"STAT 2630","id":"STAT2630","sections":[{"id":"2105963","name":"1030","instructor":"REN"}]},{"name":"STAT 2631","id":"STAT2631","sections":[{"id":"2105964","name":"1060","instructor":"JUNG"}]},{"name":"STAT 2661","id":"STAT2661","sections":[{"id":"2147518","name":"1010","instructor":"SAMPSON"}]},{"name":"STAT 2691","id":"STAT2691","sections":[{"id":"2105965","name":"1010","instructor":"REN"}]},{"name":"STAT 2711","id":"STAT2711","sections":[{"id":"2105966","name":"1020","instructor":"IYENGAR"}]},{"name":"STAT 2900","id":"STAT2900","sections":[{"id":"2105967","name":"1010","instructor":"IYENGAR"},{"id":"2105968","name":"1020","instructor":"CHENG"},{"id":"2105969","name":"1025","instructor":"SAMPSON"},{"id":"2105970","name":"1035","instructor":null},{"id":"2105971","name":"1040","instructor":"STOFFER"},{"id":"2105972","name":"1045","instructor":"JUNG"},{"id":"2105973","name":"1050","instructor":"CHEN"},{"id":"2105974","name":"1060","instructor":"REN"},{"id":"2105975","name":"1070","instructor":"MENTCH"}]},{"name":"STAT 2991","id":"STAT2991","sections":[{"id":"2105976","name":"1010","instructor":"IYENGAR"},{"id":"2105977","name":"1025","instructor":"CHENG"},{"id":"2105978","name":"1030","instructor":"SAMPSON"},{"id":"2105979","name":"1040","instructor":null},{"id":"2105980","name":"1045","instructor":"STOFFER"},{"id":"2105981","name":"1050","instructor":"JUNG"},{"id":"2105982","name":"1055","instructor":"CHEN"},{"id":"2105983","name":"1060","instructor":"REN"},{"id":"2105984","name":"1070","instructor":"MENTCH"}]},{"name":"STAT 3001","id":"STAT3001","sections":[{"id":"2105985","name":"1010","instructor":"IYENGAR"},{"id":"2105986","name":"1025","instructor":"CHENG"},{"id":"2105987","name":"1030","instructor":"SAMPSON"},{"id":"2105988","name":"1040","instructor":null},{"id":"2105989","name":"1045","instructor":"STOFFER"},{"id":"2105990","name":"1050","instructor":"JUNG"},{"id":"2105991","name":"1055","instructor":"CHEN"},{"id":"2105992","name":"1060","instructor":"REN"},{"id":"2105993","name":"1070","instructor":"MENTCH"}]},{"name":"STAT 3901","id":"STAT3901","sections":[{"id":"2105994","name":"1030","instructor":"IYENGAR"},{"id":"2105995","name":"1060","instructor":"CHENG"},{"id":"2105996","name":"1070","instructor":"SAMPSON"},{"id":"2105997","name":"1090","instructor":null},{"id":"2105998","name":"1100","instructor":"STOFFER"},{"id":"2105999","name":"1105","instructor":"JUNG"},{"id":"2106000","name":"1110","instructor":"CHEN"},{"id":"2106001","name":"1120","instructor":"REN"},{"id":"2106002","name":"1130","instructor":"MENTCH"}]}] diff --git a/tests/samples/textbook_subjects.json b/tests/samples/textbook_subjects.json new file mode 100644 index 0000000..1a87a52 --- /dev/null +++ b/tests/samples/textbook_subjects.json @@ -0,0 +1,674 @@ +[ + { + "id": "98224", + "name": "*GOGGLES" + }, + { + "id": "98225", + "name": "*LABCOATS" + }, + { + "id": "98226", + "name": "*NUR KITS" + }, + { + "id": "22401", + "name": "AFRCNA" + }, + { + "id": "22403", + "name": "ANTH" + }, + { + "id": "22404", + "name": "ARABIC" + }, + { + "id": "28594", + "name": "ARC" + }, + { + "id": "22405", + "name": "ARTSC" + }, + { + "id": "22406", + "name": "ASL" + }, + { + "id": "22407", + "name": "ASTRON" + }, + { + "id": "22408", + "name": "ATHLTR" + }, + { + "id": "22409", + "name": "BACC" + }, + { + "id": "22410", + "name": "BCHS" + }, + { + "id": "80803", + "name": "BCMS" + }, + { + "id": "22411", + "name": "BECN" + }, + { + "id": "22412", + "name": "BFIN" + }, + { + "id": "22414", + "name": "BIND" + }, + { + "id": "22415", + "name": "BIOENG" + }, + { + "id": "22416", + "name": "BIOETH" + }, + { + "id": "22417", + "name": "BIOINF" + }, + { + "id": "22418", + "name": "BIOSC" + }, + { + "id": "22419", + "name": "BIOST" + }, + { + "id": "22420", + "name": "BMIS" + }, + { + "id": "22421", + "name": "BMKT" + }, + { + "id": "22424", + "name": "BQOM" + }, + { + "id": "22425", + "name": "BSEO" + }, + { + "id": "22426", + "name": "BSPP" + }, + { + "id": "22428", + "name": "BUSACC" + }, + { + "id": "22430", + "name": "BUSBIS" + }, + { + "id": "22431", + "name": "BUSECN" + }, + { + "id": "22432", + "name": "BUSENV" + }, + { + "id": "22433", + "name": "BUSERV" + }, + { + "id": "22434", + "name": "BUSFIN" + }, + { + "id": "22435", + "name": "BUSHRM" + }, + { + "id": "22436", + "name": "BUSMKT" + }, + { + "id": "22437", + "name": "BUSORG" + }, + { + "id": "22438", + "name": "BUSQOM" + }, + { + "id": "22439", + "name": "BUSSCM" + }, + { + "id": "22440", + "name": "BUSSPP" + }, + { + "id": "22441", + "name": "CDACCT" + }, + { + "id": "22442", + "name": "CDENT" + }, + { + "id": "22443", + "name": "CEE" + }, + { + "id": "22444", + "name": "CGS" + }, + { + "id": "22445", + "name": "CHE" + }, + { + "id": "22446", + "name": "CHEM" + }, + { + "id": "22447", + "name": "CHIN" + }, + { + "id": "22448", + "name": "CLASS" + }, + { + "id": "22449", + "name": "CLRES" + }, + { + "id": "22452", + "name": "CMPBIO" + }, + { + "id": "32207", + "name": "CMPINF" + }, + { + "id": "22456", + "name": "COMMRC" + }, + { + "id": "26403", + "name": "COUN" + }, + { + "id": "22457", + "name": "CS" + }, + { + "id": "22458", + "name": "CSD" + }, + { + "id": "22459", + "name": "DENHYG" + }, + { + "id": "22463", + "name": "DSPHL" + }, + { + "id": "22466", + "name": "ECE" + }, + { + "id": "22467", + "name": "ECON" + }, + { + "id": "22468", + "name": "EDUC" + }, + { + "id": "44670", + "name": "EFOP" + }, + { + "id": "22469", + "name": "ELI" + }, + { + "id": "22470", + "name": "EM" + }, + { + "id": "22472", + "name": "ENGCMP" + }, + { + "id": "22473", + "name": "ENGFLM" + }, + { + "id": "22474", + "name": "ENGLIT" + }, + { + "id": "22475", + "name": "ENGR" + }, + { + "id": "22476", + "name": "ENGSCI" + }, + { + "id": "22477", + "name": "ENGWRT" + }, + { + "id": "22479", + "name": "EOH" + }, + { + "id": "22480", + "name": "EPIDEM" + }, + { + "id": "22481", + "name": "FACDEV" + }, + { + "id": "26401", + "name": "FMST" + }, + { + "id": "22484", + "name": "FP" + }, + { + "id": "22485", + "name": "FR" + }, + { + "id": "22491", + "name": "GEOL" + }, + { + "id": "22492", + "name": "GER" + }, + { + "id": "22493", + "name": "GERON" + }, + { + "id": "22494", + "name": "GREEK" + }, + { + "id": "22495", + "name": "GREEKM" + }, + { + "id": "22496", + "name": "GSWS" + }, + { + "id": "22497", + "name": "HAA" + }, + { + "id": "23736", + "name": "HEBREW" + }, + { + "id": "61591", + "name": "HHD" + }, + { + "id": "26198", + "name": "HI" + }, + { + "id": "22499", + "name": "HINDI" + }, + { + "id": "22500", + "name": "HIST" + }, + { + "id": "22501", + "name": "HONORS" + }, + { + "id": "22503", + "name": "HPM" + }, + { + "id": "22504", + "name": "HPS" + }, + { + "id": "22505", + "name": "HRS" + }, + { + "id": "22506", + "name": "HUGEN" + }, + { + "id": "27731", + "name": "HUN" + }, + { + "id": "22508", + "name": "IE" + }, + { + "id": "22511", + "name": "INFSCI" + }, + { + "id": "22512", + "name": "INTBP" + }, + { + "id": "22513", + "name": "IRISH" + }, + { + "id": "22516", + "name": "ITAL" + }, + { + "id": "22517", + "name": "JPNSE" + }, + { + "id": "22518", + "name": "JS" + }, + { + "id": "22519", + "name": "KOREAN" + }, + { + "id": "22520", + "name": "LATIN" + }, + { + "id": "22521", + "name": "LAW" + }, + { + "id": "61465", + "name": "LCJS" + }, + { + "id": "22522", + "name": "LCTL" + }, + { + "id": "22523", + "name": "LDRSHP" + }, + { + "id": "22525", + "name": "LING" + }, + { + "id": "22526", + "name": "LIS" + }, + { + "id": "22528", + "name": "MATH" + }, + { + "id": "22529", + "name": "ME" + }, + { + "id": "22531", + "name": "MEDEDU" + }, + { + "id": "22532", + "name": "MEMS" + }, + { + "id": "22535", + "name": "MSCBIO" + }, + { + "id": "22537", + "name": "MSCMP" + }, + { + "id": "22538", + "name": "MSE" + }, + { + "id": "22544", + "name": "MSNBIO" + }, + { + "id": "22545", + "name": "MUSIC" + }, + { + "id": "22548", + "name": "NROSCI" + }, + { + "id": "64465", + "name": "NUCE" + }, + { + "id": "22549", + "name": "NUR" + }, + { + "id": "22550", + "name": "NURCNS" + }, + { + "id": "22552", + "name": "NURNP" + }, + { + "id": "22553", + "name": "NURSAN" + }, + { + "id": "22554", + "name": "NURSP" + }, + { + "id": "22555", + "name": "NUTR" + }, + { + "id": "22560", + "name": "OT" + }, + { + "id": "22561", + "name": "PAS" + }, + { + "id": "22565", + "name": "PERS" + }, + { + "id": "22566", + "name": "PETE" + }, + { + "id": "22567", + "name": "PHARM" + }, + { + "id": "22568", + "name": "PHIL" + }, + { + "id": "22569", + "name": "PHYS" + }, + { + "id": "22570", + "name": "PIA" + }, + { + "id": "37740", + "name": "PITT" + }, + { + "id": "22571", + "name": "POLISH" + }, + { + "id": "22572", + "name": "PORT" + }, + { + "id": "22574", + "name": "PS" + }, + { + "id": "22575", + "name": "PSY" + }, + { + "id": "22578", + "name": "PT" + }, + { + "id": "22579", + "name": "PUBHLT" + }, + { + "id": "22580", + "name": "PUBSRV" + }, + { + "id": "22582", + "name": "REHSCI" + }, + { + "id": "22583", + "name": "REL" + }, + { + "id": "22584", + "name": "RELGST" + }, + { + "id": "22585", + "name": "RESTD" + }, + { + "id": "63601", + "name": "RT" + }, + { + "id": "22586", + "name": "RUSS" + }, + { + "id": "22587", + "name": "SA" + }, + { + "id": "22589", + "name": "SLAV" + }, + { + "id": "22590", + "name": "SLOVAK" + }, + { + "id": "22591", + "name": "SOC" + }, + { + "id": "22592", + "name": "SOCWRK" + }, + { + "id": "22593", + "name": "SPAN" + }, + { + "id": "22594", + "name": "STAT" + }, + { + "id": "22595", + "name": "SWAHIL" + }, + { + "id": "22596", + "name": "SWBEH" + }, + { + "id": "22598", + "name": "SWCOSA" + }, + { + "id": "22599", + "name": "SWE" + }, + { + "id": "22600", + "name": "SWGEN" + }, + { + "id": "22601", + "name": "SWINT" + }, + { + "id": "22602", + "name": "SWRES" + }, + { + "id": "22603", + "name": "SWWEL" + }, + { + "id": "22604", + "name": "TELCOM" + }, + { + "id": "22605", + "name": "THEA" + }, + { + "id": "61590", + "name": "TLL" + }, + { + "id": "22606", + "name": "TURKSH" + }, + { + "id": "22607", + "name": "UKRAIN" + }, + { + "id": "22608", + "name": "URBNST" + }, + { + "id": "38025", + "name": "ZZZ" + } +] diff --git a/tests/samples/textbook_textbooks_CS_0441_garrison.json b/tests/samples/textbook_textbooks_CS_0441_garrison.json new file mode 100644 index 0000000..ac59d62 --- /dev/null +++ b/tests/samples/textbook_textbooks_CS_0441_garrison.json @@ -0,0 +1,69 @@ +[ + { + "id": "4558031_BSZWEWZWMZYJ_0", + "isbn": "BSZWEWZWMZYJ", + "pf_id": null, + "new_item_id": "2603309:N", + "used_item_id": null, + "required": "Inclusive Access", + "sort_order": [ + "0", + "2", + "Ia Canvas Content" + ], + "cover_image_url": "//vst-coverimages.verbacompete.com/no_image.jpg", + "title": "Ia Canvas Content", + "author": "Redshelf Ia", + "notes": null, + "citation": "Ia Canvas Content by Redshelf Ia. (ISBN: BSZWEWZWMZYJ).", + "metadata": { + "section_id": "4558031", + "section_code": "22840", + "term_name": "Fall 24" + }, + "offers": [ + { + "isbn": "BSZWEWZWMZYJ", + "retailer": "bookstore", + "item_id": "2603309:N", + "condition": "new", + "title": null, + "currency": null, + "merchant": "bookstore", + "rental_days": null, + "retailer_name": "University Store", + "metadata": null, + "in_cart": null, + "selected": null, + "total": "45.75", + "retailer_order": 0, + "lowest_price": null, + "highest_price": null, + "comments": "Books Arriving Every Day-Check Frequently!", + "special_comment": "Pick it up or ship it!", + "store_branded": true, + "data_source": "bookstore", + "description": "From University Store on Fifth", + "seller_rating": null, + "fcondition": "New", + "fprice": "$45.75", + "discounted_shipping": null, + "can_checkout": false, + "feedback_count": null, + "location": null, + "price": "45.75", + "shipping": 0, + "variant": "new", + "source": "default", + "id": "bookstore_BSZWEWZWMZYJ_new_2603309:N" + } + ], + "section_id": null, + "db_section_id": null, + "inclusive_access": false, + "catalog_id": null, + "edition": null, + "copyright_year": null, + "binding": null + } +] diff --git a/tests/samples/textbook_textbooks_MATH_0430_pan.json b/tests/samples/textbook_textbooks_MATH_0430_pan.json new file mode 100644 index 0000000..38df6bf --- /dev/null +++ b/tests/samples/textbook_textbooks_MATH_0430_pan.json @@ -0,0 +1,104 @@ +[ + { + "id": "4631097_9780201763904_0", + "isbn": "9780201763904", + "pf_id": null, + "new_item_id": "1445331:N", + "used_item_id": "1445331:U", + "required": "Required", + "sort_order": [ + "0", + "1", + "First Course In Abstract Algebra" + ], + "cover_image_url": "//vst-coverimages.verbacompete.com/33f017cc-33d1-5c4c-b8e0-122cb182fa8a.jpg", + "title": "First Course In Abstract Algebra", + "author": "Fraleigh", + "notes": null, + "citation": "\u003cem\u003eFirst Course In Abstract Algebra\u003c/em\u003e by Fraleigh. Pearson Education, 7th Edition, 2002. (ISBN: 9780201763904).", + "metadata": { + "section_id": "4631097", + "section_code": "11295", + "term_name": "Fall 24" + }, + "offers": [ + { + "isbn": "9780201763904", + "retailer": "bookstore", + "item_id": "1445331:N", + "condition": "new", + "title": null, + "currency": null, + "merchant": "bookstore", + "rental_days": null, + "retailer_name": "University Store", + "metadata": null, + "in_cart": null, + "selected": null, + "total": "206.65", + "retailer_order": 0, + "lowest_price": null, + "highest_price": null, + "comments": "Books Arriving Every Day-Check Frequently!", + "special_comment": "Pick it up or ship it!", + "store_branded": true, + "data_source": "bookstore", + "description": "From University Store on Fifth", + "seller_rating": null, + "fcondition": "New", + "fprice": "$206.65", + "discounted_shipping": null, + "can_checkout": false, + "feedback_count": null, + "location": null, + "price": "206.65", + "shipping": 0, + "variant": "new", + "source": "default", + "id": "bookstore_9780201763904_new_1445331:N" + }, + { + "isbn": "9780201763904", + "retailer": "bookstore", + "item_id": "1445331:U", + "condition": "used", + "title": null, + "currency": null, + "merchant": "bookstore", + "rental_days": null, + "retailer_name": "University Store", + "metadata": null, + "in_cart": null, + "selected": null, + "total": "155.0", + "retailer_order": 0, + "lowest_price": null, + "highest_price": null, + "comments": "Pick it up or ship it!", + "special_comment": "Pick it up or ship it!", + "store_branded": true, + "data_source": "bookstore", + "description": "From University Store on Fifth", + "seller_rating": null, + "fcondition": "Used", + "fprice": "$155.00", + "discounted_shipping": null, + "can_checkout": true, + "feedback_count": null, + "location": null, + "price": "155.0", + "shipping": 0, + "variant": "used", + "source": "default", + "id": "bookstore_9780201763904_used_1445331:U" + } + ], + "section_id": null, + "db_section_id": null, + "inclusive_access": false, + "catalog_id": null, + "edition": "7", + "copyright_year": "2003", + "binding": null + } +] diff --git a/tests/textbook_test.py b/tests/textbook_test.py index 5f8b2b7..88b95a7 100644 --- a/tests/textbook_test.py +++ b/tests/textbook_test.py @@ -17,192 +17,406 @@ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ +from pittapi import textbook + import responses import json import unittest from pathlib import Path +from pytest import mark +from requests import ConnectionError +from typing import Any -from pittapi import textbook - -TERM = "1000" SAMPLE_PATH = Path() / "tests" / "samples" - -print(SAMPLE_PATH.absolute()) +CSRF_TOKEN = "1MTtTVOcQCCXDjKNKTqkfiwp0lmLWz1RvFy2ed65XeyGO4on-8zWsQpEAt4cjiH0glx9CIyjhAOKpXhIqDK_vg" +CS_SUBJECT_ID = "22457" +MATH_SUBJECT_ID = "22528" +CS_0441_GARRISON_SECTION_ID = "4558031" +MATH_0430_PAN_SECTION_ID = "4631097" class TextbookTest(unittest.TestCase): def __init__(self, *args, **kwargs): unittest.TestCase.__init__(self, *args, **kwargs) - self.validate_term = textbook._validate_term - self.validate_course = textbook._validate_course + with (SAMPLE_PATH / "textbook_base_page.html").open() as f: + self.html_text = f.read() + with (SAMPLE_PATH / "textbook_subjects.json").open() as f: + self.subjects_data = json.load(f) with (SAMPLE_PATH / "textbook_courses_CS.json").open() as f: self.cs_data = json.load(f) - with (SAMPLE_PATH / "textbook_courses_STAT.json").open() as f: - self.stat_data = json.load(f) + with (SAMPLE_PATH / "textbook_courses_MATH.json").open() as f: + self.math_data = json.load(f) + with (SAMPLE_PATH / "textbook_textbooks_CS_0441_garrison.json").open() as f: + self.cs_0441_textbook_data: list[dict[str, Any]] = json.load(f) + with (SAMPLE_PATH / "textbook_textbooks_MATH_0430_pan.json").open() as f: + self.math_0430_textbook_data: list[dict[str, Any]] = json.load(f) - @responses.activate - def test_textbook_get_textbook(self): + def setUp(self): + textbook.request_headers = None + textbook.subject_map = None + responses.start() + + def tearDown(self): + responses.stop() + responses.reset() + + def mock_base_site_success(self): + 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) + + def mock_subject_map_success(self): responses.add( responses.GET, - "http://pitt.verbacompare.com/compare/courses/?id=22457&term_id=1000", - json=self.cs_data, - status=200, + f"https://pitt.verbacompare.com/compare/departments/?term={textbook.CURRENT_TERM_ID}", + json=self.subjects_data, ) - instructor_test = textbook.get_textbook(term=TERM, department="CS", course="445", instructor="GARRISON III") - instructor_test = textbook.get_textbook(term=TERM, department="CS", course="445", instructor="GARRISON III") - section_test = textbook.get_textbook(term=TERM, department="CS", course="445", section="1030") - self.assertIsInstance(instructor_test, list) - self.assertIsInstance(section_test, list) + 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.activate - def test_textbook_get_textbooks(self): + def mock_cs_courses_success(self): responses.add( responses.GET, - "http://pitt.verbacompare.com/compare/courses/?id=22457&term_id=1000", + f"https://pitt.verbacompare.com/compare/courses/?id={CS_SUBJECT_ID}&term_id={textbook.CURRENT_TERM_ID}", json=self.cs_data, - status=200, ) + + def mock_cs_courses_failure(self): responses.add( responses.GET, - "http://pitt.verbacompare.com/compare/courses/?id=22594&term_id=1000", - json=self.stat_data, - status=200, + f"https://pitt.verbacompare.com/compare/courses/?id={CS_SUBJECT_ID}&term_id={textbook.CURRENT_TERM_ID}", + status=400, ) - multi_book_test = textbook.get_textbooks( - term=TERM, - courses=[ - {"department": "STAT", "course": "1000", "instructor": "WANG"}, - {"department": "CS", "course": "445", "section": "1030"}, - ], - ) - self.assertIsInstance(multi_book_test, list) - @responses.activate - def test_get_textbook_invalid_term(self): + def mock_cs_0441_garrison_books_success(self): responses.add( responses.GET, - "http://pitt.verbacompare.com/compare/courses/?id=22457&term_id=1000", - json=self.cs_data, - status=200, + f"https://pitt.verbacompare.com/compare/books?id={CS_0441_GARRISON_SECTION_ID}", + json=self.cs_0441_textbook_data, ) - self.assertRaises(TypeError, textbook.get_textbook, "0000", "CS", "401") - @responses.activate - def test_get_textbook_invalid_subject(self): - # TODO(@azharichenko): Added better subject verification in future update + 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=[]) + + def mock_math_courses_success(self): responses.add( responses.GET, - "http://pitt.verbacompare.com/compare/courses/?id=22457&term_id=1000", - json=self.cs_data, - status=200, - ) - self.assertRaises( - ValueError, - textbook.get_textbook, - TERM, - "Computer Science", - "000", - "EXIST", - None, + f"https://pitt.verbacompare.com/compare/courses/?id={MATH_SUBJECT_ID}&term_id={textbook.CURRENT_TERM_ID}", + json=self.math_data, ) - @responses.activate - def test_get_textbook_invalid_instructor(self): + def mock_math_courses_failure(self): responses.add( responses.GET, - "http://pitt.verbacompare.com/compare/courses/?id=22457&term_id=1000", - json=self.cs_data, - status=200, + f"https://pitt.verbacompare.com/compare/courses/?id={MATH_SUBJECT_ID}&term_id={textbook.CURRENT_TERM_ID}", + status=400, ) - self.assertRaises(LookupError, textbook.get_textbook, TERM, "CS", "447", "EXIST", None) - @responses.activate - def test_get_textbook_invalid_section(self): + def mock_math_0430_pan_books_success(self): responses.add( responses.GET, - "http://pitt.verbacompare.com/compare/courses/?id=22457&term_id=1000", - json=self.cs_data, - status=200, + f"https://pitt.verbacompare.com/compare/books?id={MATH_0430_PAN_SECTION_ID}", + json=self.math_0430_textbook_data, ) - self.assertRaises(LookupError, textbook.get_textbook, TERM, "CS", "401", None, "9999") + 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" + + course = textbook.CourseInfo(subject, course_num, instructor, section_num) + + self.assertEqual(course.subject, subject) + self.assertEqual(course.course_num, course_num) + self.assertEqual(course.instructor, instructor) + self.assertEqual(course.section_num, section_num) + + 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" + + course = textbook.CourseInfo(subject, course_num, instructor, section_num) + + self.assertEqual(course.subject, "CS") + self.assertEqual(course.course_num, "0441") + self.assertEqual(course.instructor, "GARRISON III") + self.assertEqual(course.section_num, section_num) + + def test_course_info_missing_instructor_and_section_num(self): + self.mock_base_site_success() + self.mock_subject_map_success() + subject, course_num = "cs", "0441" + + course = textbook.CourseInfo(subject, course_num) + + self.assertEqual(course.subject, "CS") + self.assertEqual(course.course_num, "0441") + self.assertIsNone(course.instructor) + self.assertIsNone(course.section_num) + + 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" + + 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" + + 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) + + 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" + + self.assertRaises(ValueError, textbook.CourseInfo, subject, course_num, instructor, section_num) + + @mark.filterwarnings("ignore:Attempt") @responses.activate - def test_get_textbook_invalid_section_and_instructor(self): - responses.add( - responses.GET, - "http://pitt.verbacompare.com/compare/courses/?id=22457&term_id=1000", - json=self.cs_data, - status=200, + def test_course_info_failing_header_requests(self): + self.mock_base_site_failure() + + 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="") + + self.assertRaises(ConnectionError, textbook.CourseInfo, "CS", "0441", instructor="GARRISON III") + + @mark.filterwarnings("ignore:Attempt") + @responses.activate + 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") + + def test_textbook_from_json(self): + self.assertEqual(len(self.cs_0441_textbook_data), 1) + + textbook_info = textbook.Textbook.from_json(self.cs_0441_textbook_data[0]) + + self.assertIsNotNone(textbook_info) + self.assertEqual(textbook_info.title, "Ia Canvas Content") + 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).") + + def test_textbook_from_json_all_empty(self): + emptied_data: dict[str, Any] = self.cs_0441_textbook_data[0].copy() + emptied_data.pop("title") + emptied_data.pop("author") + emptied_data.pop("edition") + emptied_data.pop("isbn") + emptied_data.pop("citation") + + textbook_info = textbook.Textbook.from_json(emptied_data) + + self.assertIsNone(textbook_info) + + @responses.activate + def test_get_textbooks_for_course_section_num(self): + self.mock_base_site_success() + self.mock_subject_map_success() + self.mock_cs_courses_success() + self.mock_cs_0441_garrison_books_success() + course = textbook.CourseInfo("CS", "0441", section_num="1245") + + textbooks = textbook.get_textbooks_for_course(course) + + self.assertEqual(textbook.request_headers, {"X-CSRF-Token": CSRF_TOKEN}) + self.assertEqual(len(textbook.subject_map), 168) + self.assertEqual(textbook.subject_map["CS"], CS_SUBJECT_ID) + self.assertEqual(len(textbooks), 1) + self.assertEqual(textbooks[0].title, "Ia Canvas Content") + 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).") + + @responses.activate + def test_get_textbooks_for_course_invalid_section_num(self): + self.mock_base_site_success() + self.mock_subject_map_success() + self.mock_cs_courses_success() + self.mock_cs_0441_garrison_books_success() + course = textbook.CourseInfo("CS", "0441", section_num="0000") + + self.assertRaises(LookupError, textbook.get_textbooks_for_course, course) + + @responses.activate + def test_get_textbooks_for_course_instructor(self): + self.mock_base_site_success() + self.mock_subject_map_success() + self.mock_cs_courses_success() + self.mock_cs_0441_garrison_books_success() + course = textbook.CourseInfo("CS", "0441", instructor="GARRISON III") + + textbooks = textbook.get_textbooks_for_course(course) + + self.assertEqual(textbook.request_headers, {"X-CSRF-Token": CSRF_TOKEN}) + self.assertEqual(len(textbook.subject_map), 168) + self.assertEqual(textbook.subject_map["CS"], CS_SUBJECT_ID) + self.assertEqual(len(textbooks), 1) + self.assertEqual(textbooks[0].title, "Ia Canvas Content") + 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).") + + @responses.activate + def test_get_textbooks_for_course_invalid_instructor(self): + self.mock_base_site_success() + self.mock_subject_map_success() + self.mock_cs_courses_success() + self.mock_cs_0441_garrison_books_success() + course = textbook.CourseInfo("CS", "0441", instructor="RAMIREZ") + + self.assertRaises(LookupError, textbook.get_textbooks_for_course, course) + + @responses.activate + def test_get_textbooks_for_course_invalid_course(self): + self.mock_base_site_success() + self.mock_subject_map_success() + self.mock_cs_courses_success() + course = textbook.CourseInfo("CS", "0000") + + self.assertRaises(LookupError, textbook.get_textbooks_for_course, course) + + @responses.activate + def test_get_textbooks_for_course_deduce_section(self): + self.mock_base_site_success() + self.mock_subject_map_success() + self.mock_math_courses_success() + self.mock_math_0430_pan_books_success() + course = textbook.CourseInfo("MATH", "0430") + + textbooks = textbook.get_textbooks_for_course(course) + + self.assertEqual(len(textbooks), 1) + self.assertEqual(textbooks[0].title, "First Course In Abstract Algebra") + self.assertEqual(textbooks[0].author, "Fraleigh") + self.assertEqual(textbooks[0].edition, "7") + self.assertEqual(textbooks[0].isbn, "9780201763904") + self.assertEqual( + textbooks[0].citation, + "\u003cem\u003eFirst Course In Abstract Algebra\u003c/em\u003e by Fraleigh. " + "Pearson Education, 7th Edition, 2002. (ISBN: 9780201763904).", ) - self.assertRaises(TypeError, textbook.get_textbook, TERM, "CS", "401", None, None) - - def test_term_validation(self): - self.assertEqual(self.validate_term(TERM), TERM) - - def test_term_validation_invalid(self): - self.assertRaises(ValueError, self.validate_term, "1") - self.assertRaises(ValueError, self.validate_term, "a") - self.assertRaises(ValueError, self.validate_term, "100") - self.assertRaises(ValueError, self.validate_term, "10000") - - def test_validate_course_correct_input(self): - self.assertEqual(self.validate_course("0000"), "0000") - self.assertEqual(self.validate_course("0001"), "0001") - self.assertEqual(self.validate_course("0012"), "0012") - self.assertEqual(self.validate_course("0123"), "0123") - self.assertEqual(self.validate_course("1234"), "1234") - self.assertEqual(self.validate_course("9999"), "9999") - - def test_validate_course_improper_input(self): - self.assertEqual(self.validate_course("0"), "0000") - self.assertEqual(self.validate_course("1"), "0001") - self.assertEqual(self.validate_course("12"), "0012") - self.assertEqual(self.validate_course("123"), "0123") - - def test_validate_course_incorrect_input(self): - self.assertRaises(ValueError, self.validate_course, "") - self.assertRaises(ValueError, self.validate_course, "00000") - self.assertRaises(ValueError, self.validate_course, "11111") - self.assertRaises(ValueError, self.validate_course, "hi") - - def test_construct_query(self): - construct = textbook._construct_query - course_query = "compare/courses/?id=9999&term_id=1111" - book_query = "compare/books?id=9999" - - self.assertEqual(construct("courses", "9999", "1111"), course_query) - self.assertEqual(construct("books", "9999"), book_query) - - def test_find_item(self): - find = textbook._find_item("id", "key", "test") - test_data = [ - {"id": 1, "key": 1}, - {"id": 2, "key": 4}, - {"id": 3, "key": 9}, - {"id": 4, "key": 16}, - {"id": 5, "key": 25}, - ] - for i in range(1, 6): - self.assertEqual(find(test_data, i), i**2) + @responses.activate + def test_get_textbooks_for_course_not_enough_info(self): + self.mock_base_site_success() + self.mock_subject_map_success() + self.mock_cs_courses_success() + course = textbook.CourseInfo("CS", "0441") + + self.assertRaises(LookupError, textbook.get_textbooks_for_course, course) + + @mark.filterwarnings("ignore:Attempt") + @responses.activate + def test_get_textbooks_for_course_failing_courses_requests(self): + self.mock_base_site_success() + self.mock_subject_map_success() + self.mock_cs_courses_failure() + course = textbook.CourseInfo("CS", "0441", instructor="GARRISON III") - self.assertRaises(LookupError, find, test_data, 6) + self.assertRaises(ConnectionError, textbook.get_textbooks_for_course, course) @responses.activate - def test_extract_id(self): + def test_get_textbooks_for_course_no_textbook(self): + self.mock_base_site_success() + self.mock_subject_map_success() + self.mock_cs_courses_success() + self.mock_cs_0441_garrison_books_none() + course = textbook.CourseInfo("CS", "0441", instructor="GARRISON III") + + textbooks = textbook.get_textbooks_for_course(course) + + self.assertEqual(len(textbooks), 0) + + @mark.filterwarnings("ignore:No textbook info found") + @responses.activate + def test_get_textbooks_for_course_textbook_no_info(self): + emptied_data: dict[str, Any] = self.cs_0441_textbook_data[0].copy() + emptied_data.pop("title") + emptied_data.pop("author") + emptied_data.pop("edition") + emptied_data.pop("isbn") + emptied_data.pop("citation") + self.mock_base_site_success() + self.mock_subject_map_success() + self.mock_cs_courses_success() responses.add( - responses.GET, - "http://pitt.verbacompare.com/compare/courses/?id=22457&term_id=1000", - json=self.cs_data, - status=201, + 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") - def test_filter_dictionary(self): - test_dict = {"a": 1, "b": 2, "c": 3} - test_key = ["a", "c"] - self.assertEqual(textbook._filter_dictionary(test_dict, test_key), {"a": 1, "c": 3}) + textbook_info = textbook.get_textbooks_for_course(course) + + self.assertEqual(len(textbook_info), 0) + + @responses.activate + def test_get_textbooks_for_courses(self): + self.mock_base_site_success() + self.mock_subject_map_success() + self.mock_cs_courses_success() + self.mock_math_courses_success() + self.mock_cs_0441_garrison_books_success() + self.mock_math_0430_pan_books_success() + courses = [ + textbook.CourseInfo("CS", "0441", instructor="GARRISON III"), + textbook.CourseInfo("MATH", "0430", instructor="PAN"), + ] + + textbooks = textbook.get_textbooks_for_courses(courses) + + self.assertEqual(len(textbooks), 2) + # Sort to guarantee output order, since the textbook requests are async + textbooks.sort(key=lambda x: x.author if x.author else "") + + self.assertEqual(textbooks[0].title, "First Course In Abstract Algebra") + self.assertEqual(textbooks[0].author, "Fraleigh") + self.assertEqual(textbooks[0].edition, "7") + self.assertEqual(textbooks[0].isbn, "9780201763904") + self.assertEqual( + textbooks[0].citation, + "\u003cem\u003eFirst Course In Abstract Algebra\u003c/em\u003e by Fraleigh. " + "Pearson Education, 7th Edition, 2002. (ISBN: 9780201763904).", + ) + + self.assertEqual(textbooks[1].title, "Ia Canvas Content") + 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).") + + @mark.filterwarnings("ignore:Attempt") + @responses.activate + def test_get_textbooks_for_courses_failing_courses_requests(self): + self.mock_base_site_success() + self.mock_subject_map_success() + self.mock_cs_courses_failure() + self.mock_math_courses_failure() + courses = [ + textbook.CourseInfo("CS", "0441", instructor="GARRISON III"), + textbook.CourseInfo("MATH", "0430", instructor="PAN"), + ] - def test_invalid_department_code(self): - self.assertRaises(ValueError, textbook.get_textbook, TERM, "TEST", "000", "EXIST", None) + self.assertRaises(ConnectionError, textbook.get_textbooks_for_courses, courses)