From 67c054d1f4b1885da432c266b515e4251f97489c Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Wed, 28 Aug 2024 18:48:42 +0200 Subject: [PATCH] Implement regex validators for text workflow parameter --- client/src/api/schema/schema.ts | 18 ++- .../components/Workflow/Run/WorkflowRun.vue | 2 +- .../InvocationMessage.vue | 6 + lib/galaxy/schema/invocation.py | 15 +++ lib/galaxy/tool_util/parser/xml.py | 28 ++++- lib/galaxy/tool_util/parser/yaml.py | 15 +++ lib/galaxy/tools/parameters/basic.py | 4 +- lib/galaxy/tools/parameters/validation.py | 76 ++++++------ lib/galaxy/workflow/modules.py | 114 ++++++++++-------- lib/galaxy/workflow/run_request.py | 11 ++ .../workflow_parameter_input_definitions.py | 69 +++++++++++ 11 files changed, 267 insertions(+), 91 deletions(-) create mode 100644 lib/galaxy/workflow/workflow_parameter_input_definitions.py diff --git a/client/src/api/schema/schema.ts b/client/src/api/schema/schema.ts index 373d5d14556b..2ff2335f35ae 100644 --- a/client/src/api/schema/schema.ts +++ b/client/src/api/schema/schema.ts @@ -11559,6 +11559,21 @@ export interface components { */ workflow_step_id: number; }; + /** InvocationFailureWorkflowParameterInvalidResponse */ + InvocationFailureWorkflowParameterInvalidResponse: { + /** + * Details + * @description Message raised by validator + */ + details: string; + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + reason: "workflow_parameter_invalid"; + /** Workflow parameter step that failed validation */ + workflow_step_id: number; + }; /** InvocationInput */ InvocationInput: { /** @@ -11640,7 +11655,8 @@ export interface components { | components["schemas"]["InvocationFailureExpressionEvaluationFailedResponse"] | components["schemas"]["InvocationFailureWhenNotBooleanResponse"] | components["schemas"]["InvocationUnexpectedFailureResponse"] - | components["schemas"]["InvocationEvaluationWarningWorkflowOutputNotFoundResponse"]; + | components["schemas"]["InvocationEvaluationWarningWorkflowOutputNotFoundResponse"] + | components["schemas"]["InvocationFailureWorkflowParameterInvalidResponse"]; /** InvocationOutput */ InvocationOutput: { /** diff --git a/client/src/components/Workflow/Run/WorkflowRun.vue b/client/src/components/Workflow/Run/WorkflowRun.vue index 4190257e3cdd..deaae371a21f 100644 --- a/client/src/components/Workflow/Run/WorkflowRun.vue +++ b/client/src/components/Workflow/Run/WorkflowRun.vue @@ -195,7 +195,7 @@ defineExpose({ Workflow submission failed: {{ submissionError }} { return `Defined workflow output '${invocationMessage.output_name}' was not found in step ${ invocationMessage.workflow_step_id + 1 }.`; + } else if (reason === "workflow_parameter_invalid") { + return `Workflow parameter on step ${invocationMessage.workflow_step_id + 1} failed validation: ${ + invocationMessage.details + }`; } else { return reason; } diff --git a/lib/galaxy/schema/invocation.py b/lib/galaxy/schema/invocation.py index 4d5ce80548e8..047e4250df2d 100644 --- a/lib/galaxy/schema/invocation.py +++ b/lib/galaxy/schema/invocation.py @@ -77,6 +77,7 @@ class FailureReason(str, Enum): expression_evaluation_failed = "expression_evaluation_failed" when_not_boolean = "when_not_boolean" unexpected_failure = "unexpected_failure" + workflow_parameter_invalid = "workflow_parameter_invalid" # The reasons below are attached to the invocation and user-actionable. @@ -212,6 +213,14 @@ class GenericInvocationEvaluationWarningWorkflowOutputNotFound( ) +class GenericInvocationFailureWorkflowParameterInvalid(InvocationFailureMessageBase[DatabaseIdT], Generic[DatabaseIdT]): + reason: Literal[FailureReason.workflow_parameter_invalid] + workflow_step_id: int = Field( + ..., title="Workflow parameter step that failed validation", validation_alias="workflow_step_index" + ) + details: str = Field(..., description="Message raised by validator") + + InvocationCancellationReviewFailed = GenericInvocationCancellationReviewFailed[int] InvocationCancellationHistoryDeleted = GenericInvocationCancellationHistoryDeleted[int] InvocationCancellationUserRequest = GenericInvocationCancellationUserRequest[int] @@ -223,6 +232,7 @@ class GenericInvocationEvaluationWarningWorkflowOutputNotFound( InvocationFailureWhenNotBoolean = GenericInvocationFailureWhenNotBoolean[int] InvocationUnexpectedFailure = GenericInvocationUnexpectedFailure[int] InvocationWarningWorkflowOutputNotFound = GenericInvocationEvaluationWarningWorkflowOutputNotFound[int] +InvocationFailureWorkflowParameterInvalid = GenericInvocationFailureWorkflowParameterInvalid[int] InvocationMessageUnion = Union[ InvocationCancellationReviewFailed, @@ -236,6 +246,7 @@ class GenericInvocationEvaluationWarningWorkflowOutputNotFound( InvocationFailureWhenNotBoolean, InvocationUnexpectedFailure, InvocationWarningWorkflowOutputNotFound, + InvocationFailureWorkflowParameterInvalid, ] InvocationCancellationReviewFailedResponseModel = GenericInvocationCancellationReviewFailed[EncodedDatabaseIdField] @@ -253,6 +264,9 @@ class GenericInvocationEvaluationWarningWorkflowOutputNotFound( InvocationWarningWorkflowOutputNotFoundResponseModel = GenericInvocationEvaluationWarningWorkflowOutputNotFound[ EncodedDatabaseIdField ] +InvocationFailureWorkflowParameterInvalidResponseModel = GenericInvocationFailureWorkflowParameterInvalid[ + EncodedDatabaseIdField +] _InvocationMessageResponseUnion = Annotated[ Union[ @@ -267,6 +281,7 @@ class GenericInvocationEvaluationWarningWorkflowOutputNotFound( InvocationFailureWhenNotBooleanResponseModel, InvocationUnexpectedFailureResponseModel, InvocationWarningWorkflowOutputNotFoundResponseModel, + InvocationFailureWorkflowParameterInvalidResponseModel, ], Field(discriminator="reason"), ] diff --git a/lib/galaxy/tool_util/parser/xml.py b/lib/galaxy/tool_util/parser/xml.py index acfb3342443a..1f80f0ff6a3a 100644 --- a/lib/galaxy/tool_util/parser/xml.py +++ b/lib/galaxy/tool_util/parser/xml.py @@ -1337,7 +1337,33 @@ def parse_sanitizer_elem(self): return self.input_elem.find("sanitizer") def parse_validator_elems(self): - return self.input_elem.findall("validator") + elements = [] + attributes = { + "type": str, + "message": str, + "negate": string_as_bool, + "check": str, + "table_name": str, + "filename": str, + "metadata_name": str, + "metadata_column": str, + "min": float, + "max": float, + "exclude_min": string_as_bool, + "exclude_max": string_as_bool, + "split": str, + "skip": str, + "value": str, + "value_json": lambda v: json.loads(v) if v else None, + "line_startswith": str, + } + for elem in self.input_elem.findall("validator"): + elem_dict = {"content": elem.text} + for attribute, type_cast in attributes.items(): + if val := elem.get(attribute): + elem_dict[attribute] = type_cast(val) + elements.append(elem_dict) + return elements def parse_dynamic_options(self) -> Optional[XmlDynamicOptions]: """Return a XmlDynamicOptions to describe dynamic options if options elem is available.""" diff --git a/lib/galaxy/tool_util/parser/yaml.py b/lib/galaxy/tool_util/parser/yaml.py index c2cde0752f3a..4957d5124aa9 100644 --- a/lib/galaxy/tool_util/parser/yaml.py +++ b/lib/galaxy/tool_util/parser/yaml.py @@ -375,6 +375,21 @@ def parse_when_input_sources(self): sources.append((value, case_page_source)) return sources + def parse_validator_elems(self): + elements = [] + if "validators" in self.input_dict: + for elem in self.input_dict["validators"]: + if "regex_match" in elem: + elements.append( + { + "message": elem.get("regex_doc"), + "content": elem["regex_match"], + "negate": elem.get("negate", False), + "type": "regex", + } + ) + return elements + def parse_static_options(self) -> List[Tuple[str, str, bool]]: static_options = [] input_dict = self.input_dict diff --git a/lib/galaxy/tools/parameters/basic.py b/lib/galaxy/tools/parameters/basic.py index 5eeba8aaecaa..1fd80dd04fae 100644 --- a/lib/galaxy/tools/parameters/basic.py +++ b/lib/galaxy/tools/parameters/basic.py @@ -314,7 +314,7 @@ def validate(self, value, trans=None) -> None: try: validator.validate(value, trans) except ValueError as e: - raise ValueError(f"Parameter {self.name}: {e}") from None + raise ParameterValueError(str(e), self.name, value) from None def to_dict(self, trans, other_values=None): """to_dict tool parameter. This can be overridden by subclasses.""" @@ -1981,7 +1981,7 @@ def do_validate(v): try: validator.validate(v, trans) except ValueError as e: - raise ValueError(f"Parameter {self.name}: {e}") from None + raise ParameterValueError(str(e), self.name, v) from None dataset_count = 0 if value: diff --git a/lib/galaxy/tools/parameters/validation.py b/lib/galaxy/tools/parameters/validation.py index 6334fd95f8b8..a93a4cf05a6c 100644 --- a/lib/galaxy/tools/parameters/validation.py +++ b/lib/galaxy/tools/parameters/validation.py @@ -3,7 +3,6 @@ """ import abc -import json import logging import os.path @@ -86,7 +85,7 @@ class RegexValidator(Validator): @classmethod def from_element(cls, param, elem): - return cls(elem.get("message"), elem.text, elem.get("negate", "false")) + return cls(elem.get("message"), elem.get("content"), elem.get("negate", False)) def __init__(self, message, expression, negate): if message is None: @@ -111,11 +110,11 @@ class ExpressionValidator(Validator): @classmethod def from_element(cls, param, elem): - return cls(elem.get("message"), elem.text, elem.get("negate", "false")) + return cls(elem.get("message"), elem.get("content"), elem.get("negate", False)) def __init__(self, message, expression, negate): if message is None: - message = f"Value '%s' does not evaluate to {'True' if negate == 'false' else 'False'} for '{expression}'" + message = f"Value '%s' does not evaluate to {'True' if not negate else 'False'} for '{expression}'" super().__init__(message, negate) self.expression = expression # Save compiled expression, code objects are thread safe (right?) @@ -140,9 +139,9 @@ def from_element(cls, param, elem): elem.get("message"), elem.get("min"), elem.get("max"), - elem.get("exclude_min", "false"), - elem.get("exclude_max", "false"), - elem.get("negate", "false"), + elem.get("exclude_min", False), + elem.get("exclude_max", False), + elem.get("negate", False), ) def __init__(self, message, range_min, range_max, exclude_min=False, exclude_max=False, negate=False): @@ -178,7 +177,7 @@ class LengthValidator(InRangeValidator): @classmethod def from_element(cls, param, elem): - return cls(elem.get("message"), elem.get("min"), elem.get("max"), elem.get("negate", "false")) + return cls(elem.get("message"), elem.get("min"), elem.get("max"), elem.get("negate", False)) def __init__(self, message, length_min, length_max, negate): if message is None: @@ -198,10 +197,10 @@ class DatasetOkValidator(Validator): @classmethod def from_element(cls, param, elem): - negate = elem.get("negate", "false") + negate = elem.get("negate", False) message = elem.get("message") if message is None: - if negate == "false": + if not negate: message = "The selected dataset is still being generated, select another dataset or wait until it is completed" else: message = "The selected dataset must not be in state OK" @@ -220,9 +219,9 @@ class DatasetEmptyValidator(Validator): @classmethod def from_element(cls, param, elem): message = elem.get("message") - negate = elem.get("negate", "false") + negate = elem.get("negate", False) if not message: - message = f"The selected dataset is {'non-' if negate == 'true' else ''}empty, this tool expects {'non-' if negate == 'false' else ''}empty files." + message = f"The selected dataset is {'non-' if negate else ''}empty, this tool expects {'non-' if negate == 'false' else ''}empty files." return cls(message, negate) def validate(self, value, trans=None): @@ -238,7 +237,7 @@ class DatasetExtraFilesPathEmptyValidator(Validator): @classmethod def from_element(cls, param, elem): message = elem.get("message") - negate = elem.get("negate", "false") + negate = elem.get("negate", False) if not message: message = f"The selected dataset's extra_files_path directory is {'non-' if negate == 'true' else ''}empty or does {'not ' if negate == 'false' else ''}exist, this tool expects {'non-' if negate == 'false' else ''}empty extra_files_path directories associated with the selected input." return cls(message, negate) @@ -259,10 +258,10 @@ class MetadataValidator(Validator): def from_element(cls, param, elem): message = elem.get("message") return cls( - message=message, check=elem.get("check", ""), skip=elem.get("skip", ""), negate=elem.get("negate", "false") + message=message, check=elem.get("check", ""), skip=elem.get("skip", ""), negate=elem.get("negate", False) ) - def __init__(self, message=None, check="", skip="", negate="false"): + def __init__(self, message=None, check="", skip="", negate=False): if not message: if not util.asbool(negate): message = "Metadata '%s' missing, click the pencil icon in the history item to edit / save the metadata attributes" @@ -292,7 +291,7 @@ class MetadataEqualValidator(Validator): requires_dataset_metadata = True - def __init__(self, metadata_name=None, value=None, message=None, negate="false"): + def __init__(self, metadata_name=None, value=None, message=None, negate=False): if not message: if not util.asbool(negate): message = f"Metadata value for '{metadata_name}' must be '{value}', but it is '%s'." @@ -304,12 +303,12 @@ def __init__(self, metadata_name=None, value=None, message=None, negate="false") @classmethod def from_element(cls, param, elem): - value = elem.get("value", None) or json.loads(elem.get("value_json", "null")) + value = elem.get("value") or elem.get("value_json") return cls( - metadata_name=elem.get("metadata_name", None), + metadata_name=elem.get("metadata_name"), value=value, - message=elem.get("message", None), - negate=elem.get("negate", "false"), + message=elem.get("message"), + negate=elem.get("negate", False), ) def validate(self, value, trans=None): @@ -328,9 +327,9 @@ class UnspecifiedBuildValidator(Validator): @classmethod def from_element(cls, param, elem): message = elem.get("message") - negate = elem.get("negate", "false") + negate = elem.get("negate", False) if not message: - message = f"{'Unspecified' if negate == 'false' else 'Specified'} genome build, click the pencil icon in the history item to {'set' if negate == 'false' else 'remove'} the genome build" + message = f"{'Unspecified' if not negate else 'Specified'} genome build, click the pencil icon in the history item to {'set' if negate == 'false' else 'remove'} the genome build" return cls(message, negate) def validate(self, value, trans=None): @@ -351,7 +350,7 @@ class NoOptionsValidator(Validator): @classmethod def from_element(cls, param, elem): message = elem.get("message") - negate = elem.get("negate", "false") + negate = elem.get("negate", False) if not message: message = f"{'No options' if negate == 'false' else 'Options'} available for selection" return cls(message, negate) @@ -368,9 +367,9 @@ class EmptyTextfieldValidator(Validator): @classmethod def from_element(cls, param, elem): message = elem.get("message") - negate = elem.get("negate", "false") + negate = elem.get("negate", False) if not message: - if negate == "false": + if not negate: message = elem.get("message", "Field requires a value") else: message = elem.get("message", "Field must not set a value") @@ -406,7 +405,7 @@ def from_element(cls, param, elem): line_startswith = elem.get("line_startswith") if line_startswith: line_startswith = line_startswith.strip() - negate = elem.get("negate", "false") + negate = elem.get("negate", False) return cls(filename, metadata_name, metadata_column, message, line_startswith, split, negate) def __init__( @@ -417,7 +416,7 @@ def __init__( message="Value for metadata not found.", line_startswith=None, split="\t", - negate="false", + negate=False, ): super().__init__(message, negate) self.metadata_name = metadata_name @@ -456,10 +455,10 @@ def from_element(cls, param, elem): except ValueError: pass message = elem.get("message", f"Value was not found in {table_name}.") - negate = elem.get("negate", "false") + negate = elem.get("negate", False) return cls(tool_data_table, column, message, negate) - def __init__(self, tool_data_table, column, message="Value not found.", negate="false"): + def __init__(self, tool_data_table, column, message="Value not found.", negate=False): super().__init__(message, negate) self.valid_values = [] self._data_table_content_version = None @@ -496,7 +495,7 @@ class ValueNotInDataTableColumnValidator(ValueInDataTableColumnValidator): note: this is covered in a framework test (validation_value_in_datatable) """ - def __init__(self, tool_data_table, metadata_column, message="Value already present.", negate="false"): + def __init__(self, tool_data_table, metadata_column, message="Value already present.", negate=False): super().__init__(tool_data_table, metadata_column, message, negate) def validate(self, value, trans=None): @@ -532,11 +531,11 @@ def from_element(cls, param, elem): except ValueError: pass message = elem.get("message", f"Value for metadata {metadata_name} was not found in {table_name}.") - negate = elem.get("negate", "false") + negate = elem.get("negate", False) return cls(tool_data_table, metadata_name, metadata_column, message, negate) def __init__( - self, tool_data_table, metadata_name, metadata_column, message="Value for metadata not found.", negate="false" + self, tool_data_table, metadata_name, metadata_column, message="Value for metadata not found.", negate=False ): super().__init__(tool_data_table, metadata_column, message, negate) self.metadata_name = metadata_name @@ -558,7 +557,12 @@ class MetadataNotInDataTableColumnValidator(MetadataInDataTableColumnValidator): requires_dataset_metadata = True def __init__( - self, tool_data_table, metadata_name, metadata_column, message="Value for metadata not found.", negate="false" + self, + tool_data_table, + metadata_name, + metadata_column, + message="Value for metadata not found.", + negate=False, ): super().__init__(tool_data_table, metadata_name, metadata_column, message, negate) @@ -590,9 +594,9 @@ def from_element(cls, param, elem): elem.get("message"), elem.get("min"), elem.get("max"), - elem.get("exclude_min", "false"), - elem.get("exclude_max", "false"), - elem.get("negate", "false"), + elem.get("exclude_min", False), + elem.get("exclude_max", False), + elem.get("negate", False), ) ret.message = "Metadata: " + ret.message return ret diff --git a/lib/galaxy/workflow/modules.py b/lib/galaxy/workflow/modules.py index 2fa7c9bf9443..8ed226a20c31 100644 --- a/lib/galaxy/workflow/modules.py +++ b/lib/galaxy/workflow/modules.py @@ -4,6 +4,7 @@ import json import logging +import math import re from collections import defaultdict from typing import ( @@ -46,6 +47,7 @@ InvocationFailureExpressionEvaluationFailed, InvocationFailureOutputNotFound, InvocationFailureWhenNotBoolean, + InvocationFailureWorkflowParameterInvalid, ) from galaxy.tool_util.cwl.util import set_basename_and_derived_properties from galaxy.tool_util.parser.output_objects import ToolExpressionOutput @@ -69,12 +71,9 @@ from galaxy.tools.parameters.basic import ( BaseDataToolParameter, BooleanToolParameter, - ColorToolParameter, DataCollectionToolParameter, DataToolParameter, - FloatToolParameter, HiddenToolParameter, - IntegerToolParameter, parameter_types, raw_to_galaxy, SelectToolParameter, @@ -83,6 +82,7 @@ from galaxy.tools.parameters.grouping import ( Conditional, ConditionalWhen, + Repeat, ) from galaxy.tools.parameters.history_query import HistoryQuery from galaxy.tools.parameters.workflow_utils import ( @@ -101,6 +101,7 @@ from galaxy.util.rules_dsl import RuleSet from galaxy.util.template import fill_template from galaxy.util.tool_shed.common_util import get_tool_shed_url_from_tool_shed_registry +from galaxy.workflow.workflow_parameter_input_definitions import get_default_source if TYPE_CHECKING: from galaxy.schema.invocation import InvocationMessageUnion @@ -1215,51 +1216,7 @@ def get_inputs(self): cases = [] for param_type in ["text", "integer", "float", "boolean", "color"]: - default_source: Dict[str, Union[int, float, bool, str]] = dict( - name="default", label="Default Value", type=param_type - ) - if param_type == "text": - if parameter_type == "text": - text_default = parameter_def.get("default") or "" - else: - text_default = "" - default_source["value"] = text_default - input_default_value: Union[ - TextToolParameter, - IntegerToolParameter, - FloatToolParameter, - BooleanToolParameter, - ColorToolParameter, - ] = TextToolParameter(None, default_source) - elif param_type == "integer": - if parameter_type == "integer": - integer_default = parameter_def.get("default") or 0 - else: - integer_default = 0 - default_source["value"] = integer_default - input_default_value = IntegerToolParameter(None, default_source) - elif param_type == "float": - if parameter_type == "float": - float_default = parameter_def.get("default") or 0.0 - else: - float_default = 0.0 - default_source["value"] = float_default - input_default_value = FloatToolParameter(None, default_source) - elif param_type == "boolean": - if parameter_type == "boolean": - boolean_default = parameter_def.get("default") or False - else: - boolean_default = False - default_source["value"] = boolean_default - default_source["checked"] = boolean_default - input_default_value = BooleanToolParameter(None, default_source) - elif param_type == "color": - if parameter_type == "color": - color_default = parameter_def.get("default") or "#000000" - else: - color_default = "#000000" - default_source["value"] = color_default - input_default_value = ColorToolParameter(None, default_source) + input_default_value = get_default_source(param_type, parameter_def) optional_value = optional_param(optional) optional_cond = Conditional() @@ -1313,9 +1270,44 @@ def get_inputs(self): type="boolean", checked=parameter_def.get("multiple", False), ) + specify_multiple = BooleanToolParameter(None, specify_multiple_source) + + add_validators_repeat = Repeat() + add_validators_repeat._title = "Add validator to restrict valid input" + add_validators_repeat.name = "validators" + add_validators_repeat.min = 0 + add_validators_repeat.max = math.inf + add_validators_repeat.inputs = { + "regex_match": TextToolParameter( + None, + {"optional": False, "name": "regex_match", "label": "Specify a regex that must match input"}, + ), + "regex_doc": TextToolParameter( + None, + { + "optional": False, + "name": "regex_doc", + "label": "Specify a message that should be shown as a hint", + }, + ), + } + + if "validators" in parameter_def: + from galaxy.tools.parameters import populate_state + + state = {} + populate_state( + self.trans, {"validators": add_validators_repeat}, parameter_def, state, input_format="21.01" + ) + add_validators_repeat.default = len(parameter_def["validators"]) + # Insert multiple option as first option, which is determined by dictionary insert order - when_this_type.inputs = {"multiple": specify_multiple, **when_this_type.inputs} + when_this_type.inputs = { + "multiple": specify_multiple, + "validators": add_validators_repeat, + **when_this_type.inputs, + } restrict_how_source: Dict[str, Union[str, List[Dict[str, Union[str, bool]]]]] = dict( name="how", label="Restrict Text Values?", type="select" @@ -1514,6 +1506,9 @@ def _parameter_def_list_to_options(parameter_value): parameter_kwds["options"] = _parameter_def_list_to_options(restriction_values) restricted_inputs = True + if is_text and parameter_def.get("validators"): + parameter_kwds["validators"] = parameter_def["validators"] + client_parameter_type = parameter_type if restricted_inputs: client_parameter_type = "select" @@ -1563,7 +1558,10 @@ def execute( self, trans, progress: "WorkflowProgress", invocation_step, use_cached_job: bool = False ) -> Optional[bool]: step = invocation_step.workflow_step - input_value = step.state.inputs["input"] + if step.id in progress.inputs_by_step_id: + input_value = progress.inputs_by_step_id[step.id] + else: + input_value = step.state.inputs["input"] if input_value is None: default_value = step.get_input_default_value(NO_REPLACEMENT) # TODO: look at parameter type and infer if value should be a dictionary @@ -1572,6 +1570,17 @@ def execute( if not isinstance(default_value, dict): default_value = {"value": default_value} input_value = default_value.get("value", NO_REPLACEMENT) + input_param = self.get_runtime_inputs(self)["input"] + # TODO: raise DelayedWorkflowEvaluation if replacement not ready ? Need test + # TODO: move (at least regex) to frontend. failing here is kind of stupid + try: + input_param.validate(input_value) + except ValueError as e: + raise FailWorkflowEvaluation( + why=InvocationFailureWorkflowParameterInvalid( + reason=FailureReason.workflow_parameter_invalid, workflow_step_id=step.id, details=str(e) + ) + ) step_outputs = dict(output=input_value) progress.set_outputs_for_input(invocation_step, step_outputs) return None @@ -1584,6 +1593,7 @@ def step_state_to_tool_state(self, state): default_value = state["default"] state["optional"] = True multiple = state.get("multiple") + validators = state.get("validators") restrictions = state.get("restrictions") restrictOnConnections = state.get("restrictOnConnections") suggestions = state.get("suggestions") @@ -1603,6 +1613,8 @@ def step_state_to_tool_state(self, state): } if multiple is not None: state["parameter_definition"]["multiple"] = multiple + if validators is not None: + state["parameter_definition"]["validators"] = validators state["parameter_definition"]["restrictions"] = {} state["parameter_definition"]["restrictions"]["how"] = restrictions_how @@ -1646,6 +1658,8 @@ def _parse_state_into_dict(self): optional = False if "multiple" in parameters_def: rval["multiple"] = parameters_def["multiple"] + if "validators" in parameters_def: + rval["validators"] = parameters_def["validators"] restrictions_cond_values = parameters_def.get("restrictions") if restrictions_cond_values: diff --git a/lib/galaxy/workflow/run_request.py b/lib/galaxy/workflow/run_request.py index 516c85d07eb1..cbf401b7c43b 100644 --- a/lib/galaxy/workflow/run_request.py +++ b/lib/galaxy/workflow/run_request.py @@ -25,7 +25,9 @@ ensure_object_added_to_session, transaction, ) +from galaxy.tools.parameters.basic import ParameterValueError from galaxy.tools.parameters.meta import expand_workflow_inputs +from galaxy.workflow.modules import WorkflowModuleInjector from galaxy.workflow.resources import get_resource_mapper_function if TYPE_CHECKING: @@ -358,11 +360,20 @@ def build_workflow_run_configs( steps_by_id = workflow.steps_by_id # Set workflow inputs. + module_injector = WorkflowModuleInjector(trans, False) for key, input_dict in normalized_inputs.items(): if input_dict is None: continue step = steps_by_id[key] if step.type == "parameter_input": + module_injector.inject(step) + input_param = step.module.get_runtime_inputs(step.module)["input"] + try: + input_param.validate(input_dict) + except ParameterValueError as e: + raise exceptions.RequestParameterInvalidException( + f"{step.label or step.order_index + 1}: {e.message_suffix}" + ) continue if "src" not in input_dict: raise exceptions.RequestParameterInvalidException( diff --git a/lib/galaxy/workflow/workflow_parameter_input_definitions.py b/lib/galaxy/workflow/workflow_parameter_input_definitions.py new file mode 100644 index 000000000000..382325126361 --- /dev/null +++ b/lib/galaxy/workflow/workflow_parameter_input_definitions.py @@ -0,0 +1,69 @@ +from typing import ( + Any, + Dict, + Literal, + Union, +) + +from galaxy.tools.parameters.basic import ( + BooleanToolParameter, + ColorToolParameter, + FloatToolParameter, + IntegerToolParameter, + TextToolParameter, +) + +param_types = Literal["text", "integer", "float", "color", "boolean"] +default_source_type = Dict[str, Union[int, float, bool, str]] + + +def get_default_source(param_type: param_types, parameter_def: Dict[str, Any]) -> default_source_type: + """ + param_type is the type of parameter we want to build up, stored_parameter_type is the parameter_type + as stored in the tool state + """ + stored_parameter_type = parameter_def["parameter_type"] + default_source: default_source_type = dict(name="default", label="Default Value", type=param_type) + if param_type == "text": + if stored_parameter_type == "text": + text_default = parameter_def.get("default") or "" + else: + text_default = "" + default_source["value"] = text_default + input_default_value: Union[ + TextToolParameter, + IntegerToolParameter, + FloatToolParameter, + BooleanToolParameter, + ColorToolParameter, + ] = TextToolParameter(None, default_source) + elif param_type == "integer": + if stored_parameter_type == "integer": + integer_default = parameter_def.get("default") or 0 + else: + integer_default = 0 + default_source["value"] = integer_default + input_default_value = IntegerToolParameter(None, default_source) + elif param_type == "float": + if stored_parameter_type == "float": + float_default = parameter_def.get("default") or 0.0 + else: + float_default = 0.0 + default_source["value"] = float_default + input_default_value = FloatToolParameter(None, default_source) + elif param_type == "boolean": + if stored_parameter_type == "boolean": + boolean_default = parameter_def.get("default") or False + else: + boolean_default = False + default_source["value"] = boolean_default + default_source["checked"] = boolean_default + input_default_value = BooleanToolParameter(None, default_source) + elif param_type == "color": + if stored_parameter_type == "color": + color_default = parameter_def.get("default") or "#000000" + else: + color_default = "#000000" + default_source["value"] = color_default + input_default_value = ColorToolParameter(None, default_source) + return input_default_value