Merge pull request #11872 from davelopez/fastapi_entry_points

Create FastAPI entry points for Tool Shed and Reports
This commit is contained in:
John Chilton
2021-04-21 14:23:04 -04:00
committed by GitHub
10 changed files with 317 additions and 78 deletions
+81 -4
View File
@@ -3,12 +3,24 @@
This is for use by web framework code and scripts (e.g. scripts/galaxy_main.py).
"""
import os
from typing import (
List,
NamedTuple,
Optional,
)
from galaxy.util.properties import find_config_file
from galaxy.web_stack import get_app_kwds
DEFAULT_INIS = ["config/galaxy.yml", "config/galaxy.ini", "universe_wsgi.ini", "config/galaxy.yml.sample"]
DEFAULT_INI_APP = "main"
DEFAULT_CONFIG_SECTION = "galaxy"
def default_relative_config_paths_for(app_name: str) -> List[str]:
return [f"config/{app_name}.yml", f"config/{app_name}.ini", "universe_wsgi.ini", f"config/{app_name}.yml.sample"]
def absolute_config_path(path, galaxy_root):
if path and not os.path.isabs(path):
path = os.path.join(galaxy_root, path)
@@ -19,18 +31,83 @@ def config_is_ini(config_file):
return config_file and (config_file.endswith('.ini') or config_file.endswith('.ini.sample'))
def find_config(supplied_config, galaxy_root):
def find_config(supplied_config, galaxy_root, app_name="galaxy"):
if supplied_config:
return supplied_config
if galaxy_root is None:
return os.path.abspath('galaxy.yml')
return os.path.abspath(f'{app_name}.yml')
# If not explicitly supplied an config, check galaxy.ini and then
# just resort to sample if that has not been configured.
for guess in DEFAULT_INIS:
guess = None
for guess in default_relative_config_paths_for(app_name):
config_path = os.path.join(galaxy_root, guess)
if os.path.exists(config_path):
return config_path
return guess
class WebappSetupProps(NamedTuple):
"""Basic properties to provide information about the App and the environment variables
used to resolve the App configuration."""
app_name: str
default_section_name: str
env_config_file: str
env_config_section: Optional[str] = None
check_galaxy_root: bool = False
class WebappConfig(NamedTuple):
"""The resolved configuration values for a Webapp."""
global_conf: dict
load_app_kwds: dict
wsgi_preflight: bool = False
class WebappConfigResolver:
def __init__(self, props: WebappSetupProps) -> None:
self.props = props
self.app_kwds = get_app_kwds(props.default_section_name, props.app_name)
self.config_file = self._resolve_config_file_path()
self.is_ini_file = config_is_ini(self.config_file)
self.config_section = self._resolve_section_name()
self._update_kwds()
def resolve_config(self) -> WebappConfig:
global_conf = {}
if self.is_ini_file:
global_conf["__file__"] = self.config_file
return WebappConfig(global_conf=global_conf, load_app_kwds=self.app_kwds)
def _resolve_config_file_path(self) -> str:
config_file = self.app_kwds.get("config_file")
if not config_file and os.environ.get(self.props.env_config_file):
config_file = os.path.abspath(os.environ[self.props.env_config_file])
elif self.props.check_galaxy_root:
galaxy_root = self.app_kwds.get("galaxy_root") or os.environ.get("GALAXY_ROOT_DIR")
config_file = find_config(config_file, galaxy_root, app_name=self.props.app_name)
config_file = absolute_config_path(config_file, galaxy_root=galaxy_root)
else:
config_file = find_config_file([self.props.app_name])
if not config_file or not os.path.exists(config_file):
raise FileNotFoundError(f"Can not find a configuration file for {self.props.app_name}")
return config_file
def _resolve_section_name(self) -> str:
config_section = self.props.default_section_name
if self.props.env_config_section and self.props.env_config_section in os.environ:
config_section = os.environ[self.props.env_config_section]
elif self.is_ini_file:
config_section = f"app:{DEFAULT_INI_APP}"
return config_section
def _update_kwds(self) -> None:
if "config_file" not in self.app_kwds:
self.app_kwds["config_file"] = self.config_file
if "config_section" not in self.app_kwds:
self.app_kwds["config_section"] = self.config_section
+49
View File
@@ -0,0 +1,49 @@
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from starlette.responses import Response
try:
from starlette_context.middleware import RawContextMiddleware
from starlette_context.plugins import RequestIdPlugin
except ImportError:
pass
from galaxy.exceptions import MessageException
from galaxy.web.framework.base import walk_controller_modules
from galaxy.web.framework.decorators import (
api_error_message,
validation_error_to_message_exception
)
def add_exception_handler(
app: FastAPI
) -> None:
@app.exception_handler(RequestValidationError)
async def validate_exception_middleware(request: Request, exc: RequestValidationError) -> Response:
exc = validation_error_to_message_exception(exc)
error_dict = api_error_message(None, exception=exc)
return JSONResponse(
status_code=400,
content=error_dict
)
@app.exception_handler(MessageException)
async def message_exception_middleware(request: Request, exc: MessageException) -> Response:
error_dict = api_error_message(None, exception=exc)
return JSONResponse(
status_code=exc.status_code,
content=error_dict
)
def add_request_id_middleware(app: FastAPI):
app.add_middleware(RawContextMiddleware, plugins=(RequestIdPlugin(force_new_uuid=True),))
def include_all_package_routers(app: FastAPI, package_name: str):
for _, module in walk_controller_modules(package_name):
router = getattr(module, "router", None)
if router:
app.include_router(router)
+8 -43
View File
@@ -1,20 +1,12 @@
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.wsgi import WSGIMiddleware
from fastapi.responses import JSONResponse
from starlette.middleware.cors import CORSMiddleware
from starlette.responses import Response
try:
from starlette_context.middleware import RawContextMiddleware
from starlette_context.plugins import RequestIdPlugin
except ImportError:
pass
from galaxy.exceptions import MessageException
from galaxy.web.framework.base import walk_controller_modules
from galaxy.web.framework.decorators import (
api_error_message,
validation_error_to_message_exception
from galaxy.webapps.base.api import (
add_exception_handler,
add_request_id_middleware,
include_all_package_routers,
)
from galaxy.webapps.base.webapp import config_allows_origin
@@ -66,28 +58,6 @@ class GalaxyCORSMiddleware(CORSMiddleware):
return config_allows_origin(origin, self.config)
def add_exception_handler(
app: FastAPI
) -> None:
@app.exception_handler(RequestValidationError)
async def validate_exception_middleware(request: Request, exc: RequestValidationError) -> Response:
exc = validation_error_to_message_exception(exc)
error_dict = api_error_message(None, exception=exc)
return JSONResponse(
status_code=400,
content=error_dict
)
@app.exception_handler(MessageException)
async def message_exception_middleware(request: Request, exc: MessageException) -> Response:
error_dict = api_error_message(None, exception=exc)
return JSONResponse(
status_code=exc.status_code,
content=error_dict
)
def add_galaxy_middleware(app: FastAPI, gx_app):
x_frame_options = getattr(gx_app.config, 'x_frame_options', None)
if x_frame_options:
@@ -117,21 +87,16 @@ def add_galaxy_middleware(app: FastAPI, gx_app):
return response
def add_request_id_middleware(app: FastAPI):
app.add_middleware(RawContextMiddleware, plugins=(RequestIdPlugin(force_new_uuid=True),))
def initialize_fast_app(gx_webapp, gx_app):
app = FastAPI(
openapi_tags=api_tags_metadata
title="Galaxy API",
docs_url="/api/docs",
openapi_tags=api_tags_metadata,
)
add_exception_handler(app)
add_galaxy_middleware(app, gx_app)
add_request_id_middleware(app)
include_all_package_routers(app, 'galaxy.webapps.galaxy.api')
wsgi_handler = WSGIMiddleware(gx_webapp)
for _, module in walk_controller_modules('galaxy.webapps.galaxy.api'):
router = getattr(module, "router", None)
if router:
app.include_router(router)
app.mount('/', wsgi_handler)
return app
+12 -30
View File
@@ -40,43 +40,25 @@ with the following command-line.
gunicorn 'galaxy.webapps.galaxy.fast_factory:factory()' --env GALAXY_CONFIG_FILE=config/galaxy.ini --pythonpath lib -w 4 -k uvicorn.workers.UvicornWorker
"""
import os
from galaxy.main_config import (
absolute_config_path,
config_is_ini,
DEFAULT_CONFIG_SECTION,
DEFAULT_INI_APP,
find_config,
WebappConfigResolver,
WebappSetupProps,
)
from galaxy.web_stack import get_app_kwds
from galaxy.webapps.galaxy.buildapp import app_pair
from .fast_app import initialize_fast_app
def factory():
kwds = get_app_kwds("galaxy", "galaxy")
config_file = kwds.get("config_file")
if not config_file and os.environ.get('GALAXY_CONFIG_FILE'):
config_file = os.path.abspath(os.environ["GALAXY_CONFIG_FILE"])
else:
galaxy_root = kwds.get("galaxy_root") or os.environ.get("GALAXY_ROOT_DIR")
config_file = find_config(config_file, galaxy_root)
config_file = absolute_config_path(config_file, galaxy_root=galaxy_root)
if "GALAXY_CONFIG_SECTION" in os.environ:
config_section = os.environ["GALAXY_CONFIG_SECTION"]
elif config_is_ini(config_file):
config_section = "app:%s" % DEFAULT_INI_APP
else:
config_section = DEFAULT_CONFIG_SECTION
if 'config_file' not in kwds:
kwds['config_file'] = config_file
if 'config_section' not in kwds:
kwds['config_section'] = config_section
global_conf = {}
if config_is_ini(config_file):
global_conf["__file__"] = config_file
gx_webapp, gx_app = app_pair(global_conf=global_conf, load_app_kwds=kwds, wsgi_preflight=False)
props = WebappSetupProps(
app_name='galaxy',
default_section_name=DEFAULT_CONFIG_SECTION,
env_config_file='GALAXY_CONFIG_FILE',
env_config_section='GALAXY_CONFIG_SECTION',
check_galaxy_root=True
)
config_provider = WebappConfigResolver(props)
config = config_provider.resolve_config()
gx_webapp, gx_app = app_pair(global_conf=config.global_conf, load_app_kwds=config.load_app_kwds, wsgi_preflight=config.wsgi_preflight)
return initialize_fast_app(gx_webapp, gx_app)
+9 -1
View File
@@ -4,17 +4,21 @@ import time
import galaxy.model
from galaxy.config import configure_logging
from galaxy.model.base import SharedModelMapping
from galaxy.security import idencoding
from galaxy.structured_app import BasicApp
from galaxy.web_stack import application_stack_instance
from . import config
log = logging.getLogger(__name__)
class UniverseApplication:
class UniverseApplication(BasicApp):
"""Encapsulates the state of a Universe application"""
def __init__(self, **kwargs):
super().__init__()
self[BasicApp] = self
log.debug("python path is: %s", ", ".join(sys.path))
self.name = "reports"
# Read config file and check for errors
@@ -38,6 +42,10 @@ class UniverseApplication:
self.targets_mysql = 'mysql' in self.config.database_connection
# Security helper
self.security = idencoding.IdEncodingHelper(id_secret=self.config.id_secret)
self._register_singleton(idencoding.IdEncodingHelper, self.security)
self._register_singleton(SharedModelMapping, self.model)
# used for cachebusting -- refactor this into a *SINGLE* UniverseApplication base.
self.server_starttime = int(time.time())
+25
View File
@@ -0,0 +1,25 @@
from fastapi import FastAPI
from fastapi.middleware.wsgi import WSGIMiddleware
from galaxy.webapps.base.api import (
add_exception_handler,
add_request_id_middleware,
include_all_package_routers,
)
def initialize_fast_app(gx_webapp):
app = FastAPI(
title="Galaxy Reports API",
description=(
"This API will give you insights into the Galaxy instance's usage and load. "
"It aims to provide data about users, jobs, workflows, disk space, and much more."
),
docs_url="/api/docs",
)
add_exception_handler(app)
add_request_id_middleware(app)
include_all_package_routers(app, 'galaxy.webapps.reports.api')
wsgi_handler = WSGIMiddleware(gx_webapp)
app.mount('/', wsgi_handler)
return app
@@ -0,0 +1,54 @@
"""Module containing factory class for building uvicorn app for the Galaxy Tool Shed.
Information on uvicorn, its various settings, and how to invoke it can
be found at https://www.uvicorn.org/.
The Galaxy Tool Shed can be launched with uvicorn using the following invocation:
::
uvicorn --app-dir lib --factory galaxy.webapps.reports.fast_factory:factory
Use the environment variable ``GALAXY_REPORTS_CONFIG`` to specify a Galaxy Reports
configuration file.
::
GALAXY_REPORTS_CONFIG=config/reports.yml uvicorn --app-dir lib --factory galaxy.webapps.reports.fast_factory:factory
.. note::
Information on additional ways to configure uvicorn can be found at
https://www.uvicorn.org/.
`Gunicorn <https://docs.gunicorn.org/en/stable/index.html>`__ is a server with
more complex management options.
This factory function can be executed as a uvicorn worker managed with gunicorn
with the following command-line.
::
gunicorn 'galaxy.webapps.reports.fast_factory:factory()' --env GALAXY_REPORTS_CONFIG=config/reports.yml --pythonpath lib -w 4 -k uvicorn.workers.UvicornWorker
"""
from galaxy.main_config import (
WebappConfigResolver,
WebappSetupProps
)
from galaxy.webapps.reports.buildapp import app_factory
from .fast_app import initialize_fast_app
def factory():
props = WebappSetupProps(
app_name='reports',
default_section_name='reports',
env_config_file='GALAXY_REPORTS_CONFIG',
)
config_provider = WebappConfigResolver(props)
config = config_provider.resolve_config()
gx_webapp = app_factory(global_conf=config.global_conf, load_app_kwds=config.load_app_kwds, wsgi_preflight=config.wsgi_preflight)
return initialize_fast_app(gx_webapp)
+24
View File
@@ -0,0 +1,24 @@
from fastapi import FastAPI
from fastapi.middleware.wsgi import WSGIMiddleware
from galaxy.webapps.base.api import (
add_exception_handler,
add_request_id_middleware,
include_all_package_routers,
)
def initialize_fast_app(gx_webapp):
app = FastAPI(
title="Galaxy Tool Shed API",
description=(
"This API allows you to manage the Tool Shed repositories."
),
docs_url="/api/docs",
)
add_exception_handler(app)
add_request_id_middleware(app)
include_all_package_routers(app, 'tool_shed.webapp.api')
wsgi_handler = WSGIMiddleware(gx_webapp)
app.mount('/', wsgi_handler)
return app
+55
View File
@@ -0,0 +1,55 @@
"""Module containing factory class for building uvicorn app for the Galaxy Tool Shed.
Information on uvicorn, its various settings, and how to invoke it can
be found at https://www.uvicorn.org/.
The Galaxy Tool Shed can be launched with uvicorn using the following invocation:
::
uvicorn --app-dir lib --factory tool_shed.webapp.fast_factory:factory
Use the environment variable ``TOOL_SHED_CONFIG_FILE`` to specify a Tool Shed
configuration file.
::
TOOL_SHED_CONFIG_FILE=config/tool_shed.yml uvicorn --app-dir lib --factory tool_shed.webapp.fast_factory:factory
.. note::
Information on additional ways to configure uvicorn can be found at
https://www.uvicorn.org/.
`Gunicorn <https://docs.gunicorn.org/en/stable/index.html>`__ is a server with
more complex management options.
This factory function can be executed as a uvicorn worker managed with gunicorn
with the following command-line.
::
gunicorn 'tool_shed.webapp.fast_factory:factory()' --env TOOL_SHED_CONFIG_FILE=config/tool_shed.yml --pythonpath lib -w 4 -k uvicorn.workers.UvicornWorker
"""
from galaxy.main_config import (
WebappConfigResolver,
WebappSetupProps
)
from tool_shed.webapp.buildapp import app_factory
from tool_shed.webapp.config import TOOLSHED_APP_NAME
from .fast_app import initialize_fast_app
def factory():
props = WebappSetupProps(
app_name=TOOLSHED_APP_NAME,
default_section_name=TOOLSHED_APP_NAME,
env_config_file='TOOL_SHED_CONFIG_FILE',
)
config_provider = WebappConfigResolver(props)
config = config_provider.resolve_config()
gx_webapp = app_factory(global_conf=config.global_conf, load_app_kwds=config.load_app_kwds, wsgi_preflight=config.wsgi_preflight)
return initialize_fast_app(gx_webapp)