Skip to content

Commit 54900d6

Browse files
Refactor library.py to use NamedTuple, fix mypy errors (#202)
Refactor sports.py to store API output using NamedTuples rather than plain Python dicts. This makes the output more user-friendly, less error-prone, and easier to type-hint.
1 parent e8c62c0 commit 54900d6

2 files changed

Lines changed: 78 additions & 99 deletions

File tree

pittapi/library.py

Lines changed: 55 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@
2020
from __future__ import annotations
2121

2222
import requests
23-
from html.parser import HTMLParser
24-
from typing import Any
23+
from typing import Any, NamedTuple
2524

2625
LIBRARY_URL = (
2726
"https://pitt.primo.exlibrisgroup.com/primaws/rest/pub/pnxs"
@@ -31,44 +30,63 @@
3130
"&scope=MyInst_and_CI&searchInFulltextUserSelection=false&skipDelivery=Y&sort=rank&tab=Everything"
3231
"&vid=01PITT_INST:01PITT_INST"
3332
)
34-
3533
STUDY_ROOMS_URL = (
3634
"https://pitt.libcal.com/spaces/bookings/search"
3735
"?lid=917&gid=1558&eid=0&seat=0&d=1&customDate=&q=&daily=0&draw=1&order%5B0%5D%5Bcolumn%5D=1&order%5B0%5D%5Bdir%5D=asc"
3836
"&start=0&length=25&search%5Bvalue%5D=&_=1717907260661"
3937
)
4038

41-
4239
QUERY_START = "&q=any,contains,"
4340

4441
sess = requests.session()
4542

4643

47-
class HTMLStrip(HTMLParser):
48-
def __init__(self):
49-
super().__init__()
50-
self.reset()
51-
self.data = []
44+
class Document(NamedTuple):
45+
# Field names must exactly match key names in JSON data
46+
title: list[str] | None = None
47+
language: list[str] | None = None
48+
subject: list[str] | None = None
49+
format: list[str] | None = None
50+
type: list[str] | None = None
51+
isbns: list[str] | None = None
52+
description: list[str] | None = None
53+
publisher: list[str] | None = None
54+
edition: list[str] | None = None
55+
genre: list[str] | None = None
56+
place: list[str] | None = None
57+
creator: list[str] | None = None
58+
version: list[str] | None = None
59+
creationdate: list[str] | None = None
60+
61+
62+
class QueryResult(NamedTuple):
63+
num_results: int
64+
num_pages: int
65+
docs: list[Document]
5266

53-
def handle_data(self, d: str) -> None:
54-
self.data.append(d)
5567

56-
def get_data(self) -> str:
57-
return "".join(self.data)
68+
class Reservation(NamedTuple):
69+
room: str
70+
reserved_from: str
71+
reserved_until: str
5872

5973

60-
def get_documents(query: str, page: int = 1) -> dict[str, Any]:
74+
def get_documents(query: str) -> QueryResult:
6175
"""Return ten resource results from the specified page"""
6276
parsed_query = query.replace(" ", "+")
6377
full_query = LIBRARY_URL + QUERY_START + parsed_query
6478
resp = sess.get(full_query)
6579
resp_json = resp.json()
6680

67-
results = _extract_results(resp_json)
81+
results = QueryResult(
82+
num_results=resp_json["info"]["total"],
83+
num_pages=resp_json["info"]["last"],
84+
docs=_filter_documents(resp_json["docs"]),
85+
)
6886
return results
6987

7088

71-
def get_document_by_bookmark(bookmark: str) -> dict[str, Any]:
89+
def get_document_by_bookmark(bookmark: str) -> QueryResult:
7290
"""Return resource referenced by bookmark"""
7391
payload = {"bookMark": bookmark}
7492
resp = sess.get(LIBRARY_URL, params=payload)
@@ -78,92 +96,50 @@ def get_document_by_bookmark(bookmark: str) -> dict[str, Any]:
7896
for error in resp_json.get("errors"):
7997
if error["code"] == "invalid.bookmark.format":
8098
raise ValueError("Invalid bookmark")
81-
results = _extract_results(resp_json)
82-
return results
83-
84-
85-
def _strip_html(html: str) -> str:
86-
strip = HTMLStrip()
87-
strip.feed(html)
88-
return strip.get_data()
89-
90-
91-
def _extract_results(json: dict[str, Any]) -> dict[str, Any]:
92-
results = {
93-
"total_results": json["info"]["total"],
94-
"pages": json["info"]["last"],
95-
"docs": _extract_documents(json["docs"]),
96-
}
99+
results = QueryResult(
100+
num_results=resp_json["info"]["total"],
101+
num_pages=resp_json["info"]["last"],
102+
docs=_filter_documents(resp_json["docs"]),
103+
)
97104
return results
98105

99106

100-
def _extract_documents(documents: list[dict[str, Any]]) -> list[dict[str, Any]]:
101-
new_docs = []
102-
keep_keys = {
103-
"title",
104-
"language",
105-
"subject",
106-
"format",
107-
"type",
108-
"isbns",
109-
"description",
110-
"publisher",
111-
"edition",
112-
"genre",
113-
"place",
114-
"creator",
115-
"edition",
116-
"version",
117-
"creationdate",
118-
}
107+
def _filter_documents(documents: list[dict[str, Any]]) -> list[Document]:
108+
new_docs: list[Document] = []
119109

120110
for doc in documents:
121-
new_doc = {}
122-
for key in set(doc["pnx"]["display"].keys()) & keep_keys:
123-
new_doc[key] = doc["pnx"]["display"][key]
124-
new_docs.append(new_doc)
111+
filtered_doc = {key: vals for key, vals in doc["pnx"]["display"].items() if key in Document._fields}
112+
new_docs.append(Document(**filtered_doc))
125113

126114
return new_docs
127115

128116

129-
def _extract_facets(facet_fields: list[dict[str, Any]]) -> dict[str, list[dict[str, Any]]]:
130-
facets: dict[str, list[dict[str, Any]]] = {}
131-
for facet in facet_fields:
132-
facets[facet["display_name"]] = []
133-
for count in facet["counts"]:
134-
facets[facet["display_name"]].append({"value": count["value"], "count": count["count"]})
135-
136-
return facets
137-
138-
139-
def hillman_total_reserved() -> dict[str, int]:
117+
def hillman_total_reserved() -> int:
140118
"""Returns a simple count dictionary of the total amount of reserved rooms appointments"""
141-
count = {}
142119
resp = requests.get(STUDY_ROOMS_URL)
143-
resp = resp.json()
144-
# Total records is kept track of by default in the JSON
145-
total_records = resp["recordsTotal"]
120+
resp_json = resp.json()
121+
total_records: int = resp_json["recordsTotal"] # Total records is kept track of by default in the JSON
146122

147123
# Note: this must align with the amount of entries in reserved times function; renamed for further clarification
148-
count["Total Hillman Reservations"] = total_records
149-
return count
124+
return total_records
150125

151126

152-
def reserved_hillman_times() -> list[dict[str, str | list[str]]]:
127+
def reserved_hillman_times() -> list[Reservation]:
153128
"""Returns a list of dictionaries of reserved rooms in Hillman with their respective times"""
154129
resp = requests.get(STUDY_ROOMS_URL)
155-
resp = resp.json()
156-
data = resp["data"]
130+
resp_json = resp.json()
131+
data = resp_json["data"]
157132

158133
if data is None:
159134
return []
160135

161136
# Note: there can be multiple reservations in the same room, so we must use a list of maps and not a singular map
162137
bookings = [
163-
{
164-
"Room": reservation["itemName"],
165-
"Reserved": [reservation["from"], reservation["to"]],
166-
}
138+
Reservation(
139+
room=reservation["itemName"],
140+
reserved_from=reservation["from"],
141+
reserved_until=reservation["to"],
142+
)
167143
for reservation in data
168144
]
169145
return bookings

tests/library_test.py

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,8 @@ def test_get_documents(self):
4343
status=200,
4444
)
4545
query_result = library.get_documents("water")
46-
self.assertIsInstance(query_result, dict)
47-
self.assertEqual(query_result["pages"], 10)
48-
self.assertEqual(len(query_result["docs"]), 10)
46+
self.assertEqual(query_result.num_pages, 10)
47+
self.assertEqual(len(query_result.docs), 10)
4948

