diff --git a/lib/galaxy/managers/executables.py b/lib/galaxy/managers/executables.py new file mode 100644 index 000000000000..83866f9d25b2 --- /dev/null +++ b/lib/galaxy/managers/executables.py @@ -0,0 +1,39 @@ +"""Utilities for loading tools and workflows from paths for admin user requests.""" + +from gxformat2.converter import ordered_load + +from galaxy import exceptions + + +def artifact_class(trans, as_dict): + object_id = as_dict.get("object_id", None) + if as_dict.get("src", None) == "from_path": + if trans and not trans.user_is_admin: + raise exceptions.AdminRequiredException() + + workflow_path = as_dict.get("path") + with open(workflow_path, "r") as f: + as_dict = ordered_load(f) + + artifact_class = as_dict.get("class", None) + if artifact_class is None and "$graph" in as_dict: + object_id = object_id or "main" + graph = as_dict["$graph"] + target_object = None + if isinstance(graph, dict): + target_object = graph.get(object_id) + else: + for item in graph: + found_id = item.get("id") + if found_id == object_id or found_id == "#" + object_id: + target_object = item + + if target_object and target_object.get("class"): + artifact_class = target_object["class"] + + return artifact_class, as_dict, object_id + + +__all__ = ( + 'artifact_class', +) diff --git a/lib/galaxy/managers/tools.py b/lib/galaxy/managers/tools.py index 03a849fbca60..ce368e247398 100644 --- a/lib/galaxy/managers/tools.py +++ b/lib/galaxy/managers/tools.py @@ -7,6 +7,7 @@ from galaxy import model from galaxy.exceptions import DuplicatedIdentifierException from .base import ModelManager +from .executables import artifact_class log = logging.getLogger(__name__) @@ -37,23 +38,32 @@ def get_tool_by_id(self, object_id): ) return dynamic_tool - def create_tool(self, tool_payload, allow_load=False): - if "representation" not in tool_payload: - raise exceptions.ObjectAttributeMissingException( - "A tool 'representation' is required." - ) + def create_tool(self, trans, tool_payload, allow_load=True): + src = tool_payload.get("src", "representation") + is_path = src == "from_path" - representation = tool_payload["representation"] - if "class" not in representation: - raise exceptions.ObjectAttributeMissingException( - "Current tool representations require 'class'." - ) + if is_path: + tool_format, representation, object_id = artifact_class(None, tool_payload) + else: + assert src == "representation" + if "representation" not in tool_payload: + raise exceptions.ObjectAttributeMissingException( + "A tool 'representation' is required." + ) + + representation = tool_payload["representation"] + if "class" not in representation: + raise exceptions.ObjectAttributeMissingException( + "Current tool representations require 'class'." + ) enable_beta_formats = getattr(self.app.config, "enable_beta_tool_formats", False) if not enable_beta_formats: raise exceptions.ConfigDoesNotAllowException("Set 'enable_beta_tool_formats' in Galaxy config to create dynamic tools.") tool_format = representation["class"] + tool_directory = tool_payload.get("tool_directory", None) + tool_path = None if tool_format == "GalaxyTool": uuid = tool_payload.get("uuid", None) if uuid is None: @@ -79,10 +89,12 @@ def create_tool(self, tool_payload, allow_load=False): tool_format=tool_format, tool_id=tool_id, tool_version=tool_version, + tool_path=tool_path, + tool_directory=tool_directory, uuid=uuid, value=value, ) - self.app.toolbox.load_dynamic_tool(dynamic_tool) + self.app.toolbox.load_dynamic_tool(dynamic_tool) return dynamic_tool def list_tools(self, active=True): diff --git a/lib/galaxy/managers/workflows.py b/lib/galaxy/managers/workflows.py index 42290a266118..1ba986cd041e 100644 --- a/lib/galaxy/managers/workflows.py +++ b/lib/galaxy/managers/workflows.py @@ -12,7 +12,6 @@ ImportOptions, python_to_workflow, ) -from gxformat2.converter import ordered_load from six import string_types from sqlalchemy import and_ from sqlalchemy.orm import joinedload, subqueryload @@ -46,6 +45,7 @@ from galaxy.workflow.resources import get_resource_mapper_function from galaxy.workflow.steps import attach_ordered_steps from .base import decode_id +from .executables import artifact_class log = logging.getLogger(__name__) @@ -275,12 +275,10 @@ def normalize_workflow_format(self, trans, as_dict): raise exceptions.AdminRequiredException() workflow_path = as_dict.get("path") - with open(workflow_path, "r") as f: - as_dict = ordered_load(f) workflow_directory = os.path.normpath(os.path.dirname(workflow_path)) - workflow_class = as_dict.get("class", None) - if workflow_class == "GalaxyWorkflow" or "$graph" in as_dict or "yaml_content" in as_dict: + workflow_class, as_dict, object_id = artifact_class(trans, as_dict) + if workflow_class == "GalaxyWorkflow" or "yaml_content" in as_dict: # Format 2 Galaxy workflow. galaxy_interface = Format2ConverterGalaxyInterface() import_options = ImportOptions() diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index 67546f1fdcda..0f96c1b332e2 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -616,11 +616,13 @@ class DynamicTool(Dictifiable): dict_collection_visible_keys = ('id', 'tool_id', 'tool_format', 'tool_version', 'uuid', 'active', 'hidden') dict_element_visible_keys = ('id', 'tool_id', 'tool_format', 'tool_version', 'uuid', 'active', 'hidden') - def __init__(self, tool_format=None, tool_id=None, tool_version=None, + def __init__(self, tool_format=None, tool_id=None, tool_version=None, tool_path=None, tool_directory=None, uuid=None, active=True, hidden=True, value=None): self.tool_format = tool_format self.tool_id = tool_id self.tool_version = tool_version + self.tool_path = tool_path + self.tool_directory = tool_directory self.active = active self.hidden = hidden self.value = value diff --git a/lib/galaxy/webapps/galaxy/api/dynamic_tools.py b/lib/galaxy/webapps/galaxy/api/dynamic_tools.py index a5c471a7c8f2..1cd41d72a441 100644 --- a/lib/galaxy/webapps/galaxy/api/dynamic_tools.py +++ b/lib/galaxy/webapps/galaxy/api/dynamic_tools.py @@ -57,7 +57,7 @@ def create(self, trans, payload, **kwd): :param uuid: the uuid to associate with the tool being created """ dynamic_tool = self.app.dynamic_tools_manager.create_tool( - payload + trans, payload, allow_load=util.asbool(kwd.get("allow_load", True)) ) return dynamic_tool.to_dict() diff --git a/lib/galaxy/webapps/galaxy/api/workflows.py b/lib/galaxy/webapps/galaxy/api/workflows.py index 7f40b7f8648f..898b40991c58 100644 --- a/lib/galaxy/webapps/galaxy/api/workflows.py +++ b/lib/galaxy/webapps/galaxy/api/workflows.py @@ -373,7 +373,11 @@ def create(self, trans, payload, **kwd): if 'from_path' in payload: from_path = payload.get('from_path') - payload["workflow"] = {"src": "from_path", "path": from_path} + object_id = payload.get("object_id") + workflow_src = {"src": "from_path", "path": from_path} + if object_id is not None: + workflow_src["object_id"] = object_id + payload["workflow"] = workflow_src return self.__api_import_new_workflow(trans, payload, **kwd) if 'shared_workflow_id' in payload: diff --git a/lib/galaxy/workflow/modules.py b/lib/galaxy/workflow/modules.py index fcd5298bafd3..57c6f2102e1d 100644 --- a/lib/galaxy/workflow/modules.py +++ b/lib/galaxy/workflow/modules.py @@ -811,7 +811,7 @@ def from_dict(Class, trans, d, **kwds): if not trans.user_is_admin: raise exceptions.AdminRequiredException("Only admin users can create tools dynamically.") dynamic_tool = trans.app.dynamic_tool_manager.create_tool( - create_request, allow_load=False + trans, create_request, allow_load=False ) tool_uuid = dynamic_tool.uuid if tool_id is None and tool_uuid is None: diff --git a/lib/galaxy_test/api/test_tools.py b/lib/galaxy_test/api/test_tools.py index a01e5a6214ec..b29404c3751f 100644 --- a/lib/galaxy_test/api/test_tools.py +++ b/lib/galaxy_test/api/test_tools.py @@ -8,6 +8,7 @@ from requests import get from six import BytesIO +from galaxy.util import galaxy_root_path from galaxy_test.base import rules_test_data from galaxy_test.base.populators import ( DatasetCollectionPopulator, @@ -936,6 +937,21 @@ def test_dynamic_tool_1(self): output_content = self.dataset_populator.get_history_dataset_content(history_id) self.assertEqual(output_content, "Hello World\n") + def test_dynamic_tool_from_path(self): + # Create tool. + dynamic_tool_path = os.path.join(galaxy_root_path, "lib", "galaxy_test", "base", "data", "minimal_tool_no_id.json") + tool_response = self.dataset_populator.create_tool_from_path(dynamic_tool_path) + self._assert_has_keys(tool_response, "uuid") + + # Run tool. + history_id = self.dataset_populator.new_history() + inputs = {} + self._run(history_id=history_id, inputs=inputs, tool_uuid=tool_response["uuid"]) + + self.dataset_populator.wait_for_history(history_id, assert_ok=True) + output_content = self.dataset_populator.get_history_dataset_content(history_id) + self.assertEqual(output_content, "Hello World 2\n") + def test_dynamic_tool_no_id(self): # Create tool. tool_response = self.dataset_populator.create_tool(MINIMAL_TOOL_NO_ID) diff --git a/lib/galaxy_test/base/data/minimal_tool_no_id.json b/lib/galaxy_test/base/data/minimal_tool_no_id.json new file mode 100644 index 000000000000..1ebd230c86a4 --- /dev/null +++ b/lib/galaxy_test/base/data/minimal_tool_no_id.json @@ -0,0 +1,12 @@ +{ + "name": "Minimal Tool", + "class": "GalaxyTool", + "version": "1.0.0", + "command": "echo 'Hello World 2' > $output1", + "inputs": [], + "outputs": { + "output1": { + "format": "txt" + } + } +} diff --git a/lib/galaxy_test/base/populators.py b/lib/galaxy_test/base/populators.py index 88fb63530f63..baf71e175812 100644 --- a/lib/galaxy_test/base/populators.py +++ b/lib/galaxy_test/base/populators.py @@ -282,14 +282,30 @@ def delete_dataset(self, history_id, content_id): delete_response = self._delete("histories/%s/contents/%s" % (history_id, content_id)) return delete_response - def create_tool(self, representation): + def create_tool_from_path(self, tool_path): + tool_directory = os.path.dirname(os.path.abspath(tool_path)) + payload = dict( + src="from_path", + path=tool_path, + tool_directory=tool_directory, + ) + return self._create_tool_raw(payload) + + def create_tool(self, representation, tool_directory=None): if isinstance(representation, dict): representation = json.dumps(representation) payload = dict( representation=representation, + tool_directory=tool_directory, ) - create_response = self._post("dynamic_tools", data=payload, admin=True) - assert create_response.status_code == 200, create_response + return self._create_tool_raw(payload) + + def _create_tool_raw(self, payload): + try: + create_response = self._post("dynamic_tools", data=payload, admin=True) + except TypeError: + create_response = self._post("dynamic_tools", data=payload) + assert create_response.status_code == 200, create_response.json() return create_response.json() def list_dynamic_tools(self):