From 8d7ff7c3f97c2e9f681a5619bfff13517437fbe0 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 30 Sep 2024 15:35:00 -0400 Subject: [PATCH] More json schema API options... --- lib/galaxy/tool_util/parameters/__init__.py | 2 + lib/galaxy/webapps/galaxy/api/__init__.py | 13 +++++ lib/galaxy/webapps/galaxy/api/tools.py | 64 ++++++++++++++++++++- lib/galaxy_test/api/test_tools.py | 27 +++++++++ lib/tool_shed/webapp/api2/tools.py | 43 +++++++++++--- 5 files changed, 139 insertions(+), 10 deletions(-) diff --git a/lib/galaxy/tool_util/parameters/__init__.py b/lib/galaxy/tool_util/parameters/__init__.py index 22dd7e6053aa..3435f81d25e1 100644 --- a/lib/galaxy/tool_util/parameters/__init__.py +++ b/lib/galaxy/tool_util/parameters/__init__.py @@ -66,6 +66,7 @@ ValidationFunctionT, ) from .state import ( + HasToolParameters, JobInternalToolState, LandingRequestInternalToolState, LandingRequestToolState, @@ -140,6 +141,7 @@ "ToolState", "TestCaseToolState", "ToolParameterT", + "HasToolParameters", "to_json_schema_string", "test_case_state", "validate_test_cases_for_tool_source", diff --git a/lib/galaxy/webapps/galaxy/api/__init__.py b/lib/galaxy/webapps/galaxy/api/__init__.py index 95ec6cf4069a..8d2db1544981 100644 --- a/lib/galaxy/webapps/galaxy/api/__init__.py +++ b/lib/galaxy/webapps/galaxy/api/__init__.py @@ -81,6 +81,11 @@ from galaxy.schema.fields import DecodedDatabaseIdField from galaxy.security.idencoding import IdEncodingHelper from galaxy.structured_app import StructuredApp +from galaxy.tool_util.parameters import ( + HasToolParameters, + to_json_schema_string, + ToolState, +) from galaxy.web.framework.decorators import require_admin_message from galaxy.webapps.base.controller import BaseAPIController from galaxy.webapps.galaxy.api.cbv import cbv @@ -611,6 +616,14 @@ async def _as_form(**data): return cls +def json_schema_response_for_tool_state_model( + state_type: Type[ToolState], has_parameters: HasToolParameters +) -> Response: + pydantic_model = state_type.parameter_model_for(has_parameters) + json_str = to_json_schema_string(pydantic_model) + return Response(content=json_str, media_type="application/json") + + async def try_get_request_body_as_json(request: Request) -> Optional[Any]: """Returns the request body as a JSON object if the content type is JSON.""" if "application/json" in request.headers.get("content-type", ""): diff --git a/lib/galaxy/webapps/galaxy/api/tools.py b/lib/galaxy/webapps/galaxy/api/tools.py index dc0c2ecaeb28..d2591bf7335a 100644 --- a/lib/galaxy/webapps/galaxy/api/tools.py +++ b/lib/galaxy/webapps/galaxy/api/tools.py @@ -15,6 +15,7 @@ Path, Query, Request, + Response, UploadFile, ) from pydantic import UUID4 @@ -47,7 +48,12 @@ ToolLandingRequest, ToolRequestModel, ) -from galaxy.tool_util.parameters import ToolParameterT +from galaxy.tool_util.parameters import ( + LandingRequestToolState, + RequestToolState, + TestCaseToolState, + ToolParameterT, +) from galaxy.tool_util.verify import ToolTestDescriptionDict from galaxy.tools.evaluation import global_tool_errors from galaxy.util.zipstream import ZipstreamWrapper @@ -67,6 +73,7 @@ BaseGalaxyAPIController, depends, DependsOnTrans, + json_schema_response_for_tool_state_model, LandingUuidPathParam, Router, ) @@ -209,6 +216,61 @@ def tool_inputs( tool_run_ref = ToolRunReference(tool_id=tool_id, tool_version=tool_version, tool_uuid=None) return self.service.inputs(trans, tool_run_ref) + @router.get( + "/api/tools/{tool_id}/parameter_request_schema", + operation_id="tools__parameter_request_schema", + summary="Return a JSON schema description of the tool's inputs for the tool request API that will be added to Galaxy at some point", + description="The tool request schema includes validation of map/reduce concepts that can be consumed by the tool execution API and not just the request for a single execution.", + ) + def tool_state_request( + self, + tool_id: str = ToolIDPathParam, + tool_version: Optional[str] = ToolVersionQueryParam, + trans: ProvidesHistoryContext = DependsOnTrans, + ) -> Response: + tool_run_ref = ToolRunReference(tool_id=tool_id, tool_version=tool_version, tool_uuid=None) + inputs = self.service.inputs(trans, tool_run_ref) + return json_schema_response_for_tool_state_model( + RequestToolState, + inputs, + ) + + @router.get( + "/api/tools/{tool_id}/parameter_landing_request_schema", + operation_id="tools__parameter_landing_request_schema", + summary="Return a JSON schema description of the tool's inputs for the tool landing request API.", + ) + def tool_state_landing_request( + self, + tool_id: str = ToolIDPathParam, + tool_version: Optional[str] = ToolVersionQueryParam, + trans: ProvidesHistoryContext = DependsOnTrans, + ) -> Response: + tool_run_ref = ToolRunReference(tool_id=tool_id, tool_version=tool_version, tool_uuid=None) + inputs = self.service.inputs(trans, tool_run_ref) + return json_schema_response_for_tool_state_model( + LandingRequestToolState, + inputs, + ) + + @router.get( + "/api/tools/{tool_id}/parameter_test_case_xml_schema", + operation_id="tools__parameter_test_case_xml_schema", + summary="Return a JSON schema description of the tool's inputs for test case construction.", + ) + def tool_state_test_case_xml( + self, + tool_id: str = ToolIDPathParam, + tool_version: Optional[str] = ToolVersionQueryParam, + trans: ProvidesHistoryContext = DependsOnTrans, + ) -> Response: + tool_run_ref = ToolRunReference(tool_id=tool_id, tool_version=tool_version, tool_uuid=None) + inputs = self.service.inputs(trans, tool_run_ref) + return json_schema_response_for_tool_state_model( + TestCaseToolState, + inputs, + ) + class ToolsController(BaseGalaxyAPIController, UsesVisualizationMixin): """ diff --git a/lib/galaxy_test/api/test_tools.py b/lib/galaxy_test/api/test_tools.py index 41d982d246c9..ff2d9a6fe7a0 100644 --- a/lib/galaxy_test/api/test_tools.py +++ b/lib/galaxy_test/api/test_tools.py @@ -13,6 +13,8 @@ from uuid import uuid4 import pytest +from jsonschema import validate +from jsonschema.exceptions import ValidationError from requests import ( get, put, @@ -247,6 +249,31 @@ def test_legacy_biotools_xref_injection(self): assert xref["reftype"] == "bio.tools" assert xref["value"] == "bwa" + @skip_without_tool("gx_int") + def test_tool_schemas(self): + tool_id = "gx_int" + + def get_jsonschema(state_type: str): + schema_url = self._api_url(f"tools/{tool_id}/parameter_{state_type}_schema") + schema_response = get(schema_url) + schema_response.raise_for_status() + return schema_response.json() + + request_schema = get_jsonschema("request") + validate(instance={"parameter": 5}, schema=request_schema) + with pytest.raises(ValidationError): + validate(instance={"parameter": "Foobar"}, schema=request_schema) + + test_case_schema = get_jsonschema("test_case_xml") + validate(instance={"parameter": 5}, schema=test_case_schema) + with pytest.raises(ValidationError): + validate(instance={"parameter": "Foobar"}, schema=test_case_schema) + + landing_schema = get_jsonschema("landing_request") + validate(instance={"parameter": 5}, schema=landing_schema) + with pytest.raises(ValidationError): + validate(instance={"parameter": "Foobar"}, schema=landing_schema) + @skip_without_tool("test_data_source") @skip_if_github_down def test_data_source_ok_request(self): diff --git a/lib/tool_shed/webapp/api2/tools.py b/lib/tool_shed/webapp/api2/tools.py index be5d04da9aeb..b8709060eda9 100644 --- a/lib/tool_shed/webapp/api2/tools.py +++ b/lib/tool_shed/webapp/api2/tools.py @@ -9,9 +9,11 @@ from galaxy.tool_util.models import ParsedTool from galaxy.tool_util.parameters import ( + LandingRequestToolState, RequestToolState, - to_json_schema_string, + TestCaseToolState, ) +from galaxy.webapps.galaxy.api import json_schema_response_for_tool_state_model from tool_shed.context import SessionRequestContext from tool_shed.managers.tools import ( parsed_tool_model_cached_for, @@ -57,11 +59,6 @@ ) -def json_schema_response(pydantic_model) -> Response: - json_str = to_json_schema_string(pydantic_model) - return Response(content=json_str, media_type="application/json") - - @router.cbv class FastAPITools: app: ToolShedApp = depends(ToolShedApp) @@ -158,15 +155,43 @@ def show_tool( @router.get( "/api/tools/{tool_id}/versions/{tool_version}/parameter_request_schema", - operation_id="tools__parameter_request_model", + operation_id="tools__parameter_request_schema", summary="Return a JSON schema description of the tool's inputs for the tool request API that will be added to Galaxy at some point", description="The tool request schema includes validation of map/reduce concepts that can be consumed by the tool execution API and not just the request for a single execution.", ) - def tool_state( + def tool_state_request( + self, + trans: SessionRequestContext = DependsOnTrans, + tool_id: str = TOOL_ID_PATH_PARAM, + tool_version: str = TOOL_VERSION_PATH_PARAM, + ) -> Response: + parsed_tool = parsed_tool_model_cached_for(trans, tool_id, tool_version) + return json_schema_response_for_tool_state_model(RequestToolState, parsed_tool.inputs) + + @router.get( + "/api/tools/{tool_id}/versions/{tool_version}/parameter_landing_request_schema", + operation_id="tools__parameter_landing_request_schema", + summary="Return a JSON schema description of the tool's inputs for the tool landing request API.", + ) + def tool_state_landing_request( + self, + trans: SessionRequestContext = DependsOnTrans, + tool_id: str = TOOL_ID_PATH_PARAM, + tool_version: str = TOOL_VERSION_PATH_PARAM, + ) -> Response: + parsed_tool = parsed_tool_model_cached_for(trans, tool_id, tool_version) + return json_schema_response_for_tool_state_model(LandingRequestToolState, parsed_tool.inputs) + + @router.get( + "/api/tools/{tool_id}/versions/{tool_version}/parameter_test_case_xml_schema", + operation_id="tools__parameter_test_case_xml_schema", + summary="Return a JSON schema description of the tool's inputs for test case construction.", + ) + def tool_state_test_case_xml( self, trans: SessionRequestContext = DependsOnTrans, tool_id: str = TOOL_ID_PATH_PARAM, tool_version: str = TOOL_VERSION_PATH_PARAM, ) -> Response: parsed_tool = parsed_tool_model_cached_for(trans, tool_id, tool_version) - return json_schema_response(RequestToolState.parameter_model_for(parsed_tool.inputs)) + return json_schema_response_for_tool_state_model(TestCaseToolState, parsed_tool.inputs)