From e903460af620682518c4642608c50bf11827acc3 Mon Sep 17 00:00:00 2001 From: Jan Okraska Date: Fri, 30 Nov 2018 18:08:52 +0100 Subject: [PATCH] api: use local proxy instead of the decorator * introduces single variable local proxy instead of decorator * remove Client class because it was using inheritance (from BaseAPIClient) and it wasn't a correct relation. Without it it was just a collection of methods so we decided to leave them as separate functions. * remove mock_base_api_client fixture because it was returning a Client object which is no longer present. (something similar is necessery because now we have a repeating code in lots of places) --- reana_client/api/__init__.py | 2 - reana_client/api/client.py | 81 ++++++------- reana_client/cli/__init__.py | 1 - reana_client/cli/cwl_runner.py | 21 ++-- reana_client/cli/files.py | 46 ++++---- reana_client/cli/ping.py | 19 ++- reana_client/cli/workflow.py | 87 +++++++------- reana_client/decorators.py | 53 --------- setup.py | 1 + tests/conftest.py | 22 ---- tests/test_cli_files.py | 205 ++++++++++++++++++--------------- tests/test_cli_ping.py | 25 ++-- tests/test_cli_workflows.py | 196 +++++++++++++++++-------------- 13 files changed, 371 insertions(+), 388 deletions(-) delete mode 100644 reana_client/decorators.py diff --git a/reana_client/api/__init__.py b/reana_client/api/__init__.py index ef0c8910..113db2de 100644 --- a/reana_client/api/__init__.py +++ b/reana_client/api/__init__.py @@ -9,5 +9,3 @@ """Python api for connecting to REANA server.""" from __future__ import absolute_import, print_function - -from .client import Client diff --git a/reana_client/api/client.py b/reana_client/api/client.py index d018e6ca..69947aa6 100644 --- a/reana_client/api/client.py +++ b/reana_client/api/client.py @@ -7,20 +7,20 @@ # under the terms of the MIT License; see LICENSE file for more details. """REANA REST API client.""" -import traceback import enum import json import logging import os +import traceback from functools import partial -from werkzeug.local import LocalProxy import pkg_resources from bravado.exception import HTTPError -from reana_commons.api_client import BaseAPIClient -from reana_commons.api_client import get_current_api_client +from werkzeug.local import LocalProxy + from reana_client.errors import FileDeletionError, FileUploadError from reana_client.utils import get_workflow_root +from reana_commons.api_client import BaseAPIClient, get_current_api_client current_rs_api_client = LocalProxy( partial(get_current_api_client, component='reana-server')) @@ -43,8 +43,8 @@ def ping(): 'REANA server health check failed: ' '\nStatus: {}\nReason: {}\n' 'Message: {}'.format(e.response.status_code, - e.response.reason, - e.response.json()['message'])) + e.response.reason, + e.response.json()['message'])) raise Exception(e.response.json()['message']) except Exception as e: raise e @@ -68,8 +68,8 @@ def get_workflows(access_token): 'The list of workflows could not be retrieved: ' '\nStatus: {}\nReason: {}\n' 'Message: {}'.format(e.response.status_code, - e.response.reason, - e.response.json()['message'])) + e.response.reason, + e.response.json()['message'])) raise Exception(e.response.json()['message']) except Exception as e: raise e @@ -95,8 +95,8 @@ def get_workflow_status(workflow, access_token): 'Analysis status could not be retrieved: ' '\nStatus: {}\nReason: {}\n' 'Message: {}'.format(e.response.status_code, - e.response.reason, - e.response.json()['message'])) + e.response.reason, + e.response.json()['message'])) raise Exception(e.response.json()['message']) except Exception as e: raise e @@ -124,8 +124,8 @@ def create_workflow(reana_specification, name, access_token): 'Workflow creation failed: ' '\nStatus: {}\nReason: {}\n' 'Message: {}'.format(e.response.status_code, - e.response.reason, - e.response.json()['message'])) + e.response.reason, + e.response.json()['message'])) raise Exception(e.response.json()['message']) except Exception as e: raise e @@ -182,8 +182,8 @@ def upload_file(workflow_id, file_, file_name, access_token): 'File could not be uploaded: ' '\nStatus: {}\nReason: {}\n' 'Message: {}'.format(e.response.status_code, - e.response.reason, - e.response.json()['message'])) + e.response.reason, + e.response.json()['message'])) raise Exception(e.response.json()['message']) except Exception as e: raise e @@ -210,8 +210,8 @@ def get_workflow_logs(workflow_id, access_token): 'Workflow logs could not be retrieved: ' '\nStatus: {}\nReason: {}\n' 'Message: {}'.format(e.response.status_code, - e.response.reason, - e.response.json()['message'])) + e.response.reason, + e.response.json()['message'])) raise Exception(e.response.json()['message']) except Exception as e: raise e @@ -245,8 +245,8 @@ def download_file(workflow_id, file_name, access_token): 'Output file could not be downloaded: ' '\nStatus: {}\nReason: {}\n' 'Message: {}'.format(e.response.status_code, - e.response.reason, - e.response.json()['message'])) + e.response.reason, + e.response.json()['message'])) raise Exception(e.response.json()['message']) except Exception as e: raise e @@ -265,7 +265,7 @@ def delete_file(workflow_id, file_name, access_token): file_name=file_name, access_token=access_token).result() if http_response.status_code == 200 and (response['deleted'] or - response['failed']): + response['failed']): return response elif not (response['deleted'] or response['failed']): raise FileDeletionError('{} did not match any existing ' @@ -281,14 +281,14 @@ def delete_file(workflow_id, file_name, access_token): 'File could not be downloaded: ' '\nStatus: {}\nReason: {}\n' 'Message: {}'.format(e.response.status_code, - e.response.reason, - e.response.json()['message'])) + e.response.reason, + e.response.json()['message'])) raise Exception(e.response.json()['message']) except Exception as e: raise e -def get_files(workflow_id, access_token): +def list_files(workflow_id, access_token): """Return the list of file for a given workflow workspace. :param workflow_id: UUID which identifies the workflow. @@ -314,8 +314,8 @@ def get_files(workflow_id, access_token): 'File list could not be retrieved: ' '\nStatus: {}\nReason: {}\n' 'Message: {}'.format(e.response.status_code, - e.response.reason, - e.response.json()['message'])) + e.response.reason, + e.response.json()['message'])) raise Exception(e.response.json()['message']) except Exception as e: raise e @@ -355,8 +355,7 @@ def upload_to_server(workflow, paths, access_token): if os.path.isdir(path): logging.debug("'{}' is a directory.".format(path)) - logging.info("Uploading contents of folder '{}' ..." - .format(path)) + logging.info("Uploading contents of folder '{}' ...".format(path)) for root, dirs, files in os.walk(path, topdown=False): uploaded_files = [] for next_path in files + dirs: @@ -394,7 +393,7 @@ def upload_to_server(workflow, paths, access_token): save_path = "/".join( save_path.strip("/").split('/')[1:]) logging.debug("'{}' is an absolute filepath." - .format(os.path.basename(fname))) + .format(os.path.basename(fname))) logging.info("Uploading '{}' ...".format(fname)) try: response = upload_file(workflow, f, save_path, @@ -407,8 +406,8 @@ def upload_to_server(workflow, paths, access_token): except Exception as e: logging.debug(traceback.format_exc()) logging.debug(str(e)) - logging.info("Something went wrong while uploading {}". - format(fname)) + logging.info("Something went wrong while uploading {}" + .format(fname)) def get_workflow_parameters(workflow, access_token): @@ -432,8 +431,8 @@ def get_workflow_parameters(workflow, access_token): 'Workflow parameters could not be retrieved: ' '\nStatus: {}\nReason: {}\n' 'Message: {}'.format(e.response.status_code, - e.response.reason, - e.response.json()['message'])) + e.response.reason, + e.response.json()['message'])) raise Exception(e.response.json()['message']) except Exception as e: raise e @@ -443,10 +442,11 @@ def delete_workflow(workflow, all_runs, hard_delete, workspace, access_token): """Delete a workflow.""" try: - parameters = {'all_runs': True if all_runs == 1 else False, - 'hard_delete': True if hard_delete == 1 else False, - 'workspace': True if hard_delete == 1 or - workspace == 1 else False} + parameters = { + 'all_runs': True if all_runs == 1 else False, + 'hard_delete': True if hard_delete == 1 else False, + 'workspace': True if hard_delete == 1 or workspace == 1 else False + } (response, http_response) = current_rs_api_client.api.set_workflow_status( workflow_id_or_name=workflow, @@ -466,8 +466,8 @@ def delete_workflow(workflow, all_runs, hard_delete, 'Workflow run could not be deleted: ' '\nStatus: {}\nReason: {}\n' 'Message: {}'.format(e.response.status_code, - e.response.reason, - e.response.json()['message'])) + e.response.reason, + e.response.json()['message'])) raise Exception(e.response.json()['message']) except Exception as e: raise e @@ -477,7 +477,8 @@ def stop_workflow(workflow, force_stop, access_token): """Stop a workflow.""" try: parameters = {'force_stop': force_stop} - (response, http_response) = current_rs_api_client.api.set_workflow_status( + (response, http_response) = current_rs_api_client.api\ + .set_workflow_status( workflow_id_or_name=workflow, status='stop', access_token=access_token, @@ -494,8 +495,8 @@ def stop_workflow(workflow, force_stop, access_token): 'Workflow run could not be stopped: ' '\nStatus: {}\nReason: {}\n' 'Message: {}'.format(e.response.status_code, - e.response.reason, - e.response.json()['message'])) + e.response.reason, + e.response.json()['message'])) raise Exception(e.response.json()['message']) except Exception as e: raise e diff --git a/reana_client/cli/__init__.py b/reana_client/cli/__init__.py index f58e256e..48ec8bdb 100644 --- a/reana_client/cli/__init__.py +++ b/reana_client/cli/__init__.py @@ -12,7 +12,6 @@ import click -from reana_client.api import Client from reana_client.cli import workflow, files, ping DEBUG_LOG_FORMAT = '[%(asctime)s] p%(process)s ' \ diff --git a/reana_client/cli/cwl_runner.py b/reana_client/cli/cwl_runner.py index d8a90c35..72e1fdd6 100644 --- a/reana_client/cli/cwl_runner.py +++ b/reana_client/cli/cwl_runner.py @@ -24,14 +24,14 @@ from cwltool.main import printdeps from cwltool.workflow import findfiles -from reana_client.api import Client +from reana_client.api.client import (create_workflow, current_rs_api_client, + get_workflow_logs, start_workflow, + upload_file) from reana_client.cli.utils import add_access_token_options from reana_client.config import default_user -from reana_client.decorators import with_api_client from reana_client.utils import load_workflow_spec from reana_client.version import __version__ - PY3 = sys.version_info > (3,) @@ -68,7 +68,6 @@ def get_file_dependencies_obj(cwl_obj, basedir): @click.argument('processfile', required=False) @click.argument('jobfile') @click.pass_context -@with_api_client def cwl_runner(ctx, quiet, outdir, basedir, processfile, jobfile, access_token): """Run CWL files in a standard format .""" @@ -104,9 +103,9 @@ def cwl_runner(ctx, quiet, outdir, basedir, processfile, jobfile, reana_spec['workflow']['spec'] = replace_location_in_cwl_spec( reana_spec['workflow']['spec']) - logging.info('Connecting to {0}'.format(ctx.obj.client.server_url)) - response = ctx.obj.client.create_workflow(reana_spec, 'cwl-test', - access_token) + logging.info('Connecting to {0}'.format( + current_rs_api_client.swagger_spec.api_url)) + response = create_workflow(reana_spec, 'cwl-test', access_token) logging.error(response) workflow_name = response['workflow_name'] workflow_id = response['workflow_id'] @@ -122,11 +121,10 @@ def cwl_runner(ctx, quiet, outdir, basedir, processfile, jobfile, file_path = cwl_file_object.get('location') abs_file_path = os.path.join(basedir, file_path) with open(abs_file_path, 'r') as f: - ctx.obj.client.upload_file(workflow_id, f, file_path, - access_token) + upload_file(workflow_id, f, file_path, access_token) logging.error('File {} uploaded.'.format(file_path)) - response = ctx.obj.client.start_workflow( + response = start_workflow( workflow_id, access_token, reana_spec['inputs']['parameters']) logging.error(response) @@ -134,8 +132,7 @@ def cwl_runner(ctx, quiet, outdir, basedir, processfile, jobfile, while True: sleep(1) logging.error('Polling workflow logs') - response = ctx.obj.client.get_workflow_logs(workflow_id, - access_token) + response = get_workflow_logs(workflow_id, access_token) logs = response['logs'] if logs != first_logs: diff --git a/reana_client/cli/files.py b/reana_client/cli/files.py index bfddfa14..b4a9209f 100644 --- a/reana_client/cli/files.py +++ b/reana_client/cli/files.py @@ -13,17 +13,21 @@ import traceback import click - import tablib +from reana_client.api.client import delete_file, download_file, \ + current_rs_api_client +from reana_client.api.client import list_files +from reana_client.api.client import upload_to_server +from reana_client.cli.utils import add_access_token_options from reana_client.config import ERROR_MESSAGES, default_user from reana_client.errors import FileDeletionError, FileUploadError from reana_client.utils import get_workflow_root, load_reana_spec -from reana_client.cli.utils import add_access_token_options -from reana_client.decorators import with_api_client +from reana_commons.errors import MissingAPIClientConfiguration from reana_commons.utils import click_table_printer + @click.group( help='All interaction related to files.') @click.pass_context @@ -54,7 +58,6 @@ def files(ctx): help='Get output in JSON format.') @add_access_token_options @click.pass_context -@with_api_client def get_files(ctx, workflow, _filter, output_format, access_token): """List workflow workspace files.""" @@ -62,6 +65,15 @@ def get_files(ctx, workflow, _filter, for p in ctx.params: logging.debug('{param}: {value}'.format(param=p, value=ctx.params[p])) + try: + _url = current_rs_api_client.swagger_spec.api_url + except MissingAPIClientConfiguration as e: + click.secho( + 'REANA client is not connected to any REANA cluster.', + fg='red', err=True + ) + sys.exit(1) + if not access_token: click.echo( click.style(ERROR_MESSAGES['missing_access_token'], @@ -70,7 +82,7 @@ def get_files(ctx, workflow, _filter, if workflow: logging.info('Workflow "{}" selected'.format(workflow)) try: - response = ctx.obj.client.get_files(workflow, access_token) + response = list_files(workflow, access_token) headers = ['name', 'size', 'last-modified'] data = [] for file_ in response: @@ -132,7 +144,6 @@ def get_files(ctx, workflow, _filter, help='Path to the directory where files will be downloaded.') @add_access_token_options @click.pass_context -@with_api_client def download_files(ctx, workflow, filenames, output_directory, access_token): """Download workflow workspace file(s).""" logging.debug('command: {}'.format(ctx.command_path.replace(" ", "."))) @@ -155,10 +166,10 @@ def download_files(ctx, workflow, filenames, output_directory, access_token): if workflow: for file_name in filenames: try: - binary_file = \ - ctx.obj.client.download_file(workflow, - file_name, - access_token) + binary_file = download_file(workflow, + file_name, + access_token) + logging.info('{0} binary file downloaded ... writing to {1}'. format(file_name, output_directory)) @@ -213,7 +224,6 @@ def download_files(ctx, workflow, filenames, output_directory, access_token): 'Overrides value of REANA_WORKON environment variable.') @add_access_token_options @click.pass_context -@with_api_client def upload_files(ctx, workflow, filenames, access_token): """Upload files and directories to workflow workspace.""" logging.debug('command: {}'.format(ctx.command_path.replace(" ", "."))) @@ -241,10 +251,9 @@ def upload_files(ctx, workflow, filenames, access_token): if workflow: for filename in filenames: try: - response = ctx.obj.client.\ - upload_to_server(workflow, - filename, - access_token) + response = upload_to_server(workflow, + filename, + access_token) for file_ in response: if file_.startswith('symlink:'): click.echo( @@ -313,7 +322,6 @@ def upload_files(ctx, workflow, filenames, access_token): 'Overrides value of REANA_WORKON environment variable.') @add_access_token_options @click.pass_context -@with_api_client def delete_files(ctx, workflow, filenames, access_token): """Delete files contained in the workflow workspace.""" logging.debug('command: {}'.format(ctx.command_path.replace(" ", "."))) @@ -329,11 +337,7 @@ def delete_files(ctx, workflow, filenames, access_token): if workflow: for filename in filenames: try: - response = ctx.obj.client.\ - delete_file(workflow, - filename, - access_token) - + response = delete_file(workflow, filename, access_token) freed_space = 0 for file_ in response['deleted']: freed_space += response['deleted'][file_]['size'] diff --git a/reana_client/cli/ping.py b/reana_client/cli/ping.py index 929308d3..e6fd57ad 100644 --- a/reana_client/cli/ping.py +++ b/reana_client/cli/ping.py @@ -12,25 +12,32 @@ import click -from reana_client.decorators import with_api_client +from reana_client.api.client import current_rs_api_client +from reana_client.api.client import ping as rs_ping +from reana_commons.errors import MissingAPIClientConfiguration @click.command('ping', help='Health check REANA server.') @click.pass_context -@with_api_client def ping(ctx): """Health check REANA server.""" try: - logging.info('Connecting to {0}'.format(ctx.obj.client.server_url)) - response = ctx.obj.client.ping() + logging.info('Connecting to {0}'.format( + current_rs_api_client.swagger_spec.api_url)) + response = rs_ping() click.echo(click.style('Connected to {0} - Server is running.'.format( - ctx.obj.client.server_url), fg='green')) + current_rs_api_client.swagger_spec.api_url), fg='green')) logging.debug('Server response:\n{}'.format(response)) + except MissingAPIClientConfiguration as e: + click.secho( + 'REANA client is not connected to any REANA cluster.', fg='red') + except Exception as e: logging.debug(traceback.format_exc()) logging.debug(str(e)) click.echo(click.style( 'Could not connect to the selected ' - 'REANA cluster server at {0}.'.format(ctx.obj.client.server_url), + 'REANA cluster server at {0}.'.format( + current_rs_api_client.swagger_spec.api_url), fg='red'), err=True) diff --git a/reana_client/cli/workflow.py b/reana_client/cli/workflow.py index fddd47af..db3376d1 100644 --- a/reana_client/cli/workflow.py +++ b/reana_client/cli/workflow.py @@ -7,8 +7,8 @@ # under the terms of the MIT License; see LICENSE file for more details. """REANA client workflow related commands.""" -import logging import json +import logging import os import sys import traceback @@ -17,22 +17,25 @@ import click import tablib from jsonschema.exceptions import ValidationError -from reana_commons.utils import click_table_printer +from reana_db.database import Session +from reana_db.models import Workflow +from reana_client.api.client import (create_workflow, current_rs_api_client, + delete_workflow, get_workflow_logs, + get_workflow_parameters, + get_workflow_status, get_workflows, + start_workflow, stop_workflow) +from reana_client.cli.files import upload_files +from reana_client.cli.utils import add_access_token_options from reana_client.config import ERROR_MESSAGES, reana_yaml_default_file_path -from reana_client.decorators import with_api_client from reana_client.utils import (get_workflow_name_and_run_number, is_uuid_v4, - load_reana_spec, workflow_uuid_or_name, + load_reana_spec, validate_cwl_operational_options, + validate_input_parameters, validate_serial_operational_options, - validate_input_parameters) -from reana_client.cli.utils import add_access_token_options -from reana_client.cli.files import upload_files - -from reana_client.cli.files import upload_files - -from reana_db.database import Session -from reana_db.models import Workflow + workflow_uuid_or_name) +from reana_commons.errors import MissingAPIClientConfiguration +from reana_commons.utils import click_table_printer class _WorkflowStatus(Enum): @@ -84,6 +87,14 @@ def workflow_workflows(ctx, _filter, output_format, access_token, for p in ctx.params: logging.debug('{param}: {value}'.format(param=p, value=ctx.params[p])) + try: + _url = current_rs_api_client.swagger_spec.api_url + except MissingAPIClientConfiguration as e: + click.secho( + 'REANA client is not connected to any REANA cluster.', + fg='red', err=True + ) + sys.exit(1) if not access_token: click.echo( click.style(ERROR_MESSAGES['missing_access_token'], @@ -91,7 +102,6 @@ def workflow_workflows(ctx, _filter, output_format, access_token, sys.exit(1) try: - from reana_client.api import get_workflows response = get_workflows(access_token) verbose_headers = ['id', 'user'] headers = ['name', 'run_number', 'created', 'status'] @@ -166,7 +176,6 @@ def workflow_workflows(ctx, _filter, output_format, access_token, "submitting it's contents to REANA server.") @add_access_token_options @click.pass_context -@with_api_client def workflow_create(ctx, file, name, skip_validation, access_token): """Create a REANA compatible workflow from REANA spec file.""" logging.debug('command: {}'.format(ctx.command_path.replace(" ", "."))) @@ -188,10 +197,11 @@ def workflow_create(ctx, file, name, skip_validation, access_token): try: reana_specification = load_reana_spec(click.format_filename(file), skip_validation) - logging.info('Connecting to {0}'.format(ctx.obj.client.server_url)) - response = ctx.obj.client.create_workflow(reana_specification, - name, - access_token) + logging.info('Connecting to {0}'.format( + current_rs_api_client.swagger_spec.api_url)) + response = create_workflow(reana_specification, + name, + access_token) click.echo(click.style(response['workflow_name'], fg='green')) # check if command is called from wrapper command if 'invoked_by_subcommand' in ctx.parent.__dict__: @@ -241,7 +251,6 @@ def workflow_create(ctx, file, name, skip_validation, access_token): 'E.g. --debug (workflow engine - cwl)', ) @click.pass_context -@with_api_client def workflow_start(ctx, workflow, access_token, parameters, options): # noqa: D301 """Start previously created workflow.""" @@ -260,9 +269,7 @@ def workflow_start(ctx, workflow, access_token, if workflow: if parameters or options: try: - response = \ - ctx.obj.client.get_workflow_parameters(workflow, - access_token) + response = get_workflow_parameters(workflow, access_token) if response['type'] == 'cwl': validate_cwl_operational_options( parsed_parameters['operational_options']) @@ -281,10 +288,11 @@ def workflow_start(ctx, workflow, access_token, err=True) try: - logging.info('Connecting to {0}'.format(ctx.obj.client.server_url)) - response = ctx.obj.client.start_workflow(workflow, - access_token, - parsed_parameters) + logging.info('Connecting to {0}'.format( + current_rs_api_client.swagger_spec.api_url)) + response = start_workflow(workflow, + access_token, + parsed_parameters) click.echo( click.style('{} has been started.'.format(workflow), fg='green')) @@ -335,7 +343,6 @@ def workflow_start(ctx, workflow, access_token, count=True, help='Set status information verbosity.') @click.pass_context -@with_api_client def workflow_status(ctx, workflow, _filter, output_format, access_token, verbose): """Get status of previously created workflow.""" @@ -406,8 +413,7 @@ def add_verbose_data_from_response(response, verbose_headers, if workflow: try: - response = ctx.obj.client.get_workflow_status(workflow, - access_token) + response = get_workflow_status(workflow, access_token) headers = ['name', 'run_number', 'created', 'status', 'progress'] verbose_headers = ['id', 'user', 'command'] data = [] @@ -456,7 +462,6 @@ def add_verbose_data_from_response(response, verbose_headers, 'Overrides value of REANA_WORKON environment variable.') @add_access_token_options @click.pass_context -@with_api_client def workflow_logs(ctx, workflow, access_token): """Get workflow logs.""" logging.debug('command: {}'.format(ctx.command_path.replace(" ", "."))) @@ -465,7 +470,7 @@ def workflow_logs(ctx, workflow, access_token): if workflow: try: - response = ctx.obj.client.get_workflow_logs(workflow, access_token) + response = get_workflow_logs(workflow, access_token) click.echo(response) except Exception as e: logging.debug(traceback.format_exc()) @@ -533,7 +538,6 @@ def workflow_validate(ctx, file): 'Overrides value of REANA_WORKON environment variable.') @add_access_token_options @click.pass_context -@with_api_client def workflow_stop(ctx, workflow, force_stop, access_token): """Stop given workflow.""" if not force_stop: @@ -551,9 +555,7 @@ def workflow_stop(ctx, workflow, force_stop, access_token): try: logging.info( 'Sending a request to stop workflow {}'.format(workflow)) - response = ctx.obj.client.stop_workflow(workflow, - force_stop, - access_token) + response = stop_workflow(workflow, force_stop, access_token) click.secho('{} has been stopped.'.format(workflow), fg='green') except Exception as e: logging.debug(traceback.format_exc()) @@ -665,7 +667,6 @@ def workflow_run(ctx, file, filenames, name, skip_validation, 'Overrides value of REANA_WORKON environment variable.') @add_access_token_options @click.pass_context -@with_api_client def workflow_delete(ctx, workflow, all_runs, workspace, hard_delete, access_token): """Delete a workflow run given the workflow name and run number.""" @@ -681,12 +682,13 @@ def workflow_delete(ctx, workflow, all_runs, workspace, if workflow: try: - logging.info('Connecting to {0}'.format(ctx.obj.client.server_url)) - response = ctx.obj.client.delete_workflow(workflow, - all_runs, - hard_delete, - workspace, - access_token) + logging.info('Connecting to {0}'.format( + current_rs_api_client.swagger_spec.api_url)) + response = delete_workflow(workflow, + all_runs, + hard_delete, + workspace, + access_token) if all_runs: message = 'All workflows named \'{}\' have been deleted.'.\ format(workflow.split('.')[0]) @@ -730,7 +732,6 @@ def workflow_delete(ctx, workflow, all_runs, workspace, help="Sets number of context lines for workspace diff output.") @add_access_token_options @click.pass_context -@with_api_client def workflow_diff(ctx, workflow_a, workflow_b, brief, access_token, context_lines): """Show diff between two worklows.""" diff --git a/reana_client/decorators.py b/reana_client/decorators.py deleted file mode 100644 index 22764c68..00000000 --- a/reana_client/decorators.py +++ /dev/null @@ -1,53 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is part of REANA. -# Copyright (C) 2018 CERN. -# -# REANA is free software; you can redistribute it and/or modify it -# under the terms of the MIT License; see LICENSE file for more details. -"""REANA command line decorators.""" - -import logging -import os -import sys - -import click -from click.core import Context - -from reana_client.api import Client - - -def with_api_client(f): - """Decorator to inject the REANA server API client to a Click command.""" - def wrapper(*args, **kwargs): - """Initialize config for API client. - - This decorator should be used right after @click.pass_context since it - is injecting the API client inside the ``ctx`` object. - """ - # Since we require the @click.pass_context decorator to be passed - # before, we know ``ctx`` is the first argument. - ctx = args[0] - if isinstance(ctx, Context): - server_url = os.environ.get('REANA_SERVER_URL', None) - - if not server_url: - click.secho( - 'REANA client is not connected to any REANA cluster.\n' - 'Please set REANA_SERVER_URL environment variable to ' - 'the remote REANA cluster you would like to connect to.\n' - 'For example: export ' - 'REANA_SERVER_URL=https://reana.cern.ch/', - fg='red', - err=True) - sys.exit(1) - - logging.info('REANA server URL ($REANA_SERVER_URL) is: {}' - .format(server_url)) - if not ctx.obj.client: - ctx.obj.client = Client('reana-server') - else: - raise Exception( - 'This decorator should be used after click.pass_context.') - f(*args, **kwargs) - return wrapper diff --git a/setup.py b/setup.py index dc99d6f2..bc826f9a 100644 --- a/setup.py +++ b/setup.py @@ -59,6 +59,7 @@ 'strict-rfc3339==0.7', # FIXME remove once yadage-schemas solves deps. 'tablib>=0.12.1,<0.13', 'webcolors==1.7', # FIXME remove once yadage-schemas solves deps. + 'werkzeug==0.14.1', 'yadage-schemas==0.7.16', ] diff --git a/tests/conftest.py b/tests/conftest.py index ef2c334b..baf8b90a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,28 +11,6 @@ from __future__ import absolute_import, print_function import pytest -from mock import Mock -from pytest_reana.test_utils import make_mock_api_client - -from reana_client.api.client import Client - - -@pytest.fixture() -def mock_base_api_client(): - """Create mocked api client.""" - def _make_mock_api_client(status_code=200, - response=None, - component='reana-server'): - mock_http_response, mock_response = Mock(), Mock() - mock_http_response.status_code = status_code - mock_http_response.raw_bytes = str(response).encode() - mock_response = response - reana_server_client = make_mock_api_client( - component)(mock_response, mock_http_response) - reana_client_server_api = Client(component) - reana_client_server_api._client = reana_server_client - return reana_client_server_api - return _make_mock_api_client @pytest.fixture() diff --git a/tests/test_cli_files.py b/tests/test_cli_files.py index a69dd099..8cfd52c9 100644 --- a/tests/test_cli_files.py +++ b/tests/test_cli_files.py @@ -13,8 +13,10 @@ import os from click.testing import CliRunner +from mock import Mock, patch +from pytest_reana.test_utils import make_mock_api_client -from reana_client.cli import Config, cli +from reana_client.cli import cli def test_list_files_server_not_reachable(): @@ -36,7 +38,7 @@ def test_list_files_server_no_token(): assert message in result.output -def test_list_files_ok(mock_base_api_client): +def test_list_files_ok(): """Test list workflow workspace files successfull.""" status_code = 200 response = [ @@ -46,125 +48,144 @@ def test_list_files_ok(mock_base_api_client): "size": 0 } ] - reana_token = '000000' env = {'REANA_SERVER_URL': 'localhost'} - mocked_api_client = mock_base_api_client(status_code, - response, - 'reana-server') - config = Config(mocked_api_client) + mock_http_response, mock_response = Mock(), Mock() + mock_http_response.status_code = status_code + mock_response = response + reana_token = '000000' runner = CliRunner(env=env) - result = runner.invoke( - cli, - ['list', '-at', reana_token, '--workflow', 'mytest.1', '--json'], - obj=config - ) - json_response = json.loads(result.output) - assert result.exit_code == 0 - assert isinstance(json_response, list) - assert len(json_response) == 1 - assert json_response[0]['name'] in response[0]['name'] - - -def test_download_file(mock_base_api_client): + with runner.isolation(): + with patch( + "reana_client.api.client.current_rs_api_client", + make_mock_api_client('reana-server')(mock_response, + mock_http_response)): + result = runner.invoke( + cli, ['list', '-at', reana_token, '--workflow', 'mytest.1', + '--json']) + json_response = json.loads(result.output) + assert result.exit_code == 0 + assert isinstance(json_response, list) + assert len(json_response) == 1 + assert json_response[0]['name'] in response[0]['name'] + + +def test_download_file(): """Test file downloading.""" status_code = 200 - reana_token = '000000' - env = {'REANA_SERVER_URL': 'localhost'} response = 'Content of file to download' - response_md5 = hashlib.md5(response.encode('utf-8')).hexdigest() + env = {'REANA_SERVER_URL': 'localhost'} + mock_http_response = Mock() + mock_http_response.status_code = status_code + mock_http_response.raw_bytes = str(response).encode() + mock_response = response + reana_token = '000000' + response_md5 = hashlib.md5(mock_response.encode('utf-8')).hexdigest() file = 'dummy_file.txt' message = 'File {0} downloaded to'.format(file) - mocked_api_client = mock_base_api_client(status_code, - response, - 'reana-server') - config = Config(mocked_api_client) runner = CliRunner(env=env) - result = runner.invoke( - cli, - ['download', '-at', reana_token, '--workflow', 'mytest.1', file], - obj=config - ) - assert result.exit_code == 0 - assert os.path.isfile(file) is True - file_md5 = hashlib.md5(open(file, 'rb').read()).hexdigest() - assert file_md5 == response_md5 - assert message in result.output - os.remove(file) - - -def test_upload_file(mock_base_api_client, create_yaml_workflow_schema): + with runner.isolation(): + with patch( + "reana_client.api.client.current_rs_api_client", + make_mock_api_client('reana-server')(mock_response, + mock_http_response)): + result = runner.invoke( + cli, ['download', '-at', reana_token, '--workflow', 'mytest.1', + file] + ) + assert result.exit_code == 0 + assert os.path.isfile(file) is True + file_md5 = hashlib.md5(open(file, 'rb').read()).hexdigest() + assert file_md5 == response_md5 + assert message in result.output + os.remove(file) + + +def test_upload_file(create_yaml_workflow_schema): """Test upload file.""" status_code = 200 reana_token = '000000' - env = {'REANA_SERVER_URL': 'localhost'} file = 'file.txt' response = [file] + env = {'REANA_SERVER_URL': 'localhost'} message = 'was successfully uploaded.' - mocked_api_client = mock_base_api_client(status_code, - response, - 'reana-server') - config = Config(mocked_api_client) + mock_http_response = Mock() + mock_http_response.status_code = status_code + mock_http_response.raw_bytes = str(response).encode() + mock_response = response runner = CliRunner(env=env) - with runner.isolated_filesystem(): - with open(file, 'w') as f: - f.write('test') - with open('reana.yaml', 'w') as reana_schema: - reana_schema.write(create_yaml_workflow_schema) - result = runner.invoke( - cli, - ['upload', '-at', reana_token, '--workflow', 'mytest.1', file], - obj=config - ) - assert result.exit_code == 0 - assert message in result.output - - -def test_delete_file(mock_base_api_client): + with runner.isolation(): + with patch( + "reana_client.api.client.current_rs_api_client", + make_mock_api_client('reana-server')(mock_response, + mock_http_response)): + with runner.isolated_filesystem(): + with open(file, 'w') as f: + f.write('test') + with open('reana.yaml', 'w') as reana_schema: + reana_schema.write(create_yaml_workflow_schema) + result = runner.invoke( + cli, ['upload', '-at', reana_token, '--workflow', + 'mytest.1', file] + ) + assert result.exit_code == 0 + assert message in result.output + + +def test_delete_file(): """Test delete file.""" status_code = 200 reana_token = '000000' - env = {'REANA_SERVER_URL': 'localhost'} filename1 = 'file1' filename2 = 'problematic_file' filename2_error_message = '{} could not be deleted.'.format(filename2) response = {'deleted': {filename1: {'size': 19}}, 'failed': {filename2: {'error': filename2_error_message}}} message1 = 'file1 was successfully deleted' - mocked_api_client = mock_base_api_client(status_code, - response, - 'reana-server') - config = Config(mocked_api_client) + mock_http_response = Mock() + mock_http_response.status_code = status_code + mock_http_response.raw_bytes = str(response).encode() + mock_response = response + env = {'REANA_SERVER_URL': 'localhost'} runner = CliRunner(env=env) - with runner.isolated_filesystem(): - result = runner.invoke( - cli, - ['remove', '-at', reana_token, - '--workflow', 'mytest.1', filename1], - obj=config - ) - assert result.exit_code == 0 - assert message1 in result.output - assert filename2_error_message in result.output - - -def test_delete_non_existing_file(mock_base_api_client): + with runner.isolation(): + with patch( + "reana_client.api.client.current_rs_api_client", + make_mock_api_client('reana-server')(mock_response, + mock_http_response)): + with runner.isolated_filesystem(): + result = runner.invoke( + cli, + ['remove', '-at', reana_token, + '--workflow', 'mytest.1', filename1] + ) + assert result.exit_code == 0 + assert message1 in result.output + assert filename2_error_message in result.output + + +def test_delete_non_existing_file(): """Test delete non existing file.""" status_code = 200 reana_token = '000000' - env = {'REANA_SERVER_URL': 'localhost'} filename = 'file11' response = {'deleted': {}, 'failed': {}} message = '{} did not match any existing file.'.format(filename) - mocked_api_client = mock_base_api_client(status_code, - response, - 'reana-server') - config = Config(mocked_api_client) + mock_http_response = Mock() + mock_http_response.status_code = status_code + mock_http_response.raw_bytes = str(response).encode() + mock_response = response + env = {'REANA_SERVER_URL': 'localhost'} runner = CliRunner(env=env) - with runner.isolated_filesystem(): - result = runner.invoke( - cli, - ['remove', '-at', reana_token, '--workflow', 'mytest.1', filename], - obj=config - ) - assert result.exit_code == 0 - assert message in result.output + with runner.isolation(): + with patch( + "reana_client.api.client.current_rs_api_client", + make_mock_api_client('reana-server')(mock_response, + mock_http_response)): + with runner.isolated_filesystem(): + result = runner.invoke( + cli, + ['remove', '-at', reana_token, '--workflow', 'mytest.1', + filename] + ) + assert result.exit_code == 0 + assert message in result.output diff --git a/tests/test_cli_ping.py b/tests/test_cli_ping.py index c5caaa65..788c7ea1 100644 --- a/tests/test_cli_ping.py +++ b/tests/test_cli_ping.py @@ -9,8 +9,10 @@ """REANA client ping tests.""" from click.testing import CliRunner +from mock import Mock, patch +from pytest_reana.test_utils import make_mock_api_client -from reana_client.cli import Config, cli +from reana_client.cli import cli def test_ping_server_not_set(): @@ -30,16 +32,21 @@ def test_ping_server_not_reachable(): assert message in result.output -def test_ping_ok(mock_base_api_client): +def test_ping_ok(): """Test ping server is set and reachable.""" env = {'REANA_SERVER_URL': 'localhost'} status_code = 200 response = {"status": 200, "message": "OK"} - mocked_api_client = mock_base_api_client(status_code, - response, - 'reana-server') - config = Config(mocked_api_client) + mock_http_response, mock_response = Mock(), Mock() + mock_http_response.status_code = status_code + mock_response = response runner = CliRunner(env=env) - result = runner.invoke(cli, ['ping'], obj=config) - message = 'Server is running' - assert message in result.output + with runner.isolation(): + with patch( + "reana_client.api.client.current_rs_api_client", + make_mock_api_client('reana-server')(mock_response, + mock_http_response)): + + result = runner.invoke(cli, ['ping']) + message = 'Server is running' + assert message in result.output diff --git a/tests/test_cli_workflows.py b/tests/test_cli_workflows.py index 948ad6e5..655bbbda 100644 --- a/tests/test_cli_workflows.py +++ b/tests/test_cli_workflows.py @@ -11,9 +11,10 @@ import json from click.testing import CliRunner -from mock import patch +from mock import Mock, patch +from pytest_reana.test_utils import make_mock_api_client -from reana_client.cli import Config, cli +from reana_client.cli import cli def test_workflows_server_not_connected(): @@ -30,12 +31,12 @@ def test_workflows_no_token(): env = {'REANA_SERVER_URL': 'localhost'} runner = CliRunner(env=env) result = runner.invoke(cli, ['workflows']) - message = 'Please provide your access token' + message = 'Please provide your access token by using the -at' assert result.exit_code == 1 assert message in result.output -def test_workflows_server_ok(mock_base_api_client): +def test_workflows_server_ok(): """Test workflows command when server is reachable.""" response = [ { @@ -47,20 +48,24 @@ def test_workflows_server_ok(mock_base_api_client): } ] status_code = 200 - mocked_api_client = mock_base_api_client(status_code, - response, - 'reana-server') - config = Config(mocked_api_client) + mock_http_response, mock_response = Mock(), Mock() + mock_http_response.status_code = status_code + mock_response = response env = {'REANA_SERVER_URL': 'localhost', 'REANA_WORKON': 'mytest.1'} reana_token = '000000' runner = CliRunner(env=env) - result = runner.invoke(cli, ['workflows', '-at', reana_token], obj=config) - message = 'RUN_NUMBER' - assert result.exit_code == 0 - assert message in result.output + with runner.isolation(): + with patch( + "reana_client.api.client.current_rs_api_client", + make_mock_api_client('reana-server')(mock_response, + mock_http_response)): + result = runner.invoke(cli, ['workflows', '-at', reana_token]) + message = 'RUN_NUMBER' + assert result.exit_code == 0 + assert message in result.output -def test_workflows_valid_json(mock_base_api_client): +def test_workflows_valid_json(): """Test workflows command with --json and -v flags.""" response = [ { @@ -72,26 +77,30 @@ def test_workflows_valid_json(mock_base_api_client): } ] status_code = 200 - mocked_api_client = mock_base_api_client(status_code, - response, - 'reana-server') - config = Config(mocked_api_client) + mock_http_response, mock_response = Mock(), Mock() + mock_http_response.status_code = status_code + mock_response = response env = {'REANA_SERVER_URL': 'localhost'} reana_token = '000000' runner = CliRunner(env=env) - result = runner.invoke(cli, - ['workflows', '-v', '-at', reana_token, '--json'], - obj=config) - json_response = json.loads(result.output) - assert result.exit_code == 0 - assert isinstance(json_response, list) - assert len(json_response) == 1 - assert 'name' in json_response[0] - assert 'run_number' in json_response[0] - assert 'created' in json_response[0] - assert 'status' in json_response[0] - assert 'id' in json_response[0] - assert 'user' in json_response[0] + with runner.isolation(): + with patch( + "reana_client.api.client.current_rs_api_client", + make_mock_api_client('reana-server')(mock_response, + mock_http_response)): + result = runner.invoke(cli, + ['workflows', '-v', '-at', + reana_token, '--json']) + json_response = json.loads(result.output) + assert result.exit_code == 0 + assert isinstance(json_response, list) + assert len(json_response) == 1 + assert 'name' in json_response[0] + assert 'run_number' in json_response[0] + assert 'created' in json_response[0] + assert 'status' in json_response[0] + assert 'id' in json_response[0] + assert 'user' in json_response[0] def test_workflow_create_failed(): @@ -103,8 +112,7 @@ def test_workflow_create_failed(): assert result.exit_code == 2 -def test_workflow_create_successful(mock_base_api_client, - create_yaml_workflow_schema): +def test_workflow_create_successful(create_yaml_workflow_schema): """Test workflow create when creation is successfull.""" status_code = 201 response = { @@ -114,24 +122,27 @@ def test_workflow_create_successful(mock_base_api_client, } env = {'REANA_SERVER_URL': 'localhost'} reana_token = '000000' - mocked_api_client = mock_base_api_client(status_code, - response, - 'reana-server') - config = Config(mocked_api_client) + mock_http_response, mock_response = Mock(), Mock() + mock_http_response.status_code = status_code + mock_response = response runner = CliRunner(env=env) - with runner.isolated_filesystem(): - with open('reana.yaml', 'w') as f: - f.write(create_yaml_workflow_schema) - result = runner.invoke( - cli, - ['create', '-at', reana_token, '--skip-validation'], - obj=config - ) - assert result.exit_code == 0 - assert response["workflow_name"] in result.output + with runner.isolation(): + with patch( + "reana_client.api.client.current_rs_api_client", + make_mock_api_client('reana-server')(mock_response, + mock_http_response)): + with runner.isolated_filesystem(): + with open('reana.yaml', 'w') as f: + f.write(create_yaml_workflow_schema) + result = runner.invoke( + cli, + ['create', '-at', reana_token, '--skip-validation'] + ) + assert result.exit_code == 0 + assert response["workflow_name"] in result.output -def test_workflow_start_successful(mock_base_api_client): +def test_workflow_start_successful(): """Test workflow start when creation is successfull.""" response = { "status": "created", @@ -141,21 +152,24 @@ def test_workflow_start_successful(mock_base_api_client): "user": "00000000-0000-0000-0000-000000000000" } status_code = 200 - mocked_api_client = mock_base_api_client(status_code, - response, - 'reana-server') - config = Config(mocked_api_client) - env = {'REANA_SERVER_URL': 'localhost'} reana_token = '000000' message = 'mytest.1 has been started' + mock_http_response = Mock() + mock_http_response.status_code = status_code + mock_response = response + env = {'REANA_SERVER_URL': 'localhost'} runner = CliRunner(env=env) - result = runner.invoke( - cli, - ['start', '-at', reana_token, '-w', response["workflow_name"]], - obj=config - ) - assert result.exit_code == 0 - assert message in result.output + with runner.isolation(): + with patch( + "reana_client.api.client.current_rs_api_client", + make_mock_api_client('reana-server')(mock_response, + mock_http_response)): + result = runner.invoke( + cli, + ['start', '-at', reana_token, '-w', response["workflow_name"]] + ) + assert result.exit_code == 0 + assert message in result.output def test_workflows_validate(create_yaml_workflow_schema): @@ -173,7 +187,7 @@ def test_workflows_validate(create_yaml_workflow_schema): assert message in result.output -def test_get_workflow_status_ok(mock_base_api_client): +def test_get_workflow_status_ok(): """Test workflow status.""" status_code = 200 response = { @@ -193,23 +207,27 @@ def test_get_workflow_status_ok(mock_base_api_client): 'status': 'running', 'user': '00000000-0000-0000-0000-000000000000' } - mocked_api_client = mock_base_api_client(status_code, - response, - 'reana-server') - config = Config(mocked_api_client) env = {'REANA_SERVER_URL': 'localhost'} + mock_http_response, mock_response = Mock(), Mock() + mock_http_response.status_code = status_code + mock_response = response reana_token = '000000' runner = CliRunner(env=env) - result = runner.invoke( - cli, - ['status', '-at', reana_token, '--json', '-v', '-w', response['name']], - obj=config - ) - json_response = json.loads(result.output) - assert result.exit_code == 0 - assert isinstance(json_response, list) - assert len(json_response) == 1 - assert json_response[0]['name'] in response['name'] + with runner.isolation(): + with patch( + "reana_client.api.client.current_rs_api_client", + make_mock_api_client('reana-server')(mock_response, + mock_http_response)): + result = runner.invoke( + cli, + ['status', '-at', reana_token, '--json', '-v', '-w', + response['name']] + ) + json_response = json.loads(result.output) + assert result.exit_code == 0 + assert isinstance(json_response, list) + assert len(json_response) == 1 + assert json_response[0]['name'] in response['name'] @patch('reana_client.cli.workflow.workflow_create') @@ -234,7 +252,7 @@ def test_run(workflow_start_mock, assert workflow_start_mock.called is True -def test_workflow_input_parameters(mock_base_api_client): +def test_workflow_input_parameters(): """Test if not existing input parameters from CLI are applied.""" status_code = 200 response = {'id': 'd9304bdf-0d19-45d9-ae87-d5fd18059193', @@ -245,17 +263,21 @@ def test_workflow_input_parameters(mock_base_api_client): 'outputfile': 'results/greetings.txt', 'sleeptime': 2}} env = {'REANA_SERVER_URL': 'localhost'} - mocked_api_client = mock_base_api_client(status_code, - response, - 'reana-server') - parameter = "Debug" - expected_message = '{0}, is not in reana.yaml'.format(parameter) - config = Config(mocked_api_client) + mock_http_response, mock_response = Mock(), Mock() + mock_http_response.status_code = status_code + mock_response = response reana_token = '000000' runner = CliRunner(env=env) - result = runner.invoke( - cli, - ['start', '-at', reana_token, '-w workflow.19', - '-p {0}=True'.format(parameter)], - obj=config) - assert expected_message in result.output + with runner.isolation(): + with patch( + "reana_client.api.client.current_rs_api_client", + make_mock_api_client('reana-server')(mock_response, + mock_http_response)): + parameter = "Debug" + expected_message = '{0}, is not in reana.yaml'.format(parameter) + result = runner.invoke( + cli, + ['start', '-at', reana_token, '-w workflow.19', + '-p {0}=True'.format(parameter)] + ) + assert expected_message in result.output