Skip to content

Commit 673ce84

Browse files
committed
Added responses to openapi.
Signed-off-by: Pavel Kirilin <win10@list.ru>
1 parent 11b6dbb commit 673ce84

3 files changed

Lines changed: 68 additions & 17 deletions

File tree

aiohttp_deps/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from aiohttp_deps.initializer import init
55
from aiohttp_deps.router import Router
6-
from aiohttp_deps.swagger import extra_openapi, setup_swagger
6+
from aiohttp_deps.swagger import extra_openapi, openapi_response, setup_swagger
77
from aiohttp_deps.utils import Form, Header, Json, Path, Query
88
from aiohttp_deps.view import View
99

@@ -19,4 +19,5 @@
1919
"Query",
2020
"Form",
2121
"Path",
22+
"openapi_response",
2223
]

aiohttp_deps/swagger.py

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import inspect
22
from collections import defaultdict
33
from logging import getLogger
4-
from typing import Any, Awaitable, Callable, Dict, Optional, get_type_hints
4+
from typing import Any, Awaitable, Callable, Dict, Optional, TypeVar, get_type_hints
55

66
import pydantic
77
from aiohttp import web
@@ -11,7 +11,8 @@
1111
from aiohttp_deps.initializer import InjectableFuncHandler, InjectableViewHandler
1212
from aiohttp_deps.utils import Form, Header, Json, Path, Query
1313

14-
REF_TEMPLATE = "#/components/schemas/{model}"
14+
_T = TypeVar("_T") # noqa: WPS111
15+
1516
SCHEMA_KEY = "openapi_schema"
1617
SWAGGER_HTML_TEMPALTE = """
1718
<html lang="en">
@@ -74,7 +75,7 @@ def dummy(_var: annotation.annotation) -> None: # type: ignore
7475
return var == Optional[var]
7576

7677

77-
def _add_route_def( # noqa: C901
78+
def _add_route_def( # noqa: C901, WPS210
7879
openapi_schema: Dict[str, Any],
7980
route: web.ResourceRoute,
8081
method: str,
@@ -89,6 +90,19 @@ def _add_route_def( # noqa: C901
8990
if route.resource is None: # pragma: no cover
9091
return
9192

93+
params: Dict[tuple[str, str], Any] = {}
94+
95+
def _insert_in_params(data: Dict[str, Any]) -> None:
96+
element = params.get((data["name"], data["in"]))
97+
if element is None:
98+
params[(data["name"], data["in"])] = data
99+
return
100+
element["required"] = element.get("required") or data.get("required")
101+
element["allowEmptyValue"] = element.get("allowEmptyValue") and data.get(
102+
"allowEmptyValue",
103+
)
104+
params[(data["name"], data["in"])] = element
105+
92106
for dependency in graph.ordered_deps:
93107
if isinstance(dependency.dependency, (Json, Form)):
94108
content_type = "application/json"
@@ -100,9 +114,7 @@ def _add_route_def( # noqa: C901
100114
):
101115
input_schema = pydantic.TypeAdapter(
102116
dependency.signature.annotation,
103-
).json_schema(
104-
ref_template=REF_TEMPLATE,
105-
)
117+
).json_schema()
106118
openapi_schema["components"]["schemas"].update(
107119
input_schema.pop("definitions", {}),
108120
)
@@ -114,7 +126,7 @@ def _add_route_def( # noqa: C901
114126
"content": {content_type: {}},
115127
}
116128
elif isinstance(dependency.dependency, Query):
117-
route_info["parameters"].append(
129+
_insert_in_params(
118130
{
119131
"name": dependency.dependency.alias or dependency.param_name,
120132
"in": "query",
@@ -123,16 +135,17 @@ def _add_route_def( # noqa: C901
123135
},
124136
)
125137
elif isinstance(dependency.dependency, Header):
126-
route_info["parameters"].append(
138+
name = dependency.dependency.alias or dependency.param_name
139+
_insert_in_params(
127140
{
128-
"name": dependency.dependency.alias or dependency.param_name,
141+
"name": name.capitalize(),
129142
"in": "header",
130143
"description": dependency.dependency.description,
131144
"required": not _is_optional(dependency.signature),
132145
},
133146
)
134147
elif isinstance(dependency.dependency, Path):
135-
route_info["parameters"].append(
148+
_insert_in_params(
136149
{
137150
"name": dependency.dependency.alias or dependency.param_name,
138151
"in": "path",
@@ -142,6 +155,7 @@ def _add_route_def( # noqa: C901
142155
},
143156
)
144157

158+
route_info["parameters"] = list(params.values())
145159
openapi_schema["paths"][route.resource.canonical].update(
146160
{method.lower(): always_merger.merge(route_info, extra_openapi)},
147161
)
@@ -260,7 +274,7 @@ async def event_handler(app: web.Application) -> None:
260274
return event_handler
261275

262276

263-
def extra_openapi(additional_schema: Dict[str, Any]) -> Callable[..., Any]:
277+
def extra_openapi(additional_schema: Dict[str, Any]) -> Callable[[_T], _T]:
264278
"""
265279
Add extra openapi schema.
266280
@@ -271,8 +285,44 @@ def extra_openapi(additional_schema: Dict[str, Any]) -> Callable[..., Any]:
271285
:return: same function with new attributes.
272286
"""
273287

274-
def decorator(func: Any) -> Any:
275-
func.__extra_openapi__ = additional_schema
288+
def decorator(func: _T) -> _T:
289+
func.__extra_openapi__ = additional_schema # type: ignore
290+
return func
291+
292+
return decorator
293+
294+
295+
def openapi_response(
296+
status: int,
297+
model: Any,
298+
*,
299+
content_type: str = "application/json",
300+
description: Optional[str] = None,
301+
) -> Callable[[_T], _T]:
302+
"""
303+
Add response schema to the endpoint.
304+
305+
This function takes a status and model,
306+
which is going to represent the response.
307+
308+
:param status: Status of a response.
309+
:param model: Response model.
310+
:param content_type: Content-type of a response.
311+
:param description: Response's description.
312+
313+
:returns: decorator that modifies your function.
314+
"""
315+
316+
def decorator(func: _T) -> _T:
317+
openapi = getattr(func, "__extra_openapi__", {})
318+
adapter: "pydantic.TypeAdapter[Any]" = pydantic.TypeAdapter(model)
319+
responses = openapi.get("responses", {})
320+
responses[status] = {
321+
"description": description,
322+
"content": {content_type: {"schema": adapter.json_schema()}},
323+
}
324+
openapi["responses"] = responses
325+
func.__extra_openapi__ = openapi # type: ignore
276326
return func
277327

278328
return decorator

tests/test_swagger.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -269,20 +269,20 @@ async def my_handler(my_var: int = Depends(Query(alias="qqq"))):
269269
),
270270
(
271271
Header(),
272-
{"name": "my_var", "required": True, "in": "header", "description": ""},
272+
{"name": "My_var", "required": True, "in": "header", "description": ""},
273273
),
274274
(
275275
Header(description="my header"),
276276
{
277-
"name": "my_var",
277+
"name": "My_var",
278278
"required": True,
279279
"in": "header",
280280
"description": "my header",
281281
},
282282
),
283283
(
284284
Header(alias="a"),
285-
{"name": "a", "required": True, "in": "header", "description": ""},
285+
{"name": "A", "required": True, "in": "header", "description": ""},
286286
),
287287
(
288288
Path(),

0 commit comments

Comments
 (0)