Skip to content

v3 - #609

Draft
razor-x wants to merge 30 commits into
mainfrom
beta
Draft

v3#609
razor-x wants to merge 30 commits into
mainfrom
beta

Conversation

@razor-x

@razor-x razor-x commented Aug 13, 2026

Copy link
Copy Markdown
Member

razor-x and others added 30 commits August 12, 2026 20:22
* build: Migrate from Poetry to uv

Replace Poetry with uv for dependency management, packaging, and
publishing:

- Convert pyproject.toml to PEP 621 project metadata with the uv_build
  backend and PEP 735 dependency groups.
- Replace poetry.lock with uv.lock, keeping the previously locked
  versions of the lint toolchain (black, pylint, mypy, rstcheck,
  docutils) to avoid behavior drift.
- Update the justfile to use uv run, uv build, and uv version.
- Replace the Poetry setup and caching in GitHub Actions with
  astral-sh/setup-uv, and switch publishing to uv publish.
- Install uv instead of Poetry in the devcontainer.
- Update the README development docs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xi2DoWdLxtV7AqqxtbQVqN

* style: Format code with black targeting Python 3.10

With PEP 621 metadata, black now infers its target versions from
requires-python and applies newer formatting (trailing commas after
**kwargs).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xi2DoWdLxtV7AqqxtbQVqN

* build: Resolve uv version from pyproject.toml in CI

Declare required-version under [tool.uv] and let setup-uv discover it
from the config file instead of passing a version through an action
input.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xi2DoWdLxtV7AqqxtbQVqN

---------

Co-authored-by: Claude <noreply@anthropic.com>
The pagination heading called it nextPageCursor, which is how the JavaScript
SDK spells it. In this SDK it is pagination.next_page_cursor, as the example
under that very heading already shows.


Claude-Session: https://claude.ai/code/session_01PpqwDhZmyikGbjmCPqFW2A

