Metadata-Version: 2.4
Name: fileboy
Version: 0.1.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)` |
| 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.

### 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 |
| `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.
* There is no endpoint yet to update, delete or list files, so the client has no such methods.
* 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.
