ToolShed API 2.0

This commit is contained in:
John Chilton
2023-01-18 14:47:10 -05:00
parent bad135b9d7
commit d1d629074b
46 changed files with 1963 additions and 283 deletions
+2
View File
@@ -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()
+3
View File
@@ -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'
+4
View File
@@ -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():
+14 -5
View File
@@ -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.
+67 -3
View File
@@ -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)
+24 -8
View File
@@ -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}
+41 -12
View File
@@ -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)
+1 -1
View File
@@ -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):
"""
+27 -1
View File
@@ -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
+14 -1
View File
@@ -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]:
+55
View File
@@ -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-&gt;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.
+21 -8
View File
@@ -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
+16 -9
View File
@@ -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,
+3 -2
View File
@@ -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
+2 -8
View File
@@ -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
+4 -7
View File
@@ -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,
+273
View File
@@ -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")
+27
View File
@@ -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)
+80
View File
@@ -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",
)
+494
View File
@@ -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__"]
+55
View File
@@ -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,
)
+130
View File
@@ -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
+8 -1
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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:
+59 -7
View File
@@ -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",
)
+2
View File
@@ -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="?")
+30 -6
View File
@@ -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"
+11 -2
View File
@@ -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:
+38 -6
View File
@@ -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:
+6
View File
@@ -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,
+12 -13
View File
@@ -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,