Metadata-Version: 2.5
Name: riffsdk
Version: 0.18.0
Summary: Python SDK for the Riff Storage API
Project-URL: Homepage, https://riff.ai
Author-email: Martin Sandve Alnæs <msa@databutton.io>
License-Expression: MIT
License-File: LICENCE
Requires-Python: >=3.11
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2.6
Requires-Dist: tenacity>=8.3
Description-Content-Type: text/markdown

# riffsdk

Python SDK for Riff. Provides sync and async clients for the Storage API, and
lookups against an app's task board.

## Installation

```bash
uv add riffsdk
# or
pip install riffsdk
```

Or install from a branch (for pre-release testing):

```bash
uv add git+https://github.com/databutton/riff-sdk-python.git@main
# or
pip install git+https://github.com/databutton/riff-sdk-python.git@main
```

## Quick start

```python
from riffsdk.storage import StorageClient

client = StorageClient()

# Upload (content type derived from the key: text/plain; charset=utf-8)
meta = client.put("hello.txt", "Hello, world!")
meta = client.put("notes.md", "# Title")      # -> text/markdown; charset=utf-8

# Download
data = client.get("hello.txt")

# List
for obj in client.list("hello"):
    print(f"{obj.key} ({obj.size} bytes)")

# Delete
client.delete("hello.txt")

client.close()
```

### Async

```python
from riffsdk.storage import AsyncStorageClient

async with AsyncStorageClient() as client:
    await client.put("key", b"data", content_type="application/octet-stream")
    data = await client.get("key")
```

## Tasks

Look one task up on the app's task board, in whatever state it is in —
completed ones included:

```python
import riffsdk.tasks.v0 as tasks

task = tasks.get_task("TASK-42")   # display ID as it appears on the board, or the task's id

if task is None:
    ...                            # nothing on the board for this item
elif task["status"] == "completed":
    ...                            # already handled
```

This exists for trigger tools. A trigger is handed the tasks that still have
work left on them and nothing about the ones already finished, so it cannot
tell an item it has never seen from one whose task is done — and proposes the
same finished work every time it fires. `get_task` closes that gap.

Import the version you are writing against, bound to a short name as above.
A version does not change under you: when the API changes it appears as a new
version, and the one you imported keeps behaving as it did. Importing
`riffsdk` or `riffsdk.tasks` loads no version.

### API

- `get_task(ref)` -- returns a `Task`, or `None` if there is no such task.
  `get_task_async` is the async form.
- `Task` is the task as JSON plus the `version` of that shape. Read fields by
  name (`task["status"]`, `task["metadata"]`, `task.get("summary")`, or
  `task.data` for the whole dict), so a task gaining a field needs no SDK
  release.
- Raises `TaskError` if the lookup failed. A task that does not exist is not a
  failure; it comes back as `None`.
- The board read is the app's own: a deployed app sees its production tasks,
  the same app in the workspace sees the workspace's. There is nothing to
  configure for that, and no way to read the other one.
- Runs from an app's backend, where the environment it needs is already set.

## Authentication

Set the `RIFF_TOKEN` environment variable. The SDK picks it up automatically.

## API

### Clients

- `StorageClient` -- sync client
- `AsyncStorageClient` -- async client

Both support: `put`, `get`, `stat`, `exists`, `list`, `delete`, `close`, and context manager usage.

### Models

- `ObjectMeta` -- metadata for a stored object (key, version, size, content_type, timestamps)
- `UploadResult`, `DownloadResult` -- operation results
- `ListPage` -- paginated listing
- `Scope` -- access scope (use `account_scope()`, `project_scope()`, `session_scope()`)

### Uploads

- `ResumableUpload` / `AsyncResumableUpload` -- multipart resumable uploads for large files
- `StorageReader` / `StorageWriter` -- streaming read/write

### Content types

`content_type` is optional on `put`, `upload_file`, `upload_stream`, `begin_upload` and
`create_write_stream`. When omitted it is derived from the **storage key's** extension using a
table bundled with the SDK, so the result does not depend on the host's `/etc/mime.types` or the
Python version. Unknown extensions fall back to `application/octet-stream`.

- `upload_file` prefers the key's extension and falls back to the local filename's -- so
  `upload_file("docs/notes.md", "/tmp/tmpXY123")` still stores `text/markdown`.
- `str` payloads passed to `put()` are encoded as UTF-8, and get `; charset=utf-8` appended when
  the derived type is `text/*`.
- An explicit `content_type=` is always used verbatim.

### Exceptions

All exceptions inherit from `StorageError`:

- `AuthorisationError`
- `ObjectNotFoundError`
- `VersionConflictError`
- `AlreadyExistsError`
- `LeaseConflictError`
- `UploadNotFoundError`
- `QuotaExceededError`
- `PartMismatchError`
- `StorageTransportError`

## Examples

See the `examples/` directory for complete working examples:

- `basic_crud.py` -- put, get, list, delete
- `async_client.py` -- async usage with asyncio
- `file_upload_download.py` -- file uploads with progress
- `optimistic_concurrency.py` -- version-based conflict handling
- `get_task.py` -- reading the task board from a trigger tool

## Development

Requires Python 3.11+ and [uv](https://docs.astral.sh/uv/).

```bash
uv sync --dev        # Install dependencies
mise run test        # Run tests
mise run lint        # Lint
mise run format      # Format code
```

See `AGENTS.md` for full development workflow details.
