From abb69b552cfaea1f869ea9d9f72917f620543216 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 11 Apr 2022 17:19:38 -0400 Subject: [PATCH] Implement API endpoints and tests for workflow sharing options. --- lib/galaxy/managers/workflows.py | 28 +++- lib/galaxy/webapps/galaxy/api/workflows.py | 123 +++++++++++++++++- .../webapps/galaxy/services/workflows.py | 17 +++ lib/galaxy_test/api/test_workflows.py | 26 ++++ test/unit/workflows/workflow_support.py | 13 +- 5 files changed, 194 insertions(+), 13 deletions(-) create mode 100644 lib/galaxy/webapps/galaxy/services/workflows.py diff --git a/lib/galaxy/managers/workflows.py b/lib/galaxy/managers/workflows.py index da66b41341b..c78aa2284d5 100644 --- a/lib/galaxy/managers/workflows.py +++ b/lib/galaxy/managers/workflows.py @@ -30,6 +30,7 @@ from galaxy import ( util, ) from galaxy.job_execution.actions.post import ActionBox +from galaxy.managers import sharable from galaxy.model.item_attrs import UsesAnnotations from galaxy.structured_app import MinimalManagerApp from galaxy.tools.parameters import ( @@ -68,13 +69,18 @@ from .executables import artifact_class log = logging.getLogger(__name__) -class WorkflowsManager: +class WorkflowsManager(sharable.SharableModelManager): """Handle CRUD type operations related to workflows. More interesting stuff regarding workflow execution, step sorting, etc... can be found in the galaxy.workflow module. """ + model_class = model.StoredWorkflow + foreign_key_name = "stored_workflow" + user_share_model = model.StoredWorkflowUserShareAssociation + def __init__(self, app: MinimalManagerApp): + super().__init__(app) self.app = app def get_stored_workflow(self, trans, workflow_id, by_stored_id=True): @@ -349,6 +355,26 @@ class WorkflowsManager: CreatedWorkflow = namedtuple("CreatedWorkflow", ["stored_workflow", "workflow", "missing_tools"]) +class WorkflowSerializer(sharable.SharableModelSerializer): + """ + Interface/service object for serializing stored workflows into dictionaries. + + These are used for simple workflow operations - much more detailed serialization operations + that allow recovering the workflow in its entirity or rendering it in a workflow + editor are available inside the WorkflowContentsManager. + """ + + model_manager_class = WorkflowsManager + SINGLE_CHAR_ABBR = "w" + + def __init__(self, app: MinimalManagerApp): + super().__init__(app) + + def add_serializers(self): + super().add_serializers() + self.serializers.update({}) + + class WorkflowContentsManager(UsesAnnotations): def __init__(self, app: MinimalManagerApp): self.app = app diff --git a/lib/galaxy/webapps/galaxy/api/workflows.py b/lib/galaxy/webapps/galaxy/api/workflows.py index 8fbeff0c1c4..65e3ed11dce 100644 --- a/lib/galaxy/webapps/galaxy/api/workflows.py +++ b/lib/galaxy/webapps/galaxy/api/workflows.py @@ -12,6 +12,12 @@ from typing import ( ) import requests +from fastapi import ( + Body, + Path, + Response, + status, +) from gxformat2._yaml import ordered_dump from markupsafe import escape from sqlalchemy import ( @@ -40,7 +46,14 @@ from galaxy.managers.workflows import ( WorkflowUpdateOptions, ) from galaxy.model.item_attrs import UsesAnnotations -from galaxy.schema.schema import InvocationIndexPayload +from galaxy.schema.fields import EncodedDatabaseIdField +from galaxy.schema.schema import ( + InvocationIndexPayload, + SetSlugPayload, + ShareWithPayload, + ShareWithStatus, + SharingStatus, +) from galaxy.structured_app import StructuredApp from galaxy.tool_shed.galaxy_install.install_manager import InstallRepositoryManager from galaxy.tools import recommendations @@ -61,14 +74,22 @@ from galaxy.webapps.base.controller import ( UsesStoredWorkflowMixin, ) from galaxy.webapps.base.webapp import GalaxyWebTransaction +from galaxy.webapps.galaxy.services.workflows import WorkflowsService from galaxy.workflow.extract import extract_workflow from galaxy.workflow.modules import module_factory from galaxy.workflow.run import queue_invoke from galaxy.workflow.run_request import build_workflow_run_configs -from . import BaseGalaxyAPIController +from . import ( + BaseGalaxyAPIController, + depends, + DependsOnTrans, + Router, +) log = logging.getLogger(__name__) +router = Router(tags=["workflows"]) + class WorkflowsAPIController(BaseGalaxyAPIController, UsesStoredWorkflowMixin, UsesAnnotations, SharableMixin): def __init__(self, app: StructuredApp): @@ -1492,3 +1513,101 @@ class WorkflowsAPIController(BaseGalaxyAPIController, UsesStoredWorkflowMixin, U def __encode_invocation(self, invocation, **kwd): return self.workflow_manager.serialize_workflow_invocation(invocation, **kwd) + + +StoredWorkflowIDPathParam: EncodedDatabaseIdField = Path( + ..., title="Stored Workflow ID", description="The encoded database identifier of the Stored Workflow." +) + + +@router.cbv +class FastAPIWorkflows: + service: WorkflowsService = depends(WorkflowsService) + + @router.get( + "/api/workflows/{id}/sharing", + summary="Get the current sharing status of the given item.", + ) + def sharing( + self, + trans: ProvidesUserContext = DependsOnTrans, + id: EncodedDatabaseIdField = StoredWorkflowIDPathParam, + ) -> SharingStatus: + """Return the sharing status of the item.""" + return self.service.shareable_service.sharing(trans, id) + + @router.put( + "/api/workflows/{id}/enable_link_access", + summary="Makes this item accessible by a URL link.", + ) + def enable_link_access( + self, + trans: ProvidesUserContext = DependsOnTrans, + id: EncodedDatabaseIdField = StoredWorkflowIDPathParam, + ) -> SharingStatus: + """Makes this item accessible by a URL link and return the current sharing status.""" + return self.service.shareable_service.enable_link_access(trans, id) + + @router.put( + "/api/workflows/{id}/disable_link_access", + summary="Makes this item inaccessible by a URL link.", + ) + def disable_link_access( + self, + trans: ProvidesUserContext = DependsOnTrans, + id: EncodedDatabaseIdField = StoredWorkflowIDPathParam, + ) -> SharingStatus: + """Makes this item inaccessible by a URL link and return the current sharing status.""" + return self.service.shareable_service.disable_link_access(trans, id) + + @router.put( + "/api/workflows/{id}/publish", + summary="Makes this item public and accessible by a URL link.", + ) + def publish( + self, + trans: ProvidesUserContext = DependsOnTrans, + id: EncodedDatabaseIdField = StoredWorkflowIDPathParam, + ) -> SharingStatus: + """Makes this item publicly available by a URL link and return the current sharing status.""" + return self.service.shareable_service.publish(trans, id) + + @router.put( + "/api/workflows/{id}/unpublish", + summary="Removes this item from the published list.", + ) + def unpublish( + self, + trans: ProvidesUserContext = DependsOnTrans, + id: EncodedDatabaseIdField = StoredWorkflowIDPathParam, + ) -> SharingStatus: + """Removes this item from the published list and return the current sharing status.""" + return self.service.shareable_service.unpublish(trans, id) + + @router.put( + "/api/workflows/{id}/share_with_users", + summary="Share this item with specific users.", + ) + def share_with_users( + self, + trans: ProvidesUserContext = DependsOnTrans, + id: EncodedDatabaseIdField = StoredWorkflowIDPathParam, + payload: ShareWithPayload = Body(...), + ) -> ShareWithStatus: + """Shares this item with specific users and return the current sharing status.""" + return self.service.shareable_service.share_with_users(trans, id, payload) + + @router.put( + "/api/workflows/{id}/slug", + summary="Set a new slug for this shared item.", + status_code=status.HTTP_204_NO_CONTENT, + ) + def set_slug( + self, + trans: ProvidesUserContext = DependsOnTrans, + id: EncodedDatabaseIdField = StoredWorkflowIDPathParam, + payload: SetSlugPayload = Body(...), + ): + """Sets a new slug to access this item by URL. The new slug must be unique.""" + self.service.shareable_service.set_slug(trans, id, payload) + return Response(status_code=status.HTTP_204_NO_CONTENT) diff --git a/lib/galaxy/webapps/galaxy/services/workflows.py b/lib/galaxy/webapps/galaxy/services/workflows.py new file mode 100644 index 00000000000..4ec3aef5b6a --- /dev/null +++ b/lib/galaxy/webapps/galaxy/services/workflows.py @@ -0,0 +1,17 @@ +from galaxy.managers.workflows import ( + WorkflowSerializer, + WorkflowsManager, +) +from galaxy.webapps.galaxy.services.base import ServiceBase +from galaxy.webapps.galaxy.services.sharable import ShareableService + + +class WorkflowsService(ServiceBase): + def __init__( + self, + workflows_manager: WorkflowsManager, + serializer: WorkflowSerializer, + ): + self._workflows_manager = workflows_manager + self._serializer = serializer + self.shareable_service = ShareableService(workflows_manager, serializer) diff --git a/lib/galaxy_test/api/test_workflows.py b/lib/galaxy_test/api/test_workflows.py index 5a6981d6f7e..61a4814bf9b 100644 --- a/lib/galaxy_test/api/test_workflows.py +++ b/lib/galaxy_test/api/test_workflows.py @@ -53,6 +53,7 @@ from galaxy_test.base.workflow_fixtures import ( WORKFLOW_WITH_RULES_1, ) from ._framework import ApiTestCase +from .sharable import SharingApiTests WORKFLOW_SIMPLE = """ class: GalaxyWorkflow @@ -223,6 +224,31 @@ input1: assert details_dataset_new_col["metadata_startCol"] == 3 +class WorkflowsSharingApiTestCase(ApiTestCase, SharingApiTests): + + api_name = "workflows" + + def create(self, name: str) -> str: + """Creates a shareable resource with the given name and returns it's ID. + + :param name: The name of the shareable resource to create. + :return: The ID of the resource. + """ + workflow = self.workflow_populator.load_workflow(name=name) + data = dict( + workflow=dumps(workflow), + ) + route = "workflows" + upload_response = self._post(route, data=data) + + self._assert_status_code_is(upload_response, 200) + return upload_response.json()["id"] + + def setUp(self): + super().setUp() + self.workflow_populator = WorkflowPopulator(self.galaxy_interactor) + + # Workflow API TODO: # - Allow history_id as param to workflow run action. (hist_id) # - Allow post to workflows//run in addition to posting to diff --git a/test/unit/workflows/workflow_support.py b/test/unit/workflows/workflow_support.py index c35cef2b12a..a4d54e0d3e5 100644 --- a/test/unit/workflows/workflow_support.py +++ b/test/unit/workflows/workflow_support.py @@ -3,10 +3,8 @@ from functools import partial import yaml from galaxy import model +from galaxy.app_unittest_utils import galaxy_mock from galaxy.managers.workflows import WorkflowsManager -from galaxy.model import mapping -from galaxy.security.idencoding import IdEncodingHelper -from galaxy.util import bunch from galaxy.workflow.modules import module_factory @@ -32,15 +30,10 @@ class MockTrans: return self._user -class MockApp: +class MockApp(galaxy_mock.MockApp): def __init__(self): - self.config = bunch.Bunch( - tool_secret="awesome_secret", - ) - self.model = mapping.init("/tmp", "sqlite:///:memory:", create_tables=True) + super().__init__() self.toolbox = MockToolbox() - self.datatypes_registry = MockDatatypesRegistry() - self.security = IdEncodingHelper(id_secret="testing") self.workflow_manager = WorkflowsManager(self)