Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 39 additions & 8 deletions docs/api-reference/NEWS-API.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,46 @@
> [Home](README.md) > News API

---

# News API

### **get_news(feed, max_news_items)**
`NewsClient` searches Pittwire and discovers its available filters from the
current Pittwire page. Topic IDs, category paths, and publication years are
not maintained as hardcoded lists in PittAPI.

## Discovering filters

```python
from pittapi import NewsClient

with NewsClient() as news:
topics = news.get_topics()
categories = news.get_categories()
years = news.get_years()
```

`get_topics()` returns `NewsTopic` models containing Pittwire's topic ID and
display name. `get_categories()` returns `NewsCategory` models containing the
category path and display name. `get_years()` returns integers.

## Fetching articles

```python
from pittapi import NewsClient

#### **Parameters**
- `feed`: News feed - can be one of ("main_news", "cssd", "news_chronicle", "news_alerts"). Default is "main_news"
- `max_news_items`: Maximum number of news items. Default is 10.
with NewsClient() as news:
topics = news.get_topics()
technology = next(
topic for topic in topics if topic.name == "Technology & Science"
)
articles = news.get_articles_by_topic(
technology,
query="robotics",
year=2026,
max_num_results=5,
)
```

#### **Returns**:
Returns a list of dictionaries with parameters 'title' and 'url' of each news article from each news feed category.
News fetched from `feed`.
Maximum length specified by `max_news_items`.
`get_articles_by_topic()` returns an immutable tuple of `Article` models in
Pittwire's page order. Pass a discovered `NewsCategory` with `category=` to
search a category other than Features & Articles.
38 changes: 8 additions & 30 deletions docs/api-reference/PEOPLE-API.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,16 @@
> [Home](README.md) > People API
---

# People API

### **get_person(query, max_people)**
`PeopleClient.get_person(query)` returns matching directory entries as immutable `Person` models:

#### **Parameters**:
- `query`: Query to find people | Example: `smith` or `abc123`
- `max_people`: Max number of people to return | Example: `10` or `100`
- Default value is `10`

#### **Returns**:
Returns a dictionary with data of user profiles.

#### **Example**:

###### **Code**:
```python
people.get_person('Jane')
```
from pittapi import PeopleClient

###### **Sample Output**:
```python
[
{
"name": "Jane Doe",
"email": "jdoe@pitt.edu",
"phone": "(999)999-999"
},
{
"name": "Janedo Smith",
"school": "School of Dental Medicine"
]
},
...
]
with PeopleClient() as people:
matches = people.get_person("Jane Doe")
```

Each person's `fields` tuple contains `PersonField` models. Field names are the non-empty labels published by Pitt;
repeated labels are grouped into one field whose `values` are a tuple. This preserves new directory fields without a
PittAPI release.
259 changes: 145 additions & 114 deletions pittapi/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,131 +15,162 @@
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""

from __future__ import annotations

