Skip to content
Draft
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
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 ###
* @TODO before merging: expand the SDK surface so this release supports all Transloadit API endpoints, and update these release notes with the final endpoint coverage.
* **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.
* 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, async form fields match sync boolean serialization, async TUS cancellation waits for worker cleanup, async polling rate-limit retries reset after successful polls, 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