Bundles the flamenco-api and kitsu-api skills as Claude Code plugins under an "adm-tools" marketplace. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
382 lines
12 KiB
Markdown
382 lines
12 KiB
Markdown
---
|
|
name: kitsu-api
|
|
description: >
|
|
Comprehensive reference for the Kitsu production tracking API (Zou backend) and the gazu Python SDK.
|
|
Use this skill whenever working with Kitsu, gazu, CGWire, or production tracking API calls —
|
|
whether writing new integrations, debugging API issues, building pipeline tools that talk to Kitsu,
|
|
or understanding how the ADM Gadget pipeline interacts with the tracker. Also use when the user
|
|
mentions tasks, shots, assets, comments, previews, playlists, or casting in the context of
|
|
production tracking. Covers both the REST API (493 endpoints) and the gazu Python 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}"]
|
|
```
|