"""
Classes related to parameter validation.
"""
import abc
import logging
import os
from typing import (
Any,
cast,
List,
Optional,
Union,
)
from galaxy import (
model,
util,
)
from galaxy.tool_util.parser.parameter_validators import (
AnyValidatorModel,
EmptyFieldParameterValidatorModel,
ExpressionParameterValidatorModel,
InRangeParameterValidatorModel,
MetadataParameterValidatorModel,
parse_xml_validators as parse_xml_validators_models,
raise_error_if_valiation_fails,
RegexParameterValidatorModel,
)
log = logging.getLogger(__name__)
[docs]class Validator(abc.ABC):
"""
A validator checks that a value meets some conditions OR raises ValueError
"""
requires_dataset_metadata = False
[docs] def __init__(self, message: str, negate: bool = False):
self.message = message
self.negate = util.asbool(negate)
super().__init__()
[docs] @abc.abstractmethod
def validate(self, value, trans=None, message=None, value_to_show=None):
"""
validate a value
needs to be implemented in classes derived from validator.
the implementation needs to call `super().validate()`
giving result as a bool (which should be true if the
validation is positive and false otherwise) and the value
that is validated.
the Validator.validate function will then negate the value
depending on `self.negate` and return None if
- value is True and negate is False
- value is False and negate is True
and raise a ValueError otherwise.
return None if positive validation, otherwise a ValueError is raised
"""
raise_error_if_valiation_fails(value, self, message=message, value_to_show=value_to_show)
[docs]class RegexValidator(Validator):
"""
Validator that evaluates a regular expression
"""
[docs] def __init__(self, message: str, expression: str, negate: bool):
super().__init__(message, negate)
# Compile later. RE objects used to not be thread safe. Not sure about
# the sre module.
self.expression = expression
[docs] def validate(self, value, trans=None):
RegexParameterValidatorModel.regex_validation(self.expression, value, self)
[docs]class ExpressionValidator(Validator):
"""
Validator that evaluates a python expression using the value
"""
[docs] def __init__(self, message: str, expression: str, negate: bool):
super().__init__(message, negate)
self.expression = expression
# Save compiled expression, code objects are thread safe (right?)
self.compiled_expression = ExpressionParameterValidatorModel.ensure_compiled(expression)
[docs] def validate(self, value, trans=None):
ExpressionParameterValidatorModel.expression_validation(
self.expression, value, self, compiled_expression=self.compiled_expression
)
[docs]class InRangeValidator(ExpressionValidator):
"""
Validator that ensures a number is in a specified range
"""
[docs] def __init__(
self,
message: str,
min: Optional[float] = None,
max: Optional[float] = None,
exclude_min: bool = False,
exclude_max: bool = False,
negate: bool = False,
):
"""
When the optional exclude_min and exclude_max attributes are set
to true, the range excludes the end points (i.e., min < value < max),
while if set to False (the default), then range includes the end points
(1.e., min <= value <= max). Combinations of exclude_min and exclude_max
values are allowed.
"""
self.min = str(min) if min is not None else "-inf"
self.exclude_min = exclude_min
self.max = str(max) if max is not None else "inf"
self.exclude_max = exclude_max
assert float(self.min) <= float(self.max), "min must be less than or equal to max"
# Remove unneeded 0s and decimal from floats to make message pretty.
op1 = "<="
op2 = "<="
if self.exclude_min:
op1 = "<"
if self.exclude_max:
op2 = "<"
expression = f"float('{self.min}') {op1} float(value) {op2} float('{self.max}')"
super().__init__(message, expression, negate)
[docs] @staticmethod
def simple_range_validator(min: Optional[float], max: Optional[float]):
return cast(
InRangeParameterValidatorModel,
_to_validator(None, InRangeParameterValidatorModel(min=min, max=max, implicit=True)),
)
[docs]class LengthValidator(InRangeValidator):
"""
Validator that ensures the length of the provided string (value) is in a specific range
"""
[docs] def __init__(self, message: str, min: float, max: float, negate: bool):
super().__init__(message, min=min, max=max, negate=negate)
[docs] def validate(self, value, trans=None):
if value is None:
raise ValueError("No value provided")
super().validate(len(value) if value else 0, trans)
[docs]class DatasetOkValidator(Validator):
"""
Validator that checks if a dataset is in an 'ok' state
"""
[docs] def __init__(self, message: str, negate: bool = False):
super().__init__(message, negate=negate)
[docs] def validate(self, value, trans=None):
if value:
super().validate(value.state == model.Dataset.states.OK)
[docs]class DatasetEmptyValidator(Validator):
"""
Validator that checks if a dataset has a positive file size.
"""
[docs] def __init__(self, message: str, negate: bool = False):
super().__init__(message, negate=negate)
[docs] def validate(self, value, trans=None):
if value:
super().validate(value.get_size() != 0)
[docs]class UnspecifiedBuildValidator(Validator):
"""
Validator that checks for dbkey not equal to '?'
"""
requires_dataset_metadata = True
[docs] def __init__(self, message: str, negate: bool = False):
super().__init__(message, negate=negate)
[docs] def validate(self, value, trans=None):
# if value is None, we cannot validate
if value:
dbkey = value.metadata.dbkey
# TODO can dbkey really be a list?
if isinstance(dbkey, list):
dbkey = dbkey[0]
super().validate(dbkey != "?")
[docs]class NoOptionsValidator(Validator):
"""
Validator that checks for empty select list
"""
[docs] def __init__(self, message: str, negate: bool = False):
super().__init__(message, negate=negate)
[docs] def validate(self, value, trans=None):
super().validate(value is not None)
[docs]class EmptyTextfieldValidator(Validator):
"""
Validator that checks for empty text field
"""
[docs] def __init__(self, message: str, negate: bool = False):
super().__init__(message, negate=negate)
[docs] def validate(self, value, trans=None):
EmptyFieldParameterValidatorModel.empty_validate(value, self)
[docs]class ValueInDataTableColumnValidator(Validator):
"""
Validator that checks if a value is in a tool data table column.
note: this is covered in a framework test (validation_value_in_datatable)
"""
[docs] def __init__(
self,
tool_data_table,
metadata_column: Union[str, int],
message: str,
negate: bool = False,
):
super().__init__(message, negate)
self.valid_values: List[Any] = []
self._data_table_content_version = None
self._tool_data_table = tool_data_table
if isinstance(metadata_column, str):
metadata_column = tool_data_table.columns[metadata_column]
self._column = metadata_column
self._load_values()
def _load_values(self):
self._data_table_content_version, data_fields = self._tool_data_table.get_version_fields()
self.valid_values = []
for fields in data_fields:
if self._column < len(fields):
self.valid_values.append(fields[self._column])
[docs] def validate(self, value, trans=None):
if not value:
return
if not self._tool_data_table.is_current_version(self._data_table_content_version):
log.debug(
"ValueInDataTableColumnValidator: values are out of sync with data table (%s), updating validator.",
self._tool_data_table.name,
)
self._load_values()
super().validate(value in self.valid_values)
[docs]class ValueNotInDataTableColumnValidator(ValueInDataTableColumnValidator):
"""
Validator that checks if a value is NOT in a tool data table column.
Equivalent to ValueInDataTableColumnValidator with `negate="true"`.
note: this is covered in a framework test (validation_value_in_datatable)
"""
[docs] def __init__(
self, tool_data_table, metadata_column: Union[str, int], message="Value already present.", negate: bool = False
):
super().__init__(tool_data_table, metadata_column, message, negate)
[docs] def validate(self, value, trans=None):
try:
super().validate(value)
except ValueError:
return
else:
raise ValueError(self.message)
validator_types = dict(
expression=ExpressionValidator,
regex=RegexValidator,
in_range=InRangeValidator,
length=LengthValidator,
metadata=MetadataValidator,
dataset_metadata_equal=MetadataEqualValidator,
unspecified_build=UnspecifiedBuildValidator,
no_options=NoOptionsValidator,
empty_field=EmptyTextfieldValidator,
empty_dataset=DatasetEmptyValidator,
empty_extra_files_path=DatasetExtraFilesPathEmptyValidator,
dataset_metadata_in_data_table=MetadataInDataTableColumnValidator,
dataset_metadata_not_in_data_table=MetadataNotInDataTableColumnValidator,
dataset_metadata_in_range=MetadataInRangeValidator,
value_in_data_table=ValueInDataTableColumnValidator,
value_not_in_data_table=ValueNotInDataTableColumnValidator,
dataset_ok_validator=DatasetOkValidator,
)
deprecated_validator_types = dict(dataset_metadata_in_file=MetadataInFileColumnValidator)
validator_types.update(deprecated_validator_types)
[docs]def parse_xml_validators(app, xml_el: util.Element) -> List[Validator]:
return to_validators(app, parse_xml_validators_models(xml_el))
[docs]def to_validators(app, validator_models: List[AnyValidatorModel]) -> List[Validator]:
validators = []
for validator_model in validator_models:
validators.append(_to_validator(app, validator_model))
return validators
def _to_validator(app, validator_model: AnyValidatorModel) -> Validator:
as_dict = validator_model.model_dump()
validator_type = as_dict.pop("type")
del as_dict["implicit"]
if "table_name" in as_dict and app is not None:
table_name = as_dict.pop("table_name")
tool_data_table = app.tool_data_tables[table_name]
as_dict["tool_data_table"] = tool_data_table
if "filename" in as_dict and app is not None:
filename = as_dict.pop("filename")
as_dict["filename"] = f"{app.config.tool_data_path}/{filename}"
return validator_types[validator_type](**as_dict)