5049

5150
class StudyRoomTest(unittest.TestCase):
@@ -62,7 +61,7 @@ def test_hillman_total_reserved(self):
6261
json=self.hillman_query,
6362
status=200,
6463
)
65-
self.assertEqual(library.hillman_total_reserved(), {"Total Hillman Reservations": 4})
64+
self.assertEqual(library.hillman_total_reserved(), 4)
6665

6766
@responses.activate
6867
def test_reserved_hillman_times(self):
@@ -73,21 +72,25 @@ def test_reserved_hillman_times(self):
7372
status=200,
7473
)
7574
mock_answer = [
76-
{
77-
"Room": "408 HL (Max. 5 persons) (Enclosed Room)",
78-
"Reserved": ["2024-06-12 17:30:00", "2024-06-12 20:30:00"],
79-
},
80-
{
81-
"Room": "409 HL (Max. 5 persons) (Enclosed Room)",
82-
"Reserved": ["2024-06-12 18:00:00", "2024-06-12 21:00:00"],
83-
},
84-
{
85-
"Room": "303 HL (Max. 5 persons) (Enclosed Room)",
86-
"Reserved": ["2024-06-12 18:30:00", "2024-06-12 21:30:00"],
87-
},
88-
{
89-
"Room": "217 HL (Max. 10 persons) (Enclosed Room)",
90-
"Reserved": ["2024-06-12 19:00:00", "2024-06-12 22:30:00"],
91-
},
75+
library.Reservation(
76+
room="408 HL (Max. 5 persons) (Enclosed Room)",
77+
reserved_from="2024-06-12 17:30:00",
78+
reserved_until="2024-06-12 20:30:00",
79+
),
80+
library.Reservation(
81+
room="409 HL (Max. 5 persons) (Enclosed Room)",
82+
reserved_from="2024-06-12 18:00:00",
83+
reserved_until="2024-06-12 21:00:00",
84+
),
85+
library.Reservation(
86+
room="303 HL (Max. 5 persons) (Enclosed Room)",
87+
reserved_from="2024-06-12 18:30:00",
88+
reserved_until="2024-06-12 21:30:00",
89+
),
90+
library.Reservation(
91+
room="217 HL (Max. 10 persons) (Enclosed Room)",
92+
reserved_from="2024-06-12 19:00:00",
93+
reserved_until="2024-06-12 22:30:00",
94+
),
9295
]
9396
self.assertEqual(mock_answer, library.reserved_hillman_times())

0 commit comments

Comments
 (0)