mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Merge pull request #11872 from davelopez/fastapi_entry_points
Create FastAPI entry points for Tool Shed and Reports
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user