diff --git a/cromwell_tools/cli.py b/cromwell_tools/cli.py index e2370bf..279245a 100644 --- a/cromwell_tools/cli.py +++ b/cromwell_tools/cli.py @@ -41,6 +41,11 @@ def parser(arguments=None): help='release_hold help', description='Request Cromwell to release the hold on a workflow.', ) + metadata = subparsers.add_parser( + 'metadata', + help='metadata help', + description='Retrieve the workflow and call-level metadata for a specified workflow by UUID.', + ) query = subparsers.add_parser( 'query', help='query help', @@ -53,7 +58,16 @@ def parser(arguments=None): ) # cromwell url and authentication arguments apply to all sub-commands - cromwell_sub_commands = (submit, wait, status, abort, release_hold, query, health) + cromwell_sub_commands = ( + submit, + wait, + status, + abort, + release_hold, + metadata, + query, + health, + ) auth_args = { 'url': 'The URL to the Cromwell server. e.g. "https://cromwell.server.org/"', 'username': 'Cromwell username for HTTPBasicAuth.', @@ -186,6 +200,31 @@ def add_auth_args(subcommand_parser): help='A Cromwell workflow UUID, which is the workflow identifier.', ) + # metadata arguments + metadata.add_argument( + '--uuid', + required=True, + help='A Cromwell workflow UUID, which is the workflow identifier.', + ) + # TODO: add a mutually exclusive group to make it fail early + metadata.add_argument( + '--includeKey', + nargs='+', + default=None, + help='When specified key(s) to include from the metadata. Matches any key starting with the value. May not be used with excludeKey.', + ) + metadata.add_argument( + '--excludeKey', + nargs='+', + default=None, + help='When specified key(s) to exclude from the metadata. Matches any key starting with the value. May not be used with includeKey.', + ) + metadata.add_argument( + '--expandSubWorkflows', + default=False, + help='When true, metadata for sub workflows will be fetched and inserted automatically in the metadata response.', + ) + # query arguments # TODO: implement CLI entry for query API. @@ -200,6 +239,7 @@ def add_auth_args(subcommand_parser): 'abort', 'release_hold', 'health', + 'metadata', ): auth_arg_dict = {k: args.get(k) for k in auth_args.keys()} auth = CromwellAuth.harmonize_credentials(**auth_arg_dict) diff --git a/cromwell_tools/cromwell_api.py b/cromwell_tools/cromwell_api.py index be0b056..263e3a7 100644 --- a/cromwell_tools/cromwell_api.py +++ b/cromwell_tools/cromwell_api.py @@ -92,6 +92,67 @@ def abort(cls, uuid, auth, raise_for_status=False): cls._check_and_raise_status(response) return response + @classmethod + def metadata( + cls, + uuid, + auth, + includeKey=None, + excludeKey=None, + expandSubWorkflows=False, + raise_for_status=False, + ): + """Retrieve the workflow and call-level metadata for a specified workflow by UUID. + + Args: + uuid (str): A Cromwell workflow UUID, which is the workflow identifier. + auth (cromwell_tools.cromwell_auth.CromwellAuth): The authentication class holding headers or auth + information to a Cromwell server. + includeKey (Optional[List]): When specified key(s) to include from the metadata. Matches any key + starting with the value. May not be used with excludeKey. (default None) + excludeKey (Optional[List]): When specified key(s) to exclude from the metadata. Matches any key + starting with the value. May not be used with includeKey. (default None) + expandSubWorkflows (Optional[bool]): When true, metadata for sub workflows will be fetched + and inserted automatically in the metadata response. (default False) + raise_for_status (Optional[bool]): Whether to check and raise for status based on the response. (default + False) + + Raises: + requests.exceptions.HTTPError: This will be raised when raise_for_status is True and Cromwell returns + a response that satisfies 400 <= response.status_code < 600. + + Returns: + requests.Response: HTTP response from Cromwell. + """ + + if excludeKey and includeKey: + raise ValueError('includeKey and excludeKey may not be specified together!') + + params = {'expandSubWorkflows': json.dumps(expandSubWorkflows)} + + if isinstance(excludeKey, str): + logger.info(f'Adding {excludeKey} to the request parameter list.') + params['excludeKey'] = [excludeKey] + elif isinstance(excludeKey, list) and len(excludeKey) >= 1: + params['excludeKey'] = excludeKey + + if isinstance(includeKey, str): + logger.info(f'Adding {includeKey} to the request parameter list.') + params['includeKey'] = [includeKey] + elif isinstance(includeKey, list) and len(includeKey) >= 1: + params['includeKey'] = includeKey + + response = requests.get( + url=auth.url + cls._metadata_endpoint.format(uuid=uuid), + auth=auth.auth, + headers=auth.header, + params=params, + ) + + if raise_for_status: + cls._check_and_raise_status(response) + return response + @classmethod def status(cls, uuid, auth, raise_for_status=False): """Retrieves the current state for a workflow by UUID. diff --git a/cromwell_tools/tests/test_cromwell_api.py b/cromwell_tools/tests/test_cromwell_api.py index 55a00e1..8e5a6bc 100644 --- a/cromwell_tools/tests/test_cromwell_api.py +++ b/cromwell_tools/tests/test_cromwell_api.py @@ -270,6 +270,29 @@ def _request_callback(request, context): with self.assertRaises(requests.exceptions.HTTPError): CromwellAPI.release_hold(workflow_id, cromwell_auth).raise_for_status() + @requests_mock.mock() + def test_metadata_returns_200(self, mock_request): + workflow_id = '12345abcde' + test_include_key = 'workflow' + + def _request_callback(request, context): + context.status_code = 200 + context.headers['test'] = 'header' + return {'id': '12345abcde', 'actualWorkflowLanguageVersion': 'draft-2'} + + for cromwell_auth in self.auth_options: + mock_request.get( + '{0}/api/workflows/v1/{1}/metadata?expandSubWorkflows=false&includeKey={2}'.format( + cromwell_auth.url, workflow_id, test_include_key + ), + json=_request_callback, + ) + result = CromwellAPI.metadata( + workflow_id, cromwell_auth, includeKey=test_include_key + ) + self.assertEqual(result.status_code, 200) + self.assertEqual(result.json()['id'], workflow_id) + @requests_mock.mock() def test_health_returns_200(self, mock_request): expected = {