#!/usr/bin/env python

from __future__ import print_function
import argparse
import base64
from collections import OrderedDict
import errno
import json
import os
import re
import sys
import time
import requests

from prominence import ProminenceJob
from prominence import ProminenceTask
from prominence import ProminenceClient
from prominence import __version__

def elapsed(job):
    """
    Print elapsed job runtime in a nice way
    """
    if 'startTime' in job['events']:
        if 'endTime' in job['events']:
            elapsed_time = job['events']['endTime'] - job['events']['startTime']
        else:
            elapsed_time = time.time() - job['events']['startTime']
        days = int(elapsed_time/86400)
        time_fmt = '%H:%M:%S'
        return '%d+%s' % (days, time.strftime(time_fmt, time.gmtime(elapsed_time)))

    return ''

def datetime_format(epoch):
    """
    Convert a unix epoch in a formatted date/time string
    """
    datetime_fmt = '%Y-%m-%dT%H:%M:%S'
    return time.strftime(datetime_fmt, time.gmtime(epoch))

def print_json(content, transform=False, detail=False, resource='job'):
    """
    Print JSON in a nice way
    """
    if transform:
        content = transform_item_list(content, detail, resource)
    print(json.dumps(content, indent=2))

def image_name(name):
    """
    Extract container image name for display purposes only
    """
    if 'http' in name:
        name = os.path.basename(name)
        name = name[:name.find('?')]
    return name

def list_jobs(jobs):
    """
    Print list of jobs
    """

    # Firstly determine column widths
    width_id = 2
    width_name = 4
    width_created = 19
    width_status = 6
    width_elapsed = 10
    width_container = 5
    width_cmd = 3

    for job in jobs:
        my_cmd = ''
        if 'cmd' in job['tasks'][0]:
            my_cmd = job['tasks'][0]['cmd']

        width_id_current = len(str(job['id']))
        width_name_current = len(job['name'])
        width_status_current = len(job['status'])
        width_container_current = len(image_name(job['tasks'][0]['image']))
        width_cmd_current = len(my_cmd)

        if width_id_current > width_id:
            width_id = width_id_current
        if width_name_current > width_name:
            width_name = width_name_current
        if width_status_current > width_status:
            width_status = width_status_current
        if width_container_current > width_container:
            width_container = width_container_current
        if width_cmd_current > width_cmd:
            width_cmd = width_cmd_current

    # Print headings
    print('%s   %s   %s   %s   %s   %s   %s' % ('ID'.ljust(width_id),
                                                'NAME'.ljust(width_name),
                                                'CREATED'.ljust(width_created),
                                                'STATUS'.ljust(width_status),
                                                'ELAPSED'.ljust(width_elapsed),
                                                'IMAGE'.ljust(width_container),
                                                'CMD'.ljust(width_cmd)))

    # Print jobs
    for job in jobs:
        my_cmd = ''
        if 'cmd' in job['tasks'][0]:
            my_cmd = job['tasks'][0]['cmd']
        print('%s   %s   %s   %s   %s   %s   %s' % (str(job['id']).ljust(width_id),
                                                    job['name'].ljust(width_name),
                                                    datetime_format(job['events']['createTime']).ljust(width_created),
                                                    job['status'].ljust(width_status),
                                                    elapsed(job).ljust(width_elapsed),
                                                    image_name(job['tasks'][0]['image']).ljust(width_container),
                                                    my_cmd.ljust(width_cmd)))

