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