Skip to content

Commit c826e9f

Browse files
razor-xclaudeseambot
authored
Add migration guide for seam v2 to v3 upgrade (#615)
* docs: add v2 to v3 migration guide Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GK2tFMid8Mus3fsRRwgKBY * ci: Format code * docs: drop removed endpoint params section and add v1 to v2 guide Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GK2tFMid8Mus3fsRRwgKBY --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Seam Bot <seambot@getseam.com>
1 parent d8d070e commit c826e9f

1 file changed

Lines changed: 263 additions & 0 deletions

File tree

MIGRATION.md

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
# Migrating from seam v2 to v3
2+
3+
This guide covers upgrading from `seam` v2.x to v3 of the [Seam Python SDK](https://github.com/seamapi/python).
4+
5+
Version 3 replaces the underlying HTTP library, adds client-side validation and explicit null support, and regenerates the API surface against the latest Seam API. Most application code — authentication, method names, resource models, action attempts, and pagination — works unchanged. The breaking changes are concentrated in client configuration and error handling.
6+
7+
## Installation
8+
9+
While v3 is in prerelease, install it explicitly:
10+
11+
```sh
12+
pip install --pre seam
13+
# or pin a specific beta
14+
pip install 'seam==3.0.0b6'
15+
```
16+
17+
## Summary of breaking changes
18+
19+
| Change | Affects you if... |
20+
| ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
21+
| [Python 3.11+ required](#python-311-or-later-is-required) | You run Python 3.10 |
22+
| [httpx replaces niquests](#httpx-replaces-niquests) | You pass `niquests_options`, catch `niquests` exceptions, or touch `seam.client` directly |
23+
| [`retries` takes an `httpx_retries.Retry`](#retry-configuration-uses-httpx-retries) | You pass a custom `retries` option |
24+
| [Endpoints validate parameters client-side](#client-side-parameter-validation) | You call endpoints with no parameters, or rely on the server's 400 response |
25+
| [`lts_version` removed](#lts_version-is-removed) | You read `Seam.lts_version` or the `seam-lts-version` header |
26+
| [Preferred HTTP methods and URL search params](#endpoints-use-preferred-http-methods) | You inspect traffic in a proxy, mock server, or firewall rules |
27+
28+
## Python 3.11 or later is required
29+
30+
Version 2 supported Python 3.10. Version 3 requires Python >= 3.11 and is tested on Python 3.11 through 3.14.
31+
32+
## httpx replaces niquests
33+
34+
The SDK's HTTP layer is now [httpx](https://www.python-httpx.org/) instead of [niquests](https://niquests.readthedocs.io/). This surfaces in three places.
35+
36+
### The `niquests_options` option is renamed to `httpx_options`
37+
38+
Options are now passed to the underlying `httpx.Client`, so both the option name and its contents change. For example, connection pool limits:
39+
40+
```python
41+
# v2
42+
seam = Seam(
43+
api_key="your-api-key",
44+
niquests_options={"pool_connections": 20, "pool_maxsize": 25},
45+
)
46+
47+
# v3
48+
from httpx import Limits
49+
50+
seam = Seam(
51+
api_key="your-api-key",
52+
httpx_options={
53+
"limits": Limits(max_connections=25, max_keepalive_connections=20),
54+
},
55+
)
56+
```
57+
58+
This applies to `Seam()`, `Seam.from_api_key()`, `Seam.from_personal_access_token()`, and `SeamWithoutWorkspace`.
59+
60+
### Transport-level exceptions are httpx exceptions
61+
62+
Requests that time out now raise `httpx.TimeoutException` instead of `niquests.exceptions.Timeout`, and connection failures raise httpx transport errors (`httpx.ConnectError`, etc.) instead of niquests/urllib3 ones.
63+
64+
```python
65+
# v2
66+
import niquests
67+
68+
try:
69+
seam.devices.list()
70+
except niquests.exceptions.Timeout:
71+
...
72+
73+
# v3
74+
import httpx
75+
76+
try:
77+
seam.devices.list()
78+
except httpx.TimeoutException:
79+
...
80+
```
81+
82+
Seam API errors are unchanged: `SeamHttpApiError`, `SeamHttpInvalidInputError`, and `SeamHttpUnauthorizedError` are raised exactly as in v2.
83+
84+
### `seam.client` is an httpx.Client
85+
86+
If you access the client directly, it is now an `httpx.Client` subclass rather than a niquests `Session`. Notably, response hooks are registered via `event_hooks` instead of `hooks`.
87+
88+
## Retry configuration uses httpx-retries
89+
90+
The `retries` option now takes a `Retry` object from [httpx-retries](https://will-ockmore.github.io/httpx-retries/) instead of `urllib3.util.retry.Retry`. The class is re-exported from `seam` for convenience:
91+
92+
```python
93+
# v2
94+
from urllib3.util.retry import Retry
95+
96+
seam = Seam(api_key="your-api-key", retries=Retry(total=3))
97+
98+
# v3
99+
from seam import Seam, Retry
100+
101+
seam = Seam(
102+
api_key="your-api-key",
103+
retries=Retry(total=3, backoff_factor=0.5, status_forcelist=[503]),
104+
)
105+
```
106+
107+
The default retry policy is now explicit and documented. Out of the box, the SDK makes up to three attempts: the initial request and two retries. Retries are limited to `GET`, `HEAD`, `OPTIONS`, `PUT`, and `DELETE` requests that fail because of a transport error, timeout, HTTP 429 response, or HTTP 5xx response. `POST` and `PATCH` requests are never retried. Retries use exponential backoff with jitter, and a `Retry-After` header is honored instead of the calculated backoff.
108+
109+
In v2, the default was urllib3's implicit `Retry()` (connection-level retries only, with no retries on HTTP status codes such as 429 or 5xx). If you depended on requests never being retried on 429/5xx, pass an explicit policy, e.g. `retries=Retry(total=0)`.
110+
111+
## Client-side parameter validation
112+
113+
Endpoints that require at least one parameter now raise `ValueError` locally instead of sending the request and letting the server reject it:
114+
115+
```python
116+
# v2: raises SeamHttpInvalidInputError after a round trip to the server
117+
# v3: raises ValueError("At least one parameter is required for /locks/get")
118+
seam.locks.get()
119+
```
120+
121+
`create_paginator` is validated the same way. It raises `ValueError` when given a non-paginated endpoint, and when given an endpoint that requires parameters without any:
122+
123+
```python
124+
# v3: raises ValueError - /devices/get is not paginated
125+
seam.create_paginator(seam.devices.get)
126+
```
127+
128+
If you catch `SeamHttpInvalidInputError` around calls that could be sent with no parameters, also handle `ValueError` (or fix the call site).
129+
130+
## `lts_version` is removed
131+
132+
The `Seam.lts_version` / `SeamWithoutWorkspace.lts_version` attribute and the `seam-lts-version` request header no longer exist. There is no replacement; use the package version instead:
133+
134+
```python
135+
from importlib.metadata import version
136+
137+
version("seam")
138+
```
139+
140+
## Endpoints use preferred HTTP methods
141+
142+
In v2, every endpoint was called with `POST` and a JSON body. In v3, endpoints use the HTTP method the Seam API prefers:
143+
144+
- Read endpoints (`get`, `list`, and friends) use `GET`, with parameters sent as URL search params serialized per [Seam's URL search params standard](https://github.com/seamapi/url-search-params-serializer).
145+
- Update endpoints use `PATCH` or `PUT`.
146+
- Delete endpoints use `DELETE`.
147+
- Create and action endpoints (`create`, `lock_door`, etc.) remain `POST`.
148+
149+
Method signatures, arguments, and return values are unchanged — this only matters if something outside your code observes the HTTP traffic: proxy or firewall rules that allowlist methods, request logging, or test mocks registered against `POST` routes. Note the interaction with the new retry defaults: because reads are now `GET`, they are retried by default, which they were not in v2 (as `POST`).
150+
151+
If you call the Seam API with your own HTTP client, the serializer used for `GET` params is exported:
152+
153+
```python
154+
import httpx
155+
from seam import serialize_url_search_params
156+
157+
httpx.get(
158+
"https://connect.getseam.com/devices/list",
159+
params=serialize_url_search_params({"device_ids": ["device1", "device2"]}),
160+
headers={"Authorization": "Bearer your-api-key"},
161+
)
162+
```
163+
164+
## New in v3
165+
166+
These are additions, not breaking changes, but they are worth adopting while you migrate.
167+
168+
### Explicit null with `NULL`
169+
170+
The Seam API distinguishes an omitted parameter from one explicitly set to null: in an update request, an omitted parameter leaves the current value unchanged, while a null parameter unsets it. Version 2 had no way to send null — `None` always meant "omit". Version 3 keeps that behavior for `None` and adds a `NULL` sentinel for sending an explicit null:
171+
172+
```python
173+
from seam import NULL, Seam
174+
175+
seam = Seam()
176+
177+
# Leaves the name unchanged (same as v2).
178+
seam.devices.update(device_id="your-device-id", name=None)
179+
180+
# Unsets the name (new in v3).
181+
seam.devices.update(device_id="your-device-id", name=NULL)
182+
```
183+
184+
Only parameters the Seam API documents as nullable are typed to accept `NULL`, so a type checker will flag misuse. The sentinel's type is exported as `Null` for annotating your own code.
185+
186+
### New exports
187+
188+
`seam` now exports `NULL`, `Null`, `Retry` (from httpx-retries), `UrlSearchParams`, `serialize_url_search_params`, `update_url_search_params`, and `UnserializableParamError`, alongside everything exported in v2.
189+
190+
## Migration checklist
191+
192+
1. Upgrade your runtime to Python 3.11 or later.
193+
2. Update the dependency: `seam>=3,<4` (or a pinned `3.0.0bN` while in prerelease).
194+
3. Rename `niquests_options` to `httpx_options` and translate its contents to `httpx.Client` options.
195+
4. Replace `urllib3.util.retry.Retry` with `seam.Retry` (httpx-retries) in any `retries` argument, and review the new default retry policy.
196+
5. Replace handling of `niquests`/`urllib3` exceptions with the `httpx` equivalents (`httpx.TimeoutException`, `httpx.ConnectError`, ...). Seam error classes are unchanged.
197+
6. Remove any use of `lts_version` or the `seam-lts-version` header.
198+
7. Handle `ValueError` from endpoints and `create_paginator` where calls might carry no parameters.
199+
8. If proxies, firewalls, or test mocks assume all requests are `POST`, update them for `GET`/`PATCH`/`PUT`/`DELETE`.
200+
9. Optionally, adopt `NULL` where you need to unset nullable values.
201+
202+
# Migrating from seam v1 to v2
203+
204+
If you are still on v1.x, migrate to v2 first (or apply both guides together). Version 2 is a much smaller upgrade than v3: client configuration, authentication, endpoint methods, and error handling are all unchanged. The breaking changes are in resource objects and one class rename.
205+
206+
## Summary of breaking changes
207+
208+
| Change | Affects you if... |
209+
| ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
210+
| [Nested resource properties are typed objects](#nested-resource-properties-are-typed-objects) | You treat nested properties as dicts, or rely on unknown-attribute reads |
211+
| [`SeamMultiWorkspace` renamed to `SeamWithoutWorkspace`](#seammultiworkspace-is-renamed-to-seamwithoutworkspace) | You use `SeamMultiWorkspace` |
212+
213+
## Nested resource properties are typed objects
214+
215+
In v1, nested properties on resources — for example `device.properties` or `action_attempt.result` — were dict subclasses with attribute access layered on top. In v2, they hydrate as typed dataclasses scoped to their parent resource, such as `Device.Properties` and `ActionAttempt.Result`, so IDEs and type checkers can see their fields.
216+
217+
Attribute access and dictionary-style _reads_ keep working:
218+
219+
```python
220+
device = seam.devices.get(device_id="your-device-id")
221+
222+
device.properties.locked # still works
223+
device.properties["locked"] # still works
224+
device.properties.get("online") # still works
225+
"locked" in device.properties # still works
226+
```
227+
228+
What breaks:
229+
230+
- **They are no longer dicts.** `isinstance(device.properties, dict)` is now `False`, and mutation (`device.properties["x"] = ...`) and dict-only methods such as `.items()` and `.values()` are gone. Iterate over `.keys()` and index instead.
231+
- **Typoed attributes raise `AttributeError`.** In v1, reading an unknown attribute silently returned (and inserted) an empty mapping, so typos went unnoticed and were truthy-checked as empty dicts. In v2 they fail loudly — code that probed for optional fields via bare attribute access should use `.get("field")` or `hasattr`.
232+
- **Undocumented nested fields are stripped.** API fields not (yet) in the SDK's generated types are dropped during hydration instead of being passed through. If you depend on a field the SDK does not model, upgrade the SDK to a version that includes it.
233+
234+
Free-form record properties, such as `custom_metadata`, remain plain mappings and are not affected.
235+
236+
## `SeamMultiWorkspace` is renamed to `SeamWithoutWorkspace`
237+
238+
The client for personal access tokens without a workspace is renamed; there is no compatibility alias. Its constructor, options, and methods are otherwise identical:
239+
240+
```python
241+
# v1
242+
from seam import SeamMultiWorkspace
243+
244+
seam = SeamMultiWorkspace(personal_access_token="your-personal-access-token")
245+
246+
# v2
247+
from seam import SeamWithoutWorkspace
248+
249+
seam = SeamWithoutWorkspace(personal_access_token="your-personal-access-token")
250+
```
251+
252+
The abstract base class is likewise renamed from `AbstractSeamMultiWorkspace` to `AbstractSeamWithoutWorkspace`.
253+
254+
## New in v2
255+
256+
Version 2.2 also reads authentication from the environment: `SEAM_PERSONAL_ACCESS_TOKEN` and `SEAM_WORKSPACE_ID` are picked up when no explicit credentials are passed (`SEAM_API_KEY` was already supported in v1). Setting both `SEAM_API_KEY` and `SEAM_PERSONAL_ACCESS_TOKEN` is an error.
257+
258+
## Migration checklist
259+
260+
1. Update the dependency: `seam>=2,<3`.
261+
2. Rename `SeamMultiWorkspace` to `SeamWithoutWorkspace` (and `AbstractSeamMultiWorkspace` to `AbstractSeamWithoutWorkspace`).
262+
3. Replace dict-style mutation and `.items()`/`.values()`/`isinstance(..., dict)` usage on nested resource properties with attribute access or `.keys()` iteration.
263+
4. Replace bare attribute probes for optional nested fields with `.get()` or `hasattr` — unknown attributes now raise `AttributeError`.

0 commit comments

Comments
 (0)