diff --git a/lib/galaxy/webapps/galaxy/api/tools.py b/lib/galaxy/webapps/galaxy/api/tools.py index 2a5d016e03d..b8733314123 100644 --- a/lib/galaxy/webapps/galaxy/api/tools.py +++ b/lib/galaxy/webapps/galaxy/api/tools.py @@ -548,6 +548,9 @@ class ToolsController(BaseGalaxyAPIController, UsesVisualizationMixin): :param q: if present search on the given query will be performed :param tool_id: if present the given tool_id will be searched for all installed versions + + Note: When lazy toolbox is enabled, search and flat listing use a + pre-computed index for O(1) access instead of iterating over all tools. """ # Read params. @@ -565,20 +568,19 @@ class ToolsController(BaseGalaxyAPIController, UsesVisualizationMixin): hits = favorites["tools"] else: hits = None - else: - hits = self.service._search(q, view) - results = [] - if hits: - for hit in hits: - try: - tool = self.service._get_tool(trans, hit, user=trans.user) - if tool: - results.append(tool.id) - except exceptions.AuthenticationFailed: - pass - except exceptions.ObjectNotFound: - pass - return results + results = [] + if hits: + for hit in hits: + try: + tool = self.service._get_tool(trans, hit, user=trans.user) + if tool: + results.append(tool.id) + except exceptions.AuthenticationFailed: + pass + except exceptions.ObjectNotFound: + pass + return results + return self.service.search_tools(trans, q, view=view) # Find whether to detect. if tool_id: @@ -587,7 +589,7 @@ class ToolsController(BaseGalaxyAPIController, UsesVisualizationMixin): # Return everything. try: - return self.app.toolbox.to_dict(trans, in_panel=in_panel, tool_help=tool_help, view=view) + return self.service.list_tools(trans, in_panel=in_panel, tool_help=tool_help, view=view) except exceptions.MessageException: raise except Exception: @@ -600,12 +602,11 @@ class ToolsController(BaseGalaxyAPIController, UsesVisualizationMixin): """ GET /api/tool_panels returns a dictionary of available tool panel views and default view - """ - rval = {} - rval["default_panel_view"] = self.app.toolbox._default_panel_view(trans) - rval["views"] = self.app.toolbox.panel_view_dicts() - return rval + Note: When lazy toolbox is enabled, this endpoint uses a pre-computed index + for O(1) access. + """ + return self.service.get_panel_views(trans) @expose_api_anonymous_and_sessionless def panel_view(self, trans: GalaxyWebTransaction, view, **kwds): @@ -716,20 +717,11 @@ class ToolsController(BaseGalaxyAPIController, UsesVisualizationMixin): the tests. Fetch complete test data for each tool with /api/tools/{tool_id}/test_data?tool_version= + + Note: When lazy toolbox is enabled, this endpoint uses a pre-computed index + for O(1) access instead of iterating over all tools. """ - test_counts_by_tool: dict[str, dict] = {} - for _id, tool in self.app.toolbox.tools(): - if not tool.is_datatype_converter: - tests = tool.tests - if tests: - if tool.id not in test_counts_by_tool: - test_counts_by_tool[tool.id] = {} - available_versions = test_counts_by_tool[tool.id] - available_versions[tool.version] = { - "tool_name": tool.name, - "count": len(tests), - } - return test_counts_by_tool + return self.service.get_tests_summary(trans) @expose_api_anonymous_and_sessionless def test_data(self, trans: GalaxyWebTransaction, id, **kwd) -> list[ToolTestDescriptionDict]: @@ -779,9 +771,11 @@ class ToolsController(BaseGalaxyAPIController, UsesVisualizationMixin): """ GET /api/tools/all_requirements Return list of unique requirements for all tools. - """ - return trans.app.toolbox.all_requirements + Note: When lazy toolbox is enabled, this endpoint uses a pre-computed index + for O(1) access instead of iterating over all tools. + """ + return self.service.get_all_requirements(trans) @web.require_admin @expose_api diff --git a/lib/galaxy/webapps/galaxy/services/tools.py b/lib/galaxy/webapps/galaxy/services/tools.py index 0446646ebdc..86959c35edb 100644 --- a/lib/galaxy/webapps/galaxy/services/tools.py +++ b/lib/galaxy/webapps/galaxy/services/tools.py @@ -7,6 +7,8 @@ from typing import ( Any, cast, get_args, + Optional, + TYPE_CHECKING, ) from uuid import UUID @@ -65,6 +67,9 @@ from galaxy.util.path import safe_contains from galaxy.webapps.galaxy.services._fetch_util import validate_and_normalize_targets from galaxy.webapps.galaxy.services.base import ServiceBase +if TYPE_CHECKING: + from galaxy.tools.lazy_toolbox import LazyToolBox + log = logging.getLogger(__name__) ToolRunPayload = dict[str, Any] @@ -553,3 +558,118 @@ class ToolsService(ServiceBase): if os.path.exists(file_path): return file_path return None + + # === Batch Endpoint Methods (use lazy toolbox index when available) === + + def _get_lazy_toolbox(self, trans: ProvidesUserContext) -> Optional["LazyToolBox"]: + """Return the active toolbox if it's a LazyToolBox, else None.""" + from galaxy.tools.lazy_toolbox import LazyToolBox + + toolbox = trans.app.toolbox + return toolbox if isinstance(toolbox, LazyToolBox) else None + + def get_tests_summary(self, trans: ProvidesUserContext) -> dict[str, dict[str, dict[str, Any]]]: + """ + Get tests summary for all tools. + + Uses the lazy toolbox index when available for O(1) access, + otherwise falls back to iterating over the traditional toolbox. + + Returns: + Dictionary of {tool_id: {version: {tool_name, count}}}. + """ + lazy_toolbox = self._get_lazy_toolbox(trans) + if lazy_toolbox and lazy_toolbox.tool_index: + return lazy_toolbox.tool_index.get_tests_summary() + + # Fallback to traditional toolbox iteration + test_counts_by_tool: dict[str, dict] = {} + for _id, tool in trans.app.toolbox.tools(): + if not tool.is_datatype_converter: + tests = tool.tests + if tests: + if tool.id not in test_counts_by_tool: + test_counts_by_tool[tool.id] = {} + available_versions = test_counts_by_tool[tool.id] + available_versions[tool.version] = { + "tool_name": tool.name, + "count": len(tests), + } + return test_counts_by_tool + + def get_all_requirements(self, trans: ProvidesUserContext) -> list[dict[str, Any]]: + """ + Get all unique requirements from all tools. + + Uses the lazy toolbox index when available for O(1) access. + + Returns: + List of unique requirement dictionaries. + """ + lazy_toolbox = self._get_lazy_toolbox(trans) + if lazy_toolbox and lazy_toolbox.tool_index: + return lazy_toolbox.tool_index.get_all_requirements() + + # Fallback to traditional toolbox + return trans.app.toolbox.all_requirements + + def get_panel_views(self, trans: ProvidesUserContext) -> dict[str, Any]: + """ + Get panel views information. + + Uses the lazy toolbox index when available. + + Returns: + Dictionary with default_panel_view and views. + """ + toolbox = trans.app.toolbox + lazy_toolbox = self._get_lazy_toolbox(trans) + if lazy_toolbox and lazy_toolbox.tool_index: + return { + "default_panel_view": toolbox.default_panel_view(trans), + "views": lazy_toolbox.tool_index.get_panel_views(), + } + + # Fallback to traditional toolbox + return { + "default_panel_view": toolbox.default_panel_view(trans), + "views": toolbox.panel_view_dicts(), + } + + def list_tools( + self, + trans: ProvidesUserContext, + in_panel: bool, + tool_help: bool, + view: Optional[str], + ) -> list[dict[str, Any]]: + """ + List tools, preferring the lazy toolbox index when available for flat listings. + + For panel listings or when the index is unavailable, falls back to the + traditional toolbox. + """ + if not in_panel: + lazy_toolbox = self._get_lazy_toolbox(trans) + if lazy_toolbox and lazy_toolbox.tool_index: + entries = lazy_toolbox.tool_index.list_all() + return [entry.to_api_dict() for entry in entries] + return trans.app.toolbox.to_dict(trans, in_panel=in_panel, tool_help=tool_help, view=view) + + def search_tools( + self, + trans: ProvidesUserContext, + query: str, + view: Optional[str] = None, + limit: int = 50, + ) -> list[str]: + """ + Search tools, preferring the lazy toolbox index when available. + + Returns a list of matching tool IDs. + """ + lazy_toolbox = self._get_lazy_toolbox(trans) + if lazy_toolbox and lazy_toolbox.tool_index: + results = lazy_toolbox.tool_index.search(query, limit) + return [entry.id for entry in results] + return list(self._search(query, view) or [])