Skip to content

feat: initial Python SDK setup#1

Merged
justin-layerv merged 8 commits intomainfrom
feat/initial-setup
Mar 11, 2026
Merged

feat: initial Python SDK setup#1
justin-layerv merged 8 commits intomainfrom
feat/initial-setup

Conversation

@justin-layerv
Copy link
Contributor

Summary

  • Extracted Python SDK from layervai/qurl-integrations (apps/sdk-python/)
  • Sync (QURLClient) and async (AsyncQURLClient) API clients
  • LangChain tool integration (QURLToolkit)
  • Full test suite (sync, async, LangChain)
  • CI workflow (Python 3.10, 3.12, 3.13)
  • Release Please + PyPI publish via OIDC
  • Claude code review workflow
  • Dependabot for Actions + pip

Test plan

  • CI passes on all Python versions
  • pip install -e '.[dev]' works
  • ruff check passes
  • pytest tests/ -v passes

🤖 Generated with Claude Code

Extracted from layervai/qurl-integrations (apps/sdk-python/).

Includes:
- Sync and async QURL API clients
- LangChain tool integration
- Full test suite
- CI, Release Please, and PyPI publish workflows
- Claude code review workflow

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@justin-layerv justin-layerv self-assigned this Mar 11, 2026
justin-layerv and others added 7 commits March 11, 2026 15:00
CI installs `.[dev]` which doesn't include langchain-core.
Without it, BaseTool falls back to `object` and tool classes
reject kwargs. Use pytest.importorskip to skip gracefully.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The dev extra doesn't include langchain-core. Install with
`.[dev,langchain]` so the langchain tool tests actually run.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add py.typed marker for PEP 561
- Add README badges (PyPI, CI, Python versions, license)
- Add CONTRIBUTING.md, SECURITY.md
- Add issue and PR templates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
`2 ** (attempt - 1)` returns int, making `min()` return type
ambiguous under mypy strict mode. Explicit annotation resolves it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add AuthenticationError, AuthorizationError, NotFoundError,
  ValidationError, RateLimitError, ServerError subclasses of QURLError
  so callers can catch specific HTTP status codes
- Add extend() convenience method to both sync and async clients
- Add debug logging throughout request lifecycle (enable with
  logging.getLogger("layerv_qurl").setLevel(logging.DEBUG))
- Improve create() docstring to clarify CreateOutput vs QURL
- Export all new error classes from __init__.py
- Add 11 new tests: error subclasses, extend(), AccessPolicy serialization
- Update README with "Why QURL?" section, granular error handling
  examples, debug logging docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace dataclasses.asdict() with fields()/getattr() to avoid
  unnecessary deep copy on every request with AccessPolicy
- Extract build_list_params() to _utils.py, deduplicating list()
  query-param building between sync and async clients
- Narrow except clauses: Exception → PackageNotFoundError for version
  lookup, AttributeError → TypeError in parse_error() fallback
- Add async_client test fixture to reduce boilerplate across 8 tests
- Add sync note to async_client.py module docstring

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- json=body if body is not None else None → json=body (httpx
  treats None as no-body already)
- Replace redundant HTTP call in test_401 with isinstance check

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@justin-layerv justin-layerv merged commit 198966a into main Mar 11, 2026
4 checks passed
@justin-layerv justin-layerv deleted the feat/initial-setup branch March 11, 2026 21:00
@claude
Copy link

claude bot commented Mar 11, 2026

Code Review: feat: initial Python SDK setup

Overall this is a well-structured initial SDK. Clean separation of concerns, good error hierarchy, and solid test coverage. Here's detailed feedback:


Code Quality & Design

Strengths:

  • Excellent shared utility pattern (_utils.py) keeping sync/async clients DRY
  • Clean error hierarchy with specific subclasses (AuthenticationError, RateLimitError, etc.) — makes caller code expressive
  • Good use of dataclasses for types instead of heavier alternatives — appropriate for an SDK
  • TYPE_CHECKING guards used correctly throughout
  • __all__ exports are comprehensive and well-organized
  • Context manager support (__enter__/__aenter__) with proper ownership tracking for injected clients

Suggestions:

  • _utils.py:49 — The _RESOURCE_ID_RE regex (^[a-zA-Z0-9_\-]+$) is quite permissive. Consider documenting what format resource IDs and access tokens actually follow (e.g., r_ prefix, at_ prefix) — even if you don't enforce prefixes now, a comment noting the expected patterns would help future maintainers.
  • types.py:11QURLStatus = Literal["active", "expired", "revoked", "consumed", "frozen"] | str — the | str effectively makes the Literal a no-op at type-checking time since str subsumes all literals. This is intentional for forward compatibility (noted in comment), but worth knowing that mypy won't catch typos like status="actve". An alternative is an Enum with a fallback, but the current approach is reasonable for an SDK that needs to tolerate new API values.

