Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
3892a58
Add generated TUS protocol canary
kvz May 26, 2026
f4e81cd
Fetch LFS fixtures in CI
kvz May 26, 2026
dea66fa
Make URL storage test portable
kvz May 26, 2026
e6d6029
Regenerate TUS protocol contract fixture
kvz May 26, 2026
e45afe6
Regenerate TUS feature contract fixture
kvz May 26, 2026
3104c05
Regenerate upload body protocol fixture
kvz May 27, 2026
329e116
Assert generated TUS upload events
kvz May 28, 2026
89a5099
Cover TUS request lifecycle conformance
kvz May 28, 2026
54c7990
Cover TUS abort conformance
kvz May 29, 2026
bdf0180
Cover TUS URL storage conformance
kvz May 29, 2026
562998b
Cover TUS relative Location conformance
kvz May 29, 2026
744fffa
Refresh TUS input source contract
kvz May 29, 2026
d9ddd68
Refresh TUS retry state contract
kvz May 29, 2026
beae40c
Refresh TUS URL storage contract
kvz May 29, 2026
2450c80
Refresh TUS protocol selection contract
kvz May 29, 2026
42eed31
Refresh TUS start validation contract
kvz May 29, 2026
f4a1e15
Update detailed error conformance
kvz May 29, 2026
4daa6dc
Expose generated conformance scenarios
kvz May 31, 2026
bc8ec4a
Add generated conformance event canary
kvz May 31, 2026
1f63608
Emit generated TUS runtime progress events
kvz May 31, 2026
7e45502
Support generated resume cleanup canary
kvz May 31, 2026
eb384a5
Cover generated deferred-length runtime events
kvz May 31, 2026
ce12bef
Assert generated runtime request headers
kvz May 31, 2026
da33ef1
Regenerate TUS event policy fixture
kvz May 31, 2026
b635e65
Regenerate TUS event contract
kvz Jun 1, 2026
79dc4d8
Honor generated TUS event policy
kvz Jun 1, 2026
e4873ee
Update generated TUS retry events
kvz Jun 1, 2026
d4dd289
Add generated TUS proof profile canaries
kvz Jun 1, 2026
7b035d5
Update generated TUS execution hints
kvz Jun 1, 2026
8540222
Use generated TUS execution hints in runtime tests
kvz Jun 1, 2026
b9c47cc
Expose TUS request-start cancellation hints
kvz Jun 1, 2026
ea0e2fc
Expose TUS parallel request gates
kvz Jun 1, 2026
2a026b3
Expose TUS managed upload contract
kvz Jun 1, 2026
888ab9f
Expose managed upload proof cases
kvz Jun 1, 2026
9dd5b88
Update managed upload proof fixture
kvz Jun 1, 2026
fba9b53
Update managed upload proof fixture
kvz Jun 1, 2026
bbb931b
Update managed upload proof fixture
kvz Jun 1, 2026
314b70c
Update managed upload proof fixture
kvz Jun 1, 2026
4c9dff0
Update generated protocol contract fixture
kvz Jun 1, 2026
35b69bb
Update generated managed upload contract
kvz Jun 1, 2026
fbcd3ee
Add devdock TUS upload example
kvz Jun 1, 2026
2b9ee70
Cap aiohttp below 3.14
kvz Jun 1, 2026
d767ccf
Emit devdock example result
kvz Jun 1, 2026
a43105d
Normalize generated request facts
kvz Jun 2, 2026
59ca230
Regenerate TUS runtime event proofs
kvz Jun 3, 2026
e4467d0
Use generated TUS default headers
kvz Jun 3, 2026
fa512b4
Regenerate Python TUS default header fixtures
kvz Jun 3, 2026
dbaa0ee
Add generated TUS request ID proof
kvz Jun 4, 2026
d2bc87e
Regenerate TUS deferred length proofs
kvz Jun 4, 2026
71d46b3
Regenerate TUS event alternatives
kvz Jun 4, 2026
3accbcc
Regenerate TUS extra event prefixes
kvz Jun 4, 2026
70b8a03
Regenerate Python TUS event prefix policy
kvz Jun 4, 2026
082b811
Regenerate Python TUS event key helpers
kvz Jun 4, 2026
a856670
Use generic TUS extra event matching policy
kvz Jun 4, 2026
05f777f
Regenerate Python TUS event key helpers
kvz Jun 4, 2026
0cbe27e
Use generated TUS fixture event keys
kvz Jun 4, 2026
bd69af7
Require generated TUS runtime event policy
kvz Jun 4, 2026
dc7dfe9
Require generated TUS runtime execution keys
kvz Jun 4, 2026
d8565dd
Regenerate TUS retry decision fixtures
kvz Jun 4, 2026
25a0f73
Regenerate TUS event kind fixtures
kvz Jun 4, 2026
2e0bd48
Regenerate TUS completion fact fixtures
kvz Jun 4, 2026
24571e9
Regenerate TUS execution phase fixtures
kvz Jun 4, 2026
0b4f669
Regenerate TUS source and URL fixtures
kvz Jun 4, 2026
1015ee1
Regenerate TUS input option fixtures
kvz Jun 4, 2026
be9c4d2
Regenerate TUS runtime setup fixtures
kvz Jun 4, 2026
17d94b5
Drop raw input from TUS generated fixtures
kvz Jun 4, 2026
c4d9d21
Use generated before-start runtime facts
kvz Jun 4, 2026
60ea90b
Regenerate TUS protocol fixtures
kvz Jun 5, 2026
42b1cd3
Regenerate TUS protocol response fixtures
kvz Jun 6, 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
2 changes: 2 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ jobs:

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

