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
15 changes: 9 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ This includes making sure that your code compiles, passes all unit tests, and ad
### Unit Testing

PittAPI uses `pytest` for unit testing.
To run all unit tests, run
The suite requires 100% statement and branch coverage. To run it:
```sh
uv run pytest --cov=pittapi tests/
uv run pytest --cov=pittapi --cov-branch --cov-fail-under=100 tests/
```

### Code Quality
Expand All @@ -63,10 +63,13 @@ uv run flake8 --max-line-length=127 .
uv run black --line-length=127 .
```

Apart from general code styling, you should also document and comment your code based on general best practices.
This means that most if not all functions should have docstrings explaining their purpose, inputs, and outputs.
This also means that comments should primarily be written to clarify code whose function isn't immediately obvious to the average reader.
We may ask you to make changes to your documentation and comments during PR reviews.
PittAPI should remain understandable to developers who are still learning Python. Prefer explicit control flow,
descriptive intermediate variables, and early returns over dense expressions. Add a helper only when it names a
meaningful domain operation, removes real duplication, or substantially reduces nesting. Do not add pass-through
wrappers or speculative defensive checks.

Ordinary helper functions, methods, and classes do not begin with `_`; modules use `__all__` to document their
supported exports. Comments should explain non-obvious provider behavior rather than narrating ordinary Python.

In terms of writing style, we expect you to write in a professional manner and follow proper commenting etiquette—pretend that this is a work environment and your comments are being reviewed by your manager and coworkers.

Expand Down
134 changes: 75 additions & 59 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,83 +3,99 @@
![Build Status](https://img.shields.io/github/actions/workflow/status/pittcsc/PittAPI/autotest.yml?branch=dev)
![License](https://img.shields.io/badge/license-GPLv2-blue.svg)
![Python Version](https://img.shields.io/badge/python-3.13-green.svg)
[![Pypi Version](https://img.shields.io/pypi/v/pittapi.svg)](https://pypi.org/project/PittAPI/)
[![PyPI Version](https://img.shields.io/pypi/v/pittapi.svg)](https://pypi.org/project/PittAPI/)

The Pitt API is an unofficial Python API made by Ritwik Gupta at the University of Pittsburgh in an effort to get more open data from Pitt.
PittAPI is an unofficial Python library for public data from the University of Pittsburgh and related services.
Version 2.0 provides synchronous service clients and immutable, typed result models.

## Installation

PittAPI requires Python 3.13. Add the published package to a uv-managed project with:
PittAPI requires Python 3.13. Add it to a uv-managed project with:

```sh
uv add pittapi
```

To work on PittAPI itself, install [uv](https://docs.astral.sh/uv/getting-started/installation/), clone this repository,
and create the locked development environment:
## Client basics

```sh
uv sync --locked
Each data source has its own client. A client accepts an optional `requests.Session` and a timeout in seconds. When
PittAPI creates the session, using the client as a context manager closes it automatically:

```python
from pittapi import CourseClient

with CourseClient(timeout=15) as courses:
cs = courses.get_subject_courses("CS")
for course in cs.courses:
print(course.course_number, course.course_title)
```

Run Python and project tools inside that environment with `uv run`, for example `uv run python`.
An injected session remains owned by the caller and is not closed by PittAPI.

All structured results are frozen, slotted dataclasses. Nested collections are tuples, so callers cannot accidentally
change a response after it is returned.

## Usage examples
## Examples

```python
from pittapi import course, dining, lab, laundry, library, news, people, shuttle, textbook

### Courses
# Returns a dictionary of all courses for a given subject
cs_subject = course.get_subject_courses(subject='CS')
courses_dict = cs_subject.courses

# Returns a list of sections of a course during a given term
cs_course = course.get_course_details(term='2244', subject='CS', course='1501')
section_list = cs_course.sections

### Textbook
# Returns a list of dictionaries containing textbooks for a class
# Term number comes from pitt.verbacompare.com
small_dict = textbook.get_textbook(term="3150", department="CS", course="445", instructor="RAMIREZ")

### Library
# Returns a dictionary containing results from query
big_dict = library.get_documents(query="computer")

### News
# Returns a list of dictionaries containing news from main news feed
medium_dict = news.get_news()

### Laundry
# Returns a dictionary with number of washers and dryers in use vs.
# total washers and dryers in building
small_dict = laundry.get_status_simple(building_name="TOWERS")

### Computer Lab
# Returns a dictionary with status of the lab and number of open machines
small_dict = lab.get_status(lab_name="ALUMNI")

### Shuttle
# Returns a list of dictionaries containing routes of shuttles
big_dict = shuttle.get_routes()

### People
# Returns a list of people based on the query
list_of_peeps = people.get_person(query="Smith")

### Dining
# Returns a dictionary of dictionaries containing each dining location with its
# name and open/closed status
medium_dict = dining.get_locations()
# Returns a dictionary of a dining location with its hours for a given day
hours = dining.get_location_hours("The Eatery", datetime.datetime(2024, 4, 12))
from datetime import datetime

from pittapi import DiningClient, LibraryClient, NewsClient

with DiningClient() as dining:
locations = dining.get_locations()
hours = dining.get_location_hours("The Eatery", datetime(2024, 4, 12))

with LibraryClient() as library:
books = library.get_documents("computer science")

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, max_num_results=5)
```