def list_workflows(workflows):
    """
    Print list of workflows
    """

    # Firstly determine column widths
    width_id = 2
    width_name = 4
    width_created = 19
    width_status = 6
    width_elapsed = 10
    width_progress = 8

    for workflow in workflows:
        width_id_current = len(str(workflow['id']))
        width_name_current = len(workflow['name'])
        width_status_current = len(workflow['status'])
        width_progress_current = len('%d/%d' % (workflow['progress']['done'], workflow['progress']['total']))

        if width_id_current > width_id:
            width_id = width_id_current
        if width_name_current > width_name:
            width_name = width_name_current
        if width_status_current > width_status:
            width_status = width_status_current
        if width_progress_current > width_progress:
            width_progress = width_progress_current

    # Print headings
    print('%s   %s   %s   %s   %s   %s' % ('ID'.ljust(width_id),
                                           'NAME'.ljust(width_name),
                                           'CREATED'.ljust(width_created),
                                           'STATUS'.ljust(width_status),
                                           'ELAPSED'.ljust(width_elapsed),
                                           'PROGRESS'.ljust(width_progress)))

    # Print workflows
    for workflow in workflows:
        print('%s   %s   %s   %s   %s   %s' % (str(workflow['id']).ljust(width_id),
                                               workflow['name'].ljust(width_name),
                                               datetime_format(workflow['events']['createTime']).ljust(width_created),
                                               workflow['status'].ljust(width_status),
                                               ''.ljust(width_elapsed),
                                               ('%d/%d' % (workflow['progress']['done'], workflow['progress']['total'])).ljust(width_progress)))

def transform_job(job, detail):
    """
    Transform a job into the required format for printing
    """
    job_t = OrderedDict()
    job_t['id'] = job['id']
    if job['name'] != '' or not detail:
        job_t['name'] = job['name']

    job_t['status'] = job['status']

    if detail and 'statusReason' in job:
        job_t['statusReason'] = job['statusReason']

    if detail:
        if 'storage' in job:
            job_t['storage'] = job['storage']
        job_t['resources'] = job['resources']
        if 'labels' in job:
            job_t['labels'] = job['labels']
        if 'artifacts' in job:
            job_t['artifacts'] = job['artifacts']
        if 'inputFiles' in job:
            job_t['inputFiles'] = job['inputFiles']
        if 'outputFiles' in job:
            job_t['outputFiles'] = job['outputFiles']

    job_t['tasks'] = job['tasks']

    if detail:
        if 'preemptible' in job:
            job_t['preemptible'] = True

    events = OrderedDict()
    if 'events' in job:
        if 'createTime' in job['events']:
            if detail:
                events['createTime'] = datetime_format(job['events']['createTime'])
            else:
                events['createTime'] = job['events']['createTime']

    if 'startTime' in job['events']:
        if detail:
            events['startTime'] = datetime_format(job['events']['startTime'])
        else:
            events['startTime'] = job['events']['startTime']
    if 'endTime' in job['events']:
        if detail:
            events['endTime'] = datetime_format(job['events']['endTime'])
        else:
            events['endTime'] = job['events']['endTime']
    job_t['events'] = events

    execution = OrderedDict()
    if 'execution' in job:
        if 'site' in job['execution']:
            execution['site'] = job['execution']['site']
        job_t['execution'] = execution

    return job_t

def transform_workflow(workflow, detail):
    """
    Transform a workflow into the required format for printing
    """
    workflow_t = OrderedDict()
    workflow_t['id'] = workflow['id']
    if workflow['name'] != '' or not detail:
        workflow_t['name'] = workflow['name']

    workflow_t['status'] = workflow['status']

    if detail and 'statusReason' in workflow:
        workflow_t['statusReason'] = workflow['statusReason']

    if detail:
        if 'storage' in workflow:
            workflow_t['storage'] = workflow['storage']
        workflow_t['jobs'] = workflow['jobs']
        if 'dependencies' in workflow:
            workflow_t['dependencies'] = workflow['dependencies']

    events = OrderedDict()
    if 'events' in workflow:
        if 'createTime' in workflow['events']:
            if detail:
                events['createTime'] = datetime_format(workflow['events']['createTime'])
            else:
                events['createTime'] = workflow['events']['createTime']

    if 'startTime' in workflow['events']:
        if detail:
            events['startTime'] = datetime_format(workflow['events']['startTime'])
        else:
            events['startTime'] = workflow['events']['startTime']
    if 'endTime' in workflow['events']:
        if detail:
            events['endTime'] = datetime_format(workflow['events']['endTime'])
        else:
            events['endTime'] = workflow['events']['endTime']
    workflow_t['events'] = events

    if 'progress' in workflow:
        workflow_t['progress'] = workflow['progress']

    return workflow_t

def transform_item_list(result, detail, resource):
    """
    Transform a job/workflow list into the required format ordered by id
    """
    if 'job' in resource:
        items = [transform_job(job, detail) for job in result]
    else:
        items = [transform_workflow(workflow, detail) for workflow in result]
    return sorted(items, key=lambda k: int(k['id']))

def command_login(args):
    """
    Obtain token from OIDC provider
    """
    data = {}
    data['scope'] = 'openid profile email'
    data['client_id'] = os.environ['PROMINENCE_OIDC_CLIENT_ID']

    try:
        request = requests.post(os.environ['PROMINENCE_OIDC_URL']+'/devicecode',
                                data=data,
                                timeout=HTTP_TIMEOUT,
                                auth=(os.environ['PROMINENCE_OIDC_CLIENT_ID'],
                                      os.environ['PROMINENCE_OIDC_CLIENT_SECRET']),
                                allow_redirects=True)
    except requests.exceptions.RequestException:
        print('Error: Cannot connect to OIDC server')
        exit(1)

    device_code_response = request.json()

    print('To login, use a web browser to open the page %s and enter the code %s when requested' % (device_code_response['verification_uri'], device_code_response['user_code']))

    data = {}
    data['grant_type'] = 'urn:ietf:params:oauth:grant-type:device_code'
    data['device_code'] = device_code_response['device_code']

    # Wait for the user to authenticate
    current_time = time.time()
    authenticated = False
    while time.time() < current_time + int(device_code_response['expires_in']) and not authenticated:
        time.sleep(5)
        try:
            request = requests.post(os.environ['PROMINENCE_OIDC_URL']+'/token',
                                    data=data,
                                    timeout=HTTP_TIMEOUT,
                                    auth=(os.environ['PROMINENCE_OIDC_CLIENT_ID'],
                                          os.environ['PROMINENCE_OIDC_CLIENT_SECRET']),
                                    allow_redirects=True)
        except requests.exceptions.RequestException:
            print('Error: Cannot connect to OIDC server')
            exit(1)
        if request.status_code == 200:
            authenticated = True
            with open(os.path.expanduser('~/.prominence'), 'w') as token_file:
                json.dump(request.json(), token_file)
            os.chmod(os.path.expanduser('~/.prominence'), 384)
            print('Authentication successful')
            exit(0)

    if not authenticated:
        print('Error: Authentication failed')
        exit(1)

    try:
        with open(args.file) as json_file:
            data = json.load(json_file)
    except IOError as err:
        print('Error: %s' % err)
        exit(1)
    except ValueError as err:
        print('Error: %s' % err)
        exit(1)

def get_token():
    """
    Load saved token
    """
    if os.path.isfile(os.path.expanduser('~/.prominence')):
        try:
            with open(os.path.expanduser('~/.prominence')) as json_data:
                data = json.load(json_data)
        except IOError as err:
            print('Error: %s' % err)
            exit(1)
        except ValueError as err:
            print('Error: %s' % err)
            exit(1)

        if 'access_token' in data:
            return data['access_token']
        else:
            print('Error: the saved token file does not contain access_token')
            exit(1)
    return None

def command_list(args):
    """
    List running/idle jobs/workflows or completed jobs/workflows
    """
    completed = False
    if args.completed:
        completed = True
    all = False
    if args.all:
        all = True
    num = None
    if args.num:
        num = args.num
    constraint = None
    if args.constraint:
        constraint = args.constraint

    client = ProminenceClient(url=URL, token=TOKEN)
    if args.resource == 'jobs':
        response = client.list_jobs(completed, all, num, constraint)
    else:
        response = client.list_workflows(completed, all, num, constraint)
    if response.return_code == 0:
        if args.resource == 'jobs':
            list_jobs(transform_item_list(response.data, False, 'job'))
        else:
            list_workflows(transform_item_list(response.data, False, 'workflow'))
        exit(0)
    else:
        print('Error: %s' % response.data['error'])
        exit(1)

