Python SDK Quickstart
picora-sdk is the official Python SDK for the Picora API. Synchronous (requests-based), Python 3.9+.
Install
pip install picora-sdk # coming soon — not yet on PyPI60-second example (API Key)
from picora_sdk import PicoraClient
picora = PicoraClient(api_key="sk_live_xxx")
# Whoamime = picora.auth_me()print(f"Logged in as {me['email']} ({me['plan']})")
# List collectionsresult = picora.collections.list(limit=10)print(f"Found {len(result['items'])} collections")OAuth Device Flow (CLI / headless)
from picora_sdk import start_device_flow, PicoraClient
flow = start_device_flow( client_id="cli_xxx", scopes=["collection.read", "episode.write"],)print(f"Visit {flow.verification_uri} and enter {flow.user_code}")
# Built-in RFC 8628 backofftoken = flow.poll()
picora = PicoraClient(oauth_token=token.access_token)DeviceFlowToken fields:
access_token— pass toPicoraClient(oauth_token=...)refresh_token— optionalexpires_at— unix seconds (absolute)scopes— list of actually granted scopes
Collections + Episodes
# Create or find a collectionseries = picora.collections.create( name="My TV Show", slug="my-tv-show", collection_type="tv_series", allowed_resource_types=["video", "audio", "doc"],)
# Add an episodeep = picora.episodes.create(series["id"], sequence_no=1, title="EP01 — Pilot")
# Sync uploaded assets to the episode (idempotent + dedup)result = picora.episodes.sync( series["id"], ep["id"], idempotency_key="comfy-batch-001", assets=[ {"resource_type": "video", "resource_id": "vid_uploaded_xxx"}, {"resource_type": "doc", "resource_id": "doc_script_yyy"}, ],)print(f"Applied: {result['appliedCount']} / Skipped: {result['skippedCount']} / Failed: {result['failedCount']}")Note: the request body uses camelCase keys (matching the Picora REST API), but the Python wrapper accepts snake_case kwargs (resource_type, idempotency_key). Conversion is automatic.
Custom collection types (Pro+)
picora.collection_types.create( name="My Podcast Series", slug="my_podcast", allowed_resource_types=["audio", "doc"], icon="mic", description="Audio drama with companion scripts",)Error handling
All SDK errors extend PicoraApiError:
from picora_sdk import PicoraClient, PicoraApiError, PicoraRateLimitError
try: picora.collections.create(name="X", slug="taken")except PicoraRateLimitError as exc: # Auto-retry happens by default; only raised if all 3 retries exhausted print(f"Rate limited; retry after {exc.retry_after_sec}s")except PicoraApiError as exc: if exc.code == "COLLECTION_SLUG_TAKEN": # pick a different slug pass else: raiseToken persistence
MemoryTokenStorage keeps the token only in-process. For CLI / desktop tools you usually want the token to survive restarts — use the built-in FileTokenStorage (v0.2.1+):
import timefrom picora_sdk import start_device_flow, FileTokenStorage, PicoraClient
storage = FileTokenStorage() # defaults to ~/.picora/token.json
token = storage.get()if not token or token.expires_at < time.time() + 60: flow = start_device_flow(client_id="cli_xxx", scopes=["collection.read"]) print(f"Visit {flow.verification_uri} and enter {flow.user_code}") token = flow.poll() storage.put(token)
picora = PicoraClient(oauth_token=token.access_token)Properties:
- Default path
~/.picora/token.json; pass a custom path to the constructor to override - Atomic write (tmp file + rename), so a crash mid-write won’t leave half a JSON
- POSIX
chmod 0600on the file (current user only); silently skipped on Windows - Read errors (missing file / corrupted JSON / missing fields) return
None— let the caller fall through to a fresh Device Flow
OS Keychain (recommended for CLI / desktop)
Plaintext on disk is fine for dev, but production CLI tools should put refresh_token in the OS keychain (macOS Keychain / Windows Credential Manager / Linux libsecret). picora-sdk ships KeychainTokenStorage (v0.2.2+) with the same interface, backed by the keyring package (optional extra).
pip install 'picora-sdk[keychain]'# or: pip install picora-sdk keyringimport timefrom picora_sdk import start_device_flow, KeychainTokenStorage, PicoraClient
# service defaults to 'picora-sdk', account defaults to 'default'storage = KeychainTokenStorage(account="cli_acme")
token = storage.get()if not token or token.expires_at < time.time() + 60: flow = start_device_flow(client_id="cli_acme", scopes=["collection.read"]) print(f"Visit {flow.verification_uri} and enter {flow.user_code}") token = flow.poll() storage.put(token)
picora = PicoraClient(oauth_token=token.access_token)Properties:
-
Encrypted at rest by the OS (no plaintext on disk)
-
servicenamespace lets you isolate prod vs staging tokens -
accountlets one machine hold tokens for multiple Picora users / OAuth clients -
keyringis an optional extra — importingKeychainTokenStorageworks without it; the firstget()/put()/clear()call raises a clearRuntimeErrorpointing topip install picora-sdk[keychain] -
Pass
backend=...to inject a custom adapter (Vault / 1Password CLI / corporate secret store):KeychainTokenStorage(backend=my_keyring_adapter)
For other secret-store backends, implement the KeyringBackend Protocol:
from picora_sdk import KeyringBackend
class VaultBackend: def get_password(self, service: str, username: str) -> str | None: ... def set_password(self, service: str, username: str, password: str) -> None: ... def delete_password(self, service: str, username: str) -> None: ...Retry behavior
By default the client retries:
- 429 — exponential backoff (1s / 2s / 4s, max 3 attempts; respects
Retry-Afterheader) - 5xx and network errors — 0.5s + 1.5s (max 2 attempts)
Disable: PicoraClient(api_key="...", retry=False)
Available namespaces (v0.2.2)
| Namespace | Methods |
|---|---|
client.collections | list / get / create / delete |
client.collection_types | list / create / delete |
client.episodes | list / create / sync |
client.auth_me() | Returns current user |
start_device_flow(...) | Top-level helper |
MemoryTokenStorage | In-process token persistence |
FileTokenStorage | File-backed persistence; default ~/.picora/token.json, atomic write + chmod 0600 |
KeychainTokenStorage | OS keychain persistence (optional keyring extra); service / account namespacing |
What’s next
- AI Video Sync guide — end-to-end batch flow with worked examples
- JavaScript SDK — same shape, in TypeScript
- GitHub repo — source, issues, contributions