This is the multi-page printable view of this section.
Click here to print.
Return to the regular view of this page.
Recipes (examples)
Complete, copy-and-run SDK scripts grouped by domain area
Every example in this section is a complete script that uses argparse.
Pass --help to any script to see all options. The same files live in
cvat-sdk/examples/.
Shared conventions:
- Every recipe takes
--host and --token. Create a token in the CVAT UI under
Profile -> Security. Wrap values that contain URL punctuation in single quotes,
e.g. --host 'https://app.cvat.ai'. auth_profile.py and auth_cli.py show
alternative sign-in flows.
- Recipes that create resources keep them and print their ids and UI links. Pass
--cleanup to delete what the script created (never the sources it read).
- Recipes that inspect or export take an existing resource id
(
--project-id, --task-id), so they work directly against your data.
- List-valued options accept multiple values, e.g.
--labels car person.
- Missing arguments exit with a friendly message; SDK errors surface as normal
Python tracebacks.
All examples are tested in the latest SDK version.
Topics
- Authenticate —
auth_token.py, auth_profile.py, auth_cli.py
- Projects — create/list, backup, restore, dataset export
- Tasks — create from a bucket, bulk-create in a project,
inspect and export
- Jobs — list jobs, round-robin assignment, batch-advance stages
- Cloud storage — attach an S3-compatible bucket
1 - Authenticate a client
Copy-and-run auth recipes: PAT (recommended), saved profiles, and the CLI-compatible argument set
Three recipes: auth_token.py is the recommended PAT path, auth_profile.py
signs in from a saved profile with no secret in your code, and auth_cli.py
wires up the shared cvat-cli argument set (--server-host, --auth,
--profile, …) so your scripts feel like an extension of the CLI.
Connect with a Personal Access Token
Opens an authenticated client with a PAT, prints the server version, and prints
who you are — a quick sanity check any script can copy.
| Flag |
Required |
Meaning |
--host |
yes |
Server URL, e.g. 'https://app.cvat.ai' |
--token |
yes |
Token created in the CVAT UI (Profile -> Security) |
python auth_token.py --host 'https://app.cvat.ai' --token '<your token>'
The script
# Copyright (C) CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT
"""Connect to CVAT with a Personal Access Token (PAT) — the recommended way.
Steps:
1. Open an authenticated client.
2. Print the server version.
3. Print who you are authenticated as (a quick sanity check for scripts).
Usage (run ``python auth_token.py --help`` for the full list of options):
python auth_token.py --host 'https://app.cvat.ai' --token '<your token>'
Create a token in the CVAT UI under Profile -> Security.
"""
import argparse
from cvat_sdk import make_client
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
parser.add_argument("--host", required=True, help="CVAT server URL, e.g. 'https://app.cvat.ai'")
parser.add_argument(
"--token",
required=True,
help="Personal Access Token (create one in the CVAT UI: Profile -> Security)",
)
return parser.parse_args()
def main() -> None:
args = parse_args()
with make_client(args.host, access_token=args.token) as client:
print("Server version:", client.get_server_version())
me = client.users.retrieve_current_user()
print(f"Authenticated as {me.username} (id={me.id})")
if __name__ == "__main__":
main()
Sign in from a saved profile
Uses a saved CLI profile so no secret lives in the code. Create a profile once
with cvat-cli; then any script can pick it by name or fall back to the
default profile.
Create a profile once:
cvat-cli --server-host 'https://app.cvat.ai' profile create --name app --set-default
| Flag |
Required |
Meaning |
--profile |
no |
Name of a saved profile; omit to use the default profile |
python auth_profile.py --profile app
python auth_profile.py # uses the default profile
The script
# Copyright (C) CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT
"""Authenticate without putting a token in your code: use a saved profile.
Create a profile once on the command line, then any script can use it:
cvat-cli --server-host 'https://app.cvat.ai' profile create --name app --set-default
Steps:
1. If --profile is passed, use that profile; otherwise use the default profile.
2. Print who you are authenticated as.
Usage (run ``python auth_profile.py --help`` for the full list of options):
python auth_profile.py --profile app
python auth_profile.py # uses the default profile
"""
import argparse
import sys
from cvat_sdk import make_client_from_profile
from cvat_sdk.core.auth import AuthStore
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
parser.add_argument(
"--profile", help="name of a saved profile; omit to use the default profile"
)
return parser.parse_args()
def main() -> None:
args = parse_args()
store = AuthStore()
if args.profile:
profile = store.get_profile(args.profile)
if profile is None:
sys.exit(f"Profile {args.profile!r} not found. Create it with cvat-cli.")
print(f"Using profile {args.profile!r}")
else:
default = store.get_default_profile()
if default is None:
sys.exit(
"No default profile configured. Create one with:\n"
" cvat-cli --server-host 'https://app.cvat.ai' profile create"
" --name app --set-default"
)
name, profile = default
print(f"Using default profile {name!r}")
with make_client_from_profile(profile) as client:
me = client.users.retrieve_current_user()
print(f"Authenticated as {me.username} (id={me.id})")
if __name__ == "__main__":
main()
Build a CLI-compatible script
Reuses cvat-cli’s shared auth arguments (--server-host, --server-port,
--auth, --profile, --insecure, --organization) with
configure_client_auth_arguments, then hands the parsed namespace to
make_client_from_cli, which picks the right factory (profile / PAT / password)
from the arguments. This is the go-to pattern when your script should feel like
an extension of cvat-cli.
| Flag |
Required |
Meaning |
--server-host |
fallback |
Server URL when not using a profile |
--auth |
fallback |
USER:PASS (deprecated password sign-in) or USER — see cvat-cli |
--profile |
fallback |
Named saved profile; falls back to the default profile if no host/auth |
--insecure, --organization, --server-port |
no |
Reused from cvat-cli’s shared arg set |
Also honors CVAT_ACCESS_TOKEN / CVAT_PASSWORD environment variables the same
way cvat-cli does.
python auth_cli.py --profile app
python auth_cli.py --server-host 'https://app.cvat.ai' # uses CVAT_ACCESS_TOKEN env
python auth_cli.py --server-host 'https://app.cvat.ai' --auth me:secret
The script
# Copyright (C) CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT
"""Build a CLI-compatible script that reuses ``cvat-cli``'s auth argument set:
``--server-host`` / ``--server-port`` / ``--auth`` / ``--profile`` / ``--insecure`` / ``--organization``.
This is the go-to pattern when your script should feel like an extension of
``cvat-cli`` — it accepts the same flags, honors the ``CVAT_ACCESS_TOKEN`` and
``PASS`` env variables, and resolves profiles the same way (explicit
``--profile``, else the default profile if no host/auth is passed).
Steps:
1. Register the shared auth flags with ``configure_client_auth_arguments()``.
2. Add your own script-specific arguments on top.
3. Hand the parsed namespace to ``make_client_from_cli()`` to create a server API client object.
Usage (run ``python auth_cli.py --help`` for the full list of options):
python auth_cli.py --profile app
# export CVAT_ACCESS_TOKEN='<token>' # for macOS/Linux
# $env:CVAT_ACCESS_TOKEN = "<token>" # for PowerShell
python auth_cli.py --server-host 'https://app.cvat.ai'
python auth_cli.py --server-host 'https://app.cvat.ai' --auth me:secret
"""
import argparse
from cvat_sdk import make_client_from_cli
from cvat_sdk.core.auth import configure_client_auth_arguments
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
configure_client_auth_arguments(parser)
# Add your script's own arguments here, e.g.
# parser.add_argument("--task-id", type=int, required=True)
return parser.parse_args()
def main() -> None:
args = parse_args()
with make_client_from_cli(args) as client:
me = client.users.retrieve_current_user()
print(f"Authenticated as {me.username} (id={me.id})")
if __name__ == "__main__":
main()
Notes:
- Personal Access Tokens are the recommended path. Password sign-in (via
--auth USER:PASS) is a deprecated fallback that will be removed in a future release.
- Full recipes:
auth_token.py,
auth_profile.py,
auth_cli.py.
2 - Project recipes
Create/list, backup, restore, dataset export — one recipe per file
Four recipes cover the project lifecycle: project_create_and_list.py for the
common CRUD path, project_backup.py and project_restore.py for portable
copies, and project_export_dataset.py for dataset export (local + cloud).
For a CSV overview of a project’s jobs, see job_list.py --project-id --csv
in the job recipes.
Create, list, filter, retrieve, rename
Creates a project with labels, then lists all projects, filters by name,
retrieves by id, and renames it. Pass --cleanup to delete it at the end.
| Flag |
Required |
Meaning |
--host |
yes |
Server URL, e.g. 'https://app.cvat.ai' |
--token |
yes |
Personal Access Token |
--name |
no |
Project name (default 'Example project') |
--labels |
no |
Label names, space-separated (default car person) |
--cleanup |
no |
Delete the created project at the end |
python project_create_and_list.py --host 'https://app.cvat.ai' --token '<your token>' \
--name 'My project' --labels car person
The script
# Copyright (C) CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT
"""Create a project with labels, then list, filter, retrieve, and rename it.
Steps:
1. Create a project with a simple label schema.
2. List all projects visible to you (pagination is handled by the SDK).
3. Filter projects by a name substring.
4. Retrieve one project by id and read its labels.
5. Rename it.
6. Optionally delete it (--cleanup).
Usage (run ``python project_create_and_list.py --help`` for the full list of options):
python project_create_and_list.py --host 'https://app.cvat.ai' --token '<your token>' \\
--name 'My project' --labels car person
"""
import argparse
from cvat_sdk import make_client, models
from cvat_sdk.core.filters import F
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
parser.add_argument("--host", required=True, help="CVAT server URL, e.g. 'https://app.cvat.ai'")
parser.add_argument(
"--token",
required=True,
help="Personal Access Token (CVAT UI: Profile -> Security)",
)
parser.add_argument(
"--name", default="Example project", help="project name (default: '%(default)s')"
)
parser.add_argument(
"--labels",
nargs="+",
default=["car", "person"],
help="label names (default: %(default)s)",
)
parser.add_argument(
"--cleanup", action="store_true", help="delete the created project at the end"
)
return parser.parse_args()
def main() -> None:
args = parse_args()
with make_client(args.host, access_token=args.token) as client:
# 1. Create a project with labels
project = client.projects.create(
models.ProjectWriteRequest(
name=args.name,
labels=[models.PatchedLabelRequest(name=name) for name in args.labels],
)
)
print(f"Created project {project.id}: {args.host}/projects/{project.id}")
# 2. List all projects
projects = client.projects.list()
print(f"Projects visible to you: {len(projects)}")
# 3. Filter by name substring
matches = client.projects.list(filter=F.name.contains(args.name))
print(f"Projects with {args.name!r} in the name: {[p.id for p in matches]}")
# 4. Retrieve by id
fetched = client.projects.retrieve(project.id)
print(f"Project {fetched.id} labels: {[label.name for label in fetched.get_labels()]}")
# 5. Rename
renamed = fetched.update(models.PatchedProjectWriteRequest(name=f"{args.name} (renamed)"))
print(f"Renamed to: {renamed.name}")
# 6. Opt-in cleanup
if args.cleanup:
renamed.remove()
print(f"Deleted project {project.id}")
else:
print("Keeping the project; pass --cleanup to delete it")
if __name__ == "__main__":
main()
Back up a project
Downloads a full project backup zip — tasks, jobs, annotations, and settings.
Pair with project_restore.py to migrate or clone.
| Flag |
Required |
Meaning |
--host |
yes |
Server URL |
--token |
yes |
Personal Access Token |
--project-id |
yes |
Id of the project to back up |
--output |
no |
Destination file (default project_<id>_backup.zip) |
python project_backup.py --host 'https://app.cvat.ai' --token '<your token>' \
--project-id 42
The script
# Copyright (C) CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT
"""Download a backup zip of an existing project.
A backup contains the project's tasks, jobs, annotations, and settings. Pair
this recipe with project_restore.py to migrate or clone a project.
Steps:
1. Retrieve the project by id.
2. Download its backup to --output (default: project_<id>_backup.zip).
Usage (run ``python project_backup.py --help`` for the full list of options):
python project_backup.py --host 'https://app.cvat.ai' --token '<your token>' \\
--project-id 42
"""
import argparse
from pathlib import Path
from cvat_sdk import make_client
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
parser.add_argument("--host", required=True, help="CVAT server URL, e.g. 'https://app.cvat.ai'")
parser.add_argument(
"--token",
required=True,
help="Personal Access Token (CVAT UI: Profile -> Security)",
)
parser.add_argument(
"--project-id", type=int, required=True, help="id of an existing project, e.g. 42"
)
parser.add_argument(
"--output",
type=Path,
help="destination file path (default: project_<id>_backup.zip)",
)
return parser.parse_args()
def main() -> None:
args = parse_args()
with make_client(args.host, access_token=args.token) as client:
project = client.projects.retrieve(args.project_id)
output = args.output or Path(f"project_{project.id}_backup.zip")
project.download_backup(output)
print(f"Backed up project {project.id} to {output.resolve()}")
if __name__ == "__main__":
main()
Restore a project
Restores a project from a backup zip as a brand-new project. Pass --cleanup
to delete the restored copy afterwards — useful when validating a backup file.
| Flag |
Required |
Meaning |
--host |
yes |
Server URL |
--token |
yes |
Personal Access Token |
--backup |
yes |
Path to a project backup zip |
--cleanup |
no |
Delete the restored copy (never touches the backup file) |
python project_restore.py --host 'https://app.cvat.ai' --token '<your token>' \
--backup './project_42_backup.zip'
The script
# Copyright (C) CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT
"""Restore a project from a backup zip as a new project.
Pair with project_backup.py to migrate or clone a project.
Steps:
1. Restore --backup as a brand-new project.
2. Optionally delete the restored copy (--cleanup) — useful when testing a
backup file.
Usage (run ``python project_restore.py --help`` for the full list of options):
python project_restore.py --host 'https://app.cvat.ai' --token '<your token>' \\
--backup './project_42_backup.zip'
"""
import argparse
import sys
from pathlib import Path
from cvat_sdk import make_client
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
parser.add_argument("--host", required=True, help="CVAT server URL, e.g. 'https://app.cvat.ai'")
parser.add_argument(
"--token",
required=True,
help="Personal Access Token (CVAT UI: Profile -> Security)",
)
parser.add_argument("--backup", type=Path, required=True, help="path to a project backup zip")
parser.add_argument(
"--cleanup",
action="store_true",
help="delete the restored project at the end (never touches the source backup)",
)
return parser.parse_args()
def main() -> None:
args = parse_args()
if not args.backup.is_file():
sys.exit(f"--backup {args.backup} does not exist")
with make_client(args.host, access_token=args.token) as client:
restored = client.projects.create_from_backup(args.backup)
print(f"Restored a copy as project {restored.id}: {args.host}/projects/{restored.id}")
if args.cleanup:
restored.remove()
print(f"Deleted restored project {restored.id}")
else:
print("Keeping the restored project; pass --cleanup to delete it")
if __name__ == "__main__":
main()
Export a project’s tasks individually (local + cloud)
Exports each task in a project as its own dataset, both to a local zip and
straight to a registered cloud storage. By default every task is exported;
pass --task-id to export only a specific subset. Validates the format name
against the server’s list before starting.
| Flag |
Required |
Meaning |
--host |
yes |
Server URL |
--token |
yes |
Personal Access Token |
--project-id |
yes |
Id of the project to export |
--cloud-storage-id |
yes |
Registered cloud storage id (see cloud_storage_register.py) |
--export-format |
no |
Exporter name (default 'COCO 1.0') |
--task-id |
no |
Task ids to export, space-separated (default: every task in the project) |
python project_export_dataset.py --host 'https://app.cvat.ai' --token '<your token>' \
--project-id 42 --cloud-storage-id 7 --export-format 'COCO 1.0'
The script
# Copyright (C) CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT
"""Export a project's tasks individually, without images, to local zips AND to
a registered cloud storage.
By default every task in the project is exported; pass --task-id to export
only a specific subset. This is the SDK-only stand-in for what could become a
bulk per-task export command in cvat-cli.
Steps:
1. Fetch the server's export format list and validate --export-format.
2. Resolve which tasks to export: --task-id filters to a subset of the
project's tasks; omit it to export every task in the project.
3. For each task: export to task_<id>_dataset.zip in the current directory,
then export the same dataset straight to the cloud storage (no local
download).
Usage (run ``python project_export_dataset.py --help`` for the full list of options):
# every task in the project
python project_export_dataset.py --host 'https://app.cvat.ai' --token '<your token>' \\
--project-id 42 --cloud-storage-id 7 --export-format 'COCO 1.0'
# only tasks 10 and 11
python project_export_dataset.py --host 'https://app.cvat.ai' --token '<your token>' \\
--project-id 42 --cloud-storage-id 7 --task-id 10 11
"""
import argparse
import sys
from pathlib import Path
from cvat_sdk import make_client
from cvat_sdk.core.proxies.types import Location
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
parser.add_argument("--host", required=True, help="CVAT server URL, e.g. 'https://app.cvat.ai'")
parser.add_argument(
"--token",
required=True,
help="Personal Access Token (CVAT UI: Profile -> Security)",
)
parser.add_argument(
"--project-id", type=int, required=True, help="id of an existing project, e.g. 42"
)
parser.add_argument(
"--cloud-storage-id",
type=int,
required=True,
help="a registered cloud storage id (see cloud_storage_register.py)",
)
parser.add_argument(
"--export-format",
default="COCO 1.0",
help="exporter name, e.g. 'COCO 1.0' (default: '%(default)s')",
)
parser.add_argument(
"--task-id",
type=int,
nargs="+",
metavar="ID",
help="export only these task ids (must belong to the project); "
"omit to export every task in the project",
)
return parser.parse_args()
def main() -> None:
args = parse_args()
with make_client(args.host, access_token=args.token) as client:
# 1. Validate the format against the server's list.
# Low-level API: there is no high-level proxy for the format list yet.
formats, _ = client.api_client.server_api.retrieve_annotation_formats()
names = [f.name for f in formats.exporters]
if args.export_format not in names:
sys.exit(
f"Unknown export format {args.export_format!r}. Choose one of: {', '.join(names)}"
)
# 2. Resolve which tasks to export.
project = client.projects.retrieve(args.project_id)
tasks_by_id = {task.id: task for task in project.get_tasks()}
if args.task_id:
missing = [str(tid) for tid in args.task_id if tid not in tasks_by_id]
if missing:
sys.exit(f"Task id(s) {', '.join(missing)} not found in project {project.id}")
tasks = [tasks_by_id[tid] for tid in args.task_id]
else:
tasks = list(tasks_by_id.values())
if not tasks:
sys.exit(f"Project {project.id} has no tasks to export")
# 3. Export each task individually: a local zip AND straight to the cloud storage.
for task in tasks:
local_path = Path(f"task_{task.id}_dataset.zip")
task.export_dataset(
args.export_format, local_path, include_images=False, location=Location.LOCAL
)
print(f"Exported {local_path.resolve()}")
remote_name = f"task_{task.id}_dataset.zip"
task.export_dataset(
args.export_format,
remote_name,
include_images=False,
location=Location.CLOUD_STORAGE,
cloud_storage_id=args.cloud_storage_id,
)
print(f"Exported {remote_name} to cloud storage {args.cloud_storage_id}")
print(f"Exported {len(tasks)} task dataset(s) from project {project.id}")
if __name__ == "__main__":
main()
Other SDK options:
| SDK method / parameter |
What it adds |
Project.download_backup(..., lightweight=True) |
Produce a smaller backup that omits media. |
client.projects.create_from_dataset(...) |
Create a project directly from a dataset archive. |
Project.import_dataset(format_name, path) |
Import annotations/data into an existing project - the import counterpart of export_dataset. |
Project.get_annotations() |
Fetch the project’s labeled data. |
Notes:
list() returns the whole collection; pagination is handled for you.
- A project backup captures tasks, jobs, users, and settings in a single zip -
but no raw media beyond what
export_dataset would include.
- For a CSV overview of a project’s jobs (no annotation geometry), use
job_list.py --project-id <id> --csv. For an actual dataset export,
use project_export_dataset.py.
include_images=False exports annotations only and is much smaller.
- Full recipes:
project_create_and_list.py,
project_backup.py,
project_restore.py,
project_export_dataset.py.
3 - Task recipes
Create one task or a batch of tasks from a bucket; inspect and export existing tasks
Three recipes cover the task lifecycle: task_create_from_cloud.py creates one
task from object keys already in a registered bucket,
tasks_bulk_from_cloud.py creates a whole batch of tasks in a project from
that same bucket, and task_inspect_and_export.py inspects an existing task,
exports its dataset locally, and reports analytics from its event log.
Create a task from cloud object keys
Creates a task from images that already live in a registered bucket.
| Flag |
Required |
Meaning |
--host |
yes |
Server URL |
--token |
yes |
Personal Access Token |
--cloud-storage-id |
yes |
Registered cloud storage id (see cloud_storage_register.py) |
--cloud-keys |
yes |
Object keys in the bucket, space-separated |
--name |
no |
Task name (default 'Task from cloud storage') |
--labels |
no |
Label names, space-separated (default object) |
--cleanup |
no |
Delete the created task at the end |
python task_create_from_cloud.py --host 'https://app.cvat.ai' --token '<your token>' \
--cloud-storage-id 7 --cloud-keys 'images/0001.jpg' 'images/0002.jpg' \
--labels car person
The script
# Copyright (C) CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT
"""Create an annotation task from images that already live in a registered
cloud storage.
Steps:
1. Create a task whose data is a list of object keys in the bucket.
2. Print the result.
3. Optionally delete it (--cleanup).
Register a bucket first with cloud_storage_register.py to get the storage id.
Usage (run ``python task_create_from_cloud.py --help`` for the full list of options):
python task_create_from_cloud.py --host 'https://app.cvat.ai' --token '<your token>' \\
--cloud-storage-id 7 --cloud-keys 'images/0001.jpg' 'images/0002.jpg' \\
--labels car person
"""
import argparse
from cvat_sdk import make_client, models
from cvat_sdk.core.proxies.tasks import ResourceType
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
parser.add_argument("--host", required=True, help="CVAT server URL, e.g. 'https://app.cvat.ai'")
parser.add_argument(
"--token",
required=True,
help="Personal Access Token (CVAT UI: Profile -> Security)",
)
parser.add_argument(
"--cloud-storage-id",
type=int,
required=True,
help="a registered cloud storage id (see cloud_storage_register.py)",
)
parser.add_argument(
"--cloud-keys",
nargs="+",
required=True,
help="object keys in the bucket, e.g. 'images/0001.jpg' 'images/0002.jpg'",
)
parser.add_argument(
"--name",
default="Task from cloud storage",
help="task name (default: '%(default)s')",
)
parser.add_argument(
"--labels", nargs="+", default=["object"], help="label names (default: %(default)s)"
)
parser.add_argument("--cleanup", action="store_true", help="delete the created task at the end")
return parser.parse_args()
def main() -> None:
args = parse_args()
with make_client(args.host, access_token=args.token) as client:
# ResourceType.SHARE + cloud_storage_id = read images from the bucket
task = client.tasks.create_from_data(
spec=models.TaskWriteRequest(
name=args.name,
labels=[models.PatchedLabelRequest(name=name) for name in args.labels],
),
resource_type=ResourceType.SHARE,
resources=args.cloud_keys,
data_params={"cloud_storage_id": args.cloud_storage_id},
)
print(f"Created task {task.id} with {task.size} frames: {args.host}/tasks/{task.id}")
if args.cleanup:
task.remove()
print(f"Deleted task {task.id}")
else:
print("Keeping the task; pass --cleanup to delete it")
if __name__ == "__main__":
main()
Bulk-create tasks in a project from a bucket
Creates several tasks in one call, all inside the same project, each reading
its data from a registered cloud storage. Two ways to spell a task’s data,
repeatable and mixable: --task KEY[,KEY,...] lists explicit object keys (a
single key makes a video/single-image task; multiple keys make an image task
whose frames are those keys in order), and --task-pattern PATTERN makes one
task from every bucket file matching a fnmatch wildcard (e.g. 'batch_a/*.jpg'),
resolved from the bucket’s manifest instead of listing every key by hand.
Because every task belongs to the project, they share its label schema — no
--labels here.
| Flag |
Required |
Meaning |
--host |
yes |
Server URL |
--token |
yes |
Personal Access Token |
--cloud-storage-id |
yes |
Registered cloud storage id (see cloud_storage_register.py) |
--project-id |
yes |
Project the tasks are created in; supplies the labels |
--task KEY[,KEY,...] |
one of --task / --task-pattern |
One --task per task; repeat the flag for more |
--task-pattern PATTERN |
one of --task / --task-pattern |
One task per wildcard, matched via the bucket’s manifest; repeat for more |
--manifest |
no |
Manifest object key used to resolve --task-pattern (default 'manifest.jsonl') |
--name-prefix |
no |
Task-name prefix; each task is named <prefix> N (default 'Bulk task') |
--cleanup |
no |
Delete every created task at the end |
# three video tasks in project 42
python tasks_bulk_from_cloud.py --host 'https://app.cvat.ai' --token '<your token>' \
--cloud-storage-id 7 --project-id 42 \
--task 'videos/clip_01.mp4' --task 'videos/clip_02.mp4' --task 'videos/clip_03.mp4'
# two image-batch tasks in project 42
python tasks_bulk_from_cloud.py --host 'https://app.cvat.ai' --token '<your token>' \
--cloud-storage-id 7 --project-id 42 \
--task 'batch_a/img_1.jpg,batch_a/img_2.jpg' \
--task 'batch_b/img_1.jpg,batch_b/img_2.jpg'
# the same two batches, without listing every key: one task per wildcard match
python tasks_bulk_from_cloud.py --host 'https://app.cvat.ai' --token '<your token>' \
--cloud-storage-id 7 --project-id 42 --manifest manifest.jsonl \
--task-pattern 'batch_a/*.jpg' --task-pattern 'batch_b/*.jpg'
The script
# Copyright (C) CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT
"""Bulk-create tasks inside a project, each task's data read from a registered
cloud storage.
Two ways to spell a task's data, repeatable and mixable:
--task KEY[,KEY,...] explicit object keys, in order:
* a single key -> a video task (or single-image task);
* several keys -> an image task, in the given order.
--task-pattern PATTERN every bucket file matching a fnmatch wildcard (e.g.
'batch_a/*.jpg'), resolved from the bucket's
manifest instead of being listed one by one.
All tasks land in the same project, so they share its label schema.
Steps:
1. For each --task, create a task in --project-id from its explicit keys.
2. For each --task-pattern, create a task in --project-id from every bucket
file the wildcard matches, resolved via the bucket's manifest.
3. Print the created ids and a summary count.
4. Optionally delete every created task (--cleanup).
Register a bucket first with cloud_storage_register.py to get the storage id.
A --task-pattern also needs a manifest file already generated for the bucket -
see "How to generate manifest file" in the CVAT docs on attaching cloud storage.
Usage (run ``python tasks_bulk_from_cloud.py --help`` for the full list of options):
# three video tasks in project 42
python tasks_bulk_from_cloud.py --host 'https://app.cvat.ai' --token '<your token>' \\
--cloud-storage-id 7 --project-id 42 \\
--task 'videos/clip_01.mp4' --task 'videos/clip_02.mp4' --task 'videos/clip_03.mp4'
# two image-batch tasks in project 42
python tasks_bulk_from_cloud.py --host 'https://app.cvat.ai' --token '<your token>' \\
--cloud-storage-id 7 --project-id 42 \\
--task 'batch_a/img_1.jpg,batch_a/img_2.jpg' \\
--task 'batch_b/img_1.jpg,batch_b/img_2.jpg'
# the same two batches, without listing every key: one task per wildcard match
python tasks_bulk_from_cloud.py --host 'https://app.cvat.ai' --token '<your token>' \\
--cloud-storage-id 7 --project-id 42 --manifest manifest.jsonl \\
--task-pattern 'batch_a/*.jpg' --task-pattern 'batch_b/*.jpg'
"""
import argparse
from cvat_sdk import make_client, models
from cvat_sdk.core.proxies.tasks import ResourceType
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
parser.add_argument("--host", required=True, help="CVAT server URL, e.g. 'https://app.cvat.ai'")
parser.add_argument(
"--token",
required=True,
help="Personal Access Token (CVAT UI: Profile -> Security)",
)
parser.add_argument(
"--cloud-storage-id",
type=int,
required=True,
help="a registered cloud storage id (see cloud_storage_register.py)",
)
parser.add_argument(
"--project-id",
type=int,
required=True,
help="tasks are created in this project and inherit its labels",
)
parser.add_argument(
"--task",
dest="tasks",
action="append",
default=[],
metavar="KEY[,KEY,...]",
help="comma-separated object keys for one task; repeat for more tasks",
)
parser.add_argument(
"--task-pattern",
dest="task_patterns",
action="append",
default=[],
metavar="PATTERN",
help="one task from every bucket file matching this fnmatch wildcard "
"(e.g. 'batch_a/*.jpg'); repeat for more tasks. Needs --manifest. "
"(default: '%(default)s')",
)
parser.add_argument(
"--manifest",
default="manifest.jsonl",
help="manifest object key in the bucket, used to resolve --task-pattern "
"(default: '%(default)s')",
)
parser.add_argument(
"--name-prefix",
default="Bulk task",
help="task name prefix; each task is named '<prefix> N' (default: '%(default)s')",
)
parser.add_argument(
"--cleanup", action="store_true", help="delete every created task at the end"
)
args = parser.parse_args()
if not args.tasks and not args.task_patterns:
parser.error("at least one --task or --task-pattern is required")
return args
def main() -> None:
args = parse_args()
task_key_groups = [
[key.strip() for key in spec.split(",") if key.strip()] for spec in args.tasks
]
if any(not group for group in task_key_groups):
raise SystemExit("each --task must contain at least one non-empty key")
with make_client(args.host, access_token=args.token) as client:
created = []
for keys in task_key_groups:
# Tasks in a project inherit the project's labels — do NOT pass labels.
# ResourceType.SHARE + cloud_storage_id reads the objects from the bucket.
task = client.tasks.create_from_data(
spec=models.TaskWriteRequest(
name=f"{args.name_prefix} {len(created) + 1}", project_id=args.project_id
),
resource_type=ResourceType.SHARE,
resources=keys,
data_params={"cloud_storage_id": args.cloud_storage_id},
)
created.append(task)
print(f"Created task {task.id} ({task.size} frames): {args.host}/tasks/{task.id}")
for pattern in args.task_patterns:
# A wildcard task needs the bucket's manifest as its only resource;
# the server expands filename_pattern against it (fnmatch syntax).
# use_cache=True is required to serve data straight from the bucket.
task = client.tasks.create_from_data(
spec=models.TaskWriteRequest(
name=f"{args.name_prefix} {len(created) + 1}", project_id=args.project_id
),
resource_type=ResourceType.SHARE,
resources=[args.manifest],
data_params={
"cloud_storage_id": args.cloud_storage_id,
"use_cache": True,
"filename_pattern": pattern,
},
)
created.append(task)
print(
f"Created task {task.id} ({task.size} frames) from pattern {pattern!r}: "
f"{args.host}/tasks/{task.id}"
)
print(f"Created {len(created)} tasks in project {args.project_id}")
if args.cleanup:
for task in created:
task.remove()
print(f"Deleted {len(created)} tasks")
else:
print("Keeping the tasks; pass --cleanup to delete them")
if __name__ == "__main__":
main()
Inspect a task and export its dataset
Prints a summary of an existing task (labels, jobs, frames), exports its
dataset to a local zip, then exports the task’s event log and reports two
analytics computed from it: how many people are currently assigned to a job,
and how many jobs were rejected in review and sent back for rework.
| Flag |
Required |
Meaning |
--host |
yes |
Server URL |
--token |
yes |
Personal Access Token |
--task-id |
yes |
Id of the task to inspect and export |
--export-format |
no |
Exporter name (default 'COCO 1.0') |
python task_inspect_and_export.py --host 'https://app.cvat.ai' --token '<your token>' \
--task-id 42 --export-format 'COCO 1.0'
The script
# Copyright (C) CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT
"""Inspect an existing task (labels, jobs, frames), export its dataset to a
local zip, and export its event log to report quick analytics.
Steps:
1. Retrieve the task and print a summary: labels, jobs (stage/state), frames.
2. Fetch the server's export format list and validate --export-format.
3. Export the dataset to task_<id>_dataset.zip in the current directory.
4. Export the task's event log to task_<id>_events.csv and report two
analytics: how many people are currently assigned to a job, and how
many jobs were rejected in review and sent back for rework - the second
one needs the log, since a job's current state doesn't show its history.
Usage (run ``python task_inspect_and_export.py --help`` for the full list of options):
python task_inspect_and_export.py --host 'https://app.cvat.ai' --token '<your token>' \\
--task-id 42 --export-format 'COCO 1.0'
"""
import argparse
import csv
import sys
from pathlib import Path
from cvat_sdk import make_client
from cvat_sdk.core.downloading import Downloader
from cvat_sdk.core.proxies.types import Location
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
parser.add_argument("--host", required=True, help="CVAT server URL, e.g. 'https://app.cvat.ai'")
parser.add_argument(
"--token",
required=True,
help="Personal Access Token (CVAT UI: Profile -> Security)",
)
parser.add_argument(
"--task-id", type=int, required=True, help="id of an existing task, e.g. 42"
)
parser.add_argument(
"--export-format",
default="COCO 1.0",
help="exporter name, e.g. 'COCO 1.0' (default: '%(default)s')",
)
return parser.parse_args()
def count_reworks(events_path: Path) -> int:
"""Count how many times a job in the log was rejected in review, i.e. sent
back to the annotator for rework. A job's current state only shows where
it stands now, not how many times it got there, so this needs the log.
"""
with events_path.open(newline="") as f:
return sum(
1
for row in csv.DictReader(f)
if row["scope"] == "update:job"
and row["obj_name"] == "state"
and row["obj_val"] == "rejected"
)
def main() -> None:
args = parse_args()
with make_client(args.host, access_token=args.token) as client:
# 1. Inspect
task = client.tasks.retrieve(args.task_id)
jobs = task.get_jobs()
print(f"Task {task.id}: {task.name!r}, {task.size} frames")
print(f" labels: {[label.name for label in task.get_labels()]}")
for job in jobs:
print(f" job {job.id}: stage={job.stage}, state={job.state}")
# 2. Validate the export format against the server's list.
# Low-level API: there is no high-level proxy for the format list yet.
formats, _ = client.api_client.server_api.retrieve_annotation_formats()
names = [f.name for f in formats.exporters]
if args.export_format not in names:
sys.exit(
f"Unknown export format {args.export_format!r}. Choose one of: {', '.join(names)}"
)
# 3. Export the dataset to a local zip
local_path = Path(f"task_{task.id}_dataset.zip")
task.export_dataset(
args.export_format, local_path, include_images=False, location=Location.LOCAL
)
print(f"Exported {local_path.resolve()}")
# 4. Export the task's event log and report quick analytics.
events_path = Path(f"task_{task.id}_events.csv")
Downloader(client).prepare_and_download_file_from_endpoint(
client.api_client.events_api.create_export_endpoint,
events_path,
query_params={"task_id": task.id},
)
print(f"Exported {events_path.resolve()}")
assigned = {job.assignee.id for job in jobs if job.assignee}
print(f" {len(assigned)} people currently assigned, {count_reworks(events_path)} reworks")
if __name__ == "__main__":
main()
Other SDK options:
| SDK method / parameter |
What it adds |
client.tasks.create_from_data(..., resource_type=ResourceType.LOCAL | SHARE | REMOTE) |
Where resources come from: LOCAL (upload local files), SHARE (keys in a cloud storage / mounted share), REMOTE (URLs). Defaults to LOCAL. |
client.tasks.create_from_data(..., data_params={...}) |
Extra data options as a dict, e.g. image_quality (1-100), sorting_method ("lexicographical"/"natural"/"predefined"/"random"), cloud_storage_id (int). |
client.tasks.create_from_data(..., annotation_path="path.zip", annotation_format="CVAT XML 1.1") |
Upload an initial annotations file at creation. annotation_path is a str file path; annotation_format is a str, default "CVAT XML 1.1". |
client.tasks.create_from_data(..., status_check_period=<int seconds>, pbar=ProgressReporter()) |
status_check_period (int, seconds) is the upload status poll interval (defaults to Config.status_check_period); pbar is a cvat_sdk.core.progress.ProgressReporter for upload progress. |
client.tasks.list(..., search=, sort=) |
Free-text search and server-side ordering (sort), in addition to filter. |
client.tasks.create_from_backup(path) |
Recreate a task from a task backup archive. |
Task.import_annotations(format_name, path) |
Load annotations into an existing task - the import counterpart of export_dataset. |
Task.get_frame(frame_id: int, *, quality="original" | "compressed") |
Return a single frame as a file-like object (io.RawIOBase) of image bytes. quality is an optional keyword argument ("original" or "compressed"); if omitted, the server default is used. |
Task.download_frames(frame_ids: Sequence[int], outdir=".", quality="original", image_extension=None, filename_pattern="frame_{frame_id:06d}{frame_ext}") |
Save the given frames to disk under outdir. image_extension (e.g. "png") overrides the auto-detected extension; quality is "original" or "compressed". |
Task.get_meta() / Task.get_frames_info() |
Read frame count, chunk layout, and per-frame metadata. |
Task.export_dataset(..., pbar=ProgressReporter()) |
Report local-download progress (a cvat_sdk.core.progress.ProgressReporter). |
Task.export_dataset(..., status_check_period=<int seconds>) |
Poll interval (int, seconds) between server status checks; defaults to Config.status_check_period. |
Task.export_dataset(filename=<directory>) |
Pass a directory as filename for a local export and the server-generated file name is used. |
Task.export_dataset(..., location=Location.CLOUD_STORAGE, cloud_storage_id=<int>) |
Export straight to a registered cloud storage instead of downloading locally. |
client.api_client.events_api.create_export(project_id=, job_id=, user_id=, _from=, to=) |
Scope or time-bound the event-log export beyond a single task. |
Notes:
- To add a task to a project, pass
project_id in TaskWriteRequest and do
not pass labels — the task inherits the project’s label schema.
- Both cloud recipes use
ResourceType.SHARE, so the images are read from the
bucket rather than uploaded from your machine.
include_images=False exports annotations only and is much smaller.
- Pass a valid
format_name from the server’s exporter list, e.g. "COCO 1.0"
or "CVAT for images 1.1". An unknown format name is rejected by the recipe.
- Full recipes:
task_create_from_cloud.py,
tasks_bulk_from_cloud.py,
task_inspect_and_export.py.
4 - Job recipes
List a task’s or project’s jobs, round-robin unassigned jobs, batch-advance completed jobs
Three recipes: job_list.py lists a task’s or project’s jobs with optional
stage/state filters and an optional CSV report, job_assign.py round-robins
unassigned jobs across a resolved pool of users and writes a CSV report, and
job_workflow.py batch-advances every completed job at a given stage to the
next stage.
List a task’s or project’s jobs
Queries the jobs of a task or a project (pick one with --task-id or
--project-id) with optional server-side --stage / --state filters,
ordered by most recently updated. Pass --csv to also write report.csv
(project_id, project_name, task_id, task_name, job_id, stage, state, assignee, frames) into the current directory.
| Flag |
Required |
Meaning |
--host |
yes |
Server URL |
--token |
yes |
Personal Access Token |
--task-id |
one of --task-id / --project-id |
Id of the task whose jobs to list |
--project-id |
one of --task-id / --project-id |
Id of the project whose jobs to list |
--stage |
no |
Only jobs at this stage, e.g. annotation |
--state |
no |
Only jobs in this state, e.g. new |
--csv |
no |
Also write report.csv into the current directory |
python job_list.py --host 'https://app.cvat.ai' --token '<your token>' \
--task-id 42
python job_list.py --host 'https://app.cvat.ai' --token '<your token>' \
--task-id 42 --stage annotation --state new
python job_list.py --host 'https://app.cvat.ai' --token '<your token>' \
--project-id 7 --csv
The script
# Copyright (C) CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT
"""List the jobs of an existing task or project with their stage, state, and
assignee, optionally as a CSV report.
Steps:
1. Query jobs scoped to --task-id or --project-id, most recently updated
first. --stage / --state filter server-side, so large tasks/projects
stay cheap. The same endpoint also accepts free-text search, e.g.
search='alice'.
2. Print one row per job.
3. If --csv is passed, also write report.csv into the current directory
(project_id, project_name, task_id, task_name, job_id, stage, state,
assignee, frames).
Usage (run ``python job_list.py --help`` for the full list of options):
python job_list.py --host 'https://app.cvat.ai' --token '<your token>' \\
--task-id 42
python job_list.py --host 'https://app.cvat.ai' --token '<your token>' \\
--task-id 42 --stage annotation --state new
python job_list.py --host 'https://app.cvat.ai' --token '<your token>' \\
--project-id 7 --csv
"""
import argparse
import csv
from collections.abc import Iterable
from pathlib import Path
from cvat_sdk import make_client
from cvat_sdk.core.filters import F, all_
from cvat_sdk.core.proxies.jobs import Job
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
parser.add_argument("--host", required=True, help="CVAT server URL, e.g. 'https://app.cvat.ai'")
parser.add_argument(
"--token",
required=True,
help="Personal Access Token (CVAT UI: Profile -> Security)",
)
scope = parser.add_mutually_exclusive_group(required=True)
scope.add_argument("--task-id", type=int, help="id of an existing task, e.g. 42")
scope.add_argument("--project-id", type=int, help="id of an existing project, e.g. 7")
parser.add_argument("--stage", help="only jobs at this stage, e.g. 'annotation'")
parser.add_argument("--state", help="only jobs in this state, e.g. 'new'")
parser.add_argument(
"--csv", action="store_true", help="also write report.csv into the current directory"
)
return parser.parse_args()
def write_report(jobs: Iterable[Job], path: Path) -> None:
with path.open("w", newline="") as f:
writer = csv.writer(f)
writer.writerow(
[
"project_id",
"project_name",
"task_id",
"task_name",
"job_id",
"stage",
"state",
"assignee",
"frames",
]
)
for job in jobs:
assignee = job.assignee.username if job.assignee else ""
writer.writerow(
[
job.project_id or "",
job.project_name or "",
job.task_id,
job.task_name,
job.id,
job.stage,
job.state,
assignee,
job.frame_count,
]
)
def main() -> None:
args = parse_args()
with make_client(args.host, access_token=args.token) as client:
if args.task_id is not None:
conditions = [F.task_id == args.task_id]
scope_label = f"Task {args.task_id}"
else:
conditions = [F.project_id == args.project_id]
scope_label = f"Project {args.project_id}"
if args.stage:
conditions.append(F.stage == args.stage)
if args.state:
conditions.append(F.state == args.state)
jobs = client.jobs.list(filter=all_(*conditions), sort="-updated_date")
print(f"{scope_label}: {len(jobs)} matching jobs")
for job in jobs:
assignee = job.assignee.username if job.assignee else "-"
print(f" job {job.id}: stage={job.stage}, state={job.state}, assignee={assignee}")
if args.csv:
report_path = Path("report.csv")
write_report(jobs, report_path)
print(f"Wrote {report_path.resolve()}")
if __name__ == "__main__":
main()
Round-robin assign a task’s jobs
Distributes the unassigned jobs of a task across a resolved user pool and
writes assignments.csv (job_id, previous_assignee, new_assignee, new_assignee_id). The pool is resolved by looking up usernames exactly with
--assignees, by searching an organization’s members with --search, or
self-assigns if neither is passed.
| Flag |
Required |
Meaning |
--host |
yes |
Server URL |
--token |
yes |
Personal Access Token |
--task-id |
yes |
Id of the task |
--org SLUG |
no |
Organization slug to scope the user and job queries |
--org-id ID |
no |
Organization id, as an alternative to --org |
--assignees USERNAME [...] |
no |
Usernames to round-robin (exact match) |
--search QUERY |
no |
Search the organization’s members; every match becomes an assignee |
--assignees and --search are mutually exclusive, and so are --org and
--org-id. Omit both --assignees and --search to self-assign.
--search requires an organization, so pass it together with --org or
--org-id. Search matches the username, first_name, and last_name
fields, which is only meaningful scoped to a team.
# self-assign every unassigned job
python job_assign.py --host 'https://app.cvat.ai' --token '<your token>' \
--task-id 42
# round-robin across an explicit pool
python job_assign.py --host 'https://app.cvat.ai' --token '<your token>' \
--task-id 42 --assignees alice bob
# pool = every organization member matching the search
python job_assign.py --host 'https://app.cvat.ai' --token '<your token>' \
--task-id 42 --org 'annotators' --search 'annotator-team'
The script
# Copyright (C) CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT
"""Round-robin the unassigned jobs of a task across a set of annotators and
write a CSV report of the assignments (job_id, previous_assignee, new_assignee).
The user API supports server-side search within an organization, so you rarely
need to know user ids — pass usernames, or an organization and search query,
and let the recipe resolve them.
Steps:
1. Resolve the assignee pool:
--assignees USERNAME [USERNAME ...] : look up each username exactly.
--search QUERY --org SLUG : search organization members,
print the matches, use them all.
--search QUERY --org-id ID : same, using the organization id.
neither : assign to me (the authenticated user).
2. Filter the task's unassigned jobs.
3. Round-robin the jobs across the resolved users.
4. Write assignments.csv into the current directory.
Usage (run ``python job_assign.py --help`` for the full list of options):
python job_assign.py --host 'https://app.cvat.ai' --token '<your token>' \\
--task-id 42 # self-assign
python job_assign.py --host 'https://app.cvat.ai' --token '<your token>' \\
--task-id 42 --assignees alice bob
python job_assign.py --host 'https://app.cvat.ai' --token '<your token>' \\
--task-id 42 --org 'annotators' --search 'annotator-team'
# pool = matches in the organization
"""
import argparse
import csv
import sys
from pathlib import Path
from cvat_sdk import make_client, models
from cvat_sdk.core.filters import F, all_, not_
from cvat_sdk.core.proxies.users import User
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
parser.add_argument("--host", required=True, help="CVAT server URL, e.g. 'https://app.cvat.ai'")
parser.add_argument(
"--token",
required=True,
help="Personal Access Token (CVAT UI: Profile -> Security)",
)
parser.add_argument(
"--task-id", type=int, required=True, help="id of an existing task, e.g. 42"
)
organization = parser.add_mutually_exclusive_group()
organization.add_argument(
"--org", metavar="SLUG", help="organization slug to scope user and job queries"
)
organization.add_argument(
"--org-id", type=int, metavar="ID", help="organization id to scope user and job queries"
)
group = parser.add_mutually_exclusive_group()
group.add_argument(
"--assignees",
nargs="+",
metavar="USERNAME",
help="usernames to round-robin across (looked up exactly on the server)",
)
group.add_argument(
"--search",
metavar="QUERY",
help="server-side search within --org/--org-id; every matching member becomes an assignee",
)
args = parser.parse_args()
if args.search and args.org is None and args.org_id is None:
parser.error("--search requires --org or --org-id")
return args
def organization_filters(args: argparse.Namespace) -> dict[str, str | int]:
if args.org is not None:
return {"org": args.org}
if args.org_id is not None:
return {"org_id": args.org_id}
return {}
def resolve_pool(client, args: argparse.Namespace) -> list[User]:
"""Resolve --assignees / --search / nothing to a list of User objects."""
org_filters = organization_filters(args)
if args.search:
matches = client.users.list(search=args.search, **org_filters)
if not matches:
sys.exit(f"No users matched search {args.search!r}")
print(f"Users matching {args.search!r}:")
for user in matches:
print(f" {user.id}\t{user.username}")
return matches
if args.assignees:
pool: list[User] = []
for username in args.assignees:
found = client.users.list(filter=F.username == username, **org_filters)
if not found:
sys.exit(f"User {username!r} not found")
pool.append(found[0])
return pool
me = client.users.retrieve_current_user()
print(f"No --assignees / --search; self-assigning as {me.username} (id={me.id})")
return [me]
def main() -> None:
args = parse_args()
report_path = Path("assignments.csv")
with make_client(args.host, access_token=args.token) as client:
pool = resolve_pool(client, args)
unassigned = client.jobs.list(
filter=all_(F.task_id == args.task_id, not_(F.assignee.is_set())),
**organization_filters(args),
)
print(f"Task {args.task_id}: {len(unassigned)} unassigned jobs to distribute")
with report_path.open("w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["job_id", "previous_assignee", "new_assignee", "new_assignee_id"])
for i, job in enumerate(unassigned):
user = pool[i % len(pool)]
previous = job.assignee.username if job.assignee else ""
job.update(models.PatchedJobWriteRequest(assignee=user.id))
writer.writerow([job.id, previous, user.username, user.id])
print(f"Assigned job {job.id} -> {user.username} (id={user.id})")
print(f"Wrote {report_path.resolve()}")
if __name__ == "__main__":
main()
Batch-advance completed jobs
Finds every job whose state is completed at --from-stage and moves each
one to the next stage (annotation → validation → acceptance). Optionally
restrict the sweep to a single task.
| Flag |
Required |
Meaning |
--host |
yes |
Server URL |
--token |
yes |
Personal Access Token |
--from-stage |
yes |
Advance completed jobs at this stage (annotation or validation) |
--task-id |
no |
Restrict the sweep to a single task |
# send everything annotators finished into review
python job_workflow.py --host 'https://app.cvat.ai' --token '<your token>' \
--from-stage annotation
# accept everything that passed review, scoped to one task
python job_workflow.py --host 'https://app.cvat.ai' --token '<your token>' \
--from-stage validation --task-id 42
The script
# Copyright (C) CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT
"""Batch-advance completed jobs to the next workflow stage
Find every job whose state is 'completed' at --from-stage, move each one to
the next stage, and print the list of modified jobs. Optionally restrict the
sweep to a single task with --task-id.
Steps:
1. Query jobs matching (stage == --from-stage, state == 'completed').
2. Update each job's stage to the next one in the workflow.
3. Print the modified job ids.
Usage (run ``python job_workflow.py --help`` for the full list of options):
# Send everything annotators finished into review:
python job_workflow.py --host 'https://app.cvat.ai' --token '<your token>' \\
--from-stage annotation
# Accept everything that passed review, scoped to one task:
python job_workflow.py --host 'https://app.cvat.ai' --token '<your token>' \\
--from-stage validation --task-id 42
"""
import argparse
from cvat_sdk import make_client, models
from cvat_sdk.core.filters import F, all_
NEXT_STAGE = {"annotation": "validation", "validation": "acceptance"}
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
parser.add_argument("--host", required=True, help="CVAT server URL, e.g. 'https://app.cvat.ai'")
parser.add_argument(
"--token",
required=True,
help="Personal Access Token (CVAT UI: Profile -> Security)",
)
parser.add_argument(
"--from-stage",
required=True,
choices=sorted(NEXT_STAGE),
help="advance completed jobs currently at this stage",
)
parser.add_argument(
"--task-id",
type=int,
help="restrict the sweep to a single task (default: every task you can see)",
)
return parser.parse_args()
def main() -> None:
args = parse_args()
to_stage = NEXT_STAGE[args.from_stage]
with make_client(args.host, access_token=args.token) as client:
conditions = [F.stage == args.from_stage, F.state == "completed"]
if args.task_id is not None:
conditions.append(F.task_id == args.task_id)
jobs = client.jobs.list(filter=all_(*conditions))
print(f"Found {len(jobs)} completed jobs at stage {args.from_stage!r}")
for job in jobs:
job.update(models.PatchedJobWriteRequest(stage=to_stage))
print(f" job {job.id}: {args.from_stage} -> {to_stage}")
print(f"Moved {len(jobs)} jobs to stage {to_stage!r}")
if __name__ == "__main__":
main()
Other SDK options:
| SDK method / parameter |
What it adds |
Job.update(models.PatchedJobWriteRequest(stage=...)) |
Change a job’s stage (retrieve the job, then update). Must be one of: annotation, validation, acceptance. |
Job.update(models.PatchedJobWriteRequest(state=...)) |
Change a job’s state, must be one of these values: new, in progress, rejected, completed. |
Job.import_annotations(..., import_mode="replace" | "append") |
"replace" overwrites the job’s existing annotations (default); "append" merges the imported ones in. |
Job.import_annotations(..., conv_mask_to_poly=True | False) |
Convert imported mask annotations to polygons (bool, server default True). |
Job.import_annotations(..., pbar=ProgressReporter()) |
Report upload progress (a cvat_sdk.core.progress.ProgressReporter). |
Job.get_issues() |
Fetch the review issues raised on a job. |
Job.export_dataset(format_name, path) |
Export a single job’s dataset - the export counterpart of import_annotations. |
Job.get_frame(frame_id: int, *, quality="original" | "compressed") |
Return a single frame as a file-like object (io.RawIOBase) of image bytes. quality is an optional keyword argument ("original" or "compressed"); if omitted, the server default is used. |
Job.download_frames(frame_ids: Sequence[int], outdir=".", quality="original", image_extension=None, filename_pattern="frame_{frame_id:06d}{frame_ext}") |
Save the given frames to disk under outdir. image_extension (e.g. "png") overrides the auto-detected extension; quality is "original" or "compressed". |
Job.get_meta() / Job.get_labels() |
Read a job’s frame metadata and label schema. |
Notes:
stage is one of annotation, validation, acceptance; state is one of
new, in progress, rejected, completed.
- Jobs are created automatically with their task (controlled by
segment_size
at task creation) — you can update and assign them, but not create a job on
its own.
- CVAT has no built-in auto-assignment, so
job_assign.py is the scripted
pattern.
- Full recipes:
job_list.py,
job_assign.py,
job_workflow.py.
5 - Cloud storage recipes
Attach an S3-compatible bucket to CVAT via the low-level cloud storages API
One recipe: cloud_storage_register.py registers an S3-compatible bucket
(AWS S3, MinIO, DigitalOcean Spaces, …) as a CVAT cloud storage. It uses the
low-level client.api_client.cloudstorages_api because there is no high-level
proxy for cloud storages yet.
Attach a bucket to CVAT
Registers a bucket by key/secret, lists all registered storages, retrieves the
new one, lists the bucket’s actual content, and renames it — a smoke test that
the credentials work.
| Flag |
Required |
Meaning |
--host |
yes |
Server URL |
--token |
yes |
Personal Access Token |
--bucket |
yes |
Bucket name |
--access-key |
yes |
Bucket access key id |
--secret-key |
yes |
Bucket secret key |
--endpoint-url |
yes |
Endpoint URL, e.g. 'https://s3.amazonaws.com' |
--page-size |
no |
Entries per bucket listing request (default: the server maximum, 500) |
--cleanup |
no |
Detach the bucket from CVAT at the end (data untouched) |
python cloud_storage_register.py --host 'https://app.cvat.ai' --token '<your token>' \
--bucket 'my-bucket' --access-key '<key>' --secret-key '<secret>' \
--endpoint-url 'https://s3.amazonaws.com'
The script
# Copyright (C) CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT
"""Attach an S3-compatible bucket to CVAT as a cloud storage, then list,
retrieve, and update it. Any S3-compatible service works (AWS S3, minio, ...)
via the AWS_S3_BUCKET provider and a custom endpoint URL.
There is no high-level proxy for cloud storages yet, so this recipe uses the
low-level API (client.api_client.cloudstorages_api).
Steps:
1. Attach the bucket with key/secret credentials to CVAT.
2. List all registered storages.
3. Retrieve the new one.
4. List the bucket's content, a page at a time.
5. Update its display name.
6. Optionally, detach it from CVAT.
Usage (run ``python cloud_storage_register.py --help`` for the full list of options):
python cloud_storage_register.py --host 'https://app.cvat.ai' --token '<your token>' \\
--bucket 'my-bucket' --access-key '<key>' --secret-key '<secret>' \\
--endpoint-url 'https://s3.amazonaws.com'
"""
import argparse
from cvat_sdk import make_client, models
from cvat_sdk.core.helpers import get_paginated_collection
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
parser.add_argument("--host", required=True, help="CVAT server URL, e.g. 'https://app.cvat.ai'")
parser.add_argument(
"--token",
required=True,
help="Personal Access Token (CVAT UI: Profile -> Security)",
)
parser.add_argument("--bucket", required=True, help="the bucket name, e.g. 'my-bucket'")
parser.add_argument("--access-key", required=True, help="the bucket's access key id")
parser.add_argument("--secret-key", required=True, help="the bucket's secret key")
parser.add_argument(
"--endpoint-url",
required=True,
help="e.g. 'https://s3.amazonaws.com' or 'http://minio:9000'",
)
parser.add_argument(
"--page-size",
type=int,
help="entries to fetch per bucket listing request (default: the server's "
"maximum, 500); a small value makes the pagination loop visible",
)
parser.add_argument(
"--cleanup",
action="store_true",
help="detach the storage at the end (data is never touched)",
)
return parser.parse_args()
def main() -> None:
args = parse_args()
with make_client(args.host, access_token=args.token) as client:
api = client.api_client.cloudstorages_api
# 1. Register
storage, _ = api.create(
models.CloudStorageWriteRequest(
provider_type="AWS_S3_BUCKET", # any S3-compatible service
resource=args.bucket,
display_name=args.bucket,
credentials_type="KEY_SECRET_KEY_PAIR",
key=args.access_key,
secret_key=args.secret_key,
specific_attributes=f"endpoint_url={args.endpoint_url}",
)
)
print(f"Registered cloud storage {storage.id} -> {args.bucket}")
# 2. List — api.list() returns a single page. Pair it with
# get_paginated_collection to walk every page of any low-level list
# endpoint (works for tasks_api.list_endpoint, jobs_api.list_endpoint, ...).
storages = get_paginated_collection(api.list_endpoint)
print(f"Registered storages: {[cs.id for cs in storages]}")
# 3. Retrieve — credentials are never returned, only metadata
fetched, _ = api.retrieve(storage.id)
print(f"Storage {fetched.id}: {fetched.display_name!r} ({fetched.provider_type})")
# 4. List the bucket's content, a page at a time via next_token.
page_params = {"page_size": args.page_size} if args.page_size else {}
files = []
pages = 0
next_token = None
while True:
content, _ = api.retrieve_content_v2(
storage.id,
**page_params,
**({"next_token": next_token} if next_token else {}),
)
files.extend(content.content)
pages += 1
if not content.next:
break
next_token = content.next
print(f"Bucket {args.bucket!r} contains {len(files)} entries in {pages} page(s):")
for f in files:
print(f" {f.type.value:>3} {f.name}")
# 5. Update the display name (PATCH — only the passed fields change)
updated, _ = api.partial_update(
storage.id,
patched_cloud_storage_write_request=models.PatchedCloudStorageWriteRequest(
display_name=f"{args.bucket} (updated)"
),
)
print(f"Renamed storage {updated.id} to {updated.display_name!r}")
# 6. Opt-in cleanup: detaches the bucket from CVAT, never deletes data
if args.cleanup:
api.destroy(storage.id)
print(f"Deleted cloud storage {storage.id}")
else:
print("Keeping the storage; pass --cleanup to delete it")
if __name__ == "__main__":
main()
Other SDK options:
The recipe uses the low-level client.api_client.cloudstorages_api because
there is no high-level proxy for cloud storages yet.
| SDK method / parameter |
What it adds |
models.CloudStorageWriteRequest(description=...) |
Free-text description shown alongside the storage. |
models.CloudStorageWriteRequest(manifests=[...]) |
Attach manifest files so CVAT can index large buckets faster. |
CloudStorageWriteRequest(session_token=..., connection_string=..., account_name=...) |
Alternative credential fields for other providers (e.g. Azure, temporary S3 sessions). |
cloudstorages_api.retrieve_status(id=...) |
Check whether a registered storage is reachable/healthy. |
cloudstorages_api.retrieve_actions(id: int) |
Return the operations the credentials allow on the bucket (e.g. "read" / "read,write") as a string. id is the cloud storage id; the string is the returned data (first tuple element). |
cloudstorages_api.retrieve_content_v2(id, prefix=..., manifest_path=..., page_size=...) |
List the bucket’s actual files/directories. prefix filters to one “directory”; manifest_path lists from a manifest instead of a live bucket scan (faster for large buckets). |
cloudstorages_api.retrieve_preview(id: int) |
Fetch a preview image for the storage. id is the cloud storage id; the image bytes are on the HTTP response (response.data, the second tuple element), not the parsed data. |
PatchedCloudStorageWriteRequest(key=..., secret_key=...) |
Rotate credentials through partial_update (any writable field can be patched). |
get_paginated_collection(api.list_endpoint) |
Walk every page of any low-level *_api.list_endpoint (tasks, jobs, cloud storages, …); returns a flat list. |
Notes:
- The server validates the bucket by connecting to
endpoint_url itself, so
use an address the server container can reach.
- Cleanup detaches the bucket from CVAT; the bucket’s contents are never
touched.
- Full recipe:
cloud_storage_register.py.