Compare commits

...

11 Commits

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

*  Add tutorial source examples and tests

* 📝 Add docs for custom exception handlers

* 📝 Update docs section titles
2019-05-29 16:27:55 +04:00
Sebastián Ramírez 15da01af5c 📝 Update release notes 2019-05-29 13:46:27 +04:00
William Hayes d544bdf092 📝 Update docs for paths in path params (#256) 2019-05-29 13:43:41 +04:00
Sebastián Ramírez 703ade7967 🐛 Fix path in path parameters (#272) 2019-05-29 13:34:46 +04:00
Sebastián Ramírez 58f135ba2f 📝 Update link in release notes 2019-05-29 11:51:43 +04:00
Sebastián Ramírez 713d374484 📝 Update release notes 2019-05-29 11:47:46 +04:00
Sebastián Ramírez 24e9ea28d3 Update testing docs, examples for testing POST, headers (#271) 2019-05-29 11:47:21 +04:00
Sebastián Ramírez cae53138b2 📝 Update release notes 2019-05-27 21:56:49 +04:00
Sebastián Ramírez a49d45eaa9 🐛 Fix response_model type to allow List[Model] (#266) 2019-05-27 21:56:20 +04:00
20 changed files with 862 additions and 66 deletions
+21
View File
@@ -1,5 +1,26 @@
## 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`.
+36
View File
@@ -0,0 +1,36 @@
from fastapi import FastAPI, Header, HTTPException
from pydantic import BaseModel
fake_secret_token = "coneofsilence"
fake_db = {
"foo": {"id": "foo", "title": "Foo", "description": "There goes my hero"},
"bar": {"id": "bar", "title": "Bar", "description": "The bartenders"},
}
app = FastAPI()
class Item(BaseModel):
id: str
title: str
description: str = None
@app.get("/items/{item_id}", response_model=Item)
async def read_main(item_id: str, x_token: str = Header(...)):
if x_token != fake_secret_token:
raise HTTPException(status_code=400, detail="Invalid X-Token header")
if item_id not in fake_db:
raise HTTPException(status_code=404, detail="Item not found")
return fake_db[item_id]
@app.post("/items/", response_model=Item)
async def create_item(item: Item, x_token: str = Header(...)):
if x_token != fake_secret_token:
raise HTTPException(status_code=400, detail="Invalid X-Token header")
if item.id in fake_db:
raise HTTPException(status_code=400, detail="Item already exists")
fake_db[item.id] = item
return item
+65
View File
@@ -0,0 +1,65 @@
from starlette.testclient import TestClient
from .main_b import app
client = TestClient(app)
def test_read_item():
response = client.get("/items/foo", headers={"X-Token": "coneofsilence"})
assert response.status_code == 200
assert response.json() == {
"id": "foo",
"title": "Foo",
"description": "There goes my hero",
}
def test_read_item_bad_token():
response = client.get("/items/foo", headers={"X-Token": "hailhydra"})
assert response.status_code == 400
assert response.json() == {"detail": "Invalid X-Token header"}
def test_read_inexistent_item():
response = client.get("/items/baz", headers={"X-Token": "coneofsilence"})
assert response.status_code == 404
assert response.json() == {"detail": "Item not found"}
def test_create_item():
response = client.post(
"/items/",
headers={"X-Token": "coneofsilence"},
json={"id": "foobar", "title": "Foo Bar", "description": "The Foo Barters"},
)
assert response.status_code == 200
assert response.json() == {
"id": "foobar",
"title": "Foo Bar",
"description": "The Foo Barters",
}
def test_create_item_bad_token():
response = client.post(
"/items/",
headers={"X-Token": "hailhydra"},
json={"id": "bazz", "title": "Bazz", "description": "Drop the bazz"},
)
assert response.status_code == 400
assert response.json() == {"detail": "Invalid X-Token header"}
def test_create_existing_token():
response = client.post(
"/items/",
headers={"X-Token": "coneofsilence"},
json={
"id": "foo",
"title": "The Foo ID Stealers",
"description": "There goes my stealer",
},
)
assert response.status_code == 400
assert response.json() == {"detail": "Item already exists"}
+19 -8
View File
@@ -1,15 +1,26 @@
from fastapi import FastAPI
from starlette.exceptions import HTTPException
from starlette.responses import PlainTextResponse
from starlette.requests import Request
from starlette.responses import JSONResponse
class UnicornException(Exception):
def __init__(self, name: str):
self.name = name
app = FastAPI()
@app.exception_handler(HTTPException)
async def http_exception(request, exc):
return PlainTextResponse(str(exc.detail), status_code=exc.status_code)
@app.exception_handler(UnicornException)
async def unicorn_exception_handler(request: Request, exc: UnicornException):
return JSONResponse(
status_code=418,
content={"message": f"Oops! {exc.name} did something. There goes a rainbow..."},
)
@app.get("/")
async def root():
return {"message": "Hello World"}
@app.get("/unicorns/{name}")
async def read_unicorn(name: str):
if name == "yolo":
raise UnicornException(name=name)
return {"unicorn_name": name}
+23
View File
@@ -0,0 +1,23 @@
from fastapi import FastAPI, HTTPException
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.responses import PlainTextResponse
app = FastAPI()
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request, exc):
return PlainTextResponse(str(exc.detail), status_code=exc.status_code)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
return PlainTextResponse(str(exc), status_code=400)
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id == 3:
raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
return {"item_id": item_id}
+28
View File
@@ -0,0 +1,28 @@
from fastapi import FastAPI, HTTPException
from fastapi.exception_handlers import (
http_exception_handler,
request_validation_exception_handler,
)
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
app = FastAPI()
@app.exception_handler(StarletteHTTPException)
async def custom_http_exception_handler(request, exc):
print(f"OMG! An HTTP error!: {exc}")
return await http_exception_handler(request, exc)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
print(f"OMG! The client sent invalid data!: {exc}")
return await request_validation_exception_handler(request, exc)
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id == 3:
raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
return {"item_id": item_id}
+8
View File
@@ -0,0 +1,8 @@
from fastapi import FastAPI
app = FastAPI()
@app.get("/files/{file_path:path}")
async def read_user_me(file_path: str):
return {"file_path": file_path}
+124 -10
View File
@@ -68,7 +68,7 @@ But if the client requests `http://example.com/items/bar` (a non-existent `item_
They are handled automatically by **FastAPI** and converted to JSON.
### Adding custom headers
## Add custom headers
There are some situations in where it's useful to be able to add custom headers to the HTTP error. For example, for some types of security.
@@ -76,24 +76,138 @@ You probably won't need to use it directly in your code.
But in case you needed it for an advanced scenario, you can add custom headers:
```Python hl_lines="14"
{!./src/handling_errors/tutorial002.py!}
```
### Installing custom handlers
## Install custom exception handlers
If you need to add other custom exception handlers, or override the default one (that sends the errors as JSON), you can use <a href="https://www.starlette.io/exceptions/" target="_blank">the same exception utilities from Starlette</a>.
You can add custom exception handlers with <a href="https://www.starlette.io/exceptions/" target="_blank">the same exception utilities from Starlette</a>.
For example, you could override the default exception handler with:
Let's say you have a custom exception `UnicornException` that you (or a library you use) might `raise`.
```Python hl_lines="2 3 8 9 10"
And you want to handle this exception globally with FastAPI.
You could add a custom exception handler with `@app.exception_handler()`:
```Python hl_lines="6 7 8 14 15 16 17 18 24"
{!./src/handling_errors/tutorial003.py!}
```
...this would make it return "plain text" responses with the errors, instead of JSON responses.
Here, if you request `/unicorns/yolo`, the *path operation* will `raise` a `UnicornException`.
!!! info
Note that in this example we set the exception handler with Starlette's `HTTPException` instead of FastAPI's `HTTPException`.
But it will be handled by the `unicorn_exception_handler`.
This would ensure that if you use a plug-in or any other third-party tool that raises Starlette's `HTTPException` directly, it will be caught by your exception handler.
So, you will receive a clean error, with an HTTP status code of `418` and a JSON content of:
```JSON
{"message": "Oops! yolo did something. There goes a rainbow..."}
```
## Override the default exception handlers
**FastAPI** has some default exception handlers.
These handlers are in charge or returning the default JSON responses when you `raise` an `HTTPException` and when the request has invalid data.
You can override these exception handlers with your own.
### Override request validation exceptions
When a request contains invalid data, **FastAPI** internally raises a `RequestValidationError`.
And it also includes a default exception handler for it.
To override it, import the `RequestValidationError` and use it with `@app.exception_handler(RequestValidationError)` to decorate the exception handler.
The exception handler will receive a `Request` and the exception.
```Python hl_lines="2 14 15 16"
{!./src/handling_errors/tutorial004.py!}
```
Now, if you go to `/items/foo`, instead of getting the default JSON error with:
```JSON
{
"detail": [
{
"loc": [
"path",
"item_id"
],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
]
}
```
you will get a text version, with:
```
1 validation error
path -> item_id
value is not a valid integer (type=type_error.integer)
```
#### `RequestValidationError` vs `ValidationError`
!!! warning
These are technical details that you might skip if it's not important for you now.
`RequestValidationError` is a sub-class of Pydantic's <a href="https://pydantic-docs.helpmanual.io/#error-handling" target="_blank">`ValidationError`</a>.
**FastAPI** uses it so that, if you use a Pydantic model in `response_model`, and your data has an error, you will see the error in your log.
But the client/user will not see it. Instead, the client will receive an "Internal Server Error" with a HTTP status code `500`.
It should be this way because if you have a Pydantic `ValidationError` in your *response* or anywhere in your code (not in the client's *request*), it's actually a bug in your code.
And while you fix it, your clients/users shouldn't have access to internal information about the error, as that could expose a security vulnerability.
### Override the `HTTPException` error handler
The same way, you can override the `HTTPException` handler.
For example, you could want to return a plain text response instead of JSON for these errors:
```Python hl_lines="1 3 9 10 11 22"
{!./src/handling_errors/tutorial004.py!}
```
#### FastAPI's `HTTPException` vs Starlette's `HTTPException`
**FastAPI** has its own `HTTPException`.
And **FastAPI**'s `HTTPException` error class inherits from Starlette's `HTTPException` error class.
The only difference, is that **FastAPI**'s `HTTPException` allows you to add headers to be included in the response.
This is needed/used internally for OAuth 2.0 and some security utilities.
So, you can keep raising **FastAPI**'s `HTTPException` as normally in your code.
But when you register an exception handler, you should register it for Starlette's `HTTPException`.
This way, if any part of Starlette's internal code, or a Starlette extension or plug-in, raises an `HTTPException`, your handler will be able to catch handle it.
In this example, to be able to have both `HTTPException`s in the same code, Starlette's exceptions is renamed to `StarletteHTTPException`:
```Python
from starlette.exceptions import HTTPException as StarletteHTTPException
```
### Re-use **FastAPI**'s exception handlers
You could also just want to use the exception somehow, but then use the same default exception handlers from **FastAPI**.
You can import and re-use the default exception handlers from `fastapi.exception_handlers`:
```Python hl_lines="2 3 4 5 15 21"
{!./src/handling_errors/tutorial005.py!}
```
In this example, you are just `print`ing the error with a very expressive notification.
But you get the idea, you can use the exception and then just re-use the default exception handlers.
+37 -1
View File
@@ -115,6 +115,42 @@ Because path operations are evaluated in order, you need to make sure that the p
Otherwise, the path for `/users/{user_id}` would match also for `/users/me`, "thinking" that it's receiving a parameter `user_id` with a value of `"me"`.
## Path parameters containing paths
Let's say you have a *path operation* with a path `/files/{file_path}`.
But you need `file_path` itself to contain a *path*, like `home/johndoe/myfile.txt`.
So, the URL for that file would be something like: `/files/home/johndoe/myfile.txt`.
### OpenAPI support
OpenAPI doesn't support a way to declare a *path parameter* to contain a *path* inside, as that could lead to scenarios that are difficult to test and define.
Nevertheless, you can still do it in **FastAPI**, using one of the internal tools from Starlette.
And the docs would still work, although not adding any documentation telling that the parameter should contain a path.
### Path convertor
Using an option directly from Starlette you can declare a *path parameter* containing a *path* using a URL like:
```
/files/{file_path:path}
```
In this case, the name of the parameter is `file_path`, and the last part, `:path`, tells it that the parameter should match any *path*.
So, you can use it with:
```Python hl_lines="6"
{!./src/path_params/tutorial004.py!}
```
!!! tip
You could need the parameter to contain `/home/johndoe/myfile.txt`, with a leading slash (`/`).
In that case, the URL would be: `/files//home/johndoe/myfile.txt`, with a double slash (`//`) between `files` and `home`.
## Recap
@@ -127,4 +163,4 @@ With **FastAPI**, by using short, intuitive and standard Python type declaration
And you only have to declare them once.
That's probably the main visible advantage of **FastAPI** compared to alternative frameworks (apart from the raw performance).
That's probably the main visible advantage of **FastAPI** compared to alternative frameworks (apart from the raw performance).
+46 -2
View File
@@ -22,12 +22,11 @@ Write simple `assert` statements with the standard Python expressions that you n
!!! tip
Notice that the testing functions are normal `def`, not `async def`.
And the calls to the client are also normal calls, not using `await`.
This allows you to use `pytest` directly without complications.
## Separating tests
In a real application, you probably would have your tests in a different file.
@@ -50,6 +49,51 @@ Then you could have a file `test_main.py` with your tests, and import your `app`
{!./src/app_testing/test_main.py!}
```
## Testing: extended example
Now let's extend this example and add more details to see how to test different parts.
### Extended **FastAPI** app file
Let's say you have a file `main_b.py` with your **FastAPI** app.
It has a `GET` operation that could return an error.
It has a `POST` operation that could return several errors.
Both *path operations* require an `X-Token` header.
```Python
{!./src/app_testing/main_b.py!}
```
### Extended testing file
You could then have a `test_main_b.py`, the same as before, with the extended tests:
```Python
{!./src/app_testing/test_main_b.py!}
```
Whenever you need the client to pass information in the request and you don't know how to, you can search (Google) how to do it in `requests`.
Then you just do the same in your tests.
E.g.:
* To pass a *path* or *query* parameter, add it to the URL itself.
* To pass a JSON body, pass a Python object (e.g. a `dict`) to the parameter `json`.
* If you need to send *Form Data* instead of JSON, use the `data` parameter instead.
* To pass *headers*, use a `dict` in the `headers` parameter.
* For *cookies*, a `dict` in the `cookies` parameter.
For more information about how to pass data to the backend (using `requests` or the `TestClient`) check the <a href="http://docs.python-requests.org" target="_blank">Requests documentation</a>.
!!! info
Note that the `TestClient` receives data that can be converted to JSON, not Pydantic models.
If you have a Pydantic model in your test and you want to send its data to the application during testing, you can use the <a href="https://fastapi.tiangolo.com/tutorial/encoder/" target="_blank">JSON compatible encoder: `jsonable_encoder`</a>.
## Testing WebSockets
You can use the same `TestClient` to test WebSockets.
+1 -1
View File
@@ -1,6 +1,6 @@
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
__version__ = "0.25.0"
__version__ = "0.26.0"
from starlette.background import BackgroundTasks
+19 -22
View File
@@ -1,6 +1,11 @@
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,
@@ -173,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,
@@ -252,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,
@@ -295,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,
@@ -338,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,
@@ -381,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,
@@ -424,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,
@@ -467,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,
@@ -510,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,
@@ -553,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,
+23
View File
@@ -0,0 +1,23 @@
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY
async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse:
headers = getattr(exc, "headers", None)
if headers:
return JSONResponse(
{"detail": exc.detail}, status_code=exc.status_code, headers=headers
)
else:
return JSONResponse({"detail": exc.detail}, status_code=exc.status_code)
async def request_validation_exception_handler(
request: Request, exc: RequestValidationError
) -> JSONResponse:
return JSONResponse(
status_code=HTTP_422_UNPROCESSABLE_ENTITY, content={"detail": exc.errors()}
)
+9
View File
@@ -1,3 +1,4 @@
from pydantic import ValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
@@ -7,3 +8,11 @@ class HTTPException(StarletteHTTPException):
) -> None:
super().__init__(status_code=status_code, detail=detail)
self.headers = headers
class RequestValidationError(ValidationError):
pass
class WebSocketRequestValidationError(ValidationError):
pass
+18 -22
View File
@@ -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,7 +29,7 @@ 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
@@ -103,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:
@@ -141,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)
@@ -169,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,
@@ -255,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(
@@ -282,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,
@@ -329,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,
@@ -450,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,
@@ -495,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,
@@ -539,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,
@@ -583,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,
@@ -627,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,
@@ -671,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,
@@ -715,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,
@@ -759,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,
@@ -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..."
}
@@ -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}
@@ -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,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"}