Compare commits

...

114 Commits

Author SHA1 Message Date
Sebastián Ramírez
cc4c13e4ae 🔖 Release 0.44.0, with support for Pydantic v1 and above! 🎉 2019-11-27 22:16:29 +01:00
Sebastián Ramírez
4f3764faa9 📝 Update release notes 2019-11-27 22:14:36 +01:00
Sebastián Ramírez
c27ad0dc26 💚 Make GitHub action Issue Manager run once every night 2019-11-27 22:14:01 +01:00
Sebastián Ramírez
6d0caf7522 👷 Add GitHub action Issue Manager (#742) 2019-11-27 21:43:56 +01:00
Sebastián Ramírez
06df32e84c 📝 Update release notes 2019-11-27 21:29:14 +01:00
bundabrg
28c089c029 ✏️ Fix typos in docs (#734) 2019-11-27 21:27:56 +01:00
Sebastián Ramírez
44b26bb64c 📝 Update release notes 2019-11-27 21:25:09 +01:00
Stéphane Wirtel
e04bae2286 🐛 Fix the usage of custom_encoder for jsonable_encoder #714 (#715) 2019-11-27 21:23:23 +01:00
Sebastián Ramírez
23f5940e8b 📝 Update release notes 2019-11-27 21:08:37 +01:00
OcasoProtal
4915cf0561 🐛 Fix XML example (#710) 2019-11-27 21:06:50 +01:00
Sebastián Ramírez
a1c9eff041 📝 Update release notes 2019-11-27 21:05:39 +01:00
Nicolas Marier
57cb3f3089 📝 Fix typos and wording in deployment docs (#700) 2019-11-27 21:03:41 +01:00
Sebastián Ramírez
bd6b3b07c5 📝 Update release notes 2019-11-27 21:01:56 +01:00
Nicolas Marier
3cf8b86dc1 📝 Clarify docs for APIRouter dependencies (#698) 2019-11-27 21:00:34 +01:00
Sebastián Ramírez
55165f292a 📝 Update release notes 2019-11-27 20:53:00 +01:00
François Voron
f3ddc7bdeb 🐛 Allow async class methods as dependencies (#681) 2019-11-27 20:51:30 +01:00
Sebastián Ramírez
4356cc9588 📝 Update release notes 2019-11-27 20:45:49 +01:00
euri10
cd9e87e60e 📝 Add FastAPI cheatsheet to links (#671) 2019-11-27 20:43:23 +01:00
Sebastián Ramírez
4834d87dcd 📝 Update release notes 2019-11-27 20:40:26 +01:00
Forest Monsen
7781cc0936 ✏️ Fix protocol separator typo (#647) 2019-11-27 20:37:19 +01:00
Sebastián Ramírez
23459d4a35 📝 Update release notes 2019-11-27 20:36:37 +01:00
dmontagu
ab2b86fe2c Add support for Pydantic v1 and above 🎉 (#646)
* Make compatible with pydantic v1

* Remove unused import

* Remove unused ignores

* Update pydantic version

* Fix minor formatting issue

*  Revert removing iterate_in_threadpool

*  Add backwards compatibility with Pydantic 0.32.2

with deprecation warnings

*  Update tests to not break when using Pydantic < 1.0.0

* 📝 Update docs for Pydantic version 1.0.0

* 📌 Update Pydantic range version to support from 0.32.2

* 🎨 Format test imports

*  Add support for Pydantic < 1.2 for populate_validators

*  Add backwards compatibility for Pydantic < 1.2.0 with required fields

* 📌 Relax requirement for Pydantic to < 2.0.0 🎉 🚀

* 💚 Update pragma coverage for older Pydantic versions
2019-11-27 20:32:02 +01:00
Sebastián Ramírez
90a5796b94 🔖 Release 0.43.0 2019-11-24 18:56:11 +01:00
Sebastián Ramírez
bb8a630fc3 📝 Update release notes 2019-11-24 15:12:56 +01:00
Nicolas Delaby
f5a503afae 📝 Replace guys by developers when a group of people is targeted (#645)
Just to make sure we include everyone, disregarding their gender.
2019-11-24 15:09:45 +01:00
Sebastián Ramírez
49fba853c2 📝 Update release notes 2019-11-24 15:06:31 +01:00
Steven Kalt
bac2f587b7 📝 Document overriding operationId for all path operations using their function names (#642) 2019-11-24 15:00:51 +01:00
Sebastián Ramírez
e1fd6785aa 📝 Update release notes 2019-11-24 14:25:51 +01:00
James Addison
4e50f53459 🐛 Fixing validator-caused incorrect output key order (#637) 2019-11-24 14:23:33 +01:00
Sebastián Ramírez
933d4327fb 📝 Update release notes 2019-11-24 14:18:03 +01:00
Daniel Brotsky
c7902dd23a Generate correct OpenAPI docs for responses with no content (#621) 2019-11-24 14:15:39 +01:00
Sebastián Ramírez
c5f5e63810 📝 Update release notes 2019-11-23 23:00:52 +01:00
Nico Stapelbroek
c3cc077fa9 📝 Remove $ sign from bash codeblocs in markdown (#613) 2019-11-23 22:59:15 +01:00
Sebastián Ramírez
c6f98c009f 📝 Update release notes 2019-11-23 22:57:47 +01:00
Sebastián Ramírez
e4206772cb 📝 Update release notes 2019-11-23 22:54:06 +01:00
svalouch
723ef07ccf 📝 Add documentation for self-serving static Swagger UI (#112) (#557) 2019-11-23 22:50:58 +01:00
François Voron
8609beb9ab 🚨 Fix black linting (#682) 2019-11-23 22:43:43 +01:00
Sebastián Ramírez
65536cbf63 🔖 Release version 0.42.0: Answer to the Ultimate Question of Life, the Universe, and Everything 2019-10-09 13:16:45 -05:00
Sebastián Ramírez
0192eab557 📝 Update release notes 2019-10-09 13:13:04 -05:00
Sebastián Ramírez
3f9f4a0f8f Add dependencies with yield (used as context managers) (#595)
*  Add development/testing dependencies for Python 3.6

*  Add concurrency submodule with contextmanager_in_threadpool

*  Add AsyncExitStack to ASGI scope in FastAPI app call

*  Use async stack for contextmanager-able dependencies

including running in threadpool sync dependencies

*  Add tests for contextmanager dependencies

including internal raise checks when exceptions should be handled and when not

*  Add test for fake asynccontextmanager raiser

* 🐛 Fix mypy errors and coverage

* 🔇 Remove development logs and prints

*  Add tests for sub-contextmanagers, background tasks, and sync functions

* 🐛 Fix mypy errors for Python 3.7

* 💬 Fix error texts for clarity

* 📝 Add docs for dependencies with yield

*  Update SQL with SQLAlchemy tutorial to use dependencies with yield

and add an alternative with a middleware (from the old tutorial)

*  Update SQL tests to remove DB file during the same tests

*  Add tests for example with middleware

as a copy from the tests with dependencies with yield, removing the DB in the tests

* ✏️ Fix typos with suggestions from code review

Co-Authored-By: dmontagu <35119617+dmontagu@users.noreply.github.com>
2019-10-09 13:01:58 -05:00
Sebastián Ramírez
380e3731a8 📝 Update release notes 2019-10-09 12:48:01 -05:00
Samuel Colvin
d6d99b86cb 🐛 Fix sitemap.xml in website, fix #597 (#598) 2019-10-09 12:45:44 -05:00
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
146 changed files with 5163 additions and 741 deletions

19
.github/workflows/main.yml vendored Normal file
View File

@@ -0,0 +1,19 @@
on:
schedule:
- cron: "0 0 * * *"
jobs:
issue-manager:
runs-on: ubuntu-latest
steps:
- uses: tiangolo/issue-manager@master
with:
token: ${{ secrets.GITHUB_TOKEN }}
config: >
{
"answered": {
"users": ["tiangolo", "dmontagu"],
"delay": 864000,
"message": "Assuming the original issue was solved, it will be automatically closed now. But feel free to add more comments or create new issues."
}
}

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 = "==1.0.0"
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
@@ -79,13 +90,13 @@ FastAPI stands on the shoulders of giants:
## Installation
```bash
$ pip install fastapi
pip install fastapi
```
You will also need an ASGI server, for production such as <a href="http://www.uvicorn.org" target="_blank">Uvicorn</a> or <a href="https://gitlab.com/pgjones/hypercorn" target="_blank">Hypercorn</a>.
```bash
$ pip install uvicorn
pip install uvicorn
```
## Example

View File

@@ -142,12 +142,12 @@ Another big feature required by APIs is <abbr title="reading and converting to P
Webargs is a tool that was made to provide that on top of several frameworks, including Flask.
It uses Marshmallow underneath to do the data validation. And it was created by the same guys.
It uses Marshmallow underneath to do the data validation. And it was created by the same developers.
It's a great tool and I have used it a lot too, before having **FastAPI**.
!!! info
Webargs was created by the same Marshmallow guys.
Webargs was created by the same Marshmallow developers.
!!! check "Inspired **FastAPI** to"
Have automatic validation of incoming request data.
@@ -171,7 +171,7 @@ But then, we have again the problem of having a micro-syntax, inside of a Python
The editor can't help much with that. And if we modify parameters or Marshmallow schemas and forget to also modify that YAML docstring, the generated schema would be obsolete.
!!! info
APISpec was created by the same Marshmallow guys.
APISpec was created by the same Marshmallow developers.
!!! check "Inspired **FastAPI** to"
@@ -198,7 +198,7 @@ Using it led to the creation of several Flask full-stack generators. These are t
And these same full-stack generators were the base of the <a href="/project-generation/" target="_blank">**FastAPI** project generator</a>.
!!! info
Flask-apispec was created by the same Marshmallow guys.
Flask-apispec was created by the same Marshmallow developers.
!!! check "Inspired **FastAPI** to"
Generate the OpenAPI schema automatically, from the same code that defines serialization and validation.

View File

@@ -170,32 +170,33 @@ Now, from a developer's perspective, here are several things to have in mind whi
* So, the certificate and encryption handling is done before HTTP.
* TCP doesn't know about "domains". Only about IP addresses.
* The information about the specific domain requested goes in the HTTP data.
* The HTTPS certificates "certificate" a certain domain, but the protocol and encryption happen at the TCP level, before knowing which domain is being dealt with.
* The HTTPS certificates "certify" a certain domain, but the protocol and encryption happen at the TCP level, before knowing which domain is being dealt with.
* By default, that would mean that you can only have one HTTPS certificate per IP address.
* No matter how big is your server and how small each application you have there might be. But...
* No matter how big your server is or how small each application you have on it might be.
* There is a solution to this, however.
* There's an extension to the TLS protocol (the one handling the encryption at the TCP level, before HTTP) called <a href="https://en.wikipedia.org/wiki/Server_Name_Indication" target="_blank"><abbr title="Server Name Indication">SNI</abbr></a>.
* This SNI extension allows one single server (with a single IP address) to have several HTTPS certificates and server multiple HTTPS domains/applications.
* For this to work, a single component (program) running in the server, listening in the public IP address, must have all the HTTPS certificates in the server.
* After having a secure connection, the communication protocol is the same HTTP.
* It goes encrypted, but the encrypted contents are the same HTTP protocol.
* This SNI extension allows one single server (with a single IP address) to have several HTTPS certificates and serve multiple HTTPS domains/applications.
* For this to work, a single component (program) running on the server, listening on the public IP address, must have all the HTTPS certificates in the server.
* After obtaining a secure connection, the communication protocol is still HTTP.
* The contents are encrypted, even though they are being sent with the HTTP protocol.
It is a common practice to have one program/HTTP server running in the server (the machine, host, etc) and managing all the HTTPS parts, sending the decrypted HTTP requests to the actual HTTP application running in the same server (the **FastAPI** application, in this case), take the HTTP response from the application, encrypt it using the appropriate certificate and sending it back to the client using HTTPS. This server is ofter called a <a href="https://en.wikipedia.org/wiki/TLS_termination_proxy" target="_blank">TLS Termination Proxy</a>.
It is a common practice to have one program/HTTP server running on the server (the machine, host, etc.) and managing all the HTTPS parts : sending the decrypted HTTP requests to the actual HTTP application running in the same server (the **FastAPI** application, in this case), take the HTTP response from the application, encrypt it using the appropriate certificate and sending it back to the client using HTTPS. This server is often called a <a href="https://en.wikipedia.org/wiki/TLS_termination_proxy" target="_blank">TLS Termination Proxy</a>.
### Let's Encrypt
Up to some years ago, these HTTPS certificates were sold by trusted third-parties.
Before Let's Encrypt, these HTTPS certificates were sold by trusted third-parties.
The process to acquire one of these certificates used to be cumbersome, require quite some paperwork and the certificates were quite expensive.
But then <a href="https://letsencrypt.org/" target="_blank">Let's Encrypt</a> was created.
It is a project from the Linux Foundation. It provides HTTPS certificates for free. In an automated way. These certificates use all the standard cryptographic security, and are short lived (about 3 months), so, the security is actually increased, by reducing their lifespan.
It is a project from the Linux Foundation. It provides HTTPS certificates for free. In an automated way. These certificates use all the standard cryptographic security, and are short lived (about 3 months), so the security is actually better because of their reduced lifespan.
The domains are securely verified and the certificates are generated automatically. This also allows automatizing the renewal of these certificates.
The domains are securely verified and the certificates are generated automatically. This also allows automating the renewal of these certificates.
The idea is to automatize the acquisition and renewal of these certificates, so that you can have secure HTTPS, free, forever.
The idea is to automate the acquisition and renewal of these certificates, so that you can have secure HTTPS, for free, forever.
### Traefik
@@ -204,7 +205,7 @@ The idea is to automatize the acquisition and renewal of these certificates, so
It has integration with Let's Encrypt. So, it can handle all the HTTPS parts, including certificate acquisition and renewal.
It also has integrations with Docker. So, you can declare your domains in each application configurations and have it read those configurations, generate the HTTPS certificates and serve HTTPS to your application, all automatically. Without requiring any change in its configuration.
It also has integrations with Docker. So, you can declare your domains in each application configurations and have it read those configurations, generate the HTTPS certificates and serve HTTPS to your application automatically, without requiring any change in its configuration.
---
@@ -230,7 +231,7 @@ It is designed to be integrated with this Docker Swarm cluster with Traefik and
You can generate a project in about 2 min.
The generated project has instructions to deploy it, doing it takes other 2 min.
The generated project has instructions to deploy it, doing it takes another 2 min.
## Alternatively, deploy **FastAPI** without Docker

View File

@@ -25,6 +25,18 @@ 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>.
* <a href="https://gitlab.com/euri10/fastapi_cheatsheet" target="_blank">A FastAPI and Swagger UI visual cheatsheet</a> by <a href="https://gitlab.com/euri10" target="_blank">@euri10</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
@@ -79,13 +90,13 @@ FastAPI stands on the shoulders of giants:
## Installation
```bash
$ pip install fastapi
pip install fastapi
```
You will also need an ASGI server, for production such as <a href="http://www.uvicorn.org" target="_blank">Uvicorn</a> or <a href="https://gitlab.com/pgjones/hypercorn" target="_blank">Hypercorn</a>.
```bash
$ pip install uvicorn
pip install uvicorn
```
## Example

View File

@@ -1,5 +1,116 @@
## Latest changes
## 0.44.0
* Add GitHub action [Issue Manager](https://github.com/tiangolo/issue-manager). PR [#742](https://github.com/tiangolo/fastapi/pull/742).
* Fix typos in docs. PR [734](https://github.com/tiangolo/fastapi/pull/734) by [@bundabrg](https://github.com/bundabrg).
* Fix usage of `custom_encoder` in `jsonable_encoder`. PR [#715](https://github.com/tiangolo/fastapi/pull/715) by [@matrixise](https://github.com/matrixise).
* Fix invalid XML example. PR [710](https://github.com/tiangolo/fastapi/pull/710) by [@OcasoProtal](https://github.com/OcasoProtal).
* Fix typos and update wording in deployment docs. PR [#700](https://github.com/tiangolo/fastapi/pull/700) by [@marier-nico](https://github.com/tiangolo/fastapi/pull/700).
* Add note about dependencies in `APIRouter` docs. PR [#698](https://github.com/tiangolo/fastapi/pull/698) by [@marier-nico](https://github.com/marier-nico).
* Add support for async class methods as dependencies [#681](https://github.com/tiangolo/fastapi/pull/681) by [@frankie567](https://github.com/frankie567).
* Add FastAPI with Swagger UI cheatsheet to external links. PR [#671](https://github.com/tiangolo/fastapi/pull/671) by [@euri10](https://github.com/euri10).
* Fix typo in HTTP protocol in CORS example. PR [#647](https://github.com/tiangolo/fastapi/pull/647) by [@forestmonster](https://github.com/forestmonster).
* Add support for Pydantic versions `1.0.0` and above, with temporary (deprecated) backwards compatibility for Pydantic `0.32.2`. PR [#646](https://github.com/tiangolo/fastapi/pull/646) by [@dmontagu](https://github.com/dmontagu).
## 0.43.0
* Update docs to reduce gender bias. PR [#645](https://github.com/tiangolo/fastapi/pull/645) by [@ticosax](https://github.com/ticosax).
* Add docs about [overriding the `operationId` for all the *path operations*](https://fastapi.tiangolo.com/tutorial/path-operation-advanced-configuration/#using-the-path-operation-function-name-as-the-operationid) based on their function name. PR [#642](https://github.com/tiangolo/fastapi/pull/642) by [@SKalt](https://github.com/SKalt).
* Fix validators in models generating an incorrect key order. PR [#637](https://github.com/tiangolo/fastapi/pull/637) by [@jaddison](https://github.com/jaddison).
* Generate correct OpenAPI docs for responses with no content. PR [#621](https://github.com/tiangolo/fastapi/pull/621) by [@brotskydotcom](https://github.com/brotskydotcom).
* Remove `$` from Bash code blocks in docs for consistency. PR [#613](https://github.com/tiangolo/fastapi/pull/613) by [@nstapelbroek](https://github.com/nstapelbroek).
* Add docs for [self-serving docs' (Swagger UI) static assets](https://fastapi.tiangolo.com/tutorial/extending-openapi/#self-hosting-javascript-and-css-for-docs), e.g. to use the docs offline, or without Internet. Initial PR [#557](https://github.com/tiangolo/fastapi/pull/557) by [@svalouch](https://github.com/svalouch).
* Fix `black` linting after upgrade. PR [#682](https://github.com/tiangolo/fastapi/pull/682) by [@frankie567](https://github.com/frankie567).
## 0.42.0
* Add dependencies with `yield`, a.k.a. exit steps, context managers, cleanup, teardown, ...
* This allows adding extra code after a dependency is done. It can be used, for example, to close database connections.
* Dependencies with `yield` can be normal or `async`, **FastAPI** will run normal dependencies in a threadpool.
* They can be combined with normal dependencies.
* It's possible to have arbitrary trees/levels of dependencies with `yield` and exit steps are handled in the correct order automatically.
* It works by default in Python 3.7 or above. For Python 3.6, it requires the extra backport dependencies:
* `async-exit-stack`
* `async-generator`
* New docs at [Dependencies with `yield`](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-with-yield/).
* Updated database docs [SQL (Relational) Databases: Main **FastAPI** app](https://fastapi.tiangolo.com/tutorial/sql-databases/#main-fastapi-app).
* PR [#595](https://github.com/tiangolo/fastapi/pull/595).
* Fix `sitemap.xml` in website. PR [#598](https://github.com/tiangolo/fastapi/pull/598) by [@samuelcolvin](https://github.com/samuelcolvin).
## 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

@@ -1,13 +1,13 @@
from fastapi import Body, FastAPI
from pydantic import BaseModel, Schema
from pydantic import BaseModel, Field
app = FastAPI()
class Item(BaseModel):
name: str
description: str = Schema(None, title="The description of the item", max_length=300)
price: float = Schema(..., gt=0, description="The price must be greater than zero")
description: str = Field(None, title="The description of the item", max_length=300)
price: float = Field(..., gt=0, description="The price must be greater than zero")
tax: float = None

View File

@@ -31,7 +31,7 @@ async def read_item(item_id: str):
async def update_item(item_id: str, item: Item):
stored_item_data = items[item_id]
stored_item_model = Item(**stored_item_data)
update_data = item.dict(skip_defaults=True)
update_data = item.dict(exclude_unset=True)
updated_item = stored_item_model.copy(update=update_data)
items[item_id] = jsonable_encoder(updated_item)
return updated_item

View File

@@ -6,8 +6,8 @@ app = FastAPI()
origins = [
"http://localhost.tiangolo.com",
"https://localhost.tiangolo.com",
"http:localhost",
"http:localhost:8080",
"http://localhost",
"http://localhost:8080",
]
app.add_middleware(

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

@@ -1,21 +1,6 @@
from fastapi import Depends, FastAPI
app = FastAPI()
class FixedContentQueryChecker:
def __init__(self, fixed_content: str):
self.fixed_content = fixed_content
def __call__(self, q: str = ""):
if q:
return self.fixed_content in q
return False
checker = FixedContentQueryChecker("bar")
@app.get("/query-checker/")
async def read_query_check(fixed_content_included: bool = Depends(checker)):
return {"fixed_content_in_query": fixed_content_included}
async def get_db():
db = DBSession()
try:
yield db
finally:
db.close()

View File

@@ -0,0 +1,25 @@
from fastapi import Depends
async def dependency_a():
dep_a = generate_dep_a()
try:
yield dep_a
finally:
dep_a.close()
async def dependency_b(dep_a=Depends(dependency_a)):
dep_b = generate_dep_b()
try:
yield dep_b
finally:
dep_b.close(dep_a)
async def dependency_c(dep_b=Depends(dependency_b)):
dep_c = generate_dep_c()
try:
yield dep_c
finally:
dep_c.close(dep_b)

View File

@@ -0,0 +1,25 @@
from fastapi import Depends
async def dependency_a():
dep_a = generate_dep_a()
try:
yield dep_a
finally:
dep_a.close()
async def dependency_b(dep_a=Depends(dependency_a)):
dep_b = generate_dep_b()
try:
yield dep_b
finally:
dep_b.close(dep_a)
async def dependency_c(dep_b=Depends(dependency_b)):
dep_c = generate_dep_c()
try:
yield dep_c
finally:
dep_c.close(dep_b)

View File

@@ -0,0 +1,14 @@
class MySuperContextManager:
def __init__(self):
self.db = DBSession()
def __enter__(self):
return self.db
def __exit__(self, exc_type, exc_value, traceback):
self.db.close()
async def get_db():
with MySuperContextManager() as db:
yield db

View File

@@ -0,0 +1,21 @@
from fastapi import Depends, FastAPI
app = FastAPI()
class FixedContentQueryChecker:
def __init__(self, fixed_content: str):
self.fixed_content = fixed_content
def __call__(self, q: str = ""):
if q:
return self.fixed_content in q
return False
checker = FixedContentQueryChecker("bar")
@app.get("/query-checker/")
async def read_query_check(fixed_content_included: bool = Depends(checker)):
return {"fixed_content_in_query": fixed_content_included}

View File

@@ -0,0 +1,41 @@
from fastapi import FastAPI
from fastapi.openapi.docs import (
get_redoc_html,
get_swagger_ui_html,
get_swagger_ui_oauth2_redirect_html,
)
from starlette.staticfiles import StaticFiles
app = FastAPI(docs_url=None, redoc_url=None)
app.mount("/static", StaticFiles(directory="static"), name="static")
@app.get("/docs", include_in_schema=False)
async def custom_swagger_ui_html():
return get_swagger_ui_html(
openapi_url=app.openapi_url,
title=app.title + " - Swagger UI",
oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url,
swagger_js_url="/static/swagger-ui-bundle.js",
swagger_css_url="/static/swagger-ui.css",
)
@app.get(app.swagger_ui_oauth2_redirect_url, include_in_schema=False)
async def swagger_ui_redirect():
return get_swagger_ui_oauth2_redirect_html()
@app.get("/redoc", include_in_schema=False)
async def redoc_html():
return get_redoc_html(
openapi_url=app.openapi_url,
title=app.title + " - ReDoc",
redoc_js_url="/static/redoc.standalone.js",
)
@app.get("/users/{username}")
async def read_user(username: str):
return {"message": f"Hello {username}"}

View File

@@ -1,6 +1,5 @@
from fastapi import FastAPI
from pydantic import BaseModel
from pydantic.types import EmailStr
from pydantic import BaseModel, EmailStr
app = FastAPI()

View File

@@ -1,6 +1,5 @@
from fastapi import FastAPI
from pydantic import BaseModel
from pydantic.types import EmailStr
from pydantic import BaseModel, EmailStr
app = FastAPI()

View File

@@ -1,8 +1,24 @@
from fastapi import FastAPI
from fastapi.routing import APIRoute
app = FastAPI()
@app.get("/items/", include_in_schema=False)
@app.get("/items/")
async def read_items():
return [{"item_id": "Foo"}]
def use_route_names_as_operation_ids(app: FastAPI) -> None:
"""
Simplify operation IDs so that generated API clients have simpler function
names.
Should be called only after all routes have been added.
"""
for route in app.routes:
if isinstance(route, APIRoute):
route.operation_id = route.name # in this case, 'read_items'
use_route_names_as_operation_ids(app)

View File

@@ -0,0 +1,8 @@
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/", include_in_schema=False)
async def read_items():
return [{"item_id": "Foo"}]

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

@@ -6,12 +6,11 @@ app = FastAPI()
@app.get("/legacy/")
def get_legacy_data():
data = """
<?xml version="1.0"?>
data = """<?xml version="1.0"?>
<shampoo>
<Header>
Apply shampoo here.
<Header>
</Header>
<Body>
You'll have to use soap here.
</Body>

View File

@@ -1,6 +1,5 @@
from fastapi import FastAPI
from pydantic import BaseModel
from pydantic.types import EmailStr
from pydantic import BaseModel, EmailStr
app = FastAPI()

View File

@@ -1,6 +1,5 @@
from fastapi import FastAPI
from pydantic import BaseModel
from pydantic.types import EmailStr
from pydantic import BaseModel, EmailStr
app = FastAPI()

View File

@@ -21,6 +21,6 @@ items = {
}
@app.get("/items/{item_id}", response_model=Item, response_model_skip_defaults=True)
@app.get("/items/{item_id}", response_model=Item, response_model_exclude_unset=True)
async def read_item(item_id: str):
return items[item_id]

View File

@@ -0,0 +1,64 @@
from typing import List
from fastapi import Depends, FastAPI, HTTPException
from sqlalchemy.orm import Session
from starlette.requests import Request
from starlette.responses import Response
from . import crud, models, schemas
from .database import SessionLocal, engine
models.Base.metadata.create_all(bind=engine)
app = FastAPI()
@app.middleware("http")
async def db_session_middleware(request: Request, call_next):
response = Response("Internal server error", status_code=500)
try:
request.state.db = SessionLocal()
response = await call_next(request)
finally:
request.state.db.close()
return response
# Dependency
def get_db(request: Request):
return request.state.db
@app.post("/users/", response_model=schemas.User)
def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
db_user = crud.get_user_by_email(db, email=user.email)
if db_user:
raise HTTPException(status_code=400, detail="Email already registered")
return crud.create_user(db=db, user=user)
@app.get("/users/", response_model=List[schemas.User])
def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
users = crud.get_users(db, skip=skip, limit=limit)
return users
@app.get("/users/{user_id}", response_model=schemas.User)
def read_user(user_id: int, db: Session = Depends(get_db)):
db_user = crud.get_user(db, user_id=user_id)
if db_user is None:
raise HTTPException(status_code=404, detail="User not found")
return db_user
@app.post("/users/{user_id}/items/", response_model=schemas.Item)
def create_item_for_user(
user_id: int, item: schemas.ItemCreate, db: Session = Depends(get_db)
):
return crud.create_user_item(db=db, item=item, user_id=user_id)
@app.get("/items/", response_model=List[schemas.Item])
def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
items = crud.get_items(db, skip=skip, limit=limit)
return items

View File

@@ -2,8 +2,6 @@ from typing import List
from fastapi import Depends, FastAPI, HTTPException
from sqlalchemy.orm import Session
from starlette.requests import Request
from starlette.responses import Response
from . import crud, models, schemas
from .database import SessionLocal, engine
@@ -13,20 +11,13 @@ models.Base.metadata.create_all(bind=engine)
app = FastAPI()
@app.middleware("http")
async def db_session_middleware(request: Request, call_next):
response = Response("Internal server error", status_code=500)
try:
request.state.db = SessionLocal()
response = await call_next(request)
finally:
request.state.db.close()
return response
# Dependency
def get_db(request: Request):
return request.state.db
def get_db():
try:
db = SessionLocal()
yield db
finally:
db.close()
@app.post("/users/", response_model=schemas.User)

View File

@@ -174,6 +174,11 @@ For example, you can add an additional media type of `image/png`, declaring that
!!! note
Notice that you have to return the image using a `FileResponse` directly.
!!! info
Unless you specify a different media type explicitly in your `responses` parameter, FastAPI will assume the response has the same media type as the main response class (default `application/json`).
But if you have specified a custom response class with `None` as its media type, FastAPI will use `application/json` for any additional response that has an associated model.
## Combining information
You can also combine response information from multiple places, including the `response_model`, `status_code`, and `responses` parameters.

View File

@@ -41,7 +41,7 @@ Later, for your production application, you might want to use a database server
```
!!! tip
If you where connecting to a different database (e.g. PostgreSQL), you would need to change the `DATABASE_URL`.
If you were connecting to a different database (e.g. PostgreSQL), you would need to change the `DATABASE_URL`.
## Create the tables

View File

@@ -246,7 +246,7 @@ We can also add a list of `tags` that will be applied to all the *path operation
And we can add predefined `responses` that will be included in all the *path operations* too.
And we can add a list of `dependencies` that will be added to all the *path operations* in the router and will be executed/solved for each request made to them.
And we can add a list of `dependencies` that will be added to all the *path operations* in the router and will be executed/solved for each request made to them. Note that, much like dependencies in *path operation decorators*, no value will be passed to your *path operation function*.
```Python hl_lines="8 9 10 14 15 16 17 18 19 20"
{!./src/bigger_applications/app/main.py!}

View File

@@ -1,6 +1,6 @@
The same way you can declare additional validation and metadata in path operation function parameters with `Query`, `Path` and `Body`, you can declare validation and metadata inside of Pydantic models using `Schema`.
The same way you can declare additional validation and metadata in path operation function parameters with `Query`, `Path` and `Body`, you can declare validation and metadata inside of Pydantic models using Pydantic's `Field`.
## Import Schema
## Import `Field`
First, you have to import it:
@@ -9,32 +9,34 @@ First, you have to import it:
```
!!! warning
Notice that `Schema` is imported directly from `pydantic`, not from `fastapi` as are all the rest (`Query`, `Path`, `Body`, etc).
Notice that `Field` is imported directly from `pydantic`, not from `fastapi` as are all the rest (`Query`, `Path`, `Body`, etc).
## Declare model attributes
You can then use `Schema` with model attributes:
You can then use `Field` with model attributes:
```Python hl_lines="9 10"
{!./src/body_schema/tutorial001.py!}
```
`Schema` works the same way as `Query`, `Path` and `Body`, it has all the same parameters, etc.
`Field` works the same way as `Query`, `Path` and `Body`, it has all the same parameters, etc.
!!! note "Technical Details"
Actually, `Query`, `Path` and others you'll see next are subclasses of a common `Param` which is itself a subclass of Pydantic's `Schema`.
Actually, `Query`, `Path` and others you'll see next create objects of subclasses of a common `Param` class, which is itself a subclass of Pydantic's `FieldInfo` class.
`Body` is also a subclass of `Schema` directly. And there are others you will see later that are subclasses of `Body`.
And Pydantic's `Field` returns an instance of `FieldInfo` as well.
But remember that when you import `Query`, `Path` and others from `fastapi`, <a href="https://fastapi.tiangolo.com/tutorial/path-params-numeric-validations/#recap" target="_blank">those are actually functions that return classes of the same name</a>.
`Body` also returns objects of a subclass of `FieldInfo` directly. And there are others you will see later that are subclasses of the `Body` class.
Remember that when you import `Query`, `Path`, and others from `fastapi`, <a href="https://fastapi.tiangolo.com/tutorial/path-params-numeric-validations/#recap" target="_blank">those are actually functions that return classes of the same name</a>.
!!! tip
Notice how each model's attribute with a type, default value and `Schema` has the same structure as a path operation function's parameter, with `Schema` instead of `Path`, `Query` and `Body`.
Notice how each model's attribute with a type, default value and `Field` has the same structure as a path operation function's parameter, with `Field` instead of `Path`, `Query` and `Body`.
## Schema extras
In `Schema`, `Path`, `Query`, `Body` and others you'll see later, you can declare extra parameters apart from those described before.
In `Field`, `Path`, `Query`, `Body` and others you'll see later, you can declare extra parameters apart from those described before.
Those parameters will be added as-is to the output JSON Schema.
@@ -55,6 +57,6 @@ And it would look in the `/docs` like this:
## Recap
You can use Pydantic's `Schema` to declare extra validations and metadata for model attributes.
You can use Pydantic's `Field` to declare extra validations and metadata for model attributes.
You can also use the extra keyword arguments to pass additional JSON Schema metadata.
You can also use the extra keyword arguments to pass additional JSON Schema metadata.

View File

@@ -41,15 +41,15 @@ This means that you can send only the data that you want to update, leaving the
But this guide shows you, more or less, how they are intended to be used.
### Using Pydantic's `skip_defaults` parameter
### Using Pydantic's `exclude_unset` parameter
If you want to receive partial updates, it's very useful to use the parameter `skip_defaults` in Pydantic's model's `.dict()`.
If you want to receive partial updates, it's very useful to use the parameter `exclude_unset` in Pydantic's model's `.dict()`.
Like `item.dict(skip_defaults=True)`.
Like `item.dict(exclude_unset=True)`.
That would generate a `dict` with only the data that was set when creating the `item` model, excluding default values.
Then you can use this to generate a `dict` with only the data that was set, omitting default values:
Then you can use this to generate a `dict` with only the data that was set (sent in the request), omitting default values:
```Python hl_lines="34"
{!./src/body_updates/tutorial002.py!}
@@ -72,7 +72,7 @@ In summary, to apply partial updates you would:
* (Optionally) use `PATCH` instead of `PUT`.
* Retrieve the stored data.
* Put that data in a Pydantic model.
* Generate a `dict` without default values from the input model (using `skip_defaults`).
* Generate a `dict` without default values from the input model (using `exclude_unset`).
* This way you can update only the values actually set by the user, instead of overriding values already stored with default values in your model.
* Create a copy of the stored model, updating it's attributes with the received partial updates (using the `update` parameter).
* Convert the copied model to something that can be stored in your DB (for example, using the `jsonable_encoder`).

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

@@ -15,6 +15,9 @@ The contents that you return from your *path operation function* will be put ins
And if that `Response` has a JSON media type (`application/json`), like is the case with the `JSONResponse` and `UJSONResponse`, the data you return will be automatically converted (and filtered) with any Pydantic `response_model` that you declared in the *path operation decorator*.
!!! note
If you use a response class with no media type, FastAPI will expect your response to have no content, so it will not document the response format in its generated OpenAPI docs.
## Use `UJSONResponse`
For example, if you are squeezing performance, you can install and use `ujson` and set the response to be Starlette's `UJSONResponse`.

View File

@@ -1,4 +1,4 @@
!!! danger
!!! warning
This is, more or less, an "advanced" chapter.
If you are just starting with **FastAPI** you might want to skip this chapter and come back to it later.
@@ -22,7 +22,7 @@ Not the class itself (which is already a callable), but an instance of that clas
To do that, we declare a method `__call__`:
```Python hl_lines="10"
{!./src/dependencies/tutorial007.py!}
{!./src/dependencies/tutorial011.py!}
```
In this case, this `__call__` is what **FastAPI** will use to check for additional parameters and sub-dependencies, and this is what will be called to pass a value to the parameter in your *path operation function* later.
@@ -32,7 +32,7 @@ In this case, this `__call__` is what **FastAPI** will use to check for addition
And now, we can use `__init__` to declare the parameters of the instance that we can use to "parameterize" the dependency:
```Python hl_lines="7"
{!./src/dependencies/tutorial007.py!}
{!./src/dependencies/tutorial011.py!}
```
In this case, **FastAPI** won't ever touch or care about `__init__`, we will use it directly in our code.
@@ -42,7 +42,7 @@ In this case, **FastAPI** won't ever touch or care about `__init__`, we will use
We could create an instance of this class with:
```Python hl_lines="16"
{!./src/dependencies/tutorial007.py!}
{!./src/dependencies/tutorial011.py!}
```
And that way we are able to "parameterize" our dependency, that now has `"bar"` inside of it, as the attribute `checker.fixed_content`.
@@ -60,7 +60,7 @@ checker(q="somequery")
...and pass whatever that returns as the value of the dependency in our path operation function as the parameter `fixed_content_included`:
```Python hl_lines="20"
{!./src/dependencies/tutorial007.py!}
{!./src/dependencies/tutorial011.py!}
```
!!! tip

View File

@@ -2,7 +2,7 @@ Before diving deeper into the **Dependency Injection** system, let's upgrade the
## A `dict` from the previous example
In the previous example, we where returning a `dict` from our dependency ("dependable"):
In the previous example, we are returning a `dict` from our dependency ("dependable"):
```Python hl_lines="7"
{!./src/dependencies/tutorial001.py!}

View File

@@ -0,0 +1,153 @@
# Dependencies with `yield`
FastAPI supports dependencies that do some <abbr title='sometimes also called "exit", "cleanup", "teardown", "close", "context managers", ...'>extra steps after finishing</abbr>.
To do this, use `yield` instead of `return`, and write the extra steps after.
!!! tip
Make sure to use `yield` one single time.
!!! info
For this to work, you need to use **Python 3.7** or above, or in **Python 3.6**, install the "backports":
```bash
pip install async-exit-stack async-generator
```
This installs <a href="https://github.com/sorcio/async_exit_stack" target="_blank">async-exit-stack</a> and <a href="https://github.com/python-trio/async_generator" target="_blank">async-generator</a>.
!!! note "Technical Details"
Any function that is valid to use with:
* <a href="https://docs.python.org/3/library/contextlib.html#contextlib.contextmanager" target="_blank">`@contextlib.contextmanager`</a> or
* <a href="https://docs.python.org/3/library/contextlib.html#contextlib.asynccontextmanager" target="_blank">`@contextlib.asynccontextmanager`</a>
would be valid to use as a **FastAPI** dependency.
In fact, FastAPI uses those two decorators internally.
## A database dependency with `yield`
For example, you could use this to create a database session and close it after finishing.
Only the code prior to and including the `yield` statement is executed before sending a response:
```Python hl_lines="2 3 4"
{!./src/dependencies/tutorial007.py!}
```
The yielded value is what is injected into *path operations* and other dependencies:
```Python hl_lines="4"
{!./src/dependencies/tutorial007.py!}
```
The code following the `yield` statement is executed after the response has been delivered:
```Python hl_lines="5 6"
{!./src/dependencies/tutorial007.py!}
```
!!! tip
You can use `async` or normal functions.
**FastAPI** will do the right thing with each, the same as with normal dependencies.
## A dependency with `yield` and `try`
If you use a `try` block in a dependency with `yield`, you'll receive any exception that was thrown when using the dependency.
For example, if some code at some point in the middle, in another dependency or in a *path operation*, made a database transaction "rollback" or create any other error, you will receive the exception in your dependency.
So, you can look for that specific exception inside the dependency with `except SomeException`.
In the same way, you can use `finally` to make sure the exit steps are executed, no matter if there was an exception or not.
```Python hl_lines="3 5"
{!./src/dependencies/tutorial007.py!}
```
## Sub-dependencies with `yield`
You can have sub-dependencies and "trees" of sub-dependencies of any size and shape, and any or all of them can use `yield`.
**FastAPI** will make sure that the "exit code" in each dependency with `yield` is run in the correct order.
For example, `dependency_c` can have a dependency on `dependency_b`, and `dependency_b` on `dependency_a`:
```Python hl_lines="4 12 20"
{!./src/dependencies/tutorial008.py!}
```
And all of them can use `yield`.
In this case `dependency_c`, to execute its exit code, needs the value from `dependency_b` (here named `dep_b`) to still be available.
And, in turn, `dependency_b` needs the value from `dependency_a` (here named `dep_a`) to be available for its exit code.
```Python hl_lines="16 17 24 25"
{!./src/dependencies/tutorial008.py!}
```
The same way, you could have dependencies with `yield` and `return` mixed.
And you could have a single dependency that requires several other dependencies with `yield`, etc.
You can have any combinations of dependencies that you want.
**FastAPI** will make sure everything is run in the correct order.
!!! note "Technical Details"
This works thanks to Python's <a href="https://docs.python.org/3/library/contextlib.html" target="_blank">Context Managers</a>.
**FastAPI** uses them internally to achieve this.
## Context Managers
### What are "Context Managers"
"Context Managers" are any of those Python objects that you can use in a `with` statement.
For example, <a href="https://docs.python.org/3/tutorial/inputoutput.html#reading-and-writing-files" target="_blank">you can use `with` to read a file</a>:
```Python
with open("./somefile.txt") as f:
contents = f.read()
print(contents)
```
Underneath, the `open("./somefile.txt")` returns an object that is a called a "Context Manager".
When the `with` block finishes, it makes sure to close the file, even if there were exceptions.
When you create a dependency with `yield`, **FastAPI** will internally convert it to a context manager, and combine it with some other related tools.
### Using context managers in dependencies with `yield`
!!! warning
This is, more or less, an "advanced" idea.
If you are just starting with **FastAPI** you might want to skip it for now.
In Python, you can create context managers by <a href="https://docs.python.org/3/reference/datamodel.html#context-managers" target="_blank">creating a class with two methods: `__enter__()` and `__exit__()`</a>.
You can also use them with **FastAPI** dependencies with `yield` by using
`with` or `async with` statements inside of the dependency function:
```Python hl_lines="1 2 3 4 5 6 7 8 9 13"
{!./src/dependencies/tutorial010.py!}
```
!!! tip
Another way to create a context manager is with:
* <a href="https://docs.python.org/3/library/contextlib.html#contextlib.contextmanager" target="_blank">`@contextlib.contextmanager`</a> or
* <a href="https://docs.python.org/3/library/contextlib.html#contextlib.asynccontextmanager" target="_blank">`@contextlib.asynccontextmanager`</a>
using them to decorate a function with a single `yield`.
That's what **FastAPI** uses internally for dependencies with `yield`.
But you don't have to use the decorators for FastAPI dependencies (and you shouldn't).
FastAPI will do it for you internally.

View File

@@ -88,3 +88,156 @@ Now you can replace the `.openapi()` method with your new function.
Once you go to <a href="http://127.0.0.1:8000/redoc" target="_blank">http://127.0.0.1:8000/redoc</a> you will see that you are using your custom logo (in this example, **FastAPI**'s logo):
<img src="/img/tutorial/extending-openapi/image01.png">
## Self-hosting JavaScript and CSS for docs
The API docs use **Swagger UI** and **ReDoc**, and each of those need some JavaScript and CSS files.
By default, those files are served from a <abbr title="Content Delivery Network: A service, normally composed of several servers, that provides static files, like JavaScript and CSS. It's commonly used to serve those files from the server closer to the client, improving performance.">CDN</abbr>.
But it's possible to customize it, you can set a specific CDN, or serve the files yourself.
That's useful, for example, if you need your app to keep working even while offline, without open Internet access, or in a local network.
Here you'll see how to serve those files yourself, in the same FastAPI app, and configure the docs to use them.
### Project file structure
Let's say your project file structure looks like this:
```
.
├── app
│ ├── __init__.py
│ ├── main.py
```
Now create a directory to store those static files.
Your new file structure could look like this:
```
.
├── app
│   ├── __init__.py
│   ├── main.py
└── static/
```
### Download the files
Download the static files needed for the docs and put them on that `static/` directory.
You can probably right-click each link and select an option similar to `Save link as...`.
**Swagger UI** uses the files:
* <a href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui-bundle.js">`swagger-ui-bundle.js`</a>
* <a href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui.css" target="_blank">`swagger-ui.css`</a>
And **ReDoc** uses the file:
* <a href="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js" target="_blank">`redoc.standalone.js`</a>
After that, your file structure could look like:
```
.
├── app
│   ├── __init__.py
│   ├── main.py
└── static
├── redoc.standalone.js
├── swagger-ui-bundle.js
└── swagger-ui.css
```
### Install `aiofiles`
Now you need to install `aiofiles`:
```bash
pip install aiofiles
```
### Serve the static files
* Import `StaticFiles` from Starlette.
* "Mount" it the same way you would <a href="https://fastapi.tiangolo.com/tutorial/sub-applications-proxy/" target="_blank">mount a Sub-Application</a>.
```Python hl_lines="7 11"
{!./src/extending_openapi/tutorial002.py!}
```
### Test the static files
Start your application and go to <a href="http://127.0.0.1:8000/static/redoc.standalone.js" target="_blank">http://127.0.0.1:8000/static/redoc.standalone.js</a>.
You should see a very long JavaScript file for **ReDoc**.
It could start with something like:
```JavaScript
/*!
* ReDoc - OpenAPI/Swagger-generated API Reference Documentation
* -------------------------------------------------------------
* Version: "2.0.0-rc.18"
* Repo: https://github.com/Redocly/redoc
*/
!function(e,t){"object"==typeof exports&&"object"==typeof m
...
```
That confirms that you are being able to serve static files from your app, and that you placed the static files for the docs in the correct place.
Now we can configure the app to use those static files for the docs.
### Disable the automatic docs
The first step is to disable the automatic docs, as those use the CDN by default.
To disable them, set their URLs to `None` when creating your `FastAPI` app:
```Python hl_lines="9"
{!./src/extending_openapi/tutorial002.py!}
```
### Include the custom docs
Now you can create the *path operations* for the custom docs.
You can re-use FastAPI's internal functions to create the HTML pages for the docs, and pass them the needed arguments:
* `openapi_url`: the URL where the HTML page for the docs can get the OpenAPI schema for your API. You can use here the attribute `app.openapi_url`.
* `title`: the title of your API.
* `oauth2_redirect_url`: you can use `app.swagger_ui_oauth2_redirect_url` here to use the default.
* `swagger_js_url`: the URL where the HTML for your Swagger UI docs can get the **JavaScript** file. This is the one that your own app is now serving.
* `swagger_css_url`: the URL where the HTML for your Swagger UI docs can get the **CSS** file. This is the one that your own app is now serving.
And similarly for ReDoc...
```Python hl_lines="2 3 4 5 6 14 15 16 17 18 19 20 21 22 25 26 27 30 31 32 33 34 35 36"
{!./src/extending_openapi/tutorial002.py!}
```
!!! tip
The *path operation* for `swagger_ui_redirect` is a helper for when you use OAuth2.
If you integrate your API with an OAuth2 provider, you will be able to authenticate and come back to the API docs with the acquired credentials. And interact with it using the real OAuth2 authentication.
Swagger UI will handle it behind the scenes for you, but it needs this "redirect" helper.
### Create a *path operation* to test it
Now, to be able to test that everything works, create a path operation:
```Python hl_lines="39 40 41"
{!./src/extending_openapi/tutorial002.py!}
```
### Test it
Now, you should be able to disconnect your WiFi, go to your docs at <a href="http://127.0.0.1:8000/docs" target="_blank">http://127.0.0.1:8000/docs</a>, and reload the page.
And even without Internet, you would be able to see the docs for your API and interact with it.

View File

@@ -15,7 +15,7 @@ This is especially the case for user models, because:
Here's a general idea of how the models could look like with their password fields and the places where they are used:
```Python hl_lines="8 10 15 21 23 32 34 39 40"
```Python hl_lines="7 9 14 20 22 27 28 31 32 33 38 39"
{!./src/extra_models/tutorial001.py!}
```
@@ -148,7 +148,7 @@ All the data conversion, validation, documentation, etc. will still work as norm
That way, we can declare just the differences between the models (with plaintext `password`, with `hashed_password` and without password):
```Python hl_lines="8 14 15 18 19 22 23"
```Python hl_lines="7 13 14 17 18 21 22"
{!./src/extra_models/tutorial002.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

@@ -11,10 +11,40 @@ You would have to make sure that it is unique for each operation.
{!./src/path_operation_advanced_configuration/tutorial001.py!}
```
### Using the *path operation function* name as the operationId
If you want to use your APIs' function names as `operationId`s, you can iterate over all of them and override each *path operation's* `operation_id` using their `APIRoute.name`.
You should do it after adding all your *path operations*.
```Python hl_lines="2 12 13 14 15 16 17 18 19 20 21 24"
{!./src/path_operation_advanced_configuration/tutorial002.py!}
```
!!! tip
If you manually call `app.openapi()`, you should update the `operationId`s before that.
!!! warning
If you do this, you have to make sure each one of your *path operation functions* has a unique name.
Even if they are in different modules (Python files).
## Exclude from OpenAPI
To exclude a path operation from the generated OpenAPI schema (and thus, from the automatic documentation systems), use the parameter `include_in_schema` and set it to `False`;
```Python hl_lines="6"
{!./src/path_operation_advanced_configuration/tutorial002.py!}
{!./src/path_operation_advanced_configuration/tutorial003.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/tutorial004.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

@@ -33,13 +33,13 @@ But most importantly:
Here we are declaring a `UserIn` model, it will contain a plaintext password:
```Python hl_lines="8 10"
```Python hl_lines="7 9"
{!./src/response_model/tutorial002.py!}
```
And we are using this model to declare our input and the same model to declare our output:
```Python hl_lines="16 17"
```Python hl_lines="15 16"
{!./src/response_model/tutorial002.py!}
```
@@ -56,19 +56,19 @@ But if we use the same model for another path operation, we could be sending our
We can instead create an input model with the plaintext password and an output model without it:
```Python hl_lines="8 10 15"
```Python hl_lines="7 9 14"
{!./src/response_model/tutorial003.py!}
```
Here, even though our path operation function is returning the same input user that contains the password:
```Python hl_lines="23"
```Python hl_lines="22"
{!./src/response_model/tutorial003.py!}
```
...we declared the `response_model` to be our model `UserOut`, that doesn't include the password:
```Python hl_lines="21"
```Python hl_lines="20"
{!./src/response_model/tutorial003.py!}
```
@@ -100,15 +100,15 @@ but you might want to omit them from the result if they were not actually stored
For example, if you have models with many optional attributes in a NoSQL database, but you don't want to send very long JSON responses full of default values.
### Use the `response_model_skip_defaults` parameter
### Use the `response_model_exclude_unset` parameter
You can set the *path operation decorator* parameter `response_model_skip_defaults=True`:
You can set the *path operation decorator* parameter `response_model_exclude_unset=True`:
```Python hl_lines="24"
{!./src/response_model/tutorial004.py!}
```
and those default values won't be included in the response.
and those default values won't be included in the response, only the values actually set.
So, if you send a request to that *path operation* for the item with ID `foo`, the response (not including default values) will be:
@@ -120,7 +120,7 @@ So, if you send a request to that *path operation* for the item with ID `foo`, t
```
!!! info
FastAPI uses Pydantic model's `.dict()` with <a href="https://pydantic-docs.helpmanual.io/#copying" target="_blank">its `skip_defaults` parameter</a> to achieve this.
FastAPI uses Pydantic model's `.dict()` with <a href="https://pydantic-docs.helpmanual.io/usage/exporting_models/#modeldict" target="_blank">its `exclude_unset` parameter</a> to achieve this.
#### Data with values for fields with defaults
@@ -194,4 +194,4 @@ If you forget to use a `set` and use a `list` or `tuple` instead, FastAPI will s
Use the path operation decorator's parameter `response_model` to define response models and especially to ensure private data is filtered out.
Use `response_model_skip_defaults` to return only the values explicitly set.
Use `response_model_exclude_unset` to return only the values explicitly set.

View File

@@ -22,6 +22,10 @@ It will:
<img src="/img/tutorial/response-status-code/image01.png">
!!! note
Some response codes (see the next section) indicate that the response does not have a body.
FastAPI knows this, and will produce OpenAPI docs that state there is no response body.
## About HTTP status codes
@@ -34,11 +38,12 @@ These status codes have a name associated to recognize them, but the important p
In short:
* `100` and above are for "Information". You rarely use them directly.
* `100` and above are for "Information". You rarely use them directly. Responses with these status codes cannot have a body.
* **`200`** and above are for "Successful" responses. These are the ones you would use the most.
* `200` is the default status code, which means everything was "OK".
* Another example would be `201`, "Created". It is commonly used after creating a new record in the database.
* `300` and above are for "Redirection".
* A special case is `204`, "No Content". This response is used when there is no content to return to the client, and so the response must not have a body.
* **`300`** and above are for "Redirection". Responses with these status codes may or may not have a body, except for `304`, "Not Modified", which must not have one.
* **`400`** and above are for "Client error" responses. These are the second type you would probably use the most.
* An example is `404`, for a "Not Found" response.
* For generic errors from the client, you can just use `400`.

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

@@ -427,21 +427,30 @@ And you would also use Alembic for "migrations" (that's its main job).
A "migration" is the set of steps needed whenever you change the structure of your SQLAlchemy models, add a new attribute, etc. to replicate those changes in the database, add a new column, a new table, etc.
### Create a middleware to handle sessions
### Create a dependency
Now use the `SessionLocal` class we created in the `sql_app/databases.py` file.
!!! info
For this to work, you need to use **Python 3.7** or above, or in **Python 3.6**, install the "backports":
```bash
pip install async-exit-stack async-generator
```
This installs <a href="https://github.com/sorcio/async_exit_stack" target="_blank">async-exit-stack</a> and <a href="https://github.com/python-trio/async_generator" target="_blank">async-generator</a>.
You can also use the alternative method with a "middleware" explained at the end.
Now use the `SessionLocal` class we created in the `sql_app/databases.py` file to create a dependency.
We need to have an independent database session/connection (`SessionLocal`) per request, use the same session through all the request and then close it after the request is finished.
And then a new session will be created for the next request.
For that, we will create a new middleware.
For that, we will create a new dependency with `yield`, as explained before in the section about <a href="https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-with-yield/" target="_blank">Dependencies with `yield`</a>.
A "middleware" is a function that is always executed for each request, and have code before and after the request.
Our dependency will create a new SQLAlchemy `SessionLocal` that will be used in a single request, and then close it once the request is finished.
This middleware (just a function) will create a new SQLAlchemy `SessionLocal` for each request, add it to the request and then close it once the request is finished.
```Python hl_lines="16 17 18 19 20 21 22 23 24"
```Python hl_lines="15 16 17 18 19 20"
{!./src/sql_databases/sql_app/main.py!}
```
@@ -452,21 +461,11 @@ This middleware (just a function) will create a new SQLAlchemy `SessionLocal` fo
This way we make sure the database session is always closed after the request. Even if there was an exception while processing the request.
#### About `request.state`
And then, when using the dependency in a *path operation function*, we declare it with the type `Session` we imported directly from SQLAlchemy.
<a href="https://www.starlette.io/requests/#other-state" target="_blank">`request.state` is a property of each Starlette `Request` object</a>, it is there to store arbitrary objects attached to the request itself, like the database session in this case.
This will then give us better editor support inside the *path operation function*, because the editor will know that the `db` parameter is of type `Session`:
For us in this case, it helps us ensuring a single database session is used through all the request, and then closed afterwards (in the middleware).
### Create a dependency
To simplify the code, reduce repetition and get better editor support, we will create a dependency that returns this same database session from the request.
And when using the dependency in a path operation function, we declare it with the type `Session` we imported directly from SQLAlchemy.
This will then give us better editor support inside the path operation function, because the editor will know that the `db` parameter is of type `Session`.
```Python hl_lines="28 29"
```Python hl_lines="24 32 38 47 53"
{!./src/sql_databases/sql_app/main.py!}
```
@@ -479,22 +478,16 @@ This will then give us better editor support inside the path operation function,
Now, finally, here's the standard **FastAPI** *path operations* code.
```Python hl_lines="32 33 34 35 36 37 40 41 42 43 46 47 48 49 50 51 54 55 56 57 58 61 62 63 64 65"
```Python hl_lines="23 24 25 26 27 28 31 32 33 34 37 38 39 40 41 42 45 46 47 48 49 52 53 54 55"
{!./src/sql_databases/sql_app/main.py!}
```
We are creating the database session before each request, attaching it to the request, and then closing it afterwards.
We are creating the database session before each request in the dependency with `yield`, and then closing it afterwards.
All of this is done in the middleware explained above.
Then, in the dependency `get_db()` we are extracting the database session from the request.
And then we can create the dependency in the path operation function, to get that session directly.
And then we can create the required dependency in the path operation function, to get that session directly.
With that, we can just call `crud.get_user` directly from inside of the path operation function and use that session.
Having this 3-step process (middleware, dependency, path operation) you get better support/checks/completion in all the path operation functions while reducing code repetition.
!!! tip
Notice that the values you return are SQLAlchemy models, or lists of SQLAlchemy models.
@@ -507,7 +500,7 @@ Having this 3-step process (middleware, dependency, path operation) you get bett
### About `def` vs `async def`
Here we are using SQLAlchemy code inside of the path operation function, and, in turn, it will go and communicate with an external database.
Here we are using SQLAlchemy code inside of the path operation function and in the dependency, and, in turn, it will go and communicate with an external database.
That could potentially require some "waiting".
@@ -523,7 +516,7 @@ user = await db.query(User).first()
user = db.query(User).first()
```
Then we should declare the path operation without `async def`, just with a normal `def`, as:
Then we should declare the *path operation functions* and the dependency without `async def`, just with a normal `def`, as:
```Python hl_lines="2"
@app.get("/users/{user_id}", response_model=schemas.User)
@@ -548,8 +541,8 @@ For example, in a background task worker with <a href="http://www.celeryproject.
## Review all the files
Remember you should have a directory named `my_super_project` that contains a sub-directory called `sql_app`.
`sql_app` should have the following files:
`sql_app` should have the following files:
* `sql_app/__init__.py`: is an empty file.
@@ -591,9 +584,6 @@ You can copy this code and use it as is.
In fact, the code shown here is part of the tests. As most of the code in these docs.
You can copy it as is.
Then you can run it with Uvicorn:
```bash
@@ -615,3 +605,51 @@ It will look like this:
<img src="/img/tutorial/sql-databases/image02.png">
You can also use an online SQLite browser like <a href="https://inloop.github.io/sqlite-viewer/" target="_blank">SQLite Viewer</a> or <a href="https://extendsclass.com/sqlite-browser.html" target="_blank">ExtendsClass</a>.
## Alternative DB session with middleware
If you can't use dependencies with `yield` -- for example, if you are not using **Python 3.7** and can't install the "backports" mentioned above for **Python 3.6** -- you can set up the session in a "middleware" in a similar way.
A "middleware" is basically a function that is always executed for each request, with some code executed before, and some code executed after the endpoint function.
### Create a middleware
The middleware we'll add (just a function) will create a new SQLAlchemy `SessionLocal` for each request, add it to the request and then close it once the request is finished.
```Python hl_lines="16 17 18 19 20 21 22 23 24"
{!./src/sql_databases/sql_app/alt_main.py!}
```
!!! info
We put the creation of the `SessionLocal()` and handling of the requests in a `try` block.
And then we close it in the `finally` block.
This way we make sure the database session is always closed after the request. Even if there was an exception while processing the request.
### About `request.state`
<a href="https://www.starlette.io/requests/#other-state" target="_blank">`request.state` is a property of each Starlette `Request` object</a>. It is there to store arbitrary objects attached to the request itself, like the database session in this case.
For us in this case, it helps us ensure a single database session is used through all the request, and then closed afterwards (in the middleware).
### Dependencies with `yield` or middleware
Adding a **middleware** here is similar to what a dependency with `yield` does, with some differences:
* It requires more code and is a bit more complex.
* The middleware has to be an `async` function.
* If there is code in it that has to "wait" for the network, it could "block" your application there and degrade performance a bit.
* Although it's probably not very problematic here with the way `SQLAlchemy` works.
* But if you added more code to the middleware that had a lot of <abbr title="input and output">I/O</abbr> waiting, it could then be problematic.
* A middleware is run for *every* request.
* So, a connection will be created for every request.
* Even when the *path operation* that handles that request didn't need the DB.
!!! tip
It's probably better to use dependencies with `yield` when they are enough for the use case.
!!! info
Dependencies with `yield` were added recently to **FastAPI**.
A previous version of this tutorial only had the examples with a middleware and there are probably several applications using the middleware for database session management.

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.44.0"
from starlette.background import BackgroundTasks

View File

@@ -1,6 +1,8 @@
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.concurrency import AsyncExitStack
from fastapi.encoders import DictIntStrAny, SetIntStr
from fastapi.exception_handlers import (
http_exception_handler,
request_validation_exception_handler,
@@ -13,12 +15,15 @@ from fastapi.openapi.docs import (
)
from fastapi.openapi.utils import get_openapi
from fastapi.params import Depends
from fastapi.utils import warning_response_model_skip_defaults_deprecated
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
from starlette.responses import HTMLResponse, JSONResponse, Response
from starlette.routing import BaseRoute
from starlette.types import Receive, Scope, Send
class FastAPI(Starlette):
@@ -32,12 +37,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 +63,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 +105,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)
@@ -122,6 +133,14 @@ class FastAPI(Starlette):
RequestValidationError, request_validation_exception_handler
)
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if AsyncExitStack:
async with AsyncExitStack() as stack:
scope["fastapi_astack"] = stack
await super().__call__(scope, receive, send)
else:
await super().__call__(scope, receive, send) # pragma: no cover
def add_api_route(
self,
path: str,
@@ -138,14 +157,17 @@ 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,
response_model_skip_defaults: bool = None,
response_model_exclude_unset: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Type[Response] = None,
name: str = None,
) -> None:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
self.router.add_api_route(
path,
endpoint=endpoint,
@@ -163,9 +185,11 @@ class FastAPI(Starlette):
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults,
response_model_exclude_unset=bool(
response_model_exclude_unset or 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,14 +208,18 @@ 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,
response_model_skip_defaults: bool = None,
response_model_exclude_unset: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Type[Response] = None,
name: str = None,
) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
def decorator(func: Callable) -> Callable:
self.router.add_api_route(
path,
@@ -210,9 +238,11 @@ class FastAPI(Starlette):
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults,
response_model_exclude_unset=bool(
response_model_exclude_unset or 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 +269,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 +277,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,14 +295,17 @@ 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,
response_model_skip_defaults: bool = None,
response_model_exclude_unset: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Type[Response] = None,
name: str = None,
) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
return self.router.get(
path,
response_model=response_model,
@@ -285,9 +321,11 @@ class FastAPI(Starlette):
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults,
response_model_exclude_unset=bool(
response_model_exclude_unset or 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,14 +343,17 @@ 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,
response_model_skip_defaults: bool = None,
response_model_exclude_unset: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Type[Response] = None,
name: str = None,
) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
return self.router.put(
path,
response_model=response_model,
@@ -328,9 +369,11 @@ class FastAPI(Starlette):
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults,
response_model_exclude_unset=bool(
response_model_exclude_unset or 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,14 +391,17 @@ 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,
response_model_skip_defaults: bool = None,
response_model_exclude_unset: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Type[Response] = None,
name: str = None,
) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
return self.router.post(
path,
response_model=response_model,
@@ -371,9 +417,11 @@ class FastAPI(Starlette):
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults,
response_model_exclude_unset=bool(
response_model_exclude_unset or 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,14 +439,17 @@ 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,
response_model_skip_defaults: bool = None,
response_model_exclude_unset: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Type[Response] = None,
name: str = None,
) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
return self.router.delete(
path,
response_model=response_model,
@@ -414,9 +465,11 @@ class FastAPI(Starlette):
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
operation_id=operation_id,
response_model_skip_defaults=response_model_skip_defaults,
response_model_exclude_unset=bool(
response_model_exclude_unset or 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,14 +487,17 @@ 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,
response_model_skip_defaults: bool = None,
response_model_exclude_unset: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Type[Response] = None,
name: str = None,
) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
return self.router.options(
path,
response_model=response_model,
@@ -457,9 +513,11 @@ class FastAPI(Starlette):
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults,
response_model_exclude_unset=bool(
response_model_exclude_unset or 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,14 +535,17 @@ 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,
response_model_skip_defaults: bool = None,
response_model_exclude_unset: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Type[Response] = None,
name: str = None,
) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
return self.router.head(
path,
response_model=response_model,
@@ -500,9 +561,11 @@ class FastAPI(Starlette):
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults,
response_model_exclude_unset=bool(
response_model_exclude_unset or 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,14 +583,17 @@ 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,
response_model_skip_defaults: bool = None,
response_model_exclude_unset: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Type[Response] = None,
name: str = None,
) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
return self.router.patch(
path,
response_model=response_model,
@@ -543,9 +609,11 @@ class FastAPI(Starlette):
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults,
response_model_exclude_unset=bool(
response_model_exclude_unset or 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,14 +631,17 @@ 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,
response_model_skip_defaults: bool = None,
response_model_exclude_unset: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Type[Response] = None,
name: str = None,
) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
return self.router.trace(
path,
response_model=response_model,
@@ -586,8 +657,10 @@ class FastAPI(Starlette):
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults,
response_model_exclude_unset=bool(
response_model_exclude_unset or 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,
)

46
fastapi/concurrency.py Normal file
View File

@@ -0,0 +1,46 @@
from typing import Any, Callable
from starlette.concurrency import iterate_in_threadpool # noqa
from starlette.concurrency import run_in_threadpool
asynccontextmanager_error_message = """
FastAPI's contextmanager_in_threadpool require Python 3.7 or above,
or the backport for Python 3.6, installed with:
pip install async-generator
"""
def _fake_asynccontextmanager(func: Callable) -> Callable:
def raiser(*args: Any, **kwargs: Any) -> Any:
raise RuntimeError(asynccontextmanager_error_message)
return raiser
try:
from contextlib import asynccontextmanager # type: ignore
except ImportError:
try:
from async_generator import asynccontextmanager # type: ignore
except ImportError: # pragma: no cover
asynccontextmanager = _fake_asynccontextmanager
try:
from contextlib import AsyncExitStack # type: ignore
except ImportError:
try:
from async_exit_stack import AsyncExitStack # type: ignore
except ImportError: # pragma: no cover
AsyncExitStack = None # type: ignore
@asynccontextmanager
async def contextmanager_in_threadpool(cm: Any) -> Any:
try:
yield await run_in_threadpool(cm.__enter__)
except Exception as e:
ok = await run_in_threadpool(cm.__exit__, type(e), e, None)
if not ok:
raise e
else:
await run_in_threadpool(cm.__exit__, None, None, None)

View File

@@ -1,7 +1,12 @@
from typing import Callable, List, Sequence
from fastapi.security.base import SecurityBase
from pydantic.fields import Field
try:
from pydantic.fields import ModelField
except ImportError: # pragma: nocover
# TODO: remove when removing support for Pydantic < 1.0.0
from pydantic.fields import Field as ModelField # type: ignore
param_supported_types = (str, int, float, bool)
@@ -16,11 +21,11 @@ class Dependant:
def __init__(
self,
*,
path_params: List[Field] = None,
query_params: List[Field] = None,
header_params: List[Field] = None,
cookie_params: List[Field] = None,
body_params: List[Field] = None,
path_params: List[ModelField] = None,
query_params: List[ModelField] = None,
header_params: List[ModelField] = None,
cookie_params: List[ModelField] = None,
body_params: List[ModelField] = None,
dependencies: List["Dependant"] = None,
security_schemes: List[SecurityRequirement] = None,
name: str = None,

View File

@@ -1,5 +1,6 @@
import asyncio
import inspect
from contextlib import contextmanager
from copy import deepcopy
from typing import (
Any,
@@ -16,16 +17,20 @@ from typing import (
)
from fastapi import params
from fastapi.concurrency import (
AsyncExitStack,
_fake_asynccontextmanager,
asynccontextmanager,
contextmanager_in_threadpool,
)
from fastapi.dependencies.models import Dependant, SecurityRequirement
from fastapi.security.base import SecurityBase
from fastapi.security.oauth2 import OAuth2, SecurityScopes
from fastapi.security.open_id_connect_url import OpenIdConnect
from fastapi.utils import get_path_param_names
from pydantic import BaseConfig, BaseModel, Schema, create_model
from fastapi.utils import PYDANTIC_1, get_field_info, get_path_param_names
from pydantic import BaseConfig, BaseModel, create_model
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 starlette.background import BackgroundTasks
from starlette.concurrency import run_in_threadpool
@@ -34,20 +39,55 @@ from starlette.requests import Request
from starlette.responses import Response
from starlette.websockets import WebSocket
try:
from pydantic.fields import (
SHAPE_LIST,
SHAPE_SEQUENCE,
SHAPE_SET,
SHAPE_SINGLETON,
SHAPE_TUPLE,
SHAPE_TUPLE_ELLIPSIS,
FieldInfo,
ModelField,
Required,
)
from pydantic.schema import get_annotation_from_field_info
from pydantic.typing import ForwardRef, evaluate_forwardref
except ImportError: # pragma: nocover
# TODO: remove when removing support for Pydantic < 1.0.0
from pydantic.fields import Field as ModelField # type: ignore
from pydantic.fields import Required, Shape # type: ignore
from pydantic import Schema as FieldInfo # type: ignore
from pydantic.schema import get_annotation_from_schema # type: ignore
from pydantic.utils import ForwardRef, evaluate_forwardref # type: ignore
SHAPE_LIST = Shape.LIST
SHAPE_SEQUENCE = Shape.SEQUENCE
SHAPE_SET = Shape.SET
SHAPE_SINGLETON = Shape.SINGLETON
SHAPE_TUPLE = Shape.TUPLE
SHAPE_TUPLE_ELLIPSIS = Shape.TUPLE_ELLIPS
def get_annotation_from_field_info(
annotation: Any, field_info: FieldInfo, field_name: str
) -> Type[Any]:
return get_annotation_from_schema(annotation, field_info)
sequence_shapes = {
Shape.LIST,
Shape.SET,
Shape.TUPLE,
Shape.SEQUENCE,
Shape.TUPLE_ELLIPS,
SHAPE_LIST,
SHAPE_SET,
SHAPE_TUPLE,
SHAPE_SEQUENCE,
SHAPE_TUPLE_ELLIPSIS,
}
sequence_types = (list, set, tuple)
sequence_shape_to_type = {
Shape.LIST: list,
Shape.SET: set,
Shape.TUPLE: tuple,
Shape.SEQUENCE: list,
Shape.TUPLE_ELLIPS: list,
SHAPE_LIST: list,
SHAPE_SET: set,
SHAPE_TUPLE: tuple,
SHAPE_SEQUENCE: list,
SHAPE_TUPLE_ELLIPSIS: list,
}
@@ -108,7 +148,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 +169,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)
@@ -130,12 +183,13 @@ def get_flat_dependant(dependant: Dependant) -> Dependant:
return flat_dependant
def is_scalar_field(field: Field) -> bool:
def is_scalar_field(field: ModelField) -> bool:
field_info = get_field_info(field)
if not (
field.shape == Shape.SINGLETON
field.shape == SHAPE_SINGLETON
and not lenient_issubclass(field.type_, BaseModel)
and not lenient_issubclass(field.type_, sequence_types + (dict,))
and not isinstance(field.schema, params.Body)
and not isinstance(field_info, params.Body)
):
return False
if field.sub_fields:
@@ -144,7 +198,7 @@ def is_scalar_field(field: Field) -> bool:
return True
def is_scalar_sequence_field(field: Field) -> bool:
def is_scalar_sequence_field(field: ModelField) -> bool:
if (field.shape in sequence_shapes) and not lenient_issubclass(
field.type_, BaseModel
):
@@ -158,6 +212,42 @@ 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
async_contextmanager_dependencies_error = """
FastAPI dependencies with yield require Python 3.7 or above,
or the backports for Python 3.6, installed with:
pip install async-exit-stack async-generator
"""
def check_dependency_contextmanagers() -> None:
if AsyncExitStack is None or asynccontextmanager == _fake_asynccontextmanager:
raise RuntimeError(async_contextmanager_dependencies_error) # pragma: no cover
def get_dependant(
*,
path: str,
@@ -167,8 +257,10 @@ 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
if inspect.isgeneratorfunction(call) or inspect.isasyncgenfunction(call):
check_dependency_contextmanagers()
dependant = Dependant(call=call, name=name, path=path, use_cache=use_cache)
for param_name, param in signature_params.items():
if isinstance(param.default, params.Depends):
@@ -181,18 +273,23 @@ def get_dependant(
continue
if add_non_field_param_to_dependency(param=param, dependant=dependant):
continue
param_field = get_param_field(param=param, default_schema=params.Query)
param_field = get_param_field(
param=param, default_field_info=params.Query, param_name=param_name
)
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,
param_name=param_name,
default_field_info=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):
@@ -202,8 +299,9 @@ def get_dependant(
) and is_scalar_sequence_field(param_field):
add_param_to_fields(field=param_field, dependant=dependant)
else:
field_info = get_field_info(param_field)
assert isinstance(
param_field.schema, params.Body
field_info, params.Body
), f"Param: {param_field.name} can only be a request body, using Body(...)"
dependant.body_params.append(param_field)
return dependant
@@ -233,64 +331,88 @@ def add_non_field_param_to_dependency(
def get_param_field(
*,
param: inspect.Parameter,
default_schema: Type[params.Param] = params.Param,
param_name: str,
default_field_info: Type[params.Param] = params.Param,
force_type: params.ParamTypes = None,
) -> Field:
ignore_default: bool = False,
) -> ModelField:
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):
if isinstance(default_value, FieldInfo):
had_schema = True
schema = default_value
default_value = schema.default
if isinstance(schema, params.Param) and getattr(schema, "in_", None) is None:
schema.in_ = default_schema.in_
field_info = default_value
default_value = field_info.default
if (
isinstance(field_info, params.Param)
and getattr(field_info, "in_", None) is None
):
field_info.in_ = default_field_info.in_
if force_type:
schema.in_ = force_type # type: ignore
field_info.in_ = force_type # type: ignore
else:
schema = default_schema(default_value)
field_info = default_field_info(default_value)
required = default_value == Required
annotation: Any = Any
if not param.annotation == param.empty:
annotation = param.annotation
annotation = get_annotation_from_schema(annotation, schema)
if not schema.alias and getattr(schema, "convert_underscores", None):
annotation = get_annotation_from_field_info(annotation, field_info, param_name)
if not field_info.alias and getattr(field_info, "convert_underscores", None):
alias = param.name.replace("_", "-")
else:
alias = schema.alias or param.name
field = Field(
name=param.name,
type_=annotation,
default=None if required else default_value,
alias=alias,
required=required,
model_config=BaseConfig,
class_validators={},
schema=schema,
)
alias = field_info.alias or param.name
if PYDANTIC_1:
field = ModelField(
name=param.name,
type_=annotation,
default=None if required else default_value,
alias=alias,
required=required,
model_config=BaseConfig,
class_validators={},
field_info=field_info,
)
# TODO: remove when removing support for Pydantic < 1.2.0
field.required = required
else: # pragma: nocover
field = ModelField( # type: ignore
name=param.name,
type_=annotation,
default=None if required else default_value,
alias=alias,
required=required,
model_config=BaseConfig,
class_validators={},
schema=field_info,
)
field.required = required
if not had_schema and not is_scalar_field(field=field):
field.schema = params.Body(schema.default)
if PYDANTIC_1:
field.field_info = params.Body(field_info.default)
else:
field.schema = params.Body(field_info.default) # type: ignore # pragma: nocover
return field
def add_param_to_fields(*, field: Field, dependant: Dependant) -> None:
field.schema = cast(params.Param, field.schema)
if field.schema.in_ == params.ParamTypes.path:
def add_param_to_fields(*, field: ModelField, dependant: Dependant) -> None:
field_info = cast(params.Param, get_field_info(field))
if field_info.in_ == params.ParamTypes.path:
dependant.path_params.append(field)
elif field.schema.in_ == params.ParamTypes.query:
elif field_info.in_ == params.ParamTypes.query:
dependant.query_params.append(field)
elif field.schema.in_ == params.ParamTypes.header:
elif field_info.in_ == params.ParamTypes.header:
dependant.header_params.append(field)
else:
assert (
field.schema.in_ == params.ParamTypes.cookie
field_info.in_ == params.ParamTypes.cookie
), f"non-body parameters must be in path, query, header or cookie: {field.name}"
dependant.cookie_params.append(field)
def is_coroutine_callable(call: Callable) -> bool:
if inspect.isfunction(call):
if inspect.isroutine(call):
return asyncio.iscoroutinefunction(call)
if inspect.isclass(call):
return False
@@ -298,6 +420,16 @@ def is_coroutine_callable(call: Callable) -> bool:
return asyncio.iscoroutinefunction(call)
async def solve_generator(
*, call: Callable, stack: AsyncExitStack, sub_values: Dict[str, Any]
) -> Any:
if inspect.isgeneratorfunction(call):
cm = contextmanager_in_threadpool(contextmanager(call)(**sub_values))
elif inspect.isasyncgenfunction(call):
cm = asynccontextmanager(call)(**sub_values)
return await stack.enter_async_context(cm)
async def solve_dependencies(
*,
request: Union[Request, WebSocket],
@@ -316,8 +448,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
@@ -353,9 +489,13 @@ async def solve_dependencies(
dependency_overrides_provider=dependency_overrides_provider,
dependency_cache=dependency_cache,
)
sub_values, sub_errors, background_tasks, sub_response, sub_dependency_cache = (
solved_result
)
(
sub_values,
sub_errors,
background_tasks,
sub_response,
sub_dependency_cache,
) = solved_result
sub_response = cast(Response, sub_response)
response.headers.raw.extend(sub_response.headers.raw)
if sub_response.status_code:
@@ -366,6 +506,15 @@ async def solve_dependencies(
continue
if sub_dependant.use_cache and sub_dependant.cache_key in dependency_cache:
solved = dependency_cache[sub_dependant.cache_key]
elif inspect.isgeneratorfunction(call) or inspect.isasyncgenfunction(call):
stack = request.scope.get("fastapi_astack")
if stack is None:
raise RuntimeError(
async_contextmanager_dependencies_error
) # pragma: no cover
solved = await solve_generator(
call=call, stack=stack, sub_values=sub_values
)
elif is_coroutine_callable(call):
solved = await call(**sub_values)
else:
@@ -392,7 +541,10 @@ 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)
@@ -415,7 +567,7 @@ async def solve_dependencies(
def request_params_to_args(
required_params: Sequence[Field],
required_params: Sequence[ModelField],
received_params: Union[Mapping[str, Any], QueryParams, Headers],
) -> Tuple[Dict[str, Any], List[ErrorWrapper]]:
values = {}
@@ -427,21 +579,32 @@ def request_params_to_args(
value = received_params.getlist(field.alias) or field.default
else:
value = received_params.get(field.alias)
schema = field.schema
assert isinstance(schema, params.Param), "Params must be subclasses of Param"
field_info = get_field_info(field)
assert isinstance(
field_info, params.Param
), "Params must be subclasses of Param"
if value is None:
if field.required:
errors.append(
ErrorWrapper(
MissingError(),
loc=(schema.in_.value, field.alias),
config=BaseConfig,
if PYDANTIC_1:
errors.append(
ErrorWrapper(
MissingError(), loc=(field_info.in_.value, field.alias)
)
)
else: # pragma: nocover
errors.append(
ErrorWrapper( # type: ignore
MissingError(),
loc=(field_info.in_.value, field.alias),
config=BaseConfig,
)
)
)
else:
values[field.name] = deepcopy(field.default)
continue
v_, errors_ = field.validate(value, values, loc=(schema.in_.value, field.alias))
v_, errors_ = field.validate(
value, values, loc=(field_info.in_.value, field.alias)
)
if isinstance(errors_, ErrorWrapper):
errors.append(errors_)
elif isinstance(errors_, list):
@@ -452,14 +615,15 @@ def request_params_to_args(
async def request_body_to_args(
required_params: List[Field],
required_params: List[ModelField],
received_body: Optional[Union[Dict[str, Any], FormData]],
) -> Tuple[Dict[str, Any], List[ErrorWrapper]]:
values = {}
errors = []
if required_params:
field = required_params[0]
embed = getattr(field.schema, "embed", None)
field_info = get_field_info(field)
embed = getattr(field_info, "embed", None)
if len(required_params) == 1 and not embed:
received_body = {field.alias: received_body}
for field in required_params:
@@ -473,31 +637,38 @@ async def request_body_to_args(
value = received_body.get(field.alias)
if (
value is None
or (isinstance(field.schema, params.Form) and value == "")
or (isinstance(field_info, params.Form) and value == "")
or (
isinstance(field.schema, params.Form)
isinstance(field_info, params.Form)
and field.shape in sequence_shapes
and len(value) == 0
)
):
if field.required:
errors.append(
ErrorWrapper(
MissingError(), loc=("body", field.alias), config=BaseConfig
if PYDANTIC_1:
errors.append(
ErrorWrapper(MissingError(), loc=("body", field.alias))
)
else: # pragma: nocover
errors.append(
ErrorWrapper( # type: ignore
MissingError(),
loc=("body", field.alias),
config=BaseConfig,
)
)
)
else:
values[field.name] = deepcopy(field.default)
continue
if (
isinstance(field.schema, params.File)
isinstance(field_info, params.File)
and lenient_issubclass(field.type_, bytes)
and isinstance(value, UploadFile)
):
value = await value.read()
elif (
field.shape in sequence_shapes
and isinstance(field.schema, params.File)
and isinstance(field_info, params.File)
and lenient_issubclass(field.type_, bytes)
and isinstance(value, sequence_types)
):
@@ -514,31 +685,45 @@ async def request_body_to_args(
return values, errors
def get_schema_compatible_field(*, field: Field) -> Field:
def get_schema_compatible_field(*, field: ModelField) -> ModelField:
out_field = field
if lenient_issubclass(field.type_, UploadFile):
use_type: type = bytes
if field.shape in sequence_shapes:
use_type = List[bytes]
out_field = Field(
name=field.name,
type_=use_type,
class_validators=field.class_validators,
model_config=field.model_config,
default=field.default,
required=field.required,
alias=field.alias,
schema=field.schema,
)
if PYDANTIC_1:
out_field = ModelField(
name=field.name,
type_=use_type,
class_validators=field.class_validators,
model_config=field.model_config,
default=field.default,
required=field.required,
alias=field.alias,
field_info=field.field_info,
)
else: # pragma: nocover
out_field = ModelField( # type: ignore
name=field.name,
type_=use_type,
class_validators=field.class_validators,
model_config=field.model_config,
default=field.default,
required=field.required,
alias=field.alias,
schema=field.schema, # type: ignore
)
return out_field
def get_body_field(*, dependant: Dependant, name: str) -> Optional[Field]:
def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]:
flat_dependant = get_flat_dependant(dependant)
if not flat_dependant.body_params:
return None
first_param = flat_dependant.body_params[0]
embed = getattr(first_param.schema, "embed", None)
field_info = get_field_info(first_param)
embed = getattr(field_info, "embed", None)
if len(flat_dependant.body_params) == 1 and not embed:
return get_schema_compatible_field(field=first_param)
model_name = "Body_" + name
@@ -546,21 +731,46 @@ 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)
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):
BodySchema = params.Form
else:
BodySchema = params.Body
field = Field(
name="body",
type_=BodyModel,
default=None,
required=required,
model_config=BaseConfig,
class_validators={},
alias="body",
schema=BodySchema(None),
)
BodyFieldInfo_kwargs: Dict[str, Any] = dict(default=None)
if any(
isinstance(get_field_info(f), params.File) for f in flat_dependant.body_params
):
BodyFieldInfo: Type[params.Body] = params.File
elif any(
isinstance(get_field_info(f), params.Form) for f in flat_dependant.body_params
):
BodyFieldInfo = params.Form
else:
BodyFieldInfo = params.Body
body_param_media_types = [
getattr(get_field_info(f), "media_type")
for f in flat_dependant.body_params
if isinstance(get_field_info(f), params.Body)
]
if len(set(body_param_media_types)) == 1:
BodyFieldInfo_kwargs["media_type"] = body_param_media_types[0]
if PYDANTIC_1:
field = ModelField(
name="body",
type_=BodyModel,
default=None,
required=required,
model_config=BaseConfig,
class_validators={},
alias="body",
field_info=BodyFieldInfo(**BodyFieldInfo_kwargs),
)
else: # pragma: nocover
field = ModelField( # type: ignore
name="body",
type_=BodyModel,
default=None,
required=required,
model_config=BaseConfig,
class_validators={},
alias="body",
schema=BodyFieldInfo(**BodyFieldInfo_kwargs),
)
return field

View File

@@ -1,34 +1,56 @@
from enum import Enum
from types import GeneratorType
from typing import Any, List, Set
from typing import Any, Dict, List, Set, Union
from fastapi.utils import PYDANTIC_1, logger
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,
skip_defaults: bool = None,
exclude_unset: bool = False,
include_none: bool = True,
custom_encoder: dict = {},
sqlalchemy_safe: bool = True,
) -> Any:
if skip_defaults is not None:
logger.warning( # pragma: nocover
"skip_defaults in jsonable_encoder has been deprecated in \
favor of exclude_unset to keep in line with Pydantic v1, support for it \
will be removed soon."
)
if include is not None and not isinstance(include, set):
include = set(include)
if exclude is not None and not isinstance(exclude, set):
exclude = set(exclude)
if isinstance(obj, BaseModel):
encoder = getattr(obj.Config, "json_encoders", custom_encoder)
return jsonable_encoder(
obj.dict(
encoder = getattr(obj.Config, "json_encoders", {})
if custom_encoder:
encoder.update(custom_encoder)
if PYDANTIC_1:
obj_dict = obj.dict(
include=include,
exclude=exclude,
by_alias=by_alias,
skip_defaults=skip_defaults,
),
exclude_unset=bool(exclude_unset or skip_defaults),
)
else: # pragma: nocover
obj_dict = obj.dict(
include=include,
exclude=exclude,
by_alias=by_alias,
skip_defaults=bool(exclude_unset or skip_defaults),
)
return jsonable_encoder(
obj_dict,
include_none=include_none,
custom_encoder=encoder,
sqlalchemy_safe=sqlalchemy_safe,
@@ -52,7 +74,7 @@ def jsonable_encoder(
encoded_key = jsonable_encoder(
key,
by_alias=by_alias,
skip_defaults=skip_defaults,
exclude_unset=exclude_unset,
include_none=include_none,
custom_encoder=custom_encoder,
sqlalchemy_safe=sqlalchemy_safe,
@@ -60,7 +82,7 @@ def jsonable_encoder(
encoded_value = jsonable_encoder(
value,
by_alias=by_alias,
skip_defaults=skip_defaults,
exclude_unset=exclude_unset,
include_none=include_none,
custom_encoder=custom_encoder,
sqlalchemy_safe=sqlalchemy_safe,
@@ -76,7 +98,7 @@ def jsonable_encoder(
include=include,
exclude=exclude,
by_alias=by_alias,
skip_defaults=skip_defaults,
exclude_unset=exclude_unset,
include_none=include_none,
custom_encoder=custom_encoder,
sqlalchemy_safe=sqlalchemy_safe,
@@ -104,7 +126,7 @@ def jsonable_encoder(
return jsonable_encoder(
data,
by_alias=by_alias,
skip_defaults=skip_defaults,
exclude_unset=exclude_unset,
include_none=include_none,
custom_encoder=custom_encoder,
sqlalchemy_safe=sqlalchemy_safe,

View File

@@ -1,7 +1,11 @@
from typing import Any
from typing import Any, Sequence
from pydantic import ValidationError
from fastapi.utils import PYDANTIC_1
from pydantic import ValidationError, create_model
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):
@@ -12,9 +16,21 @@ class HTTPException(StarletteHTTPException):
self.headers = headers
RequestErrorModel = create_model("Request")
WebSocketErrorModel = create_model("WebSocket")
class RequestValidationError(ValidationError):
pass
def __init__(self, errors: Sequence[ErrorList]) -> None:
if PYDANTIC_1:
super().__init__(errors, RequestErrorModel)
else:
super().__init__(errors, Request) # type: ignore # pragma: nocover
class WebSocketRequestValidationError(ValidationError):
pass
def __init__(self, errors: Sequence[ErrorList]) -> None:
if PYDANTIC_1:
super().__init__(errors, WebSocketErrorModel)
else:
super().__init__(errors, WebSocket) # type: ignore # pragma: nocover

View File

@@ -1,2 +1,3 @@
METHODS_WITH_BODY = set(("POST", "PUT", "DELETE", "PATCH"))
STATUS_CODES_WITH_NO_BODY = set((100, 101, 102, 103, 204, 304))
REF_PREFIX = "#/components/schemas/"

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

@@ -1,17 +1,25 @@
import logging
from enum import Enum
from typing import Any, Dict, List, Optional, Union
from pydantic import BaseModel, Schema as PSchema
from pydantic.types import UrlStr
from fastapi.utils import logger
from pydantic import BaseModel
logger = logging.getLogger("fastapi")
try:
from pydantic import AnyUrl, Field
except ImportError: # pragma: nocover
# TODO: remove when removing support for Pydantic < 1.0.0
from pydantic import Schema as Field # type: ignore
from pydantic import UrlStr as AnyUrl # type: ignore
try:
import email_validator
assert email_validator # make autoflake ignore the unused import
from pydantic.types import EmailStr # type: ignore
try:
from pydantic import EmailStr
except ImportError: # pragma: nocover
# TODO: remove when removing support for Pydantic < 1.0.0
from pydantic.types import EmailStr # type: ignore
except ImportError: # pragma: no cover
logger.warning(
"email-validator not installed, email fields will be treated as str.\n"
@@ -24,13 +32,13 @@ except ImportError: # pragma: no cover
class Contact(BaseModel):
name: Optional[str] = None
url: Optional[UrlStr] = None
url: Optional[AnyUrl] = None
email: Optional[EmailStr] = None
class License(BaseModel):
name: str
url: Optional[UrlStr] = None
url: Optional[AnyUrl] = None
class Info(BaseModel):
@@ -49,13 +57,13 @@ class ServerVariable(BaseModel):
class Server(BaseModel):
url: UrlStr
url: AnyUrl
description: Optional[str] = None
variables: Optional[Dict[str, ServerVariable]] = None
class Reference(BaseModel):
ref: str = PSchema(..., alias="$ref") # type: ignore
ref: str = Field(..., alias="$ref")
class Discriminator(BaseModel):
@@ -73,32 +81,32 @@ class XML(BaseModel):
class ExternalDocumentation(BaseModel):
description: Optional[str] = None
url: UrlStr
url: AnyUrl
class SchemaBase(BaseModel):
ref: Optional[str] = PSchema(None, alias="$ref") # type: ignore
ref: Optional[str] = Field(None, alias="$ref")
title: Optional[str] = None
multipleOf: Optional[float] = None
maximum: Optional[float] = None
exclusiveMaximum: Optional[float] = None
minimum: Optional[float] = None
exclusiveMinimum: Optional[float] = None
maxLength: Optional[int] = PSchema(None, gte=0) # type: ignore
minLength: Optional[int] = PSchema(None, gte=0) # type: ignore
maxLength: Optional[int] = Field(None, gte=0)
minLength: Optional[int] = Field(None, gte=0)
pattern: Optional[str] = None
maxItems: Optional[int] = PSchema(None, gte=0) # type: ignore
minItems: Optional[int] = PSchema(None, gte=0) # type: ignore
maxItems: Optional[int] = Field(None, gte=0)
minItems: Optional[int] = Field(None, gte=0)
uniqueItems: Optional[bool] = None
maxProperties: Optional[int] = PSchema(None, gte=0) # type: ignore
minProperties: Optional[int] = PSchema(None, gte=0) # type: ignore
maxProperties: Optional[int] = Field(None, gte=0)
minProperties: Optional[int] = Field(None, gte=0)
required: Optional[List[str]] = None
enum: Optional[List[str]] = None
type: Optional[str] = None
allOf: Optional[List[Any]] = None
oneOf: Optional[List[Any]] = None
anyOf: Optional[List[Any]] = None
not_: Optional[List[Any]] = PSchema(None, alias="not") # type: ignore
not_: Optional[List[Any]] = Field(None, alias="not")
items: Optional[Any] = None
properties: Optional[Dict[str, Any]] = None
additionalProperties: Optional[Union[Dict[str, Any], bool]] = None
@@ -119,17 +127,17 @@ class Schema(SchemaBase):
allOf: Optional[List[SchemaBase]] = None
oneOf: Optional[List[SchemaBase]] = None
anyOf: Optional[List[SchemaBase]] = None
not_: Optional[List[SchemaBase]] = PSchema(None, alias="not") # type: ignore
not_: Optional[List[SchemaBase]] = Field(None, alias="not")
items: Optional[SchemaBase] = None
properties: Optional[Dict[str, SchemaBase]] = None
additionalProperties: Optional[Union[SchemaBase, bool]] = None # type: ignore
additionalProperties: Optional[Union[Dict[str, Any], bool]] = None
class Example(BaseModel):
summary: Optional[str] = None
description: Optional[str] = None
value: Optional[Any] = None
externalValue: Optional[UrlStr] = None
externalValue: Optional[AnyUrl] = None
class ParameterInType(Enum):
@@ -149,9 +157,7 @@ class Encoding(BaseModel):
class MediaType(BaseModel):
schema_: Optional[Union[Schema, Reference]] = PSchema( # type: ignore
None, alias="schema"
)
schema_: Optional[Union[Schema, Reference]] = Field(None, alias="schema")
example: Optional[Any] = None
examples: Optional[Dict[str, Union[Example, Reference]]] = None
encoding: Optional[Dict[str, Encoding]] = None
@@ -165,9 +171,7 @@ class ParameterBase(BaseModel):
style: Optional[str] = None
explode: Optional[bool] = None
allowReserved: Optional[bool] = None
schema_: Optional[Union[Schema, Reference]] = PSchema( # type: ignore
None, alias="schema"
)
schema_: Optional[Union[Schema, Reference]] = Field(None, alias="schema")
example: Optional[Any] = None
examples: Optional[Dict[str, Union[Example, Reference]]] = None
# Serialization rules for more complex scenarios
@@ -176,7 +180,7 @@ class ParameterBase(BaseModel):
class Parameter(ParameterBase):
name: str
in_: ParameterInType = PSchema(..., alias="in") # type: ignore
in_: ParameterInType = Field(..., alias="in")
class Header(ParameterBase):
@@ -210,10 +214,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 +222,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
@@ -231,7 +231,7 @@ class Operation(BaseModel):
class PathItem(BaseModel):
ref: Optional[str] = PSchema(None, alias="$ref") # type: ignore
ref: Optional[str] = Field(None, alias="$ref")
summary: Optional[str] = None
description: Optional[str] = None
get: Optional[Operation] = None
@@ -259,7 +259,7 @@ class SecuritySchemeType(Enum):
class SecurityBase(BaseModel):
type_: SecuritySchemeType = PSchema(..., alias="type") # type: ignore
type_: SecuritySchemeType = Field(..., alias="type")
description: Optional[str] = None
@@ -270,13 +270,13 @@ class APIKeyIn(Enum):
class APIKey(SecurityBase):
type_ = PSchema(SecuritySchemeType.apiKey, alias="type") # type: ignore
in_: APIKeyIn = PSchema(..., alias="in") # type: ignore
type_ = Field(SecuritySchemeType.apiKey, alias="type")
in_: APIKeyIn = Field(..., alias="in")
name: str
class HTTPBase(SecurityBase):
type_ = PSchema(SecuritySchemeType.http, alias="type") # type: ignore
type_ = Field(SecuritySchemeType.http, alias="type")
scheme: str
@@ -315,12 +315,12 @@ class OAuthFlows(BaseModel):
class OAuth2(SecurityBase):
type_ = PSchema(SecuritySchemeType.oauth2, alias="type") # type: ignore
type_ = Field(SecuritySchemeType.oauth2, alias="type")
flows: OAuthFlows
class OpenIdConnect(SecurityBase):
type_ = PSchema(SecuritySchemeType.openIdConnect, alias="type") # type: ignore
type_ = Field(SecuritySchemeType.openIdConnect, alias="type")
openIdConnectUrl: str

View File

@@ -5,21 +5,32 @@ from fastapi import routing
from fastapi.dependencies.models import Dependant
from fastapi.dependencies.utils import get_flat_dependant
from fastapi.encoders import jsonable_encoder
from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX
from fastapi.openapi.constants import (
METHODS_WITH_BODY,
REF_PREFIX,
STATUS_CODES_WITH_NO_BODY,
)
from fastapi.openapi.models import OpenAPI
from fastapi.params import Body, Param
from fastapi.utils import (
generate_operation_id_for_path,
get_field_info,
get_flat_models_from_routes,
get_model_definitions,
)
from pydantic.fields import Field
from pydantic import BaseModel
from pydantic.schema import field_schema, get_model_name_map
from pydantic.utils import lenient_issubclass
from starlette.responses import JSONResponse
from starlette.routing import BaseRoute
from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY
try:
from pydantic.fields import ModelField
except ImportError: # pragma: nocover
# TODO: remove when removing support for Pydantic < 1.0.0
from pydantic.fields import Field as ModelField # type: ignore
validation_error_definition = {
"title": "ValidationError",
"type": "object",
@@ -43,9 +54,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)
def get_openapi_params(dependant: Dependant) -> List[ModelField]:
flat_dependant = get_flat_dependant(dependant, skip_repeats=True)
return (
flat_dependant.path_params
+ flat_dependant.query_params
@@ -70,41 +90,37 @@ 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] = {}
all_route_params: Sequence[ModelField],
) -> 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
field_info = get_field_info(param)
field_info = cast(Param, field_info)
parameter = {
"name": param.alias,
"in": schema.in_.value,
"in": field_info.in_.value,
"required": param.required,
"schema": field_schema(param, model_name_map={})[0],
}
if schema.description:
parameter["description"] = schema.description
if schema.deprecated:
parameter["deprecated"] = schema.deprecated
if field_info.description:
parameter["description"] = field_info.description
if field_info.deprecated:
parameter["deprecated"] = field_info.deprecated
parameters.append(parameter)
return definitions, parameters
return parameters
def get_openapi_operation_request_body(
*, body_field: Optional[Field], model_name_map: Dict[Type, str]
*, body_field: Optional[ModelField], model_name_map: Dict[Type[BaseModel], str]
) -> Optional[Dict]:
if not body_field:
return None
assert isinstance(body_field, Field)
assert isinstance(body_field, ModelField)
body_schema, _, _ = field_schema(
body_field, model_name_map=model_name_map, ref_prefix=REF_PREFIX
)
body_field.schema = cast(Body, body_field.schema)
request_media_type = body_field.schema.media_type
field_info = cast(Body, get_field_info(body_field))
request_media_type = field_info.media_type
required = body_field.required
request_body_oai: Dict[str, Any] = {}
if required:
@@ -146,11 +162,13 @@ 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, "A response class is needed to generate OpenAPI"
route_response_media_type: Optional[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 +177,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 +187,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,36 +198,50 @@ 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 or "application/json", {}
)["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):
if route.response_field:
response_schema, _, _ = field_schema(
route.response_field,
model_name_map=model_name_map,
ref_prefix=REF_PREFIX,
)
else:
response_schema = {}
operation.setdefault("responses", {}).setdefault(status_code, {})[
"description"
] = route.response_description
operation.setdefault("responses", {}).setdefault(
status_code, {}
).setdefault("content", {}).setdefault(route.response_class.media_type, {})[
"schema"
] = response_schema
if all_route_params or route.body_field:
operation["responses"][str(HTTP_422_UNPROCESSABLE_ENTITY)] = {
if (
route_response_media_type
and route.status_code not in STATUS_CODES_WITH_NO_BODY
):
response_schema = {"type": "string"}
if lenient_issubclass(route.response_class, JSONResponse):
if route.response_field:
response_schema, _, _ = field_schema(
route.response_field,
model_name_map=model_name_map,
ref_prefix=REF_PREFIX,
)
else:
response_schema = {}
operation.setdefault("responses", {}).setdefault(
status_code, {}
).setdefault("content", {}).setdefault(route_response_media_type, {})[
"schema"
] = response_schema
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 +249,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 +296,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

@@ -1,7 +1,11 @@
from enum import Enum
from typing import Any, Callable, Sequence
from pydantic import Schema
try:
from pydantic.fields import FieldInfo
except ImportError: # pragma: nocover
# TODO: remove when removing support for Pydantic < 1.0.0
from pydantic import Schema as FieldInfo # type: ignore
class ParamTypes(Enum):
@@ -11,7 +15,7 @@ class ParamTypes(Enum):
cookie = "cookie"
class Param(Schema):
class Param(FieldInfo):
in_: ParamTypes
def __init__(
@@ -199,7 +203,7 @@ class Cookie(Param):
)
class Body(Schema):
class Body(FieldInfo):
def __init__(
self,
default: Any,

View File

@@ -11,12 +11,18 @@ 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
from fastapi.openapi.constants import STATUS_CODES_WITH_NO_BODY
from fastapi.utils import (
PYDANTIC_1,
create_cloned_field,
generate_operation_id_for_path,
get_field_info,
warning_response_model_skip_defaults_deprecated,
)
from pydantic import BaseConfig, BaseModel
from pydantic.error_wrappers import ErrorWrapper, ValidationError
from pydantic.fields import Field
from pydantic.utils import lenient_issubclass
from starlette import routing
from starlette.concurrency import run_in_threadpool
@@ -33,51 +39,63 @@ from starlette.status import WS_1008_POLICY_VIOLATION
from starlette.types import ASGIApp
from starlette.websockets import WebSocket
try:
from pydantic.fields import FieldInfo, ModelField
except ImportError: # pragma: nocover
# TODO: remove when removing support for Pydantic < 1.0.0
from pydantic import Schema as FieldInfo # type: ignore
from pydantic.fields import Field as ModelField # type: ignore
def serialize_response(
*,
field: Field = None,
field: ModelField = 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,
exclude_unset: bool = False,
) -> Any:
if field:
errors = []
if exclude_unset and isinstance(response, BaseModel):
if PYDANTIC_1:
response = response.dict(exclude_unset=exclude_unset)
else:
response = response.dict(skip_defaults=exclude_unset) # pragma: nocover
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,
exclude=exclude,
by_alias=by_alias,
skip_defaults=skip_defaults,
exclude_unset=exclude_unset,
)
else:
return jsonable_encoder(response)
def get_app(
def get_request_handler(
dependant: Dependant,
body_field: Field = None,
body_field: ModelField = 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_field: ModelField = None,
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,
response_model_exclude_unset: bool = False,
dependency_overrides_provider: Any = None,
) -> Callable:
assert dependant.call is not None, "dependant.call must be a function"
is_coroutine = asyncio.iscoroutinefunction(dependant.call)
is_body_form = body_field and isinstance(body_field.schema, params.Form)
is_body_form = body_field and isinstance(get_field_info(body_field), params.Form)
async def app(request: Request) -> Response:
try:
@@ -119,7 +137,7 @@ def get_app(
include=response_model_include,
exclude=response_model_exclude,
by_alias=response_model_by_alias,
skip_defaults=response_model_skip_defaults,
exclude_unset=response_model_exclude_unset,
)
response = response_class(
content=response_data,
@@ -193,15 +211,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,
response_model_exclude_unset: 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,19 +231,30 @@ 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"
assert (
status_code not in STATUS_CODES_WITH_NO_BODY
), f"Status code {status_code} must not have a response body"
response_name = "Response_" + self.unique_id
self.response_field: Optional[Field] = Field(
name=response_name,
type_=self.response_model,
class_validators={},
default=None,
required=False,
model_config=BaseConfig,
schema=Schema(None),
)
if PYDANTIC_1:
self.response_field: Optional[ModelField] = ModelField(
name=response_name,
type_=self.response_model,
class_validators={},
default=None,
required=False,
model_config=BaseConfig,
field_info=FieldInfo(None),
)
else:
self.response_field: Optional[ModelField] = ModelField( # type: ignore # pragma: nocover
name=response_name,
type_=self.response_model,
class_validators={},
default=None,
required=False,
model_config=BaseConfig,
schema=FieldInfo(None),
)
# Create a clone of the field, so that a Pydantic submodel is not returned
# as is just because it's an instance of a subclass of a more limited class
# e.g. UserInDB (containing hashed_password) could be a subclass of User
@@ -234,9 +262,9 @@ class APIRoute(routing.Route):
# would pass the validation and be returned as is.
# By being a new field, no inheritance will be passed as is. A new model
# will be always created.
self.secure_cloned_response_field: Optional[Field] = create_cloned_field(
self.response_field
)
self.secure_cloned_response_field: Optional[
ModelField
] = create_cloned_field(self.response_field)
else:
self.response_field = None
self.secure_cloned_response_field = None
@@ -248,6 +276,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 = {}
@@ -255,22 +286,36 @@ class APIRoute(routing.Route):
assert isinstance(response, dict), "An additional response must be a dict"
model = response.get("model")
if model:
assert (
additional_status_code not in STATUS_CODES_WITH_NO_BODY
), f"Status code {additional_status_code} must not have a response body"
assert lenient_issubclass(
model, BaseModel
), "A response model must be a Pydantic model"
response_name = f"Response_{additional_status_code}_{self.unique_id}"
response_field = Field(
name=response_name,
type_=model,
class_validators=None,
default=None,
required=False,
model_config=BaseConfig,
schema=Schema(None),
)
if PYDANTIC_1:
response_field = ModelField(
name=response_name,
type_=model,
class_validators=None,
default=None,
required=False,
model_config=BaseConfig,
field_info=FieldInfo(None),
)
else:
response_field = ModelField( # type: ignore # pragma: nocover
name=response_name,
type_=model,
class_validators=None,
default=None,
required=False,
model_config=BaseConfig,
schema=FieldInfo(None),
)
response_fields[additional_status_code] = response_field
if response_fields:
self.response_fields: Dict[Union[int, str], Field] = response_fields
self.response_fields: Dict[Union[int, str], ModelField] = response_fields
else:
self.response_fields = {}
self.deprecated = deprecated
@@ -278,7 +323,7 @@ class APIRoute(routing.Route):
self.response_model_include = response_model_include
self.response_model_exclude = response_model_exclude
self.response_model_by_alias = response_model_by_alias
self.response_model_skip_defaults = response_model_skip_defaults
self.response_model_exclude_unset = response_model_exclude_unset
self.include_in_schema = include_in_schema
self.response_class = response_class
@@ -293,19 +338,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_exclude_unset=self.response_model_exclude_unset,
dependency_overrides_provider=self.dependency_overrides_provider,
)
@@ -316,11 +362,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 +386,20 @@ 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,
response_model_skip_defaults: bool = None,
response_model_exclude_unset: 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(
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
route_class = route_class_override or self.route_class
route = route_class(
path,
endpoint=endpoint,
response_model=response_model,
@@ -363,7 +416,9 @@ class APIRouter(routing.Router):
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults,
response_model_exclude_unset=bool(
response_model_exclude_unset or response_model_skip_defaults
),
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@@ -386,14 +441,18 @@ 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,
response_model_skip_defaults: bool = None,
response_model_exclude_unset: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Type[Response] = None,
name: str = None,
) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
def decorator(func: Callable) -> Callable:
self.add_api_route(
path,
@@ -412,7 +471,9 @@ class APIRouter(routing.Router):
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults,
response_model_exclude_unset=bool(
response_model_exclude_unset or response_model_skip_defaults
),
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@@ -442,12 +503,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:
@@ -471,10 +541,11 @@ class APIRouter(routing.Router):
response_model_include=route.response_model_include,
response_model_exclude=route.response_model_exclude,
response_model_by_alias=route.response_model_by_alias,
response_model_skip_defaults=route.response_model_skip_defaults,
response_model_exclude_unset=route.response_model_exclude_unset,
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 +578,17 @@ 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,
response_model_skip_defaults: bool = None,
response_model_exclude_unset: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Type[Response] = None,
name: str = None,
) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
return self.api_route(
path=path,
response_model=response_model,
@@ -532,7 +605,9 @@ class APIRouter(routing.Router):
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults,
response_model_exclude_unset=bool(
response_model_exclude_unset or response_model_skip_defaults
),
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@@ -552,14 +627,17 @@ 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,
response_model_skip_defaults: bool = None,
response_model_exclude_unset: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Type[Response] = None,
name: str = None,
) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
return self.api_route(
path=path,
response_model=response_model,
@@ -576,7 +654,9 @@ class APIRouter(routing.Router):
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults,
response_model_exclude_unset=bool(
response_model_exclude_unset or response_model_skip_defaults
),
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@@ -596,14 +676,17 @@ 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,
response_model_skip_defaults: bool = None,
response_model_exclude_unset: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Type[Response] = None,
name: str = None,
) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
return self.api_route(
path=path,
response_model=response_model,
@@ -620,7 +703,9 @@ class APIRouter(routing.Router):
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults,
response_model_exclude_unset=bool(
response_model_exclude_unset or response_model_skip_defaults
),
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@@ -640,14 +725,17 @@ 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,
response_model_skip_defaults: bool = None,
response_model_exclude_unset: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Type[Response] = None,
name: str = None,
) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
return self.api_route(
path=path,
response_model=response_model,
@@ -664,7 +752,9 @@ class APIRouter(routing.Router):
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults,
response_model_exclude_unset=bool(
response_model_exclude_unset or response_model_skip_defaults
),
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@@ -684,14 +774,17 @@ 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,
response_model_skip_defaults: bool = None,
response_model_exclude_unset: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Type[Response] = None,
name: str = None,
) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
return self.api_route(
path=path,
response_model=response_model,
@@ -708,7 +801,9 @@ class APIRouter(routing.Router):
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults,
response_model_exclude_unset=bool(
response_model_exclude_unset or response_model_skip_defaults
),
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@@ -728,14 +823,17 @@ 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,
response_model_skip_defaults: bool = None,
response_model_exclude_unset: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Type[Response] = None,
name: str = None,
) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
return self.api_route(
path=path,
response_model=response_model,
@@ -752,7 +850,9 @@ class APIRouter(routing.Router):
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults,
response_model_exclude_unset=bool(
response_model_exclude_unset or response_model_skip_defaults
),
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@@ -772,14 +872,17 @@ 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,
response_model_skip_defaults: bool = None,
response_model_exclude_unset: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Type[Response] = None,
name: str = None,
) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
return self.api_route(
path=path,
response_model=response_model,
@@ -796,7 +899,9 @@ class APIRouter(routing.Router):
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults,
response_model_exclude_unset=bool(
response_model_exclude_unset or response_model_skip_defaults
),
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@@ -816,14 +921,17 @@ 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,
response_model_skip_defaults: bool = None,
response_model_exclude_unset: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
response_class: Type[Response] = None,
name: str = None,
) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
return self.api_route(
path=path,
response_model=response_model,
@@ -840,7 +948,9 @@ class APIRouter(routing.Router):
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults,
response_model_exclude_unset=bool(
response_model_exclude_unset or response_model_skip_defaults
),
include_in_schema=include_in_schema,
response_class=response_class,
name=name,

View File

@@ -1,25 +1,60 @@
import logging
import re
from dataclasses import is_dataclass
from typing import Any, Dict, List, Sequence, Set, Type, cast
from fastapi import routing
from fastapi.openapi.constants import REF_PREFIX
from pydantic import BaseConfig, BaseModel, Schema, create_model
from pydantic.fields import Field
from pydantic import BaseConfig, BaseModel, create_model
from pydantic.schema import get_flat_models_from_fields, model_process_schema
from pydantic.utils import lenient_issubclass
from starlette.routing import BaseRoute
logger = logging.getLogger("fastapi")
try:
from pydantic.fields import FieldInfo, ModelField
PYDANTIC_1 = True
except ImportError: # pragma: nocover
# TODO: remove when removing support for Pydantic < 1.0.0
from pydantic.fields import Field as ModelField # type: ignore
from pydantic import Schema as FieldInfo # type: ignore
logger.warning(
"Pydantic versions < 1.0.0 are deprecated in FastAPI and support will be \
removed soon"
)
PYDANTIC_1 = False
# TODO: remove when removing support for Pydantic < 1.0.0
def get_field_info(field: ModelField) -> FieldInfo:
if PYDANTIC_1:
return field.field_info # type: ignore
else:
return field.schema # type: ignore # pragma: nocover
# TODO: remove when removing support for Pydantic < 1.0.0
def warning_response_model_skip_defaults_deprecated() -> None:
logger.warning( # pragma: nocover
"response_model_skip_defaults has been deprecated in favor \
of response_model_exclude_unset to keep in line with Pydantic v1, \
support for it will be removed soon."
)
def get_flat_models_from_routes(routes: Sequence[BaseRoute]) -> Set[Type[BaseModel]]:
body_fields_from_routes: List[Field] = []
responses_from_routes: List[Field] = []
body_fields_from_routes: List[ModelField] = []
responses_from_routes: List[ModelField] = []
for route in routes:
if getattr(route, "include_in_schema", None) and isinstance(
route, routing.APIRoute
):
if route.body_field:
assert isinstance(
route.body_field, Field
route.body_field, ModelField
), "A request body must be a Pydantic Field"
body_fields_from_routes.append(route.body_field)
if route.response_field:
@@ -50,34 +85,49 @@ def get_path_param_names(path: str) -> Set[str]:
return {item.strip("{}") for item in re.findall("{[^}]*}", path)}
def create_cloned_field(field: Field) -> Field:
def create_cloned_field(field: ModelField) -> ModelField:
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
original_type.__name__,
__config__=original_type.__config__,
__validators__=original_type.__validators__,
use_type = create_model(
original_type.__name__, __config__=original_type.__config__
)
for f in original_type.__fields__.values():
use_type.__fields__[f.name] = f
new_field = Field(
name=field.name,
type_=use_type,
class_validators={},
default=None,
required=False,
model_config=BaseConfig,
schema=Schema(None),
)
use_type.__validators__ = original_type.__validators__
if PYDANTIC_1:
new_field = ModelField(
name=field.name,
type_=use_type,
class_validators={},
default=None,
required=False,
model_config=BaseConfig,
field_info=FieldInfo(None),
)
else: # pragma: nocover
new_field = ModelField( # type: ignore
name=field.name,
type_=use_type,
class_validators={},
default=None,
required=False,
model_config=BaseConfig,
schema=FieldInfo(None),
)
new_field.has_alias = field.has_alias
new_field.alias = field.alias
new_field.class_validators = field.class_validators
new_field.default = field.default
new_field.required = field.required
new_field.model_config = field.model_config
new_field.schema = field.schema
if PYDANTIC_1:
new_field.field_info = field.field_info
else: # pragma: nocover
new_field.schema = field.schema # type: ignore
new_field.allow_none = field.allow_none
new_field.validate_always = field.validate_always
if field.sub_fields:
@@ -87,11 +137,19 @@ def create_cloned_field(field: Field) -> Field:
if field.key_field:
new_field.key_field = create_cloned_field(field.key_field)
new_field.validators = field.validators
new_field.whole_pre_validators = field.whole_pre_validators
new_field.whole_post_validators = field.whole_post_validators
if PYDANTIC_1:
new_field.pre_validators = field.pre_validators
new_field.post_validators = field.post_validators
else: # pragma: nocover
new_field.whole_pre_validators = field.whole_pre_validators # type: ignore
new_field.whole_post_validators = field.whole_post_validators # type: ignore
new_field.parse_json = field.parse_json
new_field.shape = field.shape
new_field._populate_validators()
try:
new_field.populate_validators()
except AttributeError: # pragma: nocover
# TODO: remove when removing support for Pydantic < 1.0.0
new_field._populate_validators() # type: ignore
return new_field

View File

@@ -1,5 +1,6 @@
site_name: FastAPI
site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production
site_url: https://fastapi.tiangolo.com/
theme:
name: 'material'
@@ -57,6 +58,7 @@ nav:
- Classes as Dependencies: 'tutorial/dependencies/classes-as-dependencies.md'
- Sub-dependencies: 'tutorial/dependencies/sub-dependencies.md'
- Dependencies in path operation decorators: 'tutorial/dependencies/dependencies-in-path-operation-decorators.md'
- Dependencies with yield: 'tutorial/dependencies/dependencies-with-yield.md'
- Advanced Dependencies: 'tutorial/dependencies/advanced-dependencies.md'
- Security:
- Security Intro: 'tutorial/security/intro.md'
@@ -81,6 +83,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,<2.0.0"
]
description-file = "README.md"
requires-python = ">=3.6"
@@ -39,6 +39,9 @@ test = [
"email_validator",
"sqlalchemy",
"databases[sqlite]",
"orjson",
"async_exit_stack",
"async_generator"
]
doc = [
"mkdocs",
@@ -60,4 +63,6 @@ all = [
"ujson",
"email_validator",
"uvicorn",
"async_exit_stack",
"async_generator"
]

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

@@ -68,7 +68,7 @@ openapi_schema = {
"parameters": [
{
"required": True,
"schema": {"title": "Item_Id"},
"schema": {"title": "Item Id"},
"name": "item_id",
"in": "path",
}
@@ -98,7 +98,7 @@ openapi_schema = {
"parameters": [
{
"required": True,
"schema": {"title": "Item_Id", "type": "string"},
"schema": {"title": "Item Id", "type": "string"},
"name": "item_id",
"in": "path",
}
@@ -128,7 +128,7 @@ openapi_schema = {
"parameters": [
{
"required": True,
"schema": {"title": "Item_Id", "type": "integer"},
"schema": {"title": "Item Id", "type": "integer"},
"name": "item_id",
"in": "path",
}
@@ -158,7 +158,7 @@ openapi_schema = {
"parameters": [
{
"required": True,
"schema": {"title": "Item_Id", "type": "number"},
"schema": {"title": "Item Id", "type": "number"},
"name": "item_id",
"in": "path",
}
@@ -188,7 +188,7 @@ openapi_schema = {
"parameters": [
{
"required": True,
"schema": {"title": "Item_Id", "type": "boolean"},
"schema": {"title": "Item Id", "type": "boolean"},
"name": "item_id",
"in": "path",
}
@@ -218,7 +218,7 @@ openapi_schema = {
"parameters": [
{
"required": True,
"schema": {"title": "Item_Id", "type": "string"},
"schema": {"title": "Item Id", "type": "string"},
"name": "item_id",
"in": "path",
}
@@ -248,7 +248,7 @@ openapi_schema = {
"parameters": [
{
"required": True,
"schema": {"title": "Item_Id", "type": "string"},
"schema": {"title": "Item Id", "type": "string"},
"name": "item_id",
"in": "path",
}
@@ -279,7 +279,7 @@ openapi_schema = {
{
"required": True,
"schema": {
"title": "Item_Id",
"title": "Item Id",
"minLength": 3,
"type": "string",
},
@@ -313,7 +313,7 @@ openapi_schema = {
{
"required": True,
"schema": {
"title": "Item_Id",
"title": "Item Id",
"maxLength": 3,
"type": "string",
},
@@ -347,7 +347,7 @@ openapi_schema = {
{
"required": True,
"schema": {
"title": "Item_Id",
"title": "Item Id",
"maxLength": 3,
"minLength": 2,
"type": "string",
@@ -382,7 +382,7 @@ openapi_schema = {
{
"required": True,
"schema": {
"title": "Item_Id",
"title": "Item Id",
"exclusiveMinimum": 3.0,
"type": "number",
},
@@ -416,7 +416,7 @@ openapi_schema = {
{
"required": True,
"schema": {
"title": "Item_Id",
"title": "Item Id",
"exclusiveMinimum": 0.0,
"type": "number",
},
@@ -450,7 +450,7 @@ openapi_schema = {
{
"required": True,
"schema": {
"title": "Item_Id",
"title": "Item Id",
"minimum": 3.0,
"type": "number",
},
@@ -484,7 +484,7 @@ openapi_schema = {
{
"required": True,
"schema": {
"title": "Item_Id",
"title": "Item Id",
"exclusiveMaximum": 3.0,
"type": "number",
},
@@ -518,7 +518,7 @@ openapi_schema = {
{
"required": True,
"schema": {
"title": "Item_Id",
"title": "Item Id",
"exclusiveMaximum": 0.0,
"type": "number",
},
@@ -552,7 +552,7 @@ openapi_schema = {
{
"required": True,
"schema": {
"title": "Item_Id",
"title": "Item Id",
"maximum": 3.0,
"type": "number",
},
@@ -586,7 +586,7 @@ openapi_schema = {
{
"required": True,
"schema": {
"title": "Item_Id",
"title": "Item Id",
"exclusiveMaximum": 3.0,
"exclusiveMinimum": 1.0,
"type": "number",
@@ -621,7 +621,7 @@ openapi_schema = {
{
"required": True,
"schema": {
"title": "Item_Id",
"title": "Item Id",
"maximum": 3.0,
"minimum": 1.0,
"type": "number",
@@ -656,7 +656,7 @@ openapi_schema = {
{
"required": True,
"schema": {
"title": "Item_Id",
"title": "Item Id",
"exclusiveMaximum": 3.0,
"type": "integer",
},
@@ -690,7 +690,7 @@ openapi_schema = {
{
"required": True,
"schema": {
"title": "Item_Id",
"title": "Item Id",
"exclusiveMinimum": 3.0,
"type": "integer",
},
@@ -724,7 +724,7 @@ openapi_schema = {
{
"required": True,
"schema": {
"title": "Item_Id",
"title": "Item Id",
"maximum": 3.0,
"type": "integer",
},
@@ -758,7 +758,7 @@ openapi_schema = {
{
"required": True,
"schema": {
"title": "Item_Id",
"title": "Item Id",
"minimum": 3.0,
"type": "integer",
},
@@ -792,7 +792,7 @@ openapi_schema = {
{
"required": True,
"schema": {
"title": "Item_Id",
"title": "Item Id",
"exclusiveMaximum": 3.0,
"exclusiveMinimum": 1.0,
"type": "integer",
@@ -827,7 +827,7 @@ openapi_schema = {
{
"required": True,
"schema": {
"title": "Item_Id",
"title": "Item Id",
"maximum": 3.0,
"minimum": 1.0,
"type": "integer",

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,70 @@
import pytest
from fastapi import Depends, FastAPI
from starlette.testclient import TestClient
app = FastAPI()
class CallableDependency:
def __call__(self, value: str) -> str:
return value
class AsyncCallableDependency:
async def __call__(self, value: str) -> str:
return value
class MethodsDependency:
def synchronous(self, value: str) -> str:
return value
async def asynchronous(self, value: str) -> str:
return value
callable_dependency = CallableDependency()
async_callable_dependency = AsyncCallableDependency()
methods_dependency = MethodsDependency()
@app.get("/callable-dependency")
async def get_callable_dependency(value: str = Depends(callable_dependency)):
return value
@app.get("/async-callable-dependency")
async def get_callable_dependency(value: str = Depends(async_callable_dependency)):
return value
@app.get("/synchronous-method-dependency")
async def get_synchronous_method_dependency(
value: str = Depends(methods_dependency.synchronous),
):
return value
@app.get("/asynchronous-method-dependency")
async def get_asynchronous_method_dependency(
value: str = Depends(methods_dependency.asynchronous),
):
return value
client = TestClient(app)
@pytest.mark.parametrize(
"route,value",
[
("/callable-dependency", "callable-dependency"),
("/async-callable-dependency", "async-callable-dependency"),
("/synchronous-method-dependency", "synchronous-method-dependency"),
("/asynchronous-method-dependency", "asynchronous-method-dependency"),
],
)
def test_class_dependency(route, value):
response = client.get(route, params={"value": value})
assert response.status_code == 200
assert response.json() == value

View File

@@ -0,0 +1,349 @@
from typing import Dict
import pytest
from fastapi import BackgroundTasks, Depends, FastAPI
from starlette.testclient import TestClient
app = FastAPI()
state = {
"/async": "asyncgen not started",
"/sync": "generator not started",
"/async_raise": "asyncgen raise not started",
"/sync_raise": "generator raise not started",
"context_a": "not started a",
"context_b": "not started b",
"bg": "not set",
"sync_bg": "not set",
}
errors = []
async def get_state():
return state
class AsyncDependencyError(Exception):
pass
class SyncDependencyError(Exception):
pass
class OtherDependencyError(Exception):
pass
async def asyncgen_state(state: Dict[str, str] = Depends(get_state)):
state["/async"] = "asyncgen started"
yield state["/async"]
state["/async"] = "asyncgen completed"
def generator_state(state: Dict[str, str] = Depends(get_state)):
state["/sync"] = "generator started"
yield state["/sync"]
state["/sync"] = "generator completed"
async def asyncgen_state_try(state: Dict[str, str] = Depends(get_state)):
state["/async_raise"] = "asyncgen raise started"
try:
yield state["/async_raise"]
except AsyncDependencyError:
errors.append("/async_raise")
finally:
state["/async_raise"] = "asyncgen raise finalized"
def generator_state_try(state: Dict[str, str] = Depends(get_state)):
state["/sync_raise"] = "generator raise started"
try:
yield state["/sync_raise"]
except SyncDependencyError:
errors.append("/sync_raise")
finally:
state["/sync_raise"] = "generator raise finalized"
async def context_a(state: dict = Depends(get_state)):
state["context_a"] = "started a"
try:
yield state
finally:
state["context_a"] = "finished a"
async def context_b(state: dict = Depends(context_a)):
state["context_b"] = "started b"
try:
yield state
finally:
state["context_b"] = f"finished b with a: {state['context_a']}"
@app.get("/async")
async def get_async(state: str = Depends(asyncgen_state)):
return state
@app.get("/sync")
async def get_sync(state: str = Depends(generator_state)):
return state
@app.get("/async_raise")
async def get_async_raise(state: str = Depends(asyncgen_state_try)):
assert state == "asyncgen raise started"
raise AsyncDependencyError()
@app.get("/sync_raise")
async def get_sync_raise(state: str = Depends(generator_state_try)):
assert state == "generator raise started"
raise SyncDependencyError()
@app.get("/async_raise_other")
async def get_async_raise_other(state: str = Depends(asyncgen_state_try)):
assert state == "asyncgen raise started"
raise OtherDependencyError()
@app.get("/sync_raise_other")
async def get_sync_raise_other(state: str = Depends(generator_state_try)):
assert state == "generator raise started"
raise OtherDependencyError()
@app.get("/context_b")
async def get_context_b(state: dict = Depends(context_b)):
return state
@app.get("/context_b_raise")
async def get_context_b_raise(state: dict = Depends(context_b)):
assert state["context_b"] == "started b"
assert state["context_a"] == "started a"
raise OtherDependencyError()
@app.get("/context_b_bg")
async def get_context_b_bg(tasks: BackgroundTasks, state: dict = Depends(context_b)):
async def bg(state: dict):
state["bg"] = f"bg set - b: {state['context_b']} - a: {state['context_a']}"
tasks.add_task(bg, state)
return state
# Sync versions
@app.get("/sync_async")
def get_sync_async(state: str = Depends(asyncgen_state)):
return state
@app.get("/sync_sync")
def get_sync_sync(state: str = Depends(generator_state)):
return state
@app.get("/sync_async_raise")
def get_sync_async_raise(state: str = Depends(asyncgen_state_try)):
assert state == "asyncgen raise started"
raise AsyncDependencyError()
@app.get("/sync_sync_raise")
def get_sync_sync_raise(state: str = Depends(generator_state_try)):
assert state == "generator raise started"
raise SyncDependencyError()
@app.get("/sync_async_raise_other")
def get_sync_async_raise_other(state: str = Depends(asyncgen_state_try)):
assert state == "asyncgen raise started"
raise OtherDependencyError()
@app.get("/sync_sync_raise_other")
def get_sync_sync_raise_other(state: str = Depends(generator_state_try)):
assert state == "generator raise started"
raise OtherDependencyError()
@app.get("/sync_context_b")
def get_sync_context_b(state: dict = Depends(context_b)):
return state
@app.get("/sync_context_b_raise")
def get_sync_context_b_raise(state: dict = Depends(context_b)):
assert state["context_b"] == "started b"
assert state["context_a"] == "started a"
raise OtherDependencyError()
@app.get("/sync_context_b_bg")
async def get_sync_context_b_bg(
tasks: BackgroundTasks, state: dict = Depends(context_b)
):
async def bg(state: dict):
state[
"sync_bg"
] = f"sync_bg set - b: {state['context_b']} - a: {state['context_a']}"
tasks.add_task(bg, state)
return state
client = TestClient(app)
def test_async_state():
assert state["/async"] == f"asyncgen not started"
response = client.get("/async")
assert response.status_code == 200
assert response.json() == f"asyncgen started"
assert state["/async"] == f"asyncgen completed"
def test_sync_state():
assert state["/sync"] == f"generator not started"
response = client.get("/sync")
assert response.status_code == 200
assert response.json() == f"generator started"
assert state["/sync"] == f"generator completed"
def test_async_raise_other():
assert state["/async_raise"] == "asyncgen raise not started"
with pytest.raises(OtherDependencyError):
client.get("/async_raise_other")
assert state["/async_raise"] == "asyncgen raise finalized"
assert "/async_raise" not in errors
def test_sync_raise_other():
assert state["/sync_raise"] == "generator raise not started"
with pytest.raises(OtherDependencyError):
client.get("/sync_raise_other")
assert state["/sync_raise"] == "generator raise finalized"
assert "/sync_raise" not in errors
def test_async_raise():
response = client.get("/async_raise")
assert response.status_code == 500
assert state["/async_raise"] == "asyncgen raise finalized"
assert "/async_raise" in errors
errors.clear()
def test_context_b():
response = client.get("/context_b")
data = response.json()
assert data["context_b"] == "started b"
assert data["context_a"] == "started a"
assert state["context_b"] == "finished b with a: started a"
assert state["context_a"] == "finished a"
def test_context_b_raise():
with pytest.raises(OtherDependencyError):
client.get("/context_b_raise")
assert state["context_b"] == "finished b with a: started a"
assert state["context_a"] == "finished a"
def test_background_tasks():
response = client.get("/context_b_bg")
data = response.json()
assert data["context_b"] == "started b"
assert data["context_a"] == "started a"
assert data["bg"] == "not set"
assert state["context_b"] == "finished b with a: started a"
assert state["context_a"] == "finished a"
assert state["bg"] == "bg set - b: started b - a: started a"
def test_sync_raise():
response = client.get("/sync_raise")
assert response.status_code == 500
assert state["/sync_raise"] == "generator raise finalized"
assert "/sync_raise" in errors
errors.clear()
def test_sync_async_state():
response = client.get("/sync_async")
assert response.status_code == 200
assert response.json() == f"asyncgen started"
assert state["/async"] == f"asyncgen completed"
def test_sync_sync_state():
response = client.get("/sync_sync")
assert response.status_code == 200
assert response.json() == f"generator started"
assert state["/sync"] == f"generator completed"
def test_sync_async_raise_other():
with pytest.raises(OtherDependencyError):
client.get("/sync_async_raise_other")
assert state["/async_raise"] == "asyncgen raise finalized"
assert "/async_raise" not in errors
def test_sync_sync_raise_other():
with pytest.raises(OtherDependencyError):
client.get("/sync_sync_raise_other")
assert state["/sync_raise"] == "generator raise finalized"
assert "/sync_raise" not in errors
def test_sync_async_raise():
response = client.get("/sync_async_raise")
assert response.status_code == 500
assert state["/async_raise"] == "asyncgen raise finalized"
assert "/async_raise" in errors
errors.clear()
def test_sync_sync_raise():
response = client.get("/sync_sync_raise")
assert response.status_code == 500
assert state["/sync_raise"] == "generator raise finalized"
assert "/sync_raise" in errors
errors.clear()
def test_sync_context_b():
response = client.get("/sync_context_b")
data = response.json()
assert data["context_b"] == "started b"
assert data["context_a"] == "started a"
assert state["context_b"] == "finished b with a: started a"
assert state["context_a"] == "finished a"
def test_sync_context_b_raise():
with pytest.raises(OtherDependencyError):
client.get("/sync_context_b_raise")
assert state["context_b"] == "finished b with a: started a"
assert state["context_a"] == "finished a"
def test_sync_background_tasks():
response = client.get("/sync_context_b_bg")
data = response.json()
assert data["context_b"] == "started b"
assert data["context_a"] == "started a"
assert data["sync_bg"] == "not set"
assert state["context_b"] == "finished b with a: started a"
assert state["context_a"] == "finished a"
assert state["sync_bg"] == "sync_bg set - b: started b - a: started a"

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

@@ -77,7 +77,7 @@ openapi_schema = {
"parameters": [
{
"required": True,
"schema": {"title": "Item_Id", "type": "string"},
"schema": {"title": "Item Id", "type": "string"},
"name": "item_id",
"in": "path",
}
@@ -105,7 +105,7 @@ openapi_schema = {
"parameters": [
{
"required": True,
"schema": {"title": "Item_Id", "type": "string"},
"schema": {"title": "Item Id", "type": "string"},
"name": "item_id",
"in": "path",
}
@@ -141,7 +141,7 @@ openapi_schema = {
"parameters": [
{
"required": True,
"schema": {"title": "Item_Id", "type": "string"},
"schema": {"title": "Item Id", "type": "string"},
"name": "item_id",
"in": "path",
}
@@ -169,7 +169,7 @@ openapi_schema = {
"parameters": [
{
"required": True,
"schema": {"title": "Item_Id", "type": "string"},
"schema": {"title": "Item Id", "type": "string"},
"name": "item_id",
"in": "path",
}
@@ -197,7 +197,7 @@ openapi_schema = {
"parameters": [
{
"required": True,
"schema": {"title": "Item_Id", "type": "string"},
"schema": {"title": "Item Id", "type": "string"},
"name": "item_id",
"in": "path",
}
@@ -233,7 +233,7 @@ openapi_schema = {
"parameters": [
{
"required": True,
"schema": {"title": "Item_Id", "type": "string"},
"schema": {"title": "Item Id", "type": "string"},
"name": "item_id",
"in": "path",
}
@@ -263,7 +263,7 @@ openapi_schema = {
"parameters": [
{
"required": True,
"schema": {"title": "Item_Id", "type": "string"},
"schema": {"title": "Item Id", "type": "string"},
"name": "item_id",
"in": "path",
}

12
tests/test_fakeasync.py Normal file
View File

@@ -0,0 +1,12 @@
import pytest
from fastapi.concurrency import _fake_asynccontextmanager
@_fake_asynccontextmanager
def never_run():
pass # pragma: no cover
def test_fake_async():
with pytest.raises(RuntimeError):
never_run()

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

@@ -3,7 +3,13 @@ from enum import Enum
import pytest
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel, Schema, ValidationError
from pydantic import BaseModel, ValidationError
try:
from pydantic import Field
except ImportError: # pragma: nocover
# TODO: remove when removing support for Pydantic < 1.0.0
from pydantic import Schema as Field
class Person:
@@ -60,7 +66,7 @@ class ModelWithConfig(BaseModel):
class ModelWithAlias(BaseModel):
foo: str = Schema(..., alias="Foo")
foo: str = Field(..., alias="Foo")
def test_encode_class():
@@ -99,3 +105,18 @@ def test_encode_model_with_alias_raises():
def test_encode_model_with_alias():
model = ModelWithAlias(Foo="Bar")
assert jsonable_encoder(model) == {"Foo": "Bar"}
def test_custom_encoders():
class safe_datetime(datetime):
pass
class MyModel(BaseModel):
dt_field: safe_datetime
instance = MyModel(dt_field=safe_datetime.now())
encoded_instance = jsonable_encoder(
instance, custom_encoder={safe_datetime: lambda o: o.isoformat()}
)
assert encoded_instance["dt_field"] == instance.dt_field.isoformat()

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

@@ -18,6 +18,16 @@ def test_nonexistent():
assert response.json() == {"detail": "Not Found"}
response_not_valid_bool = {
"detail": [
{
"loc": ["path", "item_id"],
"msg": "value could not be parsed to a boolean",
"type": "type_error.bool",
}
]
}
response_not_valid_int = {
"detail": [
{
@@ -173,10 +183,10 @@ response_less_than_equal_3 = {
("/path/float/True", 422, response_not_valid_float),
("/path/float/42", 200, 42),
("/path/float/42.5", 200, 42.5),
("/path/bool/foobar", 200, False),
("/path/bool/foobar", 422, response_not_valid_bool),
("/path/bool/True", 200, True),
("/path/bool/42", 200, False),
("/path/bool/42.5", 200, False),
("/path/bool/42", 422, response_not_valid_bool),
("/path/bool/42.5", 422, response_not_valid_bool),
("/path/bool/1", 200, True),
("/path/bool/0", 200, False),
("/path/bool/true", 200, True),

View File

@@ -39,7 +39,7 @@ openapi_schema = {
"parameters": [
{
"required": True,
"schema": {"title": "Item_Id", "type": "string"},
"schema": {"title": "Item Id", "type": "string"},
"name": "item_id",
"in": "path",
}

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

@@ -0,0 +1,114 @@
import typing
from fastapi import FastAPI
from pydantic import BaseModel
from starlette.responses import JSONResponse, Response
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=Response,
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/json": {
"schema": {"$ref": "#/components/schemas/JsonApiError"}
}
},
},
"200": {"description": "Successful Response"},
},
"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

@@ -0,0 +1,108 @@
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",
status_code=204,
response_class=JsonApiResponse,
responses={500: {"description": "Error", "model": JsonApiError}},
)
async def a():
pass # pragma: no cover
@app.get("/b", responses={204: {"description": "No Content"}})
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"}
}
},
},
"204": {"description": "Successful Response"},
},
"summary": "A",
"operationId": "a_a_get",
}
},
"/b": {
"get": {
"responses": {
"204": {"description": "No Content"},
"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

@@ -11,7 +11,7 @@ security = HTTPBase(scheme="Other", auto_error=False)
@app.get("/users/me")
def read_current_user(
credentials: Optional[HTTPAuthorizationCredentials] = Security(security)
credentials: Optional[HTTPAuthorizationCredentials] = Security(security),
):
if credentials is None:
return {"msg": "Create an account first"}

View File

@@ -11,7 +11,7 @@ security = HTTPBearer(auto_error=False)
@app.get("/users/me")
def read_current_user(
credentials: Optional[HTTPAuthorizationCredentials] = Security(security)
credentials: Optional[HTTPAuthorizationCredentials] = Security(security),
):
if credentials is None:
return {"msg": "Create an account first"}

View File

@@ -11,7 +11,7 @@ security = HTTPDigest(auto_error=False)
@app.get("/users/me")
def read_current_user(
credentials: Optional[HTTPAuthorizationCredentials] = Security(security)
credentials: Optional[HTTPAuthorizationCredentials] = Security(security),
):
if credentials is None:
return {"msg": "Create an account first"}

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
@@ -96,15 +99,15 @@ openapi_schema = {
"type": "object",
"properties": {
"grant_type": {
"title": "Grant_Type",
"title": "Grant Type",
"pattern": "password",
"type": "string",
},
"username": {"title": "Username", "type": "string"},
"password": {"title": "Password", "type": "string"},
"scope": {"title": "Scope", "type": "string", "default": ""},
"client_id": {"title": "Client_Id", "type": "string"},
"client_secret": {"title": "Client_Secret", "type": "string"},
"client_id": {"title": "Client Id", "type": "string"},
"client_secret": {"title": "Client Secret", "type": "string"},
},
},
"ValidationError": {

View File

@@ -103,15 +103,15 @@ openapi_schema = {
"type": "object",
"properties": {
"grant_type": {
"title": "Grant_Type",
"title": "Grant Type",
"pattern": "password",
"type": "string",
},
"username": {"title": "Username", "type": "string"},
"password": {"title": "Password", "type": "string"},
"scope": {"title": "Scope", "type": "string", "default": ""},
"client_id": {"title": "Client_Id", "type": "string"},
"client_secret": {"title": "Client_Secret", "type": "string"},
"client_id": {"title": "Client Id", "type": "string"},
"client_secret": {"title": "Client Secret", "type": "string"},
},
},
"ValidationError": {

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]},
]

Some files were not shown because too many files have changed in this diff Show More