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.
This commit is contained in:
mvdbeek
2026-07-28 17:26:56 +02:00
parent 53aa0cf95f
commit b639bed95a
2 changed files with 149 additions and 35 deletions
+29 -35
View File
@@ -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=<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
+120
View File
@@ -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 [])