Tighten flamenco-api and kitsu-api descriptions per Anthropic's "concise is key" guidance: drop redundant trigger-list sentences and collapse to a single "what + when" line. Also remove the flamenco-api reference to usage-docs.md, which was never bundled. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
376 lines
12 KiB
Markdown
376 lines
12 KiB
Markdown
---
|
|
name: kitsu-api
|
|
description: Kitsu production tracking API (Zou backend) and gazu Python SDK reference. Use when integrating with Kitsu/CGWire, debugging API calls, building tools that talk to the tracker, or working with shots, assets, tasks, comments, previews, playlists, or casting. Covers the REST API (493 endpoints) and the gazu wrapper.
|
|
---
|
|
|
|
# Kitsu API & gazu SDK Reference
|
|
|
|
Kitsu is an open-source production tracking tool for animation/VFX studios, built by CGWire. The backend is called **Zou** and exposes a REST API. **gazu** is the official Python client SDK that wraps this API.
|
|
|
|
## Quick Reference
|
|
|
|
- **API docs**: https://api-docs.kitsu.cloud/
|
|
- **OpenAPI spec**: https://api-docs.kitsu.cloud/source.json
|
|
- **Developer guides**: https://dev.kitsu.cloud/
|
|
- **gazu docs**: https://gazu.cg-wire.com/
|
|
- **Zou source**: https://github.com/cgwire/zou
|
|
- **gazu source**: https://github.com/cgwire/gazu
|
|
|
|
## Authentication
|
|
|
|
### User Login (email/password → JWT)
|
|
|
|
```python
|
|
import gazu
|
|
|
|
gazu.set_host("https://kitsu.mystudio.com/api")
|
|
gazu.log_in("user@studio.com", "password")
|
|
```
|
|
|
|
Under the hood this POSTs to `/auth/login` with `{"email": "...", "password": "..."}` and receives a JWT access token + refresh token.
|
|
|
|
### Bot Authentication (pre-generated token)
|
|
|
|
Bots are non-physical users that don't count against subscription seats. Create them in Admin > Bots. Use the token directly:
|
|
|
|
```python
|
|
gazu.set_host("https://kitsu.mystudio.com/api")
|
|
gazu.set_token("bot_jwt_token_here")
|
|
```
|
|
|
|
### Token Lifecycle
|
|
|
|
- **Access token**: Short-lived JWT, passed as `Authorization: Bearer <token>` header
|
|
- **Refresh token**: Use `GET /auth/refresh-token` to get a new access token
|
|
- **Logout**: `GET /auth/logout`
|
|
- **Check auth**: `GET /auth/authenticated` (200 = valid, 401 = expired)
|
|
|
|
### 2FA Support
|
|
|
|
Kitsu supports TOTP, email OTP, FIDO/WebAuthn, and recovery codes. See `/auth/totp`, `/auth/email-otp`, `/auth/fido` endpoints.
|
|
|
|
### ADM Pipeline Authentication
|
|
|
|
In the ADM Gadget pipeline, credentials come from environment variables set by `adm_env.yml`:
|
|
- `TRACKER_URL` — Kitsu instance URL (must end with `/api`)
|
|
- `TRACKER_LOGIN` — email
|
|
- `TRACKER_PASSWORD` — password
|
|
- `TRACKER_PROJECT` — project name to connect to
|
|
|
|
The `Kitsu` adapter class (`gadget/src/gadget/resources/trackers/kitsu.py`) wraps gazu and handles connection/reconnection.
|
|
|
|
## Core Concepts
|
|
|
|
### Entity Hierarchy
|
|
|
|
```
|
|
Project
|
|
├── Episodes (optional, for TV shows)
|
|
│ └── Sequences
|
|
│ └── Shots
|
|
├── Sequences (for shorts/features)
|
|
│ └── Shots
|
|
├── Asset Types
|
|
│ └── Assets
|
|
├── Edits
|
|
├── Concepts
|
|
└── Scenes
|
|
```
|
|
|
|
### Data Model
|
|
|
|
Every entity in Kitsu is a dict with at minimum:
|
|
- `id` — UUID string
|
|
- `name` — display name
|
|
- `created_at` / `updated_at` — ISO timestamps
|
|
- `data` — custom metadata dict (called `custom_data` in Gadget after normalization)
|
|
- `project_id` — parent project UUID
|
|
|
|
### Key Relationships
|
|
|
|
- **Shot** → belongs to **Sequence** (via `parent_id`) → belongs to **Episode** (optional)
|
|
- **Asset** → belongs to **Asset Type** (via `entity_type_id`)
|
|
- **Task** → belongs to **Entity** (shot/asset) + **Task Type** + has **Task Status**
|
|
- **Comment** → belongs to **Task**, may have **Preview Files** and **Attachments**
|
|
- **Casting** → links Assets to Shots (with `nb_occurences`)
|
|
|
|
## gazu SDK Patterns
|
|
|
|
### Project Operations
|
|
|
|
```python
|
|
# List/find projects
|
|
projects = gazu.project.all_projects()
|
|
open_projects = gazu.project.all_open_projects()
|
|
project = gazu.project.get_project_by_name("My Project")
|
|
project = gazu.project.get_project(project_id)
|
|
|
|
# Create project
|
|
project = gazu.project.new_project("Name", production_type="short") # short|featurefilm|tvshow
|
|
|
|
# Team management
|
|
gazu.project.add_person_to_team(project, person)
|
|
gazu.project.remove_person_from_team(project, person)
|
|
team = gazu.project.get_team(project)
|
|
```
|
|
|
|
### Shot Operations
|
|
|
|
```python
|
|
# Episodes (TV shows only)
|
|
episode = gazu.shot.new_episode(project, "E01")
|
|
episodes = gazu.shot.all_episodes_for_project(project)
|
|
|
|
# Sequences
|
|
sequence = gazu.shot.new_sequence(project, "SQ010", episode=episode)
|
|
sequences = gazu.shot.all_sequences_for_project(project)
|
|
|
|
# Shots
|
|
shot = gazu.shot.new_shot(project, sequence, "SH0010",
|
|
nb_frames=100, frame_in=101, frame_out=200,
|
|
data={"custom_field": "value"})
|
|
shot = gazu.shot.get_shot(shot_id)
|
|
shot = gazu.shot.get_shot_by_name(sequence, "SH0010")
|
|
shots = gazu.shot.all_shots_for_sequence(sequence)
|
|
gazu.shot.update_shot(shot) # after modifying dict fields
|
|
gazu.shot.remove_shot(shot_id, force=True)
|
|
```
|
|
|
|
### Asset Operations
|
|
|
|
```python
|
|
# Asset types
|
|
asset_type = gazu.asset.new_asset_type("Character")
|
|
asset_types = gazu.asset.all_asset_types_for_project(project)
|
|
|
|
# Assets
|
|
asset = gazu.asset.new_asset(project, asset_type, "Hero",
|
|
description="Main character",
|
|
extra_data={"variant": "default"})
|
|
asset = gazu.asset.get_asset(asset_id)
|
|
asset = gazu.asset.get_asset_by_name(project, "Hero", asset_type)
|
|
assets = gazu.asset.all_assets_for_project(project)
|
|
gazu.asset.remove_asset(asset_id, force=True)
|
|
```
|
|
|
|
### Task Operations
|
|
|
|
```python
|
|
# Task types & statuses
|
|
task_type = gazu.task.get_task_type_by_name("Animation")
|
|
wip = gazu.task.get_task_status_by_short_name("wip")
|
|
done = gazu.task.get_task_status_by_short_name("done")
|
|
|
|
# Create tasks
|
|
task = gazu.task.new_task(entity, task_type, task_status=wip, assignees=[person])
|
|
|
|
# Find tasks
|
|
tasks = gazu.task.all_tasks_for_shot(shot)
|
|
tasks = gazu.task.all_tasks_for_asset(asset)
|
|
task = gazu.task.get_task_by_name(entity, task_type) # get_task_by_entity in some versions
|
|
|
|
# Assignment
|
|
gazu.task.assign_task(task, person)
|
|
|
|
# Time tracking
|
|
gazu.task.set_time_spent(task, person, "2024-03-18", duration=8*3600) # seconds
|
|
gazu.task.add_time_spent(task, person, "2024-03-18", duration=3600)
|
|
|
|
# Scheduling
|
|
task["start_date"] = "2024-03-01"
|
|
task["due_date"] = "2024-03-15"
|
|
task["estimation"] = 5 * 8 * 3600 # 5 days in seconds
|
|
gazu.task.update_task(task)
|
|
```
|
|
|
|
### Comments & Publishing
|
|
|
|
```python
|
|
# Add comment (also changes task status)
|
|
comment = gazu.task.add_comment(task, task_status,
|
|
comment="Looking good!",
|
|
person=person,
|
|
attachments=["/path/to/file.pdf"],
|
|
created_at=None)
|
|
|
|
# Add preview to comment
|
|
preview = gazu.task.add_preview(task, comment, "/path/to/render.mp4")
|
|
gazu.task.set_main_preview(preview) # set as entity thumbnail
|
|
|
|
# One-step publish shortcut
|
|
comment, preview = gazu.task.publish_preview(task, wip,
|
|
comment="WIP update",
|
|
preview_file_path="/path/to/file.mp4")
|
|
|
|
# Read comments
|
|
comments = gazu.task.all_comments_for_task(task)
|
|
last = gazu.task.get_last_comment_for_task(task)
|
|
|
|
# Download previews
|
|
gazu.files.download_preview_file(preview, "/path/to/output.mp4")
|
|
gazu.files.download_preview_file_thumbnail(preview, "/path/to/thumb.png")
|
|
```
|
|
|
|
### Casting (Breakdown)
|
|
|
|
```python
|
|
# Get casting
|
|
casting = gazu.casting.get_shot_casting(shot) # assets in a shot
|
|
cast_in = gazu.casting.get_asset_cast_in(asset) # shots using an asset
|
|
|
|
# Update casting
|
|
gazu.casting.update_shot_casting(project, shot_id, [
|
|
{"asset_id": asset1_id, "nb_occurences": 2},
|
|
{"asset_id": asset2_id, "nb_occurences": 1},
|
|
])
|
|
```
|
|
|
|
### Persons & Teams
|
|
|
|
```python
|
|
person = gazu.person.new_person("John", "Doe", "john@studio.com",
|
|
role="user", departments=[dept])
|
|
persons = gazu.person.all_persons()
|
|
person = gazu.person.get_person_by_full_name("John Doe")
|
|
gazu.person.add_person_to_department(person, department)
|
|
```
|
|
|
|
### Playlists
|
|
|
|
```python
|
|
playlist = gazu.playlist.new_playlist(project, "Daily Review",
|
|
for_client=False, for_entity="shot")
|
|
gazu.playlist.add_entity_to_playlist(playlist, entity, preview_file=preview)
|
|
```
|
|
|
|
### Event Listeners (Real-time)
|
|
|
|
```python
|
|
gazu.set_event_host("https://kitsu.mystudio.com") # note: no /api suffix
|
|
event_client = gazu.events.init()
|
|
|
|
def on_task_status_changed(data):
|
|
print(f"Task {data['task_id']} status changed")
|
|
|
|
gazu.events.add_listener(event_client, "task:status-changed", on_task_status_changed)
|
|
gazu.events.run_client(event_client) # blocks current thread
|
|
```
|
|
|
|
Event naming: `entity:action` (e.g., `asset:new`, `shot:update`, `comment:new`, `task:assign`).
|
|
Most entities emit `new`, `update`, `delete`. See `references/events.md` for the full list.
|
|
|
|
### Caching
|
|
|
|
```python
|
|
gazu.cache.enable() # enable in-memory cache for all read ops
|
|
gazu.cache.disable()
|
|
gazu.cache.clear_all()
|
|
|
|
# Per-function control
|
|
gazu.asset.all_assets.set_expire(120) # seconds
|
|
gazu.asset.all_assets.clear_cache()
|
|
gazu.asset.all_assets.disable_cache()
|
|
```
|
|
|
|
### Low-level Client
|
|
|
|
When gazu doesn't wrap an endpoint, use the raw client:
|
|
|
|
```python
|
|
# GET
|
|
data = gazu.client.get("data/projects")
|
|
data = gazu.client.fetch_all("tasks", {"project_id": pid, "task_type_id": ttid})
|
|
data = gazu.client.fetch_one("projects", project_id)
|
|
data = gazu.client.fetch_first("projects", {"name": "MyProject"})
|
|
|
|
# POST
|
|
result = gazu.client.post("data/projects", {"name": "New Project"})
|
|
|
|
# PUT
|
|
result = gazu.client.put(f"data/entities/{entity_id}", updated_data)
|
|
|
|
# Pagination
|
|
page1 = gazu.client.fetch_all("tasks?page=1") # 100 per page
|
|
```
|
|
|
|
### Search
|
|
|
|
```python
|
|
# Full-text search
|
|
results = gazu.search.search_entities("bird")
|
|
# Returns: {"persons": [...], "assets": [...], "shots": [...]}
|
|
|
|
# REST API search
|
|
results = gazu.client.post("data/search", {
|
|
"query": "hero",
|
|
"project_id": project_id,
|
|
"limit": 10,
|
|
"offset": 0,
|
|
"index_names": ["assets"] # filter to specific entity types
|
|
})
|
|
```
|
|
|
|
## Roles & Permissions
|
|
|
|
| Role | Access Level |
|
|
|------|-------------|
|
|
| **Admin** (Studio Manager) | Full read/write to all productions and settings |
|
|
| **Manager** (Production Manager) | Create assets/shots, manage tasks, post comments — no studio settings |
|
|
| **Supervisor** (Department Lead) | Read/write within assigned departments |
|
|
| **User** (Artist) | Comment/upload on assigned tasks only |
|
|
| **Vendor** | Like User but can only see assigned tasks |
|
|
| **Client** | View-only on assigned productions, client playlists only |
|
|
|
|
## Custom Actions
|
|
|
|
Custom Actions trigger HTTP POST requests from the Kitsu web UI to your endpoint. Created by admins in Admin > Custom Actions.
|
|
|
|
Payload sent to your endpoint:
|
|
```json
|
|
{
|
|
"personid": "uuid",
|
|
"personemail": "user@studio.com",
|
|
"projectid": "uuid",
|
|
"currentpath": "/productions/{id}/assets",
|
|
"currentserver": "kitsu.mystudio.com",
|
|
"selection": ["task_uuid_1", "task_uuid_2"],
|
|
"entitytype": "asset"
|
|
}
|
|
```
|
|
|
|
## ADM Gadget Integration
|
|
|
|
The Gadget pipeline's `Kitsu` adapter (`gadget/src/gadget/resources/trackers/kitsu.py`) wraps gazu with these conventions:
|
|
|
|
- **ID normalization**: `get_id()` accepts strings (UUID), dicts (extracts `["id"]`), or Gadget entity objects (uses `.id`)
|
|
- **Data normalization**: `_norm_data()` renames `data` → `custom_data` to avoid collision with Python dict usage
|
|
- **Task normalization**: `retake_count` → `nb_retakes`, `for_entity` → `for_shots` boolean
|
|
- **Comment normalization**: `attachment_files` → `attachments`, adds index-based `id` to checklist items, renames preview `original_name` → `name`
|
|
- **Shot creation**: Uses raw `gazu.client.post()` because gazu's `new_shot` didn't support all fields needed
|
|
|
|
Key Gadget patterns:
|
|
```python
|
|
# Gadget entity-level usage
|
|
project = Project.from_env() # loads from env vars
|
|
shot = project.shots.get(name="sq170_sh0224")
|
|
task = shot.tasks.fetch("Anim3D")
|
|
task.comments.new(comment="...", preview=["/path/to/img.png"], set_main_preview=True)
|
|
```
|
|
|
|
## Detailed References
|
|
|
|
For the complete endpoint catalog (all 493 endpoints), see `references/endpoints.md`.
|
|
For the full event types list, see `references/events.md`.
|
|
|
|
When you need details about a specific endpoint's parameters or response format, fetch the OpenAPI spec:
|
|
```bash
|
|
curl -s https://api-docs.kitsu.cloud/source.json | python3 -m json.tool
|
|
```
|
|
|
|
Or read it programmatically:
|
|
```python
|
|
import json, urllib.request
|
|
spec = json.loads(urllib.request.urlopen("https://api-docs.kitsu.cloud/source.json").read())
|
|
endpoint = spec["paths"]["/data/shots/{shot_id}"]
|
|
```
|