Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
95 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
07ff964
Add Python TUS devdock resume coverage
kvz Jun 7, 2026
9edef72
Add request lifecycle hooks and retry proof
kvz Jun 7, 2026
14970da
Add TUS request lifecycle devdock proof
kvz Jun 7, 2026
7d64c2e
Add API2 upload callback proof
kvz Jun 7, 2026
8958224
Add API2 custom request headers proof
kvz Jun 7, 2026
91a9c70
Add API2 request ID headers proof
kvz Jun 7, 2026
ed7fedf
Add API2 upload body headers proof
kvz Jun 7, 2026
62e2e3b
Add API2 terminate upload proof
kvz Jun 7, 2026
c275fec
Add TUS creation-with-upload proof
kvz Jun 7, 2026
9f6638c
Add deferred-length TUS devdock proof
kvz Jun 7, 2026
1961c82
Add start option validation proof
kvz Jun 9, 2026
4640cdd
Add detailed error proof
kvz Jun 9, 2026
4708bfa
Fix detailed error header matching
kvz Jun 9, 2026
c4c1f37
Add relative Location proof
kvz Jun 9, 2026
db69a52
Add Python override PATCH proof
kvz Jun 9, 2026
20f0a90
Prove Python file URL storage
kvz Jun 9, 2026
03e1f06
Prove Python protocol selection
kvz Jun 9, 2026
b7af40f
Prove Python TUS abort upload
kvz Jun 9, 2026
7337b68
Prove Python TUS path input source
kvz Jun 9, 2026
6d8c8f1
Prove Python TUS retry state transitions
kvz Jun 9, 2026
0969ad6
Support Python TUS parallel upload concat
kvz Jun 10, 2026
04bc4fd
Generate the TUS termination retry runtime
kvz Jun 12, 2026
685de6c
Generate the chunk upload retry runtime
kvz Jun 12, 2026
bc5da81
Generate the async chunk upload retry runtime
kvz Jun 12, 2026
ef48a69
Drop the dead source-method assert from request_method_plan
kvz Jun 13, 2026
6e4e841
Trim generated Python TUS method overrides
kvz Jun 13, 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
61 changes: 61 additions & 0 deletions examples/api2-devdock-transloadit-assembly-upload/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""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 sys
from io import BytesIO
from pathlib import Path

from tusclient import client as tus

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from api2devdock import (
fail,
load_scenario,
scenario_bytes,
tus_url,
upload_metadata,
write_result,
)


def upload_with_tus(scenario, create_response):
upload_config = scenario["upload"]
endpoint_url = tus_url(upload_config, scenario, create_response)
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 main():
scenario = load_scenario(Path(__file__).with_name("api2-scenario.json"))
create_response = scenario["prepared"]["createResponse"]
upload_url = upload_with_tus(scenario, create_response)
write_result({"uploadUrl": upload_url})
print(
"Python TUS SDK devdock scenario {} uploaded to {}".format(
scenario["scenarioId"], upload_url
)
)


if __name__ == "__main__":
main()
155 changes: 155 additions & 0 deletions examples/api2-devdock-tus-abort-upload/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
"""Abort a TUS upload against the API2 devdock conformance server."""

import sys
from io import BytesIO
from pathlib import Path

from tusclient import client as tus
from tusclient.exceptions import TusUploadAborted
from tusclient.fingerprint.interface import Fingerprint
from tusclient.storage.interface import Storage

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from api2devdock import (
TusConformancePlanServer,
bool_value,
conformance_input_options,
conformance_input_source_bytes,
fail,
load_scenario,
object_value,
scenario_id,
string_value,
write_result,
)


class MemoryStorage(Storage):
def __init__(self):
self.values = {}

def get_item(self, key):
return self.values.get(key)

def set_item(self, key, value):
self.values[key] = value

def remove_item(self, key):
self.values.pop(key, None)


class FixedFingerprint(Fingerprint):
def __init__(self, fingerprint):
self.fingerprint = fingerprint

def get_fingerprint(self, fs):
return self.fingerprint


def upload_and_abort(conformance_scenario):
input_options = conformance_input_options(conformance_scenario)
content = conformance_input_source_bytes(conformance_scenario)
endpoint_url = string_value(input_options["endpointUrl"], "endpointUrl")
headers = object_value(input_options.get("headers", {}), "headers")
metadata = object_value(input_options["metadata"], "metadata")
override_patch_method = bool_value(
input_options.get("overridePatchMethod", False),
"overridePatchMethod",
)
runtime_setup = object_value(
conformance_scenario["runtimeSetup"],
"conformanceScenario.runtimeSetup",
)
abort_setup = object_value(
runtime_setup["abort"],
"conformanceScenario.runtimeSetup.abort",
)
terminate_upload_on_abort = bool_value(
abort_setup["terminateUpload"],
"conformanceScenario.runtimeSetup.abort.terminateUpload",
)

client_ref = {}
uploader_ref = {}

def on_abort_request(event):
active_client = client_ref.get("client")
active_uploader = uploader_ref.get("uploader")
if active_client is None:
fail("abort request observed before client was initialized")
active_client.abort_upload(active_uploader, False)

with TusConformancePlanServer(
conformance_scenario,
endpoint_url,
on_abort_request=on_abort_request,
) as conformance_server:
client = tus.TusClient(conformance_server.endpoint_url(), headers=headers)
client_ref["client"] = client

