From 70590559fa4fa8f220471fc99334719b5246f798 Mon Sep 17 00:00:00 2001 From: Joseph Friedrich Date: Fri, 16 Oct 2026 12:46:36 -0500 Subject: [PATCH 1/2] Prepare Python SDK 5.0.0-beta.1 release - Set python.version to 5.0.0-beta.1 in gen.yaml so the next generation produces a pre-release (pip skips it unless users opt in). - CI: test 3.10/3.11/3.13 instead of 3.9/3.11 (5.x requires >=3.10), scope the concurrency group per PR and Python version, and key the venv cache on poetry.lock (setup.py does not exist). - Remove the stale skip markers on test_hooks.py and test_debug.py; both pass on 3.11. - Add MIGRATION.md covering 3.x -> 5.0 breaking changes. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/sdk_validation.yaml | 8 +- .speakeasy/gen.yaml | 2 +- MIGRATION.md | 123 ++++++++++++++++++++++++++ tests/test_debug.py | 1 - tests/test_hooks.py | 1 - 5 files changed, 129 insertions(+), 6 deletions(-) create mode 100644 MIGRATION.md diff --git a/.github/workflows/sdk_validation.yaml b/.github/workflows/sdk_validation.yaml index d9d2d1e1..28af12d0 100644 --- a/.github/workflows/sdk_validation.yaml +++ b/.github/workflows/sdk_validation.yaml @@ -14,13 +14,15 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [ '3.9', '3.11' ] + # 5.x requires Python >=3.10. 3.14 waits until the lockfile's pydantic-core ships 3.14 wheels. + python-version: [ '3.10', '3.11', '3.13' ] name: Python ${{ matrix.python-version }} timeout-minutes: 10 env: SHIPPO_TOKEN: ${{ secrets.SHIPPO_TOKEN }} concurrency: - group: python-sdk-check + group: python-sdk-check-${{ github.event.pull_request.number }}-${{ matrix.python-version }} + cancel-in-progress: true steps: - name: Check out repository uses: actions/checkout@v4 @@ -37,7 +39,7 @@ jobs: uses: actions/cache@v4 with: path: ./.venv - key: python-${{ steps.setup_python.outputs.python-version }}-${{ hashFiles('setup.py') }} + key: python-${{ steps.setup_python.outputs.python-version }}-${{ hashFiles('poetry.lock') }} - name: Install dependencies run: | make install diff --git a/.speakeasy/gen.yaml b/.speakeasy/gen.yaml index 8e514783..3e5691b1 100755 --- a/.speakeasy/gen.yaml +++ b/.speakeasy/gen.yaml @@ -15,7 +15,7 @@ generation: oAuth2ClientCredentialsEnabled: true oAuth2PasswordEnabled: false python: - version: 4.0.0 + version: 5.0.0-beta.1 additionalDependencies: dev: httpretty: ==1.1.4 diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 00000000..5bacfe31 --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,123 @@ +# Migrating from 3.x to 5.0 + +Version 5.0 of the `shippo` package is a major release with breaking changes. The SDK is now built on +Speakeasy's v2 Python generator, which uses [pydantic](https://docs.pydantic.dev/) models and the +[httpx](https://www.python-httpx.org/) HTTP client. This guide covers what you need to change when +upgrading from 3.x (the last release was 3.9.0). + +Version 4.0.0 was never published. Upgrades go directly from 3.x to 5.0. + +## Before you upgrade + +- **Python 3.10 or newer is required.** On Python 3.9 or older, pip keeps installing 3.9.0. +- **Pin the old version if you are not ready.** For example, add `shippo~=3.9` or `shippo<5` to your requirements. +- **Try the beta first.** Pre-releases are only installed when you ask for one explicitly: + + ```bash + pip install --pre shippo # or: pip install "shippo==5.0.0b1" + ``` + +## 1. Method arguments are keyword-only + +All arguments must now be passed by name. Positional calls raise a `TypeError`. + +```python +# 3.x +address = shippo_sdk.addresses.get("adr_123") +shipment = shippo_sdk.shipments.create(shipment_request) +addresses = shippo_sdk.addresses.list(1, 25) + +# 5.0 +address = shippo_sdk.addresses.get(address_id="adr_123") +shipment = shippo_sdk.shipments.create(request=shipment_request) +addresses = shippo_sdk.addresses.list(page=1, results=25) +``` + +## 2. Request bodies can be models or plain dicts + +Every `request=` argument accepts either the model class or a plain `dict` with the same fields. + +```python +from shippo.models import components + +shippo_sdk.addresses.create(request=components.AddressCreateRequest(country="US", zip="94117")) +shippo_sdk.addresses.create(request={"country": "US", "zip": "94117"}) +``` + +## 3. Methods whose arguments moved into a request body + +These methods used to take individual arguments. They now take a single `request=`. + +| 3.x | 5.0 | +| --- | --- | +| `refunds.create(transaction, async_=False)` | `refunds.create(request={"transaction": ..., "async_": False})` | +| `tracking_status.create(carrier, tracking_number, metadata=None)` | `tracking_status.create(request={"carrier": ..., "tracking_number": ...})` | +| `rates_at_checkout.update_default_parcel_template(object_id=...)` | `rates_at_checkout.update_default_parcel_template(request={"object_id": ...})` | + +**Known issue in 5.0.0b1:** the following methods take no parameters but still require an empty +`request=` argument. We plan to remove this requirement before 5.0.0 is final. + +```python +shippo_sdk.service_groups.list(request={}) +shippo_sdk.refunds.list(request={}) +shippo_sdk.user_parcel_templates.list(request={}) +shippo_sdk.rates_at_checkout.get_default_parcel_template(request={}) +shippo_sdk.rates_at_checkout.delete_default_parcel_template(request={}) +``` + +## 4. Models are pydantic, not dataclasses + +| 3.x (dataclasses-json) | 5.0 (pydantic) | +| --- | --- | +| `Address.from_dict(data)` | `Address.model_validate(data)` | +| `address.to_dict()` | `address.model_dump(by_alias=True, exclude_none=True)` | +| `address.to_json()` | `address.model_dump_json(by_alias=True, exclude_none=True)` | +| `dataclasses.replace(address, ...)` | `address.model_copy(update={...})` | + +Each model also has a `TypedDict` counterpart (for example `AddressCreateRequestTypedDict`) that you +can use for type-checked dict requests. + +## 5. The HTTP client is httpx, not requests + +If you pass a custom client, use `httpx.Client` instead of `requests.Session`: + +```python +# 3.x +import requests +session = requests.Session() +session.headers.update({"x-custom-header": "value"}) +shippo_sdk = shippo.Shippo(api_key_header="", client=session) + +# 5.0 +import httpx +shippo_sdk = shippo.Shippo( + api_key_header="", + client=httpx.Client(headers={"x-custom-header": "value"}), +) +``` + +`shippo.debug.DebugSession` still works the same way. `last_request` and `last_response` are now +`httpx.Request` and `httpx.Response` objects. + +## 6. Errors + +`SDKError` is still raised for API errors, and it now inherits from `shippo.models.errors.ShippoError`. +`status_code`, `body`, and `raw_response` are still available. `raw_response` is now an +`httpx.Response`. If you construct `SDKError` yourself, for example in tests, its signature changed to +`SDKError(message, raw_response, body=None)`. + +```python +from shippo.models.errors import ShippoError + +try: + shippo_sdk.addresses.get(address_id="adr_missing") +except ShippoError as e: + print(e.status_code, e.body) +``` + +## New in 5.0 + +- **Async methods.** Every operation has an `*_async` variant, for example `await shippo_sdk.addresses.list_async()`. +- **Per-call options.** Every method accepts `retries=`, `timeout_ms=`, `server_url=`, and `http_headers=`. +- **Client-wide timeout.** `Shippo(timeout_ms=...)`. +- **Context manager.** Use `with Shippo(...) as shippo_sdk:` to close the HTTP client when you are done. diff --git a/tests/test_debug.py b/tests/test_debug.py index b35e08bc..303f59d7 100644 --- a/tests/test_debug.py +++ b/tests/test_debug.py @@ -11,7 +11,6 @@ BASE_URL = "http://localhost:9500" -@pytest.mark.skip(reason="Skipping tests due to Python 3.11 compatibility issues") class TestDebugClient: @pytest.fixture(scope="session") diff --git a/tests/test_hooks.py b/tests/test_hooks.py index a2e13647..e3c8f90b 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -6,7 +6,6 @@ BASE_URL = "http://localhost:9500" -@pytest.mark.skip(reason="Skipping tests due to Python 3.11 compatibility issues") class TestHooks: @pytest.mark.parametrize("api_token,should_prefix", [ From 0067320724c9a2a3012ea55337eecfa6720a49d5 Mon Sep 17 00:00:00 2001 From: Joseph Friedrich Date: Fri, 16 Oct 2026 13:54:32 -0500 Subject: [PATCH 2/2] Address review: serialize CI matrix, clarify 3.14, pin beta in MIGRATION - max-parallel: 1 so the three Python jobs don't race on the shared test account (test_rates_at_checkout deletes every service group). - 3.14 comment: the pinned pydantic-core versions will never get 3.14 wheels; 2.35.0 is the first that has them. - MIGRATION.md: lead with pip install "shippo==5.0.0b1"; --pre also pulls pre-release dependencies. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/sdk_validation.yaml | 6 +++++- MIGRATION.md | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sdk_validation.yaml b/.github/workflows/sdk_validation.yaml index 28af12d0..43809f08 100644 --- a/.github/workflows/sdk_validation.yaml +++ b/.github/workflows/sdk_validation.yaml @@ -13,8 +13,12 @@ jobs: check: runs-on: ubuntu-latest strategy: + # All versions share one test account, and tests like test_rates_at_checkout + # delete account-wide state, so run them one at a time. + max-parallel: 1 matrix: - # 5.x requires Python >=3.10. 3.14 waits until the lockfile's pydantic-core ships 3.14 wheels. + # 5.x requires Python >=3.10. 3.14 needs a pydantic-core bump: the pinned + # versions have no 3.14 wheels (2.35.0 is the first that does). python-version: [ '3.10', '3.11', '3.13' ] name: Python ${{ matrix.python-version }} timeout-minutes: 10 diff --git a/MIGRATION.md b/MIGRATION.md index 5bacfe31..511c93a9 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -14,9 +14,12 @@ Version 4.0.0 was never published. Upgrades go directly from 3.x to 5.0. - **Try the beta first.** Pre-releases are only installed when you ask for one explicitly: ```bash - pip install --pre shippo # or: pip install "shippo==5.0.0b1" + pip install "shippo==5.0.0b1" ``` + Pin the exact version rather than using `pip install --pre shippo`. `--pre` also allows + pre-release versions of every dependency, not just `shippo`. + ## 1. Method arguments are keyword-only All arguments must now be passed by name. Positional calls raise a `TypeError`.