diff --git a/lib/galaxy/schema/schema.py b/lib/galaxy/schema/schema.py index 06490242b75f..2614c4c5403e 100644 --- a/lib/galaxy/schema/schema.py +++ b/lib/galaxy/schema/schema.py @@ -3749,6 +3749,48 @@ class ToolRequestModel(Model): state_message: Optional[str] +class LandingRequestState(str, Enum): + UNCLAIMED = "unclaimed" + CLAIMED = "claimed" + + +ToolLandingRequestIdField = Field(title="ID", description="Encoded ID of the tool landing request") +WorkflowLandingRequestIdField = Field(title="ID", description="Encoded ID of the workflow landing request") + + +class CreateToolLandingRequestPayload(Model): + tool_id: str + tool_version: Optional[str] = None + request_state: Optional[Dict[str, Any]] = None + client_secret: Optional[str] = None + + +class CreateWorkflowLandingRequestPayload(Model): + workflow_id: str + workflow_target_type: Literal["stored_workflow", "workflow"] + request_state: Optional[Dict[str, Any]] = None + + +class ClaimLandingPayload(Model): + client_secret: Optional[str] = None + + +class ToolLandingRequest(Model): + uuid: UuidField + tool_id: str + tool_version: Optional[str] = None + request_state: Optional[Dict[str, Any]] = None + state: LandingRequestState + + +class WorkflowLandingRequest(Model): + uuid: UuidField + workflow_id: EncodedDatabaseIdField + workflow_target_type: Literal["stored_workflow", "workflow"] + request_state: Dict[str, Any] + state: LandingRequestState + + class AsyncFile(Model): storage_request_id: UUID task: AsyncTaskResultSummary diff --git a/lib/galaxy/webapps/galaxy/api/tools.py b/lib/galaxy/webapps/galaxy/api/tools.py index 9b6ea943f0cb..57d38f37c8c7 100644 --- a/lib/galaxy/webapps/galaxy/api/tools.py +++ b/lib/galaxy/webapps/galaxy/api/tools.py @@ -8,6 +8,7 @@ List, Optional, ) +from pydantic import UUID4 from fastapi import ( Body, @@ -26,16 +27,25 @@ ) 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.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 ( + CreateToolLandingRequestPayload, + ClaimLandingPayload, + ToolLandingRequest, + ToolRequestModel, +) from galaxy.tool_util.parameters import ToolParameterT from galaxy.tool_util.verify import ToolTestDescriptionDict from galaxy.tools.evaluation import global_tool_errors @@ -91,10 +101,17 @@ class JsonApiRoute(APIContentTypeRoute): ) ToolVersionQueryParam: Optional[str] = Query(default=None, title="Tool Version", description="") +LandingUuidPathParam: UUID4 = Path( + ..., + title="Landing UUID", + description="The UUID used to identify a persisted landing request.", +) + @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) async def fetch_json(self, payload: FetchDataPayload = Body(...), trans: ProvidesHistoryContext = DependsOnTrans): @@ -160,6 +177,33 @@ 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: + log.info("\n\n\n\n\n\n... in here....") + try: + return self.landing_manager.create_tool_landing_request(tool_landing_request) + except Exception: + log.exception("Problem...") + raise + + @router.post("/api/tool_landings/{uuid}/claim") + def claim_landing( + self, trans: ProvidesUserContext = DependsOnTrans, uuid: UUID4 = LandingUuidPathParam, payload: Optional[ClaimLandingPayload] = Body(...) + ) -> ToolLandingRequest: + try: + return self.landing_manager.claim_tool_landing_request(trans, uuid, payload) + except Exception: + log.exception("claiim problem...") + raise + + @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, tool_landing_request) + @router.get( "/api/tools/{tool_id}/inputs", summary="Get tool inputs.", diff --git a/lib/galaxy_test/base/populators.py b/lib/galaxy_test/base/populators.py index 3941b87831cf..5f2bfb2f1f1b 100644 --- a/lib/galaxy_test/base/populators.py +++ b/lib/galaxy_test/base/populators.py @@ -79,8 +79,13 @@ from gxformat2.yaml import ordered_load from requests import Response from rocrate.rocrate import ROCrate +from pydantic import UUID4 from typing_extensions import Literal +from galaxy.schema.schema import ( + CreateToolLandingRequestPayload, + ToolLandingRequest, +) from galaxy.tool_util.client.staging import InteractorStaging from galaxy.tool_util.cwl.util import ( download_output, @@ -758,6 +763,20 @@ def _wait_for_purge(): wait_on(_wait_for_purge, "dataset to become purged", timeout=2) 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) + api_asserts.assert_status_code_is(create_response, 200) + create_response.raise_for_status() + return ToolLandingRequest.model_validate(create_response.json()) + + def claim_tool_landing(self, uuid: UUID4): + url = f"tool_landings/{uuid}/claim" + claim_response = self._post(url, {"client_secret": "foobar"}, json=True) + api_asserts.assert_status_code_is(claim_response, 200) + return ToolLandingRequest.model_validate(claim_response.json()) + def create_tool_from_path(self, tool_path: str) -> Dict[str, Any]: tool_directory = os.path.dirname(os.path.abspath(tool_path)) payload = dict( @@ -1681,8 +1700,8 @@ class GalaxyInteractorHttpMixin: def _api_key(self): return self.galaxy_interactor.api_key - def _post(self, route, data=None, files=None, headers=None, admin=False, json: bool = False) -> Response: - return self.galaxy_interactor.post(route, data, files=files, admin=admin, headers=headers, json=json) + def _post(self, route, data=None, files=None, headers=None, admin=False, json: bool = False, anon: bool = False) -> Response: + return self.galaxy_interactor.post(route, data, files=files, admin=admin, headers=headers, json=json, anon=anon) def _put(self, route, data=None, headers=None, admin=False, json: bool = False): return self.galaxy_interactor.put(route, data, headers=headers, admin=admin, json=json)