storage = None
fingerprint = input_options.get("fingerprint")
uploader_options = {}
if fingerprint is not None:
storage = MemoryStorage()
uploader_options.update(
{
"fingerprinter": FixedFingerprint(
string_value(fingerprint, "fingerprint"),
),
"store_url": True,
"url_storage": storage,
}
)

uploader = client.uploader(
file_stream=BytesIO(content),
chunk_size=len(content),
metadata=metadata,
override_patch_method=override_patch_method,
**uploader_options,
)
uploader_ref["uploader"] = uploader

try:
uploader.upload()
except TusUploadAborted:
pass
else:
fail("abort scenario completed without TusUploadAborted")

if terminate_upload_on_abort:
if not uploader.url:
fail("abort scenario requested termination before upload URL was known")
client.abort_upload(uploader, True)

conformance_server.assert_exhausted()
result = conformance_server.result()
result["completionKind"] = "aborted"
result["errorCalled"] = False
result["successCalled"] = False
result["uploadUrl"] = (
conformance_server.canonical_url(uploader.url) if uploader.url else None
)
if storage is not None and fingerprint is not None:
result["storedUrlAfterAbort"] = storage.get_item(fingerprint)
return result


def main():
scenario = load_scenario(Path(__file__).with_name("api2-scenario.json"))
conformance_scenario = object_value(
scenario["conformanceScenario"],
"conformanceScenario",
)
result = upload_and_abort(conformance_scenario)
write_result(result)
print(
"Python TUS SDK devdock scenario {} aborted the upload".format(
scenario_id(scenario),
)
)


if __name__ == "__main__":
main()
71 changes: 71 additions & 0 deletions examples/api2-devdock-tus-creation-with-upload/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""Create a Transloadit devdock TUS upload with bytes in the creation request."""

import sys
from io import BytesIO
from pathlib import Path

from tusclient import client as tus

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from api2devdock import (
fail,
load_scenario,
scenario_bytes,
scenario_id,
tus_url,
upload_metadata,
write_result,
)


def upload_with_creation_body(scenario, create_response):
upload_config = scenario["upload"]
content = scenario_bytes(upload_config)

if upload_config["chunkSize"] != "full-file":
fail("unsupported chunk size policy {!r}".format(upload_config["chunkSize"]))
if not upload_config["uploadDataDuringCreation"]:
fail("scenario does not enable uploadDataDuringCreation")

uploader = tus.TusClient(
tus_url(upload_config, scenario, create_response)
).create_upload_with_data(
len(content),
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("creation-with-upload did not expose an upload URL")
if uploader.offset != len(content):
fail(
"creation-with-upload accepted {} bytes, expected {}".format(
uploader.offset,
len(content),
)
)

return {
"acceptedBytes": uploader.offset,
"uploadUrl": uploader.url,
}


def main():
scenario = load_scenario(Path(__file__).with_name("api2-scenario.json"))
create_response = scenario["prepared"]["createResponse"]
result = upload_with_creation_body(scenario, create_response)
write_result(result)
print(
"Python TUS SDK devdock scenario {} uploaded during creation to {}".format(
scenario_id(scenario),
result["uploadUrl"],
)
)


if __name__ == "__main__":
main()
99 changes: 99 additions & 0 deletions examples/api2-devdock-tus-custom-request-headers/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""Send Transloadit devdock TUS custom request headers."""

import sys
from io import BytesIO
from pathlib import Path

from tusclient import client as tus
from tusclient.request_lifecycle import RequestLifecycleHooks

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from api2devdock import (
fail,
load_scenario,
scenario_bytes,
scenario_id,
tus_url,
upload_headers,
upload_metadata,
write_result,
)


def record_custom_request_headers(context, expected_headers):
observed = {}
for header_name, expected_value in expected_headers.items():
actual_value = context.headers.get(header_name)
if actual_value != expected_value:
fail(
"custom request header {} expected {!r}, got {!r}".format(
header_name,
expected_value,
actual_value,
)
)
observed[header_name] = actual_value
return observed


def upload_with_custom_request_headers(scenario, create_response):
upload_config = scenario["upload"]
expected_headers = upload_headers(scenario)
content = scenario_bytes(upload_config)
headers_by_method = {}
if upload_config["chunkSize"] != "full-file":
fail("unsupported chunk size policy {!r}".format(upload_config["chunkSize"]))

def before_request(context):
if context.method in ("POST", "PATCH"):
headers_by_method[context.method] = record_custom_request_headers(
context,
expected_headers,
)

uploader = tus.TusClient(
tus_url(upload_config, scenario, create_response),
headers=expected_headers,
request_hooks=RequestLifecycleHooks(before_request=before_request),
).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("custom request headers upload did not expose an upload URL")
if uploader.offset != len(content):
fail(
"custom request headers upload offset {}, expected {}".format(
uploader.offset,
len(content),
)
)
for method in ("POST", "PATCH"):
if method not in headers_by_method:
fail("custom request headers did not observe {} request".format(method))

return {
"headersByMethod": headers_by_method,
"uploadUrl": uploader.url,
}


def main():
scenario = load_scenario(Path(__file__).with_name("api2-scenario.json"))
create_response = scenario["prepared"]["createResponse"]
result = upload_with_custom_request_headers(scenario, create_response)
write_result(result)
print(
"Python TUS SDK devdock scenario {} sent custom request headers for {}".format(
scenario_id(scenario),
result["uploadUrl"],
)
)


if __name__ == "__main__":
main()
Loading
Loading