Skip to main content

Fileboy for Python

The official Python client for Fileboy: upload files and videos, build image URLs that resize on the fly, and read video data. Its only dependency is requests.

Install

pip install fileboy

Python 3.8 or newer.

Quick start

import os
import fileboy

client = fileboy.Client(public_key=os.environ["FILEBOY_PUBLIC_KEY"])

file = client.upload_file("report.pdf")   # a path, a binary file object or bytes
print(file.id)                            # store this

client.build_file_url(file.id, download=True)
client.build_image_url(file.id, transforms=[{"type": "resize", "width": 300}, {"type": "format", "format": "webp"}])
# https://api.fileboy.io/images/get/<id>?transform=resize:300x_;format:webp

Django and other frameworks

Configure once, then call fileboy.client() anywhere:

# settings.py (or an AppConfig.ready())
import os
import fileboy

fileboy.configure(
    public_key=os.environ["FILEBOY_PUBLIC_KEY"],
    # private_key=...,   only if your app uses signed requests
    # api_url="https://api.fileboy.io",
)
# views.py
import fileboy

def upload(request):
    file = fileboy.client().upload_file(request.FILES["file"], name=request.FILES["file"].name)
    ...

FILEBOY_PUBLIC_KEY, FILEBOY_PRIVATE_KEY and FILEBOY_API_URL are read from the environment when you do not set them.

What you can do

Task Method
Upload a path, file object or bytes upload_file(file, name=, mime=, metadata=, folder_id=, folder_path=)
Upload from a path upload_file_from_path(path, ...)
Upload from a URL upload_file_from_url(url, name=, metadata=, expect=, folder_id=, folder_path=)
File details fetch_file_data(id_or_label)
File URL build_file_url(id_or_label, filename=, download=)
Image URL with transforms build_image_url(id_or_label, transforms=, filename=, download=)
Download bytes download_file(id_or_label, range=), download_image(id_or_label, transforms=, range=)
Upload a video upload_video(file, name=, mime=, description=, tags=, metadata=)
Video details fetch_video_data(id), fetch_video_info(id)
Manifest and poster URLs build_video_manifest_url(id, type="hls"), build_video_poster_url(id)
Seek-preview thumbnails build_video_thumbnails_url(id) (WebVTT), build_video_thumbnails_url(id, format="json")
Custom poster upload_video_poster(video_id, image), download_video_poster(video_id)
Transcripts upload_video_transcript(id, file, language="English"), fetch_video_transcript(id, transcript_id)
List, update and delete files (private key) list_files(...), iter_files(...), update_file(id, ...), delete_file(id), delete_files(ids)
Folders (private key) list_folders(...), iter_folders(...), get_folder(id), create_folder(name, parent_id=), create_folder(path=), update_folder(id, ...), delete_folder(id, contents=), move_files(ids, folder_id)
Manage videos, labels, usage (private key) list_videos, update_video, delete_video, list_labels, create_label, delete_label, get_usage, get_storage_usage
A photo's EXIF data (private key) get_file_exif(id_or_label, personal=True) returns a FileExif
Expiring links you can revoke (private key) create_file_link(id, expires_in=), list_file_links(id), iter_file_links(id), revoke_file_link(id, link_id)
Signed access token create_access_token(read=, write=, upload=, expires_in=)

An id that is a UUID is used as is. Anything else is treated as a label in your app, and a value that already contains a + is passed through. Labels are created in the dashboard.

Uploads return a FileData dataclass (id, name, original_filename, mime, metadata, file_size, content_hash, signed, dies_at, created_at, folder_id, folder_path). upload_video returns a Video (id, name, raw_file_id). range is a (first_byte, last_byte) tuple.

Managing files

Listing, updating and deleting files, videos and labels needs your app's private key. With only the public key these methods raise ConfigurationError before any request is made.

client = fileboy.Client(public_key="PUBLIC", private_key="PRIVATE")

# One page, or every match across all pages
page = client.list_files(mime="image/", limit=50)
for file in page.items:
    print(file.name, file.labels)
more = client.list_files(cursor=page.next_cursor)  # next_cursor is None on the last page
for file in client.iter_files(metadata={"orderId": "42"}):
    print(file.id)

# Metadata is merged (None removes a key). Pass replace_metadata=True to replace it all.
client.update_file(file_id, name="new.png", metadata={"keep": "1", "drop": None},
                   add_labels=["hero"], remove_labels=["old"])
client.update_file("hero", name="renamed.png")  # an id without a hyphen is a label

client.delete_file(file_id)  # NotFoundError if it is already gone
result = client.delete_files([id_a, id_b])  # up to 100; result.deleted, .not_found, .failed

client.list_videos(status="ready", tags=["launch"], metadata={"orderId": "42"})
client.update_video(video_id, name="launch.mp4")
# The description shows on the watch page. Tags and metadata are private, for your own records.
video = client.update_video(video_id, description="Launch day", add_tags=["launch"], remove_tags=["draft"],
                            metadata={"orderId": "42", "old": None})
video.description, video.tags, video.metadata  # also in every video.* webhook
client.delete_video(video_id)

client.list_labels()
client.create_label("hero", file_id=file_id)
client.delete_label("hero")

# An image's colours: the dominant one, up to 16 main colours with their share of the image,
# vibrant and muted swatches, and a readable text colour (black or white) for each
palette = client.get_file_palette(file_id, colors=4)
palette["dominant"]["hex"], palette["colors"][0]["share"], palette["swatches"]["vibrant"]

usage = client.get_usage()  # usage.files, usage.videos, usage.storage_bytes, usage.limits.files

# A photo's camera, lens, exposure, date taken and GPS position (EXIF, IPTC and XMP)
exif = client.get_file_exif(file_id)
exif.fields.model, exif.fields.aperture, exif.fields.shutter_speed, exif.fields.date_taken
exif.fields.gps  # ExifGps(latitude=..., longitude=..., altitude=...) or None
exif.raw["exif"]["LensModel"]  # every tag read, by source
client.get_file_exif(file_id, personal=False)  # without GPS, place names, serial numbers or owner

# What the storage is made of, the biggest files and videos, and a daily total
storage = client.get_storage_usage(days=90, top=5)
storage.kinds["videoRenditions"].bytes      # also images, files, videoOriginals, imageTransforms...
storage.largest_files[0].id, storage.largest_videos[0].storage_bytes
[(day.date, day.storage_bytes) for day in storage.history]

Errors keep the API's code and status on the exception, and api_message holds the API's own message. NotFoundError, FileboyPermissionError/AuthenticationError and ValidationError tell you what went wrong without reading the code.

Folders

Folders organise files. They are never part of a URL, so moving or renaming one changes no links. Every file starts at the root. These calls need the private key, like the rest of this section.

shoes = client.create_folder(path="/Products/Shoes")  # creates what is missing, like mkdir -p
sale = client.create_folder("Sale", parent_id=shoes.id)  # ValidationError (folder_exists) if it is taken

file = client.upload_file("shoe.png", folder_path="/Products/Shoes")  # or folder_id=shoes.id
file.folder_id, file.folder_path

page = client.list_files(folder_id=shoes.id)  # only the files directly in it
client.list_files(folder_path="/Products", recursive=True)  # subfolders included
client.list_files(folder_id="root")  # files in no folder

client.update_file(file.id, folder_path="/Archive")  # one file; folder_id=None moves it to the root
client.move_files([id_a, id_b], shoes.id)  # up to 100; result.moved, .not_found, .folder
client.move_files([id_a], None)  # to the root

client.list_folders(parent_id="root")  # top level, by name; also name=, sort=, order=, limit=, cursor=
client.list_folders(path="/Products/Shoes").items  # [] or the one folder
client.get_folder(shoes.id).ancestors  # the folders above, for breadcrumbs

client.update_folder(shoes.id, name="Boots")
client.update_folder(shoes.id, parent_id=None)  # move it to the root

client.delete_folder(sale.id)  # only an empty folder, otherwise ValidationError (folder_not_empty)
client.delete_folder(shoes.id, contents="moveToParent")  # its files and folders move up a level
client.delete_folder(shoes.id, contents="delete")  # deletes the files too, at most 1000

Uploads made with only the public key, or an access token, can use folders that exist but cannot create them.

An expiring link is a URL to one file that works until it expires or you revoke it. Hand it to a customer, an auditor or a partner, and take it back if you need to. Needs the private key.

from datetime import datetime, timezone

# A link that works for 7 days
link = client.create_file_link(file_id, expires_in=7 * 24 * 3600, note="for the auditors")
print(link.url, link.expires_at)

# A resized WebP of an image, downloaded as logo.webp, until a set time
client.create_file_link("logo", expires_at=datetime(2026, 12, 1, 9, tzinfo=timezone.utc),
                        transform="resize:400x_;format:webp", download=True, filename="logo.webp")

page = client.list_file_links(file_id, status="active")  # newest first; status="all" is the default
for link in page.items:
    print(link.id, link.status, link.use_count, link.last_used_at)

client.revoke_file_link(file_id, link.id)  # stops working at once, safe to repeat

Give exactly one of expires_in (seconds) or expires_at (a datetime or ISO 8601 string), from one minute to 365 days away. A file can have at most 100 active links (ValidationError, too_many_links). Results are FileLink dataclasses (id, file_id, url, status, expires_at, revoked_at, transform, download, filename, note, created_via, created_at, last_used_at, use_count).

A link works until it expires or is revoked even if the file is "Signed URLs only" or the app requires signed reads. It does not make a public file private: the file's own URL keeps working. For a link per page view, sign_url is the better tool (no request, nothing stored); stored links are for links you hand out and might take back.

Image transforms

transforms= takes a list of dicts applied in order. Pixel sizes are whole numbers; where a percentage is allowed write {"pc": 50} (or "50pc"). Colours can be #rgb, #rrggbb, #rrggbbaa, rgb(...), rgba(...) or a CSS name like "white".

[
    # Size and shape
    {"type": "resize", "width": 200, "height": 200, "fit": "cover"},   # fit: contain cover fill inside outside
    {"type": "resize", "width": 400, "fit": "contain", "position": "top-left", "background": "#ffffff"},
    {"type": "resize", "width": 300, "height": 300, "focus": {"x": {"pc": 30}, "y": {"pc": 70}}},  # keep this point in view
    {"type": "resize", "width": 800, "upscale": False},                # never bigger than the original
    {"type": "resizeAR", "ratio": [16, 9], "fit": "contain", "background": "black"},
    {"type": "crop", "width": 400, "height": 300, "left": {"pc": 50}, "top": 120},     # centre of the area
    {"type": "crop", "width": {"pc": 50}, "height": {"pc": 50}, "position": "top-left"},
    {"type": "smartCrop", "width": 400, "height": 400},                # crops to the most detailed area
    {"type": "trim", "threshold": 10, "color": "white"},               # cuts off a plain border; both optional
    {"type": "rotate", "angle": 90},                                   # clockwise; other angles leave a background
    {"type": "flip", "direction": "x"},                                # x y xy
    {"type": "radius", "radius": 24},                                  # or "max" for a circle
    {"type": "pad", "padding": [10, 20], "background": "#fff"},        # all, [v, h] or [top, right, bottom, left]
    {"type": "border", "width": 4, "color": "#000"},
    {"type": "dpr", "ratio": 2},                                       # multiplies every pixel size
    # Look
    {"type": "blur", "amount": 5},                                     # 0 to 100
    {"type": "sharpen"},                                               # or "amount": 0 to 10
    {"type": "grayscale"},
    {"type": "saturate", "amount": 1.5},                               # 0 grey, 1 unchanged
    {"type": "brightness", "amount": 10},                              # -100 to 100
    {"type": "contrast", "amount": 10},                                # -100 to 100
    {"type": "hue", "degrees": 90},
    {"type": "gamma", "value": 1.2},
    {"type": "sepia"},                                                 # or "amount": 0 to 100
    {"type": "tint", "color": "#ff0000"},
    {"type": "opacity", "amount": 50},
    {"type": "pixelate", "size": 12},
    {"type": "invert"},
    # Layers: another image file of the same app, or text, drawn on top
    {"type": "overlay", "file": logo_id, "position": "bottom-right", "x": 20, "y": 20, "width": {"pc": 20}, "opacity": 80},
    {"type": "overlay", "file": logo_id, "tile": True, "gap": 40, "rotate": -30, "opacity": 25},  # a watermark pattern
    {"type": "overlay", "file": logo_id, "transforms": [{"type": "grayscale"}, {"type": "radius", "radius": "max"}]},
    {"type": "text", "text": "© 2026 Fileboy", "font": "sans-bold", "size": {"pc": 4}, "color": "#fff",
     "background": "#00000080", "padding": 8, "position": "bottom-left", "x": 16, "y": 16},
    # Output
    {"type": "format", "format": "auto"},                              # auto (AVIF, then WebP, when the browser takes them) avif webp jpeg png tiff bmp
    {"type": "format", "format": "json"},                              # the image's details as JSON, not an image
    {"type": "palette", "output": "css", "colors": 4},                 # the main colours as JSON (default) or CSS, not an image
    {"type": "quality", "value": 75},
    {"type": "lossless"},                                              # lossless WebP or AVIF
    {"type": "background", "color": "#fff"},                           # default for transforms that leave space
    {"type": "strip"},                                                 # re-encode without metadata
]

Overlays and text take position (default center), x and y (moved in from that edge, pixels or {"pc": n}, negative allowed), opacity (0 to 100), rotate, and tile with gap. An overlay is the id of an image file in the same app; width and height size it (one keeps its shape, both fit it inside the box) and transforms run on it first. Text is encoded for you (transforms.encode_text), in the fonts sans, sans-bold, sans-italic, mono or mono-bold, with size (pixels, or {"pc": n} of the image's height), color, background, padding, wrap and align. If the overlay file is set to signed URLs only, sign the image URL: the signature covers the overlay.

You can also pass a dict of name: value, such as {"resize": "300x_", "format": "webp", "grayscale": True}, or a ready-made string. The builder checks what it can before you make a request; the image service answers anything it cannot apply with a 400 that says why.

Signed URLs

If your app requires signed reads, sign URLs on your server with the private key:

url = client.sign_url(client.build_image_url(file_id, transforms={"resize": (400, 400)}), expires_in=600)
query = client.sign_video(video_id, expires_in=4 * 3600)  # add to any /videos/{id}/... URL

Signing makes no request. See https://fileboy.io/docs/delivery/signed-urls.

Share pages

A share page is a page anyone can open at https://fileboy.io/f/{id}, with a preview of the file and a download button. It is off until you turn it on, and it follows the file's read access:

client.update_file(file_id, share_page=True)
link = client.build_share_page_url(file_id)  # https://fileboy.io/f/2a86...

# A file set to Signed URLs only, or any file of an app that requires signed reads, only opens
# through a signed share link. Its preview and download stop working when the link does.
signed_link = client.sign_share_page_url(file_id, expires_in=7 * 24 * 3600)

update_file(file_id, share_page=False) turns the page off, and every link to it stops working. ManagedFile.share_page says whether it is on. See https://fileboy.io/docs/delivery/share-pages.

Errors

Every error is a FileboyError with status, code (the API's error code) and body.

Class When
AuthenticationError 401
FileboyPermissionError 403, including plan limits
NotFoundError 404
RateLimitError 429
BadRequestError other 4xx, such as a file that is too large
ValidationError the management API rejected your values (a BadRequestError)
ServerError 5xx
FileboyConnectionError timeouts and network failures
InvalidArgumentError, ConfigurationError bad arguments, before any request

FileboyPermissionError and FileboyConnectionError are prefixed so they do not shadow the Python built-ins PermissionError and ConnectionError.

Notes

  • File, image and video reads need no authorization, so URL builders never sign anything.
  • The shared demo key only accepts small images and cannot upload video.
  • The client uses a requests.Session, so connections are reused between calls.

Development

pip install -e ".[dev]"
pytest

The tests run against a local fake server. The transform builder is also checked against packages/sdk-fixtures/transforms.json, the same cases the Go image parser is tested with.

MIT licensed.

Webhooks

Verify the requests Fileboy sends to your endpoints. Use the raw body, exactly as received:

# Django
import fileboy
from django.http import HttpResponse, HttpResponseBadRequest
from django.views.decorators.csrf import csrf_exempt

@csrf_exempt
def fileboy_webhook(request):
    try:
        event = fileboy.verify_webhook(request.body, request.headers, settings.FILEBOY_WEBHOOK_SECRET)
    except fileboy.WebhookVerificationError as error:
        return HttpResponseBadRequest(error.code)  # "invalid_signature", "timestamp_out_of_tolerance", ...
    if event["type"] == "video.ready":
        ...  # event["resource"]["urls"]["hls"], event["resource"]["duration"]
    return HttpResponse(status=200)

verify_webhook returns the parsed event (a dict) or raises WebhookVerificationError. The timestamp must be within 5 minutes (tolerance_seconds= changes that). Events carry a unique id that is the same on every retry, so use it to ignore duplicates. fileboy.sign_webhook(body, timestamp, secret) builds a signature for your own tests.

Publishing (maintainers)

You need a PyPI account that owns fileboy (the first upload claims the name) and an API token (account settings, scope: that project once it exists). Try TestPyPI first.

  1. Bump the version in src/fileboy/_version.py and pyproject.toml (keep them equal) and add a section to CHANGELOG.md.
  2. pip install -e '.[dev]' && pytest must pass.
  3. python -m pip install build twine && rm -rf dist && python -m build && twine check dist/*
  4. Rehearse: twine upload --repository testpypi dist/*.
  5. Publish: twine upload dist/* with username __token__ and the API token as the password.
  6. Tag the commit: git tag python-vX.Y.Z && git push origin python-vX.Y.Z.

PyPI never lets you reuse a version number, so bump it for every fix.

Metadata

Release files for fileboy 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for fileboy 0.3.0
File Size Uploaded Provenance Actions
fileboy-0.3.0.tar.gz 59.4 kB Without provenance Details for fileboy-0.3.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for fileboy 0.3.0
File Interpreter ABI Platform Actions
fileboy-0.3.0-py3-none-any.whl Python 3 none any Details for fileboy-0.3.0-py3-none-any.whl

Total release size: 102.6 kB

Release files / fileboy-0.3.0.tar.gz

Download URL fileboy-0.3.0.tar.gz
Size 59.4 kB
Tags Source
SHA-256 checksum
How to use checksums
1536da9b041e4e1421bbefa12570d79bc0c27aae1bff05445d84cc229170a268
BLAKE2b-256 checksum
How to use checksums
d5e612fe093cbbdf0048a95f54461feb627136c2b560bf02e0da1357f478f1b4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.9.6

Release files / fileboy-0.3.0-py3-none-any.whl

Download URL fileboy-0.3.0-py3-none-any.whl
Size 43.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d0859a00d0ec94e23ac1575961bba9d2c3c6fcfb3181e59abb983479ffd23e66
BLAKE2b-256 checksum
How to use checksums
3498e6998b8c907309e81c264f8773141c0c7403746d2d4b7d9bc011322da90c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.9.6

Security for fileboy 0.3.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page