Skip to content

Commit 569ff75

Browse files
committed
Merge branch 'release/0.1.2'
2 parents 0267556 + d66e491 commit 569ff75

File tree

8 files changed

+326
-6
lines changed

8 files changed

+326
-6
lines changed

README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,20 @@ web.run_app(app)
5252

5353
```
5454

55+
Also, you can nest routers with prefixes,
56+
57+
```python
58+
api_router = Router()
59+
60+
memes_router = Router()
61+
62+
main_router = Router()
63+
64+
main_router.add_routes(api_router, prefix="/api")
65+
main_router.add_routes(memes_router, prefix="/memes")
66+
```
67+
68+
5569
## Default dependencies
5670

5771
By default this library provides only two injectables. `web.Request` and `web.Application`.
@@ -233,3 +247,34 @@ class MyView(View):
233247
return web.json_response({"app": str(app)})
234248

235249
```
250+
251+
252+
## Forms
253+
254+
Now you can easiy get and validate form data from your request.
255+
To make the magic happen, please add `arbitrary_types_allowed` to the config of your model.
256+
257+
258+
```python
259+
from pydantic import BaseModel
260+
from aiohttp_deps import Router, Depends, Form
261+
from aiohttp import web
262+
263+
router = Router()
264+
265+
266+
class MyForm(BaseModel):
267+
id: int
268+
file: web.FileField
269+
270+
class Config:
271+
arbitrary_types_allowed = True
272+
273+
274+
@router.post("/")
275+
async def handler(my_form: MyForm = Depends(Form())):
276+
with open("my_file", "wb") as f:
277+
f.write(my_form.file.file.read())
278+
return web.json_response({"id": my_form.id})
279+
280+
```

aiohttp_deps/__init__.py

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

44
from aiohttp_deps.initializer import init
55
from aiohttp_deps.router import Router
6-
from aiohttp_deps.utils import Header, Json, Query
6+
from aiohttp_deps.utils import Form, Header, Json, Query
77
from aiohttp_deps.view import View
88

99
__all__ = [
@@ -14,4 +14,5 @@
1414
"View",
1515
"Json",
1616
"Query",
17+
"Form",
1718
]

aiohttp_deps/router.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Iterable
2+
13
from aiohttp import web
24

35

