Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
275095b
Add asyncio client support
kvz May 20, 2026
346818b
Harden asyncio support
kvz May 20, 2026
25f498c
Improve async coverage
kvz May 20, 2026
85697ba
Fix async retry and response edge cases
kvz May 20, 2026
37e4cc5
Handle plain-text async assembly responses
kvz May 20, 2026
fb1bcfd
Gate async TUS upload on successful create
kvz May 20, 2026
def57f6
Harden async retry rewinds
kvz May 20, 2026
559b875
Fix resumable async wait and rewind retries
kvz May 20, 2026
f8d3482
Refine async retries and upload metadata
kvz May 20, 2026
83ea871
Polish async retries and uploads
kvz May 20, 2026
3cb1bb4
Improve async coverage and retry safety
kvz May 20, 2026
924fffc
Address async council review findings
kvz May 20, 2026
bb58520
Add async E2E coverage
kvz May 21, 2026
1722c93
Harden async upload edge cases
kvz May 21, 2026
7f8fc8c
Harden request URL and upload handling
kvz May 21, 2026
88807ad
Harden async retry and service URLs
kvz May 21, 2026
d19832a
Add E2E coverage for resumable uploads and templates
kvz May 21, 2026
dc77820
Add runnable SDK examples
kvz May 21, 2026
51fdac5
Run quickstart examples in CI
kvz May 21, 2026
86afdcc
Add async template lifecycle example
kvz May 21, 2026
017fefc
Fix council review edge cases
kvz May 21, 2026
bb04389
Note endpoint coverage TODO
kvz May 21, 2026
a779f9e
Add generated low-level endpoint methods
kvz May 23, 2026
3cd48ac
Mark generated endpoint method blocks
kvz May 23, 2026
8090d14
Harden sync assembly retries and generated docs
kvz May 24, 2026
0c4c974
Regenerate endpoint methods from API2 contracts
kvz May 24, 2026
43e999e
Harden async SDK edge cases
kvz May 25, 2026
d80ba14
Mark generated blocks as contract-owned
kvz May 26, 2026
7341ccf
Add generated wait for assembly helpers
kvz May 26, 2026
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
### 2.0.0 / 2026-05-20 ###
* **Breaking Change**: Raised the supported Python runtime floor from 3.9+ to 3.12+ so the SDK no longer has to retain vulnerable locked dependency versions for EOL Python 3.9 or depend on tooling lines that are already dropping older runtime support.
* Expanded low-level API endpoint coverage for Assemblies, Assembly Notifications, Templates, Template Credentials, Priority Job Slots, and Billing, with sync and async methods generated from the API2 endpoint contract.
* Added explicit asyncio support with `AsyncTransloadit`, async request/assembly/template helpers, and `asyncio.sleep`-based polling. Resumable uploads stay on the existing TUS client, but run through `asyncio.to_thread()` so the event loop remains responsive instead of pretending the sync uploader is natively async.
* Hardened upload and response edge cases: invalid service URLs and empty template IDs now fail fast, external absolute API URLs are no longer signed, sync TUS uploads now handle nameless streams and submit rate limits before uploading, sync non-resumable retries rewind seekable files and reject non-seekable retry streams, sync/async polling rate-limit retries reset after successful polls and cap repeated rate limits, async form fields match sync boolean serialization, async TUS cancellation waits for worker cleanup, async rate-limit backoff honors server `retryIn`, Smart CDN signing rejects invalid workspace slugs/reserved query keys, and sync non-JSON responses fall back to response text.
* Hardened sync and async request handling by preserving custom `auth` constraints, quoting path IDs, and keeping explicit/custom service URLs compatible with local, CI, and [Transloadit Gateway](https://github.com/transloadit/gateway) deployments.
* Fixed sync and async template creation to send the current API `template` payload shape.
* Raised the runtime HTTP stack to patched versions by requiring `requests` 2.33+ and adding an explicit `urllib3` 2.7+ floor.
* Updated development and documentation tooling, including `pytest` 9.0.3, `Sphinx` 9.1, `sphinx-autobuild` 2025.8, `coverage` 7.14, `tox` 4.54, and `requests-mock` 1.12.
* Updated CI and local Docker test coverage to a representative Python 3.12, 3.13, and 3.14 matrix.
Expand Down
34 changes: 29 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,33 @@ print(assembly_response.data.get('assembly_id'))
print(assembly_response.data['assembly_id'])
```

## Example
## Async usage

For fully working examples, take a look at [`examples/`](https://github.com/transloadit/python-sdk/tree/HEAD/examples).
```python
from transloadit.async_client import AsyncTransloadit

async with AsyncTransloadit("TRANSLOADIT_KEY", "TRANSLOADIT_SECRET") as tl:
response = await tl.get_assembly(assembly_id="abc")
print(response.data["ok"])

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

The async client keeps polling on `asyncio.sleep`. Resumable uploads still use the existing TUS client, but are offloaded with `asyncio.to_thread()` so the event loop stays responsive.

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

## Examples

For copy/paste runnable 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 @@ -60,17 +84,17 @@ This script will:
- 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.14 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.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.14 -- 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
28 changes: 25 additions & 3 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,31 @@ Usage
# or
print(assembly_response.data['assembly_id'])

Example
-------
Async usage
-----------

.. code:: python

from transloadit.async_client import AsyncTransloadit

async with AsyncTransloadit('TRANSLOADIT_KEY', 'TRANSLOADIT_SECRET') as tl:
response = await tl.get_assembly(assembly_id='abc')
print(response.data['ok'])

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)

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

