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
40 changes: 20 additions & 20 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,23 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
python-version: ['3.12', '3.13', '3.14']
env:
POETRY_VERSION: "1.8.5"
POETRY_VERSION: "2.4.1"

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
lfs: true

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

- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
architecture: x64
Expand All @@ -41,7 +41,7 @@ jobs:
run: poetry install

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

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

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

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

- name: Upload coverage artifacts
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'
uses: actions/upload-artifact@v4
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.14'
uses: actions/upload-artifact@v7
with:
name: coverage-reports
path: |
Expand All @@ -92,25 +92,25 @@ jobs:
needs: python
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
env:
POETRY_VERSION: "1.8.5"
POETRY_VERSION: "2.4.1"
PYTHON_SDK_E2E: "1"
TRANSLOADIT_KEY: ${{ secrets.TRANSLOADIT_KEY }}
TRANSLOADIT_SECRET: ${{ secrets.TRANSLOADIT_SECRET }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
lfs: true

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

- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v6
with:
python-version: '3.12'
python-version: '3.14'
architecture: x64
cache: 'pip'

Expand All @@ -127,8 +127,8 @@ jobs:
exit 1
fi

- name: Run E2E upload test
- name: Run E2E tests and examples
env:
TEST_NODE_PARITY: 0
run: |
poetry run pytest tests/test_e2e_upload.py -q --maxfail=1 --no-cov
poetry run pytest tests/test_e2e_upload.py tests/test_examples.py -q --maxfail=1 --no-cov
8 changes: 4 additions & 4 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v6

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

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

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v4
11 changes: 5 additions & 6 deletions .readthedocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,8 @@ sphinx:
# - pdf
# - epub

# Optional but recommended, declare the Python requirements required
# to build your documentation
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
# python:
# install:
# - requirements: docs/requirements.txt
# Install the documented Sphinx version and the SDK dependencies for autodoc.
python:
install:
- requirements: docs/source/requirements.txt
- path: .
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
### 2.0.0 / Unreleased ###
* 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.
* **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).
* **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`.

### 1.0.4 / 2026-05-20 ###
* 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.
* Updated development tooling to Python 3.9-compatible majors: `pytest` 8.4, `pytest-cov` 7.1, `Sphinx` 7.4, and `sphinx-autobuild` 2024.10.
Expand Down
28 changes: 17 additions & 11 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,42 @@
- Push access to `transloadit/python-sdk`
- PyPI API token with publish rights (`PYPI_TOKEN`), exported or stored in `.env`

**Steps for version `1.0.3` (example)**
1. Bump version in `pyproject.toml`, `transloadit/__init__.py`, and `tests/test_request.py`.
2. Add the `### 1.0.3 / YYYY-MM-DD ###` entry to `CHANGELOG.md`.
3. Run the matrix (add `PYTHON_SDK_E2E=1` if you want the live upload):
**Steps for version `2.0.0` (example)**
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.
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.
3. Run the full Python 3.12, 3.13, and 3.14 matrix, then the optional live upload with test-account credentials:
```bash
./scripts/test-in-docker.sh --python 3.12
./scripts/test-in-docker.sh
PYTHON_SDK_E2E=1 ./scripts/test-in-docker.sh --python 3.14 -- pytest tests/test_e2e_upload.py --no-cov
```
4. Commit on `main`: `git commit -am "Release v1.0.3"`
4. Commit on `main`: `git commit -am "Release v2.0.0"`
5. Tag & push:
```bash
git tag v1.0.3
git push origin main --tags
git tag v2.0.0
git push origin main v2.0.0
```
6. Publish to PyPI via Docker helper (ensures clean tree & version alignment):
```bash
PYPI_TOKEN=... ./scripts/notify-registry.sh
```
7. Publish the GitHub release (pulls notes from the changelog section):
```bash
NOTES=$(python - <<'PY'
NOTES=$(python3 - <<'PY'
import pathlib, re
version = "1.0.3"
version = "2.0.0"
text = pathlib.Path("CHANGELOG.md").read_text()
pattern = rf"^### {re.escape(version)}.*?(?=^### |\Z)"
match = re.search(pattern, text, flags=re.MULTILINE | re.DOTALL)
print(match.group(0).strip() if match else "")
PY
)
gh release create v1.0.3 --title "v1.0.3" --notes "$NOTES"
gh release create v2.0.0 --title "v2.0.0" --notes "$NOTES"
```
8. Verify the Read the Docs build kicked off: <https://transloadit.readthedocs.io/en/latest/>
9. Verify the published package and the security posture:
```bash
python3 -m pip index versions pytransloadit | head
gh api repos/transloadit/python-sdk/dependabot/alerts --jq 'map(select(.state == "open")) | length'
```

