Skip to content

Commit acb3c24

Browse files
authored
Release Python SDK 2.0 with asyncio and Python 3.12+ (#56)
Add AsyncTransloadit over the handwritten SDK client surface and require Python 3.12+. Prepare version 2.0.0, update runtime/tooling and document migration. Contract-generated endpoints are deferred to #61 and API2-owned emitters in transloadit/api2#9252. Closes #9.
1 parent a9f606d commit acb3c24

52 files changed

Lines changed: 5552 additions & 1528 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/ci.yml‎

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,23 @@ jobs:
1212
strategy:
1313
matrix:
1414
os: [ubuntu-latest, windows-latest]
15-
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
15+
python-version: ['3.12', '3.13', '3.14']
1616
env:
17-
POETRY_VERSION: "1.8.5"
17+
POETRY_VERSION: "2.4.1"
1818

1919
steps:
20-
- uses: actions/checkout@v4
20+
- uses: actions/checkout@v6
2121
with:
2222
lfs: true
2323

24-
- uses: actions/setup-node@v4
24+
- uses: actions/setup-node@v6
2525
with:
26-
node-version: '20'
26+
node-version: '24'
2727
- name: Install Transloadit CLI
2828
run: npm install -g transloadit
2929

3030
- name: Set up Python
31-
uses: actions/setup-python@v4
31+
uses: actions/setup-python@v6
3232
with:
3333
python-version: ${{ matrix.python-version }}
3434
architecture: x64
@@ -41,7 +41,7 @@ jobs:
4141
run: poetry install
4242

4343
- name: Test with coverage
44-
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'
44+
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.14'
4545
run: |
4646
poetry run pytest --cov=transloadit \
4747
--cov-report=xml \
@@ -54,12 +54,12 @@ jobs:
5454
TEST_NODE_PARITY: 1
5555

5656
- name: Test without coverage
57-
if: matrix.os != 'ubuntu-latest' || matrix.python-version != '3.12'
57+
if: matrix.os != 'ubuntu-latest' || matrix.python-version != '3.14'
5858
run: poetry run pytest tests
5959

6060
- name: Upload coverage reports
6161
# Only upload coverage if we have a token (skip for Dependabot PRs)
62-
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12' && (github.event_name != 'pull_request' || github.actor != 'dependabot[bot]')
62+
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.14' && (github.event_name != 'pull_request' || github.actor != 'dependabot[bot]')
6363
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f
6464
with:
6565
token: ${{ secrets.CODECOV_TOKEN }}
@@ -70,7 +70,7 @@ jobs:
7070

7171
- name: Upload coverage reports (tokenless)
7272
# Use tokenless upload for Dependabot PRs
73-
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12' && github.event_name == 'pull_request' && github.actor == 'dependabot[bot]'
73+
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.14' && github.event_name == 'pull_request' && github.actor == 'dependabot[bot]'
7474
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f
7575
with:
7676
files: ./coverage.xml
@@ -79,8 +79,8 @@ jobs:
7979
fail_ci_if_error: false
8080

8181
- name: Upload coverage artifacts
82-
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'
83-
uses: actions/upload-artifact@v4
82+
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.14'
83+
uses: actions/upload-artifact@v7
8484
with:
8585
name: coverage-reports
8686
path: |
@@ -92,25 +92,25 @@ jobs:
9292
needs: python
9393
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
9494
env:
95-
POETRY_VERSION: "1.8.5"
95+
POETRY_VERSION: "2.4.1"
9696
PYTHON_SDK_E2E: "1"
9797
TRANSLOADIT_KEY: ${{ secrets.TRANSLOADIT_KEY }}
9898
TRANSLOADIT_SECRET: ${{ secrets.TRANSLOADIT_SECRET }}
9999
steps:
100-
- uses: actions/checkout@v4
100+
- uses: actions/checkout@v6
101101
with:
102102
lfs: true
103103

104-
- uses: actions/setup-node@v4
104+
- uses: actions/setup-node@v6
105105
with:
106-
node-version: '20'
106+
node-version: '24'
107107
- name: Install Transloadit CLI
108108
run: npm install -g transloadit
109109

110110
- name: Set up Python
111-
uses: actions/setup-python@v4
111+
uses: actions/setup-python@v6
112112
with:
113-
python-version: '3.12'
113+
python-version: '3.14'
114114
architecture: x64
115115
cache: 'pip'
116116

@@ -127,8 +127,8 @@ jobs:
127127
exit 1
128128
fi
129129
130-
- name: Run E2E upload test
130+
- name: Run E2E tests and examples
131131
env:
132132
TEST_NODE_PARITY: 0
133133
run: |
134-
poetry run pytest tests/test_e2e_upload.py -q --maxfail=1 --no-cov
134+
poetry run pytest tests/test_e2e_upload.py tests/test_examples.py -q --maxfail=1 --no-cov

‎.github/workflows/codeql-analysis.yml‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@ jobs:
3535

3636
steps:
3737
- name: Checkout repository
38-
uses: actions/checkout@v3
38+
uses: actions/checkout@v6
3939

4040
# Initializes the CodeQL tools for scanning.
4141
- name: Initialize CodeQL
42-
uses: github/codeql-action/init@v2
42+
uses: github/codeql-action/init@v4
4343
with:
4444
languages: ${{ matrix.language }}
4545
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -50,7 +50,7 @@ jobs:
5050
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
5151
# If this step fails, then you should remove it and run the build manually (see below)
5252
- name: Autobuild
53-
uses: github/codeql-action/autobuild@v2
53+
uses: github/codeql-action/autobuild@v4
5454

5555
# ℹ️ Command-line programs to run using the OS shell.
5656
# 📚 https://git.io/JvXDl
@@ -64,4 +64,4 @@ jobs:
6464
# make release
6565

6666
- name: Perform CodeQL Analysis
67-
uses: github/codeql-action/analyze@v2
67+
uses: github/codeql-action/analyze@v4

‎.readthedocs.yaml‎

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,8 @@ sphinx:
2727
# - pdf
2828
# - epub
2929

30-
# Optional but recommended, declare the Python requirements required
31-
# to build your documentation
32-
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
33-
# python:
34-
# install:
35-
# - requirements: docs/requirements.txt
30+
# Install the documented Sphinx version and the SDK dependencies for autodoc.
31+
python:
32+
install:
33+
- requirements: docs/source/requirements.txt
34+
- path: .

‎CHANGELOG.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
### 2.0.0 / Unreleased ###
2+
* Added `AsyncTransloadit` with asyncio support for the existing synchronous client's Assembly, Template and billing methods, plus Smart CDN URL signing. Polling uses `asyncio.sleep`; resumable uploads run tuspy in worker threads.
3+
* **Breaking Change**: Python 3.12 or newer is required. Upgrade development, CI and deployment interpreters before installing 2.0. Applications that need Python 3.9–3.11 can pin `pytransloadit<2` (latest 1.x: 1.0.4).
4+
* **Upgrade notes**: Update application constraints for `requests>=2.33,<3` and `urllib3>=2.7,<3`, then regenerate your lockfile. Existing synchronous imports and methods remain available; asyncio applications can use `async with AsyncTransloadit(...)` and await network operations. See README for request-handling corrections, including explicit polling retry exhaustion, trusted Assembly URLs, redirect handling and Smart CDN validation/boolean serialization. Keep file context managers open around awaited uploads; cancellation waits for active file reads and resumable workers to finish. Resumable transfers do not inherit an injected aiohttp session's proxy/TLS settings. Source contributors use Poetry 2.4.1 with `[project]` metadata; isolated source builds require `poetry-core>=2.2,<3`.
5+
16
### 1.0.4 / 2026-05-20 ###
27
* Refreshed locked runtime and development dependencies, including `aiohttp` 3.13.5, `idna` 3.15, `pygments` 2.20.0, Python-version-specific `requests` updates, and `tuspy` 1.1.0.
38
* Updated development tooling to Python 3.9-compatible majors: `pytest` 8.4, `pytest-cov` 7.1, `Sphinx` 7.4, and `sphinx-autobuild` 2024.10.

‎CONTRIBUTING.md‎

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,36 +7,42 @@
77
- Push access to `transloadit/python-sdk`
88
- PyPI API token with publish rights (`PYPI_TOKEN`), exported or stored in `.env`
99

10-
**Steps for version `1.0.3` (example)**
11-
1. Bump version in `pyproject.toml`, `transloadit/__init__.py`, and `tests/test_request.py`.
12-
2. Add the `### 1.0.3 / YYYY-MM-DD ###` entry to `CHANGELOG.md`.
13-
3. Run the matrix (add `PYTHON_SDK_E2E=1` if you want the live upload):
10+
**Steps for version `2.0.0` (example)**
11+
1. Bump version in `pyproject.toml`, `transloadit/__init__.py`, and `tests/test_request.py`, and update `version`/`release` in `docs/source/conf.py`. PR #56 prepares these as `2.0.0`/`2.0` already.
12+
2. Replace `Unreleased` in the `### 2.0.0 / Unreleased ###` entry in `CHANGELOG.md` with the actual release date (`YYYY-MM-DD`). Review the breaking changes and upgrade notes in README and the Sphinx docs. The publishing helper rejects an `Unreleased` entry; `--dry-run` allows it so a prepared release can still be validated.
13+
3. Run the full Python 3.12, 3.13, and 3.14 matrix, then the optional live upload with test-account credentials:
1414
```bash
15-
./scripts/test-in-docker.sh --python 3.12
15+
./scripts/test-in-docker.sh
16+
PYTHON_SDK_E2E=1 ./scripts/test-in-docker.sh --python 3.14 -- pytest tests/test_e2e_upload.py --no-cov
1617
```
17-
4. Commit on `main`: `git commit -am "Release v1.0.3"`
18+
4. Commit on `main`: `git commit -am "Release v2.0.0"`
1819
5. Tag & push:
1920
```bash
20-
git tag v1.0.3
21-
git push origin main --tags
21+
git tag v2.0.0
22+
git push origin main v2.0.0
2223
```
2324
6. Publish to PyPI via Docker helper (ensures clean tree & version alignment):
2425
```bash
2526
PYPI_TOKEN=... ./scripts/notify-registry.sh
2627
```
2728
7. Publish the GitHub release (pulls notes from the changelog section):
2829
```bash
29-
NOTES=$(python - <<'PY'
30+
NOTES=$(python3 - <<'PY'
3031
import pathlib, re
31-
version = "1.0.3"
32+
version = "2.0.0"
3233
text = pathlib.Path("CHANGELOG.md").read_text()
3334
pattern = rf"^### {re.escape(version)}.*?(?=^### |\Z)"
3435
match = re.search(pattern, text, flags=re.MULTILINE | re.DOTALL)
3536
print(match.group(0).strip() if match else "")
3637
PY
3738
)
38-
gh release create v1.0.3 --title "v1.0.3" --notes "$NOTES"
39+
gh release create v2.0.0 --title "v2.0.0" --notes "$NOTES"
3940
```
4041
8. Verify the Read the Docs build kicked off: <https://transloadit.readthedocs.io/en/latest/>
42+
9. Verify the published package and the security posture:
43+
```bash
44+
python3 -m pip index versions pytransloadit | head
45+
gh api repos/transloadit/python-sdk/dependabot/alerts --jq 'map(select(.state == "open")) | length'
46+
```
4147

4248
Additional background lives here: <https://github.com/transloadit/team-internals/blob/HEAD/_howtos/2020-12-14-maintain-python-sdk.md>.

‎Dockerfile‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# syntax=docker/dockerfile:1
22

3-
ARG PYTHON_VERSION=3.12
3+
ARG PYTHON_VERSION=3.14
44
FROM python:${PYTHON_VERSION}-slim AS base
55

66
ENV DEBIAN_FRONTEND=noninteractive \
@@ -20,15 +20,15 @@ RUN apt-get update \
2020

2121
RUN git lfs install --system
2222

23-
# Install Node.js 20 (for Smart CDN parity tests) and supporting CLI tooling
24-
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
23+
# Install Node.js 24 (for Smart CDN parity tests) and supporting CLI tooling
24+
RUN curl -fsSL https://deb.nodesource.com/setup_24.x | bash - \
2525
&& apt-get update \
2626
&& apt-get install -y --no-install-recommends nodejs \
2727
&& npm install -g transloadit \
2828
&& rm -rf /var/lib/apt/lists/*
2929

3030
# Install Poetry so we match the GitHub Actions toolchain
3131
RUN pip install --no-cache-dir --upgrade pip \
32-
&& pip install --no-cache-dir poetry
32+
&& pip install --no-cache-dir poetry==2.4.1
3333

3434
WORKDIR /workspace

‎README.md‎

Lines changed: 86 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,45 @@ A **Python** Integration for [Transloadit](https://transloadit.com)'s file uploa
1111

1212
This is a **Python** SDK to make it easy to talk to the [Transloadit](https://transloadit.com) REST API.
1313

14-
Only Python 3.9+ versions are supported.
14+
Only Python 3.12+ versions are supported.
1515

1616
## Install
1717

1818
```bash
1919
pip install pytransloadit
2020
```
2121

22+
## Upgrading from 1.x to 2.0
23+
24+
Python 3.9, 3.10, and 3.11 are no longer supported. Upgrade your development,
25+
CI, and deployment interpreters to Python 3.12 or newer before installing 2.0.
26+
If you must keep an older interpreter, pin `pytransloadit<2` to stay on 1.x
27+
(the latest 1.x release is 1.0.4).
28+
29+
2.0 requires `requests>=2.33,<3` and `urllib3>=2.7,<3`. Update any older
30+
application pins or constraints and regenerate your lockfile before upgrading:
31+
32+
```bash
33+
python -m pip install --upgrade 'pytransloadit>=2,<3'
34+
```
35+
36+
The synchronous imports and methods remain available on supported interpreters.
37+
SDK 2.0 adds the separate `AsyncTransloadit` client for asyncio
38+
applications. The dependency refresh shipped in 1.0.4 does not require upgrading
39+
to 2.0.
40+
41+
Request-handling changes to account for when upgrading:
42+
43+
- Smart CDN boolean parameters use `true`/`false` to match the reference CLI. Workspace slugs must be DNS-safe; `auth_key`, `exp` and `sig` are reserved query keys.
44+
- Assembly status/cancellation uses credential-free requests to trusted Assembly URLs. Default Transloadit clients reject foreign destinations; explicitly configured services retain their worker hosts.
45+
- HTTP redirects are returned to the caller instead of followed automatically. Invalid service URLs and empty/dot-segment Template IDs raise `ValueError`.
46+
- Template creation sends steps inside the `template` object. When combining a `template` option with `add_step`, supply an object rather than a serialized JSON string.
47+
- Non-JSON responses return text or bytes in `Response.data`; empty bodies return an empty string. With `wait=True`, exhausting consecutive status rate-limit retries raises `AssemblyPollingError` (a `RuntimeError` subclass). The Assembly may still be running: use the error's `assembly_url` to resume polling or cancel, and inspect `assembly_response`/`last_response` for creation and rate-limit details. Normal API error responses still need an `error` check.
48+
49+
Source contributors use Poetry 2.4.1 with the new `[project]` metadata; isolated
50+
source builds require `poetry-core>=2.2,<3`. Normal pip installation manages the
51+
build backend automatically.
52+
2253
## Usage
2354

2455
```python
@@ -36,9 +67,55 @@ print(assembly_response.data.get('assembly_id'))
3667
print(assembly_response.data['assembly_id'])
3768
```
3869

39-
## Example
70+
## Async usage
71+
72+
```python
73+
import asyncio
74+
import os
75+
from transloadit.async_client import AsyncTransloadit
76+
77+
async def main():
78+
async with AsyncTransloadit(os.environ["TRANSLOADIT_KEY"], os.environ["TRANSLOADIT_SECRET"]) as tl:
79+
assembly = tl.new_assembly()
80+
assembly.add_step("resize", "/image/resize", {"width": 70, "height": 70})
81+
with open("PATH/TO/FILE.jpg", "rb") as upload:
82+
assembly.add_file(upload)
83+
response = await assembly.create(wait=True, resumable=False)
84+
print(response.data["ok"])
85+
86+
asyncio.run(main())
87+
```
88+
89+
The async client keeps polling on `asyncio.sleep`. Resumable uploads still use the existing TUS client, but are offloaded to worker threads so the event loop stays responsive.
90+
91+
An injected aiohttp session configures Assembly creation and polling. Resumable
92+
file transfers use tuspy's separate Requests transport and do not inherit that
93+
session's explicit proxy or TLS connector settings; configure the Requests
94+
environment for those transfers.
95+
96+
Cancellation of a resumable upload waits for the tuspy upload batch, including
97+
retries, to finish before releasing your files. Multipart cancellation waits for
98+
any active file read. Timeout and shutdown cleanup can therefore exceed the
99+
requested deadline; keep file context managers open around the awaited call.
100+
101+
If you do not use `async with`, call `await tl.aclose()` when you are done with the session.
102+
103+
## Client features
104+
105+
`AsyncTransloadit` mirrors the existing synchronous client: Assembly creation,
106+
retrieval, listing and cancellation; Template creation, retrieval, listing,
107+
updates and deletion; monthly billing; and Smart CDN URL signing. Use the
108+
Assembly helpers for multipart or resumable uploads and completion polling.
109+
Await the async client's network methods; local factories and URL signing stay
110+
synchronous.
111+
112+
## Examples
113+
114+
For copy/paste runnable examples, take a look at
115+
[`examples/`](https://github.com/transloadit/python-sdk/tree/HEAD/examples).
40116

41-
For fully working examples, take a look at [`examples/`](https://github.com/transloadit/python-sdk/tree/HEAD/examples).
117+
The examples cover sync uploads, async uploads, resumable uploads, Template usage,
118+
sync and async Template lifecycle management, and Smart CDN URL signing.
42119

43120
## Documentation
44121

@@ -56,21 +133,21 @@ scripts/test-in-docker.sh
56133

57134
This script will:
58135

59-
- build images for the Python versions we test in CI (3.9–3.13)
60-
- install Poetry, Node.js 20, and the Transloadit CLI
136+
- build images for the Python versions we test in CI (3.12, 3.13, and 3.14)
137+
- install Poetry, Node.js 24, and the Transloadit CLI
61138
- pass credentials from `.env` (if present) so end-to-end tests can run against real Transloadit accounts
62139

63-
Signature parity tests use `npx transloadit smart_sig` under the hood, matching the reference implementation used by our other SDKs. Our GitHub Actions workflow also runs the E2E upload against Python 3.12 on every push/PR using a dedicated Transloadit test account (wired through the `TRANSLOADIT_KEY` and `TRANSLOADIT_SECRET` secrets).
140+
Signature parity tests use `npx transloadit smart_sig` under the hood, matching the reference implementation used by our other SDKs. Our GitHub Actions workflow also runs the E2E upload and quickstart examples against Python 3.14 on every push/PR using a dedicated Transloadit test account (wired through the `TRANSLOADIT_KEY` and `TRANSLOADIT_SECRET` secrets).
64141

65-
Pass `--python 3.12` (or set `PYTHON_VERSIONS`) to restrict the matrix, or append a custom command after `--`, for example `scripts/test-in-docker.sh -- pytest -k smartcdn`.
142+
Pass `--python 3.14` (or set `PYTHON_VERSIONS`) to restrict the matrix, or append a custom command after `--`, for example `scripts/test-in-docker.sh -- pytest -k smartcdn`.
66143

67144
To exercise the optional end-to-end upload against a real Transloadit account, provide `TRANSLOADIT_KEY` and `TRANSLOADIT_SECRET` (via environment variables or `.env`) and set `PYTHON_SDK_E2E=1`:
68145

69146
```bash
70-
PYTHON_SDK_E2E=1 scripts/test-in-docker.sh --python 3.12 -- pytest tests/test_e2e_upload.py
147+
PYTHON_SDK_E2E=1 scripts/test-in-docker.sh --python 3.14 -- pytest tests/test_e2e_upload.py tests/test_examples.py
71148
```
72149

73-
The test uploads `chameleon.jpg`, resizes it, and asserts on the live assembly results.
150+
The tests upload `chameleon.jpg`, run the copy/paste quickstart examples, and assert on the live assembly results.
74151

75152
If you have a global installation of `poetry`, you can run the tests with:
76153

0 commit comments

Comments
 (0)