Skip to content

Commit

Permalink
Tool Landing API...
Browse files Browse the repository at this point in the history
  • Loading branch information
jmchilton committed Dec 3, 2024
1 parent afd76a6 commit 7c1897a
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 41 deletions.
30 changes: 26 additions & 4 deletions lib/galaxy/managers/landing.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,20 @@
WorkflowLandingRequest,
)
from galaxy.security.idencoding import IdEncodingHelper
from galaxy.structured_app import StructuredApp
from galaxy.structured_app import (
MinimalManagerApp,
StructuredApp,
)
from galaxy.tool_util.parameters import (
landing_decode,
LandingRequestToolState,
)
from galaxy.util import safe_str_cmp
from .context import ProvidesUserContext
from .tools import (
get_tool_from_toolbox,
ToolRunReference,
)

LandingRequestModel = Union[ToolLandingRequestModel, WorkflowLandingRequestModel]

Expand All @@ -44,16 +55,27 @@ def __init__(
sa_session: galaxy_scoped_session,
security: IdEncodingHelper,
workflow_contents_manager: WorkflowContentsManager,
app: MinimalManagerApp,
):
self.sa_session = sa_session
self.security = security
self.workflow_contents_manager = workflow_contents_manager
self.app = app

def create_tool_landing_request(self, payload: CreateToolLandingRequestPayload, user_id=None) -> ToolLandingRequest:
tool_id = payload.tool_id
tool_version = payload.tool_version
request_state = payload.request_state

ref = ToolRunReference(tool_id=tool_id, tool_version=tool_version, tool_uuid=None)
tool = get_tool_from_toolbox(self.app.toolbox, ref)
landing_request_state = LandingRequestToolState(request_state or {})
internal_landing_request_state = landing_decode(landing_request_state, tool, self.security.decode_id)

model = ToolLandingRequestModel()
model.tool_id = payload.tool_id
model.tool_version = payload.tool_version
model.request_state = payload.request_state
model.tool_id = tool_id
model.tool_version = tool_version
model.request_state = internal_landing_request_state.input_state
model.uuid = uuid4()
model.client_secret = payload.client_secret
model.public = payload.public
Expand Down
27 changes: 27 additions & 0 deletions lib/galaxy/managers/tools.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
from typing import (
NamedTuple,
Optional,
TYPE_CHECKING,
Union,
Expand All @@ -16,8 +17,10 @@
model,
)
from galaxy.exceptions import DuplicatedIdentifierException
from galaxy.managers.context import ProvidesUserContext
from galaxy.model import DynamicTool
from galaxy.tool_util.cwl import tool_proxy
from galaxy.tools import Tool
from .base import (
ModelManager,
raise_filter_err,
Expand All @@ -30,6 +33,30 @@
from galaxy.managers.base import OrmFilterParsersType


class ToolRunReference(NamedTuple):
tool_id: Optional[str]
tool_uuid: Optional[str]
tool_version: Optional[str]


def get_tool_from_trans(trans: ProvidesUserContext, tool_ref: ToolRunReference) -> Tool:
return get_tool_from_toolbox(trans.app.toolbox, tool_ref)


def get_tool_from_toolbox(toolbox, tool_ref: ToolRunReference) -> Tool:
get_kwds = dict(
tool_id=tool_ref.tool_id,
tool_uuid=tool_ref.tool_uuid,
tool_version=tool_ref.tool_version,
)

tool = toolbox.get_tool(**get_kwds)
if not tool:
log.debug(f"Not found tool with kwds [{tool_ref}]")
raise exceptions.ToolMissingException("Tool not found.")
return tool


class DynamicToolManager(ModelManager):
"""Manages dynamic tools stored in Galaxy's database."""

Expand Down
47 changes: 41 additions & 6 deletions lib/galaxy/webapps/galaxy/api/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
Request,
UploadFile,
)
from pydantic import UUID4
from starlette.datastructures import UploadFile as StarletteUploadFile

