mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-08-31 01:02:04 +08:00
ToolShed API 2.0
This commit is contained in:
@@ -22,6 +22,7 @@ jobs:
|
||||
matrix:
|
||||
python-version: ['3.7']
|
||||
test-install-client: ['standalone', 'galaxy_api']
|
||||
shed-api: ['v1', 'v2']
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:13
|
||||
@@ -57,6 +58,7 @@ jobs:
|
||||
run: './run_tests.sh -toolshed'
|
||||
env:
|
||||
TOOL_SHED_TEST_INSTALL_CLIENT: ${{ matrix.test-install-client }}
|
||||
TOOL_SHED_API_VERSION: ${{ matrix.shed-api }}
|
||||
working-directory: 'galaxy root'
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: failure()
|
||||
|
||||
@@ -14,3 +14,6 @@ _schema.yaml:
|
||||
#/paths/~1api~1histories~1{history_id}~1contents~1{history_content_id}~1metadata_file
|
||||
- '#/paths/~1api~1histories~1{history_id}~1contents~1{id}~1validate'
|
||||
- '#/paths/~1api~1histories~1{history_id}~1contents~1{type}s~1{id}'
|
||||
_shed_schema.yaml:
|
||||
no-empty-servers:
|
||||
- '#/openapi'
|
||||
|
||||
@@ -182,9 +182,11 @@ endif
|
||||
|
||||
build-api-schema:
|
||||
$(IN_VENV) python scripts/dump_openapi_schema.py _schema.yaml
|
||||
$(IN_VENV) python scripts/dump_openapi_schema.py --app shed _shed_schema.yaml
|
||||
|
||||
remove-api-schema:
|
||||
rm _schema.yaml
|
||||
rm _shed_schema.yaml
|
||||
|
||||
update-client-api-schema: client-node-deps build-api-schema
|
||||
$(IN_VENV) cd client && node openapi_to_schema.mjs ../_schema.yaml > src/schema/schema.ts && npx prettier --write src/schema/schema.ts
|
||||
@@ -192,7 +194,9 @@ update-client-api-schema: client-node-deps build-api-schema
|
||||
|
||||
lint-api-schema: build-api-schema
|
||||
$(IN_VENV) npx --yes @redocly/cli lint _schema.yaml
|
||||
$(IN_VENV) npx --yes @redocly/cli lint _shed_schema.yaml
|
||||
$(IN_VENV) codespell -I .ci/ignore-spelling.txt _schema.yaml
|
||||
$(IN_VENV) codespell -I .ci/ignore-spelling.txt _shed_schema.yaml
|
||||
$(MAKE) remove-api-schema
|
||||
|
||||
update-navigation-schema: client-node-deps
|
||||
|
||||
@@ -5,6 +5,8 @@ from typing import (
|
||||
Optional,
|
||||
)
|
||||
|
||||
from typing_extensions import Literal
|
||||
|
||||
from galaxy.util import parse_xml_string
|
||||
from galaxy.util.path import StrPath
|
||||
from galaxy.util.tool_shed import common_util
|
||||
@@ -20,6 +22,8 @@ DEFAULT_TOOL_SHEDS_CONF_XML = f"""<?xml version="1.0"?>
|
||||
</tool_sheds>
|
||||
"""
|
||||
|
||||
API_VERSION = Literal["v1", "v2"]
|
||||
|
||||
|
||||
class AUTH_TUPLE(NamedTuple):
|
||||
username: str
|
||||
@@ -28,11 +32,13 @@ class AUTH_TUPLE(NamedTuple):
|
||||
|
||||
class Registry:
|
||||
tool_sheds: Dict[str, str]
|
||||
tool_shed_api_versions: Dict[str, API_VERSION]
|
||||
tool_sheds_auth: Dict[str, Optional[AUTH_TUPLE]]
|
||||
|
||||
def __init__(self, config: Optional[StrPath] = None):
|
||||
self.tool_sheds = {}
|
||||
self.tool_sheds_auth = {}
|
||||
self.tool_shed_api_versions = {}
|
||||
if config:
|
||||
# Parse tool_sheds_conf.xml
|
||||
tree, error_message = parse_xml(config)
|
||||
@@ -48,10 +54,17 @@ class Registry:
|
||||
try:
|
||||
name = elem.get("name", None)
|
||||
url = elem.get("url", None)
|
||||
version_raw = elem.get("version", "1")
|
||||
version: API_VERSION
|
||||
if version_raw == "1":
|
||||
version = "v1"
|
||||
else:
|
||||
version = "v2"
|
||||
username = elem.get("user", None)
|
||||
password = elem.get("pass", None)
|
||||
if name and url:
|
||||
self.tool_sheds[name] = url
|
||||
self.tool_shed_api_versions[name] = version
|
||||
self.tool_sheds_auth[name] = None
|
||||
log.debug(f"Loaded reference to tool shed: {name}")
|
||||
if name and url and username and password:
|
||||
@@ -75,6 +88,13 @@ class Registry:
|
||||
log.debug(f"Invalid url '{str(url)}' received by tool shed registry's url_auth method.")
|
||||
return None
|
||||
|
||||
def is_legacy(self, url: str) -> bool:
|
||||
shed_name = self._shed_name_for_url(url)
|
||||
if shed_name is None:
|
||||
return True
|
||||
else:
|
||||
return self.tool_shed_api_versions[shed_name] == "v1"
|
||||
|
||||
def _shed_name_for_url(self, url: str) -> Optional[str]:
|
||||
url_sans_protocol = common_util.remove_protocol_from_tool_shed_url(url)
|
||||
for shed_name, shed_url in self.tool_sheds.items():
|
||||
|
||||
@@ -109,7 +109,7 @@ class GalaxyTypeDepends(Depends):
|
||||
self.galaxy_type_depends = dep_type
|
||||
|
||||
|
||||
def depends(dep_type: Type[T]) -> T:
|
||||
def depends(dep_type: Type[T], get_app=get_app) -> T:
|
||||
def _do_resolve(request: Request):
|
||||
return get_app().resolve(dep_type)
|
||||
|
||||
@@ -178,7 +178,10 @@ class UrlBuilder:
|
||||
query_params = path_params.pop("query_params", None)
|
||||
try:
|
||||
if qualified:
|
||||
url = str(self.request.url_for(name, **path_params))
|
||||
if name == "/":
|
||||
url = str(self.request.base_url)
|
||||
else:
|
||||
url = str(self.request.url_for(name, **path_params))
|
||||
else:
|
||||
url = self.request.app.url_path_for(name, **path_params)
|
||||
if query_params:
|
||||
@@ -301,9 +304,11 @@ class RestVerb(str, Enum):
|
||||
options = "OPTIONS"
|
||||
|
||||
|
||||
class Router(InferringRouter):
|
||||
class FrameworkRouter(InferringRouter):
|
||||
"""A FastAPI Inferring Router tailored to Galaxy."""
|
||||
|
||||
admin_user_dependency: Any
|
||||
|
||||
def wrap_with_alias(self, verb: RestVerb, *args, alias: Optional[str] = None, **kwd):
|
||||
"""
|
||||
Wraps FastAPI methods with additional alias keyword and require_admin handling.
|
||||
@@ -382,9 +387,9 @@ class Router(InferringRouter):
|
||||
require_admin = kwd.pop("require_admin", False)
|
||||
if require_admin:
|
||||
if "dependencies" in kwd:
|
||||
kwd["dependencies"].append(AdminUserRequired)
|
||||
kwd["dependencies"].append(self.admin_user_dependency)
|
||||
else:
|
||||
kwd["dependencies"] = [AdminUserRequired]
|
||||
kwd["dependencies"] = [self.admin_user_dependency]
|
||||
|
||||
return kwd
|
||||
|
||||
@@ -398,6 +403,10 @@ class Router(InferringRouter):
|
||||
return cbv(self)
|
||||
|
||||
|
||||
class Router(FrameworkRouter):
|
||||
admin_user_dependency = AdminUserRequired
|
||||
|
||||
|
||||
class APIContentTypeRoute(APIRoute):
|
||||
"""
|
||||
Determines endpoint to match using content-type.
|
||||
|
||||
@@ -2,6 +2,7 @@ import abc
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy.orm import scoped_session
|
||||
from typing_extensions import Protocol
|
||||
|
||||
from galaxy.security.idencoding import IdEncodingHelper
|
||||
from galaxy.work.context import (
|
||||
@@ -16,7 +17,7 @@ from tool_shed.webapp.model import (
|
||||
from tool_shed.webapp.model.mapping import ToolShedModelMapping
|
||||
|
||||
|
||||
class ProvidesAppContext:
|
||||
class ProvidesAppContext(Protocol):
|
||||
"""For transaction-like objects to provide the shed convenience layer for
|
||||
database and event handling.
|
||||
|
||||
@@ -45,7 +46,7 @@ class ProvidesAppContext:
|
||||
return self.app.model
|
||||
|
||||
|
||||
class ProvidesUserContext(ProvidesAppContext):
|
||||
class ProvidesUserContext(ProvidesAppContext, Protocol):
|
||||
"""For transaction-like objects to provide Galaxy convenience layer for
|
||||
reasoning about users.
|
||||
|
||||
@@ -72,7 +73,13 @@ class ProvidesUserContext(ProvidesAppContext):
|
||||
return not self.anonymous and user is not None and user.bootstrap_admin_user
|
||||
|
||||
|
||||
class SessionRequestContext(ProvidesUserContext):
|
||||
class ProvidesRepositoriesContext(ProvidesUserContext, Protocol):
|
||||
@abc.abstractproperty
|
||||
def repositories_hostname(self) -> str:
|
||||
"""Provide access to hostname used by target mercurial server."""
|
||||
|
||||
|
||||
class SessionRequestContext(ProvidesRepositoriesContext, Protocol):
|
||||
@abc.abstractmethod
|
||||
def get_galaxy_session(self) -> Optional[GalaxySession]:
|
||||
...
|
||||
@@ -84,3 +91,60 @@ class SessionRequestContext(ProvidesUserContext):
|
||||
@abc.abstractproperty
|
||||
def response(self) -> GalaxyAbstractResponse:
|
||||
...
|
||||
|
||||
@abc.abstractmethod
|
||||
def url_builder(self):
|
||||
...
|
||||
|
||||
|
||||
class SessionRequestContextImpl(SessionRequestContext):
|
||||
_app: ToolShedApp
|
||||
_user: Optional[User]
|
||||
_galaxy_session: Optional[GalaxySession]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
app: ToolShedApp,
|
||||
request: GalaxyAbstractRequest,
|
||||
response: GalaxyAbstractResponse,
|
||||
user: Optional[User] = None,
|
||||
galaxy_session: Optional[GalaxySession] = None,
|
||||
url_builder=None,
|
||||
):
|
||||
self._app = app
|
||||
self._user = user
|
||||
self._galaxy_session = galaxy_session
|
||||
self._url_builder = url_builder
|
||||
self.__request = request
|
||||
self.__response = response
|
||||
|
||||
@property
|
||||
def app(self) -> ToolShedApp:
|
||||
return self._app
|
||||
|
||||
@property
|
||||
def url_builder(self):
|
||||
return self._url_builder
|
||||
|
||||
@property
|
||||
def user(self) -> Optional[User]:
|
||||
return self._user
|
||||
|
||||
def get_galaxy_session(self) -> Optional[GalaxySession]:
|
||||
return self._galaxy_session
|
||||
|
||||
@property
|
||||
def repositories_hostname(self) -> str:
|
||||
return str(self.request.base).rstrip("/")
|
||||
|
||||
@property
|
||||
def host(self):
|
||||
return self.__request.host
|
||||
|
||||
@property
|
||||
def request(self) -> GalaxyAbstractRequest:
|
||||
return self.__request
|
||||
|
||||
@property
|
||||
def response(self) -> GalaxyAbstractResponse:
|
||||
return self.__response
|
||||
|
||||
@@ -5,13 +5,13 @@ from typing import (
|
||||
List,
|
||||
Optional,
|
||||
Tuple,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
from galaxy.util import (
|
||||
asbool,
|
||||
etree,
|
||||
)
|
||||
from galaxy.web import url_for
|
||||
from tool_shed.dependencies.tool import tag_attribute_handler
|
||||
from tool_shed.repository_types.util import (
|
||||
REPOSITORY_DEPENDENCY_DEFINITION_FILENAME,
|
||||
@@ -24,12 +24,21 @@ from tool_shed.util import (
|
||||
xml_util,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tool_shed.context import ProvidesRepositoriesContext
|
||||
from tool_shed.structured_app import ToolShedApp
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RepositoryDependencyAttributeHandler:
|
||||
def __init__(self, app, unpopulate):
|
||||
self.app = app
|
||||
trans: "ProvidesRepositoriesContext"
|
||||
app: "ToolShedApp"
|
||||
|
||||
def __init__(self, trans: "ProvidesRepositoriesContext", unpopulate):
|
||||
self.trans = trans
|
||||
self.app = trans.app
|
||||
self.file_name = REPOSITORY_DEPENDENCY_DEFINITION_FILENAME
|
||||
self.unpopulate = unpopulate
|
||||
|
||||
@@ -111,7 +120,7 @@ class RepositoryDependencyAttributeHandler:
|
||||
# From here on we're populating the toolshed and changeset_revision attributes if necessary.
|
||||
if not toolshed:
|
||||
# Default the setting to the current tool shed.
|
||||
toolshed = str(url_for("/", qualified=True)).rstrip("/")
|
||||
toolshed = str(self.trans.url_builder("/", qualified=True)).rstrip("/")
|
||||
elem.attrib["toolshed"] = toolshed
|
||||
altered = True
|
||||
if not changeset_revision:
|
||||
@@ -188,8 +197,12 @@ class RepositoryDependencyAttributeHandler:
|
||||
|
||||
|
||||
class ToolDependencyAttributeHandler:
|
||||
def __init__(self, app, unpopulate):
|
||||
self.app = app
|
||||
trans: "ProvidesRepositoriesContext"
|
||||
app: "ToolShedApp"
|
||||
|
||||
def __init__(self, trans: "ProvidesRepositoriesContext", unpopulate):
|
||||
self.trans = trans
|
||||
self.app = trans.app
|
||||
self.file_name = TOOL_DEPENDENCY_DEFINITION_FILENAME
|
||||
self.unpopulate = unpopulate
|
||||
|
||||
@@ -198,7 +211,7 @@ class ToolDependencyAttributeHandler:
|
||||
Populate or unpopulate the tooshed and changeset_revision attributes of each <repository>
|
||||
tag defined within a tool_dependencies.xml file.
|
||||
"""
|
||||
rdah = RepositoryDependencyAttributeHandler(self.app, self.unpopulate)
|
||||
rdah = RepositoryDependencyAttributeHandler(self.trans, self.unpopulate)
|
||||
tah = tag_attribute_handler.TagAttributeHandler(self.app, rdah, self.unpopulate)
|
||||
altered = False
|
||||
error_message = ""
|
||||
|
||||
@@ -16,7 +16,7 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RelationBuilder:
|
||||
def __init__(self, app, repository, repository_metadata, tool_shed_url):
|
||||
def __init__(self, app, repository, repository_metadata, tool_shed_url, trans=None):
|
||||
self.all_repository_dependencies = {}
|
||||
self.app = app
|
||||
self.circular_repository_dependencies = []
|
||||
@@ -25,6 +25,7 @@ class RelationBuilder:
|
||||
self.handled_key_rd_dicts = []
|
||||
self.key_rd_dicts_to_be_processed = []
|
||||
self.tool_shed_url = tool_shed_url
|
||||
self.trans = trans
|
||||
|
||||
def can_add_to_key_rd_dicts(self, key_rd_dict, key_rd_dicts):
|
||||
"""Handle the case where an update to the changeset revision was done."""
|
||||
@@ -212,7 +213,8 @@ class RelationBuilder:
|
||||
rd_prior_installation_required,
|
||||
rd_only_if_compiling_contained_td,
|
||||
) = common_util.parse_repository_dependency_tuple(repository_dependency)
|
||||
if suc.tool_shed_is_this_tool_shed(rd_toolshed):
|
||||
tool_shed_is_this_tool_shed = suc.tool_shed_is_this_tool_shed(rd_toolshed, trans=self.trans)
|
||||
if tool_shed_is_this_tool_shed:
|
||||
repository = tool_shed.util.repository_util.get_repository_by_name_and_owner(
|
||||
self.app, rd_name, rd_owner
|
||||
)
|
||||
@@ -318,7 +320,7 @@ class RelationBuilder:
|
||||
prior_installation_required,
|
||||
only_if_compiling_contained_td,
|
||||
) = common_util.parse_repository_dependency_tuple(repository_dependency)
|
||||
if suc.tool_shed_is_this_tool_shed(toolshed):
|
||||
if suc.tool_shed_is_this_tool_shed(toolshed, trans=self.trans):
|
||||
required_repository = tool_shed.util.repository_util.get_repository_by_name_and_owner(self.app, name, owner)
|
||||
self.repository = required_repository
|
||||
repository_id = self.app.security.encode_id(required_repository.id)
|
||||
|
||||
@@ -14,7 +14,10 @@ from galaxy.model.base import transaction
|
||||
from tool_shed.context import ProvidesUserContext
|
||||
from tool_shed.structured_app import ToolShedApp
|
||||
from tool_shed.webapp.model import Category
|
||||
from tool_shed_client.schema import CreateCategoryRequest
|
||||
from tool_shed_client.schema import (
|
||||
Category as CategoryResponse,
|
||||
CreateCategoryRequest,
|
||||
)
|
||||
|
||||
|
||||
class CategoryManager:
|
||||
@@ -50,16 +53,29 @@ class CategoryManager:
|
||||
def index(self, trans: ProvidesUserContext, deleted: bool) -> List[Dict[str, Any]]:
|
||||
category_dicts: List[Dict[str, Any]] = []
|
||||
for category in self.index_db(trans, deleted):
|
||||
category_dict = category.to_dict(view="collection", value_mapper=get_value_mapper(trans.app))
|
||||
category_dict["url"] = web.url_for(
|
||||
controller="categories", action="show", id=trans.security.encode_id(category.id)
|
||||
)
|
||||
category_dict[
|
||||
"repositories"
|
||||
] = self.app.repository_registry.viewable_repositories_and_suites_by_category.get(category.name, 0)
|
||||
category_dict = self.to_dict(category)
|
||||
category_dicts.append(category_dict)
|
||||
return category_dicts
|
||||
|
||||
def to_dict(self, category: Category) -> Dict[str, Any]:
|
||||
category_dict = category.to_dict(view="collection", value_mapper=get_value_mapper(self.app))
|
||||
category_dict["repositories"] = self.app.repository_registry.viewable_repositories_and_suites_by_category.get(
|
||||
category.name, 0
|
||||
)
|
||||
category_dict["url"] = web.url_for(
|
||||
controller="categories", action="show", id=self.app.security.encode_id(category.id)
|
||||
)
|
||||
return category_dict
|
||||
|
||||
def to_model(self, category: Category) -> CategoryResponse:
|
||||
as_dict = self.to_dict(category)
|
||||
return CategoryResponse(
|
||||
id=as_dict["id"],
|
||||
name=as_dict["name"],
|
||||
description=as_dict["description"],
|
||||
repositories=as_dict["repositories"],
|
||||
)
|
||||
|
||||
|
||||
def get_value_mapper(app: ToolShedApp) -> Dict[str, Callable]:
|
||||
value_mapper = {"id": app.security.encode_id}
|
||||
|
||||
@@ -24,6 +24,7 @@ from sqlalchemy import (
|
||||
from galaxy import web
|
||||
from galaxy.exceptions import (
|
||||
ConfigDoesNotAllowException,
|
||||
InsufficientPermissionsException,
|
||||
InternalServerError,
|
||||
MalformedContents,
|
||||
ObjectNotFound,
|
||||
@@ -31,7 +32,10 @@ from galaxy.exceptions import (
|
||||
)
|
||||
from galaxy.tool_shed.util import dependency_display
|
||||
from galaxy.util import listify
|
||||
from tool_shed.context import ProvidesUserContext
|
||||
from tool_shed.context import (
|
||||
ProvidesRepositoriesContext,
|
||||
ProvidesUserContext,
|
||||
)
|
||||
from tool_shed.metadata import repository_metadata_manager
|
||||
from tool_shed.repository_types import util as rt_util
|
||||
from tool_shed.structured_app import ToolShedApp
|
||||
@@ -57,17 +61,18 @@ from tool_shed.util.repository_util import (
|
||||
validate_repository_name,
|
||||
)
|
||||
from tool_shed.util.shed_util_common import (
|
||||
get_category,
|
||||
count_repositories_in_category,
|
||||
get_category,
|
||||
)
|
||||
from tool_shed.util.tool_util import generate_message_for_invalid_tools
|
||||
from tool_shed.webapp.model import (
|
||||
Repository,
|
||||
User,
|
||||
RepositoryMetadata,
|
||||
)
|
||||
from tool_shed.webapp.search.repo_search import RepoSearch
|
||||
from tool_shed_client.schema import (
|
||||
CreateRepositoryRequest,
|
||||
DetailedRepository,
|
||||
ExtraRepoInfo,
|
||||
LegacyInstallInfoTuple,
|
||||
Repository as SchemaRepository,
|
||||
@@ -296,7 +301,27 @@ def can_manage_repo(trans: ProvidesUserContext, repository: Repository) -> bool:
|
||||
return trans.user_is_admin or security_agent.user_can_administer_repository(trans.user, repository)
|
||||
|
||||
|
||||
def get_install_info(app: ToolShedApp, name, owner, changeset_revision) -> LegacyInstallInfoTuple:
|
||||
def can_update_repo(trans: ProvidesUserContext, repository: Repository) -> bool:
|
||||
app = trans.app
|
||||
security_agent = app.security_agent
|
||||
return can_manage_repo(trans, repository) or security_agent.can_push(app, trans.user, repository)
|
||||
|
||||
|
||||
def get_repository_metadata_for_management(
|
||||
trans: ProvidesUserContext, encoded_repository_id: str, changeset_revision: str
|
||||
) -> RepositoryMetadata:
|
||||
repository = get_repository_in_tool_shed(trans.app, encoded_repository_id)
|
||||
if not can_manage_repo(trans, repository):
|
||||
raise InsufficientPermissionsException("Cannot manage target repository")
|
||||
revisions = [r for r in repository.metadata_revisions if r.changeset_revision == changeset_revision]
|
||||
if len(revisions) != 1:
|
||||
raise ObjectNotFound()
|
||||
repository_metadata = revisions[0]
|
||||
return repository_metadata
|
||||
|
||||
|
||||
def get_install_info(trans: ProvidesRepositoriesContext, name, owner, changeset_revision) -> LegacyInstallInfoTuple:
|
||||
app = trans.app
|
||||
value_mapper = get_value_mapper(app)
|
||||
# Example URL:
|
||||
# http://<xyz>/api/repositories/get_repository_revision_install_info?name=<n>&owner=<o>&changeset_revision=<cr>
|
||||
@@ -343,7 +368,7 @@ def get_install_info(app: ToolShedApp, name, owner, changeset_revision) -> Legac
|
||||
includes_tools_for_display_in_tool_panel,
|
||||
has_repository_dependencies,
|
||||
has_repository_dependencies_only_if_compiling_contained_td,
|
||||
) = get_repo_info_dict(app, None, encoded_repository_id, changeset_revision)
|
||||
) = get_repo_info_dict(trans, encoded_repository_id, changeset_revision)
|
||||
return repository_dict, repository_metadata_dict, repo_info_dict
|
||||
else:
|
||||
log.debug(
|
||||
@@ -429,8 +454,7 @@ def reset_metadata_on_repository(trans: ProvidesUserContext, repository_id) -> R
|
||||
results = dict(start_time=start_time, repository_status=[])
|
||||
try:
|
||||
rmm = repository_metadata_manager.RepositoryMetadataManager(
|
||||
app=app,
|
||||
user=trans.user,
|
||||
trans,
|
||||
repository=repository,
|
||||
resetting_all_metadata_on_repository=True,
|
||||
updating_installed_repository=False,
|
||||
@@ -525,20 +549,25 @@ def to_model(app, repository: Repository) -> SchemaRepository:
|
||||
return SchemaRepository(**to_element_dict(app, repository))
|
||||
|
||||
|
||||
def to_detailed_model(app, repository: Repository) -> DetailedRepository:
|
||||
return DetailedRepository(**to_element_dict(app, repository))
|
||||
|
||||
|
||||
def upload_tar_and_set_metadata(
|
||||
app: ToolShedApp,
|
||||
trans: ProvidesRepositoriesContext,
|
||||
host: str,
|
||||
user: User,
|
||||
repository: Repository,
|
||||
uploaded_file,
|
||||
commit_message: str,
|
||||
dry_run: bool = False,
|
||||
):
|
||||
app = trans.app
|
||||
user = trans.user
|
||||
assert user
|
||||
repo_dir = repository.repo_path(app)
|
||||
tip = repository.tip()
|
||||
(ok, message, _, content_alert_str, _, _,) = upload_tar(
|
||||
app,
|
||||
host,
|
||||
trans,
|
||||
user.username,
|
||||
repository,
|
||||
uploaded_file,
|
||||
@@ -551,7 +580,7 @@ def upload_tar_and_set_metadata(
|
||||
if tip == repository.tip():
|
||||
raise MalformedContents("No changes to repository.")
|
||||
else:
|
||||
rmm = repository_metadata_manager.RepositoryMetadataManager(app=app, user=user, repository=repository)
|
||||
rmm = repository_metadata_manager.RepositoryMetadataManager(trans, repository=repository)
|
||||
_, error_message = rmm.set_repository_metadata_due_to_new_tip(host, content_alert_str=content_alert_str)
|
||||
if error_message:
|
||||
raise InternalServerError(error_message)
|
||||
|
||||
@@ -40,5 +40,5 @@ def search(trans: SessionRequestContext, q: str, page: int = 1, page_size: int =
|
||||
)
|
||||
|
||||
results = tool_search.search(trans.app, search_term, page, page_size, boosts)
|
||||
results["hostname"] = trans.url_builder("/", qualified=True)
|
||||
results["hostname"] = trans.repositories_hostname
|
||||
return results
|
||||
|
||||
@@ -21,8 +21,8 @@ from galaxy.tool_shed.metadata.metadata_generator import (
|
||||
InvalidFileT,
|
||||
)
|
||||
from galaxy.util import inflector
|
||||
from galaxy.web import url_for
|
||||
from galaxy.web.form_builder import SelectField
|
||||
from tool_shed.context import ProvidesRepositoriesContext
|
||||
from tool_shed.repository_types import util as rt_util
|
||||
from tool_shed.repository_types.metadata import TipOnly
|
||||
from tool_shed.structured_app import ToolShedApp
|
||||
@@ -54,7 +54,7 @@ class ToolShedMetadataGenerator(BaseMetadataGenerator):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
app: ToolShedApp,
|
||||
trans: ProvidesRepositoriesContext,
|
||||
repository: Optional[Repository] = None,
|
||||
changeset_revision: Optional[str] = None,
|
||||
repository_clone_url: Optional[str] = None,
|
||||
@@ -67,7 +67,8 @@ class ToolShedMetadataGenerator(BaseMetadataGenerator):
|
||||
metadata_dict=None,
|
||||
user=None,
|
||||
):
|
||||
self.app = app
|
||||
self.trans = trans
|
||||
self.app = trans.app
|
||||
self.user = user
|
||||
self.repository = repository
|
||||
if changeset_revision is None and self.repository is not None:
|
||||
@@ -75,9 +76,7 @@ class ToolShedMetadataGenerator(BaseMetadataGenerator):
|
||||
else:
|
||||
self.changeset_revision = changeset_revision
|
||||
if repository_clone_url is None and self.repository is not None:
|
||||
self.repository_clone_url = common_util.generate_clone_url_for_repository_in_tool_shed(
|
||||
self.user, self.repository
|
||||
)
|
||||
self.repository_clone_url = common_util.generate_clone_url_for(self.trans, self.repository)
|
||||
else:
|
||||
self.repository_clone_url = repository_clone_url
|
||||
if shed_config_dict is None:
|
||||
@@ -98,7 +97,7 @@ class ToolShedMetadataGenerator(BaseMetadataGenerator):
|
||||
self.updating_installed_repository = updating_installed_repository
|
||||
self.persist = persist
|
||||
self.invalid_file_tups = []
|
||||
self.sa_session = app.model.session
|
||||
self.sa_session = trans.app.model.session
|
||||
|
||||
def initial_metadata_dict(self) -> Dict[str, Any]:
|
||||
return {}
|
||||
@@ -141,11 +140,15 @@ class ToolShedMetadataGenerator(BaseMetadataGenerator):
|
||||
]
|
||||
if not toolshed:
|
||||
# Default to the current tool shed.
|
||||
toolshed = str(url_for("/", qualified=True)).rstrip("/")
|
||||
toolshed = self.trans.repositories_hostname
|
||||
log.warning(f"\n\n\n\n\n\nin not toolshed with {toolshed}\n\n\n\n")
|
||||
# toolshed = str(url_for("/", qualified=True)).rstrip("/")
|
||||
repository_dependency_tup[0] = toolshed
|
||||
else:
|
||||
log.warning(f"moooocww.....{toolshed}\n\n\n\n\n")
|
||||
toolshed = common_util.remove_protocol_from_tool_shed_url(toolshed)
|
||||
|
||||
if suc.tool_shed_is_this_tool_shed(toolshed):
|
||||
if suc.tool_shed_is_this_tool_shed(toolshed, trans=self.trans):
|
||||
try:
|
||||
user = (
|
||||
self.sa_session.query(self.app.model.User)
|
||||
@@ -225,8 +228,7 @@ class ToolShedMetadataGenerator(BaseMetadataGenerator):
|
||||
class RepositoryMetadataManager(ToolShedMetadataGenerator):
|
||||
def __init__(
|
||||
self,
|
||||
app: ToolShedApp,
|
||||
user,
|
||||
trans: ProvidesRepositoriesContext,
|
||||
repository=None,
|
||||
changeset_revision=None,
|
||||
repository_clone_url=None,
|
||||
@@ -239,7 +241,7 @@ class RepositoryMetadataManager(ToolShedMetadataGenerator):
|
||||
metadata_dict=None,
|
||||
):
|
||||
super().__init__(
|
||||
app,
|
||||
trans,
|
||||
repository,
|
||||
changeset_revision,
|
||||
repository_clone_url,
|
||||
@@ -250,8 +252,10 @@ class RepositoryMetadataManager(ToolShedMetadataGenerator):
|
||||
updating_installed_repository,
|
||||
persist,
|
||||
metadata_dict=metadata_dict,
|
||||
user=user,
|
||||
user=trans.user,
|
||||
)
|
||||
app = trans.app
|
||||
user = trans.user
|
||||
self.sa_session = app.model.context
|
||||
self.app = app
|
||||
self.user = user
|
||||
@@ -997,9 +1001,7 @@ class RepositoryMetadataManager(ToolShedMetadataGenerator):
|
||||
|
||||
def set_repository(self, repository, repository_clone_url=None):
|
||||
super().set_repository(repository)
|
||||
self.repository_clone_url = repository_clone_url or common_util.generate_clone_url_for_repository_in_tool_shed(
|
||||
self.user, repository
|
||||
)
|
||||
self.repository_clone_url = repository_clone_url or common_util.generate_clone_url_for(self.trans, repository)
|
||||
|
||||
def set_repository_metadata(self, host, content_alert_str="", **kwd):
|
||||
"""
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
from functools import wraps
|
||||
from typing import (
|
||||
Any,
|
||||
Dict,
|
||||
@@ -38,6 +39,12 @@ class ShedBaseTestCase(DrivenFunctionalTestCase):
|
||||
def admin_api_interactor(self) -> ShedApiInteractor:
|
||||
return ShedApiInteractor(self.url, get_admin_api_key())
|
||||
|
||||
def _api_interactor_for_key(self, key: str) -> ShedApiInteractor:
|
||||
return self._api_interactor(key)
|
||||
|
||||
def populator_for_key(self, key: str) -> ToolShedPopulator:
|
||||
return self._get_populator(self._api_interactor_for_key(key))
|
||||
|
||||
@property
|
||||
def api_interactor(self) -> ShedApiInteractor:
|
||||
user_api_key = get_user_api_key()
|
||||
@@ -46,7 +53,7 @@ class ShedBaseTestCase(DrivenFunctionalTestCase):
|
||||
password = "testpassword"
|
||||
ensure_user_with_email(self.admin_api_interactor, email, password)
|
||||
user_api_key = self.admin_api_interactor.create_api_key(email, password)
|
||||
return self._api_interactor(user_api_key)
|
||||
return self._api_interactor_for_key(user_api_key)
|
||||
|
||||
def _api_interactor_by_credentials(self, email: str, password: str) -> ShedApiInteractor:
|
||||
ensure_user_with_email(self.admin_api_interactor, email, password)
|
||||
@@ -96,6 +103,25 @@ class ShedGalaxyInteractorApi(GalaxyInteractorApi):
|
||||
super().__init__(**interactor_kwds)
|
||||
|
||||
|
||||
def make_skip_if_api_version_wrapper(version):
|
||||
def wrapper(method):
|
||||
@wraps(method)
|
||||
def wrapped_method(api_test_case, *args, **kwd):
|
||||
interactor: ShedApiInteractor = api_test_case.api_interactor
|
||||
api_version = interactor.api_version
|
||||
if api_version == version:
|
||||
raise pytest.skip(f"{version} tool shed API found, skipping test")
|
||||
return method(api_test_case, *args, **kwd)
|
||||
|
||||
return wrapped_method
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
skip_if_api_v1 = make_skip_if_api_version_wrapper("v1")
|
||||
skip_if_api_v2 = make_skip_if_api_version_wrapper("v2")
|
||||
|
||||
|
||||
class ShedApiTestCase(ShedBaseTestCase, UsesShedApi):
|
||||
_galaxy_interactor: Optional[GalaxyInteractorApi] = None
|
||||
|
||||
|
||||
@@ -3,13 +3,14 @@ import re
|
||||
from functools import wraps
|
||||
from typing import (
|
||||
Any,
|
||||
Dict,
|
||||
Callable,
|
||||
Dict,
|
||||
Optional,
|
||||
)
|
||||
from urllib.parse import urljoin
|
||||
|
||||
import requests
|
||||
from typing_extensions import Literal
|
||||
|
||||
from galaxy_test.base.api_asserts import (
|
||||
assert_has_keys,
|
||||
@@ -85,6 +86,18 @@ class ShedApiInteractor:
|
||||
get = decorate_method(requests.get)
|
||||
post = decorate_method(requests.post)
|
||||
put = decorate_method(requests.put)
|
||||
delete = decorate_method(requests.delete)
|
||||
|
||||
@property
|
||||
def api_version(self) -> Literal["v1", "v2"]:
|
||||
config = self.version()
|
||||
api_version = config.get("api_version", "v1")
|
||||
return api_version
|
||||
|
||||
def version(self) -> Dict[str, Any]:
|
||||
response = self.get("version")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
|
||||
def create_user(admin_interactor: ShedApiInteractor, user_dict: Dict[str, Any], assert_ok=True) -> Dict[str, Any]:
|
||||
|
||||
@@ -40,6 +40,7 @@ from tool_shed_client.schema import (
|
||||
ResetMetadataOnRepositoryResponse,
|
||||
ToolSearchRequest,
|
||||
ToolSearchResults,
|
||||
Version,
|
||||
)
|
||||
from .api_util import (
|
||||
ensure_user_with_email,
|
||||
@@ -156,6 +157,11 @@ class ToolShedPopulator:
|
||||
repository = self.setup_column_maker_repo(prefix=prefix)
|
||||
return self.get_metadata(repository)
|
||||
|
||||
def get_install_info_for_repository(self, has_repository_id: HasRepositoryId) -> InstallInfo:
|
||||
repository_id = self._repository_id(has_repository_id)
|
||||
metadata = self.get_metadata(repository_id, True)
|
||||
return self.get_install_info(metadata)
|
||||
|
||||
def get_install_info(self, repository_metadata: RepositoryMetadata) -> InstallInfo:
|
||||
revision_metadata = repository_metadata.latest_revision
|
||||
repo = revision_metadata.repository
|
||||
@@ -303,6 +309,41 @@ class ToolShedPopulator:
|
||||
delete_response = self._api_interactor.delete(f"repositories/{repository_id}/allow_push/{username}")
|
||||
delete_response.raise_for_status()
|
||||
|
||||
def set_malicious(self, repository: HasRepositoryId, changeset_revision: str):
|
||||
repository_id = self._repository_id(repository)
|
||||
put_response = self._api_interactor.put(
|
||||
f"repositories/{repository_id}/revisions/{changeset_revision}/malicious"
|
||||
)
|
||||
put_response.raise_for_status()
|
||||
|
||||
def unset_malicious(self, repository: HasRepositoryId, changeset_revision: str):
|
||||
repository_id = self._repository_id(repository)
|
||||
delete_response = self._api_interactor.delete(
|
||||
f"repositories/{repository_id}/revisions/{changeset_revision}/malicious"
|
||||
)
|
||||
delete_response.raise_for_status()
|
||||
|
||||
def tip_is_malicious(self, repository: HasRepositoryId) -> bool:
|
||||
repository_metadata = self.get_metadata(repository)
|
||||
revision = repository_metadata.latest_revision
|
||||
return revision.malicious
|
||||
|
||||
def set_deprecated(self, repository: HasRepositoryId):
|
||||
repository_id = self._repository_id(repository)
|
||||
put_response = self._api_interactor.put(f"repositories/{repository_id}/deprecated")
|
||||
put_response.raise_for_status()
|
||||
|
||||
def unset_deprecated(self, repository: HasRepositoryId):
|
||||
repository_id = self._repository_id(repository)
|
||||
delete_response = self._api_interactor.delete(f"repositories/{repository_id}/deprecated")
|
||||
delete_response.raise_for_status()
|
||||
|
||||
def is_deprecated(self, repository: HasRepositoryId) -> bool:
|
||||
repository_id = self._repository_id(repository)
|
||||
repository_response = self._api_interactor.get(f"repositories/{repository_id}")
|
||||
repository_response.raise_for_status()
|
||||
return Repository(**repository_response.json()).deprecated
|
||||
|
||||
def get_metadata(self, repository: HasRepositoryId, downloadable_only=True) -> RepositoryMetadata:
|
||||
repository_id = self._repository_id(repository)
|
||||
metadata_response = self._api_interactor.get(
|
||||
@@ -318,6 +359,11 @@ class ToolShedPopulator:
|
||||
api_asserts.assert_status_code_is_ok(reset_response)
|
||||
return ResetMetadataOnRepositoryResponse(**reset_response.json())
|
||||
|
||||
def version(self) -> Version:
|
||||
version_response = self._admin_api_interactor.get("version")
|
||||
api_asserts.assert_status_code_is_ok(version_response)
|
||||
return Version(**version_response.json())
|
||||
|
||||
def tool_search_query(self, query: str) -> ToolSearchResults:
|
||||
return self.tool_search(ToolSearchRequest(q=query))
|
||||
|
||||
@@ -350,6 +396,15 @@ class ToolShedPopulator:
|
||||
api_asserts.assert_status_code_is_ok(search_response)
|
||||
return RepositorySearchResults(**search_response.json())
|
||||
|
||||
def delete_api_key(self) -> None:
|
||||
response = self._api_interactor.delete("users/current/api_key")
|
||||
response.raise_for_status()
|
||||
|
||||
def create_new_api_key(self) -> str:
|
||||
response = self._api_interactor.post("users/current/api_key")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def guid(self, repository: Repository, tool_id: str, tool_version: str) -> str:
|
||||
url = self._api_interactor.url
|
||||
base = url.split("://")[1].split("/")[0]
|
||||
|
||||
@@ -23,7 +23,6 @@ for deletion instead of writing new tests and modernizing the API.
|
||||
|
||||
- reset_metadata_on_repositories
|
||||
- remove_repository_registry_entry
|
||||
- get_repository_revision_install_info
|
||||
- get_installable_revisions
|
||||
- The whole Groups API.
|
||||
- The whole Repository Revisions API.
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
from ..base.api import ShedApiTestCase
|
||||
|
||||
|
||||
class TestShedConfigurationApi(ShedApiTestCase):
|
||||
def test_version(self) -> None:
|
||||
version = self.populator.version()
|
||||
assert version.version
|
||||
assert version.version_major
|
||||
@@ -6,12 +6,21 @@ from galaxy.util.compression_utils import CompressedFile
|
||||
from galaxy.util.resources import resource_path
|
||||
from galaxy_test.base import api_asserts
|
||||
from tool_shed.test.base.api_util import create_user
|
||||
from tool_shed.test.base.populators import repo_tars
|
||||
from ..base.api import ShedApiTestCase
|
||||
from tool_shed.test.base.populators import (
|
||||
HasRepositoryId,
|
||||
repo_tars,
|
||||
)
|
||||
from tool_shed_client.schema import RepositoryRevisionMetadata
|
||||
from ..base.api import (
|
||||
ShedApiTestCase,
|
||||
skip_if_api_v1,
|
||||
skip_if_api_v2,
|
||||
)
|
||||
|
||||
COLUMN_MAKER_PATH = resource_path(__package__, "../test_data/column_maker/column_maker.tar")
|
||||
|
||||
|
||||
# test_0000 tests commit_message - find a way to test it here
|
||||
class TestShedRepositoriesApi(ShedApiTestCase):
|
||||
def test_create(self):
|
||||
populator = self.populator
|
||||
@@ -69,6 +78,7 @@ class TestShedRepositoriesApi(ShedApiTestCase):
|
||||
assert repository.owner == repo.owner
|
||||
assert repository.name == repo.name
|
||||
|
||||
@skip_if_api_v1
|
||||
def test_allow_push(self):
|
||||
populator = self.populator
|
||||
request = {
|
||||
@@ -100,6 +110,32 @@ class TestShedRepositoriesApi(ShedApiTestCase):
|
||||
assert "sharewith" not in populator.get_usernames_allowed_to_push(repo)
|
||||
assert "alsosharewith" in populator.get_usernames_allowed_to_push(repo)
|
||||
|
||||
@skip_if_api_v1
|
||||
def test_set_malicious(self):
|
||||
populator = self.populator
|
||||
repository = populator.setup_column_maker_repo(prefix="repoformalicious")
|
||||
|
||||
only_revision = self._get_only_revision(repository)
|
||||
assert only_revision.downloadable
|
||||
assert not only_revision.malicious
|
||||
|
||||
assert not populator.tip_is_malicious(repository)
|
||||
populator.set_malicious(repository, only_revision.changeset_revision)
|
||||
assert populator.tip_is_malicious(repository)
|
||||
populator.unset_malicious(repository, only_revision.changeset_revision)
|
||||
assert not populator.tip_is_malicious(repository)
|
||||
|
||||
@skip_if_api_v1
|
||||
def test_set_deprecated(self):
|
||||
populator = self.populator
|
||||
repository = populator.setup_column_maker_repo(prefix="repofordeprecated")
|
||||
assert not repository.deprecated
|
||||
assert not populator.is_deprecated(repository)
|
||||
populator.set_deprecated(repository)
|
||||
assert populator.is_deprecated(repository)
|
||||
populator.unset_deprecated(repository)
|
||||
assert not populator.is_deprecated(repository)
|
||||
|
||||
def test_install_info(self):
|
||||
# actually installing requires a whole Galaxy setup and the install manager but
|
||||
# we can test the response validates against the future facing InstallInfo pydandic
|
||||
@@ -157,6 +193,19 @@ class TestShedRepositoriesApi(ShedApiTestCase):
|
||||
else:
|
||||
raise AssertionError("Wrong number of repo tars returned...")
|
||||
|
||||
@skip_if_api_v1
|
||||
def test_readmes(self):
|
||||
populator = self.populator
|
||||
repository = populator.setup_test_data_repo("column_maker_with_readme")
|
||||
only_revision = self._get_only_revision(repository)
|
||||
populator.assert_has_n_installable_revisions(repository, 1)
|
||||
response = self.api_interactor.get(
|
||||
f"repositories/{repository.id}/revisions/{only_revision.changeset_revision}/readmes"
|
||||
)
|
||||
api_asserts.assert_status_code_is_ok(response)
|
||||
readme_dicts = response.json()
|
||||
assert "readme.txt" in readme_dicts
|
||||
|
||||
def test_reset_on_simple_repository(self):
|
||||
populator = self.populator
|
||||
repository = populator.setup_test_data_repo("column_maker")
|
||||
@@ -178,6 +227,7 @@ class TestShedRepositoriesApi(ShedApiTestCase):
|
||||
api_asserts.assert_status_code_is_ok(response)
|
||||
populator.assert_has_n_installable_revisions(repository, 3)
|
||||
|
||||
@skip_if_api_v2
|
||||
def test_reset_all(self):
|
||||
populator = self.populator
|
||||
repository = populator.setup_test_data_repo("column_maker_with_download_gaps")
|
||||
@@ -190,3 +240,14 @@ class TestShedRepositoriesApi(ShedApiTestCase):
|
||||
)
|
||||
api_asserts.assert_status_code_is_ok(response)
|
||||
populator.assert_has_n_installable_revisions(repository, 3)
|
||||
|
||||
def _get_only_revision(self, repository: HasRepositoryId) -> RepositoryRevisionMetadata:
|
||||
populator = self.populator
|
||||
repository_metadata = populator.get_metadata(repository)
|
||||
metadata_for_revisions = repository_metadata.__root__
|
||||
assert len(metadata_for_revisions) == 1
|
||||
only_key = list(metadata_for_revisions.keys())[0]
|
||||
assert only_key.startswith("0:")
|
||||
only_revision = list(metadata_for_revisions.values())[0]
|
||||
assert only_revision
|
||||
return only_revision
|
||||
|
||||
@@ -7,7 +7,10 @@ from tool_shed_client.schema import (
|
||||
CreateUserRequest,
|
||||
User,
|
||||
)
|
||||
from ..base.api import ShedApiTestCase
|
||||
from ..base.api import (
|
||||
ShedApiTestCase,
|
||||
skip_if_api_v1,
|
||||
)
|
||||
from ..base.api_util import (
|
||||
email_to_username,
|
||||
ensure_user_with_email,
|
||||
@@ -77,5 +80,24 @@ class TestShedUsersApi(ShedApiTestCase):
|
||||
assert show_response.json()["username"] == username
|
||||
assert show_response.json()["id"] == user_id
|
||||
|
||||
def _verify_username_password(self, email, password):
|
||||
self.api_interactor.create_api_key(email, password)
|
||||
@skip_if_api_v1
|
||||
def test_api_key_endpoints(self):
|
||||
email = "testindexapi@bx.psu.edu"
|
||||
password = "mycoolpassword123"
|
||||
ensure_user_with_email(self.admin_api_interactor, email, password)
|
||||
api_key = self._verify_username_password(email, password)
|
||||
second_try_api_key = self._verify_username_password(email, password)
|
||||
assert api_key == second_try_api_key
|
||||
|
||||
user_populator = self.populator_for_key(api_key)
|
||||
user_populator.delete_api_key()
|
||||
new_api_key = self._verify_username_password(email, password)
|
||||
assert api_key != new_api_key
|
||||
|
||||
user_populator = self.populator_for_key(new_api_key)
|
||||
another_new_api_key = user_populator.create_new_api_key()
|
||||
assert new_api_key != another_new_api_key
|
||||
assert new_api_key != api_key
|
||||
|
||||
def _verify_username_password(self, email: str, password: str) -> str:
|
||||
return self.api_interactor.create_api_key(email, password)
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
<tool id="Add_a_column1" name="Compute" version="1.1.0">
|
||||
<description>an expression on every row</description>
|
||||
<command interpreter="python">
|
||||
column_maker.py $input $out_file1 "$cond" $round ${input.metadata.columns} "${input.metadata.column_types}"
|
||||
</command>
|
||||
<inputs>
|
||||
<param name="cond" size="40" type="text" value="c3-c2" label="Add expression"/>
|
||||
<param format="tabular" name="input" type="data" label="as a new column to" help="Query missing? See TIP below"/>
|
||||
<param name="round" type="select" label="Round result?">
|
||||
<option value="no">NO</option>
|
||||
<option value="yes">YES</option>
|
||||
</param>
|
||||
</inputs>
|
||||
<outputs>
|
||||
<data format="input" name="out_file1" metadata_source="input"/>
|
||||
</outputs>
|
||||
<tests>
|
||||
<test>
|
||||
<param name="cond" value="c3-c2"/>
|
||||
<param name="input" value="1.bed"/>
|
||||
<param name="round" value="no"/>
|
||||
<output name="out_file1" file="column_maker_out1.interval"/>
|
||||
</test>
|
||||
<test>
|
||||
<param name="cond" value="c4*1"/>
|
||||
<param name="input" value="1.interval"/>
|
||||
<param name="round" value="no"/>
|
||||
<output name="out_file1" file="column_maker_out2.interval"/>
|
||||
</test>
|
||||
<test>
|
||||
<param name="cond" value="c4*1"/>
|
||||
<param name="input" value="1.interval"/>
|
||||
<param name="round" value="yes"/>
|
||||
<output name="out_file1" file="column_maker_out3.interval"/>
|
||||
</test>
|
||||
</tests>
|
||||
<help>
|
||||
|
||||
.. class:: infomark
|
||||
|
||||
**TIP:** If your data is not TAB delimited, use *Text Manipulation->Convert*
|
||||
|
||||
-----
|
||||
|
||||
**What it does**
|
||||
|
||||
This tool computes an expression for every row of a query and appends the result as a new column (field).
|
||||
|
||||
- Columns are referenced with **c** and a **number**. For example, **c1** refers to the first column of a tab-delimited file
|
||||
|
||||
- **c3-c2** will add a length column to the query if **c2** and **c3** are start and end position
|
||||
|
||||
-----
|
||||
|
||||
**Example**
|
||||
|
||||
If this is your input::
|
||||
|
||||
chr1 151077881 151077918 2 200 -
|
||||
chr1 151081985 151082078 3 500 +
|
||||
|
||||
computing "c4*c5" will produce::
|
||||
|
||||
chr1 151077881 151077918 2 200 - 400.0
|
||||
chr1 151081985 151082078 3 500 + 1500.0
|
||||
|
||||
if, at the same time, "Round result?" is set to **YES** results will look like this::
|
||||
|
||||
chr1 151077881 151077918 2 200 - 400
|
||||
chr1 151081985 151082078 3 500 + 1500
|
||||
|
||||
You can also use this tool to evaluate expressions. For example, computing "c3>=c2" for Input will result in the following::
|
||||
|
||||
chr1 151077881 151077918 2 200 - True
|
||||
chr1 151081985 151082078 3 500 + True
|
||||
|
||||
or computing "type(c2)==type('') for Input will return::
|
||||
|
||||
chr1 151077881 151077918 2 200 - False
|
||||
chr1 151081985 151082078 3 500 + False
|
||||
|
||||
</help>
|
||||
</tool>
|
||||
@@ -0,0 +1 @@
|
||||
This is a readme that will be rendered as rst.
|
||||
@@ -1,3 +1,8 @@
|
||||
from typing import (
|
||||
Optional,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
from routes import url_for
|
||||
|
||||
from galaxy.util.tool_shed.common_util import (
|
||||
@@ -19,16 +24,24 @@ from galaxy.util.tool_shed.common_util import (
|
||||
remove_protocol_from_tool_shed_url,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tool_shed.context import ProvidesRepositoriesContext
|
||||
from tool_shed.webapp.model import (
|
||||
Repository,
|
||||
User,
|
||||
)
|
||||
|
||||
def generate_clone_url_for_repository_in_tool_shed(user, repository) -> str:
|
||||
|
||||
def generate_clone_url_for(trans: "ProvidesRepositoriesContext", repository: "Repository") -> str:
|
||||
return generate_clone_url_for_repository_in_tool_shed(trans.user, repository, trans.repositories_hostname)
|
||||
|
||||
|
||||
def generate_clone_url_for_repository_in_tool_shed(
|
||||
user: Optional["User"], repository: "Repository", hostname: Optional[str] = None
|
||||
) -> str:
|
||||
"""Generate the URL for cloning a repository that is in the tool shed."""
|
||||
base_url = url_for("/", qualified=True).rstrip("/")
|
||||
if user:
|
||||
protocol, base = base_url.split("://")
|
||||
username = f"{user.username}@"
|
||||
return f"{protocol}://{username}{base}/repos/{repository.user.username}/{repository.name}"
|
||||
else:
|
||||
return f"{base_url}/repos/{repository.user.username}/{repository.name}"
|
||||
base_url = hostname or url_for("/", qualified=True).rstrip("/")
|
||||
return f"{base_url}/repos/{repository.user.username}/{repository.name}"
|
||||
|
||||
|
||||
__all__ = (
|
||||
|
||||
@@ -22,7 +22,7 @@ from tool_shed.util.commit_util import (
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tool_shed.structured_app import ToolShedApp
|
||||
from tool_shed.context import ProvidesRepositoriesContext
|
||||
from tool_shed.webapp.model import Repository
|
||||
|
||||
|
||||
@@ -41,8 +41,7 @@ def tar_open(uploaded_file):
|
||||
|
||||
|
||||
def upload_tar(
|
||||
app: "ToolShedApp",
|
||||
host: str,
|
||||
trans: "ProvidesRepositoriesContext",
|
||||
username: str,
|
||||
repository: "Repository",
|
||||
uploaded_file,
|
||||
@@ -54,10 +53,12 @@ def upload_tar(
|
||||
rdah: Optional[RepositoryDependencyAttributeHandler] = None,
|
||||
tdah: Optional[ToolDependencyAttributeHandler] = None,
|
||||
) -> ChangeResponseT:
|
||||
host = trans.repositories_hostname
|
||||
app = trans.app
|
||||
if tar is None:
|
||||
tar = tar_open(uploaded_file)
|
||||
rdah = rdah or RepositoryDependencyAttributeHandler(app, unpopulate=False)
|
||||
tdah = tdah or ToolDependencyAttributeHandler(app, unpopulate=False)
|
||||
rdah = rdah or RepositoryDependencyAttributeHandler(trans, unpopulate=False)
|
||||
tdah = tdah or ToolDependencyAttributeHandler(trans, unpopulate=False)
|
||||
# Upload a tar archive of files.
|
||||
undesirable_dirs_removed = 0
|
||||
undesirable_files_removed = 0
|
||||
|
||||
@@ -48,7 +48,7 @@ from galaxy.tool_shed.util.repository_util import (
|
||||
repository_was_previously_installed,
|
||||
set_repository_attributes,
|
||||
)
|
||||
from galaxy.util.tool_shed import common_util
|
||||
from tool_shed.util.common_util import generate_clone_url_for
|
||||
from tool_shed.util.hg_util import (
|
||||
changeset2rev,
|
||||
create_hgrc_file,
|
||||
@@ -62,7 +62,10 @@ from tool_shed.util.metadata_util import (
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tool_shed.context import ProvidesUserContext
|
||||
from tool_shed.context import (
|
||||
ProvidesRepositoriesContext,
|
||||
ProvidesUserContext,
|
||||
)
|
||||
from tool_shed.structured_app import ToolShedApp
|
||||
from tool_shed.webapp.model import Repository
|
||||
|
||||
@@ -83,6 +86,7 @@ def create_repo_info_dict(
|
||||
repository_metadata=None,
|
||||
tool_dependencies=None,
|
||||
repository_dependencies=None,
|
||||
trans=None,
|
||||
):
|
||||
"""
|
||||
Return a dictionary that includes all of the information needed to install a repository into a local
|
||||
@@ -110,15 +114,16 @@ def create_repo_info_dict(
|
||||
repository = get_repository_by_name_and_owner(app, repository_name, repository_owner)
|
||||
if app.name == "tool_shed":
|
||||
# We're in the tool shed.
|
||||
repository_metadata = repository_metadata_by_changeset_revision(
|
||||
app.model, repository.id, changeset_revision
|
||||
)
|
||||
repository_metadata = repository_metadata_by_changeset_revision(app.model, repository.id, changeset_revision)
|
||||
if repository_metadata:
|
||||
metadata = repository_metadata.metadata
|
||||
if metadata:
|
||||
tool_shed_url = web.url_for("/", qualified=True).rstrip("/")
|
||||
if trans is not None:
|
||||
tool_shed_url = trans.repositories_hostname
|
||||
else:
|
||||
tool_shed_url = web.url_for("/", qualified=True).rstrip("/")
|
||||
rb = tool_shed.dependencies.repository.relation_builder.RelationBuilder(
|
||||
app, repository, repository_metadata, tool_shed_url
|
||||
app, repository, repository_metadata, tool_shed_url, trans=trans
|
||||
)
|
||||
# Get a dictionary of all repositories upon which the contents of the received repository depends.
|
||||
repository_dependencies = rb.get_repository_dependencies_for_changeset_revision()
|
||||
@@ -259,9 +264,10 @@ def get_repository_in_tool_shed(app, id, eagerload_columns=None):
|
||||
return q.get(app.security.decode_id(id))
|
||||
|
||||
|
||||
def get_repo_info_dict(app: "ToolShedApp", user, repository_id, changeset_revision):
|
||||
def get_repo_info_dict(trans: "ProvidesRepositoriesContext", repository_id, changeset_revision):
|
||||
app = trans.app
|
||||
repository = get_repository_in_tool_shed(app, repository_id)
|
||||
repository_clone_url = common_util.generate_clone_url_for_repository_in_tool_shed(user, repository)
|
||||
repository_clone_url = generate_clone_url_for(trans, repository)
|
||||
repository_metadata = get_repository_metadata_by_changeset_revision(app, repository_id, changeset_revision)
|
||||
if not repository_metadata:
|
||||
# The received changeset_revision is no longer installable, so get the next changeset_revision
|
||||
@@ -314,6 +320,7 @@ def get_repo_info_dict(app: "ToolShedApp", user, repository_id, changeset_revisi
|
||||
repository_metadata=repository_metadata,
|
||||
tool_dependencies=None,
|
||||
repository_dependencies=None,
|
||||
trans=trans,
|
||||
)
|
||||
return (
|
||||
repo_info_dict,
|
||||
|
||||
@@ -435,10 +435,11 @@ def open_repository_files_folder(app, folder_path, repository_id, is_admin=False
|
||||
return folder_contents
|
||||
|
||||
|
||||
def tool_shed_is_this_tool_shed(toolshed_base_url):
|
||||
def tool_shed_is_this_tool_shed(toolshed_base_url, trans=None):
|
||||
"""Determine if a tool shed is the current tool shed."""
|
||||
cleaned_toolshed_base_url = common_util.remove_protocol_from_tool_shed_url(toolshed_base_url)
|
||||
cleaned_tool_shed = common_util.remove_protocol_from_tool_shed_url(str(url_for("/", qualified=True)))
|
||||
hostname = trans.repositories_hostname if trans else str(url_for("/", qualified=True))
|
||||
cleaned_tool_shed = common_util.remove_protocol_from_tool_shed_url(hostname)
|
||||
return cleaned_toolshed_base_url == cleaned_tool_shed
|
||||
|
||||
|
||||
|
||||
@@ -18,10 +18,7 @@ from galaxy.web import (
|
||||
require_admin,
|
||||
)
|
||||
from galaxy.webapps.galaxy.api import depends
|
||||
from tool_shed.managers.categories import (
|
||||
CategoryManager,
|
||||
get_value_mapper,
|
||||
)
|
||||
from tool_shed.managers.categories import CategoryManager
|
||||
from tool_shed.managers.repositories import repositories_by_category
|
||||
from tool_shed.webapp.model import Category
|
||||
from . import BaseShedAPIController
|
||||
@@ -117,8 +114,5 @@ class CategoriesController(BaseShedAPIController):
|
||||
if category is None:
|
||||
category_dict = dict(message=f"Unable to locate category record for id {str(id)}.", status="error")
|
||||
return category_dict
|
||||
category_dict = category.to_dict(view="element", value_mapper=get_value_mapper(trans.app))
|
||||
category_dict["url"] = web.url_for(
|
||||
controller="categories", action="show", id=trans.security.encode_id(category.id)
|
||||
)
|
||||
category_dict = self.category_manager.to_dict(category)
|
||||
return category_dict
|
||||
|
||||
@@ -202,7 +202,7 @@ class RepositoriesController(BaseShedAPIController):
|
||||
}
|
||||
|
||||
"""
|
||||
return get_install_info(self.app, name, owner, changeset_revision)
|
||||
return get_install_info(trans, name, owner, changeset_revision)
|
||||
|
||||
@web.legacy_expose_api_anonymous
|
||||
def get_installable_revisions(self, trans, **kwd):
|
||||
@@ -378,8 +378,7 @@ class RepositoriesController(BaseShedAPIController):
|
||||
log.debug(f"Resetting metadata on repository {repository.name}")
|
||||
try:
|
||||
rmm = repository_metadata_manager.RepositoryMetadataManager(
|
||||
app=self.app,
|
||||
user=trans.user,
|
||||
trans,
|
||||
resetting_all_metadata_on_repository=True,
|
||||
updating_installed_repository=False,
|
||||
repository=repository,
|
||||
@@ -423,8 +422,7 @@ class RepositoriesController(BaseShedAPIController):
|
||||
else:
|
||||
my_writable = True
|
||||
rmm = repository_metadata_manager.RepositoryMetadataManager(
|
||||
app=self.app,
|
||||
user=trans.user,
|
||||
trans,
|
||||
resetting_all_metadata_on_repository=True,
|
||||
updating_installed_repository=False,
|
||||
persist=False,
|
||||
@@ -725,9 +723,8 @@ class RepositoriesController(BaseShedAPIController):
|
||||
uploaded_file_name = file_data["local_filename"]
|
||||
try:
|
||||
message = upload_tar_and_set_metadata(
|
||||
self.app,
|
||||
trans,
|
||||
trans.request.host,
|
||||
trans.user,
|
||||
repository,
|
||||
uploaded_file_name,
|
||||
commit_message,
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
from json import JSONDecodeError
|
||||
from typing import (
|
||||
AsyncGenerator,
|
||||
cast,
|
||||
List,
|
||||
Optional,
|
||||
Type,
|
||||
TypeVar,
|
||||
)
|
||||
|
||||
from fastapi import (
|
||||
Depends,
|
||||
HTTPException,
|
||||
Path,
|
||||
Query,
|
||||
Request,
|
||||
Response,
|
||||
Security,
|
||||
)
|
||||
from fastapi.security import (
|
||||
APIKeyCookie,
|
||||
APIKeyHeader,
|
||||
APIKeyQuery,
|
||||
)
|
||||
from pydantic import BaseModel
|
||||
from starlette_context import context as request_context
|
||||
|
||||
from galaxy.exceptions import AdminRequiredException
|
||||
from galaxy.managers.session import GalaxySessionManager
|
||||
from galaxy.managers.users import UserManager
|
||||
from galaxy.security.idencoding import IdEncodingHelper
|
||||
from galaxy.web.framework.decorators import require_admin_message
|
||||
from galaxy.webapps.galaxy.api import (
|
||||
depends as framework_depends,
|
||||
FrameworkRouter,
|
||||
GalaxyASGIRequest,
|
||||
GalaxyASGIResponse,
|
||||
T,
|
||||
UrlBuilder,
|
||||
)
|
||||
from tool_shed.context import (
|
||||
SessionRequestContext,
|
||||
SessionRequestContextImpl,
|
||||
)
|
||||
from tool_shed.structured_app import ToolShedApp
|
||||
from tool_shed.webapp import app as tool_shed_app_mod
|
||||
from tool_shed.webapp.model import (
|
||||
GalaxySession,
|
||||
User,
|
||||
)
|
||||
|
||||
|
||||
def get_app() -> ToolShedApp:
|
||||
if tool_shed_app_mod.app is None:
|
||||
raise Exception("Failed to initialize the tool shed app correctly for FastAPI")
|
||||
return cast(ToolShedApp, tool_shed_app_mod.app)
|
||||
|
||||
|
||||
async def get_app_with_request_session() -> AsyncGenerator[ToolShedApp, None]:
|
||||
app = get_app()
|
||||
request_id = request_context.data["X-Request-ID"]
|
||||
app.model.set_request_id(request_id)
|
||||
try:
|
||||
yield app
|
||||
finally:
|
||||
app.model.unset_request_id(request_id)
|
||||
|
||||
|
||||
DependsOnApp = cast(ToolShedApp, Depends(get_app_with_request_session))
|
||||
|
||||
api_key_query = APIKeyQuery(name="key", auto_error=False)
|
||||
api_key_header = APIKeyHeader(name="x-api-key", auto_error=False)
|
||||
api_key_cookie = APIKeyCookie(name="galaxycommunitysession", auto_error=False)
|
||||
|
||||
|
||||
def depends(dep_type: Type[T]) -> T:
|
||||
return framework_depends(dep_type, get_app=get_app)
|
||||
|
||||
|
||||
def get_api_user(
|
||||
user_manager: UserManager = depends(UserManager),
|
||||
key: str = Security(api_key_query),
|
||||
x_api_key: str = Security(api_key_header),
|
||||
) -> Optional[User]:
|
||||
api_key = key or x_api_key
|
||||
if not api_key:
|
||||
return None
|
||||
user = user_manager.by_api_key(api_key=api_key)
|
||||
return user
|
||||
|
||||
|
||||
def get_session_manager(app: ToolShedApp = DependsOnApp) -> GalaxySessionManager:
|
||||
# TODO: find out how to adapt dependency for Galaxy/Report/TS
|
||||
return GalaxySessionManager(app.model)
|
||||
|
||||
|
||||
def get_session(
|
||||
session_manager=cast(GalaxySessionManager, Depends(get_session_manager)),
|
||||
security: IdEncodingHelper = depends(IdEncodingHelper),
|
||||
galaxysession: str = Security(api_key_cookie),
|
||||
) -> Optional[GalaxySession]:
|
||||
if galaxysession:
|
||||
session_key = security.decode_guid(galaxysession)
|
||||
if session_key:
|
||||
return session_manager.get_session_from_session_key(session_key)
|
||||
# TODO: What should we do if there is no session? Since this is the API, maybe nothing is the right choice?
|
||||
return None
|
||||
|
||||
|
||||
def get_user(
|
||||
galaxy_session=cast(Optional[GalaxySession], Depends(get_session)),
|
||||
api_user=cast(Optional[User], Depends(get_api_user)),
|
||||
) -> Optional[User]:
|
||||
if galaxy_session:
|
||||
return galaxy_session.user
|
||||
return api_user
|
||||
|
||||
|
||||
def get_trans(
|
||||
request: Request,
|
||||
response: Response,
|
||||
app: ToolShedApp = DependsOnApp,
|
||||
user=cast(Optional[User], Depends(get_user)),
|
||||
galaxy_session=cast(Optional[GalaxySession], Depends(get_session)),
|
||||
) -> SessionRequestContext:
|
||||
url_builder = UrlBuilder(request)
|
||||
galaxy_request = GalaxyASGIRequest(request)
|
||||
galaxy_response = GalaxyASGIResponse(response)
|
||||
return SessionRequestContextImpl(
|
||||
app,
|
||||
galaxy_request,
|
||||
galaxy_response,
|
||||
user=user,
|
||||
galaxy_session=galaxy_session,
|
||||
url_builder=url_builder,
|
||||
)
|
||||
|
||||
|
||||
DependsOnTrans: SessionRequestContext = cast(SessionRequestContext, Depends(get_trans))
|
||||
|
||||
|
||||
def get_admin_user(trans: SessionRequestContext = DependsOnTrans):
|
||||
if not trans.user_is_admin:
|
||||
raise AdminRequiredException(require_admin_message(trans.app.config, trans.user))
|
||||
return trans.user
|
||||
|
||||
|
||||
AdminUserRequired = Depends(get_admin_user)
|
||||
|
||||
|
||||
class Router(FrameworkRouter):
|
||||
admin_user_dependency = AdminUserRequired
|
||||
|
||||
|
||||
B = TypeVar("B", bound=BaseModel)
|
||||
|
||||
|
||||
# async def depend_on_either_json_or_form_data(model: Type[T]):
|
||||
# return Depends(get_body)
|
||||
|
||||
|
||||
def depend_on_either_json_or_form_data(model: Type[B]) -> B:
|
||||
async def get_body(request: Request):
|
||||
content_type = request.headers.get("Content-Type")
|
||||
if content_type is None:
|
||||
raise HTTPException(status_code=400, detail="No Content-Type provided!")
|
||||
elif content_type == "application/json":
|
||||
try:
|
||||
return model(**await request.json())
|
||||
except JSONDecodeError:
|
||||
raise HTTPException(status_code=400, detail="Invalid JSON data")
|
||||
elif content_type == "application/x-www-form-urlencoded" or content_type.startswith("multipart/form-data"):
|
||||
try:
|
||||
return model(**await request.form())
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid Form data")
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="Content-Type not supported!")
|
||||
|
||||
return Depends(get_body)
|
||||
|
||||
|
||||
UserIdPathParam: str = Path(..., title="User ID", description="The encoded database identifier of the user.")
|
||||
|
||||
RequiredRepoOwnerParam: str = Query(
|
||||
title="owner",
|
||||
description="Owner of the target repository.",
|
||||
)
|
||||
|
||||
RequiredRepoNameParam: str = Query(
|
||||
title="Name",
|
||||
description="Name of the target repository.",
|
||||
)
|
||||
|
||||
RequiredChangesetParam: str = Query(
|
||||
title="changeset",
|
||||
description="Changeset of the target repository.",
|
||||
)
|
||||
|
||||
RepositoryIdPathParam: str = Path(
|
||||
..., title="Repository ID", description="The encoded database identifier of the repository."
|
||||
)
|
||||
|
||||
ChangesetRevisionPathParam: str = Path(
|
||||
...,
|
||||
title="Change Revision",
|
||||
description="The changeset revision corresponding to the target revision of the target repository.",
|
||||
)
|
||||
|
||||
UsernameIdPathParam: str = Path(..., title="Username", description="The target username.")
|
||||
|
||||
CommitMessageQueryParam: Optional[str] = Query(
|
||||
default=None,
|
||||
title="Commit Message",
|
||||
description="Set commit message as a query parameter.",
|
||||
)
|
||||
|
||||
DownloadableQueryParam: bool = Query(
|
||||
default=True,
|
||||
title="downloadable_only",
|
||||
description="Include only downable repositories.",
|
||||
)
|
||||
|
||||
CommitMessage: str = Query(
|
||||
None,
|
||||
title="Commit message",
|
||||
description="A commit message to store with repository update.",
|
||||
)
|
||||
|
||||
RepositoryIndexQueryParam: Optional[str] = Query(
|
||||
default=None,
|
||||
title="Search Query",
|
||||
)
|
||||
|
||||
ToolsIndexQueryParam: str = Query(
|
||||
default=...,
|
||||
title="Search Query",
|
||||
)
|
||||
|
||||
RepositorySearchPageQueryParam: int = Query(
|
||||
default=1,
|
||||
title="Page",
|
||||
)
|
||||
|
||||
RepositorySearchPageSizeQueryParam: int = Query(
|
||||
default=10,
|
||||
title="Page Size",
|
||||
)
|
||||
|
||||
RepositoryIndexDeletedQueryParam: Optional[bool] = Query(False, title="Deleted?")
|
||||
|
||||
RepositoryIndexOwnerQueryParam: Optional[str] = Query(None, title="Owner")
|
||||
|
||||
RepositoryIndexNameQueryParam: Optional[str] = Query(None, title="Name")
|
||||
|
||||
RepositoryIndexToolIdsQueryParam: Optional[List[str]] = Query(
|
||||
None, title="Tool IDs", description="List of tool GUIDs to find the repository for"
|
||||
)
|
||||
|
||||
|
||||
OptionalRepositoryOwnerParam: Optional[str] = Query(None, title="Owner")
|
||||
OptionalRepositoryNameParam: Optional[str] = Query(None, title="Name")
|
||||
RequiredRepositoryChangesetRevisionParam: str = Query(..., title="Changeset Revision")
|
||||
OptionalRepositoryIdParam: Optional[str] = Query(None, title="TSR ID")
|
||||
OptionalHexlifyParam: Optional[bool] = Query(True, title="Hexlify response")
|
||||
|
||||
CategoryIdPathParam: str = Path(
|
||||
..., title="Category ID", description="The encoded database identifier of the category."
|
||||
)
|
||||
CategoryRepositoriesInstallableQueryParam: bool = Query(False, title="Installable?")
|
||||
CategoryRepositoriesSortKeyQueryParam: str = Query("name", title="Sort Key")
|
||||
CategoryRepositoriesSortOrderQueryParam: str = Query("asc", title="Sort Order")
|
||||
CategoryRepositoriesPageQueryParam: Optional[int] = Query(None, title="Page")
|
||||
@@ -0,0 +1,27 @@
|
||||
from fastapi import Request
|
||||
|
||||
from galaxy.webapps.galaxy.services.authenticate import (
|
||||
APIKeyResponse,
|
||||
AuthenticationService,
|
||||
)
|
||||
from . import (
|
||||
depends,
|
||||
Router,
|
||||
)
|
||||
|
||||
router = Router(tags=["authenticate"])
|
||||
|
||||
|
||||
@router.cbv
|
||||
class FastAPIAuthenticate:
|
||||
authentication_service: AuthenticationService = depends(AuthenticationService)
|
||||
|
||||
@router.get(
|
||||
"/api/authenticate/baseauth",
|
||||
summary="Returns returns an API key for authenticated user based on BaseAuth headers.",
|
||||
operation_id="authenticate__baseauth",
|
||||
)
|
||||
def get_api_key(self, request: Request) -> APIKeyResponse:
|
||||
authorization = request.headers.get("Authorization")
|
||||
auth = {"HTTP_AUTHORIZATION": authorization}
|
||||
return self.authentication_service.get_api_key(auth, request)
|
||||
@@ -0,0 +1,80 @@
|
||||
from typing import (
|
||||
List,
|
||||
Optional,
|
||||
)
|
||||
|
||||
from fastapi import Body
|
||||
|
||||
from tool_shed.context import SessionRequestContext
|
||||
from tool_shed.managers.categories import CategoryManager
|
||||
from tool_shed.managers.repositories import repositories_by_category
|
||||
from tool_shed_client.schema import (
|
||||
Category as CategoryResponse,
|
||||
CreateCategoryRequest,
|
||||
RepositoriesByCategory,
|
||||
)
|
||||
from . import (
|
||||
CategoryIdPathParam,
|
||||
CategoryRepositoriesInstallableQueryParam,
|
||||
CategoryRepositoriesPageQueryParam,
|
||||
CategoryRepositoriesSortKeyQueryParam,
|
||||
CategoryRepositoriesSortOrderQueryParam,
|
||||
depends,
|
||||
DependsOnTrans,
|
||||
Router,
|
||||
)
|
||||
|
||||
router = Router(tags=["categories"])
|
||||
|
||||
|
||||
@router.cbv
|
||||
class FastAPICategories:
|
||||
category_manager: CategoryManager = depends(CategoryManager)
|
||||
|
||||
@router.post(
|
||||
"/api/categories",
|
||||
description="create a category",
|
||||
operation_id="categories__create",
|
||||
require_admin=True,
|
||||
)
|
||||
def create(
|
||||
self, trans: SessionRequestContext = DependsOnTrans, request: CreateCategoryRequest = Body(...)
|
||||
) -> CategoryResponse:
|
||||
category = self.category_manager.create(trans, request)
|
||||
return self.category_manager.to_model(category)
|
||||
|
||||
@router.get(
|
||||
"/api/categories",
|
||||
description="index category",
|
||||
operation_id="categories__index",
|
||||
)
|
||||
def index(self, trans: SessionRequestContext = DependsOnTrans) -> List[CategoryResponse]:
|
||||
"""
|
||||
Return a list of dictionaries that contain information about each Category.
|
||||
"""
|
||||
deleted = False
|
||||
categories = self.category_manager.index_db(trans, deleted)
|
||||
return [self.category_manager.to_model(c) for c in categories]
|
||||
|
||||
@router.get(
|
||||
"/api/categories/{encoded_category_id}/repositories",
|
||||
description="display repositories by category",
|
||||
operation_id="categories__repositories",
|
||||
)
|
||||
def repositories(
|
||||
self,
|
||||
trans: SessionRequestContext = DependsOnTrans,
|
||||
encoded_category_id: str = CategoryIdPathParam,
|
||||
installable: bool = CategoryRepositoriesInstallableQueryParam,
|
||||
sort_key: str = CategoryRepositoriesSortKeyQueryParam,
|
||||
sort_order: str = CategoryRepositoriesSortOrderQueryParam,
|
||||
page: Optional[int] = CategoryRepositoriesPageQueryParam,
|
||||
) -> RepositoriesByCategory:
|
||||
return repositories_by_category(
|
||||
trans.app,
|
||||
encoded_category_id,
|
||||
page=page,
|
||||
sort_key=sort_key,
|
||||
sort_order=sort_order,
|
||||
installable=installable,
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
from tool_shed.structured_app import ToolShedApp
|
||||
from tool_shed_client.schema import Version
|
||||
from . import (
|
||||
depends,
|
||||
Router,
|
||||
)
|
||||
|
||||
router = Router(tags=["configuration"])
|
||||
|
||||
|
||||
@router.cbv
|
||||
class FastAPIConfiguration:
|
||||
app: ToolShedApp = depends(ToolShedApp)
|
||||
|
||||
@router.get(
|
||||
"/api/version",
|
||||
operation_id="configuration__version",
|
||||
)
|
||||
def version(self) -> Version:
|
||||
return Version(
|
||||
version_major=self.app.config.version_major,
|
||||
version=self.app.config.version,
|
||||
api_version="v2",
|
||||
)
|
||||
@@ -0,0 +1,494 @@
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
from typing import (
|
||||
cast,
|
||||
IO,
|
||||
List,
|
||||
Optional,
|
||||
Union,
|
||||
)
|
||||
|
||||
from fastapi import (
|
||||
Body,
|
||||
Depends,
|
||||
Request,
|
||||
Response,
|
||||
status,
|
||||
UploadFile,
|
||||
)
|
||||
from starlette.datastructures import UploadFile as StarletteUploadFile
|
||||
|
||||
from galaxy.exceptions import InsufficientPermissionsException
|
||||
from galaxy.webapps.galaxy.api import as_form
|
||||
from tool_shed.context import SessionRequestContext
|
||||
from tool_shed.managers.repositories import (
|
||||
can_manage_repo,
|
||||
can_update_repo,
|
||||
check_updates,
|
||||
create_repository,
|
||||
get_install_info,
|
||||
get_ordered_installable_revisions,
|
||||
get_repository_metadata_dict,
|
||||
get_repository_metadata_for_management,
|
||||
index_repositories,
|
||||
readmes,
|
||||
reset_metadata_on_repository,
|
||||
search,
|
||||
to_detailed_model,
|
||||
to_model,
|
||||
UpdatesRequest,
|
||||
upload_tar_and_set_metadata,
|
||||
)
|
||||
from tool_shed.structured_app import ToolShedApp
|
||||
from tool_shed.util.repository_util import get_repository_in_tool_shed
|
||||
from tool_shed_client.schema import (
|
||||
CreateRepositoryRequest,
|
||||
DetailedRepository,
|
||||
from_legacy_install_info,
|
||||
InstallInfo,
|
||||
Repository,
|
||||
RepositoryMetadata,
|
||||
RepositoryPermissions,
|
||||
RepositoryRevisionReadmes,
|
||||
RepositorySearchResults,
|
||||
RepositoryUpdate,
|
||||
RepositoryUpdateRequest,
|
||||
ResetMetadataOnRepositoryRequest,
|
||||
ResetMetadataOnRepositoryResponse,
|
||||
ValidRepostiroyUpdateMessage,
|
||||
)
|
||||
from . import (
|
||||
ChangesetRevisionPathParam,
|
||||
CommitMessageQueryParam,
|
||||
depend_on_either_json_or_form_data,
|
||||
depends,
|
||||
DependsOnTrans,
|
||||
DownloadableQueryParam,
|
||||
OptionalHexlifyParam,
|
||||
OptionalRepositoryIdParam,
|
||||
OptionalRepositoryNameParam,
|
||||
OptionalRepositoryOwnerParam,
|
||||
RepositoryIdPathParam,
|
||||
RepositoryIndexDeletedQueryParam,
|
||||
RepositoryIndexNameQueryParam,
|
||||
RepositoryIndexOwnerQueryParam,
|
||||
RepositoryIndexQueryParam,
|
||||
RepositorySearchPageQueryParam,
|
||||
RepositorySearchPageSizeQueryParam,
|
||||
RequiredChangesetParam,
|
||||
RequiredRepoNameParam,
|
||||
RequiredRepoOwnerParam,
|
||||
RequiredRepositoryChangesetRevisionParam,
|
||||
Router,
|
||||
UsernameIdPathParam,
|
||||
)
|
||||
|
||||
router = Router(tags=["repositories"])
|
||||
|
||||
IndexResponse = Union[RepositorySearchResults, List[Repository]]
|
||||
|
||||
|
||||
@as_form
|
||||
class RepositoryUpdateRequestFormData(RepositoryUpdateRequest):
|
||||
pass
|
||||
|
||||
|
||||
@router.cbv
|
||||
class FastAPIRepositories:
|
||||
app: ToolShedApp = depends(ToolShedApp)
|
||||
|
||||
@router.get(
|
||||
"/api/repositories",
|
||||
description="Get a list of repositories or perform a search.",
|
||||
operation_id="repositories__index",
|
||||
)
|
||||
def index(
|
||||
self,
|
||||
q: Optional[str] = RepositoryIndexQueryParam,
|
||||
page: Optional[int] = RepositorySearchPageQueryParam,
|
||||
page_size: Optional[int] = RepositorySearchPageSizeQueryParam,
|
||||
deleted: Optional[bool] = RepositoryIndexDeletedQueryParam,
|
||||
owner: Optional[str] = RepositoryIndexOwnerQueryParam,
|
||||
name: Optional[str] = RepositoryIndexNameQueryParam,
|
||||
trans: SessionRequestContext = DependsOnTrans,
|
||||
) -> IndexResponse:
|
||||
if q:
|
||||
assert page is not None
|
||||
assert page_size is not None
|
||||
search_results = search(trans, q, page, page_size)
|
||||
return RepositorySearchResults(**search_results)
|
||||
# See API notes - was added in https://github.com/galaxyproject/galaxy/pull/3626/files
|
||||
# but I think is currently unused. So probably we should just drop it until someone
|
||||
# complains.
|
||||
# elif params.tool_ids:
|
||||
# response = index_tool_ids(self.app, params.tool_ids)
|
||||
# return response
|
||||
else:
|
||||
repositories = index_repositories(self.app, name, owner, deleted or False)
|
||||
return [to_model(self.app, r) for r in repositories]
|
||||
|
||||
@router.get(
|
||||
"/api/repositories/get_repository_revision_install_info",
|
||||
description="Get information used by the install client to install this repository.",
|
||||
operation_id="repositories__legacy_install_info",
|
||||
)
|
||||
def legacy_install_info(
|
||||
self,
|
||||
trans: SessionRequestContext = DependsOnTrans,
|
||||
name: str = RequiredRepoNameParam,
|
||||
owner: str = RequiredRepoOwnerParam,
|
||||
changeset_revision: str = RequiredChangesetParam,
|
||||
) -> list:
|
||||
legacy_install_info = get_install_info(
|
||||
trans,
|
||||
name,
|
||||
owner,
|
||||
changeset_revision,
|
||||
)
|
||||
return list(legacy_install_info)
|
||||
|
||||
@router.get(
|
||||
"/api/repositories/install_info",
|
||||
description="Get information used by the install client to install this repository.",
|
||||
operation_id="repositories__install_info",
|
||||
)
|
||||
def install_info(
|
||||
self,
|
||||
trans: SessionRequestContext = DependsOnTrans,
|
||||
name: str = RequiredRepoNameParam,
|
||||
owner: str = RequiredRepoOwnerParam,
|
||||
changeset_revision: str = RequiredChangesetParam,
|
||||
) -> InstallInfo:
|
||||
# A less problematic version of the above API, but I guess we
|
||||
# need to maintain the older version for older Galaxy API clients
|
||||
# for... sometime... or forever.
|
||||
legacy_install_info = get_install_info(
|
||||
trans,
|
||||
name,
|
||||
owner,
|
||||
changeset_revision,
|
||||
)
|
||||
return from_legacy_install_info(legacy_install_info)
|
||||
|
||||
@router.get(
|
||||
"/api/repositories/{encoded_repository_id}/metadata",
|
||||
description="Get information about repository metadata",
|
||||
operation_id="repositories__metadata",
|
||||
# See comment below.
|
||||
# response_model=RepositoryMetadata,
|
||||
)
|
||||
def metadata(
|
||||
self,
|
||||
encoded_repository_id: str = RepositoryIdPathParam,
|
||||
downloadable_only: bool = DownloadableQueryParam,
|
||||
) -> dict:
|
||||
recursive = True
|
||||
as_dict = get_repository_metadata_dict(self.app, encoded_repository_id, recursive, downloadable_only)
|
||||
# fails 1020 if we try to use the model - I guess repository dependencies
|
||||
# are getting lost
|
||||
return as_dict
|
||||
# return _hack_fastapi_4428(as_dict)
|
||||
|
||||
@router.get(
|
||||
"/api/repositories/get_ordered_installable_revisions",
|
||||
description="Get an ordered list of the repository changeset revisions that are installable",
|
||||
operation_id="repositories__get_ordered_installable_revisions",
|
||||
)
|
||||
def get_ordered_installable_revisions(
|
||||
self,
|
||||
owner: Optional[str] = OptionalRepositoryOwnerParam,
|
||||
name: Optional[str] = OptionalRepositoryNameParam,
|
||||
tsr_id: Optional[str] = OptionalRepositoryIdParam,
|
||||
) -> List[str]:
|
||||
return get_ordered_installable_revisions(self.app, name, owner, tsr_id)
|
||||
|
||||
@router.post(
|
||||
"/api/repositories/reset_metadata_on_repository",
|
||||
description="reset metadata on a repository",
|
||||
operation_id="repositories__reset_legacy",
|
||||
)
|
||||
def reset_metadata_on_repository_legacy(
|
||||
self,
|
||||
trans: SessionRequestContext = DependsOnTrans,
|
||||
request: ResetMetadataOnRepositoryRequest = depend_on_either_json_or_form_data(
|
||||
ResetMetadataOnRepositoryRequest
|
||||
),
|
||||
) -> ResetMetadataOnRepositoryResponse:
|
||||
return reset_metadata_on_repository(trans, request.repository_id)
|
||||
|
||||
@router.post(
|
||||
"/api/repositories/{encoded_repository_id}/reset_metadata",
|
||||
description="reset metadata on a repository",
|
||||
operation_id="repositories__reset",
|
||||
)
|
||||
def reset_metadata_on_repository(
|
||||
self,
|
||||
trans: SessionRequestContext = DependsOnTrans,
|
||||
encoded_repository_id: str = RepositoryIdPathParam,
|
||||
) -> ResetMetadataOnRepositoryResponse:
|
||||
return reset_metadata_on_repository(trans, encoded_repository_id)
|
||||
|
||||
@router.get(
|
||||
"/api/repositories/updates",
|
||||
operation_id="repositories__update",
|
||||
)
|
||||
@router.get(
|
||||
"/api/repositories/updates/",
|
||||
)
|
||||
def updates(
|
||||
self,
|
||||
owner: Optional[str] = OptionalRepositoryOwnerParam,
|
||||
name: Optional[str] = OptionalRepositoryNameParam,
|
||||
changeset_revision: str = RequiredRepositoryChangesetRevisionParam,
|
||||
hexlify: Optional[bool] = OptionalHexlifyParam,
|
||||
):
|
||||
request = UpdatesRequest(
|
||||
name=name,
|
||||
owner=owner,
|
||||
changeset_revision=changeset_revision,
|
||||
hexlify=hexlify,
|
||||
)
|
||||
return Response(content=check_updates(self.app, request))
|
||||
|
||||
@router.post(
|
||||
"/api/repositories",
|
||||
description="create a new repository",
|
||||
operation_id="repositories__create",
|
||||
)
|
||||
def create(
|
||||
self,
|
||||
trans: SessionRequestContext = DependsOnTrans,
|
||||
request: CreateRepositoryRequest = Body(...),
|
||||
) -> Repository:
|
||||
db_repository = create_repository(
|
||||
trans,
|
||||
request,
|
||||
)
|
||||
return to_model(self.app, db_repository)
|
||||
|
||||
@router.get(
|
||||
"/api/repositories/{encoded_repository_id}",
|
||||
operation_id="repositories__show",
|
||||
)
|
||||
def show(
|
||||
self,
|
||||
encoded_repository_id: str = RepositoryIdPathParam,
|
||||
) -> DetailedRepository:
|
||||
repository = get_repository_in_tool_shed(self.app, encoded_repository_id)
|
||||
return to_detailed_model(self.app, repository)
|
||||
|
||||
@router.get(
|
||||
"/api/repositories/{encoded_repository_id}/permissions",
|
||||
operation_id="repositories__permissions",
|
||||
)
|
||||
def permissions(
|
||||
self,
|
||||
trans: SessionRequestContext = DependsOnTrans,
|
||||
encoded_repository_id: str = RepositoryIdPathParam,
|
||||
) -> RepositoryPermissions:
|
||||
repository = get_repository_in_tool_shed(self.app, encoded_repository_id)
|
||||
if not can_update_repo(trans, repository):
|
||||
raise InsufficientPermissionsException(
|
||||
"You do not have permission to inspect repository repository permissions."
|
||||
)
|
||||
return RepositoryPermissions(
|
||||
allow_push=trans.app.security_agent.usernames_that_can_push(repository),
|
||||
can_manage=can_manage_repo(trans, repository),
|
||||
can_push=can_update_repo(trans, repository),
|
||||
)
|
||||
|
||||
@router.get(
|
||||
"/api/repositories/{encoded_repository_id}/allow_push",
|
||||
operation_id="repositories__show_allow_push",
|
||||
)
|
||||
def show_allow_push(
|
||||
self,
|
||||
trans: SessionRequestContext = DependsOnTrans,
|
||||
encoded_repository_id: str = RepositoryIdPathParam,
|
||||
) -> List[str]:
|
||||
repository = get_repository_in_tool_shed(self.app, encoded_repository_id)
|
||||
if not can_manage_repo(trans, repository):
|
||||
raise InsufficientPermissionsException("You do not have permission to update this repository.")
|
||||
return trans.app.security_agent.usernames_that_can_push(repository)
|
||||
|
||||
@router.post(
|
||||
"/api/repositories/{encoded_repository_id}/allow_push/{username}",
|
||||
operation_id="repositories__add_allow_push",
|
||||
)
|
||||
def add_allow_push(
|
||||
self,
|
||||
trans: SessionRequestContext = DependsOnTrans,
|
||||
encoded_repository_id: str = RepositoryIdPathParam,
|
||||
username: str = UsernameIdPathParam,
|
||||
) -> List[str]:
|
||||
repository = get_repository_in_tool_shed(self.app, encoded_repository_id)
|
||||
if not can_manage_repo(trans, repository):
|
||||
raise InsufficientPermissionsException("You do not have permission to update this repository.")
|
||||
repository.set_allow_push([username])
|
||||
return trans.app.security_agent.usernames_that_can_push(repository)
|
||||
|
||||
@router.put(
|
||||
"/api/repositories/{encoded_repository_id}/revisions/{changeset_revision}/malicious",
|
||||
operation_id="repositories__set_malicious",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
)
|
||||
def set_malicious(
|
||||
self,
|
||||
trans: SessionRequestContext = DependsOnTrans,
|
||||
encoded_repository_id: str = RepositoryIdPathParam,
|
||||
changeset_revision: str = ChangesetRevisionPathParam,
|
||||
):
|
||||
repository_metadata = get_repository_metadata_for_management(trans, encoded_repository_id, changeset_revision)
|
||||
repository_metadata.malicious = True
|
||||
trans.sa_session.add(repository_metadata)
|
||||
trans.sa_session.flush()
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@router.delete(
|
||||
"/api/repositories/{encoded_repository_id}/revisions/{changeset_revision}/malicious",
|
||||
operation_id="repositories__unset_malicious",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
)
|
||||
def unset_malicious(
|
||||
self,
|
||||
trans: SessionRequestContext = DependsOnTrans,
|
||||
encoded_repository_id: str = RepositoryIdPathParam,
|
||||
changeset_revision: str = ChangesetRevisionPathParam,
|
||||
):
|
||||
repository_metadata = get_repository_metadata_for_management(trans, encoded_repository_id, changeset_revision)
|
||||
repository_metadata.malicious = False
|
||||
trans.sa_session.add(repository_metadata)
|
||||
trans.sa_session.flush()
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@router.put(
|
||||
"/api/repositories/{encoded_repository_id}/deprecated",
|
||||
operation_id="repositories__set_deprecated",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
)
|
||||
def set_deprecated(
|
||||
self,
|
||||
trans: SessionRequestContext = DependsOnTrans,
|
||||
encoded_repository_id: str = RepositoryIdPathParam,
|
||||
):
|
||||
repository = get_repository_in_tool_shed(self.app, encoded_repository_id)
|
||||
if not can_manage_repo(trans, repository):
|
||||
raise InsufficientPermissionsException("You do not have permission to update this repository.")
|
||||
repository.deprecated = True
|
||||
trans.sa_session.add(repository)
|
||||
trans.sa_session.flush()
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@router.delete(
|
||||
"/api/repositories/{encoded_repository_id}/deprecated",
|
||||
operation_id="repositories__unset_deprecated",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
)
|
||||
def unset_deprecated(
|
||||
self,
|
||||
trans: SessionRequestContext = DependsOnTrans,
|
||||
encoded_repository_id: str = RepositoryIdPathParam,
|
||||
):
|
||||
repository = get_repository_in_tool_shed(self.app, encoded_repository_id)
|
||||
if not can_manage_repo(trans, repository):
|
||||
raise InsufficientPermissionsException("You do not have permission to update this repository.")
|
||||
repository.deprecated = False
|
||||
trans.sa_session.add(repository)
|
||||
trans.sa_session.flush()
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@router.delete(
|
||||
"/api/repositories/{encoded_repository_id}/allow_push/{username}",
|
||||
operation_id="repositories__remove_allow_push",
|
||||
)
|
||||
def remove_allow_push(
|
||||
self,
|
||||
trans: SessionRequestContext = DependsOnTrans,
|
||||
encoded_repository_id: str = RepositoryIdPathParam,
|
||||
username: str = UsernameIdPathParam,
|
||||
) -> List[str]:
|
||||
repository = get_repository_in_tool_shed(self.app, encoded_repository_id)
|
||||
if not can_manage_repo(trans, repository):
|
||||
raise InsufficientPermissionsException("You do not have permission to update this repository.")
|
||||
repository.set_allow_push(None, remove_auth=username)
|
||||
return trans.app.security_agent.usernames_that_can_push(repository)
|
||||
|
||||
@router.post(
|
||||
"/api/repositories/{encoded_repository_id}/changeset_revision",
|
||||
description="upload new revision to the repository",
|
||||
operation_id="repositories__create_revision",
|
||||
)
|
||||
async def create_changeset_revision(
|
||||
self,
|
||||
request: Request,
|
||||
encoded_repository_id: str = RepositoryIdPathParam,
|
||||
commit_message: Optional[str] = CommitMessageQueryParam,
|
||||
trans: SessionRequestContext = DependsOnTrans,
|
||||
files: Optional[List[UploadFile]] = None,
|
||||
revision_request: RepositoryUpdateRequest = Depends(RepositoryUpdateRequestFormData.as_form), # type: ignore[attr-defined]
|
||||
) -> RepositoryUpdate:
|
||||
try:
|
||||
# Code stolen from Marius' work in Galaxy's Tools API.
|
||||
|
||||
files2: List[StarletteUploadFile] = cast(List[StarletteUploadFile], files or [])
|
||||
# FastAPI's UploadFile is a very light wrapper around starlette's UploadFile
|
||||
if not files2:
|
||||
data = await request.form()
|
||||
for value in data.values():
|
||||
if isinstance(value, StarletteUploadFile):
|
||||
files2.append(value)
|
||||
|
||||
repository = get_repository_in_tool_shed(self.app, encoded_repository_id)
|
||||
|
||||
if not can_update_repo(trans, repository):
|
||||
raise InsufficientPermissionsException("You do not have permission to update this repository.")
|
||||
|
||||
assert trans.user
|
||||
assert files2
|
||||
the_file = files2[0]
|
||||
with tempfile.NamedTemporaryFile(
|
||||
dir=trans.app.config.new_file_path, prefix="upload_file_data_", delete=False
|
||||
) as dest:
|
||||
upload_file_like: IO[bytes] = the_file.file
|
||||
shutil.copyfileobj(upload_file_like, dest) # type: ignore[misc] # https://github.com/python/mypy/issues/15031
|
||||
the_file.file.close()
|
||||
filename = dest.name
|
||||
try:
|
||||
message = upload_tar_and_set_metadata(
|
||||
trans,
|
||||
trans.request.host,
|
||||
repository,
|
||||
filename,
|
||||
commit_message or revision_request.commit_message or "Uploaded",
|
||||
)
|
||||
return RepositoryUpdate(__root__=ValidRepostiroyUpdateMessage(message=message))
|
||||
finally:
|
||||
if os.path.exists(filename):
|
||||
os.remove(filename)
|
||||
except Exception:
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.exception("Problem in here...")
|
||||
raise
|
||||
|
||||
@router.get(
|
||||
"/api/repositories/{encoded_repository_id}/revisions/{changeset_revision}/readmes",
|
||||
description="fetch readmes for repository revision",
|
||||
operation_id="repositories__readmes",
|
||||
response_model=RepositoryRevisionReadmes,
|
||||
)
|
||||
def get_readmes(
|
||||
self,
|
||||
encoded_repository_id: str = RepositoryIdPathParam,
|
||||
changeset_revision: str = ChangesetRevisionPathParam,
|
||||
) -> dict:
|
||||
repository = get_repository_in_tool_shed(self.app, encoded_repository_id)
|
||||
return readmes(self.app, repository, changeset_revision)
|
||||
|
||||
|
||||
def _hack_fastapi_4428(as_dict) -> dict:
|
||||
# https://github.com/tiangolo/fastapi/pull/4428#issuecomment-1145429263
|
||||
# after pydantic2 swap to really returning the object
|
||||
return RepositoryMetadata(__root__=as_dict).dict()["__root__"]
|
||||
@@ -0,0 +1,55 @@
|
||||
from tool_shed.context import SessionRequestContext
|
||||
from tool_shed.managers.tools import search
|
||||
from tool_shed.structured_app import ToolShedApp
|
||||
from tool_shed.util.shed_index import build_index
|
||||
from tool_shed_client.schema import BuildSearchIndexResponse
|
||||
from . import (
|
||||
depends,
|
||||
DependsOnTrans,
|
||||
RepositorySearchPageQueryParam,
|
||||
RepositorySearchPageSizeQueryParam,
|
||||
Router,
|
||||
ToolsIndexQueryParam,
|
||||
)
|
||||
|
||||
router = Router(tags=["tools"])
|
||||
|
||||
|
||||
@router.cbv
|
||||
class FastAPITools:
|
||||
app: ToolShedApp = depends(ToolShedApp)
|
||||
|
||||
@router.get(
|
||||
"/api/tools",
|
||||
operation_id="tools__index",
|
||||
)
|
||||
def index(
|
||||
self,
|
||||
q: str = ToolsIndexQueryParam,
|
||||
page: int = RepositorySearchPageQueryParam,
|
||||
page_size: int = RepositorySearchPageSizeQueryParam,
|
||||
trans: SessionRequestContext = DependsOnTrans,
|
||||
):
|
||||
search_results = search(trans, q, page, page_size)
|
||||
return search_results
|
||||
|
||||
@router.put(
|
||||
"/api/tools/build_search_index",
|
||||
operation_id="tools__build_search_index",
|
||||
require_admin=True,
|
||||
)
|
||||
def build_search_index(self) -> BuildSearchIndexResponse:
|
||||
"""Not part of the stable API, just something to simplify
|
||||
bootstrapping tool sheds, scripting, testing, etc...
|
||||
"""
|
||||
config = self.app.config
|
||||
repos_indexed, tools_indexed = build_index(
|
||||
config.whoosh_index_dir,
|
||||
config.file_path,
|
||||
config.hgweb_config_dir,
|
||||
config.database_connection,
|
||||
)
|
||||
return BuildSearchIndexResponse(
|
||||
repositories_indexed=repos_indexed,
|
||||
tools_indexed=tools_indexed,
|
||||
)
|
||||
@@ -0,0 +1,130 @@
|
||||
from typing import (
|
||||
List,
|
||||
Optional,
|
||||
)
|
||||
|
||||
from fastapi import (
|
||||
Body,
|
||||
Response,
|
||||
status,
|
||||
)
|
||||
from pydantic import BaseModel
|
||||
|
||||
import tool_shed.util.shed_util_common as suc
|
||||
from galaxy.exceptions import (
|
||||
InsufficientPermissionsException,
|
||||
ObjectNotFound,
|
||||
RequestParameterInvalidException,
|
||||
)
|
||||
from galaxy.managers.api_keys import ApiKeyManager
|
||||
from tool_shed.context import SessionRequestContext
|
||||
from tool_shed.managers.users import (
|
||||
api_create_user,
|
||||
get_api_user,
|
||||
index,
|
||||
)
|
||||
from tool_shed_client.schema import (
|
||||
CreateUserRequest,
|
||||
User,
|
||||
)
|
||||
from . import (
|
||||
depends,
|
||||
DependsOnTrans,
|
||||
Router,
|
||||
UserIdPathParam,
|
||||
)
|
||||
|
||||
router = Router(tags=["users"])
|
||||
|
||||
|
||||
@router.cbv
|
||||
class FastAPIUsers:
|
||||
api_key_manager: ApiKeyManager = depends(ApiKeyManager)
|
||||
|
||||
@router.get(
|
||||
"/api/users",
|
||||
description="index users",
|
||||
operation_id="users__index",
|
||||
)
|
||||
def index(self, trans: SessionRequestContext = DependsOnTrans) -> List[User]:
|
||||
deleted = False
|
||||
return index(trans.app, deleted)
|
||||
|
||||
@router.post(
|
||||
"/api/users",
|
||||
description="create a user",
|
||||
operation_id="users__create",
|
||||
require_admin=True,
|
||||
)
|
||||
def create(self, trans: SessionRequestContext = DependsOnTrans, request: CreateUserRequest = Body(...)) -> User:
|
||||
return api_create_user(trans, request)
|
||||
|
||||
@router.get(
|
||||
"/api/users/current",
|
||||
description="show current user",
|
||||
operation_id="users__current",
|
||||
)
|
||||
def current(self, trans: SessionRequestContext = DependsOnTrans) -> User:
|
||||
user = trans.user
|
||||
assert user
|
||||
return get_api_user(trans.app, user)
|
||||
|
||||
@router.get(
|
||||
"/api/users/{encoded_user_id}",
|
||||
description="show a user",
|
||||
operation_id="users__show",
|
||||
)
|
||||
def show(self, trans: SessionRequestContext = DependsOnTrans, encoded_user_id: str = UserIdPathParam) -> User:
|
||||
user = suc.get_user(trans.app, encoded_user_id)
|
||||
if user is None:
|
||||
raise ObjectNotFound()
|
||||
return get_api_user(trans.app, user)
|
||||
|
||||
@router.get(
|
||||
"/api/users/{encoded_user_id}/api_key",
|
||||
name="get_or_create_api_key",
|
||||
summary="Return the user's API key",
|
||||
operation_id="users__get_or_create_api_key",
|
||||
)
|
||||
def get_or_create_api_key(
|
||||
self, trans: SessionRequestContext = DependsOnTrans, encoded_user_id: str = UserIdPathParam
|
||||
) -> str:
|
||||
user = self._get_user(trans, encoded_user_id)
|
||||
return self.api_key_manager.get_or_create_api_key(user)
|
||||
|
||||
@router.post(
|
||||
"/api/users/{encoded_user_id}/api_key",
|
||||
summary="Creates a new API key for the user",
|
||||
operation_id="users__create_api_key",
|
||||
)
|
||||
def create_api_key(
|
||||
self, trans: SessionRequestContext = DependsOnTrans, encoded_user_id: str = UserIdPathParam
|
||||
) -> str:
|
||||
user = self._get_user(trans, encoded_user_id)
|
||||
return self.api_key_manager.create_api_key(user).key
|
||||
|
||||
@router.delete(
|
||||
"/api/users/{encoded_user_id}/api_key",
|
||||
summary="Delete the current API key of the user",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
operation_id="users__delete_api_key",
|
||||
)
|
||||
def delete_api_key(
|
||||
self,
|
||||
trans: SessionRequestContext = DependsOnTrans,
|
||||
encoded_user_id: str = UserIdPathParam,
|
||||
):
|
||||
user = self._get_user(trans, encoded_user_id)
|
||||
self.api_key_manager.delete_api_key(user)
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def _get_user(self, trans: SessionRequestContext, encoded_user_id: str):
|
||||
if encoded_user_id == "current":
|
||||
user = trans.user
|
||||
else:
|
||||
user = suc.get_user(trans.app, encoded_user_id)
|
||||
if user is None:
|
||||
raise ObjectNotFound()
|
||||
if not (trans.user_is_admin or trans.user == user):
|
||||
raise InsufficientPermissionsException()
|
||||
return user
|
||||
@@ -1,7 +1,10 @@
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
from typing import Any
|
||||
from typing import (
|
||||
Any,
|
||||
Optional,
|
||||
)
|
||||
|
||||
from sqlalchemy.orm.scoping import scoped_session
|
||||
|
||||
@@ -108,3 +111,7 @@ class UniverseApplication(ToolShedApp, SentryClientMixin, HaltableContainer):
|
||||
# used for cachebusting -- refactor this into a *SINGLE* UniverseApplication base.
|
||||
self.server_starttime = int(time.time())
|
||||
log.debug("Tool shed hgweb.config file is: %s", self.hgweb_config_manager.hgweb_config)
|
||||
|
||||
|
||||
# Global instance of the universe app.
|
||||
app: Optional[ToolShedApp] = None
|
||||
|
||||
+145
-120
@@ -13,20 +13,36 @@ from routes.middleware import RoutesMiddleware
|
||||
|
||||
import galaxy.webapps.base.webapp
|
||||
from galaxy import util
|
||||
from galaxy.structured_app import BasicSharedApp
|
||||
from galaxy.util import asbool
|
||||
from galaxy.util.properties import load_app_properties
|
||||
from galaxy.web import url_for
|
||||
from galaxy.web.framework.middleware.error import ErrorMiddleware
|
||||
from galaxy.web.framework.middleware.request_id import RequestIDMiddleware
|
||||
from galaxy.web.framework.middleware.xforwardedhost import XForwardedHostMiddleware
|
||||
from galaxy.webapps.base.webapp import build_url_map
|
||||
from galaxy.webapps.base.webapp import (
|
||||
build_url_map,
|
||||
GalaxyWebTransaction,
|
||||
)
|
||||
from galaxy.webapps.util import wrap_if_allowed
|
||||
|
||||
SHED_API_VERSION = os.environ.get("TOOL_SHED_API_VERSION", "v1")
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ToolShedGalaxyWebTransaction(GalaxyWebTransaction):
|
||||
@property
|
||||
def repositories_hostname(self) -> str:
|
||||
return url_for("/", qualified=True).rstrip("/")
|
||||
|
||||
|
||||
class CommunityWebApplication(galaxy.webapps.base.webapp.WebApplication):
|
||||
injection_aware: bool = True
|
||||
|
||||
def transaction_chooser(self, environ, galaxy_app: BasicSharedApp, session_cookie: str):
|
||||
return ToolShedGalaxyWebTransaction(environ, galaxy_app, self, session_cookie)
|
||||
|
||||
|
||||
def add_ui_controllers(webapp, app):
|
||||
"""
|
||||
@@ -65,11 +81,15 @@ def app_pair(global_conf, load_app_kwds=None, **kwargs):
|
||||
kwargs = load_app_properties(kwds=kwargs, config_prefix="TOOL_SHED_CONFIG_", **load_app_kwds)
|
||||
if "app" in kwargs:
|
||||
app = kwargs.pop("app")
|
||||
import tool_shed.webapp.app
|
||||
|
||||
tool_shed.webapp.app.app = app
|
||||
else:
|
||||
try:
|
||||
from tool_shed.webapp.app import UniverseApplication
|
||||
import tool_shed.webapp.app
|
||||
|
||||
app = UniverseApplication(global_conf=global_conf, **kwargs)
|
||||
app = tool_shed.webapp.app.UniverseApplication(global_conf=global_conf, **kwargs)
|
||||
tool_shed.webapp.app.app = app
|
||||
except Exception:
|
||||
import sys
|
||||
import traceback
|
||||
@@ -98,123 +118,128 @@ def app_pair(global_conf, load_app_kwds=None, **kwargs):
|
||||
# Enable 'hg clone' functionality on repos by letting hgwebapp handle the request
|
||||
webapp.add_route("/repos/*path_info", controller="hg", action="handle_request", path_info="/")
|
||||
# Add the web API. # A good resource for RESTful services - https://routes.readthedocs.io/en/latest/restful.html
|
||||
webapp.add_api_controllers("tool_shed.webapp.api", app)
|
||||
webapp.mapper.connect(
|
||||
"api_key_retrieval",
|
||||
"/api/authenticate/baseauth/",
|
||||
controller="authenticate",
|
||||
action="get_tool_shed_api_key",
|
||||
conditions=dict(method=["GET"]),
|
||||
)
|
||||
webapp.mapper.connect("group", "/api/groups/", controller="groups", action="index", conditions=dict(method=["GET"]))
|
||||
webapp.mapper.connect(
|
||||
"group", "/api/groups/", controller="groups", action="create", conditions=dict(method=["POST"])
|
||||
)
|
||||
webapp.mapper.connect(
|
||||
"group", "/api/groups/{encoded_id}", controller="groups", action="show", conditions=dict(method=["GET"])
|
||||
)
|
||||
webapp.mapper.resource(
|
||||
"category",
|
||||
"categories",
|
||||
controller="categories",
|
||||
name_prefix="category_",
|
||||
path_prefix="/api",
|
||||
parent_resources=dict(member_name="category", collection_name="categories"),
|
||||
)
|
||||
webapp.mapper.connect(
|
||||
"repositories_in_category",
|
||||
"/api/categories/{category_id}/repositories",
|
||||
controller="categories",
|
||||
action="get_repositories",
|
||||
conditions=dict(method=["GET"]),
|
||||
)
|
||||
webapp.mapper.connect(
|
||||
"show_updates_for_repository",
|
||||
"/api/repositories/updates",
|
||||
controller="repositories",
|
||||
action="updates",
|
||||
conditions=dict(method=["GET"]),
|
||||
)
|
||||
webapp.mapper.resource(
|
||||
"repository",
|
||||
"repositories",
|
||||
controller="repositories",
|
||||
collection={
|
||||
"add_repository_registry_entry": "POST",
|
||||
"get_repository_revision_install_info": "GET",
|
||||
"get_ordered_installable_revisions": "GET",
|
||||
"get_installable_revisions": "GET",
|
||||
"remove_repository_registry_entry": "POST",
|
||||
"reset_metadata_on_repositories": "POST",
|
||||
"reset_metadata_on_repository": "POST",
|
||||
},
|
||||
name_prefix="repository_",
|
||||
path_prefix="/api",
|
||||
parent_resources=dict(member_name="repository", collection_name="repositories"),
|
||||
)
|
||||
webapp.mapper.resource(
|
||||
"repository_revision",
|
||||
"repository_revisions",
|
||||
member={"repository_dependencies": "GET", "export": "POST"},
|
||||
controller="repository_revisions",
|
||||
name_prefix="repository_revision_",
|
||||
path_prefix="/api",
|
||||
parent_resources=dict(member_name="repository_revision", collection_name="repository_revisions"),
|
||||
)
|
||||
webapp.mapper.resource(
|
||||
"user",
|
||||
"users",
|
||||
controller="users",
|
||||
name_prefix="user_",
|
||||
path_prefix="/api",
|
||||
parent_resources=dict(member_name="user", collection_name="users"),
|
||||
)
|
||||
webapp.mapper.connect(
|
||||
"update_repository",
|
||||
"/api/repositories/{id}",
|
||||
controller="repositories",
|
||||
action="update",
|
||||
conditions=dict(method=["PATCH", "PUT"]),
|
||||
)
|
||||
webapp.mapper.connect(
|
||||
"repository_create_changeset_revision",
|
||||
"/api/repositories/{id}/changeset_revision",
|
||||
controller="repositories",
|
||||
action="create_changeset_revision",
|
||||
conditions=dict(method=["POST"]),
|
||||
)
|
||||
webapp.mapper.connect(
|
||||
"repository_get_metadata",
|
||||
"/api/repositories/{id}/metadata",
|
||||
controller="repositories",
|
||||
action="metadata",
|
||||
conditions=dict(method=["GET"]),
|
||||
)
|
||||
webapp.mapper.connect(
|
||||
"repository_show_tools",
|
||||
"/api/repositories/{id}/{changeset}/show_tools",
|
||||
controller="repositories",
|
||||
action="show_tools",
|
||||
conditions=dict(method=["GET"]),
|
||||
)
|
||||
webapp.mapper.connect(
|
||||
"create_repository",
|
||||
"/api/repositories",
|
||||
controller="repositories",
|
||||
action="create",
|
||||
conditions=dict(method=["POST"]),
|
||||
)
|
||||
webapp.mapper.connect(
|
||||
"tools",
|
||||
"/api/tools/build_search_index",
|
||||
controller="tools",
|
||||
action="build_search_index",
|
||||
conditions=dict(method=["PUT"]),
|
||||
)
|
||||
webapp.mapper.connect("tools", "/api/tools", controller="tools", action="index", conditions=dict(method=["GET"]))
|
||||
webapp.mapper.connect(
|
||||
"version", "/api/version", controller="configuration", action="version", conditions=dict(method=["GET"])
|
||||
)
|
||||
if SHED_API_VERSION == "v1":
|
||||
webapp.add_api_controllers("tool_shed.webapp.api", app)
|
||||
webapp.mapper.connect(
|
||||
"api_key_retrieval",
|
||||
"/api/authenticate/baseauth/",
|
||||
controller="authenticate",
|
||||
action="get_tool_shed_api_key",
|
||||
conditions=dict(method=["GET"]),
|
||||
)
|
||||
webapp.mapper.connect(
|
||||
"group", "/api/groups/", controller="groups", action="index", conditions=dict(method=["GET"])
|
||||
)
|
||||
webapp.mapper.connect(
|
||||
"group", "/api/groups/", controller="groups", action="create", conditions=dict(method=["POST"])
|
||||
)
|
||||
webapp.mapper.connect(
|
||||
"group", "/api/groups/{encoded_id}", controller="groups", action="show", conditions=dict(method=["GET"])
|
||||
)
|
||||
webapp.mapper.resource(
|
||||
"category",
|
||||
"categories",
|
||||
controller="categories",
|
||||
name_prefix="category_",
|
||||
path_prefix="/api",
|
||||
parent_resources=dict(member_name="category", collection_name="categories"),
|
||||
)
|
||||
webapp.mapper.connect(
|
||||
"repositories_in_category",
|
||||
"/api/categories/{category_id}/repositories",
|
||||
controller="categories",
|
||||
action="get_repositories",
|
||||
conditions=dict(method=["GET"]),
|
||||
)
|
||||
webapp.mapper.connect(
|
||||
"show_updates_for_repository",
|
||||
"/api/repositories/updates",
|
||||
controller="repositories",
|
||||
action="updates",
|
||||
conditions=dict(method=["GET"]),
|
||||
)
|
||||
webapp.mapper.resource(
|
||||
"repository",
|
||||
"repositories",
|
||||
controller="repositories",
|
||||
collection={
|
||||
"add_repository_registry_entry": "POST",
|
||||
"get_repository_revision_install_info": "GET",
|
||||
"get_ordered_installable_revisions": "GET",
|
||||
"get_installable_revisions": "GET",
|
||||
"remove_repository_registry_entry": "POST",
|
||||
"reset_metadata_on_repositories": "POST",
|
||||
"reset_metadata_on_repository": "POST",
|
||||
},
|
||||
name_prefix="repository_",
|
||||
path_prefix="/api",
|
||||
parent_resources=dict(member_name="repository", collection_name="repositories"),
|
||||
)
|
||||
webapp.mapper.resource(
|
||||
"repository_revision",
|
||||
"repository_revisions",
|
||||
member={"repository_dependencies": "GET", "export": "POST"},
|
||||
controller="repository_revisions",
|
||||
name_prefix="repository_revision_",
|
||||
path_prefix="/api",
|
||||
parent_resources=dict(member_name="repository_revision", collection_name="repository_revisions"),
|
||||
)
|
||||
webapp.mapper.resource(
|
||||
"user",
|
||||
"users",
|
||||
controller="users",
|
||||
name_prefix="user_",
|
||||
path_prefix="/api",
|
||||
parent_resources=dict(member_name="user", collection_name="users"),
|
||||
)
|
||||
webapp.mapper.connect(
|
||||
"update_repository",
|
||||
"/api/repositories/{id}",
|
||||
controller="repositories",
|
||||
action="update",
|
||||
conditions=dict(method=["PATCH", "PUT"]),
|
||||
)
|
||||
webapp.mapper.connect(
|
||||
"repository_create_changeset_revision",
|
||||
"/api/repositories/{id}/changeset_revision",
|
||||
controller="repositories",
|
||||
action="create_changeset_revision",
|
||||
conditions=dict(method=["POST"]),
|
||||
)
|
||||
webapp.mapper.connect(
|
||||
"repository_get_metadata",
|
||||
"/api/repositories/{id}/metadata",
|
||||
controller="repositories",
|
||||
action="metadata",
|
||||
conditions=dict(method=["GET"]),
|
||||
)
|
||||
webapp.mapper.connect(
|
||||
"repository_show_tools",
|
||||
"/api/repositories/{id}/{changeset}/show_tools",
|
||||
controller="repositories",
|
||||
action="show_tools",
|
||||
conditions=dict(method=["GET"]),
|
||||
)
|
||||
webapp.mapper.connect(
|
||||
"create_repository",
|
||||
"/api/repositories",
|
||||
controller="repositories",
|
||||
action="create",
|
||||
conditions=dict(method=["POST"]),
|
||||
)
|
||||
webapp.mapper.connect(
|
||||
"tools",
|
||||
"/api/tools/build_search_index",
|
||||
controller="tools",
|
||||
action="build_search_index",
|
||||
conditions=dict(method=["PUT"]),
|
||||
)
|
||||
webapp.mapper.connect(
|
||||
"tools", "/api/tools", controller="tools", action="index", conditions=dict(method=["GET"])
|
||||
)
|
||||
webapp.mapper.connect(
|
||||
"version", "/api/version", controller="configuration", action="version", conditions=dict(method=["GET"])
|
||||
)
|
||||
|
||||
webapp.finalize_config()
|
||||
# Wrap the webapp in some useful middleware
|
||||
|
||||
@@ -362,7 +362,7 @@ class AdminController(BaseUIController, Admin):
|
||||
@web.expose
|
||||
@web.require_admin
|
||||
def reset_metadata_on_selected_repositories_in_tool_shed(self, trans, **kwd):
|
||||
rmm = repository_metadata_manager.RepositoryMetadataManager(trans.app, trans.user)
|
||||
rmm = repository_metadata_manager.RepositoryMetadataManager(trans)
|
||||
if "reset_metadata_on_selected_repositories_button" in kwd:
|
||||
message, status = rmm.reset_metadata_on_selected_repositories(**kwd)
|
||||
else:
|
||||
|
||||
@@ -1235,7 +1235,7 @@ class RepositoryController(BaseUIController, ratings_util.ItemRatings):
|
||||
cur_includes_tools_for_display_in_tool_panel,
|
||||
cur_has_repository_dependencies,
|
||||
cur_has_repository_dependencies_only_if_compiling_contained_td,
|
||||
) = repository_util.get_repo_info_dict(trans.app, trans.user, repository_id, changeset_revision)
|
||||
) = repository_util.get_repo_info_dict(trans, repository_id, changeset_revision)
|
||||
if cur_has_repository_dependencies and not has_repository_dependencies:
|
||||
has_repository_dependencies = True
|
||||
if (
|
||||
@@ -2119,7 +2119,7 @@ class RepositoryController(BaseUIController, ratings_util.ItemRatings):
|
||||
# This method is called only from the ~/templates/webapps/tool_shed/repository/manage_repository.mako template.
|
||||
repository = repository_util.get_repository_in_tool_shed(trans.app, id)
|
||||
rmm = repository_metadata_manager.RepositoryMetadataManager(
|
||||
app=trans.app, user=trans.user, repository=repository, resetting_all_metadata_on_repository=True
|
||||
trans, repository=repository, resetting_all_metadata_on_repository=True
|
||||
)
|
||||
rmm.reset_all_metadata_on_repository_in_tool_shed()
|
||||
rmm_metadata_dict = rmm.get_metadata_dict()
|
||||
@@ -2138,9 +2138,7 @@ class RepositoryController(BaseUIController, ratings_util.ItemRatings):
|
||||
|
||||
@web.expose
|
||||
def reset_metadata_on_my_writable_repositories_in_tool_shed(self, trans, **kwd):
|
||||
rmm = repository_metadata_manager.RepositoryMetadataManager(
|
||||
trans.app, trans.user, resetting_all_metadata_on_repository=True
|
||||
)
|
||||
rmm = repository_metadata_manager.RepositoryMetadataManager(trans, resetting_all_metadata_on_repository=True)
|
||||
if "reset_metadata_on_selected_repositories_button" in kwd:
|
||||
message, status = rmm.reset_metadata_on_selected_repositories(**kwd)
|
||||
else:
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
from typing import (
|
||||
Any,
|
||||
Dict,
|
||||
)
|
||||
|
||||
from a2wsgi import WSGIMiddleware
|
||||
from fastapi import FastAPI
|
||||
|
||||
@@ -6,25 +11,72 @@ from galaxy.webapps.base.api import (
|
||||
add_request_id_middleware,
|
||||
include_all_package_routers,
|
||||
)
|
||||
from galaxy.webapps.openapi.utils import get_openapi
|
||||
|
||||
api_tags_metadata = [
|
||||
{
|
||||
"name": "authenticate",
|
||||
"description": "Authentication-related endpoints.",
|
||||
},
|
||||
{
|
||||
"name": "categories",
|
||||
"description": "Category-related endpoints.",
|
||||
},
|
||||
{
|
||||
"name": "repositories",
|
||||
"description": "Repository-related endpoints.",
|
||||
},
|
||||
{
|
||||
"name": "users",
|
||||
"description": "User-related endpoints.",
|
||||
},
|
||||
{"name": "undocumented", "description": "API routes that have not yet been ported to FastAPI."},
|
||||
]
|
||||
|
||||
|
||||
def initialize_fast_app(gx_webapp, tool_shed_app):
|
||||
app = FastAPI(
|
||||
title="Galaxy Tool Shed API",
|
||||
description=("This API allows you to manage the Tool Shed repositories."),
|
||||
docs_url="/api/docs",
|
||||
redoc_url="/api/redoc",
|
||||
)
|
||||
app = get_fastapi_instance()
|
||||
add_exception_handler(app)
|
||||
add_request_id_middleware(app)
|
||||
include_all_package_routers(app, "tool_shed.webapp.api")
|
||||
from .buildapp import SHED_API_VERSION
|
||||
|
||||
routes_package = "tool_shed.webapp.api" if SHED_API_VERSION == "v1" else "tool_shed.webapp.api2"
|
||||
include_all_package_routers(app, routes_package)
|
||||
wsgi_handler = WSGIMiddleware(gx_webapp)
|
||||
tool_shed_app.haltables.append(("WSGI Middleware threadpool", wsgi_handler.executor.shutdown))
|
||||
app.mount("/", wsgi_handler)
|
||||
return app
|
||||
|
||||
|
||||
def get_fastapi_instance() -> FastAPI:
|
||||
return FastAPI(
|
||||
title="Galaxy Tool Shed API",
|
||||
description=("This API allows you to manage the Tool Shed repositories."),
|
||||
docs_url="/api/docs",
|
||||
redoc_url="/api/redoc",
|
||||
tags=api_tags_metadata,
|
||||
license_info={"name": "MIT", "url": "https://github.com/galaxyproject/galaxy/blob/dev/LICENSE.txt"},
|
||||
)
|
||||
|
||||
|
||||
def get_openapi_schema() -> Dict[str, Any]:
|
||||
"""
|
||||
Dumps openAPI schema without starting a full app and webserver.
|
||||
"""
|
||||
app = get_fastapi_instance()
|
||||
include_all_package_routers(app, "tool_shed.webapp.api2")
|
||||
return get_openapi(
|
||||
title=app.title,
|
||||
version=app.version,
|
||||
openapi_version="3.1.0",
|
||||
description=app.description,
|
||||
routes=app.routes,
|
||||
license_info=app.license_info,
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"add_request_id_middleware",
|
||||
"get_openapi_schema",
|
||||
"initialize_fast_app",
|
||||
)
|
||||
|
||||
@@ -399,6 +399,7 @@ class Repository(Base, Dictifiable):
|
||||
"times_downloaded",
|
||||
"deprecated",
|
||||
"create_time",
|
||||
"update_time",
|
||||
]
|
||||
dict_element_visible_keys = [
|
||||
"id",
|
||||
@@ -414,6 +415,7 @@ class Repository(Base, Dictifiable):
|
||||
"times_downloaded",
|
||||
"deprecated",
|
||||
"create_time",
|
||||
"update_time",
|
||||
]
|
||||
file_states = Bunch(NORMAL="n", NEEDS_MERGING="m", MARKED_FOR_REMOVAL="r", MARKED_FOR_ADDITION="a", NOT_TRACKED="?")
|
||||
|
||||
|
||||
@@ -33,6 +33,17 @@ class Repository(BaseModel):
|
||||
times_downloaded: int
|
||||
deprecated: bool
|
||||
create_time: str
|
||||
update_time: str
|
||||
|
||||
|
||||
class DetailedRepository(Repository):
|
||||
long_description: Optional[str]
|
||||
|
||||
|
||||
class RepositoryPermissions(BaseModel):
|
||||
allow_push: List[str]
|
||||
can_manage: bool # can the requesting user manage the repository
|
||||
can_push: bool
|
||||
|
||||
|
||||
class RepositoryRevisionReadmes(BaseModel):
|
||||
@@ -53,6 +64,8 @@ class User(BaseModel):
|
||||
class Category(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
description: str
|
||||
repositories: int
|
||||
|
||||
|
||||
class CreateCategoryRequest(BaseModel):
|
||||
@@ -117,10 +130,6 @@ class RepositoryUpdate(BaseModel):
|
||||
return isinstance(self.__root__, ValidRepostiroyUpdateMessage)
|
||||
|
||||
|
||||
class RepositoryDependency(BaseModel):
|
||||
pass
|
||||
|
||||
|
||||
class RepositoryTool(BaseModel):
|
||||
pass
|
||||
|
||||
@@ -128,8 +137,8 @@ class RepositoryTool(BaseModel):
|
||||
class RepositoryRevisionMetadata(BaseModel):
|
||||
id: str
|
||||
repository: Repository
|
||||
repository_dependencies: List[RepositoryDependency]
|
||||
tools: Optional[List[RepositoryTool]]
|
||||
repository_dependencies: List["RepositoryDependency"]
|
||||
tools: Optional[List["RepositoryTool"]]
|
||||
repository_id: str
|
||||
numeric_revision: int
|
||||
changeset_revision: str
|
||||
@@ -145,6 +154,15 @@ class RepositoryRevisionMetadata(BaseModel):
|
||||
includes_workflows: Optional[bool]
|
||||
|
||||
|
||||
class RepositoryDependency(RepositoryRevisionMetadata):
|
||||
# This only needs properties for tests it seems?
|
||||
# e.g. test_0550_metadata_updated_dependencies.py
|
||||
pass
|
||||
|
||||
|
||||
RepositoryRevisionMetadata.update_forward_refs()
|
||||
|
||||
|
||||
class RepositoryMetadata(BaseModel):
|
||||
__root__: Dict[str, RepositoryRevisionMetadata]
|
||||
|
||||
@@ -438,3 +456,9 @@ def from_legacy_install_info(legacy_install_info: LegacyInstallInfoTuple) -> Ins
|
||||
class BuildSearchIndexResponse(BaseModel):
|
||||
repositories_indexed: int
|
||||
tools_indexed: int
|
||||
|
||||
|
||||
class Version(BaseModel):
|
||||
version_major: str
|
||||
version: str
|
||||
api_version: str = "v1"
|
||||
|
||||
@@ -28,8 +28,17 @@ YamlDumper.add_implicit_resolver("!anyurl", url_regex(), None)
|
||||
|
||||
@click.command("Write openapi schema to path")
|
||||
@click.argument("schema_path", type=click.Path(dir_okay=False, writable=True), required=False)
|
||||
def write_open_api_schema(schema_path):
|
||||
openapi_schema = get_openapi_schema()
|
||||
@click.option("--app", type=click.Choice(["gx", "shed"]), required=False, default="gx")
|
||||
def write_open_api_schema(schema_path, app: str):
|
||||
if app == "shed":
|
||||
# Importing this causes the Galaxy schema to generate
|
||||
# in a different fashion and causes a diff in downstream
|
||||
# typescript generation for instance. So delay this.
|
||||
from tool_shed.webapp.fast_app import get_openapi_schema as get_openapi_schema_shed
|
||||
|
||||
openapi_schema = get_openapi_schema_shed()
|
||||
else:
|
||||
openapi_schema = get_openapi_schema()
|
||||
if schema_path:
|
||||
if schema_path.endswith((".yml", ".yaml")):
|
||||
with open(schema_path, "w") as f:
|
||||
|
||||
@@ -12,6 +12,7 @@ from typing import Optional
|
||||
import tool_shed.repository_registry
|
||||
from galaxy.security.idencoding import IdEncodingHelper
|
||||
from galaxy.util import safe_makedirs
|
||||
from tool_shed.context import ProvidesRepositoriesContext
|
||||
from tool_shed.managers.repositories import upload_tar_and_set_metadata
|
||||
from tool_shed.managers.users import create_user
|
||||
from tool_shed.repository_types import util as rt_util
|
||||
@@ -88,7 +89,32 @@ def user_fixture(
|
||||
)
|
||||
|
||||
|
||||
def repository_fixture(app: TestToolShedApp, user: User, name: str) -> Repository:
|
||||
class ProvidesRepositoriesImpl(ProvidesRepositoriesContext):
|
||||
def __init__(self, app: TestToolShedApp, user: User):
|
||||
self._app = app
|
||||
self._user = user
|
||||
|
||||
@property
|
||||
def app(self) -> ToolShedApp:
|
||||
return self._app
|
||||
|
||||
@property
|
||||
def user(self) -> User:
|
||||
return self._user
|
||||
|
||||
@property
|
||||
def repositories_hostname(self) -> str:
|
||||
return "shed_unit_test://localhost"
|
||||
|
||||
|
||||
def provides_repositories_fixture(
|
||||
app: TestToolShedApp,
|
||||
user: User,
|
||||
):
|
||||
return ProvidesRepositoriesImpl(app, user)
|
||||
|
||||
|
||||
def repository_fixture(app: ToolShedApp, user: User, name: str) -> Repository:
|
||||
type = rt_util.UNRESTRICTED
|
||||
description = f"test repo named {name}"
|
||||
long_description = f"test repo named {name} a longer description"
|
||||
@@ -116,7 +142,12 @@ from unittest import mock
|
||||
patch_url_for = mock.patch("galaxy.util.tool_shed.common_util.url_for", _mock_url_for)
|
||||
|
||||
|
||||
def upload(app: TestToolShedApp, repository: Repository, path: Path, arcname: Optional[str] = None):
|
||||
def upload(
|
||||
provides_repositories: ProvidesRepositoriesContext,
|
||||
repository: Repository,
|
||||
path: Path,
|
||||
arcname: Optional[str] = None,
|
||||
):
|
||||
if path.is_dir():
|
||||
tf = NamedTemporaryFile(delete=False)
|
||||
with tarfile.open(tf.name, "w:gz") as tar:
|
||||
@@ -127,19 +158,20 @@ def upload(app: TestToolShedApp, repository: Repository, path: Path, arcname: Op
|
||||
else:
|
||||
tar_path = str(path)
|
||||
return upload_tar_and_set_metadata(
|
||||
app,
|
||||
provides_repositories,
|
||||
TEST_HOST,
|
||||
repository.user,
|
||||
repository,
|
||||
tar_path,
|
||||
commit_message=TEST_COMMIT_MESSAGE,
|
||||
)
|
||||
|
||||
|
||||
def upload_directories_to_repository(app: TestToolShedApp, repository: Repository, test_data_path: str):
|
||||
def upload_directories_to_repository(
|
||||
provides_repositories: ProvidesRepositoriesContext, repository: Repository, test_data_path: str
|
||||
):
|
||||
paths = repo_tars(test_data_path)
|
||||
for path in paths:
|
||||
upload(app, repository, Path(path), arcname=test_data_path)
|
||||
upload(provides_repositories, repository, Path(path), arcname=test_data_path)
|
||||
|
||||
|
||||
def random_name(len: int = 10) -> str:
|
||||
|
||||
@@ -5,6 +5,7 @@ from tool_shed.webapp.model import (
|
||||
User,
|
||||
)
|
||||
from ._util import (
|
||||
provides_repositories_fixture,
|
||||
random_name,
|
||||
repository_fixture,
|
||||
TestToolShedApp,
|
||||
@@ -26,3 +27,8 @@ def new_user(shed_app: TestToolShedApp) -> User:
|
||||
@pytest.fixture
|
||||
def new_repository(shed_app: TestToolShedApp, new_user: User) -> Repository:
|
||||
return repository_fixture(shed_app, new_user, random_name())
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def provides_repositories(shed_app: TestToolShedApp, new_user: User) -> User:
|
||||
return provides_repositories_fixture(shed_app, new_user)
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
from tool_shed.context import ProvidesRepositoriesContext
|
||||
from tool_shed.metadata import repository_metadata_manager
|
||||
from tool_shed.webapp.model import Repository
|
||||
from ._util import (
|
||||
patch_url_for,
|
||||
TestToolShedApp,
|
||||
upload_directories_to_repository,
|
||||
)
|
||||
from ._util import upload_directories_to_repository
|
||||
|
||||
|
||||
@patch_url_for
|
||||
def test_reset_simple(shed_app: TestToolShedApp, new_repository: Repository):
|
||||
upload_directories_to_repository(shed_app, new_repository, "column_maker")
|
||||
def test_reset_simple(provides_repositories: ProvidesRepositoriesContext, new_repository: Repository):
|
||||
shed_app = provides_repositories.app
|
||||
upload_directories_to_repository(provides_repositories, new_repository, "column_maker")
|
||||
assert len(new_repository.downloadable_revisions) == 3
|
||||
assert "2:" in new_repository.revision()
|
||||
rmm = repository_metadata_manager.RepositoryMetadataManager(
|
||||
app=shed_app,
|
||||
user=new_repository.user,
|
||||
provides_repositories,
|
||||
repository=new_repository,
|
||||
resetting_all_metadata_on_repository=True,
|
||||
updating_installed_repository=False,
|
||||
@@ -25,14 +21,15 @@ def test_reset_simple(shed_app: TestToolShedApp, new_repository: Repository):
|
||||
assert len(new_repository.downloadable_revisions) == 3
|
||||
|
||||
|
||||
@patch_url_for
|
||||
def test_reset_on_repo_with_uninstallable_revisions(shed_app: TestToolShedApp, new_repository: Repository):
|
||||
upload_directories_to_repository(shed_app, new_repository, "column_maker_with_download_gaps")
|
||||
def test_reset_on_repo_with_uninstallable_revisions(
|
||||
provides_repositories: ProvidesRepositoriesContext, new_repository: Repository
|
||||
):
|
||||
shed_app = provides_repositories.app
|
||||
upload_directories_to_repository(provides_repositories, new_repository, "column_maker_with_download_gaps")
|
||||
assert len(new_repository.downloadable_revisions) == 3
|
||||
assert "3:" in new_repository.revision()
|
||||
rmm = repository_metadata_manager.RepositoryMetadataManager(
|
||||
app=shed_app,
|
||||
user=new_repository.user,
|
||||
provides_repositories,
|
||||
repository=new_repository,
|
||||
resetting_all_metadata_on_repository=True,
|
||||
updating_installed_repository=False,
|
||||
@@ -43,14 +40,15 @@ def test_reset_on_repo_with_uninstallable_revisions(shed_app: TestToolShedApp, n
|
||||
assert len(new_repository.downloadable_revisions) == 3
|
||||
|
||||
|
||||
@patch_url_for
|
||||
def test_reset_dm_with_uninstallable_revisions(shed_app: TestToolShedApp, new_repository: Repository):
|
||||
upload_directories_to_repository(shed_app, new_repository, "data_manager_gaps")
|
||||
def test_reset_dm_with_uninstallable_revisions(
|
||||
provides_repositories: ProvidesRepositoriesContext, new_repository: Repository
|
||||
):
|
||||
shed_app = provides_repositories.app
|
||||
upload_directories_to_repository(provides_repositories, new_repository, "data_manager_gaps")
|
||||
assert len(new_repository.downloadable_revisions) == 1
|
||||
assert "2:" in new_repository.revision()
|
||||
rmm = repository_metadata_manager.RepositoryMetadataManager(
|
||||
app=shed_app,
|
||||
user=new_repository.user,
|
||||
provides_repositories,
|
||||
repository=new_repository,
|
||||
resetting_all_metadata_on_repository=True,
|
||||
updating_installed_repository=False,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from tool_shed.context import ProvidesRepositoriesContext
|
||||
from tool_shed.util.repository_content_util import upload_tar
|
||||
from tool_shed.webapp.model import (
|
||||
Repository,
|
||||
@@ -25,12 +26,11 @@ def test_create_repository(shed_app: TestToolShedApp, new_user: User):
|
||||
assert entry
|
||||
|
||||
|
||||
def test_upload_tar(shed_app: TestToolShedApp, new_repository: Repository):
|
||||
def test_upload_tar(provides_repositories: ProvidesRepositoriesContext, new_repository: Repository):
|
||||
tar_resource = TEST_DATA_FILES.joinpath("column_maker/column_maker.tar")
|
||||
old_tip = new_repository.tip()
|
||||
upload_ok, _, _, alert, dirs_removed, files_removed = upload_tar(
|
||||
shed_app,
|
||||
"localhost",
|
||||
provides_repositories,
|
||||
new_repository.user.username,
|
||||
new_repository,
|
||||
tar_resource,
|
||||
@@ -42,18 +42,19 @@ def test_upload_tar(shed_app: TestToolShedApp, new_repository: Repository):
|
||||
assert files_removed == 0
|
||||
new_tip = new_repository.tip()
|
||||
assert old_tip != new_tip
|
||||
changesets = new_repository.get_changesets_for_setting_metadata(shed_app)
|
||||
changesets = new_repository.get_changesets_for_setting_metadata(provides_repositories.app)
|
||||
assert len(changesets) == 1
|
||||
for change in changesets:
|
||||
ctx = new_repository.hg_repo[change]
|
||||
assert str(ctx) == new_tip
|
||||
|
||||
|
||||
def test_upload_fails_if_contains_symlink(shed_app: TestToolShedApp, new_repository: Repository):
|
||||
def test_upload_fails_if_contains_symlink(
|
||||
provides_repositories: ProvidesRepositoriesContext, new_repository: Repository
|
||||
):
|
||||
tar_resource = TEST_DATA_FILES.joinpath("safetar_with_symlink.tar")
|
||||
upload_ok, message, _, _, _, _ = upload_tar(
|
||||
shed_app,
|
||||
"localhost",
|
||||
provides_repositories,
|
||||
new_repository.user.username,
|
||||
new_repository,
|
||||
tar_resource,
|
||||
@@ -63,12 +64,11 @@ def test_upload_fails_if_contains_symlink(shed_app: TestToolShedApp, new_reposit
|
||||
assert "Invalid paths" in message
|
||||
|
||||
|
||||
def test_upload_dry_run_ok(shed_app: TestToolShedApp, new_repository: Repository):
|
||||
def test_upload_dry_run_ok(provides_repositories: ProvidesRepositoriesContext, new_repository: Repository):
|
||||
tar_resource = TEST_DATA_FILES.joinpath("column_maker/column_maker.tar")
|
||||
old_tip = new_repository.tip()
|
||||
upload_ok, _, _, alert, dirs_removed, files_removed = upload_tar(
|
||||
shed_app,
|
||||
"localhost",
|
||||
provides_repositories,
|
||||
new_repository.user.username,
|
||||
new_repository,
|
||||
tar_resource,
|
||||
@@ -83,11 +83,10 @@ def test_upload_dry_run_ok(shed_app: TestToolShedApp, new_repository: Repository
|
||||
assert old_tip == new_tip
|
||||
|
||||
|
||||
def test_upload_dry_run_failed(shed_app: TestToolShedApp, new_repository: Repository):
|
||||
def test_upload_dry_run_failed(provides_repositories: ProvidesRepositoriesContext, new_repository: Repository):
|
||||
tar_resource = TEST_DATA_FILES.joinpath("safetar_with_symlink.tar")
|
||||
upload_ok, message, _, _, _, _ = upload_tar(
|
||||
shed_app,
|
||||
"localhost",
|
||||
provides_repositories,
|
||||
new_repository.user.username,
|
||||
new_repository,
|
||||
tar_resource,
|
||||
|
||||
Reference in New Issue
Block a user