Skip to content

feat(sdk-examples): add domain-organized SDK examples - #11049

Open
Pr1ncee wants to merge 7 commits into
developfrom
feat/sdk-domain-examples
Open

feat(sdk-examples): add domain-organized SDK examples#11049
Pr1ncee wants to merge 7 commits into
developfrom
feat/sdk-domain-examples

Conversation

@Pr1ncee

@Pr1ncee Pr1ncee commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Add copy-pasteable CVAT Python SDK examples grouped by domain area under cvat-sdk/examples/ (authentication, projects, tasks, jobs, dataset export, and a shared MinIO cloud-storage helper). Each example is a small function taking an already-authenticated client, so a single function can be lifted straight into user code.

A pytest wrapper (tests/python/sdk/test_examples.py) exercises every example against the SDK test fixtures, including the CI MinIO cloud storage, so the examples cannot silently rot.

Docs are added as a hierarchical Common tasks section.

Motivation and context

How has this been tested?

Checklist

  • I submit my changes into the develop branch
  • I have created a changelog fragment
  • I have updated the documentation accordingly
  • I have added tests to cover my changes
  • I have linked related issues (see GitHub docs)

License

  • I submit my code changes under the same MIT License that covers the project.
    Feel free to contact the maintainers if that's a concern.

…docs

Add copy-pasteable CVAT Python SDK examples grouped by domain area under
cvat-sdk/examples/ (authentication, projects, tasks, jobs, dataset export,
and a shared MinIO cloud-storage helper). Each example is a small function
taking an already-authenticated client, so a single function can be lifted
straight into user code.

A pytest wrapper (tests/python/sdk/test_examples.py) exercises every example
against the SDK test fixtures, including the CI MinIO cloud storage, so the
examples cannot silently rot.

Docs are added as a hierarchical Common tasks section
(site/content/en/docs/api_sdk/sdk/examples/) mirroring the workspace layout,
with one topic page per domain area.
Replace the function-library SDK examples with 12 standalone, copy-and-run
recipe scripts covering authentication, projects, tasks, jobs, and cloud
storage. Each recipe is a self-contained module driven by environment
variables, with opt-in cleanup via CVAT_EXAMPLES_CLEANUP=1.

Add a subprocess-based test harness in tests/python/sdk/test_examples.py
that runs every recipe end-to-end against the local CVAT test server using
a generated Personal Access Token; 15 tests covering happy paths,
in-project variants, and expected-failure edge cases.

Rewrite the six docs pages under site/content/en/docs/api_sdk/sdk/examples/
so the python blocks are the exact script contents (verified by
concatenation), and update the changelog fragment and the examples README.
…/cvat into feat/sdk-domain-examples

# Conflicts:
#	changelog.d/20260814_211211_sdk_domain_examples.md
#	cvat-sdk/examples/README.md
#	site/content/en/docs/api_sdk/sdk/examples/_index.md
#	site/content/en/docs/api_sdk/sdk/examples/authentication.md
#	site/content/en/docs/api_sdk/sdk/examples/cloud-storage.md
#	site/content/en/docs/api_sdk/sdk/examples/jobs.md
#	site/content/en/docs/api_sdk/sdk/examples/projects.md
#	site/content/en/docs/api_sdk/sdk/examples/tasks.md
#	tests/python/sdk/test_examples.py
@@ -0,0 +1,3 @@
### Added

- SDK: New CVAT SDK usage examples under `cvat-sdk/examples/`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing PR link

ACCESS_KEY = require_env("S3_ACCESS_KEY", "the bucket's access key id")
SECRET_KEY = require_env("S3_SECRET_KEY", "the bucket's secret key")
ENDPOINT_URL = require_env("S3_ENDPOINT_URL", "e.g. https://s3.amazonaws.com or http://minio:9000")
CLEANUP = os.environ.get("CVAT_EXAMPLES_CLEANUP") == "1"

@zhiltsov-max zhiltsov-max Aug 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you choose to use env vars instead of CLI args? I think with argparse and CLI args we'd get automatic launch documentation via --help. Also, the examples currently assume a Unix-like shell, and switching to CLI args should make the examples applicable for windows as well.

Comment thread cvat-sdk/examples/cloud_storage_register.py Outdated
export S3_BUCKET=my-bucket
export S3_ACCESS_KEY=...
export S3_SECRET_KEY=...
export S3_ENDPOINT_URL=https://s3.amazonaws.com

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should add single quotes around the env variable values, otherwise some values can be treated differently by the shell.

print(f"Registered cloud storage {storage.id} -> {BUCKET}")

# 2. List
page, _ = api.list()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes sense to include an example for get_paginated_collection() here, as it's supposed to be used together with list() methods in the low-level API. Otherwise the users will tend to reinvent the wheel for getting the paginated list.

Co-authored-by: Maxim Zhiltsov <maxim@cvat.ai>
Comment on lines +13 to +17
1. If CVAT_PROFILE is set, use that profile; otherwise try the default profile.
2. If no profile exists and CVAT_USERNAME/CVAT_PASSWORD are set, fall back to
the DEPRECATED password sign-in (kept for local/dev servers only).
3. Print who you are authenticated as.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think, it would be better to keep only the profile auth and add another example on how to use the make_client_from_cli function to make a CLI-compatible script instead. The problem with the current variant is that it's nether a recommended, nor a full, nor a go-to client creation routine, that would be replaced either by token auth or by the CLI params in practice. If we change it as suggested, we'll get examples for the 2 or 3 recommended options - a general one for token clients, one for custom CLI-integrated clients (using profiles), and one for custom CLI-like clients, all practical.

