mirror of
https://github.com/fastapi/fastapi.git
synced 2025-12-24 06:39:31 -05:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83b1a117cc | ||
|
|
2a1ff213a0 | ||
|
|
62af6e0eeb | ||
|
|
15da01af5c | ||
|
|
d544bdf092 | ||
|
|
703ade7967 | ||
|
|
58f135ba2f | ||
|
|
713d374484 | ||
|
|
24e9ea28d3 | ||
|
|
cae53138b2 | ||
|
|
a49d45eaa9 | ||
|
|
3986f79029 | ||
|
|
7379fde5ee | ||
|
|
7b63bc5551 | ||
|
|
747ae8210f | ||
|
|
c651416e05 | ||
|
|
814f95e2bf | ||
|
|
d8716f94ae |
1
CONTRIBUTING.md
Normal file
1
CONTRIBUTING.md
Normal file
@@ -0,0 +1 @@
|
||||
Please read the [Development - Contributing](https://fastapi.tiangolo.com/contributing/) guidelines in the documentation site.
|
||||
@@ -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.
|
||||
|
||||
36
docs/src/app_testing/main_b.py
Normal file
36
docs/src/app_testing/main_b.py
Normal 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
|
||||
65
docs/src/app_testing/test_main_b.py
Normal file
65
docs/src/app_testing/test_main_b.py
Normal 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"}
|
||||
34
docs/src/body_updates/tutorial001.py
Normal file
34
docs/src/body_updates/tutorial001.py
Normal 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
|
||||
37
docs/src/body_updates/tutorial002.py
Normal file
37
docs/src/body_updates/tutorial002.py
Normal 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
|
||||
@@ -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}
|
||||
|
||||
23
docs/src/handling_errors/tutorial004.py
Normal file
23
docs/src/handling_errors/tutorial004.py
Normal 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}
|
||||
28
docs/src/handling_errors/tutorial005.py
Normal file
28
docs/src/handling_errors/tutorial005.py
Normal 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}
|
||||
8
docs/src/path_params/tutorial004.py
Normal file
8
docs/src/path_params/tutorial004.py
Normal 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}
|
||||
@@ -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
|
||||
|
||||
26
docs/src/response_model/tutorial004.py
Normal file
26
docs/src/response_model/tutorial004.py
Normal 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]
|
||||
37
docs/src/response_model/tutorial005.py
Normal file
37
docs/src/response_model/tutorial005.py
Normal 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]
|
||||
37
docs/src/response_model/tutorial006.py
Normal file
37
docs/src/response_model/tutorial006.py
Normal 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]
|
||||
97
docs/tutorial/body-updates.md
Normal file
97
docs/tutorial/body-updates.md
Normal 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>.
|
||||
@@ -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.
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
23
fastapi/exception_handlers.py
Normal file
23
fastapi/exception_handlers.py
Normal 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()}
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
22
tests/test_operations_signatures.py
Normal file
22
tests/test_operations_signatures.py
Normal 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
|
||||
0
tests/test_tutorial/test_body_updates/__init__.py
Normal file
0
tests/test_tutorial/test_body_updates/__init__.py
Normal file
162
tests/test_tutorial/test_body_updates/test_tutorial001.py
Normal file
162
tests/test_tutorial/test_body_updates/test_tutorial001.py
Normal 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": [],
|
||||
}
|
||||
91
tests/test_tutorial/test_handling_errors/test_tutorial003.py
Normal file
91
tests/test_tutorial/test_handling_errors/test_tutorial003.py
Normal 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..."
|
||||
}
|
||||
100
tests/test_tutorial/test_handling_errors/test_tutorial004.py
Normal file
100
tests/test_tutorial/test_handling_errors/test_tutorial004.py
Normal 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}
|
||||
103
tests/test_tutorial/test_handling_errors/test_tutorial005.py
Normal file
103
tests/test_tutorial/test_handling_errors/test_tutorial005.py
Normal 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}
|
||||
0
tests/test_tutorial/test_path_params/__init__.py
Normal file
0
tests/test_tutorial/test_path_params/__init__.py
Normal file
91
tests/test_tutorial/test_path_params/test_tutorial004.py
Normal file
91
tests/test_tutorial/test_path_params/test_tutorial004.py
Normal 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"}
|
||||
125
tests/test_tutorial/test_response_model/test_tutorial004.py
Normal file
125
tests/test_tutorial/test_response_model/test_tutorial004.py
Normal 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
|
||||
142
tests/test_tutorial/test_response_model/test_tutorial005.py
Normal file
142
tests/test_tutorial/test_response_model/test_tutorial005.py
Normal 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,
|
||||
}
|
||||
142
tests/test_tutorial/test_response_model/test_tutorial006.py
Normal file
142
tests/test_tutorial/test_response_model/test_tutorial006.py
Normal 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,
|
||||
}
|
||||
Reference in New Issue
Block a user