Skip to content

Commit

Permalink
Merge pull request #19092 from jmchilton/min_max_regex
Browse files Browse the repository at this point in the history
Implement workflow parameter validators.
  • Loading branch information
mvdbeek authored Nov 15, 2024
2 parents f67dc5c + 03c72e6 commit 215b459
Show file tree
Hide file tree
Showing 25 changed files with 523 additions and 203 deletions.
18 changes: 17 additions & 1 deletion client/src/api/schema/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11941,6 +11941,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: {
/**
Expand Down Expand Up @@ -12022,7 +12037,8 @@ export interface components {
| components["schemas"]["InvocationFailureExpressionEvaluationFailedResponse"]
| components["schemas"]["InvocationFailureWhenNotBooleanResponse"]
| components["schemas"]["InvocationUnexpectedFailureResponse"]
| components["schemas"]["InvocationEvaluationWarningWorkflowOutputNotFoundResponse"];
| components["schemas"]["InvocationEvaluationWarningWorkflowOutputNotFoundResponse"]
| components["schemas"]["InvocationFailureWorkflowParameterInvalidResponse"];
/** InvocationOutput */
InvocationOutput: {
/**
Expand Down
11 changes: 10 additions & 1 deletion client/src/components/Form/FormElement.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import FormSelection from "./Elements/FormSelection.vue";
import FormTags from "./Elements/FormTags.vue";
import FormText from "./Elements/FormText.vue";
import FormUpload from "./Elements/FormUpload.vue";
import FormElementHelpMarkdown from "./FormElementHelpMarkdown.vue";
interface FormElementProps {
id?: string;
Expand All @@ -35,6 +36,7 @@ interface FormElementProps {
title?: string;
refreshOnChange?: boolean;
help?: string;
helpFormat?: string;
error?: string;
warning?: string;
disabled?: boolean;
Expand Down Expand Up @@ -63,6 +65,7 @@ const props = withDefaults(defineProps<FormElementProps>(), {
connectedDisableText: "Add connection to module.",
connectedEnableIcon: "fa fa-times",
connectedDisableIcon: "fa fa-arrows-alt-h",
helpFormat: "html",
workflowBuildingMode: false,
});
Expand Down Expand Up @@ -337,7 +340,13 @@ function onAlert(value: string | undefined) {
</div>

<div v-if="showPreview" class="ui-form-preview pt-1 pl-2 mt-1">{{ previewText }}</div>
<span v-if="Boolean(helpText)" class="ui-form-info form-text text-muted" v-html="helpText" />
<span
v-if="Boolean(helpText) && helpFormat != 'markdown'"
class="ui-form-info form-text text-muted"
v-html="helpText" />
<span v-else-if="Boolean(helpText)" class="ui-form-info form-text text-muted"
><FormElementHelpMarkdown :content="helpText"
/></span>
</div>
</template>

Expand Down
56 changes: 56 additions & 0 deletions client/src/components/Form/FormElementHelpMarkdown.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import { markup } from "@/components/ObjectStore/configurationMarkdown";
import { getAppRoot } from "@/onload/loadConfig";
import HelpPopover from "@/components/Help/HelpPopover.vue";
const props = defineProps<{
content: string;
}>();
const markdownHtml = computed(() => markup(props.content ?? "", false));
const helpHtml = ref<HTMLDivElement>();
interface InternalTypeReference {
element: HTMLElement;
term: string;
}
const internalHelpReferences = ref<InternalTypeReference[]>([]);
function setupPopovers() {
internalHelpReferences.value.length = 0;
if (helpHtml.value) {
const links = helpHtml.value.getElementsByTagName("a");
Array.from(links).forEach((link) => {
if (link.href.startsWith("gxhelp://")) {
const uri = link.href.substr("gxhelp://".length);
internalHelpReferences.value.push({ element: link, term: uri });
link.href = `${getAppRoot()}help/terms/${uri}`;
link.style.color = "inherit";
link.style.textDecorationLine = "underline";
link.style.textDecorationStyle = "dashed";
}
});
}
}
onMounted(setupPopovers);
</script>

<template>
<span>
<!-- Disable v-html warning because we allow markdown generated HTML
in various places in the Galaxy interface. Raw HTML is not allowed
here because admin = false in the call to markup.
-->
<!-- eslint-disable-next-line vue/no-v-html -->
<div ref="helpHtml" v-html="markdownHtml" />
<span v-for="(value, i) in internalHelpReferences" :key="i">
<HelpPopover :target="value.element" :term="value.term" />
</span>
</span>
</template>
2 changes: 2 additions & 0 deletions client/src/components/Form/FormInputs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
v-model="input.test_param.value"
:type="input.test_param.type"
:help="input.test_param.help"
:help-format="input.test_param.help_format"
:refresh-on-change="false"
:disabled="sustainConditionals"
:attributes="input.test_param"
Expand Down Expand Up @@ -51,6 +52,7 @@
:error="input.error"
:warning="input.warning"
:help="input.help"
:help-format="input.help_format"
:refresh-on-change="input.refresh_on_change"
:attributes="input.attributes || input"
:collapsed-enable-text="collapsedEnableText"
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/Help/HelpPopover.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ defineProps<Props>();
</script>

<template>
<BPopover :target="target" triggers="hover" placement="bottom">
<BPopover v-if="target" :target="target" triggers="hover" placement="bottom">
<HelpTerm :term="term" />
</BPopover>
</template>
5 changes: 5 additions & 0 deletions client/src/components/Help/terms.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ unix:
debug what is wrong with the execution of an application.
More information on stack traces can be found on [Wikipedia](https://en.wikipedia.org/wiki/Stack_trace).
programming:
python:
regex: |
Regular expressions are patterns used to match character combinations in strings. This input accepts Python-style regular expressions, find more information about these in [this Python for Biologists tutorial](https://pythonforbiologists.com/tutorial/regex.html).
The website [regex101](https://regex101.com/) is a useful playground to explore regular expressions (be sure to enable "Python" as your flavor) and language models such as ChatGPT can help interactively build up and explain regular expressions from natural language prompts or examples.
galaxy:
collections:
flatList: |
Expand Down
12 changes: 5 additions & 7 deletions client/src/components/RuleBuilder/RegularExpressionInput.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<div>
<label v-b-tooltip.hover for="regular_expression" :title="title">{{ label }}</label>
<span v-b-popover.html="popoverContent" :title="popoverTitle" class="fa fa-question"></span>
<label ref="helpTarget" v-b-tooltip.hover for="regular_expression">{{ label }}</label>
<HelpPopover :target="$refs.helpTarget" term="programming.python.regex" />
<input
v-b-tooltip.hover.left
:title="title"
Expand All @@ -16,7 +16,10 @@
<script>
import _l from "utils/localization";
import HelpPopover from "@/components/Help/HelpPopover.vue";
export default {
components: { HelpPopover },
props: {
target: {
required: true,
Expand All @@ -32,11 +35,6 @@ export default {
popoverTitle() {
return _l("Regular Expressions");
},
popoverContent() {
return _l(
`Regular expressions are patterns used to match character combinations in strings. This input accepts Python-style regular expressions, find more information about these in <a href="https://pythonforbiologists.com/tutorial/regex.html">this Python for Biologists tutorial</a>.`
);
},
},
};
</script>
2 changes: 1 addition & 1 deletion client/src/components/Workflow/Run/WorkflowRun.vue
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ defineExpose({
Workflow submission failed: {{ submissionError }}
</BAlert>
<WorkflowRunFormSimple
v-else-if="fromVariant === 'simple'"
v-if="fromVariant === 'simple'"
:model="workflowModel"
:target-history="simpleFormTargetHistory"
:use-job-cache="simpleFormUseJobCache"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type ReasonToLevel = {
when_not_boolean: "error";
unexpected_failure: "error";
workflow_output_not_found: "warning";
workflow_parameter_invalid: "error";
};
const level: ReasonToLevel = {
Expand All @@ -34,6 +35,7 @@ const level: ReasonToLevel = {
when_not_boolean: "error",
unexpected_failure: "error",
workflow_output_not_found: "warning",
workflow_parameter_invalid: "error",
};
const levelClasses = {
Expand Down Expand Up @@ -165,6 +167,10 @@ const infoString = computed(() => {
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;
}
Expand Down
15 changes: 15 additions & 0 deletions lib/galaxy/schema/invocation.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,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.
Expand Down Expand Up @@ -225,6 +226,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]
Expand All @@ -236,6 +245,7 @@ class GenericInvocationEvaluationWarningWorkflowOutputNotFound(
InvocationFailureWhenNotBoolean = GenericInvocationFailureWhenNotBoolean[int]
InvocationUnexpectedFailure = GenericInvocationUnexpectedFailure[int]
InvocationWarningWorkflowOutputNotFound = GenericInvocationEvaluationWarningWorkflowOutputNotFound[int]
InvocationFailureWorkflowParameterInvalid = GenericInvocationFailureWorkflowParameterInvalid[int]

InvocationMessageUnion = Union[
InvocationCancellationReviewFailed,
Expand All @@ -249,6 +259,7 @@ class GenericInvocationEvaluationWarningWorkflowOutputNotFound(
InvocationFailureWhenNotBoolean,
InvocationUnexpectedFailure,
InvocationWarningWorkflowOutputNotFound,
InvocationFailureWorkflowParameterInvalid,
]

InvocationCancellationReviewFailedResponseModel = GenericInvocationCancellationReviewFailed[EncodedDatabaseIdField]
Expand All @@ -266,6 +277,9 @@ class GenericInvocationEvaluationWarningWorkflowOutputNotFound(
InvocationWarningWorkflowOutputNotFoundResponseModel = GenericInvocationEvaluationWarningWorkflowOutputNotFound[
EncodedDatabaseIdField
]
InvocationFailureWorkflowParameterInvalidResponseModel = GenericInvocationFailureWorkflowParameterInvalid[
EncodedDatabaseIdField
]

_InvocationMessageResponseUnion = Annotated[
Union[
Expand All @@ -280,6 +294,7 @@ class GenericInvocationEvaluationWarningWorkflowOutputNotFound(
InvocationFailureWhenNotBooleanResponseModel,
InvocationUnexpectedFailureResponseModel,
InvocationWarningWorkflowOutputNotFoundResponseModel,
InvocationFailureWorkflowParameterInvalidResponseModel,
],
Field(discriminator="reason"),
]
Expand Down
9 changes: 7 additions & 2 deletions lib/galaxy/tool_util/parser/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,17 +119,22 @@ def get_tool_source_from_representation(tool_format, tool_representation):
raise Exception(f"Unknown tool representation format [{tool_format}].")


def get_input_source(content):
def get_input_source(content, trusted: bool = True):
"""Wrap dicts or XML elements as InputSource if needed.
If the supplied content is already an InputSource object,
it is simply returned. This allow Galaxy to uniformly
consume using the tool input source interface.
Setting trusted to false indicates that no dynamic code should be
executed - no eval. This should be used for user-defined tools (in
the future) and for workflow inputs.
"""
if not isinstance(content, InputSource):
if isinstance(content, dict):
content = YamlInputSource(content)
content = YamlInputSource(content, trusted=trusted)
else:
assert trusted # trust is not implemented for XML inputs
content = XmlInputSource(content)
return content

Expand Down
26 changes: 25 additions & 1 deletion lib/galaxy/tool_util/parser/parameter_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import (
Any,
cast,
Dict,
List,
Optional,
Sequence,
Expand All @@ -14,6 +15,7 @@
Field,
model_validator,
PrivateAttr,
TypeAdapter,
)
from typing_extensions import (
Annotated,
Expand Down Expand Up @@ -96,6 +98,9 @@ class ParameterValidatorModel(StrictModel):
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:
Expand Down Expand Up @@ -163,6 +168,7 @@ class RegexParameterValidatorModel(StaticValidatorModel):
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:
Expand All @@ -189,6 +195,7 @@ class InRangeParameterValidatorModel(StaticValidatorModel):
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)):
Expand All @@ -211,7 +218,9 @@ def default_message(self) -> str:
op1 = "<"
if self.exclude_max:
op2 = "<"
range_description_str = f"({self.min} {op1} value {op2} {self.max})"
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}"


Expand All @@ -220,6 +229,7 @@ class LengthParameterValidatorModel(StaticValidatorModel):
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):
Expand Down Expand Up @@ -458,6 +468,20 @@ def default_message(self) -> str:
]


DiscriminatedAnyValidatorModel = TypeAdapter(AnyValidatorModel) # type:ignore[var-annotated]


def parse_dict_validators(validator_dicts: List[Dict[str, Any]], trusted: bool) -> List[AnyValidatorModel]:
validator_models = []
for validator_dict in validator_dicts:
validator = DiscriminatedAnyValidatorModel.validate_python(validator_dict)
if not trusted:
# Don't risk instantiating unsafe validators for user-defined code
assert validator._safe
validator_models.append(validator)
return validator_models


def parse_xml_validators(input_elem: Element) -> List[AnyValidatorModel]:
validator_els: List[Element] = input_elem.findall("validator") or []
models = []
Expand Down
Loading

0 comments on commit 215b459

Please sign in to comment.