Compare commits
No commits in common. "v1.120.7" and "main" have entirely different histories.
@ -1,12 +0,0 @@
|
|||||||
stages:
|
|
||||||
- package
|
|
||||||
|
|
||||||
publish-pypi:
|
|
||||||
stage: package
|
|
||||||
image: python:3.10
|
|
||||||
script:
|
|
||||||
- sed -i -e "s/1.12.1/${CI_COMMIT_TAG:1}/g" aiohttp_pydantic/__init__.py
|
|
||||||
- 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}
|
|
||||||
only:
|
|
||||||
- tags
|
|
@ -2,4 +2,4 @@ from .view import PydanticView
|
|||||||
|
|
||||||
__version__ = "1.12.1"
|
__version__ = "1.12.1"
|
||||||
|
|
||||||
__all__ = ("PydanticView", "__version__")
|
__all__ = ("PydanticView", "__version__")
|
||||||
|
@ -15,12 +15,11 @@ def setup(
|
|||||||
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"] = custom_template or jinja2.Template(
|
oas_app["index template"] = 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,19 +120,6 @@ 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.
|
||||||
@ -140,7 +127,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*:.*|operation_?id\\s*:.*", line, re.IGNORECASE):
|
if re.fullmatch("status\\s+codes?\\s*:|tags\\s*:.*", line, re.IGNORECASE):
|
||||||
lines.rewind()
|
lines.rewind()
|
||||||
for _ in _i_extract_block(lines):
|
for _ in _i_extract_block(lines):
|
||||||
pass
|
pass
|
||||||
|
@ -1,27 +1,45 @@
|
|||||||
{# This updated file is part of swagger_ui_bundle (https://github.com/bartsanchez/swagger_ui_bundle) #}
|
{# This updated file is part of swagger_ui_bundle (https://github.com/dtkav/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-standalone-preset.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"> </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 }}",
|
||||||
@ -36,15 +54,16 @@
|
|||||||
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,17 +207,6 @@ 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):
|
||||||
|
@ -34,8 +34,7 @@ class _OASResponseBuilder:
|
|||||||
).copy()
|
).copy()
|
||||||
if def_sub_schemas := response_schema.pop("definitions", None):
|
if def_sub_schemas := response_schema.pop("definitions", None):
|
||||||
self._oas.components.schemas.update(def_sub_schemas)
|
self._oas.components.schemas.update(def_sub_schemas)
|
||||||
self._oas.components.schemas.update({response_schema['title']: response_schema})
|
return response_schema
|
||||||
return {'$ref': f'#/components/schemas/{response_schema["title"]}'}
|
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def _handle_list(self, obj):
|
def _handle_list(self, obj):
|
||||||
@ -88,7 +87,6 @@ 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 = {}
|
||||||
@ -128,10 +126,6 @@ def _add_http_method_to_oas(
|
|||||||
ref_template="#/components/schemas/{model}"
|
ref_template="#/components/schemas/{model}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# move definitions
|
|
||||||
if def_sub_schemas := oas_operation.parameters[i].schema.pop("definitions", 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(
|
||||||
@ -191,8 +185,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(
|
||||||
|
@ -3,7 +3,4 @@ requires = [
|
|||||||
"setuptools >= 46.4.0",
|
"setuptools >= 46.4.0",
|
||||||
"wheel",
|
"wheel",
|
||||||
]
|
]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
|
||||||
asyncio_mode = "auto"
|
|
@ -1,42 +1,42 @@
|
|||||||
aiohttp==3.8.1
|
async-timeout==3.0.1
|
||||||
aiosignal==1.2.0
|
attrs==21.2.0
|
||||||
async-timeout==4.0.2
|
bleach==4.0.0
|
||||||
atomicwrites==1.4.1
|
certifi==2021.5.30
|
||||||
attrs==21.4.0
|
cffi==1.14.6
|
||||||
bleach==5.0.1
|
chardet==4.0.0
|
||||||
certifi==2022.6.15
|
charset-normalizer==2.0.4
|
||||||
charset-normalizer==2.1.0
|
codecov==2.1.11
|
||||||
codecov==2.1.12
|
colorama==0.4.4
|
||||||
colorama==0.4.5
|
coverage==5.5
|
||||||
commonmark==0.9.1
|
cryptography==3.4.7
|
||||||
coverage==6.4.2
|
docutils==0.17.1
|
||||||
docutils==0.19
|
idna==3.2
|
||||||
frozenlist==1.3.0
|
importlib-metadata==4.6.3
|
||||||
idna==3.3
|
|
||||||
importlib-metadata==4.12.0
|
|
||||||
iniconfig==1.1.1
|
iniconfig==1.1.1
|
||||||
keyring==23.7.0
|
jeepney==0.7.1
|
||||||
multidict==6.0.2
|
keyring==23.0.1
|
||||||
packaging==21.3
|
multidict==5.1.0
|
||||||
pkginfo==1.8.3
|
packaging==21.0
|
||||||
pluggy==1.0.0
|
pkginfo==1.7.1
|
||||||
py==1.11.0
|
pluggy==0.13.1
|
||||||
Pygments==2.12.0
|
py==1.10.0
|
||||||
pyparsing==3.0.9
|
pycparser==2.20
|
||||||
pytest==7.1.2
|
Pygments==2.9.0
|
||||||
pytest-aiohttp==1.0.4
|
pyparsing==2.4.7
|
||||||
pytest-asyncio==0.19.0
|
pytest==6.1.2
|
||||||
pytest-cov==3.0.0
|
pytest-aiohttp==0.3.0
|
||||||
pywin32-ctypes==0.2.0
|
pytest-cov==2.10.1
|
||||||
readme-renderer==35.0
|
readme-renderer==29.0
|
||||||
requests==2.28.1
|
requests==2.26.0
|
||||||
requests-toolbelt==0.9.1
|
requests-toolbelt==0.9.1
|
||||||
rfc3986==2.0.0
|
rfc3986==1.5.0
|
||||||
rich==12.5.1
|
SecretStorage==3.3.1
|
||||||
six==1.16.0
|
six==1.16.0
|
||||||
tomli==2.0.1
|
toml==0.10.2
|
||||||
twine==4.0.1
|
tqdm==4.62.0
|
||||||
urllib3==1.26.11
|
twine==3.4.2
|
||||||
|
typing-extensions==3.10.0.0
|
||||||
|
urllib3==1.26.6
|
||||||
webencodings==0.5.1
|
webencodings==0.5.1
|
||||||
yarl==1.7.2
|
yarl==1.6.3
|
||||||
zipp==3.8.1
|
zipp==3.5.0
|
||||||
|
@ -1,28 +1,23 @@
|
|||||||
aiohttp==3.8.1
|
async-timeout==3.0.1
|
||||||
aiosignal==1.2.0
|
attrs==21.2.0
|
||||||
async-timeout==4.0.2
|
bleach==4.0.0
|
||||||
atomicwrites==1.4.1
|
chardet==4.0.0
|
||||||
attrs==21.4.0
|
coverage==5.5
|
||||||
bleach==5.0.1
|
docutils==0.17.1
|
||||||
charset-normalizer==2.1.0
|
idna==3.2
|
||||||
colorama==0.4.5
|
|
||||||
coverage==6.4.2
|
|
||||||
docutils==0.19
|
|
||||||
frozenlist==1.3.0
|
|
||||||
idna==3.3
|
|
||||||
iniconfig==1.1.1
|
iniconfig==1.1.1
|
||||||
multidict==6.0.2
|
multidict==5.1.0
|
||||||
packaging==21.3
|
packaging==21.0
|
||||||
pluggy==1.0.0
|
pluggy==0.13.1
|
||||||
py==1.11.0
|
py==1.10.0
|
||||||
Pygments==2.12.0
|
Pygments==2.9.0
|
||||||
pyparsing==3.0.9
|
pyparsing==2.4.7
|
||||||
pytest==7.1.2
|
pytest==6.1.2
|
||||||
pytest-aiohttp==1.0.4
|
pytest-aiohttp==0.3.0
|
||||||
pytest-asyncio==0.19.0
|
pytest-cov==2.10.1
|
||||||
pytest-cov==3.0.0
|
readme-renderer==29.0
|
||||||
readme-renderer==35.0
|
|
||||||
six==1.16.0
|
six==1.16.0
|
||||||
tomli==2.0.1
|
toml==0.10.2
|
||||||
|
typing-extensions==3.10.0.0
|
||||||
webencodings==0.5.1
|
webencodings==0.5.1
|
||||||
yarl==1.7.2
|
yarl==1.6.3
|
||||||
|
14
setup.cfg
14
setup.cfg
@ -32,18 +32,18 @@ python_requires = >=3.8
|
|||||||
install_requires =
|
install_requires =
|
||||||
aiohttp
|
aiohttp
|
||||||
pydantic>=1.7
|
pydantic>=1.7
|
||||||
swagger-4-ui-bundle
|
swagger-ui-bundle
|
||||||
|
|
||||||
[options.extras_require]
|
[options.extras_require]
|
||||||
test =
|
test =
|
||||||
pytest==7.1.2
|
pytest==6.1.2
|
||||||
pytest-aiohttp==1.0.4
|
pytest-aiohttp==0.3.0
|
||||||
pytest-cov==3.0.0
|
pytest-cov==2.10.1
|
||||||
readme-renderer==35.0
|
readme-renderer==29.0
|
||||||
ci =
|
ci =
|
||||||
%(test)s
|
%(test)s
|
||||||
codecov==2.1.12
|
codecov==2.1.11
|
||||||
twine==4.0.1
|
twine==3.4.2
|
||||||
|
|
||||||
[options.packages.find]
|
[options.packages.find]
|
||||||
exclude =
|
exclude =
|
||||||
|
13
tasks.py
13
tasks.py
@ -102,9 +102,8 @@ def test(c, isolate=False):
|
|||||||
"""
|
"""
|
||||||
Launch tests
|
Launch tests
|
||||||
"""
|
"""
|
||||||
#opt = "I" if isolate else ""
|
opt = "I" if isolate else ""
|
||||||
#c.run(f"python -{opt}m pytest --cov-report=xml --cov=aiohttp_pydantic tests/")
|
c.run(f"python -{opt}m pytest --cov-report=xml --cov=aiohttp_pydantic tests/")
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@task()
|
@task()
|
||||||
@ -137,7 +136,6 @@ def prepare_ci_env(c):
|
|||||||
|
|
||||||
title("Installing wheel", "=")
|
title("Installing wheel", "=")
|
||||||
package_version = read_configuration("./setup.cfg")["metadata"]["version"]
|
package_version = read_configuration("./setup.cfg")["metadata"]["version"]
|
||||||
print([x for x in Path("dist").glob('*')])
|
|
||||||
dist = next(Path("dist").glob(f"aiohttp_pydantic-{package_version}-*.whl"))
|
dist = next(Path("dist").glob(f"aiohttp_pydantic-{package_version}-*.whl"))
|
||||||
c.run(f"dist_venv/bin/python -m pip install {dist}")
|
c.run(f"dist_venv/bin/python -m pip install {dist}")
|
||||||
|
|
||||||
@ -158,7 +156,7 @@ def prepare_upload(c):
|
|||||||
|
|
||||||
|
|
||||||
@task(tag_eq_version, prepare_upload)
|
@task(tag_eq_version, prepare_upload)
|
||||||
def upload(c, pypi_user=None, pypi_password=None, pypi_url=None):
|
def upload(c, pypi_user=None, pypi_password=None):
|
||||||
"""
|
"""
|
||||||
Upload on pypi
|
Upload on pypi
|
||||||
"""
|
"""
|
||||||
@ -167,9 +165,8 @@ def upload(c, pypi_user=None, pypi_password=None, pypi_url=None):
|
|||||||
if pypi_user is not None and pypi_password is not None:
|
if pypi_user is not None and pypi_password is not None:
|
||||||
c.run(
|
c.run(
|
||||||
f"dist_venv/bin/twine upload --non-interactive"
|
f"dist_venv/bin/twine upload --non-interactive"
|
||||||
f" -u {pypi_user} -p {pypi_password} {dist}"
|
f" -u {pypi_user} -p {pypi_password} {dist}",
|
||||||
f" --repository-url {pypi_url}",
|
|
||||||
hide=True,
|
hide=True,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
c.run(f"dist_venv/bin/twine upload --repository-url {pypi_url} --repository aiohttp-pydantic {dist}")
|
c.run(f"dist_venv/bin/twine upload --repository aiohttp-pydantic {dist}")
|
||||||
|
@ -37,14 +37,13 @@ class ArticleView(PydanticView):
|
|||||||
|
|
||||||
|
|
||||||
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, loop
|
||||||
):
|
):
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
app.router.add_view("/article", ArticleView)
|
app.router.add_view("/article", ArticleView)
|
||||||
|
|
||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
resp = await client.post("/article", json={"name": "foo", "nb_page": "foo"})
|
resp = await client.post("/article", json={"name": "foo", "nb_page": "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() == [
|
||||||
|
@ -6,12 +6,11 @@ from uuid import UUID
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from pydantic import Field
|
|
||||||
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, r400
|
from aiohttp_pydantic.oas.typing import r200, r201, r204, r404
|
||||||
from aiohttp_pydantic.oas.view import generate_oas
|
from aiohttp_pydantic.oas.view import generate_oas
|
||||||
|
|
||||||
|
|
||||||
@ -21,11 +20,6 @@ class Color(str, Enum):
|
|||||||
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
|
||||||
@ -33,18 +27,13 @@ class Toy(BaseModel):
|
|||||||
|
|
||||||
class Pet(BaseModel):
|
class Pet(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
name: Optional[str] = Field(None)
|
name: str
|
||||||
toys: List[Toy]
|
toys: List[Toy]
|
||||||
|
|
||||||
|
|
||||||
class Error(BaseModel):
|
|
||||||
code: int
|
|
||||||
text: str
|
|
||||||
|
|
||||||
|
|
||||||
class PetCollectionView(PydanticView):
|
class PetCollectionView(PydanticView):
|
||||||
async def get(
|
async def get(
|
||||||
self, format: str, lang: Lang = Lang.EN, name: Optional[str] = None, *, promo: Optional[UUID] = None
|
self, format: str, name: Optional[str] = None, *, promo: Optional[UUID] = None
|
||||||
) -> r200[List[Pet]]:
|
) -> r200[List[Pet]]:
|
||||||
"""
|
"""
|
||||||
Get a list of pets
|
Get a list of pets
|
||||||
@ -52,7 +41,6 @@ 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()
|
||||||
|
|
||||||
@ -63,12 +51,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[Error], r400[Error]]:
|
) -> Union[r200[Pet], r404]:
|
||||||
return web.json_response()
|
return web.json_response()
|
||||||
|
|
||||||
async def put(self, id: int, /, pet: Pet):
|
async def put(self, id: int, /, pet: Pet):
|
||||||
@ -110,7 +98,7 @@ async def ensure_content_durability(client):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
async def generated_oas(aiohttp_client, event_loop) -> web.Application:
|
async def generated_oas(aiohttp_client, loop) -> web.Application:
|
||||||
app = web.Application()
|
app = 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)
|
||||||
@ -128,12 +116,6 @@ async def test_generated_oas_should_have_components_schemas(generated_oas):
|
|||||||
"title": "Color",
|
"title": "Color",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
},
|
},
|
||||||
'Lang': {
|
|
||||||
'description': 'An enumeration.',
|
|
||||||
'enum': ['en', 'fr'],
|
|
||||||
'title': 'Lang',
|
|
||||||
'type': 'string'
|
|
||||||
},
|
|
||||||
"Toy": {
|
"Toy": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"color": {"$ref": "#/components/schemas/Color"},
|
"color": {"$ref": "#/components/schemas/Color"},
|
||||||
@ -153,7 +135,6 @@ 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": [
|
||||||
{
|
{
|
||||||
@ -162,16 +143,6 @@ 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",
|
||||||
@ -201,7 +172,7 @@ async def test_pets_route_should_have_get_method(generated_oas):
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"required": ["id", "toys"],
|
"required": ["id", "name", "toys"],
|
||||||
"title": "Pet",
|
"title": "Pet",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
},
|
},
|
||||||
@ -230,7 +201,7 @@ async def test_pets_route_should_have_post_method(generated_oas):
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"required": ["id", "toys"],
|
"required": ["id", "name", "toys"],
|
||||||
"title": "Pet",
|
"title": "Pet",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
}
|
}
|
||||||
@ -252,7 +223,7 @@ async def test_pets_route_should_have_post_method(generated_oas):
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"required": ["id", "toys"],
|
"required": ["id", "name", "toys"],
|
||||||
"title": "Pet",
|
"title": "Pet",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
}
|
}
|
||||||
@ -329,7 +300,7 @@ async def test_pets_id_route_should_have_get_method(generated_oas):
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"required": ["id", "toys"],
|
"required": ["id", "name", "toys"],
|
||||||
"title": "Pet",
|
"title": "Pet",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
}
|
}
|
||||||
@ -364,7 +335,7 @@ async def test_pets_id_route_should_have_put_method(generated_oas):
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"required": ["id", "toys"],
|
"required": ["id", "name", "toys"],
|
||||||
"title": "Pet",
|
"title": "Pet",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
}
|
}
|
||||||
|
@ -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, loop
|
||||||
):
|
):
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
app.router.add_view("/article", ArticleView)
|
app.router.add_view("/article", ArticleView)
|
||||||
@ -49,7 +49,7 @@ async def test_post_an_article_without_required_field_should_return_an_error_mes
|
|||||||
|
|
||||||
|
|
||||||
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, loop
|
||||||
):
|
):
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
app.router.add_view("/article", ArticleView)
|
app.router.add_view("/article", ArticleView)
|
||||||
@ -68,7 +68,7 @@ async def test_post_an_article_with_wrong_type_field_should_return_an_error_mess
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
async def test_post_an_array_json_is_supported(aiohttp_client, event_loop):
|
async def test_post_an_array_json_is_supported(aiohttp_client, loop):
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
app.router.add_view("/article", ArticleView)
|
app.router.add_view("/article", ArticleView)
|
||||||
|
|
||||||
@ -81,7 +81,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, loop
|
||||||
):
|
):
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
app.router.add_view("/article", ArticleView)
|
app.router.add_view("/article", ArticleView)
|
||||||
@ -101,7 +101,7 @@ async def test_post_an_array_json_to_an_object_model_should_return_an_error(
|
|||||||
|
|
||||||
|
|
||||||
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, loop
|
||||||
):
|
):
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
app.router.add_view("/article", ArticleView)
|
app.router.add_view("/article", ArticleView)
|
||||||
@ -120,7 +120,7 @@ async def test_post_an_object_json_to_a_list_model_should_return_an_error(
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
async def test_post_a_valid_article_should_return_the_parsed_type(aiohttp_client, event_loop):
|
async def test_post_a_valid_article_should_return_the_parsed_type(aiohttp_client, loop):
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
app.router.add_view("/article", ArticleView)
|
app.router.add_view("/article", ArticleView)
|
||||||
|
|
||||||
|
@ -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, loop
|
||||||
):
|
):
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
app.router.add_view("/article", ArticleView)
|
app.router.add_view("/article", ArticleView)
|
||||||
@ -81,7 +81,7 @@ async def test_get_article_without_required_header_should_return_an_error_messag
|
|||||||
|
|
||||||
|
|
||||||
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, loop
|
||||||
):
|
):
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
app.router.add_view("/article", ArticleView)
|
app.router.add_view("/article", ArticleView)
|
||||||
@ -101,7 +101,7 @@ async def test_get_article_with_wrong_header_type_should_return_an_error_message
|
|||||||
|
|
||||||
|
|
||||||
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, loop
|
||||||
):
|
):
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
app.router.add_view("/article", ArticleView)
|
app.router.add_view("/article", ArticleView)
|
||||||
@ -116,7 +116,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, loop
|
||||||
):
|
):
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
app.router.add_view("/article", ArticleView)
|
app.router.add_view("/article", ArticleView)
|
||||||
@ -130,7 +130,7 @@ async def test_get_article_with_valid_header_containing_hyphen_should_be_returne
|
|||||||
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, event_loop):
|
async def test_wrong_value_to_header_defined_with_str_enum(aiohttp_client, loop):
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
app.router.add_view("/coord", ViewWithEnumType)
|
app.router.add_view("/coord", ViewWithEnumType)
|
||||||
|
|
||||||
@ -153,7 +153,7 @@ async def test_wrong_value_to_header_defined_with_str_enum(aiohttp_client, event
|
|||||||
assert resp.content_type == "application/json"
|
assert resp.content_type == "application/json"
|
||||||
|
|
||||||
|
|
||||||
async def test_correct_value_to_header_defined_with_str_enum(aiohttp_client, event_loop):
|
async def test_correct_value_to_header_defined_with_str_enum(aiohttp_client, loop):
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
app.router.add_view("/coord", ViewWithEnumType)
|
app.router.add_view("/coord", ViewWithEnumType)
|
||||||
|
|
||||||
@ -164,7 +164,7 @@ async def test_correct_value_to_header_defined_with_str_enum(aiohttp_client, eve
|
|||||||
assert resp.content_type == "application/json"
|
assert resp.content_type == "application/json"
|
||||||
|
|
||||||
|
|
||||||
async def test_with_signature_group(aiohttp_client, event_loop):
|
async def test_with_signature_group(aiohttp_client, loop):
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
app.router.add_view("/article", ArticleViewWithSignatureGroup)
|
app.router.add_view("/article", ArticleViewWithSignatureGroup)
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ class ArticleView(PydanticView):
|
|||||||
|
|
||||||
|
|
||||||
async def test_get_article_with_correct_path_parameters_should_return_parameters_in_path(
|
async def test_get_article_with_correct_path_parameters_should_return_parameters_in_path(
|
||||||
aiohttp_client, event_loop
|
aiohttp_client, loop
|
||||||
):
|
):
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
app.router.add_view("/article/{author_id}/tag/{tag}/before/{date}", ArticleView)
|
app.router.add_view("/article/{author_id}/tag/{tag}/before/{date}", ArticleView)
|
||||||
@ -24,7 +24,7 @@ async def test_get_article_with_correct_path_parameters_should_return_parameters
|
|||||||
|
|
||||||
|
|
||||||
async def test_get_article_with_wrong_path_parameters_should_return_error(
|
async def test_get_article_with_wrong_path_parameters_should_return_error(
|
||||||
aiohttp_client, event_loop
|
aiohttp_client, loop
|
||||||
):
|
):
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
app.router.add_view("/article/{author_id}/tag/{tag}/before/{date}", ArticleView)
|
app.router.add_view("/article/{author_id}/tag/{tag}/before/{date}", ArticleView)
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
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
|
||||||
@ -55,22 +54,8 @@ 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, loop
|
||||||
):
|
):
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
app.router.add_view("/article", ArticleView)
|
app.router.add_view("/article", ArticleView)
|
||||||
@ -90,7 +75,7 @@ async def test_get_article_without_required_qs_should_return_an_error_message(
|
|||||||
|
|
||||||
|
|
||||||
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, loop
|
||||||
):
|
):
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
app.router.add_view("/article", ArticleView)
|
app.router.add_view("/article", ArticleView)
|
||||||
@ -110,7 +95,7 @@ async def test_get_article_with_wrong_qs_type_should_return_an_error_message(
|
|||||||
|
|
||||||
|
|
||||||
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, loop
|
||||||
):
|
):
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
app.router.add_view("/article", ArticleView)
|
app.router.add_view("/article", ArticleView)
|
||||||
@ -129,7 +114,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, loop
|
||||||
):
|
):
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
app.router.add_view("/article", ArticleView)
|
app.router.add_view("/article", ArticleView)
|
||||||
@ -148,7 +133,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, loop
|
||||||
):
|
):
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
app.router.add_view("/article", ArticleView)
|
app.router.add_view("/article", ArticleView)
|
||||||
@ -168,7 +153,7 @@ async def test_get_article_with_multiple_value_for_qs_age_must_failed(
|
|||||||
assert resp.content_type == "application/json"
|
assert resp.content_type == "application/json"
|
||||||
|
|
||||||
|
|
||||||
async def test_get_article_with_multiple_value_of_tags(aiohttp_client, event_loop):
|
async def test_get_article_with_multiple_value_of_tags(aiohttp_client, loop):
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
app.router.add_view("/article", ArticleView)
|
app.router.add_view("/article", ArticleView)
|
||||||
|
|
||||||
@ -187,7 +172,7 @@ async def test_get_article_with_multiple_value_of_tags(aiohttp_client, event_loo
|
|||||||
assert resp.content_type == "application/json"
|
assert resp.content_type == "application/json"
|
||||||
|
|
||||||
|
|
||||||
async def test_get_article_with_one_value_of_tags_must_be_a_list(aiohttp_client, event_loop):
|
async def test_get_article_with_one_value_of_tags_must_be_a_list(aiohttp_client, loop):
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
app.router.add_view("/article", ArticleView)
|
app.router.add_view("/article", ArticleView)
|
||||||
|
|
||||||
@ -206,7 +191,7 @@ async def test_get_article_with_one_value_of_tags_must_be_a_list(aiohttp_client,
|
|||||||
assert resp.content_type == "application/json"
|
assert resp.content_type == "application/json"
|
||||||
|
|
||||||
|
|
||||||
async def test_get_article_without_required_field_page(aiohttp_client, event_loop):
|
async def test_get_article_without_required_field_page(aiohttp_client, loop):
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
app.router.add_view("/article", ArticleViewWithPaginationGroup)
|
app.router.add_view("/article", ArticleViewWithPaginationGroup)
|
||||||
|
|
||||||
@ -225,7 +210,7 @@ async def test_get_article_without_required_field_page(aiohttp_client, event_loo
|
|||||||
assert resp.content_type == "application/json"
|
assert resp.content_type == "application/json"
|
||||||
|
|
||||||
|
|
||||||
async def test_get_article_with_page(aiohttp_client, event_loop):
|
async def test_get_article_with_page(aiohttp_client, loop):
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
app.router.add_view("/article", ArticleViewWithPaginationGroup)
|
app.router.add_view("/article", ArticleViewWithPaginationGroup)
|
||||||
|
|
||||||
@ -237,7 +222,7 @@ async def test_get_article_with_page(aiohttp_client, event_loop):
|
|||||||
assert resp.content_type == "application/json"
|
assert resp.content_type == "application/json"
|
||||||
|
|
||||||
|
|
||||||
async def test_get_article_with_page_and_page_size(aiohttp_client, event_loop):
|
async def test_get_article_with_page_and_page_size(aiohttp_client, loop):
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
app.router.add_view("/article", ArticleViewWithPaginationGroup)
|
app.router.add_view("/article", ArticleViewWithPaginationGroup)
|
||||||
|
|
||||||
@ -251,21 +236,7 @@ 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):
|
async def test_get_article_with_page_and_wrong_page_size(aiohttp_client, 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):
|
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
app.router.add_view("/article", ArticleViewWithPaginationGroup)
|
app.router.add_view("/article", ArticleViewWithPaginationGroup)
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user