Co-authored-by: Claude <noreply@anthropic.com>
…es disabled (#608)

* fix: Export type information from the package root

seam/__init__.py carried a blanket "# type: ignore", which hid the module
from type checkers entirely. Since the package also ships py.typed, every
downstream project that type checks its own code saw the documented import
fail:

    app.py:1: error: Module "seam" has no attribute "Seam"  [attr-defined]

The lint step only pointed mypy at seam/resources, so nothing in this
repository could catch it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8duZRXUbcxu7y5WeoACqM

* fix: Type the decoded response body on the HTTP client

SeamHttpClient overrides request to return the decoded body rather than the
Response that niquests.Session promises, but the verb helpers kept the
inherited Response return type. Indexing what post and get actually return
did not type check, which accounted for every "Value of type Response is not
indexable" error in the generated routes.

Both helpers now name the arguments they forward: Session.request takes
params in the position Session.post gives data, so collecting them into
*args would have rerouted positional calls.

Handling of a response without a status code is no longer a TypeError.
niquests types status_code as optional because a Response exists before it
has one, so the comparison against it is guarded and the narrowed value is
passed to the error path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8duZRXUbcxu7y5WeoACqM

* fix: Correct optional handling across the hand-written modules

Type checking these files for the first time turned up a latent crash:
SeamPaginator.flatten_to_list read pagination.has_next_page without the None
guard its sibling flatten already had, so a page whose response carried no
pagination raised AttributeError instead of ending the loop.

The rest are annotations that did not match their callers. request_id is
read from a response header that may be absent, so the HTTP exceptions take
Optional[str]. SeamActionAttemptTimeoutError documented its timeout as str
while every caller passes a float. poll_until_ready declared its timeout and
polling interval Optional though neither is ever None, which made arithmetic
on them unsound. SeamPaginator.params used an implicit Optional default, and
its response hook is handed either side of the exchange, so the request side
is turned away before the pagination is read.

A failed action attempt is assumed to carry an error; reading through it
unguarded would raise AttributeError over the actual failure.

Two invariants are beyond what the checker can follow and are ignored in
place: the auth option guards return True only once the values they check
are set, and Seam borrows Routes.__init__ to attach the route namespaces
even though the two are siblings under AbstractRoutes rather than parent and
child.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8duZRXUbcxu7y5WeoACqM

* feat: Declare resource fields the API may omit as Optional

Generated dataclasses declared every field as present and non-null, but
from_dict reads each one with dict.get, so a field the payload omits arrives
as None behind a type that rules None out. That mismatch was the whole of
the ~1720 arg-type errors the lint step disabled the error code to hide.

Fields the blueprint marks optional or nullable are now generated as
Optional. So are the ones a merged shape cannot guarantee: a property only
some variants of a discriminated union carry is absent whenever the
dataclass holds a variant that omits it.

Nested objects are Optional regardless of what the schema says, because the
schema is not a reliable guide to when they arrive. An action attempt
documents both error and result as required, yet a pending one carries
neither, so from_dict keeps its None fallback and the field admits it. No
field gains a default, so the constructors are unchanged.

from_dict now takes Any. The payload is decoded JSON and every value read
out of it is untyped, so keeping that at the boundary avoids casting each
read while the fields carry the real types.

Route methods annotate json_payload as Dict[str, Any]. Left bare, its type
was inferred from whichever parameter was written first, and every later
parameter of a different type was reported as an incompatible assignment.

BREAKING CHANGE: Resource fields that the API may omit or send as null are
now typed Optional. Code that reads them under a type checker may need a
None check. Runtime behavior is unchanged: those fields could already be
None.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8duZRXUbcxu7y5WeoACqM

* build: Type check the whole project

The mypy step checked seam/resources with two error codes disabled. That
covered 34 of the 94 files in the package and left every hand-written module
unchecked, including the package root whose blanket ignore broke type
checking for every downstream user.

It now checks seam and test with nothing disabled. The import-not-found
disable is dropped outright: it suppressed nothing, since pointing mypy at
the whole package resolves the imports it was added for.

The paginator tests narrow the pagination they assert on, the way
test_paginator_first_page already did, and the test that passes None as a
cursor on purpose says so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8duZRXUbcxu7y5WeoACqM

---------

Co-authored-by: Claude <noreply@anthropic.com>
* feat!: Replace niquests with httpx as the HTTP client

The niquests dependency urllib3-future installs into the urllib3
package namespace, which conflicts with packages depending on genuine
urllib3 in customer environments and cannot be mitigated from the SDK.
httpx is already in the dependency tree via svix and its dependency
chain does not touch urllib3.

BREAKING CHANGE: The niquests_options option is now httpx_options and
is passed to the underlying httpx.Client. The retries option now takes
a seam.Retry instead of a urllib3.util.Retry; the fields mirror the
urllib3 names. Seam.client is now an httpx.Client subclass, and
request errors are raised as httpx exceptions, e.g. httpx.
HTTPStatusError instead of niquests.HTTPError and httpx.
TimeoutException instead of niquests.exceptions.Timeout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QxwdLpWpFhxSn7mZ2Fb6p1

* feat: Use httpx-retries instead of a hand-rolled retry transport

The retries option now takes an httpx_retries.Retry, re-exported as
seam.Retry. Its fields mirror the urllib3.util.Retry names. Unlike the
previous urllib3 default, the default policy does not retry connection
errors for POST requests; pass allowed_methods=["POST"] to opt in to
retrying Seam API requests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QxwdLpWpFhxSn7mZ2Fb6p1

* feat: Apply configured retry policy to API requests regardless of method

Which HTTP methods the Seam API uses is not part of the SDK's public
API, so a configured retry policy must not require consumers to name
them in allowed_methods.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QxwdLpWpFhxSn7mZ2Fb6p1

* chore: Remove unused dataclasses-json dependency

Resource classes define their own from_dict on standard library
dataclasses and nothing imports dataclasses_json.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QxwdLpWpFhxSn7mZ2Fb6p1

* fix: Relax httpx version constraints

The SDK is a library, so keep dependency constraints as permissive as
possible. The httpx floor matches svix, and the httpx-retries floor is
the release that introduced Retry.copy_with.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QxwdLpWpFhxSn7mZ2Fb6p1

* fix: Do not override the configured retry policy allowed_methods

The retries option should not silently modify the policy it is given.
Since httpx-retries does not treat POST as retryable, the retry policy
currently has no effect on Seam API requests; the affected tests are
marked xfail. A follow-up PR will apply the retry policy to API
requests without exposing the HTTP method in the SDK's public API.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QxwdLpWpFhxSn7mZ2Fb6p1

* style: Import httpx names directly except for the client

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QxwdLpWpFhxSn7mZ2Fb6p1

* style: Reword fixme comment flagged by pylint

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QxwdLpWpFhxSn7mZ2Fb6p1

---------

Co-authored-by: Claude <noreply@anthropic.com>
The required parameter guard generated one any() argument per parameter,
so every call raised TypeError instead of validating: any() takes a single
iterable, not a variadic list of conditions. Wrap the conditions in a list
and regenerate.

Also fix the fallout that this masked in the rest of the suite:

- Type the put and patch client helpers as returning the decoded body,
  matching get, post and delete, now that generated routes use them.
- Set the route metadata attributes through Any, since a function does not
  declare them.
- Allow TODO comments, which the test suite uses to track routes that
  cannot use the generated method yet.
- Assert the GET request line and empty body in the default headers test,
  and drive the invalid input test through a route that still takes a JSON
  payload, now that /devices/get is a GET route.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EFYHA1UHvARfcMBkUnWgGs
The guard restated every parameter, which ran to seventeen lines on the
widest routes. The payload gains a key exactly when its parameter is not
None, so checking it after it is built says the same thing in one line
and drops 446 lines from the generated routes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EFYHA1UHvARfcMBkUnWgGs
semantic-release computes semver versions, but uv normalizes prerelease
versions to PEP 440 when cutting them, so the tag it produces (v3.0.0b1)
is not valid semver. semantic-release silently skips tags it cannot
parse, so every run on a prerelease branch reported the channel's first
prerelease as the next version and tried to cut a version that had
already been published.

Mirror each PEP 440 prerelease tag onto its semver equivalent before
running semantic-release. The mirrored tags are local to the workflow
run, so the PEP 440 tags stay the only real ones and continue to match
the published package version.

Recording the channel a prerelease went out on, which semantic-release
requires before it will treat a prerelease tag as released, stays in the
Version workflow where the release is cut. It now keys the note to the
PEP 440 tag that actually gets created, rather than to a semver tag name
that never exists, and derives the channel from the normalized version
so a release cut by hand is recorded the same way. Notes attach to
commits rather than tags, so the mirrored tag picks the channel up.


Claude-Session: https://claude.ai/code/session_01WKRv7ht6XA9Sm8brKctEva

Co-authored-by: Claude <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.

3 participants