Comment thread cvat-sdk/examples/cloud_storage_register.py Outdated
Comment thread cvat-sdk/examples/cloud_storage_register.py Outdated
Comment on lines +11 to +13
3. Assign them round-robin across CVAT_ASSIGNEE_IDS — or all to you,
if CVAT_ASSIGNEE_IDS is not set.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better to represent self-assignment with me instead. Having the variable unset looks like an error (in the context of these examples) and having it empty looks like an unassignment request.

Steps:
1. List all jobs of the task with their stage/state/assignee.
2. Filter the jobs that have no assignee yet.
3. Assign them round-robin across CVAT_ASSIGNEE_IDS — or all to you,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, the example is not really practical. You need to get a list of users somehow, and when you have them because you maintain them, they typically do not contain ids. Consider providing a way to retrieve a list of such users. I think it would work fine, if there was an example on getting a list of users satisfying a search query. So you could get a list of users in the first call and then use the output to assign users in the second call.

Pr1ncee and others added 2 commits August 18, 2026 20:25
Co-authored-by: Maxim Zhiltsov <maxim@cvat.ai>
Co-authored-by: Maxim Zhiltsov <maxim@cvat.ai>
Comment on lines +58 to +62
for i, job in enumerate(unassigned):
user_id = assignees[i % len(assignees)]
job.update(models.PatchedJobWriteRequest(assignee=user_id))
print(f"Assigned job {job.id} -> user {user_id}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We had a really practical example with returning a csv table in the result, consider doing it here. Such a table can be easily imported into other tools, e.g. for analysis or bookkeeping.

Comment on lines +44 to +50
# 1. List all jobs of the task
jobs = client.jobs.list(filter=F.task_id == TASK_ID)
print(f"Task {TASK_ID} has {len(jobs)} 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}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part can be a standalone example of job listing in a task. I don't think it really makes sense in this example though.

Comment on lines +52 to +53
unassigned = client.jobs.list(filter=all_(F.task_id == TASK_ID, not_(F.assignee.is_set())))
print(f"Unassigned jobs: {[job.id for job in unassigned]}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a filter on the job stage and state (basically, annotation - new should work fine).

Comment on lines +51 to +54
# 2. Only the unassigned ones
unassigned = client.jobs.list(filter=all_(F.task_id == TASK_ID, not_(F.assignee.is_set())))
print(f"Unassigned jobs: {[job.id for job in unassigned]}")

@zhiltsov-max zhiltsov-max Aug 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# 2. Only the unassigned ones
unassigned = client.jobs.list(filter=all_(F.task_id == TASK_ID, not_(F.assignee.is_set())))
print(f"Unassigned jobs: {[job.id for job in unassigned]}")
task = client.tasks.retrieve(TASK_ID)
unassigned = client.jobs.list(filter=all_(F.task_id == TASK_ID, not_(F.assignee.is_set())))
print(f"Unassigned jobs: {[job.id for job in unassigned]} out of {task.jobs.count}")

Consider doing something like this instead of (1).

Comment on lines +5 to +14
"""Drive a job through its workflow: pick the most recently updated job of a
task, import annotations into it, and move it to the validation stage.

Steps:
1. List the task's jobs, most recently updated first (server-side ordering;
the same endpoint also accepts free-text search, e.g. search="alice").
2. Import annotations from a file into the first job. The file's format must
match ANNOTATIONS_FORMAT (an importer name, e.g. "COCO 1.0").
3. Verify the shapes arrived, then move the job to the validation stage.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This workflow is quite meaningless. Basically, if you have a recently updated job (likely, annotated), why would you need to override the annotations? And why would you need to annotate the job manually, if you had annotations?

I'd suggest making a script that finds all annotation - completed jobs and moves them to the review stage, or finds all review - completed jobs to move them to the acceptance stage. Then prints the list of modified jobs.

shapes = job.get_annotations().shapes
print(f"Job {job.id} now has {len(shapes)} shapes")

# 3. Advance the workflow stage: annotation -> validation -> acceptance

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment is invalid. There's nothing about the acceptance stage in the code.

Comment on lines +7 to +9
A backup contains the project's tasks, jobs, annotations, and settings, so
this doubles as a copy-a-project recipe.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you rarely need to make both actions together in a single call. It's a valid for the project transfer case, but this example does not do a transfer.

Comment on lines +8 to +16
Steps:
1. Collect *.jpg / *.jpeg / *.png files from IMAGE_DIR (sorted).
2. Create the task and upload the images. With CVAT_PROJECT_ID the task is
created inside that project and inherits its labels; without it, the task
gets its own labels from CVAT_LABELS.
3. List tasks currently in the "annotation" status.
4. Retrieve the new task by id and rename it.
5. Optionally delete it (CVAT_EXAMPLES_CLEANUP=1).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we have enough coverage for single task creation from images already. We have a hello world example in the high-level SDK docs and we have a CLI command for this. I'd suggest changing the example to show bulk task creation from cloud images or videos. Putting a set of tasks into a project would make even more sense.

Comment on lines +58 to +60
for task in project.get_tasks():
for job in task.get_jobs():
assignee = job.assignee.username if job.assignee else ""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd call this pair an antipattern, please replace it with client.jobs.list(<project_id filter>)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants