Fix wrong link in OAS components with nested pydantic.BaseModel
This commit is contained in:
parent
f2b16a46b5
commit
25fcac18ec
@ -1,5 +1,5 @@
|
|||||||
from .view import PydanticView
|
from .view import PydanticView
|
||||||
|
|
||||||
__version__ = "1.6.0"
|
__version__ = "1.6.1"
|
||||||
|
|
||||||
__all__ = ("PydanticView", "__version__")
|
__all__ = ("PydanticView", "__version__")
|
||||||
|
@ -68,7 +68,7 @@ class BodyGetter(AbstractInjector):
|
|||||||
except JSONDecodeError:
|
except JSONDecodeError:
|
||||||
raise HTTPBadRequest(
|
raise HTTPBadRequest(
|
||||||
text='{"error": "Malformed JSON"}', content_type="application/json"
|
text='{"error": "Malformed JSON"}', content_type="application/json"
|
||||||
)
|
) from None
|
||||||
|
|
||||||
kwargs_view[self.arg_name] = self.model(**body)
|
kwargs_view[self.arg_name] = self.model(**body)
|
||||||
|
|
||||||
|
@ -293,6 +293,15 @@ class Servers:
|
|||||||
return Server(spec)
|
return Server(spec)
|
||||||
|
|
||||||
|
|
||||||
|
class Components:
|
||||||
|
def __init__(self, spec: dict):
|
||||||
|
self._spec = spec.setdefault("components", {})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def schemas(self) -> dict:
|
||||||
|
return self._spec.setdefault("schemas", {})
|
||||||
|
|
||||||
|
|
||||||
class OpenApiSpec3:
|
class OpenApiSpec3:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._spec = {"openapi": "3.0.0"}
|
self._spec = {"openapi": "3.0.0"}
|
||||||
@ -309,6 +318,10 @@ class OpenApiSpec3:
|
|||||||
def paths(self) -> Paths:
|
def paths(self) -> Paths:
|
||||||
return Paths(self._spec)
|
return Paths(self._spec)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def components(self) -> Components:
|
||||||
|
return Components(self._spec)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def spec(self):
|
def spec(self):
|
||||||
return self._spec
|
return self._spec
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import typing
|
import typing
|
||||||
from datetime import date, datetime
|
|
||||||
from inspect import getdoc
|
from inspect import getdoc
|
||||||
from itertools import count
|
from itertools import count
|
||||||
from typing import List, Type
|
from typing import List, Type
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
from aiohttp.web import Response, json_response
|
from aiohttp.web import Response, json_response
|
||||||
from aiohttp.web_app import Application
|
from aiohttp.web_app import Application
|
||||||
@ -16,16 +14,6 @@ from ..utils import is_pydantic_base_model
|
|||||||
from ..view import PydanticView, is_pydantic_view
|
from ..view import PydanticView, is_pydantic_view
|
||||||
from .typing import is_status_code_type
|
from .typing import is_status_code_type
|
||||||
|
|
||||||
JSON_SCHEMA_TYPES = {
|
|
||||||
float: {"type": "number"},
|
|
||||||
str: {"type": "string"},
|
|
||||||
int: {"type": "integer"},
|
|
||||||
UUID: {"type": "string", "format": "uuid"},
|
|
||||||
bool: {"type": "boolean"},
|
|
||||||
datetime: {"type": "string", "format": "date-time"},
|
|
||||||
date: {"type": "string", "format": "date"},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _handle_optional(type_):
|
def _handle_optional(type_):
|
||||||
"""
|
"""
|
||||||
@ -48,13 +36,16 @@ class _OASResponseBuilder:
|
|||||||
generate the OAS operation response.
|
generate the OAS operation response.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, oas_operation):
|
def __init__(self, oas: OpenApiSpec3, oas_operation):
|
||||||
self._oas_operation = oas_operation
|
self._oas_operation = oas_operation
|
||||||
|
self._oas = oas
|
||||||
|
|
||||||
@staticmethod
|
def _handle_pydantic_base_model(self, obj):
|
||||||
def _handle_pydantic_base_model(obj):
|
|
||||||
if is_pydantic_base_model(obj):
|
if is_pydantic_base_model(obj):
|
||||||
return obj.schema()
|
response_schema = obj.schema(ref_template="#/components/schemas/{model}")
|
||||||
|
if def_sub_schemas := response_schema.get("definitions", None):
|
||||||
|
self._oas.components.schemas.update(def_sub_schemas)
|
||||||
|
return response_schema
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def _handle_list(self, obj):
|
def _handle_list(self, obj):
|
||||||
@ -89,7 +80,7 @@ class _OASResponseBuilder:
|
|||||||
|
|
||||||
|
|
||||||
def _add_http_method_to_oas(
|
def _add_http_method_to_oas(
|
||||||
oas_path: PathItem, http_method: str, view: Type[PydanticView]
|
oas: OpenApiSpec3, oas_path: PathItem, http_method: str, view: Type[PydanticView]
|
||||||
):
|
):
|
||||||
http_method = http_method.lower()
|
http_method = http_method.lower()
|
||||||
oas_operation: OperationObject = getattr(oas_path, http_method)
|
oas_operation: OperationObject = getattr(oas_path, http_method)
|
||||||
@ -102,8 +93,14 @@ def _add_http_method_to_oas(
|
|||||||
oas_operation.description = description
|
oas_operation.description = description
|
||||||
|
|
||||||
if body_args:
|
if body_args:
|
||||||
|
body_schema = next(iter(body_args.values())).schema(
|
||||||
|
ref_template="#/components/schemas/{model}"
|
||||||
|
)
|
||||||
|
if def_sub_schemas := body_schema.get("definitions", None):
|
||||||
|
oas.components.schemas.update(def_sub_schemas)
|
||||||
|
|
||||||
oas_operation.request_body.content = {
|
oas_operation.request_body.content = {
|
||||||
"application/json": {"schema": next(iter(body_args.values())).schema()}
|
"application/json": {"schema": body_schema}
|
||||||
}
|
}
|
||||||
|
|
||||||
indexes = count()
|
indexes = count()
|
||||||
@ -122,14 +119,15 @@ def _add_http_method_to_oas(
|
|||||||
if name in defaults:
|
if name in defaults:
|
||||||
attrs["__root__"] = defaults[name]
|
attrs["__root__"] = defaults[name]
|
||||||
|
|
||||||
oas_operation.parameters[i].schema = type(
|
oas_operation.parameters[i].schema = type(name, (BaseModel,), attrs).schema(
|
||||||
name, (BaseModel,), attrs
|
ref_template="#/components/schemas/{model}"
|
||||||
).schema()
|
)
|
||||||
|
|
||||||
oas_operation.parameters[i].required = optional_type is None
|
oas_operation.parameters[i].required = optional_type is None
|
||||||
|
|
||||||
return_type = handler.__annotations__.get("return")
|
return_type = handler.__annotations__.get("return")
|
||||||
if return_type is not None:
|
if return_type is not None:
|
||||||
_OASResponseBuilder(oas_operation).build(return_type)
|
_OASResponseBuilder(oas, oas_operation).build(return_type)
|
||||||
|
|
||||||
|
|
||||||
def generate_oas(apps: List[Application]) -> dict:
|
def generate_oas(apps: List[Application]) -> dict:
|
||||||
@ -148,9 +146,9 @@ def generate_oas(apps: List[Application]) -> dict:
|
|||||||
path = oas.paths[info.get("path", info.get("formatter"))]
|
path = oas.paths[info.get("path", info.get("formatter"))]
|
||||||
if resource_route.method == "*":
|
if resource_route.method == "*":
|
||||||
for method_name in view.allowed_methods:
|
for method_name in view.allowed_methods:
|
||||||
_add_http_method_to_oas(path, method_name, view)
|
_add_http_method_to_oas(oas, path, method_name, view)
|
||||||
else:
|
else:
|
||||||
_add_http_method_to_oas(path, resource_route.method, view)
|
_add_http_method_to_oas(oas, path, resource_route.method, view)
|
||||||
|
|
||||||
return oas.spec
|
return oas.spec
|
||||||
|
|
||||||
|
@ -1,10 +1,17 @@
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
class Friend(BaseModel):
|
||||||
|
name: str
|
||||||
|
age: str
|
||||||
|
|
||||||
|
|
||||||
class Pet(BaseModel):
|
class Pet(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
name: str
|
name: str
|
||||||
age: int
|
age: int
|
||||||
|
friends: Friend
|
||||||
|
|
||||||
|
|
||||||
class Error(BaseModel):
|
class Error(BaseModel):
|
||||||
|
@ -31,7 +31,7 @@ packages = find:
|
|||||||
python_requires = >=3.8
|
python_requires = >=3.8
|
||||||
install_requires =
|
install_requires =
|
||||||
aiohttp
|
aiohttp
|
||||||
pydantic
|
pydantic>=1.7
|
||||||
swagger-ui-bundle
|
swagger-ui-bundle
|
||||||
|
|
||||||
[options.extras_require]
|
[options.extras_require]
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from enum import Enum
|
||||||
from typing import List, Optional, Union
|
from typing import List, Optional, Union
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
@ -9,9 +10,21 @@ from aiohttp_pydantic import PydanticView, oas
|
|||||||
from aiohttp_pydantic.oas.typing import r200, r201, r204, r404
|
from aiohttp_pydantic.oas.typing import r200, r201, r204, r404
|
||||||
|
|
||||||
|
|
||||||
|
class Color(str, Enum):
|
||||||
|
RED = "red"
|
||||||
|
GREEN = "green"
|
||||||
|
PINK = "pink"
|
||||||
|
|
||||||
|
|
||||||
|
class Toy(BaseModel):
|
||||||
|
name: str
|
||||||
|
color: Color
|
||||||
|
|
||||||
|
|
||||||
class Pet(BaseModel):
|
class Pet(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
name: str
|
name: str
|
||||||
|
toys: List[Toy]
|
||||||
|
|
||||||
|
|
||||||
class PetCollectionView(PydanticView):
|
class PetCollectionView(PydanticView):
|
||||||
@ -53,6 +66,26 @@ async def generated_oas(aiohttp_client, loop) -> web.Application:
|
|||||||
return await response.json()
|
return await response.json()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_generated_oas_should_have_components_schemas(generated_oas):
|
||||||
|
assert generated_oas["components"]["schemas"] == {
|
||||||
|
"Color": {
|
||||||
|
"description": "An enumeration.",
|
||||||
|
"enum": ["red", "green", "pink"],
|
||||||
|
"title": "Color",
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"Toy": {
|
||||||
|
"properties": {
|
||||||
|
"color": {"$ref": "#/components/schemas/Color"},
|
||||||
|
"name": {"title": "Name", "type": "string"},
|
||||||
|
},
|
||||||
|
"required": ["name", "color"],
|
||||||
|
"title": "Toy",
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def test_generated_oas_should_have_pets_paths(generated_oas):
|
async def test_generated_oas_should_have_pets_paths(generated_oas):
|
||||||
assert "/pets" in generated_oas["paths"]
|
assert "/pets" in generated_oas["paths"]
|
||||||
|
|
||||||
@ -77,7 +110,7 @@ async def test_pets_route_should_have_get_method(generated_oas):
|
|||||||
"in": "header",
|
"in": "header",
|
||||||
"name": "promo",
|
"name": "promo",
|
||||||
"required": False,
|
"required": False,
|
||||||
"schema": {"title": "promo", "format": "uuid", "type": "string"},
|
"schema": {"format": "uuid", "title": "promo", "type": "string"},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
@ -86,11 +119,35 @@ async def test_pets_route_should_have_get_method(generated_oas):
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"items": {
|
"items": {
|
||||||
|
"definitions": {
|
||||||
|
"Color": {
|
||||||
|
"description": "An enumeration.",
|
||||||
|
"enum": ["red", "green", "pink"],
|
||||||
|
"title": "Color",
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"Toy": {
|
||||||
|
"properties": {
|
||||||
|
"color": {
|
||||||
|
"$ref": "#/components/schemas/Color"
|
||||||
|
},
|
||||||
|
"name": {"title": "Name", "type": "string"},
|
||||||
|
},
|
||||||
|
"required": ["name", "color"],
|
||||||
|
"title": "Toy",
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
},
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {"title": "Id", "type": "integer"},
|
"id": {"title": "Id", "type": "integer"},
|
||||||
"name": {"title": "Name", "type": "string"},
|
"name": {"title": "Name", "type": "string"},
|
||||||
|
"toys": {
|
||||||
|
"items": {"$ref": "#/components/schemas/Toy"},
|
||||||
|
"title": "Toys",
|
||||||
|
"type": "array",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"required": ["id", "name"],
|
"required": ["id", "name", "toys"],
|
||||||
"title": "Pet",
|
"title": "Pet",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
},
|
},
|
||||||
@ -110,13 +167,35 @@ async def test_pets_route_should_have_post_method(generated_oas):
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"title": "Pet",
|
"definitions": {
|
||||||
"type": "object",
|
"Color": {
|
||||||
|
"description": "An enumeration.",
|
||||||
|
"enum": ["red", "green", "pink"],
|
||||||
|
"title": "Color",
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"Toy": {
|
||||||
|
"properties": {
|
||||||
|
"color": {"$ref": "#/components/schemas/Color"},
|
||||||
|
"name": {"title": "Name", "type": "string"},
|
||||||
|
},
|
||||||
|
"required": ["name", "color"],
|
||||||
|
"title": "Toy",
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
},
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {"title": "Id", "type": "integer"},
|
"id": {"title": "Id", "type": "integer"},
|
||||||
"name": {"title": "Name", "type": "string"},
|
"name": {"title": "Name", "type": "string"},
|
||||||
|
"toys": {
|
||||||
|
"items": {"$ref": "#/components/schemas/Toy"},
|
||||||
|
"title": "Toys",
|
||||||
|
"type": "array",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"required": ["id", "name"],
|
"required": ["id", "name", "toys"],
|
||||||
|
"title": "Pet",
|
||||||
|
"type": "object",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -126,13 +205,35 @@ async def test_pets_route_should_have_post_method(generated_oas):
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"title": "Pet",
|
"definitions": {
|
||||||
"type": "object",
|
"Color": {
|
||||||
|
"description": "An enumeration.",
|
||||||
|
"enum": ["red", "green", "pink"],
|
||||||
|
"title": "Color",
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"Toy": {
|
||||||
|
"properties": {
|
||||||
|
"color": {"$ref": "#/components/schemas/Color"},
|
||||||
|
"name": {"title": "Name", "type": "string"},
|
||||||
|
},
|
||||||
|
"required": ["name", "color"],
|
||||||
|
"title": "Toy",
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
},
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {"title": "Id", "type": "integer"},
|
"id": {"title": "Id", "type": "integer"},
|
||||||
"name": {"title": "Name", "type": "string"},
|
"name": {"title": "Name", "type": "string"},
|
||||||
|
"toys": {
|
||||||
|
"items": {"$ref": "#/components/schemas/Toy"},
|
||||||
|
"title": "Toys",
|
||||||
|
"type": "array",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"required": ["id", "name"],
|
"required": ["id", "name", "toys"],
|
||||||
|
"title": "Pet",
|
||||||
|
"type": "object",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -174,11 +275,33 @@ async def test_pets_id_route_should_have_get_method(generated_oas):
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
|
"definitions": {
|
||||||
|
"Color": {
|
||||||
|
"description": "An enumeration.",
|
||||||
|
"enum": ["red", "green", "pink"],
|
||||||
|
"title": "Color",
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"Toy": {
|
||||||
|
"properties": {
|
||||||
|
"color": {"$ref": "#/components/schemas/Color"},
|
||||||
|
"name": {"title": "Name", "type": "string"},
|
||||||
|
},
|
||||||
|
"required": ["name", "color"],
|
||||||
|
"title": "Toy",
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
},
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {"title": "Id", "type": "integer"},
|
"id": {"title": "Id", "type": "integer"},
|
||||||
"name": {"title": "Name", "type": "string"},
|
"name": {"title": "Name", "type": "string"},
|
||||||
|
"toys": {
|
||||||
|
"items": {"$ref": "#/components/schemas/Toy"},
|
||||||
|
"title": "Toys",
|
||||||
|
"type": "array",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"required": ["id", "name"],
|
"required": ["id", "name", "toys"],
|
||||||
"title": "Pet",
|
"title": "Pet",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
}
|
}
|
||||||
@ -204,11 +327,33 @@ async def test_pets_id_route_should_have_put_method(generated_oas):
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
|
"definitions": {
|
||||||
|
"Color": {
|
||||||
|
"description": "An enumeration.",
|
||||||
|
"enum": ["red", "green", "pink"],
|
||||||
|
"title": "Color",
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"Toy": {
|
||||||
|
"properties": {
|
||||||
|
"color": {"$ref": "#/components/schemas/Color"},
|
||||||
|
"name": {"title": "Name", "type": "string"},
|
||||||
|
},
|
||||||
|
"required": ["name", "color"],
|
||||||
|
"title": "Toy",
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
},
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {"title": "Id", "type": "integer"},
|
"id": {"title": "Id", "type": "integer"},
|
||||||
"name": {"title": "Name", "type": "string"},
|
"name": {"title": "Name", "type": "string"},
|
||||||
|
"toys": {
|
||||||
|
"items": {"$ref": "#/components/schemas/Toy"},
|
||||||
|
"title": "Toys",
|
||||||
|
"type": "array",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"required": ["id", "name"],
|
"required": ["id", "name", "toys"],
|
||||||
"title": "Pet",
|
"title": "Pet",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user