Potential Bugs & Issues

  1. client.py:352-354 / async_client.py:336-339 — When response.status_code < 400 and response.content is empty (but status is not 204), you return (None, None). This means a 200 with an empty body silently returns None data. For most endpoints this is fine, but create() and resolve() would then call parse_create_output(None) which would raise an unhandled TypeError on data["resource_id"]. Consider raising a descriptive error for unexpected empty responses on non-204 endpoints.

  2. _utils.py:246base: float = 0.5 * (2 ** (attempt - 1)) — With attempt=1 (first retry), delay starts at 0.5s, then 1s, 2s, etc. This is good, but the jitter formula random.random() * base * 0.5 only adds jitter (0 to 50% of base), never subtracts. Consider full jitter (random.uniform(0, base)) for better retry distribution across concurrent clients, per AWS's recommended approach.

  3. async_client.py:183list_all is typed as returning AsyncIterator[QURL] but is an async def with yield, making it an AsyncGenerator. The return type annotation works at runtime but AsyncGenerator[QURL, None] would be more precise. Minor point — current annotation is acceptable.


Security

Good practices already in place:

  • validate_id() prevents path traversal in URL construction — important since resource IDs are interpolated into URL paths
  • API key masking in __repr__ prevents accidental logging of secrets
  • Bearer token sent via Authorization header (not query params)
  • Input validation rejects empty/whitespace API keys

Considerations:

  • _utils.py:273-277mask_key shows first 4 + last 4 chars. For keys like lv_live_xxx, the first 4 chars (lv_l) reveal the key type but not the secret — this is fine. Just ensure documentation notes that repr() output is safe for logs.
  • The SDK doesn't pin TLS versions or certificate verification settings, relying on httpx defaults. This is correct — httpx uses the system CA bundle and enforces TLS by default.

Performance

  • default_user_agent() is correctly cached with @lru_cache(maxsize=1) — avoids repeated importlib.metadata lookups
  • Retry delay uses time.sleep (sync) and asyncio.sleep (async) appropriately
  • build_body iterates kwargs once — efficient
  • list_all() pagination is lazy (generator/async generator) — no unnecessary memory allocation

Test Coverage

Comprehensive coverage including:

  • All CRUD operations (create, get, list, delete, update, extend, mint_link, resolve, get_quota)
  • Pagination (list_all with multiple pages)
  • All error subclasses (401, 403, 404, 422, 429, 500)
  • Retry logic (429 retry, retry exhaustion, Retry-After header, 30s cap)
  • Network errors (connection refused, timeout, timeout-is-network-error hierarchy)
  • Context manager lifecycle (owned vs injected client)
  • Body serialization (None omission, AccessPolicy dataclass serialization)
  • Both sync and async clients
  • LangChain tools with mock client

Gaps to consider for follow-up:

  • No test for async retry logic (only sync retry is tested) — the code is mirrored but still worth covering
  • No test for build_list_params with various combinations of None/non-None params
  • No test for _parse_dt edge cases (e.g., already has +00:00, non-Z timezone offsets)
  • No test for the _HAS_LANGCHAIN = False path — what happens when QURLToolkit is instantiated without langchain-core installed
  • No test for create() with expires_at as a datetime object (only string tested via mint_link) — verifies the datetime.isoformat() path in build_body

CI/CD

  • CI matrix covers 3.10, 3.12, 3.13 — good range. Consider adding 3.11 to CI (it's in the release workflow but not the PR CI workflow)
  • mypy only runs on 3.12 — reasonable since strict mode is version-independent
  • Action SHAs are pinned with version comments — excellent security practice
  • Release workflow correctly gates publish on test success
  • OIDC-based PyPI publishing (no stored tokens) — best practice

Minor Nits

  • pyproject.toml:54 — Ruff select list doesn't include S (flake8-bandit). The # noqa: S311 on line 247 of _utils.py suggests it was used at some point. Either add S to the select list or remove the noqa comment.
  • CONTRIBUTING.md / SECURITY.md / issue templates are good to have from day one.

Verdict

This is a solid initial SDK. The architecture is clean, security basics are handled well, and test coverage is thorough. The items above are mostly incremental improvements rather than blockers. Ship it!

🤖 Generated with Claude Code

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.

1 participant