def command_describe(args):
    """
    Describe a specific job/workflow
    """
    completed = False
    if args.completed:
        completed = True

    client = ProminenceClient(url=URL, token=TOKEN)
    if args.resource == 'job':
        response = client.describe_job(args.id, completed)
    else:
        response = client.describe_workflow(args.id, completed)

    if response.return_code != 0:
        print('Error: %s' % response.data['error'])
        exit(1)
    print_json(response.data, transform=True, detail=True, resource=args.resource)
    exit(0)

def command_upload(args):
    """
    Upload a file to transient storage
    """
    if args.name is None:
        print('Error: a name must be specified')
        exit(1)
    if args.filename is None:
        print('Error: a filename must be specified')
        exit(1)

    client = ProminenceClient(url=URL, token=TOKEN)
    response = client.upload(args.name, args.filename)
    if response.return_code != 0:
        print('Error: %s' % response.data['error'])
        exit(1)
    print('Success')
    exit(0)

def command_download(args):
    """
    Download output files and directories
    """
    client = ProminenceClient(url=URL, token=TOKEN)

    if args.constraint:
        response = client.list(False, True, 0, args.constraint)
    else:
        if not args.id:
            print('Error: A job id must be given if a constraint if not specified')
            exit(1)
        response = client.describe_job(args.id, True)

    if response.return_code != 0:
        print('Error: %s' % response.data['error'])
        exit(1)

    jobs = response.data
    for job in jobs:
        if ('outputFiles' in job or 'outputDirs' in job) and job['status'] == 'completed':
            # Create per-job directory if necessary & set path for files
            path = './'
            if args.dir:
                path = './%d/' % job['id']
                try:
                    os.mkdir(path)
                except OSError as exc:
                    if exc.errno != errno.EEXIST:
                        print('WARNING: skipping job %d as job directory cannot be created' % job['id'])
                        continue
                    else:
                        pass

            files_and_dirs = []
            if 'outputFiles' in job:
                files_and_dirs += job['outputFiles']
            if 'outputDirs' in job:
                files_and_dirs += job['outputDirs']

            for pair in files_and_dirs:
                file_name = os.path.basename(pair['name'])
                if 'outputDirs' in job:
                    if pair in job['outputDirs']:
                        file_name = file_name + '.tgz'

                url = pair['url']

                if os.path.isfile(path + file_name) and not args.force:
                    print('WARNING: skipping "%s" from job %d as file already exists and force option not specified' % (file_name, job['id']))
                    continue

                response = requests.get(url, stream=True)
                total_length = response.headers.get('content-length')

                if response.status_code != 200:
                    print('WARNING: skipping "%s" from job %d as it does not exist' % (file_name, job['id']))
                    continue

                with open(path + file_name, 'wb') as file_download:
                    print('Downloading file "%s" from job %d' % (file_name, job['id']))
                    if total_length is None:
                        file_download.write(response.content)
                    else:
                        downloaded = 0
                        total_length = int(total_length)
                        for data in response.iter_content(chunk_size=4096):
                            downloaded += len(data)
                            file_download.write(data)
                            done = int(50 * downloaded / total_length)
                            sys.stdout.write("\r[%s%s]" % ('=' * done, ' ' * (50-done)))
                            sys.stdout.flush()
                print('')

def command_delete(args):
    """
    Delete a job
    """
    client = ProminenceClient(url=URL, token=TOKEN)
    if args.resource == 'job':
        response = client.delete_job(args.id)
    else:
        response = client.delete_workflow(args.id)

    if response.return_code != 0:
        print('Error: %s' % response.data['error'])
        exit(1)
    print('Success')
    exit(0)

def command_stdout(args):
    """
    Get standard output for a specific job/workflow
    """
    client = ProminenceClient(url=URL, token=TOKEN)

    if args.job:
        response = client.stdout_workflow(args.id, args.job)
    else:
        response = client.stdout_job(args.id)

    if response.return_code != 0:
        print('Error: %s' % response.data['error'])
        exit(1)
    print(response.data)
    exit(0)

def command_stderr(args):
    """
    Get standard error for a specific job/workflow
    """
    client = ProminenceClient(url=URL, token=TOKEN)

    if args.job:
        response = client.stderr_workflow(args.id, args.job)
    else:
        response = client.stderr_job(args.id)

    if response.return_code != 0:
        print('Error: %s' % response.data['error'])
        exit(1)
    print(response.data)
    exit(0)

