Compare commits
3 Commits
fix-defini
...
v1.6.1
Author | SHA1 | Date | |
---|---|---|---|
|
25fcac18ec | ||
|
f2b16a46b5 | ||
|
c4c18ee4a1 |
@@ -68,6 +68,7 @@ Example:
|
|||||||
$ curl -X GET http://127.0.0.1:8080/article?with_comments=a
|
$ curl -X GET http://127.0.0.1:8080/article?with_comments=a
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
"in": "query string",
|
||||||
"loc": [
|
"loc": [
|
||||||
"with_comments"
|
"with_comments"
|
||||||
],
|
],
|
||||||
@@ -82,6 +83,7 @@ Example:
|
|||||||
$ curl -H "Content-Type: application/json" -X post http://127.0.0.1:8080/article --data '{}'
|
$ curl -H "Content-Type: application/json" -X post http://127.0.0.1:8080/article --data '{}'
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
"in": "body",
|
||||||
"loc": [
|
"loc": [
|
||||||
"name"
|
"name"
|
||||||
],
|
],
|
||||||
@@ -116,7 +118,7 @@ Example:
|
|||||||
Inject Query String Parameters
|
Inject Query String Parameters
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
To declare a query parameters, you must declare your argument as simple argument:
|
To declare a query parameters, you must declare your argument as a simple argument:
|
||||||
|
|
||||||
|
|
||||||
.. code-block:: python3
|
.. code-block:: python3
|
||||||
@@ -131,7 +133,7 @@ To declare a query parameters, you must declare your argument as simple argument
|
|||||||
Inject Request Body
|
Inject Request Body
|
||||||
~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
To declare a body parameters, you must declare your argument as a simple argument annotated with `pydantic Model`_.
|
To declare a body parameter, you must declare your argument as a simple argument annotated with `pydantic Model`_.
|
||||||
|
|
||||||
|
|
||||||
.. code-block:: python3
|
.. code-block:: python3
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
from .view import PydanticView
|
from .view import PydanticView
|
||||||
|
|
||||||
__version__ = "1.5.1"
|
__version__ = "1.6.1"
|
||||||
|
|
||||||
__all__ = ("PydanticView", "__version__")
|
__all__ = ("PydanticView", "__version__")
|
||||||
|
@@ -15,8 +15,16 @@ class AbstractInjector(metaclass=abc.ABCMeta):
|
|||||||
An injector parse HTTP request and inject params to the view.
|
An injector parse HTTP request and inject params to the view.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def __init__(self, args_spec: dict):
|
def context(self) -> str:
|
||||||
|
"""
|
||||||
|
The name of part of parsed request
|
||||||
|
i.e "HTTP header", "URL path", ...
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def __init__(self, args_spec: dict, default_values: dict):
|
||||||
"""
|
"""
|
||||||
args_spec - ordered mapping: arg_name -> type
|
args_spec - ordered mapping: arg_name -> type
|
||||||
"""
|
"""
|
||||||
@@ -33,8 +41,12 @@ class MatchInfoGetter(AbstractInjector):
|
|||||||
Validates and injects the part of URL path inside the view positional args.
|
Validates and injects the part of URL path inside the view positional args.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, args_spec: dict):
|
context = "path"
|
||||||
self.model = type("PathModel", (BaseModel,), {"__annotations__": args_spec})
|
|
||||||
|
def __init__(self, args_spec: dict, default_values: dict):
|
||||||
|
attrs = {"__annotations__": args_spec}
|
||||||
|
attrs.update(default_values)
|
||||||
|
self.model = type("PathModel", (BaseModel,), attrs)
|
||||||
|
|
||||||
def inject(self, request: BaseRequest, args_view: list, kwargs_view: dict):
|
def inject(self, request: BaseRequest, args_view: list, kwargs_view: dict):
|
||||||
args_view.extend(self.model(**request.match_info).dict().values())
|
args_view.extend(self.model(**request.match_info).dict().values())
|
||||||
@@ -45,7 +57,9 @@ class BodyGetter(AbstractInjector):
|
|||||||
Validates and injects the content of request body inside the view kwargs.
|
Validates and injects the content of request body inside the view kwargs.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, args_spec: dict):
|
context = "body"
|
||||||
|
|
||||||
|
def __init__(self, args_spec: dict, default_values: dict):
|
||||||
self.arg_name, self.model = next(iter(args_spec.items()))
|
self.arg_name, self.model = next(iter(args_spec.items()))
|
||||||
|
|
||||||
async def inject(self, request: BaseRequest, args_view: list, kwargs_view: dict):
|
async def inject(self, request: BaseRequest, args_view: list, kwargs_view: dict):
|
||||||
@@ -54,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)
|
||||||
|
|
||||||
@@ -64,8 +78,12 @@ class QueryGetter(AbstractInjector):
|
|||||||
Validates and injects the query string inside the view kwargs.
|
Validates and injects the query string inside the view kwargs.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, args_spec: dict):
|
context = "query string"
|
||||||
self.model = type("QueryModel", (BaseModel,), {"__annotations__": args_spec})
|
|
||||||
|
def __init__(self, args_spec: dict, default_values: dict):
|
||||||
|
attrs = {"__annotations__": args_spec}
|
||||||
|
attrs.update(default_values)
|
||||||
|
self.model = type("QueryModel", (BaseModel,), attrs)
|
||||||
|
|
||||||
def inject(self, request: BaseRequest, args_view: list, kwargs_view: dict):
|
def inject(self, request: BaseRequest, args_view: list, kwargs_view: dict):
|
||||||
kwargs_view.update(self.model(**request.query).dict())
|
kwargs_view.update(self.model(**request.query).dict())
|
||||||
@@ -76,27 +94,33 @@ class HeadersGetter(AbstractInjector):
|
|||||||
Validates and injects the HTTP headers inside the view kwargs.
|
Validates and injects the HTTP headers inside the view kwargs.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, args_spec: dict):
|
context = "headers"
|
||||||
self.model = type("HeaderModel", (BaseModel,), {"__annotations__": args_spec})
|
|
||||||
|
def __init__(self, args_spec: dict, default_values: dict):
|
||||||
|
attrs = {"__annotations__": args_spec}
|
||||||
|
attrs.update(default_values)
|
||||||
|
self.model = type("HeaderModel", (BaseModel,), attrs)
|
||||||
|
|
||||||
def inject(self, request: BaseRequest, args_view: list, kwargs_view: dict):
|
def inject(self, request: BaseRequest, args_view: list, kwargs_view: dict):
|
||||||
header = {k.lower().replace("-", "_"): v for k, v in request.headers.items()}
|
header = {k.lower().replace("-", "_"): v for k, v in request.headers.items()}
|
||||||
kwargs_view.update(self.model(**header).dict())
|
kwargs_view.update(self.model(**header).dict())
|
||||||
|
|
||||||
|
|
||||||
def _parse_func_signature(func: Callable) -> Tuple[dict, dict, dict, dict]:
|
def _parse_func_signature(func: Callable) -> Tuple[dict, dict, dict, dict, dict]:
|
||||||
"""
|
"""
|
||||||
Analyse function signature and returns 4-tuple:
|
Analyse function signature and returns 4-tuple:
|
||||||
0 - arguments will be set from the url path
|
0 - arguments will be set from the url path
|
||||||
1 - argument will be set from the request body.
|
1 - argument will be set from the request body.
|
||||||
2 - argument will be set from the query string.
|
2 - argument will be set from the query string.
|
||||||
3 - argument will be set from the HTTP headers.
|
3 - argument will be set from the HTTP headers.
|
||||||
|
4 - Default value for each parameters
|
||||||
"""
|
"""
|
||||||
|
|
||||||
path_args = {}
|
path_args = {}
|
||||||
body_args = {}
|
body_args = {}
|
||||||
qs_args = {}
|
qs_args = {}
|
||||||
header_args = {}
|
header_args = {}
|
||||||
|
defaults = {}
|
||||||
|
|
||||||
for param_name, param_spec in signature(func).parameters.items():
|
for param_name, param_spec in signature(func).parameters.items():
|
||||||
if param_name == "self":
|
if param_name == "self":
|
||||||
@@ -105,8 +129,12 @@ def _parse_func_signature(func: Callable) -> Tuple[dict, dict, dict, dict]:
|
|||||||
if param_spec.annotation == param_spec.empty:
|
if param_spec.annotation == param_spec.empty:
|
||||||
raise RuntimeError(f"The parameter {param_name} must have an annotation")
|
raise RuntimeError(f"The parameter {param_name} must have an annotation")
|
||||||
|
|
||||||
|
if param_spec.default is not param_spec.empty:
|
||||||
|
defaults[param_name] = param_spec.default
|
||||||
|
|
||||||
if param_spec.kind is param_spec.POSITIONAL_ONLY:
|
if param_spec.kind is param_spec.POSITIONAL_ONLY:
|
||||||
path_args[param_name] = param_spec.annotation
|
path_args[param_name] = param_spec.annotation
|
||||||
|
|
||||||
elif param_spec.kind is param_spec.POSITIONAL_OR_KEYWORD:
|
elif param_spec.kind is param_spec.POSITIONAL_OR_KEYWORD:
|
||||||
if is_pydantic_base_model(param_spec.annotation):
|
if is_pydantic_base_model(param_spec.annotation):
|
||||||
body_args[param_name] = param_spec.annotation
|
body_args[param_name] = param_spec.annotation
|
||||||
@@ -117,4 +145,4 @@ def _parse_func_signature(func: Callable) -> Tuple[dict, dict, dict, dict]:
|
|||||||
else:
|
else:
|
||||||
raise RuntimeError(f"You cannot use {param_spec.VAR_POSITIONAL} parameters")
|
raise RuntimeError(f"You cannot use {param_spec.VAR_POSITIONAL} parameters")
|
||||||
|
|
||||||
return path_args, body_args, qs_args, header_args
|
return path_args, body_args, qs_args, header_args, defaults
|
||||||
|
@@ -13,13 +13,10 @@ def setup(
|
|||||||
apps_to_expose: Iterable[web.Application] = (),
|
apps_to_expose: Iterable[web.Application] = (),
|
||||||
url_prefix: str = "/oas",
|
url_prefix: str = "/oas",
|
||||||
enable: bool = True,
|
enable: bool = True,
|
||||||
raise_validation_errors: bool = False,
|
|
||||||
):
|
):
|
||||||
if enable:
|
if enable:
|
||||||
oas_app = web.Application()
|
oas_app = web.Application()
|
||||||
oas_app["apps to expose"] = tuple(apps_to_expose) or (app,)
|
oas_app["apps to expose"] = tuple(apps_to_expose) or (app,)
|
||||||
for a in oas_app["apps to expose"]:
|
|
||||||
a['raise_validation_errors'] = raise_validation_errors
|
|
||||||
oas_app["index template"] = jinja2.Template(
|
oas_app["index template"] = jinja2.Template(
|
||||||
resources.read_text("aiohttp_pydantic.oas", "index.j2")
|
resources.read_text("aiohttp_pydantic.oas", "index.j2")
|
||||||
)
|
)
|
||||||
|
@@ -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"}
|
||||||
@@ -310,10 +319,9 @@ class OpenApiSpec3:
|
|||||||
return Paths(self._spec)
|
return Paths(self._spec)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def spec(self):
|
def components(self) -> Components:
|
||||||
return self._spec
|
return Components(self._spec)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def definitions(self):
|
def spec(self):
|
||||||
self._spec.setdefault('definitions', {})
|
return self._spec
|
||||||
return self._spec['definitions']
|
|
||||||
|
@@ -13,7 +13,7 @@ Example:
|
|||||||
|
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from types import new_class
|
from types import new_class
|
||||||
from typing import Protocol, TypeVar, Optional, Type
|
from typing import Protocol, TypeVar
|
||||||
|
|
||||||
RespContents = TypeVar("RespContents", covariant=True)
|
RespContents = TypeVar("RespContents", covariant=True)
|
||||||
|
|
||||||
|
@@ -1,12 +1,11 @@
|
|||||||
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
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from aiohttp_pydantic.oas.struct import OpenApiSpec3, OperationObject, PathItem
|
from aiohttp_pydantic.oas.struct import OpenApiSpec3, OperationObject, PathItem
|
||||||
|
|
||||||
@@ -15,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_):
|
||||||
"""
|
"""
|
||||||
@@ -47,21 +36,16 @@ class _OASResponseBuilder:
|
|||||||
generate the OAS operation response.
|
generate the OAS operation response.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, oas_operation, definitions):
|
def __init__(self, oas: OpenApiSpec3, oas_operation):
|
||||||
self._oas_operation = oas_operation
|
self._oas_operation = oas_operation
|
||||||
self._definitions = definitions
|
self._oas = oas
|
||||||
|
|
||||||
def _process_definitions(self, schema):
|
|
||||||
if 'definitions' in schema:
|
|
||||||
for k, v in schema['definitions'].items():
|
|
||||||
self._definitions[k] = v
|
|
||||||
|
|
||||||
return {i:schema[i] for i in schema if i!='definitions'}
|
|
||||||
|
|
||||||
def _handle_pydantic_base_model(self, obj):
|
def _handle_pydantic_base_model(self, obj):
|
||||||
if is_pydantic_base_model(obj):
|
if is_pydantic_base_model(obj):
|
||||||
return self._process_definitions(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):
|
||||||
@@ -96,19 +80,27 @@ class _OASResponseBuilder:
|
|||||||
|
|
||||||
|
|
||||||
def _add_http_method_to_oas(
|
def _add_http_method_to_oas(
|
||||||
oas_path: PathItem, http_method: str, view: Type[PydanticView], definitions: dict
|
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)
|
||||||
handler = getattr(view, http_method)
|
handler = getattr(view, http_method)
|
||||||
path_args, body_args, qs_args, header_args = _parse_func_signature(handler)
|
path_args, body_args, qs_args, header_args, defaults = _parse_func_signature(
|
||||||
|
handler
|
||||||
|
)
|
||||||
description = getdoc(handler)
|
description = getdoc(handler)
|
||||||
if description:
|
if description:
|
||||||
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,16 +114,20 @@ def _add_http_method_to_oas(
|
|||||||
oas_operation.parameters[i].in_ = args_location
|
oas_operation.parameters[i].in_ = args_location
|
||||||
oas_operation.parameters[i].name = name
|
oas_operation.parameters[i].name = name
|
||||||
optional_type = _handle_optional(type_)
|
optional_type = _handle_optional(type_)
|
||||||
if optional_type is None:
|
|
||||||
oas_operation.parameters[i].schema = JSON_SCHEMA_TYPES[type_]
|
attrs = {"__annotations__": {"__root__": type_}}
|
||||||
oas_operation.parameters[i].required = True
|
if name in defaults:
|
||||||
else:
|
attrs["__root__"] = defaults[name]
|
||||||
oas_operation.parameters[i].schema = JSON_SCHEMA_TYPES[optional_type]
|
|
||||||
oas_operation.parameters[i].required = False
|
oas_operation.parameters[i].schema = type(name, (BaseModel,), attrs).schema(
|
||||||
|
ref_template="#/components/schemas/{model}"
|
||||||
|
)
|
||||||
|
|
||||||
|
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, definitions).build(return_type)
|
_OASResponseBuilder(oas, oas_operation).build(return_type)
|
||||||
|
|
||||||
|
|
||||||
def generate_oas(apps: List[Application]) -> dict:
|
def generate_oas(apps: List[Application]) -> dict:
|
||||||
@@ -139,19 +135,20 @@ def generate_oas(apps: List[Application]) -> dict:
|
|||||||
Generate and return Open Api Specification from PydanticView in application.
|
Generate and return Open Api Specification from PydanticView in application.
|
||||||
"""
|
"""
|
||||||
oas = OpenApiSpec3()
|
oas = OpenApiSpec3()
|
||||||
|
|
||||||
for app in apps:
|
for app in apps:
|
||||||
for resources in app.router.resources():
|
for resources in app.router.resources():
|
||||||
for resource_route in resources:
|
for resource_route in resources:
|
||||||
if is_pydantic_view(resource_route.handler):
|
if not is_pydantic_view(resource_route.handler):
|
||||||
view: Type[PydanticView] = resource_route.handler
|
continue
|
||||||
info = resource_route.get_info()
|
|
||||||
path = oas.paths[info.get("path", info.get("formatter"))]
|
view: Type[PydanticView] = resource_route.handler
|
||||||
if resource_route.method == "*":
|
info = resource_route.get_info()
|
||||||
for method_name in view.allowed_methods:
|
path = oas.paths[info.get("path", info.get("formatter"))]
|
||||||
_add_http_method_to_oas(path, method_name, view, oas.definitions)
|
if resource_route.method == "*":
|
||||||
else:
|
for method_name in view.allowed_methods:
|
||||||
_add_http_method_to_oas(path, resource_route.method, view, oas.definitions)
|
_add_http_method_to_oas(oas, path, method_name, view)
|
||||||
|
else:
|
||||||
|
_add_http_method_to_oas(oas, path, resource_route.method, view)
|
||||||
|
|
||||||
return oas.spec
|
return oas.spec
|
||||||
|
|
||||||
@@ -172,9 +169,6 @@ async def oas_ui(request):
|
|||||||
|
|
||||||
static_url = request.app.router["static"].url_for(filename="")
|
static_url = request.app.router["static"].url_for(filename="")
|
||||||
spec_url = request.app.router["spec"].url_for()
|
spec_url = request.app.router["spec"].url_for()
|
||||||
|
|
||||||
if request.scheme != request.headers.get('x-forwarded-proto', request.scheme):
|
|
||||||
request = request.clone(scheme=request.headers['x-forwarded-proto'])
|
|
||||||
host = request.url.origin()
|
host = request.url.origin()
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
|
@@ -9,14 +9,8 @@ from aiohttp.web_exceptions import HTTPMethodNotAllowed
|
|||||||
from aiohttp.web_response import StreamResponse
|
from aiohttp.web_response import StreamResponse
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from .injectors import (
|
from .injectors import (AbstractInjector, BodyGetter, HeadersGetter,
|
||||||
AbstractInjector,
|
MatchInfoGetter, QueryGetter, _parse_func_signature)
|
||||||
BodyGetter,
|
|
||||||
HeadersGetter,
|
|
||||||
MatchInfoGetter,
|
|
||||||
QueryGetter,
|
|
||||||
_parse_func_signature,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PydanticView(AbstractView):
|
class PydanticView(AbstractView):
|
||||||
@@ -50,16 +44,25 @@ class PydanticView(AbstractView):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_func_signature(func: Callable) -> Iterable[AbstractInjector]:
|
def parse_func_signature(func: Callable) -> Iterable[AbstractInjector]:
|
||||||
path_args, body_args, qs_args, header_args = _parse_func_signature(func)
|
path_args, body_args, qs_args, header_args, defaults = _parse_func_signature(
|
||||||
|
func
|
||||||
|
)
|
||||||
injectors = []
|
injectors = []
|
||||||
|
|
||||||
|
def default_value(args: dict) -> dict:
|
||||||
|
"""
|
||||||
|
Returns the default values of args.
|
||||||
|
"""
|
||||||
|
return {name: defaults[name] for name in args if name in defaults}
|
||||||
|
|
||||||
if path_args:
|
if path_args:
|
||||||
injectors.append(MatchInfoGetter(path_args))
|
injectors.append(MatchInfoGetter(path_args, default_value(path_args)))
|
||||||
if body_args:
|
if body_args:
|
||||||
injectors.append(BodyGetter(body_args))
|
injectors.append(BodyGetter(body_args, default_value(body_args)))
|
||||||
if qs_args:
|
if qs_args:
|
||||||
injectors.append(QueryGetter(qs_args))
|
injectors.append(QueryGetter(qs_args, default_value(qs_args)))
|
||||||
if header_args:
|
if header_args:
|
||||||
injectors.append(HeadersGetter(header_args))
|
injectors.append(HeadersGetter(header_args, default_value(header_args)))
|
||||||
return injectors
|
return injectors
|
||||||
|
|
||||||
|
|
||||||
@@ -83,10 +86,11 @@ def inject_params(
|
|||||||
else:
|
else:
|
||||||
injector.inject(self.request, args, kwargs)
|
injector.inject(self.request, args, kwargs)
|
||||||
except ValidationError as error:
|
except ValidationError as error:
|
||||||
if self.request.app['raise_validation_errors']:
|
errors = error.errors()
|
||||||
raise
|
for error in errors:
|
||||||
else:
|
error["in"] = injector.context
|
||||||
return json_response(text=error.json(), status=400)
|
|
||||||
|
return json_response(data=errors, status=400)
|
||||||
|
|
||||||
return await handler(self, *args, **kwargs)
|
return await handler(self, *args, **kwargs)
|
||||||
|
|
||||||
|
@@ -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]
|
||||||
|
@@ -30,6 +30,7 @@ def test_show_oad_of_app(cmd_line, capfd):
|
|||||||
"name": "a",
|
"name": "a",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
|
"title": "a",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -44,6 +45,7 @@ def test_show_oad_of_app(cmd_line, capfd):
|
|||||||
"name": "b",
|
"name": "b",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
|
"title": "b",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,6 +77,7 @@ def test_show_oad_of_sub_app(cmd_line, capfd):
|
|||||||
"name": "b",
|
"name": "b",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
|
"title": "b",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,6 +109,7 @@ def test_show_oad_of_a_callable(cmd_line, capfd):
|
|||||||
"name": "a",
|
"name": "a",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
|
"title": "a",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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"]
|
||||||
|
|
||||||
@@ -65,19 +98,19 @@ async def test_pets_route_should_have_get_method(generated_oas):
|
|||||||
"in": "query",
|
"in": "query",
|
||||||
"name": "format",
|
"name": "format",
|
||||||
"required": True,
|
"required": True,
|
||||||
"schema": {"type": "string"},
|
"schema": {"title": "format", "type": "string"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"in": "query",
|
"in": "query",
|
||||||
"name": "name",
|
"name": "name",
|
||||||
"required": False,
|
"required": False,
|
||||||
"schema": {"type": "string"},
|
"schema": {"title": "name", "type": "string"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"in": "header",
|
"in": "header",
|
||||||
"name": "promo",
|
"name": "promo",
|
||||||
"required": False,
|
"required": False,
|
||||||
"schema": {"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",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -152,7 +253,7 @@ async def test_pets_id_route_should_have_delete_method(generated_oas):
|
|||||||
"required": True,
|
"required": True,
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"name": "id",
|
"name": "id",
|
||||||
"schema": {"type": "integer"},
|
"schema": {"title": "id", "type": "integer"},
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {"204": {"content": {}}},
|
"responses": {"204": {"content": {}}},
|
||||||
@@ -166,7 +267,7 @@ async def test_pets_id_route_should_have_get_method(generated_oas):
|
|||||||
"in": "path",
|
"in": "path",
|
||||||
"name": "id",
|
"name": "id",
|
||||||
"required": True,
|
"required": True,
|
||||||
"schema": {"type": "integer"},
|
"schema": {"title": "id", "type": "integer"},
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
@@ -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",
|
||||||
}
|
}
|
||||||
@@ -197,18 +320,40 @@ async def test_pets_id_route_should_have_put_method(generated_oas):
|
|||||||
"in": "path",
|
"in": "path",
|
||||||
"name": "id",
|
"name": "id",
|
||||||
"required": True,
|
"required": True,
|
||||||
"schema": {"type": "integer"},
|
"schema": {"title": "id", "type": "integer"},
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
"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",
|
||||||
}
|
}
|
||||||
|
@@ -38,32 +38,42 @@ def test_parse_func_signature():
|
|||||||
def path_body_qs_and_header(self, id: str, /, user: User, page: int, *, auth: UUID):
|
def path_body_qs_and_header(self, id: str, /, user: User, page: int, *, auth: UUID):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
assert _parse_func_signature(body_only) == ({}, {"user": User}, {}, {})
|
assert _parse_func_signature(body_only) == ({}, {"user": User}, {}, {}, {})
|
||||||
assert _parse_func_signature(path_only) == ({"id": str}, {}, {}, {})
|
assert _parse_func_signature(path_only) == ({"id": str}, {}, {}, {}, {})
|
||||||
assert _parse_func_signature(qs_only) == ({}, {}, {"page": int}, {})
|
assert _parse_func_signature(qs_only) == ({}, {}, {"page": int}, {}, {})
|
||||||
assert _parse_func_signature(header_only) == ({}, {}, {}, {"auth": UUID})
|
assert _parse_func_signature(header_only) == ({}, {}, {}, {"auth": UUID}, {})
|
||||||
assert _parse_func_signature(path_and_qs) == ({"id": str}, {}, {"page": int}, {})
|
assert _parse_func_signature(path_and_qs) == (
|
||||||
|
{"id": str},
|
||||||
|
{},
|
||||||
|
{"page": int},
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
)
|
||||||
assert _parse_func_signature(path_and_header) == (
|
assert _parse_func_signature(path_and_header) == (
|
||||||
{"id": str},
|
{"id": str},
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
{"auth": UUID},
|
{"auth": UUID},
|
||||||
|
{},
|
||||||
)
|
)
|
||||||
assert _parse_func_signature(qs_and_header) == (
|
assert _parse_func_signature(qs_and_header) == (
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
{"page": int},
|
{"page": int},
|
||||||
{"auth": UUID},
|
{"auth": UUID},
|
||||||
|
{},
|
||||||
)
|
)
|
||||||
assert _parse_func_signature(path_qs_and_header) == (
|
assert _parse_func_signature(path_qs_and_header) == (
|
||||||
{"id": str},
|
{"id": str},
|
||||||
{},
|
{},
|
||||||
{"page": int},
|
{"page": int},
|
||||||
{"auth": UUID},
|
{"auth": UUID},
|
||||||
|
{},
|
||||||
)
|
)
|
||||||
assert _parse_func_signature(path_body_qs_and_header) == (
|
assert _parse_func_signature(path_body_qs_and_header) == (
|
||||||
{"id": str},
|
{"id": str},
|
||||||
{"user": User},
|
{"user": User},
|
||||||
{"page": int},
|
{"page": int},
|
||||||
{"auth": UUID},
|
{"auth": UUID},
|
||||||
|
{},
|
||||||
)
|
)
|
||||||
|
@@ -27,7 +27,12 @@ async def test_post_an_article_without_required_field_should_return_an_error_mes
|
|||||||
assert resp.status == 400
|
assert resp.status == 400
|
||||||
assert resp.content_type == "application/json"
|
assert resp.content_type == "application/json"
|
||||||
assert await resp.json() == [
|
assert await resp.json() == [
|
||||||
{"loc": ["name"], "msg": "field required", "type": "value_error.missing"}
|
{
|
||||||
|
"in": "body",
|
||||||
|
"loc": ["name"],
|
||||||
|
"msg": "field required",
|
||||||
|
"type": "value_error.missing",
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -43,6 +48,7 @@ async def test_post_an_article_with_wrong_type_field_should_return_an_error_mess
|
|||||||
assert resp.content_type == "application/json"
|
assert resp.content_type == "application/json"
|
||||||
assert await resp.json() == [
|
assert await resp.json() == [
|
||||||
{
|
{
|
||||||
|
"in": "body",
|
||||||
"loc": ["nb_page"],
|
"loc": ["nb_page"],
|
||||||
"msg": "value is not a valid integer",
|
"msg": "value is not a valid integer",
|
||||||
"type": "type_error.integer",
|
"type": "type_error.integer",
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
@@ -21,6 +22,16 @@ class ArticleView(PydanticView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FormatEnum(str, Enum):
|
||||||
|
UTM = "UMT"
|
||||||
|
MGRS = "MGRS"
|
||||||
|
|
||||||
|
|
||||||
|
class ViewWithEnumType(PydanticView):
|
||||||
|
async def get(self, *, format: FormatEnum):
|
||||||
|
return web.json_response({"format": format}, dumps=JSONEncoder().encode)
|
||||||
|
|
||||||
|
|
||||||
async def test_get_article_without_required_header_should_return_an_error_message(
|
async def test_get_article_without_required_header_should_return_an_error_message(
|
||||||
aiohttp_client, loop
|
aiohttp_client, loop
|
||||||
):
|
):
|
||||||
@@ -33,6 +44,7 @@ async def test_get_article_without_required_header_should_return_an_error_messag
|
|||||||
assert resp.content_type == "application/json"
|
assert resp.content_type == "application/json"
|
||||||
assert await resp.json() == [
|
assert await resp.json() == [
|
||||||
{
|
{
|
||||||
|
"in": "headers",
|
||||||
"loc": ["signature_expired"],
|
"loc": ["signature_expired"],
|
||||||
"msg": "field required",
|
"msg": "field required",
|
||||||
"type": "value_error.missing",
|
"type": "value_error.missing",
|
||||||
@@ -52,6 +64,7 @@ async def test_get_article_with_wrong_header_type_should_return_an_error_message
|
|||||||
assert resp.content_type == "application/json"
|
assert resp.content_type == "application/json"
|
||||||
assert await resp.json() == [
|
assert await resp.json() == [
|
||||||
{
|
{
|
||||||
|
"in": "headers",
|
||||||
"loc": ["signature_expired"],
|
"loc": ["signature_expired"],
|
||||||
"msg": "invalid datetime format",
|
"msg": "invalid datetime format",
|
||||||
"type": "value_error.datetime",
|
"type": "value_error.datetime",
|
||||||
@@ -87,3 +100,37 @@ async def test_get_article_with_valid_header_containing_hyphen_should_be_returne
|
|||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
assert resp.content_type == "application/json"
|
assert resp.content_type == "application/json"
|
||||||
assert await resp.json() == {"signature": "2020-10-04T18:01:00"}
|
assert await resp.json() == {"signature": "2020-10-04T18:01:00"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_wrong_value_to_header_defined_with_str_enum(aiohttp_client, loop):
|
||||||
|
app = web.Application()
|
||||||
|
app.router.add_view("/coord", ViewWithEnumType)
|
||||||
|
|
||||||
|
client = await aiohttp_client(app)
|
||||||
|
resp = await client.get("/coord", headers={"format": "WGS84"})
|
||||||
|
assert (
|
||||||
|
await resp.json()
|
||||||
|
== [
|
||||||
|
{
|
||||||
|
"ctx": {"enum_values": ["UMT", "MGRS"]},
|
||||||
|
"in": "headers",
|
||||||
|
"loc": ["format"],
|
||||||
|
"msg": "value is not a valid enumeration member; permitted: 'UMT', 'MGRS'",
|
||||||
|
"type": "type_error.enum",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
!= {"signature": "2020-10-04T18:01:00"}
|
||||||
|
)
|
||||||
|
assert resp.status == 400
|
||||||
|
assert resp.content_type == "application/json"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_correct_value_to_header_defined_with_str_enum(aiohttp_client, loop):
|
||||||
|
app = web.Application()
|
||||||
|
app.router.add_view("/coord", ViewWithEnumType)
|
||||||
|
|
||||||
|
client = await aiohttp_client(app)
|
||||||
|
resp = await client.get("/coord", headers={"format": "UMT"})
|
||||||
|
assert await resp.json() == {"format": "UMT"}
|
||||||
|
assert resp.status == 200
|
||||||
|
assert resp.content_type == "application/json"
|
||||||
|
@@ -8,7 +8,7 @@ class ArticleView(PydanticView):
|
|||||||
return web.json_response({"path": [author_id, tag, date]})
|
return web.json_response({"path": [author_id, tag, date]})
|
||||||
|
|
||||||
|
|
||||||
async def test_get_article_without_required_qs_should_return_an_error_message(
|
async def test_get_article_with_correct_path_parameters_should_return_parameters_in_path(
|
||||||
aiohttp_client, loop
|
aiohttp_client, loop
|
||||||
):
|
):
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
@@ -19,3 +19,23 @@ async def test_get_article_without_required_qs_should_return_an_error_message(
|
|||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
assert resp.content_type == "application/json"
|
assert resp.content_type == "application/json"
|
||||||
assert await resp.json() == {"path": ["1234", "music", 1980]}
|
assert await resp.json() == {"path": ["1234", "music", 1980]}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_article_with_wrong_path_parameters_should_return_error(
|
||||||
|
aiohttp_client, loop
|
||||||
|
):
|
||||||
|
app = web.Application()
|
||||||
|
app.router.add_view("/article/{author_id}/tag/{tag}/before/{date}", ArticleView)
|
||||||
|
|
||||||
|
client = await aiohttp_client(app)
|
||||||
|
resp = await client.get("/article/1234/tag/music/before/now")
|
||||||
|
assert resp.status == 400
|
||||||
|
assert resp.content_type == "application/json"
|
||||||
|
assert await resp.json() == [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"loc": ["date"],
|
||||||
|
"msg": "value is not a valid integer",
|
||||||
|
"type": "type_error.integer",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
@@ -6,8 +6,12 @@ from aiohttp_pydantic import PydanticView
|
|||||||
|
|
||||||
|
|
||||||
class ArticleView(PydanticView):
|
class ArticleView(PydanticView):
|
||||||
async def get(self, with_comments: bool, age: Optional[int] = None):
|
async def get(
|
||||||
return web.json_response({"with_comments": with_comments, "age": age})
|
self, with_comments: bool, age: Optional[int] = None, nb_items: int = 7
|
||||||
|
):
|
||||||
|
return web.json_response(
|
||||||
|
{"with_comments": with_comments, "age": age, "nb_items": nb_items}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_get_article_without_required_qs_should_return_an_error_message(
|
async def test_get_article_without_required_qs_should_return_an_error_message(
|
||||||
@@ -22,6 +26,7 @@ async def test_get_article_without_required_qs_should_return_an_error_message(
|
|||||||
assert resp.content_type == "application/json"
|
assert resp.content_type == "application/json"
|
||||||
assert await resp.json() == [
|
assert await resp.json() == [
|
||||||
{
|
{
|
||||||
|
"in": "query string",
|
||||||
"loc": ["with_comments"],
|
"loc": ["with_comments"],
|
||||||
"msg": "field required",
|
"msg": "field required",
|
||||||
"type": "value_error.missing",
|
"type": "value_error.missing",
|
||||||
@@ -41,6 +46,7 @@ async def test_get_article_with_wrong_qs_type_should_return_an_error_message(
|
|||||||
assert resp.content_type == "application/json"
|
assert resp.content_type == "application/json"
|
||||||
assert await resp.json() == [
|
assert await resp.json() == [
|
||||||
{
|
{
|
||||||
|
"in": "query string",
|
||||||
"loc": ["with_comments"],
|
"loc": ["with_comments"],
|
||||||
"msg": "value could not be parsed to a boolean",
|
"msg": "value could not be parsed to a boolean",
|
||||||
"type": "type_error.bool",
|
"type": "type_error.bool",
|
||||||
@@ -59,10 +65,10 @@ async def test_get_article_with_valid_qs_should_return_the_parsed_type(
|
|||||||
resp = await client.get("/article", params={"with_comments": "yes", "age": 3})
|
resp = await client.get("/article", params={"with_comments": "yes", "age": 3})
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
assert resp.content_type == "application/json"
|
assert resp.content_type == "application/json"
|
||||||
assert await resp.json() == {"with_comments": True, "age": 3}
|
assert await resp.json() == {"with_comments": True, "age": 3, "nb_items": 7}
|
||||||
|
|
||||||
|
|
||||||
async def test_get_article_with_valid_qs_and_omitted_optional_should_return_none(
|
async def test_get_article_with_valid_qs_and_omitted_optional_should_return_default_value(
|
||||||
aiohttp_client, loop
|
aiohttp_client, loop
|
||||||
):
|
):
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
@@ -71,6 +77,6 @@ async def test_get_article_with_valid_qs_and_omitted_optional_should_return_none
|
|||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
|
|
||||||
resp = await client.get("/article", params={"with_comments": "yes"})
|
resp = await client.get("/article", params={"with_comments": "yes"})
|
||||||
|
assert await resp.json() == {"with_comments": True, "age": None, "nb_items": 7}
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
assert resp.content_type == "application/json"
|
assert resp.content_type == "application/json"
|
||||||
assert await resp.json() == {"with_comments": True, "age": None}
|
|
||||||
|
Reference in New Issue
Block a user