From c2e3d004eaf110a1f951a785dffa2ab7f2f1315b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 1 Aug 2026 07:38:52 +0000 Subject: [PATCH] ci: separate validation and document HTTP transport Make deterministic offline tests the normal CI gate, move live MLB API tests to a manual/scheduled workflow, remove automatic publishing workflows, and document HTTP transport behavior for 0.8.0. Co-authored-by: Matthew Spah --- .../build-and-test-mlbstatsapi-prd.yml | 35 --- .../build-and-test-mlbstatsapi-test.yml | 36 --- .github/workflows/build-and-test.yml | 54 +++- .github/workflows/external-tests.yml | 34 +++ README.md | 61 +++- docs/http-transport.md | 277 ++++++++++++++++++ 6 files changed, 404 insertions(+), 93 deletions(-) delete mode 100644 .github/workflows/build-and-test-mlbstatsapi-prd.yml delete mode 100644 .github/workflows/build-and-test-mlbstatsapi-test.yml create mode 100644 .github/workflows/external-tests.yml create mode 100644 docs/http-transport.md diff --git a/.github/workflows/build-and-test-mlbstatsapi-prd.yml b/.github/workflows/build-and-test-mlbstatsapi-prd.yml deleted file mode 100644 index eb88028..0000000 --- a/.github/workflows/build-and-test-mlbstatsapi-prd.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Python Build MLBstats API - -on: - push: - branches: - - main - -jobs: - build: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.10"] - - steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Install Poetry - uses: snok/install-poetry@v1 - with: - virtualenvs-create: true - virtualenvs-in-project: true - - name: Install dependencies - run: poetry install --no-interaction - - name: Test external tests with pytest - run: poetry run pytest tests/external_tests/ - - name: Build package - run: poetry build - - name: Publish a Python distribution to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/build-and-test-mlbstatsapi-test.yml b/.github/workflows/build-and-test-mlbstatsapi-test.yml deleted file mode 100644 index 0421b4e..0000000 --- a/.github/workflows/build-and-test-mlbstatsapi-test.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Build - -on: - push: - branches: - - development - -jobs: - build: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.10"] - - steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Install Poetry - uses: snok/install-poetry@v1 - with: - virtualenvs-create: true - virtualenvs-in-project: true - - name: Install dependencies - run: poetry install --no-interaction - - name: Test external tests with pytest - run: poetry run pytest tests/external_tests/ - - name: Build package - run: poetry build - - name: Publish package to TestPyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - password: ${{ secrets.TEST_PYPI_API_TOKEN }} - repository_url: https://test.pypi.org/legacy/ diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index c5d1fe0..0cd4bcd 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -1,17 +1,33 @@ -name: Python Build MLBstats API +name: Offline CI on: + pull_request: + branches: + - main + - release/0.8.0 push: - branches-ignore: - - 'main' - - 'development' + branches: + - main + - release/0.8.0 + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: - build: + offline-tests: + name: Offline tests - Python ${{ matrix.python-version }} runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.10", "3.11", "3.12"] + python-version: + - "3.10" + - "3.11" + - "3.12" steps: - uses: actions/checkout@v4 @@ -26,7 +42,29 @@ jobs: virtualenvs-in-project: true - name: Install dependencies run: poetry install --no-interaction - - name: Test external tests with pytest - run: poetry run pytest tests/external_tests/ + - name: Run offline tests + run: | + poetry run pytest \ + tests/ \ + --ignore=tests/external_tests + + build-package: + name: Build package + needs: offline-tests + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: true + - name: Install dependencies + run: poetry install --no-interaction - name: Build package run: poetry build diff --git a/.github/workflows/external-tests.yml b/.github/workflows/external-tests.yml new file mode 100644 index 0000000..cc76258 --- /dev/null +++ b/.github/workflows/external-tests.yml @@ -0,0 +1,34 @@ +name: External MLB API Tests + +on: + workflow_dispatch: + schedule: + - cron: "0 12 * * 1" + +permissions: + contents: read + +jobs: + external-tests: + name: External MLB API tests - Python 3.12 + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: true + - name: Install dependencies + run: poetry install --no-interaction + - name: Run external MLB API tests + run: | + poetry run pytest \ + tests/external_tests/ \ + -v diff --git a/README.md b/README.md index acfbc4f..8ab625d 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ **The Unofficial Python Wrapper for the MLB Stats API** [![PyPI version](https://badge.fury.io/py/python-mlb-statsapi.svg)](https://badge.fury.io/py/python-mlb-statsapi) -![Development Branch Status](https://github.com/zero-sum-seattle/python-mlb-statsapi/actions/workflows/build-and-test-mlbstatsapi-test.yml/badge.svg?event=push) +[![Offline CI](https://github.com/zero-sum-seattle/python-mlb-statsapi/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/zero-sum-seattle/python-mlb-statsapi/actions/workflows/build-and-test.yml) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/python-mlb-statsapi) ![GitHub](https://img.shields.io/github/license/zero-sum-seattle/python-mlb-statsapi) @@ -65,6 +65,29 @@ Ty France Seattle Mariners Seattle ``` +## HTTP Reliability and Configuration + +The client remains synchronous. Sessions and retries are handled automatically for ordinary users, and existing `Mlb()` construction remains valid. + +Prefer a context manager when you want automatic cleanup: + +```python +import mlbstatsapi + +with mlbstatsapi.Mlb() as mlb: + player = mlb.get_person(664034) +``` + +Customize connect and read timeouts when needed: + +```python +mlb = mlbstatsapi.Mlb( + timeout=(5.0, 60.0), +) +``` + +For shared Sessions, Session injection, retries, and structured exceptions, see [HTTP Transport](docs/http-transport.md). + ## Working with Pydantic Models All returned objects are Pydantic models, giving you access to powerful serialization and validation features. @@ -180,17 +203,35 @@ Contributions are welcome! Whether it's bug fixes, new features, or documentatio ### Development +Offline tests are deterministic and should run before every pull request: + ```bash -# Run tests -poetry run pytest +poetry run pytest \ + tests/ \ + --ignore=tests/external_tests +``` + +External tests contact the live MLB API. They require internet access and are separate from normal offline CI: + +```bash +poetry run pytest \ + tests/external_tests/ +``` + +These live tests may fail because the MLB service is unavailable or because MLB changes undocumented payloads. -# Run external tests (requires internet) -poetry run pytest tests/external_tests/ +Full local validation: + +```bash +poetry run pytest tests/ +poetry build ``` +Offline CI is the normal pull-request gate. External tests are available manually, on a weekly schedule, and before releases. + ### Pull Request Guidelines -- **All tests must pass** before submitting a PR +- Run offline tests before submitting a PR - Use the [PR template](.github/pull_request_template.md) when creating your pull request - Follow the branch naming convention: - `feat/` - New features @@ -207,14 +248,6 @@ Found a bug or have a feature request? Please [open an issue](https://github.com - Expected vs actual behavior - Python version and package version -### Note on External Tests - -Some tests make real API calls to the MLB Stats API. These may occasionally fail due to: -- API changes (new fields, removed endpoints) -- Season/data availability - -If you notice external test failures, please check if the MLB API has changed and update the models accordingly. - ## Examples diff --git a/docs/http-transport.md b/docs/http-transport.md new file mode 100644 index 0000000..9150f64 --- /dev/null +++ b/docs/http-transport.md @@ -0,0 +1,277 @@ +# HTTP Transport + +This document describes the HTTP transport behavior introduced in version 0.8.0. + +The public client remains synchronous. Ordinary usage does not need to configure sessions or retries. + +## Existing usage + +Existing construction continues to work: + +```python +import mlbstatsapi + +mlb = mlbstatsapi.Mlb() +player = mlb.get_person(664034) +``` + +The client remains synchronous. Async support is not part of version 0.8.0. + +## Context manager + +Prefer a context manager when you want automatic cleanup of a library-created Session: + +```python +import mlbstatsapi + +with mlbstatsapi.Mlb() as mlb: + player = mlb.get_person(664034) +``` + +Exiting the block closes a Session created by the library. + +It does not close a caller-owned injected Session. + +## Explicit cleanup + +You can also close the client explicitly: + +```python +mlb = mlbstatsapi.Mlb() + +try: + player = mlb.get_person(664034) +finally: + mlb.close() +``` + +Repeated `close()` calls are safe. + +## Default timeout + +Every request uses an explicit timeout. + +The default is: + +```python +DEFAULT_TIMEOUT = (3.05, 30.0) +``` + +That means: + +```text +3.05 seconds: connection timeout +30 seconds: read timeout +``` + +The read timeout is the maximum wait while reading response data. It is not one total wall-clock duration for the complete request. + +## Custom timeout + +A scalar applies the same value to both connect and read phases: + +```python +mlb = mlbstatsapi.Mlb(timeout=10) +``` + +A tuple provides separate connect and read values: + +```python +mlb = mlbstatsapi.Mlb( + timeout=(5.0, 60.0), +) +``` + +## Shared Session model + +One `requests.Session` is shared by the client's adapters: + +```text +Mlb client +├── v1 adapter ────┐ +│ ├── shared requests.Session +└── v1.1 adapter ──┘ +``` + +A Session manages: + +* Reusable connection pools +* Shared HTTP configuration +* Mounted retry adapters on library-created Sessions + +A Session is not: + +* A response cache +* One guaranteed permanent TCP connection +* An async transport + +## Session injection + +Advanced callers may inject a Session: + +```python +import requests +import mlbstatsapi + +session = requests.Session() + +try: + mlb = mlbstatsapi.Mlb(session=session) + player = mlb.get_person(664034) +finally: + session.close() +``` + +Ownership rules: + +```text +Library-created Session + The library owns and closes it + +Caller-injected Session + The caller owns and closes it +``` + +The library does not install or replace retry adapters on caller-injected Sessions. + +Callers who inject a Session control its retry, TLS, proxy, and adapter configuration. + +## Default retry policy + +Library-created Sessions mount a bounded retry policy for GET requests. + +```text +Initial request: 1 +Maximum retries: 3 +Maximum total attempts: 4 +Allowed method: GET +Backoff factor: 0.5 +Retry-After respected: yes +``` + +Retryable HTTP statuses: + +```text +429 +500 +502 +503 +504 +``` + +Non-retryable ordinary client statuses: + +```text +400 +401 +403 +404 +``` + +Additional rules: + +* Retries are bounded +* Only GET requests are retried +* Invalid JSON is not retried +* Pydantic validation failures are not retried +* Application parsing failures are not retried +* A final 404 preserves existing not-found behavior +* A final 429 preserves existing 4xx compatibility +* A final 5xx raises `MlbHttpError` + +Retries improve resilience for transient failures. They do not guarantee success. + +## Structured exceptions + +```text +TheMlbStatsApiException +├── MlbTransportError +│ └── MlbTimeoutError +├── MlbHttpError +└── MlbDecodeError +``` + +Imports: + +```python +from mlbstatsapi import ( + MlbDecodeError, + MlbHttpError, + MlbTimeoutError, + MlbTransportError, + TheMlbStatsApiException, +) +``` + +Precise handling: + +```python +try: + player = mlb.get_person(664034) +except MlbTimeoutError: + print("The MLB API timed out") +except MlbTransportError: + print("The request could not reach the MLB API") +except MlbHttpError as exc: + print( + exc.status_code, + exc.reason, + exc.url, + ) +except MlbDecodeError: + print("The MLB API returned invalid JSON") +``` + +Backward-compatible handling remains valid because all new errors inherit from `TheMlbStatsApiException`: + +```python +try: + player = mlb.get_person(664034) +except TheMlbStatsApiException: + print("The MLB request failed") +``` + +Notes: + +* `MlbTimeoutError` is a subtype of `MlbTransportError` +* All new errors inherit from `TheMlbStatsApiException` +* Existing broad exception handling remains valid +* Original Requests or JSON decoding failures are preserved through exception chaining + +## HTTP exception attributes + +`MlbHttpError` exposes: + +```text +status_code +reason +url +``` + +It does not expose a response body or Response object. + +## Existing 404 behavior + +Version 0.8.0 preserves endpoint-specific not-found behavior. + +Depending on the endpoint, a 404 may become: + +```text +None +[] +{} +``` + +Not every 404 raises `MlbHttpError`. + +## No response caching + +Shared Sessions pool network connections. They do not cache MLB response bodies. + +The client has no default response cache. + +## No async support + +The client remains synchronous. + +Async support is not part of version 0.8.0.