Examples
--------

For copy/paste runnable examples, take a look at `examples/`_.

For fully working examples, take a look at `examples/`_.
The examples cover sync uploads, async uploads, resumable uploads, Template usage,
sync and async Template lifecycle management, and Smart CDN URL signing.

.. _examples/: https://github.com/transloadit/python-sdk/tree/HEAD/examples
33 changes: 32 additions & 1 deletion docs/source/transloadit.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,38 @@ transloadit.request module
:undoc-members:
:show-inheritance:

transloadit.async_client module
-------------------------------

.. automodule:: transloadit.async_client
:members:
:undoc-members:
:show-inheritance:

transloadit.async_assembly module
----------------------------------

.. automodule:: transloadit.async_assembly
:members:
:undoc-members:
:show-inheritance:

transloadit.async_template module
----------------------------------

.. automodule:: transloadit.async_template
:members:
:undoc-members:
:show-inheritance:

transloadit.async_request module
--------------------------------

.. automodule:: transloadit.async_request
:members:
:undoc-members:
:show-inheritance:

transloadit.response module
---------------------------

Expand All @@ -57,4 +89,3 @@ transloadit.response module
:undoc-members:
:show-inheritance:


41 changes: 41 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Transloadit Python SDK Examples

Run the examples from the repository root after installing the project:

```bash
poetry install
export TRANSLOADIT_KEY="YOUR_TRANSLOADIT_KEY"
export TRANSLOADIT_SECRET="YOUR_TRANSLOADIT_SECRET"
```

## Quickstart Examples

```bash
poetry run python examples/image_resize.py
poetry run python examples/async_image_resize.py
poetry run python examples/resumable_upload.py
poetry run python examples/assembly_with_template.py
poetry run python examples/template_lifecycle.py
poetry run python examples/async_template_lifecycle.py
poetry run python examples/smart_cdn_url.py
```

`smart_cdn_url.py` only signs a URL locally. The other quickstart examples contact
Transloadit and may create temporary Assemblies or Templates in your account.

These quickstart examples run in CI against a dedicated Transloadit test account, so they
are kept in sync with the SDK and API.

## Advanced Examples

These examples require pre-created Templates and, depending on your Template, third-party
provider configuration:

```bash
export TRANSLOADIT_TTS_TEMPLATE_ID="YOUR_TEMPLATE_ID"
poetry run python examples/file_to_tts.py

export TRANSLOADIT_TRANSCRIBE_TEMPLATE_ID="YOUR_TRANSCRIBE_TEMPLATE_ID"
export TRANSLOADIT_TRANSLATE_TEMPLATE_ID="YOUR_TRANSLATE_TEMPLATE_ID"
poetry run python examples/video_translator.py
```
78 changes: 78 additions & 0 deletions examples/assembly_with_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""Create a temporary Template and use it to process an uploaded image.

