mirror of
https://github.com/fastapi/fastapi.git
synced 2025-12-24 14:48:35 -05:00
Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9673f145a | ||
|
|
aa28784f6b | ||
|
|
7b3319ddab | ||
|
|
6a20078259 | ||
|
|
d0ab909544 | ||
|
|
13da029dca | ||
|
|
91fe90e8e6 | ||
|
|
a0c8f93231 | ||
|
|
cad6a6e0c1 | ||
|
|
fd5ba77b83 | ||
|
|
cb1410426e | ||
|
|
7b31e52766 | ||
|
|
4151616681 | ||
|
|
462e24e864 | ||
|
|
9f9ed7a6bd | ||
|
|
b6ea9ea2ca | ||
|
|
b85b2e3942 | ||
|
|
08fc2a41ca | ||
|
|
8d3dcbcd1b | ||
|
|
861ed37c97 | ||
|
|
7a445402d4 | ||
|
|
04c8502cc7 | ||
|
|
c7c69586ae | ||
|
|
4e09feda9e | ||
|
|
73260971b5 | ||
|
|
b36bfff56e | ||
|
|
83d04df8a6 | ||
|
|
7bc78c5fd3 | ||
|
|
ae8fa3aacd | ||
|
|
08bc120771 | ||
|
|
a39efb029f | ||
|
|
58ca98285f | ||
|
|
3f5f81bbdc | ||
|
|
90236c8135 | ||
|
|
c200bc2240 | ||
|
|
e9861cd918 | ||
|
|
202fa11d50 | ||
|
|
4b6e09296c | ||
|
|
9bd0d6fa96 | ||
|
|
35510a5ea7 | ||
|
|
c1788a25c7 | ||
|
|
19c77e35bd | ||
|
|
cc4c13e4ae | ||
|
|
4f3764faa9 | ||
|
|
c27ad0dc26 | ||
|
|
6d0caf7522 | ||
|
|
06df32e84c | ||
|
|
28c089c029 | ||
|
|
44b26bb64c | ||
|
|
e04bae2286 | ||
|
|
23f5940e8b | ||
|
|
4915cf0561 | ||
|
|
a1c9eff041 | ||
|
|
57cb3f3089 | ||
|
|
bd6b3b07c5 | ||
|
|
3cf8b86dc1 | ||
|
|
55165f292a | ||
|
|
f3ddc7bdeb | ||
|
|
4356cc9588 | ||
|
|
cd9e87e60e | ||
|
|
4834d87dcd | ||
|
|
7781cc0936 | ||
|
|
23459d4a35 | ||
|
|
ab2b86fe2c |
56
.github/ISSUE_TEMPLATE/bug_report.md
vendored
56
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -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.
|
||||
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -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.
|
||||
|
||||
11
.github/ISSUE_TEMPLATE/question.md
vendored
11
.github/ISSUE_TEMPLATE/question.md
vendored
@@ -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
19
.github/workflows/main.yml
vendored
Normal 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."
|
||||
}
|
||||
}
|
||||
13
.travis.yml
13
.travis.yml
@@ -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:
|
||||
@@ -26,8 +25,8 @@ after_script:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
|
||||
deploy:
|
||||
provider: script
|
||||
script: bash scripts/deploy.sh
|
||||
on:
|
||||
tags: true
|
||||
python: "3.6"
|
||||
provider: script
|
||||
script: bash scripts/deploy.sh
|
||||
on:
|
||||
tags: true
|
||||
python: "3.6"
|
||||
|
||||
2
Pipfile
2
Pipfile
@@ -26,7 +26,7 @@ uvicorn = "*"
|
||||
|
||||
[packages]
|
||||
starlette = "==0.12.9"
|
||||
pydantic = "==0.32.2"
|
||||
pydantic = "==1.0.0"
|
||||
databases = {extras = ["sqlite"],version = "*"}
|
||||
hypercorn = "*"
|
||||
orjson = "*"
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
<em>FastAPI framework, high performance, easy to learn, fast to code, ready for production</em>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://travis-ci.org/tiangolo/fastapi" target="_blank">
|
||||
<img src="https://travis-ci.org/tiangolo/fastapi.svg?branch=master" alt="Build Status">
|
||||
<a href="https://travis-ci.com/tiangolo/fastapi" target="_blank">
|
||||
<img src="https://travis-ci.com/tiangolo/fastapi.svg?branch=master" alt="Build Status">
|
||||
</a>
|
||||
<a href="https://codecov.io/gh/tiangolo/fastapi" target="_blank">
|
||||
<img src="https://codecov.io/gh/tiangolo/fastapi/branch/master/graph/badge.svg" alt="Coverage">
|
||||
@@ -206,8 +206,7 @@ Now modify the file `main.py` to receive a body from a `PUT` request.
|
||||
|
||||
Declare the body using standard Python types, thanks to Pydantic.
|
||||
|
||||
|
||||
```Python hl_lines="2 7 8 9 10 24"
|
||||
```Python hl_lines="2 7 8 9 10 23 24 25"
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
|
||||
@@ -407,7 +406,7 @@ Used by FastAPI / Starlette:
|
||||
|
||||
* <a href="http://www.uvicorn.org" target="_blank"><code>uvicorn</code></a> - for the server that loads and serves your application.
|
||||
|
||||
You can install all of these with `pip3 install fastapi[all]`.
|
||||
You can install all of these with `pip install fastapi[all]`.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">FastAPI|DB接続してCRUDするPython製APIサーバーを構築</a> by <a href="https://qiita.com/mtitg" target="_blank">@mtitg</a>.
|
||||
@@ -49,10 +51,20 @@ Here's an incomplete list of some of them.
|
||||
|
||||
* <a href="https://qiita.com/hikarut/items/b178af2e2440c67c6ac4" target="_blank">フロントエンド開発者向けのDockerによるPython開発環境構築</a> by <a href="https://qiita.com/hikarut" target="_blank">Hikaru Takahashi</a>.
|
||||
|
||||
* <a href="https://rightcode.co.jp/blog/information-technology/fastapi-tutorial-todo-apps-environment" target="_blank">【第1回】FastAPIチュートリアル: ToDoアプリを作ってみよう【環境構築編】</a> by <a href="https://rightcode.co.jp/author/jun" target="_blank">ライトコードメディア編集部</a>
|
||||
|
||||
* <a href="https://rightcode.co.jp/blog/information-technology/fastapi-tutorial-todo-apps-model-building" target="_blank">【第2回】FastAPIチュートリアル: ToDoアプリを作ってみよう【モデル構築編】</a> by <a href="https://rightcode.co.jp/author/jun" target="_blank">ライトコードメディア編集部</a>
|
||||
|
||||
* <a href="https://rightcode.co.jp/blog/information-technology/fastapi-tutorial-todo-apps-authentication-user-registration" target="_blank">【第3回】FastAPIチュートリアル: toDoアプリを作ってみよう【認証・ユーザ登録編】</a> by <a href="https://rightcode.co.jp/author/jun" target="_blank">ライトコードメディア編集部</a>
|
||||
|
||||
* <a href="https://rightcode.co.jp/blog/information-technology/fastapi-tutorial-todo-apps-admin-page-improvement" target="_blank">【第4回】FastAPIチュートリアル: toDoアプリを作ってみよう【管理者ページ改良編】</a> by <a href="https://rightcode.co.jp/author/jun" target="_blank">ライトコードメディア編集部</a>
|
||||
|
||||
### Chinese
|
||||
|
||||
* <a href="https://cloud.tencent.com/developer/article/1431448" target="_blank">使用FastAPI框架快速构建高性能的api服务</a> by <a href="https://cloud.tencent.com/developer/user/5471722" target="_blank">逍遥散人</a>.
|
||||
|
||||
* <a href="https://wxq0309.github.io/" target="_blank">FastAPI框架中文文档</a> by <a href="https://wxq0309.github.io/" target="_blank">何大仙</a>.
|
||||
|
||||
### Vietnamese
|
||||
|
||||
* <a href="https://fullstackstation.com/fastapi-trien-khai-bang-docker/" target="_blank">FASTAPI: TRIỂN KHAI BẰNG DOCKER</a> by <a href="https://fullstackstation.com/author/figonking/" target="_blank">Nguyễn Nhân</a>.
|
||||
@@ -61,6 +73,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>.
|
||||
|
||||
@@ -56,7 +56,7 @@ You can:
|
||||
|
||||
## Tweet about **FastAPI**
|
||||
|
||||
<a href="http://twitter.com/home/?status=I'm loving FastAPI because... https://github.com/tiangolo/fastapi cc @tiangolo" target="_blank">Tweet about **FastAPI**</a> and let me and others why you like it.
|
||||
<a href="https://twitter.com/compose/tweet?text=I'm loving FastAPI because... https://github.com/tiangolo/fastapi cc @tiangolo" target="_blank">Tweet about **FastAPI**</a> and let me and others know why you like it.
|
||||
|
||||
## Let me know how are you using **FastAPI**
|
||||
|
||||
@@ -64,7 +64,7 @@ I love to hear about how **FastAPI** is being used, what have you liked in it, i
|
||||
|
||||
You can let me know:
|
||||
|
||||
* <a href="http://twitter.com/home/?status=Hey @tiangolo, I'm using FastAPI at..." target="_blank">On **Twitter**</a>.
|
||||
* <a href="https://twitter.com/compose/tweet?text=Hey @tiangolo, I'm using FastAPI at..." target="_blank">On **Twitter**</a>.
|
||||
* <a href="https://www.linkedin.com/in/tiangolo/" target="_blank">On **Linkedin**</a>.
|
||||
* <a href="https://medium.com/@tiangolo" target="_blank">On **Medium**</a>.
|
||||
|
||||
|
||||
BIN
docs/img/github-social-preview.png
Normal file
BIN
docs/img/github-social-preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
98
docs/img/github-social-preview.svg
Normal file
98
docs/img/github-social-preview.svg
Normal 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 |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
BIN
docs/img/tutorial/openapi-callbacks/image01.png
Normal file
BIN
docs/img/tutorial/openapi-callbacks/image01.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
@@ -5,8 +5,8 @@
|
||||
<em>FastAPI framework, high performance, easy to learn, fast to code, ready for production</em>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://travis-ci.org/tiangolo/fastapi" target="_blank">
|
||||
<img src="https://travis-ci.org/tiangolo/fastapi.svg?branch=master" alt="Build Status">
|
||||
<a href="https://travis-ci.com/tiangolo/fastapi" target="_blank">
|
||||
<img src="https://travis-ci.com/tiangolo/fastapi.svg?branch=master" alt="Build Status">
|
||||
</a>
|
||||
<a href="https://codecov.io/gh/tiangolo/fastapi" target="_blank">
|
||||
<img src="https://codecov.io/gh/tiangolo/fastapi/branch/master/graph/badge.svg" alt="Coverage">
|
||||
@@ -206,8 +206,7 @@ Now modify the file `main.py` to receive a body from a `PUT` request.
|
||||
|
||||
Declare the body using standard Python types, thanks to Pydantic.
|
||||
|
||||
|
||||
```Python hl_lines="2 7 8 9 10 24"
|
||||
```Python hl_lines="2 7 8 9 10 23 24 25"
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
|
||||
@@ -407,7 +406,7 @@ Used by FastAPI / Starlette:
|
||||
|
||||
* <a href="http://www.uvicorn.org" target="_blank"><code>uvicorn</code></a> - for the server that loads and serves your application.
|
||||
|
||||
You can install all of these with `pip3 install fastapi[all]`.
|
||||
You can install all of these with `pip install fastapi[all]`.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -1,5 +1,52 @@
|
||||
## Latest changes
|
||||
|
||||
## 0.46.0
|
||||
|
||||
* Fix typos and tweak configs. PR [#837](https://github.com/tiangolo/fastapi/pull/837).
|
||||
* Add link to Chinese article in [External Links](https://fastapi.tiangolo.com/external-links/). PR [810](https://github.com/tiangolo/fastapi/pull/810) by [@wxq0309](https://github.com/wxq0309).
|
||||
* Implement `OAuth2AuthorizationCodeBearer` class. PR [#797](https://github.com/tiangolo/fastapi/pull/797) by [@kuwv](https://github.com/kuwv).
|
||||
* Update example upgrade in docs main page. PR [#795](https://github.com/tiangolo/fastapi/pull/795) by [@cdeil](https://github.com/cdeil).
|
||||
* Fix callback handling for sub-routers. PR [#792](https://github.com/tiangolo/fastapi/pull/792) by [@jekirl](https://github.com/jekirl).
|
||||
* Fix typos. PR [#784](https://github.com/tiangolo/fastapi/pull/784) by [@kkinder](https://github.com/kkinder).
|
||||
* Add 4 Japanese articles to [External Links](https://fastapi.tiangolo.com/external-links/). PR [#783](https://github.com/tiangolo/fastapi/pull/783) by [@HymanZHAN](https://github.com/HymanZHAN).
|
||||
* Add support for subtypes of main types in `jsonable_encoder`, e.g. asyncpg's UUIDs. PR [#756](https://github.com/tiangolo/fastapi/pull/756) by [@RmStorm](https://github.com/RmStorm).
|
||||
* Fix usage of Pydantic's `HttpUrl` in docs. PR [#832](https://github.com/tiangolo/fastapi/pull/832) by [@Dustyposa](https://github.com/Dustyposa).
|
||||
* Fix Twitter links in docs. PR [#813](https://github.com/tiangolo/fastapi/pull/813) by [@justindujardin](https://github.com/justindujardin).
|
||||
* Add docs for correctly [using FastAPI with Peewee ORM](https://fastapi.tiangolo.com/tutorial/sql-databases-peewee/). Including how to overwrite parts of Peewee to correctly handle async threads. PR [#789](https://github.com/tiangolo/fastapi/pull/789).
|
||||
|
||||
## 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).
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
from typing import Set
|
||||
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel, UrlStr
|
||||
from pydantic import BaseModel, HttpUrl
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class Image(BaseModel):
|
||||
url: UrlStr
|
||||
url: HttpUrl
|
||||
name: str
|
||||
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
from typing import List, Set
|
||||
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel, UrlStr
|
||||
from pydantic import BaseModel, HttpUrl
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class Image(BaseModel):
|
||||
url: UrlStr
|
||||
url: HttpUrl
|
||||
name: str
|
||||
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
from typing import List, Set
|
||||
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel, UrlStr
|
||||
from pydantic import BaseModel, HttpUrl
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class Image(BaseModel):
|
||||
url: UrlStr
|
||||
url: HttpUrl
|
||||
name: str
|
||||
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
from typing import List
|
||||
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel, UrlStr
|
||||
from pydantic import BaseModel, HttpUrl
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class Image(BaseModel):
|
||||
url: UrlStr
|
||||
url: HttpUrl
|
||||
name: str
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
from pydantic.types import EmailStr
|
||||
from pydantic import BaseModel, EmailStr
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
from pydantic.types import EmailStr
|
||||
from pydantic import BaseModel, EmailStr
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
52
docs/src/openapi_callbacks/tutorial001.py
Normal file
52
docs/src/openapi_callbacks/tutorial001.py
Normal file
@@ -0,0 +1,52 @@
|
||||
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"}
|
||||
@@ -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>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
from pydantic.types import EmailStr
|
||||
from pydantic import BaseModel, EmailStr
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
from pydantic.types import EmailStr
|
||||
from pydantic import BaseModel, EmailStr
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
0
docs/src/sql_databases_peewee/sql_app/__init__.py
Normal file
0
docs/src/sql_databases_peewee/sql_app/__init__.py
Normal file
30
docs/src/sql_databases_peewee/sql_app/crud.py
Normal file
30
docs/src/sql_databases_peewee/sql_app/crud.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from . import models, schemas
|
||||
|
||||
|
||||
def get_user(user_id: int):
|
||||
return models.User.filter(models.User.id == user_id).first()
|
||||
|
||||
|
||||
def get_user_by_email(email: str):
|
||||
return models.User.filter(models.User.email == email).first()
|
||||
|
||||
|
||||
def get_users(skip: int = 0, limit: int = 100):
|
||||
return list(models.User.select().offset(skip).limit(limit))
|
||||
|
||||
|
||||
def create_user(user: schemas.UserCreate):
|
||||
fake_hashed_password = user.password + "notreallyhashed"
|
||||
db_user = models.User(email=user.email, hashed_password=fake_hashed_password)
|
||||
db_user.save()
|
||||
return db_user
|
||||
|
||||
|
||||
def get_items(skip: int = 0, limit: int = 100):
|
||||
return list(models.Item.select().offset(skip).limit(limit))
|
||||
|
||||
|
||||
def create_user_item(item: schemas.ItemCreate, user_id: int):
|
||||
db_item = models.Item(**item.dict(), owner_id=user_id)
|
||||
db_item.save()
|
||||
return db_item
|
||||
26
docs/src/sql_databases_peewee/sql_app/database.py
Normal file
26
docs/src/sql_databases_peewee/sql_app/database.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from contextvars import ContextVar
|
||||
|
||||
import peewee
|
||||
|
||||
DATABASE_NAME = "test.db"
|
||||
|
||||
|
||||
class PeeweeConnectionState(peewee._ConnectionState):
|
||||
def __init__(self, **kwargs):
|
||||
super().__setattr__("_state", {})
|
||||
self._state["closed"] = ContextVar("closed", default=True)
|
||||
self._state["conn"] = ContextVar("conn", default=None)
|
||||
self._state["ctx"] = ContextVar("ctx", default=[])
|
||||
self._state["transactions"] = ContextVar("transactions", default=[])
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
self._state[name].set(value)
|
||||
|
||||
def __getattr__(self, name):
|
||||
return self._state[name].get()
|
||||
|
||||
|
||||
db = peewee.SqliteDatabase(DATABASE_NAME, check_same_thread=False)
|
||||
|
||||
db._state = PeeweeConnectionState()
|
||||
70
docs/src/sql_databases_peewee/sql_app/main.py
Normal file
70
docs/src/sql_databases_peewee/sql_app/main.py
Normal file
@@ -0,0 +1,70 @@
|
||||
import time
|
||||
from typing import List
|
||||
|
||||
from fastapi import Depends, FastAPI, HTTPException
|
||||
|
||||
from . import crud, database, models, schemas
|
||||
|
||||
database.db.connect()
|
||||
database.db.create_tables([models.User, models.Item])
|
||||
database.db.close()
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
# Dependency
|
||||
def get_db():
|
||||
try:
|
||||
database.db.connect()
|
||||
yield
|
||||
finally:
|
||||
if not database.db.is_closed():
|
||||
database.db.close()
|
||||
|
||||
|
||||
@app.post("/users/", response_model=schemas.User, dependencies=[Depends(get_db)])
|
||||
def create_user(user: schemas.UserCreate):
|
||||
db_user = crud.get_user_by_email(email=user.email)
|
||||
if db_user:
|
||||
raise HTTPException(status_code=400, detail="Email already registered")
|
||||
return crud.create_user(user=user)
|
||||
|
||||
|
||||
@app.get("/users/", response_model=List[schemas.User], dependencies=[Depends(get_db)])
|
||||
def read_users(skip: int = 0, limit: int = 100):
|
||||
users = crud.get_users(skip=skip, limit=limit)
|
||||
return users
|
||||
|
||||
|
||||
@app.get(
|
||||
"/users/{user_id}", response_model=schemas.User, dependencies=[Depends(get_db)]
|
||||
)
|
||||
def read_user(user_id: int):
|
||||
db_user = crud.get_user(user_id=user_id)
|
||||
if db_user is None:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
return db_user
|
||||
|
||||
|
||||
@app.post(
|
||||
"/users/{user_id}/items/",
|
||||
response_model=schemas.Item,
|
||||
dependencies=[Depends(get_db)],
|
||||
)
|
||||
def create_item_for_user(user_id: int, item: schemas.ItemCreate):
|
||||
return crud.create_user_item(item=item, user_id=user_id)
|
||||
|
||||
|
||||
@app.get("/items/", response_model=List[schemas.Item], dependencies=[Depends(get_db)])
|
||||
def read_items(skip: int = 0, limit: int = 100):
|
||||
items = crud.get_items(skip=skip, limit=limit)
|
||||
return items
|
||||
|
||||
|
||||
@app.get(
|
||||
"/slowusers/", response_model=List[schemas.User], dependencies=[Depends(get_db)]
|
||||
)
|
||||
def read_slow_users(skip: int = 0, limit: int = 100):
|
||||
time.sleep(15) # Fake long processing request
|
||||
users = crud.get_users(skip=skip, limit=limit)
|
||||
return users
|
||||
21
docs/src/sql_databases_peewee/sql_app/models.py
Normal file
21
docs/src/sql_databases_peewee/sql_app/models.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import peewee
|
||||
|
||||
from .database import db
|
||||
|
||||
|
||||
class User(peewee.Model):
|
||||
email = peewee.CharField(unique=True, index=True)
|
||||
hashed_password = peewee.CharField()
|
||||
is_active = peewee.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
database = db
|
||||
|
||||
|
||||
class Item(peewee.Model):
|
||||
title = peewee.CharField(index=True)
|
||||
description = peewee.CharField(index=True)
|
||||
owner = peewee.ForeignKeyField(User, backref="items")
|
||||
|
||||
class Meta:
|
||||
database = db
|
||||
49
docs/src/sql_databases_peewee/sql_app/schemas.py
Normal file
49
docs/src/sql_databases_peewee/sql_app/schemas.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from typing import Any, List
|
||||
|
||||
import peewee
|
||||
from pydantic import BaseModel
|
||||
from pydantic.utils import GetterDict
|
||||
|
||||
|
||||
class PeeweeGetterDict(GetterDict):
|
||||
def get(self, key: Any, default: Any = None):
|
||||
res = getattr(self._obj, key, default)
|
||||
if isinstance(res, peewee.ModelSelect):
|
||||
return list(res)
|
||||
return res
|
||||
|
||||
|
||||
class ItemBase(BaseModel):
|
||||
title: str
|
||||
description: str = None
|
||||
|
||||
|
||||
class ItemCreate(ItemBase):
|
||||
pass
|
||||
|
||||
|
||||
class Item(ItemBase):
|
||||
id: int
|
||||
owner_id: int
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
getter_dict = PeeweeGetterDict
|
||||
|
||||
|
||||
class UserBase(BaseModel):
|
||||
email: str
|
||||
|
||||
|
||||
class UserCreate(UserBase):
|
||||
password: str
|
||||
|
||||
|
||||
class User(UserBase):
|
||||
id: int
|
||||
is_active: bool
|
||||
items: List[Item] = []
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
getter_dict = PeeweeGetterDict
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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!}
|
||||
|
||||
62
docs/tutorial/body-fields.md
Normal file
62
docs/tutorial/body-fields.md
Normal 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.
|
||||
@@ -118,7 +118,7 @@ Apart from normal singular types like `str`, `int`, `float`, etc. You can use mo
|
||||
|
||||
To see all the options you have, checkout the docs for <a href="https://pydantic-docs.helpmanual.io/#exotic-types" target="_blank">Pydantic's exotic types</a>. You will see some examples in the next chapter.
|
||||
|
||||
For example, as in the `Image` model we have a `url` field, we can declare it to be instead of a `str`, a Pydantic's `UrlStr`:
|
||||
For example, as in the `Image` model we have a `url` field, we can declare it to be instead of a `str`, a Pydantic's `HttpUrl`:
|
||||
|
||||
```Python hl_lines="4 10"
|
||||
{!./src/body_nested_models/tutorial005.py!}
|
||||
|
||||
@@ -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.
|
||||
@@ -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`).
|
||||
|
||||
@@ -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!}
|
||||
|
||||
@@ -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!}
|
||||
```
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
186
docs/tutorial/openapi-callbacks.md
Normal file
186
docs/tutorial/openapi-callbacks.md
Normal 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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53"
|
||||
{!./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"
|
||||
{!./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="34"
|
||||
{!./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">
|
||||
@@ -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.
|
||||
|
||||
408
docs/tutorial/sql-databases-peewee.md
Normal file
408
docs/tutorial/sql-databases-peewee.md
Normal file
@@ -0,0 +1,408 @@
|
||||
!!! warning
|
||||
If you are just starting, the <a href="https://fastapi.tiangolo.com/tutorial/sql-databases/" target="_blank">SQLAlchemy tutorial</a> should be enough.
|
||||
|
||||
Feel free to skip this.
|
||||
|
||||
If you are starting a project from scratch, you are probably better off with <a href="https://fastapi.tiangolo.com/tutorial/sql-databases/" target="_blank">SQLAlchemy ORM</a>, or any other async ORM.
|
||||
|
||||
If you already have a code base that uses <a href="http://docs.peewee-orm.com/en/latest/" target="_blank">Peewee ORM</a>, you can check here how to use it with **FastAPI**.
|
||||
|
||||
!!! warning "Python 3.7+ required"
|
||||
You will need Python 3.7 or above to safely use Peewee with FastAPI.
|
||||
|
||||
## Peewee for async
|
||||
|
||||
Peewee was not designed for async frameworks, or with them in mind.
|
||||
|
||||
Peewee has some heavy assumptions about its defaults and about how it should be used.
|
||||
|
||||
If you are developing an application with an older non-async framework, and can work with all its defaults, **it can be a great tool**.
|
||||
|
||||
But if you need to change some of the defaults, support more than one predefined database, work with an async framework (like FastAPI), etc, you will need to add quite some complex extra code to override those defaults.
|
||||
|
||||
Nevertheless, it's possible to do it, and here you'll see exactly what code you have to add to be able to use Peewee with FastAPI.
|
||||
|
||||
!!! note "Technical Details"
|
||||
You can read more about Peewee's stand about async in Python <a href="http://docs.peewee-orm.com/en/latest/peewee/database.html#async-with-gevent" target="_blank">in the docs</a>, <a href="https://github.com/coleifer/peewee/issues/263#issuecomment-517347032" target="_blank">an issue</a>, <a href="https://github.com/coleifer/peewee/pull/2072#issuecomment-563215132" target="_blank">a PR</a>.
|
||||
|
||||
## The same app
|
||||
|
||||
We are going to create the same application as in the <a href="https://fastapi.tiangolo.com/tutorial/sql-databases/" target="_blank">SQLAlchemy tutorial</a>.
|
||||
|
||||
Most of the code is actually the same.
|
||||
|
||||
So, we are going to focus only on the differences.
|
||||
|
||||
## File structure
|
||||
|
||||
Let's say you have a directory named `my_super_project` that contains a sub-directory called `sql_app` with a structure like this:
|
||||
|
||||
```
|
||||
.
|
||||
└── sql_app
|
||||
├── __init__.py
|
||||
├── crud.py
|
||||
├── database.py
|
||||
├── main.py
|
||||
└── schemas.py
|
||||
```
|
||||
|
||||
This is almost the same structure as we had for the SQLAlchemy tutorial.
|
||||
|
||||
Now let's see what each file/module does.
|
||||
|
||||
## Create the Peewee parts
|
||||
|
||||
Let's refer to the file `sql_app/database.py`.
|
||||
|
||||
### The standard Peewee code
|
||||
|
||||
Let's first check all the normal Peewee code, create a Peewee database:
|
||||
|
||||
```Python hl_lines="3 5 24"
|
||||
{!./src/sql_databases_peewee/sql_app/database.py!}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
Have in mind that if you wanted to use a different database, like PostgreSQL, you couldn't just change the string. You would need to use a different Peewee database class.
|
||||
|
||||
#### Note
|
||||
|
||||
The argument:
|
||||
|
||||
```Python
|
||||
check_same_thread=False
|
||||
```
|
||||
|
||||
is equivalent to the one in the SQLAlchemy tutorial:
|
||||
|
||||
```Python
|
||||
connect_args={"check_same_thread": False}
|
||||
```
|
||||
|
||||
...it is needed only for `SQLite`.
|
||||
|
||||
!!! info "Technical Details"
|
||||
|
||||
Exactly the same technical details as in the <a href="https://fastapi.tiangolo.com/tutorial/sql-databases/#note" target="_blank">SQLAlchemy tutorial</a> apply.
|
||||
|
||||
### Make Peewee async-compatible `PeeweeConnectionState`
|
||||
|
||||
The main issue with Peewee and FastAPI is that Peewee relies heavily on <a href="https://docs.python.org/3/library/threading.html#thread-local-data" target="_blank">Python's `threading.local`</a>, and it doesn't have a direct way to override it or let you handle connections/sessions directly (as is done in the SQLAlchemy tutorial).
|
||||
|
||||
And `threading.local` is not compatible with the new async features of modern Python.
|
||||
|
||||
!!! note "Technical Details"
|
||||
`threading.local` is used to have a "magic" variable that has a different value for each thread.
|
||||
|
||||
This was useful in older frameworks designed to have one single thread per request, no more, no less.
|
||||
|
||||
Using this, each request would have its own database connection/session, which is the actual final goal.
|
||||
|
||||
But FastAPI, using the new async features, could handle more than one request on the same thread. And at the same time, for a single request, it could run multiple things in different threads (in a threadpool), depending on if you use `async def` or normal `def`. This is what gives all the performance improvements to FastAPI.
|
||||
|
||||
But Python 3.7 and above provide a more advanced alternative to `threading.local`, that can also be used in the places where `threading.local` would be used, but is compatible with the new async features.
|
||||
|
||||
We are going to use that. It's called <a href="https://docs.python.org/3/library/contextvars.html" target="_blank">`contextvars`</a>.
|
||||
|
||||
We are going to override the internal parts of Peewee that use `threading.local` and replace them with `contextvars`, with the corresponding updates.
|
||||
|
||||
This might seem a bit complex (and it actually is), you don't really need to completely understand how it works to use it.
|
||||
|
||||
We will create a `PeeweeConnectionState`:
|
||||
|
||||
```Python hl_lines="8 9 10 11 12 13 14 15 16 17 18 19 20 21"
|
||||
{!./src/sql_databases_peewee/sql_app/database.py!}
|
||||
```
|
||||
|
||||
This class inherits from a special internal class used by Peewee.
|
||||
|
||||
It has all the logic to make Peewee use `contextvars` instead of `threading.local`.
|
||||
|
||||
`contextvars` works a bit differently than `threading.local`. But the rest of Peewee's internal code assumes that this class works with `threading.local`.
|
||||
|
||||
So, we need to do some extra tricks to make it work as if it was just using `threading.local`. The `__init__`, `__setattr__`, and `__getattr__` implement all the required tricks for this to be used by Peewee without knowing that it is now compatible with FastAPI.
|
||||
|
||||
!!! tip
|
||||
This will just make Peewee behave correctly when used with FastAPI. Not randomly opening or closing connections that are being used, creating errors, etc.
|
||||
|
||||
But it doesn't give Peewee async super-powers. You should still use normal `def` functions and not `async def`.
|
||||
|
||||
### Use the custom `PeeweeConnectionState` class
|
||||
|
||||
Now, overwrite the `._state` internal attribute in the Peewee database `db` object using the new `PeeweeConnectionState`:
|
||||
|
||||
```Python hl_lines="26"
|
||||
{!./src/sql_databases_peewee/sql_app/database.py!}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
Make sure you overwrite `db._state` *after* creating `db`.
|
||||
|
||||
!!! tip
|
||||
You would do the same for any other Peewee database, including `PostgresqlDatabase`, `MySQLDatabase`, etc.
|
||||
|
||||
## Create the database models
|
||||
|
||||
Let's now see the file `sql_app/models.py`.
|
||||
|
||||
### Create Peewee models for our data
|
||||
|
||||
Now create the Peewee models (classes) for `User` and `Item`.
|
||||
|
||||
This is the same you would do if you followed the Peewee tutorial and updated the models to have the same data as in the SQLAlchemy tutorial.
|
||||
|
||||
!!! tip
|
||||
Peewee also uses the term "**model**" to refer to these classes and instances that interact with the database.
|
||||
|
||||
But Pydantic also uses the term "**model**" to refer to something different, the data validation, conversion, and documentation classes and instances.
|
||||
|
||||
Import `db` from `database` (the file `database.py` from above) and use it here.
|
||||
|
||||
```Python hl_lines="3 6 7 8 9 10 11 12 15 16 17 18 19 20 21"
|
||||
{!./src/sql_databases_peewee/sql_app/models.py!}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
Peewee creates several magic attributes.
|
||||
|
||||
It will automatically add an `id` attribute as an integer to be the primary key.
|
||||
|
||||
It will chose the name of the tables based on the class names.
|
||||
|
||||
For the `Item`, it will create an attribute `owner_id` with the integer ID of the `User`. But we don't declare it anywhere.
|
||||
|
||||
## Create the Pydantic models
|
||||
|
||||
Now let's check the file `sql_app/schemas.py`.
|
||||
|
||||
!!! tip
|
||||
To avoid confusion between the Peewee *models* and the Pydantic *models*, we will have the file `models.py` with the Peewee models, and the file `schemas.py` with the Pydantic models.
|
||||
|
||||
These Pydantic models define more or less a "schema" (a valid data shape).
|
||||
|
||||
So this will help us avoiding confusion while using both.
|
||||
|
||||
### Create the Pydantic *models* / schemas
|
||||
|
||||
Create all the same Pydantic models as in the SQLAlchemy tutorial:
|
||||
|
||||
```Python hl_lines="16 17 18 21 22 25 26 27 28 29 30 34 35 38 39 42 43 44 45 46 47 48"
|
||||
{!./src/sql_databases_peewee/sql_app/schemas.py!}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
Here we are creating the models with an `id`.
|
||||
|
||||
We didn't explicitly specify an `id` attribute in the Peewee models, but Peewee adds one automatically.
|
||||
|
||||
We are also adding the magic `owner_id` attribute to `Item`.
|
||||
|
||||
### Create a `PeeweeGetterDict` for the Pydantic *models* / schemas
|
||||
|
||||
When you access a relationship in a Peewee object, like in `some_user.items`, Peewee doesn't provide a `list` of `Item`.
|
||||
|
||||
It provides a special custom object of class `ModelSelect`.
|
||||
|
||||
It's possible to create a `list` of its items with `list(some_user.items)`.
|
||||
|
||||
But the object itself is not a `list`. And it's also not an actual Python <a href="https://docs.python.org/3/glossary.html#term-generator" target="_blank">generator</a>. Because of this, Pydantic doesn't know by default how to convert it to a `list` of Pydantic *models* / schemas.
|
||||
|
||||
But recent versions of Pydantic allow providing a custom class that inherits from `pydantic.utils.GetterDict`, to provide the functionality used when using the `orm_mode = True` to retrieve the values for ORM model attributes.
|
||||
|
||||
We are going to create a custom `PeeweeGetterDict` class and use it in all the same Pydantic *models* / schemas that use `orm_mode`:
|
||||
|
||||
```Python hl_lines="3 8 9 10 11 12 13 31 49"
|
||||
{!./src/sql_databases_peewee/sql_app/schemas.py!}
|
||||
```
|
||||
|
||||
Here we are checking if the attribute that is being accessed (e.g. `.items` in `some_user.items`) is an instance of `peewee.ModelSelect`.
|
||||
|
||||
And if that's the case, just return a `list` with it.
|
||||
|
||||
And then we use it in the Pydantic *models* / schemas that use `orm_mode = True`, with the configuration variable `getter_dict = PeeweeGetterDict`.
|
||||
|
||||
!!! tip
|
||||
We only need to create one `PeeweeGetterDict` class, and we can use it in all the Pydantic *models* / schemas.
|
||||
|
||||
## CRUD utils
|
||||
|
||||
Now let's see the file `sql_app/crud.py`.
|
||||
|
||||
### Create all the CRUD utils
|
||||
|
||||
Create all the same CRUD utils as in the SQLAlchemy tutorial, all the code is very similar:
|
||||
|
||||
```Python hl_lines="1 4 5 8 9 12 13 16 17 18 19 20 23 24 27 28 29 30"
|
||||
{!./src/sql_databases_peewee/sql_app/crud.py!}
|
||||
```
|
||||
|
||||
There are some differences with the code for the SQLAlchemy tutorial.
|
||||
|
||||
We don't pass a `db` attribute around. Instead we use the models directly. This is because the `db` object is a global object, that includes all the connection logic. That's why we had to do all the `contextvars` updates above.
|
||||
|
||||
Aso, when returning several objects, like in `get_users`, we directly call `list`, like in:
|
||||
|
||||
```Python
|
||||
list(models.User.select())
|
||||
```
|
||||
|
||||
This is for the same reason that we had to create a custom `PeeweeGetterDict`. But by returning something that is already a `list` instead of the `peewee.ModelSelect` the `response_model` in the path operation with `List[models.User]` (that we'll see later) will work correctly.
|
||||
|
||||
## Main **FastAPI** app
|
||||
|
||||
And now in the file `sql_app/main.py` let's integrate and use all the other parts we created before.
|
||||
|
||||
### Create the database tables
|
||||
|
||||
In a very simplistic way create the database tables:
|
||||
|
||||
```Python hl_lines="8 9 10"
|
||||
{!./src/sql_databases_peewee/sql_app/main.py!}
|
||||
```
|
||||
|
||||
### Create a dependency
|
||||
|
||||
Create a dependency that will connect the database right at the beginning of a request and disconnect it at the end:
|
||||
|
||||
```Python hl_lines="15 16 17 18 19 20 21 22"
|
||||
{!./src/sql_databases_peewee/sql_app/main.py!}
|
||||
```
|
||||
|
||||
Here we have an empty `yield` because we are actually not using the database object directly.
|
||||
|
||||
It is connecting to the database and storing the connection data in an internal variable that is independent for each request (using the `contextvars` tricks from above).
|
||||
|
||||
And then, in each *path operation function* that needs to access the database we add it as a dependency.
|
||||
|
||||
But we are not using the value given by this dependency (it actually doesn't give any value, as it has an empty `yield`). So, we don't add it to the *path operation function* but to the *path operation decorator* in the `dependencies` parameter:
|
||||
|
||||
```Python hl_lines="25 33 40 52 58 65"
|
||||
{!./src/sql_databases_peewee/sql_app/main.py!}
|
||||
```
|
||||
|
||||
### Create your **FastAPI** *path operations*
|
||||
|
||||
Now, finally, here's the standard **FastAPI** *path operations* code.
|
||||
|
||||
```Python hl_lines="25 26 27 28 29 30 33 34 35 36 39 40 41 42 43 44 45 46 49 50 51 52 53 54 55 58 59 60 61 64 65 66 67 68 69 70"
|
||||
{!./src/sql_databases_peewee/sql_app/main.py!}
|
||||
```
|
||||
|
||||
### About `def` vs `async def`
|
||||
|
||||
The same as with SQLAlchemy, we are not doing something like:
|
||||
|
||||
```Python
|
||||
user = await models.User.select().first()
|
||||
```
|
||||
|
||||
...but instead we are using:
|
||||
|
||||
```Python
|
||||
user = models.User.select().first()
|
||||
```
|
||||
|
||||
So, again, we should declare the *path operation functions* and the dependency without `async def`, just with a normal `def`, as:
|
||||
|
||||
```Python hl_lines="2"
|
||||
# Something goes here
|
||||
def read_users(skip: int = 0, limit: int = 100):
|
||||
# Something goes here
|
||||
```
|
||||
|
||||
## Testing Peewee with async
|
||||
|
||||
This example includes an extra *path operation* that simulates a long processing request with `time.sleep(15)`.
|
||||
|
||||
It will have the database connection open at the beginning and will just wait 15 seconds before replying back.
|
||||
|
||||
This will easily let you test that your app with Peewee and FastAPI is behaving correctly with all the stuff about threads.
|
||||
|
||||
If you want to check how Peewee would break your app if used without modification, go the the `sql_app/database.py` file and comment the line:
|
||||
|
||||
```Python
|
||||
# db._state = PeeweeConnectionState()
|
||||
```
|
||||
|
||||
Then run your app with Uvicorn:
|
||||
|
||||
```bash
|
||||
uvicorn sql_app.main:app --reload
|
||||
```
|
||||
|
||||
Open your browser at <a href="http://127.0.0.1:8000/docs" target="_blank">http://127.0.0.1:8000/docs</a> and create a couple of users.
|
||||
|
||||
Then open 10 tabs at <a href="http://127.0.0.1:8000/docs#/default/read_slow_users_slowusers__get" target="_blank">http://127.0.0.1:8000/docs#/default/read_slow_users_slowusers__get</a> at the same time.
|
||||
|
||||
Go to the *path operation* "Get `/slowusers/`" in all of the tabs. Use the "Try it out" button and execute the request in each tab, one right after the other.
|
||||
|
||||
The tabs will wait for a bit and then some of them will show `Internal Server Error`.
|
||||
|
||||
### What happens
|
||||
|
||||
The first tab will make your app create a connection to the database and wait for 15 seconds before replying back and closing the connection.
|
||||
|
||||
Then one of the other tabs will try to open a database connection, but as one of those requests for the other tabs will probably be handled in the same thread as the first one, it will have the same database connection that is already open, and Peewee will throw an error and you will see it in the terminal, and the response will have an `Internal Server Error`.
|
||||
|
||||
This will probably happen for more than one of those tabs.
|
||||
|
||||
If you had multiple clients talking to your app exactly at the same time, this is what could happen.
|
||||
|
||||
And as your app starts to handle more and more clients at the same time, the waiting time in a single requests needs to be shorter and shorter to trigger the error.
|
||||
|
||||
### Fix Peewee with FastAPI
|
||||
|
||||
Now go back to the file `sql_app/database.py`, and uncomment the line:
|
||||
|
||||
```Python
|
||||
db._state = PeeweeConnectionState()
|
||||
```
|
||||
|
||||
Terminate your running app and start it again.
|
||||
|
||||
Repeat the same process with the 10 tabs. This time all of them will wait and you will get all the results without errors.
|
||||
|
||||
...You fixed it!
|
||||
|
||||
## Review all the files
|
||||
|
||||
Remember you should have a directory named `my_super_project` that contains a sub-directory called `sql_app`.
|
||||
|
||||
`sql_app` should have the following files:
|
||||
|
||||
* `sql_app/__init__.py`: is an empty file.
|
||||
|
||||
* `sql_app/database.py`:
|
||||
|
||||
```Python hl_lines=""
|
||||
{!./src/sql_databases_peewee/sql_app/database.py!}
|
||||
```
|
||||
|
||||
* `sql_app/models.py`:
|
||||
|
||||
```Python hl_lines=""
|
||||
{!./src/sql_databases_peewee/sql_app/models.py!}
|
||||
```
|
||||
|
||||
* `sql_app/schemas.py`:
|
||||
|
||||
```Python hl_lines=""
|
||||
{!./src/sql_databases_peewee/sql_app/schemas.py!}
|
||||
```
|
||||
|
||||
* `sql_app/crud.py`:
|
||||
|
||||
```Python hl_lines=""
|
||||
{!./src/sql_databases_peewee/sql_app/crud.py!}
|
||||
```
|
||||
|
||||
* `sql_app/main.py`:
|
||||
|
||||
```Python hl_lines=""
|
||||
{!./src/sql_databases_peewee/sql_app/main.py!}
|
||||
```
|
||||
|
||||
## Technical Details
|
||||
|
||||
If you want to go deeper into the technical details related to Peewee with FastAPI, you can <a href="https://github.com/coleifer/peewee/pull/2072" target="_blank">read more about it here</a>.
|
||||
@@ -55,20 +55,24 @@ Common ORMs are for example: Django-ORM (part of the Django framework), SQLAlche
|
||||
|
||||
Here we will see how to work with **SQLAlchemy ORM**.
|
||||
|
||||
The same way, you could use Peewee or any other.
|
||||
In a similar way you could use any other ORM.
|
||||
|
||||
!!! tip
|
||||
There's an equivalent article using Peewee here in the docs.
|
||||
|
||||
## File structure
|
||||
|
||||
For these examples, let's say you have a directory named `my_super_project` that contains a sub-directory called `sql_app` with a structure like this:
|
||||
|
||||
```
|
||||
├── sql_app
|
||||
│ ├── __init__.py
|
||||
│ ├── crud.py
|
||||
│ ├── database.py
|
||||
│ ├── main.py
|
||||
│ ├── models.py
|
||||
│ ├── schemas.py
|
||||
.
|
||||
└── sql_app
|
||||
├── __init__.py
|
||||
├── crud.py
|
||||
├── database.py
|
||||
├── main.py
|
||||
├── models.py
|
||||
└── schemas.py
|
||||
```
|
||||
|
||||
The file `__init__.py` is just an empty file, but it tells Python that `sql_app` with all its modules (Python files) is a package.
|
||||
@@ -131,12 +135,17 @@ connect_args={"check_same_thread": False}
|
||||
|
||||
!!! info "Technical Details"
|
||||
|
||||
That argument `check_same_thread` is there mainly to be able to run the tests that cover this example.
|
||||
|
||||
By default SQLite will only allow one thread to communicate with it, assuming that each thread would handle an independent request.
|
||||
|
||||
This is to prevent accidentally sharing the same connection for different things (for different requests).
|
||||
|
||||
But in FastAPI, using normal functions (`def`) more than one thread could interact with the database for the same request, so we need to make SQLite know that it should allow that with `connect_args={"check_same_thread": False}`.
|
||||
|
||||
Also, we will make sure each request gets its own database connection session in a dependency, so there's no need for that default mechanism.
|
||||
|
||||
### Create a `SessionLocal` class
|
||||
|
||||
Each instance of the `SessionLocal` class will be a database session. The class itself is not a database session yet.
|
||||
Each instance of the `SessionLocal` class will be a database session. The class itself is not a database session yet.
|
||||
|
||||
But once we create an instance of the `SessionLocal` class, this instance will be the actual database session.
|
||||
|
||||
@@ -413,9 +422,9 @@ And now in the file `sql_app/main.py` let's integrate and use all the other part
|
||||
|
||||
### Create the database tables
|
||||
|
||||
In a very simplistic way, create the database tables:
|
||||
In a very simplistic way create the database tables:
|
||||
|
||||
```Python hl_lines="11"
|
||||
```Python hl_lines="9"
|
||||
{!./src/sql_databases/sql_app/main.py!}
|
||||
```
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.43.0"
|
||||
__version__ = "0.46.0"
|
||||
|
||||
from starlette.background import BackgroundTasks
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -506,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 = {}
|
||||
@@ -518,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):
|
||||
@@ -543,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:
|
||||
@@ -564,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)
|
||||
):
|
||||
@@ -605,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
|
||||
@@ -638,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
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from enum import Enum
|
||||
from types import GeneratorType
|
||||
from typing import Any, Dict, List, Set, Union
|
||||
from typing import Any, Callable, Dict, List, Set, Tuple, Union
|
||||
|
||||
from fastapi.logger import logger
|
||||
from fastapi.utils import PYDANTIC_1
|
||||
from pydantic import BaseModel
|
||||
from pydantic.json import ENCODERS_BY_TYPE
|
||||
|
||||
@@ -9,29 +11,62 @@ SetIntStr = Set[Union[int, str]]
|
||||
DictIntStrAny = Dict[Union[int, str], Any]
|
||||
|
||||
|
||||
def generate_encoders_by_class_tuples(
|
||||
type_encoder_map: Dict[Any, Callable]
|
||||
) -> Dict[Callable, Tuple]:
|
||||
encoders_by_classes: Dict[Callable, List] = {}
|
||||
for type_, encoder in type_encoder_map.items():
|
||||
encoders_by_classes.setdefault(encoder, []).append(type_)
|
||||
encoders_by_class_tuples: Dict[Callable, Tuple] = {}
|
||||
for encoder, classes in encoders_by_classes.items():
|
||||
encoders_by_class_tuples[encoder] = tuple(classes)
|
||||
return encoders_by_class_tuples
|
||||
|
||||
|
||||
encoders_by_class_tuples = generate_encoders_by_class_tuples(ENCODERS_BY_TYPE)
|
||||
|
||||
|
||||
def jsonable_encoder(
|
||||
obj: Any,
|
||||
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 +90,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 +98,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,35 +114,42 @@ 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,
|
||||
)
|
||||
)
|
||||
return encoded_list
|
||||
|
||||
if custom_encoder:
|
||||
if type(obj) in custom_encoder:
|
||||
return custom_encoder[type(obj)](obj)
|
||||
else:
|
||||
for encoder_type, encoder in custom_encoder.items():
|
||||
if isinstance(obj, encoder_type):
|
||||
return encoder(obj)
|
||||
|
||||
if type(obj) in ENCODERS_BY_TYPE:
|
||||
return ENCODERS_BY_TYPE[type(obj)](obj)
|
||||
for encoder, classes_tuple in encoders_by_class_tuples.items():
|
||||
if isinstance(obj, classes_tuple):
|
||||
return encoder(obj)
|
||||
|
||||
errors: List[Exception] = []
|
||||
try:
|
||||
if custom_encoder and type(obj) in custom_encoder:
|
||||
encoder = custom_encoder[type(obj)]
|
||||
else:
|
||||
encoder = ENCODERS_BY_TYPE[type(obj)]
|
||||
return encoder(obj)
|
||||
except KeyError as e:
|
||||
data = dict(obj)
|
||||
except Exception as e:
|
||||
errors.append(e)
|
||||
try:
|
||||
data = dict(obj)
|
||||
data = vars(obj)
|
||||
except Exception as e:
|
||||
errors.append(e)
|
||||
try:
|
||||
data = vars(obj)
|
||||
except Exception as e:
|
||||
errors.append(e)
|
||||
raise ValueError(errors)
|
||||
raise ValueError(errors)
|
||||
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,
|
||||
|
||||
@@ -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())},
|
||||
)
|
||||
|
||||
@@ -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
3
fastapi/logger.py
Normal file
@@ -0,0 +1,3 @@
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("fastapi")
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -14,16 +14,23 @@ 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",
|
||||
@@ -57,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
|
||||
@@ -83,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:
|
||||
@@ -180,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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,11 +12,17 @@ from fastapi.dependencies.utils import (
|
||||
)
|
||||
from fastapi.encoders import DictIntStrAny, SetIntStr, jsonable_encoder
|
||||
from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError
|
||||
from fastapi.logger import logger
|
||||
from fastapi.openapi.constants import STATUS_CODES_WITH_NO_BODY
|
||||
from fastapi.utils import create_cloned_field, generate_operation_id_for_path
|
||||
from pydantic import BaseConfig, BaseModel, Schema
|
||||
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
|
||||
@@ -34,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_)
|
||||
@@ -60,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)
|
||||
@@ -68,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:
|
||||
@@ -93,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
|
||||
@@ -122,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,
|
||||
@@ -199,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
|
||||
@@ -220,15 +236,26 @@ class APIRoute(routing.Route):
|
||||
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
|
||||
@@ -236,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
|
||||
@@ -267,18 +294,29 @@ class APIRoute(routing.Route):
|
||||
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
|
||||
@@ -286,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
|
||||
|
||||
@@ -301,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:
|
||||
@@ -313,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,
|
||||
)
|
||||
|
||||
@@ -326,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,
|
||||
@@ -352,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,
|
||||
@@ -376,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)
|
||||
|
||||
@@ -402,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,
|
||||
@@ -425,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
|
||||
|
||||
@@ -493,11 +549,12 @@ 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,
|
||||
route_class_override=type(route),
|
||||
callbacks=route.callbacks,
|
||||
)
|
||||
elif isinstance(route, routing.Route):
|
||||
self.add_route(
|
||||
@@ -533,11 +590,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,
|
||||
@@ -554,10 +615,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(
|
||||
@@ -577,11 +641,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,
|
||||
@@ -598,10 +666,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(
|
||||
@@ -621,11 +692,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,
|
||||
@@ -642,10 +717,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(
|
||||
@@ -665,11 +743,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,
|
||||
@@ -686,10 +768,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(
|
||||
@@ -709,11 +794,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,
|
||||
@@ -730,10 +819,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(
|
||||
@@ -753,11 +845,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,
|
||||
@@ -774,10 +870,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(
|
||||
@@ -797,11 +896,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,
|
||||
@@ -818,10 +921,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(
|
||||
@@ -841,11 +947,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,
|
||||
@@ -862,8 +972,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,
|
||||
)
|
||||
|
||||
@@ -8,6 +8,7 @@ from .http import (
|
||||
)
|
||||
from .oauth2 import (
|
||||
OAuth2,
|
||||
OAuth2AuthorizationCodeBearer,
|
||||
OAuth2PasswordBearer,
|
||||
OAuth2PasswordRequestForm,
|
||||
SecurityScopes,
|
||||
|
||||
@@ -163,6 +163,43 @@ class OAuth2PasswordBearer(OAuth2):
|
||||
return param
|
||||
|
||||
|
||||
class OAuth2AuthorizationCodeBearer(OAuth2):
|
||||
def __init__(
|
||||
self,
|
||||
authorizationUrl: str,
|
||||
tokenUrl: str,
|
||||
refreshUrl: str = None,
|
||||
scheme_name: str = None,
|
||||
scopes: dict = None,
|
||||
auto_error: bool = True,
|
||||
):
|
||||
if not scopes:
|
||||
scopes = {}
|
||||
flows = OAuthFlowsModel(
|
||||
authorizationCode={
|
||||
"authorizationUrl": authorizationUrl,
|
||||
"tokenUrl": tokenUrl,
|
||||
"refreshUrl": refreshUrl,
|
||||
"scopes": scopes,
|
||||
}
|
||||
)
|
||||
super().__init__(flows=flows, scheme_name=scheme_name, auto_error=auto_error)
|
||||
|
||||
async def __call__(self, request: Request) -> Optional[str]:
|
||||
authorization: str = request.headers.get("Authorization")
|
||||
scheme, param = get_authorization_scheme_param(authorization)
|
||||
if not authorization or scheme.lower() != "bearer":
|
||||
if self.auto_error:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_401_UNAUTHORIZED,
|
||||
detail="Not authenticated",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
else:
|
||||
return None # pragma: nocover
|
||||
return param
|
||||
|
||||
|
||||
class SecurityScopes:
|
||||
def __init__(self, scopes: List[str] = None):
|
||||
self.scopes = scopes or []
|
||||
|
||||
@@ -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
|
||||
@@ -64,22 +99,36 @@ def create_cloned_field(field: Field) -> Field:
|
||||
for f in original_type.__fields__.values():
|
||||
use_type.__fields__[f.name] = f
|
||||
use_type.__validators__ = original_type.__validators__
|
||||
new_field = Field(
|
||||
name=field.name,
|
||||
type_=use_type,
|
||||
class_validators={},
|
||||
default=None,
|
||||
required=False,
|
||||
model_config=BaseConfig,
|
||||
schema=Schema(None),
|
||||
)
|
||||
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:
|
||||
@@ -89,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
|
||||
|
||||
39
mkdocs.yml
39
mkdocs.yml
@@ -5,8 +5,8 @@ site_url: https://fastapi.tiangolo.com/
|
||||
theme:
|
||||
name: 'material'
|
||||
palette:
|
||||
primary: 'teal'
|
||||
accent: 'amber'
|
||||
primary: 'teal'
|
||||
accent: 'amber'
|
||||
logo: 'img/icon-white.svg'
|
||||
favicon: 'img/favicon.png'
|
||||
|
||||
@@ -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'
|
||||
@@ -72,6 +72,7 @@ nav:
|
||||
- CORS (Cross-Origin Resource Sharing): 'tutorial/cors.md'
|
||||
- Using the Request Directly: 'tutorial/using-request-directly.md'
|
||||
- SQL (Relational) Databases: 'tutorial/sql-databases.md'
|
||||
- SQL (Relational) Databases with Peewee: 'tutorial/sql-databases-peewee.md'
|
||||
- Async SQL (Relational) Databases: 'tutorial/async-sql-databases.md'
|
||||
- NoSQL (Distributed / Big Data) Databases: 'tutorial/nosql-databases.md'
|
||||
- Bigger Applications - Multiple Files: 'tutorial/bigger-applications.md'
|
||||
@@ -88,6 +89,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'
|
||||
@@ -100,10 +102,27 @@ nav:
|
||||
- Release Notes: release-notes.md
|
||||
|
||||
markdown_extensions:
|
||||
- markdown.extensions.codehilite:
|
||||
guess_lang: false
|
||||
- markdown_include.include:
|
||||
base_path: docs
|
||||
- admonition
|
||||
- codehilite
|
||||
- extra
|
||||
- toc:
|
||||
permalink: true
|
||||
- markdown.extensions.codehilite:
|
||||
guess_lang: false
|
||||
- markdown_include.include:
|
||||
base_path: docs
|
||||
- admonition
|
||||
- codehilite
|
||||
- extra
|
||||
|
||||
extra:
|
||||
social:
|
||||
- type: 'github'
|
||||
link: 'https://github.com/tiangolo/typer'
|
||||
- type: 'twitter'
|
||||
link: 'https://twitter.com/tiangolo'
|
||||
- type: 'linkedin'
|
||||
link: 'https://www.linkedin.com/in/tiangolo'
|
||||
- type: 'rss'
|
||||
link: 'https://dev.to/tiangolo'
|
||||
- type: 'medium'
|
||||
link: 'https://medium.com/@tiangolo'
|
||||
- type: 'globe'
|
||||
link: 'https://tiangolo.com'
|
||||
|
||||
@@ -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"
|
||||
@@ -38,6 +51,7 @@ test = [
|
||||
"requests",
|
||||
"email_validator",
|
||||
"sqlalchemy",
|
||||
"peewee",
|
||||
"databases[sqlite]",
|
||||
"orjson",
|
||||
"async_exit_stack",
|
||||
|
||||
@@ -11,3 +11,5 @@ fi
|
||||
export PYTHONPATH=./docs/src
|
||||
pytest --cov=fastapi --cov=tests --cov=docs/src --cov-report=term-missing ${@}
|
||||
bash ./scripts/lint.sh
|
||||
# Check README.md is up to date
|
||||
diff --brief docs/index.md README.md
|
||||
|
||||
@@ -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,
|
||||
|
||||
70
tests/test_dependency_class.py
Normal file
70
tests/test_dependency_class.py
Normal 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
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
73
tests/test_inherited_custom_class.py
Normal file
73
tests/test_inherited_custom_class.py
Normal file
@@ -0,0 +1,73 @@
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class MyUuid:
|
||||
def __init__(self, uuid_string: str):
|
||||
self.uuid = uuid_string
|
||||
|
||||
def __str__(self):
|
||||
return self.uuid
|
||||
|
||||
@property
|
||||
def __class__(self):
|
||||
return uuid.UUID
|
||||
|
||||
@property
|
||||
def __dict__(self):
|
||||
"""Spoof a missing __dict__ by raising TypeError, this is how
|
||||
asyncpg.pgroto.pgproto.UUID behaves"""
|
||||
raise TypeError("vars() argument must have __dict__ attribute")
|
||||
|
||||
|
||||
@app.get("/fast_uuid")
|
||||
def return_fast_uuid():
|
||||
# I don't want to import asyncpg for this test so I made my own UUID
|
||||
# Import asyncpg and uncomment the two lines below for the actual bug
|
||||
|
||||
# from asyncpg.pgproto import pgproto
|
||||
# asyncpg_uuid = pgproto.UUID("a10ff360-3b1e-4984-a26f-d3ab460bdb51")
|
||||
|
||||
asyncpg_uuid = MyUuid("a10ff360-3b1e-4984-a26f-d3ab460bdb51")
|
||||
assert isinstance(asyncpg_uuid, uuid.UUID)
|
||||
assert type(asyncpg_uuid) != uuid.UUID
|
||||
with pytest.raises(TypeError):
|
||||
vars(asyncpg_uuid)
|
||||
return {"fast_uuid": asyncpg_uuid}
|
||||
|
||||
|
||||
class SomeCustomClass(BaseModel):
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
json_encoders = {uuid.UUID: str}
|
||||
|
||||
a_uuid: MyUuid
|
||||
|
||||
|
||||
@app.get("/get_custom_class")
|
||||
def return_some_user():
|
||||
# Test that the fix also works for custom pydantic classes
|
||||
return SomeCustomClass(a_uuid=MyUuid("b8799909-f914-42de-91bc-95c819218d01"))
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_dt():
|
||||
with client:
|
||||
response_simple = client.get("/fast_uuid")
|
||||
response_pydantic = client.get("/get_custom_class")
|
||||
|
||||
assert response_simple.json() == {
|
||||
"fast_uuid": "a10ff360-3b1e-4984-a26f-d3ab460bdb51"
|
||||
}
|
||||
|
||||
assert response_pydantic.json() == {
|
||||
"a_uuid": "b8799909-f914-42de-91bc-95c819218d01"
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
77
tests/test_security_oauth2_authorization_code_bearer.py
Normal file
77
tests/test_security_oauth2_authorization_code_bearer.py
Normal file
@@ -0,0 +1,77 @@
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import FastAPI, Security
|
||||
from fastapi.security import OAuth2AuthorizationCodeBearer
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
oauth2_scheme = OAuth2AuthorizationCodeBearer(
|
||||
authorizationUrl="/authorize", tokenUrl="/token", auto_error=True
|
||||
)
|
||||
|
||||
|
||||
@app.get("/items/")
|
||||
async def read_items(token: Optional[str] = Security(oauth2_scheme)):
|
||||
return {"token": token}
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/items/": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read Items",
|
||||
"operationId": "read_items_items__get",
|
||||
"security": [{"OAuth2AuthorizationCodeBearer": []}],
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"securitySchemes": {
|
||||
"OAuth2AuthorizationCodeBearer": {
|
||||
"type": "oauth2",
|
||||
"flows": {
|
||||
"authorizationCode": {
|
||||
"authorizationUrl": "/authorize",
|
||||
"tokenUrl": "/token",
|
||||
"scopes": {},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_no_token():
|
||||
response = client.get("/items")
|
||||
assert response.status_code == 401
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
|
||||
|
||||
def test_incorrect_token():
|
||||
response = client.get("/items", headers={"Authorization": "Non-existent testtoken"})
|
||||
assert response.status_code == 401
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
|
||||
|
||||
def test_token():
|
||||
response = client.get("/items", headers={"Authorization": "Bearer testtoken"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"token": "testtoken"}
|
||||
@@ -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": {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
230
tests/test_sub_callbacks.py
Normal file
230
tests/test_sub_callbacks.py
Normal file
@@ -0,0 +1,230 @@
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from pydantic import BaseModel, HttpUrl
|
||||
from starlette.responses import JSONResponse
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
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
|
||||
|
||||
|
||||
subrouter = APIRouter()
|
||||
|
||||
|
||||
@subrouter.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"}
|
||||
|
||||
|
||||
app.include_router(subrouter)
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/invoices/": {
|
||||
"post": {
|
||||
"summary": "Create Invoice",
|
||||
"description": 'Create an invoice.\n\nThis will (let\'s imagine) let the API user (some external developer) create an\ninvoice.\n\nAnd this path operation will:\n\n* Send the invoice to the client.\n* Collect the money from the client.\n* Send a notification back to the API user (the external developer), as a callback.\n * At this point is that the API will somehow send a POST request to the\n external API with the notification of the invoice event\n (e.g. "payment successful").',
|
||||
"operationId": "create_invoice_invoices__post",
|
||||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"title": "Callback Url",
|
||||
"maxLength": 2083,
|
||||
"minLength": 1,
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
},
|
||||
"name": "callback_url",
|
||||
"in": "query",
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/Invoice"}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"callbacks": {
|
||||
"invoice_notification": {
|
||||
"{$callback_url}/invoices/{$request.body.id}": {
|
||||
"post": {
|
||||
"summary": "Invoice Notification",
|
||||
"operationId": "invoice_notification__callback_url__invoices___request_body_id__post",
|
||||
"requestBody": {
|
||||
"required": True,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InvoiceEvent"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InvoiceEventReceived"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"HTTPValidationError": {
|
||||
"title": "HTTPValidationError",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"detail": {
|
||||
"title": "Detail",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||
}
|
||||
},
|
||||
},
|
||||
"Invoice": {
|
||||
"title": "Invoice",
|
||||
"required": ["id", "customer", "total"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"title": "Id", "type": "string"},
|
||||
"title": {"title": "Title", "type": "string"},
|
||||
"customer": {"title": "Customer", "type": "string"},
|
||||
"total": {"title": "Total", "type": "number"},
|
||||
},
|
||||
},
|
||||
"InvoiceEvent": {
|
||||
"title": "InvoiceEvent",
|
||||
"required": ["description", "paid"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"description": {"title": "Description", "type": "string"},
|
||||
"paid": {"title": "Paid", "type": "boolean"},
|
||||
},
|
||||
},
|
||||
"InvoiceEventReceived": {
|
||||
"title": "InvoiceEventReceived",
|
||||
"required": ["ok"],
|
||||
"type": "object",
|
||||
"properties": {"ok": {"title": "Ok", "type": "boolean"}},
|
||||
},
|
||||
"ValidationError": {
|
||||
"title": "ValidationError",
|
||||
"required": ["loc", "msg", "type"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"loc": {
|
||||
"title": "Location",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"msg": {"title": "Message", "type": "string"},
|
||||
"type": {"title": "Error Type", "type": "string"},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi():
|
||||
with client:
|
||||
response = client.get("/openapi.json")
|
||||
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_get():
|
||||
response = client.post(
|
||||
"/invoices/", json={"id": "fooinvoice", "customer": "John", "total": 5.3}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"msg": "Invoice received"}
|
||||
|
||||
|
||||
def test_dummy_callback():
|
||||
# Just for coverage
|
||||
invoice_notification({})
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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"},
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
0
tests/test_tutorial/test_body_fields/__init__.py
Normal file
0
tests/test_tutorial/test_body_fields/__init__.py
Normal 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",
|
||||
}
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user