New packages galaxy-tool-util-models, galaxy-tool-shed-schema.

The galaxy-tool-shed-schema package is basically pydantic models for the whole tool shed API. It is used by both the backend code for the new tool shed and the testing code that runs on both the old and new tool shed.

The creation of galaxy-tool-util-models package is something I've wanted to do for a while - it should solve some TODOs I've left in the base. I think beyond enabling galaxy-tool-shed-schema package - the other nice thing is that galaxy-schema code can now rely on various tool stuff without requiring dependency on any of the parser or runtime code.

This should also fix things for the new record types in #19377. I've introduced a type that can be used tools and is included schema for collection creation. The galaxy-tool-util-models packages can now be dependended on by galaxy-schema and galaxy-tool-util and these two packages do not need to depend on each other - either direction of that dependency would make me uncomfortable. xref
https://github.com/galaxyproject/galaxy/actions/runs/13704893929/job/38327692654?pr=19377
This commit is contained in:
John Chilton
2025-04-15 15:21:21 -04:00
committed by mvdbeek
parent e8e167a8b9
commit f91012244b
75 changed files with 1105 additions and 802 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ from beaker.cache import CacheManager
from beaker.util import parse_cache_config_options
from galaxy.structured_app import BasicSharedApp
from galaxy.tool_util.parser.interface import Citation
from galaxy.tool_util_models.tool_source import Citation
from galaxy.util import (
DEFAULT_SOCKET_TIMEOUT,
requests,
+1 -1
View File
@@ -12,7 +12,7 @@ from packaging.version import Version
from galaxy.tool_util.lint import Linter
from galaxy.tool_util.parameters import validate_test_cases_for_tool_source
from galaxy.tool_util.verify.assertion_models import assertion_list
from galaxy.tool_util_models.assertions import assertion_list
from galaxy.util import asbool
from ._util import is_datasource
+50
View File
@@ -0,0 +1,50 @@
from typing import (
Type,
TypeVar,
)
from galaxy.tool_util_models import ParsedTool
from .parameters import input_models_for_tool_source
from .parser.interface import (
ToolSource,
)
from .parser.output_objects import from_tool_source
def parse_tool(tool_source: ToolSource) -> ParsedTool:
return parse_tool_custom(tool_source, ParsedTool)
P = TypeVar("P", bound=ParsedTool)
def parse_tool_custom(tool_source: ToolSource, model_type: Type[P]) -> P:
id = tool_source.parse_id()
version = tool_source.parse_version()
name = tool_source.parse_name()
description = tool_source.parse_description()
inputs = input_models_for_tool_source(tool_source).parameters
outputs = from_tool_source(tool_source)
citations = tool_source.parse_citations()
license = tool_source.parse_license()
profile = tool_source.parse_profile()
edam_operations = tool_source.parse_edam_operations()
edam_topics = tool_source.parse_edam_topics()
xrefs = tool_source.parse_xrefs()
help = tool_source.parse_help()
return model_type(
id=id,
version=version,
name=name,
description=description,
profile=profile,
inputs=inputs,
outputs=outputs,
license=license,
citations=citations,
edam_operations=edam_operations,
edam_topics=edam_topics,
xrefs=xrefs,
help=help,
)
@@ -9,10 +9,8 @@ from typing import (
)
from galaxy.tool_util.biotools import BiotoolsMetadataSource
from galaxy.tool_util.parser import (
ToolSource,
XrefDict,
)
from galaxy.tool_util.parser import ToolSource
from galaxy.tool_util_models.tool_source import XrefDict
from galaxy.util.resources import resource_string
+25 -23
View File
@@ -1,26 +1,4 @@
from .case import (
test_case_state,
validate_test_cases_for_tool_source,
)
from .convert import (
decode,
dereference,
encode,
encode_test,
fill_static_defaults,
landing_decode,
landing_encode,
)
from .factory import (
from_input_source,
input_models_for_pages,
input_models_for_tool_source,
input_models_from_json,
ParameterDefinitionError,
tool_parameter_bundle_from_json,
)
from .json import to_json_schema_string
from .models import (
from galaxy.tool_util_models.parameters import (
BooleanParameterModel,
ColorParameterModel,
ConditionalParameterModel,
@@ -53,6 +31,30 @@ from .models import (
ToolParameterBundleModel,
ToolParameterModel,
ToolParameterT,
)
from .case import (
test_case_state,
validate_test_cases_for_tool_source,
)
from .convert import (
decode,
dereference,
encode,
encode_test,
fill_static_defaults,
landing_decode,
landing_encode,
)
from .factory import (
from_input_source,
input_models_for_pages,
input_models_for_tool_source,
input_models_from_json,
ParameterDefinitionError,
tool_parameter_bundle_from_json,
)
from .json import to_json_schema_string
from .model_validation import (
validate_against_model,
validate_internal_job,
validate_internal_landing_request,
+3 -3
View File
@@ -21,9 +21,7 @@ from galaxy.tool_util.parser.interface import (
XmlTestCollectionDefDict,
)
from galaxy.tool_util.parser.util import multiple_select_value_split
from galaxy.util import asbool
from .factory import input_models_for_tool_source
from .models import (
from galaxy.tool_util_models.parameters import (
BooleanParameterModel,
ConditionalParameterModel,
ConditionalWhen,
@@ -38,6 +36,8 @@ from .models import (
SectionParameterModel,
ToolParameterT,
)
from galaxy.util import asbool
from .factory import input_models_for_tool_source
from .state import TestCaseToolState
from .visitor import (
flat_state_path,
+5 -5
View File
@@ -11,11 +11,7 @@ from typing import (
Union,
)
from galaxy.tool_util.parser.interface import (
JsonTestCollectionDefDict,
JsonTestDatasetDefDict,
)
from .models import (
from galaxy.tool_util_models.parameters import (
ConditionalParameterModel,
ConditionalWhen,
DataCollectionRequest,
@@ -29,6 +25,10 @@ from .models import (
ToolParameterBundle,
ToolParameterT,
)
from galaxy.tool_util_models.tool_source import (
JsonTestCollectionDefDict,
JsonTestDatasetDefDict,
)
from .state import (
JobInternalToolState,
LandingRequestInternalToolState,
+3 -5
View File
@@ -14,16 +14,13 @@ from galaxy.tool_util.parser.interface import (
PagesSource,
ToolSource,
)
from galaxy.tool_util.parser.parameter_validators import (
static_validators,
)
from galaxy.tool_util.parser.parameter_validators import static_validators
from galaxy.tool_util.parser.util import (
multiple_select_value_split,
parse_profile_version,
text_input_is_optional,
)
from galaxy.util import string_as_bool
from .models import (
from galaxy.tool_util_models.parameters import (
BaseUrlParameterModel,
BooleanParameterModel,
ColorParameterModel,
@@ -61,6 +58,7 @@ from .models import (
ToolParameterBundleModel,
ToolParameterT,
)
from galaxy.util import string_as_bool
class ParameterDefinitionError(Exception):
@@ -0,0 +1,58 @@
from typing import (
Any,
Dict,
Optional,
Type,
)
from pydantic import (
BaseModel,
ValidationError,
)
from typing_extensions import (
Protocol,
)
from galaxy.exceptions import RequestParameterInvalidException
from galaxy.tool_util_models.parameters import (
create_field_model,
DEFAULT_MODEL_NAME,
RawStateDict,
StateRepresentationT,
ToolParameterBundle,
)
def validate_against_model(pydantic_model: Type[BaseModel], parameter_state: Dict[str, Any]) -> None:
try:
pydantic_model(**parameter_state)
except ValidationError as e:
# TODO: Improve this or maybe add a handler for this in the FastAPI exception
# handler.
raise RequestParameterInvalidException(str(e))
class ValidationFunctionT(Protocol):
def __call__(self, tool: ToolParameterBundle, request: RawStateDict, name: Optional[str] = None) -> None: ...
def validate_model_type_factory(state_representation: StateRepresentationT) -> ValidationFunctionT:
def validate_request(tool: ToolParameterBundle, request: Dict[str, Any], name: Optional[str] = None) -> None:
name = name or DEFAULT_MODEL_NAME
pydantic_model = create_field_model(tool.parameters, name=name, state_representation=state_representation)
validate_against_model(pydantic_model, request)
return validate_request
validate_request = validate_model_type_factory("request")
validate_internal_request = validate_model_type_factory("request_internal")
validate_internal_request_dereferenced = validate_model_type_factory("request_internal_dereferenced")
validate_landing_request = validate_model_type_factory("landing_request")
validate_internal_landing_request = validate_model_type_factory("landing_request_internal")
validate_internal_job = validate_model_type_factory("job_internal")
validate_test_case = validate_model_type_factory("test_case_xml")
validate_workflow_step = validate_model_type_factory("workflow_step")
validate_workflow_step_linked = validate_model_type_factory("workflow_step_linked")
+3 -1
View File
@@ -14,7 +14,7 @@ from typing import (
from pydantic import BaseModel
from typing_extensions import Literal
from .models import (
from galaxy.tool_util_models.parameters import (
create_job_internal_model,
create_landing_request_internal_model,
create_landing_request_model,
@@ -28,6 +28,8 @@ from .models import (
ToolParameterBundle,
ToolParameterBundleModel,
ToolParameterT,
)
from .model_validation import (
validate_against_model,
)
+1 -1
View File
@@ -11,7 +11,7 @@ from typing import (
from typing_extensions import Protocol
from .models import (
from galaxy.tool_util_models.parameters import (
ConditionalParameterModel,
ConditionalWhen,
simple_input_models,
-2
View File
@@ -8,7 +8,6 @@ from .factory import (
from .interface import (
RequiredFiles,
ToolSource,
XrefDict,
)
from .output_objects import ToolOutputCollectionPart
@@ -19,5 +18,4 @@ __all__ = (
"RequiredFiles",
"ToolOutputCollectionPart",
"ToolSource",
"XrefDict",
)
+1 -1
View File
@@ -10,8 +10,8 @@ import packaging.version
from galaxy.tool_util.cwl.parser import tool_proxy
from galaxy.tool_util.deps import requirements
from galaxy.tool_util_models.tool_source import HelpContent
from .interface import (
HelpContent,
PageSource,
PagesSource,
ToolSource,
+16 -93
View File
@@ -5,7 +5,6 @@ from abc import (
ABCMeta,
abstractmethod,
)
from enum import Enum
from os.path import join
from typing import (
Any,
@@ -19,16 +18,30 @@ from typing import (
)
import packaging.version
from pydantic import BaseModel
from typing_extensions import (
Literal,
NotRequired,
TypedDict,
)
from galaxy.tool_util_models.parameter_validators import (
AnyValidatorModel,
)
from galaxy.tool_util_models.tool_source import (
BaseJsonTestCollectionDefCollectionElementDict,
Citation,
DrillDownOptionsDict,
HelpContent,
JsonTestCollectionDefCollectionElementDict,
JsonTestCollectionDefDatasetElementDict,
JsonTestCollectionDefDict,
JsonTestCollectionDefElementDict,
JsonTestDatasetDefDict,
OutputCompareType,
XrefDict,
)
from galaxy.util import Element
from galaxy.util.path import safe_walk
from .parameter_validators import AnyValidatorModel
from .util import _parse_name
if TYPE_CHECKING:
@@ -57,15 +70,6 @@ AssertionList = Optional[List[AssertionDict]]
XmlInt = Union[str, int]
class OutputCompareType(str, Enum):
diff = "diff"
re_match = "re_match"
sim_size = "sim_size"
re_match_multiline = "re_match_multiline"
contains = "contains"
image_diff = "image_diff"
class ToolSourceTestOutputAttributes(TypedDict):
object: NotRequired[Optional[Any]]
compare: OutputCompareType
@@ -129,21 +133,6 @@ class ToolSourceTests(TypedDict):
tests: List[ToolSourceTest]
class XrefDict(TypedDict):
value: str
reftype: str
class Citation(BaseModel):
type: str
content: str
class HelpContent(BaseModel):
format: Literal["restructuredtext", "plain_text", "markdown"]
content: str
class ToolSource(metaclass=ABCMeta):
"""This interface represents an abstract source to parse tool
information from.
@@ -612,65 +601,6 @@ class XmlTestCollectionDefDict(TypedDict):
name: str
JsonTestDatasetDefDict = TypedDict(
"JsonTestDatasetDefDict",
{
"class": Literal["File"],
"path": NotRequired[Optional[str]],
"location": NotRequired[Optional[str]],
"name": NotRequired[Optional[str]],
"dbkey": NotRequired[Optional[str]],
"filetype": NotRequired[Optional[str]],
"composite_data": NotRequired[Optional[List[str]]],
"tags": NotRequired[Optional[List[str]]],
},
)
JsonTestCollectionDefElementDict = Union[
"JsonTestCollectionDefDatasetElementDict", "JsonTestCollectionDefCollectionElementDict"
]
JsonTestCollectionDefDatasetElementDict = TypedDict(
"JsonTestCollectionDefDatasetElementDict",
{
"identifier": str,
"class": Literal["File"],
"path": NotRequired[Optional[str]],
"location": NotRequired[Optional[str]],
"name": NotRequired[Optional[str]],
"dbkey": NotRequired[Optional[str]],
"filetype": NotRequired[Optional[str]],
"composite_data": NotRequired[Optional[List[str]]],
"tags": NotRequired[Optional[List[str]]],
},
)
BaseJsonTestCollectionDefCollectionElementDict = TypedDict(
"BaseJsonTestCollectionDefCollectionElementDict",
{
"class": Literal["Collection"],
"collection_type": str,
"elements": NotRequired[Optional[List[JsonTestCollectionDefElementDict]]],
},
)
JsonTestCollectionDefCollectionElementDict = TypedDict(
"JsonTestCollectionDefCollectionElementDict",
{
"identifier": str,
"class": Literal["Collection"],
"collection_type": str,
"elements": NotRequired[Optional[List[JsonTestCollectionDefElementDict]]],
},
)
JsonTestCollectionDefDict = TypedDict(
"JsonTestCollectionDefDict",
{
"class": Literal["Collection"],
"collection_type": str,
"elements": NotRequired[Optional[List[JsonTestCollectionDefElementDict]]],
"name": NotRequired[Optional[str]],
},
)
def xml_data_input_to_json(xml_input: ToolSourceTestInput) -> Optional["JsonTestDatasetDefDict"]:
attributes = xml_input["attributes"]
value = xml_input["value"]
@@ -913,10 +843,3 @@ class TestCollectionOutputDef:
def to_dict(self):
return dict(name=self.name, attributes=self.attrib, element_tests=self.element_tests, element_count=self.count)
class DrillDownOptionsDict(TypedDict):
name: Optional[str]
value: str
options: List["DrillDownOptionsDict"]
selected: bool
@@ -8,8 +8,7 @@ from typing import (
Optional,
)
from galaxy.util import asbool
from .output_models import (
from galaxy.tool_util_models.tool_outputs import (
DatasetCollectionDescriptionT,
DiscoverViaT,
FilePatternDatasetCollectionDescription as FilePatternDatasetCollectionDescriptionModel,
@@ -17,6 +16,7 @@ from .output_models import (
SortKeyT,
ToolProvidedMetadataDatasetCollection as ToolProvidedMetadataDatasetCollectionModel,
)
from galaxy.util import asbool
from .util import is_dict
DEFAULT_EXTRA_FILENAME_PATTERN = (
+24 -10
View File
@@ -4,6 +4,7 @@ from typing import (
Dict,
List,
Optional,
Sequence,
Type,
TYPE_CHECKING,
Union,
@@ -11,6 +12,16 @@ from typing import (
from typing_extensions import TypedDict
from galaxy.tool_util_models.tool_outputs import (
ToolOutputBoolean as ToolOutputBooleanModel,
ToolOutputCollection as ToolOutputCollectionModel,
ToolOutputCollectionStructure as ToolOutputCollectionStructureModel,
ToolOutputDataset as ToolOutputDataModel,
ToolOutputFloat as ToolOutputFloatModel,
ToolOutputInteger as ToolOutputIntegerModel,
ToolOutputT as ToolOutputModel,
ToolOutputText as ToolOutputTextModel,
)
from galaxy.util import Element
from galaxy.util.dictifiable import Dictifiable
from .output_actions import (
@@ -21,16 +32,9 @@ from .output_collection_def import (
dataset_collector_descriptions_from_output_dict,
DatasetCollectionDescription,
)
from .output_models import (
ToolOutputBoolean as ToolOutputBooleanModel,
ToolOutputCollection as ToolOutputCollectionModel,
ToolOutputCollectionStructure as ToolOutputCollectionStructureModel,
ToolOutputDataset as ToolOutputDataModel,
ToolOutputFloat as ToolOutputFloatModel,
ToolOutputInteger as ToolOutputIntegerModel,
ToolOutputT as ToolOutputModel,
ToolOutputText as ToolOutputTextModel,
)
if TYPE_CHECKING:
from galaxy.tool_util.parser import ToolSource
if TYPE_CHECKING:
from typing_extensions import TypeIs # Supported only under Python >=3.8
@@ -521,3 +525,13 @@ class ToolOutputCollectionPart:
def split_output_name(name):
assert ToolOutputCollectionPart.is_named_collection_part_name(name)
return name.split("|__part__|")
def from_tool_source(tool_source: "ToolSource") -> Sequence[ToolOutputModel]:
tool_outputs, tool_output_collections = tool_source.parse_outputs(None)
outputs = []
for tool_output in tool_outputs.values():
outputs.append(tool_output.to_model())
# for tool_output_collection in tool_output_collections.values():
# outputs.append(tool_output_collection.to_model())
return outputs
@@ -9,467 +9,39 @@ from typing import (
Union,
)
from pydantic import (
BaseModel,
ConfigDict,
Field,
model_validator,
PrivateAttr,
TypeAdapter,
)
from typing_extensions import (
Annotated,
get_args,
Literal,
Protocol,
Self,
)
from typing_extensions import get_args
from galaxy.tool_util_models.parameter_validators import (
AnyValidatorModel,
DatasetMetadataEqualParameterValidatorModel,
DatasetMetadataInDataTableParameterValidatorModel,
DatasetMetadataInFileParameterValidatorModel,
DatasetMetadataInRangeParameterValidatorModel,
DatasetMetadataNotInDataTableParameterValidatorModel,
DatasetOkValidatorParameterValidatorModel,
DiscriminatedAnyValidatorModel,
EmptyDatasetParameterValidatorModel,
EmptyExtraFilesPathParameterValidatorModel,
EmptyFieldParameterValidatorModel,
ExpressionParameterValidatorModel,
InRangeParameterValidatorModel,
LengthParameterValidatorModel,
MetadataParameterValidatorModel,
NoOptionsParameterValidatorModel,
ParameterValidatorModel,
RegexParameterValidatorModel,
SPLIT_DEFAULT,
StaticValidatorModel,
UnspecifiedBuildParameterValidatorModel,
ValidatorType,
ValueInDataTableParameterValidatorModel,
ValueNotInDataTableParameterValidatorModel,
)
from galaxy.util import (
asbool,
Element,
)
try:
import regex
except ImportError:
import re as regex
class ValidationArgument:
doc: Optional[str]
xml_body: bool
xml_allow_json_load: bool
def __init__(
self,
doc: Optional[str],
xml_body: bool = False,
xml_allow_json_load: bool = False,
):
self.doc = doc
self.xml_body = xml_body
self.xml_allow_json_load = xml_allow_json_load
Negate = Annotated[
bool,
ValidationArgument("Negates the result of the validator."),
]
NEGATE_DEFAULT = False
SPLIT_DEFAULT = "\t"
DEFAULT_VALIDATOR_MESSAGE = "Parameter validation error."
ValidatorType = Literal[
"expression",
"regex",
"in_range",
"length",
"metadata",
"dataset_metadata_equal",
"unspecified_build",
"no_options",
"empty_field",
"empty_dataset",
"empty_extra_files_path",
"dataset_metadata_in_data_table",
"dataset_metadata_not_in_data_table",
"dataset_metadata_in_range",
"value_in_data_table",
"value_not_in_data_table",
"dataset_ok_validator",
"dataset_metadata_in_file",
]
class StrictModel(BaseModel):
model_config = ConfigDict(extra="forbid")
class ParameterValidatorModel(StrictModel):
type: ValidatorType
message: Annotated[
Optional[str],
ValidationArgument(
"""The error message displayed on the tool form if validation fails. A placeholder string ``%s`` will be repaced by the ``value``"""
),
] = None
# track validators setup by other input parameters and not validation explicitly
implicit: bool = False
_static: bool = PrivateAttr(False)
_deprecated: bool = PrivateAttr(False)
# validators must be explicitly set as 'safe' to operate as user-defined workflow parameters or to be used
# within future user-defined tool parameters
_safe: bool = PrivateAttr(False)
@model_validator(mode="after")
def set_default_message(self) -> Self:
if self.message is None:
self.message = self.default_message
return self
@property
def default_message(self) -> str:
return DEFAULT_VALIDATOR_MESSAGE
class StaticValidatorModel(ParameterValidatorModel):
_static: bool = PrivateAttr(True)
def statically_validate(self, v: Any) -> None: ...
class ExpressionParameterValidatorModel(StaticValidatorModel):
"""Check if a one line python expression given expression evaluates to True.
The expression is given is the content of the validator tag."""
type: Literal["expression"] = "expression"
negate: Negate = NEGATE_DEFAULT
expression: Annotated[str, ValidationArgument("Python expression to validate.", xml_body=True)]
def statically_validate(self, value: Any) -> None:
ExpressionParameterValidatorModel.expression_validation(self.expression, value, self)
@staticmethod
def ensure_compiled(expression: Union[str, Any]) -> Any:
if isinstance(expression, str):
return compile(expression, "<string>", "eval")
else:
return expression
@staticmethod
def expression_validation(
expression: str, value: Any, validator: "ValidatorDescription", compiled_expression: Optional[Any] = None
):
if compiled_expression is None:
compiled_expression = ExpressionParameterValidatorModel.ensure_compiled(expression)
message = None
try:
evalresult = eval(compiled_expression, dict(value=value))
except Exception:
message = f"Validator '{expression}' could not be evaluated on '{value}'"
evalresult = False
raise_error_if_valiation_fails(bool(evalresult), validator, message=message, value_to_show=value)
@property
def default_message(self) -> str:
return f"Value '%s' does not evaluate to {'True' if not self.negate else 'False'} for '{self.expression}'"
class RegexParameterValidatorModel(StaticValidatorModel):
"""Check if a regular expression **matches** the value, i.e. appears
at the beginning of the value. To enforce a match of the complete value use
``$`` at the end of the expression. The expression is given is the content
of the validator tag. Note that for ``selects`` each option is checked
separately."""
type: Literal["regex"] = "regex"
negate: Negate = NEGATE_DEFAULT
expression: Annotated[str, ValidationArgument("Regular expression to validate against.", xml_body=True)]
_safe: bool = PrivateAttr(True)
@property
def default_message(self) -> str:
return f"Value '%s' does {'not ' if not self.negate else ''}match regular expression '{self.expression.replace('%', '%%')}'"
def statically_validate(self, value: Any) -> None:
if value and not isinstance(value, str):
raise ValueError(f"Wrong type found value {value}")
RegexParameterValidatorModel.regex_validation(self.expression, value, self)
@staticmethod
def regex_validation(expression: str, value: Any, validator: "ValidatorDescription"):
if not isinstance(value, list):
value = [value]
for val in value:
match = regex.match(expression, val or "")
raise_error_if_valiation_fails(match is not None, validator, value_to_show=val)
class InRangeParameterValidatorModel(StaticValidatorModel):
type: Literal["in_range"] = "in_range"
min: Optional[Union[float, int]] = None
max: Optional[Union[float, int]] = None
exclude_min: bool = False
exclude_max: bool = False
negate: Negate = NEGATE_DEFAULT
_safe: bool = PrivateAttr(True)
def statically_validate(self, value: Any):
if isinstance(value, (int, float)):
validates = True
if self.min is not None and value == self.min and self.exclude_min:
validates = False
elif self.min is not None and value < self.min:
validates = False
elif self.max is not None and value == self.max and self.exclude_max:
validates = False
if self.max is not None and value > self.max:
validates = False
raise_error_if_valiation_fails(validates, self)
@property
def default_message(self) -> str:
op1 = "<="
op2 = "<="
if self.exclude_min:
op1 = "<"
if self.exclude_max:
op2 = "<"
min_str = str(self.min) if self.min is not None else "-infinity"
max_str = str(self.max) if self.max is not None else "+infinity"
range_description_str = f"({min_str} {op1} value {op2} {max_str})"
return f"Value ('%s') must {'not ' if self.negate else ''}fulfill {range_description_str}"
class LengthParameterValidatorModel(StaticValidatorModel):
type: Literal["length"] = "length"
min: Optional[int] = None
max: Optional[int] = None
negate: Negate = NEGATE_DEFAULT
_safe: bool = PrivateAttr(True)
def statically_validate(self, value: Any):
if isinstance(value, str):
length = len(value)
validates = True
if self.min is not None and length < self.min:
validates = False
if self.max is not None and length > self.max:
validates = False
raise_error_if_valiation_fails(validates, self)
@property
def default_message(self) -> str:
return f"Must {'not ' if self.negate else ''}have length of at least {self.min} and at most {self.max}"
class MetadataParameterValidatorModel(ParameterValidatorModel):
type: Literal["metadata"] = "metadata"
check: Optional[List[str]] = None
skip: Optional[List[str]] = None
negate: Negate = NEGATE_DEFAULT
@property
def default_message(self) -> str:
check = self.check
skip = self.skip
message = DEFAULT_VALIDATOR_MESSAGE
if not self.negate:
message = "Metadata '%s' missing, click the pencil icon in the history item to edit / save the metadata attributes"
else:
if check:
message = f"""At least one of the checked metadata '{",".join(check)}' is set, click the pencil icon in the history item to edit / save the metadata attributes"""
elif skip:
message = f"""At least one of the non skipped metadata '{",".join(skip)}' is set, click the pencil icon in the history item to edit / save the metadata attributes"""
return message
class DatasetMetadataEqualParameterValidatorModel(ParameterValidatorModel):
type: Literal["dataset_metadata_equal"] = "dataset_metadata_equal"
metadata_name: str
value: Annotated[Any, ValidationArgument("Value to test against", xml_allow_json_load=True)]
negate: Negate = NEGATE_DEFAULT
@property
def default_message(self) -> str:
if not self.negate:
message = f"Metadata value for '{self.metadata_name}' must be '{self.value}', but it is '%s'."
else:
message = f"Metadata value for '{self.metadata_name}' must not be '{self.value}' but it is."
return message
class UnspecifiedBuildParameterValidatorModel(ParameterValidatorModel):
type: Literal["unspecified_build"] = "unspecified_build"
negate: Negate = NEGATE_DEFAULT
@property
def default_message(self) -> str:
return f"{'Unspecified' if not self.negate else 'Specified'} genome build, click the pencil icon in the history item to {'set' if not self.negate else 'remove'} the genome build"
class NoOptionsParameterValidatorModel(StaticValidatorModel):
type: Literal["no_options"] = "no_options"
negate: Negate = NEGATE_DEFAULT
@staticmethod
def no_options_validate(value: Any, validator: "ValidatorDescription"):
raise_error_if_valiation_fails(value is not None, validator)
def statically_validate(self, value: Any) -> None:
NoOptionsParameterValidatorModel.no_options_validate(value, self)
@property
def default_message(self) -> str:
return f"{'No options' if not self.negate else 'Options'} available for selection"
class EmptyFieldParameterValidatorModel(StaticValidatorModel):
type: Literal["empty_field"] = "empty_field"
negate: Negate = NEGATE_DEFAULT
@staticmethod
def empty_validate(value: Any, validator: "ValidatorDescription"):
raise_error_if_valiation_fails((value not in ("", None)), validator)
def statically_validate(self, value: Any) -> None:
EmptyFieldParameterValidatorModel.empty_validate(value, self)
@property
def default_message(self) -> str:
if not self.negate:
message = "Field requires a value"
else:
message = "Field must not set a value"
return message
class EmptyDatasetParameterValidatorModel(ParameterValidatorModel):
type: Literal["empty_dataset"] = "empty_dataset"
negate: Negate = NEGATE_DEFAULT
@property
def default_message(self) -> str:
return f"The selected dataset is {'non-' if self.negate else ''}empty, this tool expects {'non-' if not self.negate else ''}empty files."
class EmptyExtraFilesPathParameterValidatorModel(ParameterValidatorModel):
type: Literal["empty_extra_files_path"] = "empty_extra_files_path"
negate: Negate = NEGATE_DEFAULT
@property
def default_message(self) -> str:
negate = self.negate
return f"The selected dataset's extra_files_path directory is {'non-' if negate else ''}empty or does {'not ' if not negate else ''}exist, this tool expects {'non-' if not negate else ''}empty extra_files_path directories associated with the selected input."
class DatasetMetadataInDataTableParameterValidatorModel(ParameterValidatorModel):
type: Literal["dataset_metadata_in_data_table"] = "dataset_metadata_in_data_table"
table_name: str
metadata_name: str
metadata_column: Union[int, str]
negate: Negate = NEGATE_DEFAULT
@property
def default_message(self) -> str:
return f"Value for metadata {self.metadata_name} was not found in {self.table_name}."
class DatasetMetadataNotInDataTableParameterValidatorModel(ParameterValidatorModel):
type: Literal["dataset_metadata_not_in_data_table"] = "dataset_metadata_not_in_data_table"
table_name: str
metadata_name: str
metadata_column: Union[int, str]
negate: Negate = NEGATE_DEFAULT
@property
def default_message(self) -> str:
return f"Value for metadata {self.metadata_name} was not found in {self.table_name}."
class DatasetMetadataInRangeParameterValidatorModel(ParameterValidatorModel):
type: Literal["dataset_metadata_in_range"] = "dataset_metadata_in_range"
metadata_name: str
min: Optional[Union[float, int]] = None
max: Optional[Union[float, int]] = None
exclude_min: bool = False
exclude_max: bool = False
negate: Negate = NEGATE_DEFAULT
@property
def default_message(self) -> str:
op1 = "<="
op2 = "<="
if self.exclude_min:
op1 = "<"
if self.exclude_max:
op2 = "<"
range_description_str = f"({self.min} {op1} value {op2} {self.max})"
return f"Value ('%s') must {'not ' if self.negate else ''}fulfill {range_description_str}"
class ValueInDataTableParameterValidatorModel(ParameterValidatorModel):
type: Literal["value_in_data_table"] = "value_in_data_table"
table_name: str
metadata_column: Union[int, str]
negate: Negate = NEGATE_DEFAULT
@property
def default_message(self) -> str:
return "Value for metadata not found."
class ValueNotInDataTableParameterValidatorModel(ParameterValidatorModel):
type: Literal["value_not_in_data_table"] = "value_not_in_data_table"
table_name: str
metadata_column: Union[int, str]
negate: Negate = NEGATE_DEFAULT
@property
def default_message(self) -> str:
return f"Value was not found in {self.table_name}."
class DatasetOkValidatorParameterValidatorModel(ParameterValidatorModel):
type: Literal["dataset_ok_validator"] = "dataset_ok_validator"
negate: Negate = NEGATE_DEFAULT
@property
def default_message(self) -> str:
if not self.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"
return message
class DatasetMetadataInFileParameterValidatorModel(ParameterValidatorModel):
type: Literal["dataset_metadata_in_file"] = "dataset_metadata_in_file"
filename: str
metadata_name: str
metadata_column: Union[int, str]
line_startswith: Optional[str] = None
split: str = SPLIT_DEFAULT
negate: Negate = NEGATE_DEFAULT
_deprecated: bool = PrivateAttr(True)
@property
def default_message(self) -> str:
return f"Value for metadata {self.metadata_name} was not found in {self.filename}."
AnyValidatorModel = Annotated[
Union[
ExpressionParameterValidatorModel,
RegexParameterValidatorModel,
InRangeParameterValidatorModel,
LengthParameterValidatorModel,
MetadataParameterValidatorModel,
DatasetMetadataEqualParameterValidatorModel,
UnspecifiedBuildParameterValidatorModel,
NoOptionsParameterValidatorModel,
EmptyFieldParameterValidatorModel,
EmptyDatasetParameterValidatorModel,
EmptyExtraFilesPathParameterValidatorModel,
DatasetMetadataInDataTableParameterValidatorModel,
DatasetMetadataNotInDataTableParameterValidatorModel,
DatasetMetadataInRangeParameterValidatorModel,
ValueInDataTableParameterValidatorModel,
ValueNotInDataTableParameterValidatorModel,
DatasetOkValidatorParameterValidatorModel,
DatasetMetadataInFileParameterValidatorModel,
],
Field(discriminator="type"),
]
DiscriminatedAnyValidatorModel = TypeAdapter(AnyValidatorModel) # type:ignore[var-annotated]
def parse_dict_validators(validator_dicts: List[Dict[str, Any]], trusted: bool) -> List[AnyValidatorModel]:
validator_models = []
@@ -651,34 +223,6 @@ def parse_xml_validator(validator_el: Element) -> AnyValidatorModel:
raise ValueError(f"Unhandled 'type' attribute in validator {validator_type}")
class ValidatorDescription(Protocol):
@property
def negate(self) -> bool: ...
@property
def message(self) -> Optional[str]: ...
def raise_error_if_valiation_fails(
value: bool, validator: ValidatorDescription, message: Optional[str] = None, value_to_show: Optional[str] = None
):
if not isinstance(value, bool):
raise AssertionError("Validator logic problem - computed validation value must be boolean")
if message is None:
message = validator.message
if not message:
message = DEFAULT_VALIDATOR_MESSAGE
assert message is not None
if value_to_show and "%s" in message:
message = message % value_to_show
negate = validator.negate
if (not negate and value) or (negate and not value):
return
else:
raise ValueError(message)
def _parse_message(xml_el: Element) -> Optional[str]:
message = xml_el.get("message")
return message
+9 -9
View File
@@ -28,6 +28,14 @@ from galaxy.tool_util.parser.util import (
DEFAULT_PIN_LABELS,
DEFAULT_SORT,
)
from galaxy.tool_util_models.parameter_validators import AnyValidatorModel
from galaxy.tool_util_models.tool_source import (
Citation,
DrillDownOptionsDict,
HelpContent,
OutputCompareType,
XrefDict,
)
from galaxy.util import (
Element,
ElementTree,
@@ -40,13 +48,9 @@ from galaxy.util import (
)
from .interface import (
AssertionList,
Citation,
DrillDownDynamicOptions,
DrillDownOptionsDict,
DynamicOptions,
HelpContent,
InputSource,
OutputCompareType,
PageSource,
PagesSource,
RequiredFiles,
@@ -63,7 +67,6 @@ from .interface import (
ToolSourceTestOutputs,
ToolSourceTests,
XmlTestCollectionDefDict,
XrefDict,
)
from .output_actions import (
ToolOutputActionApp,
@@ -77,10 +80,7 @@ from .output_objects import (
ToolOutputCollection,
ToolOutputCollectionStructure,
)
from .parameter_validators import (
AnyValidatorModel,
parse_xml_validators,
)
from .parameter_validators import parse_xml_validators
from .stdio import (
aggressive_error_checks,
error_on_exit_code,
+6 -6
View File
@@ -16,17 +16,20 @@ from galaxy.tool_util.parser.util import (
DEFAULT_DELTA_FRAC,
DEFAULT_SORT,
)
from galaxy.tool_util_models.parameter_validators import AnyValidatorModel
from galaxy.tool_util_models.tool_source import (
HelpContent,
XrefDict,
)
from .interface import (
AssertionDict,
AssertionList,
HelpContent,
InputSource,
PageSource,
PagesSource,
ToolSource,
ToolSourceTest,
ToolSourceTests,
XrefDict,
)
from .output_actions import ToolOutputActionApp
from .output_collection_def import dataset_collector_descriptions_from_output_dict
@@ -35,10 +38,7 @@ from .output_objects import (
ToolOutputCollection,
ToolOutputCollectionStructure,
)
from .parameter_validators import (
AnyValidatorModel,
parse_dict_validators,
)
from .parameter_validators import parse_dict_validators
from .stdio import error_on_exit_code
from .util import is_dict
+1 -1
View File
@@ -5,7 +5,7 @@ import sys
import yaml
from galaxy.tool_util.models import Tests
from galaxy.tool_util_models import Tests
DESCRIPTION = """
A small utility to verify the Planemo test format.
+1 -1
View File
@@ -30,12 +30,12 @@ from galaxy.tool_util.parser.util import (
parse_tool_version_with_defaults,
)
from galaxy.tool_util.parser.xml import __parse_assert_list_from_elem
from galaxy.tool_util.verify.assertion_models import relaxed_assertion_list
from galaxy.tool_util.verify.interactor import (
InvalidToolTestDict,
ToolTestDescription,
ValidToolTestDict,
)
from galaxy.tool_util_models.assertions import relaxed_assertion_list
from galaxy.util import (
string_as_bool,
string_as_bool_or_none,
@@ -9,8 +9,6 @@ from typing import (
Dict,
List,
Optional,
Type,
TypeVar,
Union,
)
@@ -29,22 +27,19 @@ from typing_extensions import (
TypedDict,
)
from .assertions import assertions
from .parameters import (
input_models_for_tool_source,
ToolParameterT,
)
from .parser.interface import (
from .tool_outputs import (
ToolOutput,
)
from .tool_source import (
Citation,
HelpContent,
OutputCompareType,
ToolSource,
XrefDict,
)
from .parser.output_models import (
from_tool_source,
ToolOutput,
)
from .verify.assertion_models import assertions
class ParsedTool(BaseModel):
@@ -63,45 +58,6 @@ class ParsedTool(BaseModel):
help: Optional[HelpContent]
def parse_tool(tool_source: ToolSource) -> ParsedTool:
return parse_tool_custom(tool_source, ParsedTool)
P = TypeVar("P", bound=ParsedTool)
def parse_tool_custom(tool_source: ToolSource, model_type: Type[P]) -> P:
id = tool_source.parse_id()
version = tool_source.parse_version()
name = tool_source.parse_name()
description = tool_source.parse_description()
inputs = input_models_for_tool_source(tool_source).parameters
outputs = from_tool_source(tool_source)
citations = tool_source.parse_citations()
license = tool_source.parse_license()
profile = tool_source.parse_profile()
edam_operations = tool_source.parse_edam_operations()
edam_topics = tool_source.parse_edam_topics()
xrefs = tool_source.parse_xrefs()
help = tool_source.parse_help()
return model_type(
id=id,
version=version,
name=name,
description=description,
profile=profile,
inputs=inputs,
outputs=outputs,
license=license,
citations=citations,
edam_operations=edam_operations,
edam_topics=edam_topics,
xrefs=xrefs,
help=help,
)
class StrictModel(BaseModel):
model_config = ConfigDict(
@@ -0,0 +1,517 @@
from typing import (
Any,
List,
Optional,
Union,
)
from pydantic import (
BaseModel,
ConfigDict,
Field,
model_validator,
PrivateAttr,
TypeAdapter,
)
from typing_extensions import (
Annotated,
Literal,
Protocol,
Self,
)
try:
import regex
except ImportError:
import re as regex
class ValidationArgument:
doc: Optional[str]
xml_body: bool
xml_allow_json_load: bool
def __init__(
self,
doc: Optional[str],
xml_body: bool = False,
xml_allow_json_load: bool = False,
):
self.doc = doc
self.xml_body = xml_body
self.xml_allow_json_load = xml_allow_json_load
Negate = Annotated[
bool,
ValidationArgument("Negates the result of the validator."),
]
NEGATE_DEFAULT = False
SPLIT_DEFAULT = "\t"
DEFAULT_VALIDATOR_MESSAGE = "Parameter validation error."
ValidatorType = Literal[
"expression",
"regex",
"in_range",
"length",
"metadata",
"dataset_metadata_equal",
"unspecified_build",
"no_options",
"empty_field",
"empty_dataset",
"empty_extra_files_path",
"dataset_metadata_in_data_table",
"dataset_metadata_not_in_data_table",
"dataset_metadata_in_range",
"value_in_data_table",
"value_not_in_data_table",
"dataset_ok_validator",
"dataset_metadata_in_file",
]
class ValidatorDescription(Protocol):
@property
def negate(self) -> bool: ...
@property
def message(self) -> Optional[str]: ...
class StrictModel(BaseModel):
model_config = ConfigDict(extra="forbid")
class ParameterValidatorModel(StrictModel):
type: ValidatorType
message: Annotated[
Optional[str],
ValidationArgument(
"""The error message displayed on the tool form if validation fails. A placeholder string ``%s`` will be repaced by the ``value``"""
),
] = None
# track validators setup by other input parameters and not validation explicitly
implicit: bool = False
_static: bool = PrivateAttr(False)
_deprecated: bool = PrivateAttr(False)
# validators must be explicitly set as 'safe' to operate as user-defined workflow parameters or to be used
# within future user-defined tool parameters
_safe: bool = PrivateAttr(False)
@model_validator(mode="after")
def set_default_message(self) -> Self:
if self.message is None:
self.message = self.default_message
return self
@property
def default_message(self) -> str:
return DEFAULT_VALIDATOR_MESSAGE
class StaticValidatorModel(ParameterValidatorModel):
_static: bool = PrivateAttr(True)
def statically_validate(self, v: Any) -> None: ...
class ExpressionParameterValidatorModel(StaticValidatorModel):
"""Check if a one line python expression given expression evaluates to True.
The expression is given is the content of the validator tag."""
type: Literal["expression"] = "expression"
negate: Negate = NEGATE_DEFAULT
expression: Annotated[str, ValidationArgument("Python expression to validate.", xml_body=True)]
def statically_validate(self, value: Any) -> None:
ExpressionParameterValidatorModel.expression_validation(self.expression, value, self)
@staticmethod
def ensure_compiled(expression: Union[str, Any]) -> Any:
if isinstance(expression, str):
return compile(expression, "<string>", "eval")
else:
return expression
@staticmethod
def expression_validation(
expression: str, value: Any, validator: "ValidatorDescription", compiled_expression: Optional[Any] = None
):
if compiled_expression is None:
compiled_expression = ExpressionParameterValidatorModel.ensure_compiled(expression)
message = None
try:
evalresult = eval(compiled_expression, dict(value=value))
except Exception:
message = f"Validator '{expression}' could not be evaluated on '{value}'"
evalresult = False
raise_error_if_validation_fails(bool(evalresult), validator, message=message, value_to_show=value)
@property
def default_message(self) -> str:
return f"Value '%s' does not evaluate to {'True' if not self.negate else 'False'} for '{self.expression}'"
class RegexParameterValidatorModel(StaticValidatorModel):
"""Check if a regular expression **matches** the value, i.e. appears
at the beginning of the value. To enforce a match of the complete value use
``$`` at the end of the expression. The expression is given is the content
of the validator tag. Note that for ``selects`` each option is checked
separately."""
type: Literal["regex"] = "regex"
negate: Negate = NEGATE_DEFAULT
expression: Annotated[str, ValidationArgument("Regular expression to validate against.", xml_body=True)]
_safe: bool = PrivateAttr(True)
@property
def default_message(self) -> str:
return f"Value '%s' does {'not ' if not self.negate else ''}match regular expression '{self.expression.replace('%', '%%')}'"
def statically_validate(self, value: Any) -> None:
if value and not isinstance(value, str):
raise ValueError(f"Wrong type found value {value}")
RegexParameterValidatorModel.regex_validation(self.expression, value, self)
@staticmethod
def regex_validation(expression: str, value: Any, validator: "ValidatorDescription"):
if not isinstance(value, list):
value = [value]
for val in value:
match = regex.match(expression, val or "")
raise_error_if_validation_fails(match is not None, validator, value_to_show=val)
class InRangeParameterValidatorModel(StaticValidatorModel):
type: Literal["in_range"] = "in_range"
min: Optional[Union[float, int]] = None
max: Optional[Union[float, int]] = None
exclude_min: bool = False
exclude_max: bool = False
negate: Negate = NEGATE_DEFAULT
_safe: bool = PrivateAttr(True)
def statically_validate(self, value: Any):
if isinstance(value, (int, float)):
validates = True
if self.min is not None and value == self.min and self.exclude_min:
validates = False
elif self.min is not None and value < self.min:
validates = False
elif self.max is not None and value == self.max and self.exclude_max:
validates = False
if self.max is not None and value > self.max:
validates = False
raise_error_if_validation_fails(validates, self)
@property
def default_message(self) -> str:
op1 = "<="
op2 = "<="
if self.exclude_min:
op1 = "<"
if self.exclude_max:
op2 = "<"
min_str = str(self.min) if self.min is not None else "-infinity"
max_str = str(self.max) if self.max is not None else "+infinity"
range_description_str = f"({min_str} {op1} value {op2} {max_str})"
return f"Value ('%s') must {'not ' if self.negate else ''}fulfill {range_description_str}"
class LengthParameterValidatorModel(StaticValidatorModel):
type: Literal["length"] = "length"
min: Optional[int] = None
max: Optional[int] = None
negate: Negate = NEGATE_DEFAULT
_safe: bool = PrivateAttr(True)
def statically_validate(self, value: Any):
if isinstance(value, str):
length = len(value)
validates = True
if self.min is not None and length < self.min:
validates = False
if self.max is not None and length > self.max:
validates = False
raise_error_if_validation_fails(validates, self)
@property
def default_message(self) -> str:
return f"Must {'not ' if self.negate else ''}have length of at least {self.min} and at most {self.max}"
class MetadataParameterValidatorModel(ParameterValidatorModel):
type: Literal["metadata"] = "metadata"
check: Optional[List[str]] = None
skip: Optional[List[str]] = None
negate: Negate = NEGATE_DEFAULT
@property
def default_message(self) -> str:
check = self.check
skip = self.skip
message = DEFAULT_VALIDATOR_MESSAGE
if not self.negate:
message = "Metadata '%s' missing, click the pencil icon in the history item to edit / save the metadata attributes"
else:
if check:
message = f"""At least one of the checked metadata '{",".join(check)}' is set, click the pencil icon in the history item to edit / save the metadata attributes"""
elif skip:
message = f"""At least one of the non skipped metadata '{",".join(skip)}' is set, click the pencil icon in the history item to edit / save the metadata attributes"""
return message
class DatasetMetadataEqualParameterValidatorModel(ParameterValidatorModel):
type: Literal["dataset_metadata_equal"] = "dataset_metadata_equal"
metadata_name: str
value: Annotated[Any, ValidationArgument("Value to test against", xml_allow_json_load=True)]
negate: Negate = NEGATE_DEFAULT
@property
def default_message(self) -> str:
if not self.negate:
message = f"Metadata value for '{self.metadata_name}' must be '{self.value}', but it is '%s'."
else:
message = f"Metadata value for '{self.metadata_name}' must not be '{self.value}' but it is."
return message
class UnspecifiedBuildParameterValidatorModel(ParameterValidatorModel):
type: Literal["unspecified_build"] = "unspecified_build"
negate: Negate = NEGATE_DEFAULT
@property
def default_message(self) -> str:
return f"{'Unspecified' if not self.negate else 'Specified'} genome build, click the pencil icon in the history item to {'set' if not self.negate else 'remove'} the genome build"
class NoOptionsParameterValidatorModel(StaticValidatorModel):
type: Literal["no_options"] = "no_options"
negate: Negate = NEGATE_DEFAULT
@staticmethod
def no_options_validate(value: Any, validator: "ValidatorDescription"):
raise_error_if_validation_fails(value is not None, validator)
def statically_validate(self, value: Any) -> None:
NoOptionsParameterValidatorModel.no_options_validate(value, self)
@property
def default_message(self) -> str:
return f"{'No options' if not self.negate else 'Options'} available for selection"
class EmptyFieldParameterValidatorModel(StaticValidatorModel):
type: Literal["empty_field"] = "empty_field"
negate: Negate = NEGATE_DEFAULT
@staticmethod
def empty_validate(value: Any, validator: "ValidatorDescription"):
raise_error_if_validation_fails((value not in ("", None)), validator)
def statically_validate(self, value: Any) -> None:
EmptyFieldParameterValidatorModel.empty_validate(value, self)
@property
def default_message(self) -> str:
if not self.negate:
message = "Field requires a value"
else:
message = "Field must not set a value"
return message
class EmptyDatasetParameterValidatorModel(ParameterValidatorModel):
type: Literal["empty_dataset"] = "empty_dataset"
negate: Negate = NEGATE_DEFAULT
@property
def default_message(self) -> str:
return f"The selected dataset is {'non-' if self.negate else ''}empty, this tool expects {'non-' if not self.negate else ''}empty files."
class EmptyExtraFilesPathParameterValidatorModel(ParameterValidatorModel):
type: Literal["empty_extra_files_path"] = "empty_extra_files_path"
negate: Negate = NEGATE_DEFAULT
@property
def default_message(self) -> str:
negate = self.negate
return f"The selected dataset's extra_files_path directory is {'non-' if negate else ''}empty or does {'not ' if not negate else ''}exist, this tool expects {'non-' if not negate else ''}empty extra_files_path directories associated with the selected input."
class DatasetMetadataInDataTableParameterValidatorModel(ParameterValidatorModel):
type: Literal["dataset_metadata_in_data_table"] = "dataset_metadata_in_data_table"
table_name: str
metadata_name: str
metadata_column: Union[int, str]
negate: Negate = NEGATE_DEFAULT
@property
def default_message(self) -> str:
return f"Value for metadata {self.metadata_name} was not found in {self.table_name}."
class DatasetMetadataNotInDataTableParameterValidatorModel(ParameterValidatorModel):
type: Literal["dataset_metadata_not_in_data_table"] = "dataset_metadata_not_in_data_table"
table_name: str
metadata_name: str
metadata_column: Union[int, str]
negate: Negate = NEGATE_DEFAULT
@property
def default_message(self) -> str:
return f"Value for metadata {self.metadata_name} was not found in {self.table_name}."
class DatasetMetadataInRangeParameterValidatorModel(ParameterValidatorModel):
type: Literal["dataset_metadata_in_range"] = "dataset_metadata_in_range"
metadata_name: str
min: Optional[Union[float, int]] = None
max: Optional[Union[float, int]] = None
exclude_min: bool = False
exclude_max: bool = False
negate: Negate = NEGATE_DEFAULT
@property
def default_message(self) -> str:
op1 = "<="
op2 = "<="
if self.exclude_min:
op1 = "<"
if self.exclude_max:
op2 = "<"
range_description_str = f"({self.min} {op1} value {op2} {self.max})"
return f"Value ('%s') must {'not ' if self.negate else ''}fulfill {range_description_str}"
class ValueInDataTableParameterValidatorModel(ParameterValidatorModel):
type: Literal["value_in_data_table"] = "value_in_data_table"
table_name: str
metadata_column: Union[int, str]
negate: Negate = NEGATE_DEFAULT
@property
def default_message(self) -> str:
return "Value for metadata not found."
class ValueNotInDataTableParameterValidatorModel(ParameterValidatorModel):
type: Literal["value_not_in_data_table"] = "value_not_in_data_table"
table_name: str
metadata_column: Union[int, str]
negate: Negate = NEGATE_DEFAULT
@property
def default_message(self) -> str:
return f"Value was not found in {self.table_name}."
class DatasetOkValidatorParameterValidatorModel(ParameterValidatorModel):
type: Literal["dataset_ok_validator"] = "dataset_ok_validator"
negate: Negate = NEGATE_DEFAULT
@property
def default_message(self) -> str:
if not self.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"
return message
class DatasetMetadataInFileParameterValidatorModel(ParameterValidatorModel):
type: Literal["dataset_metadata_in_file"] = "dataset_metadata_in_file"
filename: str
metadata_name: str
metadata_column: Union[int, str]
line_startswith: Optional[str] = None
split: str = SPLIT_DEFAULT
negate: Negate = NEGATE_DEFAULT
_deprecated: bool = PrivateAttr(True)
@property
def default_message(self) -> str:
return f"Value for metadata {self.metadata_name} was not found in {self.filename}."
AnyValidatorModel = Annotated[
Union[
ExpressionParameterValidatorModel,
RegexParameterValidatorModel,
InRangeParameterValidatorModel,
LengthParameterValidatorModel,
MetadataParameterValidatorModel,
DatasetMetadataEqualParameterValidatorModel,
UnspecifiedBuildParameterValidatorModel,
NoOptionsParameterValidatorModel,
EmptyFieldParameterValidatorModel,
EmptyDatasetParameterValidatorModel,
EmptyExtraFilesPathParameterValidatorModel,
DatasetMetadataInDataTableParameterValidatorModel,
DatasetMetadataNotInDataTableParameterValidatorModel,
DatasetMetadataInRangeParameterValidatorModel,
ValueInDataTableParameterValidatorModel,
ValueNotInDataTableParameterValidatorModel,
DatasetOkValidatorParameterValidatorModel,
DatasetMetadataInFileParameterValidatorModel,
],
Field(discriminator="type"),
]
DiscriminatedAnyValidatorModel = TypeAdapter(AnyValidatorModel) # type:ignore[var-annotated]
def raise_error_if_validation_fails(
value: bool, validator: ValidatorDescription, message: Optional[str] = None, value_to_show: Optional[str] = None
):
if not isinstance(value, bool):
raise AssertionError("Validator logic problem - computed validation value must be boolean")
if message is None:
message = validator.message
if not message:
message = DEFAULT_VALIDATOR_MESSAGE
assert message is not None
if value_to_show and "%s" in message:
message = message % value_to_show
negate = validator.negate
if (not negate and value) or (negate and not value):
return
else:
raise ValueError(message)
__all__ = (
"AnyValidatorModel",
"DiscriminatedAnyValidatorModel",
"ValidatorType",
"ParameterValidatorModel",
"SPLIT_DEFAULT",
"StaticValidatorModel",
"ExpressionParameterValidatorModel",
"RegexParameterValidatorModel",
"InRangeParameterValidatorModel",
"LengthParameterValidatorModel",
"MetadataParameterValidatorModel",
"DatasetMetadataEqualParameterValidatorModel",
"UnspecifiedBuildParameterValidatorModel",
"NoOptionsParameterValidatorModel",
"EmptyFieldParameterValidatorModel",
"EmptyDatasetParameterValidatorModel",
"EmptyExtraFilesPathParameterValidatorModel",
"DatasetMetadataInDataTableParameterValidatorModel",
"DatasetMetadataNotInDataTableParameterValidatorModel",
"DatasetMetadataInRangeParameterValidatorModel",
"ValueInDataTableParameterValidatorModel",
"ValueNotInDataTableParameterValidatorModel",
"DatasetOkValidatorParameterValidatorModel",
"DatasetMetadataInFileParameterValidatorModel",
)
@@ -33,7 +33,6 @@ from pydantic import (
StrictInt,
StrictStr,
Tag,
ValidationError,
)
from typing_extensions import (
Annotated,
@@ -41,21 +40,6 @@ from typing_extensions import (
Protocol,
)
from galaxy.exceptions import RequestParameterInvalidException
from galaxy.tool_util.parser.interface import (
DrillDownOptionsDict,
JsonTestCollectionDefDict,
JsonTestDatasetDefDict,
)
from galaxy.tool_util.parser.parameter_validators import (
EmptyFieldParameterValidatorModel,
ExpressionParameterValidatorModel,
InRangeParameterValidatorModel,
LengthParameterValidatorModel,
NoOptionsParameterValidatorModel,
RegexParameterValidatorModel,
StaticValidatorModel,
)
from ._types import (
cast_as_type,
expand_annotation,
@@ -65,6 +49,20 @@ from ._types import (
optional_if_needed,
union_type,
)
from .parameter_validators import (
EmptyFieldParameterValidatorModel,
ExpressionParameterValidatorModel,
InRangeParameterValidatorModel,
LengthParameterValidatorModel,
NoOptionsParameterValidatorModel,
RegexParameterValidatorModel,
StaticValidatorModel,
)
from .tool_source import (
DrillDownOptionsDict,
JsonTestCollectionDefDict,
JsonTestDatasetDefDict,
)
# TODO:
# - implement data_ref on rules and implement some cross model validation
@@ -1543,38 +1541,3 @@ def create_field_model(
def _is_landing_request(state_representation: StateRepresentationT):
return state_representation in ["landing_request", "landing_request_internal"]
def validate_against_model(pydantic_model: Type[BaseModel], parameter_state: Dict[str, Any]) -> None:
try:
pydantic_model(**parameter_state)
except ValidationError as e:
# TODO: Improve this or maybe add a handler for this in the FastAPI exception
# handler.
raise RequestParameterInvalidException(str(e))
class ValidationFunctionT(Protocol):
def __call__(self, tool: ToolParameterBundle, request: RawStateDict, name: Optional[str] = None) -> None: ...
def validate_model_type_factory(state_representation: StateRepresentationT) -> ValidationFunctionT:
def validate_request(tool: ToolParameterBundle, request: Dict[str, Any], name: Optional[str] = None) -> None:
name = name or DEFAULT_MODEL_NAME
pydantic_model = create_field_model(tool.parameters, name=name, state_representation=state_representation)
validate_against_model(pydantic_model, request)
return validate_request
validate_request = validate_model_type_factory("request")
validate_internal_request = validate_model_type_factory("request_internal")
validate_internal_request_dereferenced = validate_model_type_factory("request_internal_dereferenced")
validate_landing_request = validate_model_type_factory("landing_request")
validate_internal_landing_request = validate_model_type_factory("landing_request_internal")
validate_internal_job = validate_model_type_factory("job_internal")
validate_test_case = validate_model_type_factory("test_case_xml")
validate_workflow_step = validate_model_type_factory("workflow_step")
validate_workflow_step_linked = validate_model_type_factory("workflow_step_linked")
@@ -8,7 +8,6 @@ code where actual tool objects aren't created.
from typing import (
List,
Optional,
Sequence,
Union,
)
@@ -21,8 +20,6 @@ from typing_extensions import (
Literal,
)
from .interface import ToolSource
class ToolOutputBaseModel(BaseModel):
name: str
@@ -104,13 +101,3 @@ ToolOutputT = Union[
ToolOutputDataset, ToolOutputCollection, ToolOutputText, ToolOutputInteger, ToolOutputFloat, ToolOutputBoolean
]
ToolOutput = Annotated[ToolOutputT, Field(discriminator="type")]
def from_tool_source(tool_source: ToolSource) -> Sequence[ToolOutput]:
tool_outputs, tool_output_collections = tool_source.parse_outputs(None)
outputs = []
for tool_output in tool_outputs.values():
outputs.append(tool_output.to_model())
# for tool_output_collection in tool_output_collections.values():
# outputs.append(tool_output_collection.to_model())
return outputs
+107
View File
@@ -0,0 +1,107 @@
from enum import Enum
from typing import (
List,
Optional,
Union,
)
from pydantic import BaseModel
from typing_extensions import (
Literal,
NotRequired,
TypedDict,
)
class XrefDict(TypedDict):
value: str
reftype: str
class Citation(BaseModel):
type: str
content: str
class HelpContent(BaseModel):
format: Literal["restructuredtext", "plain_text", "markdown"]
content: str
class OutputCompareType(str, Enum):
diff = "diff"
re_match = "re_match"
sim_size = "sim_size"
re_match_multiline = "re_match_multiline"
contains = "contains"
image_diff = "image_diff"
class DrillDownOptionsDict(TypedDict):
name: Optional[str]
value: str
options: List["DrillDownOptionsDict"]
selected: bool
JsonTestDatasetDefDict = TypedDict(
"JsonTestDatasetDefDict",
{
"class": Literal["File"],
"path": NotRequired[Optional[str]],
"location": NotRequired[Optional[str]],
"name": NotRequired[Optional[str]],
"dbkey": NotRequired[Optional[str]],
"filetype": NotRequired[Optional[str]],
"composite_data": NotRequired[Optional[List[str]]],
"tags": NotRequired[Optional[List[str]]],
},
)
JsonTestCollectionDefElementDict = Union[
"JsonTestCollectionDefDatasetElementDict", "JsonTestCollectionDefCollectionElementDict"
]
JsonTestCollectionDefDatasetElementDict = TypedDict(
"JsonTestCollectionDefDatasetElementDict",
{
"identifier": str,
"class": Literal["File"],
"path": NotRequired[Optional[str]],
"location": NotRequired[Optional[str]],
"name": NotRequired[Optional[str]],
"dbkey": NotRequired[Optional[str]],
"filetype": NotRequired[Optional[str]],
"composite_data": NotRequired[Optional[List[str]]],
"tags": NotRequired[Optional[List[str]]],
},
)
BaseJsonTestCollectionDefCollectionElementDict = TypedDict(
"BaseJsonTestCollectionDefCollectionElementDict",
{
"class": Literal["Collection"],
"collection_type": str,
"elements": NotRequired[Optional[List[JsonTestCollectionDefElementDict]]],
},
)
JsonTestCollectionDefCollectionElementDict = TypedDict(
"JsonTestCollectionDefCollectionElementDict",
{
"identifier": str,
"class": Literal["Collection"],
"collection_type": str,
"elements": NotRequired[Optional[List[JsonTestCollectionDefElementDict]]],
},
)
JsonTestCollectionDefDict = TypedDict(
"JsonTestCollectionDefDict",
{
"class": Literal["Collection"],
"collection_type": str,
"elements": NotRequired[Optional[List[JsonTestCollectionDefElementDict]]],
"name": NotRequired[Optional[str]],
},
)
+1 -1
View File
@@ -84,7 +84,6 @@ from galaxy.tool_util.parser import (
ToolOutputCollectionPart,
)
from galaxy.tool_util.parser.interface import (
HelpContent,
InputSource,
PageSource,
ToolSource,
@@ -113,6 +112,7 @@ from galaxy.tool_util.version import (
LegacyVersion,
parse_version,
)
from galaxy.tool_util_models.tool_source import HelpContent
from galaxy.tools import expressions
from galaxy.tools.actions import (
DefaultToolAction,
+1 -1
View File
@@ -45,7 +45,6 @@ from galaxy.model.dataset_collections import builder
from galaxy.schema.fetch_data import FilesPayload
from galaxy.tool_util.parameters.factory import get_color_value
from galaxy.tool_util.parser import get_input_source as ensure_input_source
from galaxy.tool_util.parser.interface import DrillDownOptionsDict
from galaxy.tool_util.parser.util import (
boolean_is_checked,
boolean_true_and_false_values,
@@ -53,6 +52,7 @@ from galaxy.tool_util.parser.util import (
ParameterParseException,
text_input_is_optional,
)
from galaxy.tool_util_models.tool_source import DrillDownOptionsDict
from galaxy.tools.parameters.options import ParameterOption
from galaxy.tools.parameters.workflow_utils import (
NO_REPLACEMENT,
+5 -3
View File
@@ -18,13 +18,15 @@ from galaxy import (
util,
)
from galaxy.tool_util.parser.parameter_validators import (
parse_xml_validators as parse_xml_validators_models,
)
from galaxy.tool_util_models.parameter_validators import (
AnyValidatorModel,
EmptyFieldParameterValidatorModel,
ExpressionParameterValidatorModel,
InRangeParameterValidatorModel,
MetadataParameterValidatorModel,
parse_xml_validators as parse_xml_validators_models,
raise_error_if_valiation_fails,
raise_error_if_validation_fails,
RegexParameterValidatorModel,
)
@@ -62,7 +64,7 @@ class Validator(abc.ABC):
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)
raise_error_if_validation_fails(value, self, message=message, value_to_show=value_to_show)
class RegexValidator(Validator):
@@ -8,12 +8,6 @@ import requests
import yaml
from gxformat2.yaml import ordered_load
from galaxy.tool_util.models import (
OutputChecks,
OutputsDict,
TestDicts,
TestJobDict,
)
from galaxy.tool_util.parser.interface import TestCollectionOutputDef
from galaxy.tool_util.verify import verify_file_contents_against_dict
from galaxy.tool_util.verify.interactor import (
@@ -21,6 +15,12 @@ from galaxy.tool_util.verify.interactor import (
get_metadata_to_test,
verify_collection,
)
from galaxy.tool_util_models import (
OutputChecks,
OutputsDict,
TestDicts,
TestJobDict,
)
from galaxy.util import asbool
from galaxy_test.api._framework import ApiTestCase
from galaxy_test.base.populators import (
+3 -15
View File
@@ -21,10 +21,7 @@ from galaxy.tool_shed.util.hg_util import (
clone_repository,
get_changectx_for_changeset,
)
from galaxy.tool_util.models import (
parse_tool_custom,
ParsedTool,
)
from galaxy.tool_util.model_factory import parse_tool_custom
from galaxy.tool_util.parser import (
get_tool_source,
ToolSource,
@@ -38,22 +35,13 @@ from tool_shed.context import (
from tool_shed.util.common_util import generate_clone_url_for
from tool_shed.webapp.model import RepositoryMetadata
from tool_shed.webapp.search.tool_search import ToolSearch
from tool_shed_client.schema import RepositoryRevisionMetadata
from tool_shed_client.schema import ShedParsedTool
from .repositories import get_repository_revision_metadata_model
from .trs import trs_tool_id_to_repository_metadata
STOCK_TOOL_SOURCES: Optional[Dict[str, Dict[str, ToolSource]]] = None
class ShedParsedTool(ParsedTool):
repository_revision: Optional[RepositoryRevisionMetadata] = None
def parse_tool(tool_source: ToolSource) -> ShedParsedTool:
parsed_tool = parse_tool_custom(tool_source, ShedParsedTool)
return parsed_tool
def search(trans: SessionRequestContext, q: str, page: int = 1, page_size: int = 10) -> dict:
"""
Perform the search over TS tools index.
@@ -131,7 +119,7 @@ def parsed_tool_model_for(
tool_source, repository_metadata = tool_source_for(
trans, trs_tool_id, tool_version, repository_clone_url=repository_clone_url
)
parsed_tool = parse_tool(tool_source)
parsed_tool = parse_tool_custom(tool_source, ShedParsedTool)
if repository_metadata:
revision_model = get_repository_revision_metadata_model(
trans.app, repository_metadata.repository, repository_metadata, recursive=False
+4 -2
View File
@@ -15,7 +15,6 @@ from tool_shed.context import SessionRequestContext
from tool_shed.managers.tools import (
parsed_tool_model_cached_for,
search,
ShedParsedTool,
)
from tool_shed.managers.trs import (
get_tool,
@@ -24,7 +23,10 @@ from tool_shed.managers.trs import (
)
from tool_shed.structured_app import ToolShedApp
from tool_shed.util.shed_index import build_index
from tool_shed_client.schema import BuildSearchIndexResponse
from tool_shed_client.schema import (
BuildSearchIndexResponse,
ShedParsedTool,
)
from tool_shed_client.schema.trs import (
Tool,
ToolClass,
+6
View File
@@ -19,6 +19,8 @@ from typing_extensions import (
TypedDict,
)
from galaxy.tool_util_models import ParsedTool
class Repository(BaseModel):
# element/collection view on the backend have same keys/impl
@@ -540,3 +542,7 @@ class Version(BaseModel):
version_major: str
version: str
api_version: str = "v1"
class ShedParsedTool(ParsedTool):
repository_revision: Optional[RepositoryRevisionMetadata] = None
+1
View File
@@ -40,6 +40,7 @@ install_requires =
galaxy-job-metrics
galaxy-objectstore
galaxy-tool-util[cwl,edam]
galaxy-tool-shed-schema
galaxy-tours
galaxy-util[image_util]
galaxy-web-framework
+3
View File
@@ -1,4 +1,7 @@
util
tool_util_models
tool_shed_schema
config
files
@@ -1,4 +1,5 @@
util
tool_util_models
job_metrics
objectstore
tool_util
+10
View File
@@ -0,0 +1,10 @@
History
-------
.. to_doc
---------
25.0.dev0
---------
* Initial creation of package during 25.0 development cycle.
+1
View File
@@ -0,0 +1 @@
../../LICENSE.txt
+1
View File
@@ -0,0 +1 @@
../package.Makefile
+1
View File
@@ -0,0 +1 @@
../../mypy.ini
+1
View File
@@ -0,0 +1 @@
../package-pyproject.toml
+1
View File
@@ -0,0 +1 @@
../build_scripts
+47
View File
@@ -0,0 +1,47 @@
[metadata]
author = Galaxy Project and Community
author_email = galaxy-committers@lists.galaxyproject.org
classifiers =
Development Status :: 5 - Production/Stable
Environment :: Console
Intended Audience :: Developers
License :: OSI Approved :: Academic Free License (AFL)
Natural Language :: English
Operating System :: POSIX
Programming Language :: Python :: 3
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
Programming Language :: Python :: 3.12
Programming Language :: Python :: 3.13
Topic :: Software Development
Topic :: Software Development :: Code Generators
Topic :: Software Development :: Testing
description = Galaxy tool and tool dependency utilities
keywords =
Galaxy
license = AFL
license_files =
LICENSE
long_description = file: README.rst, HISTORY.rst
long_description_content_type = text/x-rst
name = galaxy-tool-shed-schema
url = https://github.com/galaxyproject/galaxy
version = 25.0.dev0
[options]
include_package_data = True
install_requires =
galaxy-tool-util-models
pydantic>=2,!=2.6.0,!=2.6.1
packages = find:
python_requires = >=3.7
[options.entry_points]
console_scripts =
[options.packages.find]
exclude =
tests*
@@ -0,0 +1 @@
pytest
+1
View File
@@ -0,0 +1 @@
../../../test/unit/tool_shed_schema
+1
View File
@@ -0,0 +1 @@
../../lib/tool_shed_client/
+1
View File
@@ -34,6 +34,7 @@ version = 25.0.dev0
[options]
include_package_data = True
install_requires =
galaxy-tool-util-models
galaxy-util[image_util]>=22.1
conda-package-streaming
lxml!=4.2.2
+10
View File
@@ -0,0 +1,10 @@
History
-------
.. to_doc
---------
25.0.dev0
---------
* Initial creation of package during 25.0 development cycle.
+1
View File
@@ -0,0 +1 @@
../../LICENSE.txt
+1
View File
@@ -0,0 +1 @@
include *.rst *.txt LICENSE */py.typed
+1
View File
@@ -0,0 +1 @@
../package.Makefile
+13
View File
@@ -0,0 +1,13 @@
.. image:: https://badge.fury.io/py/galaxy-tool-util-models.svg
:target: https://pypi.org/project/galaxy-tool-util-models/
Overview
--------
Pydantic models to support Galaxy_ tool utilities.
* Code: https://github.com/galaxyproject/galaxy/tree/dev/packages/tool_util_models
.. _Galaxy: http://galaxyproject.org/
+1
View File
@@ -0,0 +1 @@
../package-dev-requirements.txt
+1
View File
@@ -0,0 +1 @@
../../package.__init__.py
+1
View File
@@ -0,0 +1 @@
../../../lib/galaxy/tool_util_models
+1
View File
@@ -0,0 +1 @@
../../mypy.ini
+1
View File
@@ -0,0 +1 @@
../package-pyproject.toml
+1
View File
@@ -0,0 +1 @@
../build_scripts
+46
View File
@@ -0,0 +1,46 @@
[metadata]
author = Galaxy Project and Community
author_email = galaxy-committers@lists.galaxyproject.org
classifiers =
Development Status :: 5 - Production/Stable
Environment :: Console
Intended Audience :: Developers
License :: OSI Approved :: Academic Free License (AFL)
Natural Language :: English
Operating System :: POSIX
Programming Language :: Python :: 3
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
Programming Language :: Python :: 3.12
Programming Language :: Python :: 3.13
Topic :: Software Development
Topic :: Software Development :: Code Generators
Topic :: Software Development :: Testing
description = Galaxy tool and tool dependency utilities
keywords =
Galaxy
license = AFL
license_files =
LICENSE
long_description = file: README.rst, HISTORY.rst
long_description_content_type = text/x-rst
name = galaxy-tool-util-models
url = https://github.com/galaxyproject/galaxy
version = 25.0.dev0
[options]
include_package_data = True
install_requires =
pydantic>=2,!=2.6.0,!=2.6.1
packages = find:
python_requires = >=3.7
[options.entry_points]
console_scripts =
[options.packages.find]
exclude =
tests*
@@ -0,0 +1 @@
pytest
+1
View File
@@ -0,0 +1 @@
../../../test/unit/tool_util_models
@@ -0,0 +1,12 @@
from tool_shed_client.schema import Category
def test_create_model():
category = Category(
id="1234567",
name="my category",
description="the description",
deleted=False,
repositories=3,
)
assert category.name == "my category"
+1 -1
View File
@@ -5,9 +5,9 @@ import lxml.etree as ET
import pytest
from pydantic import ValidationError
from galaxy.tool_util.verify.assertion_models import assertion_list
from galaxy.tool_util.verify.codegen import galaxy_xsd_path
from galaxy.tool_util.verify.parse import assertion_xml_els_to_models
from galaxy.tool_util_models.assertions import assertion_list
from galaxy.util.commands import shell
from galaxy.util.unittest_utils import skip_unless_executable
+1 -1
View File
@@ -1,5 +1,5 @@
from galaxy.tool_util.parser.factory import get_tool_source
from galaxy.tool_util.parser.output_models import from_tool_source
from galaxy.tool_util.parser.output_objects import from_tool_source
from galaxy.tool_util.unittest_utils import functional_test_tool_path
@@ -10,7 +10,7 @@ from typing import (
import pytest
from galaxy.tool_util.models import parse_tool
from galaxy.tool_util.model_factory import parse_tool
from galaxy.tool_util.parameters import (
DataCollectionRequest,
DataRequestHda,
@@ -25,13 +25,15 @@ from galaxy.tool_util.parameters.case import (
)
from galaxy.tool_util.parser.factory import get_tool_source
from galaxy.tool_util.parser.interface import (
JsonTestCollectionDefDict,
JsonTestDatasetDefDict,
ToolSource,
ToolSourceTest,
)
from galaxy.tool_util.unittest_utils import functional_test_tool_directory
from galaxy.tool_util.verify.parse import parse_tool_test_descriptions
from galaxy.tool_util_models.tool_source import (
JsonTestCollectionDefDict,
JsonTestDatasetDefDict,
)
from .util import dict_verify_each
# legacy tools allows specifying parameter and repeat parameters without
+3 -3
View File
@@ -11,13 +11,13 @@ from typing import (
)
from galaxy.tool_util.parser.factory import get_tool_source
from galaxy.tool_util.parser.output_models import (
from_tool_source,
from galaxy.tool_util.parser.output_objects import from_tool_source
from galaxy.tool_util.unittest_utils import functional_test_tool_path
from galaxy.tool_util_models.tool_outputs import (
ToolOutput,
ToolOutputCollection,
ToolOutputDataset,
)
from galaxy.tool_util.unittest_utils import functional_test_tool_path
from galaxy.util import galaxy_directory
from galaxy.util.unittest import TestCase
@@ -4,7 +4,7 @@ from typing import List
import yaml
from galaxy.tool_util.models import Tests
from galaxy.tool_util_models import Tests
from galaxy.util import galaxy_directory
from galaxy.util.unittest_utils import skip_unless_environ
@@ -0,0 +1,23 @@
from galaxy.tool_util_models import Tests
simple_json_test = """
[{ "doc": "Test simple use of __ZIP_COLLECTION__ in a workflow.",
"job": {
"test_input_1": "samp1 10.0 samp2 20.0 ",
"test_input_2": "samp1 20.0 samp2 40.0 "
},
"outputs": {
"out": {
"asserts": [
{"that": "has_text",
"text": "samp1 10.0 samp2 20.0 samp1 20.0 samp2 40.0"
}
]
}
}
}]
"""
def test_simple_validate():
Tests.model_validate_json(simple_json_test)