import logging import os from pathlib import Path from typing import ( Any, cast, Optional, ) from a2wsgi import WSGIMiddleware from fastapi import ( Depends, FastAPI, ) from fastapi.responses import HTMLResponse from fastapi.staticfiles import StaticFiles from slowapi import ( _rate_limit_exceeded_handler, Limiter, ) from slowapi.errors import RateLimitExceeded from slowapi.util import get_remote_address from galaxy.webapps.base.api import ( add_exception_handler, add_request_id_middleware, include_all_package_routers, ) from galaxy.webapps.openapi.utils import get_openapi from tool_shed.webapp.api2 import ( ensure_valid_session, get_trans, ) log = logging.getLogger(__name__) 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."}, {"name": "legacy_install", "description": "Legacy Galaxy install protocol endpoints."}, ] # Set this if asset handling should be sent to vite. # Run vite with: # pnpm dev # Start tool shed with: # TOOL_SHED_VITE_PORT=4040 ./run_tool_shed.sh TOOL_SHED_VITE_PORT: Optional[str] = os.environ.get("TOOL_SHED_VITE_PORT", None) TOOL_SHED_FRONTEND_TARGET: str = os.environ.get("TOOL_SHED_FRONTEND_TARGET") or "auto" # auto, src, or node TOOL_SHED_USE_HMR: bool = TOOL_SHED_VITE_PORT is not None WEBAPP_DIR = Path(__file__).parent.resolve() FRONTEND = WEBAPP_DIR / "frontend" FRONTEND_DIST = FRONTEND / "dist" INSTALLED_FRONTEND = WEBAPP_DIR / "node_modules" / "@galaxyproject" / "tool-shed-frontend" / "dist" INDEX_FILENAME = "index.html" def find_frontend_target() -> Path: src_target = FRONTEND_DIST node_target = INSTALLED_FRONTEND if TOOL_SHED_FRONTEND_TARGET == "src": return src_target elif TOOL_SHED_FRONTEND_TARGET == "node": return node_target elif src_target.exists(): return src_target else: return node_target def frontend_controller(app): shed_entry_point = "main.ts" vite_runtime = "@vite/client" def index(trans=Depends(get_trans)): if TOOL_SHED_USE_HMR: if TOOL_SHED_FRONTEND_TARGET != "auto": raise Exception("Cannot configure HMR and with this frontend target.") index = FRONTEND / INDEX_FILENAME index_html = index.read_text() index_html = index_html.replace( f"""""", f"""""", ) else: index = find_frontend_target() / INDEX_FILENAME index_html = index.read_text() ensure_valid_session(trans) cookie = trans.session_csrf_token r: HTMLResponse = cast(HTMLResponse, trans.response) r.set_cookie("session_csrf_token", cookie) return index_html return app, index def frontend_route(controller, path): app, index = controller app.get(path, response_class=HTMLResponse)(index) FRONT_END_ROUTES = [ "/", "/admin", "/login", "/register", "/logout_success", "/login_success", "/registration_success", "/help", "/repositories_by_search", "/repositories_by_category", "/repositories_by_category/{category_id}", "/repositories_by_owner", "/repositories_by_owner/{username}", "/repositories/{repository_id}", "/repositories/{repository_id}/metadata-inspector", "/repositories_search", "/tools/{trs_tool_id}/versions/{version}", "/_component_showcase", "/user/api_key", "/user/change_password", "/user/change_password_success", "/view/{username}", "/view/{username}/{repository_name}", "/view/{username}/{repository_name}/{changeset_revision}", ] limiter = Limiter(key_func=get_remote_address) def initialize_fast_app(gx_webapp, tool_shed_app): app = get_fastapi_instance() add_exception_handler(app) add_request_id_middleware(app) app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) # type: ignore[arg-type] def mount_static(directory: Path): name = directory.name if directory.exists(): app.mount(f"/{name}", StaticFiles(directory=directory), name=name) controller = frontend_controller(app) for route in FRONT_END_ROUTES: frontend_route(controller, route) mount_static(FRONTEND / "static") if TOOL_SHED_USE_HMR: mount_static(FRONTEND / "node_modules") else: mount_static(find_frontend_target() / "assets") include_all_package_routers(app, "tool_shed.webapp.api2") wsgi_handler = WSGIMiddleware(gx_webapp) tool_shed_app.haltables.append(("WSGI Middleware threadpool", wsgi_handler.executor.shutdown)) # https://github.com/abersheeran/a2wsgi/issues/44 app.mount("/", wsgi_handler) # type: ignore[arg-type] 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", )