Skip to content

Commit

Permalink
Minor fix to restore admin-style dynamic tools
Browse files Browse the repository at this point in the history
  • Loading branch information
mvdbeek committed Dec 5, 2024
1 parent 4316332 commit e5f3dcc
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 55 deletions.
2 changes: 1 addition & 1 deletion lib/galaxy/managers/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def create_tool(self, trans, tool_payload, allow_load=True):
is_path = src == "from_path"

if is_path:
tool_format, representation, _ = artifact_class(None, tool_payload)
tool_format, representation, _ = artifact_class(None, tool_payload.dict())
else:
assert src == "representation"
representation = tool_payload.representation.dict(by_alias=True, exclude_unset=True)
Expand Down
39 changes: 34 additions & 5 deletions lib/galaxy/tool_util/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
BaseModel,
ConfigDict,
Field,
model_validator,
RootModel,
)
from typing_extensions import (
Expand Down Expand Up @@ -47,13 +48,20 @@
from .verify.assertion_models import assertions


class UserToolSource(BaseModel):
class_: Annotated[Literal["GalaxyUserTool"], Field(alias="class")]
def normalize_dict(values, keys: List[str]):
for key in keys:
items = values.get(key)
if isinstance(items, dict): # dict-of-dicts format
# Transform dict-of-dicts to list-of-dicts
values[key] = [{"name": k, **v} for k, v in items.items()]


class ToolSourceBase(BaseModel):
id: Optional[str] = None
name: str
container: str
version: str
version: Optional[str] = "1.0"
description: Optional[str] = None
shell_command: str
container: Optional[str] = None
inputs: List[GalaxyParameterT]
outputs: List[IncomingToolOutput]
citations: Optional[List[Citation]] = None
Expand All @@ -64,6 +72,27 @@ class UserToolSource(BaseModel):
xrefs: Optional[List[XrefDict]] = None
help: Optional[HelpContent] = None

@model_validator(mode="before")
@classmethod
def normalize_items(cls, values):
if isinstance(values, dict):
normalize_dict(values, ["inputs", "outputs"])
return values


class UserToolSource(ToolSourceBase):
class_: Annotated[Literal["GalaxyUserTool"], Field(alias="class")]
shell_command: str
container: str


class AdminToolSource(ToolSourceBase):
class_: Annotated[Literal["GalaxyTool"], Field(alias="class")]
command: str


DynamicToolSources = Annotated[Union[UserToolSource, AdminToolSource], Field(discriminator="class_")]


class ParsedTool(BaseModel):
id: str
Expand Down
23 changes: 17 additions & 6 deletions lib/galaxy/webapps/galaxy/api/dynamic_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import (
Literal,
Optional,
Union,
)

from pydantic.main import BaseModel
Expand All @@ -10,25 +11,35 @@
from galaxy.managers.context import ProvidesUserContext
from galaxy.managers.tools import DynamicToolManager
from galaxy.schema.fields import DecodedDatabaseIdField
from galaxy.tool_util.models import UserToolSource
from galaxy.tool_util.models import DynamicToolSources
from . import (
depends,
DependsOnTrans,
Router,
)


class DynamicToolCreatePayload(BaseModel):
class BaseDynamicToolCreatePayload(BaseModel):
allow_load: bool = True
uuid: Optional[str] = None
src: Literal["representation", "path"] = "representation"
representation: UserToolSource
active: Optional[bool] = None
hidden: Optional[bool] = None
path: Optional[str] = None
tool_directory: Optional[str] = None


class DynamicToolCreatePayload(BaseDynamicToolCreatePayload):
src: Literal["representation"] = "representation"
representation: DynamicToolSources


class PathBasedDynamicToolCreatePayload(BaseDynamicToolCreatePayload):
src: Literal["from_path"]
path: str


DynamicToolPayload = Union[DynamicToolCreatePayload, PathBasedDynamicToolCreatePayload]


log = logging.getLogger(__name__)

router = Router(tags=["dynamic_tools"])
Expand All @@ -50,7 +61,7 @@ def show(self, dynamic_tool_id: DecodedDatabaseIdField):
return dynamic_tool.to_dict()

@router.post("/api/dynamic_tools", require_admin=True)
def create(self, payload: DynamicToolCreatePayload, trans: ProvidesUserContext = DependsOnTrans):
def create(self, payload: DynamicToolPayload, trans: ProvidesUserContext = DependsOnTrans):
dynamic_tool = self.dynamic_tools_manager.create_tool(trans, payload, allow_load=payload.allow_load)
return dynamic_tool.to_dict()

Expand Down
83 changes: 41 additions & 42 deletions lib/galaxy_test/api/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,40 +43,17 @@
"version": "1.0.0",
"command": "echo 'Hello World' > $output1",
"inputs": [],
"outputs": dict(
output1=dict(format="txt"),
),
"outputs": {"output1": {"format": "txt", "type": "data"}},
}
MINIMAL_TOOL_NO_ID = {
"name": "Minimal Tool",
"class": "GalaxyTool",
"version": "1.0.0",
"command": "echo 'Hello World 2' > $output1",
"inputs": [],
"outputs": dict(
output1=dict(format="txt"),
),
}
TOOL_WITH_BASE_COMMAND = {
"name": "Base command tool",
"class": "GalaxyTool",
"version": "1.0.0",
"base_command": "cat",
"arguments": ["$(inputs.input.path)", ">", "output.fastq"],
"inputs": [
{
"type": "data",
"name": "input",
}
],
"outputs": {
"output": {
"type": "data",
"from_work_dir": "output.fastq",
"name": "output",
}
},
"outputs": {"output1": {"format": "txt", "type": "data"}},
}

TOOL_WITH_SHELL_COMMAND = {
"name": "Base command tool",
"class": "GalaxyUserTool",
Expand Down Expand Up @@ -1422,22 +1399,44 @@ def test_dynamic_tool_no_id(self):
output_content = self.dataset_populator.get_history_dataset_content(history_id)
assert output_content == "Hello World 2\n"

def test_dynamic_tool_base_command(self):
tool_response = self.dataset_populator.create_tool(TOOL_WITH_BASE_COMMAND)
self._assert_has_keys(tool_response, "uuid")

# Run tool.
history_id = self.dataset_populator.new_history()
dataset = self.dataset_populator.new_dataset(history_id=history_id, content="abc")
self._run(
history_id=history_id,
tool_uuid=tool_response["uuid"],
inputs={"input": {"src": "hda", "id": dataset["id"]}},
)

self.dataset_populator.wait_for_history(history_id, assert_ok=True)
output_content = self.dataset_populator.get_history_dataset_content(history_id)
assert output_content == "abc\n"
# This works except I don't want to add it to the schema right now,
# since I think the shell_command is what we'lll go with (at least initially)
# def test_dynamic_tool_base_command(self):
# TOOL_WITH_BASE_COMMAND = {
# "name": "Base command tool",
# "class": "GalaxyTool",
# "version": "1.0.0",
# "base_command": "cat",
# "arguments": ["$(inputs.input.path)", ">", "output.fastq"],
# "inputs": [
# {
# "type": "data",
# "name": "input",
# }
# ],
# "outputs": {
# "output": {
# "type": "data",
# "from_work_dir": "output.fastq",
# "name": "output",
# }
# },
# }
# tool_response = self.dataset_populator.create_tool(TOOL_WITH_BASE_COMMAND)
# self._assert_has_keys(tool_response, "uuid")

# # Run tool.
# history_id = self.dataset_populator.new_history()
# dataset = self.dataset_populator.new_dataset(history_id=history_id, content="abc")
# self._run(
# history_id=history_id,
# tool_uuid=tool_response["uuid"],
# inputs={"input": {"src": "hda", "id": dataset["id"]}},
# )

# self.dataset_populator.wait_for_history(history_id, assert_ok=True)
# output_content = self.dataset_populator.get_history_dataset_content(history_id)
# assert output_content == "abc\n"

def test_dynamic_tool_shell_command(self):
tool_response = self.dataset_populator.create_tool(TOOL_WITH_SHELL_COMMAND)
Expand Down
3 changes: 2 additions & 1 deletion lib/galaxy_test/base/data/minimal_tool_no_id.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"inputs": [],
"outputs": {
"output1": {
"format": "txt"
"format": "txt",
"type": "data"
}
}
}

0 comments on commit e5f3dcc

Please sign in to comment.