@@ -13,3 +15,25 @@ class Router(web.RouteTableDef):
1315
1416
New types are introduced in stub file: router.pyi.
1517
"""
18+
19+
def add_routes(self, router: Iterable[web.RouteDef], prefix: str = "") -> None:
20+
"""
21+
Append another router's routes to this one.
22+
23+
:param router: router to get routes from.
24+
:param prefix: url prefix for routes, defaults to ""
25+
:raises ValueError: if prefix is incorrect.
26+
"""
27+
if prefix and not prefix.startswith("/"):
28+
raise ValueError("Prefix must start with a `/`")
29+
if prefix and prefix.endswith("/"):
30+
raise ValueError("Prefix should not end with a `/`")
31+
for route in router:
32+
self._items.append(
33+
web.RouteDef(
34+
method=route.method,
35+
path=prefix + route.path,
36+
handler=route.handler,
37+
kwargs=route.kwargs,
38+
),
39+
)

aiohttp_deps/router.pyi

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, Awaitable, Callable, Type, Union
1+
from typing import Any, Awaitable, Callable, Iterable, Type, Union
22

33
from aiohttp import web
44
from aiohttp.abc import AbstractView
@@ -18,3 +18,4 @@ class Router(web.RouteTableDef):
1818
def delete(self, path: str, **kwargs: Any) -> _Deco: ...
1919
def options(self, path: str, **kwargs: Any) -> _Deco: ...
2020
def view(self, path: str, **kwargs: Any) -> _Deco: ...
21+
def add_routes(self, router: Iterable[web.RouteDef], prefix: str = "") -> None: ...

aiohttp_deps/utils.py

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,10 @@ def __call__( # noqa: C901, WPS210
7070
except pydantic.ValidationError as err:
7171
errors = err.errors()
7272
for error in errors:
73-
error["loc"] = (f"header:{header_name}",)
73+
error["loc"] = (
74+
"header",
75+
header_name,
76+
) + error["loc"]
7477
raise web.HTTPBadRequest(
7578
headers={"Content-Type": "application/json"},
7679
text=json.dumps(errors),
@@ -119,7 +122,7 @@ async def __call__( # noqa: C901
119122
except pydantic.ValidationError as err:
120123
errors = err.errors()
121124
for error in errors:
122-
error["loc"] = ("body",)
125+
error["loc"] = ("body",) + error["loc"]
123126
raise web.HTTPBadRequest(
124127
headers={"Content-Type": "application/json"},
125128
text=json.dumps(errors),
@@ -189,7 +192,57 @@ def __call__( # noqa: C901, WPS210
189192
except pydantic.ValidationError as err:
190193
errors = err.errors()
191194
for error in errors:
192-
error["loc"] = (f"querystring:{param_name}",)
195+
error["loc"] = (
196+
"query",
197+
param_name,
198+
) + error["loc"]
199+
raise web.HTTPBadRequest(
200+
headers={"Content-Type": "application/json"},
201+
text=json.dumps(errors),
202+
)
203+
204+
205+
class Form:
206+
"""
207+
Get and validate form data.
208+
209+
This dependency grabs form data and validates
210+
it against given schema.
211+
212+
You should provide schema with typehints.
213+
"""
214+
215+
async def __call__(
216+
self,
217+
param_info: ParamInfo = Depends(),
218+
request: web.Request = Depends(),
219+
) -> Any:
220+
"""
221+
Performs actual logic, described above.
222+
223+
:param param_info: information about how the dependency
224+
was defined with name and type.
225+
:param request: current request.
226+
:raises HTTPBadRequest: if incorrect data was found.
227+
:return: parsed data.
228+
"""
229+
form_data = await request.post()
230+
definition = None
231+
if ( # noqa: WPS337
232+
param_info.definition
233+
and param_info.definition.annotation != inspect.Parameter.empty
234+
):
235+
definition = param_info.definition.annotation
236+
237+
if definition is None:
238+
return form_data
239+
240+
try:
241+
return pydantic.parse_obj_as(definition, form_data)
242+
except pydantic.ValidationError as err:
243+
errors = err.errors()
244+
for error in errors:
245+
error["loc"] = ("form",) + error["loc"]
193246
raise web.HTTPBadRequest(
194247
headers={"Content-Type": "application/json"},
195248
text=json.dumps(errors),

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name = "aiohttp-deps"
33
description = "Dependency injection for AioHTTP"
44
authors = ["Taskiq team <taskiq@no-reply.com>"]
55
maintainers = ["Taskiq team <taskiq@no-reply.com>"]
6-
version = "0.1.1"
6+
version = "0.1.2"
77
readme = "README.md"
88
license = "LICENSE"
99
classifiers = [

tests/test_form.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import pytest
2+
from aiohttp import web
3+
from pydantic import BaseModel
4+
5+
from aiohttp_deps import Depends, Form
6+
from tests.conftest import ClientGenerator
7+
8+
9+
class InputSchema(BaseModel):
10+
id: int
11+
file: web.FileField
12+
13+
class Config:
14+
arbitrary_types_allowed = True
15+
16+
17+
@pytest.mark.anyio
18+
async def test_form_dependency(
19+
my_app: web.Application,
20+
aiohttp_client: ClientGenerator,
21+
):
22+
async def handler(my_form: InputSchema = Depends(Form())):
23+
return web.Response(body=my_form.file.file.read())
24+
25+
my_app.router.add_post("/", handler)
26+
27+
file_data = b"bytes"
28+
client = await aiohttp_client(my_app)
29+
resp = await client.post(
30+
"/",
31+
data={"id": "1", "file": b"bytes"},
32+
)
33+
assert resp.status == 200
34+
assert await resp.content.read() == file_data
35+
36+
37+
@pytest.mark.anyio
38+
async def test_form_empty(
39+
my_app: web.Application,
40+
aiohttp_client: ClientGenerator,
41+
):
42+
async def handler(_: InputSchema = Depends(Form())):
43+
"""Nothing."""
44+
45+
my_app.router.add_post("/", handler)
46+
47+
client = await aiohttp_client(my_app)
48+
resp = await client.post(
49+
"/",
50+
)
51+
assert resp.status == 400
52+
53+
54+
@pytest.mark.anyio
55+
async def test_form_incorrect_data(
56+
my_app: web.Application,
57+
aiohttp_client: ClientGenerator,
58+
):
59+
async def handler(_: InputSchema = Depends(Form())):
60+
"""Nothing."""
61+
62+
my_app.router.add_post("/", handler)
63+
64+
client = await aiohttp_client(my_app)
65+
resp = await client.post("/", data={"id": "meme", "file": b""})
66+
assert resp.status == 400
67+
68+
69+
@pytest.mark.anyio
70+
async def test_form_untyped(
71+
my_app: web.Application,
72+
aiohttp_client: ClientGenerator,
73+
):
74+
async def handler(form=Depends(Form())):
75+
return web.Response(body=form["file"].file.read())
76+
77+
my_app.router.add_post("/", handler)
78+
79+
form_data = b"meme"
80+
client = await aiohttp_client(my_app)
81+
resp = await client.post("/", data={"id": "meme", "file": form_data})
82+
assert resp.status == 200
83+
assert await resp.content.read() == form_data

0 commit comments

Comments
 (0)