Compare commits

...

18 Commits

Author SHA1 Message Date
Sebastián Ramírez
83b1a117cc 🔖 Release version 0.26.0 2019-05-29 19:29:44 +04:00
Sebastián Ramírez
2a1ff213a0 📝 Update release notes 2019-05-29 16:33:19 +04:00
Sebastián Ramírez
62af6e0eeb Separate Pydantic's ValidationError handler and improve docs for error handling (#273)
*  Implement separated ValidationError handlers and custom exceptions

*  Add tutorial source examples and tests

* 📝 Add docs for custom exception handlers

* 📝 Update docs section titles
2019-05-29 16:27:55 +04:00
Sebastián Ramírez
15da01af5c 📝 Update release notes 2019-05-29 13:46:27 +04:00
William Hayes
d544bdf092 📝 Update docs for paths in path params (#256) 2019-05-29 13:43:41 +04:00
Sebastián Ramírez
703ade7967 🐛 Fix path in path parameters (#272) 2019-05-29 13:34:46 +04:00
Sebastián Ramírez
58f135ba2f 📝 Update link in release notes 2019-05-29 11:51:43 +04:00
Sebastián Ramírez
713d374484 📝 Update release notes 2019-05-29 11:47:46 +04:00
Sebastián Ramírez
24e9ea28d3 Update testing docs, examples for testing POST, headers (#271) 2019-05-29 11:47:21 +04:00
Sebastián Ramírez
cae53138b2 📝 Update release notes 2019-05-27 21:56:49 +04:00
Sebastián Ramírez
a49d45eaa9 🐛 Fix response_model type to allow List[Model] (#266) 2019-05-27 21:56:20 +04:00
Sebastián Ramírez
3986f79029 🔖 Release version 0.25.0 2019-05-27 20:21:03 +04:00
Sebastián Ramírez
7379fde5ee 📝 Update release notes 2019-05-27 16:28:24 +04:00
Sebastián Ramírez
7b63bc5551 Add include, exclude, and by_alias to path operation methods (#264)
*  Make jsonable_encoder's include and exclude receive sequences

*  Add include, exclude, and by_alias to app and router

*  Add and update tutorial code with new parameters

* 📝 Update docs for new parameters and add docs for updating data

*  Add tests for consistency in path operation methods

*  Add tests for new parameters and update tests
2019-05-27 16:08:13 +04:00
Sebastián Ramírez
747ae8210f 📝 Update release notes 2019-05-25 19:48:27 +04:00
William Hayes
c651416e05 📝 Create CONTRIBUTING.md (#255) 2019-05-25 19:47:49 +04:00
Sebastián Ramírez
814f95e2bf 📝 Update release notes 2019-05-25 19:40:04 +04:00
William Hayes
d8716f94ae Add skip_defaults support for path operations (for #242) (#248) 2019-05-25 19:35:57 +04:00
37 changed files with 2088 additions and 82 deletions

1
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1 @@
Please read the [Development - Contributing](https://fastapi.tiangolo.com/contributing/) guidelines in the documentation site.

View File

@@ -1,5 +1,42 @@
## Next release
## 0.26.0
* Separate error handling for validation errors.
* This will allow developers to customize the exception handlers.
* Document better how to handle exceptions and use error handlers.
* Include `RequestValidationError` and `WebSocketRequestValidationError` (this last one will be useful once [encode/starlette#527](https://github.com/encode/starlette/pull/527) or equivalent is merged).
* New documentation about exceptions handlers:
* [Install custom exception handlers](https://fastapi.tiangolo.com/tutorial/handling-errors/#install-custom-exception-handlers).
* [Override the default exception handlers](https://fastapi.tiangolo.com/tutorial/handling-errors/#override-the-default-exception-handlers).
* [Re-use **FastAPI's** exception handlers](https://fastapi.tiangolo.com/tutorial/handling-errors/#re-use-fastapis-exception-handlers).
* PR [#273](https://github.com/tiangolo/fastapi/pull/273).
* Fix support for *paths* in *path parameters* without needing explicit `Path(...)`.
* PR [#256](https://github.com/tiangolo/fastapi/pull/256).
* Documented in PR [#272](https://github.com/tiangolo/fastapi/pull/272) by [@wshayes](https://github.com/wshayes).
* New documentation at: [Path Parameters containing paths](https://fastapi.tiangolo.com/tutorial/path-params/#path-parameters-containing-paths).
* Update docs for testing FastAPI. Include using `POST`, sending JSON, testing headers, etc. New documentation: [Testing](https://fastapi.tiangolo.com/tutorial/testing/#testing-extended-example). PR [#271](https://github.com/tiangolo/fastapi/pull/271).
* Fix type declaration of `response_model` to allow generic Python types as `List[Model]`. Mainly to fix `mypy` for users. PR [#266](https://github.com/tiangolo/fastapi/pull/266).
## 0.25.0
* Add support for Pydantic's `include`, `exclude`, `by_alias`.
* Update documentation: [Response Model](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude).
* Add docs for: [Body - updates](https://fastapi.tiangolo.com/tutorial/body-updates/), using Pydantic's `skip_defaults`.
* Add method consistency tests.
* PR [#264](https://github.com/tiangolo/fastapi/pull/264).
* Add `CONTRIBUTING.md` file to GitHub, to help new contributors. PR [#255](https://github.com/tiangolo/fastapi/pull/255) by [@wshayes](https://github.com/wshayes).
* Add support for Pydantic's `skip_defaults`:
* There's a new *path operation decorator* parameter `response_model_skip_defaults`.
* The name of the parameter will most probably change in a future version to `response_skip_defaults`, `model_skip_defaults` or something similar.
* New [documentation section about using `response_model_skip_defaults`](https://fastapi.tiangolo.com/tutorial/response-model/#response-model-encoding-parameters).
* PR [#248](https://github.com/tiangolo/fastapi/pull/248) by [@wshayes](https://github.com/wshayes).
## 0.24.0
* Add support for WebSockets with dependencies and parameters.

View File

@@ -0,0 +1,36 @@
from fastapi import FastAPI, Header, HTTPException
from pydantic import BaseModel
fake_secret_token = "coneofsilence"
fake_db = {
"foo": {"id": "foo", "title": "Foo", "description": "There goes my hero"},
"bar": {"id": "bar", "title": "Bar", "description": "The bartenders"},
}
app = FastAPI()
class Item(BaseModel):
id: str
title: str
description: str = None
@app.get("/items/{item_id}", response_model=Item)
async def read_main(item_id: str, x_token: str = Header(...)):
if x_token != fake_secret_token:
raise HTTPException(status_code=400, detail="Invalid X-Token header")
if item_id not in fake_db:
raise HTTPException(status_code=404, detail="Item not found")
return fake_db[item_id]
@app.post("/items/", response_model=Item)
async def create_item(item: Item, x_token: str = Header(...)):
if x_token != fake_secret_token:
raise HTTPException(status_code=400, detail="Invalid X-Token header")
if item.id in fake_db:
raise HTTPException(status_code=400, detail="Item already exists")
fake_db[item.id] = item
return item

View File

@@ -0,0 +1,65 @@
from starlette.testclient import TestClient
from .main_b import app
client = TestClient(app)
def test_read_item():
response = client.get("/items/foo", headers={"X-Token": "coneofsilence"})
assert response.status_code == 200
assert response.json() == {
"id": "foo",
"title": "Foo",
"description": "There goes my hero",
}
def test_read_item_bad_token():
response = client.get("/items/foo", headers={"X-Token": "hailhydra"})
assert response.status_code == 400
assert response.json() == {"detail": "Invalid X-Token header"}
def test_read_inexistent_item():
response = client.get("/items/baz", headers={"X-Token": "coneofsilence"})
assert response.status_code == 404
assert response.json() == {"detail": "Item not found"}
def test_create_item():
response = client.post(
"/items/",
headers={"X-Token": "coneofsilence"},
json={"id": "foobar", "title": "Foo Bar", "description": "The Foo Barters"},
)
assert response.status_code == 200
assert response.json() == {
"id": "foobar",
"title": "Foo Bar",
"description": "The Foo Barters",
}
def test_create_item_bad_token():
response = client.post(
"/items/",
headers={"X-Token": "hailhydra"},
json={"id": "bazz", "title": "Bazz", "description": "Drop the bazz"},
)
assert response.status_code == 400
assert response.json() == {"detail": "Invalid X-Token header"}
def test_create_existing_token():
response = client.post(
"/items/",
headers={"X-Token": "coneofsilence"},
json={
"id": "foo",
"title": "The Foo ID Stealers",
"description": "There goes my stealer",
},
)
assert response.status_code == 400
assert response.json() == {"detail": "Item already exists"}

View File

@@ -0,0 +1,34 @@
from typing import List
from fastapi import FastAPI
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str = None
description: str = None
price: float = None
tax: float = 10.5
tags: List[str] = []
items = {
"foo": {"name": "Foo", "price": 50.2},
"bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2},
"baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []},
}
@app.get("/items/{item_id}", response_model=Item)
async def read_item(item_id: str):
return items[item_id]
@app.put("/items/{item_id}", response_model=Item)
async def update_item(item_id: str, item: Item):
update_item_encoded = jsonable_encoder(item)
items[item_id] = update_item_encoded
return update_item_encoded

View File

@@ -0,0 +1,37 @@
from typing import List
from fastapi import FastAPI
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str = None
description: str = None
price: float = None
tax: float = 10.5
tags: List[str] = []
items = {
"foo": {"name": "Foo", "price": 50.2},
"bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2},
"baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []},
}
@app.get("/items/{item_id}", response_model=Item)
async def read_item(item_id: str):
return items[item_id]
@app.patch("/items/{item_id}", response_model=Item)
async def update_item(item_id: str, item: Item):
stored_item_data = items[item_id]
stored_item_model = Item(**stored_item_data)
update_data = item.dict(skip_defaults=True)
updated_item = stored_item_model.copy(update=update_data)
items[item_id] = jsonable_encoder(updated_item)
return updated_item

View File

@@ -1,15 +1,26 @@
from fastapi import FastAPI
from starlette.exceptions import HTTPException
from starlette.responses import PlainTextResponse
from starlette.requests import Request
from starlette.responses import JSONResponse
class UnicornException(Exception):
def __init__(self, name: str):
self.name = name
app = FastAPI()
@app.exception_handler(HTTPException)
async def http_exception(request, exc):
return PlainTextResponse(str(exc.detail), status_code=exc.status_code)
@app.exception_handler(UnicornException)
async def unicorn_exception_handler(request: Request, exc: UnicornException):
return JSONResponse(
status_code=418,
content={"message": f"Oops! {exc.name} did something. There goes a rainbow..."},
)
@app.get("/")
async def root():
return {"message": "Hello World"}
@app.get("/unicorns/{name}")
async def read_unicorn(name: str):
if name == "yolo":
raise UnicornException(name=name)
return {"unicorn_name": name}

View File

@@ -0,0 +1,23 @@
from fastapi import FastAPI, HTTPException
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.responses import PlainTextResponse
app = FastAPI()
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request, exc):
return PlainTextResponse(str(exc.detail), status_code=exc.status_code)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
return PlainTextResponse(str(exc), status_code=400)
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id == 3:
raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
return {"item_id": item_id}

View File

@@ -0,0 +1,28 @@
from fastapi import FastAPI, HTTPException
from fastapi.exception_handlers import (
http_exception_handler,
request_validation_exception_handler,
)
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
app = FastAPI()
@app.exception_handler(StarletteHTTPException)
async def custom_http_exception_handler(request, exc):
print(f"OMG! An HTTP error!: {exc}")
return await http_exception_handler(request, exc)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
print(f"OMG! The client sent invalid data!: {exc}")
return await request_validation_exception_handler(request, exc)
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id == 3:
raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
return {"item_id": item_id}

View File

@@ -0,0 +1,8 @@
from fastapi import FastAPI
app = FastAPI()
@app.get("/files/{file_path:path}")
async def read_user_me(file_path: str):
return {"file_path": file_path}

View File

@@ -1,4 +1,4 @@
from typing import Set
from typing import List
from fastapi import FastAPI
from pydantic import BaseModel
@@ -11,9 +11,9 @@ class Item(BaseModel):
description: str = None
price: float
tax: float = None
tags: Set[str] = []
tags: List[str] = []
@app.post("/items/", response_model=Item)
async def create_item(*, item: Item):
async def create_item(item: Item):
return item

View File

@@ -0,0 +1,26 @@
from typing import List
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str = None
price: float
tax: float = 10.5
tags: List[str] = []
items = {
"foo": {"name": "Foo", "price": 50.2},
"bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2},
"baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []},
}
@app.get("/items/{item_id}", response_model=Item, response_model_skip_defaults=True)
async def read_item(item_id: str):
return items[item_id]

View File

@@ -0,0 +1,37 @@
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str = None
price: float
tax: float = 10.5
items = {
"foo": {"name": "Foo", "price": 50.2},
"bar": {"name": "Bar", "description": "The Bar fighters", "price": 62, "tax": 20.2},
"baz": {
"name": "Baz",
"description": "There goes my baz",
"price": 50.2,
"tax": 10.5,
},
}
@app.get(
"/items/{item_id}/name",
response_model=Item,
response_model_include={"name", "description"},
)
async def read_item_name(item_id: str):
return items[item_id]
@app.get("/items/{item_id}/public", response_model=Item, response_model_exclude={"tax"})
async def read_item_public_data(item_id: str):
return items[item_id]

View File

@@ -0,0 +1,37 @@
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str = None
price: float
tax: float = 10.5
items = {
"foo": {"name": "Foo", "price": 50.2},
"bar": {"name": "Bar", "description": "The Bar fighters", "price": 62, "tax": 20.2},
"baz": {
"name": "Baz",
"description": "There goes my baz",
"price": 50.2,
"tax": 10.5,
},
}
@app.get(
"/items/{item_id}/name",
response_model=Item,
response_model_include=["name", "description"],
)
async def read_item_name(item_id: str):
return items[item_id]
@app.get("/items/{item_id}/public", response_model=Item, response_model_exclude=["tax"])
async def read_item_public_data(item_id: str):
return items[item_id]

View File

@@ -0,0 +1,97 @@
## Update replacing with `PUT`
To update an item you can use the [HTTP `PUT`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT) operation.
You can use the `jsonable_encoder` to convert the input data to data that can be stored as JSON (e.g. with a NoSQL database). For example, converting `datetime` to `str`.
```Python hl_lines="30 31 32 33 34 35"
{!./src/body_updates/tutorial001.py!}
```
`PUT` is used to receive data that should replace the existing data.
### Warning about replacing
That means that if you want to update the item `bar` using `PUT` with a body containing:
```Python
{
"name": "Barz",
"price": 3,
"description": None,
}
```
because it doesn't include the already stored attribute `"tax": 20.2`, the input model would take the default value of `"tax": 10.5`.
And the data would be saved with that "new" `tax` of `10.5`.
## Partial updates with `PATCH`
You can also use the [HTTP `PATCH`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PATCH) operation to *partially* update data.
This means that you can send only the data that you want to update, leaving the rest intact.
!!! Note
`PATCH` is less commonly used and known than `PUT`.
And many teams use only `PUT`, even for partial updates.
You are **free** to use them however you want, **FastAPI** doesn't impose any restrictions.
But this guide shows you, more or less, how they are intended to be used.
### Using Pydantic's `skip_defaults` parameter
If you want to receive partial updates, it's very useful to use the parameter `skip_defaults` in Pydantic's model's `.dict()`.
Like `item.dict(skip_defaults=True)`.
That would generate a `dict` with only the data that was set when creating the `item` model, excluding default values.
Then you can use this to generate a `dict` with only the data that was set, omitting default values:
```Python hl_lines="34"
{!./src/body_updates/tutorial002.py!}
```
### Using Pydantic's `update` parameter
Now, you can create a copy of the existing model using `.copy()`, and pass the `update` parameter with a `dict` containing the data to update.
Like `stored_item_model.copy(update=update_data)`:
```Python hl_lines="35"
{!./src/body_updates/tutorial002.py!}
```
### Partial updates recap
In summary, to apply partial updates you would:
* (Optionally) use `PATCH` instead of `PUT`.
* Retrieve the stored data.
* Put that data in a Pydantic model.
* Generate a `dict` without default values from the input model (using `skip_defaults`).
* This way you can update only the values actually set by the user, instead of overriding values already stored with default values in your model.
* Create a copy of the stored model, updating it's attributes with the received partial updates (using the `update` parameter).
* Convert the copied model to something that can be stored in your DB (for example, using the `jsonable_encoder`).
* This is comparable to using the model's `.dict()` method again, but it makes sure (and converts) the values to data types that can be converted to JSON, for example, `datetime` to `str`.
* Save the data to your DB.
* Return the updated model.
```Python hl_lines="30 31 32 33 34 35 36 37"
{!./src/body_updates/tutorial002.py!}
```
!!! tip
You can actually use this same technique with an HTTP `PUT` operation.
But the example here uses `PATCH` because it was created for these use cases.
!!! note
Notice that the input model is still validated.
So, if you want to receive partial updates that can omit all the attributes, you need to have a model with all the attributes marked as optional (with default values or `None`).
To distinguish from the models with all optional values for **updates** and models with required values for **creation**, you can use the ideas described in <a href="https://fastapi.tiangolo.com/tutorial/extra-models/" target="_blank">Extra Models</a>.

View File

@@ -68,7 +68,7 @@ But if the client requests `http://example.com/items/bar` (a non-existent `item_
They are handled automatically by **FastAPI** and converted to JSON.
### Adding custom headers
## Add custom headers
There are some situations in where it's useful to be able to add custom headers to the HTTP error. For example, for some types of security.
@@ -76,24 +76,138 @@ You probably won't need to use it directly in your code.
But in case you needed it for an advanced scenario, you can add custom headers:
```Python hl_lines="14"
{!./src/handling_errors/tutorial002.py!}
```
### Installing custom handlers
## Install custom exception handlers
If you need to add other custom exception handlers, or override the default one (that sends the errors as JSON), you can use <a href="https://www.starlette.io/exceptions/" target="_blank">the same exception utilities from Starlette</a>.
You can add custom exception handlers with <a href="https://www.starlette.io/exceptions/" target="_blank">the same exception utilities from Starlette</a>.
For example, you could override the default exception handler with:
Let's say you have a custom exception `UnicornException` that you (or a library you use) might `raise`.
```Python hl_lines="2 3 8 9 10"
And you want to handle this exception globally with FastAPI.
You could add a custom exception handler with `@app.exception_handler()`:
```Python hl_lines="6 7 8 14 15 16 17 18 24"
{!./src/handling_errors/tutorial003.py!}
```
...this would make it return "plain text" responses with the errors, instead of JSON responses.
Here, if you request `/unicorns/yolo`, the *path operation* will `raise` a `UnicornException`.
!!! info
Note that in this example we set the exception handler with Starlette's `HTTPException` instead of FastAPI's `HTTPException`.
But it will be handled by the `unicorn_exception_handler`.
This would ensure that if you use a plug-in or any other third-party tool that raises Starlette's `HTTPException` directly, it will be caught by your exception handler.
So, you will receive a clean error, with an HTTP status code of `418` and a JSON content of:
```JSON
{"message": "Oops! yolo did something. There goes a rainbow..."}
```
## Override the default exception handlers
**FastAPI** has some default exception handlers.
These handlers are in charge or returning the default JSON responses when you `raise` an `HTTPException` and when the request has invalid data.
You can override these exception handlers with your own.
### Override request validation exceptions
When a request contains invalid data, **FastAPI** internally raises a `RequestValidationError`.
And it also includes a default exception handler for it.
To override it, import the `RequestValidationError` and use it with `@app.exception_handler(RequestValidationError)` to decorate the exception handler.
The exception handler will receive a `Request` and the exception.
```Python hl_lines="2 14 15 16"
{!./src/handling_errors/tutorial004.py!}
```
Now, if you go to `/items/foo`, instead of getting the default JSON error with:
```JSON
{
"detail": [
{
"loc": [
"path",
"item_id"
],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
]
}
```
you will get a text version, with:
```
1 validation error
path -> item_id
value is not a valid integer (type=type_error.integer)
```
#### `RequestValidationError` vs `ValidationError`
!!! warning
These are technical details that you might skip if it's not important for you now.
`RequestValidationError` is a sub-class of Pydantic's <a href="https://pydantic-docs.helpmanual.io/#error-handling" target="_blank">`ValidationError`</a>.
**FastAPI** uses it so that, if you use a Pydantic model in `response_model`, and your data has an error, you will see the error in your log.
But the client/user will not see it. Instead, the client will receive an "Internal Server Error" with a HTTP status code `500`.
It should be this way because if you have a Pydantic `ValidationError` in your *response* or anywhere in your code (not in the client's *request*), it's actually a bug in your code.
And while you fix it, your clients/users shouldn't have access to internal information about the error, as that could expose a security vulnerability.
### Override the `HTTPException` error handler
The same way, you can override the `HTTPException` handler.
For example, you could want to return a plain text response instead of JSON for these errors:
```Python hl_lines="1 3 9 10 11 22"
{!./src/handling_errors/tutorial004.py!}
```
#### FastAPI's `HTTPException` vs Starlette's `HTTPException`
**FastAPI** has its own `HTTPException`.
And **FastAPI**'s `HTTPException` error class inherits from Starlette's `HTTPException` error class.
The only difference, is that **FastAPI**'s `HTTPException` allows you to add headers to be included in the response.
This is needed/used internally for OAuth 2.0 and some security utilities.
So, you can keep raising **FastAPI**'s `HTTPException` as normally in your code.
But when you register an exception handler, you should register it for Starlette's `HTTPException`.
This way, if any part of Starlette's internal code, or a Starlette extension or plug-in, raises an `HTTPException`, your handler will be able to catch handle it.
In this example, to be able to have both `HTTPException`s in the same code, Starlette's exceptions is renamed to `StarletteHTTPException`:
```Python
from starlette.exceptions import HTTPException as StarletteHTTPException
```
### Re-use **FastAPI**'s exception handlers
You could also just want to use the exception somehow, but then use the same default exception handlers from **FastAPI**.
You can import and re-use the default exception handlers from `fastapi.exception_handlers`:
```Python hl_lines="2 3 4 5 15 21"
{!./src/handling_errors/tutorial005.py!}
```
In this example, you are just `print`ing the error with a very expressive notification.
But you get the idea, you can use the exception and then just re-use the default exception handlers.

View File

@@ -115,6 +115,42 @@ Because path operations are evaluated in order, you need to make sure that the p
Otherwise, the path for `/users/{user_id}` would match also for `/users/me`, "thinking" that it's receiving a parameter `user_id` with a value of `"me"`.
## Path parameters containing paths
Let's say you have a *path operation* with a path `/files/{file_path}`.
But you need `file_path` itself to contain a *path*, like `home/johndoe/myfile.txt`.
So, the URL for that file would be something like: `/files/home/johndoe/myfile.txt`.
### OpenAPI support
OpenAPI doesn't support a way to declare a *path parameter* to contain a *path* inside, as that could lead to scenarios that are difficult to test and define.
Nevertheless, you can still do it in **FastAPI**, using one of the internal tools from Starlette.
And the docs would still work, although not adding any documentation telling that the parameter should contain a path.
### Path convertor
Using an option directly from Starlette you can declare a *path parameter* containing a *path* using a URL like:
```
/files/{file_path:path}
```
In this case, the name of the parameter is `file_path`, and the last part, `:path`, tells it that the parameter should match any *path*.
So, you can use it with:
```Python hl_lines="6"
{!./src/path_params/tutorial004.py!}
```
!!! tip
You could need the parameter to contain `/home/johndoe/myfile.txt`, with a leading slash (`/`).
In that case, the URL would be: `/files//home/johndoe/myfile.txt`, with a double slash (`//`) between `files` and `home`.
## Recap
@@ -127,4 +163,4 @@ With **FastAPI**, by using short, intuitive and standard Python type declaration
And you only have to declare them once.
That's probably the main visible advantage of **FastAPI** compared to alternative frameworks (apart from the raw performance).
That's probably the main visible advantage of **FastAPI** compared to alternative frameworks (apart from the raw performance).

View File

@@ -13,12 +13,14 @@ You can declare the model used for the response with the parameter `response_mod
!!! note
Notice that `response_model` is a parameter of the "decorator" method (`get`, `post`, etc). Not of your path operation function, like all the parameters and body.
It receives a standard Pydantic model and will:
It receives the same type you would declare for a Pydantic model attribute, so, it can be a Pydantic model, but it can also be, e.g. a `list` of Pydantic models, like `List[Item]`.
* Convert the output data to the type declarations of the model
* Validate the data
* Add a JSON Schema for the response, in the OpenAPI path operation
* Will be used by the automatic documentation systems
FastAPI will use this `response_model` to:
* Convert the output data to its type declaration.
* Validate the data.
* Add a JSON Schema for the response, in the OpenAPI path operation.
* Will be used by the automatic documentation systems.
But most importantly:
@@ -45,7 +47,7 @@ Now, whenever a browser is creating a user with a password, the API will return
In this case, it might not be a problem, because the user himself is sending the password.
But if we use the same model for another path operation, we could be sending the passwords of our users to every client.
But if we use the same model for another path operation, we could be sending our user's passwords to every client.
!!! danger
Never send the plain password of a user in a response.
@@ -82,6 +84,114 @@ And both models will be used for the interactive API documentation:
<img src="/img/tutorial/response-model/image02.png">
## Response Model encoding parameters
Your response model could have default values, like:
```Python hl_lines="11 13 14"
{!./src/response_model/tutorial004.py!}
```
* `description: str = None` has a default of `None`.
* `tax: float = None` has a default of `None`.
* `tags: List[str] = []` has a default of an empty list: `[]`.
but you might want to omit them from the result if they were not actually stored.
For example, if you have models with many optional attributes in a NoSQL database, but you don't want to send very long JSON responses full of default values.
### Use the `response_model_skip_defaults` parameter
You can set the *path operation decorator* parameter `response_model_skip_defaults=True`:
```Python hl_lines="24"
{!./src/response_model/tutorial004.py!}
```
and those default values won't be included in the response.
So, if you send a request to that *path operation* for the item with ID `foo`, the response (not including default values) will be:
```JSON
{
"name": "Foo",
"price": 50.2
}
```
!!! info
FastAPI uses Pydantic model's `.dict()` with <a href="https://pydantic-docs.helpmanual.io/#copying" target="_blank">its `skip_defaults` parameter</a> to achieve this.
#### Data with values for fields with defaults
But if your data has values for the model's fields with default values, like the item with ID `bar`:
```Python hl_lines="3 5"
{
"name": "Bar",
"description": "The bartenders",
"price": 62,
"tax": 20.2
}
```
they will be included in the response.
#### Data with the same values as the defaults
If the data has the same values as the default ones, like the item with ID `baz`:
```Python hl_lines="3 5 6"
{
"name": "Baz",
"description": None,
"price": 50.2,
"tax": 10.5,
"tags": []
}
```
FastAPI is smart enough (actually, Pydantic is smart enough) to realize that, even though `description`, `tax`, and `tags` have the same values as the defaults, they were set explicitly (instead of taken from the defaults).
So, they will be included in the JSON response.
!!! tip
Notice that the default values can be anything, not only `None`.
They can be a list (`[]`), a `float` of `10.5`, etc.
### `response_model_include` and `response_model_exclude`
You can also use the *path operation decorator* parameters `response_model_include` and `response_model_exclude`.
They take a `set` of `str` with the name of the attributes to include (omitting the rest) or to exclude (including the rest).
This can be used as a quick shortcut if you have only one Pydantic model and want to remove some data from the output.
!!! tip
But it is still recommended to use the ideas above, using multiple classes, instead of these parameters.
This is because the JSON Schema generated in your app's OpenAPI (and the docs) will still be the one for the complete model, even if you use `response_model_include` or `response_model_exclude` to omit some attributes.
```Python hl_lines="29 35"
{!./src/response_model/tutorial005.py!}
```
!!! tip
The syntax `{"name", "description"}` creates a `set` with those two values.
It is equivalent to `set(["name", "description"])`.
#### Using `list`s instead of `set`s
If you forget to use a `set` and use a `list` or `tuple` instead, FastAPI will still convert it to a `set` and it will work correctly:
```Python hl_lines="29 35"
{!./src/response_model/tutorial006.py!}
```
## Recap
Use the path operation decorator's parameter `response_model` to define response models and especially to ensure private data is filtered out.
Use `response_model_skip_defaults` to return only the values explicitly set.

View File

@@ -22,12 +22,11 @@ Write simple `assert` statements with the standard Python expressions that you n
!!! tip
Notice that the testing functions are normal `def`, not `async def`.
And the calls to the client are also normal calls, not using `await`.
This allows you to use `pytest` directly without complications.
## Separating tests
In a real application, you probably would have your tests in a different file.
@@ -50,6 +49,51 @@ Then you could have a file `test_main.py` with your tests, and import your `app`
{!./src/app_testing/test_main.py!}
```
## Testing: extended example
Now let's extend this example and add more details to see how to test different parts.
### Extended **FastAPI** app file
Let's say you have a file `main_b.py` with your **FastAPI** app.
It has a `GET` operation that could return an error.
It has a `POST` operation that could return several errors.
Both *path operations* require an `X-Token` header.
```Python
{!./src/app_testing/main_b.py!}
```
### Extended testing file
You could then have a `test_main_b.py`, the same as before, with the extended tests:
```Python
{!./src/app_testing/test_main_b.py!}
```
Whenever you need the client to pass information in the request and you don't know how to, you can search (Google) how to do it in `requests`.
Then you just do the same in your tests.
E.g.:
* To pass a *path* or *query* parameter, add it to the URL itself.
* To pass a JSON body, pass a Python object (e.g. a `dict`) to the parameter `json`.
* If you need to send *Form Data* instead of JSON, use the `data` parameter instead.
* To pass *headers*, use a `dict` in the `headers` parameter.
* For *cookies*, a `dict` in the `cookies` parameter.
For more information about how to pass data to the backend (using `requests` or the `TestClient`) check the <a href="http://docs.python-requests.org" target="_blank">Requests documentation</a>.
!!! info
Note that the `TestClient` receives data that can be converted to JSON, not Pydantic models.
If you have a Pydantic model in your test and you want to send its data to the application during testing, you can use the <a href="https://fastapi.tiangolo.com/tutorial/encoder/" target="_blank">JSON compatible encoder: `jsonable_encoder`</a>.
## Testing WebSockets
You can use the same `TestClient` to test WebSockets.

View File

@@ -1,6 +1,6 @@
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
__version__ = "0.24.0"
__version__ = "0.26.0"
from starlette.background import BackgroundTasks

View File

@@ -1,6 +1,11 @@
from typing import Any, Callable, Dict, List, Optional, Type, Union
from typing import Any, Callable, Dict, List, Optional, Set, Type, Union
from fastapi import routing
from fastapi.exception_handlers import (
http_exception_handler,
request_validation_exception_handler,
)
from fastapi.exceptions import RequestValidationError
from fastapi.openapi.docs import (
get_redoc_html,
get_swagger_ui_html,
@@ -8,7 +13,6 @@ from fastapi.openapi.docs import (
)
from fastapi.openapi.utils import get_openapi
from fastapi.params import Depends
from pydantic import BaseModel
from starlette.applications import Starlette
from starlette.exceptions import ExceptionMiddleware, HTTPException
from starlette.middleware.errors import ServerErrorMiddleware
@@ -17,16 +21,6 @@ from starlette.responses import HTMLResponse, JSONResponse, Response
from starlette.routing import BaseRoute
async def http_exception(request: Request, exc: HTTPException) -> JSONResponse:
headers = getattr(exc, "headers", None)
if headers:
return JSONResponse(
{"detail": exc.detail}, status_code=exc.status_code, headers=headers
)
else:
return JSONResponse({"detail": exc.detail}, status_code=exc.status_code)
class FastAPI(Starlette):
def __init__(
self,
@@ -120,14 +114,17 @@ class FastAPI(Starlette):
)
self.add_route(self.redoc_url, redoc_html, include_in_schema=False)
self.add_exception_handler(HTTPException, http_exception)
self.add_exception_handler(HTTPException, http_exception_handler)
self.add_exception_handler(
RequestValidationError, request_validation_exception_handler
)
def add_api_route(
self,
path: str,
endpoint: Callable,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[Depends] = None,
@@ -138,6 +135,10 @@ class FastAPI(Starlette):
deprecated: bool = None,
methods: List[str] = None,
operation_id: str = None,
response_model_include: Set[str] = None,
response_model_exclude: Set[str] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@@ -156,6 +157,10 @@ class FastAPI(Starlette):
deprecated=deprecated,
methods=methods,
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@@ -165,7 +170,7 @@ class FastAPI(Starlette):
self,
path: str,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[Depends] = None,
@@ -176,6 +181,10 @@ class FastAPI(Starlette):
deprecated: bool = None,
methods: List[str] = None,
operation_id: str = None,
response_model_include: Set[str] = None,
response_model_exclude: Set[str] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@@ -195,6 +204,10 @@ class FastAPI(Starlette):
deprecated=deprecated,
methods=methods,
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@@ -236,7 +249,7 @@ class FastAPI(Starlette):
self,
path: str,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[Depends] = None,
@@ -246,6 +259,10 @@ class FastAPI(Starlette):
responses: Dict[Union[int, str], Dict[str, Any]] = None,
deprecated: bool = None,
operation_id: str = None,
response_model_include: Set[str] = None,
response_model_exclude: Set[str] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@@ -262,6 +279,10 @@ class FastAPI(Starlette):
responses=responses or {},
deprecated=deprecated,
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@@ -271,7 +292,7 @@ class FastAPI(Starlette):
self,
path: str,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[Depends] = None,
@@ -281,6 +302,10 @@ class FastAPI(Starlette):
responses: Dict[Union[int, str], Dict[str, Any]] = None,
deprecated: bool = None,
operation_id: str = None,
response_model_include: Set[str] = None,
response_model_exclude: Set[str] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@@ -297,6 +322,10 @@ class FastAPI(Starlette):
responses=responses or {},
deprecated=deprecated,
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@@ -306,7 +335,7 @@ class FastAPI(Starlette):
self,
path: str,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[Depends] = None,
@@ -316,6 +345,10 @@ class FastAPI(Starlette):
responses: Dict[Union[int, str], Dict[str, Any]] = None,
deprecated: bool = None,
operation_id: str = None,
response_model_include: Set[str] = None,
response_model_exclude: Set[str] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@@ -332,6 +365,10 @@ class FastAPI(Starlette):
responses=responses or {},
deprecated=deprecated,
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@@ -341,7 +378,7 @@ class FastAPI(Starlette):
self,
path: str,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[Depends] = None,
@@ -351,6 +388,10 @@ class FastAPI(Starlette):
responses: Dict[Union[int, str], Dict[str, Any]] = None,
deprecated: bool = None,
operation_id: str = None,
response_model_include: Set[str] = None,
response_model_exclude: Set[str] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@@ -366,7 +407,11 @@ class FastAPI(Starlette):
response_description=response_description,
responses=responses or {},
deprecated=deprecated,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
operation_id=operation_id,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@@ -376,7 +421,7 @@ class FastAPI(Starlette):
self,
path: str,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[Depends] = None,
@@ -386,6 +431,10 @@ class FastAPI(Starlette):
responses: Dict[Union[int, str], Dict[str, Any]] = None,
deprecated: bool = None,
operation_id: str = None,
response_model_include: Set[str] = None,
response_model_exclude: Set[str] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@@ -402,6 +451,10 @@ class FastAPI(Starlette):
responses=responses or {},
deprecated=deprecated,
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@@ -411,7 +464,7 @@ class FastAPI(Starlette):
self,
path: str,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[Depends] = None,
@@ -421,6 +474,10 @@ class FastAPI(Starlette):
responses: Dict[Union[int, str], Dict[str, Any]] = None,
deprecated: bool = None,
operation_id: str = None,
response_model_include: Set[str] = None,
response_model_exclude: Set[str] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@@ -437,6 +494,10 @@ class FastAPI(Starlette):
responses=responses or {},
deprecated=deprecated,
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@@ -446,7 +507,7 @@ class FastAPI(Starlette):
self,
path: str,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[Depends] = None,
@@ -456,6 +517,10 @@ class FastAPI(Starlette):
responses: Dict[Union[int, str], Dict[str, Any]] = None,
deprecated: bool = None,
operation_id: str = None,
response_model_include: Set[str] = None,
response_model_exclude: Set[str] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@@ -472,6 +537,10 @@ class FastAPI(Starlette):
responses=responses or {},
deprecated=deprecated,
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@@ -481,7 +550,7 @@ class FastAPI(Starlette):
self,
path: str,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[Depends] = None,
@@ -491,6 +560,10 @@ class FastAPI(Starlette):
responses: Dict[Union[int, str], Dict[str, Any]] = None,
deprecated: bool = None,
operation_id: str = None,
response_model_include: Set[str] = None,
response_model_exclude: Set[str] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@@ -507,6 +580,10 @@ class FastAPI(Starlette):
responses=responses or {},
deprecated=deprecated,
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,

View File

@@ -11,14 +11,24 @@ def jsonable_encoder(
include: Set[str] = None,
exclude: Set[str] = set(),
by_alias: bool = True,
skip_defaults: bool = False,
include_none: bool = True,
custom_encoder: dict = {},
sqlalchemy_safe: bool = True,
) -> Any:
if include is not None and not isinstance(include, set):
include = set(include)
if exclude is not None and not isinstance(exclude, set):
exclude = set(exclude)
if isinstance(obj, BaseModel):
encoder = getattr(obj.Config, "json_encoders", custom_encoder)
return jsonable_encoder(
obj.dict(include=include, exclude=exclude, by_alias=by_alias),
obj.dict(
include=include,
exclude=exclude,
by_alias=by_alias,
skip_defaults=skip_defaults,
),
include_none=include_none,
custom_encoder=encoder,
sqlalchemy_safe=sqlalchemy_safe,
@@ -42,6 +52,7 @@ def jsonable_encoder(
encoded_key = jsonable_encoder(
key,
by_alias=by_alias,
skip_defaults=skip_defaults,
include_none=include_none,
custom_encoder=custom_encoder,
sqlalchemy_safe=sqlalchemy_safe,
@@ -49,6 +60,7 @@ def jsonable_encoder(
encoded_value = jsonable_encoder(
value,
by_alias=by_alias,
skip_defaults=skip_defaults,
include_none=include_none,
custom_encoder=custom_encoder,
sqlalchemy_safe=sqlalchemy_safe,
@@ -64,6 +76,7 @@ def jsonable_encoder(
include=include,
exclude=exclude,
by_alias=by_alias,
skip_defaults=skip_defaults,
include_none=include_none,
custom_encoder=custom_encoder,
sqlalchemy_safe=sqlalchemy_safe,
@@ -91,6 +104,7 @@ def jsonable_encoder(
return jsonable_encoder(
data,
by_alias=by_alias,
skip_defaults=skip_defaults,
include_none=include_none,
custom_encoder=custom_encoder,
sqlalchemy_safe=sqlalchemy_safe,

View File

@@ -0,0 +1,23 @@
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY
async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse:
headers = getattr(exc, "headers", None)
if headers:
return JSONResponse(
{"detail": exc.detail}, status_code=exc.status_code, headers=headers
)
else:
return JSONResponse({"detail": exc.detail}, status_code=exc.status_code)
async def request_validation_exception_handler(
request: Request, exc: RequestValidationError
) -> JSONResponse:
return JSONResponse(
status_code=HTTP_422_UNPROCESSABLE_ENTITY, content={"detail": exc.errors()}
)

View File

@@ -1,3 +1,4 @@
from pydantic import ValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
@@ -7,3 +8,11 @@ class HTTPException(StarletteHTTPException):
) -> None:
super().__init__(status_code=status_code, detail=detail)
self.headers = headers
class RequestValidationError(ValidationError):
pass
class WebSocketRequestValidationError(ValidationError):
pass

View File

@@ -2,7 +2,7 @@ import asyncio
import inspect
import logging
import re
from typing import Any, Callable, Dict, List, Optional, Type, Union
from typing import Any, Callable, Dict, List, Optional, Set, Type, Union
from fastapi import params
from fastapi.dependencies.models import Dependant
@@ -13,6 +13,7 @@ from fastapi.dependencies.utils import (
solve_dependencies,
)
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError
from pydantic import BaseConfig, BaseModel, Schema
from pydantic.error_wrappers import ErrorWrapper, ValidationError
from pydantic.fields import Field
@@ -28,12 +29,27 @@ from starlette.routing import (
request_response,
websocket_session,
)
from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY, WS_1008_POLICY_VIOLATION
from starlette.status import WS_1008_POLICY_VIOLATION
from starlette.websockets import WebSocket
def serialize_response(*, field: Field = None, response: Response) -> Any:
encoded = jsonable_encoder(response)
def serialize_response(
*,
field: Field = None,
response: Response,
include: Set[str] = None,
exclude: Set[str] = set(),
by_alias: bool = True,
skip_defaults: bool = False,
) -> Any:
encoded = jsonable_encoder(
response,
include=include,
exclude=exclude,
by_alias=by_alias,
skip_defaults=skip_defaults,
)
if field:
errors = []
value, errors_ = field.validate(encoded, {}, loc=("response",))
@@ -43,7 +59,13 @@ def serialize_response(*, field: Field = None, response: Response) -> Any:
errors.extend(errors_)
if errors:
raise ValidationError(errors)
return jsonable_encoder(value)
return jsonable_encoder(
value,
include=include,
exclude=exclude,
by_alias=by_alias,
skip_defaults=skip_defaults,
)
else:
return encoded
@@ -54,6 +76,10 @@ def get_app(
status_code: int = 200,
response_class: Type[Response] = JSONResponse,
response_field: Field = None,
response_model_include: Set[str] = None,
response_model_exclude: Set[str] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
) -> Callable:
assert dependant.call is not None, "dependant.call must be a function"
is_coroutine = asyncio.iscoroutinefunction(dependant.call)
@@ -78,10 +104,7 @@ def get_app(
request=request, dependant=dependant, body=body
)
if errors:
errors_out = ValidationError(errors)
raise HTTPException(
status_code=HTTP_422_UNPROCESSABLE_ENTITY, detail=errors_out.errors()
)
raise RequestValidationError(errors)
else:
assert dependant.call is not None, "dependant.call must be a function"
if is_coroutine:
@@ -93,7 +116,12 @@ def get_app(
raw_response.background = background_tasks
return raw_response
response_data = serialize_response(
field=response_field, response=raw_response
field=response_field,
response=raw_response,
include=response_model_include,
exclude=response_model_exclude,
by_alias=response_model_by_alias,
skip_defaults=response_model_skip_defaults,
)
return response_class(
content=response_data,
@@ -111,10 +139,7 @@ def get_websocket_app(dependant: Dependant) -> Callable:
)
if errors:
await websocket.close(code=WS_1008_POLICY_VIOLATION)
errors_out = ValidationError(errors)
raise HTTPException(
status_code=HTTP_422_UNPROCESSABLE_ENTITY, detail=errors_out.errors()
)
raise WebSocketRequestValidationError(errors)
assert dependant.call is not None, "dependant.call must me a function"
await dependant.call(**values)
@@ -139,7 +164,7 @@ class APIRoute(routing.Route):
path: str,
endpoint: Callable,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[params.Depends] = None,
@@ -151,6 +176,10 @@ class APIRoute(routing.Route):
name: str = None,
methods: List[str] = None,
operation_id: str = None,
response_model_include: Set[str] = None,
response_model_exclude: Set[str] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
) -> None:
@@ -210,6 +239,10 @@ class APIRoute(routing.Route):
methods = ["GET"]
self.methods = methods
self.operation_id = operation_id
self.response_model_include = response_model_include
self.response_model_exclude = response_model_exclude
self.response_model_by_alias = response_model_by_alias
self.response_model_skip_defaults = response_model_skip_defaults
self.include_in_schema = include_in_schema
self.response_class = response_class
@@ -217,10 +250,11 @@ class APIRoute(routing.Route):
assert inspect.isfunction(endpoint) or inspect.ismethod(
endpoint
), f"An endpoint must be a function or method"
self.dependant = get_dependant(path=path, call=self.endpoint)
self.dependant = get_dependant(path=self.path_format, call=self.endpoint)
for depends in self.dependencies[::-1]:
self.dependant.dependencies.insert(
0, get_parameterless_sub_dependant(depends=depends, path=path)
0,
get_parameterless_sub_dependant(depends=depends, path=self.path_format),
)
self.body_field = get_body_field(dependant=self.dependant, name=self.name)
self.app = request_response(
@@ -230,6 +264,10 @@ class APIRoute(routing.Route):
status_code=self.status_code,
response_class=self.response_class,
response_field=self.response_field,
response_model_include=self.response_model_include,
response_model_exclude=self.response_model_exclude,
response_model_by_alias=self.response_model_by_alias,
response_model_skip_defaults=self.response_model_skip_defaults,
)
)
@@ -240,7 +278,7 @@ class APIRouter(routing.Router):
path: str,
endpoint: Callable,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[params.Depends] = None,
@@ -251,6 +289,10 @@ class APIRouter(routing.Router):
deprecated: bool = None,
methods: List[str] = None,
operation_id: str = None,
response_model_include: Set[str] = None,
response_model_exclude: Set[str] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@@ -269,6 +311,10 @@ class APIRouter(routing.Router):
deprecated=deprecated,
methods=methods,
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@@ -279,7 +325,7 @@ class APIRouter(routing.Router):
self,
path: str,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[params.Depends] = None,
@@ -290,6 +336,10 @@ class APIRouter(routing.Router):
deprecated: bool = None,
methods: List[str] = None,
operation_id: str = None,
response_model_include: Set[str] = None,
response_model_exclude: Set[str] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@@ -309,6 +359,10 @@ class APIRouter(routing.Router):
deprecated=deprecated,
methods=methods,
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@@ -363,6 +417,10 @@ class APIRouter(routing.Router):
deprecated=route.deprecated,
methods=route.methods,
operation_id=route.operation_id,
response_model_include=route.response_model_include,
response_model_exclude=route.response_model_exclude,
response_model_by_alias=route.response_model_by_alias,
response_model_skip_defaults=route.response_model_skip_defaults,
include_in_schema=route.include_in_schema,
response_class=route.response_class,
name=route.name,
@@ -388,7 +446,7 @@ class APIRouter(routing.Router):
self,
path: str,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[params.Depends] = None,
@@ -398,10 +456,15 @@ class APIRouter(routing.Router):
responses: Dict[Union[int, str], Dict[str, Any]] = None,
deprecated: bool = None,
operation_id: str = None,
response_model_include: Set[str] = None,
response_model_exclude: Set[str] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
) -> Callable:
return self.api_route(
path=path,
response_model=response_model,
@@ -415,6 +478,10 @@ class APIRouter(routing.Router):
deprecated=deprecated,
methods=["GET"],
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@@ -424,7 +491,7 @@ class APIRouter(routing.Router):
self,
path: str,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[params.Depends] = None,
@@ -434,6 +501,10 @@ class APIRouter(routing.Router):
responses: Dict[Union[int, str], Dict[str, Any]] = None,
deprecated: bool = None,
operation_id: str = None,
response_model_include: Set[str] = None,
response_model_exclude: Set[str] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@@ -451,6 +522,10 @@ class APIRouter(routing.Router):
deprecated=deprecated,
methods=["PUT"],
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@@ -460,7 +535,7 @@ class APIRouter(routing.Router):
self,
path: str,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[params.Depends] = None,
@@ -470,6 +545,10 @@ class APIRouter(routing.Router):
responses: Dict[Union[int, str], Dict[str, Any]] = None,
deprecated: bool = None,
operation_id: str = None,
response_model_include: Set[str] = None,
response_model_exclude: Set[str] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@@ -487,6 +566,10 @@ class APIRouter(routing.Router):
deprecated=deprecated,
methods=["POST"],
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@@ -496,7 +579,7 @@ class APIRouter(routing.Router):
self,
path: str,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[params.Depends] = None,
@@ -506,6 +589,10 @@ class APIRouter(routing.Router):
responses: Dict[Union[int, str], Dict[str, Any]] = None,
deprecated: bool = None,
operation_id: str = None,
response_model_include: Set[str] = None,
response_model_exclude: Set[str] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@@ -523,6 +610,10 @@ class APIRouter(routing.Router):
deprecated=deprecated,
methods=["DELETE"],
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@@ -532,7 +623,7 @@ class APIRouter(routing.Router):
self,
path: str,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[params.Depends] = None,
@@ -542,6 +633,10 @@ class APIRouter(routing.Router):
responses: Dict[Union[int, str], Dict[str, Any]] = None,
deprecated: bool = None,
operation_id: str = None,
response_model_include: Set[str] = None,
response_model_exclude: Set[str] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@@ -559,6 +654,10 @@ class APIRouter(routing.Router):
deprecated=deprecated,
methods=["OPTIONS"],
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@@ -568,7 +667,7 @@ class APIRouter(routing.Router):
self,
path: str,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[params.Depends] = None,
@@ -578,6 +677,10 @@ class APIRouter(routing.Router):
responses: Dict[Union[int, str], Dict[str, Any]] = None,
deprecated: bool = None,
operation_id: str = None,
response_model_include: Set[str] = None,
response_model_exclude: Set[str] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@@ -595,6 +698,10 @@ class APIRouter(routing.Router):
deprecated=deprecated,
methods=["HEAD"],
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@@ -604,7 +711,7 @@ class APIRouter(routing.Router):
self,
path: str,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[params.Depends] = None,
@@ -614,6 +721,10 @@ class APIRouter(routing.Router):
responses: Dict[Union[int, str], Dict[str, Any]] = None,
deprecated: bool = None,
operation_id: str = None,
response_model_include: Set[str] = None,
response_model_exclude: Set[str] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@@ -631,6 +742,10 @@ class APIRouter(routing.Router):
deprecated=deprecated,
methods=["PATCH"],
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@@ -640,7 +755,7 @@ class APIRouter(routing.Router):
self,
path: str,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[params.Depends] = None,
@@ -650,6 +765,10 @@ class APIRouter(routing.Router):
responses: Dict[Union[int, str], Dict[str, Any]] = None,
deprecated: bool = None,
operation_id: str = None,
response_model_include: Set[str] = None,
response_model_exclude: Set[str] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@@ -667,6 +786,10 @@ class APIRouter(routing.Router):
deprecated=deprecated,
methods=["TRACE"],
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,

View File

@@ -45,6 +45,7 @@ nav:
- Path Operation Advanced Configuration: 'tutorial/path-operation-advanced-configuration.md'
- Additional Status Codes: 'tutorial/additional-status-codes.md'
- JSON compatible encoder: 'tutorial/encoder.md'
- Body - updates: 'tutorial/body-updates.md'
- Return a Response directly: 'tutorial/response-directly.md'
- Custom Response Class: 'tutorial/custom-response.md'
- Additional Responses in OpenAPI: 'tutorial/additional-responses.md'

View File

@@ -0,0 +1,22 @@
import inspect
from fastapi import APIRouter, FastAPI
method_names = ["get", "put", "post", "delete", "options", "head", "patch", "trace"]
def test_signatures_consistency():
base_sig = inspect.signature(APIRouter.get)
for method_name in method_names:
router_method = getattr(APIRouter, method_name)
app_method = getattr(FastAPI, method_name)
router_sig = inspect.signature(router_method)
app_sig = inspect.signature(app_method)
param: inspect.Parameter
for key, param in base_sig.parameters.items():
router_param: inspect.Parameter = router_sig.parameters[key]
app_param: inspect.Parameter = app_sig.parameters[key]
assert param.annotation == router_param.annotation
assert param.annotation == app_param.annotation
assert param.default == router_param.default
assert param.default == app_param.default

View File

View File

@@ -0,0 +1,162 @@
from starlette.testclient import TestClient
from body_updates.tutorial001 import app
client = TestClient(app)
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/items/{item_id}": {
"get": {
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/Item"}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
"summary": "Read Item",
"operationId": "read_item_items__item_id__get",
"parameters": [
{
"required": True,
"schema": {"title": "Item_Id", "type": "string"},
"name": "item_id",
"in": "path",
}
],
},
"put": {
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/Item"}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
"summary": "Update Item",
"operationId": "update_item_items__item_id__put",
"parameters": [
{
"required": True,
"schema": {"title": "Item_Id", "type": "string"},
"name": "item_id",
"in": "path",
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/Item"}
}
},
"required": True,
},
},
}
},
"components": {
"schemas": {
"Item": {
"title": "Item",
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
"description": {"title": "Description", "type": "string"},
"price": {"title": "Price", "type": "number"},
"tax": {"title": "Tax", "type": "number", "default": 10.5},
"tags": {
"title": "Tags",
"type": "array",
"items": {"type": "string"},
"default": [],
},
},
},
"ValidationError": {
"title": "ValidationError",
"required": ["loc", "msg", "type"],
"type": "object",
"properties": {
"loc": {
"title": "Location",
"type": "array",
"items": {"type": "string"},
},
"msg": {"title": "Message", "type": "string"},
"type": {"title": "Error Type", "type": "string"},
},
},
"HTTPValidationError": {
"title": "HTTPValidationError",
"type": "object",
"properties": {
"detail": {
"title": "Detail",
"type": "array",
"items": {"$ref": "#/components/schemas/ValidationError"},
}
},
},
}
},
}
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_schema
def test_get():
response = client.get("/items/baz")
assert response.status_code == 200
assert response.json() == {
"name": "Baz",
"description": None,
"price": 50.2,
"tax": 10.5,
"tags": [],
}
def test_put():
response = client.put(
"/items/bar", json={"name": "Barz", "price": 3, "description": None}
)
assert response.json() == {
"name": "Barz",
"description": None,
"price": 3,
"tax": 10.5,
"tags": [],
}

View File

@@ -0,0 +1,91 @@
from starlette.testclient import TestClient
from handling_errors.tutorial003 import app
client = TestClient(app)
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/unicorns/{name}": {
"get": {
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
"summary": "Read Unicorn",
"operationId": "read_unicorn_unicorns__name__get",
"parameters": [
{
"required": True,
"schema": {"title": "Name", "type": "string"},
"name": "name",
"in": "path",
}
],
}
}
},
"components": {
"schemas": {
"ValidationError": {
"title": "ValidationError",
"required": ["loc", "msg", "type"],
"type": "object",
"properties": {
"loc": {
"title": "Location",
"type": "array",
"items": {"type": "string"},
},
"msg": {"title": "Message", "type": "string"},
"type": {"title": "Error Type", "type": "string"},
},
},
"HTTPValidationError": {
"title": "HTTPValidationError",
"type": "object",
"properties": {
"detail": {
"title": "Detail",
"type": "array",
"items": {"$ref": "#/components/schemas/ValidationError"},
}
},
},
}
},
}
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_schema
def test_get():
response = client.get("/unicorns/shinny")
assert response.status_code == 200
assert response.json() == {"unicorn_name": "shinny"}
def test_get_exception():
response = client.get("/unicorns/yolo")
assert response.status_code == 418
assert response.json() == {
"message": "Oops! yolo did something. There goes a rainbow..."
}

View File

@@ -0,0 +1,100 @@
from starlette.testclient import TestClient
from handling_errors.tutorial004 import app
client = TestClient(app)
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/items/{item_id}": {
"get": {
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
"summary": "Read Item",
"operationId": "read_item_items__item_id__get",
"parameters": [
{
"required": True,
"schema": {"title": "Item_Id", "type": "integer"},
"name": "item_id",
"in": "path",
}
],
}
}
},
"components": {
"schemas": {
"ValidationError": {
"title": "ValidationError",
"required": ["loc", "msg", "type"],
"type": "object",
"properties": {
"loc": {
"title": "Location",
"type": "array",
"items": {"type": "string"},
},
"msg": {"title": "Message", "type": "string"},
"type": {"title": "Error Type", "type": "string"},
},
},
"HTTPValidationError": {
"title": "HTTPValidationError",
"type": "object",
"properties": {
"detail": {
"title": "Detail",
"type": "array",
"items": {"$ref": "#/components/schemas/ValidationError"},
}
},
},
}
},
}
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_schema
def test_get_validation_error():
response = client.get("/items/foo")
assert response.status_code == 400
validation_error_str_lines = [
b"1 validation error",
b"path -> item_id",
b" value is not a valid integer (type=type_error.integer)",
]
assert response.content == b"\n".join(validation_error_str_lines)
def test_get_http_error():
response = client.get("/items/3")
assert response.status_code == 418
assert response.content == b"Nope! I don't like 3."
def test_get():
response = client.get("/items/2")
assert response.status_code == 200
assert response.json() == {"item_id": 2}

View File

@@ -0,0 +1,103 @@
from starlette.testclient import TestClient
from handling_errors.tutorial005 import app
client = TestClient(app)
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/items/{item_id}": {
"get": {
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
"summary": "Read Item",
"operationId": "read_item_items__item_id__get",
"parameters": [
{
"required": True,
"schema": {"title": "Item_Id", "type": "integer"},
"name": "item_id",
"in": "path",
}
],
}
}
},
"components": {
"schemas": {
"ValidationError": {
"title": "ValidationError",
"required": ["loc", "msg", "type"],
"type": "object",
"properties": {
"loc": {
"title": "Location",
"type": "array",
"items": {"type": "string"},
},
"msg": {"title": "Message", "type": "string"},
"type": {"title": "Error Type", "type": "string"},
},
},
"HTTPValidationError": {
"title": "HTTPValidationError",
"type": "object",
"properties": {
"detail": {
"title": "Detail",
"type": "array",
"items": {"$ref": "#/components/schemas/ValidationError"},
}
},
},
}
},
}
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_schema
def test_get_validation_error():
response = client.get("/items/foo")
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"loc": ["path", "item_id"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
}
]
}
def test_get_http_error():
response = client.get("/items/3")
assert response.status_code == 418
assert response.json() == {"detail": "Nope! I don't like 3."}
def test_get():
response = client.get("/items/2")
assert response.status_code == 200
assert response.json() == {"item_id": 2}

View File

View File

@@ -0,0 +1,91 @@
from starlette.testclient import TestClient
from path_params.tutorial004 import app
client = TestClient(app)
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/files/{file_path}": {
"get": {
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
"summary": "Read User Me",
"operationId": "read_user_me_files__file_path__get",
"parameters": [
{
"required": True,
"schema": {"title": "File_Path", "type": "string"},
"name": "file_path",
"in": "path",
}
],
}
}
},
"components": {
"schemas": {
"ValidationError": {
"title": "ValidationError",
"required": ["loc", "msg", "type"],
"type": "object",
"properties": {
"loc": {
"title": "Location",
"type": "array",
"items": {"type": "string"},
},
"msg": {"title": "Message", "type": "string"},
"type": {"title": "Error Type", "type": "string"},
},
},
"HTTPValidationError": {
"title": "HTTPValidationError",
"type": "object",
"properties": {
"detail": {
"title": "Detail",
"type": "array",
"items": {"$ref": "#/components/schemas/ValidationError"},
}
},
},
}
},
}
def test_openapi():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_schema
def test_file_path():
response = client.get("/files/home/johndoe/myfile.txt")
print(response.content)
assert response.status_code == 200
assert response.json() == {"file_path": "home/johndoe/myfile.txt"}
def test_root_file_path():
response = client.get("/files//home/johndoe/myfile.txt")
print(response.content)
assert response.status_code == 200
assert response.json() == {"file_path": "/home/johndoe/myfile.txt"}

View File

@@ -0,0 +1,125 @@
import pytest
from starlette.testclient import TestClient
from response_model.tutorial004 import app
client = TestClient(app)
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/items/{item_id}": {
"get": {
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/Item"}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
"summary": "Read Item",
"operationId": "read_item_items__item_id__get",
"parameters": [
{
"required": True,
"schema": {"title": "Item_Id", "type": "string"},
"name": "item_id",
"in": "path",
}
],
}
}
},
"components": {
"schemas": {
"Item": {
"title": "Item",
"required": ["name", "price"],
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
"price": {"title": "Price", "type": "number"},
"description": {"title": "Description", "type": "string"},
"tax": {"title": "Tax", "type": "number", "default": 10.5},
"tags": {
"title": "Tags",
"type": "array",
"items": {"type": "string"},
"default": [],
},
},
},
"ValidationError": {
"title": "ValidationError",
"required": ["loc", "msg", "type"],
"type": "object",
"properties": {
"loc": {
"title": "Location",
"type": "array",
"items": {"type": "string"},
},
"msg": {"title": "Message", "type": "string"},
"type": {"title": "Error Type", "type": "string"},
},
},
"HTTPValidationError": {
"title": "HTTPValidationError",
"type": "object",
"properties": {
"detail": {
"title": "Detail",
"type": "array",
"items": {"$ref": "#/components/schemas/ValidationError"},
}
},
},
}
},
}
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_schema
@pytest.mark.parametrize(
"url,data",
[
("/items/foo", {"name": "Foo", "price": 50.2}),
(
"/items/bar",
{"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2},
),
(
"/items/baz",
{
"name": "Baz",
"description": None,
"price": 50.2,
"tax": 10.5,
"tags": [],
},
),
],
)
def test_get(url, data):
response = client.get(url)
assert response.status_code == 200
assert response.json() == data

View File

@@ -0,0 +1,142 @@
from starlette.testclient import TestClient
from response_model.tutorial005 import app
client = TestClient(app)
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/items/{item_id}/name": {
"get": {
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/Item"}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
"summary": "Read Item Name",
"operationId": "read_item_name_items__item_id__name_get",
"parameters": [
{
"required": True,
"schema": {"title": "Item_Id", "type": "string"},
"name": "item_id",
"in": "path",
}
],
}
},
"/items/{item_id}/public": {
"get": {
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/Item"}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
"summary": "Read Item Public Data",
"operationId": "read_item_public_data_items__item_id__public_get",
"parameters": [
{
"required": True,
"schema": {"title": "Item_Id", "type": "string"},
"name": "item_id",
"in": "path",
}
],
}
},
},
"components": {
"schemas": {
"Item": {
"title": "Item",
"required": ["name", "price"],
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
"price": {"title": "Price", "type": "number"},
"description": {"title": "Description", "type": "string"},
"tax": {"title": "Tax", "type": "number", "default": 10.5},
},
},
"ValidationError": {
"title": "ValidationError",
"required": ["loc", "msg", "type"],
"type": "object",
"properties": {
"loc": {
"title": "Location",
"type": "array",
"items": {"type": "string"},
},
"msg": {"title": "Message", "type": "string"},
"type": {"title": "Error Type", "type": "string"},
},
},
"HTTPValidationError": {
"title": "HTTPValidationError",
"type": "object",
"properties": {
"detail": {
"title": "Detail",
"type": "array",
"items": {"$ref": "#/components/schemas/ValidationError"},
}
},
},
}
},
}
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_schema
def test_read_item_name():
response = client.get("/items/bar/name")
assert response.status_code == 200
assert response.json() == {"name": "Bar", "description": "The Bar fighters"}
def test_read_item_public_data():
response = client.get("/items/bar/public")
assert response.status_code == 200
assert response.json() == {
"name": "Bar",
"description": "The Bar fighters",
"price": 62,
}

View File

@@ -0,0 +1,142 @@
from starlette.testclient import TestClient
from response_model.tutorial006 import app
client = TestClient(app)
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/items/{item_id}/name": {
"get": {
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/Item"}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
"summary": "Read Item Name",
"operationId": "read_item_name_items__item_id__name_get",
"parameters": [
{
"required": True,
"schema": {"title": "Item_Id", "type": "string"},
"name": "item_id",
"in": "path",
}
],
}
},
"/items/{item_id}/public": {
"get": {
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/Item"}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
"summary": "Read Item Public Data",
"operationId": "read_item_public_data_items__item_id__public_get",
"parameters": [
{
"required": True,
"schema": {"title": "Item_Id", "type": "string"},
"name": "item_id",
"in": "path",
}
],
}
},
},
"components": {
"schemas": {
"Item": {
"title": "Item",
"required": ["name", "price"],
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
"price": {"title": "Price", "type": "number"},
"description": {"title": "Description", "type": "string"},
"tax": {"title": "Tax", "type": "number", "default": 10.5},
},
},
"ValidationError": {
"title": "ValidationError",
"required": ["loc", "msg", "type"],
"type": "object",
"properties": {
"loc": {
"title": "Location",
"type": "array",
"items": {"type": "string"},
},
"msg": {"title": "Message", "type": "string"},
"type": {"title": "Error Type", "type": "string"},
},
},
"HTTPValidationError": {
"title": "HTTPValidationError",
"type": "object",
"properties": {
"detail": {
"title": "Detail",
"type": "array",
"items": {"$ref": "#/components/schemas/ValidationError"},
}
},
},
}
},
}
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_schema
def test_read_item_name():
response = client.get("/items/bar/name")
assert response.status_code == 200
assert response.json() == {"name": "Bar", "description": "The Bar fighters"}
def test_read_item_public_data():
response = client.get("/items/bar/public")
assert response.status_code == 200
assert response.json() == {
"name": "Bar",
"description": "The Bar fighters",
"price": 62,
}