Compare commits
	
		
			21 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 5c5a701daa | ||
|  | 5ead48ec92 | ||
|  | 1f2ac30106 | ||
|  | 483b457b14 | ||
|  | 3126c2fd2e | ||
|  | a45101637c | ||
|  | 937c09e2b7 | ||
|  | 82c638c1e0 | ||
|  | 7ce5e5d0d4 | ||
|  | 93e391b7b2 | ||
|  | a94c9d4863 | ||
|  | 26fd6fa19f | ||
|  | be944ac98e | ||
|  | b896020a4f | ||
|  | ba0530d6b1 | ||
|  | 83739c7c8e | ||
|  | 1dd98d2752 | ||
|  | 207204fe53 | ||
|  | 7618066b7f | ||
|  | 554e76ce51 | ||
|  | 2c51e9d929 | 
							
								
								
									
										24
									
								
								.gitea/workflows/release.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								.gitea/workflows/release.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | |||||||
|  | name: Release | ||||||
|  | run-name: ${{ gitea.actor }} is runs ci pipeline | ||||||
|  |  | ||||||
|  | on: | ||||||
|  |   push: | ||||||
|  |     tags: | ||||||
|  |       - 'v*.*.*' | ||||||
|  |  | ||||||
|  | jobs: | ||||||
|  |   packaging: | ||||||
|  |     name: Distribution | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |       - uses: actions/checkout@v4 | ||||||
|  |       - name: Set up Python 3.12 | ||||||
|  |         uses: actions/setup-python@v5 | ||||||
|  |         with: | ||||||
|  |           python-version: '3.12' | ||||||
|  |       - name: Update version | ||||||
|  |         run: sed -i -e "s/1.12.1/${{ gitea.ref_name:1 }}/g" aiohttp_pydantic/__init__.py | ||||||
|  |       - name: Install invoke | ||||||
|  |         run: python -m pip install setuptools wheel invoke | ||||||
|  |       - name: Push to PyPi | ||||||
|  |         run: invoke upload --pypi-user ${{ secrets.REPO_USER }} --pypi-password ${{ secrets.REPO_PASS }} --pypi-url https://git.ahax86.ru/api/packages/pub/pypi | ||||||
| @@ -3,10 +3,9 @@ stages: | |||||||
|  |  | ||||||
| publish-pypi: | publish-pypi: | ||||||
|   stage: package |   stage: package | ||||||
|   image: python:3.10 |   image: python:3.11 | ||||||
|   script: |   script: | ||||||
|     - sed -i -e 's/1.12.1/${CI_COMMIT_TAG}/g' aiohttp_pydantic/__init__.py |     - sed -i -e "s/1.12.1/${CI_COMMIT_TAG:1}/g" aiohttp_pydantic/__init__.py | ||||||
|     - cat aiohttp_pydantic/__init__.py |  | ||||||
|     - pip install -U setuptools wheel pip; pip install invoke |     - pip install -U setuptools wheel pip; pip install invoke | ||||||
|     - invoke upload --pypi-user ${PYPI_REPO_USER} --pypi-password ${PYPI_REPO_PASSWORD} --pypi-url ${PYPI_REPO_URL} |     - invoke upload --pypi-user ${PYPI_REPO_USER} --pypi-password ${PYPI_REPO_PASSWORD} --pypi-url ${PYPI_REPO_URL} | ||||||
|   only: |   only: | ||||||
|   | |||||||
| @@ -56,7 +56,7 @@ class MatchInfoGetter(AbstractInjector): | |||||||
|         self.model = type("PathModel", (BaseModel,), attrs) |         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).model_dump().values()) | ||||||
|  |  | ||||||
|  |  | ||||||
| class BodyGetter(AbstractInjector): | class BodyGetter(AbstractInjector): | ||||||
| @@ -68,7 +68,10 @@ class BodyGetter(AbstractInjector): | |||||||
|  |  | ||||||
|     def __init__(self, args_spec: dict, default_values: dict): |     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())) | ||||||
|         self._expect_object = self.model.schema()["type"] == "object" |         schema = self.model.model_json_schema() | ||||||
|  |         if "type" not in schema: | ||||||
|  |             schema["type"] = "object" | ||||||
|  |         self._expect_object = schema["type"] == "object" | ||||||
|  |  | ||||||
|     async def inject(self, request: BaseRequest, args_view: list, kwargs_view: dict): |     async def inject(self, request: BaseRequest, args_view: list, kwargs_view: dict): | ||||||
|         try: |         try: | ||||||
| @@ -82,7 +85,7 @@ class BodyGetter(AbstractInjector): | |||||||
|         # to a dict. Prevent this by requiring the body to be a dict for object models. |         # to a dict. Prevent this by requiring the body to be a dict for object models. | ||||||
|         if self._expect_object and not isinstance(body, dict): |         if self._expect_object and not isinstance(body, dict): | ||||||
|             raise HTTPBadRequest( |             raise HTTPBadRequest( | ||||||
|                 text='[{"in": "body", "loc": ["__root__"], "msg": "value is not a ' |                 text='[{"loc_in": "body", "loc": ["root"], "msg": "value is not a ' | ||||||
|                 'valid dict", "type": "type_error.dict"}]', |                 'valid dict", "type": "type_error.dict"}]', | ||||||
|                 content_type="application/json", |                 content_type="application/json", | ||||||
|             ) from None |             ) from None | ||||||
| @@ -117,7 +120,7 @@ class QueryGetter(AbstractInjector): | |||||||
|  |  | ||||||
|     def inject(self, request: BaseRequest, args_view: list, kwargs_view: dict): |     def inject(self, request: BaseRequest, args_view: list, kwargs_view: dict): | ||||||
|         data = self._query_to_dict(request.query) |         data = self._query_to_dict(request.query) | ||||||
|         cleaned = self.model(**data).dict() |         cleaned = self.model(**data).model_dump() | ||||||
|         for group_name, (group_cls, group_attrs) in self._groups.items(): |         for group_name, (group_cls, group_attrs) in self._groups.items(): | ||||||
|             group = group_cls() |             group = group_cls() | ||||||
|             for attr_name in group_attrs: |             for attr_name in group_attrs: | ||||||
| @@ -163,7 +166,7 @@ class HeadersGetter(AbstractInjector): | |||||||
|  |  | ||||||
|     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()} | ||||||
|         cleaned = self.model(**header).dict() |         cleaned = self.model(**header).model_dump() | ||||||
|         for group_name, (group_cls, group_attrs) in self._groups.items(): |         for group_name, (group_cls, group_attrs) in self._groups.items(): | ||||||
|             group = group_cls() |             group = group_cls() | ||||||
|             for attr_name in group_attrs: |             for attr_name in group_attrs: | ||||||
|   | |||||||
| @@ -14,12 +14,13 @@ def setup( | |||||||
|     url_prefix: str = "/oas", |     url_prefix: str = "/oas", | ||||||
|     enable: bool = True, |     enable: bool = True, | ||||||
|     version_spec: Optional[str] = None, |     version_spec: Optional[str] = None, | ||||||
|     title_spec: Optional[str] = None |     title_spec: Optional[str] = None, | ||||||
|  |     custom_template: Optional[jinja2.Template] = None | ||||||
| ): | ): | ||||||
|     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,) | ||||||
|         oas_app["index template"] = jinja2.Template( |         oas_app["index template"] = custom_template or jinja2.Template( | ||||||
|             resources.read_text("aiohttp_pydantic.oas", "index.j2") |             resources.read_text("aiohttp_pydantic.oas", "index.j2") | ||||||
|         ) |         ) | ||||||
|         oas_app["version_spec"] = version_spec |         oas_app["version_spec"] = version_spec | ||||||
|   | |||||||
| @@ -120,6 +120,19 @@ def tags(docstring: str) -> List[str]: | |||||||
|     return [] |     return [] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def operation_id(docstring: str) -> str | None: | ||||||
|  |     """ | ||||||
|  |     Extract the "OperationId:" block of the docstring. | ||||||
|  |     """ | ||||||
|  |     iterator = LinesIterator(docstring) | ||||||
|  |     for line in iterator: | ||||||
|  |         if re.fullmatch("operation_?id\\s*:.*", line, re.IGNORECASE): | ||||||
|  |             iterator.rewind() | ||||||
|  |             return line.split(":")[1].strip(' ') | ||||||
|  |  | ||||||
|  |     return None | ||||||
|  |  | ||||||
|  |  | ||||||
| def operation(docstring: str) -> str: | def operation(docstring: str) -> str: | ||||||
|     """ |     """ | ||||||
|     Extract all docstring except the "Status Code:" block. |     Extract all docstring except the "Status Code:" block. | ||||||
| @@ -127,7 +140,7 @@ def operation(docstring: str) -> str: | |||||||
|     lines = LinesIterator(docstring) |     lines = LinesIterator(docstring) | ||||||
|     ret = [] |     ret = [] | ||||||
|     for line in lines: |     for line in lines: | ||||||
|         if re.fullmatch("status\\s+codes?\\s*:|tags\\s*:.*", line, re.IGNORECASE): |         if re.fullmatch("status\\s+codes?\\s*:|tags\\s*:.*|operation_?id\\s*:.*", line, re.IGNORECASE): | ||||||
|             lines.rewind() |             lines.rewind() | ||||||
|             for _ in _i_extract_block(lines): |             for _ in _i_extract_block(lines): | ||||||
|                 pass |                 pass | ||||||
|   | |||||||
| @@ -1,45 +1,27 @@ | |||||||
| {# This updated file is part of swagger_ui_bundle (https://github.com/dtkav/swagger_ui_bundle) #} | {# This updated file is part of swagger_ui_bundle (https://github.com/bartsanchez/swagger_ui_bundle) #} | ||||||
| <!DOCTYPE html> | <!DOCTYPE html> | ||||||
| <html lang="en"> | <html lang="en"> | ||||||
|   <head> |   <head> | ||||||
|     <meta charset="UTF-8"> |     <meta charset="UTF-8"> | ||||||
|     <title>{{ title | default('Swagger UI') }}</title> |     <title>{{ title | default('Swagger UI') }}</title> | ||||||
|     <link rel="stylesheet" type="text/css" href="{{ static_url | trim('/') }}/swagger-ui.css" > |     <link rel="stylesheet" type="text/css" href="{{ static_url | trim('/') }}/swagger-ui.css" /> | ||||||
|  |     <link rel="stylesheet" type="text/css" href="{{ static_url | trim('/') }}/index.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-32x32.png" sizes="32x32" /> | ||||||
|     <link rel="icon" type="image/png" href="{{ static_url | trim('/') }}/favicon-16x16.png" sizes="16x16" /> |     <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> |   </head> | ||||||
|  |  | ||||||
|   <body> |   <body> | ||||||
|     <div id="swagger-ui"></div> |     <div id="swagger-ui"></div> | ||||||
|  |     <script src="{{ static_url | trim('/') }}/swagger-ui-bundle.js" charset="UTF-8"> </script> | ||||||
|     <script src="{{ static_url | trim('/') }}/swagger-ui-bundle.js"> </script> |     <script src="{{ static_url | trim('/') }}/swagger-ui-standalone-preset.js" charset="UTF-8"> </script> | ||||||
|     <script src="{{ static_url | trim('/') }}/swagger-ui-standalone-preset.js"> </script> |  | ||||||
|     <script> |     <script> | ||||||
|     window.onload = function() { |     window.onload = function() { | ||||||
|       // Begin Swagger UI call region |       // Begin Swagger UI call region | ||||||
|       const ui = SwaggerUIBundle({ |       const ui = SwaggerUIBundle({ | ||||||
|         url: "{{ openapi_spec_url }}", |         url: "{{ openapi_spec_url }}", | ||||||
|  |         {% if urls is defined %} | ||||||
|  |         urls: {{ urls|tojson|safe }}, | ||||||
|  |         {% endif %} | ||||||
|         validatorUrl: {{ validatorUrl | default('null') }}, |         validatorUrl: {{ validatorUrl | default('null') }}, | ||||||
|         {% if configUrl is defined %} |         {% if configUrl is defined %} | ||||||
|         configUrl: "{{ configUrl }}", |         configUrl: "{{ configUrl }}", | ||||||
| @@ -54,16 +36,15 @@ | |||||||
|           SwaggerUIBundle.plugins.DownloadUrl |           SwaggerUIBundle.plugins.DownloadUrl | ||||||
|         ], |         ], | ||||||
|         layout: "StandaloneLayout" |         layout: "StandaloneLayout" | ||||||
|       }) |       }); | ||||||
|       {% if initOAuth is defined %} |       {% if initOAuth is defined %} | ||||||
|       ui.initOAuth( |       ui.initOAuth( | ||||||
|         {{ initOAuth|tojson|safe }} |         {{ initOAuth|tojson|safe }} | ||||||
|       ) |       ) | ||||||
|       {% endif %} |       {% endif %} | ||||||
|       // End Swagger UI call region |       // End Swagger UI call region | ||||||
|  |       window.ui = ui; | ||||||
|       window.ui = ui |     }; | ||||||
|     } |  | ||||||
|   </script> |   </script> | ||||||
|   </body> |   </body> | ||||||
| </html> | </html> | ||||||
| @@ -207,6 +207,17 @@ class OperationObject: | |||||||
|         else: |         else: | ||||||
|             self._spec.pop("tags", None) |             self._spec.pop("tags", None) | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def operation_id(self) -> str | None: | ||||||
|  |         return self._spec.get("operationId", None) | ||||||
|  |  | ||||||
|  |     @operation_id.setter | ||||||
|  |     def operation_id(self, operation_id: str | None) -> None: | ||||||
|  |         if operation_id: | ||||||
|  |             self._spec["operationId"] = operation_id | ||||||
|  |         else: | ||||||
|  |             self._spec.pop("operationId", None) | ||||||
|  |  | ||||||
|  |  | ||||||
| class PathItem: | class PathItem: | ||||||
|     def __init__(self, spec: dict): |     def __init__(self, spec: dict): | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ from typing import List, Type, Optional, get_type_hints | |||||||
|  |  | ||||||
| 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 pydantic import BaseModel, RootModel | ||||||
|  |  | ||||||
| from aiohttp_pydantic.oas.struct import OpenApiSpec3, OperationObject, PathItem | from aiohttp_pydantic.oas.struct import OpenApiSpec3, OperationObject, PathItem | ||||||
| from . import docstring_parser | from . import docstring_parser | ||||||
| @@ -32,9 +32,10 @@ class _OASResponseBuilder: | |||||||
|             response_schema = obj.schema( |             response_schema = obj.schema( | ||||||
|                 ref_template="#/components/schemas/{model}" |                 ref_template="#/components/schemas/{model}" | ||||||
|             ).copy() |             ).copy() | ||||||
|             if def_sub_schemas := response_schema.pop("definitions", None): |             if def_sub_schemas := response_schema.pop("$defs", None): | ||||||
|                 self._oas.components.schemas.update(def_sub_schemas) |                 self._oas.components.schemas.update(def_sub_schemas) | ||||||
|             return response_schema |             self._oas.components.schemas.update({response_schema['title']: response_schema}) | ||||||
|  |             return {'$ref': f'#/components/schemas/{response_schema["title"]}'} | ||||||
|         return {} |         return {} | ||||||
|  |  | ||||||
|     def _handle_list(self, obj): |     def _handle_list(self, obj): | ||||||
| @@ -87,6 +88,7 @@ def _add_http_method_to_oas( | |||||||
|     if description: |     if description: | ||||||
|         oas_operation.description = docstring_parser.operation(description) |         oas_operation.description = docstring_parser.operation(description) | ||||||
|         oas_operation.tags = docstring_parser.tags(description) |         oas_operation.tags = docstring_parser.tags(description) | ||||||
|  |         oas_operation.operation_id = docstring_parser.operation_id(description) | ||||||
|         status_code_descriptions = docstring_parser.status_code(description) |         status_code_descriptions = docstring_parser.status_code(description) | ||||||
|     else: |     else: | ||||||
|         status_code_descriptions = {} |         status_code_descriptions = {} | ||||||
| @@ -97,7 +99,7 @@ def _add_http_method_to_oas( | |||||||
|             .schema(ref_template="#/components/schemas/{model}") |             .schema(ref_template="#/components/schemas/{model}") | ||||||
|             .copy() |             .copy() | ||||||
|         ) |         ) | ||||||
|         if def_sub_schemas := body_schema.pop("definitions", None): |         if def_sub_schemas := body_schema.pop("$defs", None): | ||||||
|             oas.components.schemas.update(def_sub_schemas) |             oas.components.schemas.update(def_sub_schemas) | ||||||
|  |  | ||||||
|         oas_operation.request_body.content = { |         oas_operation.request_body.content = { | ||||||
| @@ -115,17 +117,24 @@ 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 | ||||||
|  |  | ||||||
|             attrs = {"__annotations__": {"__root__": type_}} |             attrs = {"__annotations__": {"root": type_}} | ||||||
|             if name in defaults: |             if name in defaults: | ||||||
|                 attrs["__root__"] = defaults[name] |                 attrs["root"] = defaults[name] | ||||||
|                 oas_operation.parameters[i].required = False |                 oas_operation.parameters[i].required = False | ||||||
|             else: |             else: | ||||||
|                 oas_operation.parameters[i].required = True |                 oas_operation.parameters[i].required = True | ||||||
|  |  | ||||||
|             oas_operation.parameters[i].schema = type(name, (BaseModel,), attrs).schema( |             oas_operation.parameters[i].schema = type(name, (RootModel,), attrs).schema( | ||||||
|                 ref_template="#/components/schemas/{model}" |                 ref_template="#/components/schemas/{model}" | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|  |             if 'description' in oas_operation.parameters[i].schema: | ||||||
|  |                 oas_operation.parameters[i].description = oas_operation.parameters[i].schema['description'] | ||||||
|  |  | ||||||
|  |             # move definitions | ||||||
|  |             if def_sub_schemas := oas_operation.parameters[i].schema.pop("$defs", None): | ||||||
|  |                 oas.components.schemas.update(def_sub_schemas) | ||||||
|  |  | ||||||
|     return_type = get_type_hints(handler).get("return") |     return_type = get_type_hints(handler).get("return") | ||||||
|     if return_type is not None: |     if return_type is not None: | ||||||
|         _OASResponseBuilder(oas, oas_operation, status_code_descriptions).build( |         _OASResponseBuilder(oas, oas_operation, status_code_descriptions).build( | ||||||
|   | |||||||
| @@ -10,6 +10,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 pydantic_core import ErrorDetails | ||||||
|  |  | ||||||
| from .injectors import ( | from .injectors import ( | ||||||
|     AbstractInjector, |     AbstractInjector, | ||||||
|     BodyGetter, |     BodyGetter, | ||||||
| @@ -22,6 +24,10 @@ from .injectors import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class PydanticValidationError(ErrorDetails): | ||||||
|  |     loc_in: CONTEXT | ||||||
|  |  | ||||||
|  |  | ||||||
| class PydanticView(AbstractView): | class PydanticView(AbstractView): | ||||||
|     """ |     """ | ||||||
|     An AIOHTTP View that validate request using function annotations. |     An AIOHTTP View that validate request using function annotations. | ||||||
| @@ -91,7 +97,7 @@ class PydanticView(AbstractView): | |||||||
|         return injectors |         return injectors | ||||||
|  |  | ||||||
|     async def on_validation_error( |     async def on_validation_error( | ||||||
|         self, exception: ValidationError, context: CONTEXT |             self, exception: ValidationError, context: CONTEXT | ||||||
|     ) -> StreamResponse: |     ) -> StreamResponse: | ||||||
|         """ |         """ | ||||||
|         This method is a hook to intercept ValidationError. |         This method is a hook to intercept ValidationError. | ||||||
| @@ -101,14 +107,13 @@ class PydanticView(AbstractView): | |||||||
|         "headers", "path" or "query string" |         "headers", "path" or "query string" | ||||||
|         """ |         """ | ||||||
|         errors = exception.errors() |         errors = exception.errors() | ||||||
|         for error in errors: |         own_errors = [PydanticValidationError(**x, loc_in=context) for x in errors] | ||||||
|             error["in"] = context |  | ||||||
|  |  | ||||||
|         return json_response(data=errors, status=400) |         return json_response(data=own_errors, status=400) | ||||||
|  |  | ||||||
|  |  | ||||||
| def inject_params( | def inject_params( | ||||||
|     handler, parse_func_signature: Callable[[Callable], Iterable[AbstractInjector]] |         handler, parse_func_signature: Callable[[Callable], Iterable[AbstractInjector]] | ||||||
| ): | ): | ||||||
|     """ |     """ | ||||||
|     Decorator to unpack the query string, route path, body and http header in |     Decorator to unpack the query string, route path, body and http header in | ||||||
| @@ -146,6 +151,7 @@ def is_pydantic_view(obj) -> bool: | |||||||
|  |  | ||||||
|  |  | ||||||
| __all__ = ( | __all__ = ( | ||||||
|  |     "PydanticValidationError", | ||||||
|     "AbstractInjector", |     "AbstractInjector", | ||||||
|     "BodyGetter", |     "BodyGetter", | ||||||
|     "HeadersGetter", |     "HeadersGetter", | ||||||
|   | |||||||
| @@ -1,42 +1,11 @@ | |||||||
| aiohttp==3.8.1 | aiohttp==3.8.6 | ||||||
| aiosignal==1.2.0 | pydantic==2.5.1 | ||||||
| async-timeout==4.0.2 | jinja2==3.1.2 | ||||||
| atomicwrites==1.4.1 | swagger-4-ui-bundle==0.0.4 | ||||||
| attrs==21.4.0 | pytest==7.4.3 | ||||||
| bleach==5.0.1 | pytest-aiohttp==1.0.5 | ||||||
| certifi==2022.6.15 | pytest-asyncio==0.21.1 | ||||||
| charset-normalizer==2.1.0 | pytest-cov==4.1.0 | ||||||
| codecov==2.1.12 | readme-renderer==42.0 | ||||||
| colorama==0.4.5 | codecov==2.1.13 | ||||||
| commonmark==0.9.1 | twine==4.0.2 | ||||||
| coverage==6.4.2 |  | ||||||
| docutils==0.19 |  | ||||||
| frozenlist==1.3.0 |  | ||||||
| idna==3.3 |  | ||||||
| importlib-metadata==4.12.0 |  | ||||||
| iniconfig==1.1.1 |  | ||||||
| keyring==23.7.0 |  | ||||||
| multidict==6.0.2 |  | ||||||
| packaging==21.3 |  | ||||||
| pkginfo==1.8.3 |  | ||||||
| pluggy==1.0.0 |  | ||||||
| py==1.11.0 |  | ||||||
| Pygments==2.12.0 |  | ||||||
| pyparsing==3.0.9 |  | ||||||
| pytest==7.1.2 |  | ||||||
| pytest-aiohttp==1.0.4 |  | ||||||
| pytest-asyncio==0.19.0 |  | ||||||
| pytest-cov==3.0.0 |  | ||||||
| pywin32-ctypes==0.2.0 |  | ||||||
| readme-renderer==35.0 |  | ||||||
| requests==2.28.1 |  | ||||||
| requests-toolbelt==0.9.1 |  | ||||||
| rfc3986==2.0.0 |  | ||||||
| rich==12.5.1 |  | ||||||
| six==1.16.0 |  | ||||||
| tomli==2.0.1 |  | ||||||
| twine==4.0.1 |  | ||||||
| urllib3==1.26.11 |  | ||||||
| webencodings==0.5.1 |  | ||||||
| yarl==1.7.2 |  | ||||||
| zipp==3.8.1 |  | ||||||
| @@ -1,28 +1,9 @@ | |||||||
| aiohttp==3.8.1 | aiohttp==3.8.6 | ||||||
| aiosignal==1.2.0 | pydantic==2.5.1 | ||||||
| async-timeout==4.0.2 | jinja2==3.1.2 | ||||||
| atomicwrites==1.4.1 | swagger-4-ui-bundle==0.0.4 | ||||||
| attrs==21.4.0 | pytest==7.4.3 | ||||||
| bleach==5.0.1 | pytest-aiohttp==1.0.5 | ||||||
| charset-normalizer==2.1.0 | pytest-asyncio==0.21.1 | ||||||
| colorama==0.4.5 | pytest-cov==4.1.0 | ||||||
| coverage==6.4.2 | readme-renderer==42.0 | ||||||
| docutils==0.19 |  | ||||||
| frozenlist==1.3.0 |  | ||||||
| idna==3.3 |  | ||||||
| iniconfig==1.1.1 |  | ||||||
| multidict==6.0.2 |  | ||||||
| packaging==21.3 |  | ||||||
| pluggy==1.0.0 |  | ||||||
| py==1.11.0 |  | ||||||
| Pygments==2.12.0 |  | ||||||
| pyparsing==3.0.9 |  | ||||||
| pytest==7.1.2 |  | ||||||
| pytest-aiohttp==1.0.4 |  | ||||||
| pytest-asyncio==0.19.0 |  | ||||||
| pytest-cov==3.0.0 |  | ||||||
| readme-renderer==35.0 |  | ||||||
| six==1.16.0 |  | ||||||
| tomli==2.0.1 |  | ||||||
| webencodings==0.5.1 |  | ||||||
| yarl==1.7.2 |  | ||||||
							
								
								
									
										22
									
								
								setup.cfg
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								setup.cfg
									
									
									
									
									
								
							| @@ -18,8 +18,8 @@ classifiers = | |||||||
|     Programming Language :: Python |     Programming Language :: Python | ||||||
|     Programming Language :: Python :: 3 |     Programming Language :: Python :: 3 | ||||||
|     Programming Language :: Python :: 3 :: Only |     Programming Language :: Python :: 3 :: Only | ||||||
|     Programming Language :: Python :: 3.8 |     Programming Language :: Python :: 3.10 | ||||||
|     Programming Language :: Python :: 3.9 |     Programming Language :: Python :: 3.11 | ||||||
|     Topic :: Software Development :: Libraries :: Application Frameworks |     Topic :: Software Development :: Libraries :: Application Frameworks | ||||||
|     Framework :: aiohttp |     Framework :: aiohttp | ||||||
|     License :: OSI Approved :: MIT License |     License :: OSI Approved :: MIT License | ||||||
| @@ -28,22 +28,22 @@ classifiers = | |||||||
| zip_safe = False | zip_safe = False | ||||||
| include_package_data = True | include_package_data = True | ||||||
| packages = find: | packages = find: | ||||||
| python_requires = >=3.8 | python_requires = >=3.10 | ||||||
| install_requires = | install_requires = | ||||||
|     aiohttp |     aiohttp | ||||||
|     pydantic>=1.7 |     pydantic>=2.5.0 | ||||||
|     swagger-ui-bundle |     swagger-4-ui-bundle | ||||||
|  |  | ||||||
| [options.extras_require] | [options.extras_require] | ||||||
| test = | test = | ||||||
|     pytest==7.1.2 |     pytest==7.4.0 | ||||||
|     pytest-aiohttp==1.0.4 |     pytest-aiohttp==1.0.5 | ||||||
|     pytest-cov==3.0.0 |     pytest-cov==4.1.0 | ||||||
|     readme-renderer==35.0 |     readme-renderer==42.0 | ||||||
| ci = | ci = | ||||||
|     %(test)s |     %(test)s | ||||||
|     codecov==2.1.12 |     codecov==2.1.13 | ||||||
|     twine==4.0.1 |     twine==4.0.2 | ||||||
|  |  | ||||||
| [options.packages.find] | [options.packages.find] | ||||||
| exclude = | exclude = | ||||||
|   | |||||||
| @@ -4,9 +4,10 @@ from typing import Iterator, List, Optional | |||||||
|  |  | ||||||
| from aiohttp import web | from aiohttp import web | ||||||
| from aiohttp.web_response import json_response | from aiohttp.web_response import json_response | ||||||
| from pydantic import BaseModel | from pydantic import BaseModel, RootModel | ||||||
|  |  | ||||||
| from aiohttp_pydantic import PydanticView | from aiohttp_pydantic import PydanticView | ||||||
|  | from aiohttp_pydantic.view import PydanticValidationError | ||||||
|  |  | ||||||
|  |  | ||||||
| class ArticleModel(BaseModel): | class ArticleModel(BaseModel): | ||||||
| @@ -14,11 +15,11 @@ class ArticleModel(BaseModel): | |||||||
|     nb_page: Optional[int] |     nb_page: Optional[int] | ||||||
|  |  | ||||||
|  |  | ||||||
| class ArticleModels(BaseModel): | class ArticleModels(RootModel): | ||||||
|     __root__: List[ArticleModel] |     root: List[ArticleModel] | ||||||
|  |  | ||||||
|     def __iter__(self) -> Iterator[ArticleModel]: |     def __iter__(self) -> Iterator[ArticleModel]: | ||||||
|         return iter(self.__root__) |         return iter(self.root) | ||||||
|  |  | ||||||
|  |  | ||||||
| class ArticleView(PydanticView): | class ArticleView(PydanticView): | ||||||
| @@ -30,14 +31,12 @@ class ArticleView(PydanticView): | |||||||
|  |  | ||||||
|     async def on_validation_error(self, exception, context): |     async def on_validation_error(self, exception, context): | ||||||
|         errors = exception.errors() |         errors = exception.errors() | ||||||
|         for error in errors: |         own_errors = [PydanticValidationError(**x, loc_in=context) for x in errors] | ||||||
|             error["in"] = context |         return json_response(data=own_errors, status=400) | ||||||
|             error["custom"] = "custom" |  | ||||||
|         return json_response(data=errors, status=400) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| async def test_post_an_article_with_wrong_type_field_should_return_an_error_message( | async def test_post_an_article_with_wrong_type_field_should_return_an_error_message( | ||||||
|     aiohttp_client, event_loop |         aiohttp_client, event_loop | ||||||
| ): | ): | ||||||
|     app = web.Application() |     app = web.Application() | ||||||
|     app.router.add_view("/article", ArticleView) |     app.router.add_view("/article", ArticleView) | ||||||
| @@ -47,12 +46,14 @@ async def test_post_an_article_with_wrong_type_field_should_return_an_error_mess | |||||||
|  |  | ||||||
|     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() == [ | ||||||
|         { |         { | ||||||
|             "in": "body", |             'loc_in': 'body', | ||||||
|             "loc": ["nb_page"], |             'input': 'foo', | ||||||
|             "msg": "value is not a valid integer", |             'loc': ['nb_page'], | ||||||
|             "custom": "custom", |             'msg': 'Input should be a valid integer, unable to parse string as an integer', | ||||||
|             "type": "type_error.integer", |             'type': 'int_parsing', | ||||||
|  |             'url': 'https://errors.pydantic.dev/2.5/v/int_parsing' | ||||||
|         } |         } | ||||||
|     ] |     ] | ||||||
|   | |||||||
| @@ -1,25 +1,34 @@ | |||||||
| from __future__ import annotations | from __future__ import annotations | ||||||
|  |  | ||||||
| from enum import Enum | from enum import Enum | ||||||
| from typing import List, Optional, Union, Literal | from typing import List, Optional, Union, Literal, Annotated | ||||||
| from uuid import UUID | from uuid import UUID | ||||||
|  |  | ||||||
| import pytest | import pytest | ||||||
| from aiohttp import web | from aiohttp import web | ||||||
|  | from pydantic import Field, RootModel | ||||||
| from pydantic.main import BaseModel | from pydantic.main import BaseModel | ||||||
|  |  | ||||||
| from aiohttp_pydantic import PydanticView, oas | from aiohttp_pydantic import PydanticView, oas | ||||||
| from aiohttp_pydantic.injectors import Group | from aiohttp_pydantic.injectors import Group | ||||||
| from aiohttp_pydantic.oas.typing import r200, r201, r204, r404 | from aiohttp_pydantic.oas.typing import r200, r201, r204, r404, r400 | ||||||
| from aiohttp_pydantic.oas.view import generate_oas | from aiohttp_pydantic.oas.view import generate_oas | ||||||
|  |  | ||||||
|  |  | ||||||
| class Color(str, Enum): | class Color(str, Enum): | ||||||
|  |     """ | ||||||
|  |     Pet color | ||||||
|  |     """ | ||||||
|     RED = "red" |     RED = "red" | ||||||
|     GREEN = "green" |     GREEN = "green" | ||||||
|     PINK = "pink" |     PINK = "pink" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Lang(str, Enum): | ||||||
|  |     EN = 'en' | ||||||
|  |     FR = 'fr' | ||||||
|  |  | ||||||
|  |  | ||||||
| class Toy(BaseModel): | class Toy(BaseModel): | ||||||
|     name: str |     name: str | ||||||
|     color: Color |     color: Color | ||||||
| @@ -27,13 +36,32 @@ class Toy(BaseModel): | |||||||
|  |  | ||||||
| class Pet(BaseModel): | class Pet(BaseModel): | ||||||
|     id: int |     id: int | ||||||
|     name: str |     name: Optional[str] = Field(None) | ||||||
|     toys: List[Toy] |     toys: List[Toy] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Error(BaseModel): | ||||||
|  |     code: int | ||||||
|  |     text: str | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Cat(BaseModel): | ||||||
|  |     pet_type: Literal['cat'] | ||||||
|  |     meows: int | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Dog(BaseModel): | ||||||
|  |     pet_type: Literal['dog'] | ||||||
|  |     barks: float | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Animal(RootModel): | ||||||
|  |     root: Annotated[Union[Cat, Dog], Field(discriminator='pet_type')] | ||||||
|  |  | ||||||
|  |  | ||||||
| class PetCollectionView(PydanticView): | class PetCollectionView(PydanticView): | ||||||
|     async def get( |     async def get( | ||||||
|         self, format: str, name: Optional[str] = None, *, promo: Optional[UUID] = None |             self, format: str, lang: Lang = Lang.EN, name: Optional[str] = None, *, promo: Optional[UUID] = None | ||||||
|     ) -> r200[List[Pet]]: |     ) -> r200[List[Pet]]: | ||||||
|         """ |         """ | ||||||
|         Get a list of pets |         Get a list of pets | ||||||
| @@ -41,6 +69,7 @@ class PetCollectionView(PydanticView): | |||||||
|         Tags: pet |         Tags: pet | ||||||
|         Status Codes: |         Status Codes: | ||||||
|           200: Successful operation |           200: Successful operation | ||||||
|  |         OperationId: createPet | ||||||
|         """ |         """ | ||||||
|         return web.json_response() |         return web.json_response() | ||||||
|  |  | ||||||
| @@ -51,12 +80,12 @@ class PetCollectionView(PydanticView): | |||||||
|  |  | ||||||
| class PetItemView(PydanticView): | class PetItemView(PydanticView): | ||||||
|     async def get( |     async def get( | ||||||
|         self, |             self, | ||||||
|         id: int, |             id: int, | ||||||
|         /, |             /, | ||||||
|         size: Union[int, Literal["x", "l", "s"]], |             size: Union[int, Literal["x", "l", "s"]], | ||||||
|         day: Union[int, Literal["now"]] = "now", |             day: Union[int, Literal["now"]] = "now", | ||||||
|     ) -> Union[r200[Pet], r404]: |     ) -> Union[r200[Pet], r404[Error], r400[Error]]: | ||||||
|         return web.json_response() |         return web.json_response() | ||||||
|  |  | ||||||
|     async def put(self, id: int, /, pet: Pet): |     async def put(self, id: int, /, pet: Pet): | ||||||
| @@ -79,6 +108,11 @@ class ViewResponseReturnASimpleType(PydanticView): | |||||||
|         return web.json_response() |         return web.json_response() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class DiscriminatedView(PydanticView): | ||||||
|  |     async def post(self, /, request: Animal) -> r200[int]: | ||||||
|  |         return web.json_response() | ||||||
|  |  | ||||||
|  |  | ||||||
| async def ensure_content_durability(client): | async def ensure_content_durability(client): | ||||||
|     """ |     """ | ||||||
|     Reload the page 2 times to ensure that content is always the same |     Reload the page 2 times to ensure that content is always the same | ||||||
| @@ -103,6 +137,7 @@ async def generated_oas(aiohttp_client, event_loop) -> web.Application: | |||||||
|     app.router.add_view("/pets", PetCollectionView) |     app.router.add_view("/pets", PetCollectionView) | ||||||
|     app.router.add_view("/pets/{id}", PetItemView) |     app.router.add_view("/pets/{id}", PetItemView) | ||||||
|     app.router.add_view("/simple-type", ViewResponseReturnASimpleType) |     app.router.add_view("/simple-type", ViewResponseReturnASimpleType) | ||||||
|  |     app.router.add_view("/animals", DiscriminatedView) | ||||||
|     oas.setup(app) |     oas.setup(app) | ||||||
|  |  | ||||||
|     return await ensure_content_durability(await aiohttp_client(app)) |     return await ensure_content_durability(await aiohttp_client(app)) | ||||||
| @@ -110,12 +145,35 @@ async def generated_oas(aiohttp_client, event_loop) -> web.Application: | |||||||
|  |  | ||||||
| async def test_generated_oas_should_have_components_schemas(generated_oas): | async def test_generated_oas_should_have_components_schemas(generated_oas): | ||||||
|     assert generated_oas["components"]["schemas"] == { |     assert generated_oas["components"]["schemas"] == { | ||||||
|  |         'Cat': {'properties': {'meows': {'title': 'Meows', 'type': 'integer'}, | ||||||
|  |                                'pet_type': {'const': 'cat', 'title': 'Pet Type'}}, | ||||||
|  |                 'required': ['pet_type', 'meows'], | ||||||
|  |                 'title': 'Cat', | ||||||
|  |                 'type': 'object'}, | ||||||
|         "Color": { |         "Color": { | ||||||
|             "description": "An enumeration.", |             "description": "Pet color", | ||||||
|             "enum": ["red", "green", "pink"], |             "enum": ["red", "green", "pink"], | ||||||
|             "title": "Color", |             "title": "Color", | ||||||
|             "type": "string", |             "type": "string", | ||||||
|         }, |         }, | ||||||
|  |         'Dog': {'properties': {'barks': {'title': 'Barks', 'type': 'number'}, | ||||||
|  |                                'pet_type': {'const': 'dog', 'title': 'Pet Type'}}, | ||||||
|  |                 'required': ['pet_type', 'barks'], | ||||||
|  |                 'title': 'Dog', | ||||||
|  |                 'type': 'object'}, | ||||||
|  |         'Error': { | ||||||
|  |             'properties': { | ||||||
|  |                 'code': {'title': 'Code', 'type': 'integer'}, | ||||||
|  |                 'text': {'title': 'Text', 'type': 'string'}}, | ||||||
|  |             'required': ['code', 'text'], | ||||||
|  |             'title': 'Error', | ||||||
|  |             'type': 'object' | ||||||
|  |         }, | ||||||
|  |         'Lang': { | ||||||
|  |             'enum': ['en', 'fr'], | ||||||
|  |             'title': 'Lang', | ||||||
|  |             'type': 'string' | ||||||
|  |         }, | ||||||
|         "Toy": { |         "Toy": { | ||||||
|             "properties": { |             "properties": { | ||||||
|                 "color": {"$ref": "#/components/schemas/Color"}, |                 "color": {"$ref": "#/components/schemas/Color"}, | ||||||
| @@ -125,6 +183,26 @@ async def test_generated_oas_should_have_components_schemas(generated_oas): | |||||||
|             "title": "Toy", |             "title": "Toy", | ||||||
|             "type": "object", |             "type": "object", | ||||||
|         }, |         }, | ||||||
|  |         'Pet': { | ||||||
|  |             'properties': { | ||||||
|  |                 'id': {'title': 'Id', 'type': 'integer'}, | ||||||
|  |                 'name': { | ||||||
|  |                     'anyOf': [ | ||||||
|  |                         {'type': 'string'}, | ||||||
|  |                         {'type': 'null'} | ||||||
|  |                     ], | ||||||
|  |                     'default': None, | ||||||
|  |                     'title': 'Name'}, | ||||||
|  |                 'toys': { | ||||||
|  |                     'items': {'$ref': '#/components/schemas/Toy'}, | ||||||
|  |                     'title': 'Toys', | ||||||
|  |                     'type': 'array' | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             'required': ['id', 'toys'], | ||||||
|  |             'title': 'Pet', | ||||||
|  |             'type': 'object' | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -135,6 +213,7 @@ async def test_generated_oas_should_have_pets_paths(generated_oas): | |||||||
| async def test_pets_route_should_have_get_method(generated_oas): | async def test_pets_route_should_have_get_method(generated_oas): | ||||||
|     assert generated_oas["paths"]["/pets"]["get"] == { |     assert generated_oas["paths"]["/pets"]["get"] == { | ||||||
|         "description": "Get a list of pets", |         "description": "Get a list of pets", | ||||||
|  |         "operationId": "createPet", | ||||||
|         "tags": ["pet"], |         "tags": ["pet"], | ||||||
|         "parameters": [ |         "parameters": [ | ||||||
|             { |             { | ||||||
| @@ -143,17 +222,38 @@ async def test_pets_route_should_have_get_method(generated_oas): | |||||||
|                 "required": True, |                 "required": True, | ||||||
|                 "schema": {"title": "format", "type": "string"}, |                 "schema": {"title": "format", "type": "string"}, | ||||||
|             }, |             }, | ||||||
|  |             { | ||||||
|  |                 'in': 'query', | ||||||
|  |                 'name': 'lang', | ||||||
|  |                 'required': False, | ||||||
|  |                 'schema': { | ||||||
|  |                     'allOf': [{'$ref': '#/components/schemas/Lang'}], | ||||||
|  |                     'default': 'en', | ||||||
|  |                     'title': 'lang' | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|             { |             { | ||||||
|                 "in": "query", |                 "in": "query", | ||||||
|                 "name": "name", |                 "name": "name", | ||||||
|                 "required": False, |                 "required": False, | ||||||
|                 "schema": {"title": "name", "type": "string"}, |                 "schema": { | ||||||
|  |                     'anyOf': [{'type': 'string'}, {'type': 'null'}], | ||||||
|  |                     'default': None, | ||||||
|  |                     'title': 'name' | ||||||
|  |                 }, | ||||||
|             }, |             }, | ||||||
|             { |             { | ||||||
|                 "in": "header", |                 "in": "header", | ||||||
|                 "name": "promo", |                 "name": "promo", | ||||||
|                 "required": False, |                 "required": False, | ||||||
|                 "schema": {"format": "uuid", "title": "promo", "type": "string"}, |                 "schema": { | ||||||
|  |                     'anyOf': [ | ||||||
|  |                         {'format': 'uuid', 'type': 'string'}, | ||||||
|  |                         {'type': 'null'} | ||||||
|  |                     ], | ||||||
|  |                     'default': None, | ||||||
|  |                     'title': 'promo' | ||||||
|  |                 }, | ||||||
|             }, |             }, | ||||||
|         ], |         ], | ||||||
|         "responses": { |         "responses": { | ||||||
| @@ -162,20 +262,7 @@ async def test_pets_route_should_have_get_method(generated_oas): | |||||||
|                 "content": { |                 "content": { | ||||||
|                     "application/json": { |                     "application/json": { | ||||||
|                         "schema": { |                         "schema": { | ||||||
|                             "items": { |                             "items": {'$ref': '#/components/schemas/Pet'}, | ||||||
|                                 "properties": { |  | ||||||
|                                     "id": {"title": "Id", "type": "integer"}, |  | ||||||
|                                     "name": {"title": "Name", "type": "string"}, |  | ||||||
|                                     "toys": { |  | ||||||
|                                         "items": {"$ref": "#/components/schemas/Toy"}, |  | ||||||
|                                         "title": "Toys", |  | ||||||
|                                         "type": "array", |  | ||||||
|                                     }, |  | ||||||
|                                 }, |  | ||||||
|                                 "required": ["id", "name", "toys"], |  | ||||||
|                                 "title": "Pet", |  | ||||||
|                                 "type": "object", |  | ||||||
|                             }, |  | ||||||
|                             "type": "array", |                             "type": "array", | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
| @@ -194,14 +281,21 @@ async def test_pets_route_should_have_post_method(generated_oas): | |||||||
|                     "schema": { |                     "schema": { | ||||||
|                         "properties": { |                         "properties": { | ||||||
|                             "id": {"title": "Id", "type": "integer"}, |                             "id": {"title": "Id", "type": "integer"}, | ||||||
|                             "name": {"title": "Name", "type": "string"}, |                             "name": { | ||||||
|  |                                 'anyOf': [ | ||||||
|  |                                     {'type': 'string'}, | ||||||
|  |                                     {'type': 'null'} | ||||||
|  |                                 ], | ||||||
|  |                                 'default': None, | ||||||
|  |                                 'title': 'Name' | ||||||
|  |                             }, | ||||||
|                             "toys": { |                             "toys": { | ||||||
|                                 "items": {"$ref": "#/components/schemas/Toy"}, |                                 "items": {"$ref": "#/components/schemas/Toy"}, | ||||||
|                                 "title": "Toys", |                                 "title": "Toys", | ||||||
|                                 "type": "array", |                                 "type": "array", | ||||||
|                             }, |                             }, | ||||||
|                         }, |                         }, | ||||||
|                         "required": ["id", "name", "toys"], |                         "required": ["id", "toys"], | ||||||
|                         "title": "Pet", |                         "title": "Pet", | ||||||
|                         "type": "object", |                         "type": "object", | ||||||
|                     } |                     } | ||||||
| @@ -213,20 +307,7 @@ async def test_pets_route_should_have_post_method(generated_oas): | |||||||
|                 "description": "", |                 "description": "", | ||||||
|                 "content": { |                 "content": { | ||||||
|                     "application/json": { |                     "application/json": { | ||||||
|                         "schema": { |                         "schema": {'$ref': '#/components/schemas/Pet'} | ||||||
|                             "properties": { |  | ||||||
|                                 "id": {"title": "Id", "type": "integer"}, |  | ||||||
|                                 "name": {"title": "Name", "type": "string"}, |  | ||||||
|                                 "toys": { |  | ||||||
|                                     "items": {"$ref": "#/components/schemas/Toy"}, |  | ||||||
|                                     "title": "Toys", |  | ||||||
|                                     "type": "array", |  | ||||||
|                                 }, |  | ||||||
|                             }, |  | ||||||
|                             "required": ["id", "name", "toys"], |  | ||||||
|                             "title": "Pet", |  | ||||||
|                             "type": "object", |  | ||||||
|                         } |  | ||||||
|                     } |                     } | ||||||
|                 }, |                 }, | ||||||
|             } |             } | ||||||
| @@ -279,36 +360,20 @@ async def test_pets_id_route_should_have_get_method(generated_oas): | |||||||
|                 "name": "day", |                 "name": "day", | ||||||
|                 "required": False, |                 "required": False, | ||||||
|                 "schema": { |                 "schema": { | ||||||
|                     "anyOf": [{"type": "integer"}, {"enum": ["now"], "type": "string"}], |                     'anyOf': [{'type': 'integer'}, {'const': 'now'}], | ||||||
|                     "default": "now", |                     'default': 'now', | ||||||
|                     "title": "day", |                     'title': 'day' | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|         ], |         ], | ||||||
|         "responses": { |         'responses': { | ||||||
|             "200": { |             '200': {'content': {'application/json': {'schema': {'$ref': '#/components/schemas/Pet'}}}, | ||||||
|                 "description": "", |                     'description': ''}, | ||||||
|                 "content": { |             '400': {'content': {'application/json': {'schema': {'$ref': '#/components/schemas/Error'}}}, | ||||||
|                     "application/json": { |                     'description': ''}, | ||||||
|                         "schema": { |             '404': {'content': {'application/json': {'schema': {'$ref': '#/components/schemas/Error'}}}, | ||||||
|                             "properties": { |                     'description': ''} | ||||||
|                                 "id": {"title": "Id", "type": "integer"}, |         } | ||||||
|                                 "name": {"title": "Name", "type": "string"}, |  | ||||||
|                                 "toys": { |  | ||||||
|                                     "items": {"$ref": "#/components/schemas/Toy"}, |  | ||||||
|                                     "title": "Toys", |  | ||||||
|                                     "type": "array", |  | ||||||
|                                 }, |  | ||||||
|                             }, |  | ||||||
|                             "required": ["id", "name", "toys"], |  | ||||||
|                             "title": "Pet", |  | ||||||
|                             "type": "object", |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 }, |  | ||||||
|             }, |  | ||||||
|             "404": {"description": "", "content": {}}, |  | ||||||
|         }, |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -328,14 +393,20 @@ async def test_pets_id_route_should_have_put_method(generated_oas): | |||||||
|                     "schema": { |                     "schema": { | ||||||
|                         "properties": { |                         "properties": { | ||||||
|                             "id": {"title": "Id", "type": "integer"}, |                             "id": {"title": "Id", "type": "integer"}, | ||||||
|                             "name": {"title": "Name", "type": "string"}, |                             "name": { | ||||||
|  |                                 'anyOf': [ | ||||||
|  |                                     {'type': 'string'}, | ||||||
|  |                                     {'type': 'null'} | ||||||
|  |                                 ], | ||||||
|  |                                 'default': None, | ||||||
|  |                                 'title': 'Name'}, | ||||||
|                             "toys": { |                             "toys": { | ||||||
|                                 "items": {"$ref": "#/components/schemas/Toy"}, |                                 "items": {"$ref": "#/components/schemas/Toy"}, | ||||||
|                                 "title": "Toys", |                                 "title": "Toys", | ||||||
|                                 "type": "array", |                                 "type": "array", | ||||||
|                             }, |                             }, | ||||||
|                         }, |                         }, | ||||||
|                         "required": ["id", "name", "toys"], |                         "required": ["id", "toys"], | ||||||
|                         "title": "Pet", |                         "title": "Pet", | ||||||
|                         "type": "object", |                         "type": "object", | ||||||
|                     } |                     } | ||||||
|   | |||||||
| @@ -3,21 +3,21 @@ from __future__ import annotations | |||||||
| from typing import Iterator, List, Optional | from typing import Iterator, List, Optional | ||||||
|  |  | ||||||
| from aiohttp import web | from aiohttp import web | ||||||
| from pydantic import BaseModel | from pydantic import BaseModel, RootModel | ||||||
|  |  | ||||||
| from aiohttp_pydantic import PydanticView | from aiohttp_pydantic import PydanticView | ||||||
|  |  | ||||||
|  |  | ||||||
| class ArticleModel(BaseModel): | class ArticleModel(BaseModel): | ||||||
|     name: str |     name: str | ||||||
|     nb_page: Optional[int] |     nb_page: Optional[int] = None | ||||||
|  |  | ||||||
|  |  | ||||||
| class ArticleModels(BaseModel): | class ArticleModels(RootModel): | ||||||
|     __root__: List[ArticleModel] |     root: List[ArticleModel] | ||||||
|  |  | ||||||
|     def __iter__(self) -> Iterator[ArticleModel]: |     def __iter__(self) -> Iterator[ArticleModel]: | ||||||
|         return iter(self.__root__) |         return iter(self.root) | ||||||
|  |  | ||||||
|  |  | ||||||
| class ArticleView(PydanticView): | class ArticleView(PydanticView): | ||||||
| @@ -29,7 +29,7 @@ class ArticleView(PydanticView): | |||||||
|  |  | ||||||
|  |  | ||||||
| async def test_post_an_article_without_required_field_should_return_an_error_message( | async def test_post_an_article_without_required_field_should_return_an_error_message( | ||||||
|     aiohttp_client, event_loop |         aiohttp_client, event_loop | ||||||
| ): | ): | ||||||
|     app = web.Application() |     app = web.Application() | ||||||
|     app.router.add_view("/article", ArticleView) |     app.router.add_view("/article", ArticleView) | ||||||
| @@ -38,18 +38,21 @@ async def test_post_an_article_without_required_field_should_return_an_error_mes | |||||||
|     resp = await client.post("/article", json={}) |     resp = await client.post("/article", json={}) | ||||||
|     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() == [ | ||||||
|         { |         { | ||||||
|             "in": "body", |             'input': {}, | ||||||
|             "loc": ["name"], |             'loc': ['name'], | ||||||
|             "msg": "field required", |             'loc_in': 'body', | ||||||
|             "type": "value_error.missing", |             'msg': 'Field required', | ||||||
|  |             'type': 'missing', | ||||||
|  |             'url': 'https://errors.pydantic.dev/2.5/v/missing' | ||||||
|         } |         } | ||||||
|     ] |     ] | ||||||
|  |  | ||||||
|  |  | ||||||
| async def test_post_an_article_with_wrong_type_field_should_return_an_error_message( | async def test_post_an_article_with_wrong_type_field_should_return_an_error_message( | ||||||
|     aiohttp_client, event_loop |         aiohttp_client, event_loop | ||||||
| ): | ): | ||||||
|     app = web.Application() |     app = web.Application() | ||||||
|     app.router.add_view("/article", ArticleView) |     app.router.add_view("/article", ArticleView) | ||||||
| @@ -60,10 +63,12 @@ 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", |             'input': 'foo', | ||||||
|             "loc": ["nb_page"], |             'loc': ['nb_page'], | ||||||
|             "msg": "value is not a valid integer", |             'loc_in': 'body', | ||||||
|             "type": "type_error.integer", |             'msg': 'Input should be a valid integer, unable to parse string as an integer', | ||||||
|  |             'type': 'int_parsing', | ||||||
|  |             'url': 'https://errors.pydantic.dev/2.5/v/int_parsing' | ||||||
|         } |         } | ||||||
|     ] |     ] | ||||||
|  |  | ||||||
| @@ -81,7 +86,7 @@ async def test_post_an_array_json_is_supported(aiohttp_client, event_loop): | |||||||
|  |  | ||||||
|  |  | ||||||
| async def test_post_an_array_json_to_an_object_model_should_return_an_error( | async def test_post_an_array_json_to_an_object_model_should_return_an_error( | ||||||
|     aiohttp_client, event_loop |         aiohttp_client, event_loop | ||||||
| ): | ): | ||||||
|     app = web.Application() |     app = web.Application() | ||||||
|     app.router.add_view("/article", ArticleView) |     app.router.add_view("/article", ArticleView) | ||||||
| @@ -92,16 +97,16 @@ async def test_post_an_array_json_to_an_object_model_should_return_an_error( | |||||||
|     assert resp.content_type == "application/json" |     assert resp.content_type == "application/json" | ||||||
|     assert await resp.json() == [ |     assert await resp.json() == [ | ||||||
|         { |         { | ||||||
|             "in": "body", |             'loc': ['root'], | ||||||
|             "loc": ["__root__"], |             'loc_in': 'body', | ||||||
|             "msg": "value is not a valid dict", |             'msg': 'value is not a valid dict', | ||||||
|             "type": "type_error.dict", |             'type': 'type_error.dict' | ||||||
|         } |         } | ||||||
|     ] |     ] | ||||||
|  |  | ||||||
|  |  | ||||||
| async def test_post_an_object_json_to_a_list_model_should_return_an_error( | async def test_post_an_object_json_to_a_list_model_should_return_an_error( | ||||||
|     aiohttp_client, event_loop |         aiohttp_client, event_loop | ||||||
| ): | ): | ||||||
|     app = web.Application() |     app = web.Application() | ||||||
|     app.router.add_view("/article", ArticleView) |     app.router.add_view("/article", ArticleView) | ||||||
| @@ -110,12 +115,15 @@ async def test_post_an_object_json_to_a_list_model_should_return_an_error( | |||||||
|     resp = await client.put("/article", json={"name": "foo", "nb_page": 3}) |     resp = await client.put("/article", json={"name": "foo", "nb_page": 3}) | ||||||
|     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() == [ | ||||||
|         { |         { | ||||||
|             "in": "body", |             'input': {'name': 'foo', 'nb_page': 3}, | ||||||
|             "loc": ["__root__"], |             'loc': [], | ||||||
|             "msg": "value is not a valid list", |             'loc_in': 'body', | ||||||
|             "type": "type_error.list", |             'msg': 'Input should be a valid list', | ||||||
|  |             'type': 'list_type', | ||||||
|  |             'url': 'https://errors.pydantic.dev/2.5/v/list_type' | ||||||
|         } |         } | ||||||
|     ] |     ] | ||||||
|  |  | ||||||
|   | |||||||
| @@ -50,9 +50,9 @@ class Signature(Group): | |||||||
|  |  | ||||||
| class ArticleViewWithSignatureGroup(PydanticView): | class ArticleViewWithSignatureGroup(PydanticView): | ||||||
|     async def get( |     async def get( | ||||||
|         self, |             self, | ||||||
|         *, |             *, | ||||||
|         signature: Signature, |             signature: Signature, | ||||||
|     ): |     ): | ||||||
|         return web.json_response( |         return web.json_response( | ||||||
|             {"expired": signature.expired, "scope": signature.scope}, |             {"expired": signature.expired, "scope": signature.scope}, | ||||||
| @@ -61,7 +61,7 @@ class ArticleViewWithSignatureGroup(PydanticView): | |||||||
|  |  | ||||||
|  |  | ||||||
| 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, event_loop |         aiohttp_client, event_loop | ||||||
| ): | ): | ||||||
|     app = web.Application() |     app = web.Application() | ||||||
|     app.router.add_view("/article", ArticleView) |     app.router.add_view("/article", ArticleView) | ||||||
| @@ -70,18 +70,24 @@ async def test_get_article_without_required_header_should_return_an_error_messag | |||||||
|     resp = await client.get("/article", headers={}) |     resp = await client.get("/article", headers={}) | ||||||
|     assert resp.status == 400 |     assert resp.status == 400 | ||||||
|     assert resp.content_type == "application/json" |     assert resp.content_type == "application/json" | ||||||
|     assert await resp.json() == [ |  | ||||||
|  |     result = await resp.json() | ||||||
|  |     assert len(result) == 1 | ||||||
|  |     result[0].pop('input') | ||||||
|  |  | ||||||
|  |     assert result == [ | ||||||
|         { |         { | ||||||
|             "in": "headers", |             'type': 'missing', | ||||||
|             "loc": ["signature_expired"], |             'loc': ['signature_expired'], | ||||||
|             "msg": "field required", |             'msg': 'Field required', | ||||||
|             "type": "value_error.missing", |             'url': 'https://errors.pydantic.dev/2.5/v/missing', | ||||||
|  |             'loc_in': 'headers' | ||||||
|         } |         } | ||||||
|     ] |     ] | ||||||
|  |  | ||||||
|  |  | ||||||
| async def test_get_article_with_wrong_header_type_should_return_an_error_message( | async def test_get_article_with_wrong_header_type_should_return_an_error_message( | ||||||
|     aiohttp_client, event_loop |         aiohttp_client, event_loop | ||||||
| ): | ): | ||||||
|     app = web.Application() |     app = web.Application() | ||||||
|     app.router.add_view("/article", ArticleView) |     app.router.add_view("/article", ArticleView) | ||||||
| @@ -90,18 +96,22 @@ async def test_get_article_with_wrong_header_type_should_return_an_error_message | |||||||
|     resp = await client.get("/article", headers={"signature_expired": "foo"}) |     resp = await client.get("/article", headers={"signature_expired": "foo"}) | ||||||
|     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() == [ | ||||||
|         { |         { | ||||||
|             "in": "headers", |             'type': 'datetime_parsing', | ||||||
|             "loc": ["signature_expired"], |             'loc': ['signature_expired'], | ||||||
|             "msg": "invalid datetime format", |             'msg': 'Input should be a valid datetime, input is too short', | ||||||
|             "type": "value_error.datetime", |             'input': 'foo', | ||||||
|  |             'ctx': {'error': 'input is too short'}, | ||||||
|  |             'url': 'https://errors.pydantic.dev/2.5/v/datetime_parsing', | ||||||
|  |             'loc_in': 'headers' | ||||||
|         } |         } | ||||||
|     ] |     ] | ||||||
|  |  | ||||||
|  |  | ||||||
| async def test_get_article_with_valid_header_should_return_the_parsed_type( | async def test_get_article_with_valid_header_should_return_the_parsed_type( | ||||||
|     aiohttp_client, event_loop |         aiohttp_client, event_loop | ||||||
| ): | ): | ||||||
|     app = web.Application() |     app = web.Application() | ||||||
|     app.router.add_view("/article", ArticleView) |     app.router.add_view("/article", ArticleView) | ||||||
| @@ -116,7 +126,7 @@ async def test_get_article_with_valid_header_should_return_the_parsed_type( | |||||||
|  |  | ||||||
|  |  | ||||||
| async def test_get_article_with_valid_header_containing_hyphen_should_be_returned( | async def test_get_article_with_valid_header_containing_hyphen_should_be_returned( | ||||||
|     aiohttp_client, event_loop |         aiohttp_client, event_loop | ||||||
| ): | ): | ||||||
|     app = web.Application() |     app = web.Application() | ||||||
|     app.router.add_view("/article", ArticleView) |     app.router.add_view("/article", ArticleView) | ||||||
| @@ -136,18 +146,20 @@ async def test_wrong_value_to_header_defined_with_str_enum(aiohttp_client, event | |||||||
|  |  | ||||||
|     client = await aiohttp_client(app) |     client = await aiohttp_client(app) | ||||||
|     resp = await client.get("/coord", headers={"format": "WGS84"}) |     resp = await client.get("/coord", headers={"format": "WGS84"}) | ||||||
|  |  | ||||||
|     assert ( |     assert ( | ||||||
|         await resp.json() |             await resp.json() | ||||||
|         == [ |             == [ | ||||||
|             { |                 { | ||||||
|                 "ctx": {"enum_values": ["UMT", "MGRS"]}, |                     'ctx': {'expected': "'UMT' or 'MGRS'"}, | ||||||
|                 "in": "headers", |                     'input': 'WGS84', | ||||||
|                 "loc": ["format"], |                     'loc': ['format'], | ||||||
|                 "msg": "value is not a valid enumeration member; permitted: 'UMT', 'MGRS'", |                     'loc_in': 'headers', | ||||||
|                 "type": "type_error.enum", |                     'msg': "Input should be 'UMT' or 'MGRS'", | ||||||
|             } |                     'type': 'enum' | ||||||
|         ] |                 } | ||||||
|         != {"signature": "2020-10-04T18:01:00"} |             ] | ||||||
|  |             != {"signature": "2020-10-04T18:01:00"} | ||||||
|     ) |     ) | ||||||
|     assert resp.status == 400 |     assert resp.status == 400 | ||||||
|     assert resp.content_type == "application/json" |     assert resp.content_type == "application/json" | ||||||
|   | |||||||
| @@ -33,11 +33,14 @@ async def test_get_article_with_wrong_path_parameters_should_return_error( | |||||||
|     resp = await client.get("/article/1234/tag/music/before/now") |     resp = await client.get("/article/1234/tag/music/before/now") | ||||||
|     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() == [ | ||||||
|         { |         { | ||||||
|             "in": "path", |             'input': 'now', | ||||||
|             "loc": ["date"], |             'loc': ['date'], | ||||||
|             "msg": "value is not a valid integer", |             'loc_in': 'path', | ||||||
|             "type": "type_error.integer", |             'msg': 'Input should be a valid integer, unable to parse string as an integer', | ||||||
|  |             'type': 'int_parsing', | ||||||
|  |             'url': 'https://errors.pydantic.dev/2.5/v/int_parsing' | ||||||
|         } |         } | ||||||
|     ] |     ] | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| from __future__ import annotations | from __future__ import annotations | ||||||
|  |  | ||||||
|  | from enum import Enum | ||||||
| from typing import Optional, List | from typing import Optional, List | ||||||
| from pydantic import Field | from pydantic import Field | ||||||
| from aiohttp import web | from aiohttp import web | ||||||
| @@ -10,11 +11,11 @@ from aiohttp_pydantic.injectors import Group | |||||||
|  |  | ||||||
| class ArticleView(PydanticView): | class ArticleView(PydanticView): | ||||||
|     async def get( |     async def get( | ||||||
|         self, |             self, | ||||||
|         with_comments: bool, |             with_comments: bool, | ||||||
|         age: Optional[int] = None, |             age: Optional[int] = None, | ||||||
|         nb_items: int = 7, |             nb_items: int = 7, | ||||||
|         tags: List[str] = Field(default_factory=list), |             tags: List[str] = Field(default_factory=list), | ||||||
|     ): |     ): | ||||||
|         return web.json_response( |         return web.json_response( | ||||||
|             { |             { | ||||||
| @@ -41,9 +42,9 @@ class Pagination(Group): | |||||||
|  |  | ||||||
| class ArticleViewWithPaginationGroup(PydanticView): | class ArticleViewWithPaginationGroup(PydanticView): | ||||||
|     async def get( |     async def get( | ||||||
|         self, |             self, | ||||||
|         with_comments: bool, |             with_comments: bool, | ||||||
|         page: Pagination, |             page: Pagination, | ||||||
|     ): |     ): | ||||||
|         return web.json_response( |         return web.json_response( | ||||||
|             { |             { | ||||||
| @@ -54,8 +55,22 @@ class ArticleViewWithPaginationGroup(PydanticView): | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Lang(str, Enum): | ||||||
|  |     EN = 'en' | ||||||
|  |     FR = 'fr' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ArticleViewWithEnumInQuery(PydanticView): | ||||||
|  |     async def get(self, lang: Lang): | ||||||
|  |         return web.json_response( | ||||||
|  |             { | ||||||
|  |                 "lang": lang | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
| 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( | ||||||
|     aiohttp_client, event_loop |         aiohttp_client, event_loop | ||||||
| ): | ): | ||||||
|     app = web.Application() |     app = web.Application() | ||||||
|     app.router.add_view("/article", ArticleView) |     app.router.add_view("/article", ArticleView) | ||||||
| @@ -64,18 +79,21 @@ async def test_get_article_without_required_qs_should_return_an_error_message( | |||||||
|     resp = await client.get("/article") |     resp = await client.get("/article") | ||||||
|     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() == [ | ||||||
|         { |         { | ||||||
|             "in": "query string", |             'input': {}, | ||||||
|             "loc": ["with_comments"], |             'loc': ['with_comments'], | ||||||
|             "msg": "field required", |             'loc_in': 'query string', | ||||||
|             "type": "value_error.missing", |             'msg': 'Field required', | ||||||
|  |             'type': 'missing', | ||||||
|  |             'url': 'https://errors.pydantic.dev/2.5/v/missing' | ||||||
|         } |         } | ||||||
|     ] |     ] | ||||||
|  |  | ||||||
|  |  | ||||||
| async def test_get_article_with_wrong_qs_type_should_return_an_error_message( | async def test_get_article_with_wrong_qs_type_should_return_an_error_message( | ||||||
|     aiohttp_client, event_loop |         aiohttp_client, event_loop | ||||||
| ): | ): | ||||||
|     app = web.Application() |     app = web.Application() | ||||||
|     app.router.add_view("/article", ArticleView) |     app.router.add_view("/article", ArticleView) | ||||||
| @@ -86,16 +104,18 @@ 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", |             'input': 'foo', | ||||||
|             "loc": ["with_comments"], |             'loc': ['with_comments'], | ||||||
|             "msg": "value could not be parsed to a boolean", |             'loc_in': 'query string', | ||||||
|             "type": "type_error.bool", |             'msg': 'Input should be a valid boolean, unable to interpret input', | ||||||
|  |             'type': 'bool_parsing', | ||||||
|  |             'url': 'https://errors.pydantic.dev/2.5/v/bool_parsing' | ||||||
|         } |         } | ||||||
|     ] |     ] | ||||||
|  |  | ||||||
|  |  | ||||||
| async def test_get_article_with_valid_qs_should_return_the_parsed_type( | async def test_get_article_with_valid_qs_should_return_the_parsed_type( | ||||||
|     aiohttp_client, event_loop |         aiohttp_client, event_loop | ||||||
| ): | ): | ||||||
|     app = web.Application() |     app = web.Application() | ||||||
|     app.router.add_view("/article", ArticleView) |     app.router.add_view("/article", ArticleView) | ||||||
| @@ -114,7 +134,7 @@ async def test_get_article_with_valid_qs_should_return_the_parsed_type( | |||||||
|  |  | ||||||
|  |  | ||||||
| async def test_get_article_with_valid_qs_and_omitted_optional_should_return_default_value( | async def test_get_article_with_valid_qs_and_omitted_optional_should_return_default_value( | ||||||
|     aiohttp_client, event_loop |         aiohttp_client, event_loop | ||||||
| ): | ): | ||||||
|     app = web.Application() |     app = web.Application() | ||||||
|     app.router.add_view("/article", ArticleView) |     app.router.add_view("/article", ArticleView) | ||||||
| @@ -133,7 +153,7 @@ async def test_get_article_with_valid_qs_and_omitted_optional_should_return_defa | |||||||
|  |  | ||||||
|  |  | ||||||
| async def test_get_article_with_multiple_value_for_qs_age_must_failed( | async def test_get_article_with_multiple_value_for_qs_age_must_failed( | ||||||
|     aiohttp_client, event_loop |         aiohttp_client, event_loop | ||||||
| ): | ): | ||||||
|     app = web.Application() |     app = web.Application() | ||||||
|     app.router.add_view("/article", ArticleView) |     app.router.add_view("/article", ArticleView) | ||||||
| @@ -143,10 +163,12 @@ async def test_get_article_with_multiple_value_for_qs_age_must_failed( | |||||||
|     resp = await client.get("/article", params={"age": ["2", "3"], "with_comments": 1}) |     resp = await client.get("/article", params={"age": ["2", "3"], "with_comments": 1}) | ||||||
|     assert await resp.json() == [ |     assert await resp.json() == [ | ||||||
|         { |         { | ||||||
|             "in": "query string", |             'input': ['2', '3'], | ||||||
|             "loc": ["age"], |             'loc': ['age'], | ||||||
|             "msg": "value is not a valid integer", |             'loc_in': 'query string', | ||||||
|             "type": "type_error.integer", |             'msg': 'Input should be a valid integer', | ||||||
|  |             'type': 'int_type', | ||||||
|  |             'url': 'https://errors.pydantic.dev/2.5/v/int_type' | ||||||
|         } |         } | ||||||
|     ] |     ] | ||||||
|     assert resp.status == 400 |     assert resp.status == 400 | ||||||
| @@ -200,10 +222,12 @@ async def test_get_article_without_required_field_page(aiohttp_client, event_loo | |||||||
|     resp = await client.get("/article", params={"with_comments": 1}) |     resp = await client.get("/article", params={"with_comments": 1}) | ||||||
|     assert await resp.json() == [ |     assert await resp.json() == [ | ||||||
|         { |         { | ||||||
|             "in": "query string", |             'input': {'with_comments': '1'}, | ||||||
|             "loc": ["page_num"], |             'loc': ['page_num'], | ||||||
|             "msg": "field required", |             'loc_in': 'query string', | ||||||
|             "type": "value_error.missing", |             'msg': 'Field required', | ||||||
|  |             'type': 'missing', | ||||||
|  |             'url': 'https://errors.pydantic.dev/2.5/v/missing' | ||||||
|         } |         } | ||||||
|     ] |     ] | ||||||
|     assert resp.status == 400 |     assert resp.status == 400 | ||||||
| @@ -236,6 +260,20 @@ async def test_get_article_with_page_and_page_size(aiohttp_client, event_loop): | |||||||
|     assert resp.content_type == "application/json" |     assert resp.content_type == "application/json" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def test_get_article_with_enum_in_query(aiohttp_client, event_loop): | ||||||
|  |     app = web.Application() | ||||||
|  |     app.router.add_view("/article", ArticleViewWithEnumInQuery) | ||||||
|  |  | ||||||
|  |     client = await aiohttp_client(app) | ||||||
|  |  | ||||||
|  |     resp = await client.get( | ||||||
|  |         "/article", params={"lang": Lang.EN.value} | ||||||
|  |     ) | ||||||
|  |     assert await resp.json() == {'lang': Lang.EN} | ||||||
|  |     assert resp.status == 200 | ||||||
|  |     assert resp.content_type == "application/json" | ||||||
|  |  | ||||||
|  |  | ||||||
| async def test_get_article_with_page_and_wrong_page_size(aiohttp_client, event_loop): | async def test_get_article_with_page_and_wrong_page_size(aiohttp_client, event_loop): | ||||||
|     app = web.Application() |     app = web.Application() | ||||||
|     app.router.add_view("/article", ArticleViewWithPaginationGroup) |     app.router.add_view("/article", ArticleViewWithPaginationGroup) | ||||||
| @@ -247,10 +285,13 @@ async def test_get_article_with_page_and_wrong_page_size(aiohttp_client, event_l | |||||||
|     ) |     ) | ||||||
|     assert await resp.json() == [ |     assert await resp.json() == [ | ||||||
|         { |         { | ||||||
|             "in": "query string", |             'input': 'large', | ||||||
|             "loc": ["page_size"], |             'loc': ['page_size'], | ||||||
|             "msg": "value is not a valid integer", |             'loc_in': 'query string', | ||||||
|             "type": "type_error.integer", |             'msg': 'Input should be a valid integer, unable to parse string as an ' | ||||||
|  |                    'integer', | ||||||
|  |             'type': 'int_parsing', | ||||||
|  |             'url': 'https://errors.pydantic.dev/2.5/v/int_parsing' | ||||||
|         } |         } | ||||||
|     ] |     ] | ||||||
|     assert resp.status == 400 |     assert resp.status == 400 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user