From b639bed95ad2ca76af97e656d925cc7aa8f10ac6 Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Mon, 4 May 2026 16:26:24 +0200 Subject: [PATCH] Route batch tool source endpoints through the LazyToolBox index Replaces ad-hoc iteration over ``app.toolbox`` in the ``/api/tools``, ``/api/tool_panels``, tests-summary, and ``/api/tools/all_requirements`` endpoints with service methods that query the LazyToolBox pre-computed index when available, falling back to the existing toolbox path otherwise. This converts the batch endpoints from O(N tools) to O(1) when the lazy toolbox is in use, without changing behavior for the default eager toolbox. --- lib/galaxy/webapps/galaxy/api/tools.py | 64 +++++------ lib/galaxy/webapps/galaxy/services/tools.py | 120 ++++++++++++++++++++ 2 files changed, 149 insertions(+), 35 deletions(-) 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 [])