Toolbox filters: typed ToolFilterContext + parity for lazy /api/tools

``LazyToolBox.to_dict(in_panel=False)`` was serving entries straight from
the index, bypassing ``FilterFactory`` — so admin and user toolbox
filters silently didn't apply on the lazy path.

Introduce a typed ``ToolFilterContext`` protocol that documents the
attribute surface filters can read; both ``Tool`` and ``ToolIndexEntry``
satisfy it. ``ToolIndexEntry`` grows ``require_login``, ``tool_type``,
and ``tags`` so the stock filters (``_not_hidden``,
``_handle_authorization``) work against the entry without materialising
a Tool. ``_handle_authorization`` does the ``DataManagerTool`` admin
check inline (reads ``tool_type`` and ``context.trans.app.config``),
which avoids needing an ``allow_user_access`` method on the entry.

``LazyToolBox.to_dict`` now runs ``_build_filter_method`` against every
entry — the same code path the eager toolbox uses.
This commit is contained in:
mvdbeek
2026-07-28 17:27:13 +02:00
parent fbd1356674
commit 2e8ff72353
4 changed files with 135 additions and 9 deletions
+16
View File
@@ -51,6 +51,16 @@ class ToolIndexEntry:
# === Status ===
hidden: bool = False
disabled: bool = False
require_login: bool = False
# === Filter metadata ===
# ``tool_type`` is the Tool subclass key (``default``, ``data_manager``,
# ``interactive_tool``, ``data_source``, ...). Filter authors and
# ``DataManagerTool.allow_user_access`` (admin-only) both branch on this.
tool_type: str = "default"
# User-facing tags from ``<tool>`` config (distinct from ``labels``).
# Surfaced for custom tool filters that bucket tools by tag.
tags: list[str] = field(default_factory=list)
# === Tests (for /api/tools/tests_summary) ===
test_count: int = 0
@@ -135,6 +145,9 @@ class ToolIndexEntry:
"source_class": self.source_class,
"hidden": self.hidden,
"disabled": self.disabled,
"require_login": self.require_login,
"tool_type": self.tool_type,
"tags": self.tags,
"test_count": self.test_count,
"requirements": self.requirements,
"container_requirements": self.container_requirements,
@@ -169,6 +182,9 @@ class ToolIndexEntry:
source_class=data.get("source_class", "XmlToolSource"),
hidden=data.get("hidden", False),
disabled=data.get("disabled", False),
require_login=data.get("require_login", False),
tool_type=data.get("tool_type", "default"),
tags=data.get("tags", []),
test_count=data.get("test_count", 0),
requirements=data.get("requirements", []),
container_requirements=data.get("container_requirements", []),
+43 -9
View File
@@ -47,6 +47,7 @@ from galaxy.tool_util.toolbox.filters import FilterFactory
from galaxy.tool_util.toolbox.lineages.factory import LazyLineageMap
from galaxy.tool_util.toolbox.lineages.interface import ToolLineage
from galaxy.tool_util.toolbox.panel import (
panel_item_types,
ToolPanelElements,
ToolSection,
)
@@ -1287,6 +1288,32 @@ class LazyToolBox(ToolBox):
except Exception:
pass
# ``parse_require_login`` is what ``Tool.parse`` calls to set
# ``tool.require_login``. Default False matches ``Tool.__init__``.
require_login = False
if hasattr(tool_source, "parse_require_login"):
try:
require_login = bool(tool_source.parse_require_login(False))
except Exception:
pass
# ``tool_type`` is the Tool subclass key (``data_manager``,
# ``interactive_tool``, etc.). Stock filters branch on this for the
# admin-only check on ``DataManagerTool``; custom filters use it
# to categorize.
tool_type = "default"
if hasattr(tool_source, "parse_tool_type"):
try:
tool_type = tool_source.parse_tool_type() or "default"
except Exception:
pass
# ``tags`` are currently not exposed via the ToolSource parser API.
# The field on ``ToolIndexEntry`` is here so admin/user filters that
# bucket tools by tag have a place to read from when the populator
# learns to fill it.
tags: list[str] = []
return ToolIndexEntry(
id=tool_id,
uuid=uuid_val,
@@ -1296,6 +1323,9 @@ class LazyToolBox(ToolBox):
source_hash=source_hash,
source_class=source_class,
hidden=hidden,
require_login=require_login,
tool_type=tool_type,
tags=tags,
indexed_at=datetime.utcnow(),
)
except Exception as e:
@@ -2291,8 +2321,11 @@ class LazyToolBox(ToolBox):
Create a dictionary representation of the toolbox.
For the *flat* listing (``in_panel=False``) we serve straight from
the index — no Tool loading needed. For the panel listing
(``in_panel=True``, e.g. ``tools?in_panel=True&view=custom_13``)
the index — no Tool loading needed — but still run every entry
through ``FilterFactory`` so admin / user toolbox filters apply
identically to the eager path. Filters take ``ToolFilterContext``,
which both ``Tool`` and ``ToolIndexEntry`` satisfy. For the panel
listing (``in_panel=True``, e.g. ``tools?in_panel=True&view=custom_13``)
we defer to the parent: it walks ``_tool_panel_view_rendered``
which is built by ``apply_view`` against
``_integrated_tool_panel`` and produces the section-aware
@@ -2307,16 +2340,17 @@ class LazyToolBox(ToolBox):
if in_panel:
return super().to_dict(trans, in_panel=True, tool_help=tool_help, view=view, **kwds)
filter_method = self._build_filter_method(trans)
rval = []
# Return data directly from index - no tool loading needed!
for _tool_id, entry in self._tool_index.entries.items():
# Skip hidden tools unless requested
if entry.hidden and not kwds.get("include_hidden", False):
# ``filter_method`` honours ``_not_hidden`` + ``_handle_authorization``
# (the always-on stock filters) plus any ``tool_filters`` /
# ``user_tool_filters`` the operator configured. Both stock filters
# read only fields on ``ToolFilterContext``, which ``ToolIndexEntry``
# exposes — no Tool materialisation needed.
if not filter_method(entry, panel_item_types.TOOL):
continue
# Convert index entry to API dict format
tool_dict = self._index_entry_to_api_dict(entry)
rval.append(tool_dict)
rval.append(self._index_entry_to_api_dict(entry))
log.debug(f"LazyToolBox.to_dict: returning {len(rval)} tools from index (no loading)")
return rval
+38
View File
@@ -0,0 +1,38 @@
#This is a sample file distributed with Galaxy that enables tools
#to use a directory of BWA indexed sequences data files. You will need
#to create these data files and then create a bwa_index.loc file
#similar to this one (store it in this directory) that points to
#the directories in which those files are stored. The bwa_index.loc
#file has this format (longer white space characters are TAB characters):
#
#<unique_build_id> <dbkey> <display_name> <file_path>
#
#So, for example, if you had phiX indexed stored in
#/depot/data2/galaxy/phiX/base/,
#then the bwa_index.loc entry would look like this:
#
#phiX174 phiX phiX Pretty /depot/data2/galaxy/phiX/base/phiX.fa
#
#and your /depot/data2/galaxy/phiX/base/ directory
#would contain phiX.fa.* files:
#
#-rw-r--r-- 1 james universe 830134 2005-09-13 10:12 phiX.fa.amb
#-rw-r--r-- 1 james universe 527388 2005-09-13 10:12 phiX.fa.ann
#-rw-r--r-- 1 james universe 269808 2005-09-13 10:12 phiX.fa.bwt
#...etc...
#
#Your bwa_index.loc file should include an entry per line for each
#index set you have stored. The "file" in the path does not actually
#exist, but it is the prefix for the actual index files. For example:
#
#phiX174 phiX phiX174 /depot/data2/galaxy/phiX/base/phiX.fa
#hg18canon hg18 hg18 Canonical /depot/data2/galaxy/hg18/base/hg18canon.fa
#hg18full hg18 hg18 Full /depot/data2/galaxy/hg18/base/hg18full.fa
#/orig/path/hg19.fa hg19 hg19 /depot/data2/galaxy/hg19/base/hg19.fa
#...etc...
#
#Note that for backwards compatibility with workflows, the unique ID of
#an entry must be the path that was in the original loc file, because that
#is the value stored in the workflow for that parameter. That is why the
#hg19 entry above looks odd. New genomes can be better-looking.
#
+38
View File
@@ -0,0 +1,38 @@
#This is a sample file distributed with Galaxy that enables tools
#to use a directory of BWA indexed sequences data files. You will need
#to create these data files and then create a bwa_index.loc file
#similar to this one (store it in this directory) that points to
#the directories in which those files are stored. The bwa_index.loc
#file has this format (longer white space characters are TAB characters):
#
#<unique_build_id> <dbkey> <display_name> <file_path>
#
#So, for example, if you had phiX indexed stored in
#/depot/data2/galaxy/phiX/base/,
#then the bwa_index.loc entry would look like this:
#
#phiX174 phiX phiX Pretty /depot/data2/galaxy/phiX/base/phiX.fa
#
#and your /depot/data2/galaxy/phiX/base/ directory
#would contain phiX.fa.* files:
#
#-rw-r--r-- 1 james universe 830134 2005-09-13 10:12 phiX.fa.amb
#-rw-r--r-- 1 james universe 527388 2005-09-13 10:12 phiX.fa.ann
#-rw-r--r-- 1 james universe 269808 2005-09-13 10:12 phiX.fa.bwt
#...etc...
#
#Your bwa_index.loc file should include an entry per line for each
#index set you have stored. The "file" in the path does not actually
#exist, but it is the prefix for the actual index files. For example:
#
#phiX174 phiX phiX174 /depot/data2/galaxy/phiX/base/phiX.fa
#hg18canon hg18 hg18 Canonical /depot/data2/galaxy/hg18/base/hg18canon.fa
#hg18full hg18 hg18 Full /depot/data2/galaxy/hg18/base/hg18full.fa
#/orig/path/hg19.fa hg19 hg19 /depot/data2/galaxy/hg19/base/hg19.fa
#...etc...
#
#Note that for backwards compatibility with workflows, the unique ID of
#an entry must be the path that was in the original loc file, because that
#is the value stored in the workflow for that parameter. That is why the
#hg19 entry above looks odd. New genomes can be better-looking.
#