django_airavata/apps/api/output_views.py (251 lines of code) (raw):

import collections import inspect import json import logging import os from functools import partial import nbformat import papermill as pm from airavata.model.application.io.ttypes import DataType from airavata_django_portal_sdk import user_storage from django.conf import settings from nbconvert import HTMLExporter logger = logging.getLogger(__name__) BASE_DIR = os.path.dirname(os.path.abspath(__file__)) # This is populated by apps.ApiConfig.ready() OUTPUT_VIEW_PROVIDERS = {} class DefaultViewProvider: display_type = 'default' immediate = False name = "Default" def generate_data( self, request, experiment_output, experiment, output_file=None, **kwargs): return { } class ParameterizedNotebookViewProvider: display_type = 'notebook' name = "Example Parameterized Notebook View" # test_output_file = os.path.join(BASE_DIR, "data", "Gaussian.log") def generate_data(self, request, experiment_output, experiment, output_file=None, output_dir=None): # use papermill to generate the output notebook output_file_path = os.path.realpath(output_file.name) pm.execute_notebook( os.path.join(BASE_DIR, "path", "to", "notebook.ipynb"), # TODO: use TemporaryFile instead '/tmp/output.ipynb', parameters=dict( experiment_output={}, experiment={}, output_file=output_file_path, output_dir=output_dir ) ) # TODO: convert the output notebook into html format output_notebook = nbformat.read('/tmp/output.ipynb', as_version=4) html_exporter = HTMLExporter() (body, resources) = html_exporter.from_notebook_node(output_notebook) # TODO: return the HTML output as the output key return { 'output': body } DEFAULT_VIEW_PROVIDERS = { 'default': DefaultViewProvider() } def get_output_views(request, experiment, application_interface=None): output_views = {} for output in experiment.experimentOutputs: output_views[output.name] = [] output_view_provider_ids = _get_output_view_providers( output, application_interface) for output_view_provider_id in output_view_provider_ids: output_view_provider = None if output_view_provider_id in DEFAULT_VIEW_PROVIDERS: output_view_provider = DEFAULT_VIEW_PROVIDERS[ output_view_provider_id] elif output_view_provider_id in OUTPUT_VIEW_PROVIDERS: output_view_provider = OUTPUT_VIEW_PROVIDERS[ output_view_provider_id] else: logger.warning("Unable to find output view provider with " "name '{}'".format(output_view_provider_id)) if output_view_provider is not None: view_config = { 'provider-id': output_view_provider_id, 'display-type': output_view_provider.display_type, 'name': getattr(output_view_provider, 'name', output_view_provider_id), } if getattr(output_view_provider, 'immediate', False): # Immediately call generate_data function data = _generate_data( request, output_view_provider, output, experiment) view_config['data'] = data else: view_config['data'] = {} output_views[output.name].append(view_config) return output_views def _get_output_view_provider(output_view_provider_id): if output_view_provider_id in DEFAULT_VIEW_PROVIDERS: return DEFAULT_VIEW_PROVIDERS[output_view_provider_id] elif output_view_provider_id in OUTPUT_VIEW_PROVIDERS: return OUTPUT_VIEW_PROVIDERS[output_view_provider_id] def _get_output_view_providers(experiment_output, application_interface): output_view_providers = [] logger.debug("experiment_output={}".format(experiment_output)) if experiment_output.metaData: try: output_metadata = json.loads(experiment_output.metaData) logger.debug("output_metadata={}".format(output_metadata)) if 'output-view-providers' in output_metadata: output_view_providers.extend( output_metadata['output-view-providers']) except Exception: logger.exception( "Failed to parse metadata for output {}".format( experiment_output.name)) # Add in any output view providers defined on the application interface if application_interface is not None: app_output_view_providers = _get_application_output_view_providers( application_interface, experiment_output.name) for view_provider in app_output_view_providers: if view_provider not in output_view_providers: output_view_providers.append(view_provider) if 'default' not in output_view_providers: output_view_providers.insert(0, 'default') return output_view_providers def _get_application_output_view_providers(application_interface, output_name): app_output = [o for o in application_interface.applicationOutputs if o.name == output_name] if len(app_output) == 1: logger.debug("{}: {}".format(output_name, app_output)) app_output = app_output[0] else: return [] if app_output.metaData: try: output_metadata = json.loads(app_output.metaData) if 'output-view-providers' in output_metadata: return output_metadata['output-view-providers'] except Exception: logger.exception( "Failed to parse metadata for output {}".format( app_output.name)) return [] def generate_data(request, output_view_provider_id, experiment_output_name, experiment_id, test_mode=False, **kwargs): output_view_provider = _get_output_view_provider(output_view_provider_id) # TODO if output_view_provider is None, return 404 experiment = request.airavata_client.getExperiment( request.authz_token, experiment_id) experiment_output = [o for o in experiment.experimentOutputs if o.name == experiment_output_name] # TODO: handle experiment_output not found by name experiment_output = experiment_output[0] # TODO: add experiment_output_dir # convert the extra/interactive arguments to appropriate types kwargs = _convert_params_to_type(output_view_provider, kwargs) return _generate_data(request, output_view_provider, experiment_output, experiment, test_mode=test_mode, **kwargs) def _generate_data(request, output_view_provider, experiment_output, experiment, test_mode=False, **kwargs): output_files = [] # test_mode can only be used in DEBUG=True mode if test_mode and settings.DEBUG: test_output_file = getattr(output_view_provider, 'test_output_file', None) if test_output_file is None: raise Exception(f"test_output_file is not set on {output_view_provider}") logger.info(f"Using {test_output_file} instead of regular output file") output_file = open(test_output_file, 'rb') output_files.append(output_file) elif (experiment_output.value and experiment_output.type in (DataType.URI, DataType.URI_COLLECTION, DataType.STDOUT, DataType.STDERR) and experiment_output.value.startswith("airavata-dp")): data_product_uris = experiment_output.value.split(",") data_products = map(lambda dpid: request.airavata_client.getDataProduct(request.authz_token, dpid), data_product_uris) for data_product in data_products: if user_storage.exists(request, data_product): output_file = user_storage.open_file(request, data_product) output_files.append(output_file) generate_data_func = output_view_provider.generate_data method_sig = inspect.signature(generate_data_func) if 'output_files' in method_sig.parameters: generate_data_func = partial(generate_data_func, output_files=output_files) # TODO: convert experiment and experiment_output to dict/JSON data = generate_data_func(request, experiment_output, experiment, output_file=output_files[0] if len(output_files) > 0 else None, **kwargs) _process_interactive_params(data) return data def _process_interactive_params(data): if 'interactive' in data: _convert_options(data) for param in data['interactive']: if 'type' not in param: param['type'] = _infer_interactive_param_type(param) # integer type implicitly has a step size of 1 if param['type'] == "integer" and 'step' not in param: param['step'] = 1 def _convert_options(data): """Convert interactive options to explicit text/value dicts.""" for param in data['interactive']: if 'options' in param and isinstance(param['options'][0], str): param['options'] = _convert_options_strings(param['options']) elif 'options' in param and isinstance( param['options'][0], collections.Sequence): param['options'] = _convert_options_sequences(param['options']) def _convert_options_strings(options): return [{"text": o, "value": o} for o in options] def _convert_options_sequences(options): return [{"text": o[0], "value": o[1]} for o in options] def _infer_interactive_param_type(param): v = param['value'] # Boolean test must come first since bools are also integers if isinstance(v, bool): return "boolean" elif isinstance(v, float): return "float" elif isinstance(v, int): return "integer" elif isinstance(v, str): return "string" def _convert_params_to_type(output_view_provider, params): method_sig = inspect.signature(output_view_provider.generate_data) method_params = method_sig.parameters # Special query parameter _meta holds type information for interactive # parameters (will only be present if there are interactive parameters) meta = json.loads(params.pop("_meta", "{}")) for k, v in params.items(): meta_type = meta[k]['type'] if k in meta else None default_value = None if (k in method_params and method_params[k].default is not inspect.Parameter.empty and method_params[k].default is not None): default_value = method_params[k].default # TODO: handle lists? # Handle boolean and numeric values, converting from string if meta_type == 'boolean' or isinstance(default_value, bool): params[k] = v == "true" elif meta_type == 'float' or isinstance(default_value, float): params[k] = float(v) elif meta_type == 'integer' or isinstance(default_value, int): params[k] = int(v) elif meta_type == 'string' or isinstance(default_value, str): params[k] = v else: logger.warning( f"Unrecognized type for parameter {k}: " f"meta_type={meta_type}, default_value={default_value}") return params