import requests
from typing import Any, NamedTuple
Library catalog search and Hillman study-room reservations.
"""

LIBRARY_URL = (
"https://pitt.primo.exlibrisgroup.com/primaws/rest/pub/pnxs"
"?acTriggered=false&blendFacetsSeparately=false&citationTrailFilterByAvailability=true&disableCache=false&getMore=0"
"&inst=01PITT_INST&isCDSearch=false&lang=en&limit=10&newspapersActive=false&newspapersSearch=false&offset=0"
"&otbRanking=false&pcAvailability=false&qExclude=&qInclude=&rapido=false&refEntryActive=false&rtaLinks=true"
"&scope=MyInst_and_CI&searchInFulltextUserSelection=false&skipDelivery=Y&sort=rank&tab=Everything"
"&vid=01PITT_INST:01PITT_INST"
)
STUDY_ROOMS_URL = (
"https://pitt.libcal.com/spaces/bookings/search"
"?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"
"&start=0&length=25&search%5Bvalue%5D=&_=1717907260661"
from dataclasses import dataclass
from typing import Any

from pittapi.base_client import BaseClient

__all__ = ["Document", "LibraryClient", "QueryResult", "Reservation"]

LIBRARY_URL = "https://pitt.primo.exlibrisgroup.com/primaws/rest/pub/pnxs"
LIBRARY_PARAMS = {
"acTriggered": "false",
"blendFacetsSeparately": "false",
"citationTrailFilterByAvailability": "true",
"disableCache": "false",
"getMore": "0",
"inst": "01PITT_INST",
"isCDSearch": "false",
"lang": "en",
"limit": "10",
"newspapersActive": "false",
"newspapersSearch": "false",
"offset": "0",
"otbRanking": "false",
"pcAvailability": "false",
"qExclude": "",
"qInclude": "",
"rapido": "false",
"refEntryActive": "false",
"rtaLinks": "true",
"scope": "MyInst_and_CI",
"searchInFulltextUserSelection": "false",
"skipDelivery": "Y",
"sort": "rank",
"tab": "Everything",
"vid": "01PITT_INST:01PITT_INST",
}
STUDY_ROOMS_URL = "https://pitt.libcal.com/spaces/bookings/search"
STUDY_ROOM_PARAMS = {
"lid": "917",
"gid": "1558",
"eid": "0",
"seat": "0",
"d": "1",
"customDate": "",
"q": "",
"daily": "0",
"draw": "1",
"order[0][column]": "1",
"order[0][dir]": "asc",
"start": "0",
"length": "25",
"search[value]": "",
}
DOCUMENT_FIELDS = (
"title",
"language",
"subject",
"format",
"type",
"isbns",
"description",
"publisher",
"edition",
"genre",
"place",
"creator",
"version",
"creationdate",
)

QUERY_START = "&q=any,contains,"

sess = requests.session()


class Document(NamedTuple):
# Field names must exactly match key names in JSON data
title: list[str] | None = None
language: list[str] | None = None
subject: list[str] | None = None
format: list[str] | None = None
type: list[str] | None = None
isbns: list[str] | None = None
description: list[str] | None = None
publisher: list[str] | None = None
edition: list[str] | None = None
genre: list[str] | None = None
place: list[str] | None = None
creator: list[str] | None = None
version: list[str] | None = None
creationdate: list[str] | None = None


class QueryResult(NamedTuple):
@dataclass(frozen=True, slots=True)
class Document:
title: tuple[str, ...] = ()
language: tuple[str, ...] = ()
subject: tuple[str, ...] = ()
format: tuple[str, ...] = ()
type: tuple[str, ...] = ()
isbns: tuple[str, ...] = ()
description: tuple[str, ...] = ()
publisher: tuple[str, ...] = ()
edition: tuple[str, ...] = ()
genre: tuple[str, ...] = ()
place: tuple[str, ...] = ()
creator: tuple[str, ...] = ()
version: tuple[str, ...] = ()
creationdate: tuple[str, ...] = ()


@dataclass(frozen=True, slots=True)
class QueryResult:
num_results: int
num_pages: int
docs: list[Document]
documents: tuple[Document, ...]


class Reservation(NamedTuple):
@dataclass(frozen=True, slots=True)
class Reservation:
room: str
reserved_from: str
reserved_until: str


def get_documents(query: str) -> QueryResult:
"""Return ten resource results from the specified page"""
parsed_query = query.replace(" ", "+")
full_query = LIBRARY_URL + QUERY_START + parsed_query
resp = sess.get(full_query)
resp_json = resp.json()

results = QueryResult(
num_results=resp_json["info"]["total"],
num_pages=resp_json["info"]["last"],
docs=_filter_documents(resp_json["docs"]),
)
return results


def get_document_by_bookmark(bookmark: str) -> QueryResult:
"""Return resource referenced by bookmark"""
payload = {"bookMark": bookmark}
resp = sess.get(LIBRARY_URL, params=payload)
resp_json = resp.json()

if resp_json.get("errors"):
for error in resp_json.get("errors"):
if error["code"] == "invalid.bookmark.format":
raise ValueError("Invalid bookmark")
results = QueryResult(
num_results=resp_json["info"]["total"],
num_pages=resp_json["info"]["last"],
docs=_filter_documents(resp_json["docs"]),
)
return results


def _filter_documents(documents: list[dict[str, Any]]) -> list[Document]:
new_docs: list[Document] = []

for doc in documents:
filtered_doc = {key: vals for key, vals in doc["pnx"]["display"].items() if key in Document._fields}
new_docs.append(Document(**filtered_doc))

return new_docs


def hillman_total_reserved() -> int:
"""Returns a simple count dictionary of the total amount of reserved rooms appointments"""
resp = requests.get(STUDY_ROOMS_URL)
resp_json = resp.json()
total_records: int = resp_json["recordsTotal"] # Total records is kept track of by default in the JSON

# Note: this must align with the amount of entries in reserved times function; renamed for further clarification
return total_records


def reserved_hillman_times() -> list[Reservation]:
"""Returns a list of dictionaries of reserved rooms in Hillman with their respective times"""
resp = requests.get(STUDY_ROOMS_URL)
resp_json = resp.json()
data = resp_json["data"]

if data is None:
return []

# Note: there can be multiple reservations in the same room, so we must use a list of maps and not a singular map
bookings = [
Reservation(
room=reservation["itemName"],
reserved_from=reservation["from"],
reserved_until=reservation["to"],
)
for reservation in data
]
return bookings
class LibraryClient(BaseClient):
"""Search Pitt's library catalog and study-room reservations."""

def get_documents(self, query: str) -> QueryResult:
params = {**LIBRARY_PARAMS, "q": f"any,contains,{query}"}
return parse_query_result(self.request("GET", LIBRARY_URL, params=params).json())

def get_document_by_bookmark(self, bookmark: str) -> QueryResult:
params = {**LIBRARY_PARAMS, "bookMark": bookmark}
data = self.request("GET", LIBRARY_URL, params=params).json()
for error in data.get("errors", ()):
if error.get("code") == "invalid.bookmark.format":
raise ValueError("invalid bookmark")
return parse_query_result(data)

def hillman_total_reserved(self) -> int:
data = self.request("GET", STUDY_ROOMS_URL, params=STUDY_ROOM_PARAMS).json()
try:
return data["recordsTotal"]
except (KeyError, TypeError) as error:
raise ValueError("reservation response is missing its total") from error

def reserved_hillman_times(self) -> tuple[Reservation, ...]:
data = self.request("GET", STUDY_ROOMS_URL, params=STUDY_ROOM_PARAMS).json()
try:
reservations = data["data"] or ()
return tuple(
Reservation(
room=item["itemName"],
reserved_from=item["from"],
reserved_until=item["to"],
)
for item in reservations
)
except (KeyError, TypeError) as error:
raise ValueError("reservation response is missing required data") from error


def parse_query_result(data: dict[str, Any]) -> QueryResult:
try:
info = data["info"]
documents = tuple(parse_document(item) for item in data["docs"])
return QueryResult(num_results=info["total"], num_pages=info["last"], documents=documents)
except (KeyError, TypeError) as error:
raise ValueError("library response is missing required data") from error


def parse_document(data: dict[str, Any]) -> Document:
display = data["pnx"]["display"]
fields = {}
for name in DOCUMENT_FIELDS:
fields[name] = tuple(display.get(name, ()))
return Document(**fields)
Loading
Loading