mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-11-02 19:52:14 +08:00
The galaxy-tool-shed-schema package is basically pydantic models for the whole tool shed API. It is used by both the backend code for the new tool shed and the testing code that runs on both the old and new tool shed. The creation of galaxy-tool-util-models package is something I've wanted to do for a while - it should solve some TODOs I've left in the base. I think beyond enabling galaxy-tool-shed-schema package - the other nice thing is that galaxy-schema code can now rely on various tool stuff without requiring dependency on any of the parser or runtime code. This should also fix things for the new record types in #19377. I've introduced a type that can be used tools and is included schema for collection creation. The galaxy-tool-util-models packages can now be dependended on by galaxy-schema and galaxy-tool-util and these two packages do not need to depend on each other - either direction of that dependency would make me uncomfortable. xref https://github.com/galaxyproject/galaxy/actions/runs/13704893929/job/38327692654?pr=19377
195 lines
8.0 KiB
Python
195 lines
8.0 KiB
Python
import os
|
|
import tempfile
|
|
from collections import namedtuple
|
|
from typing import (
|
|
Dict,
|
|
List,
|
|
Optional,
|
|
Tuple,
|
|
)
|
|
|
|
from galaxy import exceptions
|
|
from galaxy.exceptions import (
|
|
InconsistentApplicationState,
|
|
InternalServerError,
|
|
ObjectNotFound,
|
|
RequestParameterInvalidException,
|
|
)
|
|
from galaxy.tool_shed.metadata.metadata_generator import RepositoryMetadataToolDict
|
|
from galaxy.tool_shed.util.basic_util import remove_dir
|
|
from galaxy.tool_shed.util.hg_util import (
|
|
clone_repository,
|
|
get_changectx_for_changeset,
|
|
)
|
|
from galaxy.tool_util.model_factory import parse_tool_custom
|
|
from galaxy.tool_util.parser import (
|
|
get_tool_source,
|
|
ToolSource,
|
|
)
|
|
from galaxy.tools.stock import stock_tool_sources
|
|
from galaxy.util import relpath
|
|
from tool_shed.context import (
|
|
ProvidesRepositoriesContext,
|
|
SessionRequestContext,
|
|
)
|
|
from tool_shed.util.common_util import generate_clone_url_for
|
|
from tool_shed.webapp.model import RepositoryMetadata
|
|
from tool_shed.webapp.search.tool_search import ToolSearch
|
|
from tool_shed_client.schema import ShedParsedTool
|
|
from .repositories import get_repository_revision_metadata_model
|
|
from .trs import trs_tool_id_to_repository_metadata
|
|
|
|
STOCK_TOOL_SOURCES: Optional[Dict[str, Dict[str, ToolSource]]] = None
|
|
|
|
|
|
def search(trans: SessionRequestContext, q: str, page: int = 1, page_size: int = 10) -> dict:
|
|
"""
|
|
Perform the search over TS tools index.
|
|
Note that search works over the Whoosh index which you have
|
|
to pre-create with scripts/tool_shed/build_ts_whoosh_index.sh manually.
|
|
Also TS config option toolshed_search_on has to be True and
|
|
whoosh_index_dir has to be specified.
|
|
"""
|
|
app = trans.app
|
|
conf = app.config
|
|
if not conf.toolshed_search_on:
|
|
raise exceptions.ConfigDoesNotAllowException(
|
|
"Searching the TS through the API is turned off for this instance."
|
|
)
|
|
if not conf.whoosh_index_dir:
|
|
raise exceptions.ConfigDoesNotAllowException(
|
|
"There is no directory for the search index specified. Please contact the administrator."
|
|
)
|
|
search_term = q.strip()
|
|
if len(search_term) < 1:
|
|
raise exceptions.RequestParameterInvalidException("The search term has to be at least one character long.")
|
|
|
|
tool_search = ToolSearch()
|
|
|
|
Boosts = namedtuple(
|
|
"Boosts", ["tool_name_boost", "tool_description_boost", "tool_help_boost", "tool_repo_owner_username_boost"]
|
|
)
|
|
boosts = Boosts(
|
|
float(conf.get("tool_name_boost", 1.2)),
|
|
float(conf.get("tool_description_boost", 0.6)),
|
|
float(conf.get("tool_help_boost", 0.4)),
|
|
float(conf.get("tool_repo_owner_username_boost", 0.3)),
|
|
)
|
|
|
|
results = tool_search.search(trans.app, search_term, page, page_size, boosts)
|
|
results["hostname"] = trans.repositories_hostname
|
|
return results
|
|
|
|
|
|
def get_repository_metadata_tool_dict(
|
|
trans: ProvidesRepositoriesContext, trs_tool_id: str, tool_version: str
|
|
) -> Tuple[RepositoryMetadata, RepositoryMetadataToolDict]:
|
|
if trs_tool_id.count("~") < 2:
|
|
RequestParameterInvalidException(f"Invalid TRS tool id ({trs_tool_id})")
|
|
|
|
name, owner, tool_id = trs_tool_id.split("~", 3)
|
|
repository, metadata_by_version = trs_tool_id_to_repository_metadata(trans, trs_tool_id)
|
|
if tool_version not in metadata_by_version:
|
|
raise ObjectNotFound()
|
|
tool_version_repository_metadata: RepositoryMetadata = metadata_by_version[tool_version]
|
|
raw_metadata = tool_version_repository_metadata.metadata
|
|
tool_dicts: List[RepositoryMetadataToolDict] = raw_metadata.get("tools", [])
|
|
for tool_dict in tool_dicts:
|
|
if tool_dict["id"] != tool_id or tool_dict["version"] != tool_version:
|
|
continue
|
|
return tool_version_repository_metadata, tool_dict
|
|
raise ObjectNotFound()
|
|
|
|
|
|
def parsed_tool_model_cached_for(
|
|
trans: ProvidesRepositoriesContext, trs_tool_id: str, tool_version: str, repository_clone_url: Optional[str] = None
|
|
) -> ShedParsedTool:
|
|
model_cache = trans.app.model_cache
|
|
parsed_tool = model_cache.get_cache_entry_for(ShedParsedTool, trs_tool_id, tool_version)
|
|
if parsed_tool is not None:
|
|
return parsed_tool
|
|
parsed_tool = parsed_tool_model_for(trans, trs_tool_id, tool_version, repository_clone_url=repository_clone_url)
|
|
model_cache.insert_cache_entry_for(parsed_tool, trs_tool_id, tool_version)
|
|
return parsed_tool
|
|
|
|
|
|
def parsed_tool_model_for(
|
|
trans: ProvidesRepositoriesContext, trs_tool_id: str, tool_version: str, repository_clone_url: Optional[str] = None
|
|
) -> ShedParsedTool:
|
|
tool_source, repository_metadata = tool_source_for(
|
|
trans, trs_tool_id, tool_version, repository_clone_url=repository_clone_url
|
|
)
|
|
parsed_tool = parse_tool_custom(tool_source, ShedParsedTool)
|
|
if repository_metadata:
|
|
revision_model = get_repository_revision_metadata_model(
|
|
trans.app, repository_metadata.repository, repository_metadata, recursive=False
|
|
)
|
|
parsed_tool.repository_revision = revision_model
|
|
return parsed_tool
|
|
|
|
|
|
def tool_source_for(
|
|
trans: ProvidesRepositoriesContext, trs_tool_id: str, tool_version: str, repository_clone_url: Optional[str] = None
|
|
) -> Tuple[ToolSource, Optional[RepositoryMetadata]]:
|
|
if "~" in trs_tool_id:
|
|
return _shed_tool_source_for(trans, trs_tool_id, tool_version, repository_clone_url)
|
|
else:
|
|
tool_source = _stock_tool_source_for(trs_tool_id, tool_version)
|
|
if tool_source is None:
|
|
raise ObjectNotFound()
|
|
return tool_source, None
|
|
|
|
|
|
def _shed_tool_source_for(
|
|
trans: ProvidesRepositoriesContext, trs_tool_id: str, tool_version: str, repository_clone_url: Optional[str] = None
|
|
) -> Tuple[ToolSource, RepositoryMetadata]:
|
|
rval = get_repository_metadata_tool_dict(trans, trs_tool_id, tool_version)
|
|
repository_metadata, tool_version_metadata = rval
|
|
tool_config = tool_version_metadata["tool_config"]
|
|
|
|
repo = repository_metadata.repository.hg_repo
|
|
ctx = get_changectx_for_changeset(repo, repository_metadata.changeset_revision)
|
|
work_dir = tempfile.mkdtemp(prefix="tmp-toolshed-tool_source")
|
|
if repository_clone_url is None:
|
|
repository_clone_url = generate_clone_url_for(trans, repository_metadata.repository)
|
|
try:
|
|
cloned_ok, error_message = clone_repository(repository_clone_url, work_dir, str(ctx.rev()))
|
|
if error_message:
|
|
raise InternalServerError("Failed to materialize target repository revision")
|
|
repo_files_dir = repository_metadata.repository.hg_repository_path(trans.app.config.file_path)
|
|
if not repo_files_dir:
|
|
raise InconsistentApplicationState(
|
|
f"Failed to resolve repository path from hgweb_config_manager for [{trs_tool_id}], inconsistent repository state or application configuration"
|
|
)
|
|
repo_rel_tool_path = relpath(tool_config, repo_files_dir)
|
|
path_to_tool = os.path.join(work_dir, repo_rel_tool_path)
|
|
if not os.path.exists(path_to_tool):
|
|
raise InconsistentApplicationState(
|
|
f"Target tool expected at [{path_to_tool}] and not found, inconsistent repository state or application configuration"
|
|
)
|
|
tool_source = get_tool_source(path_to_tool)
|
|
return tool_source, repository_metadata
|
|
finally:
|
|
remove_dir(work_dir)
|
|
|
|
|
|
def _stock_tool_source_for(tool_id: str, tool_version: str) -> Optional[ToolSource]:
|
|
_init_stock_tool_sources()
|
|
assert STOCK_TOOL_SOURCES
|
|
tool_version_sources = STOCK_TOOL_SOURCES.get(tool_id)
|
|
if tool_version_sources is None:
|
|
return None
|
|
return tool_version_sources.get(tool_version)
|
|
|
|
|
|
def _init_stock_tool_sources() -> None:
|
|
global STOCK_TOOL_SOURCES
|
|
if STOCK_TOOL_SOURCES is None:
|
|
STOCK_TOOL_SOURCES = {}
|
|
for tool_source in stock_tool_sources():
|
|
tool_id = tool_source.parse_id()
|
|
tool_version = tool_source.parse_version()
|
|
if tool_id not in STOCK_TOOL_SOURCES:
|
|
STOCK_TOOL_SOURCES[tool_id] = {}
|
|
STOCK_TOOL_SOURCES[tool_id][tool_version] = tool_source
|