Add sub-app to generate open api spec
This commit is contained in:
26
aiohttp_pydantic/oas/__init__.py
Normal file
26
aiohttp_pydantic/oas/__init__.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from typing import Iterable
|
||||
from importlib import resources
|
||||
|
||||
import jinja2
|
||||
from aiohttp import web
|
||||
from .view import get_oas, oas_ui
|
||||
from swagger_ui_bundle import swagger_ui_path
|
||||
|
||||
|
||||
def setup(
|
||||
app: web.Application,
|
||||
apps_to_expose: Iterable[web.Application] = (),
|
||||
url_prefix: str = "/oas",
|
||||
enable: bool = True,
|
||||
):
|
||||
if enable:
|
||||
oas_app = web.Application()
|
||||
oas_app["apps to expose"] = tuple(apps_to_expose) or (app,)
|
||||
oas_app["index template"] = jinja2.Template(
|
||||
resources.read_text("aiohttp_pydantic.oas", "index.j2")
|
||||
)
|
||||
oas_app.router.add_get("/spec", get_oas, name="spec")
|
||||
oas_app.router.add_static("/static", swagger_ui_path, name="static")
|
||||
oas_app.router.add_get("", oas_ui, name="index")
|
||||
|
||||
app.add_subapp(url_prefix, oas_app)
|
||||
69
aiohttp_pydantic/oas/index.j2
Normal file
69
aiohttp_pydantic/oas/index.j2
Normal file
@@ -0,0 +1,69 @@
|
||||
{# This updated file is part of swagger_ui_bundle (https://github.com/dtkav/swagger_ui_bundle) #}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{{ title | default('Swagger UI') }}</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ static_url | trim('/') }}/swagger-ui.css" >
|
||||
<link rel="icon" type="image/png" href="{{ static_url | trim('/') }}/favicon-32x32.png" sizes="32x32" />
|
||||
<link rel="icon" type="image/png" href="{{ static_url | trim('/') }}/favicon-16x16.png" sizes="16x16" />
|
||||
<style>
|
||||
html
|
||||
{
|
||||
box-sizing: border-box;
|
||||
overflow: -moz-scrollbars-vertical;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
*,
|
||||
*:before,
|
||||
*:after
|
||||
{
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
body
|
||||
{
|
||||
margin:0;
|
||||
background: #fafafa;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
|
||||
<script src="{{ static_url | trim('/') }}/swagger-ui-bundle.js"> </script>
|
||||
<script src="{{ static_url | trim('/') }}/swagger-ui-standalone-preset.js"> </script>
|
||||
<script>
|
||||
window.onload = function() {
|
||||
// Begin Swagger UI call region
|
||||
const ui = SwaggerUIBundle({
|
||||
url: "{{ openapi_spec_url }}",
|
||||
validatorUrl: {{ validatorUrl | default('null') }},
|
||||
{% if configUrl is defined %}
|
||||
configUrl: "{{ configUrl }}",
|
||||
{% endif %}
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIStandalonePreset
|
||||
],
|
||||
plugins: [
|
||||
SwaggerUIBundle.plugins.DownloadUrl
|
||||
],
|
||||
layout: "StandaloneLayout"
|
||||
})
|
||||
{% if initOAuth is defined %}
|
||||
ui.initOAuth(
|
||||
{{ initOAuth|tojson|safe }}
|
||||
)
|
||||
{% endif %}
|
||||
// End Swagger UI call region
|
||||
|
||||
window.ui = ui
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
246
aiohttp_pydantic/oas/struct.py
Normal file
246
aiohttp_pydantic/oas/struct.py
Normal file
@@ -0,0 +1,246 @@
|
||||
class Info:
|
||||
def __init__(self, spec: dict):
|
||||
self._spec = spec.setdefault("info", {})
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return self._spec["title"]
|
||||
|
||||
@title.setter
|
||||
def title(self, title):
|
||||
self._spec["title"] = title
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
return self._spec["description"]
|
||||
|
||||
@description.setter
|
||||
def description(self, description):
|
||||
self._spec["description"] = description
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
return self._spec["version"]
|
||||
|
||||
@version.setter
|
||||
def version(self, version):
|
||||
self._spec["version"] = version
|
||||
|
||||
|
||||
class RequestBody:
|
||||
def __init__(self, spec: dict):
|
||||
self._spec = spec.setdefault("requestBody", {})
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
return self._spec["description"]
|
||||
|
||||
@description.setter
|
||||
def description(self, description: str):
|
||||
self._spec["description"] = description
|
||||
|
||||
@property
|
||||
def required(self):
|
||||
return self._spec["required"]
|
||||
|
||||
@required.setter
|
||||
def required(self, required: bool):
|
||||
self._spec["required"] = required
|
||||
|
||||
@property
|
||||
def content(self):
|
||||
return self._spec["content"]
|
||||
|
||||
@content.setter
|
||||
def content(self, content: dict):
|
||||
self._spec["content"] = content
|
||||
|
||||
|
||||
class Parameter:
|
||||
def __init__(self, spec: dict):
|
||||
self._spec = spec
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._spec["name"]
|
||||
|
||||
@name.setter
|
||||
def name(self, name: str):
|
||||
self._spec["name"] = name
|
||||
|
||||
@property
|
||||
def in_(self) -> str:
|
||||
return self._spec["in"]
|
||||
|
||||
@in_.setter
|
||||
def in_(self, in_: str):
|
||||
self._spec["in"] = in_
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return self._spec["description"]
|
||||
|
||||
@description.setter
|
||||
def description(self, description: str):
|
||||
self._spec["description"] = description
|
||||
|
||||
@property
|
||||
def required(self) -> bool:
|
||||
return self._spec["required"]
|
||||
|
||||
@required.setter
|
||||
def required(self, required: bool):
|
||||
self._spec["required"] = required
|
||||
|
||||
@property
|
||||
def schema(self) -> dict:
|
||||
return self._spec["schema"]
|
||||
|
||||
@schema.setter
|
||||
def schema(self, schema: dict):
|
||||
self._spec["schema"] = schema
|
||||
|
||||
|
||||
class Parameters:
|
||||
def __init__(self, spec):
|
||||
self._spec = spec
|
||||
self._spec.setdefault("parameters", [])
|
||||
|
||||
def __getitem__(self, item: int) -> Parameter:
|
||||
if item == len(self._spec["parameters"]):
|
||||
spec = {}
|
||||
self._spec["parameters"].append(spec)
|
||||
else:
|
||||
spec = self._spec["parameters"][item]
|
||||
return Parameter(spec)
|
||||
|
||||
|
||||
class OperationObject:
|
||||
def __init__(self, spec: dict):
|
||||
self._spec = spec
|
||||
|
||||
@property
|
||||
def summary(self) -> str:
|
||||
return self._spec["summary"]
|
||||
|
||||
@summary.setter
|
||||
def summary(self, summary: str):
|
||||
self._spec["summary"] = summary
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return self._spec["description"]
|
||||
|
||||
@description.setter
|
||||
def description(self, description: str):
|
||||
self._spec["description"] = description
|
||||
|
||||
@property
|
||||
def request_body(self) -> RequestBody:
|
||||
return RequestBody(self._spec)
|
||||
|
||||
@property
|
||||
def parameters(self) -> Parameters:
|
||||
return Parameters(self._spec)
|
||||
|
||||
|
||||
class PathItem:
|
||||
def __init__(self, spec: dict):
|
||||
self._spec = spec
|
||||
|
||||
@property
|
||||
def get(self) -> OperationObject:
|
||||
return OperationObject(self._spec.setdefault("get", {}))
|
||||
|
||||
@property
|
||||
def put(self) -> OperationObject:
|
||||
return OperationObject(self._spec.setdefault("put", {}))
|
||||
|
||||
@property
|
||||
def post(self) -> OperationObject:
|
||||
return OperationObject(self._spec.setdefault("post", {}))
|
||||
|
||||
@property
|
||||
def delete(self) -> OperationObject:
|
||||
return OperationObject(self._spec.setdefault("delete", {}))
|
||||
|
||||
@property
|
||||
def options(self) -> OperationObject:
|
||||
return OperationObject(self._spec.setdefault("options", {}))
|
||||
|
||||
@property
|
||||
def head(self) -> OperationObject:
|
||||
return OperationObject(self._spec.setdefault("head", {}))
|
||||
|
||||
@property
|
||||
def patch(self) -> OperationObject:
|
||||
return OperationObject(self._spec.setdefault("patch", {}))
|
||||
|
||||
@property
|
||||
def trace(self) -> OperationObject:
|
||||
return OperationObject(self._spec.setdefault("trace", {}))
|
||||
|
||||
|
||||
class Paths:
|
||||
def __init__(self, spec: dict):
|
||||
self._spec = spec.setdefault("paths", {})
|
||||
|
||||
def __getitem__(self, path: str) -> PathItem:
|
||||
spec = self._spec.setdefault(path, {})
|
||||
return PathItem(spec)
|
||||
|
||||
|
||||
class Server:
|
||||
def __init__(self, spec: dict):
|
||||
self._spec = spec
|
||||
|
||||
@property
|
||||
def url(self) -> str:
|
||||
return self._spec["url"]
|
||||
|
||||
@url.setter
|
||||
def url(self, url: str):
|
||||
self._spec["url"] = url
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return self._spec["url"]
|
||||
|
||||
@description.setter
|
||||
def description(self, description: str):
|
||||
self._spec["description"] = description
|
||||
|
||||
|
||||
class Servers:
|
||||
def __init__(self, spec: dict):
|
||||
self._spec = spec
|
||||
self._spec.setdefault("servers", [])
|
||||
|
||||
def __getitem__(self, item: int) -> Server:
|
||||
if item == len(self._spec["servers"]):
|
||||
spec = {}
|
||||
self._spec["servers"].append(spec)
|
||||
else:
|
||||
spec = self._spec["servers"][item]
|
||||
return Server(spec)
|
||||
|
||||
|
||||
class OpenApiSpec3:
|
||||
def __init__(self):
|
||||
self._spec = {"openapi": "3.0.0"}
|
||||
|
||||
@property
|
||||
def info(self) -> Info:
|
||||
return Info(self._spec)
|
||||
|
||||
@property
|
||||
def servers(self) -> Servers:
|
||||
return Servers(self._spec)
|
||||
|
||||
@property
|
||||
def paths(self) -> Paths:
|
||||
return Paths(self._spec)
|
||||
|
||||
@property
|
||||
def spec(self):
|
||||
return self._spec
|
||||
85
aiohttp_pydantic/oas/view.py
Normal file
85
aiohttp_pydantic/oas/view.py
Normal file
@@ -0,0 +1,85 @@
|
||||
from aiohttp.web import json_response, Response
|
||||
|
||||
from aiohttp_pydantic.oas.struct import OpenApiSpec3, OperationObject, PathItem
|
||||
from typing import Type
|
||||
|
||||
from ..injectors import _parse_func_signature
|
||||
from ..view import PydanticView, is_pydantic_view
|
||||
|
||||
|
||||
JSON_SCHEMA_TYPES = {float: "number", str: "string", int: "integer"}
|
||||
|
||||
|
||||
def _add_http_method_to_oas(oas_path: PathItem, method: str, view: Type[PydanticView]):
|
||||
method = method.lower()
|
||||
mtd: OperationObject = getattr(oas_path, method)
|
||||
handler = getattr(view, method)
|
||||
path_args, body_args, qs_args, header_args = _parse_func_signature(handler)
|
||||
|
||||
if body_args:
|
||||
mtd.request_body.content = {
|
||||
"application/json": {"schema": next(iter(body_args.values())).schema()}
|
||||
}
|
||||
|
||||
i = 0
|
||||
for i, (name, type_) in enumerate(path_args.items()):
|
||||
mtd.parameters[i].required = True
|
||||
mtd.parameters[i].in_ = "path"
|
||||
mtd.parameters[i].name = name
|
||||
mtd.parameters[i].schema = {"type": JSON_SCHEMA_TYPES[type_]}
|
||||
|
||||
for i, (name, type_) in enumerate(qs_args.items(), i + 1):
|
||||
mtd.parameters[i].required = False
|
||||
mtd.parameters[i].in_ = "query"
|
||||
mtd.parameters[i].name = name
|
||||
mtd.parameters[i].schema = {"type": JSON_SCHEMA_TYPES[type_]}
|
||||
|
||||
for i, (name, type_) in enumerate(header_args.items(), i + 1):
|
||||
mtd.parameters[i].required = False
|
||||
mtd.parameters[i].in_ = "header"
|
||||
mtd.parameters[i].name = name
|
||||
mtd.parameters[i].schema = {"type": JSON_SCHEMA_TYPES[type_]}
|
||||
|
||||
|
||||
async def get_oas(request):
|
||||
"""
|
||||
Generate Open Api Specification from PydanticView in application.
|
||||
"""
|
||||
apps = request.app["apps to expose"]
|
||||
oas = OpenApiSpec3()
|
||||
for app in apps:
|
||||
for resources in app.router.resources():
|
||||
for resource_route in resources:
|
||||
if is_pydantic_view(resource_route.handler):
|
||||
view: Type[PydanticView] = resource_route.handler
|
||||
info = resource_route.get_info()
|
||||
path = oas.paths[info.get("path", info.get("formatter"))]
|
||||
if resource_route.method == "*":
|
||||
for method_name in view.allowed_methods:
|
||||
_add_http_method_to_oas(path, method_name, view)
|
||||
else:
|
||||
_add_http_method_to_oas(path, resource_route.method, view)
|
||||
|
||||
return json_response(oas.spec)
|
||||
|
||||
|
||||
async def oas_ui(request):
|
||||
"""
|
||||
View to serve the swagger-ui to read open api specification of application.
|
||||
"""
|
||||
template = request.app["index template"]
|
||||
|
||||
static_url = request.app.router["static"].url_for(filename="")
|
||||
spec_url = request.app.router["spec"].url_for()
|
||||
host = request.url.origin()
|
||||
|
||||
return Response(
|
||||
text=template.render(
|
||||
{
|
||||
"openapi_spec_url": host.with_path(str(spec_url)),
|
||||
"static_url": host.with_path(str(static_url)),
|
||||
}
|
||||
),
|
||||
content_type="text/html",
|
||||
charset="utf-8",
|
||||
)
|
||||
Reference in New Issue
Block a user