Install
pip install requests tuspy
export YUNGLE_API_KEY=yk_live_…tuspy is the Python client for tus, the resumable upload protocol Yungle uses for file bytes. The key needs transfers:write.
The script
#!/usr/bin/env python3
"""Send files with Yungle: yungle_send.py FILE... [--to EMAIL]..."""
import argparse
import os
import sys
import requests
API = "https://yungle.co/api/v1"
AUTH = {"Authorization": f"Bearer {os.environ['YUNGLE_API_KEY']}"}
import os
from tusclient import client as tus
MiB = 1024 * 1024
def upload(tus_endpoint, target, path):
"""Stream one file to Yungle, resumably."""
size = os.path.getsize(path)
# At least one server part per request, or the offset never advances.
chunk = max(64 * MiB, (size // (9000 * MiB) + 2) * MiB)
# The token authorizes every request (HEAD and PATCH too), so it goes in
# the client-wide headers, not only in the metadata.
tc = tus.TusClient(tus_endpoint, headers={"x-yungle-upload-token": target["uploadToken"]})
uploader = tc.uploader(
path,
chunk_size=chunk,
retries=5,
retry_delay=5,
metadata={
"fileId": target["id"],
"token": target["uploadToken"],
"filename": target["name"],
},
)
uploader.upload()
def main():
parser = argparse.ArgumentParser()
parser.add_argument("files", nargs="+")
parser.add_argument("--to", action="append", default=[])
parser.add_argument("--title")
args = parser.parse_args()
# 1. Register. A draft: nothing is live, nobody is emailed.
created = requests.post(
f"{API}/transfers",
headers=AUTH,
json={
"title": args.title,
"files": [
{"name": os.path.basename(p), "size": os.path.getsize(p)}
for p in args.files
],
},
)
created.raise_for_status()
created = created.json()
# 2. Upload each file to its own target, in the order they were registered.
for target, path in zip(created["files"], args.files):
print(f"uploading {target['name']}…", file=sys.stderr)
upload(created["tusEndpoint"], target, path)
# 3. Send. Safe to retry — nobody is emailed twice.
body = {"recipients": args.to} if args.to else {}
sent = requests.post(
f"{API}/transfers/{created['transfer']['id']}/finalize",
headers=AUTH,
json=body,
)
sent.raise_for_status()
sent = sent.json()
print(sent["transfer"]["url"])
if sent["notified"]:
print("emailed " + ", ".join(sent["notified"]), file=sys.stderr)
if __name__ == "__main__":
main()python3 yungle_send.py report.pdf data.zip --to client@example.com --title "Q3 delivery"The parts that are easy to get wrong
- The chunk size. The upload service commits in parts of about 32 MiB and answers each request with the last committed boundary. A smaller chunk never moves that boundary, so the upload spins without progressing. The helper uses 64 MiB, or more for very large files.
- The token goes in the headers.
x-yungle-upload-tokenauthorizes every request, HEAD and PATCH included. The metadata is only sent on the first one. sizemust be exact. Registering a size that differs from the bytes you upload fails the file instead of storing a partial one.- Upload tokens are valid for two hours to start in. A long upload already running is not cut off; registering today and uploading tomorrow is.
Checking downloads afterwards is one more request:
GET /transfers/{id}/downloads. There are no webhooks yet, so poll. The Quickstart shows it in Python.