- Python 3.10+
- uv (recommended) or pip
- Git
Clone and install in development mode:
git clone https://github.com/cgwire/kitsu-cli.git
cd kitsu-cli
uv syncThis creates a .venv/ virtual environment and installs all dependencies including dev tools.
If you prefer pip:
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"Verify the install:
.venv/bin/kitsu --helpkitsu-cli/
├── pyproject.toml # Project metadata, dependencies, entry point
├── scripts/
│ └── generate_commands.py # Coverage report: gazu functions vs CLI commands
├── src/kitsu_cli/
│ ├── __init__.py
│ ├── __main__.py # `python -m kitsu_cli` entry point
│ ├── cli.py # Root Click group, lazy-loading, global options
│ ├── config.py # Config file (~/.config/kitsu-cli/config.toml)
│ ├── client.py # Gazu client setup from resolved config
│ ├── output.py # JSON/table/CSV/id-only formatters
│ ├── resolver.py # UUID-or-name entity resolution
│ └── commands/
│ ├── auth.py # login, logout, status
│ ├── project.py # Project CRUD + task-type/task-status/asset-type
│ ├── asset.py # Asset CRUD + asset type management
│ ├── shot.py # Shot/sequence/episode CRUD, CSV import/export
│ ├── task.py # Tasks, comments, previews, time, types, statuses
│ ├── person.py # People, departments, organisations
│ ├── files.py # Output/working/preview files, software, output types
│ ├── casting.py # Asset-to-entity casting
│ ├── playlist.py # Playlist CRUD
│ ├── entity.py # Generic entities and entity types
│ ├── scene.py # Scenes and asset instances
│ ├── edit.py # Edit CRUD
│ ├── concept.py # Concept CRUD
│ ├── studio.py # User context (todo, notifications, filters, chats)
│ ├── sync.py # Event log and data import
│ ├── search.py # Name-based entity search
│ ├── cache.py # Client cache control
│ └── events.py # Real-time event listener
└── tests/
├── conftest.py # Shared fixtures (runner, invoke)
├── test_cli.py # Root group and command structure tests
├── test_auth.py # Auth command tests
├── test_config.py # Config read/write/priority tests
├── test_output.py # Output formatter tests
├── test_project.py # Project command tests
└── test_asset.py # Asset command tests
# All tests
uv run pytest
# Verbose output
uv run pytest -v
# Single test file
uv run pytest tests/test_auth.py
# Single test function
uv run pytest tests/test_auth.py::test_login_with_token
# With coverage
uv run pytest --cov=kitsu_cliTests use Click's CliRunner and mock Gazu calls with unittest.mock.patch. No running Kitsu server is needed.
Find the function in the Gazu source or run the coverage report:
uv run python scripts/generate_commands.py --module assetThis shows which Gazu functions are already covered and which are missing.
Open the relevant src/kitsu_cli/commands/<module>.py and add a Click command. Follow the existing patterns:
@group.command()
@click.argument("entity_id")
@click.option("--project", required=True, help="Project UUID or name.")
@click.pass_context
def my_command(ctx, entity_id, project):
"""Short description shown in --help."""
require_auth(ctx)
proj = resolve_project(project)
result = gazu.module.some_function(proj, entity_id)
output(result, ctx)Key conventions:
- Call
require_auth(ctx)at the top of every command that hits the API. - Use
resolve_*()functions fromresolver.pyto accept both UUIDs and names. - Pipe results through
output(result, ctx)so all formats (JSON/table/CSV/id-only) work. - Use
click.argument()for required positional parameters (entity IDs). - Use
click.option()for filters and optional parameters. - For delete commands, print a confirmation unless
--quietis set.
If you created a new module file, add it to COMMAND_MODULES in cli.py:
COMMAND_MODULES = {
...
"my_module": "kitsu_cli.commands.my_module",
}Add tests in tests/test_<module>.py. Mock Gazu and require_auth:
from unittest.mock import patch
def test_my_command(invoke):
with patch("kitsu_cli.commands.module.gazu") as mock_gazu, \
patch("kitsu_cli.commands.module.require_auth"), \
patch("kitsu_cli.commands.module.resolve_project", return_value={"id": "p1"}):
mock_gazu.module.some_function.return_value = {"id": "x1", "name": "result"}
result = invoke("--format", "json", "module", "my-command", "entity-id", "--project", "p1")
assert result.exit_code == 0
assert "result" in result.outputuv run python scripts/generate_commands.py --module assetCommand groups are loaded on demand via LazyGroup in cli.py. This keeps kitsu --help fast — only the requested command module is imported. Each command file exposes a group variable (a click.Group).
Authentication and host settings are resolved in this order (first wins):
- CLI flags (
--host,--token) - Environment variables (
KITSU_HOST,KITSU_TOKEN) - Config file (
~/.config/kitsu-cli/config.toml)
All commands call output(data, ctx) from output.py. The format is determined by --format or auto-detected (JSON when piped, table when interactive). The output() function handles all four formats:
json—json.dumps, pretty-printed in TTYtable— Rich table, falls back to JSON if Rich is unavailablecsv— Standard CSV with header rowid-only— One ID per line
resolver.py provides resolve_*() helpers that accept a string and return a Gazu dict. They check if the string is a UUID (regex match) and call the appropriate get_*() or get_*_by_name() Gazu function. This lets users pass either format on the command line.
To see which Gazu functions have CLI commands and which are still missing:
uv run python scripts/generate_commands.pyTo check a single module:
uv run python scripts/generate_commands.py --module taskOutput shows [OK] or [MISSING] for each function, with per-module and total coverage percentages.