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

Feature/docs url control #449

Merged
merged 8 commits into from
Dec 15, 2023
Merged
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
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.0.2] - 2023-12-15 :christmas_tree:

- Upgrades default SwaggerUI files to version 5, by @sinisaos
- Fixes #427, handling WebSocket errors according to ASGI specification, by @Klavionik
- Adds support for custom files URLs for ReDoc and Swagger UI, by @joshua-auchincloss

## [2.0.1] - 2023-12-09 :mount_fuji:

- Fixes #441 causing the `refresh_token` endpoint for OpenID Connect
integrations to not work when authentication is required by default.
- Fixes #427, handling WebSocket errors according to ASGI specification.
- Fixes #443, raising a detailed exception when more than one application is
sharing the same instance of `Router`
- Fixes #438 and #436, restoring support for `uvicorn` used programmatically
Expand Down
2 changes: 1 addition & 1 deletion blacksheep/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
used types to reduce the verbosity of the imports statements.
"""
__author__ = "Roberto Prevato <[email protected]>"
__version__ = "2.0.1"
__version__ = "2.0.2"

from .contents import Content as Content
from .contents import FormContent as FormContent
Expand Down
61 changes: 55 additions & 6 deletions blacksheep/server/openapi/ui.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
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/swagger-ui-dist@5/swagger-ui-bundle.js"
)
SWAGGER_UI_CSS_URL = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/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 +34,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 +58,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 +81,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 +98,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 +119,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 +135,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)
10 changes: 5 additions & 5 deletions blacksheep/server/res/redoc-ui.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
<html>
<head>
<title>##PAGE_TITLE##</title>
<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">
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.png" />
<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>
40 changes: 21 additions & 19 deletions blacksheep/server/res/swagger-ui.html
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
<!DOCTYPE html>
<html>
<head>
<head>
<title>##PAGE_TITLE##</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.png" />
<link type="text/css" rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css">
</head>
<body>
<link type="text/css" rel="stylesheet" href="##CSS_URL##" />
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
<script src="##JS_URL##"></script>
<script>
const ui = SwaggerUIBundle({
url: '##SPEC_URL##',
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
})
const ui = SwaggerUIBundle({
url: "##SPEC_URL##",
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>
</body>
</html>
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")
Loading
Loading