Skip to content

Commit

Permalink
Allow admins to import tools/workflows from paths.
Browse files Browse the repository at this point in the history
  • Loading branch information
jmchilton committed Nov 18, 2019
1 parent 707e64e commit f4b917f
Show file tree
Hide file tree
Showing 10 changed files with 122 additions and 23 deletions.
39 changes: 39 additions & 0 deletions lib/galaxy/managers/executables.py
Original file line number Diff line number Diff line change
@@ -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',
)
34 changes: 23 additions & 11 deletions lib/galaxy/managers/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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:
Expand All @@ -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):
Expand Down
8 changes: 3 additions & 5 deletions lib/galaxy/managers/workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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__)

Expand Down Expand Up @@ -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()
Expand Down
4 changes: 3 additions & 1 deletion lib/galaxy/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/galaxy/webapps/galaxy/api/dynamic_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
6 changes: 5 additions & 1 deletion lib/galaxy/webapps/galaxy/api/workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion lib/galaxy/workflow/modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
16 changes: 16 additions & 0 deletions lib/galaxy_test/api/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions lib/galaxy_test/base/data/minimal_tool_no_id.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "Minimal Tool",
"class": "GalaxyTool",
"version": "1.0.0",
"command": "echo 'Hello World 2' > $output1",
"inputs": [],
"outputs": {
"output1": {
"format": 'txt'
}
}
}
22 changes: 19 additions & 3 deletions lib/galaxy_test/base/populators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down

0 comments on commit f4b917f

Please sign in to comment.