Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add organization support to the CLI #6317

Merged
merged 2 commits into from
Jun 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,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 (<https://github.com/opencv/cvat/pull/5083>)
- New option ``semi-auto`` is available as annotations source (<https://github.com/opencv/cvat/pull/6263>)
- \[CLI\] An option to select the organization
(<https://github.com/opencv/cvat/pull/6317>)

### Changed
- Allowed to use dataset manifest for the `predefined` sorting method for task data (<https://github.com/opencv/cvat/pull/5083>)
Expand Down
6 changes: 5 additions & 1 deletion cvat-cli/src/cvat_cli/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
9 changes: 9 additions & 0 deletions cvat-cli/src/cvat_cli/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
29 changes: 22 additions & 7 deletions site/content/en/docs/api_sdk/cli/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
65 changes: 57 additions & 8 deletions tests/python/cli/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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