Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FR: custom cdn options #423

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [2.0a12] - 2023-11-?? :fallen_leaf:

- Adds support for Python 3.12, by @bymoye
- Adds support for custom files URLs for ReDoc and Swagger UI, by @joshua-auchincloss
- Replaces `pkg_resources` with `importlib.resources` for all supported Python
versions except for `3.8`.
- Runs tests against Pydantic `2.4.2` instead of Pydantic `2.0` to check
Expand Down
63 changes: 57 additions & 6 deletions blacksheep/server/openapi/ui.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,33 @@
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Callable
from typing import Callable, Optional

from blacksheep.messages import Request, Response
from blacksheep.server.files.static import get_response_for_static_content
from blacksheep.server.resources import get_resource_file_content
from blacksheep.utils.time import utcnow

SWAGGER_UI_JS_URL = (
"https://cdn.jsdelivr.net/npm/[email protected]/swagger-ui-bundle.js"
)
SWAGGER_UI_CSS_URL = (
"https://cdn.jsdelivr.net/npm/[email protected]/swagger-ui.css"
)
SWAGGER_UI_FONT = None

REDOC_UI_JS_URL = "https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"
REDOC_UI_CSS_URL = None
REDOC_UI_FONT_URL = (
"https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700"
)


@dataclass
class UIFilesOptions:
js_url: str
css_url: Optional[str] = None
fonts_url: Optional[str] = None


@dataclass
class UIOptions:
Expand All @@ -15,9 +36,17 @@ class UIOptions:


class UIProvider(ABC):
def __init__(self, ui_path: str) -> None:
ui_files: UIFilesOptions
ui_path: str

def __init__(
self,
ui_path: str,
ui_files: Optional[UIFilesOptions] = None,
) -> None:
super().__init__()
self.ui_path = ui_path
self.ui_files = ui_files if ui_files else self.default_ui_files

