feat(exceptions): expose response headers on RoeAPIException (1.0.801)#38
Conversation
…y-After parity RoeAPIException now carries response headers so callers can read Retry-After on 429s symmetrically with the raw-httpx path. Today the SDK discards headers entirely in translate_response, forcing downstream consumers (e.g. roe-mcp) to hardcode retry_after=1.0 because there's no machine-readable signal available — even though roe-main correctly emits the Retry-After header on its DRF-throttled responses. Changes: - src/roe/exceptions.py: add headers: Mapping[str, str] | None = None to RoeAPIException.__init__; store as a plain dict copy. - src/roe/exceptions.py: translate_response() pulls getattr(response, "headers", None) and threads it into the raised exception. Works for both httpx.Response (Headers) and roe._generated.types.Response (MutableMapping[str, str]). - tests/unit/test_translate_response.py: 6 new tests pinning the contract — Retry-After (numeric + HTTP-date), missing header, general header preservation on 404/500, and stub-without-headers safety. - pyproject.toml: version bumped 1.0.80 -> 1.0.801 (skip past the natural patch sequence so this doesn't collide with the next auto-bump from roe-main). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Greptile SummaryThis PR exposes response headers on
Confidence Score: 5/5Safe to merge — the change is additive, purely extends the exception with an optional attribute, and is guarded by a no-crash The headers are normalised at store time, the No files require special attention. Important Files Changed
Reviews (2): Last reviewed commit: "Revert "chore(version): revert pyproject..." | Re-trigger Greptile |
Addresses Greptile P1 + P2 on #38: * RoeAPIException.__init__ now normalises header keys to lowercase at store time so `.get("retry-after")` works on both the httpx path (which already lowercases on iteration) and the generated-client MutableMapping path (which preserves original "Retry-After" casing). Without this, callers using the generated client would silently miss Retry-After even though the server sent it. * New test test_generated_response_with_uppercased_headers_normalised_to_lowercase pins the contract by passing a stub with original-cased headers and asserting lowercase lookup succeeds. * Rename test_429_no_retry_after_header_yields_empty_map -> test_429_no_retry_after_header_when_absent. httpx always synthesises content-length etc., so the dict is not actually empty — only the absence of retry-after is meaningful. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ease) The earlier 1.0.801 bump (chosen to "skip past the natural patch sequence") had an unintended consequence: 1.0.801 > 1.0.81 in semver ordering. Once published, pip/npm/go-proxy always resolve to the highest semver, so future weekly releases (1.0.81, 1.0.82, ...) from roe-main's release-1-0-X branches would publish successfully but be permanently uninstallable — users would stay pinned to 1.0.801 forever even after subsequent releases. 1.0.81 is the natural next patch (current PyPI latest is 1.0.80) and keeps the weekly release pipeline working unchanged. Verified 1.0.81 is unclaimed on PyPI, npm, and the Go proxy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Walks back the manual 1.0.801 -> 1.0.81 bump on this branch and leaves the version at 1.0.80 (unchanged from main). Rationale: - roe-python is in active use; cleanest release path is to let roe-main's next weekly release-1-0-81 branch fire prepare-target for this repo, which will write 1.0.81 to pyproject.toml as part of the regular codegen-refresh PR. Our headers feature ships bundled into that natural 1.0.81 release with zero coordination. - Leaves tag-on-release-merge.yml a no-op on this PR's merge (pyproject.toml is unchanged from main, so no v* tag is cut, publish.yml doesn't fire). The exceptions.py + test changes sit on main until the weekly picks them up. - Preserves parity with roe-typescript (also reverted to 1.0.80). roe-go takes a different path — see roe-ai/roe-golang#14 commit message for why. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…n weekly"
Restoring the 1.0.801 bump. Earlier flip-flop was my mistake — user
intent all along is:
Python + TS at 1.0.801 (manually published immediately on merge)
Go at 1.0.81 (manually published immediately on merge)
Next roe-main weekly (release-1-0-81-*) will:
- Python/TS: write 1.0.81 to version files (a downgrade from
1.0.801). tag-on-release-merge fires v1.0.81 -> publishes 1.0.81
on PyPI / npm. Consumers with loose `>=` pins keep resolving
1.0.801 (PEP 440 / semver picks highest); consumers with exact
`==1.0.81` pins get the natural weekly bundle.
- Go: writes 1.0.81 (same value as already on main). No diff in
VERSION, no tag, no new publish — a clean "dud" weekly for Go.
This restores parity at 1.0.801 across Python + TS now and accepts
the orphan-release behaviour for the patch range 1.0.81..1.0.800
(those weekly releases will publish but stay un-installed by loose
pins until weekly version sequence catches up past 1.0.801).
This reverts commit 1aa8965a92099ef9ec24f4af9f5d2a0eed2c0d9c.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@greptile please re-review — latest commit includes Greptile-flagged fix-ups (lowercase-key contract / checked type assertions) plus the version-strategy decision (Python+TS at 1.0.801, Go at 1.0.81; see PR description for rationale) |
Adds a brief snippet right after the typed-exception section showing how to read Retry-After (and other response headers) off the exception, paired with a status_code==429 gate since Python's exception hierarchy doesn't have a dedicated RateLimitError class (429 raises bare RoeAPIException). Documents the lowercase-key contract callers need to know. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
RoeAPIExceptionnow carries the responseheadersso callers can readRetry-Afteron 429s symmetrically with the raw-httpx path. Today the SDK discards headers entirely intranslate_response, forcing downstream consumers (e.g.roe-mcp) to hardcoderetry_after=1.0because there's no machine-readable signal available — even thoughroe-maincorrectly emits the standardRetry-Afterheader on its DRF-throttled responses.Changes
src/roe/exceptions.py:RoeAPIException.__init__gains aheaders: Mapping[str, str] | None = Nonekwarg; stored as a lowercase-normaliseddictso callers can always look up byexc.headers["retry-after"](httpx Headers iterates lowercased; generated-clientMutableMappingpreserves original casing — the normalisation makes the contract uniform).translate_response()threadsgetattr(response, "headers", None)into the raised exception.tests/unit/test_translate_response.py: 7 new tests —Retry-After(numeric + HTTP-date), missing-header, general header preservation on 404/500, stub-without-headers safety, and the uppercased-headers normalisation contract.pyproject.toml: version1.0.80→1.0.801. Heads up: this intentionally skips past the natural patch sequence to ship the headers fix immediately on PR merge (auto-tagsv1.0.801viatag-on-release-merge.yml, publish.yml → PyPI). The next roe-main weekly (release-1-0-81-*) will write1.0.81to pyproject.toml; that's a downgrade in semver ordering (1.0.801 > 1.0.81), sotag-on-release-merge.ymlfiresv1.0.81→ publishes 1.0.81 to PyPI. Consumers with loose>=pins resolve to the highest published version (1.0.801) and stay on this PR's code until weekly patch sequence catches up past 1.0.801; consumers with exact==1.0.81pins get the natural weekly bundle.Parity
Companion PRs:
roe-ai/roe-typescript#13(also 1.0.801),roe-ai/roe-golang#14(1.0.81 — Go takes a different path because its weekly bump path is one of the lower-touched ones; see that PR for rationale).Test plan
uv run pytest tests/unit/test_translate_response.py -v→ 23/23 pass (16 existing + 7 new)uv run ruff check src/roe/exceptions.py tests/unit/test_translate_response.pyclean