feat(sdk-examples): add domain-organized SDK examples - #11049
Conversation
…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/`. | |||
| 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" |
There was a problem hiding this comment.
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.
| export S3_BUCKET=my-bucket | ||
| export S3_ACCESS_KEY=... | ||
| export S3_SECRET_KEY=... | ||
| export S3_ENDPOINT_URL=https://s3.amazonaws.com |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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>
| 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. | ||
|
|
There was a problem hiding this comment.
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.
| 3. Assign them round-robin across CVAT_ASSIGNEE_IDS — or all to you, | ||
| if CVAT_ASSIGNEE_IDS is not set. | ||
|
|
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
Co-authored-by: Maxim Zhiltsov <maxim@cvat.ai>
Co-authored-by: Maxim Zhiltsov <maxim@cvat.ai>
| 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}") | ||
|
|
There was a problem hiding this comment.
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.
| # 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}") | ||
|
|
There was a problem hiding this comment.
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.
| 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]}") |
There was a problem hiding this comment.
Consider adding a filter on the job stage and state (basically, annotation - new should work fine).
| # 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]}") | ||
|
|
There was a problem hiding this comment.
| # 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).
| """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. | ||
|
|
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
The comment is invalid. There's nothing about the acceptance stage in the code.
| A backup contains the project's tasks, jobs, annotations, and settings, so | ||
| this doubles as a copy-a-project recipe. | ||
|
|
There was a problem hiding this comment.
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.
| 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). | ||
|
|
There was a problem hiding this comment.
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.
| for task in project.get_tasks(): | ||
| for job in task.get_jobs(): | ||
| assignee = job.assignee.username if job.assignee else "" |
There was a problem hiding this comment.
I'd call this pair an antipattern, please replace it with client.jobs.list(<project_id filter>)
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
developbranchLicense
Feel free to contact the maintainers if that's a concern.