diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7536242 --- /dev/null +++ b/.github/workflows/ci.yml @@ -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/* diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index caea6c6..bf4316b 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -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: diff --git a/docs/api/exceptions.rst b/docs/api/exceptions.rst index 876b549..abecd01 100644 --- a/docs/api/exceptions.rst +++ b/docs/api/exceptions.rst @@ -23,6 +23,12 @@ FdcRateLimitError .. autoclass:: FdcRateLimitError :members: +FdcTimeoutError +------------- + +.. autoclass:: FdcTimeoutError + :members: + FdcValidationError ---------------- diff --git a/docs/user/error_handling.rst b/docs/user/error_handling.rst index eca39fe..23e9a38 100644 --- a/docs/user/error_handling.rst +++ b/docs/user/error_handling.rst @@ -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 -----------------