Skip to content

Commit 8eaaf8f

Browse files
committed
Temporary vendorize aiohttp-apispec
We need this [1] to be released to properly support returning the docs and specs urls. Since this is not yet released, we temporary vendorize it. [1]: maximdanilchenko/aiohttp-apispec#78
1 parent df35f86 commit 8eaaf8f

22 files changed

+805
-1
lines changed

deepaas/aiohttp_apispec/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
This is a vendorized version of:
2+
3+
https://github.com/maximdanilchenko/aiohttp-apispec/
4+
5+
All this code will be removed as soon as this pull request:
6+
7+
https://github.com/maximdanilchenko/aiohttp-apispec/pull/78
8+
9+
is released.
10+
11+
The code included here has Copyright (c) 2017 Maxim Danilchenko and is released
12+
under a MIT license.

deepaas/aiohttp_apispec/__init__.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from .aiohttp_apispec import AiohttpApiSpec, setup_aiohttp_apispec
2+
from .decorators import (
3+
docs,
4+
request_schema,
5+
match_info_schema,
6+
querystring_schema,
7+
form_schema,
8+
json_schema,
9+
headers_schema,
10+
cookies_schema,
11+
response_schema,
12+
use_kwargs,
13+
marshal_with,
14+
)
15+
from .middlewares import validation_middleware
16+
17+
__all__ = [
18+
# setup
19+
"AiohttpApiSpec",
20+
"setup_aiohttp_apispec",
21+
# decorators
22+
"docs",
23+
"request_schema",
24+
"match_info_schema",
25+
"querystring_schema",
26+
"form_schema",
27+
"json_schema",
28+
"headers_schema",
29+
"cookies_schema",
30+
"response_schema",
31+
"use_kwargs",
32+
"marshal_with",
33+
# middleware
34+
"validation_middleware",
35+
]
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
import copy
2+
from pathlib import Path
3+
from typing import Awaitable, Callable
4+
5+
from aiohttp import web
6+
from aiohttp.hdrs import METH_ALL, METH_ANY
7+
from apispec import APISpec
8+
from apispec.core import VALID_METHODS_OPENAPI_V2
9+
from apispec.ext.marshmallow import MarshmallowPlugin, common
10+
from jinja2 import Template
11+
from webargs.aiohttpparser import parser
12+
13+
from .utils import get_path, get_path_keys, issubclass_py37fix
14+
15+
_AiohttpView = Callable[[web.Request], Awaitable[web.StreamResponse]]
16+
17+
VALID_RESPONSE_FIELDS = {"description", "headers", "examples"}
18+
19+
20+
def resolver(schema):
21+
schema_instance = common.resolve_schema_instance(schema)
22+
prefix = "Partial-" if schema_instance.partial else ""
23+
schema_cls = common.resolve_schema_cls(schema)
24+
name = prefix + schema_cls.__name__
25+
if name.endswith("Schema"):
26+
return name[:-6] or name
27+
return name
28+
29+
30+
class AiohttpApiSpec:
31+
def __init__(
32+
self,
33+
url="/api/docs/swagger.json",
34+
app=None,
35+
request_data_name="data",
36+
swagger_path=None,
37+
static_path='/static/swagger',
38+
error_callback=None,
39+
in_place=False,
40+
prefix='',
41+
**kwargs
42+
):
43+
44+
self.plugin = MarshmallowPlugin(schema_name_resolver=resolver)
45+
self.spec = APISpec(plugins=(self.plugin,), openapi_version="2.0", **kwargs)
46+
47+
self.url = url
48+
self.swagger_path = swagger_path
49+
self.static_path = static_path
50+
self._registered = False
51+
self._request_data_name = request_data_name
52+
self.error_callback = error_callback
53+
self.prefix = prefix
54+
if app is not None:
55+
self.register(app, in_place)
56+
57+
def swagger_dict(self):
58+
""" Returns swagger spec representation in JSON format """
59+
return self.spec.to_dict()
60+
61+
def register(self, app: web.Application, in_place: bool = False):
62+
""" Creates spec based on registered app routes and registers needed view """
63+
if self._registered is True:
64+
return None
65+
66+
app["_apispec_request_data_name"] = self._request_data_name
67+
68+
if self.error_callback:
69+
parser.error_callback = self.error_callback
70+
app["_apispec_parser"] = parser
71+
72+
if in_place:
73+
self._register(app)
74+
else:
75+
76+
async def doc_routes(app_):
77+
self._register(app_)
78+
79+
app.on_startup.append(doc_routes)
80+
81+
self._registered = True
82+
83+
if self.url is not None:
84+
async def swagger_handler(request):
85+
return web.json_response(request.app["swagger_dict"])
86+
87+
app.router.add_route("GET", self.url, swagger_handler, name="swagger.spec")
88+
89+
if self.swagger_path is not None:
90+
self._add_swagger_web_page(app, self.static_path, self.swagger_path)
91+
92+
def _add_swagger_web_page(
93+
self, app: web.Application, static_path: str, view_path: str
94+
):
95+
static_files = Path(__file__).parent / "static"
96+
app.router.add_static(static_path, static_files)
97+
98+
with open(str(static_files / "index.html")) as swg_tmp:
99+
tmp = Template(swg_tmp.read()).render(path=self.url, static=static_path)
100+
101+
async def swagger_view(_):
102+
return web.Response(text=tmp, content_type="text/html")
103+
104+
app.router.add_route("GET", view_path, swagger_view, name="swagger.docs")
105+
106+
def _register(self, app: web.Application):
107+
for route in app.router.routes():
108+
if issubclass_py37fix(route.handler, web.View) and route.method == METH_ANY:
109+
for attr in dir(route.handler):
110+
if attr.upper() in METH_ALL:
111+
view = getattr(route.handler, attr)
112+
method = attr
113+
self._register_route(route, method, view)
114+
else:
115+
method = route.method.lower()
116+
view = route.handler
117+
self._register_route(route, method, view)
118+
app["swagger_dict"] = self.swagger_dict()
119+
120+
def _register_route(
121+
self, route: web.AbstractRoute, method: str, view: _AiohttpView
122+
):
123+
124+
if not hasattr(view, "__apispec__"):
125+
return None
126+
127+
url_path = get_path(route)
128+
if not url_path:
129+
return None
130+
131+
self._update_paths(view.__apispec__, method, self.prefix + url_path)
132+
133+
def _update_paths(self, data: dict, method: str, url_path: str):
134+
if method not in VALID_METHODS_OPENAPI_V2:
135+
return None
136+
for schema in data.pop("schemas", []):
137+
parameters = self.plugin.converter.schema2parameters(
138+
schema["schema"], **schema["options"]
139+
)
140+
data["parameters"].extend(parameters)
141+
142+
existing = [p["name"] for p in data["parameters"] if p["in"] == "path"]
143+
data["parameters"].extend(
144+
{"in": "path", "name": path_key, "required": True, "type": "string"}
145+
for path_key in get_path_keys(url_path)
146+
if path_key not in existing
147+
)
148+
149+
if "responses" in data:
150+
responses = {}
151+
for code, actual_params in data["responses"].items():
152+
if "schema" in actual_params:
153+
raw_parameters = self.plugin.converter.schema2parameters(
154+
actual_params["schema"],
155+
required=actual_params.get("required", False),
156+
)[0]
157+
updated_params = {
158+
k: v
159+
for k, v in raw_parameters.items()
160+
if k in VALID_RESPONSE_FIELDS
161+
}
162+
updated_params['schema'] = actual_params["schema"]
163+
for extra_info in ("description", "headers", "examples"):
164+
if extra_info in actual_params:
165+
updated_params[extra_info] = actual_params[extra_info]
166+
responses[code] = updated_params
167+
else:
168+
responses[code] = actual_params
169+
data["responses"] = responses
170+
171+
operations = copy.deepcopy(data)
172+
self.spec.path(path=url_path, operations={method: operations})
173+
174+
175+
def setup_aiohttp_apispec(
176+
app: web.Application,
177+
*,
178+
title: str = "API documentation",
179+
version: str = "0.0.1",
180+
url: str = "/api/docs/swagger.json",
181+
request_data_name: str = "data",
182+
swagger_path: str = None,
183+
static_path: str = '/static/swagger',
184+
error_callback=None,
185+
in_place: bool = False,
186+
prefix: str = '',
187+
**kwargs
188+
) -> None:
189+
"""
190+
aiohttp-apispec extension.
191+
192+
Usage:
193+
194+
.. code-block:: python
195+
196+
from aiohttp_apispec import docs, request_schema, setup_aiohttp_apispec
197+
from aiohttp import web
198+
from marshmallow import Schema, fields
199+
200+
201+
class RequestSchema(Schema):
202+
id = fields.Int()
203+
name = fields.Str(description='name')
204+
bool_field = fields.Bool()
205+
206+
207+
@docs(tags=['mytag'],
208+
summary='Test method summary',
209+
description='Test method description')
210+
@request_schema(RequestSchema)
211+
async def index(request):
212+
return web.json_response({'msg': 'done', 'data': {}})
213+
214+
215+
app = web.Application()
216+
app.router.add_post('/v1/test', index)
217+
218+
# init docs with all parameters, usual for ApiSpec
219+
setup_aiohttp_apispec(app=app,
220+
title='My Documentation',
221+
version='v1',
222+
url='/api/docs/api-docs')
223+
224+
# now we can find it on 'http://localhost:8080/api/docs/api-docs'
225+
web.run_app(app)
226+
227+
:param Application app: aiohttp web app
228+
:param str title: API title
229+
:param str version: API version
230+
:param str url: url for swagger spec in JSON format
231+
:param str request_data_name: name of the key in Request object
232+
where validated data will be placed by
233+
validation_middleware (``'data'`` by default)
234+
:param str swagger_path: experimental SwaggerUI support (starting from v1.1.0).
235+
By default it is None (disabled)
236+
:param str static_path: path for static files used by SwaggerUI
237+
(if it is enabled with ``swagger_path``)
238+
:param error_callback: custom error handler
239+
:param in_place: register all routes at the moment of calling this function
240+
instead of the moment of the on_startup signal.
241+
If True, be sure all routes are added to router
242+
:param prefix: prefix to add to all registered routes
243+
:param kwargs: any apispec.APISpec kwargs
244+
"""
245+
AiohttpApiSpec(
246+
url,
247+
app,
248+
request_data_name,
249+
title=title,
250+
version=version,
251+
swagger_path=swagger_path,
252+
static_path=static_path,
253+
error_callback=error_callback,
254+
in_place=in_place,
255+
prefix=prefix,
256+
**kwargs
257+
)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from .docs import docs
2+
from .request import (
3+
request_schema,
4+
use_kwargs, # for backward compatibility
5+
match_info_schema, # request_schema with locations=["match_info"]
6+
querystring_schema, # request_schema with locations=["querystring"]
7+
form_schema, # request_schema with locations=["form"]
8+
json_schema, # request_schema with locations=["json"]
9+
headers_schema, # request_schema with locations=["headers"]
10+
cookies_schema, # request_schema with locations=["cookies"]
11+
)
12+
from .response import (
13+
response_schema,
14+
marshal_with, # for backward compatibility
15+
)
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
def docs(**kwargs):
2+
"""
3+
Annotate the decorated view function with the specified Swagger
4+
attributes.
5+
6+
Usage:
7+
8+
.. code-block:: python
9+
10+
from aiohttp import web
11+
12+
@docs(tags=['my_tag'],
13+
summary='Test method summary',
14+
description='Test method description',
15+
parameters=[{
16+
'in': 'header',
17+
'name': 'X-Request-ID',
18+
'schema': {'type': 'string', 'format': 'uuid'},
19+
'required': 'true'
20+
}]
21+
)
22+
async def index(request):
23+
return web.json_response({'msg': 'done', 'data': {}})
24+
25+
"""
26+
27+
def wrapper(func):
28+
if not kwargs.get("produces"):
29+
kwargs["produces"] = ["application/json"]
30+
if not hasattr(func, "__apispec__"):
31+
func.__apispec__ = {"schemas": [], "responses": {}, "parameters": []}
32+
func.__schemas__ = []
33+
extra_parameters = kwargs.pop("parameters", [])
34+
extra_responses = kwargs.pop("responses", {})
35+
func.__apispec__["parameters"].extend(extra_parameters)
36+
func.__apispec__["responses"].update(extra_responses)
37+
func.__apispec__.update(kwargs)
38+
return func
39+
40+
return wrapper

0 commit comments

Comments
 (0)