Additional background lives here: <https://github.com/transloadit/team-internals/blob/HEAD/_howtos/2020-12-14-maintain-python-sdk.md>.
8 changes: 4 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1

ARG PYTHON_VERSION=3.12
ARG PYTHON_VERSION=3.14
FROM python:${PYTHON_VERSION}-slim AS base

ENV DEBIAN_FRONTEND=noninteractive \
Expand All @@ -20,15 +20,15 @@ RUN apt-get update \

RUN git lfs install --system

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

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

WORKDIR /workspace
95 changes: 86 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,45 @@ A **Python** Integration for [Transloadit](https://transloadit.com)'s file uploa

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

Only Python 3.9+ versions are supported.
Only Python 3.12+ versions are supported.

## Install

```bash
pip install pytransloadit
```

## Upgrading from 1.x to 2.0

Python 3.9, 3.10, and 3.11 are no longer supported. Upgrade your development,
CI, and deployment interpreters to Python 3.12 or newer before installing 2.0.
If you must keep an older interpreter, pin `pytransloadit<2` to stay on 1.x
(the latest 1.x release is 1.0.4).

2.0 requires `requests>=2.33,<3` and `urllib3>=2.7,<3`. Update any older
application pins or constraints and regenerate your lockfile before upgrading:

```bash
python -m pip install --upgrade 'pytransloadit>=2,<3'
```

The synchronous imports and methods remain available on supported interpreters.
SDK 2.0 adds the separate `AsyncTransloadit` client for asyncio
applications. The dependency refresh shipped in 1.0.4 does not require upgrading
to 2.0.

Request-handling changes to account for when upgrading:

- 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.
- Assembly status/cancellation uses credential-free requests to trusted Assembly URLs. Default Transloadit clients reject foreign destinations; explicitly configured services retain their worker hosts.
- HTTP redirects are returned to the caller instead of followed automatically. Invalid service URLs and empty/dot-segment Template IDs raise `ValueError`.
- 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.
- 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.

Source contributors use Poetry 2.4.1 with the new `[project]` metadata; isolated
source builds require `poetry-core>=2.2,<3`. Normal pip installation manages the
build backend automatically.

## Usage

```python
Expand All @@ -36,9 +67,55 @@ print(assembly_response.data.get('assembly_id'))
print(assembly_response.data['assembly_id'])
```

## Example
## Async usage

```python
import asyncio
import os
from transloadit.async_client import AsyncTransloadit

async def main():
async with AsyncTransloadit(os.environ["TRANSLOADIT_KEY"], os.environ["TRANSLOADIT_SECRET"]) as tl:
assembly = tl.new_assembly()
assembly.add_step("resize", "/image/resize", {"width": 70, "height": 70})
with open("PATH/TO/FILE.jpg", "rb") as upload:
assembly.add_file(upload)
response = await assembly.create(wait=True, resumable=False)
print(response.data["ok"])

asyncio.run(main())
```

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.

An injected aiohttp session configures Assembly creation and polling. Resumable
file transfers use tuspy's separate Requests transport and do not inherit that
session's explicit proxy or TLS connector settings; configure the Requests
environment for those transfers.

Cancellation of a resumable upload waits for the tuspy upload batch, including
retries, to finish before releasing your files. Multipart cancellation waits for
any active file read. Timeout and shutdown cleanup can therefore exceed the
requested deadline; keep file context managers open around the awaited call.

If you do not use `async with`, call `await tl.aclose()` when you are done with the session.

## Client features

`AsyncTransloadit` mirrors the existing synchronous client: Assembly creation,
retrieval, listing and cancellation; Template creation, retrieval, listing,
updates and deletion; monthly billing; and Smart CDN URL signing. Use the
Assembly helpers for multipart or resumable uploads and completion polling.
Await the async client's network methods; local factories and URL signing stay
synchronous.

## Examples

For copy/paste runnable examples, take a look at
[`examples/`](https://github.com/transloadit/python-sdk/tree/HEAD/examples).

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

## Documentation

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

This script will:

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

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).
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).

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`.
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`.

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`:

```bash
PYTHON_SDK_E2E=1 scripts/test-in-docker.sh --python 3.12 -- pytest tests/test_e2e_upload.py
PYTHON_SDK_E2E=1 scripts/test-in-docker.sh --python 3.14 -- pytest tests/test_e2e_upload.py tests/test_examples.py
```

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

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

Expand Down
Loading
Loading