Skip to content

Commit

Permalink
API for structured workflow refactoring.
Browse files Browse the repository at this point in the history
  • Loading branch information
jmchilton committed Dec 27, 2020
1 parent 0ce0007 commit 6284358
Show file tree
Hide file tree
Showing 13 changed files with 962 additions and 8 deletions.
3 changes: 2 additions & 1 deletion lib/galaxy/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from galaxy.managers.roles import RoleManager
from galaxy.managers.tools import DynamicToolManager
from galaxy.managers.users import UserManager
from galaxy.managers.workflows import WorkflowsManager
from galaxy.managers.workflows import WorkflowContentsManager, WorkflowsManager
from galaxy.model.database_heartbeat import DatabaseHeartbeat
from galaxy.model.tags import GalaxyTagHandler
from galaxy.queue_worker import (
Expand Down Expand Up @@ -105,6 +105,7 @@ def __init__(self, **kwargs):
self.history_manager = HistoryManager(self)
self.hda_manager = HDAManager(self)
self.workflow_manager = WorkflowsManager(self)
self.workflow_contents_manager = WorkflowContentsManager(self)
self.dependency_resolvers_view = DependencyResolversView(self)
self.test_data_resolver = test_data.TestDataResolver(file_dirs=self.config.tool_test_data_directories)
self.library_folder_manager = FolderManager()
Expand Down
51 changes: 44 additions & 7 deletions lib/galaxy/managers/workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
ToolModule,
WorkflowModuleInjector
)
from galaxy.workflow.refactor.execute import WorkflowRefactorExecutor
from galaxy.workflow.reports import generate_report
from galaxy.workflow.resources import get_resource_mapper_function
from galaxy.workflow.steps import attach_ordered_steps
Expand Down Expand Up @@ -305,7 +306,7 @@ def normalize_workflow_format(self, trans, as_dict):
Currently this mostly means converting format 2 workflows into standard Galaxy
workflow JSON for consumption for the rest of this module. In the future we will
want to be a lot more percise about this - preserve the original description along
want to be a lot more precise about this - preserve the original description along
side the data model and apply updates in a way that largely preserves YAML structure
so workflows can be extracted.
"""
Expand Down Expand Up @@ -415,6 +416,16 @@ def update_workflow_from_raw_description(self, trans, stored_workflow, raw_workf
# Connect up
workflow.stored_workflow = stored_workflow
stored_workflow.latest_workflow = workflow

# I'm not sure we can't just default this to True.
if kwds.get("update_stored_workflow_attributes", False):
update_dict = raw_workflow_description.as_dict
if 'name' in update_dict:
stored_workflow.name = update_dict['name']
if 'annotation' in update_dict:
newAnnotation = sanitize_html(update_dict['annotation'])
self.add_item_annotation(trans.sa_session, stored_workflow.user, stored_workflow, newAnnotation)

# Persist
trans.sa_session.flush()
if stored_workflow.from_path:
Expand All @@ -439,6 +450,12 @@ def _workflow_from_raw_description(self, trans, raw_workflow_description, name,
if 'report' in data:
workflow.reports_config = data['report']

if 'license' in data:
workflow.license = data['license']

if 'creator' in data:
workflow.creator_metadata = data['creator']

# Assume no errors until we find a step that has some
workflow.has_errors = False
# Create each step
Expand Down Expand Up @@ -520,7 +537,7 @@ def to_format_2(wf_dict, **kwds):
elif style == "ga":
wf_dict = self._workflow_to_dict_export(trans, stored, workflow=workflow)
else:
raise exceptions.RequestParameterInvalidException('Unknown workflow style [%s]' % style)
raise exceptions.RequestParameterInvalidException(f'Unknown workflow style {style}')
if version is not None:
wf_dict['version'] = version
else:
Expand Down Expand Up @@ -735,7 +752,6 @@ def _workflow_to_dict_editor(self, trans, stored, workflow, tooltip=True, is_sub
data['upgrade_messages'] = {}
data['report'] = workflow.reports_config or {}
data['license'] = workflow.license
log.info("creator_metadata is %s" % workflow.creator_metadata)
data['creator'] = workflow.creator_metadata

output_label_index = set()
Expand Down Expand Up @@ -1254,8 +1270,7 @@ def __module_from_dict(self, trans, steps, steps_by_external_id, step_dict, **kw
representing type-specific functionality from the incoming dictionary.
"""
step = model.WorkflowStep()
# TODO: Consider handling position inside module.
step.position = step_dict['position']
step.position = step_dict.get('position', model.WorkflowStep.DEFAULT_POSITION)
if step_dict.get("uuid", None) and step_dict['uuid'] != "None":
step.uuid = step_dict["uuid"]
if "label" in step_dict:
Expand All @@ -1265,13 +1280,13 @@ def __module_from_dict(self, trans, steps, steps_by_external_id, step_dict, **kw
self.__set_default_label(step, module, step_dict.get('tool_state'))
module.save_to_step(step)

annotation = step_dict['annotation']
annotation = step_dict.get('annotation')
if annotation:
annotation = sanitize_html(annotation)
self.add_item_annotation(trans.sa_session, trans.get_user(), step, annotation)

# Stick this in the step temporarily
step.temp_input_connections = step_dict['input_connections']
step.temp_input_connections = step_dict.get('input_connections', {})

# Create the model class for the step
steps.append(step)
Expand Down Expand Up @@ -1380,6 +1395,28 @@ def __set_default_label(self, step, module, state):
if default_label and util.unicodify(default_label).lower() not in ['input dataset', 'input dataset collection']:
step.label = module.label = default_label

def refactor(self, trans, stored_workflow, refactor_request):
"""Apply supplied actions to stored_workflow.latest_workflow to build a new version.
"""
workflow = stored_workflow.latest_workflow
as_dict = self.workflow_to_dict(trans, stored_workflow, style="ga")
raw_workflow_description = self.normalize_workflow_format(trans, as_dict)
module_injector = WorkflowModuleInjector(trans)
WorkflowRefactorExecutor(raw_workflow_description, workflow, module_injector).refactor(refactor_request)
if refactor_request.dry_run:
# TODO: go a bit further with dry run, try to re-populate a workflow just
# don't flush it or tie it to the stored_workflow. Still for now, there is
# a lot of things that would be caught with just what is done here.
return None, []
else:
return self.update_workflow_from_raw_description(
trans,
stored_workflow,
raw_workflow_description,
fill_defaults=True,
update_stored_workflow_attributes=True,
)


class MissingToolsException(exceptions.MessageException):

Expand Down
7 changes: 7 additions & 0 deletions lib/galaxy/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4894,6 +4894,12 @@ def step_by_index(self, order_index):
return step
raise KeyError("Workflow has no step with order_index '%s'" % order_index)

def step_by_label(self, label):
for step in self.steps:
if label == step.label:
return step
raise KeyError("Workflow has no step with label '%s'" % label)

@property
def input_steps(self):
for step in self.steps:
Expand Down Expand Up @@ -4983,6 +4989,7 @@ class WorkflowStep(RepresentById):
"data_collection_input": "dataset_collection",
"parameter_input": "parameter",
}
DEFAULT_POSITION = {"left": 0, "top": 0}