- name: Set up Python
uses: actions/setup-python@v6
Expand Down
142 changes: 142 additions & 0 deletions examples/api2-devdock-transloadit-assembly-upload/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
"""Upload to a Transloadit devdock Assembly using tus-py-client.

This example is intentionally checked into the SDK repository. API2 owns the
scenario JSON and prepares the live Transloadit Assembly; this file only shows
ordinary tus-py-client usage against the injected TUS endpoint.
"""

import json
import os
from io import BytesIO
from pathlib import Path

from tusclient import client as tus


def fail(message):
raise RuntimeError(message)


def load_scenario():
configured_path = os.environ.get("API2_SDK_EXAMPLE_SCENARIO")
scenario_path = (
Path(configured_path) if configured_path else Path(__file__).with_name("api2-scenario.json")
)
with scenario_path.open(encoding="utf-8") as scenario_file:
return json.load(scenario_file)


def read_path(value, path_parts, label):
current = value
for part in path_parts:
if isinstance(current, list) and isinstance(part, int):
if part >= len(current):
fail("{} path {!r} index {} is out of range".format(label, path_parts, part))
current = current[part]
continue

if isinstance(current, dict) and isinstance(part, str):
if part not in current:
fail("{} path {!r} is missing key {!r}".format(label, path_parts, part))
current = current[part]
continue

fail("{} path {!r} cannot read {!r} from {!r}".format(label, path_parts, part, current))

return current


def resolve_value(value_spec, context, label):
if "value" in value_spec:
return value_spec["value"]

source = value_spec.get("source")
if not isinstance(source, dict):
fail("{} value spec has no literal value or source".format(label))

root = source.get("root")
if root not in context:
fail("{} value source root {!r} is unavailable".format(label, root))

path_parts = source.get("path") or []
if not isinstance(path_parts, list):
fail("{} value source path must be a list".format(label))

return read_path(context[root], path_parts, label)


def scenario_bytes(upload_config):
source = upload_config["source"]
if source["kind"] != "bytes":
fail("unsupported scenario source kind {!r}".format(source["kind"]))
if source["encoding"] != "utf8":
fail("unsupported scenario source encoding {!r}".format(source["encoding"]))
return source["value"].encode("utf-8")


def scalar_string(value):
if value is None:
return "null"
if isinstance(value, bool):
return "true" if value else "false"
return str(value)


def upload_metadata(upload_config, scenario, create_response):
context = {"createResponse": create_response, "scenario": scenario}
metadata = {}
for field in upload_config["metadata"]:
metadata[field["name"]] = scalar_string(
resolve_value(field["value"], context, field["name"])
)
return metadata


def upload_with_tus(scenario, create_response):
upload_config = scenario["upload"]
context = {"createResponse": create_response, "scenario": scenario}
endpoint_url = scalar_string(resolve_value(upload_config["tusUrl"], context, "tusUrl"))
content = scenario_bytes(upload_config)
if upload_config["chunkSize"] != "full-file":
fail("unsupported chunk size policy {!r}".format(upload_config["chunkSize"]))

uploader = tus.TusClient(endpoint_url).uploader(
file_stream=BytesIO(content),
chunk_size=len(content),
metadata=upload_metadata(upload_config, scenario, create_response),
retries=upload_config["retries"],
)
uploader.upload()

if not uploader.url:
fail("TUS upload did not expose an upload URL")
if uploader.offset != len(content):
fail("TUS upload offset {}, expected {}".format(uploader.offset, len(content)))

return uploader.url


def write_result(upload_url):
result_path = os.environ.get("API2_SDK_EXAMPLE_RESULT")
if not result_path:
return

with Path(result_path).open("w", encoding="utf-8") as result_file:
json.dump({"uploadUrl": upload_url}, result_file, indent=2)
result_file.write("\n")


def main():
scenario = load_scenario()
create_response = scenario["prepared"]["createResponse"]
upload_url = upload_with_tus(scenario, create_response)
write_result(upload_url)
print(
"Python TUS SDK devdock scenario {} uploaded to {}".format(
scenario["scenarioId"], upload_url
)
)


if __name__ == "__main__":
main()
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
install_requires=[
'requests>=2.18.4',
'tinydb>=3.5.0',
'aiohttp>=3.6.2'
'aiohttp>=3.6.2,<3.14'
],
extras_require={
'test': [
Expand Down
Loading
Loading