Run from the repository root:

TRANSLOADIT_KEY=xxx TRANSLOADIT_SECRET=yyy poetry run python examples/assembly_with_template.py
"""

import os
from pathlib import Path
from uuid import uuid4

from transloadit.client import Transloadit


def get_credentials():
key = os.getenv("TRANSLOADIT_KEY")
secret = os.getenv("TRANSLOADIT_SECRET")
if not key or not secret:
raise RuntimeError("Please set TRANSLOADIT_KEY and TRANSLOADIT_SECRET.")
return key, secret


def get_example_image_path():
return Path(__file__).resolve().parent / "fixtures" / "lol_cat.jpg"


def extract_template_id(response_data):
template_id = response_data.get("id") or response_data.get("template_id")
if not template_id:
raise RuntimeError(f"Template response did not contain an id: {response_data}")
return template_id


def first_result_url(response_data, step_name):
results = (response_data.get("results") or {}).get(step_name) or []
if not results:
raise RuntimeError(f"No results found for step {step_name!r}: {response_data}")
url = results[0].get("ssl_url") or results[0].get("url")
if not url:
raise RuntimeError(f"No result URL found for step {step_name!r}: {response_data}")
return url


def create_resize_template(client):
template = client.new_template(f"python-sdk-template-example-{uuid4().hex[:12]}")
template.add_step(
"resize",
"/image/resize",
{
"use": ":original",
"width": 120,
"height": 120,
"resize_strategy": "fit",
"format": "png",
},
)
return extract_template_id(template.create().data)


def main():
key, secret = get_credentials()
client = Transloadit(key, secret)
template_id = create_resize_template(client)

try:
assembly = client.new_assembly({"template_id": template_id})
with get_example_image_path().open("rb") as upload:
assembly.add_file(upload, "image")
response = assembly.create(wait=True, resumable=False)

print("Assembly:", response.data.get("assembly_ssl_url") or response.data.get("assembly_url"))
print("Template result:", first_result_url(response.data, "resize"))
finally:
client.delete_template(template_id)


if __name__ == "__main__":
main()
62 changes: 62 additions & 0 deletions examples/async_image_resize.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Upload and resize an image with the async client.

Run from the repository root:

TRANSLOADIT_KEY=xxx TRANSLOADIT_SECRET=yyy poetry run python examples/async_image_resize.py
"""

import asyncio
import os
from pathlib import Path

from transloadit.async_client import AsyncTransloadit


def get_credentials():
key = os.getenv("TRANSLOADIT_KEY")
secret = os.getenv("TRANSLOADIT_SECRET")
if not key or not secret:
raise RuntimeError("Please set TRANSLOADIT_KEY and TRANSLOADIT_SECRET.")
return key, secret


def get_example_image_path():
return Path(__file__).resolve().parent / "fixtures" / "lol_cat.jpg"


def first_result_url(response_data, step_name):
results = (response_data.get("results") or {}).get(step_name) or []
if not results:
raise RuntimeError(f"No results found for step {step_name!r}: {response_data}")
url = results[0].get("ssl_url") or results[0].get("url")
if not url:
raise RuntimeError(f"No result URL found for step {step_name!r}: {response_data}")
return url


async def main():
key, secret = get_credentials()

async with AsyncTransloadit(key, secret) as client:
assembly = client.new_assembly()
with get_example_image_path().open("rb") as upload:
assembly.add_file(upload, "image")
assembly.add_step(
"resize",
"/image/resize",
{
"use": ":original",
"width": 120,
"height": 120,
"resize_strategy": "fit",
"format": "png",
},
)
response = await assembly.create(wait=True, resumable=False)

print("Assembly:", response.data.get("assembly_ssl_url") or response.data.get("assembly_url"))
print("Resized image:", first_result_url(response.data, "resize"))


if __name__ == "__main__":
asyncio.run(main())
Loading
Loading