Send files from Python

One complete script: register the files, upload them resumably, send the transfer and print the link. About sixty lines, two dependencies, and it handles files of any size your plan allows.

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-token authorizes every request, HEAD and PATCH included. The metadata is only sent on the first one.
  • size must 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.