Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions .github/workflows/sdk_validation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,20 @@ 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:
python-version: [ '3.9', '3.11' ]
# 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
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
Expand All @@ -37,7 +43,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
Expand Down
2 changes: 1 addition & 1 deletion .speakeasy/gen.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
126 changes: 126 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# 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 "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`.

```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="<KEY>", client=session)

# 5.0
import httpx
shippo_sdk = shippo.Shippo(
api_key_header="<KEY>",
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.
1 change: 0 additions & 1 deletion tests/test_debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
1 change: 0 additions & 1 deletion tests/test_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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", [
Expand Down
Loading