-
Notifications
You must be signed in to change notification settings - Fork 3.1k
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
New command line tool for working with tasks #732
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
#!/usr/bin/env python3 | ||
import functools | ||
import json | ||
import logging | ||
import requests | ||
import sys | ||
from io import BytesIO | ||
from PIL import Image | ||
from http.client import HTTPConnection | ||
from definition import parser, ResourceType | ||
log = logging.getLogger(__name__) | ||
|
||
|
||
class CLI(): | ||
|
||
def __init__(self, session, api): | ||
self.api = api | ||
self.session = session | ||
|
||
def exception(func): | ||
""" Provide generic exception handling for web requests.""" | ||
@functools.wraps(func) | ||
def wrapper(*args, **kwargs): | ||
try: | ||
return func(*args, **kwargs) | ||
except (requests.exceptions.HTTPError, | ||
requests.exceptions.ConnectionError, | ||
requests.exceptions.RequestException) as e: | ||
log.info(e) | ||
return wrapper | ||
|
||
@exception | ||
def tasks_data(self, task_id, resource_type, resources): | ||
""" Add local, remote, or shared files to an existing task. """ | ||
url = self.api.tasks_id_data(task_id) | ||
data = None | ||
files = None | ||
if resource_type == ResourceType.LOCAL: | ||
files = {f'client_files[{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)} | ||
elif resource_type == ResourceType.SHARE: | ||
data = {f'server_files[{i}]': f for i, f in enumerate(resources)} | ||
response = self.session.post(url, data=data, files=files) | ||
response.raise_for_status() | ||
|
||
@exception | ||
def tasks_list(self, use_json_output, **kwargs): | ||
""" List all tasks in either basic or JSON format. """ | ||
url = self.api.tasks | ||
response = self.session.get(url) | ||
response.raise_for_status() | ||
page = 1 | ||
while True: | ||
response_json = response.json() | ||
for r in response_json['results']: | ||
if use_json_output: | ||
log.info(json.dumps(r, indent=4)) | ||
else: | ||
log.info(f'{r["id"]},{r["name"]},{r["status"]}') | ||
if not response_json['next']: | ||
return | ||
page += 1 | ||
url = self.api.tasks_page(page) | ||
response = self.session.get(url) | ||
response.raise_for_status() | ||
|
||
@exception | ||
def tasks_create(self, name, labels, bug, resource_type, resources, **kwargs): | ||
""" Create a new task with the given name and labels JSON and | ||
add the files to it. """ | ||
url = self.api.tasks | ||
data = {'name': name, | ||
'labels': labels, | ||
'bug_tracker': bug, | ||
'image_quality': 50} | ||
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"]}') | ||
self.tasks_data(response_json['id'], resource_type, resources) | ||
|
||
@exception | ||
def tasks_delete(self, task_ids, **kwargs): | ||
""" Delete a list of tasks, ignoring those which don't exist. """ | ||
for task_id in task_ids: | ||
url = self.api.tasks_id(task_id) | ||
response = self.session.delete(url) | ||
try: | ||
response.raise_for_status() | ||
log.info(f'Task ID {task_id} deleted') | ||
except requests.exceptions.HTTPError as e: | ||
if response.status_code == 404: | ||
log.info(f'Task ID {task_id} not found') | ||
else: | ||
raise e | ||
|
||
@exception | ||
def tasks_frame(self, task_id, frame_ids, **kwargs): | ||
""" Download the requested frame numbers for a task and save images as | ||
task_<ID>_frame_<FRAME>.jpg.""" | ||
for frame_id in frame_ids: | ||
url = self.api.tasks_id_frame_id(task_id, frame_id) | ||
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' | ||
im.save(outfile) | ||
|
||
@exception | ||
def tasks_dump(self, task_id, fileformat, filename, **kwargs): | ||
""" Download annotations for a task in the specified format | ||
(e.g. 'YOLO ZIP 1.0').""" | ||
url = self.api.tasks_id(task_id) | ||
response = self.session.get(url) | ||
response.raise_for_status() | ||
response_json = response.json() | ||
|
||
url = self.api.tasks_id_annotations_filename(task_id, | ||
response_json['name'], | ||
fileformat) | ||
while True: | ||
response = self.session.get(url) | ||
response.raise_for_status() | ||
if response.status_code == 201: | ||
break | ||
|
||
response = self.session.get(url + '&action=download') | ||
response.raise_for_status() | ||
|
||
with open(filename, 'wb') as fp: | ||
fp.write(response.content) | ||
|
||
|
||
class CVAT_API_V1(): | ||
""" Build parameterized API URLs """ | ||
|
||
def __init__(self, host, port): | ||
self.base = f'http://{host}:{port}/api/v1/' | ||
|
||
@property | ||
def tasks(self): | ||
return f'{self.base}tasks' | ||
|
||
def tasks_page(self, page_id): | ||
return f'{self.tasks}?page={page_id}' | ||
|
||
def tasks_id(self, task_id): | ||
return f'{self.tasks}/{task_id}' | ||
|
||
def tasks_id_data(self, task_id): | ||
return f'{self.tasks}/{task_id}/data' | ||
|
||
def tasks_id_frame_id(self, task_id, frame_id): | ||
return f'{self.tasks}/{task_id}/frames/{frame_id}' | ||
|
||
def tasks_id_annotations_filename(self, task_id, name, fileformat): | ||
return f'{self.tasks}/{task_id}/annotations/{name}?format={fileformat}' | ||
|
||
|
||
def config_log(level): | ||
log.addHandler(logging.StreamHandler(sys.stdout)) | ||
log.setLevel(level) | ||
if level <= logging.DEBUG: | ||
HTTPConnection.debuglevel = 1 | ||
|
||
|
||
def main(): | ||
actions = {'create': CLI.tasks_create, | ||
'delete': CLI.tasks_delete, | ||
'ls': CLI.tasks_list, | ||
'frames': CLI.tasks_frame, | ||
'dump': CLI.tasks_dump} | ||
args = parser.parse_args() | ||
config_log(args.loglevel) | ||
with requests.Session() as session: | ||
session.auth = args.auth | ||
api = CVAT_API_V1(args.server_host, args.server_port) | ||
cli = CLI(session, api) | ||
actions[args.action](cli, **args.__dict__) | ||
|
||
|
||
if __name__ == '__main__': | ||
main() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,204 @@ | ||
import argparse | ||
import getpass | ||
import json | ||
import logging | ||
import os | ||
from enum import Enum | ||
|
||
|
||
def get_auth(s): | ||
""" Parse USER[:PASS] strings and prompt for password if none was | ||
supplied. """ | ||
user, _, password = s.partition(':') | ||
password = password or os.environ.get('PASS') or getpass.getpass() | ||
return user, password | ||
|
||
|
||
def parse_label_arg(s): | ||
""" If s is a file load it as JSON, otherwise parse s as JSON.""" | ||
if os.path.exists(s): | ||
fp = open(s, 'r') | ||
return json.load(fp) | ||
else: | ||
return json.loads(s) | ||
|
||
|
||
class ResourceType(Enum): | ||
|
||
LOCAL = 0 | ||
SHARE = 1 | ||
REMOTE = 2 | ||
|
||
def __str__(self): | ||
return self.name.lower() | ||
|
||
def __repr__(self): | ||
return str(self) | ||
|
||
@staticmethod | ||
def argparse(s): | ||
try: | ||
return ResourceType[s.upper()] | ||
except KeyError: | ||
return s | ||
|
||
|
||
####################################################################### | ||
# Command line interface definition | ||
####################################################################### | ||
|
||
parser = argparse.ArgumentParser( | ||
description='Perform common operations related to CVAT tasks.\n\n' | ||
) | ||
task_subparser = parser.add_subparsers(dest='action') | ||
|
||
####################################################################### | ||
# Positional arguments | ||
####################################################################### | ||
|
||
parser.add_argument( | ||
'--auth', | ||
type=get_auth, | ||
metavar='USER:[PASS]', | ||
default=getpass.getuser(), | ||
help='''defaults to the current user and supports the PASS | ||
environment variable or password prompt | ||
(default user: %(default)s).''' | ||
) | ||
parser.add_argument( | ||
'--server-host', | ||
type=str, | ||
default='localhost', | ||
help='host (default: %(default)s)' | ||
) | ||
parser.add_argument( | ||
'--server-port', | ||
type=int, | ||
default='8080', | ||
help='port (default: %(default)s)' | ||
) | ||
parser.add_argument( | ||
'--debug', | ||
action='store_const', | ||
dest='loglevel', | ||
const=logging.DEBUG, | ||
default=logging.INFO, | ||
help='show debug output' | ||
) | ||
|
||
####################################################################### | ||
# Create | ||
####################################################################### | ||
|
||
task_create_parser = task_subparser.add_parser( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It will be cool to accept a task definition as json file as well. |
||
'create', | ||
description='Create a new CVAT task.' | ||
) | ||
task_create_parser.add_argument( | ||
'--name', | ||
default='new task', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think that we need a default value here. |
||
type=str, | ||
help='name of the task (default: %(default)s)' | ||
) | ||
task_create_parser.add_argument( | ||
'--labels', | ||
default='[]', | ||
type=parse_label_arg, | ||
help='string or file containing JSON labels specification' | ||
) | ||
task_create_parser.add_argument( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is about other attributes? I'm OK to have a minimal patch. Just curious if you like to add more parameters (images quality, owner, assignee, etc...). All of them can be viewed in http://localhost:8080/api/swagger documentation |
||
'--bug', | ||
default='', | ||
type=str, | ||
help='bug tracker URL' | ||
) | ||
task_create_parser.add_argument( | ||
'resource_type', | ||
default='local', | ||
choices=list(ResourceType), | ||
type=ResourceType.argparse, | ||
help='type of files specified' | ||
) | ||
task_create_parser.add_argument( | ||
'resources', | ||
type=str, | ||
help='list of paths or URLs', | ||
nargs='+' | ||
) | ||
|
||
####################################################################### | ||
# Delete | ||
####################################################################### | ||
|
||
delete_parser = task_subparser.add_parser( | ||
'delete', | ||
description='Delete a CVAT task.' | ||
) | ||
delete_parser.add_argument( | ||
'task_ids', | ||
type=int, | ||
help='list of task IDs', | ||
nargs='+' | ||
) | ||
|
||
####################################################################### | ||
# List | ||
####################################################################### | ||
|
||
ls_parser = task_subparser.add_parser( | ||
'ls', | ||
description='List all CVAT tasks in simple or JSON format.' | ||
) | ||
ls_parser.add_argument( | ||
'--json', | ||
dest='use_json_output', | ||
default=False, | ||
action='store_true', | ||
help='output JSON data' | ||
) | ||
|
||
####################################################################### | ||
# Frames | ||
####################################################################### | ||
|
||
frames_parser = task_subparser.add_parser( | ||
'frames', | ||
description='Download all frame images for a CVAT task.' | ||
) | ||
frames_parser.add_argument( | ||
'task_id', | ||
type=int, | ||
help='task ID' | ||
) | ||
frames_parser.add_argument( | ||
'frame_ids', | ||
type=int, | ||
help='list of frame IDs to download', | ||
nargs='+' | ||
) | ||
|
||
####################################################################### | ||
# Dump | ||
####################################################################### | ||
|
||
dump_parser = task_subparser.add_parser( | ||
'dump', | ||
description='Download annotations for a CVAT task.' | ||
) | ||
dump_parser.add_argument( | ||
'task_id', | ||
type=int, | ||
help='task ID' | ||
) | ||
dump_parser.add_argument( | ||
'filename', | ||
type=str, | ||
help='output file' | ||
) | ||
dump_parser.add_argument( | ||
'--format', | ||
dest='fileformat', | ||
type=str, | ||
default='CVAT XML 1.1 for images', | ||
help='annotation format (default: %(default)s)' | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably you want to define the method outside class
CLI
. It thinks that func argument is self.