Compare commits

...

72 Commits

Author SHA1 Message Date
Sebastián Ramírez
5592fa0f6f 🔖 Release version 0.41.0 2019-10-07 06:44:07 -05:00
Sebastián Ramírez
b65be5d496 📝 Update release notes 2019-10-05 13:19:10 -05:00
Sebastián Ramírez
6c7da43e51 ⬆️ Upgrade Starlette to 0.12.9 and add State (#593) 2019-10-05 13:17:15 -05:00
Sebastián Ramírez
dfec2d7644 📝 Update release notes 2019-10-04 20:21:53 -05:00
dmontagu
8c3ef76139 Add better support for request body access/manipulation with custom classes (#589) 2019-10-04 19:23:34 -05:00
Sebastián Ramírez
7a504a721c 📝 Update release notes 2019-10-04 16:36:54 -05:00
dmontagu
dd963511d6 🐛 Fix preserving route_class when calling include_router (#538) 2019-10-04 16:35:20 -05:00
Sebastián Ramírez
fdb6d43e10 🔖 Release 0.40.0 2019-10-04 15:38:03 -05:00
Sebastián Ramírez
a7c718e968 📝 Update release notes 2019-10-04 15:35:09 -05:00
sliptonic
f4d753620b 📝 Add notes about installing python-multipart for forms (#574) 2019-10-04 15:33:42 -05:00
Sebastián Ramírez
fadfe4c586 📝 Update release notes 2019-10-04 15:11:04 -05:00
dmontagu
5fd83c5fa4 Sort schemas alphabetically (#554)
Modify openapi spec generation to include schemas in alphabetical order.
2019-10-04 15:08:41 -05:00
Sebastián Ramírez
14daaf409f 📝 Update release notes 2019-10-04 15:07:22 -05:00
svalouch
c7dc26b760 Allow docstrings to be truncated before being used for OpenAPI (#556) 2019-10-04 15:02:40 -05:00
Sebastián Ramírez
f5ccb3c35d 📝 Update release notes 2019-10-03 19:37:23 -05:00
Trim21
4cea311e6e 🐛 Fix doctype in docs (#537) 2019-10-03 19:35:44 -05:00
Sebastián Ramírez
f8718072a0 📝 Update release notes 2019-10-03 19:10:34 -05:00
tsouvarev
3dbbecdd16 🐛 Fix setting 4XX overriding default 422 validation errors(#517) 2019-10-03 19:08:29 -05:00
Sebastián Ramírez
6d5530ec1c 📝 Update release notes 2019-10-03 19:04:41 -05:00
prostomarkeloff
0761f11d1a ✏️ Fix typo in HTTP Basic auth tutorial (#514) 2019-10-03 19:01:41 -05:00
Sebastián Ramírez
f2e7ef7056 📝 Update release notes 2019-10-03 19:00:13 -05:00
Fedor Ignatov
d5d9a20937 📝 Fix incorrect example in docs - first steps (#511) 2019-10-03 18:57:49 -05:00
Sebastián Ramírez
96f092179f 📝 Update release notes 2019-10-03 18:43:15 -05:00
Zamir Amir
8505b716af Add support for setting Swagger UI initOAuth configs (clientId, appName) (#499) 2019-10-03 18:41:04 -05:00
Sebastián Ramírez
78272ac1f3 🔖 Release 0.39.0 2019-09-29 17:17:44 -05:00
Sebastián Ramírez
f1bee9a271 📝 Update release notes 2019-09-29 17:09:37 -05:00
jonathanunderwood
b20b2218cd Allow defaults in path parameters (and don't use them) (#450) (#464)
This allows using parameters that can have defaults (e.g. `None`) that can be used as query parameters.

But can also be used in routers with that include those parameters as part of the path.
2019-09-29 17:03:16 -05:00
Sebastián Ramírez
b9cf69cd42 📝 Update release notes 2019-09-29 16:50:00 -05:00
toppk
f803c77515 Add support for specifying a default_response_class (#467) 2019-09-29 16:47:35 -05:00
Sebastián Ramírez
0c67022048 📝 Update release notes 2019-09-29 16:24:52 -05:00
dmontagu
d8fe307d61 Add support for strings and __future__ type annotations (#451)
* Add support for strings and __future__ annotations

* Add comments indicating reason for string annotations

* Fix ignores (including removing some unused ignores)
2019-09-29 16:19:09 -05:00
Sebastián Ramírez
580cf8f4e2 🔖 Release 0.38.1 2019-09-01 07:56:37 -05:00
Sebastián Ramírez
af390af77c 📝 Update release notes 2019-09-01 07:53:20 -05:00
Kamal Gill
4642f63a1e 🐛 Use proper import for Request -- fixes #492 (#493) 2019-09-01 07:51:42 -05:00
Sebastián Ramírez
203e10596f 🔖 Release version 0.38.0.Support for Pydantic 0.32.2 and Starlette 0.12.8 2019-08-30 20:40:50 -05:00
Sebastián Ramírez
5a2278d09a 📝 Update release notes 2019-08-30 20:37:39 -05:00
Sebastián Ramírez
47a8387a04 📝 Add recent articles and opinions (#490) 2019-08-30 20:35:34 -05:00
Sebastián Ramírez
27ca0c9dca 📝 Update release notes 2019-08-30 19:47:17 -05:00
dmontagu
9418d78de6 ⬆️ Upgrade Starlette support range to include 0.12.8 (#477) 2019-08-30 19:45:01 -05:00
Sebastián Ramírez
4b74aef429 📝 Update release notes 2019-08-30 19:34:34 -05:00
dmontagu
fc7d123347 ⬆️ Upgrade support to Pydantic version 0.32.2 (breaking change) (#463) 2019-08-30 19:30:03 -05:00
Sebastián Ramírez
53da56146e 🔖 Release version 0.37.0 2019-08-30 19:10:43 -05:00
Sebastián Ramírez
3799b9027e 📝 Update release notes 2019-08-30 19:09:12 -05:00
dmontagu
c70f3f1198 Add support for custom route class (#468) 2019-08-30 19:05:59 -05:00
Sebastián Ramírez
58dddc5e4f 📝 Update release notes 2019-08-30 19:02:29 -05:00
b1-luettje
c90c4fb6c1 Allow disabling Google fonts in ReDoc (#481) 2019-08-30 19:00:55 -05:00
Sebastián Ramírez
5b3df28f0c 📝 Update release notes 2019-08-30 18:59:08 -05:00
dmontagu
6c6bdb6233 🔒 Ensure skip_defaults doesn't cause extra fields to be serialized (#485) 2019-08-30 18:56:14 -05:00
Sebastián Ramírez
f156f45193 📝 Update release notes 2019-08-30 18:37:42 -05:00
Aliaksei Urbanski
f24d744a3b Enable tests for Python 3.8-dev (#465) 2019-08-30 18:34:49 -05:00
Sebastián Ramírez
937b462cdd 📝 Update release notes 2019-08-30 18:15:08 -05:00
dconathan
3025a368c6 Add support and tests for Pydantic dataclasses in response_model (#454) 2019-08-30 18:12:15 -05:00
Sebastián Ramírez
c218e0d560 📝 Update release notes 2019-08-30 17:40:14 -05:00
Pablo Marti
1ed5aa23e6 ✏️ Fix typo in oauth2-jwt.md (#447) 2019-08-30 17:35:52 -05:00
Sebastián Ramírez
106d2171d8 📝 Update release notes 2019-08-30 17:34:45 -05:00
Zoltan Papp
c5817912d2 🐛 use media_type from Body params for OpenAPI requestBody (Fixes: #431) (#439) 2019-08-30 17:32:39 -05:00
Sebastián Ramírez
a7a92bc637 📝 Update release notes 2019-08-30 17:02:40 -05:00
naxty
68d1fea961 📝 Add article: Deploying a scikit-learn model with ONNX and FastAPI (#438) 2019-08-30 17:00:00 -05:00
Sebastián Ramírez
8c6b2d5804 📝 Update release notes 2019-08-30 16:48:53 -05:00
Zoltan Papp
19c53b21c1 Allow using custom 422 validation error and use media type from response class in schema (#437)
* media_type of additional responses from the response_class

* Use HTTPValidationError only if a custom one is not defined (Fixes: #429)
2019-08-30 16:46:05 -05:00
Sebastián Ramírez
44d63cd555 📝 Update release notes 2019-08-30 16:36:18 -05:00
Sebastián Ramírez
55c4b5fb0b 🐛 Fix "default" extra response with extra status codes (#489)
* 🐛 Fix lowercase "default" extra response

* 🐛 Fix model for responses, to allow "default" plus status codes

*  Add test for "default" extra response
2019-08-30 16:34:47 -05:00
Sebastián Ramírez
c32e800c23 📝 Update release notes 2019-08-30 11:30:52 -05:00
Zoltan Papp
73dbbeab55 Allow additional responses to use status ranges and "default" (#435) 2019-08-30 11:17:42 -05:00
Sebastián Ramírez
417a3ab140 🔖 Release 0.36.0 2019-08-26 08:28:33 -05:00
Sebastián Ramírez
a3235ed8de 📝 Update release notes 2019-08-26 08:27:31 -05:00
dmontagu
38495fffa5 🐛 Fix skip_defaults implementation when returning a Pydantic model (#422) 2019-08-26 08:24:58 -05:00
Sebastián Ramírez
b77a43bcac 📝 Update release notes 2019-08-24 22:08:10 -05:00
dmontagu
483eb73b26 🐛 Use caching logic to determine OpenAPI spec for duplicate dependencies (#417) 2019-08-24 21:55:25 -05:00
Sebastián Ramírez
51a928d3f5 📝 Update release notes 2019-08-24 20:08:04 -05:00
Sebastián Ramírez
e71636e381 🐛 Fix mypy route errors after merging #415 (#462) 2019-08-24 20:05:44 -05:00
Vitaliy Kucheryaviy
f7f17fcfd6 Allow empty routed path (issue #414) (#415) 2019-08-24 19:39:48 -05:00
58 changed files with 2373 additions and 194 deletions

View File

@@ -7,6 +7,13 @@ cache: pip
python:
- "3.6"
- "3.7"
- "3.8-dev"
- "nightly"
matrix:
allow_failures:
- python: "3.8-dev"
- python: "nightly"
install:
- pip install flit

View File

@@ -25,10 +25,11 @@ sqlalchemy = "*"
uvicorn = "*"
[packages]
starlette = "==0.12.7"
pydantic = "==0.30.0"
starlette = "==0.12.9"
pydantic = "==0.32.2"
databases = {extras = ["sqlite"],version = "*"}
hypercorn = "*"
orjson = "*"
[requires]
python_version = "3.6"

View File

@@ -63,8 +63,19 @@ The key features are:
---
"*If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]*"
"*We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]*"
<div style="text-align: right; margin-right: 10%;">Ines Montani - Matthew Honnibal - <strong><a href="https://explosion.ai" target="_blank">Explosion AI</a> founders - <a href="https://spacy.io" target="_blank">spaCy</a> creators</strong> <a href="https://twitter.com/_inesmontani/status/1144173225322143744" target="_blank"><small>(ref)</small></a> - <a href="https://twitter.com/honnibal/status/1144031421859655680" target="_blank"><small>(ref)</small></a></div>
---
"*We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]*"
<div style="text-align: right; margin-right: 10%;">Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - <strong>Uber</strong> <a href="https://eng.uber.com/ludwig-v0-2/" target="_blank"><small>(ref)</small></a></div>
---
## Requirements

View File

@@ -25,6 +25,16 @@ Here's an incomplete list of some of them.
* <a href="https://blog.bartab.fr/fastapi-logging-on-the-fly/" target="_blank">FastAPI, a simple use case on logging</a> by <a href="https://blog.bartab.fr/" target="_blank">@euri10</a>.
* <a href="https://medium.com/@nico.axtmann95/deploying-a-scikit-learn-model-with-onnx-und-fastapi-1af398268915" target="_blank">Deploying a scikit-learn model with ONNX and FastAPI</a> by <a href="https://www.linkedin.com/in/nico-axtmann" target="_blank">Nico Axtmann</a>.
* <a href="https://geekflare.com/python-asynchronous-web-frameworks/" target="_blank">Top 5 Asynchronous Web Frameworks for Python</a> by <a href="https://geekflare.com/author/ankush/" target="_blank">Ankush Thakur</a> on <a href="https://geekflare.com" target="_blank">GeekFlare</a>.
* <a href="https://medium.com/@gntrm/jwt-authentication-with-fastapi-and-aws-cognito-1333f7f2729e" target="_blank">JWT Authentication with FastAPI and AWS Cognito</a> by <a href="https://twitter.com/gntrm" target="_blank">Johannes Gontrum</a>.
* <a href="https://towardsdatascience.com/how-to-deploy-a-machine-learning-model-dc51200fe8cf" target="_blank">How to Deploy a Machine Learning Model</a> by <a href="https://www.linkedin.com/in/mgrootendorst/" target="_blank">Maarten Grootendorst</a> on <a href="https://towardsdatascience.com/" target="_blank">Towards Data Science</a>.
* <a href="https://eng.uber.com/ludwig-v0-2/" target="_blank">Uber: Ludwig v0.2 Adds New Features and Other Improvements to its Deep Learning Toolbox [including a FastAPI server]</a> on <a href="https://eng.uber.com" target="_blank">Uber Engineering</a>.
### Japanese
* <a href="https://qiita.com/mtitg/items/47770e9a562dd150631d" target="_blank">FastAPIDB接続してCRUDするPython製APIサーバーを構築</a> by <a href="https://qiita.com/mtitg" target="_blank">@mtitg</a>.

View File

@@ -63,8 +63,19 @@ The key features are:
---
"*If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]*"
"*We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]*"
<div style="text-align: right; margin-right: 10%;">Ines Montani - Matthew Honnibal - <strong><a href="https://explosion.ai" target="_blank">Explosion AI</a> founders - <a href="https://spacy.io" target="_blank">spaCy</a> creators</strong> <a href="https://twitter.com/_inesmontani/status/1144173225322143744" target="_blank"><small>(ref)</small></a> - <a href="https://twitter.com/honnibal/status/1144031421859655680" target="_blank"><small>(ref)</small></a></div>
---
"*We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]*"
<div style="text-align: right; margin-right: 10%;">Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - <strong>Uber</strong> <a href="https://eng.uber.com/ludwig-v0-2/" target="_blank"><small>(ref)</small></a></div>
---
## Requirements

View File

@@ -1,5 +1,78 @@
## Latest changes
## 0.41.0
* Upgrade required Starlette to `0.12.9`, the new range is `>=0.12.9,<=0.12.9`.
* Add `State` to FastAPI apps at `app.state`.
* PR [#593](https://github.com/tiangolo/fastapi/pull/593).
* Improve handling of custom classes for `Request`s and `APIRoute`s.
* This helps to more easily solve use cases like:
* Reading a body before and/or after a request (equivalent to a middleware).
* Run middleware-like code only for a subset of *path operations*.
* Process a request before passing it to a *path operation function*. E.g. decompressing, deserializing, etc.
* Processing a response after being generated by *path operation functions* but before returning it. E.g. adding custom headers, logging, adding extra metadata.
* New docs section: [Custom Request and APIRoute class](https://fastapi.tiangolo.com/tutorial/custom-request-and-route/).
* PR [#589](https://github.com/tiangolo/fastapi/pull/589) by [@dmontagu](https://github.com/dmontagu).
* Fix preserving custom route class in routers when including other sub-routers. PR [#538](https://github.com/tiangolo/fastapi/pull/538) by [@dmontagu](https://github.com/dmontagu).
## 0.40.0
* Add notes to docs about installing `python-multipart` when using forms. PR [#574](https://github.com/tiangolo/fastapi/pull/574) by [@sliptonic](https://github.com/sliptonic).
* Generate OpenAPI schemas in alphabetical order. PR [#554](https://github.com/tiangolo/fastapi/pull/554) by [@dmontagu](https://github.com/dmontagu).
* Add support for truncating docstrings from *path operation functions*.
* New docs at [Advanced description from docstring](https://fastapi.tiangolo.com/tutorial/path-operation-advanced-configuration/#advanced-description-from-docstring).
* PR [#556](https://github.com/tiangolo/fastapi/pull/556) by [@svalouch](https://github.com/svalouch).
* Fix `DOCTYPE` in HTML files generated for Swagger UI and ReDoc. PR [#537](https://github.com/tiangolo/fastapi/pull/537) by [@Trim21](https://github.com/Trim21).
* Fix handling `4XX` responses overriding default `422` validation error responses. PR [#517](https://github.com/tiangolo/fastapi/pull/517) by [@tsouvarev](https://github.com/tsouvarev).
* Fix typo in documentation for [Simple HTTP Basic Auth](https://fastapi.tiangolo.com/tutorial/security/http-basic-auth/#simple-http-basic-auth). PR [#514](https://github.com/tiangolo/fastapi/pull/514) by [@prostomarkeloff](https://github.com/prostomarkeloff).
* Fix incorrect documentation example in [first steps](https://fastapi.tiangolo.com/tutorial/first-steps/). PR [#511](https://github.com/tiangolo/fastapi/pull/511) by [@IgnatovFedor](https://github.com/IgnatovFedor).
* Add support for Swagger UI [initOauth](https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/oauth2.md) settings with the parameter `swagger_ui_init_oauth`. PR [#499](https://github.com/tiangolo/fastapi/pull/499) by [@zamiramir](https://github.com/zamiramir).
## 0.39.0
* Allow path parameters to have default values (e.g. `None`) and discard them instead of raising an error.
* This allows declaring a parameter like `user_id: str = None` that can be taken from a query parameter, but the same path operation can be included in a router with a path `/users/{user_id}`, in which case will be taken from the path and will be required.
* PR [#464](https://github.com/tiangolo/fastapi/pull/464) by [@jonathanunderwood](https://github.com/jonathanunderwood).
* Add support for setting a `default_response_class` in the `FastAPI` instance or in `include_router`. Initial PR [#467](https://github.com/tiangolo/fastapi/pull/467) by [@toppk](https://github.com/toppk).
* Add support for type annotations using strings and `from __future__ import annotations`. PR [#451](https://github.com/tiangolo/fastapi/pull/451) by [@dmontagu](https://github.com/dmontagu).
## 0.38.1
* Fix incorrect `Request` class import. PR [#493](https://github.com/tiangolo/fastapi/pull/493) by [@kamalgill](https://github.com/kamalgill).
## 0.38.0
* Add recent articles to [External Links](https://fastapi.tiangolo.com/external-links/) and recent opinions. PR [#490](https://github.com/tiangolo/fastapi/pull/490).
* Upgrade support range for Starlette to include `0.12.8`. The new range is `>=0.11.1,<=0.12.8"`. PR [#477](https://github.com/tiangolo/fastapi/pull/477) by [@dmontagu](https://github.com/dmontagu).
* Upgrade support to Pydantic version 0.32.2 and update internal code to use it (breaking change). PR [#463](https://github.com/tiangolo/fastapi/pull/463) by [@dmontagu](https://github.com/dmontagu).
## 0.37.0
* Add support for custom route classes for advanced use cases. PR [#468](https://github.com/tiangolo/fastapi/pull/468) by [@dmontagu](https://github.com/dmontagu).
* Allow disabling Google fonts in ReDoc. PR [#481](https://github.com/tiangolo/fastapi/pull/481) by [@b1-luettje](https://github.com/b1-luettje).
* Fix security issue: when returning a sub-class of a response model and using `skip_defaults` it could leak information. PR [#485](https://github.com/tiangolo/fastapi/pull/485) by [@dmontagu](https://github.com/dmontagu).
* Enable tests for Python 3.8-dev. PR [#465](https://github.com/tiangolo/fastapi/pull/465) by [@Jamim](https://github.com/Jamim).
* Add support and tests for Pydantic dataclasses in `response_model`. PR [#454](https://github.com/tiangolo/fastapi/pull/454) by [@dconathan](https://github.com/dconathan).
* Fix typo in OAuth2 JWT tutorial. PR [#447](https://github.com/tiangolo/fastapi/pull/447) by [@pablogamboa](https://github.com/pablogamboa).
* Use the `media_type` parameter in `Body()` params to set the media type in OpenAPI for `requestBody`. PR [#439](https://github.com/tiangolo/fastapi/pull/439) by [@divums](https://github.com/divums).
* Add article [Deploying a scikit-learn model with ONNX and FastAPI](https://medium.com/@nico.axtmann95/deploying-a-scikit-learn-model-with-onnx-und-fastapi-1af398268915) by [https://www.linkedin.com/in/nico-axtmann](Nico Axtmann). PR [#438](https://github.com/tiangolo/fastapi/pull/438) by [@naxty](https://github.com/naxty).
* Allow setting custom `422` (validation error) response/schema in OpenAPI.
* And use media type from response class instead of fixed `application/json` (the default).
* PR [#437](https://github.com/tiangolo/fastapi/pull/437) by [@divums](https://github.com/divums).
* Fix using `"default"` extra response with status codes at the same time. PR [#489](https://github.com/tiangolo/fastapi/pull/489).
* Allow additional responses to use status code ranges (like `5XX` and `4XX`) and `"default"`. PR [#435](https://github.com/tiangolo/fastapi/pull/435) by [@divums](https://github.com/divums).
## 0.36.0
* Fix implementation for `skip_defaults` when returning a Pydantic model. PR [#422](https://github.com/tiangolo/fastapi/pull/422) by [@dmontagu](https://github.com/dmontagu).
* Fix OpenAPI generation when using the same dependency in multiple places for the same *path operation*. PR [#417](https://github.com/tiangolo/fastapi/pull/417) by [@dmontagu](https://github.com/dmontagu).
* Allow having empty paths in *path operations* used with `include_router` and a `prefix`.
* This allows having a router for `/cats` and all its *path operations*, while having one of them for `/cats`.
* Now it doesn't have to be only `/cats/` (with a trailing slash).
* To use it, declare the path in the *path operation* as the empty string (`""`).
* PR [#415](https://github.com/tiangolo/fastapi/pull/415) by [@vitalik](https://github.com/vitalik).
* Fix mypy error after merging PR #415. PR [#462](https://github.com/tiangolo/fastapi/pull/462).
## 0.35.0
* Fix typo in routing `assert`. PR [#419](https://github.com/tiangolo/fastapi/pull/419) by [@pablogamboa](https://github.com/pablogamboa).

View File

@@ -0,0 +1,37 @@
import gzip
from typing import Callable, List
from fastapi import Body, FastAPI
from fastapi.routing import APIRoute
from starlette.requests import Request
from starlette.responses import Response
class GzipRequest(Request):
async def body(self) -> bytes:
if not hasattr(self, "_body"):
body = await super().body()
if "gzip" in self.headers.getlist("Content-Encoding"):
body = gzip.decompress(body)
self._body = body
return self._body
class GzipRoute(APIRoute):
def get_route_handler(self) -> Callable:
original_route_handler = super().get_route_handler()
async def custom_route_handler(request: Request) -> Response:
request = GzipRequest(request.scope, request.receive)
return await original_route_handler(request)
return custom_route_handler
app = FastAPI()
app.router.route_class = GzipRoute
@app.post("/sum")
async def sum_numbers(numbers: List[int] = Body(...)):
return {"sum": sum(numbers)}

View File

@@ -0,0 +1,31 @@
from typing import Callable, List
from fastapi import Body, FastAPI, HTTPException
from fastapi.exceptions import RequestValidationError
from fastapi.routing import APIRoute
from starlette.requests import Request
from starlette.responses import Response
class ValidationErrorLoggingRoute(APIRoute):
def get_route_handler(self) -> Callable:
original_route_handler = super().get_route_handler()
async def custom_route_handler(request: Request) -> Response:
try:
return await original_route_handler(request)
except RequestValidationError as exc:
body = await request.body()
detail = {"errors": exc.errors(), "body": body.decode()}
raise HTTPException(status_code=422, detail=detail)
return custom_route_handler
app = FastAPI()
app.router.route_class = ValidationErrorLoggingRoute
@app.post("/")
async def sum_numbers(numbers: List[int] = Body(...)):
return sum(numbers)

View File

@@ -0,0 +1,41 @@
import time
from typing import Callable
from fastapi import APIRouter, FastAPI
from fastapi.routing import APIRoute
from starlette.requests import Request
from starlette.responses import Response
class TimedRoute(APIRoute):
def get_route_handler(self) -> Callable:
original_route_handler = super().get_route_handler()
async def custom_route_handler(request: Request) -> Response:
before = time.time()
response: Response = await original_route_handler(request)
duration = time.time() - before
response.headers["X-Response-Time"] = str(duration)
print(f"route duration: {duration}")
print(f"route response: {response}")
print(f"route response headers: {response.headers}")
return response
return custom_route_handler
app = FastAPI()
router = APIRouter(route_class=TimedRoute)
@app.get("/")
async def not_timed():
return {"message": "Not timed"}
@router.get("/timed")
async def timed():
return {"message": "It's the time of my life"}
app.include_router(router)

View File

@@ -0,0 +1,30 @@
from typing import Set
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str = None
price: float
tax: float = None
tags: Set[str] = []
@app.post("/items/", response_model=Item, summary="Create an item")
async def create_item(*, item: Item):
"""
Create an item with all the information:
- **name**: each item must have a name
- **description**: a long description
- **price**: required
- **tax**: if the item doesn't have tax, you can omit this
- **tags**: a set of unique tag strings for this item
\f
:param item: User input.
"""
return item

View File

@@ -0,0 +1,100 @@
In some cases, you may want to override the logic used by the `Request` and `APIRoute` classes.
In particular, this may be a good alternative to logic in a middleware.
For example, if you want to read or manipulate the request body before it is processed by your application.
!!! danger
This is an "advanced" feature.
If you are just starting with **FastAPI** you might want to skip this section.
## Use cases
Some use cases include:
* Converting non-JSON request bodies to JSON (e.g. [`msgpack`](https://msgpack.org/index.html)).
* Decompressing gzip-compressed request bodies.
* Automatically logging all request bodies.
* Accessing the request body in an exception handler.
## Handling custom request body encodings
Let's see how to make use of a custom `Request` subclass to decompress gzip requests.
And an `APIRoute` subclass to use that custom request class.
### Create a custom `GzipRequest` class
First, we create a `GzipRequest` class, which will overwrite the `Request.body()` method to decompress the body in the presence of an appropriate header.
If there's no `gzip` in the header, it will not try to decompress the body.
That way, the same route class can handle gzip compressed or uncompressed requests.
```Python hl_lines="10 11 12 13 14 15 16 17"
{!./src/custom_request_and_route/tutorial001.py!}
```
### Create a custom `GzipRoute` class
Next, we create a custom subclass of `fastapi.routing.APIRoute` that will make use of the `GzipRequest`.
This time, it will overwrite the method `APIRoute.get_route_handler()`.
This method returns a function. And that function is what will receive a request and return a response.
Here we use it to create a `GzipRequest` from the original request.
```Python hl_lines="20 21 22 23 24 25 26 27 28"
{!./src/custom_request_and_route/tutorial001.py!}
```
!!! note "Technical Details"
A `Request` has a `request.scope` attribute, that's just a Python `dict` containing the metadata related to the request.
A `Request` also has a `request.receive`, that's a function to "receive" the body of the request.
The `scope` `dict` and `receive` function are both part of the ASGI specification.
And those two things, `scope` and `receive`, are what is needed to create a new `Request` instance.
To learn more about the `Request` check <a href="https://www.starlette.io/requests/" target="_blank">Starlette's docs about Requests</a>.
The only thing the function returned by `GzipRequest.get_route_handler` does differently is convert the `Request` to a `GzipRequest`.
Doing this, our `GzipRequest` will take care of decompressing the data (if necessary) before passing it to our *path operations*.
After that, all of the processing logic is the same.
But because of our changes in `GzipRequest.body`, the request body will be automatically decompressed when it is loaded by **FastAPI** when needed.
## Accessing the request body in an exception handler
We can also use this same approach to access the request body in an exception handler.
All we need to do is handle the request inside a `try`/`except` block:
```Python hl_lines="15 17"
{!./src/custom_request_and_route/tutorial002.py!}
```
If an exception occurs, the`Request` instance will still be in scope, so we can read and make use of the request body when handling the error:
```Python hl_lines="18 19 20"
{!./src/custom_request_and_route/tutorial002.py!}
```
## Custom `APIRoute` class in a router
You can also set the `route_class` parameter of an `APIRouter`:
```Python hl_lines="25"
{!./src/custom_request_and_route/tutorial003.py!}
```
In this example, the *path operations* under the `router` will use the custom `TimedRoute` class, and will have an extra `X-Response-Time` header in the response with the time it took to generate the response:
```Python hl_lines="15 16 17 18 19"
{!./src/custom_request_and_route/tutorial003.py!}
```

View File

@@ -37,7 +37,7 @@ Open your browser at <a href="http://127.0.0.1:8000" target="_blank">http://127.
You will see the JSON response as:
```JSON
{"hello": "world"}
{"message": "Hello World"}
```
### Interactive API docs

View File

@@ -18,3 +18,15 @@ To exclude a path operation from the generated OpenAPI schema (and thus, from th
```Python hl_lines="6"
{!./src/path_operation_advanced_configuration/tutorial002.py!}
```
## Advanced description from docstring
You can limit the lines used from the docstring of a *path operation function* for OpenAPI.
Adding an `\f` (an escaped "form feed" character) causes **FastAPI** to truncate the output used for OpenAPI at this point.
It won't show up in the documentation, but other tools (such as Sphinx) will be able to use the rest.
```Python hl_lines="19 20 21 22 23 24 25 26 27 28 29"
{!./src/path_operation_advanced_configuration/tutorial003.py!}
```

View File

@@ -1,5 +1,12 @@
You can define files to be uploaded by the client using `File`.
!!! info
To receive uploaded files, first install [`python-multipart`](https://andrew-d.github.io/python-multipart/).
E.g. `pip install python-multipart`.
This is because uploaded files are sent as "form data".
## Import `File`
Import `File` and `UploadFile` from `fastapi`:

View File

@@ -1,5 +1,10 @@
You can define files and form fields at the same time using `File` and `Form`.
!!! info
To receive uploaded files and/or form data, first install [`python-multipart`](https://andrew-d.github.io/python-multipart/).
E.g. `pip install python-multipart`.
## Import `File` and `Form`
```Python hl_lines="1"

View File

@@ -1,5 +1,10 @@
When you need to receive form fields instead of JSON, you can use `Form`.
!!! info
To use forms, first install [`python-multipart`](https://andrew-d.github.io/python-multipart/).
E.g. `pip install python-multipart`.
## Import `Form`
Import `Form` from `fastapi`:

View File

@@ -24,6 +24,13 @@ Copy the example in a file `main.py`:
## Run it
!!! info
First install [`python-multipart`](https://andrew-d.github.io/python-multipart/).
E.g. `pip install python-multipart`.
This is because **OAuth2** uses "form data" for sending the `username` and `password`.
Run the example with:
```bash

View File

@@ -12,8 +12,8 @@ Then, when you type that username and password, the browser sends them in the he
## Simple HTTP Basic Auth
* Import `HTTPBAsic` and `HTTPBasicCredentials`.
* Create a "`security` scheme" using `HTTPBAsic`.
* Import `HTTPBasic` and `HTTPBasicCredentials`.
* Create a "`security` scheme" using `HTTPBasic`.
* Use that `security` with a dependency in your *path operation*.
* It returns an object of type `HTTPBasicCredentials`:
* It contains the `username` and `password` sent.

View File

@@ -156,7 +156,7 @@ Then you could add permissions about that entity, like "drive" (for the car) or
And then, you could give that JWT token to a user (or bot), and he could use it to perform those actions (drive the car, or edit the blog post) without even needing to have an account, just with the JWT token your API generated for that.
Using these ideas, JWT can be used for way more sophisticate scenarios.
Using these ideas, JWT can be used for way more sophisticated scenarios.
In those cases, several of those entities could have the same ID, let's say `foo` (a user `foo`, a car `foo`, and a blog post `foo`).

View File

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

View File

@@ -1,6 +1,7 @@
from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Type, Union
from typing import Any, Callable, Dict, List, Optional, Sequence, Type, Union
from fastapi import routing
from fastapi.encoders import DictIntStrAny, SetIntStr
from fastapi.exception_handlers import (
http_exception_handler,
request_validation_exception_handler,
@@ -14,6 +15,7 @@ from fastapi.openapi.docs import (
from fastapi.openapi.utils import get_openapi
from fastapi.params import Depends
from starlette.applications import Starlette
from starlette.datastructures import State
from starlette.exceptions import ExceptionMiddleware, HTTPException
from starlette.middleware.errors import ServerErrorMiddleware
from starlette.requests import Request
@@ -32,12 +34,16 @@ class FastAPI(Starlette):
version: str = "0.1.0",
openapi_url: Optional[str] = "/openapi.json",
openapi_prefix: str = "",
default_response_class: Type[Response] = JSONResponse,
docs_url: Optional[str] = "/docs",
redoc_url: Optional[str] = "/redoc",
swagger_ui_oauth2_redirect_url: Optional[str] = "/docs/oauth2-redirect",
swagger_ui_init_oauth: Optional[dict] = None,
**extra: Dict[str, Any],
) -> None:
self.default_response_class = default_response_class
self._debug = debug
self.state = State()
self.router: routing.APIRouter = routing.APIRouter(
routes, dependency_overrides_provider=self
)
@@ -54,6 +60,7 @@ class FastAPI(Starlette):
self.docs_url = docs_url
self.redoc_url = redoc_url
self.swagger_ui_oauth2_redirect_url = swagger_ui_oauth2_redirect_url
self.swagger_ui_init_oauth = swagger_ui_init_oauth
self.extra = extra
self.dependency_overrides: Dict[Callable, Callable] = {}
@@ -95,6 +102,7 @@ class FastAPI(Starlette):
openapi_url=openapi_url,
title=self.title + " - Swagger UI",
oauth2_redirect_url=self.swagger_ui_oauth2_redirect_url,
init_oauth=self.swagger_ui_init_oauth,
)
self.add_route(self.docs_url, swagger_ui_html, include_in_schema=False)
@@ -138,12 +146,12 @@ 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_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Type[Response] = None,
name: str = None,
) -> None:
self.router.add_api_route(
@@ -165,7 +173,7 @@ class FastAPI(Starlette):
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,
response_class=response_class or self.default_response_class,
name=name,
)
@@ -184,12 +192,12 @@ 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_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Type[Response] = None,
name: str = None,
) -> Callable:
def decorator(func: Callable) -> Callable:
@@ -212,7 +220,7 @@ class FastAPI(Starlette):
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,
response_class=response_class or self.default_response_class,
name=name,
)
return func
@@ -239,6 +247,7 @@ class FastAPI(Starlette):
tags: List[str] = None,
dependencies: Sequence[Depends] = None,
responses: Dict[Union[int, str], Dict[str, Any]] = None,
default_response_class: Optional[Type[Response]] = None,
) -> None:
self.router.include_router(
router,
@@ -246,6 +255,8 @@ class FastAPI(Starlette):
tags=tags,
dependencies=dependencies,
responses=responses or {},
default_response_class=default_response_class
or self.default_response_class,
)
def get(
@@ -262,12 +273,12 @@ 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_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Type[Response] = None,
name: str = None,
) -> Callable:
return self.router.get(
@@ -287,7 +298,7 @@ class FastAPI(Starlette):
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,
response_class=response_class or self.default_response_class,
name=name,
)
@@ -305,12 +316,12 @@ 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_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Type[Response] = None,
name: str = None,
) -> Callable:
return self.router.put(
@@ -330,7 +341,7 @@ class FastAPI(Starlette):
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,
response_class=response_class or self.default_response_class,
name=name,
)
@@ -348,12 +359,12 @@ 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_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Type[Response] = None,
name: str = None,
) -> Callable:
return self.router.post(
@@ -373,7 +384,7 @@ class FastAPI(Starlette):
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,
response_class=response_class or self.default_response_class,
name=name,
)
@@ -391,12 +402,12 @@ 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_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Type[Response] = None,
name: str = None,
) -> Callable:
return self.router.delete(
@@ -416,7 +427,7 @@ class FastAPI(Starlette):
operation_id=operation_id,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
response_class=response_class or self.default_response_class,
name=name,
)
@@ -434,12 +445,12 @@ 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_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Type[Response] = None,
name: str = None,
) -> Callable:
return self.router.options(
@@ -459,7 +470,7 @@ class FastAPI(Starlette):
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,
response_class=response_class or self.default_response_class,
name=name,
)
@@ -477,12 +488,12 @@ 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_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Type[Response] = None,
name: str = None,
) -> Callable:
return self.router.head(
@@ -502,7 +513,7 @@ class FastAPI(Starlette):
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,
response_class=response_class or self.default_response_class,
name=name,
)
@@ -520,12 +531,12 @@ 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_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Type[Response] = None,
name: str = None,
) -> Callable:
return self.router.patch(
@@ -545,7 +556,7 @@ class FastAPI(Starlette):
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,
response_class=response_class or self.default_response_class,
name=name,
)
@@ -563,12 +574,12 @@ 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_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Type[Response] = None,
name: str = None,
) -> Callable:
return self.router.trace(
@@ -588,6 +599,6 @@ class FastAPI(Starlette):
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,
response_class=response_class or self.default_response_class,
name=name,
)

View File

@@ -26,7 +26,7 @@ from pydantic.error_wrappers import ErrorWrapper
from pydantic.errors import MissingError
from pydantic.fields import Field, Required, Shape
from pydantic.schema import get_annotation_from_schema
from pydantic.utils import lenient_issubclass
from pydantic.utils import ForwardRef, evaluate_forwardref, lenient_issubclass
from starlette.background import BackgroundTasks
from starlette.concurrency import run_in_threadpool
from starlette.datastructures import FormData, Headers, QueryParams, UploadFile
@@ -108,7 +108,16 @@ def get_sub_dependant(
return sub_dependant
def get_flat_dependant(dependant: Dependant) -> Dependant:
CacheKey = Tuple[Optional[Callable], Tuple[str, ...]]
def get_flat_dependant(
dependant: Dependant, *, skip_repeats: bool = False, visited: List[CacheKey] = None
) -> Dependant:
if visited is None:
visited = []
visited.append(dependant.cache_key)
flat_dependant = Dependant(
path_params=dependant.path_params.copy(),
query_params=dependant.query_params.copy(),
@@ -120,7 +129,11 @@ def get_flat_dependant(dependant: Dependant) -> Dependant:
path=dependant.path,
)
for sub_dependant in dependant.dependencies:
flat_sub = get_flat_dependant(sub_dependant)
if skip_repeats and sub_dependant.cache_key in visited:
continue
flat_sub = get_flat_dependant(
sub_dependant, skip_repeats=skip_repeats, visited=visited
)
flat_dependant.path_params.extend(flat_sub.path_params)
flat_dependant.query_params.extend(flat_sub.query_params)
flat_dependant.header_params.extend(flat_sub.header_params)
@@ -158,6 +171,30 @@ def is_scalar_sequence_field(field: Field) -> bool:
return False
def get_typed_signature(call: Callable) -> inspect.Signature:
signature = inspect.signature(call)
globalns = getattr(call, "__globals__", {})
typed_params = [
inspect.Parameter(
name=param.name,
kind=param.kind,
default=param.default,
annotation=get_typed_annotation(param, globalns),
)
for param in signature.parameters.values()
]
typed_signature = inspect.Signature(typed_params)
return typed_signature
def get_typed_annotation(param: inspect.Parameter, globalns: Dict[str, Any]) -> Any:
annotation = param.annotation
if isinstance(annotation, str):
annotation = ForwardRef(annotation)
annotation = evaluate_forwardref(annotation, globalns, globalns)
return annotation
def get_dependant(
*,
path: str,
@@ -167,7 +204,7 @@ def get_dependant(
use_cache: bool = True,
) -> Dependant:
path_param_names = get_path_param_names(path)
endpoint_signature = inspect.signature(call)
endpoint_signature = get_typed_signature(call)
signature_params = endpoint_signature.parameters
dependant = Dependant(call=call, name=name, path=path, use_cache=use_cache)
for param_name, param in signature_params.items():
@@ -183,16 +220,18 @@ def get_dependant(
continue
param_field = get_param_field(param=param, default_schema=params.Query)
if param_name in path_param_names:
assert param.default == param.empty or isinstance(
param.default, params.Path
), "Path params must have no defaults or use Path(...)"
assert is_scalar_field(
field=param_field
), f"Path params must be of one of the supported types"
if isinstance(param.default, params.Path):
ignore_default = False
else:
ignore_default = True
param_field = get_param_field(
param=param,
default_schema=params.Path,
force_type=params.ParamTypes.path,
ignore_default=ignore_default,
)
add_param_to_fields(field=param_field, dependant=dependant)
elif is_scalar_field(field=param_field):
@@ -235,10 +274,11 @@ def get_param_field(
param: inspect.Parameter,
default_schema: Type[params.Param] = params.Param,
force_type: params.ParamTypes = None,
ignore_default: bool = False,
) -> Field:
default_value = Required
had_schema = False
if not param.default == param.empty:
if not param.default == param.empty and ignore_default is False:
default_value = param.default
if isinstance(default_value, Schema):
had_schema = True
@@ -316,8 +356,12 @@ async def solve_dependencies(
]:
values: Dict[str, Any] = {}
errors: List[ErrorWrapper] = []
response = response or Response( # type: ignore
content=None, status_code=None, headers=None, media_type=None, background=None
response = response or Response(
content=None,
status_code=None, # type: ignore
headers=None,
media_type=None,
background=None,
)
dependency_cache = dependency_cache or {}
sub_dependant: Dependant
@@ -392,7 +436,7 @@ async def solve_dependencies(
values.update(cookie_values)
errors += path_errors + query_errors + header_errors + cookie_errors
if dependant.body_params:
body_values, body_errors = await request_body_to_args( # type: ignore # body_params checked above
body_values, body_errors = await request_body_to_args( # body_params checked above
required_params=dependant.body_params, received_body=body
)
values.update(body_values)
@@ -546,6 +590,8 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[Field]:
for f in flat_dependant.body_params:
BodyModel.__fields__[f.name] = get_schema_compatible_field(field=f)
required = any(True for f in flat_dependant.body_params if f.required)
BodySchema_kwargs: Dict[str, Any] = dict(default=None)
if any(isinstance(f.schema, params.File) for f in flat_dependant.body_params):
BodySchema: Type[params.Body] = params.File
elif any(isinstance(f.schema, params.Form) for f in flat_dependant.body_params):
@@ -553,6 +599,14 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[Field]:
else:
BodySchema = params.Body
body_param_media_types = [
getattr(f.schema, "media_type")
for f in flat_dependant.body_params
if isinstance(f.schema, params.Body)
]
if len(set(body_param_media_types)) == 1:
BodySchema_kwargs["media_type"] = body_param_media_types[0]
field = Field(
name="body",
type_=BodyModel,
@@ -561,6 +615,6 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[Field]:
model_config=BaseConfig,
class_validators={},
alias="body",
schema=BodySchema(None),
schema=BodySchema(**BodySchema_kwargs),
)
return field

View File

@@ -1,15 +1,18 @@
from enum import Enum
from types import GeneratorType
from typing import Any, List, Set
from typing import Any, Dict, List, Set, Union
from pydantic import BaseModel
from pydantic.json import ENCODERS_BY_TYPE
SetIntStr = Set[Union[int, str]]
DictIntStrAny = Dict[Union[int, str], Any]
def jsonable_encoder(
obj: Any,
include: Set[str] = None,
exclude: Set[str] = set(),
include: Union[SetIntStr, DictIntStrAny] = None,
exclude: Union[SetIntStr, DictIntStrAny] = set(),
by_alias: bool = True,
skip_defaults: bool = False,
include_none: bool = True,

View File

@@ -1,7 +1,10 @@
from typing import Any
from typing import Any, Sequence
from pydantic import ValidationError
from pydantic.error_wrappers import ErrorList
from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.requests import Request
from starlette.websockets import WebSocket
class HTTPException(StarletteHTTPException):
@@ -13,8 +16,10 @@ class HTTPException(StarletteHTTPException):
class RequestValidationError(ValidationError):
pass
def __init__(self, errors: Sequence[ErrorList]) -> None:
super().__init__(errors, Request)
class WebSocketRequestValidationError(ValidationError):
pass
def __init__(self, errors: Sequence[ErrorList]) -> None:
super().__init__(errors, WebSocket)

View File

@@ -1,5 +1,7 @@
import json
from typing import Optional
from fastapi.encoders import jsonable_encoder
from starlette.responses import HTMLResponse
@@ -11,10 +13,11 @@ def get_swagger_ui_html(
swagger_css_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui.css",
swagger_favicon_url: str = "https://fastapi.tiangolo.com/img/favicon.png",
oauth2_redirect_url: Optional[str] = None,
init_oauth: Optional[dict] = None,
) -> HTMLResponse:
html = f"""
<! doctype html>
<!DOCTYPE html>
<html>
<head>
<link type="text/css" rel="stylesheet" href="{swagger_css_url}">
@@ -42,7 +45,14 @@ def get_swagger_ui_html(
],
layout: "BaseLayout",
deepLinking: true
})
})"""
if init_oauth:
html += f"""
ui.initOAuth({json.dumps(jsonable_encoder(init_oauth))})
"""
html += """
</script>
</body>
</html>
@@ -56,6 +66,7 @@ def get_redoc_html(
title: str,
redoc_js_url: str = "https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js",
redoc_favicon_url: str = "https://fastapi.tiangolo.com/img/favicon.png",
with_google_fonts: bool = True,
) -> HTMLResponse:
html = f"""
<!DOCTYPE html>
@@ -65,7 +76,12 @@ def get_redoc_html(
<!-- needed for adaptive design -->
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
"""
if with_google_fonts:
html += """
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
"""
html += f"""
<link rel="shortcut icon" href="{redoc_favicon_url}">
<!--
ReDoc doesn't change outer page styles
@@ -88,7 +104,7 @@ def get_redoc_html(
def get_swagger_ui_oauth2_redirect_html() -> HTMLResponse:
html = """
<!doctype html>
<!DOCTYPE html>
<html lang="en-US">
<body onload="run()">
</body>

View File

@@ -11,7 +11,7 @@ try:
import email_validator
assert email_validator # make autoflake ignore the unused import
from pydantic.types import EmailStr # type: ignore
from pydantic.types import EmailStr
except ImportError: # pragma: no cover
logger.warning(
"email-validator not installed, email fields will be treated as str.\n"
@@ -210,10 +210,6 @@ class Response(BaseModel):
links: Optional[Dict[str, Union[Link, Reference]]] = None
class Responses(BaseModel):
default: Response
class Operation(BaseModel):
tags: Optional[List[str]] = None
summary: Optional[str] = None
@@ -222,7 +218,7 @@ class Operation(BaseModel):
operationId: Optional[str] = None
parameters: Optional[List[Union[Parameter, Reference]]] = None
requestBody: Optional[Union[RequestBody, Reference]] = None
responses: Union[Responses, Dict[str, Response]]
responses: Dict[str, Response]
# Workaround OpenAPI recursive reference
callbacks: Optional[Dict[str, Union[Dict[str, Any], Reference]]] = None
deprecated: Optional[bool] = None

View File

@@ -43,9 +43,18 @@ validation_error_response_definition = {
},
}
status_code_ranges: Dict[str, str] = {
"1XX": "Information",
"2XX": "Success",
"3XX": "Redirection",
"4XX": "Client Error",
"5XX": "Server Error",
"DEFAULT": "Default Response",
}
def get_openapi_params(dependant: Dependant) -> List[Field]:
flat_dependant = get_flat_dependant(dependant)
flat_dependant = get_flat_dependant(dependant, skip_repeats=True)
return (
flat_dependant.path_params
+ flat_dependant.query_params
@@ -71,15 +80,11 @@ def get_openapi_security_definitions(flat_dependant: Dependant) -> Tuple[Dict, L
def get_openapi_operation_parameters(
all_route_params: Sequence[Field]
) -> Tuple[Dict[str, Dict], List[Dict[str, Any]]]:
definitions: Dict[str, Dict] = {}
) -> List[Dict[str, Any]]:
parameters = []
for param in all_route_params:
schema = param.schema
schema = cast(Param, schema)
if "ValidationError" not in definitions:
definitions["ValidationError"] = validation_error_definition
definitions["HTTPValidationError"] = validation_error_response_definition
parameter = {
"name": param.alias,
"in": schema.in_.value,
@@ -91,7 +96,7 @@ def get_openapi_operation_parameters(
if schema.deprecated:
parameter["deprecated"] = schema.deprecated
parameters.append(parameter)
return definitions, parameters
return parameters
def get_openapi_operation_request_body(
@@ -146,11 +151,15 @@ def get_openapi_path(
security_schemes: Dict[str, Any] = {}
definitions: Dict[str, Any] = {}
assert route.methods is not None, "Methods must be a list"
assert (
route.response_class and route.response_class.media_type
), "A response class with media_type is needed to generate OpenAPI"
route_response_media_type: str = route.response_class.media_type
if route.include_in_schema:
for method in route.methods:
operation = get_openapi_operation_metadata(route=route, method=method)
parameters: List[Dict] = []
flat_dependant = get_flat_dependant(route.dependant)
flat_dependant = get_flat_dependant(route.dependant, skip_repeats=True)
security_definitions, operation_security = get_openapi_security_definitions(
flat_dependant=flat_dependant
)
@@ -159,10 +168,7 @@ def get_openapi_path(
if security_definitions:
security_schemes.update(security_definitions)
all_route_params = get_openapi_params(route.dependant)
validation_definitions, operation_parameters = get_openapi_operation_parameters(
all_route_params=all_route_params
)
definitions.update(validation_definitions)
operation_parameters = get_openapi_operation_parameters(all_route_params)
parameters.extend(operation_parameters)
if parameters:
operation["parameters"] = parameters
@@ -172,11 +178,6 @@ def get_openapi_path(
)
if request_body_oai:
operation["requestBody"] = request_body_oai
if "ValidationError" not in definitions:
definitions["ValidationError"] = validation_error_definition
definitions[
"HTTPValidationError"
] = validation_error_response_definition
if route.responses:
for (additional_status_code, response) in route.responses.items():
assert isinstance(
@@ -188,15 +189,18 @@ def get_openapi_path(
field, model_name_map=model_name_map, ref_prefix=REF_PREFIX
)
response.setdefault("content", {}).setdefault(
"application/json", {}
route_response_media_type, {}
)["schema"] = response_schema
status_text = http.client.responses.get(int(additional_status_code))
status_text: Optional[str] = status_code_ranges.get(
str(additional_status_code).upper()
) or http.client.responses.get(int(additional_status_code))
response.setdefault(
"description", status_text or "Additional Response"
)
operation.setdefault("responses", {})[
str(additional_status_code)
] = response
status_code_key = str(additional_status_code).upper()
if status_code_key == "DEFAULT":
status_code_key = "default"
operation.setdefault("responses", {})[status_code_key] = response
status_code = str(route.status_code)
response_schema = {"type": "string"}
if lenient_issubclass(route.response_class, JSONResponse):
@@ -213,11 +217,18 @@ def get_openapi_path(
] = route.response_description
operation.setdefault("responses", {}).setdefault(
status_code, {}
).setdefault("content", {}).setdefault(route.response_class.media_type, {})[
).setdefault("content", {}).setdefault(route_response_media_type, {})[
"schema"
] = response_schema
if all_route_params or route.body_field:
operation["responses"][str(HTTP_422_UNPROCESSABLE_ENTITY)] = {
http422 = str(HTTP_422_UNPROCESSABLE_ENTITY)
if (all_route_params or route.body_field) and not any(
[
status in operation["responses"]
for status in [http422, "4XX", "default"]
]
):
operation["responses"][http422] = {
"description": "Validation Error",
"content": {
"application/json": {
@@ -225,6 +236,13 @@ def get_openapi_path(
}
},
}
if "ValidationError" not in definitions:
definitions.update(
{
"ValidationError": validation_error_definition,
"HTTPValidationError": validation_error_response_definition,
}
)
path[method.lower()] = operation
return path, security_schemes, definitions
@@ -265,7 +283,7 @@ def get_openapi(
if path_definitions:
definitions.update(path_definitions)
if definitions:
components.setdefault("schemas", {}).update(definitions)
components["schemas"] = {k: definitions[k] for k in sorted(definitions)}
if components:
output["components"] = components
output["paths"] = paths

View File

@@ -11,7 +11,7 @@ from fastapi.dependencies.utils import (
get_parameterless_sub_dependant,
solve_dependencies,
)
from fastapi.encoders import jsonable_encoder
from fastapi.encoders import DictIntStrAny, SetIntStr, jsonable_encoder
from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError
from fastapi.utils import create_cloned_field, generate_operation_id_for_path
from pydantic import BaseConfig, BaseModel, Schema
@@ -38,20 +38,22 @@ def serialize_response(
*,
field: Field = None,
response: Response,
include: Set[str] = None,
exclude: Set[str] = set(),
include: Union[SetIntStr, DictIntStrAny] = None,
exclude: Union[SetIntStr, DictIntStrAny] = set(),
by_alias: bool = True,
skip_defaults: bool = False,
) -> Any:
if field:
errors = []
if skip_defaults and isinstance(response, BaseModel):
response = response.dict(skip_defaults=skip_defaults)
value, errors_ = field.validate(response, {}, loc=("response",))
if isinstance(errors_, ErrorWrapper):
errors.append(errors_)
elif isinstance(errors_, list):
errors.extend(errors_)
if errors:
raise ValidationError(errors)
raise ValidationError(errors, field.type_)
return jsonable_encoder(
value,
include=include,
@@ -63,14 +65,14 @@ def serialize_response(
return jsonable_encoder(response)
def get_app(
def get_request_handler(
dependant: Dependant,
body_field: Field = None,
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_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
dependency_overrides_provider: Any = None,
@@ -193,15 +195,14 @@ class APIRoute(routing.Route):
name: str = None,
methods: Optional[Union[Set[str], List[str]]] = None,
operation_id: str = None,
response_model_include: Set[str] = None,
response_model_exclude: Set[str] = set(),
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Optional[Type[Response]] = None,
dependency_overrides_provider: Any = None,
) -> None:
assert path.startswith("/"), "Routed paths must always start with '/'"
self.path = path
self.endpoint = endpoint
self.name = get_name(endpoint) if name is None else name
@@ -214,9 +215,6 @@ class APIRoute(routing.Route):
)
self.response_model = response_model
if self.response_model:
assert lenient_issubclass(
response_class, JSONResponse
), "To declare a type the response must be a JSON response"
response_name = "Response_" + self.unique_id
self.response_field: Optional[Field] = Field(
name=response_name,
@@ -248,6 +246,9 @@ class APIRoute(routing.Route):
self.dependencies = []
self.summary = summary
self.description = description or inspect.cleandoc(self.endpoint.__doc__ or "")
# if a "form feed" character (page break) is found in the description text,
# truncate description text to the content preceding the first "form feed"
self.description = self.description.split("\f")[0]
self.response_description = response_description
self.responses = responses or {}
response_fields = {}
@@ -293,19 +294,20 @@ class APIRoute(routing.Route):
)
self.body_field = get_body_field(dependant=self.dependant, name=self.unique_id)
self.dependency_overrides_provider = dependency_overrides_provider
self.app = request_response(
get_app(
dependant=self.dependant,
body_field=self.body_field,
status_code=self.status_code,
response_class=self.response_class,
response_field=self.secure_cloned_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,
dependency_overrides_provider=self.dependency_overrides_provider,
)
self.app = request_response(self.get_route_handler())
def get_route_handler(self) -> Callable:
return get_request_handler(
dependant=self.dependant,
body_field=self.body_field,
status_code=self.status_code,
response_class=self.response_class or JSONResponse,
response_field=self.secure_cloned_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,
dependency_overrides_provider=self.dependency_overrides_provider,
)
@@ -316,11 +318,13 @@ class APIRouter(routing.Router):
redirect_slashes: bool = True,
default: ASGIApp = None,
dependency_overrides_provider: Any = None,
route_class: Type[APIRoute] = APIRoute,
) -> None:
super().__init__(
routes=routes, redirect_slashes=redirect_slashes, default=default
)
self.dependency_overrides_provider = dependency_overrides_provider
self.route_class = route_class
def add_api_route(
self,
@@ -338,15 +342,17 @@ class APIRouter(routing.Router):
deprecated: bool = None,
methods: Optional[Union[Set[str], List[str]]] = None,
operation_id: str = None,
response_model_include: Set[str] = None,
response_model_exclude: Set[str] = set(),
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Type[Response] = None,
name: str = None,
route_class_override: Optional[Type[APIRoute]] = None,
) -> None:
route = APIRoute(
route_class = route_class_override or self.route_class
route = route_class(
path,
endpoint=endpoint,
response_model=response_model,
@@ -386,12 +392,12 @@ 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_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Type[Response] = None,
name: str = None,
) -> Callable:
def decorator(func: Callable) -> Callable:
@@ -442,12 +448,21 @@ class APIRouter(routing.Router):
tags: List[str] = None,
dependencies: Sequence[params.Depends] = None,
responses: Dict[Union[int, str], Dict[str, Any]] = None,
default_response_class: Optional[Type[Response]] = None,
) -> None:
if prefix:
assert prefix.startswith("/"), "A path prefix must start with '/'"
assert not prefix.endswith(
"/"
), "A path prefix must not end with '/', as the routes will start with '/'"
else:
for r in router.routes:
path = getattr(r, "path")
name = getattr(r, "name", "unknown")
if path is not None and not path:
raise Exception(
f"Prefix and path cannot be both empty (path operation: {name})"
)
if responses is None:
responses = {}
for route in router.routes:
@@ -473,8 +488,9 @@ class APIRouter(routing.Router):
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,
response_class=route.response_class or default_response_class,
name=route.name,
route_class_override=type(route),
)
elif isinstance(route, routing.Route):
self.add_route(
@@ -507,15 +523,14 @@ 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_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Type[Response] = None,
name: str = None,
) -> Callable:
return self.api_route(
path=path,
response_model=response_model,
@@ -552,12 +567,12 @@ 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_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Type[Response] = None,
name: str = None,
) -> Callable:
return self.api_route(
@@ -596,12 +611,12 @@ 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_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Type[Response] = None,
name: str = None,
) -> Callable:
return self.api_route(
@@ -640,12 +655,12 @@ 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_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Type[Response] = None,
name: str = None,
) -> Callable:
return self.api_route(
@@ -684,12 +699,12 @@ 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_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Type[Response] = None,
name: str = None,
) -> Callable:
return self.api_route(
@@ -728,12 +743,12 @@ 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_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Type[Response] = None,
name: str = None,
) -> Callable:
return self.api_route(
@@ -772,12 +787,12 @@ 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_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Type[Response] = None,
name: str = None,
) -> Callable:
return self.api_route(
@@ -816,12 +831,12 @@ 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_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Type[Response] = None,
name: str = None,
) -> Callable:
return self.api_route(

View File

@@ -1,4 +1,5 @@
import re
from dataclasses import is_dataclass
from typing import Any, Dict, List, Sequence, Set, Type, cast
from fastapi import routing
@@ -52,13 +53,15 @@ def get_path_param_names(path: str) -> Set[str]:
def create_cloned_field(field: Field) -> Field:
original_type = field.type_
if is_dataclass(original_type) and hasattr(original_type, "__pydantic_model__"):
original_type = original_type.__pydantic_model__ # type: ignore
use_type = original_type
if lenient_issubclass(original_type, BaseModel):
original_type = cast(Type[BaseModel], original_type)
use_type = create_model( # type: ignore
use_type = create_model(
original_type.__name__,
__config__=original_type.__config__,
__validators__=original_type.__validators__,
__validators__=original_type.__validators__, # type: ignore
)
for f in original_type.__fields__.values():
use_type.__fields__[f.name] = f

View File

@@ -81,6 +81,7 @@ nav:
- GraphQL: 'tutorial/graphql.md'
- WebSockets: 'tutorial/websockets.md'
- 'Events: startup - shutdown': 'tutorial/events.md'
- Custom Request and APIRoute class: 'tutorial/custom-request-and-route.md'
- Testing: 'tutorial/testing.md'
- Testing Dependencies with Overrides: 'tutorial/testing-dependencies.md'
- Debugging: 'tutorial/debugging.md'

View File

@@ -19,8 +19,8 @@ classifiers = [
"Topic :: Internet :: WWW/HTTP :: HTTP Servers",
]
requires = [
"starlette >=0.11.1,<=0.12.7",
"pydantic >=0.30,<=0.30.0"
"starlette >=0.12.9,<=0.12.9",
"pydantic >=0.32.2,<=0.32.2"
]
description-file = "README.md"
requires-python = ">=3.6"
@@ -39,6 +39,7 @@ test = [
"email_validator",
"sqlalchemy",
"databases[sqlite]",
"orjson"
]
doc = [
"mkdocs",

View File

@@ -0,0 +1,40 @@
import pytest
from fastapi import FastAPI
from starlette.testclient import TestClient
app = FastAPI()
@app.get("/a", responses={"hello": {"description": "Not a valid additional response"}})
async def a():
pass # pragma: no cover
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/a": {
"get": {
"responses": {
# this is how one would imagine the openapi schema to be
# but since the key is not valid, openapi.utils.get_openapi will raise ValueError
"hello": {"description": "Not a valid additional response"},
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
},
"summary": "A",
"operationId": "a_a_get",
}
}
},
}
client = TestClient(app)
def test_openapi_schema():
with pytest.raises(ValueError):
client.get("/openapi.json")

View File

@@ -0,0 +1,100 @@
import typing
from fastapi import FastAPI
from pydantic import BaseModel
from starlette.responses import JSONResponse
from starlette.testclient import TestClient
app = FastAPI()
class JsonApiResponse(JSONResponse):
media_type = "application/vnd.api+json"
class Error(BaseModel):
status: str
title: str
class JsonApiError(BaseModel):
errors: typing.List[Error]
@app.get(
"/a/{id}",
response_class=JsonApiResponse,
responses={422: {"description": "Error", "model": JsonApiError}},
)
async def a(id):
pass # pragma: no cover
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/a/{id}": {
"get": {
"responses": {
"422": {
"description": "Error",
"content": {
"application/vnd.api+json": {
"schema": {"$ref": "#/components/schemas/JsonApiError"}
}
},
},
"200": {
"description": "Successful Response",
"content": {"application/vnd.api+json": {"schema": {}}},
},
},
"summary": "A",
"operationId": "a_a__id__get",
"parameters": [
{
"required": True,
"schema": {"title": "Id"},
"name": "id",
"in": "path",
}
],
}
}
},
"components": {
"schemas": {
"Error": {
"title": "Error",
"required": ["status", "title"],
"type": "object",
"properties": {
"status": {"title": "Status", "type": "string"},
"title": {"title": "Title", "type": "string"},
},
},
"JsonApiError": {
"title": "JsonApiError",
"required": ["errors"],
"type": "object",
"properties": {
"errors": {
"title": "Errors",
"type": "array",
"items": {"$ref": "#/components/schemas/Error"},
}
},
},
}
},
}
client = TestClient(app)
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_schema

View File

@@ -0,0 +1,85 @@
from fastapi import FastAPI
from starlette.testclient import TestClient
app = FastAPI()
@app.get("/a/{id}")
async def a(id):
pass # pragma: no cover
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/a/{id}": {
"get": {
"responses": {
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
},
"summary": "A",
"operationId": "a_a__id__get",
"parameters": [
{
"required": True,
"schema": {"title": "Id"},
"name": "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"},
}
},
},
}
},
}
client = TestClient(app)
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_schema

View File

@@ -0,0 +1,117 @@
import typing
from fastapi import FastAPI
from pydantic import BaseModel
from starlette.responses import JSONResponse
from starlette.testclient import TestClient
app = FastAPI()
class JsonApiResponse(JSONResponse):
media_type = "application/vnd.api+json"
class Error(BaseModel):
status: str
title: str
class JsonApiError(BaseModel):
errors: typing.List[Error]
@app.get(
"/a",
response_class=JsonApiResponse,
responses={500: {"description": "Error", "model": JsonApiError}},
)
async def a():
pass # pragma: no cover
@app.get("/b", responses={500: {"description": "Error", "model": Error}})
async def b():
pass # pragma: no cover
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/a": {
"get": {
"responses": {
"500": {
"description": "Error",
"content": {
"application/vnd.api+json": {
"schema": {"$ref": "#/components/schemas/JsonApiError"}
}
},
},
"200": {
"description": "Successful Response",
"content": {"application/vnd.api+json": {"schema": {}}},
},
},
"summary": "A",
"operationId": "a_a_get",
}
},
"/b": {
"get": {
"responses": {
"500": {
"description": "Error",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/Error"}
}
},
},
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
},
"summary": "B",
"operationId": "b_b_get",
}
},
},
"components": {
"schemas": {
"Error": {
"title": "Error",
"required": ["status", "title"],
"type": "object",
"properties": {
"status": {"title": "Status", "type": "string"},
"title": {"title": "Title", "type": "string"},
},
},
"JsonApiError": {
"title": "JsonApiError",
"required": ["errors"],
"type": "object",
"properties": {
"errors": {
"title": "Errors",
"type": "array",
"items": {"$ref": "#/components/schemas/Error"},
}
},
},
}
},
}
client = TestClient(app)
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_schema

View File

@@ -10,12 +10,25 @@ async def a():
return "a"
@router.get("/b", responses={502: {"description": "Error 2"}})
@router.get(
"/b",
responses={
502: {"description": "Error 2"},
"4XX": {"description": "Error with range, upper"},
},
)
async def b():
return "b"
@router.get("/c", responses={501: {"description": "Error 3"}})
@router.get(
"/c",
responses={
"400": {"description": "Error with str"},
"5xx": {"description": "Error with range, lower"},
"default": {"description": "A default response"},
},
)
async def c():
return "c"
@@ -43,6 +56,7 @@ openapi_schema = {
"get": {
"responses": {
"502": {"description": "Error 2"},
"4XX": {"description": "Error with range, upper"},
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
@@ -55,11 +69,13 @@ openapi_schema = {
"/c": {
"get": {
"responses": {
"501": {"description": "Error 3"},
"400": {"description": "Error with str"},
"5XX": {"description": "Error with range, lower"},
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"default": {"description": "A default response"},
},
"summary": "C",
"operationId": "c_c_get",

View File

@@ -0,0 +1,114 @@
import pytest
from fastapi import APIRouter, FastAPI
from fastapi.routing import APIRoute
from starlette.testclient import TestClient
app = FastAPI()
class APIRouteA(APIRoute):
x_type = "A"
class APIRouteB(APIRoute):
x_type = "B"
class APIRouteC(APIRoute):
x_type = "C"
router_a = APIRouter(route_class=APIRouteA)
router_b = APIRouter(route_class=APIRouteB)
router_c = APIRouter(route_class=APIRouteC)
@router_a.get("/")
def get_a():
return {"msg": "A"}
@router_b.get("/")
def get_b():
return {"msg": "B"}
@router_c.get("/")
def get_c():
return {"msg": "C"}
router_b.include_router(router=router_c, prefix="/c")
router_a.include_router(router=router_b, prefix="/b")
app.include_router(router=router_a, prefix="/a")
client = TestClient(app)
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/a/": {
"get": {
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
}
},
"summary": "Get A",
"operationId": "get_a_a__get",
}
},
"/a/b/": {
"get": {
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
}
},
"summary": "Get B",
"operationId": "get_b_a_b__get",
}
},
"/a/b/c/": {
"get": {
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
}
},
"summary": "Get C",
"operationId": "get_c_a_b_c__get",
}
},
},
}
@pytest.mark.parametrize(
"path,expected_status,expected_response",
[
("/a", 200, {"msg": "A"}),
("/a/b", 200, {"msg": "B"}),
("/a/b/c", 200, {"msg": "C"}),
("/openapi.json", 200, openapi_schema),
],
)
def test_get_path(path, expected_status, expected_response):
response = client.get(path)
assert response.status_code == expected_status
assert response.json() == expected_response
def test_route_classes():
routes = {}
r: APIRoute
for r in app.router.routes:
routes[r.path] = r
assert routes["/a/"].x_type == "A"
assert routes["/a/b/"].x_type == "B"
assert routes["/a/b/c/"].x_type == "C"

View File

@@ -0,0 +1,216 @@
from typing import Any
import orjson
from fastapi import APIRouter, FastAPI
from starlette.responses import HTMLResponse, JSONResponse, PlainTextResponse
from starlette.testclient import TestClient
class ORJSONResponse(JSONResponse):
media_type = "application/x-orjson"
def render(self, content: Any) -> bytes:
return orjson.dumps(content)
class OverrideResponse(JSONResponse):
media_type = "application/x-override"
app = FastAPI(default_response_class=ORJSONResponse)
router_a = APIRouter()
router_a_a = APIRouter()
router_a_b_override = APIRouter() # Overrides default class
router_b_override = APIRouter() # Overrides default class
router_b_a = APIRouter()
router_b_a_c_override = APIRouter() # Overrides default class again
@app.get("/")
def get_root():
return {"msg": "Hello World"}
@app.get("/override", response_class=PlainTextResponse)
def get_path_override():
return "Hello World"
@router_a.get("/")
def get_a():
return {"msg": "Hello A"}
@router_a.get("/override", response_class=PlainTextResponse)
def get_a_path_override():
return "Hello A"
@router_a_a.get("/")
def get_a_a():
return {"msg": "Hello A A"}
@router_a_a.get("/override", response_class=PlainTextResponse)
def get_a_a_path_override():
return "Hello A A"
@router_a_b_override.get("/")
def get_a_b():
return "Hello A B"
@router_a_b_override.get("/override", response_class=HTMLResponse)
def get_a_b_path_override():
return "Hello A B"
@router_b_override.get("/")
def get_b():
return "Hello B"
@router_b_override.get("/override", response_class=HTMLResponse)
def get_b_path_override():
return "Hello B"
@router_b_a.get("/")
def get_b_a():
return "Hello B A"
@router_b_a.get("/override", response_class=HTMLResponse)
def get_b_a_path_override():
return "Hello B A"
@router_b_a_c_override.get("/")
def get_b_a_c():
return "Hello B A C"
@router_b_a_c_override.get("/override", response_class=OverrideResponse)
def get_b_a_c_path_override():
return {"msg": "Hello B A C"}
router_b_a.include_router(
router_b_a_c_override, prefix="/c", default_response_class=HTMLResponse
)
router_b_override.include_router(router_b_a, prefix="/a")
router_a.include_router(router_a_a, prefix="/a")
router_a.include_router(
router_a_b_override, prefix="/b", default_response_class=PlainTextResponse
)
app.include_router(router_a, prefix="/a")
app.include_router(
router_b_override, prefix="/b", default_response_class=PlainTextResponse
)
client = TestClient(app)
orjson_type = "application/x-orjson"
text_type = "text/plain; charset=utf-8"
html_type = "text/html; charset=utf-8"
override_type = "application/x-override"
def test_app():
with client:
response = client.get("/")
assert response.json() == {"msg": "Hello World"}
assert response.headers["content-type"] == orjson_type
def test_app_override():
with client:
response = client.get("/override")
assert response.content == b"Hello World"
assert response.headers["content-type"] == text_type
def test_router_a():
with client:
response = client.get("/a")
assert response.json() == {"msg": "Hello A"}
assert response.headers["content-type"] == orjson_type
def test_router_a_override():
with client:
response = client.get("/a/override")
assert response.content == b"Hello A"
assert response.headers["content-type"] == text_type
def test_router_a_a():
with client:
response = client.get("/a/a")
assert response.json() == {"msg": "Hello A A"}
assert response.headers["content-type"] == orjson_type
def test_router_a_a_override():
with client:
response = client.get("/a/a/override")
assert response.content == b"Hello A A"
assert response.headers["content-type"] == text_type
def test_router_a_b():
with client:
response = client.get("/a/b")
assert response.content == b"Hello A B"
assert response.headers["content-type"] == text_type
def test_router_a_b_override():
with client:
response = client.get("/a/b/override")
assert response.content == b"Hello A B"
assert response.headers["content-type"] == html_type
def test_router_b():
with client:
response = client.get("/b")
assert response.content == b"Hello B"
assert response.headers["content-type"] == text_type
def test_router_b_override():
with client:
response = client.get("/b/override")
assert response.content == b"Hello B"
assert response.headers["content-type"] == html_type
def test_router_b_a():
with client:
response = client.get("/b/a")
assert response.content == b"Hello B A"
assert response.headers["content-type"] == text_type
def test_router_b_a_override():
with client:
response = client.get("/b/a/override")
assert response.content == b"Hello B A"
assert response.headers["content-type"] == html_type
def test_router_b_a_c():
with client:
response = client.get("/b/a/c")
assert response.content == b"Hello B A C"
assert response.headers["content-type"] == html_type
def test_router_b_a_c_override():
with client:
response = client.get("/b/a/c/override")
assert response.json() == {"msg": "Hello B A C"}
assert response.headers["content-type"] == override_type

View File

@@ -0,0 +1,206 @@
from fastapi import APIRouter, FastAPI
from starlette.responses import HTMLResponse, JSONResponse, PlainTextResponse
from starlette.testclient import TestClient
class OverrideResponse(JSONResponse):
media_type = "application/x-override"
app = FastAPI()
router_a = APIRouter()
router_a_a = APIRouter()
router_a_b_override = APIRouter() # Overrides default class
router_b_override = APIRouter() # Overrides default class
router_b_a = APIRouter()
router_b_a_c_override = APIRouter() # Overrides default class again
@app.get("/")
def get_root():
return {"msg": "Hello World"}
@app.get("/override", response_class=PlainTextResponse)
def get_path_override():
return "Hello World"
@router_a.get("/")
def get_a():
return {"msg": "Hello A"}
@router_a.get("/override", response_class=PlainTextResponse)
def get_a_path_override():
return "Hello A"
@router_a_a.get("/")
def get_a_a():
return {"msg": "Hello A A"}
@router_a_a.get("/override", response_class=PlainTextResponse)
def get_a_a_path_override():
return "Hello A A"
@router_a_b_override.get("/")
def get_a_b():
return "Hello A B"
@router_a_b_override.get("/override", response_class=HTMLResponse)
def get_a_b_path_override():
return "Hello A B"
@router_b_override.get("/")
def get_b():
return "Hello B"
@router_b_override.get("/override", response_class=HTMLResponse)
def get_b_path_override():
return "Hello B"
@router_b_a.get("/")
def get_b_a():
return "Hello B A"
@router_b_a.get("/override", response_class=HTMLResponse)
def get_b_a_path_override():
return "Hello B A"
@router_b_a_c_override.get("/")
def get_b_a_c():
return "Hello B A C"
@router_b_a_c_override.get("/override", response_class=OverrideResponse)
def get_b_a_c_path_override():
return {"msg": "Hello B A C"}
router_b_a.include_router(
router_b_a_c_override, prefix="/c", default_response_class=HTMLResponse
)
router_b_override.include_router(router_b_a, prefix="/a")
router_a.include_router(router_a_a, prefix="/a")
router_a.include_router(
router_a_b_override, prefix="/b", default_response_class=PlainTextResponse
)
app.include_router(router_a, prefix="/a")
app.include_router(
router_b_override, prefix="/b", default_response_class=PlainTextResponse
)
client = TestClient(app)
json_type = "application/json"
text_type = "text/plain; charset=utf-8"
html_type = "text/html; charset=utf-8"
override_type = "application/x-override"
def test_app():
with client:
response = client.get("/")
assert response.json() == {"msg": "Hello World"}
assert response.headers["content-type"] == json_type
def test_app_override():
with client:
response = client.get("/override")
assert response.content == b"Hello World"
assert response.headers["content-type"] == text_type
def test_router_a():
with client:
response = client.get("/a")
assert response.json() == {"msg": "Hello A"}
assert response.headers["content-type"] == json_type
def test_router_a_override():
with client:
response = client.get("/a/override")
assert response.content == b"Hello A"
assert response.headers["content-type"] == text_type
def test_router_a_a():
with client:
response = client.get("/a/a")
assert response.json() == {"msg": "Hello A A"}
assert response.headers["content-type"] == json_type
def test_router_a_a_override():
with client:
response = client.get("/a/a/override")
assert response.content == b"Hello A A"
assert response.headers["content-type"] == text_type
def test_router_a_b():
with client:
response = client.get("/a/b")
assert response.content == b"Hello A B"
assert response.headers["content-type"] == text_type
def test_router_a_b_override():
with client:
response = client.get("/a/b/override")
assert response.content == b"Hello A B"
assert response.headers["content-type"] == html_type
def test_router_b():
with client:
response = client.get("/b")
assert response.content == b"Hello B"
assert response.headers["content-type"] == text_type
def test_router_b_override():
with client:
response = client.get("/b/override")
assert response.content == b"Hello B"
assert response.headers["content-type"] == html_type
def test_router_b_a():
with client:
response = client.get("/b/a")
assert response.content == b"Hello B A"
assert response.headers["content-type"] == text_type
def test_router_b_a_override():
with client:
response = client.get("/b/a/override")
assert response.content == b"Hello B A"
assert response.headers["content-type"] == html_type
def test_router_b_a_c():
with client:
response = client.get("/b/a/c")
assert response.content == b"Hello B A C"
assert response.headers["content-type"] == html_type
def test_router_b_a_c_override():
with client:
response = client.get("/b/a/c/override")
assert response.json() == {"msg": "Hello B A C"}
assert response.headers["content-type"] == override_type

View File

@@ -0,0 +1,33 @@
import pytest
from fastapi import APIRouter, FastAPI
from starlette.testclient import TestClient
app = FastAPI()
router = APIRouter()
@router.get("")
def get_empty():
return ["OK"]
app.include_router(router, prefix="/prefix")
client = TestClient(app)
def test_use_empty():
with client:
response = client.get("/prefix")
assert response.json() == ["OK"]
response = client.get("/prefix/")
assert response.status_code == 404
def test_include_empty():
# if both include and router.path are empty - it should raise exception
with pytest.raises(Exception):
app.include_router(router)

View File

@@ -0,0 +1,136 @@
from fastapi import APIRouter, FastAPI
from starlette.testclient import TestClient
app = FastAPI()
user_router = APIRouter()
item_router = APIRouter()
@user_router.get("/")
def get_users():
return [{"user_id": "u1"}, {"user_id": "u2"}]
@user_router.get("/{user_id}")
def get_user(user_id: str):
return {"user_id": user_id}
@item_router.get("/")
def get_items(user_id: str = None):
if user_id is None:
return [{"item_id": "i1", "user_id": "u1"}, {"item_id": "i2", "user_id": "u2"}]
else:
return [{"item_id": "i2", "user_id": user_id}]
@item_router.get("/{item_id}")
def get_item(item_id: str, user_id: str = None):
if user_id is None:
return {"item_id": item_id}
else:
return {"item_id": item_id, "user_id": user_id}
app.include_router(user_router, prefix="/users")
app.include_router(item_router, prefix="/items")
app.include_router(item_router, prefix="/users/{user_id}/items")
client = TestClient(app)
def test_get_users():
"""Check that /users returns expected data"""
response = client.get("/users")
assert response.status_code == 200
assert response.json() == [{"user_id": "u1"}, {"user_id": "u2"}]
def test_get_user():
"""Check that /users/{user_id} returns expected data"""
response = client.get("/users/abc123")
assert response.status_code == 200
assert response.json() == {"user_id": "abc123"}
def test_get_items_1():
"""Check that /items returns expected data"""
response = client.get("/items")
assert response.status_code == 200
assert response.json() == [
{"item_id": "i1", "user_id": "u1"},
{"item_id": "i2", "user_id": "u2"},
]
def test_get_items_2():
"""Check that /items returns expected data with user_id specified"""
response = client.get("/items?user_id=abc123")
assert response.status_code == 200
assert response.json() == [{"item_id": "i2", "user_id": "abc123"}]
def test_get_item_1():
"""Check that /items/{item_id} returns expected data"""
response = client.get("/items/item01")
assert response.status_code == 200
assert response.json() == {"item_id": "item01"}
def test_get_item_2():
"""Check that /items/{item_id} returns expected data with user_id specified"""
response = client.get("/items/item01?user_id=abc123")
assert response.status_code == 200
assert response.json() == {"item_id": "item01", "user_id": "abc123"}
def test_get_users_items():
"""Check that /users/{user_id}/items returns expected data"""
response = client.get("/users/abc123/items")
assert response.status_code == 200
assert response.json() == [{"item_id": "i2", "user_id": "abc123"}]
def test_get_users_item():
"""Check that /users/{user_id}/items returns expected data"""
response = client.get("/users/abc123/items/item01")
assert response.status_code == 200
assert response.json() == {"item_id": "item01", "user_id": "abc123"}
def test_schema_1():
"""Check that the user_id is a required path parameter under /users"""
response = client.get("/openapi.json")
assert response.status_code == 200
r = response.json()
d = {
"required": True,
"schema": {"title": "User_Id", "type": "string"},
"name": "user_id",
"in": "path",
}
assert d in r["paths"]["/users/{user_id}"]["get"]["parameters"]
assert d in r["paths"]["/users/{user_id}/items/"]["get"]["parameters"]
def test_schema_2():
"""Check that the user_id is an optional query parameter under /items"""
response = client.get("/openapi.json")
assert response.status_code == 200
r = response.json()
d = {
"required": False,
"schema": {"title": "User_Id", "type": "string"},
"name": "user_id",
"in": "query",
}
assert d in r["paths"]["/items/{item_id}"]["get"]["parameters"]
assert d in r["paths"]["/items/"]["get"]["parameters"]

View File

@@ -54,3 +54,14 @@ def test_strings_in_custom_redoc():
body_content = html.body.decode()
assert redoc_js_url in body_content
assert redoc_favicon_url in body_content
def test_google_fonts_in_generated_redoc():
body_with_google_fonts = get_redoc_html(
openapi_url="/docs", title="title"
).body.decode()
assert "fonts.googleapis.com" in body_with_google_fonts
body_without_google_fonts = get_redoc_html(
openapi_url="/docs", title="title", with_google_fonts=False
).body.decode()
assert "fonts.googleapis.com" not in body_without_google_fonts

View File

@@ -0,0 +1,103 @@
from fastapi import Depends, FastAPI, Header
from starlette.status import HTTP_200_OK
from starlette.testclient import TestClient
app = FastAPI()
def get_header(*, someheader: str = Header(...)):
return someheader
def get_something_else(*, someheader: str = Depends(get_header)):
return f"{someheader}123"
@app.get("/")
def get_deps(dep1: str = Depends(get_header), dep2: str = Depends(get_something_else)):
return {"dep1": dep1, "dep2": dep2}
client = TestClient(app)
schema = {
"components": {
"schemas": {
"HTTPValidationError": {
"properties": {
"detail": {
"items": {"$ref": "#/components/schemas/ValidationError"},
"title": "Detail",
"type": "array",
}
},
"title": "HTTPValidationError",
"type": "object",
},
"ValidationError": {
"properties": {
"loc": {
"items": {"type": "string"},
"title": "Location",
"type": "array",
},
"msg": {"title": "Message", "type": "string"},
"type": {"title": "Error " "Type", "type": "string"},
},
"required": ["loc", "msg", "type"],
"title": "ValidationError",
"type": "object",
},
}
},
"info": {"title": "Fast API", "version": "0.1.0"},
"openapi": "3.0.2",
"paths": {
"/": {
"get": {
"operationId": "get_deps__get",
"parameters": [
{
"in": "header",
"name": "someheader",
"required": True,
"schema": {"title": "Someheader", "type": "string"},
}
],
"responses": {
"200": {
"content": {"application/json": {"schema": {}}},
"description": "Successful " "Response",
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
"description": "Validation " "Error",
},
},
"summary": "Get Deps",
}
}
},
}
def test_schema():
response = client.get("/openapi.json")
assert response.status_code == HTTP_200_OK
actual_schema = response.json()
assert actual_schema == schema
assert (
len(actual_schema["paths"]["/"]["get"]["parameters"]) == 1
) # primary goal of this test
def test_response():
response = client.get("/", headers={"someheader": "hello"})
assert response.status_code == HTTP_200_OK
assert response.json() == {"dep1": "hello", "dep2": "hello123"}

View File

@@ -0,0 +1,67 @@
import typing
from fastapi import Body, FastAPI
from pydantic import BaseModel
from starlette.testclient import TestClient
app = FastAPI()
media_type = "application/vnd.api+json"
# NOTE: These are not valid JSON:API resources
# but they are fine for testing requestBody with custom media_type
class Product(BaseModel):
name: str
price: float
class Shop(BaseModel):
name: str
@app.post("/products")
async def create_product(data: Product = Body(..., media_type=media_type, embed=True)):
pass # pragma: no cover
@app.post("/shops")
async def create_shop(
data: Shop = Body(..., media_type=media_type),
included: typing.List[Product] = Body([], media_type=media_type),
):
pass # pragma: no cover
create_product_request_body = {
"content": {
"application/vnd.api+json": {
"schema": {"$ref": "#/components/schemas/Body_create_product_products_post"}
}
},
"required": True,
}
create_shop_request_body = {
"content": {
"application/vnd.api+json": {
"schema": {"$ref": "#/components/schemas/Body_create_shop_shops_post"}
}
},
"required": True,
}
client = TestClient(app)
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
openapi_schema = response.json()
assert (
openapi_schema["paths"]["/products"]["post"]["requestBody"]
== create_product_request_body
)
assert (
openapi_schema["paths"]["/shops"]["post"]["requestBody"]
== create_shop_request_body
)

View File

@@ -21,18 +21,21 @@ class User(BaseModel):
username: str
def get_current_user(oauth_header: str = Security(reusable_oauth2)):
# Here we use string annotations to test them
def get_current_user(oauth_header: "str" = Security(reusable_oauth2)):
user = User(username=oauth_header)
return user
@app.post("/login")
def read_current_user(form_data: OAuth2PasswordRequestFormStrict = Depends()):
# Here we use string annotations to test them
def read_current_user(form_data: "OAuth2PasswordRequestFormStrict" = Depends()):
return form_data
@app.get("/users/me")
def read_current_user(current_user: User = Depends(get_current_user)):
# Here we use string annotations to test them
def read_current_user(current_user: "User" = Depends(get_current_user)):
return current_user

View File

@@ -1,8 +1,7 @@
from typing import List
import pytest
from fastapi import FastAPI
from pydantic import BaseModel, ValidationError
from pydantic import BaseModel
from starlette.testclient import TestClient
app = FastAPI()
@@ -14,38 +13,45 @@ class Item(BaseModel):
owner_ids: List[int] = None
@app.get("/items/invalid", response_model=Item)
def get_invalid():
return {"name": "invalid", "price": "foo"}
@app.get("/items/valid", response_model=Item)
def get_valid():
return {"name": "valid", "price": 1.0}
@app.get("/items/innerinvalid", response_model=Item)
def get_innerinvalid():
return {"name": "double invalid", "price": "foo", "owner_ids": ["foo", "bar"]}
@app.get("/items/coerce", response_model=Item)
def get_coerce():
return {"name": "coerce", "price": "1.0"}
@app.get("/items/invalidlist", response_model=List[Item])
def get_invalidlist():
@app.get("/items/validlist", response_model=List[Item])
def get_validlist():
return [
{"name": "foo"},
{"name": "bar", "price": "bar"},
{"name": "baz", "price": "baz"},
{"name": "bar", "price": 1.0},
{"name": "baz", "price": 2.0, "owner_ids": [1, 2, 3]},
]
client = TestClient(app)
def test_invalid():
with pytest.raises(ValidationError):
client.get("/items/invalid")
def test_valid():
response = client.get("/items/valid")
response.raise_for_status()
assert response.json() == {"name": "valid", "price": 1.0, "owner_ids": None}
def test_double_invalid():
with pytest.raises(ValidationError):
client.get("/items/innerinvalid")
def test_coerce():
response = client.get("/items/coerce")
response.raise_for_status()
assert response.json() == {"name": "coerce", "price": 1.0, "owner_ids": None}
def test_invalid_list():
with pytest.raises(ValidationError):
client.get("/items/invalidlist")
def test_validlist():
response = client.get("/items/validlist")
response.raise_for_status()
assert response.json() == [
{"name": "foo", "price": None, "owner_ids": None},
{"name": "bar", "price": 1.0, "owner_ids": None},
{"name": "baz", "price": 2.0, "owner_ids": [1, 2, 3]},
]

View File

@@ -0,0 +1,58 @@
from typing import List
from fastapi import FastAPI
from pydantic.dataclasses import dataclass
from starlette.testclient import TestClient
app = FastAPI()
@dataclass
class Item:
name: str
price: float = None
owner_ids: List[int] = None
@app.get("/items/valid", response_model=Item)
def get_valid():
return {"name": "valid", "price": 1.0}
@app.get("/items/coerce", response_model=Item)
def get_coerce():
return {"name": "coerce", "price": "1.0"}
@app.get("/items/validlist", response_model=List[Item])
def get_validlist():
return [
{"name": "foo"},
{"name": "bar", "price": 1.0},
{"name": "baz", "price": 2.0, "owner_ids": [1, 2, 3]},
]
client = TestClient(app)
def test_valid():
response = client.get("/items/valid")
response.raise_for_status()
assert response.json() == {"name": "valid", "price": 1.0, "owner_ids": None}
def test_coerce():
response = client.get("/items/coerce")
response.raise_for_status()
assert response.json() == {"name": "coerce", "price": 1.0, "owner_ids": None}
def test_validlist():
response = client.get("/items/validlist")
response.raise_for_status()
assert response.json() == [
{"name": "foo", "price": None, "owner_ids": None},
{"name": "bar", "price": 1.0, "owner_ids": None},
{"name": "baz", "price": 2.0, "owner_ids": [1, 2, 3]},
]

View File

@@ -0,0 +1,33 @@
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel
from starlette.testclient import TestClient
app = FastAPI()
class SubModel(BaseModel):
a: Optional[str] = "foo"
class Model(BaseModel):
x: Optional[int]
sub: SubModel
class ModelSubclass(Model):
y: int
@app.get("/", response_model=Model, response_model_skip_defaults=True)
def get() -> ModelSubclass:
return ModelSubclass(sub={}, y=1)
client = TestClient(app)
def test_return_defaults():
response = client.get("/")
assert response.json() == {"sub": {}}

View File

@@ -0,0 +1,28 @@
from fastapi import FastAPI
from starlette.testclient import TestClient
swagger_ui_init_oauth = {"clientId": "the-foo-clients", "appName": "The Predendapp"}
app = FastAPI(swagger_ui_init_oauth=swagger_ui_init_oauth)
@app.get("/items/")
async def read_items():
return {"id": "foo"}
client = TestClient(app)
def test_swagger_ui():
response = client.get("/docs")
assert response.status_code == 200
print(response.text)
assert f"ui.initOAuth" in response.text
assert f'"appName": "The Predendapp"' in response.text
assert f'"clientId": "the-foo-clients"' in response.text
def test_response():
response = client.get("/items/")
assert response.json() == {"id": "foo"}

View File

View File

@@ -0,0 +1,34 @@
import gzip
import json
import pytest
from starlette.requests import Request
from starlette.testclient import TestClient
from custom_request_and_route.tutorial001 import app
@app.get("/check-class")
async def check_gzip_request(request: Request):
return {"request_class": type(request).__name__}
client = TestClient(app)
@pytest.mark.parametrize("compress", [True, False])
def test_gzip_request(compress):
n = 1000
headers = {}
body = [1] * n
data = json.dumps(body).encode()
if compress:
data = gzip.compress(data)
headers["Content-Encoding"] = "gzip"
response = client.post("/sum", data=data, headers=headers)
assert response.json() == {"sum": n}
def test_request_class():
response = client.get("/check-class")
assert response.json() == {"request_class": "GzipRequest"}

View File

@@ -0,0 +1,27 @@
from starlette.testclient import TestClient
from custom_request_and_route.tutorial002 import app
client = TestClient(app)
def test_endpoint_works():
response = client.post("/", json=[1, 2, 3])
assert response.json() == 6
def test_exception_handler_body_access():
response = client.post("/", json={"numbers": [1, 2, 3]})
assert response.json() == {
"detail": {
"body": '{"numbers": [1, 2, 3]}',
"errors": [
{
"loc": ["body", "numbers"],
"msg": "value is not a valid list",
"type": "type_error.list",
}
],
}
}

View File

@@ -0,0 +1,18 @@
from starlette.testclient import TestClient
from custom_request_and_route.tutorial003 import app
client = TestClient(app)
def test_get():
response = client.get("/")
assert response.json() == {"message": "Not timed"}
assert "X-Response-Time" not in response.headers
def test_get_timed():
response = client.get("/timed")
assert response.json() == {"message": "It's the time of my life"}
assert "X-Response-Time" in response.headers
assert float(response.headers["X-Response-Time"]) > 0

View File

@@ -81,7 +81,7 @@ def test_get_validation_error():
response = client.get("/items/foo")
assert response.status_code == 400
validation_error_str_lines = [
b"1 validation error",
b"1 validation error for Request",
b"path -> item_id",
b" value is not a valid integer (type=type_error.integer)",
]

View File

@@ -0,0 +1,112 @@
from starlette.testclient import TestClient
from path_operation_advanced_configuration.tutorial003 import app
client = TestClient(app)
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/items/": {
"post": {
"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": "Create an item",
"description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item\n",
"operationId": "create_item_items__post",
"requestBody": {
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/Item"}
}
},
"required": True,
},
}
}
},
"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"},
"tags": {
"title": "Tags",
"uniqueItems": True,
"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_query_params_str_validations():
response = client.post("/items/", json={"name": "Foo", "price": 42})
assert response.status_code == 200
assert response.json() == {
"name": "Foo",
"price": 42,
"description": None,
"tax": None,
"tags": [],
}

View File

@@ -106,8 +106,9 @@ def test_openapi():
{
"detail": [
{
"ctx": {"enum_values": ["alexnet", "resnet", "lenet"]},
"loc": ["path", "model_name"],
"msg": "value is not a valid enumeration member",
"msg": "value is not a valid enumeration member; permitted: 'alexnet', 'resnet', 'lenet'",
"type": "type_error.enum",
}
]

View File

@@ -0,0 +1,51 @@
from typing import List
import pytest
from fastapi import FastAPI
from pydantic import BaseModel, ValidationError
from starlette.testclient import TestClient
app = FastAPI()
class Item(BaseModel):
name: str
price: float = None
owner_ids: List[int] = None
@app.get("/items/invalid", response_model=Item)
def get_invalid():
return {"name": "invalid", "price": "foo"}
@app.get("/items/innerinvalid", response_model=Item)
def get_innerinvalid():
return {"name": "double invalid", "price": "foo", "owner_ids": ["foo", "bar"]}
@app.get("/items/invalidlist", response_model=List[Item])
def get_invalidlist():
return [
{"name": "foo"},
{"name": "bar", "price": "bar"},
{"name": "baz", "price": "baz"},
]
client = TestClient(app)
def test_invalid():
with pytest.raises(ValidationError):
client.get("/items/invalid")
def test_double_invalid():
with pytest.raises(ValidationError):
client.get("/items/innerinvalid")
def test_invalid_list():
with pytest.raises(ValidationError):
client.get("/items/invalidlist")

View File

@@ -0,0 +1,53 @@
from typing import List
import pytest
from fastapi import FastAPI
from pydantic import ValidationError
from pydantic.dataclasses import dataclass
from starlette.testclient import TestClient
app = FastAPI()
@dataclass
class Item:
name: str
price: float = None
owner_ids: List[int] = None
@app.get("/items/invalid", response_model=Item)
def get_invalid():
return {"name": "invalid", "price": "foo"}
@app.get("/items/innerinvalid", response_model=Item)
def get_innerinvalid():
return {"name": "double invalid", "price": "foo", "owner_ids": ["foo", "bar"]}
@app.get("/items/invalidlist", response_model=List[Item])
def get_invalidlist():
return [
{"name": "foo"},
{"name": "bar", "price": "bar"},
{"name": "baz", "price": "baz"},
]
client = TestClient(app)
def test_invalid():
with pytest.raises(ValidationError):
client.get("/items/invalid")
def test_double_invalid():
with pytest.raises(ValidationError):
client.get("/items/innerinvalid")
def test_invalid_list():
with pytest.raises(ValidationError):
client.get("/items/invalidlist")