## Contributing
Services whose available resources change over time provide discovery methods:

```python
from pittapi import LabClient, LaundryClient, ShuttleClient, TextbookClient
from pittapi.textbook import CourseInfo

with LabClient() as labs:
locations = labs.get_locations()
status = labs.get_status(locations[0])

with LaundryClient() as laundry:
rooms = laundry.get_locations()
machines = laundry.get_machine_statuses(rooms[0])

with ShuttleClient() as shuttle:
configuration = shuttle.get_configuration()
routes = shuttle.get_routes(configuration)

with TextbookClient() as textbooks:
term = next(term for term in textbooks.get_terms() if term.inquiry_enabled)
textbooks.select_term(term)
books = textbooks.get_textbooks_for_course(CourseInfo("CS", "0441"))
```

Network and HTTP failures remain standard `requests` exceptions. Invalid arguments or malformed provider responses
raise `ValueError`; a valid lookup with no matching entity raises `LookupError`.

Users upgrading from PittAPI 1.x should read the [2.0 migration guide](docs/MIGRATING-2.0.md).

## Development

Install the locked Python 3.13 development environment and run the full validation suite:

```sh
uv sync --locked
uv run pytest --cov=pittapi --cov-branch --cov-fail-under=100 tests/
uv run pre-commit run --all-files
```

Read our [contributing guidelines](/CONTRIBUTING.md) to learn how to contribute to the Pitt API.
See the [contributing guidelines](CONTRIBUTING.md) for the project’s readability and testing expectations.

## License

This project is licensed under the terms of the [GPLv2 license](/LICENSE).
PittAPI is licensed under the [GPLv2](LICENSE).
23 changes: 23 additions & 0 deletions docs/MIGRATING-2.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Migrating from PittAPI 1.x to 2.0

PittAPI 2.0 replaces module-level functions, named tuples, and structured dictionaries with service clients and
immutable dataclasses.

Create the client for the service you need and call the corresponding method:

```python
from pittapi import CourseClient

with CourseClient() as courses:
result = courses.get_subject_courses("CS")
```

Clients accept an optional `requests.Session` and timeout. A client closes sessions it creates when used as a context
manager; an injected session remains owned by the caller.

Labs, laundry rooms, shuttle configuration, textbook terms, dining locations, and Pittwire topics are discovered at
runtime. Pass the returned model to later calls instead of using an embedded string or numeric identifier.

Structured results are frozen, slotted dataclasses, and nested collections are tuples. Invalid arguments or malformed
provider responses raise `ValueError`, missing requested entities raise `LookupError`, and network failures remain
standard `requests` exceptions.
32 changes: 32 additions & 0 deletions pittapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,36 @@
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.

Readable, typed clients for public University of Pittsburgh services.
"""

from pittapi.cal import CalendarClient
from pittapi.course import CourseClient
from pittapi.dining import DiningClient
from pittapi.gym import GymClient
from pittapi.lab import LabClient
from pittapi.laundry import LaundryClient
from pittapi.library import LibraryClient
from pittapi.news import NewsClient
from pittapi.people import PeopleClient
from pittapi.shuttle import ShuttleClient
from pittapi.sports import SportsClient
from pittapi.status import StatusClient
from pittapi.textbook import TextbookClient

__all__ = [
"CalendarClient",
"CourseClient",
"DiningClient",
"GymClient",
"LabClient",
"LaundryClient",
"LibraryClient",
"NewsClient",
"PeopleClient",
"ShuttleClient",
"SportsClient",
"StatusClient",
"TextbookClient",
]
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "pittapi"
version = "1.0.0"
version = "2.0.0"
description = "An API to get data from Pitt and Pitt-related applications."
readme = "README.md"
requires-python = ">=3.13,<3.14"
Expand Down
25 changes: 25 additions & 0 deletions tests/naming_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import ast
from dataclasses import FrozenInstanceError
from pathlib import Path

import pytest

from pittapi.cal import Event


def test_ordinary_definitions_do_not_start_with_underscore():
violations = []
for path in Path("pittapi").glob("*.py"):
tree = ast.parse(path.read_text(), filename=str(path))
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
if node.name.startswith("_") and not node.name.startswith("__"):
violations.append(f"{path}:{node.lineno} {node.name}")
assert violations == []


def test_models_are_frozen_and_nested_collections_are_tuples():
event = Event("2026-01-01", "Title", "Content", ("Academic",))
with pytest.raises(FrozenInstanceError):
event.title = "Changed"
assert isinstance(event.categories, tuple)
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading