From d21de8e8c86494db7d35e1d24c6b81ef2be034e1 Mon Sep 17 00:00:00 2001 From: Sergey Golitsynskiy Date: Thu, 21 Jan 2021 23:39:45 -0500 Subject: [PATCH] Refactor; move logic into manager; add typing --- lib/galaxy/managers/configuration.py | 86 ++++++- lib/galaxy/webapps/galaxy/api/common.py | 22 ++ .../webapps/galaxy/api/configuration.py | 218 ++++++------------ templates/galaxy_client_app.mako | 2 +- 4 files changed, 178 insertions(+), 150 deletions(-) create mode 100644 lib/galaxy/webapps/galaxy/api/common.py diff --git a/lib/galaxy/managers/configuration.py b/lib/galaxy/managers/configuration.py index 36176fe5eae..1864686e448 100644 --- a/lib/galaxy/managers/configuration.py +++ b/lib/galaxy/managers/configuration.py @@ -1,21 +1,99 @@ """ -Serializers for Galaxy config file data: ConfigSerializer for all users +Managers, serializers for Galaxy config file data. ConfigSerializer for all users and a more expanded set of data for admin in AdminConfigSerializer. Used by both the API and bootstrapped data. """ -# TODO: this is a bit of an odd duck. It uses the serializer structure from managers -# but doesn't have a model like them. It might be better in config.py or a -# totally new area, but I'm leaving it in managers for now for class consistency. +import json import logging +import os import sys +from typing import ( + Any, + Dict, + List, +) +from galaxy.app import StructuredApp from galaxy.managers import base +from galaxy.managers.context import ProvidesAppContext from galaxy.web.framework.base import server_starttime log = logging.getLogger(__name__) +VERSION_JSON_FILE = 'version.json' + +class ConfigurationManager: + """Interface/service object for interacting with configuration and related data.""" + + def __init__(self, app: StructuredApp): + self._app = app + + def get_configuration(self, trans, serialization_params) -> Dict[str, Any]: + is_admin = trans.user_is_admin + serializer = AdminConfigSerializer(self._app) if is_admin else ConfigSerializer(self._app) + return serializer.serialize_to_view(self._app.config, **serialization_params) + + def version(self) -> Dict[str, Any]: + version_info = { + "version_major": self._app.config.version_major, + "version_minor": self._app.config.version_minor, + } + # Try loading extra version info + json_file = os.path.join(self._app.config.root, VERSION_JSON_FILE) # TODO: add this to schema + json_file = os.environ.get("GALAXY_VERSION_JSON_FILE", json_file) + try: + with open(json_file) as f: + extra_info = json.load(f) + except OSError: + log.info('Galaxy JSON version file not loaded') + else: + version_info['extra'] = extra_info + return version_info + + def decode_id( + self, + trans: ProvidesAppContext, + encoded_id: str + ) -> Dict[str, int]: + # Handle the special case for library folders + if ((len(encoded_id) % 16 == 1) and encoded_id.startswith('F')): + encoded_id = encoded_id[1:] + decoded_id = trans.security.decode_id(encoded_id) + return {"decoded_id": decoded_id} + + def tool_lineages(self) -> List[Dict[str, Dict]]: + rval = [] + for id, tool in self._app.toolbox.tools(): + try: + lineage_dict = tool.lineage.to_dict() + except AttributeError: + pass + else: + entry = {'id': id, 'lineage': lineage_dict} + rval.append(entry) + return rval + + def dynamic_tool_confs(self) -> List[Dict[str, str]]: + # WARNING: If this method is ever changed so as not to require admin privileges, update the nginx proxy + # documentation, since this path is used as an authentication-by-proxy method for securing other paths on the + # server. A dedicated endpoint should probably be added to do that instead. + def tool_conf_to_dict(conf): + return dict( + config_filename=conf['config_filename'], + tool_path=conf['tool_path'], + ) + confs = self._app.toolbox.dynamic_confs(include_migrated_tool_conf=True) + return list(map(tool_conf_to_dict, confs)) + + def reload_toolbox(self): + self._app.queue_worker.send_control_task('reload_toolbox') + + +# TODO: this is a bit of an odd duck. It uses the serializer structure from managers +# but doesn't have a model like them. It might be better in config.py or a +# totally new area, but I'm leaving it in managers for now for class consistency. class ConfigSerializer(base.ModelSerializer): """Configuration (galaxy.ini) settings viewable by all users""" diff --git a/lib/galaxy/webapps/galaxy/api/common.py b/lib/galaxy/webapps/galaxy/api/common.py new file mode 100644 index 00000000000..4d4817512b5 --- /dev/null +++ b/lib/galaxy/webapps/galaxy/api/common.py @@ -0,0 +1,22 @@ +"""This module contains utility functions shared across the api package.""" +from typing import Optional + +from fastapi import Query + +SerializationViewQueryParam: Optional[str] = Query( + None, + title='View', + description='View to be passed to the serializer', +) + +SerializationKeysQueryParam: Optional[str] = Query( + None, + title='Keys', + description='Comma-separated list of keys to be passed to the serializer', +) + + +def parse_serialization_params(view, keys, default_view): + if keys: + keys = keys.split(',') + return dict(view=view, keys=keys, default_view=default_view) diff --git a/lib/galaxy/webapps/galaxy/api/configuration.py b/lib/galaxy/webapps/galaxy/api/configuration.py index f084afb1305..3cc0a21ad27 100644 --- a/lib/galaxy/webapps/galaxy/api/configuration.py +++ b/lib/galaxy/webapps/galaxy/api/configuration.py @@ -2,9 +2,7 @@ API operations allowing clients to determine Galaxy instance's capabilities and configuration settings. """ -import json import logging -import os from typing import ( Any, Dict, @@ -12,14 +10,11 @@ from typing import ( Optional ) -from fastapi import Depends, Query +from fastapi import Depends from fastapi_utils.cbv import cbv from fastapi_utils.inferring_router import InferringRouter as APIRouter -from galaxy.managers.configuration import ( - AdminConfigSerializer, - ConfigSerializer -) +from galaxy.managers.configuration import ConfigurationManager from galaxy.managers.context import ( ProvidesAppContext, ProvidesUserContext, @@ -42,162 +37,84 @@ from . import ( get_trans, get_user, ) +from .common import ( + parse_serialization_params, + SerializationKeysQueryParam, + SerializationViewQueryParam, +) log = logging.getLogger(__name__) - router = APIRouter(tags=['configuration']) -VERSION_JSON_FILE = 'version.json' +AdminUserRequired = Depends(get_admin_user) -# TODO move to common.py as soon as used more than once -SerializationViewQueryParam: Optional[str] = Query( - None, - title='View', - description='todo', -) - - -# TODO move to common.py as soon as used more than once -SerializationKeysQueryParam: Optional[str] = Query( - None, - title='Keys', - description='todo', -) - - -# TODO move to common.py as soon as used more than once -def parse_serialization_params(view, keys, default_view): - if isinstance(keys, str): - keys = keys.split(',') - return dict(view=view, keys=keys, default_view=default_view) - - -def user_to_model(user): - return UserModel(**user.to_dict()) if user else None - - -def _get_configuration(trans, view, keys, default_view='all'): - is_admin = trans.user_is_admin - serialization_params = parse_serialization_params(view, keys, default_view) - return get_config_dict(trans.app, is_admin, **serialization_params) - - -def get_config_dict(app, is_admin=False, view=None, keys=None, default_view='all'): - """ - Return a dictionary with a subset of current Galaxy settings. - - If `is_admin`, include a subset of more sensitive keys. - Pass in `view` (String) and comma seperated list of keys to control which - configuration settings are returned. - """ - serializer = AdminConfigSerializer(app) if is_admin else ConfigSerializer(app) - return serializer.serialize_to_view(app.config, view=view, keys=keys, default_view=default_view) - - -def _get_version(app): - version_info = { - "version_major": app.config.version_major, - "version_minor": app.config.version_minor, - } - # Try loading extra version info - json_file = os.path.join(app.config.root, VERSION_JSON_FILE) # TODO: add this to schema - json_file = os.environ.get("GALAXY_VERSION_JSON_FILE", json_file) - try: - with open(json_file) as f: - extra_info = json.load(f) - except OSError: - log.info('Galaxy JSON version file not loaded') - else: - version_info['extra'] = extra_info - return version_info - - -def _decode_id(trans, encoded_id): - # Handle the special case for library folders - if ((len(encoded_id) % 16 == 1) and encoded_id.startswith('F')): - encoded_id = encoded_id[1:] - decoded_id = trans.security.decode_id(encoded_id) - return {"decoded_id": decoded_id} - - -def _get_dynamic_tool_confs(app): - # WARNING: If this method is ever changed so as not to require admin privileges, update the nginx proxy - # documentation, since this path is used as an authentication-by-proxy method for securing other paths on the - # server. A dedicated endpoint should probably be added to do that instead. - confs = app.toolbox.dynamic_confs(include_migrated_tool_conf=True) - return list(map(_tool_conf_to_dict, confs)) - - -def _tool_conf_to_dict(conf): - return dict( - config_filename=conf['config_filename'], - tool_path=conf['tool_path'], - ) - - -def _get_tool_lineages(app): - rval = [] - for id, tool in app.toolbox.tools(): - try: - lineage_dict = tool.lineage.to_dict() - except AttributeError: - pass - else: - entry = {'id': id, 'lineage': lineage_dict} - rval.append(entry) - return rval - - -def _reload_toolbox(app): - app.queue_worker.send_control_task('reload_toolbox') +def get_configuration_manager(app: StructuredApp = Depends(get_app)) -> ConfigurationManager: + return ConfigurationManager(app) @cbv(router) class FastAPIConfiguration: + configuration_manager: ConfigurationManager = Depends(get_configuration_manager) @router.get('/api/whoami') def whoami(self, user: User = Depends(get_user)) -> Optional[UserModel]: """Return information about the current authenticated user.""" - return user_to_model(user) + return _user_to_model(user) @router.get('/api/configuration') def index( - self, - trans: ProvidesUserContext = Depends(get_trans), - view: Optional[str] = SerializationViewQueryParam, - keys: Optional[str] = SerializationKeysQueryParam, + self, + trans: ProvidesUserContext = Depends(get_trans), + view: Optional[str] = SerializationViewQueryParam, + keys: Optional[str] = SerializationKeysQueryParam, ) -> Dict[str, Any]: - """Return an object containing exposable configuration settings.""" - return _get_configuration(trans, view, keys) + """ + Return an object containing exposable configuration settings. + + A more complete list is returned if the user is an admin. + Pass in `view` and a comma-seperated list of keys to control which + configuration settings are returned. + """ + return _index(self.configuration_manager, trans, view, keys) @router.get('/api/version') - def version(self, app: StructuredApp = Depends(get_app)) -> Dict[str, Any]: + def version(self) -> Dict[str, Any]: """Return Galaxy version information: major/minor version, optional extra info.""" - return _get_version(app) + return self.configuration_manager.version() - @router.get('/api/configuration/dynamic_tool_confs', dependencies=[Depends(get_admin_user)]) - def dynamic_tool_confs(self, app: StructuredApp = Depends(get_app)) -> List[Dict[str, str]]: - return _get_dynamic_tool_confs(app) + @router.get('/api/configuration/dynamic_tool_confs', dependencies=[AdminUserRequired]) + def dynamic_tool_confs(self) -> List[Dict[str, str]]: + return self.configuration_manager.dynamic_tool_confs() - @router.get('/api/configuration/decode/{encoded_id}', dependencies=[Depends(get_admin_user)]) - def decode_id(self, trans: ProvidesAppContext = Depends(get_trans), *, encoded_id: str) -> Dict[str, int]: + @router.get('/api/configuration/decode/{encoded_id}', dependencies=[AdminUserRequired]) + def decode_id( + self, + trans: ProvidesAppContext = Depends(get_trans), + *, + encoded_id: str + ) -> Dict[str, int]: """Decode a given id.""" - return _decode_id(trans, encoded_id) + return self.configuration_manager.decode_id(trans, encoded_id) - @router.get('/api/configuration/tool_lineages', dependencies=[Depends(get_admin_user)]) - def tool_lineages(self, app: StructuredApp = Depends(get_app)): - return _get_tool_lineages(app) + @router.get('/api/configuration/tool_lineages', dependencies=[AdminUserRequired]) + def tool_lineages(self) -> List[Dict[str, Dict]]: + """Return tool lineages for tools that have them.""" + return self.configuration_manager.tool_lineages() @router.put('/api/configuration/toolbox') - def reload_toolbox(self, app: StructuredApp = Depends(get_app)): + def reload_toolbox(self): """Reload the Galaxy toolbox (but not individual tools).""" - _reload_toolbox(app) + self.configuration_manager.reload_toolbox() class ConfigurationController(BaseAPIController): + def __init__(self, app: StructuredApp): + super().__init__(app) + self.configuration_manager = ConfigurationManager(app) + @expose_api def whoami(self, trans, **kwd): """ @@ -208,7 +125,7 @@ class ConfigurationController(BaseAPIController): :rtype: dict """ user = UserManager(self.app).current_user(trans) - return user_to_model(user) + return _user_to_model(user) @expose_api_anonymous_and_sessionless def index(self, trans, **kwd): @@ -216,13 +133,14 @@ class ConfigurationController(BaseAPIController): GET /api/configuration Return an object containing exposable configuration settings. - Note: a more complete list is returned if the user is an admin. + A more complete list is returned if the user is an admin. + Pass in `view` and a comma-seperated list of keys to control which + configuration settings are returned. """ - view, keys = kwd.get('view'), kwd.get('keys') - return _get_configuration(trans, view, keys) + return self.get_config_dict(trans, **kwd) @expose_api_anonymous_and_sessionless - def version(self, trans, **kwds): + def version(self, trans, **kwd): """ GET /api/version Return Galaxy version information: major/minor version, optional extra info. @@ -230,23 +148,23 @@ class ConfigurationController(BaseAPIController): :rtype: dict :returns: dictionary with major version keyed on 'version_major' """ - return _get_version(self.app) + return self.configuration_manager.version() @require_admin @expose_api - def dynamic_tool_confs(self, trans): - return _get_dynamic_tool_confs(self.app) + def dynamic_tool_confs(self, trans, **kwds): + return self.configuration_manager.dynamic_tool_confs() @require_admin @expose_api def decode_id(self, trans, encoded_id, **kwds): """Decode a given id.""" - return _decode_id(trans, encoded_id) + return self.configuration_manager.decode_id(trans, encoded_id) @require_admin @expose_api - def tool_lineages(self, trans): - return _get_tool_lineages(self.app) + def tool_lineages(self, trans, **kwds): + return self.configuration_manager.tool_lineages() @require_admin @expose_api @@ -255,8 +173,18 @@ class ConfigurationController(BaseAPIController): PUT /api/configuration/toolbox Reload the Galaxy toolbox (but not individual tools). """ - _reload_toolbox(self.app) + self.configuration_manager.reload_toolbox() - def get_config_dict(self, trans, return_admin=False, view=None, keys=None, default_view='all'): - # Method left for backward compatibility (templates/galaxy_client_app.mako). - return get_config_dict(self.app, return_admin, view=view, keys=keys, default_view=default_view) + def get_config_dict(self, trans, **kwd): + """Return an object containing exposable configuration settings.""" + view, keys = kwd.get('view'), kwd.get('keys') + return _index(self.configuration_manager, trans, view, keys) + + +def _user_to_model(user): + return UserModel(**user.to_dict()) if user else None + + +def _index(manager, trans, view, keys): + serialization_params = parse_serialization_params(view, keys, 'all') + return manager.get_configuration(trans, serialization_params) diff --git a/templates/galaxy_client_app.mako b/templates/galaxy_client_app.mako index 7c22f5d77fa..a907b6cec54 100644 --- a/templates/galaxy_client_app.mako +++ b/templates/galaxy_client_app.mako @@ -81,7 +81,7 @@ ${ h.dumps( dictionary, indent=( 2 if trans.debug else 0 ) ) } try: controller = trans.webapp.api_controllers.get( 'configuration', None ) if controller: - config_dict = controller.get_config_dict( trans, trans.user_is_admin ) + config_dict = controller.get_config_dict(trans) except Exception as exc: pass return config_dict