This commit is contained in:
Shahar Ilany 2025-12-16 21:07:30 +00:00 committed by GitHub
commit 6b3b0ab875
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 206 additions and 1 deletions

View File

@ -1,12 +1,13 @@
from fastapi import FastAPI
from fastapi.openapi.docs import (
get_redoc_html,
get_stoplight_elements_html,
get_swagger_ui_html,
get_swagger_ui_oauth2_redirect_html,
)
from fastapi.staticfiles import StaticFiles
app = FastAPI(docs_url=None, redoc_url=None)
app = FastAPI(docs_url=None, redoc_url=None, stoplight_elements_url=None)
app.mount("/static", StaticFiles(directory="static"), name="static")
@ -36,6 +37,16 @@ async def redoc_html():
)
@app.get("/elements", include_in_schema=False)
async def elements_html():
return get_stoplight_elements_html(
openapi_url=app.openapi_url,
title=app.title + " - Elements",
stoplight_elements_js_url="/static/web-components.min.js",
stoplight_elements_css_url="/static/styles.min.css",
)
@app.get("/users/{username}")
async def read_user(username: str):
return {"message": f"Hello {username}"}

View File

@ -0,0 +1,34 @@
from fastapi import FastAPI
from fastapi.openapi.docs import (
LayoutOptions,
RouterOptions,
TryItCredentialPolicyOptions,
get_stoplight_elements_html,
)
app = FastAPI(stoplight_elements_url=None)
@app.get("/elements", include_in_schema=False)
async def elements_html():
return get_stoplight_elements_html(
openapi_url=app.openapi_url,
title=app.title + " - Elements",
stoplight_elements_js_url="https://unpkg.com/@stoplight/elements/web-components.min.js",
stoplight_elements_css_url="https://unpkg.com/@stoplight/elements/styles.min.css",
stoplight_elements_favicon_url="https://fastapi.tiangolo.com/img/favicon.png",
api_description_document="",
base_path="",
hide_internal=False,
hide_try_it=False,
try_it_cors_proxy="",
try_it_credential_policy=TryItCredentialPolicyOptions.OMIT,
layout=LayoutOptions.SIDEBAR,
logo="",
router=RouterOptions.HISTORY,
)
@app.get("/users/{username}")
async def read_user(username: str):
return {"message": f"Hello {username}"}

View File

