From e4ba2806c51e7fbcd04d0d14f279f30b42dab05d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Jul 2026 18:32:29 +0000 Subject: [PATCH 1/2] Document FdcTimeoutError and the client timeout The 0.1.10 merge added the timeout and its exception but left the docs describing the old exception hierarchy, which no longer matches what the library raises. Co-Authored-By: Claude Opus 4.8 --- docs/api/exceptions.rst | 6 ++++++ docs/user/error_handling.rst | 24 ++++++++++++++++++++++++ 2 files changed, 30 insertions(+) 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 ----------------- From 6a098c5f614d197a7d2acf84995e5a6776243e03 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Jul 2026 18:33:14 +0000 Subject: [PATCH 2/2] Add CI, and gate the PyPI publish behind the tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The repository had no CI at all. publish-to-pypi.yml was the only workflow, so pull requests merged with nothing verifying them — PR #8 reported zero checks. Worse, that publish workflow never ran the tests: its steps were checkout, build, release, upload. A version tag went straight to PyPI regardless of whether the code worked, and a bad PyPI release cannot be undone — it can only be yanked, and the version number is burned forever. Adds ci.yml: unit tests on push and pull request across the Python versions pyproject.toml claims to support (3.8 floor, 3.13 as used by the release build), plus a build job that runs twine check so a broken package is caught before a tag can immortalize it. Adds a test step to publish-to-pypi.yml before the build, so a tag physically cannot ship code that fails its own suite, and a twine check before the release is created. Integration tests (live API key) and django tests are excluded from both gates; neither belongs in a pull-request check. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012RTKj5gZTg1zWmZM8TL2yq --- .github/workflows/ci.yml | 61 +++++++++++++++++++++++++++ .github/workflows/publish-to-pypi.yml | 12 +++++- 2 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml 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: