Metadata-Version: 2.4
Name: fileboy
Version: 0.2.0
Summary: Official Python client for Fileboy: upload files and videos, build image transform URLs, read video data.
Author: Fileboy
License: MIT
Project-URL: Homepage, https://fileboy.io/docs/libraries/python
Project-URL: Documentation, https://fileboy.io/docs/libraries/python
Project-URL: Changelog, https://github.com/CD2/Fileboy/blob/master/packages/python/CHANGELOG.md
Keywords: fileboy,file upload,image resize,video hosting,sdk
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Typing :: Typed
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.25
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: requests>=2.25; extra == "dev"
Dynamic: license-file

# Fileboy for Python

The official Python client for [Fileboy](https://fileboy.io): upload files and videos, build image
URLs that resize on the fly, and read video data. Its only dependency is `requests`.

## Install

```bash
pip install fileboy
```

Python 3.8 or newer.

## Quick start

```python
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:

```python
# 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",
)
```

```python
# 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=)` |
| Upload from a path | `upload_file_from_path(path, ...)` |
| Upload from a URL | `upload_file_from_url(url, name=, metadata=, expect=)` |
| 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=)` |
| 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)` |
| 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)` |
| Manage videos, labels, usage (private key) | `list_videos`, `update_video`, `delete_video`, `list_labels`, `create_label`, `delete_label`, `get_usage` |
| 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`). `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.

```python
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")
client.update_video(video_id, name="launch.mp4")
client.delete_video(video_id)

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

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

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.

### Image transforms

`transforms=` takes a list of dicts applied in order. The image service supports `resize`,
`format`, `quality`, `background` and `invert`:

```python
[
    {"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": "format", "format": "webp"},                              # jpeg png webp tiff bmp
    {"type": "quality", "value": 75},
    {"type": "background", "color": "#fff"},
    {"type": "invert"},
]
```

You can also pass a dict of `name: value`, such as `{"resize": "300x_", "format": "webp", "quality": 75}`, or a ready-made string.

## 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

```bash
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:

```python
# 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](https://pypi.org) account that owns `fileboy` (the first upload claims the name) and an API token (account settings, scope: that project once it exists). Try [TestPyPI](https://test.pypi.org) 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.