from galaxy import (
Expand All @@ -26,16 +27,26 @@
)
from galaxy.datatypes.data import get_params_and_input_name
from galaxy.managers.collections import DatasetCollectionManager
from galaxy.managers.context import ProvidesHistoryContext
from galaxy.managers.context import (
ProvidesHistoryContext,
ProvidesUserContext,
)
from galaxy.managers.hdas import HDAManager
from galaxy.managers.histories import HistoryManager
from galaxy.managers.landing import LandingRequestManager
from galaxy.managers.tools import ToolRunReference
from galaxy.model import ToolRequest
from galaxy.schema.fetch_data import (
FetchDataFormPayload,
FetchDataPayload,
)
from galaxy.schema.fields import DecodedDatabaseIdField
from galaxy.schema.schema import ToolRequestModel
from galaxy.schema.schema import (
ClaimLandingPayload,
CreateToolLandingRequestPayload,
ToolLandingRequest,
ToolRequestModel,
)
from galaxy.tool_util.parameters import ToolParameterT
from galaxy.tool_util.verify import ToolTestDescriptionDict
from galaxy.tools.evaluation import global_tool_errors
Expand All @@ -49,16 +60,14 @@
from galaxy.webapps.base.controller import UsesVisualizationMixin
from galaxy.webapps.base.webapp import GalaxyWebTransaction
from galaxy.webapps.galaxy.services.base import tool_request_to_model
from galaxy.webapps.galaxy.services.tools import (
ToolRunReference,
ToolsService,
)
from galaxy.webapps.galaxy.services.tools import ToolsService
from . import (
APIContentTypeRoute,
as_form,
BaseGalaxyAPIController,
depends,
DependsOnTrans,
LandingUuidPathParam,
Router,
)

Expand Down Expand Up @@ -105,6 +114,7 @@ async def get_files(request: Request, files: Optional[List[UploadFile]] = None):
@router.cbv
class FetchTools:
service: ToolsService = depends(ToolsService)
landing_manager: LandingRequestManager = depends(LandingRequestManager)

