diff --git a/README.rst b/README.rst index 73f2418..8508c33 100644 --- a/README.rst +++ b/README.rst @@ -178,8 +178,43 @@ The webgeocalc API can be call directly from the command line interface: ... - SPICE Class -- Binary PCK Lesson Kernels (Earth): (id: 39) - $ wgc-kernels --kernel Solar - - Solar System Kernels: (id: 1) + $ wgc-instruments 'Cassini Huygens' --name 'ISS' + - CASSINI_ISS_WAC_RAD: (id: -82369) + - CASSINI_ISS_NAC_RAD: (id: -82368) + - CASSINI_ISS_WAC: (id: -82361) + - CASSINI_ISS_NAC: (id: -82360) + + $ wgc-state-vector --kernels 5 \ + --times '2012-10-19T09:00:00' \ + --target 'CASSINI' \ + --observer 'SATURN' \ + --reference_frame 'IAU_SATURN' + API status: + [Calculation submit] Status: COMPLETE (id: 041bf912-178f-4450-b787-12a49c8a3101) + + Results: + DATE: + > 2012-10-19 09:00:00.000000 UTC + DISTANCE: + > 764142.63776247 + SPEED: + > 111.54765899 + X: + > 298292.85744169 + Y: + > -651606.58468976 + Z: + > 265224.81187627 + D_X_DT: + > -98.8032491 + D_Y_DT: + > -51.73211296 + D_Z_DT: + > -2.1416539 + TIME_AT_TARGET: + > 2012-10-19 08:59:57.451094 UTC + LIGHT_TIME: + > 2.54890548 More examples can be found in here_. diff --git a/docs/cli.rst b/docs/cli.rst index b8a567d..eda612c 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -2,20 +2,14 @@ Command line interface ====================== Some entry points are available through the command line -interface to get the list of available kernels, the body -objects, the frames and instruments available on the -WebGeoCalc API. - -.. note:: - - For now, calculation can not be submitted directly - through the command line. +interface. Kernel sets ----------- -List all the available kernel sets: +List all the available kernel sets available on the +WebGeoCalc API: .. code:: bash @@ -106,3 +100,119 @@ List and search instruments for a specific kernel set: - CASSINI_ISS_NAC_RAD: (id: -82368) - CASSINI_ISS_WAC: (id: -82361) - CASSINI_ISS_NAC: (id: -82360) + + +Calculations +------------ + +The command line can submit generic and specific calculation directly +with the command line interface: + +.. code:: bash + + $ wgc-calculation --help + usage: wgc-calculation [-h] [--quiet] [--payload] [--dry-run] + [--KEY [VALUE [VALUE ...]]] + + Submit generic calculation to the WebGeoCalc API + + optional arguments: + -h, --help show this help message and exit + --quiet, -q Disable verbose output status. + --payload, -p Display payload before the calculation results. + --dry-run, -d Dry run. Show only the payload. + --KEY [VALUE [VALUE ...]] + Key parameter and its value(s). + +Example: + +.. code:: bash + + $ wgc-calculation --payload \ + --kernels 1 \ + --times '2012-10-19T08:24:00.000' \ + --calculation_type 'STATE_VECTOR' \ + --target 'CASSINI' \ + --observer 'SATURN' \ + --reference_frame 'IAU_SATURN' \ + --aberration_correction 'NONE' \ + --state_representation 'PLANETOGRAPHIC' + Payload: + { + kernels: [{'type': 'KERNEL_SET', 'id': 5}], + times: ['2012-10-19T08:24:00.000'], + calculationType: STATE_VECTOR, + target: CASSINI, + observer: SATURN, + referenceFrame: IAU_SATURN, + aberrationCorrection: NONE, + stateRepresentation: PLANETOGRAPHIC, + timeSystem: UTC, + timeFormat: CALENDAR, + } + + API status: + [Calculation submit] Status: COMPLETE (id: 37d10124-a65b-44fa-9489-6c0d28cf25d2) + + Results: + DATE: + > 2012-10-19 08:24:00.000000 UTC + LONGITUDE: + > 46.18900522 + LATITUDE: + > 21.26337134 + ALTITUDE: + > 694259.8921163 + D_LONGITUDE_DT: + > 0.00888655 + D_LATITUDE_DT: + > -0.00031533 + D_ALTITUDE_DT: + > 4.77080305 + SPEED: + > 109.34997994 + TIME_AT_TARGET: + > 2012-10-19 08:24:00.000000 UTC + LIGHT_TIME: + > 2.51438831 + +The *key* parameter can be in ``underscore_case`` or ``camelCase``. +Multiple *values* can be inserted after the *key* (with ```` or ``,`` separator), +as well as duplicated *keys*. Assignation with ``=`` sign can also be used: + +.. code:: bash + + $ wgc-state-vector --dry-run \ + --kernels 1 5 \ + --times '2012-10-19T09:00:00' \ + --times '2012-10-19T10:00:00' \ + --target='CASSINI' \ + --observer = 'SATURN' \ + --referenceFrame 'IAU_SATURN' + Payload: + { + kernels: [{'type': 'KERNEL_SET', 'id': 1}, {'type': 'KERNEL_SET', 'id': 5}], + times: ['2012-10-19T09:00:00', '2012-10-19T10:00:00'], + target: CASSINI, + observer: SATURN, + referenceFrame: IAU_SATURN, + calculationType: STATE_VECTOR, + aberrationCorrection: CN, + stateRepresentation: RECTANGULAR, + timeSystem: UTC, + timeFormat: CALENDAR, + } + +Here is the list of all the calculation entry point available on the CLI: + + - ``wgc-calculation`` + - ``wgc-state-vector`` + - ``wgc-angular-separation`` + - ``wgc-angular-size`` + - ``wgc-frame-transformation`` + - ``wgc-illumination-angles`` + - ``wgc-subsolar-point`` + - ``wgc-subobserver-point`` + - ``wgc-surface-intercept-point`` + - ``wgc-osculating-elements`` + - ``wgc-time-conversion`` diff --git a/docs/index.rst b/docs/index.rst index d1e9f44..3637399 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -38,7 +38,7 @@ Python package for NAIF WebGeoCalc API In december 2018, `JPL/NAIF`_ announced an **experimental** -`API RESTful interface`_ for their new `WebGeocalc server`_ +`API RESTful interface`_ for their new `WebGeoCalc server`_ (which make online SPICE calculations). Documentation_ and `JavaScript examples`_ are already available. @@ -47,7 +47,7 @@ calculations through this API. .. _`JPL/NAIF`: https://naif.jpl.nasa.gov/naif/webgeocalc.html .. _`API RESTful interface`: https://naif.jpl.nasa.gov/naif/WebGeocalc_announcement.pdf -.. _`WebGeocalc server`: https://wgc2.jpl.nasa.gov:8443/webgeocalc +.. _`WebGeoCalc server`: https://wgc2.jpl.nasa.gov:8443/webgeocalc .. _Documentation: https://wgc2.jpl.nasa.gov:8443/webgeocalc/documents/api-info.html .. _`JavaScript examples`: https://wgc2.jpl.nasa.gov:8443/webgeocalc/example/perform-calculation.html @@ -58,7 +58,7 @@ calculations through this API. If you need to make intensive queries, use `Spiceypy`_ or `SpiceMiner`_ package with locally hosted kernels. -.. _`WebGeocalc`: https://wgc.jpl.nasa.gov:8443/webgeocalc +.. _`WebGeoCalc`: https://wgc.jpl.nasa.gov:8443/webgeocalc .. _`Spiceypy`: https://github.com/AndrewAnnex/Spiceypy .. _`SpiceMiner`: https://github.com/DaRasch/spiceminer diff --git a/setup.cfg b/setup.cfg index 5b5aeec..33e707c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -50,3 +50,14 @@ console_scripts = wgc-bodies = webgeocalc.cli:cli_bodies wgc-frames = webgeocalc.cli:cli_frames wgc-instruments = webgeocalc.cli:cli_instruments + wgc-calculation = webgeocalc.cli:cli_calculation + wgc-state-vector = webgeocalc.cli:cli_state_vector + wgc-angular-separation = webgeocalc.cli:cli_angular_separation + wgc-angular-size = webgeocalc.cli:cli_angular_size + wgc-frame-transformation = webgeocalc.cli:cli_frame_transformation + wgc-illumination-angles = webgeocalc.cli:cli_illumination_angles + wgc-subsolar-point = webgeocalc.cli:cli_subsolar_point + wgc-subobserver-point = webgeocalc.cli:cli_subobserver_point + wgc-surface-intercept-point = webgeocalc.cli:cli_surface_intercept_point + wgc-osculating-elements = webgeocalc.cli:cli_osculating_elements + wgc-time-conversion = webgeocalc.cli:cli_time_conversion diff --git a/tests/test_cli.py b/tests/test_cli.py index 91f6ed1..39c2b97 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- '''Test WGC command line inputs.''' -from webgeocalc.cli import cli_bodies, cli_frames, cli_instruments, cli_kernel_sets - +from webgeocalc.cli import _params, cli_angular_separation, cli_angular_size, cli_bodies,\ + cli_frame_transformation, cli_frames, cli_illumination_angles, cli_instruments, \ + cli_kernel_sets, cli_osculating_elements, cli_state_vector, cli_subobserver_point, \ + cli_subsolar_point, cli_surface_intercept_point, cli_time_conversion def test_cli_kernel_sets(capsys): '''Test GET kernels with CLI.''' @@ -78,3 +80,189 @@ def test_cli_instruments(capsys): captured = capsys.readouterr() assert ' - CASSINI_ISS_WAC: (id: -82361)' in captured.out assert ' - CASSINI_VIMS_IR: (id: -82370)' not in captured.out + +def test_cli_input_parameters(capsys): + '''Test CLI input parameters parsing.''' + def parse(x): + return _params(x.split()) + + assert parse('--kernels 1') == {'kernels': 1} + assert parse('--kernels=1') == {'kernels': 1} + assert parse('--kernels= 1') == {'kernels': 1} + assert parse('--kernels = 1') == {'kernels': 1} + assert parse('--kernels =1') == {'kernels': 1} + assert parse('--kernels 1 5') == {'kernels': [1, 5]} + assert parse('--kernels=1 5') == {'kernels': [1, 5]} + assert parse('--kernels 1,5') == {'kernels': [1, 5]} + assert parse('--kernels=1,5') == {'kernels': [1, 5]} + assert parse('--kernels 1, 5') == {'kernels': [1, 5]} + assert parse('--kernels=1, 5') == {'kernels': [1, 5]} + assert parse('--kernels [1,5]') == {'kernels': [1, 5]} + assert parse('--kernels=[1,5]') == {'kernels': [1, 5]} + assert parse('--kernels [1, 5]') == {'kernels': [1, 5]} + assert parse('--kernels=[1, 5]') == {'kernels': [1, 5]} + assert parse('--kernels [ 1,5]') == {'kernels': [1, 5]} + assert parse('--kernels=[ 1,5]') == {'kernels': [1, 5]} + assert parse('--kernels [ 1, 5]') == {'kernels': [1, 5]} + assert parse('--kernels [ 1, 5 ]') == {'kernels': [1, 5]} + assert parse('--kernels [1, 5 ]') == {'kernels': [1, 5]} + assert parse('--kernels [1,5 ]') == {'kernels': [1, 5]} + assert parse('--kernels=[1,5 ]') == {'kernels': [1, 5]} + assert parse('--kernels [1 ,5]') == {'kernels': [1, 5]} + assert parse('--kernels [1 , 5]') == {'kernels': [1, 5]} + assert parse('--kernels [1, 3, 5]') == {'kernels': [1, 3, 5]} + assert parse('--kernels 1, 3, 5') == {'kernels': [1, 3, 5]} + assert parse('--kernels 1 --kernels 5') == {'kernels': [1, 5]} + assert parse('--kernels=1 --kernels 5') == {'kernels': [1, 5]} + assert parse('--kernels=1 --kernels=5') == {'kernels': [1, 5]} + assert parse('null --kernels=1') == {'kernels': 1} + + assert parse('--kernels Cassini') == {'kernels': 'Cassini'} + assert parse('--kernels "Cassini"') == {'kernels': 'Cassini'} + assert parse("--kernels 'Cassini'") == {'kernels': 'Cassini'} + assert parse('--kernels "Cassini" "Solar"') == {'kernels': ['Cassini', 'Solar']} + + assert parse("--times '2012-10-19T08:24:00'") == {'times': '2012-10-19T08:24:00'} + +def test_cli_state_vector_empty(capsys): + '''Test empty state vector calculation parameter with the CLI.''' + argv = ''.split() + cli_state_vector(argv) + captured = capsys.readouterr() + assert 'usage:' in captured.out + +def test_cli_angular_separation_wrong_attr(capsys): + '''Test attribute in angular separation calculation parameter with the CLI.''' + argv = '--wrong 123'.split() + cli_angular_separation(argv) + captured = capsys.readouterr() + assert 'Attribute \'target_1\' required.\n' == captured.out + +def test_cli_angular_size_run(capsys): + '''Test run angular size calculation parameter with the CLI.''' + argv = ('--payload ' + '--kernels 5 ' + '--times 2012-10-19T08:24:00 ' + '--target ENCELADUS ' + '--observer CASSINI ' + '--aberration_correction CN+S').split() + + cli_angular_size(argv) + captured = capsys.readouterr() + assert 'Payload:' in captured.out + assert "kernels: [{'type': 'KERNEL_SET', 'id': 5}]," in captured.out + assert "times: ['2012-10-19T08:24:00']," in captured.out + assert "calculationType: ANGULAR_SIZE," in captured.out + assert "timeSystem: UTC," in captured.out + assert 'API status:\n[Calculation submit] Status:' in captured.out + assert 'Results:\nDATE:\n> 2012-10-19 08:24:00.000000 UTC' in captured.out + assert 'ANGULAR_SIZE:\n> 0.03037939' in captured.out + +def test_cli_frame_transformation_dry_run(capsys): + '''Test dry-run frame transform calculation parameter with the CLI.''' + argv = ('--dry-run ' + '--kernels 5 ' + '--times 2012-10-19T08:24:00.000 ' + '--frame_1 IAU_SATURN ' + '--frame_2 IAU_ENCELADUS ' + '--aberration_correction NONE').split() + + cli_frame_transformation(argv) + captured = capsys.readouterr() + assert 'Payload:' in captured.out + assert "calculationType: FRAME_TRANSFORMATION," in captured.out + assert 'API status:\n[Calculation submit] Status:' not in captured.out + assert 'Results:' not in captured.out + +def test_cli_illumination_angles_dry_run(capsys): + '''Test dry-run illumination angles calculation parameter with the CLI.''' + argv = ('--dry-run ' + '--kernels 5 ' + '--times 2012-10-19T08:24:00.000 ' + '--target ENCELADUS ' + '--target_frame IAU_ENCELADUS ' + '--observer CASSINI ' + '--aberration_correction CN+S ' + '--latitude 0.0 ' + '--longitude 0.0').split() + + cli_illumination_angles(argv) + captured = capsys.readouterr() + assert 'Payload:' in captured.out + assert "calculationType: ILLUMINATION_ANGLES," in captured.out + +def test_cli_subsolar_point_dry_run(capsys): + '''Test dry-run sub-solar point calculation parameter with the CLI.''' + argv = ('--dry-run ' + '--kernels 5 ' + '--times 2012-10-19T08:24:00.000 ' + '--target ENCELADUS ' + '--target_frame IAU_ENCELADUS ' + '--observer CASSINI ' + '--aberration_correction CN+S').split() + + cli_subsolar_point(argv) + captured = capsys.readouterr() + assert 'Payload:' in captured.out + assert "calculationType: SUB_SOLAR_POINT," in captured.out + +def test_cli_subobserver_point_dry_run(capsys): + '''Test dry-run sub-observer point calculation parameter with the CLI.''' + argv = ('--dry-run ' + '--kernels 5 ' + '--times 2012-10-19T08:24:00.000 ' + '--target ENCELADUS ' + '--target_frame IAU_ENCELADUS ' + '--observer CASSINI ' + '--aberration_correction CN+S').split() + + cli_subobserver_point(argv) + captured = capsys.readouterr() + assert 'Payload:' in captured.out + assert "calculationType: SUB_OBSERVER_POINT," in captured.out + +def test_cli_surface_intercept_point_dry_run(capsys): + '''Test dry-run surface intercept point calculation parameter with the CLI.''' + argv = ('--dry-run ' + '--kernels 5 ' + '--times 2012-10-19T08:24:00.000 ' + '--target SATURN ' + '--target_frame IAU_SATURN ' + '--observer CASSINI ' + '--intercept_vector_type INSTRUMENT_BORESIGHT ' + '--intercept_instrument CASSINI_ISS_NAC ' + '--aberration_correction NONE ' + '--state_representation LATITUDINAL').split() + + cli_surface_intercept_point(argv) + captured = capsys.readouterr() + assert 'Payload:' in captured.out + assert "calculationType: SURFACE_INTERCEPT_POINT," in captured.out + +def test_cli_osculating_elements_dry_run(capsys): + '''Test dry-run osculating elements calculation parameter with the CLI.''' + argv = ('--dry-run ' + '--kernels 1 5 ' + '--times 2012-10-19T08:24:00.000 ' + '--orbiting_body CASSINI ' + '--center_body SATURN').split() + + cli_osculating_elements(argv) + captured = capsys.readouterr() + assert 'Payload:' in captured.out + assert "calculationType: OSCULATING_ELEMENTS," in captured.out + +def test_cli_time_conversion_dry_run(capsys): + '''Test dry-run time conversion calculation parameter with the CLI.''' + argv = ('--dry-run ' + '--kernels 5 ' + '--times 1/1729329441.04 ' + '--time_system SPACECRAFT_CLOCK ' + '--time_format SPACECRAFT_CLOCK_STRING ' + '--sclk_id -82 ' + ).split() + + cli_time_conversion(argv) + captured = capsys.readouterr() + assert 'Payload:' in captured.out + assert "calculationType: TIME_CONVERSION," in captured.out diff --git a/webgeocalc/cli.py b/webgeocalc/cli.py index 276b229..23e1262 100644 --- a/webgeocalc/cli.py +++ b/webgeocalc/cli.py @@ -2,8 +2,12 @@ '''Command line interface.''' import argparse +import re from .api import API +from .calculation import AngularSeparation, AngularSize, \ + Calculation, FrameTransformation, IlluminationAngles, OsculatingElements, \ + StateVector, SubObserverPoint, SubSolarPoint, SurfaceInterceptPoint, TimeConversion from .errors import KernelSetNotFound, TooManyKernelSets def cli_kernel_sets(argv=None): @@ -12,7 +16,7 @@ def cli_kernel_sets(argv=None): GET: /kernel-sets ''' parser = argparse.ArgumentParser( - description='List and search kernel sets available in WebGeocalc API.') + description='List and search kernel sets available in WebGeoCalc API.') parser.add_argument('--all', '-a', action='store_true', help='List all kernel sets available') parser.add_argument('--kernel', '-k', metavar='NAME|ID', nargs='+', @@ -46,7 +50,7 @@ def cli_bodies(argv=None): GET: /kernel-set/{kernelSetId}/bodies ''' parser = argparse.ArgumentParser( - description='List bodies available in WebGeocalc API for a specific kernel set.') + description='List bodies available in WebGeoCalc API for a specific kernel set.') parser.add_argument('kernel', nargs='?', help='Kernel set name or id') parser.add_argument('--name', '-n', metavar='BODY', nargs='+', help='Search a specific body by name or id') @@ -77,7 +81,7 @@ def cli_frames(argv=None): GET: /kernel-set/{kernelSetId}/frames ''' parser = argparse.ArgumentParser( - description='List frames available in WebGeocalc API for a specific kernel set.') + description='List frames available in WebGeoCalc API for a specific kernel set.') parser.add_argument('kernel', nargs='?', help='Kernel set name or id') parser.add_argument('--name', '-n', metavar='FRAME', nargs='+', help='Search a specific frame by name or id') @@ -108,7 +112,7 @@ def cli_instruments(argv=None): GET: /kernel-set/{kernelSetId}/instruments ''' parser = argparse.ArgumentParser(description='List instruments available' - 'in WebGeocalc API for a specific kernel set.') + 'in WebGeoCalc API for a specific kernel set.') parser.add_argument('kernel', nargs='?', help='Kernel set name or id') parser.add_argument('--name', '-n', metavar='INSTRUMENT', nargs='+', help='Search a specific instrument by name or id') @@ -134,3 +138,146 @@ def cli_instruments(argv=None): for instrument in instruments])) else: parser.print_help() + +def _split(string, sep=',', replace=['[', ']', '=', '"', "'"]): + # Replace and split string + for char in replace: + string = string.replace(char, '') + return string.split(sep) + +def _underscore_case(string): + # Convert string to underscore case (ie. snake case) + string = string.replace('-', '_') + s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', string) + return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() + +def _params(params): + # Parse input parameters + out = {} + key = None + for param in params: + if param.startswith('--'): + if '=' in param: + key = param[2:].split('=') + param = ','.join(key[1:]) + key = _underscore_case(key[0]) + else: + key = _underscore_case(param[2:]) + continue + + if key is None: + continue + + for value in _split(param): + if value == '': + continue + + try: + value = int(value) + except ValueError: + try: + value = float(value) + except ValueError: + pass + + if key not in out: + out[key] = value + elif not isinstance(out[key], list): + out[key] = [out[key], value] + else: + out[key] += [value] + + return out + +def cli_calculation(argv=None, calculation=Calculation, desc='generic'): + '''Submit and get calculation results. + + - POST: /calculation/new + payload + - GET: /calculation/{id} + - GET: /calculation/{id}/results + ''' + parser = argparse.ArgumentParser( + description=f'Submit {desc} calculation to the WebGeoCalc API') + + parser.add_argument('--quiet', '-q', action='store_true', + help='Disable verbose output status.') + parser.add_argument('--payload', '-p', action='store_true', + help='Display payload before the calculation results.') + parser.add_argument('--dry-run', '-d', action='store_true', + help='Dry run. Show only the payload.') + parser.add_argument('--KEY', metavar='VALUE', nargs='*', + help='Key parameter and its value(s).') + + args, others = parser.parse_known_args(argv) + params = _params(others) + + if len(params) > 0: + params['verbose'] = not(args.quiet) + try: + calc = calculation(**params) + + if (args.payload or args.dry_run) and not(args.quiet): + + payload = calc.payload + print('Payload:\n{') + + for key, value in payload.items(): + print(f' {key}: {value},') + print('}') + + if not(args.dry_run): + if not(args.quiet): + print('\nAPI status:') + + res = calc.run() + if not(args.quiet): + print('\nResults:') + + for key, value in res.items(): + print(f'{key}:\n> {value}') + + except Exception as err: + print(err) + else: + parser.print_help() + + +def cli_state_vector(argv=None): + '''Submit state vector calculation with the CLI.''' + cli_calculation(argv, StateVector, desc='State Vector') + +def cli_angular_separation(argv=None): + '''Submit angular separation calcultion with the CLI.''' + cli_calculation(argv, AngularSeparation, desc='Angular Separation') + +def cli_angular_size(argv=None): + '''Submit angular size calcultion with the CLI.''' + cli_calculation(argv, AngularSize, desc='Angular Size') + +def cli_frame_transformation(argv=None): + '''Submit frame transformation calcultion with the CLI.''' + cli_calculation(argv, FrameTransformation, desc='Frame Transformation') + +def cli_illumination_angles(argv=None): + '''Submit illumination angles calcultion with the CLI.''' + cli_calculation(argv, IlluminationAngles, desc='Illumination Angles') + +def cli_subsolar_point(argv=None): + '''Submit sub-solar point calcultion with the CLI.''' + cli_calculation(argv, SubSolarPoint, desc='Sub-Solar Point') + +def cli_subobserver_point(argv=None): + '''Submit sub-observer point calcultion with the CLI.''' + cli_calculation(argv, SubObserverPoint, desc='Sub-Observer Point') + +def cli_surface_intercept_point(argv=None): + '''Submit surface intercept point calcultion with the CLI.''' + cli_calculation(argv, SurfaceInterceptPoint, desc='Surface Intercept Point') + +def cli_osculating_elements(argv=None): + '''Submit osculating elements calcultion with the CLI.''' + cli_calculation(argv, OsculatingElements, desc='Osculating Elements') + +def cli_time_conversion(argv=None): + '''Submit time conversion calcultion with the CLI.''' + cli_calculation(argv, TimeConversion, desc='Time Conversion')