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
61 changes: 61 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: CI

# The repository had no CI at all: publish-to-pypi.yml was the only workflow,
# so pull requests were merged with nothing verifying them.
on:
push:
branches: [main]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# The boundaries of what pyproject.toml claims to support
# (requires-python = ">=3.8"), plus the version the release
# workflow builds with.
python-version: ["3.8", "3.11", "3.13"]

name: test (py${{ matrix.python-version }})
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip

- name: Install package and test dependencies
run: |
python -m pip install --upgrade pip
pip install -e .
pip install pytest pytest-cov

# Integration tests need a live FDC API key and django tests need Django;
# neither belongs in a pull-request gate.
- name: Run unit tests
run: pytest -m "not integration and not django" -v

build:
runs-on: ubuntu-latest
name: build (sdist + wheel)
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.13"

- name: Install build tooling
run: |
python -m pip install --upgrade pip
pip install build twine

# Catches a broken package before a tag turns it into a permanent
# PyPI release — a bad version can only be yanked, never replaced.
- name: Build and check the distribution
run: |
python -m build
python -m twine check dist/*
12 changes: 11 additions & 1 deletion .github/workflows/publish-to-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,21 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build twine
pip install build twine pytest
pip install -e .

# A release to PyPI cannot be undone — a bad version can only be yanked,
# and the version number is burned forever. So the tag must not be able
# to ship code that does not pass its own tests.
- name: Run tests
run: pytest -m "not integration and not django" -v

- name: Build package
run: python -m build

- name: Check the distribution
run: python -m twine check dist/*

- name: Create GitHub Release
uses: softprops/action-gh-release@v1
with:
Expand Down
6 changes: 6 additions & 0 deletions docs/api/exceptions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ FdcRateLimitError
.. autoclass:: FdcRateLimitError
:members:

FdcTimeoutError
-------------

.. autoclass:: FdcTimeoutError
:members:

FdcValidationError
----------------

Expand Down
24 changes: 24 additions & 0 deletions docs/user/error_handling.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,33 @@ The library defines the following exception hierarchy:
- ``FdcApiError``: Base exception for all API errors
- ``FdcAuthError``: Authentication failed (invalid API key)
- ``FdcRateLimitError``: API rate limit exceeded
- ``FdcTimeoutError``: The API did not respond within the client timeout
- ``FdcValidationError``: Invalid input parameters
- ``FdcResourceNotFoundError``: Requested resource not found

Request Timeouts
--------------

Every request is issued with a timeout, so a server that accepts a connection but
never answers cannot block the caller indefinitely. The default is 30 seconds, and
it applies to both connect and read:

.. code-block:: python

from usda_fdc import FdcClient, FdcTimeoutError

# Use the 30 second default, or set your own
client = FdcClient(api_key="your_api_key_here", timeout=5.0)

try:
food = client.get_food(1750340)
except FdcTimeoutError as e:
print(f"FDC was too slow: {e}")

``FdcTimeoutError`` subclasses ``FdcApiError``, so existing ``except FdcApiError``
blocks keep catching timeouts. Catch it separately when you want to tell "slow"
apart from "broken" — a timeout is usually worth retrying, a 400 is not.

Basic Error Handling
-----------------

Expand Down
Loading