Joseph HENRY 3600c870cd Initial marketplace with flamenco and kitsu plugins
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>
2026-05-07 17:13:13 +02:00

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}"]
```