diff --git a/.travis.yml b/.travis.yml index f4218109..c75681e0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,10 +7,10 @@ python: services: - docker - + before_script: - docker-compose -f docker-compose.yml -f docker-compose.ci.yml up --build -d script: - - docker exec -it cvat /bin/bash -c 'python3 manage.py test cvat/apps/engine' + - docker exec -it cvat /bin/bash -c 'python3 manage.py test cvat/apps/engine utils/cli' - docker exec -it cvat /bin/bash -c 'cd cvat-core && npm install && npm run test && npm run coveralls' diff --git a/.vscode/launch.json b/.vscode/launch.json index ee8bb4fb..069fe74d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -135,6 +135,7 @@ "--settings", "cvat.settings.testing", "cvat/apps/engine", + "utils/cli" ], "django": true, "cwd": "${workspaceFolder}", diff --git a/Dockerfile b/Dockerfile index a4c0e655..cf99ffda 100644 --- a/Dockerfile +++ b/Dockerfile @@ -139,6 +139,7 @@ RUN if [ "$WITH_DEXTR" = "yes" ]; then \ fi COPY ssh ${HOME}/.ssh +COPY utils ${HOME}/utils COPY cvat/ ${HOME}/cvat COPY cvat-core/ ${HOME}/cvat-core COPY tests ${HOME}/tests diff --git a/README.md b/README.md index 945d9f10..12b6eb6c 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ CVAT is free, online, interactive video and image annotation tool for computer v - [Installation guide](cvat/apps/documentation/installation.md) - [User's guide](cvat/apps/documentation/user_guide.md) - [Django REST API documentation](#rest-api) +- [Command line interface](utils/cli/) - [XML annotation format](cvat/apps/documentation/xml_format.md) - [AWS Deployment Guide](cvat/apps/documentation/AWS-Deployment-Guide.md) - [Questions](#questions) diff --git a/utils/cli/README.md b/utils/cli/README.md new file mode 100644 index 00000000..ac427186 --- /dev/null +++ b/utils/cli/README.md @@ -0,0 +1,44 @@ +# Command line interface (CLI) +**Description** +A simple command line interface for working with CVAT tasks. At the moment it +implements a basic feature set but may serve as the starting point for a more +comprehensive CVAT administration tool in the future. + +Overview of functionality: + +- Create a new task (supports name, bug tracker, labels JSON, local/share/remote files) +- Delete tasks (supports deleting a list of task IDs) +- List all tasks (supports basic CSV or JSON output) +- Download JPEG frames (supports a list of frame IDs) +- Dump annotations (supports all formats via format string) + +**Usage** +```bash +usage: cli.py [-h] [--auth USER:[PASS]] [--server-host SERVER_HOST] + [--server-port SERVER_PORT] [--debug] + {create,delete,ls,frames,dump} ... + +Perform common operations related to CVAT tasks. + +positional arguments: + {create,delete,ls,frames,dump} + +optional arguments: + -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. + --server-host SERVER_HOST + host (default: localhost) + --server-port SERVER_PORT + port (default: 8080) + --debug show debug output +``` +**Examples** +- List all tasks +`cli.py --auth user:pass --server-host localhost --server-port 8080 ls` +- Create a task +`cli.py create --name "new task" --labels labels.json local file1.jpg file2.jpg` +- Delete some tasks +`cli.py delete 100 101 102` +- Dump annotations +`cli.py dump --format "CVAT XML 1.1 for images" 103 output.xml` diff --git a/utils/cli/cli.py b/utils/cli/cli.py index 3407b23b..96b6881f 100755 --- a/utils/cli/cli.py +++ b/utils/cli/cli.py @@ -35,7 +35,7 @@ def main(): except (requests.exceptions.HTTPError, requests.exceptions.ConnectionError, requests.exceptions.RequestException) as e: - log.info(e) + log.critical(e) if __name__ == '__main__': diff --git a/utils/cli/core/core.py b/utils/cli/core/core.py index 157e30d1..0ee18a9b 100644 --- a/utils/cli/core/core.py +++ b/utils/cli/core/core.py @@ -21,11 +21,11 @@ class CLI(): data = None files = None if resource_type == ResourceType.LOCAL: - files = {f'client_files[{i}]': open(f, 'rb') for i, f in enumerate(resources)} + files = {'client_files[{}]'.format(i): open(f, 'rb') for i, f in enumerate(resources)} elif resource_type == ResourceType.REMOTE: - data = {f'remote_files[{i}]': f for i, f in enumerate(resources)} + data = {'remote_files[{}]'.format(i): f for i, f in enumerate(resources)} elif resource_type == ResourceType.SHARE: - data = {f'server_files[{i}]': f for i, f in enumerate(resources)} + data = {'server_files[{}]'.format(i): f for i, f in enumerate(resources)} response = self.session.post(url, data=data, files=files) response.raise_for_status() @@ -41,7 +41,7 @@ class CLI(): if use_json_output: log.info(json.dumps(r, indent=4)) else: - log.info(f'{r["id"]},{r["name"]},{r["status"]}') + log.info('{id},{name},{status}'.format(**r)) if not response_json['next']: return page += 1 @@ -60,8 +60,7 @@ class CLI(): response = self.session.post(url, json=data) response.raise_for_status() response_json = response.json() - log.info(f'Created task ID: {response_json["id"]} ' - f'NAME: {response_json["name"]}') + log.info('Created task ID: {id} NAME: {name}'.format(**response_json)) self.tasks_data(response_json['id'], resource_type, resources) def tasks_delete(self, task_ids, **kwargs): @@ -71,10 +70,10 @@ class CLI(): response = self.session.delete(url) try: response.raise_for_status() - log.info(f'Task ID {task_id} deleted') + log.info('Task ID {} deleted'.format(task_id)) except requests.exceptions.HTTPError as e: if response.status_code == 404: - log.info(f'Task ID {task_id} not found') + log.info('Task ID {} not found'.format(task_id)) else: raise e @@ -86,7 +85,7 @@ class CLI(): response = self.session.get(url) response.raise_for_status() im = Image.open(BytesIO(response.content)) - outfile = f'task_{task_id}_frame_{frame_id:06d}.jpg' + outfile = 'task_{}_frame_{:06d}.jpg'.format(task_id, frame_id) im.save(os.path.join(outdir, outfile)) def tasks_dump(self, task_id, fileformat, filename, **kwargs): @@ -103,7 +102,7 @@ class CLI(): while True: response = self.session.get(url) response.raise_for_status() - log.info(f'STATUS {response.status_code}') + log.info('STATUS {}'.format(response.status_code)) if response.status_code == 201: break @@ -118,23 +117,24 @@ class CVAT_API_V1(): """ Build parameterized API URLs """ def __init__(self, host, port): - self.base = f'http://{host}:{port}/api/v1/' + self.base = 'http://{}:{}/api/v1/'.format(host, port) @property def tasks(self): - return f'{self.base}tasks' + return self.base + 'tasks' def tasks_page(self, page_id): - return f'{self.tasks}?page={page_id}' + return self.tasks + '?page={page_id}' def tasks_id(self, task_id): - return f'{self.tasks}/{task_id}' + return self.tasks + '/{}'.format(task_id) def tasks_id_data(self, task_id): - return f'{self.tasks}/{task_id}/data' + return self.tasks_id(task_id) + '/data' def tasks_id_frame_id(self, task_id, frame_id): - return f'{self.tasks}/{task_id}/frames/{frame_id}' + return self.tasks_id(task_id) + '/frames/{}'.format(frame_id) def tasks_id_annotations_filename(self, task_id, name, fileformat): - return f'{self.tasks}/{task_id}/annotations/{name}?format={fileformat}' + return self.tasks_id(task_id) + '/annotations/{}?format={}' \ + .format(name, fileformat) diff --git a/utils/cli/tests/test_cli.py b/utils/cli/tests/test_cli.py index b09e7371..cbc0a63b 100644 --- a/utils/cli/tests/test_cli.py +++ b/utils/cli/tests/test_cli.py @@ -51,12 +51,12 @@ class TestCLI(APITestCase): def test_tasks_list(self): self.cli.tasks_list(False) - self.assertRegex(self.mock_stdout.getvalue(), f'.*{self.taskname}.*') + self.assertRegex(self.mock_stdout.getvalue(), '.*{}.*'.format(self.taskname)) def test_tasks_delete(self): self.cli.tasks_delete([1]) self.cli.tasks_list(False) - self.assertNotRegex(self.mock_stdout.getvalue(), f'.*{self.taskname}.*') + self.assertNotRegex(self.mock_stdout.getvalue(), '.*{}.*'.format(self.taskname)) def test_tasks_dump(self): path = os.path.join(settings.SHARE_ROOT, 'test_cli.xml')