@ -26,6 +26,7 @@ from fastapi.logger import logger
from fastapi.middleware.asyncexitstack import AsyncExitStackMiddleware
from fastapi.openapi.docs import (
get_redoc_html,
get_stoplight_elements_html,
get_swagger_ui_html,
get_swagger_ui_oauth2_redirect_html,
)
@ -466,6 +467,30 @@ class FastAPI(Starlette):
"""
),
] = "/docs/oauth2-redirect",
stoplight_elements_url: Annotated[
Optional[str],
Doc(
"""
The path to the alternative automatic interactive API documentation
provided by Stoplight Elements.
The default URL is `/elements`. You can disable it by setting it to `None`.
If `openapi_url` is set to `None`, this will be automatically disabled.
Read more in the
[FastAPI docs for Metadata and Docs URLs](https://fastapi.tiangolo.com/tutorial/metadata/#docs-urls).
**Example**
```python
from fastapi import FastAPI
app = FastAPI(docs_url="/documentation", stoplight_elements_url="elementsdocs")
```
"""
),
] = "/elements",
swagger_ui_init_oauth: Annotated[
Optional[Dict[str, Any]],
Doc(
@ -869,6 +894,7 @@ class FastAPI(Starlette):
self.docs_url = docs_url
self.redoc_url = redoc_url
self.swagger_ui_oauth2_redirect_url = swagger_ui_oauth2_redirect_url
self.stoplight_elements_url = stoplight_elements_url
self.swagger_ui_init_oauth = swagger_ui_init_oauth
self.swagger_ui_parameters = swagger_ui_parameters
self.servers = servers or []
@ -1133,6 +1159,21 @@ class FastAPI(Starlette):
self.add_route(self.redoc_url, redoc_html, include_in_schema=False)
if self.openapi_url and self.stoplight_elements_url:
async def stoplight_elements_html(req: Request) -> HTMLResponse:
root_path = req.scope.get("root_path", "").rstrip("/")
openapi_url = root_path + self.openapi_url
return get_stoplight_elements_html(
openapi_url=openapi_url, title=self.title + " - Stoplight Elements"
)
self.add_route(
self.stoplight_elements_url,
stoplight_elements_html,
include_in_schema=False,
)
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if self.root_path:
scope["root_path"] = self.root_path

View File

@ -1,4 +1,5 @@
import json
from enum import Enum
from typing import Any, Dict, Optional
from annotated_doc import Doc
@ -343,3 +344,70 @@ def get_swagger_ui_oauth2_redirect_html() -> HTMLResponse:
</html>
"""
return HTMLResponse(content=html)
class TryItCredentialPolicyOptions(Enum):
OMIT = "omit"
include = "include"
SAME_ORIGIN = "same-origin"
class LayoutOptions(Enum):
SIDEBAR = "sidebar"
STACKED = "stacked"
class RouterOptions(Enum):
HISTORY = "history"
HASH = "hash"
MEMORY = "memory"
STATIC = "static"
def get_stoplight_elements_html(
*,
openapi_url: str,
title: str,
stoplight_elements_js_url: str = "https://unpkg.com/@stoplight/elements/web-components.min.js",
stoplight_elements_css_url: str = "https://unpkg.com/@stoplight/elements/styles.min.css",
stoplight_elements_favicon_url: str = "https://fastapi.tiangolo.com/img/favicon.png",
api_description_document: str = "",
base_path: str = "",
hide_internal: bool = False,
hide_try_it: bool = False,
try_it_cors_proxy: str = "",
try_it_credential_policy: TryItCredentialPolicyOptions = TryItCredentialPolicyOptions.OMIT,
layout: LayoutOptions = LayoutOptions.SIDEBAR,
logo: str = "",
router: RouterOptions = RouterOptions.HISTORY,
) -> HTMLResponse:
html = f"""
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>{title}</title>
<link rel="shortcut icon" href="{stoplight_elements_favicon_url}">
<script src="{stoplight_elements_js_url}"></script>
<link rel="stylesheet" href="{stoplight_elements_css_url}">
</head>
<body>
<elements-api
{f'apiDescriptionUrl="{openapi_url}"' if openapi_url != "" else ""}
{f'apiDescriptionDocument="{api_description_document}"' if api_description_document != "" else ""}
{f'basePath="{base_path}"' if base_path != "" else ""}
{'hideInternal="true"' if hide_internal is True else ""}
{'hideTryIt="true"' if hide_try_it is True else ""}
{f'tryItCorsProxy="{try_it_cors_proxy}"' if try_it_cors_proxy != "" else ""}
tryItCredentialPolicy="{try_it_credential_policy.value}"
layout="{layout.value}"
{f'logo="{logo}"' if logo != "" else ""}
router="{router.value}"
/>
</body>
</html>
"""
return HTMLResponse(html)

View File

@ -0,0 +1,25 @@
from fastapi import FastAPI
from fastapi.testclient import TestClient
app = FastAPI(title="Example App")
@app.get("/a/b")
async def get_a_and_b():
return {"a": "b"}
client = TestClient(app)
def test_elements_uit():
response = client.get("/elements")
assert response.status_code == 200, response.text
print(response.text)
assert app.title in response.text
assert "Stoplight" in response.text
def test_response():
response = client.get("/a/b")
assert response.json() == {"a": "b"}

View File

@ -36,6 +36,13 @@ def test_redoc_html(client: TestClient):
assert "/static/redoc.standalone.js" in response.text
def test_elements_html(client: TestClient):
response = client.get("/elements")
assert response.status_code == 200, response.text
assert "/static/web-components.min.js" in response.text
assert "/static/styles.min.css" in response.text
def test_api(client: TestClient):
response = client.get("/users/john")
assert response.status_code == 200, response.text

View File

@ -0,0 +1,19 @@
from fastapi.testclient import TestClient
from docs_src.extending_openapi.tutorial006 import app
client = TestClient(app)
def test_swagger_ui():
response = client.get("/elements")
assert response.status_code == 200, response.text
assert 'router="history"' in response.text
assert 'layout="sidebar"' in response.text
assert 'tryItCredentialPolicy="omit"' in response.text
def test_get_users():
response = client.get("/users/foo")
assert response.status_code == 200, response.text
assert response.json() == {"message": "Hello foo"}