Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rip repository_registry out of tool shed 2.0 #18647

Merged
merged 3 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions lib/galaxy/exceptions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,11 @@ class InconsistentDatabase(MessageException):
err_code = error_codes_by_name["INCONSISTENT_DATABASE"]


class InconsistentApplicationState(MessageException):
status_code = 500
err_code = error_codes_by_name["INCONSISTENT_APPLICATION_STATE"]


class InternalServerError(MessageException):
status_code = 500
err_code = error_codes_by_name["INTERNAL_SERVER_ERROR"]
Expand Down
5 changes: 5 additions & 0 deletions lib/galaxy/exceptions/error_codes.json
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,11 @@
"code": 500006,
"message": "Reference data required for program execution failed to load."
},
{
"name": "INCONSISTENT_APPLICATION_STATE",
"code": 500007,
"message": "Inconsistent application state (likely not dbms related) prevented fulfilling the request."
},
{
"name": "NOT_IMPLEMENTED",
"code": 501001,
Expand Down
4 changes: 1 addition & 3 deletions lib/tool_shed/managers/categories.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,7 @@ def index(self, trans: ProvidesUserContext, deleted: bool) -> List[Dict[str, Any

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["repositories"] = category.active_repository_count()
category_dict["url"] = web.url_for(
controller="categories", action="show", id=self.app.security.encode_id(category.id)
)
Expand Down
11 changes: 10 additions & 1 deletion lib/tool_shed/managers/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from galaxy import exceptions
from galaxy.exceptions import (
InconsistentApplicationState,
InternalServerError,
ObjectNotFound,
RequestParameterInvalidException,
Expand Down Expand Up @@ -148,9 +149,17 @@ def _shed_tool_source_for(
cloned_ok, error_message = clone_repository(repository_clone_url, work_dir, str(ctx.rev()))
if error_message:
raise InternalServerError("Failed to materialize target repository revision")
repo_files_dir = repository_metadata.repository.repo_path(trans.app)
repo_files_dir = repository_metadata.repository.hg_repository_path(trans.app.config.file_path)
if not repo_files_dir:
raise InconsistentApplicationState(
f"Failed to resolve repository path from hgweb_config_manager for [{trs_tool_id}], inconsistent repository state or application configuration"
)
repo_rel_tool_path = relpath(tool_config, repo_files_dir)
path_to_tool = os.path.join(work_dir, repo_rel_tool_path)
if not os.path.exists(path_to_tool):
raise InconsistentApplicationState(
f"Target tool expected at [{path_to_tool}] and not found, inconsistent repository state or application configuration"
)
tool_source = get_tool_source(path_to_tool)
return tool_source
finally:
Expand Down
104 changes: 96 additions & 8 deletions lib/tool_shed/repository_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
or_,
select,
)
from typing_extensions import Protocol

import tool_shed.repository_types.util as rt_util
from tool_shed.util import (
Expand All @@ -18,11 +19,77 @@
Repository,
RepositoryMetadata,
)
from .structured_app import ToolShedApp

log = logging.getLogger(__name__)


class Registry:
class RegistryInterface(Protocol):

# only used in legacy controllers - can drop when drop old tool shed webapp
def add_category_entry(self, category): ...

def add_entry(self, repository):
# used when creating a repository
# Used in legacy add_repository_registry_entry API endpoint - already deemed not worth pulling over to 2.0
# used in legacy undelete_repository admin controller
# used in legacy deprecate controller
...

def edit_category_entry(self, old_name, new_name):
# used in legacy admin controller
...

def is_valid(self, repository) -> bool:
# probably not used outside this class but also not hurting anything
...

def remove_category_entry(self, category): ...

def remove_entry(self, repository): ...


# stop gap to implement the same general interface as repository_registry but do nothing,
# I don't think this stuff is needed - outside potentially old test cases?
class NullRepositoryRegistry(RegistryInterface):

def __init__(self, app: ToolShedApp):
self.app = app

# all of these are only used by repository_grids - which I think the tool shed 2.0
# does not use at all - but they are part of the "public interface" consumed by
# the full registry.
self.certified_level_one_viewable_repositories_and_suites_by_category: dict = {}
self.certified_level_one_viewable_suites_by_category: dict = {}
self.viewable_repositories_and_suites_by_category: dict = {}
self.viewable_suites_by_category: dict = {}
self.viewable_valid_repositories_and_suites_by_category: dict = {}
self.viewable_valid_suites_by_category: dict = {}

def add_category_entry(self, category):
# only used in legacy controllers - can drop when drop old tool shed webapp
pass

def add_entry(self, repository):
# used when creating a repository, and maybe more?
pass

def edit_category_entry(self, old_name, new_name):
pass

def is_valid(self, repository) -> bool:
if repository and not repository.deleted and not repository.deprecated and repository.downloadable_revisions:
return True
return False

def remove_category_entry(self, category):
pass

def remove_entry(self, repository):
pass


class Registry(RegistryInterface):
def __init__(self, app):
log.debug("Loading the repository registry...")
self.app = app
Expand All @@ -32,17 +99,30 @@ def __init__(self, app):
self.certified_level_one_suite_tuples = []
# These category dictionaries contain entries where the key is the category and the value is the integer count
# of viewable repositories within that category.

# only used internally to class and by repository_grids
self.certified_level_one_viewable_repositories_and_suites_by_category = {}
# only used internally to class and by repository_grids
self.certified_level_one_viewable_suites_by_category = {}
self.certified_level_two_repository_and_suite_tuples = []
self.certified_level_two_suite_tuples = []
self.certified_level_two_viewable_repositories_and_suites_by_category = {}
self.certified_level_two_viewable_suites_by_category = {}

# not even used in legacy shed code...
# self.certified_level_two_repository_and_suite_tuples = []
# self.certified_level_two_suite_tuples = []
# self.certified_level_two_viewable_repositories_and_suites_by_category = {}
# self.certified_level_two_viewable_suites_by_category = {}

# only used internally to class
self.repository_and_suite_tuples = []
# only used internally to class
self.suite_tuples = []

# only used internally to class and by repository_grids
self.viewable_repositories_and_suites_by_category = {}
# only used internally to class and by repository_grids
self.viewable_suites_by_category = {}
# only used internally to class and by repository_grids
self.viewable_valid_repositories_and_suites_by_category = {}
# only used internally to class and by repository_grids
self.viewable_valid_suites_by_category = {}
self.load()

Expand Down Expand Up @@ -152,6 +232,7 @@ def edit_category_entry(self, old_name, new_name):
self.certified_level_one_viewable_suites_by_category[new_name] = 0

def get_certified_level_one_clause_list(self):
# only used internally to class
clause_list = []
for repository in get_repositories(self.sa_session):
certified_level_one_tuple = self.get_certified_level_one_tuple(repository)
Expand All @@ -169,6 +250,7 @@ def get_certified_level_one_tuple(self, repository):
"""
Return True if the latest installable changeset_revision of the received repository is level one certified.
"""
# only used internally to class
if repository is None:
return (None, False)
if repository.deleted or repository.deprecated:
Expand All @@ -190,6 +272,7 @@ def get_certified_level_one_tuple(self, repository):
return (None, False)

def is_level_one_certified(self, repository_metadata):
# only used internally to class
if repository_metadata:
repository = repository_metadata.repository
if repository:
Expand All @@ -206,12 +289,13 @@ def is_level_one_certified(self, repository_metadata):
return tuple in self.certified_level_one_repository_and_suite_tuples
return False

def is_valid(self, repository):
def is_valid(self, repository) -> bool:
if repository and not repository.deleted and not repository.deprecated and repository.downloadable_revisions:
return True
return False

def load_certified_level_one_repository_and_suite_tuple(self, repository):
# only used internally to class
# The received repository has been determined to be level one certified.
name = str(repository.name)
owner = str(repository.user.username)
Expand All @@ -226,6 +310,7 @@ def load_certified_level_one_repository_and_suite_tuple(self, repository):
self.certified_level_one_repository_and_suite_tuples.append(certified_level_one_tuple)

def load_repository_and_suite_tuple(self, repository):
# only used internally to class
name = str(repository.name)
owner = str(repository.user.username)
for repository_metadata in repository.metadata_revisions:
Expand All @@ -238,6 +323,7 @@ def load_repository_and_suite_tuple(self, repository):
self.suite_tuples.append(tuple)

def load_repository_and_suite_tuples(self):
# only used internally to class
# Load self.certified_level_one_repository_and_suite_tuples and self.certified_level_one_suite_tuples.
clauses = self.get_certified_level_one_clause_list()
for repository in get_certified_repositories_with_user(self.sa_session, clauses, model.User):
Expand All @@ -247,11 +333,12 @@ def load_repository_and_suite_tuples(self):
self.load_repository_and_suite_tuple(repository)

def load_viewable_repositories_and_suites_by_category(self):
# only used internally to class
# Clear all dictionaries just in case they were previously loaded.
self.certified_level_one_viewable_repositories_and_suites_by_category = {}
self.certified_level_one_viewable_suites_by_category = {}
self.certified_level_two_viewable_repositories_and_suites_by_category = {}
self.certified_level_two_viewable_suites_by_category = {}
# self.certified_level_two_viewable_repositories_and_suites_by_category = {}
# self.certified_level_two_viewable_suites_by_category = {}
self.viewable_repositories_and_suites_by_category = {}
self.viewable_suites_by_category = {}
self.viewable_valid_repositories_and_suites_by_category = {}
Expand Down Expand Up @@ -363,6 +450,7 @@ def remove_entry(self, repository):

@property
def sa_session(self):
# only used internally to class
return self.app.model.session

def unload_certified_level_one_repository_and_suite_tuple(self, repository):
Expand Down
4 changes: 2 additions & 2 deletions lib/tool_shed/structured_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

if TYPE_CHECKING:
from tool_shed.managers.model_cache import ModelCache
from tool_shed.repository_registry import Registry as RepositoryRegistry
from tool_shed.repository_registry import RegistryInterface
from tool_shed.repository_types.registry import Registry as RepositoryTypesRegistry
from tool_shed.util.hgweb_config import HgWebConfigManager
from tool_shed.webapp.model import mapping
Expand All @@ -14,7 +14,7 @@
class ToolShedApp(BasicSharedApp):
repository_types_registry: "RepositoryTypesRegistry"
model: "mapping.ToolShedModelMapping"
repository_registry: "RepositoryRegistry"
repository_registry: "RegistryInterface"
hgweb_config_manager: "HgWebConfigManager"
security_agent: "CommunityRBACAgent"
model_cache: "ModelCache"
7 changes: 1 addition & 6 deletions lib/tool_shed/util/repository_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,12 +231,7 @@ def create_repository(
session = sa_session()
with transaction(session):
session.commit()
dir = os.path.join(app.config.file_path, *util.directory_hash_id(repository.id))
# Define repo name inside hashed directory.
final_repository_path = os.path.join(dir, "repo_%d" % repository.id)
# Create final repository directory.
if not os.path.exists(final_repository_path):
os.makedirs(final_repository_path)
final_repository_path = repository.ensure_hg_repository_path(app.config.file_path)
os.rename(repository_path, final_repository_path)
app.hgweb_config_manager.add_entry(lhs, final_repository_path)
# Update the repository registry.
Expand Down
5 changes: 4 additions & 1 deletion lib/tool_shed/webapp/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,10 @@ def __init__(self, **kwd) -> None:
self.hgweb_config_manager.hgweb_config_dir = self.config.hgweb_config_dir
self.hgweb_config_manager.hgweb_repo_prefix = self.config.hgweb_repo_prefix
# Initialize the repository registry.
self.repository_registry = tool_shed.repository_registry.Registry(self)
if config.SHED_API_VERSION != "v2":
self.repository_registry = tool_shed.repository_registry.Registry(self)
else:
self.repository_registry = tool_shed.repository_registry.NullRepositoryRegistry(self)
# Configure Sentry client if configured
self.configure_sentry_client()
# used for cachebusting -- refactor this into a *SINGLE* UniverseApplication base.
Expand Down
3 changes: 1 addition & 2 deletions lib/tool_shed/webapp/buildapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@
GalaxyWebTransaction,
)
from galaxy.webapps.util import wrap_if_allowed

SHED_API_VERSION = os.environ.get("TOOL_SHED_API_VERSION", "v1")
from .config import SHED_API_VERSION

log = logging.getLogger(__name__)

Expand Down
1 change: 1 addition & 0 deletions lib/tool_shed/webapp/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
log = logging.getLogger(__name__)

TOOLSHED_APP_NAME = "tool_shed"
SHED_API_VERSION = os.environ.get("TOOL_SHED_API_VERSION", "v1")


class ToolShedAppConfiguration(BaseAppConfiguration, CommonConfigurationMixin):
Expand Down
2 changes: 1 addition & 1 deletion lib/tool_shed/webapp/fast_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ def initialize_fast_app(gx_webapp, tool_shed_app):
app = get_fastapi_instance()
add_exception_handler(app)
add_request_id_middleware(app)
from .buildapp import SHED_API_VERSION
from .config import SHED_API_VERSION

def mount_static(directory: Path):
name = directory.name
Expand Down
31 changes: 31 additions & 0 deletions lib/tool_shed/webapp/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@
not_,
String,
Table,
text,
TEXT,
true,
UniqueConstraint,
)
from sqlalchemy.orm import (
Mapped,
mapped_column,
object_session,
registry,
relationship,
)
Expand Down Expand Up @@ -524,6 +526,19 @@ def repo_path(self, app=None):
os.path.join(hgweb_config_manager.hgweb_repo_prefix, self.user.username, self.name)
)

def hg_repository_path(self, repositories_directory: str) -> str:
if self.id is None:
raise Exception("Attempting to call hg_repository_path before id has been set on repository object")
dir = os.path.join(repositories_directory, *util.directory_hash_id(self.id))
final_repository_path = os.path.join(dir, "repo_%d" % self.id)
return final_repository_path

def ensure_hg_repository_path(self, repositories_directory: str) -> str:
final_repository_path = self.hg_repository_path(repositories_directory)
if not os.path.exists(final_repository_path):
os.makedirs(final_repository_path)
return final_repository_path

def revision(self):
repo = self.hg_repo
tip_ctx = repo[repo.changelog.tip()]
Expand Down Expand Up @@ -605,6 +620,22 @@ class Category(Base, Dictifiable):
dict_collection_visible_keys = ["id", "name", "description", "deleted"]
dict_element_visible_keys = ["id", "name", "description", "deleted"]

def active_repository_count(self, session=None) -> int:
statement = """
SELECT count(*) as count
FROM repository r
INNER JOIN repository_category_association rca on rca.repository_id = r.id
WHERE
rca.category_id = :category_id
AND r.private = false
AND r.deleted = false
and r.deprecated = false
"""
if session is None:
session = object_session(self)
params = {"category_id": self.id}
return session.execute(text(statement), params).scalar()

def __init__(self, deleted=False, **kwd):
super().__init__(**kwd)
self.deleted = deleted
Expand Down
Loading
Loading