Compare commits

...

57 Commits

Author SHA1 Message Date
Sebastián Ramírez
c7c69586ae 🔖 Release version 0.45.0 2019-12-11 18:05:19 +01:00
Sebastián Ramírez
4e09feda9e 📝 Update release notes 2019-12-11 18:02:53 +01:00
Ben Dayan
73260971b5 Add support for OpenAPI Callbacks (#722) 2019-12-11 17:58:00 +01:00
Sebastián Ramírez
b36bfff56e 📝 Update release notes 2019-12-09 20:04:04 +01:00
Sebastián Ramírez
83d04df8a6 🔊 Refactor logging (#781) 2019-12-09 20:02:44 +01:00
Sebastián Ramírez
7bc78c5fd3 📝 Update release notes 2019-12-09 19:15:37 +01:00
prostomarkeloff
ae8fa3aacd 📝 Add article about FastAPI to external links (#766) 2019-12-09 19:13:28 +01:00
Sebastián Ramírez
08bc120771 📝 Update release notes 2019-12-09 19:01:40 +01:00
Sebastián Ramírez
a39efb029f 💬 Rephrase handling-errors to remove gender while keeping readability (#780) 2019-12-09 18:59:29 +01:00
Sebastián Ramírez
58ca98285f 📝 Update release notes 2019-12-09 18:43:09 +01:00
prostomarkeloff
3f5f81bbdc 📝 Change 'Schema' to 'Field' in docs (#746) 2019-12-09 14:48:54 +01:00
Sebastián Ramírez
90236c8135 🔖 Release version 0.44.1 2019-12-05 00:17:04 +01:00
Sebastián Ramírez
c200bc2240 📝 Update release notes 2019-11-29 09:02:21 +01:00
Sebastián Ramírez
e9861cd918 🍱 Add GitHub social preview assets to git (#752) 2019-11-29 08:52:03 +01:00
Sebastián Ramírez
202fa11d50 📝 Update release notes 2019-11-29 08:30:10 +01:00
Sebastián Ramírez
4b6e09296c 🔧 Update PyPI trove classifiers (#751) 2019-11-29 08:29:37 +01:00
Sebastián Ramírez
9bd0d6fa96 👷 Enable full Travis for Python 3.8 (#750) 2019-11-29 07:45:09 +01:00
Sebastián Ramírez
35510a5ea7 📝 Update the "new issue" templates (#749)
* 🔧 Update the "new issue" templates

* 💚 Trigger Travis CI after Travis migration
2019-11-29 07:35:25 +01:00
Sebastián Ramírez
c1788a25c7 📝 Update release notes 2019-11-29 07:04:01 +01:00
dmontagu
19c77e35bd 🐛 Fix issue with exotic pydantic error serialization (#748) 2019-11-29 07:02:10 +01:00
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
120 changed files with 2475 additions and 746 deletions

View File

@@ -7,29 +7,48 @@ assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
### Describe the bug
**To Reproduce**
Steps to reproduce the behavior:
1. Create a file with '...'
2. Add a path operation function with '....'
3. Open the browser and call it with a payload of '....'
4. See error
Write here a clear and concise description of what the bug is.
**Expected behavior**
A clear and concise description of what you expected to happen.
### To Reproduce
**Screenshots**
If applicable, add screenshots to help explain your problem.
Steps to reproduce the behavior with a minimum self-contained file.
**Environment:**
- OS: [e.g. Linux / Windows / macOS]
- FastAPI Version [e.g. 0.3.0], get it with:
Replace each part with your own scenario:
1. Create a file with:
```Python
import fastapi
print(fastapi.__version__)
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"Hello": "World"}
```
3. Open the browser and call the endpoint `/`.
4. It returns a JSON with `{"Hello": "World"}`.
5. But I expected it to return `{"Hello": "Sara"}`.
### Expected behavior
Add a clear and concise description of what you expected to happen.
### Screenshots
If applicable, add screenshots to help explain your problem.
### Environment
- OS: [e.g. Linux / Windows / macOS]
- FastAPI Version [e.g. 0.3.0], get it with:
```bash
python -c "import fastapi; print(fastapi.__version__)"
```
- Python version, get it with:
@@ -38,5 +57,6 @@ print(fastapi.__version__)
python --version
```
**Additional context**
### Additional context
Add any other context about the problem here.

View File

@@ -7,14 +7,20 @@ assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I want to be able to [...] but I can't because [...]
### Is your feature request related to a problem
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
Is your feature request related to a problem?
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
Add a clear and concise description of what the problem is. Ex. I want to be able to [...] but I can't because [...]
### The solution you would like
Add a clear and concise description of what you want to happen.
### Describe alternatives you've considered
Add a clear and concise description of any alternative solutions or features you've considered.
### Additional context
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -7,11 +7,18 @@ assignees: ''
---
**Description**
### First check
* [ ] I used the GitHub search to find a similar issue and didn't find it.
* [ ] I searched the FastAPI documentation, with the integrated search.
* [ ] I already searched in Google "How to X in FastAPI" and didn't find any information.
### Description
How can I [...]?
Is it possible to [...]?
**Additional context**
### Additional context
Add any other context or screenshots about the feature request here.

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,12 +7,11 @@ cache: pip
python:
- "3.6"
- "3.7"
- "3.8-dev"
- "3.8"
- "nightly"
matrix:
allow_failures:
- python: "3.8-dev"
- python: "nightly"
install:

View File

@@ -26,7 +26,7 @@ uvicorn = "*"
[packages]
starlette = "==0.12.9"
pydantic = "==0.32.2"
pydantic = "==1.0.0"
databases = {extras = ["sqlite"],version = "*"}
hypercorn = "*"
orjson = "*"

View File

@@ -90,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

@@ -35,6 +35,8 @@ Here's an incomplete list of some of them.
* <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>.
@@ -61,6 +63,8 @@ Here's an incomplete list of some of them.
* <a href="https://habr.com/ru/post/454440/" target="_blank">Мелкая питонячая радость #2: Starlette - Солидная примочка FastAPI</a> by <a href="https://habr.com/ru/users/57uff3r/" target="_blank">Andrey Korchak</a>.
* <a href="https://habr.com/ru/post/478620/" target="_blank">Почему Вы должны попробовать FastAPI?</a> by <a href="https://github.com/prostomarkeloff" target="_blank">prostomarkeloff</a>.
## Podcasts
* <a href="https://pythonbytes.fm/episodes/show/123/time-to-right-the-py-wrongs?time_in_sec=855" target="_blank">FastAPI on PythonBytes</a> by <a href="https://pythonbytes.fm/" target="_blank">Python Bytes FM</a>.

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View File

@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
id="svg8"
version="1.1"
viewBox="0 0 338.66665 169.33332"
height="169.33333mm"
width="338.66666mm"
sodipodi:docname="github-social-preview.svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
inkscape:export-filename="/home/user/code/fastapi/docs/img/github-social-preview.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1025"
id="namedview9"
showgrid="false"
inkscape:zoom="0.52249777"
inkscape:cx="565.37328"
inkscape:cy="403.61034"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg8" />
<defs
id="defs2" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<rect
style="opacity:0.98000004;fill:#ffffff;fill-opacity:1;stroke-width:0.26458332"
id="rect853"
width="338.66666"
height="169.33333"
x="-1.0833333e-05"
y="0.71613133"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96" />
<g
transform="matrix(0.73259569,0,0,0.73259569,64.842852,-4.5763945)"
id="layer1">
<path
style="opacity:0.98000004;fill:#009688;fill-opacity:1;stroke-width:3.20526505"
id="path817"
d="m 1.4365174,55.50154 c -17.6610514,0 -31.9886064,14.327532 -31.9886064,31.988554 0,17.661036 14.327555,31.988586 31.9886064,31.988586 17.6609756,0 31.9885196,-14.32755 31.9885196,-31.988586 0,-17.661022 -14.327544,-31.988554 -31.9885196,-31.988554 z m -1.66678692,57.63069 V 93.067264 H -11.384533 L 4.6417437,61.847974 V 81.912929 H 15.379405 Z"
inkscape:connector-curvature="0" />
<text
id="text979"
y="114.91215"
x="52.115433"
style="font-style:normal;font-weight:normal;font-size:79.71511078px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#009688;fill-opacity:1;stroke:none;stroke-width:1.99287772"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#009688;fill-opacity:1;stroke-width:1.99287772"
y="114.91215"
x="52.115433"
id="tspan977">FastAPI</tspan></text>
</g>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:10.58333302px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
x="169.60979"
y="119.20409"
id="text851"><tspan
sodipodi:role="line"
id="tspan849"
x="169.60979"
y="119.20409"
style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:'Roboto Italic';text-align:center;text-anchor:middle;stroke-width:0.26458332">High performance, easy to learn,</tspan><tspan
sodipodi:role="line"
x="169.60979"
y="132.53661"
style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:'Roboto Italic';text-align:center;text-anchor:middle;stroke-width:0.26458332"
id="tspan855">fast to code, ready for production</tspan></text>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

View File

@@ -90,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,48 @@
## Latest changes
## 0.45.0
* Add support for OpenAPI Callbacks:
* New docs: [OpenAPI Callbacks](https://fastapi.tiangolo.com/tutorial/openapi-callbacks/).
* Refactor generation of `operationId`s to be valid Python names (also valid variables in most languages).
* Add `default_response_class` parameter to `APIRouter`.
* Original PR [#722](https://github.com/tiangolo/fastapi/pull/722) by [@booooh](https://github.com/booooh).
* Refactor logging to use the same logger everywhere, update log strings and levels. PR [#781](https://github.com/tiangolo/fastapi/pull/781).
* Add article to [External Links](https://fastapi.tiangolo.com/external-links/): [Почему Вы должны попробовать FastAPI?](https://habr.com/ru/post/478620/). PR [#766](https://github.com/tiangolo/fastapi/pull/766) by [@prostomarkeloff](https://github.com/prostomarkeloff).
* Remove gender bias in docs for handling errors. PR [#780](https://github.com/tiangolo/fastapi/pull/780). Original idea in PR [#761](https://github.com/tiangolo/fastapi/pull/761) by [@classywhetten](https://github.com/classywhetten).
* Rename docs and references to `body-schema` to `body-fields` to keep in line with Pydantic. PR [#746](https://github.com/tiangolo/fastapi/pull/746) by [@prostomarkeloff](https://github.com/prostomarkeloff).
## 0.44.1
* Add GitHub social preview images to git. PR [#752](https://github.com/tiangolo/fastapi/pull/752).
* Update PyPI "trove classifiers". PR [#751](https://github.com/tiangolo/fastapi/pull/751).
* Add full support for Python 3.8. Enable Python 3.8 in full in Travis. PR [749](https://github.com/tiangolo/fastapi/pull/749).
* Update "new issue" templates. PR [#749](https://github.com/tiangolo/fastapi/pull/749).
* Fix serialization of errors for exotic Pydantic types. PR [#748](https://github.com/tiangolo/fastapi/pull/748) by [@dmontagu](https://github.com/dmontagu).
## 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, ...

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

@@ -0,0 +1,53 @@
from fastapi import APIRouter, FastAPI
from pydantic import BaseModel, HttpUrl
from starlette.responses import JSONResponse
app = FastAPI()
class Invoice(BaseModel):
id: str
title: str = None
customer: str
total: float
class InvoiceEvent(BaseModel):
description: str
paid: bool
class InvoiceEventReceived(BaseModel):
ok: bool
invoices_callback_router = APIRouter(default_response_class=JSONResponse)
@invoices_callback_router.post(
"{$callback_url}/invoices/{$request.body.id}",
response_model=InvoiceEventReceived,
)
def invoice_notification(body: InvoiceEvent):
pass
@app.post("/invoices/", callbacks=invoices_callback_router.routes)
def create_invoice(invoice: Invoice, callback_url: HttpUrl = None):
"""
Create an invoice.
This will (let's imagine) let the API user (some external developer) create an
invoice.
And this path operation will:
* Send the invoice to the client.
* Collect the money from the client.
* Send a notification back to the API user (the external developer), as a callback.
* At this point is that the API will somehow send a POST request to the
external API with the notification of the invoice event
(e.g. "payment successful").
"""
# Send the invoice, collect the money, send the notification (the callback)
return {"msg": "Invoice received"}

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

@@ -1,30 +1,8 @@
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
@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

@@ -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

@@ -0,0 +1,62 @@
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 `Field`
First, you have to import it:
```Python hl_lines="2"
{!./src/body_fields/tutorial001.py!}
```
!!! warning
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 `Field` with model attributes:
```Python hl_lines="9 10"
{!./src/body_fields/tutorial001.py!}
```
`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 create objects of subclasses of a common `Param` class, which is itself a subclass of Pydantic's `FieldInfo` class.
And Pydantic's `Field` returns an instance of `FieldInfo` as well.
`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 `Field` has the same structure as a path operation function's parameter, with `Field` instead of `Path`, `Query` and `Body`.
## JSON Schema extras
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.
If you know JSON Schema and want to add extra information apart from what we have discussed here, you can pass that as extra keyword arguments.
!!! warning
Have in mind that extra parameters passed won't add any validation, only annotation, for documentation purposes.
For example, you can use that functionality to pass a <a href="http://json-schema.org/latest/json-schema-validation.html#rfc.section.8.5" target="_blank">JSON Schema example</a> field to a body request JSON Schema:
```Python hl_lines="20 21 22 23 24 25"
{!./src/body_fields/tutorial002.py!}
```
And it would look in the `/docs` like this:
<img src="/img/tutorial/body-fields/image01.png">
## Recap
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.

View File

@@ -1,60 +0,0 @@
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`.
## Import Schema
First, you have to import it:
```Python hl_lines="2"
{!./src/body_schema/tutorial001.py!}
```
!!! warning
Notice that `Schema` 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:
```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.
!!! 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`.
`Body` is also a subclass of `Schema` directly. And there are others you will see later that are subclasses of `Body`.
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>.
!!! 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`.
## Schema extras
In `Schema`, `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.
If you know JSON Schema and want to add extra information apart from what we have discussed here, you can pass that as extra keyword arguments.
!!! warning
Have in mind that extra parameters passed won't add any validation, only annotation, for documentation purposes.
For example, you can use that functionality to pass a <a href="http://json-schema.org/latest/json-schema-validation.html#rfc.section.8.5" target="_blank">JSON Schema example</a> field to a body request JSON Schema:
```Python hl_lines="20 21 22 23 24 25"
{!./src/body_schema/tutorial002.py!}
```
And it would look in the `/docs` like this:
<img src="/img/tutorial/body-schema/image01.png">
## Recap
You can use Pydantic's `Schema` to declare extra validations and metadata for model attributes.
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

@@ -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

@@ -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

@@ -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

@@ -4,9 +4,9 @@ This client could be a browser with a frontend, the code from someone else, an I
You could need to tell that client that:
* He doesn't have enough privileges for that operation.
* He doesn't have access to that resource.
* The item he was trying to access doesn't exist.
* The client doesn't have enough privileges for that operation.
* The client doesn't have access to that resource.
* The item the client was trying to access doesn't exist.
* etc.
In these cases, you would normally return an **HTTP status code** in the range of **400** (from 400 to 499).

View File

@@ -0,0 +1,186 @@
You could create an API with a *path operation* that could trigger a request to an *external API* created by someone else (probably the same developer that would be *using* your API).
The process that happens when your API app calls the *external API* is named a "callback". Because the software that the external developer wrote sends a request to your API and then your API *calls back*, sending a request to an *external API* (that was probably created by the same developer).
In this case, you could want to document how that external API *should* look like. What *path operation* it should have, what body it should expect, what response it should return, etc.
## An app with callbacks
Let's see all this with an example.
Imagine you develop an app that allows creating invoices.
These invoices will have an `id`, `title` (optional), `customer`, and `total`.
The user of your API (an external developer) will create an invoice in your API with a POST request.
Then your API will (let's imagine):
* Send the invoice to some customer of the external developer.
* Collect the money.
* Send a notification back to the API user (the external developer).
* This will be done by sending a POST request (from *your API*) to some *external API* provided by that external developer (this is the "callback").
## The normal **FastAPI** app
Let's first see how the normal API app would look like before adding the callback.
It will have a *path operation* that will receive an `Invoice` body, and a query parameter `callback_url` that will contain the URL for the callback.
This part is pretty normal, most of the code is probably already familiar to you:
```Python hl_lines="8 9 10 11 12 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54"
{!./src/openapi_callbacks/tutorial001.py!}
```
!!! tip
The `callback_url` query parameter uses a Pydantic <a href="https://pydantic-docs.helpmanual.io/usage/types/#urls" target="_blank">URL</a> type.
The only new thing is the `callbacks=messages_callback_router.routes` as an argument to the *path operation decorator*. We'll see what that is next.
## Documenting the callback
The actual callback code will depend heavily on your own API app.
And it will probably vary a lot from one app to the next.
It could be just one or two lines of code, like:
```Python
callback_url = "https://example.com/api/v1/invoices/events/"
requests.post(callback_url, json={"description": "Invoice paid", "paid": True})
```
But possibly the most important part of the callback is making sure that your API user (the external developer) implements the *external API* correctly, according to the data that *your API* is going to send in the request body of the callback, etc.
So, what we will do next is add the code to document how that *external API* should look like to receive the callback from *your API*.
That documentation will show up in the Swagger UI at `/docs` in your API, and it will let external developers know how to build the *external API*.
This example doesn't implement the callback itself (that could be just a line of code), only the documentation part.
!!! tip
The actual callback is just an HTTP request.
When implementing the callback yourself, you could use something like <a href="https://www.encode.io/httpx/" target="_blank">HTTPX</a> or <a href="https://requests.readthedocs.io/" target="_blank">Requests</a>.
## Write the callback documentation code
This code won't be executed in your app, we only need it to *document* how that *external API* should look like.
But, you already know how to easily create automatic documentation for an API with **FastAPI**.
So we are going to use that same knowledge to document how the *external API* should look like... by creating the *path operation(s)* that the external API should implement (the ones your API will call).
!!! tip
When writing the code to document a callback, it might be useful to imagine that you are that *external developer*. And that you are currently implementing the *external API*, not *your API*.
Temporarily adopting this point of view (of the *external developer*) can help you feel like it's more obvious where to put the parameters, the Pydantic model for the body, for the response, etc. for that *external API*.
### Create a callback `APIRouter`
First create a new `APIRouter` that will contain one or more callbacks.
This router will never be added to an actual `FastAPI` app (i.e. it will never be passed to `app.include_router(...)`).
Because of that, you need to declare what will be the `default_response_class`, and set it to `JSONResponse`.
!!! Note "Technical Details"
The `response_class` is normally set by the `FastAPI` app during the call to `app.include_router(some_router)`.
But as we are never calling `app.include_router(some_router)`, we need to set the `default_response_class` during creation of the `APIRouter`.
```Python hl_lines="3 24"
{!./src/openapi_callbacks/tutorial001.py!}
```
### Create the callback *path operation*
To create the callback *path operation* use the same `APIRouter` you created above.
It should look just like a normal FastAPI *path operation*:
* It should probably have a declaration of the body it should receive, e.g. `body: InvoiceEvent`.
* And it could also have a declaration of the response it should return, e.g. `response_model=InvoiceEventReceived`.
```Python hl_lines="15 16 17 20 21 27 28 29 30 31 32"
{!./src/openapi_callbacks/tutorial001.py!}
```
There are 2 main differences from a normal *path operation*:
* It doesn't need to have any actual code, because your app will never call this code. It's only used to document the *external API*. So, the function could just have `pass`.
* The *path* can contain an <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#key-expression" target="_blank">OpenAPI 3 expression</a> (see more below) where it can use variables with parameters and parts of the original request sent to *your API*.
### The callback path expression
The callback *path* can have an <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#key-expression" target="_blank">OpenAPI 3 expression</a> that can contain parts of the original request sent to *your API*.
In this case, it's the `str`:
```Python
"{$callback_url}/invoices/{$request.body.id}"
```
So, if your API user (the external developer) sends a request to *your API* to:
```
https://yourapi.com/invoices/?callback_url=https://www.external.org/events
```
with a JSON body of:
```JSON
{
"id": "2expen51ve",
"customer": "Mr. Richie Rich",
"total": "9999"
}
```
Then *your API* will process the invoice, and at some point later, send a callback request to the `callback_url` (the *external API*):
```
https://www.external.org/events/invoices/2expen51ve
```
with a JSON body containing something like:
```JSON
{
"description": "Payment celebration",
"paid": true
}
```
and it would expect a response from that *external API* with a JSON body like:
```JSON
{
"ok": true
}
```
!!! tip
Notice how the callback URL used contains the URL received as a query parameter in `callback_url` (`https://www.external.org/events`) and also the invoice `id` from inside of the JSON body (`2expen51ve`).
### Add the callback router
At this point you have the *callback path operation(s)* needed (the one(s) that the *external developer* should implement in the *external API*) in the callback router you created above.
Now use the parameter `callbacks` in *your API's path operation decorator* to pass the attribute `.routes` (that's actually just a `list` of routes/*path operations*) from that callback router:
```Python hl_lines="35"
{!./src/openapi_callbacks/tutorial001.py!}
```
!!! tip
Notice that you are not passing the router itself (`invoices_callback_router`) to `callback=`, but the attribute `.routes`, as in `invoices_callback_router.routes`.
### Check the docs
Now you can start your app with Uvicorn and go to <a href="http://127.0.0.1:8000/docs" target="_blank">http://127.0.0.1:8000/docs</a>.
You will see your docs including a "Callback" section for your *path operation* that shows how the *external API* should look like:
<img src="/img/tutorial/openapi-callbacks/image01.png">

View File

@@ -11,12 +11,30 @@ 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
@@ -28,5 +46,5 @@ Adding an `\f` (an escaped "form feed" character) causes **FastAPI** to truncate
It won't show up in the documentation, but other tools (such as Sphinx) will be able to use the rest.
```Python hl_lines="19 20 21 22 23 24 25 26 27 28 29"
{!./src/path_operation_advanced_configuration/tutorial003.py!}
{!./src/path_operation_advanced_configuration/tutorial004.py!}
```

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

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

View File

@@ -15,6 +15,7 @@ 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
@@ -159,11 +160,14 @@ class FastAPI(Starlette):
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] = 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,
@@ -181,7 +185,9 @@ 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 or self.default_response_class,
name=name,
@@ -205,11 +211,15 @@ class FastAPI(Starlette):
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] = 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,
@@ -228,7 +238,9 @@ 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 or self.default_response_class,
name=name,
@@ -286,11 +298,15 @@ class FastAPI(Starlette):
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] = None,
name: str = None,
callbacks: List[routing.APIRoute] = 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,
@@ -306,10 +322,13 @@ 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 or self.default_response_class,
name=name,
callbacks=callbacks,
)
def put(
@@ -329,11 +348,15 @@ class FastAPI(Starlette):
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] = None,
name: str = None,
callbacks: List[routing.APIRoute] = 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,
@@ -349,10 +372,13 @@ 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 or self.default_response_class,
name=name,
callbacks=callbacks,
)
def post(
@@ -372,11 +398,15 @@ class FastAPI(Starlette):
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] = None,
name: str = None,
callbacks: List[routing.APIRoute] = 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,
@@ -392,10 +422,13 @@ 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 or self.default_response_class,
name=name,
callbacks=callbacks,
)
def delete(
@@ -415,11 +448,15 @@ class FastAPI(Starlette):
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] = None,
name: str = None,
callbacks: List[routing.APIRoute] = 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,
@@ -435,10 +472,13 @@ 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 or self.default_response_class,
name=name,
callbacks=callbacks,
)
def options(
@@ -458,11 +498,15 @@ class FastAPI(Starlette):
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] = None,
name: str = None,
callbacks: List[routing.APIRoute] = 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,
@@ -478,10 +522,13 @@ 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 or self.default_response_class,
name=name,
callbacks=callbacks,
)
def head(
@@ -501,11 +548,15 @@ class FastAPI(Starlette):
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] = None,
name: str = None,
callbacks: List[routing.APIRoute] = 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,
@@ -521,10 +572,13 @@ 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 or self.default_response_class,
name=name,
callbacks=callbacks,
)
def patch(
@@ -544,11 +598,15 @@ class FastAPI(Starlette):
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] = None,
name: str = None,
callbacks: List[routing.APIRoute] = 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,
@@ -564,10 +622,13 @@ 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 or self.default_response_class,
name=name,
callbacks=callbacks,
)
def trace(
@@ -587,11 +648,15 @@ class FastAPI(Starlette):
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] = None,
name: str = None,
callbacks: List[routing.APIRoute] = 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,
@@ -607,8 +672,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 or self.default_response_class,
name=name,
callbacks=callbacks,
)

View File

@@ -1,6 +1,7 @@
from typing import Any, Callable
from starlette.concurrency import iterate_in_threadpool, run_in_threadpool # noqa
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,

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

@@ -27,13 +27,11 @@ 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 ForwardRef, evaluate_forwardref, lenient_issubclass
from pydantic.utils import lenient_issubclass
from starlette.background import BackgroundTasks
from starlette.concurrency import run_in_threadpool
from starlette.datastructures import FormData, Headers, QueryParams, UploadFile
@@ -41,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,
}
@@ -150,12 +183,13 @@ def get_flat_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:
@@ -164,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
):
@@ -239,7 +273,9 @@ 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 is_scalar_field(
field=param_field
@@ -250,7 +286,8 @@ def get_dependant(
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,
)
@@ -262,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
@@ -293,65 +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,
ignore_default: bool = False,
) -> Field:
) -> ModelField:
default_value = Required
had_schema = False
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
@@ -428,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:
@@ -476,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( # 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)
@@ -499,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 = {}
@@ -511,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):
@@ -536,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:
@@ -557,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)
):
@@ -598,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
@@ -631,30 +732,45 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[Field]:
BodyModel.__fields__[f.name] = get_schema_compatible_field(field=f)
required = any(True for f in flat_dependant.body_params if f.required)
BodySchema_kwargs: Dict[str, Any] = dict(default=None)
if any(isinstance(f.schema, params.File) for f in flat_dependant.body_params):
BodySchema: Type[params.Body] = params.File
elif any(isinstance(f.schema, params.Form) for f in flat_dependant.body_params):
BodySchema = params.Form
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:
BodySchema = params.Body
BodyFieldInfo = params.Body
body_param_media_types = [
getattr(f.schema, "media_type")
getattr(get_field_info(f), "media_type")
for f in flat_dependant.body_params
if isinstance(f.schema, params.Body)
if isinstance(get_field_info(f), params.Body)
]
if len(set(body_param_media_types)) == 1:
BodySchema_kwargs["media_type"] = body_param_media_types[0]
field = Field(
name="body",
type_=BodyModel,
default=None,
required=required,
model_config=BaseConfig,
class_validators={},
alias="body",
schema=BodySchema(**BodySchema_kwargs),
)
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

@@ -2,6 +2,8 @@ from enum import Enum
from types import GeneratorType
from typing import Any, Dict, List, Set, Union
from fastapi.logger import logger
from fastapi.utils import PYDANTIC_1
from pydantic import BaseModel
from pydantic.json import ENCODERS_BY_TYPE
@@ -14,24 +16,42 @@ def jsonable_encoder(
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,
@@ -55,7 +75,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,
@@ -63,7 +83,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,
@@ -79,7 +99,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,
@@ -107,7 +127,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,3 +1,4 @@
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException
from starlette.requests import Request
@@ -19,5 +20,6 @@ async def request_validation_exception_handler(
request: Request, exc: RequestValidationError
) -> JSONResponse:
return JSONResponse(
status_code=HTTP_422_UNPROCESSABLE_ENTITY, content={"detail": exc.errors()}
status_code=HTTP_422_UNPROCESSABLE_ENTITY,
content={"detail": jsonable_encoder(exc.errors())},
)

View File

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

3
fastapi/logger.py Normal file
View File

@@ -0,0 +1,3 @@
import logging
logger = logging.getLogger("fastapi")

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,21 +1,29 @@
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.logger 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
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(
logger.info(
"email-validator not installed, email fields will be treated as str.\n"
+ "To install, run: pip install email-validator"
"To install, run: pip install email-validator"
)
class EmailStr(str): # type: ignore
@@ -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):
@@ -227,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
@@ -255,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
@@ -266,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
@@ -311,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",
@@ -53,7 +64,7 @@ status_code_ranges: Dict[str, str] = {
}
def get_openapi_params(dependant: Dependant) -> List[Field]:
def get_openapi_params(dependant: Dependant) -> List[ModelField]:
flat_dependant = get_flat_dependant(dependant, skip_repeats=True)
return (
flat_dependant.path_params
@@ -79,37 +90,37 @@ def get_openapi_security_definitions(flat_dependant: Dependant) -> Tuple[Dict, L
def get_openapi_operation_parameters(
all_route_params: Sequence[Field]
all_route_params: Sequence[ModelField],
) -> List[Dict[str, Any]]:
parameters = []
for param in all_route_params:
schema = param.schema
schema = cast(Param, schema)
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 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:
@@ -151,10 +162,8 @@ def get_openapi_path(
security_schemes: Dict[str, Any] = {}
definitions: Dict[str, Any] = {}
assert route.methods is not None, "Methods must be a list"
assert (
route.response_class and route.response_class.media_type
), "A response class with media_type is needed to generate OpenAPI"
route_response_media_type: str = route.response_class.media_type
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)
@@ -178,6 +187,14 @@ def get_openapi_path(
)
if request_body_oai:
operation["requestBody"] = request_body_oai
if route.callbacks:
callbacks = {}
for callback in route.callbacks:
cb_path, cb_security_schemes, cb_definitions, = get_openapi_path(
route=callback, model_name_map=model_name_map
)
callbacks[callback.name] = {callback.path: cb_path}
operation["callbacks"] = callbacks
if route.responses:
for (additional_status_code, response) in route.responses.items():
assert isinstance(
@@ -189,7 +206,7 @@ def get_openapi_path(
field, model_name_map=model_name_map, ref_prefix=REF_PREFIX
)
response.setdefault("content", {}).setdefault(
route_response_media_type, {}
route_response_media_type or "application/json", {}
)["schema"] = response_schema
status_text: Optional[str] = status_code_ranges.get(
str(additional_status_code).upper()
@@ -202,24 +219,28 @@ def get_openapi_path(
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_media_type, {})[
"schema"
] = response_schema
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(

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

@@ -1,6 +1,5 @@
import asyncio
import inspect
import logging
from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Type, Union
from fastapi import params
@@ -13,10 +12,17 @@ from fastapi.dependencies.utils import (
)
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.logger import logger
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,20 +39,30 @@ 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: 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 skip_defaults and isinstance(response, BaseModel):
response = response.dict(skip_defaults=skip_defaults)
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_)
@@ -59,7 +75,7 @@ def serialize_response(
include=include,
exclude=exclude,
by_alias=by_alias,
skip_defaults=skip_defaults,
exclude_unset=exclude_unset,
)
else:
return jsonable_encoder(response)
@@ -67,19 +83,19 @@ def serialize_response(
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_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:
@@ -92,7 +108,7 @@ def get_request_handler(
if body_bytes:
body = await request.json()
except Exception as e:
logging.error(f"Error getting request body: {e}")
logger.error(f"Error getting request body: {e}")
raise HTTPException(
status_code=400, detail="There was an error parsing the body"
) from e
@@ -121,7 +137,7 @@ def get_request_handler(
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,
@@ -198,10 +214,11 @@ class APIRoute(routing.Route):
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: Optional[Type[Response]] = None,
dependency_overrides_provider: Any = None,
callbacks: Optional[List["APIRoute"]] = None,
) -> None:
self.path = path
self.endpoint = endpoint
@@ -215,16 +232,30 @@ class APIRoute(routing.Route):
)
self.response_model = response_model
if self.response_model:
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
@@ -232,9 +263,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
@@ -256,22 +287,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
@@ -279,7 +324,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
@@ -294,6 +339,7 @@ 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.callbacks = callbacks
self.app = request_response(self.get_route_handler())
def get_route_handler(self) -> Callable:
@@ -306,7 +352,7 @@ class APIRoute(routing.Route):
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,
response_model_exclude_unset=self.response_model_exclude_unset,
dependency_overrides_provider=self.dependency_overrides_provider,
)
@@ -319,12 +365,14 @@ class APIRouter(routing.Router):
default: ASGIApp = None,
dependency_overrides_provider: Any = None,
route_class: Type[APIRoute] = APIRoute,
default_response_class: Type[Response] = None,
) -> None:
super().__init__(
routes=routes, redirect_slashes=redirect_slashes, default=default
)
self.dependency_overrides_provider = dependency_overrides_provider
self.route_class = route_class
self.default_response_class = default_response_class
def add_api_route(
self,
@@ -345,12 +393,16 @@ class APIRouter(routing.Router):
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] = None,
name: str = None,
route_class_override: Optional[Type[APIRoute]] = None,
callbacks: List[APIRoute] = None,
) -> None:
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,
@@ -369,11 +421,14 @@ 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,
response_class=response_class or self.default_response_class,
name=name,
dependency_overrides_provider=self.dependency_overrides_provider,
callbacks=callbacks,
)
self.routes.append(route)
@@ -395,11 +450,16 @@ class APIRouter(routing.Router):
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] = None,
name: str = None,
callbacks: List[APIRoute] = 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,
@@ -418,10 +478,13 @@ 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,
response_class=response_class or self.default_response_class,
name=name,
callbacks=callbacks,
)
return func
@@ -486,7 +549,7 @@ 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 or default_response_class,
name=route.name,
@@ -526,11 +589,15 @@ class APIRouter(routing.Router):
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] = None,
name: str = None,
callbacks: List[APIRoute] = 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,
@@ -547,10 +614,13 @@ 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,
response_class=response_class or self.default_response_class,
name=name,
callbacks=callbacks,
)
def put(
@@ -570,11 +640,15 @@ class APIRouter(routing.Router):
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] = None,
name: str = None,
callbacks: List[APIRoute] = 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,
@@ -591,10 +665,13 @@ 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,
response_class=response_class or self.default_response_class,
name=name,
callbacks=callbacks,
)
def post(
@@ -614,11 +691,15 @@ class APIRouter(routing.Router):
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] = None,
name: str = None,
callbacks: List[APIRoute] = 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,
@@ -635,10 +716,13 @@ 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,
response_class=response_class or self.default_response_class,
name=name,
callbacks=callbacks,
)
def delete(
@@ -658,11 +742,15 @@ class APIRouter(routing.Router):
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] = None,
name: str = None,
callbacks: List[APIRoute] = 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,
@@ -679,10 +767,13 @@ 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,
response_class=response_class or self.default_response_class,
name=name,
callbacks=callbacks,
)
def options(
@@ -702,11 +793,15 @@ class APIRouter(routing.Router):
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] = None,
name: str = None,
callbacks: List[APIRoute] = 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,
@@ -723,10 +818,13 @@ 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,
response_class=response_class or self.default_response_class,
name=name,
callbacks=callbacks,
)
def head(
@@ -746,11 +844,15 @@ class APIRouter(routing.Router):
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] = None,
name: str = None,
callbacks: List[APIRoute] = 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,
@@ -767,10 +869,13 @@ 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,
response_class=response_class or self.default_response_class,
name=name,
callbacks=callbacks,
)
def patch(
@@ -790,11 +895,15 @@ class APIRouter(routing.Router):
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] = None,
name: str = None,
callbacks: List[APIRoute] = 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,
@@ -811,10 +920,13 @@ 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,
response_class=response_class or self.default_response_class,
name=name,
callbacks=callbacks,
)
def trace(
@@ -834,11 +946,15 @@ class APIRouter(routing.Router):
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] = None,
name: str = None,
callbacks: List[APIRoute] = 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,
@@ -855,8 +971,11 @@ 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,
response_class=response_class or self.default_response_class,
name=name,
callbacks=callbacks,
)

View File

@@ -3,31 +3,66 @@ from dataclasses import is_dataclass
from typing import Any, Dict, List, Sequence, Set, Type, cast
from fastapi import routing
from fastapi.logger import logger
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
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] = []
callback_flat_models: Set[Type[BaseModel]] = set()
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:
responses_from_routes.append(route.response_field)
if route.response_fields:
responses_from_routes.extend(route.response_fields.values())
flat_models = get_flat_models_from_fields(
if route.callbacks:
callback_flat_models |= get_flat_models_from_routes(route.callbacks)
flat_models = callback_flat_models | get_flat_models_from_fields(
body_fields_from_routes + responses_from_routes, known_models=set()
)
return flat_models
@@ -51,7 +86,7 @@ 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
@@ -59,28 +94,41 @@ def create_cloned_field(field: Field) -> Field:
if lenient_issubclass(original_type, BaseModel):
original_type = cast(Type[BaseModel], original_type)
use_type = create_model(
original_type.__name__,
__config__=original_type.__config__,
__validators__=original_type.__validators__, # type: ignore
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:
@@ -90,16 +138,24 @@ 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
def generate_operation_id_for_path(*, name: str, path: str, method: str) -> str:
operation_id = name + path
operation_id = operation_id.replace("{", "_").replace("}", "_").replace("/", "_")
operation_id = re.sub("[^0-9a-zA-Z_]", "_", operation_id)
operation_id = operation_id + "_" + method.lower()
return operation_id

View File

@@ -30,7 +30,7 @@ nav:
- Query Parameters and String Validations: 'tutorial/query-params-str-validations.md'
- Path Parameters and Numeric Validations: 'tutorial/path-params-numeric-validations.md'
- Body - Multiple Parameters: 'tutorial/body-multiple-params.md'
- Body - Schema: 'tutorial/body-schema.md'
- Body - Fields: 'tutorial/body-fields.md'
- Body - Nested Models: 'tutorial/body-nested-models.md'
- Extra data types: 'tutorial/extra-data-types.md'
- Cookie Parameters: 'tutorial/cookie-params.md'
@@ -88,6 +88,7 @@ nav:
- Testing Dependencies with Overrides: 'tutorial/testing-dependencies.md'
- Debugging: 'tutorial/debugging.md'
- Extending OpenAPI: 'tutorial/extending-openapi.md'
- OpenAPI Callbacks: 'tutorial/openapi-callbacks.md'
- Concurrency and async / await: 'async.md'
- Deployment: 'deployment.md'
- Project Generation - Template: 'project-generation.md'

View File

@@ -8,19 +8,32 @@ author = "Sebastián Ramírez"
author-email = "tiangolo@gmail.com"
home-page = "https://github.com/tiangolo/fastapi"
classifiers = [
"License :: OSI Approved :: MIT License",
'Intended Audience :: Information Technology',
'Intended Audience :: System Administrators',
'Operating System :: OS Independent',
'Programming Language :: Python :: 3',
'Programming Language :: Python',
'Topic :: Internet',
'Topic :: Software Development :: Libraries :: Application Frameworks',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: Software Development :: Libraries',
'Topic :: Software Development',
'Typing :: Typed',
"Development Status :: 4 - Beta",
"Environment :: Web Environment",
"Framework :: AsyncIO",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3 :: Only",
"Topic :: Internet :: WWW/HTTP :: HTTP Servers",
"Topic :: Internet :: WWW/HTTP",
]
requires = [
"starlette >=0.12.9,<=0.12.9",
"pydantic >=0.32.2,<=0.32.2"
"pydantic >=0.32.2,<2.0.0"
]
description-file = "README.md"
requires-python = ">=3.6"

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",
}
@@ -244,11 +244,11 @@ openapi_schema = {
},
},
"summary": "Get Path Param Required Id",
"operationId": "get_path_param_required_id_path_param-required__item_id__get",
"operationId": "get_path_param_required_id_path_param_required__item_id__get",
"parameters": [
{
"required": True,
"schema": {"title": "Item_Id", "type": "string"},
"schema": {"title": "Item Id", "type": "string"},
"name": "item_id",
"in": "path",
}
@@ -274,12 +274,12 @@ openapi_schema = {
},
},
"summary": "Get Path Param Min Length",
"operationId": "get_path_param_min_length_path_param-minlength__item_id__get",
"operationId": "get_path_param_min_length_path_param_minlength__item_id__get",
"parameters": [
{
"required": True,
"schema": {
"title": "Item_Id",
"title": "Item Id",
"minLength": 3,
"type": "string",
},
@@ -308,12 +308,12 @@ openapi_schema = {
},
},
"summary": "Get Path Param Max Length",
"operationId": "get_path_param_max_length_path_param-maxlength__item_id__get",
"operationId": "get_path_param_max_length_path_param_maxlength__item_id__get",
"parameters": [
{
"required": True,
"schema": {
"title": "Item_Id",
"title": "Item Id",
"maxLength": 3,
"type": "string",
},
@@ -342,12 +342,12 @@ openapi_schema = {
},
},
"summary": "Get Path Param Min Max Length",
"operationId": "get_path_param_min_max_length_path_param-min_maxlength__item_id__get",
"operationId": "get_path_param_min_max_length_path_param_min_maxlength__item_id__get",
"parameters": [
{
"required": True,
"schema": {
"title": "Item_Id",
"title": "Item Id",
"maxLength": 3,
"minLength": 2,
"type": "string",
@@ -377,12 +377,12 @@ openapi_schema = {
},
},
"summary": "Get Path Param Gt",
"operationId": "get_path_param_gt_path_param-gt__item_id__get",
"operationId": "get_path_param_gt_path_param_gt__item_id__get",
"parameters": [
{
"required": True,
"schema": {
"title": "Item_Id",
"title": "Item Id",
"exclusiveMinimum": 3.0,
"type": "number",
},
@@ -411,12 +411,12 @@ openapi_schema = {
},
},
"summary": "Get Path Param Gt0",
"operationId": "get_path_param_gt0_path_param-gt0__item_id__get",
"operationId": "get_path_param_gt0_path_param_gt0__item_id__get",
"parameters": [
{
"required": True,
"schema": {
"title": "Item_Id",
"title": "Item Id",
"exclusiveMinimum": 0.0,
"type": "number",
},
@@ -445,12 +445,12 @@ openapi_schema = {
},
},
"summary": "Get Path Param Ge",
"operationId": "get_path_param_ge_path_param-ge__item_id__get",
"operationId": "get_path_param_ge_path_param_ge__item_id__get",
"parameters": [
{
"required": True,
"schema": {
"title": "Item_Id",
"title": "Item Id",
"minimum": 3.0,
"type": "number",
},
@@ -479,12 +479,12 @@ openapi_schema = {
},
},
"summary": "Get Path Param Lt",
"operationId": "get_path_param_lt_path_param-lt__item_id__get",
"operationId": "get_path_param_lt_path_param_lt__item_id__get",
"parameters": [
{
"required": True,
"schema": {
"title": "Item_Id",
"title": "Item Id",
"exclusiveMaximum": 3.0,
"type": "number",
},
@@ -513,12 +513,12 @@ openapi_schema = {
},
},
"summary": "Get Path Param Lt0",
"operationId": "get_path_param_lt0_path_param-lt0__item_id__get",
"operationId": "get_path_param_lt0_path_param_lt0__item_id__get",
"parameters": [
{
"required": True,
"schema": {
"title": "Item_Id",
"title": "Item Id",
"exclusiveMaximum": 0.0,
"type": "number",
},
@@ -547,12 +547,12 @@ openapi_schema = {
},
},
"summary": "Get Path Param Le",
"operationId": "get_path_param_le_path_param-le__item_id__get",
"operationId": "get_path_param_le_path_param_le__item_id__get",
"parameters": [
{
"required": True,
"schema": {
"title": "Item_Id",
"title": "Item Id",
"maximum": 3.0,
"type": "number",
},
@@ -581,12 +581,12 @@ openapi_schema = {
},
},
"summary": "Get Path Param Lt Gt",
"operationId": "get_path_param_lt_gt_path_param-lt-gt__item_id__get",
"operationId": "get_path_param_lt_gt_path_param_lt_gt__item_id__get",
"parameters": [
{
"required": True,
"schema": {
"title": "Item_Id",
"title": "Item Id",
"exclusiveMaximum": 3.0,
"exclusiveMinimum": 1.0,
"type": "number",
@@ -616,12 +616,12 @@ openapi_schema = {
},
},
"summary": "Get Path Param Le Ge",
"operationId": "get_path_param_le_ge_path_param-le-ge__item_id__get",
"operationId": "get_path_param_le_ge_path_param_le_ge__item_id__get",
"parameters": [
{
"required": True,
"schema": {
"title": "Item_Id",
"title": "Item Id",
"maximum": 3.0,
"minimum": 1.0,
"type": "number",
@@ -651,12 +651,12 @@ openapi_schema = {
},
},
"summary": "Get Path Param Lt Int",
"operationId": "get_path_param_lt_int_path_param-lt-int__item_id__get",
"operationId": "get_path_param_lt_int_path_param_lt_int__item_id__get",
"parameters": [
{
"required": True,
"schema": {
"title": "Item_Id",
"title": "Item Id",
"exclusiveMaximum": 3.0,
"type": "integer",
},
@@ -685,12 +685,12 @@ openapi_schema = {
},
},
"summary": "Get Path Param Gt Int",
"operationId": "get_path_param_gt_int_path_param-gt-int__item_id__get",
"operationId": "get_path_param_gt_int_path_param_gt_int__item_id__get",
"parameters": [
{
"required": True,
"schema": {
"title": "Item_Id",
"title": "Item Id",
"exclusiveMinimum": 3.0,
"type": "integer",
},
@@ -719,12 +719,12 @@ openapi_schema = {
},
},
"summary": "Get Path Param Le Int",
"operationId": "get_path_param_le_int_path_param-le-int__item_id__get",
"operationId": "get_path_param_le_int_path_param_le_int__item_id__get",
"parameters": [
{
"required": True,
"schema": {
"title": "Item_Id",
"title": "Item Id",
"maximum": 3.0,
"type": "integer",
},
@@ -753,12 +753,12 @@ openapi_schema = {
},
},
"summary": "Get Path Param Ge Int",
"operationId": "get_path_param_ge_int_path_param-ge-int__item_id__get",
"operationId": "get_path_param_ge_int_path_param_ge_int__item_id__get",
"parameters": [
{
"required": True,
"schema": {
"title": "Item_Id",
"title": "Item Id",
"minimum": 3.0,
"type": "integer",
},
@@ -787,12 +787,12 @@ openapi_schema = {
},
},
"summary": "Get Path Param Lt Gt Int",
"operationId": "get_path_param_lt_gt_int_path_param-lt-gt-int__item_id__get",
"operationId": "get_path_param_lt_gt_int_path_param_lt_gt_int__item_id__get",
"parameters": [
{
"required": True,
"schema": {
"title": "Item_Id",
"title": "Item Id",
"exclusiveMaximum": 3.0,
"exclusiveMinimum": 1.0,
"type": "integer",
@@ -822,12 +822,12 @@ openapi_schema = {
},
},
"summary": "Get Path Param Le Ge Int",
"operationId": "get_path_param_le_ge_int_path_param-le-ge-int__item_id__get",
"operationId": "get_path_param_le_ge_int_path_param_le_ge_int__item_id__get",
"parameters": [
{
"required": True,
"schema": {
"title": "Item_Id",
"title": "Item Id",
"maximum": 3.0,
"minimum": 1.0,
"type": "integer",
@@ -1037,7 +1037,7 @@ openapi_schema = {
},
},
"summary": "Get Query Param Required",
"operationId": "get_query_param_required_query_param-required_get",
"operationId": "get_query_param_required_query_param_required_get",
"parameters": [
{
"required": True,
@@ -1067,7 +1067,7 @@ openapi_schema = {
},
},
"summary": "Get Query Param Required Type",
"operationId": "get_query_param_required_type_query_param-required_int_get",
"operationId": "get_query_param_required_type_query_param_required_int_get",
"parameters": [
{
"required": True,

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

@@ -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",
}
@@ -259,11 +259,11 @@ openapi_schema = {
},
},
"summary": "Get Not Decorated",
"operationId": "get_not_decorated_items-not-decorated__item_id__get",
"operationId": "get_not_decorated_items_not_decorated__item_id__get",
"parameters": [
{
"required": True,
"schema": {"title": "Item_Id", "type": "string"},
"schema": {"title": "Item Id", "type": "string"},
"name": "item_id",
"in": "path",
}

View File

@@ -110,7 +110,7 @@ def test_schema_1():
d = {
"required": True,
"schema": {"title": "User_Id", "type": "string"},
"schema": {"title": "User Id", "type": "string"},
"name": "user_id",
"in": "path",
}
@@ -127,7 +127,7 @@ def test_schema_2():
d = {
"required": False,
"schema": {"title": "User_Id", "type": "string"},
"schema": {"title": "User Id", "type": "string"},
"name": "user_id",
"in": "query",
}

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

@@ -1,7 +1,8 @@
from decimal import Decimal
from typing import List
from fastapi import FastAPI
from pydantic import BaseModel
from pydantic import BaseModel, condecimal
from starlette.testclient import TestClient
app = FastAPI()
@@ -9,7 +10,7 @@ app = FastAPI()
class Item(BaseModel):
name: str
age: int
age: condecimal(gt=Decimal(0.0))
@app.post("/items/")
@@ -67,7 +68,7 @@ openapi_schema = {
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
"age": {"title": "Age", "type": "integer"},
"age": {"title": "Age", "exclusiveMinimum": 0.0, "type": "number"},
},
},
"ValidationError": {
@@ -99,6 +100,17 @@ openapi_schema = {
},
}
single_error = {
"detail": [
{
"ctx": {"limit_value": 0.0},
"loc": ["body", "item", 0, "age"],
"msg": "ensure this value is greater than 0",
"type": "value_error.number.not_gt",
}
]
}
multiple_errors = {
"detail": [
{
@@ -108,8 +120,8 @@ multiple_errors = {
},
{
"loc": ["body", "item", 0, "age"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
"msg": "value is not a valid decimal",
"type": "type_error.decimal",
},
{
"loc": ["body", "item", 1, "name"],
@@ -118,8 +130,8 @@ multiple_errors = {
},
{
"loc": ["body", "item", 1, "age"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
"msg": "value is not a valid decimal",
"type": "type_error.decimal",
},
]
}
@@ -137,7 +149,13 @@ def test_put_correct_body():
assert response.json() == {"item": [{"name": "Foo", "age": 5}]}
def test_put_incorrect_body():
def test_jsonable_encoder_requiring_error():
response = client.post("/items/", json=[{"name": "Foo", "age": -1.0}])
assert response.status_code == 422
assert response.json() == single_error
def test_put_incorrect_body_multiple():
response = client.post("/items/", json=[{"age": "five"}, {"age": "six"}])
assert response.status_code == 422
assert response.json() == multiple_errors

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

@@ -99,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

@@ -20,7 +20,7 @@ class ModelSubclass(Model):
y: int
@app.get("/", response_model=Model, response_model_skip_defaults=True)
@app.get("/", response_model=Model, response_model_exclude_unset=True)
def get() -> ModelSubclass:
return ModelSubclass(sub={}, y=1)

View File

@@ -54,7 +54,7 @@ openapi_schema = {
"parameters": [
{
"required": True,
"schema": {"title": "Item_Id", "type": "string"},
"schema": {"title": "Item Id", "type": "string"},
"name": "item_id",
"in": "path",
}
@@ -80,11 +80,11 @@ openapi_schema = {
},
},
"summary": "Create Item",
"operationId": "create_item_starlette-items__item_id__get",
"operationId": "create_item_starlette_items__item_id__get",
"parameters": [
{
"required": True,
"schema": {"title": "Item_Id", "type": "string"},
"schema": {"title": "Item Id", "type": "string"},
"name": "item_id",
"in": "path",
}

View File

@@ -43,7 +43,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

@@ -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

@@ -44,7 +44,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

@@ -42,7 +42,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

@@ -14,7 +14,7 @@ openapi_schema = {
"content": {
"application/json": {
"schema": {
"title": "Response_Read_Notes_Notes__Get",
"title": "Response Read Notes Notes Get",
"type": "array",
"items": {"$ref": "#/components/schemas/Note"},
}

View File

@@ -123,7 +123,7 @@ openapi_schema = {
"parameters": [
{
"required": True,
"schema": {"title": "Item_Id", "type": "string"},
"schema": {"title": "Item Id", "type": "string"},
"name": "item_id",
"in": "path",
},
@@ -160,7 +160,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

@@ -1,7 +1,16 @@
import pytest
from starlette.testclient import TestClient
from body_schema.tutorial001 import app
from body_fields.tutorial001 import app
# TODO: remove when removing support for Pydantic < 1.0.0
try:
from pydantic import Field # noqa
except ImportError: # pragma: nocover
import pydantic
pydantic.Field = pydantic.Schema
client = TestClient(app)
@@ -33,7 +42,7 @@ openapi_schema = {
"parameters": [
{
"required": True,
"schema": {"title": "Item_Id", "type": "integer"},
"schema": {"title": "Item Id", "type": "integer"},
"name": "item_id",
"in": "path",
}

View File

@@ -32,7 +32,7 @@ openapi_schema = {
"parameters": [
{
"required": True,
"schema": {"title": "Item_Id", "type": "integer"},
"schema": {"title": "Item Id", "type": "integer"},
"name": "item_id",
"in": "path",
}
@@ -69,7 +69,7 @@ openapi_schema = {
"type": "object",
"properties": {
"username": {"title": "Username", "type": "string"},
"full_name": {"title": "Full_Name", "type": "string"},
"full_name": {"title": "Full Name", "type": "string"},
},
},
"Body_update_item_items__item_id__put": {

View File

@@ -27,7 +27,7 @@ openapi_schema = {
},
},
"summary": "Create Index Weights",
"operationId": "create_index_weights_index-weights__post",
"operationId": "create_index_weights_index_weights__post",
"requestBody": {
"content": {
"application/json": {

View File

@@ -35,7 +35,7 @@ openapi_schema = {
"parameters": [
{
"required": True,
"schema": {"title": "Item_Id", "type": "string"},
"schema": {"title": "Item Id", "type": "string"},
"name": "item_id",
"in": "path",
}
@@ -67,7 +67,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

@@ -32,7 +32,7 @@ openapi_schema = {
"parameters": [
{
"required": False,
"schema": {"title": "Ads_Id", "type": "string"},
"schema": {"title": "Ads Id", "type": "string"},
"name": "ads_id",
"in": "cookie",
}

View File

@@ -29,7 +29,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,42 @@
import os
from pathlib import Path
import pytest
from starlette.testclient import TestClient
@pytest.fixture(scope="module")
def client():
static_dir: Path = Path(os.getcwd()) / "static"
print(static_dir)
static_dir.mkdir(exist_ok=True)
from extending_openapi.tutorial002 import app
with TestClient(app) as client:
yield client
static_dir.rmdir()
def test_swagger_ui_html(client: TestClient):
response = client.get("/docs")
assert response.status_code == 200
assert "/static/swagger-ui-bundle.js" in response.text
assert "/static/swagger-ui.css" in response.text
def test_swagger_ui_oauth2_redirect_html(client: TestClient):
response = client.get("/docs/oauth2-redirect")
assert response.status_code == 200
assert "window.opener.swaggerUIRedirectOauth2" in response.text
def test_redoc_html(client: TestClient):
response = client.get("/redoc")
assert response.status_code == 200
assert "/static/redoc.standalone.js" in response.text
def test_api(client: TestClient):
response = client.get("/users/john")
assert response.status_code == 200
assert response.json()["message"] == "Hello john"

View File

@@ -33,7 +33,7 @@ openapi_schema = {
{
"required": True,
"schema": {
"title": "Item_Id",
"title": "Item Id",
"type": "string",
"format": "uuid",
},
@@ -60,22 +60,22 @@ openapi_schema = {
"type": "object",
"properties": {
"start_datetime": {
"title": "Start_Datetime",
"title": "Start Datetime",
"type": "string",
"format": "date-time",
},
"end_datetime": {
"title": "End_Datetime",
"title": "End Datetime",
"type": "string",
"format": "date-time",
},
"repeat_at": {
"title": "Repeat_At",
"title": "Repeat At",
"type": "string",
"format": "time",
},
"process_after": {
"title": "Process_After",
"title": "Process After",
"type": "number",
"format": "time-delta",
},

View File

@@ -16,7 +16,7 @@ openapi_schema = {
"content": {
"application/json": {
"schema": {
"title": "Response_Read_Item_Items__Item_Id__Get",
"title": "Response Read Item Items Item Id Get",
"anyOf": [
{"$ref": "#/components/schemas/PlaneItem"},
{"$ref": "#/components/schemas/CarItem"},
@@ -41,7 +41,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

@@ -16,7 +16,7 @@ openapi_schema = {
"content": {
"application/json": {
"schema": {
"title": "Response_Read_Items_Items__Get",
"title": "Response Read Items Items Get",
"type": "array",
"items": {"$ref": "#/components/schemas/Item"},
}

View File

@@ -16,7 +16,7 @@ openapi_schema = {
"content": {
"application/json": {
"schema": {
"title": "Response_Read_Keyword_Weights_Keyword-Weights__Get",
"title": "Response Read Keyword Weights Keyword Weights Get",
"type": "object",
"additionalProperties": {"type": "number"},
}
@@ -25,7 +25,7 @@ openapi_schema = {
}
},
"summary": "Read Keyword Weights",
"operationId": "read_keyword_weights_keyword-weights__get",
"operationId": "read_keyword_weights_keyword_weights__get",
}
}
},

View File

@@ -31,7 +31,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

@@ -27,11 +27,11 @@ openapi_schema = {
},
},
"summary": "Read Item Header",
"operationId": "read_item_header_items-header__item_id__get",
"operationId": "read_item_header_items_header__item_id__get",
"parameters": [
{
"required": True,
"schema": {"title": "Item_Id", "type": "string"},
"schema": {"title": "Item Id", "type": "string"},
"name": "item_id",
"in": "path",
}

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