@router.post("/api/tools/fetch", summary="Upload files to Galaxy", route_class_override=JsonApiRoute)
def fetch_json(self, payload: FetchDataPayload = Body(...), trans: ProvidesHistoryContext = DependsOnTrans):
Expand Down Expand Up @@ -161,6 +171,31 @@ def _get_tool_request_or_raise_not_found(
assert tool_request
return tool_request

@router.post("/api/tool_landings", public=True)
def create_landing(
self,
trans: ProvidesUserContext = DependsOnTrans,
tool_landing_request: CreateToolLandingRequestPayload = Body(...),
) -> ToolLandingRequest:
return self.landing_manager.create_tool_landing_request(tool_landing_request)

@router.post("/api/tool_landings/{uuid}/claim")
def claim_landing(
self,
trans: ProvidesUserContext = DependsOnTrans,
uuid: UUID4 = LandingUuidPathParam,
payload: Optional[ClaimLandingPayload] = Body(...),
) -> ToolLandingRequest:
return self.landing_manager.claim_tool_landing_request(trans, uuid, payload)

@router.get("/api/tool_landings/{uuid}")
def get_landing(
self,
trans: ProvidesUserContext = DependsOnTrans,
uuid: UUID4 = LandingUuidPathParam,
) -> ToolLandingRequest:
return self.landing_manager.get_tool_landing_request(trans, uuid)

@router.get(
"/api/tools/{tool_id}/inputs",
summary="Get tool inputs.",
Expand Down
6 changes: 2 additions & 4 deletions lib/galaxy/webapps/galaxy/services/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
JobSearch,
view_show_job,
)
from galaxy.managers.tools import ToolRunReference
from galaxy.model import (
Job,
ToolRequest,
Expand Down Expand Up @@ -60,10 +61,7 @@
async_task_summary,
ServiceBase,
)
from .tools import (
ToolRunReference,
validate_tool_for_running,
)
from .tools import validate_tool_for_running

log = logging.getLogger(__name__)

Expand Down
29 changes: 6 additions & 23 deletions lib/galaxy/webapps/galaxy/services/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
cast,
Dict,
List,
NamedTuple,
Optional,
Union,
)
Expand All @@ -25,6 +24,10 @@
ProvidesUserContext,
)
from galaxy.managers.histories import HistoryManager
from galaxy.managers.tools import (
get_tool_from_trans,
ToolRunReference,
)
from galaxy.model import (
LibraryDatasetDatasetAssociation,
PostJobAction,
Expand All @@ -46,34 +49,14 @@
log = logging.getLogger(__name__)


class ToolRunReference(NamedTuple):
tool_id: Optional[str]
tool_uuid: Optional[str]
tool_version: Optional[str]


def get_tool(trans: ProvidesHistoryContext, tool_ref: ToolRunReference) -> Tool:
get_kwds = dict(
tool_id=tool_ref.tool_id,
tool_uuid=tool_ref.tool_uuid,
tool_version=tool_ref.tool_version,
)

tool = trans.app.toolbox.get_tool(**get_kwds)
if not tool:
log.debug(f"Not found tool with kwds [{tool_ref}]")
raise exceptions.ToolMissingException("Tool not found.")
return tool


def validate_tool_for_running(trans: ProvidesHistoryContext, tool_ref: ToolRunReference) -> Tool:
if trans.user_is_bootstrap_admin:
raise exceptions.RealUserRequiredException("Only real users can execute tools or run jobs.")

if tool_ref.tool_id is None and tool_ref.tool_uuid is None:
raise exceptions.RequestParameterMissingException("Must specify a valid tool_id to use this endpoint.")

tool = get_tool(trans, tool_ref)
tool = get_tool_from_trans(trans, tool_ref)
if not tool.allow_user_access(trans.user):
raise exceptions.ItemAccessibilityException("Tool not accessible.")
return tool
Expand All @@ -97,7 +80,7 @@ def inputs(
trans: ProvidesHistoryContext,
tool_ref: ToolRunReference,
) -> List[ToolParameterT]:
tool = get_tool(trans, tool_ref)
tool = get_tool_from_trans(trans, tool_ref)
return tool.parameters

def create_fetch(
Expand Down
31 changes: 31 additions & 0 deletions lib/galaxy_test/api/test_landing.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@
)

from galaxy.schema.schema import (
CreateToolLandingRequestPayload,
CreateWorkflowLandingRequestPayload,
WorkflowLandingRequest,
)
from galaxy_test.base.api_asserts import (
assert_error_code_is,
assert_status_code_is,
)
from galaxy_test.base.populators import (
DatasetPopulator,
skip_without_tool,
Expand All @@ -25,6 +30,32 @@ def setUp(self):
self.dataset_populator = DatasetPopulator(self.galaxy_interactor)
self.workflow_populator = WorkflowPopulator(self.galaxy_interactor)

@skip_without_tool("cat")
def test_tool_landing(self):
request = CreateToolLandingRequestPayload(
tool_id="create_2",
tool_version=None,
request_state={"sleep_time": 0},
)
response = self.dataset_populator.create_tool_landing(request)
assert response.tool_id == "create_2"
assert response.state == "unclaimed"
response = self.dataset_populator.claim_tool_landing(response.uuid)
assert response.tool_id == "create_2"
assert response.state == "claimed"

@skip_without_tool("gx_int")
def test_tool_landing_invalid(self):
request = CreateToolLandingRequestPayload(
tool_id="gx_int",
tool_version=None,
request_state={"parameter": "foobar"},
)
response = self.dataset_populator.create_tool_landing_raw(request)
assert_status_code_is(response, 400)
assert_error_code_is(response, 400008)
assert "Input should be a valid integer" in response.text

@skip_without_tool("cat1")
def test_create_public_workflow_landing_authenticated_user(self):
request = _get_simple_landing_payload(self.workflow_populator, public=True)
Expand Down
10 changes: 7 additions & 3 deletions lib/galaxy_test/base/populators.py
Original file line number Diff line number Diff line change
Expand Up @@ -795,13 +795,17 @@ def _wait_for_purge():
return self._get(dataset_url)

def create_tool_landing(self, payload: CreateToolLandingRequestPayload) -> ToolLandingRequest:
create_url = "tool_landings"
json = payload.model_dump(mode="json")
create_response = self._post(create_url, json, json=True, anon=True)
create_response = self.create_tool_landing_raw(payload)
api_asserts.assert_status_code_is(create_response, 200)
create_response.raise_for_status()
return ToolLandingRequest.model_validate(create_response.json())

def create_tool_landing_raw(self, payload: CreateToolLandingRequestPayload) -> Response:
create_url = "tool_landings"
json = payload.model_dump(mode="json")
create_response = self._post(create_url, json, json=True, anon=True)
return create_response

def create_workflow_landing(self, payload: CreateWorkflowLandingRequestPayload) -> WorkflowLandingRequest:
create_url = "workflow_landings"
json = payload.model_dump(mode="json")
Expand Down
Loading

0 comments on commit 7c1897a

Please sign in to comment.