def __init__(self):
self.id = None
Expand Down
26 changes: 26 additions & 0 deletions lib/galaxy/webapps/galaxy/api/workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
)
from galaxy.workflow.extract import extract_workflow
from galaxy.workflow.modules import module_factory
from galaxy.workflow.refactor.schema import RefactorRequest
from galaxy.workflow.run import invoke, queue_invoke
from galaxy.workflow.run_request import build_workflow_run_configs

Expand Down Expand Up @@ -646,6 +647,31 @@ def update(self, trans, id, payload, **kwds):
raise exceptions.RequestParameterInvalidException(message)
return self.workflow_contents_manager.workflow_to_dict(trans, stored_workflow, style="instance")

@expose_api
def refactor(self, trans, id, payload, **kwds):
"""
* PUT /api/workflows/{id}/refactor
updates the workflow stored with ``id``
:type id: str
:param id: the encoded id of the workflow to update
:param instance: true if fetch by Workflow ID instead of StoredWorkflow id, false
by default.
:type instance: boolean
:type payload: dict
:param payload: a dictionary containing list of actions to apply.
:rtype: dict
:returns: serialized version of the workflow
"""
stored_workflow = self.__get_stored_workflow(trans, id, **kwds)
refactor_request = RefactorRequest(**payload)
style = payload.get("style", "export")
result, errors = self.workflow_contents_manager.refactor(
trans, stored_workflow, refactor_request
)
# TODO: handle errors...
return self.workflow_contents_manager.workflow_to_dict(trans, stored_workflow, style=style)

@expose_api
def build_module(self, trans, payload=None):
"""
Expand Down
1 change: 1 addition & 0 deletions lib/galaxy/webapps/galaxy/buildapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,7 @@ def populate_api_routes(webapp, app):
webapp.mapper.connect('/api/workflows/build_module', action='build_module', controller="workflows")
webapp.mapper.connect('/api/workflows/menu', action='get_workflow_menu', controller="workflows", conditions=dict(method=["GET"]))
webapp.mapper.connect('/api/workflows/menu', action='set_workflow_menu', controller="workflows", conditions=dict(method=["PUT"]))
webapp.mapper.connect('/api/workflows/{id}/refactor', action='refactor', controller="workflows", conditions=dict(method=["PUT"]))
webapp.mapper.resource('workflow', 'workflows', path_prefix='/api')
webapp.mapper.connect('/api/licenses', controller='licenses', action='index')
webapp.mapper.connect('/api/licenses/{id}', controller='licenses', action='get')
Expand Down
1 change: 1 addition & 0 deletions lib/galaxy/workflow/modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,7 @@ def _parse_state_into_dict(self):


class InputParameterModule(WorkflowModule):
POSSIBLE_PARAMETER_TYPES = ["text", "integer", "float", "boolean", "color"]
type = "parameter_input"
name = "Input parameter"
default_parameter_type = "text"
Expand Down
Empty file.
Loading

0 comments on commit 6284358

Please sign in to comment.