# Frameio Python Library

[![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-Built%20with%20Fern-brightgreen)](https://buildwithfern.com?utm_source=github&utm_medium=github&utm_campaign=readme&utm_source=https%3A%2F%2Fgithub.com%2FFrameio%2Fpython-sdk)
[![pypi](https://img.shields.io/pypi/v/frameio)](https://pypi.python.org/pypi/frameio)


<img width="1644" alt="artboard_small" src="https://user-images.githubusercontent.com/19295862/66240171-ba8dd280-e6b0-11e9-9ccf-573a4fc5961f.png">

Frame.io is a cloud-based collaboration hub that allows video professionals to share files, comment on clips real-time, and compare different versions and edits of a clip. 

## Installation

```sh
pip install frameio
```

## Reference

A full reference for this library is available [here](https://next.developer.frame.io/platform/v4/docs/sdk-reference/python-sdk-reference).

## Usage

You can authenticate with a static token or OAuth 2.0 via `frameio.auth`:

**Static token** (for legacy dev tokens or when you already have an access token):

```python
client = Frameio(token="YOUR_TOKEN")
```

**OAuth 2.0** (for production with automatic token refresh):

```python
from frameio import Frameio
from frameio.auth import ServerToServerAuth

auth = ServerToServerAuth(client_id="...", client_secret="...")
client = Frameio(token=auth.get_token)
```

See the [Authentication](#authentication) section below for all OAuth flows.

Example: creating a metadata field definition:

```python
from frameio import (
    Frameio,
    SelectDefinitionParamsFieldConfiguration,
    SelectDefinitionParamsFieldConfigurationOptionsItem,
)
from frameio.metadata_fields import CreateFieldDefinitionParamsData_Select

client = Frameio(
    token="YOUR_TOKEN",
)
client.metadata_fields.metadata_field_definitions_create(
    account_id="b2702c44-c6da-4bb6-8bbd-be6e547ccf1b",
    data=CreateFieldDefinitionParamsData_Select(
        field_configuration=SelectDefinitionParamsFieldConfiguration(
            enable_add_new=False,
            options=[
                SelectDefinitionParamsFieldConfigurationOptionsItem(
                    display_name="Option 1",
                ),
                SelectDefinitionParamsFieldConfigurationOptionsItem(
                    display_name="Option 2",
                ),
            ],
        ),
        name="Fields definition name",
    ),
)
```

## Authentication

The SDK supports two authentication options:

- **Static token**: Pass a string directly to `token=`. Suitable for legacy dev tokens or when you already have an access token.
- **OAuth 2.0**: Use `frameio.auth` for automatic token management and refresh. Pass `auth.get_token` to the client.

`frameio.auth` provides three OAuth 2.0 flows (sync and async):

| Flow | Use Case | Classes |
|------|----------|---------|
| **Server-to-Server** | Backend services, scripts, no user interaction | `ServerToServerAuth`, `AsyncServerToServerAuth` |
| **Web App** | Server-side apps with client secret | `WebAppAuth`, `AsyncWebAppAuth` |
| **Single Page App (SPA)** | Browser apps without a client secret (PKCE) | `SPAAuth`, `AsyncSPAAuth` |

### Server-to-Server

For backend services and scripts that need Frame.io access without user interaction:

```python
from frameio import Frameio
from frameio.auth import ServerToServerAuth

auth = ServerToServerAuth(client_id="...", client_secret="...")
client = Frameio(token=auth.get_token)
# Tokens refresh automatically; no user interaction needed.
```

### Web App

For server-side applications with a client secret:

```python
from frameio import Frameio
from frameio.auth import WebAppAuth

auth = WebAppAuth(
    client_id="...",
    client_secret="...",
    redirect_uri="https://myapp.com/callback",
)
import secrets
url = auth.get_authorization_url(state=secrets.token_urlsafe(32))
# Redirect user to url, then:
auth.exchange_code(code="CODE_FROM_CALLBACK")
client = Frameio(token=auth.get_token)
```

### Single Page App (PKCE)

For browser-based apps that cannot store a client secret:

```python
from frameio import Frameio
from frameio.auth import SPAAuth

auth = SPAAuth(client_id="...", redirect_uri="https://myapp.com/cb")
import secrets
result = auth.get_authorization_url(state=secrets.token_urlsafe(32))
# Redirect user to result.url, store result.code_verifier, then:
auth.exchange_code(code="CODE", code_verifier=result.code_verifier)
client = Frameio(token=auth.get_token)
```

### Token persistence

For Web App and SPA flows, you can persist tokens to avoid re-authenticating on each app restart:

```python
# After exchange_code() — save tokens for later
data = auth.export_tokens()
# Store data to file or database ...

# On next app start — restore and use
auth.import_tokens(data)
client = Frameio(token=auth.get_token)
```

### Revoking tokens

To sign out and revoke the current access and refresh tokens:

```python
auth.revoke()
```

### Staging / non-production

Use `ims_base_url` to point at a staging or alternative Adobe IMS environment:

```python
auth = ServerToServerAuth(
    client_id="...",
    client_secret="...",
    ims_base_url="https://ims-na1-stg1.adobelogin.com",  # example staging URL
)
```

> **What about Native App?** The Adobe IMS Native App flow relies on custom URI scheme handlers registered at the OS level (e.g. `adobe+<hash>://…`). This requires a host application — such as Electron, Tauri, or a native mobile app — to intercept the redirect. Python has no standard mechanism for this, so `NativeAppAuth` is only available in the [TypeScript SDK](https://github.com/Frameio/typescript-sdk). For Python use cases that need user interaction, use `WebAppAuth` with a local callback server; for non-interactive workloads, use `ServerToServerAuth`.

## Async Client

The SDK also exports an `async` client so that you can make non-blocking calls to our API. Note that if you are constructing an Async httpx client class to pass into this client, use `httpx.AsyncClient()` instead of `httpx.Client()` (e.g. for the `httpx_client` parameter of this client).

Use the async auth classes with `AsyncFrameio`:

```python
from frameio import AsyncFrameio
from frameio.auth import AsyncServerToServerAuth

auth = AsyncServerToServerAuth(client_id="...", client_secret="...")
client = AsyncFrameio(token=auth.get_token)
```

Full example with an API call:

```python
import asyncio

from frameio import (
    AsyncFrameio,
    SelectDefinitionParamsFieldConfiguration,
    SelectDefinitionParamsFieldConfigurationOptionsItem,
)
from frameio.metadata_fields import CreateFieldDefinitionParamsData_Select

client = AsyncFrameio(
    token="YOUR_TOKEN",
)


async def main() -> None:
    await client.metadata_fields.metadata_field_definitions_create(
        account_id="b2702c44-c6da-4bb6-8bbd-be6e547ccf1b",
        data=CreateFieldDefinitionParamsData_Select(
            field_configuration=SelectDefinitionParamsFieldConfiguration(
                enable_add_new=False,
                options=[
                    SelectDefinitionParamsFieldConfigurationOptionsItem(
                        display_name="Option 1",
                    ),
                    SelectDefinitionParamsFieldConfigurationOptionsItem(
                        display_name="Option 2",
                    ),
                ],
            ),
            name="Fields definition name",
        ),
    )


asyncio.run(main())
```

## Exception Handling

When the API returns a non-success status code (4xx or 5xx response), a subclass of the following error
will be thrown.

```python
from frameio.core.api_error import ApiError

try:
    client.metadata_fields.metadata_field_definitions_create(...)
except ApiError as e:
    print(e.status_code)
    print(e.body)
```

For OAuth flows via `frameio.auth`, additional exceptions are available:

- `AuthenticationError` — token exchange or refresh failed
- `TokenExpiredError` — refresh token expired; user must re-authenticate
- `ConfigurationError` — invalid configuration (e.g. missing `client_id`)
- `NetworkError`, `RateLimitError` — network or rate limit issues

```python
from frameio.auth import AuthenticationError, TokenExpiredError

try:
    auth.exchange_code(code="...", code_verifier="...")
except AuthenticationError as e:
    print(e.error_code, e.error_description)
except TokenExpiredError:
    # Redirect user to sign in again
    pass
```

## Pagination

Paginated requests will return a `SyncPager` or `AsyncPager`, which can be used as generators for the underlying object.

```python
from frameio import Frameio

client = Frameio(
    token="YOUR_TOKEN",
)
response = client.project_permissions.index(
    account_id="b2702c44-c6da-4bb6-8bbd-be6e547ccf1b",
    project_id="b2702c44-c6da-4bb6-8bbd-be6e547ccf1b",
    page_size=10,
    include_total_count=False,
)
for item in response:
    yield item
# alternatively, you can paginate page-by-page
for page in response.iter_pages():
    yield page
```

## Advanced

### Access Raw Response Data

The SDK provides access to raw response data, including headers, through the `.with_raw_response` property.
The `.with_raw_response` property returns a "raw" client that can be used to access the `.headers` and `.data` attributes.

```python
from frameio import Frameio

client = Frameio(
    ...,
)
response = (
    client.metadata_fields.with_raw_response.metadata_field_definitions_create(
        ...
    )
)
print(response.headers)  # access the response headers
print(response.data)  # access the underlying object
pager = client.project_permissions.index(...)
print(pager.response.headers)  # access the response headers for the first page
for item in pager:
    print(item)  # access the underlying object(s)
for page in pager.iter_pages():
    print(page.response.headers)  # access the response headers for each page
    for item in page:
        print(item)  # access the underlying object(s)
```

### Retries

The SDK is instrumented with automatic retries with exponential backoff. A request will be retried as long
as the request is deemed retryable and the number of retry attempts has not grown larger than the configured
retry limit (default: 2).

A request is deemed retryable when any of the following HTTP status codes is returned:

- [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) (Timeout)
- [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) (Too Many Requests)
- [5XX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500) (Internal Server Errors)

Use the `max_retries` request option to configure this behavior.

```python
client.metadata_fields.metadata_field_definitions_create(..., request_options={
    "max_retries": 1
})
```

### Timeouts

The SDK defaults to a 60 second timeout. You can configure this with a timeout option at the client or request level.

```python

from frameio import Frameio

client = Frameio(
    ...,
    timeout=20.0,
)


# Override timeout for a specific method
client.metadata_fields.metadata_field_definitions_create(..., request_options={
    "timeout_in_seconds": 1
})
```

### Custom Client

You can override the `httpx` client to customize it for your use-case. Some common use-cases include support for proxies
and transports.

```python
import httpx
from frameio import Frameio

client = Frameio(
    ...,
    httpx_client=httpx.Client(
        proxy="http://my.test.proxy.example.com",
        transport=httpx.HTTPTransport(local_address="0.0.0.0"),
    ),
)
```

## Contributing

While we value open-source contributions to this SDK, this library is generated programmatically.
Additions made directly to this library would have to be moved over to our generation code,
otherwise they would be overwritten upon the next generated release. Feel free to open a PR as
a proof of concept, but know that we will not be able to merge it as-is. We suggest opening
an issue first to discuss with us!

On the other hand, contributions to the README are always very welcome!