@abstractmethod
def build_ui(self, options: UIOptions) -> None:
Expand All @@ -31,10 +60,18 @@ def get_ui_handler(self) -> Callable[[Request], Response]:
Returns a request handler for the route that serves a UI.
"""

@property
def default_ui_files(self) -> UIFilesOptions:
...


class SwaggerUIProvider(UIProvider):
def __init__(self, ui_path: str = "/docs") -> None:
super().__init__(ui_path)
def __init__(
self,
ui_path: str = "/docs",
ui_files_options: Optional[UIFilesOptions] = None,
) -> None:
super().__init__(ui_path, ui_files_options)

self._ui_html: bytes = b""

Expand All @@ -46,6 +83,8 @@ def get_openapi_ui_html(self, options: UIOptions) -> str:
get_resource_file_content("swagger-ui.html")
.replace("##SPEC_URL##", options.spec_url)
.replace("##PAGE_TITLE##", options.page_title)
.replace("##JS_URL##", self.ui_files.js_url)
.replace("##CSS_URL##", self.ui_files.css_url or "")
)

def build_ui(self, options: UIOptions) -> None:
Expand All @@ -61,10 +100,16 @@ def get_open_api_ui(request: Request) -> Response:

return get_open_api_ui

@property
def default_ui_files(self) -> UIFilesOptions:
return UIFilesOptions(SWAGGER_UI_JS_URL, SWAGGER_UI_CSS_URL, SWAGGER_UI_FONT)


class ReDocUIProvider(UIProvider):
def __init__(self, ui_path: str = "/redocs") -> None:
super().__init__(ui_path)
def __init__(
self, ui_path: str = "/redocs", ui_files: Optional[UIFilesOptions] = None
) -> None:
super().__init__(ui_path, ui_files)

self._ui_html: bytes = b""

Expand All @@ -76,6 +121,8 @@ def get_openapi_ui_html(self, options: UIOptions) -> str:
get_resource_file_content("redoc-ui.html")
.replace("##SPEC_URL##", options.spec_url)
.replace("##PAGE_TITLE##", options.page_title)
.replace("##JS_URL##", self.ui_files.js_url)
.replace("##FONT_URL##", self.ui_files.fonts_url or "")
)

def build_ui(self, options: UIOptions) -> None:
Expand All @@ -90,3 +137,7 @@ def get_open_api_ui(request: Request) -> Response:
)

return get_open_api_ui

@property
def default_ui_files(self) -> UIFilesOptions:
return UIFilesOptions(REDOC_UI_JS_URL, REDOC_UI_CSS_URL, REDOC_UI_FONT_URL)
4 changes: 2 additions & 2 deletions blacksheep/server/res/redoc-ui.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="/favicon.png"/>
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
<link href="##FONT_URL##" rel="stylesheet">
<style>
body {
margin: 0;
Expand All @@ -15,6 +15,6 @@
</head>
<body>
<redoc spec-url="##SPEC_URL##"></redoc>
<script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"> </script>
<script src="##JS_URL##"> </script>
</body>
</html>
4 changes: 2 additions & 2 deletions blacksheep/server/res/swagger-ui.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
<head>
<title>##PAGE_TITLE##</title>
<link rel="icon" href="/favicon.png"/>
<link type="text/css" rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/swagger-ui.css">
<link type="text/css" rel="stylesheet" href="##CSS_URL##">
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/swagger-ui-bundle.js"></script>
<script src="##JS_URL##"></script>
<script>
const ui = SwaggerUIBundle({
url: '##SPEC_URL##',
Expand Down
20 changes: 20 additions & 0 deletions itests/app_4.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,20 @@
from datetime import datetime

import uvicorn
from openapidocs.v3 import Info

from blacksheep import JSONContent, Response
from blacksheep.server import Application
from blacksheep.server.bindings import FromJSON
from blacksheep.server.compression import use_gzip_compression
from blacksheep.server.openapi.ui import ReDocUIProvider, UIFilesOptions
from blacksheep.server.openapi.v3 import OpenAPIHandler
from blacksheep.server.responses import json
from blacksheep.server.websocket import WebSocket
from blacksheep.settings.json import default_json_dumps, json_settings

from .utils import get_test_files_url

SINGLE_PID = None


Expand Down Expand Up @@ -150,6 +155,21 @@ async def echo_json(websocket: WebSocket):
await websocket.send_json(msg)


docs = OpenAPIHandler(info=Info(title="Cats API", version="0.0.1"))
docs.ui_providers[0].ui_files = UIFilesOptions(
js_url=get_test_files_url("swag-js"),
css_url=get_test_files_url("swag-css"),
)
docs.ui_providers.append(
ReDocUIProvider(
ui_files=UIFilesOptions(
js_url=get_test_files_url("redoc-js"),
fonts_url=get_test_files_url("redoc-fonts"),
)
)
)
docs.bind_app(app_4)

if __name__ == "__main__":
configure_json_settings()
uvicorn.run(app_4, host="127.0.0.1", port=44557, log_level="debug")
73 changes: 72 additions & 1 deletion itests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from .client_fixtures import get_static_path
from .server_fixtures import * # NoQA
from .utils import assert_files_equals, ensure_success
from .utils import assert_files_equals, ensure_success, get_test_files_url


def test_hello_world(session_1):
Expand Down Expand Up @@ -421,6 +421,77 @@ def test_open_api_redoc_ui(session_2):
)


def test_open_api_ui_custom_cdn(session_4):
response = session_4.get("/docs")

assert response.status_code == 200
text = response.text
assert (
text.strip()
== f"""
<!DOCTYPE html>
<html>
<head>
<title>Cats API</title>
<link rel="icon" href="/favicon.png"/>
<link type="text/css" rel="stylesheet" href="{get_test_files_url("swag-css")}">
</head>
<body>
<div id="swagger-ui"></div>
<script src="{get_test_files_url("swag-js")}"></script>
<script>
const ui = SwaggerUIBundle({{
url: '/openapi.json',
oauth2RedirectUrl: window.location.origin + '/docs/oauth2-redirect',
dom_id: '#swagger-ui',
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIBundle.SwaggerUIStandalonePreset
],
layout: "BaseLayout",
deepLinking: true,
showExtensions: true,
showCommonExtensions: true
}})
</script>
</body>
</html>
""".strip()
)


def test_open_api_redoc_ui_custom_cdn(session_4):
response = session_4.get("/redocs")

assert response.status_code == 200
text = response.text
assert (
text.strip()
== f"""
<!DOCTYPE html>
<html>
<head>
<title>Cats API</title>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="/favicon.png"/>
<link href="{get_test_files_url("redoc-fonts")}" rel="stylesheet">
<style>
body {{
margin: 0;
padding: 0;
}}
</style>
</head>
<body>
<redoc spec-url="/openapi.json"></redoc>
<script src="{get_test_files_url("redoc-js")}"> </script>
</body>
</html>
""".strip()
)


def test_open_api_json(session_2):
response = session_2.get("/openapi.json")

Expand Down
4 changes: 4 additions & 0 deletions itests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,7 @@ def get_sleep_time():
if os.name == "nt":
return 1.5
return 0.5


def get_test_files_url(url: str):
return f"my-cdn-foo:{url}"
Loading