Skip to content

Commit

Permalink
feat: openapi ui for api exploration (#3041)
Browse files Browse the repository at this point in the history
  • Loading branch information
mikeldking authored Apr 30, 2024
1 parent dee6681 commit 5b22961
Show file tree
Hide file tree
Showing 3 changed files with 227 additions and 0 deletions.
9 changes: 9 additions & 0 deletions src/phoenix/server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
from phoenix.server.api.routers.v1 import V1_ROUTES
from phoenix.server.api.schema import schema
from phoenix.server.grpc_server import GrpcServer
from phoenix.server.openapi.docs import get_swagger_ui_html
from phoenix.server.telemetry import initialize_opentelemetry_tracer_provider
from phoenix.trace.schemas import Span

Expand Down Expand Up @@ -239,6 +240,10 @@ async def openapi_schema(request: Request) -> Response:
return schemas.OpenAPIResponse(request=request)


async def api_docs(request: Request) -> Response:
return get_swagger_ui_html(openapi_url="/schema", title="arize-phoenix API")


def create_app(
database_url: str,
export_path: Path,
Expand Down Expand Up @@ -353,6 +358,10 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
{"path": export_path},
),
),
Route(
"/docs",
api_docs,
),
Route(
"/graphql",
graphql,
Expand Down
Empty file.
218 changes: 218 additions & 0 deletions src/phoenix/server/openapi/docs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import json
from typing import Any, Dict, Optional

from starlette.responses import HTMLResponse

swagger_ui_default_parameters: Dict[str, Any] = {
"dom_id": "#swagger-ui",
"layout": "BaseLayout",
"deepLinking": True,
"showExtensions": True,
"showCommonExtensions": True,
}


def get_swagger_ui_html(
*,
openapi_url: str = "/schema",
title: str,
swagger_js_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.9.0/swagger-ui-bundle.js",
swagger_css_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.9.0/swagger-ui.css",
swagger_favicon_url: str = "/favicon.ico",
oauth2_redirect_url: Optional[str] = None,
init_oauth: Optional[str] = None,
swagger_ui_parameters: Optional[Dict[str, Any]] = None,
) -> HTMLResponse:
"""
Generate and return the HTML that loads Swagger UI for the interactive API
docs (normally served at `/docs`).
"""
current_swagger_ui_parameters = swagger_ui_default_parameters.copy()
if swagger_ui_parameters:
current_swagger_ui_parameters.update(swagger_ui_parameters)

html = f"""
<!DOCTYPE html>
<html>
<head>
<link type="text/css" rel="stylesheet" href="{swagger_css_url}">
<link rel="shortcut icon" href="{swagger_favicon_url}">
<title>{title}</title>
</head>
<body>
<div id="swagger-ui">
</div>
<script src="{swagger_js_url}"></script>
<!-- `SwaggerUIBundle` is now available on the page -->
<script>
const ui = SwaggerUIBundle({{
url: '{openapi_url}',
"""

for key, value in current_swagger_ui_parameters.items():
html += f"{json.dumps(key)}: {json.dumps(value)},\n"

if oauth2_redirect_url:
html += f"oauth2RedirectUrl: window.location.origin + '{oauth2_redirect_url}',"

html += """
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIBundle.SwaggerUIStandalonePreset
],
})"""

if init_oauth:
html += f"""
ui.initOAuth({json.dumps(init_oauth)})
"""

html += """
</script>
</body>
</html>
"""
return HTMLResponse(html)


def get_redoc_html(
*,
openapi_url: str,
title: str,
redoc_js_url: str = "https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js",
redoc_favicon_url: str = "/favicon.ico",
with_google_fonts: bool = True,
) -> HTMLResponse:
"""
Generate and return the HTML response that loads ReDoc for the alternative
API docs (normally served at `/redoc`).
"""
html = f"""
<!DOCTYPE html>
<html>
<head>
<title>{title}</title>
<!-- needed for adaptive design -->
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
"""
if with_google_fonts:
html += """
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
""" # noqa: E501
html += f"""
<link rel="shortcut icon" href="{redoc_favicon_url}">
<!--
ReDoc doesn't change outer page styles
-->
<style>
body {{
margin: 0;
padding: 0;
}}
</style>
</head>
<body>
<noscript>
ReDoc requires Javascript to function. Please enable it to browse the documentation.
</noscript>
<redoc spec-url="{openapi_url}"></redoc>
<script src="{redoc_js_url}"> </script>
</body>
</html>
"""
return HTMLResponse(html)


# Not needed now but copy-pasting for future reference
def get_swagger_ui_oauth2_redirect_html() -> HTMLResponse:
"""
Generate the HTML response with the OAuth2 redirection for Swagger UI.
You normally don't need to use or change this.
"""
# copied from https://github.com/swagger-api/swagger-ui/blob/v4.14.0/dist/oauth2-redirect.html
html = """
<!doctype html>
<html lang="en-US">
<head>
<title>Swagger UI: OAuth2 Redirect</title>
</head>
<body>
<script>
'use strict';
function run () {
var oauth2 = window.opener.swaggerUIRedirectOauth2;
var sentState = oauth2.state;
var redirectUrl = oauth2.redirectUrl;
var isValid, qp, arr;
if (/code|token|error/.test(window.location.hash)) {
qp = window.location.hash.substring(1).replace('?', '&');
} else {
qp = location.search.substring(1);
}
arr = qp.split("&");
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';});
qp = qp ? JSON.parse('{' + arr.join() + '}',
function (key, value) {
return key === "" ? value : decodeURIComponent(value);
}
) : {};
isValid = qp.state === sentState;
if ((
oauth2.auth.schema.get("flow") === "accessCode" ||
oauth2.auth.schema.get("flow") === "authorizationCode" ||
oauth2.auth.schema.get("flow") === "authorization_code"
) && !oauth2.auth.code) {
if (!isValid) {
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "warning",
message: "Authorization may be unsafe, passed state was changed in server. The passed state wasn't returned from auth server."
});
}
if (qp.code) {
delete oauth2.state;
oauth2.auth.code = qp.code;
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
} else {
let oauthErrorMsg;
if (qp.error) {
oauthErrorMsg = "["+qp.error+"]: " +
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
(qp.error_uri ? "More info: "+qp.error_uri : "");
}
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "error",
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server."
});
}
} else {
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
}
window.close();
}
if (document.readyState !== 'loading') {
run();
} else {
document.addEventListener('DOMContentLoaded', function () {
run();
});
}
</script>
</body>
</html>
""" # noqa: E501
return HTMLResponse(content=html)

0 comments on commit 5b22961

Please sign in to comment.