From b1ccdc4d169830c5419e12284a62fcf09b3ec0df Mon Sep 17 00:00:00 2001 From: Ilya Kreymer Date: Thu, 18 Jul 2024 11:11:38 -0700 Subject: [PATCH] OpenAPI Metadata for API Endpoints (#1941) - Updates the `/docs` and `/redoc` API endpoints to have better metadata, including using Browsertrix favicon and our logo for the `/redoc` endpoint. - add new logo file 'docs-logo.svg' to root Based on info at: https://fastapi.tiangolo.com/how-to/extending-openapi/ https://fastapi.tiangolo.com/tutorial/metadata/ --------- Co-authored-by: Henry Wilkinson --- backend/btrixcloud/main.py | 68 +++++++++++++++++++--- backend/test/test_api.py | 32 ++++++++++ frontend/src/assets/favicons/docs-logo.svg | 1 + 3 files changed, 94 insertions(+), 7 deletions(-) create mode 100644 backend/test/test_api.py create mode 100644 frontend/src/assets/favicons/docs-logo.svg diff --git a/backend/btrixcloud/main.py b/backend/btrixcloud/main.py index 015972ef..3fa96f87 100644 --- a/backend/btrixcloud/main.py +++ b/backend/btrixcloud/main.py @@ -11,6 +11,9 @@ from fastapi import FastAPI, HTTPException from fastapi.responses import JSONResponse from fastapi.routing import APIRouter +from fastapi.openapi.utils import get_openapi +from fastapi.openapi.docs import get_swagger_ui_html, get_redoc_html + from .db import init_db, await_db_and_migrations, update_and_prepare_db from .emailsender import EmailSender @@ -34,18 +37,45 @@ from .subs import init_subs_api from .crawlmanager import CrawlManager from .utils import run_once_lock, register_exit_handler, is_bool - +from .version import __version__ API_PREFIX = "/api" -app_root = FastAPI( - docs_url=API_PREFIX + "/docs", - redoc_url=API_PREFIX + "/redoc", - openapi_url=API_PREFIX + "/openapi.json", -) + +OPENAPI_URL = API_PREFIX + "/openapi.json" + +app_root = FastAPI(docs_url=None, redoc_url=None, OPENAPI_URL=OPENAPI_URL) db_inited = {"inited": False} +# ============================================================================ +def make_schema(): + """make custom openapi schema""" + schema = get_openapi( + title="Browsertrix", + description="""\ +The Browsertrix API provides access to all aspects of the Browsertrix app. + +See [https://docs.browsertrix.com/](https://docs.browsertrix.com/) for more info on deploying Browsertrix\ + """, + summary="Browsertrix Crawling System API", + version=__version__, + terms_of_service="http://browsertrix.com/terms", + contact={ + "name": "Browsertrix", + "url": "https://browsertrix.com/", + "email": "info@webrecorder.net", + }, + license_info={ + "name": "AGPL v3", + "url": "https://www.gnu.org/licenses/agpl-3.0.en.html", + }, + routes=app_root.routes, + ) + schema["info"]["x-logo"] = {"url": "/docs-logo.svg"} + return schema + + # ============================================================================ # pylint: disable=too-many-locals, duplicate-code def main(): @@ -200,7 +230,6 @@ def main(): return settings # internal routes - @app.get("/openapi.json", include_in_schema=False) async def openapi() -> JSONResponse: return JSONResponse(app_root.openapi()) @@ -221,6 +250,31 @@ def main(): app_root.include_router(app, prefix=API_PREFIX) + # API Configurations -- needed to provide custom favicon + @app_root.get(API_PREFIX + "/docs", include_in_schema=False) + def overridden_swagger(): + return get_swagger_ui_html( + openapi_url=OPENAPI_URL, + title="Browsertrix API", + swagger_favicon_url="/favicon.ico", + ) + + @app_root.get(API_PREFIX + "/redoc", include_in_schema=False) + def overridden_redoc(): + return get_redoc_html( + openapi_url=OPENAPI_URL, + title="Browsertrix API", + redoc_favicon_url="/favicon.ico", + ) + + def get_api_schema(): + if not app_root.openapi_schema: + app_root.openapi_schema = make_schema() + + return app_root.openapi_schema + + app_root.openapi = get_api_schema # type: ignore + # ============================================================================ @app_root.on_event("startup") diff --git a/backend/test/test_api.py b/backend/test/test_api.py new file mode 100644 index 00000000..ef4d7c11 --- /dev/null +++ b/backend/test/test_api.py @@ -0,0 +1,32 @@ +import requests + +from .conftest import API_PREFIX + + +def test_api_docs(): + r = requests.get(f"{API_PREFIX}/docs") + assert r.status_code == 200 + + text = r.text + assert "Browsertrix API" in text + assert "/favicon.ico" in text + assert "/api/openapi.json" in text + + +def test_api_redoc(): + r = requests.get(f"{API_PREFIX}/redoc") + assert r.status_code == 200 + + text = r.text + assert "Browsertrix API" in text + assert "/favicon.ico" in text + assert "/api/openapi.json" in text + + +def test_api_openapi(): + r = requests.get(f"{API_PREFIX}/openapi.json") + assert r.status_code == 200 + + json = r.json() + assert json["info"]["title"] == "Browsertrix" + assert json["info"]["x-logo"]["url"] == "/docs-logo.svg" diff --git a/frontend/src/assets/favicons/docs-logo.svg b/frontend/src/assets/favicons/docs-logo.svg new file mode 100644 index 00000000..73086f88 --- /dev/null +++ b/frontend/src/assets/favicons/docs-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file