Slightly enhance command line interface feature (#746)

* Slightly enhance command line interface feature.
Added README.md, run tests using travis, run CLI tests from VS code.
* Removed formatted string due to a limitation on our python version inside the container.
* Add information about command line interface to the main page.
main
Nikita Manovich 6 years ago committed by GitHub
parent b3d3ad24a9
commit 73bab7bea6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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'

@ -135,6 +135,7 @@
"--settings",
"cvat.settings.testing",
"cvat/apps/engine",
"utils/cli"
],
"django": true,
"cwd": "${workspaceFolder}",

@ -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

@ -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)

@ -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`

@ -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__':

@ -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)

@ -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')

Loading…
Cancel
Save