Repository: pittcsc/PittAPI (language: python)

Issue:
Introduce static type checking into the API
MyPy is a terrific project that lets us introduce static typing into parts of our code. Since there are certain parts of the code which follow a specific type, use MyPy to type check those portions.

MyPy: http://mypy-lang.org/

- [x] Identify parts of the code that can be type checked (all private functions)
- [x] Add static typing to them


Discussion / hints:
Hi, I'd like to pick up this issue.  I'll look at #74 as an example.

As a next step, I would recommend using [tox](https://tox.readthedocs.io/en/latest/) to wrap up testing, type checking, and linting into one simple, portable command: `tox` !
Awesome! Thanks @Jdsleppy. And `tox` is something I'm looking at, but we're waiting on a PR to be completed to make the tests hermetic first.
I opened a PR to type annotate about half of the code base.  I see somebody worked on this in the meantime, so there were pretty nasty merge conflicts and duplication of work.  I still think my changes are valuable because they tend to specify the types a little further in places where `Any` is used.  Also, it adds annotations to `library.py`.

There is also a bugfix for a test in the PR.

https://github.com/Pitt-CSC/PittAPI/pull/82
I'm reopening this issue because I don't think it was ever _actually_ completed:
1. There are some parts of the code that still aren't type-hinted
2. mypy was never added to the repo's tooling
3. Now that we're using pre-commit, we can easily introduce and (and more importantly enforce) static type checking

Fix patch:
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


Test patch:
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 @@
+<!DOCTYPE html>
+<html lang='en-US'>
+<head>
+<meta content='text/html;charset=UTF-8' http-equiv='Content-Type'>
+<meta content='width=device-width, initial-scale=1, maximum-scale=1' name='viewport'>
+<meta name="csrf-param" content="authenticity_token" />
+<meta name="csrf-token" content="1MTtTVOcQCCXDjKNKTqkfiwp0lmLWz1RvFy2ed65XeyGO4on-8zWsQpEAt4cjiH0glx9CIyjhAOKpXhIqDK_vg" />
+<link href='https://vst-assets.verbacompete.com/assets/favicon_compare-08e4986cab90bd2622956d5b0303387270841228afe70b7c758b4a40843e8be8.ico' rel='shortcut icon'>
+<title>Compare Textbook Prices Online | University Store on Fifth</title>
+<link href='https://vst-assets.verbacompete.com/assets/compare_assets/pitt-pc/v3-4d870539649f9b1797d6631e05aa4843a8cf1ccd.css' rel='stylesheet'>
+<link href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css' rel='stylesheet'>
+<script src='//use.typekit.net/vvv5bqy.js' type='text/javascript'></script>
+<script>
+  try{Typekit.load();}catch(e){}
+</script>
+<script src="https://vst-assets.verbacompete.com/assets/compare/v3-249443292d69473ed7b0d046a70c502d5f024caca540a10137900808975af855.js"></script>
+</head>
+<!--[if lt IE 7]>
+<body class="ie ie6 index" id="compare">
+<![endif]-->
+<!--[if IE 7]>
+<body class="ie ie7 index" id="compare">
+<![endif]-->
+<!--[if IE 8]>
+<body class="ie ie8 index" id="compare">
+<![endif]-->
+<!--[if IE 9]>
+<body class="ie ie9 index" id="compare">
+<![endif]-->
+<![if (!IE)]>
+<body class='index' id='compare'>
+<![endif]>
+<div id='header' role='banner'>
+<div id='banner'>
+<div class='logo'><img src="https://vst-assets.verbacompete.com/assets/compare_assets/pitt-pc/pitt_banner-13fb4a443f97e8c86f7b6f1b9020c6e74c190fdb.png" /></div>
+<div aria-hidden='true' class='addthis_toolbox addthis_default_style addthis_32x32_style'>
+<a aria-hidden='true' class='addthis_button_facebook first'>
+<i class='fa fa-facebook-f icon-facebook'></i>
+</a>
+<a aria-hidden='true' class='addthis_button_email last'>
+<i class='fa fa-envelope icon-envelope'></i>
+</a>
+<div class='clear'></div>
+</div>
+</div>
+</div>
+
+<script src="https://vst-assets.verbacompete.com/assets/framebuster-324047eac65b190ff34d8bdfd9a913905d5b70cf5c3e5d4234b9a6f203a0a515.js"></script>
+<!--[if lt IE 9]>
+<div class="ie_warning">
+  <div class="text">
+    <h1>This site isn't optimized for Internet Explorer!</h1>
+    <p>We highly recommend you access this site through a modern web browser for a fast, full-featured experience. Download one of our favorites (Chrome, Firefox, or Safari) by clicking the icons to the right.</p>
+  </div>
+  <div class="icons">
+    <div class="icons_bg">
+      <a href="http://google.com/chrome" id="chrome"></a>
+    </div>
+    <div class="icons_bg">
+      <a href="http://www.mozilla.com/en-US/products/download.html?lang=en-US" id="firefox"></a>
+    </div>
+    <div class="icons_bg">
+      <a href="http://www.apple.com/safari/download/" id="safari"></a>
+    </div>
+    <div style="clear: both"></div>
+  </div>
+</div>
+<div style="clear: both"></div>
+<![endif]-->
+
+<noscript>
+  <div class="ie_warning">
+    <div class="text">
+      <h1>This site requires JavaScript for full functionality!</h1>
+      <p>We highly recommend you access this site through a browser with JavaScript support for a full-featured experience. Download one of our favorites by clicking the icons to the right.</p>
+    </div>
+    <div class="icons">
+      <div class="icons_bg">
+        <a href="http://google.com/chrome" id="chrome"></a>
+      </div>
+      <div class="icons_bg">
+        <a href="http://www.mozilla.com/en-US/products/download.html?lang=en-US" id="firefox"></a>
+      </div>
+      <div class="icons_bg">
+        <a href="http://www.apple.com/safari/download/" id="safari"></a>
+      </div>
+      <div style="clear: both"></div>
+    </div>
+  </div>
+  <div style="clear: both"></div>
+</noscript>
+
+<!-- Allows render_partial to find both .mobile and .html templates -->
+<div class='compare index' id='content'>
+<div id='menu-flash'></div>
+<div id='menu'>
+<div class='menu-wrapper'>
+<div id='standard-menu'>
+<div class='header' id='choose'>
+Choose Your Courses
+<div class='arrow-box'>
+<div class='arrow'></div>
+</div>
+</div>
+<div class='header' id='course-list'>
+Current Course List
+<div class='arrow-box'>
+<div class='arrow'></div>
+</div>
+</div>
+</div>
+<div id='responsive-menu'>
+<div class='header' id='choose'>
+Choose Your Courses
+<div class='arrow-box'>
+<div class='arrow'></div>
+</div>
+</div>
+<a class="responsive-menu-button highlight" href="#left-menu"><i class='fa fa-align-justify icon-align-justify'></i>
+Course List
+</a><div class='clear'></div>
+</div>
+</div>
+</div>
+<div class='table-wrapper'>
+<div id='search-structure'>
+<!-- / TODO: Remove -size user.setting from v3? -->
+<!-- / - size = locale_user.setting("pc.settings.course_selector.font_size") -->
+<div id='search'>
+<label class='sr-only' for='term-select'>
+Select a Term
+</label>
+<div class='search-wrapper' id='term'>
+<select id='term-select'></select>
+<div class='spinner_box'>
+<div class='loader'></div>
+</div>
+</div>
+<label class='sr-only' for='department-select'>
+Select a department after choosing a term
+</label>
+<div class='search-wrapper' id='department'>
+<div class='spinner_box'>
+<div class='loader'></div>
+</div>
+<select id='department-select'></select>
+</div>
+<label class='sr-only' for='course-select'>
+Select a course after choosing a department
+</label>
+<div class='search-wrapper' id='course'>
+<div class='spinner_box'>
+<div class='loader'></div>
+</div>
+<select id='course-select'></select>
+</div>
+<label class='sr-only' for='section-select'>
+Select a section after choosing a course
+</label>
+<div class='search-wrapper' id='eset'>
+<div class='spinner_box'>
+<div class='loader'></div>
+</div>
+<select id='section-select'></select>
+</div>
+</div>
+<div class='selected-wrapper'>
+<div class='selected-list'>
+<div class='none-selected'>No Courses Selected!</div>
+</div>
+</div>
+</div>
+<div class='go-wrapper'>
+<a href='#' id='go'>
+<i class='fa fa-share icon-mail-forward'></i>
+Compare Prices on These Course Materials
+</a>
+</div>
+</div>
+</div>
+<script>
+  $(document).ready(function(){
+    var terms    = new Verba.Compare.Collections.Terms([{"id":"78104","name":"Fall 24","inquiry":true,"ordering":true}]);
+    var view     = new Verba.Compare.Views.CourseSelector({el: "body", terms: terms});
+  });
+</script>
+<script>
+  if (!localStorage.getItem('cookie-policy-accepted')) {
+    $(document).ready(function(){
+      var banner = $('<div id="cookie-notice"><p>We use cookies to provide you with the best experience on our website. By using our site, you are agreeing to our <a href="https://vitalsource.com/cookies" target="_blank" rel="noopener noreferrer">Cookie Policy.</a></p></div>');
+
+      var button = $('<button>Accept &amp; Close</button>');
+
+      button.on('click', function() {
+        localStorage.setItem('cookie-policy-accepted', true);
+        banner.hide();
+      });
+
+      banner.append(button);
+      $(document.body).append(banner);
+    });
+  }
+</script>
+
+
+  <!-- Google tag (gtag.js) -->
+  <script async src="https://www.googletagmanager.com/gtag/js?id=G-ZV3F11F7QR"></script>
+  <script>
+    window.dataLayer = window.dataLayer || [];
+    function gtag(){dataLayer.push(arguments);}
+    gtag('js', new Date());
+
+    // Global Compare Tracker
+    gtag('config', 'G-ZV3F11F7QR');
+
+    // Client-Specific Tracker
+      gtag('config', 'UA-41807431-1');
+  </script>
+
+<div id='footer' role='contentinfo'>
+<div class='wrapper'>
+<div class='left'>
+© 2024
+<a target="_blank" rel="noopener noreferrer" href="https://www.verbasoftware.com">Verba | VitalSource</a>
+</div>
+<div class='right'>
+<a target="_blank" rel="noopener noreferrer" href="https://vitalsource.com/privacy">Privacy Policy</a>
+<span aria-hidden='true' class='dot'></span>
+<a target="_blank" rel="noopener noreferrer" href="https://vitalsource.com/cookies">Cookie Policy</a>
+<span aria-hidden='true' class='dot'></span>
+<a href="/cdn-cgi/l/email-protection#0d7965687863647b687f7e6479747e79627f684d7d64797923686978">Contact Store</a>
+</div>
+<div class='clear'></div>
+</div>
+</div>
+
+<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script></body>
+</html>
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 +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,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": "<em>Ia Canvas Content</em> 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="<!DOCTYPE html><html lang='en-US'></html>")
+
+        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, "<em>Ia Canvas Content</em> 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, "<em>Ia Canvas Content</em> 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, "<em>Ia Canvas Content</em> 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, "<em>Ia Canvas Content</em> 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)