def command_create(args):
    """
    Create a job from a JSON file
    """
    try:
        with open(args.file) as json_file:
            data = json.load(json_file)
    except IOError as err:
        print('Error: %s' % err)
        exit(1)
    except ValueError as err:
        print('Error: %s' % err)
        exit(1)

    client = ProminenceClient(url=URL, token=TOKEN)
    if 'dependencies' in data:
        response = client.create_workflow_from_json(data)
        resource = 'Workflow'
    else:
        response = client.create_job_from_json(data)
        resource = 'Job'

    if response.return_code == 0:
        if 'id' in response.data:
            print('%s created with id %d' % (resource, response.data['id']))
        exit(0)
    else:
        if 'error' in response.data:
            print('Error: %s' % response.data['error'])

    exit(1)

def command_run(args):
    """
    Run a job
    """
    job = ProminenceJob()
    task = ProminenceTask()

    task.image = args.image
    job.memory = args.memory
    job.cpus = args.cpus
    job.nodes = args.nodes
    job.disk = args.disk
    job.name = args.name

    # Working directory
    if args.workdir:
        task.workdir = args.workdir

    # Walltine limit
    job.walltime = args.walltime

    # Container runtime - use singularity by default but use udocker if the user has
    # specified a tarball using a URL
    if args.runtime:
        task.runtime = args.runtime
    else:
        if re.match(r'^http', args.image) and '.tar' in args.image:
            task.runtime = 'udocker'
        else:
            task.runtime = 'singularity'

    # Job type
    if args.openmpi:
        task.type = 'openmpi'
    elif args.mpich:
        task.type = 'mpich'

    # If multiple nodes are specified need to specify MPI type
    if job.nodes > 1 and task.type != 'openmpi' and task.type != 'mpich':
        print('Error: more than one node has been requested but MPI has not been specified')
        exit(1)

    # Optional command to run
    if args.command:
        task.cmd = args.command

    # Output files
    if args.outputfile:
        job.output_files = args.outputfile

    # Output directories
    if args.outputdir:
        job.output_dirs = args.outputdir

    # Files to be fetched
    if args.artifact:
        job.artifacts = args.artifact

    # Files to be uploaded
    if args.inputfile:
        inputs = []
        for filename in args.inputfile:
            if os.path.isfile(filename):
                if os.path.getsize(filename) < 1000000:
                    with open(filename, 'rb') as input_file:
                        inputs.append({'filename':filename, 'content':base64.b64encode(input_file.read()).decode("utf-8")})
                else:
                    print('Error: Input file size too large')
                    exit(1)
            else:
                print('Error: File "%s" does not exist' % filename)
                exit(1)
        job.inputs = inputs

    # Environment variables
    if args.env:
        env = {}
        for pair in args.env:
            if '=' in pair:
                items = pair.split('=')
                env[items[0]] = items[1]
        task.env = env

    # Metadata
    if args.label:
        labels = {}
        for pair in args.label:
            if '=' in pair:
                items = pair.split('=')
                labels[items[0]] = items[1]
        job.labels = labels

    # Constraints
    if args.constraints:
        job.constraints = json.loads(args.constraints)

    # Preemptible
    if args.preemptible:
        job.preemptible = args.preemptible

    # Add task to job
    job.tasks = [task]

    # Print JSON description of job if requested
    if args.dryrun:
        print_json(job.to_json())
        exit(0)

    client = ProminenceClient(url=URL, token=TOKEN)
    response = client.create_job(job)
    if response.return_code == 0:
        if 'id' in response.data:
            print('Job created with id %d' % response.data['id'])
        exit(0)
    else:
        if 'error' in response.data:
            print('Error: %s' % response.data['error'])
    exit(1)

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='Prominence - \
                                     run jobs in containers across clouds')
    subparsers = parser.add_subparsers(help='sub-command help')

    # Create the parser for the "login" command
    parser_login = subparsers.add_parser('login', help='Login')
    parser_login.set_defaults(func=command_login)

    # Create the parser for the "create" command
    parser_create = subparsers.add_parser('create', help='Create a job or workflow from a JSON file')
    parser_create.add_argument('file', help='JSON file')
    parser_create.set_defaults(func=command_create)

    # Create the parser for the "run" command
    parser_run = subparsers.add_parser('run', help='Run a job')
    parser_run.add_argument('--name', dest='name', default='',
                            help='Job name.')
    parser_run.add_argument('--memory', dest='memory', default=1, type=int,
                            help='Memory in GB per node.')
    parser_run.add_argument('--cpus', dest='cpus', default=1, type=int,
                            help='Cores per node.')
    parser_run.add_argument('--nodes', dest='nodes', default=1, type=int,
                            help='Number of nodes.')
    parser_run.add_argument('--disk', dest='disk', default=10, type=int,
                            help='Size of disk containing the job\'s scratch directory. For \
                                  multi-node jobs it will be shared across each of the nodes. \
                                  By default a 10 GB disk will be used.')
    parser_run.add_argument('--walltime', dest='walltime', type=int,
                            help='Walltime limit in minutes. If the job is still running after \
                                  this time it will be killed.')
    parser_run.add_argument('--openmpi', dest='openmpi', default=False, action='store_true',
                            help="Specify that this is an OpenMPI job.")
    parser_run.add_argument('--mpich', dest='mpich', default=False, action='store_true',
                            help="Specify that this is an MPICH job.")
    parser_run.add_argument('--artifact', dest='artifact', action='append',
                            help='A URL to be transferred to the job. Archives will be \
                                  automatically unpacked/extracted. \
                                  This option can be specified multiple times.')
    parser_run.add_argument('--input', dest='inputfile', action='append',
                            help='Full path to a file on the current host to be \
                                  uploaded and made available to the job. This option \
                                  can be specified multiple times to set multiple output files.')
    parser_run.add_argument('--output', dest="outputfile", action='append',
                            help='An output file to be copied to transient storage. This option \
                                  can be specified multiple times to set multiple output files.')
    parser_run.add_argument('--outputdir', dest="outputdir", action='append',
                            help='A directory to be copied to transient storage. This option \
                                  can be specified multiple times to set multiple directories.')
    parser_run.add_argument('--workdir', dest='workdir',
                            help='Set the current working directory.')
    parser_run.add_argument('--env', dest='env', action='append',
                            help='Specify environment variables in the form name=value. \
                                  This option can be specified multiple times to set \
                                  multiple environment variables.')
    parser_run.add_argument('--label', dest='label', action='append',
                            help='Set metadata in the form key=value. This option can \
                                  be specified multiple times to set multiple labels.')
    parser_run.add_argument('--runtime', dest='runtime',
                            choices=['singularity', 'udocker'],
                            help='Container runtime, either singularity or udocker. The default \
                                  is singularity.')
    parser_run.add_argument('--preemptible', dest='preemptible', default=False, action='store_true',
                            help='Specify that this job is preemptible')
    parser_run.add_argument('--constraints', dest='constraints',
                            help='Specify constraints')
    parser_run.add_argument('--dry-run', dest='dryrun', default=False, action='store_true',
                            help='Print json to stdout but do not actually create job.')
    parser_run.add_argument('image', help='Container image')
    parser_run.add_argument('command', nargs='?',
                            help='Command to run in the container. If you need to specify \
                                  arguments, put the combined command and arguments inside quotes.')
    parser_run.set_defaults(func=command_run)

    # Create the parser for the "list" command
    parser_list = subparsers.add_parser('list', help='List jobs or workflows')
    parser_list.add_argument('--completed', dest='completed', default=False,
                             help='List completed jobs/workflows', action='store_true')
    parser_list.add_argument('--num', dest='num', default=1, type=int,
                             help='Number of completed jobs/workflows to return')
    parser_list.add_argument('--constraint', dest='constraint', action='append',
                             help='Constraint of the form key=value')
    parser_list.add_argument('--all', dest='all', default=False,
                             help='List jobs/workflows in all states', action='store_true')
    parser_list.add_argument('resource', help='Resource type', default='jobs', nargs='?',
                             choices=['jobs', 'workflows'])
    parser_list.set_defaults(func=command_list)

    # Create the parser for the "describe" command
    parser_describe = subparsers.add_parser('describe', help='Describe a job or workflow')
    parser_describe.add_argument('resource', help='Resource type', default='job', nargs='?',
                                 choices=['job', 'workflow'])
    parser_describe.add_argument('id', help='Job id', type=int)
    parser_describe.add_argument('--completed', dest='completed', default=False,
                                 help='Describe a job or workflow in the completed state', action='store_true')
    parser_describe.set_defaults(func=command_describe)

    # Create the parser for the "delete" command
    parser_delete = subparsers.add_parser('delete', help='Delete a job or workflow')
    parser_delete.add_argument('resource', help='Resource type', default='job', nargs='?',
                               choices=['job', 'workflow'])
    parser_delete.add_argument('id', help='Job/workflow id', type=int)
    parser_delete.set_defaults(func=command_delete)

    # Create the parser for the "upload" command
    parser_upload = subparsers.add_parser('upload', help='Upload a file to transient storage')
    parser_upload.add_argument('--name', dest='name', help='Name to be used by jobs to identity the file')
    parser_upload.add_argument('--filename', dest='filename', help='Local filename')
    parser_upload.set_defaults(func=command_upload)

    # Create the parser for the "download" command
    parser_download = subparsers.add_parser('download', help='Download output files from a completed job or workflow')
    parser_download.add_argument('--constraint', dest='constraint', action='append',
                                 help='Constraint of the form key=value')
    parser_download.add_argument('--force', dest='force', default=False,
                                 help='Force overwrite of existing file', action='store_true')
    parser_download.add_argument('--dir', dest='dir', default=False,
                                 help='Save output files in a directory named by the job id', action='store_true')
    parser_download.add_argument('id', help='Job id', type=int)
    parser_download.set_defaults(func=command_download)

    # Create the parser for the "stdout" command
    parser_stdout = subparsers.add_parser('stdout', help='Get standard output from a running or completed job')
    parser_stdout.add_argument('id', help='Job or workflow id', type=int)
    parser_stdout.add_argument('job', help='Job name', nargs='?')
    parser_stdout.set_defaults(func=command_stdout)

    # Create the parser for the "stderr" command
    parser_stderr = subparsers.add_parser('stderr', help='Get standard error from a running or completed job')
    parser_stderr.add_argument('id', help='Job or workflow id', type=int)
    parser_stderr.add_argument('job', help='Job name', nargs='?')
    parser_stderr.set_defaults(func=command_stderr)

    # Version
    parser.add_argument('--version', action='version',
                        version='%(prog)s {}'.format(__version__),
                        help='show the version number and exit')

    # Print help if necessary
    if len(sys.argv) < 2:
        parser.print_help(sys.stderr)
        exit(1)

    # Parse the arguments & run the required function if necessary
    args = parser.parse_args()

    # Authentication
    if 'PROMINENCE_OIDC_URL' not in os.environ:
        print('Error: environment variable PROMINENCE_OIDC_URL is not set')
        exit(1)

    if 'PROMINENCE_OIDC_CLIENT_ID' not in os.environ:
        print('Error: environment variable PROMINENCE_OIDC_CLIENT_ID is not set')
        exit(1)

    if 'PROMINENCE_OIDC_CLIENT_SECRET' not in os.environ:
        print('Error: environment variable PROMINENCE_OIDC_CLIENT_SECRET is not set')
        exit(1)

    # Get existing token if possible when necessary
    if sys.argv[1] != 'login':
        token = get_token()
        if token:
            HEADERS = {"Authorization":"Bearer %s" % token}
            TOKEN = token
        else:
            print('Error: Authentication required')
            exit(1)

    # Get URL for PROMINENCE service
    if 'PROMINENCE_URL' in os.environ:
        URL = os.environ['PROMINENCE_URL']
    else:
        print('Error: Environment variable PROMINENCE_URL is not set')
        exit(1)

    HTTP_TIMEOUT = 40

    # Run
    args.func(args)
