#!/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 shlex
import sys
import time
import requests

from prominence import auth
from prominence import exceptions
from prominence import __version__
from prominence import ProminenceClient

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-%d %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:
        if isinstance(content, list):
            content = transform_item_list(content, detail, resource)
        else:
            content = transform_item(content, detail, resource)
    print(json.dumps(content, indent=2))

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

def list_jobs(jobs):
    """
    Print list of jobs
    """
    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']
        my_name = ''
        if 'name' in job:
            my_name = job['name']

        width_id_current = len(str(job['id']))
        width_name_current = len(my_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('%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)))

    for job in jobs:
        my_cmd = ''
        if 'cmd' in job['tasks'][0]:
            my_cmd = job['tasks'][0]['cmd']
        my_name = ''
        if 'name' in job:
            my_name = job['name']
        print('%s   %s   %s   %s   %s   %s   %s' % (str(job['id']).ljust(width_id),
                                                    my_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
    """
    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('%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)))

    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),
                                               elapsed(workflow).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 'name' in job:
        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']
        if 'outputDirs' in job:
            job_t['outputDirs'] = job['outputDirs']

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

    if detail:
        if 'policies' in job:
            job_t['policies'] = job['policies']

    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']
        if 'maxMemoryUsageKB' in job['execution']:
            execution['maxMemoryUsageKB'] = job['execution']['maxMemoryUsageKB']
        if 'tasks' in job['execution']:
            tasks = []
            for task in job['execution']['tasks']:
                task_t = {}
                if 'exitCode' in task:
                    task_t['exitCode'] = task['exitCode']
                if 'retries' in task:
                    task_t['retries'] = task['retries']
                if 'imagePullTime' in task:
                    task_t['imagePullTime'] = float('%.4g' % task['imagePullTime'])
                if 'wallTimeUsage' in task:
                    task_t['wallTimeUsage'] = float('%.4g' % task['wallTimeUsage'])
                if 'cpuTimeUsage' in task:
                    task_t['cpuTimeUsage'] = float('%.4g' % task['cpuTimeUsage'])
                if 'maxResidentSetSizeKB' in task:
                    task_t['maxResidentSetSizeKB'] = task['maxResidentSetSizeKB']
                tasks.append(task_t)

            execution['tasks'] = tasks
        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']
        if 'factory' in workflow:
            workflow_t['factory'] = workflow['factory']

    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(data, detail, resource):
    """
    Transform a job/workflow into the required format ordered by id
    """
    if 'job' in resource:
        return transform_job(data, detail)
    else:
        return transform_workflow(data, detail)

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_list(args):
    """
    List running/idle or completed jobs or workflows
    """
    completed = False
    if args.completed:
        completed = True
    active_and_completed = False
    if args.all:
        active_and_completed = True
    num = 1
    if args.num:
        num = args.num
    constraint = None
    if args.constraint:
        constraint = args.constraint

    try:
        client = ProminenceClient(authenticated=True)
        if args.resource == 'jobs':
            data = client.list_jobs(completed, active_and_completed, num, constraint)
        else:
            data = client.list_workflows(completed, active_and_completed, num, constraint)
    except exceptions.AuthenticationError:
        print('Error: authentication failed')
        exit(1)
    except exceptions.TokenExpiredError:
        print('Error: access token has expired')
        exit(1)
    except (exceptions.ConnectionError, exceptions.JobGetError, exceptions.TokenError) as err:
        print('Error:', err)
        exit(1)

    if args.resource == 'jobs':
        list_jobs(transform_item_list(data, False, 'job'))
    else:
        list_workflows(transform_item_list(data, False, 'workflow'))

def command_describe(args):
    """
    Describe a specific job or workflow
    """
    try:
        client = ProminenceClient(authenticated=True)
        if args.resource == 'job':
            data = client.describe_job(args.id)
        else:
            data = client.describe_workflow(args.id)
    except exceptions.AuthenticationError:
        print('Error: authentication failed')
        exit(1)
    except exceptions.TokenExpiredError:
        print('Error: access token has expired')
        exit(1)
    except (exceptions.JobGetError, exceptions.ConnectionError, exceptions.TokenError) as err:
        print('Error:', err)
        exit(1)

    print_json(data, transform=True, detail=True, resource=args.resource)

def command_delete(args):
    """
    Delete a job or workflow
    """
    try:
        client = ProminenceClient(authenticated=True)
        if args.resource == 'job':
            client.delete_job(args.id)
        else:
            client.delete_workflow(args.id)
    except exceptions.AuthenticationError:
        print('Error: authentication failed')
        exit(1)
    except exceptions.TokenExpiredError:
        print('Error: access token has expired')
        exit(1)
    except (exceptions.ConnectionError, exceptions.TokenError, exceptions.DeletionError) as err:
        print('Error:', err)
        exit(1)

    print('Success')

def command_stdout(args):
    """
    Get standard output for a specific job/workflow
    """
    try:
        client = ProminenceClient(authenticated=True)
        if args.job:
            print(client.stdout_workflow(args.id, args.job))
        else:
            print(client.stdout_job(args.id))
    except exceptions.AuthenticationError:
        print('Error: authentication failed')
        exit(1)
    except exceptions.TokenExpiredError:
        print('Error: access token has expired')
        exit(1)
    except (exceptions.ConnectionError, exceptions.StdStreamsError, exceptions.TokenError) as err:
        print('Error:', err)
        exit(1)

def command_stderr(args):
    """
    Get standard error for a specific job/workflow
    """
    try:
        client = ProminenceClient(authenticated=True)
        if args.job:
            print(client.stderr_workflow(args.id, args.job))
        else:
            print(client.stderr_job(args.id))
    except exceptions.AuthenticationError:
        print('Error: authentication failed')
        exit(1)
    except exceptions.TokenExpiredError:
        print('Error: access token has expired')
        exit(1)
    except (exceptions.ConnectionError, exceptions.StdStreamsError, exceptions.TokenError) as err:
        print('Error:', err)
        exit(1)

def command_run(args):
    """
    Create a job from a JSON file or URL
    """
    if args.file.startswith('http://') or args.file.startswith('https://'):
        try:
            response = requests.get(args.file)
        except requests.exceptions.RequestException as err:
            print('Error getting URL due to: %s' % err)
            exit(1)
        if response.status_code == 200:
            try:
                data = response.json()
            except json.decoder.JSONDecodeError as err:
                print('Error: problem with JSON at URL: %s' % err)
                exit(1)
        else:
            print('Error: got status %d from URL with message: %s' % (response.status_code, response.text))
            exit(1)
    else:
        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)

    try:
        client = ProminenceClient(authenticated=True)
        if 'jobs' in data:
            res_id = client.create_workflow(data)
            resource = 'Workflow'
        else:
            res_id = client.create_job(data)
            resource = 'Job'
    except exceptions.AuthenticationError:
        print('Error: authentication failed')
        exit(1)
    except exceptions.TokenExpiredError:
        print('Error: access token has expired')
        exit(1)
    except (exceptions.ConnectionError, exceptions.JobCreationError, exceptions.TokenError) as err:
        print('Error:', err)
        exit(1)

    print('%s created with id %d' % (resource, res_id))
       
def command_create(args):
    """
    Create a job
    """
    resources = {}
    resources['memory'] = args.memory
    resources['cpus'] = args.cpus
    resources['nodes'] = args.nodes
    resources['disk'] = args.disk
    if args.walltime:
        resources['walltime'] = args.walltime

    job = {}
    job['resources'] = resources
    job['name'] = args.name

    task = {}
    task['image'] = args.image

    user_env = []
    if args.env:
        user_env = args.env
    
    # MPI processes per node
    if args.openmpi or args.mpich or args.intelmpi:
        if args.ppn > 0:
            task['procsPerNode'] = args.ppn

        if args.ompppn > 0 and args.ppn > 0:
            print('Error: do not specify both --procs-per-node and --openmp-procs-per-node')
            exit(1)
        elif args.ompppn > 0 and args.ppn == 0:
            task['procsPerNode'] = job['resources']['cpus'] - args.ompppn
            user_env.append('OMP_NUM_THREADS=%d' % args.ompppn)

    # Working directory
    if args.workdir:
        task['workdir'] = args.workdir

    # 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'
    elif args.intelmpi:
        task['type'] = 'intelmpi'

    # If multiple nodes are specified need to specify MPI type
    if job['resources']['nodes'] > 1 and 'type' not in task:
        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['outputFiles'] = args.outputfile

    # Output directories
    if args.outputdir:
        job['outputDirs'] = args.outputdir

    # Files to be fetched
    if args.artifact:
        job['artifacts'] = []
        for file in args.artifact:
            artifact = {}
            artifact['url'] = file
            if (':' in file and ('http:' not in file or ('http:' in file and file.count(':') > 1)) \
                            and ('https:' not in file or ('https:' in file and file.count(':') > 1))):
                artifact['url'] = file.split(':')[0]
                artifact['mountpoint'] = '%s:%s' % (file.split(':')[1], file.split(':')[2])
            else:
                artifact['url'] = file
            job['artifacts'].append(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 user_env:
        env = {}
        for pair in user_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.placement:
        job['placement'] = json.loads(args.placement)

    # Storage
    if args.storage:
        try:
            with open(args.storage) as json_file:
                storage = '{%s}' % json_file.read()
        except IOError as err:
            print('Error: %s' % err)
            exit(1)
        except ValueError as err:
            print('Error: %s' % err)
            exit(1)
        storage = json.loads(storage)
        if 'storage' in storage:
            job['storage'] = storage['storage']

    # Preemptible
    if args.preemptible:
        job['policies'] = {}
        job['policies']['preemptible'] = args.preemptible

    # Retries
    if args.retries:
        if 'policies' not in job:
            job['policies'] = {}
        job['policies']['maximumRetries'] = args.retries

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

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

    try:
        client = ProminenceClient(authenticated=True)
        job_id = client.create_job(job)
    except exceptions.AuthenticationError:
        print('Error: authentication failed')
        exit(1)
    except exceptions.TokenExpiredError:
        print('Error: access token has expired')
        exit(1)
    except (exceptions.ConnectionError, exceptions.JobCreationError, exceptions.TokenError) as err:
        print('Error:', err)
        exit(1)

    print('Job created with id %d' % job_id)

def create_parser():
    """
    Create the argument parser
    """
    parser = argparse.ArgumentParser(description='PROMINENCE - \
                                                  run jobs in containers across clouds')
    subparsers = parser.add_subparsers(help='sub-command help')

    # Create the parser for the "run" command
    parser_run = subparsers.add_parser('run',
                                       help='Create a job or workflow from JSON in a file or URL')
    parser_run.add_argument('file',
                            help='JSON filename or URL containing JSON')
    parser_run.set_defaults(func=command_run)

    # Create the parser for the "create" command
    parser_create = subparsers.add_parser('create',
                                          help='Create a job')
    parser_create.add_argument('--name',
                               dest='name',
                               default='',
                               help='Job name.')
    parser_create.add_argument('--memory',
                               dest='memory',
                               default=1,
                               type=int,
                               help='Memory in GB per node.')
    parser_create.add_argument('--cpus',
                               dest='cpus',
                               default=1,
                               type=int,
                               help='Cores per node.')
    parser_create.add_argument('--nodes',
                               dest='nodes',
                               default=1,
                               type=int,
                               help='Number of nodes.')
    parser_create.add_argument('--procs-per-node',
                               dest='ppn',
                               default=0,
                               type=int,
                               help='Number of MPI processes to launch per node. By default this \
                                     will be the number of CPU cores requested per node but it can \
                                     be changed if necessary.')
    parser_create.add_argument('--omp-procs-per-node',
                               dest='ompppn',
                               default=0,
                               type=int,
                               help='Number of OpenMP threads to launch per node. The number of MPI \
                                     processes per node will be reduced accordingly.')
    parser_create.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_create.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_create.add_argument('--openmpi',
                               dest='openmpi',
                               default=False,
                               action='store_true',
                               help="Specify that this is an OpenMPI job.")
    parser_create.add_argument('--mpich',
                               dest='mpich',
                               default=False,
                               action='store_true',
                               help="Specify that this is an MPICH job.")
    parser_create.add_argument('--intelmpi',
                               dest='intelmpi',
                               default=False,
                               action='store_true',
                               help="Specify that this is an Intel MPI job.")
    parser_create.add_argument('--artifact',
                               dest='artifact',
                               action='append',
                               help='A URL to be transferred to the job. Archives will be \
                                     automatically unpacked/extracted. Optionally, for the \
                                     case of a tarball or zip archive, a directory name and \
                                     mount point can be specified by using the format \
                                     "<URL>:<directory>:<mountpoint>". The mount point must be \
                                     an absolute path. \
                                     This option can be specified multiple times.')
    parser_create.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_create.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_create.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_create.add_argument('--workdir',
                               dest='workdir',
                               help='Set the current working directory.')
    parser_create.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_create.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_create.add_argument('--runtime',
                               dest='runtime',
                               choices=['singularity', 'udocker'],
                               help='Container runtime, either singularity or udocker. The default \
                                     is singularity.')
    parser_create.add_argument('--retries',
                               dest='retries',
                               type=int,
                               help='Number of retries if the application exit code is not 0. By \
                                     default no retries will be attempted.')
    parser_create.add_argument('--preemptible',
                               dest='preemptible',
                               default=False,
                               action='store_true',
                               help='Specify that this job is preemptible')
    parser_create.add_argument('--placement',
                               dest='placement',
                               help='Specify requirements and preferences on where to run the job/workflow')
    parser_create.add_argument('--storage',
                               dest='storage',
                               help='Filename specifying storage details')
    parser_create.add_argument('--dry-run',
                               dest='dryrun',
                               default=False,
                               action='store_true',
                               help='Print json to stdout but do not actually create job.')
    parser_create.add_argument('image',
                               help='Container image')
    parser_create.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_create.set_defaults(func=command_create)

    # 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('-n',
                             '--last',
                             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('-a',
                             '--all',
                             dest='all',
                             default=False,
                             action='store_true',
                             help='List jobs/workflows in all states')
    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.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 "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')

    return parser

if __name__ == "__main__":
    parser = create_parser()

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

    # Check that the URL for the PROMINENCE service exists; if not, define a default
    if 'PROMINENCE_URL' not in os.environ:
        os.environ['PROMINENCE_URL'] = 'https://prominence.fedcloud-tf.fedcloud.eu/api/v1'

    # Parse the arguments
    args = parser.parse_args()

    # Run the required function
    args.func(args)
