Metadata-Version: 2.5
Name: apulodi
Version: 0.1.0
Summary: Official Python SDK for the APULODI file storage API — direct-to-storage uploads, multipart, transformations and signed delivery.
Project-URL: Homepage, https://apulodi.com/docs/sdk
Project-URL: Repository, https://github.com/apulodi/apulodi
Author: APULODI
License: MIT
Keywords: apulodi,file-storage,multipart,sdk,uploads
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.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 :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: httpx<0.29,>=0.27
Provides-Extra: async
Requires-Dist: anyio>=4; extra == 'async'
Provides-Extra: dev
Requires-Dist: anyio>=4; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Description-Content-Type: text/markdown

# apulodi — official Python SDK for APULODI

Typed, dependency-light Python client for the [APULODI](https://apulodi.com) file
storage API: direct-to-storage uploads, automatic multipart for big files,
transformations, folders, usage, and signed webhooks. Mirrors the official
TypeScript SDK (`@apulodi/sdk`) endpoint-for-endpoint.

## Installation

```bash
pip install apulodi
```

Requires Python 3.9+. The only runtime dependency is `httpx`.

Server-side only: your API key is a secret credential — never ship it to
browsers, never commit it.

## Quickstart

```python
from apulodi import Apulodi

apulodi = Apulodi(api_key="apk_test_...")  # or Apulodi(api_key=..., base_url="http://localhost:3000")

# One call performs the full flow: initiate → direct-to-storage PUT → complete.
file = apulodi.files.upload(
    open("avatar.jpg", "rb"),          # bytes / bytearray / binary file object
    file_name="avatar.jpg",
    content_type="image/jpeg",
    path="users/avatars",              # logical folder, created automatically
    metadata={"source": "my-app"},
)
print(file["id"], file["status"])      # file_... uploaded
```

## Files

```python
# List with filters (cursor pagination envelope).
page = apulodi.files.list({"limit": 20, "path": "users", "search": "avatar"})

# Iterate over everything matching a filter, following cursors for you.
for file in apulodi.files.iterate({"contentType": "image/png"}):
    print(file["filename"], file["size"])

# Metadata / lifecycle.
file = apulodi.files.get("file_...")
apulodi.files.update("file_...", {"filename": "renamed.png", "visibility": "public"})
apulodi.files.copy("file_...", path="backups")
apulodi.files.delete("file_...")           # soft-delete + storage removal
apulodi.files.restore("file_...")          # while the object still exists

# Versioned content replacement.
apulodi.files.replace("file_...", new_bytes, content_type="image/jpeg")

# Short-lived signed download URL — bytes go straight from storage to the user.
signed = apulodi.files.download_url("file_...", expires_in_seconds=300)
```

## Transformations (images, video, audio)

```python
# Idempotent: identical params return the same variant.
variant = apulodi.files.transform("file_...", width=640, format="webp", quality=80)

# Processing is async — poll until ready (or wait for the file.processed webhook).
variant = apulodi.files.wait_for_variant("file_...", variant["id"], timeout=30)

if variant["status"] == "ready":
    signed = apulodi.files.variant_download_url("file_...", variant["id"], 3600)
```

## Multipart (explicit control)

```python
result = apulodi.uploads.create_multipart("big.mp4", "video/mp4", size=250_000_000)
session, file = result["session"], result["file"]

parts = []
with open("big.mp4", "rb") as f:
    for part_meta in session["parts"]:
        chunk = f.read(session["partSize"])
        parts.append(apulodi.uploads.upload_part(session, part_meta["partNumber"], chunk))

file = apulodi.uploads.complete(session["id"], parts)
```

`apulodi.files.upload()` switches to this flow automatically above 8 MiB — and
aborts the session cleanly if a part fails.

## Webhooks

```python
from apulodi import verify_webhook_signature

# In your Flask/FastAPI/Django route — verify over the RAW body:
ok = verify_webhook_signature(secret, raw_body, request.headers["APULODI-Signature"])
if not ok:
    return "invalid signature", 401
```

Constants-time comparison, `t=<unix>,v1=<hex>` parsing, 5-minute replay
window — byte-for-byte compatible with deliveries signed by the APULODI
platform (and with the TypeScript SDK's verifier).

Manage endpoints programmatically:

```python
result = apulodi.webhooks.create("https://example.com/hooks/apulodi")
secret = result["secret"]        # shown EXACTLY ONCE — store it now

apulodi.webhooks.list()
apulodi.webhooks.deliveries("wh_...")          # why didn't I get my event?
apulodi.webhooks.redeliver("wh_...", "dl_...")
```

## Errors

Every non-2xx response, network failure and timeout raises `ApulodiError`:

```python
from apulodi import ApulodiError

try:
    apulodi.files.get("file_missing")
except ApulodiError as e:
    e.status            # 404
    e.code              # "FILE_NOT_FOUND"
    e.details           # structured extras, if the API sent any
    e.is_client_error   # True — the request was wrong, retrying won't help
    e.is_server_error   # False
```

Messages are scrubbed of your API key before the error ever leaves the SDK.

## Folders & usage

```python
apulodi.folders.list("users")            # subfolders of a logical path
usage = apulodi.usage.get("2026-09")     # or apulodi.usage.get() for this month
usage["storage"]["usedBytes"]
```

## Configuration

```python
apulodi = Apulodi(
    api_key="apk_test_...",
    base_url="http://localhost:3000",  # default: https://app.apulodi.dev
    timeout=30,                        # seconds per request (storage PUTs get 4x)
)
```

Idempotent retries for `POST /v1/files/upload`:

```python
apulodi.files.upload(data, "report.pdf", idempotency_key="order-1234")
```

## Development

```bash
python3 -m venv .venv && source .venv/bin/activate
pip install -e '.[dev]'
pytest
```

## License

MIT
