From 209c873dd12fb90f08bd24f0389f93d75398e5c7 Mon Sep 17 00:00:00 2001 From: Ritwik Gupta Date: Tue, 28 Jul 2026 22:04:05 -0700 Subject: [PATCH] Finalize the PittAPI 2.0 public API --- CONTRIBUTING.md | 15 +++-- README.md | 134 +++++++++++++++++++++++------------------- docs/MIGRATING-2.0.md | 23 ++++++++ pittapi/__init__.py | 32 ++++++++++ pyproject.toml | 2 +- tests/naming_test.py | 25 ++++++++ uv.lock | 2 +- 7 files changed, 166 insertions(+), 67 deletions(-) create mode 100644 docs/MIGRATING-2.0.md create mode 100644 tests/naming_test.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ccec88e..b9d8661 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 @@ -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. diff --git a/README.md b/README.md index 051dfcc..3405c4f 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/docs/MIGRATING-2.0.md b/docs/MIGRATING-2.0.md new file mode 100644 index 0000000..4cd118e --- /dev/null +++ b/docs/MIGRATING-2.0.md @@ -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. diff --git a/pittapi/__init__.py b/pittapi/__init__.py index 9f51aec..8cf99f3 100644 --- a/pittapi/__init__.py +++ b/pittapi/__init__.py @@ -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", +] diff --git a/pyproject.toml b/pyproject.toml index 67ebe70..4c3fea7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/tests/naming_test.py b/tests/naming_test.py new file mode 100644 index 0000000..8773e4d --- /dev/null +++ b/tests/naming_test.py @@ -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) diff --git a/uv.lock b/uv.lock index 5a6f0b4..31b8025 100644 --- a/uv.lock +++ b/uv.lock @@ -306,7 +306,7 @@ wheels = [ [[package]] name = "pittapi" -version = "1.0.0" +version = "2.0.0" source = { editable = "." } dependencies = [ { name = "beautifulsoup4" },