Skip to content

Commit ab1ed0e

Browse files
authored
feat: rename FromFile and File to FromRawBody and RawBody (#117)
We're keeping backwards compatibility aliases which will be removed in a future release, so this should have no impact on existing code but we recommend the new names going forward.
1 parent 126b217 commit ab1ed0e

File tree

13 files changed

+122
-45
lines changed

13 files changed

+122
-45
lines changed

docs/tutorial/body.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,14 @@ First, import `Field` from Pydantic and `Annotated`:
5757
All it does is import `Annotated` from `typing` if your Python version is >= 3.9 and [typing_extensions] otherwise.
5858
But if you are already using Python >= 3.9, you can just replace that with `from typing import Annotated`.
5959

60-
Now use `Field()` inside of `Annotated[...]` to attach validation and schema customziation metadata to the `price` field:
60+
Now use `Field()` inside of `Annotated[...]` to attach validation and schema customization metadata to the `price` field:
6161

6262
```python hl_lines="11-17"
6363
--8<-- "docs_src/tutorial/body/tutorial_002.py"
6464
```
6565

6666
!!! tip "Tip"
67-
Pydantic also supports the syntax `field_name: str = Field(...)`, but we encourage youto get used to using `Annotated` instead.
67+
Pydantic also supports the syntax `field_name: str = Field(...)`, but we encourage you to get used to using `Annotated` instead.
6868
As you will see in later chapters about forms and multipart requests, this will allow you to mix in Pydantic's validation and schema customization with Xpresso's extractor system.
6969
That said, for JSON bodies using `field_name: str = Field(...)` will work just fine.
7070

@@ -97,4 +97,19 @@ The Swagger docs will now reflect this:
9797

9898
![Swagger UI](body_002.png)
9999

100+
## Raw bytes
101+
102+
To extract the raw bytes from the body (without validating it as JSON) see [Files](files.md).
103+
104+
## Consuming of the request body
105+
106+
By default Xpresso will _consume_ the request body.
107+
This is the equivalent of calling `Request.steam()` until completion.
108+
When you only need to extract the body once this is probably what you want since it avoids keeping around extra memory that goes unused.
109+
If you want to extract the request body and still have access to it in another extractor or via `Request` you need to set `consume=False` as an argument to `Json`:
110+
111+
```python hl_lines="15 16"
112+
--8<-- "docs_src/tutorial/body/tutorial_006.py"
113+
```
114+
100115
[Pydantic]: https://pydantic-docs.helpmanual.io

docs/tutorial/files.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Files
22

3-
You can read the request body directly into a file or bytes.
3+
You can read the raw request body directly into a file or bytes.
44
This will read the data from the top level request body, and can only support 1 file.
55
To receive multiple files, see the [multipart/form-data documentation].
66

@@ -35,7 +35,7 @@ If you want to read the bytes without buffering to disk or memory, use `AsyncIte
3535

3636
## Setting the expected content-type
3737

38-
You can set the media type via the `media_type` parameter to `File()` and enforce it via the `enforce_media_type` parameter:
38+
You can set the media type via the `media_type` parameter to `RawBody()` and enforce it via the `enforce_media_type` parameter:
3939

4040
```python
4141
--8<-- "docs_src/tutorial/files/tutorial_004.py"
@@ -44,6 +44,6 @@ You can set the media type via the `media_type` parameter to `File()` and enforc
4444
Media types can be a media type (e.g. `image/png`) or a media type range (e.g. `image/*`).
4545

4646
If you do not explicitly set the media type, all media types are accepted.
47-
Once you set an explicit media type, that media type in the requests' `Content-Type` header will be validated on incoming requests, but this behavior can be disabled via the `enforce_media_type` parameter to `File()`.
47+
Once you set an explicit media type, that media type in the requests' `Content-Type` header will be validated on incoming requests, but this behavior can be disabled via the `enforce_media_type` parameter to `RawBody()`.
4848

4949
[multipart/form-data documentation]: forms.md#multipart-requests
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import json
2+
from typing import Any, Dict
3+
4+
from xpresso import App, Json, Path, RawBody
5+
from xpresso.typing import Annotated
6+
7+
8+
async def handle_event(
9+
event: Annotated[Dict[str, Any], Json(consume=False)],
10+
raw_body: Annotated[bytes, RawBody(consume=False)],
11+
) -> bool:
12+
return json.loads(raw_body) == event
13+
14+
15+
app = App(
16+
routes=[
17+
Path(
18+
"/webhook",
19+
post=handle_event,
20+
)
21+
]
22+
)

docs_src/tutorial/files/tutorial_001.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
from xpresso import App, FromFile, Path, UploadFile
1+
from xpresso import App, FromRawBody, Path, UploadFile
22

33

4-
async def count_bytes_in_file(file: FromFile[UploadFile]) -> int:
4+
async def count_bytes_in_file(file: FromRawBody[UploadFile]) -> int:
55
return len(await file.read())
66

77

docs_src/tutorial/files/tutorial_002.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
from xpresso import App, FromFile, Path
1+
from xpresso import App, FromRawBody, Path
22

33

4-
async def count_bytes_in_file(data: FromFile[bytes]) -> int:
4+
async def count_bytes_in_file(data: FromRawBody[bytes]) -> int:
55
return len(data)
66

77

docs_src/tutorial/files/tutorial_003.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
from typing import AsyncIterator
22

3-
from xpresso import App, FromFile, Path
3+
from xpresso import App, FromRawBody, Path
44

55

66
async def count_bytes_in_file(
7-
data: FromFile[AsyncIterator[bytes]],
7+
data: FromRawBody[AsyncIterator[bytes]],
88
) -> int:
99
size = 0
1010
async for chunk in data:

docs_src/tutorial/files/tutorial_004.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
from xpresso import App, File, Path, UploadFile
1+
from xpresso import App, Path, RawBody, UploadFile
22
from xpresso.typing import Annotated
33

44

55
async def count_image_bytes(
66
file: Annotated[
77
UploadFile,
8-
File(media_type="image/*", enforce_media_type=True),
8+
RawBody(media_type="image/*", enforce_media_type=True),
99
]
1010
) -> int:
1111
return len(await file.read())

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "xpresso"
3-
version = "0.45.1"
3+
version = "0.46.0"
44
description = "A developer centric, performant Python web framework"
55
authors = ["Adrian Garcia Badaracco <[email protected]>"]
66
readme = "README.md"
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from docs_src.tutorial.body.tutorial_006 import app
2+
from xpresso.testclient import TestClient
3+
4+
client = TestClient(app)
5+
6+
7+
def test_body_tutorial_006():
8+
response = client.post("/webhook", json={"foo": "bar"})
9+
assert response.status_code == 200, response.content
10+
assert response.json() is True

tests/test_request_bodies/test_file.py

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44
from starlette.responses import Response
55
from starlette.testclient import TestClient
66

7-
from xpresso import App, File, Path, UploadFile
8-
from xpresso.bodies import FromFile
7+
from xpresso import App, Path, RawBody, UploadFile
8+
from xpresso.bodies import FromRawBody
99
from xpresso.typing import Annotated
1010

1111

1212
@pytest.mark.parametrize("consume", [True, False])
1313
def test_extract_into_bytes(consume: bool):
14-
async def endpoint(file: Annotated[bytes, File(consume=consume)]) -> Response:
14+
async def endpoint(file: Annotated[bytes, RawBody(consume=consume)]) -> Response:
1515
assert file == b"data"
1616
return Response()
1717

@@ -24,7 +24,9 @@ async def endpoint(file: Annotated[bytes, File(consume=consume)]) -> Response:
2424

2525
@pytest.mark.parametrize("consume", [True, False])
2626
def test_extract_into_uploadfile(consume: bool):
27-
async def endpoint(file: Annotated[UploadFile, File(consume=consume)]) -> Response:
27+
async def endpoint(
28+
file: Annotated[UploadFile, RawBody(consume=consume)]
29+
) -> Response:
2830
assert await file.read() == b"data"
2931
return Response()
3032

@@ -36,7 +38,7 @@ async def endpoint(file: Annotated[UploadFile, File(consume=consume)]) -> Respon
3638

3739

3840
def test_extract_into_stream():
39-
async def endpoint(file: FromFile[AsyncIterator[bytes]]) -> Response:
41+
async def endpoint(file: FromRawBody[AsyncIterator[bytes]]) -> Response:
4042
got = bytearray()
4143
async for chunk in file:
4244
got.extend(chunk)
@@ -56,7 +58,7 @@ def stream() -> Generator[bytes, None, None]:
5658

5759
def test_read_into_stream():
5860
async def endpoint(
59-
file: Annotated[AsyncIterator[bytes], File(consume=False)]
61+
file: Annotated[AsyncIterator[bytes], RawBody(consume=False)]
6062
) -> Response:
6163
...
6264

@@ -80,7 +82,7 @@ def test_extract_into_bytes_empty_file(
8082
consume: bool,
8183
):
8284
async def endpoint(
83-
file: Annotated[Optional[bytes], File(consume=consume)] = None
85+
file: Annotated[Optional[bytes], RawBody(consume=consume)] = None
8486
) -> Response:
8587
assert file is None
8688
return Response()
@@ -105,7 +107,7 @@ def test_extract_into_uploadfile_empty_file(
105107
consume: bool,
106108
):
107109
async def endpoint(
108-
file: Annotated[Optional[UploadFile], File(consume=consume)] = None
110+
file: Annotated[Optional[UploadFile], RawBody(consume=consume)] = None
109111
) -> Response:
110112
assert file is None
111113
return Response()
@@ -128,7 +130,7 @@ def test_extract_into_stream_empty_file(
128130
data: Optional[bytes],
129131
):
130132
async def endpoint(
131-
file: FromFile[Optional[AsyncIterator[bytes]]] = None,
133+
file: FromRawBody[Optional[AsyncIterator[bytes]]] = None,
132134
) -> Response:
133135
assert file is None
134136
return Response()
@@ -141,7 +143,7 @@ async def endpoint(
141143

142144

143145
def test_unknown_type():
144-
async def endpoint(file: FromFile[str]) -> Response:
146+
async def endpoint(file: FromRawBody[str]) -> Response:
145147
...
146148

147149
app = App([Path("/", post=endpoint)])
@@ -153,8 +155,8 @@ async def endpoint(file: FromFile[str]) -> Response:
153155

154156
def test_marker_used_in_multiple_locations():
155157
async def endpoint(
156-
file1: Annotated[bytes, File(consume=True)],
157-
file2: Annotated[bytes, File(consume=True)],
158+
file1: Annotated[bytes, RawBody(consume=True)],
159+
file2: Annotated[bytes, RawBody(consume=True)],
158160
) -> Response:
159161
assert file1 == file2 == b"data"
160162
return Response()
@@ -245,7 +247,7 @@ def test_openapi_content_type(
245247
given_content_type: Optional[str], expected_content_type: str
246248
):
247249
async def endpoint(
248-
file: Annotated[bytes, File(media_type=given_content_type)]
250+
file: Annotated[bytes, RawBody(media_type=given_content_type)]
249251
) -> Response:
250252
...
251253

@@ -325,7 +327,7 @@ async def endpoint(
325327

326328

327329
def test_openapi_optional():
328-
async def endpoint(file: FromFile[Optional[bytes]] = None) -> Response:
330+
async def endpoint(file: FromRawBody[Optional[bytes]] = None) -> Response:
329331
...
330332

331333
app = App([Path("/", post=endpoint)])
@@ -409,7 +411,7 @@ async def endpoint(file: FromFile[Optional[bytes]] = None) -> Response:
409411

410412
def test_openapi_include_in_schema():
411413
async def endpoint(
412-
file: Annotated[bytes, File(include_in_schema=False)]
414+
file: Annotated[bytes, RawBody(include_in_schema=False)]
413415
) -> Response:
414416
...
415417

@@ -440,7 +442,7 @@ async def endpoint(
440442

441443

442444
def test_openapi_format():
443-
async def endpoint(file: Annotated[bytes, File(format="base64")]) -> Response:
445+
async def endpoint(file: Annotated[bytes, RawBody(format="base64")]) -> Response:
444446
...
445447

446448
app = App([Path("/", post=endpoint)])
@@ -523,7 +525,7 @@ async def endpoint(file: Annotated[bytes, File(format="base64")]) -> Response:
523525

524526
def test_openapi_description():
525527
async def endpoint(
526-
file: Annotated[bytes, File(description="foo bar baz")]
528+
file: Annotated[bytes, RawBody(description="foo bar baz")]
527529
) -> Response:
528530
...
529531

0 commit comments

Comments
 (0)