Skip to content

Commit

Permalink
feat: rename FromFile and File to FromRawBody and RawBody (#117)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
adriangb authored Dec 21, 2022
1 parent 126b217 commit ab1ed0e
Show file tree
Hide file tree
Showing 13 changed files with 122 additions and 45 deletions.
19 changes: 17 additions & 2 deletions docs/tutorial/body.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,14 @@ First, import `Field` from Pydantic and `Annotated`:
All it does is import `Annotated` from `typing` if your Python version is >= 3.9 and [typing_extensions] otherwise.
But if you are already using Python >= 3.9, you can just replace that with `from typing import Annotated`.

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

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

!!! tip "Tip"
Pydantic also supports the syntax `field_name: str = Field(...)`, but we encourage youto get used to using `Annotated` instead.
Pydantic also supports the syntax `field_name: str = Field(...)`, but we encourage you to get used to using `Annotated` instead.
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.
That said, for JSON bodies using `field_name: str = Field(...)` will work just fine.

Expand Down Expand Up @@ -97,4 +97,19 @@ The Swagger docs will now reflect this:

![Swagger UI](body_002.png)

## Raw bytes

To extract the raw bytes from the body (without validating it as JSON) see [Files](files.md).

## Consuming of the request body

By default Xpresso will _consume_ the request body.
This is the equivalent of calling `Request.steam()` until completion.
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.
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`:

```python hl_lines="15 16"
--8<-- "docs_src/tutorial/body/tutorial_006.py"
```

[Pydantic]: https://pydantic-docs.helpmanual.io
6 changes: 3 additions & 3 deletions docs/tutorial/files.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Files

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

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

## Setting the expected content-type

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

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

If you do not explicitly set the media type, all media types are accepted.
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()`.
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()`.

[multipart/form-data documentation]: forms.md#multipart-requests
22 changes: 22 additions & 0 deletions docs_src/tutorial/body/tutorial_006.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import json
from typing import Any, Dict

from xpresso import App, Json, Path, RawBody
from xpresso.typing import Annotated


async def handle_event(
event: Annotated[Dict[str, Any], Json(consume=False)],
raw_body: Annotated[bytes, RawBody(consume=False)],
) -> bool:
return json.loads(raw_body) == event


app = App(
routes=[
Path(
"/webhook",
post=handle_event,
)
]
)
4 changes: 2 additions & 2 deletions docs_src/tutorial/files/tutorial_001.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from xpresso import App, FromFile, Path, UploadFile
from xpresso import App, FromRawBody, Path, UploadFile


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


Expand Down
4 changes: 2 additions & 2 deletions docs_src/tutorial/files/tutorial_002.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from xpresso import App, FromFile, Path
from xpresso import App, FromRawBody, Path


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


Expand Down
4 changes: 2 additions & 2 deletions docs_src/tutorial/files/tutorial_003.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from typing import AsyncIterator

from xpresso import App, FromFile, Path
from xpresso import App, FromRawBody, Path


async def count_bytes_in_file(
data: FromFile[AsyncIterator[bytes]],
data: FromRawBody[AsyncIterator[bytes]],
) -> int:
size = 0
async for chunk in data:
Expand Down
4 changes: 2 additions & 2 deletions docs_src/tutorial/files/tutorial_004.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from xpresso import App, File, Path, UploadFile
from xpresso import App, Path, RawBody, UploadFile
from xpresso.typing import Annotated


async def count_image_bytes(
file: Annotated[
UploadFile,
File(media_type="image/*", enforce_media_type=True),
RawBody(media_type="image/*", enforce_media_type=True),
]
) -> int:
return len(await file.read())
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "xpresso"
version = "0.45.1"
version = "0.46.0"
description = "A developer centric, performant Python web framework"
authors = ["Adrian Garcia Badaracco <[email protected]>"]
readme = "README.md"
Expand Down
10 changes: 10 additions & 0 deletions tests/test_docs/tutorial/body/test_tutorial_006.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from docs_src.tutorial.body.tutorial_006 import app
from xpresso.testclient import TestClient

client = TestClient(app)


def test_body_tutorial_006():
response = client.post("/webhook", json={"foo": "bar"})
assert response.status_code == 200, response.content
assert response.json() is True
36 changes: 19 additions & 17 deletions tests/test_request_bodies/test_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
from starlette.responses import Response
from starlette.testclient import TestClient

from xpresso import App, File, Path, UploadFile
from xpresso.bodies import FromFile
from xpresso import App, Path, RawBody, UploadFile
from xpresso.bodies import FromRawBody
from xpresso.typing import Annotated


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

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

@pytest.mark.parametrize("consume", [True, False])
def test_extract_into_uploadfile(consume: bool):
async def endpoint(file: Annotated[UploadFile, File(consume=consume)]) -> Response:
async def endpoint(
file: Annotated[UploadFile, RawBody(consume=consume)]
) -> Response:
assert await file.read() == b"data"
return Response()

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


def test_extract_into_stream():
async def endpoint(file: FromFile[AsyncIterator[bytes]]) -> Response:
async def endpoint(file: FromRawBody[AsyncIterator[bytes]]) -> Response:
got = bytearray()
async for chunk in file:
got.extend(chunk)
Expand All @@ -56,7 +58,7 @@ def stream() -> Generator[bytes, None, None]:

def test_read_into_stream():
async def endpoint(
file: Annotated[AsyncIterator[bytes], File(consume=False)]
file: Annotated[AsyncIterator[bytes], RawBody(consume=False)]
) -> Response:
...

Expand All @@ -80,7 +82,7 @@ def test_extract_into_bytes_empty_file(
consume: bool,
):
async def endpoint(
file: Annotated[Optional[bytes], File(consume=consume)] = None
file: Annotated[Optional[bytes], RawBody(consume=consume)] = None
) -> Response:
assert file is None
return Response()
Expand All @@ -105,7 +107,7 @@ def test_extract_into_uploadfile_empty_file(
consume: bool,
):
async def endpoint(
file: Annotated[Optional[UploadFile], File(consume=consume)] = None
file: Annotated[Optional[UploadFile], RawBody(consume=consume)] = None
) -> Response:
assert file is None
return Response()
Expand All @@ -128,7 +130,7 @@ def test_extract_into_stream_empty_file(
data: Optional[bytes],
):
async def endpoint(
file: FromFile[Optional[AsyncIterator[bytes]]] = None,
file: FromRawBody[Optional[AsyncIterator[bytes]]] = None,
) -> Response:
assert file is None
return Response()
Expand All @@ -141,7 +143,7 @@ async def endpoint(


def test_unknown_type():
async def endpoint(file: FromFile[str]) -> Response:
async def endpoint(file: FromRawBody[str]) -> Response:
...

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

def test_marker_used_in_multiple_locations():
async def endpoint(
file1: Annotated[bytes, File(consume=True)],
file2: Annotated[bytes, File(consume=True)],
file1: Annotated[bytes, RawBody(consume=True)],
file2: Annotated[bytes, RawBody(consume=True)],
) -> Response:
assert file1 == file2 == b"data"
return Response()
Expand Down Expand Up @@ -245,7 +247,7 @@ def test_openapi_content_type(
given_content_type: Optional[str], expected_content_type: str
):
async def endpoint(
file: Annotated[bytes, File(media_type=given_content_type)]
file: Annotated[bytes, RawBody(media_type=given_content_type)]
) -> Response:
...

Expand Down Expand Up @@ -325,7 +327,7 @@ async def endpoint(


def test_openapi_optional():
async def endpoint(file: FromFile[Optional[bytes]] = None) -> Response:
async def endpoint(file: FromRawBody[Optional[bytes]] = None) -> Response:
...

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

def test_openapi_include_in_schema():
async def endpoint(
file: Annotated[bytes, File(include_in_schema=False)]
file: Annotated[bytes, RawBody(include_in_schema=False)]
) -> Response:
...

Expand Down Expand Up @@ -440,7 +442,7 @@ async def endpoint(


def test_openapi_format():
async def endpoint(file: Annotated[bytes, File(format="base64")]) -> Response:
async def endpoint(file: Annotated[bytes, RawBody(format="base64")]) -> Response:
...

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

def test_openapi_description():
async def endpoint(
file: Annotated[bytes, File(description="foo bar baz")]
file: Annotated[bytes, RawBody(description="foo bar baz")]
) -> Response:
...

Expand Down
4 changes: 2 additions & 2 deletions tests/test_routing/test_operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import pytest
import starlette.routing

from xpresso import App, FromFile, FromJson, Operation, Path
from xpresso import App, FromJson, FromRawBody, Operation, Path
from xpresso.routing.operation import NotPreparedError
from xpresso.testclient import TestClient

Expand Down Expand Up @@ -44,7 +44,7 @@ def test_operation_comparison() -> None:
@pytest.mark.skip
def test_multiple_bodies_are_not_allowed() -> None:
async def endpoint(
body1: FromFile[bytes],
body1: FromRawBody[bytes],
body2: FromJson[str],
) -> None:
raise AssertionError("Should not be called") # pragma: no cover
Expand Down
18 changes: 12 additions & 6 deletions xpresso/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,24 @@
from starlette.responses import Response

from xpresso.applications import App
from xpresso.bodies import (
RawBody, # backwards compatibility aliases; TODO: remove in a couple of releases
)
from xpresso.bodies import (
BodyUnion,
File,
Form,
FormField,
FormFile,
FromBodyUnion,
FromFile,
FromFormData,
FromFormField,
FromFormFile,
FromJson,
FromMultipart,
Json,
Multipart,
)
from xpresso.bodies import FromRawBody
from xpresso.bodies import FromRawBody as FromFile
from xpresso.bodies import Json, Multipart
from xpresso.datastructures import UploadFile
from xpresso.dependencies import Depends
from xpresso.exception_handlers import ExcHandler
Expand Down Expand Up @@ -51,7 +53,7 @@
"FormFile",
"FromFormFile",
"Form",
"File",
"RawBody",
"Multipart",
"Depends",
"App",
Expand All @@ -69,10 +71,14 @@
"FromPath",
"FromQuery",
"HTTPException",
"FromFile",
"FromRawBody",
"status",
"Request",
"Response",
"WebSocketRoute",
"WebSocket",
# backwards compatibility aliases
# TODO: remove in a couple of releases
"File",
"FromFile",
)
Loading

0 comments on commit ab1ed0e

Please sign in to comment.