feat!: align types and client with latest API spec#8
Conversation
BREAKING CHANGE: QURL dataclass fields changed (removed one_time_use, max_sessions, qurl_link, access_policy; added tags, custom_domain). Create endpoint moved to POST /v1/qurls. create() parameter description renamed to label. update() parameter access_policy removed. New batch_create() method added. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PR Review: feat!: align types and client with latest API specOverall this is a well-structured breaking change that cleanly aligns the SDK with the new API spec. The sync/async parity is maintained, tests are comprehensive, and the commit message properly signals the breaking change. A few items to consider: Bugs / Issues
Design / API
Test Coverage
Minor / Style
SummaryThe main actionable items are: remove or wire up the dead 🤖 Generated with Claude Code |
…removal - Fix nested dataclass serialization bug in build_body(): AIAgentPolicy inside AccessPolicy was not recursively converted to a dict, causing JSON encoding failures. Extracted _serialize_value() helper for recursive handling. - Remove dead _parse_access_policy() function and its unused imports. - Add test for mint_link with nested AIAgentPolicy in AccessPolicy. - Add test for create() with custom_domain parameter. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PR Review: feat!: align types and client with latest API specOverall AssessmentClean, well-structured PR that aligns the SDK with the latest OpenAPI spec. The sync/async parity is maintained, tests are comprehensive, and the breaking changes are clearly documented. A few items worth addressing: Issues1.
2.
3.
if isinstance(v, list):
return [_serialize_value(item) for item in v]4. Using 5. Date filter params are
Positive Highlights
Minor / Nits
SummaryThe PR is in good shape. The main actionable item is #3 (list serialization in |
- Add recursive list handling in _serialize_value for future-proofing nested dataclass lists - Simplify build_list_params by replacing 9 repetitive if/assign blocks with a single dict comprehension - Fix outdated status values in ListQURLsTool description (removed expired/consumed, kept active/revoked) - Add skipif marker to langchain tests when langchain-core is not installed, preventing false failures in dev environments Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PR Review: feat!: align types and client with latest API specOverall AssessmentWell-structured breaking change that cleanly aligns the SDK with the updated API. The sync/async clients stay in lockstep, types are updated consistently, and test coverage is thorough (65 tests). A few items worth addressing: Code Quality & DesignPositives:
Suggestions:
Potential Bugs & Issues
Performance
Security
Test Coverage
Minor Nits
VerdictApprove with minor suggestions. The breaking changes are well-documented, code is clean, and test coverage is strong. The |
Code ReviewGood work on the API spec alignment. Test coverage is strong (65 tests, sync/async parity) and the recursive Medium Severity1. 2. if not items:
raise ValueError("items must not be empty")
if len(items) > 100:
raise ValueError("batch_create supports at most 100 items")3. 4. Date filter params use Low Severity (non-blocking)5. 6. 7. Cross-SDK inconsistency on 8. PATCH retry inconsistency — Python retries 5xx on PATCH, TypeScript doesn't. Should agree on retry policy for mutating requests. 9. LangChain tools lack async 10. Positive
|
The async list_all() was missing the Args docstring that the sync version has, violating the "keep both in sync" contract noted in the file header. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PR Review: feat!: align types and client with latest API specOverall AssessmentClean, well-structured PR that aligns the SDK with the latest API spec. The changes are consistent across sync/async clients, types are properly updated, and test coverage is thorough. A few items worth addressing: Code Quality & Best PracticesStrengths:
Suggestions:
Potential Bugs or Issues
Performance Considerations
Security ConcernsNo security issues identified. The Test CoverageStrengths:
Gaps:
Minor Nits
SummarySolid PR. The breaking changes are well-documented in the PR description, types align cleanly, and test coverage is comprehensive. The main actionable items are:
Everything else is minor or cosmetic. 🤖 Generated with Claude Code |
PR Review: feat!: align types and client with latest API specOverall this is a well-structured breaking change with good test coverage (65 tests). The recursive serialization fix, Bug:
|
1. _parse_access_policy missing ai_agent_policy parsing (bug): API responses with nested ai_agent_policy were silently dropped. Now parses block_all, deny_categories, allow_categories. 2. create() missing one_time_use, max_sessions, access_policy params: Python users had to create then mint_link to set policies. Now create() accepts all policy params directly, matching TS SDK. Applied to both sync and async clients. 3. batch_create() no client-side 1-100 validation: Added ValueError for empty list and >100 items. Gives users a descriptive local error instead of a cryptic API 400. 4. Date filter params str instead of datetime | str: list() and list_all() date filters (created_after, created_before, expires_before, expires_after) now accept datetime objects, consistent with create()'s expires_at. build_list_params calls .isoformat() for datetime values. 5. builtins.list annotation unnecessary: Both clients have from __future__ import annotations, so list[str] works directly. Removed import builtins from TYPE_CHECKING blocks. 6. batch_create items type simplified: builtins.list[dict] → list[dict] (same reason as #5). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PR Review: feat!: align types and client with latest API specOverall this is a well-structured PR that cleanly aligns the SDK with the updated API spec. The sync/async parity is maintained, tests are thorough, and the breaking changes are clearly documented. Here is detailed feedback: Code Quality and Best PracticesStrengths:
Suggestions:
Potential Bugs / Issues
Performance Considerations
Security Concerns
Test CoverageStrengths:
Gaps to consider:
Breaking Change DocumentationThe PR description clearly lists all breaking changes, which is great. Since this will go through Release Please, make sure the commit message retains the Verdict: Looks good with minor suggestions. The core changes are solid and well-tested. Items 2, 3, and 5 above are the most actionable. Nice work keeping sync/async in lockstep. |
…ethod mypy resolves `list` inside the class body to the `list()` method, not the builtin type. `from __future__ import annotations` makes annotations strings but mypy still evaluates them in the class scope. The `builtins.list` workaround is required for Python 3.10-3.12 mypy compat. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PR Review: feat!: align types and client with latest API specOverall this is a well-structured breaking change that cleanly aligns the SDK with the updated API. The sync/async clients stay in lockstep, tests are thorough, and the new types are well-modeled. A few observations below. Code Quality
Potential Issues
Security
Performance
Test Coverage
Minor Nits
VerdictLooks good. The breaking changes are well-documented in the PR description, the implementation is consistent across sync/async, and test coverage is solid. The items above are suggestions, not blockers. |
1. Move AIAgentPolicy import to top-level in _utils.py — was a lazy import inside _parse_access_policy, unnecessary since types.py is already imported at module scope 2. CreateOutput.qurl_id: "" → str | None = None — callers can now distinguish "API didn't return it" from "API returned empty string" 3. batch_create items serialized through build_body() — datetime objects or dataclasses in batch item dicts now get properly serialized instead of failing at JSON encoding time 4. Add test_batch_create_empty_raises — covers ValueError for empty list 5. Add test_batch_create_over_100_raises — covers ValueError for >100 6. Add test_create_with_access_policy — verifies create() serializes AccessPolicy with nested AIAgentPolicy (was only tested via mint_link) 7. Add test_mint_link_nested_serialization_e2e — end-to-end test for _serialize_value recursion with geo_denylist + AIAgentPolicy, verifies None fields are omitted Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PR Review: feat!: align types and client with latest API specOverall this is a well-structured PR that cleanly aligns the SDK with the updated API spec. The sync/async parity is maintained, tests are comprehensive, and the breaking changes are clearly documented. A few observations: Positives
Issues & Suggestions1. 2. 3. 4. 5. 6. 7. Security
Performance
VerdictLooks good to merge. The breaking changes are well-justified by the API spec alignment. Consider the 🤖 Generated with Claude Code |
…st batch datetime 1. CreateOutput docstring: explain resource_id (container) vs qurl_id (specific access token, q_ prefix) — multiple QURLs share one resource_id 2. active_qurls_percent: add inline comment noting float→float|None breaking change (callers must None-check before arithmetic) 3. _serialize_value: recurse into plain dicts — datetime values nested in batch_create item dicts now serialize correctly via .isoformat() 4. Add test_batch_create_serializes_datetime_in_items — verifies a datetime expires_at in a batch item dict is serialized to ISO string Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PR Review: feat!: align types and client with latest API specOverall this is a well-structured breaking change that cleanly aligns the SDK with the latest API spec. The sync/async parity is maintained, tests are thorough, and the new Issues & Suggestions1. In 2.
3.
4.
5. The intermediate 6. Missing Not a bug, but Positive Highlights
Security
VerdictApprove — this is ready to merge. The items above are minor and can be addressed in follow-ups if desired. |
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Code Review — PR #8:
|
| Change | Impact |
|---|---|
create() endpoint /v1/qurl → /v1/qurls |
All existing create() calls work unchanged (URL is internal) |
create(description=) → create(label=) |
Callers using named param must update |
update() removes access_policy param, adds tags |
Callers passing access_policy to update() will get TypeError |
active_qurls_percent: float → float | None |
Callers doing arithmetic without None-check will get TypeError |
QURLStatus literals narrowed to active | revoked |
No runtime impact (| str catches all) — only IDE autocomplete changes |
QURL dataclass: fields removed/added |
Callers accessing removed fields (one_time_use, max_sessions, qurl_link, access_policy) will get AttributeError |
The active_qurls_percent breaking change has a helpful inline comment (types.py:151). Consider also noting it in the changelog or migration guide if one exists.
Summary
This is a solid PR. The code is clean, well-tested, and the sync/async implementations are properly mirrored. The main gap is test coverage for the inbound AIAgentPolicy parsing path (#1 above). The rest are minor suggestions. Good to merge once #1 is addressed or accepted as a follow-up.
# Conflicts: # src/layerv_qurl/_utils.py
Code Review — PR #8:
|
Brings the Python SDK into parity with every improvement made to
qurl-typescript and qurl-mcp during the recent review and seam-audit
rounds. Cross-references the qurl-service OpenAPI spec
(qurl/api/openapi.yaml) and the Go handler code.
### Critical — real bug
* parse_error detail fallback. RFC 7807 leaves `detail` optional and
the qurl Error schema only requires type/title/status/code.
Previously the parser used `err.get("detail", "")`, producing
"Forbidden (403): " when the API omitted detail. Now falls back
`detail -> message -> title -> HTTP {status}`. QURLError also
defaults detail to title in its constructor so Exception.args is
never empty-string padded.
### RFC 7807 structured fields
* QURLError now carries `type` and `instance` (the problem-type URI
and occurrence URI). Both are optional per the spec; the SDK was
silently dropping them before.
* parse_error extracts both from the envelope.
### Backward compatibility
* Legacy `{error: {code, message}}` envelope supported in the
fallback chain. If the API ever regresses to the pre-RFC-7807
shape, the SDK degrades gracefully instead of showing empty detail.
### Type narrowing
* QURLStatus clarified as resource-only ("active" | "revoked" | str).
* New TokenStatus for AccessToken ("active" | "consumed" | "expired"
| "revoked" | str) — per QurlSummary.status in the spec, tokens
have a wider enum than resources.
* AccessToken.status now uses TokenStatus.
* New QuotaPlan ("free" | "growth" | "enterprise" | str); Quota.plan
uses it. Uses the (Literal | str) pattern so the API can add new
plans without a breaking SDK change.
### Spec-derived input validation
New validate_create_input / validate_update_input / validate_mint_input
helpers in _utils.py enforcing the constraints documented on each
request schema in openapi.yaml:
- target_url: maxLength 2048
- label: maxLength 500 (on create + mint_link)
- description: maxLength 500 (on update)
- custom_domain: maxLength 253 (on create)
- max_sessions: 0-1000 integer (on create + mint_link)
- tags: max 10, each 1-50 chars, regex ^[a-zA-Z0-9][a-zA-Z0-9 _-]*$
batch_create runs validate_create_input on every item and attributes
errors by index (`items[N]: ...`) so bulk mistakes fail fast.
### Mutual-exclusion pre-flight checks
* update: rejects both extend_by + expires_at
* update: rejects empty input (at least one field required)
* mint_link: rejects both expires_in + expires_at
Extend() inherits the update() checks via delegation.
### delete() r_ prefix enforcement
Per the OpenAPI spec DELETE /v1/qurls/:id description: "Requires a
resource ID (r_ prefix). To revoke a single token, use DELETE
/v1/resources/:id/qurls/:qurl_id". New require_resource_id_prefix
helper raises ValueError client-side for q_ IDs with a clear message
pointing at the token-scoped endpoint.
### batch_create HTTP 400 passthrough
The API returns a populated BatchCreateOutput body on HTTP 400 (all
items rejected) — see qurl/internal/api/handlers/server.go:1126.
Added `allow_statuses` to _raw_request and _request, and batch_create
whitelists 400 so the per-item errors are surfaced instead of being
swallowed by the generic raise-on-error path. Non-400 errors (401,
403, 429, 5xx) still raise the appropriate QURLError subclass.
Matches the qurl-typescript and qurl-mcp implementations.
### create() parameter cleanup
Dropped the spurious `expires_at` kwarg from both sync and async
create(). CreateQurlRequest in openapi.yaml has only `expires_in` —
the previous signature let callers pass a field the API doesn't
accept.
### Dual-prefix documentation
get/update/extend/mint_link docstrings now document that both r_
(resource) and q_ (QURL display) IDs are accepted; the API resolves
q_ IDs to the parent resource automatically. delete() stays narrow
(r_ only) matching its client-side enforcement.
### parse_create_output: normalize empty qurl_id to None
Empty-string qurl_id from a response (mock or legacy shape) is now
normalized to None so callers can use `if result.qurl_id:` as a
presence check instead of having "" be silently truthy-false.
### _serialize_value: stop stripping None from nested dicts
Previously the dict branch filtered out None values, which would
silently drop explicit nulls callers send to clear nested fields
(e.g. `{"access_policy": {"ai_agent_policy": null}}`). Top-level
None-stripping still happens in build_body since that serves the
"drop unset kwargs" case. Nested None is now preserved; dataclass
fields still skip None (dataclasses distinguish unset vs explicit).
### Misc
* build_list_params type annotation tightened — the `int | None`
arm was misordered in the old union.
* test_update_with_tags corrected to use spec-compliant tags
(previous test used `team:engineering` with a colon that the
^[a-zA-Z0-9][a-zA-Z0-9 _-]*$ regex rejects).
* test_batch_create_empty_raises regex updated for the new error
message ("requires at least 1 item").
* test_create_sends_correct_body now covers one_time_use,
max_sessions, and session_duration alongside label (reviewer #9
gap note).
### Tests (74 -> 101)
Twenty-seven new tests covering:
- Create rejection: target_url > 2048, label > 500,
custom_domain > 253, max_sessions > 1000, max_sessions < 0
- Create boundaries: max_sessions 0 and 1000 both accepted
- Update rejection: description > 500, > 10 tags, tag > 50 chars,
tag regex pattern mismatch, empty input, mutual-exclusion
- Update success: empty tags array clears all tags
- mint_link rejection: label > 500, max_sessions > 1000,
mutual-exclusion
- delete q_ prefix rejection
- batch_create per-item validation with index attribution
- batch_create missing target_url surfaces index
- Async batch_create empty/>100 (reviewer #7 symmetry gap)
- batch_create HTTP 400 passthrough with per-item errors
- batch_create still raises on 401 (passthrough is surgical)
- Error type/instance surfacing
- Error detail fallback when RFC 7807 detail missing
- Legacy error.message fallback
- parse_create_output empty qurl_id normalization
BREAKING CHANGE: `active_qurls_percent` on `Quota.usage` is now
`float | None` instead of `float` with a `0.0` default; callers
doing arithmetic must None-check. Also `create()` no longer accepts
an `expires_at` kwarg — that field wasn't in `CreateQurlRequest`.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Code Review: feat!: align types and client with latest API specOverall AssessmentThis is a substantial, well-structured PR that aligns the Python SDK with the current OpenAPI spec. The changes are thorough — types, endpoints, validation, error handling, and tests have all been updated in lockstep. The sync and async clients are kept in near-perfect parity, and the test count nearly doubled (65 → 101). The iterative commit history shows multiple review rounds already addressed, which is reflected in the code quality. Strengths
Issues1.
|
Two related fixes to ensure mypy errors always fail CI: 1. Move mypy into its own `type-check` job. The previous setup ran `mypy src/` as a step inside the `test` job but guarded it on `matrix.python-version == '3.12'` — if the matrix is ever restructured and 3.12 is dropped, the mypy gate silently vanishes. A dedicated job pins Python 3.10 (matching `python_version` in pyproject.toml, which is the strictness contract the SDK promises consumers) and always runs. The `notify` job now depends on both `test` and `type-check`, so a mypy failure fails the overall build. Also added `fail-fast: false` to the test matrix so a single failing Python version doesn't cancel sibling legs — full visibility into which versions broke. 2. Add `langchain-core` to the `[dev]` extra. The optional langchain integration was only installed via `[langchain]`, so developers running `pip install -e ".[dev]"` locally (without the `langchain` extra) got spurious mypy errors about missing modules and unused type-ignore comments in `src/layerv_qurl/langchain.py`. CI already installs `[dev,langchain]`, but local envs drifted. Pulling langchain-core into `[dev]` aligns local dev with CI so mypy runs clean in both places. Verified: `mypy src/` reports 0 issues across all 7 source files; `pytest tests/` now runs all 108 tests (the 7 previously-skipped langchain tests now execute since langchain-core is present). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Port of all qurl-typescript #14 seam fixes + mypy gate hardeningBrings the Python SDK into full parity with the improvements landed on qurl-typescript and qurl-mcp during the recent review/seam-audit rounds, cross-referenced against
🔴 Critical — real bug
RFC 7807 structured fields
Backward compatibility
Type narrowing
Spec-derived input validationNew
Mutual-exclusion pre-flight checks
|
PR Review: feat!: align types and client with latest API specThis is a thorough and well-structured PR that aligns the Python SDK with the current OpenAPI spec. The scope is large (+1937/-243 across 11 files, 13 commits) but the changes are cohesive and well-documented. Here's a detailed review: Strengths
Potential Issues1.
Consider adding a minimal structural check — e.g., verify 2.
Minor, but consider storing the raw value separately or documenting that 3.
4.
This is well-handled — just confirming it's intentional. Code Quality Observations5. Sync/async client duplication
6.
7. Tag regex is hardcoded from the spec
Test Coverage Gaps8. No test for Tests cover serialization of 9. No test for
10. No async test for Sync tests cover Security11. The call order in No SSRF concerns — PerformanceNo concerns. The double iteration in Minor Nits
VerdictThis is a solid, well-tested PR. The breaking changes are justified by API alignment and clearly documented. The main actionable items are:
Everything else is enhancement-level or style. 🤖 Generated with Claude Code |
Ten items from the latest PR review. Reviewer marked #1 as blocking and #8 + #10 as strongly recommended; the rest picked up on the "don't be lazy" directive. 1. batch_create 400 shape guard (reviewer's blocking item). _utils.py gains _validate_batch_create_shape() which verifies that a passthrough 400 body has the expected BatchCreateOutput envelope (succeeded/failed are ints, results is a list, each entry carries a boolean success discriminant). If the API ever returns 400 with a different body (plain error envelope, proxy error, malformed JSON), batch_create now raises a QURLError with status=0 and code="unexpected_response" instead of silently returning (succeeded=0, failed=0, results=[]). Defense in depth matches the qurl-typescript fix. Wired into both client.py and async_client.py. 2. QURLError docstring now documents that .detail is guaranteed non-empty at the instance level. The constructor falls back to title when the API omits detail per RFC 7807, so consumers shouldn't inspect .detail to detect "was it absent?" — use .code / .status / .type instead. 3. QURLError docstring now explains why .type shadows Python's built-in. Intentional for RFC 7807 field-name parity and consistency with qurl-typescript/qurl-mcp; the shadowing only matters inside QURLError method definitions, not external code. 4. target_url scheme check in validate_create_input. Reviewer's observation that the length check didn't catch the most common mistake (forgetting http(s)://). New _ALLOWED_URL_SCHEMES tuple with a startswith() guard; the server still owns SSRF validation. 5. Sync/async parity comment added to client.py's module docstring (async_client.py already had one). Calls out the contract so a future change can't silently update one client without the other. 6. Tag regex comment expanded with a note about keeping it in lockstep with the openapi.yaml schema, and why. 7. Quota.plan empty-string default now documented — it only exists so the dataclass can be instantiated with no arguments for tests/ bootstrap paths; the real /v1/quota endpoint always returns a populated plan. Tests (108 -> 116): - test_get_response_parses_nested_ai_agent_policy (reviewer gap #8) — mocks a GET response with a fully-populated ai_agent_policy inside a token's access_policy and asserts the deserialization round-trip. - test_list_serializes_datetime_filter_params_as_isoformat (reviewer gap #9) — passes an actual datetime to client.list(created_after=) and asserts the URL-encoded ISO 8601 output. - test_async_delete_rejects_q_prefix_client_side (reviewer gap #10) — async symmetry for the existing sync delete() q_ prefix test. - test_create_rejects_target_url_without_scheme — the new URL scheme check catches bare "example.com". - test_create_rejects_target_url_with_unsupported_scheme — rejects ftp:// etc. - test_create_accepts_http_and_https_schemes — both valid schemes pass. - test_batch_create_rejects_unexpected_400_body_shape — defense-in- depth for the new _validate_batch_create_shape. - test_batch_create_rejects_400_body_with_non_boolean_success — the per-entry discriminant check. Also updated three existing tests that depended on the pre-URL-check create() accepting invalid URLs: - test_422_raises_validation_error - test_400_raises_validation_error - test_batch_create_partial_failure Each now uses a syntactically valid URL that passes client-side validation; the mocked API response payload is unchanged, so the tests still exercise the API error-parsing paths they intended to. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Latest review addressed — commit c7c9cebTen items from the review. Picked up all the actionable ones including the three gaps the reviewer explicitly called out for before-merge attention. 🔴 Blocking fixed1. 🟡 Strongly recommended fixed8. 10. Async 🟢 Remaining action items2. 3. 6. 5. Sync/async parity comment — 7. Tag regex spec pointer — 9. Datetime in list filter params test — Nit: ⏭️ Deferred (reviewer explicitly non-actionable)
Three existing tests updated for the new URL scheme check
Validation
|
PR Review: Align SDK types with current OpenAPI specOverall assessment: This is a well-executed, thorough PR. The API alignment work is solid, the sync/async parity is maintained carefully, and the test coverage is excellent. The new Strengths
Suggestions1. Comment verbosity (style, non-blocking)Many inline comments are significantly longer than the code they annotate. For example:
These are individually reasonable, but in aggregate they make the code harder to scan. The commit messages, PR body, and tests already capture the "why" well. Consider trimming comments to the essential insight — the tests themselves serve as the durable specification. 2.
|
…n clarity Reviewer-flagged consolidation of comment verbosity that grew across multiple review rounds as different reviewers asked "why?" — each answer accumulated inline, and in aggregate the comments became harder to scan than the code they annotated. Three specific examples trimmed: - `parse_create_output` qurl_id normalization: 12 lines → 4. Keeps the essential insight (asymmetric with label: `""` is never a meaningful identifier but IS a meaningful "cleared" value for user-facing metadata) and drops the historical context about which reviewer asked which question. - `_validate_batch_create_shape._fail`: 23 lines → 11. Keeps the three load-bearing facts (DEBUG log carries structural hints not body content; `ValidationError` subclass for catch-by-class; `status=0` is the SDK convention for client-detected failures) and drops the cross-SDK comparison and the reviewer-round history. - `Quota.plan` docstring: 17 lines → 6. Keeps the forward-compat type mechanism (`QuotaPlan` = `Literal | str`) and the sentinel rationale (only hit by tests/bootstrap, since `/v1/quota` always returns a populated plan). Drops the paragraph-long justification for "unknown" vs "" that was accumulated across 5 rounds of ping-pong between reviewers. Net diff: -51 / +30 lines across these three spots, with zero semantic change. The PR comments, commit messages, and tests already capture the full "why" for anyone who needs deeper context; inline comments now serve current readers rather than review-history archaeology. Also added: - Inline comment at the `batch_create` `allow_statuses=(400,)` call site in both clients explaining why 207 Multi-Status doesn't need to be whitelisted: it flows through the normal `status_code < 400` success path automatically. Only the total-failure 400 needs the opt-in because the generic error path would otherwise swallow the populated `BatchCreateOutput` body. Prevents a future maintainer from wondering "why isn't 207 in the whitelist too?" - PR description migration notes expanded for `update()` `access_policy` removal. The spec's `UpdateQurlRequest` schema intentionally makes access policy immutable on existing resources; the migration note now spells out the two supported alternatives (new resource via `create()`, or per-token override via `mint_link(resource_id, access_policy=...)`) with code examples. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Latest review addressed — commit f2004f83 fix-now items (1 substantive trim + 2 doc additions), 4 reviewer-explicit/spec-verified defers. Fixed1. Item 1 + Issue A — Trim accumulated comment verbosity.
Net diff: -51 / +30 lines across the three spots with zero semantic change. The PR comments, commit messages, and tests already capture the full "why" for anyone who needs deeper context; inline comments now serve current readers rather than review-history archaeology. 2. Item 3 —
3. Item 4 — Deferred — reviewer-explicit or spec-verified
Pre-existing concern tracked as issue (not fixed in this PR)Flagged last round but worth referencing: #19 tracks the POST network-error retry duplicate risk. The retry loop catches fetch-level errors with a blanket Validation |
PR Review: Align SDK types and endpoints with current OpenAPI specOverall this is a high-quality, well-structured PR. The sync/async parity is maintained carefully, the test coverage is thorough (~1400 new test lines), and the defensive programming choices (batch shape guard, bool-is-int guards, Bug:
|
…tests
- **Real bug (medium severity):** `_raw_request` in both sync and
async clients called `response.json()` on the success path
(including the `allow_statuses` passthrough) without exception
handling. If a proxy/CDN returned a whitelisted 400 with non-JSON
content — a plain HTML error page, a truncated response, a
gateway's own plaintext error — the raw `json.JSONDecodeError`
propagated to the caller, bypassing both the batch shape guard
AND the normal `parse_error` path (which already has its own
try/except around JSON parsing).
Fix: wrap the success-path `response.json()` in try/except that
falls through to `parse_error(response)` on decode failure.
`parse_error` already handles non-JSON error bodies gracefully
(echoes `response.text` as the detail when JSON parse fails).
The `raise ... from None` suppresses the JSONDecodeError chain
from the user-facing traceback since it's noise — the QURLError's
own detail captures the relevant failure mode.
Added two regression tests (sync + async) that mock a 400 with
`<html>` body and assert the SDK raises a QURLError with
`status == 400`, not a raw JSONDecodeError.
- Added `@pytest.mark.asyncio` + new mirror tests for all 4 sync
batch shape-guard regression tests:
* test_async_batch_create_rejects_unexpected_400_body_shape
* test_async_batch_create_rejects_400_body_with_non_boolean_success
* test_async_batch_create_rejects_400_body_with_bool_counts
* test_async_batch_create_rejects_counts_arithmetic_mismatch
Closes the sync/async parity gap the reviewer flagged.
- Added `test_require_max_sessions_in_range_rejects_bool` as a
direct unit test of the bool-is-subclass-of-int rejection in
`_require_max_sessions_in_range`. The rejection was exercised
indirectly via batch bool-counts tests, but a direct test makes
the intent explicit and would trip immediately if a future
refactor simplifies away the `isinstance(value, bool)` guard.
Also sanity-checks that plain int inputs (0, 500, 1000) and
None still pass through.
- Added a "**Cannot change access_policy**" paragraph to the
`update()` docstrings in both sync and async clients, pointing
callers at `create()` (new resource with new policy) and
`mint_link()` (per-token override) as the two supported
alternatives. The PR description's migration notes already
cover this, but the docstring note helps callers reading the
code directly rather than the PR.
Deferred from this round:
* Comment-to-code ratio (3rd raising). Previous round trimmed
51 lines across three specific spots the previous reviewer
called out. This round's targets overlap with what was already
trimmed or contain load-bearing "why" context. Holding the line.
* `_parse_dt` malformed-string handling — reviewer explicit
"worth a note... not blocking." SDK only parses trusted API
responses.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Latest review addressed — commit 73ada8a4 fix-now items (1 real medium-severity bug, 1 doc note, 2 test gaps), 2 deferrals. Fixed1. Bug (medium) — JSON error handling on Fix: wrap the success-path Added two regression tests that mock a 400 with
2. Design question —
The PR description's migration notes already covered this in detail; the docstring note helps callers reading the code directly rather than the PR body. 3. Test gap — Async mirrors of 4 batch shape-guard regression tests.
Each mirrors the corresponding sync test exactly. Without these, an async-specific regression (e.g. a refactor that diverged the two code paths) could slip past CI. 4. Test gap — Direct Deferred
Validation |
PR Review: Align SDK with current OpenAPI specOverall assessment: This is a high-quality, well-structured PR. The changes are thorough, well-documented with inline comments explaining rationale, and the test coverage is excellent. A few observations below, organized by category. Code Quality & Best PracticesStrengths:
Minor observations:
Potential Issues
Performance Considerations
Security
Test CoverageThis is the strongest aspect of the PR. The test additions are thorough and well-organized:
One gap to note:
CI Changes
SummaryThis is a well-executed API alignment PR with strong test coverage and careful attention to edge cases, security, and backward compatibility documentation. The breaking changes are clearly called out in the PR description (and acceptable given the pre-1.0 status). No blocking issues found. 🤖 Generated with Claude Code |
Short round on PR #8 — 1 test addition + 1 PR body update. The reviewer's overall verdict was "no blocking issues found." Added: - test_update_rejects_access_policy_kwarg (sync) — locks in that `access_policy` is immutable on update() per the OpenAPI spec's UpdateQurlRequest schema. Currently the signature itself enforces this (TypeError on unknown kwargs), but this test is an explicit invariant guard: a future refactor adding **kwargs to update() for forward-compat pass-through would silently accept access_policy again, breaking the spec-enforced contract. Callers needing policy changes should create a new resource or use mint_link() per-token overrides. - test_async_update_rejects_access_policy_kwarg — async mirror. Tests: 142 → 144 passing. PR description updated separately (not in this commit): - Added Quota.plan default sentinel change to the breaking-changes summary (`""` → `"unknown"` — affects `if not quota.plan` checks). Deferred: none. The rest of the review was either strengths, "intentional per the comment", or explicit non-issues.
Latest review addressed — commit f6d3967Short round: 1 test addition + 1 PR description update. Reviewer's overall verdict was "no blocking issues found" — most observations were strengths or explicit non-issues. Fixed1. Regression tests for Reviewer flagged this as a "coverage gap" but hedged "arguably covered by the Python signature itself… not a real gap — just an observation." I still picked it up. The test isn't a tautology if framed as an invariant guard rather than a language-behavior assertion: a future refactor adding def test_update_rejects_access_policy_kwarg(client: QURLClient) -> None:
with pytest.raises(TypeError, match="access_policy"):
client.update("r_abc", access_policy={"ai_agent_policy": "allow"})
async def test_async_update_rejects_access_policy_kwarg(...) -> None:
with pytest.raises(TypeError, match="access_policy"):
await async_client.update("r_abc", access_policy={"ai_agent_policy": "allow"})2. PR description — Reviewer flagged this as worth a changelog note:
The breaking-changes summary already documented Validation
DeferredNone. All other review observations were either:
Nothing meaningful to defer this round. |
PR Review: feat!: align types and client with latest API specOverall this is a well-structured, thorough PR that cleanly aligns the SDK with the latest OpenAPI spec. The test coverage is impressive (121 tests with sync/async parity), the defensive validation is thoughtful, and the migration notes in the description are excellent. A few observations below — mostly minor. Code Quality & ArchitectureStrengths:
Observations:
Potential Bugs / Issues
Security
Performance
Test CoverageTest coverage is thorough. Highlights:
Minor gaps (non-blocking):
CI ChangesThe dedicated One minor note: the SummaryThis is a solid, well-tested PR. The breaking changes are clearly documented, the defensive validation is thorough, and the test coverage provides confidence in the sync/async parity. The issues flagged above are minor and none are merge-blockers. Nice work. 🤖 Generated with Claude Code |
6 fix-now items (2 defensive bug fixes, 1 signature improvement,
3 test coverage gaps), 4 deferrals (2 filed as issues).
Fixed:
- _parse_access_policy: add isinstance(ap, dict) guard against non-dict
ai_agent_policy values. Without this, a bare string/bool from the API
would raise AttributeError on .get("block_all"). Consistent with the
defensive posture in _validate_batch_create_shape.
- QURLError.detail: change default from str="" to str|None=None with
`detail if detail is not None else title`. Distinguishes "not provided"
(falls back to title) from "explicitly empty" (stored as-is). The old
`detail or title` coercion silently converted `detail=""` to title,
which surprised callers who explicitly passed empty string.
- build_list_params: client-side limit validation (1-100) per OpenAPI
spec (GET /v1/qurls → limit: integer, minimum: 1, maximum: 100,
default: 20). Rejects 0, negative, floats, and bool (bool is subclass
of int — must be explicitly rejected). Python-parity follow-up from
qurl-typescript PR #14 round 11 (commit 3d471fa).
Tests: 144 → 157 passing (+13 new regression guards).
- batch_create 207 Multi-Status routes through success path (sync+async)
- create(expires_at=...) TypeError invariant guard (sync+async)
- _parse_access_policy null/missing ai_agent_policy yields None
- _parse_access_policy non-dict ai_agent_policy is silently ignored
- list() rejects limit: 0 / 101 / negative / float / bool
- list() accepts limit at boundaries (1 and 100)
- list() omitted limit produces no query param
Deferred:
- sync/async duplication refactor — filed as #20
- test_client.py split — filed as #21
- QURLError.type builtin shadow — docstring already acknowledges
- http:// URL scheme warning — caller's responsibility per reviewer
Latest review addressed — commit ecb79136 fix-now items + 13 new regression tests, 4 deferrals (2 filed as tracking issues). Fixed1. Real defensive gap. If the API returned 2. Changed from
Note: the reviewer's suggested Verified no internal callers pass 3. Locks in that 207 flows through the success path (gated by 4. Parallel to the 5. The happy-path test covers a populated AIAgentPolicy, but the 6. Per OpenAPI spec ( RebuttalCI action-pin inconsistency — reviewer noted "the
No inconsistency to fix. Tests (144 → 157, +13)
Validation: ruff clean, mypy clean (7 files), 157 tests passing. Deferred (with reasoning + tracking issues)
|
PR Review: Align SDK types and methods with current OpenAPI specOverall AssessmentThis is a well-executed, thorough update that aligns the Python SDK with the current API spec. The code quality is high, with careful attention to defensive programming, security, and sync/async parity. The test suite is comprehensive (~1850 new test lines). A few items need attention before merging. Bug: Three async tests missing
|
Short round on PR #8. Reviewer's verdict: "Strong PR. Fix the three missing @pytest.mark.asyncio decorators and this is ready to merge." Fixed: - _serialize_value datetime integration test: the unit test covers the function directly, but there was no integration test passing a datetime object through the full client method pipeline. Added test_update_serializes_datetime_expires_at which passes datetime(2026, 4, 1, tzinfo=timezone.utc) to update(expires_at=...) and asserts the wire body contains "2026-04-01T00:00:00+00:00". Closes the build_body → _serialize_value → .isoformat() loop. Tests: 157 → 158 passing. Rebutted: - Missing @pytest.mark.asyncio on 3 async tests: the reviewer's main finding is factually incorrect. pyproject.toml:68 sets asyncio_mode = "auto", which automatically marks all async def test_* functions — no explicit decorator needed. Verified by running the 3 specific tests: "3 passed, 154 deselected." The async parity coverage IS being exercised. No deferrals — all other items were either positive observations or explicitly "not a blocker."
Latest review addressed — commit 818919bShort round. Reviewer's verdict: "Strong PR. Fix the three missing Rebuttal:
|
PR Review: feat!: align types and client with latest API specOverall this is a very well-structured PR. The code quality is high, the design decisions are thoroughly documented, and the test coverage is exemplary. Below are my findings organized by category. Strengths
Issues & Suggestions1. Missing
|
## Summary Adds an explicit `timeout-minutes: 10` to the `claude-review` job in `.github/workflows/claude-code-review.yml`. Without this, the job inherits GitHub Actions' default 6-hour timeout, which means a hung `anthropics/claude-code-action@v1` run (observed on layervai/qurl-python#8) consumes runner minutes and leaves a stale `in_progress` status check on the PR for hours. ## Tailored timeout The `10`-minute value is derived from this repo's observed `claude-review` run times across the last 30 successful runs: - **p50:** 130s - **p95:** 3.2min - **max:** 5.7min - **mean:** 138s Formula: `max(8, ceil(p95_minutes * 2) + 2)` — 2× p95 + 2-minute safety margin, floored at 8 minutes. This gives each repo a bound that's generous relative to its own historical upper-bound while still catching future hangs within a reasonable window. ## Test plan - [x] Single-line diff (only the new `timeout-minutes` line is added) - [x] Base64-roundtrip validated - [ ] Next `claude-review` run on this repo should complete normally within the timeout ## Context Companion PRs are being opened across all 10 layervai repos that use this workflow, each with its own tailored timeout based on the repo's p95. Motivated by the stuck run at https://github.com/layervai/qurl-python/actions/runs/24288312714 which burned ~18 minutes before manual cancellation. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three parallel review agents (code reuse, quality, efficiency) reviewed the full PR #8 diff. Reuse and efficiency: clean. Quality found actionable comment density and one dead-state issue. Fixed: - Remove dead `_user_agent` attribute from both clients — stored in __init__ but never read after being baked into `_base_headers`. Inlined the `user_agent or default_user_agent()` expression directly into the headers dict. - Trim `_raw_request` docstring from 31 lines to 7 in both clients — the hypothetical scenarios and precedence-order details are over-documentation for a private method with one non-default caller. - Trim `_validate_batch_create_shape._fail` comments from 14 lines to 2 — was more comment than code. - Trim `parse_quota` plan-default comment from 6 lines to 1. - Trim `build_list_params` type-annotation comment (3 lines → 0). - Trim spec-validation section banner (3 lines → 1). - Remove `cast` explanation comments from both clients (4 lines each). Skipped (not worth the churn): - Sync/async duplication — tracked as #20, structural limitation. - Parameter sprawl on list()/list_all() — at threshold but not over. - HTTP method Literal type annotation — low risk for a private method. - Double getattr in _serialize_value — negligible cost on max 7 fields. Net: -78 lines across 3 files. 158 tests passing (unchanged).
## Summary - Remove dead `_user_agent` attribute from both sync/async clients - Trim ~78 lines of WHAT-not-WHY comments across `_utils.py`, `client.py`, `async_client.py` - No functional changes — 158 tests passing (unchanged) Stacks on top of PR #8 (`feat/align-api-v2`). Result of running `/simplify` with three parallel review agents (code reuse, quality, efficiency). ## What was trimmed | Area | Before | After | Lines saved | |------|--------|-------|-------------| | `_raw_request` docstring (×2 clients) | 31 lines each | 7 lines each | -48 | | `_validate_batch_create_shape._fail` comments | 14 lines | 2 lines | -12 | | `parse_quota` plan-default comment | 6 lines | 1 line | -5 | | `build_list_params` type comment | 3 lines | 0 | -3 | | Spec-validation section banner | 3 lines | 1 line | -2 | | `cast` explanation (×2 clients) | 4 lines each | 0 | -8 | | Dead `_user_agent` attribute (×2 clients) | 1 line each | 0 (inlined) | 0 net | ## What was skipped - **Sync/async duplication** — tracked as #20, structural limitation - **Parameter sprawl on `list()`/`list_all()`** — at threshold (9 params) but not over - **HTTP method `Literal` type annotation** — low risk for a private method, would need both clients updated - **Double `getattr` in `_serialize_value`** — negligible cost on max 7-field dataclasses 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Bounds the `claude-review` job's wall-clock time so a hung `anthropics/claude-code-action@v1` run (observed on layervai/qurl-python#8) can't consume a runner for hours before GitHub Actions' default 6-hour job timeout kicks in. The `10`-minute value is tailored from this repo's observed `claude-review` run durations across the last 30 successful runs, using `max(8, ceil(p95_minutes * 2) + 2)` — 2x p95 + 2-minute safety margin, floored at 8 minutes. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Summary Adds an explicit `timeout-minutes: 14` to the `claude-review` job in `.github/workflows/claude-code-review.yml`. Without this, the job inherits GitHub Actions' default 6-hour timeout, which means a hung `anthropics/claude-code-action@v1` run (observed on #8) consumes runner minutes and leaves a stale `in_progress` status check on the PR for hours. ## Tailored timeout The `14`-minute value is derived from this repo's observed `claude-review` run times across the last 30 successful runs: - **p50:** 135s - **p95:** 5.12min - **max:** 19.07min - **mean:** 173s Formula: `max(8, ceil(p95_minutes * 2) + 2)` — 2× p95 + 2-minute safety margin, floored at 8 minutes. This gives each repo a bound that's generous relative to its own historical upper-bound while still catching future hangs within a reasonable window. ## Test plan - [x] Single-line diff (only the new `timeout-minutes` line is added) - [x] Base64-roundtrip validated - [ ] Next `claude-review` run on this repo should complete normally within the timeout ## Context Companion PRs are being opened across all 10 layervai repos that use this workflow, each with its own tailored timeout based on the repo's p95. Motivated by the stuck run at https://github.com/layervai/qurl-python/actions/runs/24288312714 which burned ~18 minutes before manual cancellation. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Summary Adds an explicit `timeout-minutes: 18` to the `claude-review` job in `.github/workflows/claude-code-review.yml`. Without this, the job inherits GitHub Actions' default 6-hour timeout, which means a hung `anthropics/claude-code-action@v1` run (observed on layervai/qurl-python#8) consumes runner minutes and leaves a stale `in_progress` status check on the PR for hours. ## Tailored timeout The `18`-minute value is derived from this repo's observed `claude-review` run times across the last 30 successful runs: - **p50:** 129s - **p95:** 7.85min - **max:** 9min - **mean:** 166s Formula: `max(8, ceil(p95_minutes * 2) + 2)` — 2× p95 + 2-minute safety margin, floored at 8 minutes. This gives each repo a bound that's generous relative to its own historical upper-bound while still catching future hangs within a reasonable window. ## Test plan - [x] Single-line diff (only the new `timeout-minutes` line is added) - [x] Base64-roundtrip validated - [ ] Next `claude-review` run on this repo should complete normally within the timeout ## Context Companion PRs are being opened across all 10 layervai repos that use this workflow, each with its own tailored timeout based on the repo's p95. Motivated by the stuck run at https://github.com/layervai/qurl-python/actions/runs/24288312714 which burned ~18 minutes before manual cancellation. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
/v1/qurlto/v1/qurlscreate()parameterdescriptionrenamed tolabel; addedsession_durationQURLdataclass: removedone_time_use,max_sessions,qurl_link,access_policy; addedtags,custom_domainupdate(): removedaccess_policyparam; addedtagsmint_link()expanded with full per-QURL settingsbatch_create()method forPOST /v1/qurls/batchAccessPolicynow includesai_agent_policyQuota.rate_limitsaddsmax_expiry_seconds;active_qurls_percentnow nullableQuota.plandefault sentinel changed from""to"unknown"(affects any check likeif not quota.planorquota.plan == "")labelrenameMigration notes
No live consumers exist yet (the SDK is still pre-1.0 / pre-first-release), so this is a heads-up rather than an upgrade-on-fire migration guide. For the first consumers coming online after this lands:
create()—description→label, no absolute-expiryPer the OpenAPI spec (
CreateQurlRequestatqurl/api/openapi.yaml:2300), the create endpoint only accepts relativeexpires_in, not absoluteexpires_at. To set an absolute expiry at creation time, use the minimum practical initial expiry on create and immediately update to the target absolute time:Note: there's an unavoidable ~1-second window between
create()andupdate()where the QURL is live with the initial 1-minute expiry. For most use cases this is negligible; if you need hard-synchronous absolute expiry guarantees, gate access behind your own authorization layer until after the update completes.update()—access_policyremoved (immutable after create)Access policy is now set only at create time. The OpenAPI spec's
UpdateQurlRequestschema acceptsextend_by,expires_at,tags, anddescriptionbut notaccess_policy— the field is intentionally immutable on an existing resource. If you were passingaccess_policytoupdate(), you have two migration paths:create()with the new policy and migrate callers away from the old resource. There's no in-place update.mint_link(resource_id, access_policy=...)to mint a new access token on an existing resource with a policy override scoped to that token — the resource's base policy stays the same, but the specific link you hand out carries the overridden policy.Quota.active_qurls_percentis nowfloat | NonePreviously defaulted to
0.0when unlimited; now returnsNone. Callers doing arithmetic on this field will get aTypeErrorat runtime if they don't None-check:Test plan
ruff checkpasses cleanmypy src/passes with no errors (dedicated CI gate, not matrix-dependent)pytest tests/ -qall 121 tests pass🤖 Generated with Claude Code