def test_execute_process_no_error_not_required_params(self): """ Optional parameters for execute job shouldn't raise an error if omitted, and should resolve to default values if any was specified. """ # get basic mock/data templates name = fully_qualified_name(self) execute_mock_data_tests = list() for i in range(2): mock_execute = mocked_process_job_runner("job-{}-{}".format( name, i)) data_execute = self.get_process_execute_template("{}-{}".format( name, i)) execute_mock_data_tests.append((mock_execute, data_execute)) # apply modifications for testing execute_mock_data_tests[0][1].pop( "inputs" ) # no inputs is valid (although can be required for WPS process) execute_mock_data_tests[0][1]["outputs"][0].pop( "transmissionMode") # should resolve to default value for mock_execute, data_execute in execute_mock_data_tests: with contextlib.ExitStack() as stack: for exe in mock_execute: stack.enter_context(exe) path = "/processes/{}/jobs".format( self.process_public.identifier) resp = self.app.post_json(path, params=data_execute, headers=self.json_headers) assert resp.status_code == 201, "Expected job submission without inputs created without error."
def test_execute_process_missing_required_params(self): execute_data = self.get_process_execute_template( fully_qualified_name(self)) # remove components for testing different cases execute_data_tests = [deepcopy(execute_data) for _ in range(7)] execute_data_tests[0].pop("outputs") execute_data_tests[1].pop("mode") execute_data_tests[2].pop("response") execute_data_tests[3]["mode"] = "random" execute_data_tests[4]["response"] = "random" execute_data_tests[5]["inputs"] = [{ "test_input": "test_value" }] # bad format execute_data_tests[6]["outputs"] = [{ "id": "test_output", "transmissionMode": "random" }] uri = "/processes/{}/jobs".format(self.process_public.identifier) for i, exec_data in enumerate(execute_data_tests): resp = self.app.post_json(uri, params=exec_data, headers=self.json_headers, expect_errors=True) msg = "Failed with test variation '{}' with value '{}'." assert resp.status_code in [400, 422], msg.format(i, resp.status_code) assert resp.content_type == CONTENT_TYPE_APP_JSON, msg.format( i, resp.content_type)
def test_execute_process_mode_sync_not_supported(self): execute_data = self.get_process_execute_template( fully_qualified_name(self)) execute_data["mode"] = EXECUTE_MODE_SYNC uri = "/processes/{}/jobs".format(self.process_public.identifier) resp = self.app.post_json(uri, params=execute_data, headers=self.json_headers, expect_errors=True) assert resp.status_code == 501 assert resp.content_type == CONTENT_TYPE_APP_JSON
def test_execute_process_transmission_mode_value_not_supported(self): execute_data = self.get_process_execute_template( fully_qualified_name(self)) execute_data["outputs"][0][ "transmissionMode"] = EXECUTE_TRANSMISSION_MODE_VALUE uri = "/processes/{}/jobs".format(self.process_public.identifier) with contextlib.ExitStack() as stack_exec: for mock_exec in mocked_execute_process(): stack_exec.enter_context(mock_exec) resp = self.app.post_json(uri, params=execute_data, headers=self.json_headers, expect_errors=True) assert resp.status_code == 501 assert resp.content_type == CONTENT_TYPE_APP_JSON
def save_log(self, errors=None, # type: Optional[Union[str, List[WPSException]]] logger=None, # type: Optional[Logger] message=None, # type: Optional[str] level=INFO, # type: int ): # type: (...) -> None """ Logs the specified error and/or message, and adds the log entry to the complete job log. For each new log entry, additional :class:`Job` properties are added according to :meth:`Job._get_log_msg` and the format defined by :func:`get_job_log_msg`. :param errors: An error message or a list of WPS exceptions from which to log and save generated message stack. :param logger: An additional :class:`Logger` for which to propagate logged messages on top saving them to the job. :param message: Explicit string to be logged, otherwise use the current :py:attr:`Job.status_message` is used. :param level: Logging level to apply to the logged ``message``. This parameter is ignored if ``errors`` are logged. .. note:: The job object is updated with the log but still requires to be pushed to database to actually persist it. """ if isinstance(errors, str): log_msg = [(ERROR, self._get_log_msg(message))] self.exceptions.append(errors) elif isinstance(errors, list): log_msg = [(ERROR, self._get_log_msg("{0.text} - code={0.code} - locator={0.locator}".format(error))) for error in errors] self.exceptions.extend([{ "Code": error.code, "Locator": error.locator, "Text": error.text } for error in errors]) else: log_msg = [(level, self._get_log_msg(message))] for lvl, msg in log_msg: fmt_msg = get_log_fmt() % dict(asctime=now().strftime(get_log_date_fmt()), levelname=getLevelName(lvl), name=fully_qualified_name(self), message=msg) if len(self.logs) == 0 or self.logs[-1] != fmt_msg: self.logs.append(fmt_msg) if logger: logger.log(lvl, msg)
def test_execute_process_language(self): uri = "/processes/{}/jobs".format(self.process_public.identifier) data = self.get_process_execute_template() task = "job-{}".format(fully_qualified_name(self)) mock_execute = mocked_process_job_runner(task) with contextlib.ExitStack() as stack: for exe in mock_execute: stack.enter_context(exe) headers = self.json_headers.copy() headers["Accept-Language"] = "fr-CA" resp = self.app.post_json(uri, params=data, headers=headers) assert resp.status_code == 201, "Error: {}".format(resp.text) try: job = self.job_store.fetch_by_id(resp.json["jobID"]) except JobNotFound: self.fail("Job should have been created and be retrievable.") assert job.id == resp.json["jobID"] assert job.accept_language == "fr-CA"
def test_execute_process_success(self): uri = "/processes/{}/jobs".format(self.process_public.identifier) data = self.get_process_execute_template() task = "job-{}".format(fully_qualified_name(self)) mock_execute = mocked_process_job_runner(task) with contextlib.ExitStack() as stack: for exe in mock_execute: stack.enter_context(exe) resp = self.app.post_json(uri, params=data, headers=self.json_headers) assert resp.status_code == 201, "Error: {}".format(resp.text) assert resp.content_type == CONTENT_TYPE_APP_JSON assert resp.json["location"].endswith(resp.json["jobID"]) assert resp.headers["Location"] == resp.json["location"] try: job = self.job_store.fetch_by_id(resp.json["jobID"]) except JobNotFound: self.fail("Job should have been created and be retrievable.") assert job.id == resp.json["jobID"] assert job.task_id == STATUS_ACCEPTED # temporary value until processed by celery
def test_execute_process_dont_cast_one_of(self): """ When validating the schema for OneOf values, don't cast the result to the first valid schema. """ # get basic mock/data templates name = fully_qualified_name(self) execute_mock_data_tests = list() mock_execute = mocked_process_job_runner("job-{}".format(name)) data_execute = self.get_process_execute_template("100") execute_mock_data_tests.append((mock_execute, data_execute)) with contextlib.ExitStack() as stack: for exe in mock_execute: stack.enter_context(exe) path = "/processes/{}/jobs".format(self.process_public.identifier) resp = self.app.post_json(path, params=data_execute, headers=self.json_headers) assert resp.status_code == 201, "Expected job submission without inputs created without error." job = self.job_store.fetch_by_id(resp.json["jobID"]) assert job.inputs[0][ "data"] == "100" # not cast to float or integer
def fully_qualified_test_process_name(self): return fully_qualified_name(self).replace(".", "-")
def ows_response_tween_factory_excview(handler, registry): # noqa: F811 """ Tween factory which produces a tween which transforms common exceptions into OWS specific exceptions. """ return lambda request: ows_response_tween(request, handler) def ows_response_tween_factory_ingress(handler, registry): # noqa: F811 """ Tween factory which produces a tween which transforms common exceptions into OWS specific exceptions. """ def handle_ows_tween(request): # because the EXCVIEW will also wrap any exception raised that should before be handled by OWS response # to allow conversions to occur, use a flag that will re-raise the result setattr(handler, OWS_TWEEN_HANDLED, True) return ows_response_tween(request, handler) return handle_ows_tween # names must differ to avoid conflicting configuration error OWS_RESPONSE_EXCVIEW = fully_qualified_name(ows_response_tween_factory_excview) OWS_RESPONSE_INGRESS = fully_qualified_name(ows_response_tween_factory_ingress) def includeme(config): # using 'INGRESS' to run `weaver.wps_restapi.api` views that fix HTTP code before OWS response config.add_tween(OWS_RESPONSE_INGRESS, under=INGRESS) # using 'EXCVIEW' to run over any other 'valid' exception raised to adjust formatting and log config.add_tween(OWS_RESPONSE_EXCVIEW, over=EXCVIEW)
def get_processes(request): # type: (PyramidRequest) -> AnyViewResponse """ List registered processes (GetCapabilities). Optionally list both local and provider processes. """ try: params = sd.GetProcessesQuery().deserialize(request.params) except colander.Invalid as ex: raise HTTPBadRequest(json={ "code": "ProcessInvalidParameter", "description": "Process query parameters failed validation.", "error": colander.Invalid.__name__, "cause": str(ex), "value": repr_json(ex.value or dict(request.params), force_string=False), }) detail = asbool(params.get("detail", True)) ignore = asbool(params.get("ignore", True)) try: # get local processes and filter according to schema validity # (previously deployed process schemas can become invalid because of modified schema definitions results = get_processes_filtered_by_valid_schemas(request) processes, invalid_processes, paging, with_providers, total_processes = results if invalid_processes: raise HTTPServiceUnavailable( "Previously deployed processes are causing invalid schema integrity errors. " f"Manual cleanup of following processes is required: {invalid_processes}" ) body = {"processes": processes if detail else [get_any_id(p) for p in processes]} # type: JSON if not with_providers: paging = {"page": paging.get("page"), "limit": paging.get("limit")} # remove other params body.update(paging) else: paging = {} # disable to remove paging-related links try: body["links"] = get_process_list_links(request, paging, total_processes) except IndexError as exc: raise HTTPBadRequest(json={ "description": str(exc), "cause": "Invalid paging parameters.", "error": type(exc).__name__, "value": repr_json(paging, force_string=False) }) # if 'EMS/HYBRID' and '?providers=True', also fetch each provider's processes if with_providers: # param 'check' enforced because must fetch for listing of available processes (GetCapabilities) # when 'ignore' is not enabled, any failing definition should raise any derived 'ServiceException' services = get_provider_services(request, ignore=ignore, check=True) body.update({ "providers": [svc.summary(request, ignore=ignore) if detail else {"id": svc.name} for svc in services] }) invalid_services = [False] * len(services) for i, provider in enumerate(services): # ignore failing parsing of the service description if body["providers"][i] is None: invalid_services[i] = True continue # attempt parsing available processes and ignore again failing items processes = provider.processes(request, ignore=ignore) if processes is None: invalid_services[i] = True continue total_processes += len(processes) body["providers"][i].update({ "processes": processes if detail else [get_any_id(proc) for proc in processes] }) if any(invalid_services): LOGGER.debug("Invalid providers dropped due to failing parsing and ignore query: %s", [svc.name for svc, status in zip(services, invalid_services) if status]) body["providers"] = [svc for svc, ignore in zip(body["providers"], invalid_services) if not ignore] body["total"] = total_processes body["description"] = sd.OkGetProcessesListResponse.description LOGGER.debug("Process listing generated, validating schema...") body = sd.MultiProcessesListing().deserialize(body) return HTTPOk(json=body) except ServiceException as exc: LOGGER.debug("Error when listing provider processes using query parameter raised: [%s]", exc, exc_info=exc) raise HTTPServiceUnavailable(json={ "description": "At least one provider could not list its processes. " "Failing provider errors were requested to not be ignored.", "exception": fully_qualified_name(exc), "error": str(exc) }) except HTTPException: raise except colander.Invalid as exc: raise HTTPBadRequest(json={ "type": "InvalidParameterValue", "title": "Invalid parameter value.", "description": "Submitted request parameters are invalid or could not be processed.", "cause": clean_json_text_body(f"Invalid schema: [{exc.msg or exc!s}]"), "error": exc.__class__.__name__, "value": repr_json(exc.value, force_string=False), })
def execute(self, workflow_inputs, out_dir, expected_outputs): # type: (CWL_RuntimeInputsMap, str, CWL_ExpectedOutputs) -> None """ Execute the core operation of the remote :term:`Process` using the given inputs. The function is expected to monitor the process and update the status. Retrieve the expected outputs and store them in the ``out_dir``. :param workflow_inputs: `CWL` job dict :param out_dir: directory where the outputs must be written :param expected_outputs: expected value outputs as `{'id': 'value'}` """ self.update_status("Preparing process for remote execution.", REMOTE_JOB_PROGRESS_PREPARE, STATUS_RUNNING) self.prepare() self.update_status("Process ready for execute remote process.", REMOTE_JOB_PROGRESS_READY, STATUS_RUNNING) self.update_status("Staging inputs for remote execution.", REMOTE_JOB_PROGRESS_STAGE_IN, STATUS_RUNNING) staged_inputs = self.stage_inputs(workflow_inputs) self.update_status("Preparing inputs/outputs for remote execution.", REMOTE_JOB_PROGRESS_FORMAT_IO, STATUS_RUNNING) expect_outputs = [{"id": output} for output in expected_outputs] process_inputs = self.format_inputs(staged_inputs) process_outputs = self.format_outputs(expect_outputs) try: self.update_status("Executing remote process job.", REMOTE_JOB_PROGRESS_EXECUTE, STATUS_RUNNING) monitor_ref = self.dispatch(process_inputs, process_outputs) self.update_status( "Monitoring remote process job until completion.", REMOTE_JOB_PROGRESS_MONITOR, STATUS_RUNNING) job_success = self.monitor(monitor_ref) if not job_success: raise PackageExecutionError( "Failed dispatch and monitoring of remote process execution." ) except Exception as exc: err_msg = "{0}: {1!s}".format(fully_qualified_name(exc), exc) err_ctx = "Dispatch and monitoring of remote process caused an unhandled error." LOGGER.exception("%s [%s]", err_ctx, err_msg, exc_info=exc) self.update_status( "Running final cleanup operations following failed execution.", REMOTE_JOB_PROGRESS_CLEANUP, STATUS_RUNNING) self.cleanup() raise PackageExecutionError(err_ctx) from exc self.update_status("Retrieving job results definitions.", REMOTE_JOB_PROGRESS_RESULTS, STATUS_RUNNING) results = self.get_results(monitor_ref) self.update_status("Staging job outputs from remote process.", REMOTE_JOB_PROGRESS_STAGE_OUT, STATUS_RUNNING) self.stage_results(results, expected_outputs, out_dir) self.update_status( "Running final cleanup operations before completion.", REMOTE_JOB_PROGRESS_CLEANUP, STATUS_RUNNING) self.cleanup() self.update_status( "Execution of remote process execution completed successfully.", REMOTE_JOB_PROGRESS_COMPLETED, STATUS_SUCCEEDED)