From d6e6a2216244418b510fb60f4595c428018eb249 Mon Sep 17 00:00:00 2001 From: Roman Donchenko Date: Wed, 14 Jun 2023 21:12:31 +0400 Subject: [PATCH] Add organization support to the CLI --- CHANGELOG.md | 2 + cvat-cli/src/cvat_cli/__main__.py | 6 +- cvat-cli/src/cvat_cli/parser.py | 9 +++ site/content/en/docs/api_sdk/cli/_index.md | 29 +++++++--- tests/python/cli/test_cli.py | 65 +++++++++++++++++++--- 5 files changed, 95 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51b5022bd7f..274ca879dfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - \[Server API\] An option to supply custom file ordering for task data uploads () - New option ``semi-auto`` is available as annotations source () +- \[CLI\] An option to select the organization + () ### Changed - Allowed to use dataset manifest for the `predefined` sorting method for task data () diff --git a/cvat-cli/src/cvat_cli/__main__.py b/cvat-cli/src/cvat_cli/__main__.py index ce32b832fdd..673adf3e6ae 100755 --- a/cvat-cli/src/cvat_cli/__main__.py +++ b/cvat-cli/src/cvat_cli/__main__.py @@ -37,13 +37,17 @@ def build_client(parsed_args: SimpleNamespace, logger: logging.Logger) -> Client if parsed_args.server_port: url += f":{parsed_args.server_port}" - return Client( + client = Client( url=url, logger=logger, config=config, check_server_version=False, # version is checked after auth to support versions < 2.3 ) + client.organization_slug = parsed_args.organization + + return client + def main(args: List[str] = None): actions = { diff --git a/cvat-cli/src/cvat_cli/parser.py b/cvat-cli/src/cvat_cli/parser.py index a1d6a3f78df..32630baaad8 100644 --- a/cvat-cli/src/cvat_cli/parser.py +++ b/cvat-cli/src/cvat_cli/parser.py @@ -77,6 +77,15 @@ def make_cmdline_parser() -> argparse.ArgumentParser: default=None, help="port (default: 80 for http and 443 for https connections)", ) + parser.add_argument( + "--organization", + "--org", + metavar="SLUG", + help="""short name (slug) of the organization + to use when listing or creating resources; + set to blank string to use the personal workspace + (default: list all accessible objects, create in personal workspace)""", + ) parser.add_argument( "--debug", action="store_const", diff --git a/site/content/en/docs/api_sdk/cli/_index.md b/site/content/en/docs/api_sdk/cli/_index.md index 0a5be3763a8..c44596a8983 100644 --- a/site/content/en/docs/api_sdk/cli/_index.md +++ b/site/content/en/docs/api_sdk/cli/_index.md @@ -37,23 +37,29 @@ We support Python versions 3.7 - 3.9. You can get help with `cvat-cli --help`. ``` -usage: cvat-cli [-h] [--auth USER:[PASS]] - [--server-host SERVER_HOST] [--server-port SERVER_PORT] [--debug] - {create,delete,ls,frames,dump,upload,export,import} ... +usage: cvat-cli [-h] [--version] [--insecure] [--auth USER:[PASS]] [--server-host SERVER_HOST] + [--server-port SERVER_PORT] [--organization SLUG] [--debug] + {create,delete,ls,frames,dump,upload,export,import} ... Perform common operations related to CVAT tasks. positional arguments: {create,delete,ls,frames,dump,upload,export,import} -optional arguments: +options: -h, --help show this help message and exit - --auth USER:[PASS] defaults to the current user and supports the PASS - environment variable or password prompt. + --version show program's version number and exit + --insecure Allows to disable SSL certificate check + --auth USER:[PASS] defaults to the current user and supports the PASS environment variable or password + prompt (default user: ...). --server-host SERVER_HOST host (default: localhost) --server-port SERVER_PORT - port (default: 8080) + port (default: 80 for http and 443 for https connections) + --organization SLUG, --org SLUG + short name (slug) of the organization to use when listing or creating resources; set + to blank string to use the personal workspace (default: list all accessible objects, + create in personal workspace) --debug show debug output ``` @@ -110,6 +116,11 @@ by using the [label constructor](/docs/manual/basics/creating_an_annotation_task cvat-cli --server-host example.com --auth user-1 create "task 1" \ --labels labels.json local image1.jpg ``` +- Create a task named "task 1" on the default server, with labels from "labels.json" + and local image "file1.jpg", as the current user, in organization "myorg": + ```bash + cvat-cli --org myorg create "task 1" --labels labels.json local file1.jpg + ``` - Create a task named "task 1", labels from the project with id 1 and with a remote video file, the task will be created as user "user-1": ```bash @@ -172,6 +183,10 @@ by using the [label constructor](/docs/manual/basics/creating_an_annotation_task ```bash cvat-cli ls ``` +- List all tasks in organization "myorg": + ```bash + cvat-cli --org myorg ls + ``` - Save list of all tasks into file "list_of_tasks.json": ```bash cvat-cli ls --json > list_of_tasks.json diff --git a/tests/python/cli/test_cli.py b/tests/python/cli/test_cli.py index 8b0e5bf0111..6dbcbb5241f 100644 --- a/tests/python/cli/test_cli.py +++ b/tests/python/cli/test_cli.py @@ -6,12 +6,13 @@ import json import os from pathlib import Path +from typing import Optional import packaging.version as pv import pytest from cvat_cli.cli import CLI from cvat_sdk import Client, make_client -from cvat_sdk.api_client import exceptions +from cvat_sdk.api_client import exceptions, models from cvat_sdk.core.proxies.tasks import ResourceType, Task from PIL import Image @@ -84,15 +85,21 @@ def fxt_new_task(self): return task - def run_cli(self, cmd: str, *args: str, expected_code: int = 0) -> str: + def run_cli( + self, cmd: str, *args: str, expected_code: int = 0, organization: Optional[str] = None + ) -> str: + common_args = [ + f"--auth={self.user}:{self.password}", + f"--server-host={self.host}", + f"--server-port={self.port}", + ] + + if organization is not None: + common_args.append(f"--organization={organization}") + run_cli( self, - "--auth", - f"{self.user}:{self.password}", - "--server-host", - self.host, - "--server-port", - self.port, + *common_args, cmd, *args, expected_code=expected_code, @@ -253,3 +260,45 @@ def my_init(self, *args, **kwargs): self.run_cli(*(["--insecure"] if not verify else []), "ls") assert capture.value.args[0] == verify + + def test_can_control_organization_context(self): + org = "cli-test-org" + self.client.organizations.create(models.OrganizationWriteRequest(org)) + + files = generate_images(self.tmp_path, 1) + + stdout = self.run_cli( + "create", + "personal_task", + ResourceType.LOCAL.name, + *map(os.fspath, files), + "--labels=" + json.dumps([{"name": "person"}]), + "--completion_verification_period=0.01", + organization="", + ) + + personal_task_id = int(stdout.split()[-1]) + + stdout = self.run_cli( + "create", + "org_task", + ResourceType.LOCAL.name, + *map(os.fspath, files), + "--labels=" + json.dumps([{"name": "person"}]), + "--completion_verification_period=0.01", + organization=org, + ) + + org_task_id = int(stdout.split()[-1]) + + personal_task_ids = list(map(int, self.run_cli("ls", organization="").split())) + assert personal_task_id in personal_task_ids + assert org_task_id not in personal_task_ids + + org_task_ids = list(map(int, self.run_cli("ls", organization=org).split())) + assert personal_task_id not in org_task_ids + assert org_task_id in org_task_ids + + all_task_ids = list(map(int, self.run_cli("ls").split())) + assert personal_task_id in all_task_ids + assert org_task_id in all_task_ids