Skip to content

feat: typed error hierarchy for HTTP status codes#19

Merged
jackparnell merged 1 commit intomainfrom
feature/typed-errors
Apr 9, 2026
Merged

feat: typed error hierarchy for HTTP status codes#19
jackparnell merged 1 commit intomainfrom
feature/typed-errors

Conversation

@ColonistOne
Copy link
Copy Markdown
Collaborator

Summary

Adds specific exception subclasses so callers can react to failure modes without inspecting status codes. All subclass ColonyAPIError so existing except ColonyAPIError code keeps working unchanged — fully backward compatible.

New exception HTTP Cause
ColonyAuthError 401, 403 Invalid key, expired, forbidden
ColonyNotFoundError 404 Resource doesn't exist
ColonyConflictError 409 Already voted, name taken, already following
ColonyValidationError 400, 422 Bad payload, missing fields
ColonyRateLimitError 429 Rate limited (after SDK retries exhausted) — exposes .retry_after
ColonyServerError 5xx Colony API internal failure
ColonyNetworkError DNS / connection / timeout (status=0)

Why

Downstream packages (langchain-colony, crewai-colony) currently string-match HTTP codes to format hints and decide whether to retry. That logic belongs in the SDK, not in every consumer. With this change, crewai-colony's _STATUS_HINTS table can be deleted and replaced with except ColonyRateLimitError etc.

Other changes

  • Status hints in error messages"not found — the resource doesn't exist or has been deleted", "rate limited — slow down and retry after the backoff window", etc. So logs and LLMs don't need to consult docs.
  • ColonyRateLimitError.retry_after — parsed from the Retry-After header. Callers can implement higher-level backoff on top of the SDK's built-in retries.
  • Sync URLError and async httpx.HTTPError both wrap as ColonyNetworkError(status=0).
  • _build_api_error dispatches via _error_class_for_status, shared by sync + async + register paths.

Example

from colony_sdk import ColonyClient, ColonyConflictError, ColonyRateLimitError

client = ColonyClient("col_...")
try:
    client.vote_post("post-id")
except ColonyConflictError:
    print("Already voted")
except ColonyRateLimitError as e:
    print(f"Slow down — server says retry after {e.retry_after}s")

Test plan

  • 13 new sync tests (TestTypedErrors) covering each status → subclass mapping, retry_after propagation, network error wrapping, and the unknown-status fallback to base ColonyAPIError
  • 7 new async tests covering the same in tests/test_async_client.py::TestErrors
  • All existing tests still pass — backward compat verified
  • pytest --cov reports 100% (448/448 statements)
  • ruff check / ruff format --check / mypy src/ all clean
  • CI green on Python 3.10 / 3.11 / 3.12 / 3.13
  • Codecov upload still succeeds

Follow-up (next PRs, not this one)

Once released, crewai-colony and langchain-colony can:

  1. Delete their _STATUS_HINTS tables and rely on the SDK's status hints
  2. Replace if e.status == 429 blocks with except ColonyRateLimitError
  3. Use ColonyRateLimitError.retry_after for higher-level backoff coordination

Adds specific exception subclasses so callers can react to failure modes
without inspecting status codes. All subclass ColonyAPIError so existing
`except ColonyAPIError` code keeps working unchanged.

  ColonyAuthError       — 401, 403  (invalid key / forbidden)
  ColonyNotFoundError   — 404
  ColonyConflictError   — 409       (already voted, name taken, etc.)
  ColonyValidationError — 400, 422  (bad payload)
  ColonyRateLimitError  — 429       (with .retry_after attribute)
  ColonyServerError     — 5xx
  ColonyNetworkError    — DNS / connection / timeout (status=0)

Why: downstream packages (langchain-colony, crewai-colony) currently
string-match HTTP codes to format hints and decide whether to retry.
That logic belongs in the SDK, not in every consumer. With this change,
crewai-colony's _STATUS_HINTS table can be deleted and replaced with
`except ColonyRateLimitError` etc.

Other changes:
- Status hints in error messages: "not found — the resource doesn't
  exist or has been deleted", "rate limited — slow down...", etc.
- ColonyRateLimitError exposes `.retry_after` parsed from the
  Retry-After header so callers can implement higher-level backoff
  on top of the SDK's built-in retries.
- Sync URLError and async httpx.HTTPError both wrap as
  ColonyNetworkError with status=0.
- _build_api_error dispatches to the right subclass via
  _error_class_for_status, shared by sync + async + register paths.

Tests: 13 sync + 7 async tests for the typed hierarchy. Coverage stays
at 100% (448 / 448 statements).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 9, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@jackparnell jackparnell merged commit da970bb into main Apr 9, 2026
7 checks passed
ColonistOne added a commit that referenced this pull request Apr 9, 2026
Two changes that ship together so v1.5.0 can be the first release cut
via the new automation:

1. Release workflow at .github/workflows/release.yml — triggered on
   `v*` tag push. Stages:
     - test:           runs ruff, mypy, pytest before anything else
     - build:          builds wheel + sdist, refuses to proceed if
                       the tag version doesn't match pyproject.toml
     - publish:        uploads to PyPI via OIDC trusted publishing
                       (no API token stored anywhere — short-lived
                       token minted by PyPI from the GitHub Actions
                       OIDC identity at publish time)
     - github-release: extracts the matching CHANGELOG section and
                       creates a GitHub Release with the wheel + sdist
                       attached

2. Version bump 1.4.0 → 1.5.0 in pyproject.toml and __init__.py.

3. CHANGELOG: consolidated the 1.5.0 section into a clean, ordered
   summary covering everything that's landed since 1.4.0:
     - AsyncColonyClient (PR #18)
     - Typed error hierarchy (PR #19)
     - RetryConfig + 5xx default retry (PR #20)
     - py.typed + verify_webhook + Dependabot (PR #21)
     - Pagination iterators (PR #23)
     - Coverage + Codecov (PR #17)
     - This release automation

Coverage at 100% (514/514 statements). 215 tests passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ColonistOne added a commit that referenced this pull request Apr 9, 2026
Two changes that ship together so v1.5.0 can be the first release cut
via the new automation:

1. Release workflow at .github/workflows/release.yml — triggered on
   `v*` tag push. Stages:
     - test:           runs ruff, mypy, pytest before anything else
     - build:          builds wheel + sdist, refuses to proceed if
                       the tag version doesn't match pyproject.toml
     - publish:        uploads to PyPI via OIDC trusted publishing
                       (no API token stored anywhere — short-lived
                       token minted by PyPI from the GitHub Actions
                       OIDC identity at publish time)
     - github-release: extracts the matching CHANGELOG section and
                       creates a GitHub Release with the wheel + sdist
                       attached

2. Version bump 1.4.0 → 1.5.0 in pyproject.toml and __init__.py.

3. CHANGELOG: consolidated the 1.5.0 section into a clean, ordered
   summary covering everything that's landed since 1.4.0:
     - AsyncColonyClient (PR #18)
     - Typed error hierarchy (PR #19)
     - RetryConfig + 5xx default retry (PR #20)
     - py.typed + verify_webhook + Dependabot (PR #21)
     - Pagination iterators (PR #23)
     - Coverage + Codecov (PR #17)
     - This release automation

Coverage at 100% (514/514 statements). 215 tests passing.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants