Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow admins to import tools/workflows from paths. #9003

Merged
merged 2 commits into from
Nov 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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