Compare commits

...

39 Commits

Author SHA1 Message Date
Sebastián Ramírez
c89dc736bd 📝 Update release notes 2022-03-04 23:11:21 +01:00
Sebastián Ramírez
19769d0483 🔖 Release version 0.75.0 2022-03-04 23:09:11 +01:00
Sebastián Ramírez
586d17bfb5 📝 Update release notes 2022-03-04 23:08:06 +01:00
github-actions
786b5858e5 📝 Update release notes 2022-03-04 22:02:55 +00:00
Sebastián Ramírez
8a0d4c79c1 Add support for custom generate_unique_id_function and docs for generating clients (#4650) 2022-03-04 23:02:18 +01:00
Sebastián Ramírez
440d2d2d1a 🔖 Release version 0.74.1 2022-02-21 16:54:18 +01:00
github-actions
e953059054 📝 Update release notes 2022-02-21 15:52:03 +00:00
Sebastián Ramírez
f5d7df3c6c Include route in scope to allow middleware and other tools to extract its information (#4603) 2022-02-21 16:51:26 +01:00
Sebastián Ramírez
1ce16c2f40 🔖 Release version 0.74.0 2022-02-17 17:08:23 +01:00
Sebastián Ramírez
03d4d4c38e 📝 Update release notes 2022-02-17 17:05:38 +01:00
github-actions
2d2f1dee5d 📝 Update release notes 2022-02-17 14:18:35 +00:00
Sebastián Ramírez
59e36481dc 🔧 Add Striveworks sponsor (#4596) 2022-02-17 14:18:01 +00:00
github-actions
4fcb00328c 📝 Update release notes 2022-02-17 12:40:46 +00:00
Sebastián Ramírez
9d56a3cb59 Update internal AsyncExitStack to fix context for dependencies with yield (#4575) 2022-02-17 13:40:12 +01:00
github-actions
78b07cb809 📝 Update release notes 2022-02-13 17:19:09 +00:00
Sebastián Ramírez
6034f80687 💚 Only build docs on push when on master to avoid duplicate runs from PRs (#4564) 2022-02-13 18:18:38 +01:00
github-actions
b93f8a709a 📝 Update release notes 2022-02-01 14:28:16 +00:00
github-actions[bot]
618c99d774 👥 Update FastAPI People (#4502)
Co-authored-by: github-actions <github-actions@github.com>
2022-02-01 15:27:34 +01:00
Sebastián Ramírez
291180bf2d 🔖 Release version 0.73.0 2022-01-23 23:38:51 +01:00
Sebastián Ramírez
cbe8d552c1 📝 Update release notes 2022-01-23 23:37:48 +01:00
github-actions
af18d5c49f 📝 Update release notes 2022-01-23 22:14:28 +00:00
Victor Benichoux
a698908ed6 🐛 Fix bug preventing to use OpenAPI when using tuples (#3874)
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2022-01-23 23:13:55 +01:00
github-actions
dba9ea8120 📝 Update release notes 2022-01-23 21:31:08 +00:00
Sebastián Ramírez
f8d4d04015 📝 Tweak and improve docs for Request Files (#4470) 2022-01-23 21:30:35 +00:00
github-actions
5a864d8261 📝 Update release notes 2022-01-23 19:14:47 +00:00
Sebastián Ramírez
1bf55200a9 Add support for declaring UploadFile parameters without explicit File() (#4469) 2022-01-23 19:14:13 +00:00
github-actions
59b1f353b3 📝 Update release notes 2022-01-23 17:43:36 +00:00
Sebastián Ramírez
569afb4378 Add support for tags with Enums (#4468) 2022-01-23 18:43:04 +01:00
Sebastián Ramírez
0f8349fcb7 📝 Update release notes 2022-01-23 18:03:42 +01:00
github-actions
6215fdd39e 📝 Update release notes 2022-01-23 16:34:52 +00:00
Vivek Sunder
94ca8c1e29 🐛 Prefer custom encoder over defaults if specified in jsonable_encoder (#2061)
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2022-01-23 17:34:18 +01:00
github-actions
f4963f05bf 📝 Update release notes 2022-01-23 16:32:35 +00:00
Sebastián Ramírez
d4608a00cf 🐛 Prefer custom encoder over defaults if specified in jsonable_encoder (#4467)
Co-authored-by: Vivek Sunder <sviveksunder@gmail.com>
2022-01-23 17:32:04 +01:00
github-actions
699b5ef841 📝 Update release notes 2022-01-23 16:14:28 +00:00
Sebastián Ramírez
3de0fb82bf 🐛 Fix docs dependencies cache, to get the latest Material for MkDocs (#4466) 2022-01-23 17:13:49 +01:00
github-actions
85518bc58b 📝 Update release notes 2022-01-23 15:55:36 +00:00
Mark
ca5d57ea79 Allow hiding from OpenAPI (and Swagger UI) Query, Cookie, Header, and Path parameters (#3144)
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2022-01-23 16:54:59 +01:00
github-actions
347e391271 📝 Update release notes 2022-01-23 14:56:44 +00:00
Sebastián Ramírez
a75d080124 🔧 Add sponsor Dropbase (#4465) 2022-01-23 15:56:14 +01:00
75 changed files with 5320 additions and 244 deletions

View File

@@ -1,6 +1,8 @@
name: Build Docs
on:
push:
branches:
- master
pull_request:
types: [opened, synchronize]
jobs:
@@ -20,7 +22,7 @@ jobs:
id: cache
with:
path: ${{ env.pythonLocation }}
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-docs
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-docs-v2
- name: Install Flit
if: steps.cache.outputs.cache-hit != 'true'
run: python3.7 -m pip install flit

View File

@@ -49,6 +49,8 @@ The key features are:
<a href="https://bit.ly/2QSouzH" target="_blank" title="Jina: build neural search-as-a-service for any kind of data in just minutes."><img src="https://fastapi.tiangolo.com/img/sponsors/jina.svg"></a>
<a href="https://cryptapi.io/" target="_blank" title="CryptAPI: Your easy to use, secure and privacy oriented payment gateway."><img src="https://fastapi.tiangolo.com/img/sponsors/cryptapi.svg"></a>
<a href="https://www.dropbase.io/careers" target="_blank" title="Dropbase - seamlessly collect, clean, and centralize data."><img src="https://fastapi.tiangolo.com/img/sponsors/dropbase.svg"></a>
<a href="https://striveworks.us/careers?utm_source=fastapi&utm_medium=sponsor_banner&utm_campaign=feb_march#openings" target="_blank" title="https://striveworks.us/careers"><img src="https://fastapi.tiangolo.com/img/sponsors/striveworks.png"></a>
<a href="https://www.deta.sh/?ref=fastapi" target="_blank" title="The launchpad for all your (team's) ideas"><img src="https://fastapi.tiangolo.com/img/sponsors/deta.svg"></a>
<a href="https://www.investsuite.com/jobs" target="_blank" title="Wealthtech jobs with FastAPI"><img src="https://fastapi.tiangolo.com/img/sponsors/investsuite.svg"></a>
<a href="https://www.vim.so/?utm_source=FastAPI" target="_blank" title="We help you master vim with interactive exercises"><img src="https://fastapi.tiangolo.com/img/sponsors/vimso.png"></a>

View File

@@ -1,12 +1,12 @@
maintainers:
- login: tiangolo
answers: 1230
prs: 269
answers: 1237
prs: 280
avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=5cad72c846b7aba2e960546af490edc7375dafc4&v=4
url: https://github.com/tiangolo
experts:
- login: Kludex
count: 316
count: 319
avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=3682d9b9b93bef272f379ab623dc031c8d71432e&v=4
url: https://github.com/Kludex
- login: dmontagu
@@ -14,7 +14,7 @@ experts:
avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&v=4
url: https://github.com/dmontagu
- login: ycd
count: 219
count: 221
avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=826f228edf0bab0d19ad1d5c4ba4df1047ccffef&v=4
url: https://github.com/ycd
- login: Mause
@@ -34,7 +34,7 @@ experts:
avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=81a84af39c89b898b0fbc5a04e8834f60f23e55a&v=4
url: https://github.com/ArcLightSlavik
- login: raphaelauv
count: 67
count: 68
avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4
url: https://github.com/raphaelauv
- login: falkben
@@ -57,18 +57,22 @@ experts:
count: 38
avatarUrl: https://avatars.githubusercontent.com/u/11836741?u=8bd5ef7e62fe6a82055e33c4c0e0a7879ff8cfb6&v=4
url: https://github.com/includeamin
- login: STeveShary
count: 37
avatarUrl: https://avatars.githubusercontent.com/u/5167622?u=de8f597c81d6336fcebc37b32dfd61a3f877160c&v=4
url: https://github.com/STeveShary
- login: prostomarkeloff
count: 33
avatarUrl: https://avatars.githubusercontent.com/u/28061158?u=72309cc1f2e04e40fa38b29969cb4e9d3f722e7b&v=4
url: https://github.com/prostomarkeloff
- login: STeveShary
count: 32
avatarUrl: https://avatars.githubusercontent.com/u/5167622?u=de8f597c81d6336fcebc37b32dfd61a3f877160c&v=4
url: https://github.com/STeveShary
- login: krishnardt
count: 31
avatarUrl: https://avatars.githubusercontent.com/u/31960541?u=47f4829c77f4962ab437ffb7995951e41eeebe9b&v=4
url: https://github.com/krishnardt
- login: adriangb
count: 30
avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=81f0262df34e1460ca546fbd0c211169c2478532&v=4
url: https://github.com/adriangb
- login: wshayes
count: 29
avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4
@@ -77,10 +81,10 @@ experts:
count: 29
avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=85c025e3fcc7bd79a5665c63ee87cdf8aae13374&v=4
url: https://github.com/frankie567
- login: adriangb
count: 28
avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=81f0262df34e1460ca546fbd0c211169c2478532&v=4
url: https://github.com/adriangb
- login: chbndrhnns
count: 25
avatarUrl: https://avatars.githubusercontent.com/u/7534547?v=4
url: https://github.com/chbndrhnns
- login: ghandic
count: 25
avatarUrl: https://avatars.githubusercontent.com/u/23500353?u=e2e1d736f924d9be81e8bfc565b6d8836ba99773&v=4
@@ -89,18 +93,14 @@ experts:
count: 25
avatarUrl: https://avatars.githubusercontent.com/u/43723790?u=9bcce836bbce55835291c5b2ac93a4e311f4b3c3&v=4
url: https://github.com/dbanty
- login: chbndrhnns
count: 24
avatarUrl: https://avatars.githubusercontent.com/u/7534547?v=4
url: https://github.com/chbndrhnns
- login: panla
count: 25
avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4
url: https://github.com/panla
- login: SirTelemak
count: 24
avatarUrl: https://avatars.githubusercontent.com/u/9435877?u=719327b7d2c4c62212456d771bfa7c6b8dbb9eac&v=4
url: https://github.com/SirTelemak
- login: panla
count: 23
avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4
url: https://github.com/panla
- login: acnebs
count: 22
avatarUrl: https://avatars.githubusercontent.com/u/9054108?u=c27e50269f1ef8ea950cc6f0268c8ec5cebbe9c9&v=4
@@ -129,22 +129,34 @@ experts:
count: 17
avatarUrl: https://avatars.githubusercontent.com/u/28262306?u=66ee21316275ef356081c2efc4ed7a4572e690dc&v=4
url: https://github.com/nkhitrov
- login: acidjunk
count: 16
avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4
url: https://github.com/acidjunk
- login: waynerv
count: 16
avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4
url: https://github.com/waynerv
- login: acidjunk
count: 15
avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4
url: https://github.com/acidjunk
- login: dstlny
count: 14
count: 16
avatarUrl: https://avatars.githubusercontent.com/u/41964673?u=9f2174f9d61c15c6e3a4c9e3aeee66f711ce311f&v=4
url: https://github.com/dstlny
- login: jgould22
count: 14
avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4
url: https://github.com/jgould22
- login: harunyasar
count: 14
avatarUrl: https://avatars.githubusercontent.com/u/1765494?u=5b1ab7c582db4b4016fa31affe977d10af108ad4&v=4
url: https://github.com/harunyasar
- login: haizaar
count: 13
avatarUrl: https://avatars.githubusercontent.com/u/58201?u=4f1f9843d69433ca0d380d95146cfe119e5fdac4&v=4
url: https://github.com/haizaar
- login: hellocoldworld
count: 12
avatarUrl: https://avatars.githubusercontent.com/u/47581948?v=4
url: https://github.com/hellocoldworld
- login: David-Lor
count: 12
avatarUrl: https://avatars.githubusercontent.com/u/17401854?u=474680c02b94cba810cb9032fb7eb787d9cc9d22&v=4
@@ -173,39 +185,47 @@ experts:
count: 10
avatarUrl: https://avatars.githubusercontent.com/u/20441825?u=ee1e59446b98f8ec2363caeda4c17164d0d9cc7d&v=4
url: https://github.com/stefanondisponibile
- login: hellocoldworld
count: 10
avatarUrl: https://avatars.githubusercontent.com/u/47581948?v=4
url: https://github.com/hellocoldworld
- login: oligond
count: 10
avatarUrl: https://avatars.githubusercontent.com/u/2858306?u=1bb1182a5944e93624b7fb26585f22c8f7a9d76e&v=4
url: https://github.com/oligond
last_month_active:
- login: insomnes
count: 10
avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4
url: https://github.com/insomnes
- login: raphaelauv
count: 6
avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4
url: https://github.com/raphaelauv
- login: jgould22
count: 4
avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4
url: https://github.com/jgould22
- login: harunyasar
count: 4
count: 10
avatarUrl: https://avatars.githubusercontent.com/u/1765494?u=5b1ab7c582db4b4016fa31affe977d10af108ad4&v=4
url: https://github.com/harunyasar
- login: jgould22
count: 10
avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4
url: https://github.com/jgould22
- login: rafsaf
count: 9
avatarUrl: https://avatars.githubusercontent.com/u/51059348?u=be9f06b8ced2d2b677297decc781fa8ce4f7ddbd&v=4
url: https://github.com/rafsaf
- login: STeveShary
count: 5
avatarUrl: https://avatars.githubusercontent.com/u/5167622?u=de8f597c81d6336fcebc37b32dfd61a3f877160c&v=4
url: https://github.com/STeveShary
- login: ahnaf-zamil
count: 3
avatarUrl: https://avatars.githubusercontent.com/u/57180217?u=849128b146771ace47beca5b5ff68eb82905dd6d&v=4
url: https://github.com/ahnaf-zamil
- login: lucastosetto
count: 3
avatarUrl: https://avatars.githubusercontent.com/u/89307132?u=56326696423df7126c9e7c702ee58f294db69a2a&v=4
url: https://github.com/lucastosetto
- login: blokje
count: 3
avatarUrl: https://avatars.githubusercontent.com/u/851418?v=4
url: https://github.com/blokje
- login: MatthijsKok
count: 3
avatarUrl: https://avatars.githubusercontent.com/u/7658129?u=1243e32d57e13abc45e3f5235ed5b9197e0d2b41&v=4
url: https://github.com/MatthijsKok
- login: Kludex
count: 3
avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=3682d9b9b93bef272f379ab623dc031c8d71432e&v=4
url: https://github.com/Kludex
- login: panla
count: 3
avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4
url: https://github.com/panla
top_contributors:
- login: waynerv
count: 25
@@ -219,14 +239,14 @@ top_contributors:
count: 16
avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&v=4
url: https://github.com/dmontagu
- login: jaystone776
count: 15
avatarUrl: https://avatars.githubusercontent.com/u/11191137?u=299205a95e9b6817a43144a48b643346a5aac5cc&v=4
url: https://github.com/jaystone776
- login: euri10
count: 13
avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4
url: https://github.com/euri10
- login: jaystone776
count: 13
avatarUrl: https://avatars.githubusercontent.com/u/11191137?u=299205a95e9b6817a43144a48b643346a5aac5cc&v=4
url: https://github.com/jaystone776
- login: mariacamilagl
count: 12
avatarUrl: https://avatars.githubusercontent.com/u/11489395?u=4adb6986bf3debfc2b8216ae701f2bd47d73da7d&v=4
@@ -285,7 +305,7 @@ top_contributors:
url: https://github.com/NinaHwang
top_reviewers:
- login: Kludex
count: 91
count: 93
avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=3682d9b9b93bef272f379ab623dc031c8d71432e&v=4
url: https://github.com/Kludex
- login: waynerv
@@ -301,7 +321,7 @@ top_reviewers:
avatarUrl: https://avatars.githubusercontent.com/u/41147016?u=55010621aece725aa702270b54fed829b6a1fe60&v=4
url: https://github.com/tokusumi
- login: ycd
count: 44
count: 45
avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=826f228edf0bab0d19ad1d5c4ba4df1047ccffef&v=4
url: https://github.com/ycd
- login: AdrianDeAnda
@@ -312,12 +332,16 @@ top_reviewers:
count: 31
avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=81a84af39c89b898b0fbc5a04e8834f60f23e55a&v=4
url: https://github.com/ArcLightSlavik
- login: cikay
count: 24
avatarUrl: https://avatars.githubusercontent.com/u/24587499?u=e772190a051ab0eaa9c8542fcff1892471638f2b&v=4
url: https://github.com/cikay
- login: dmontagu
count: 23
avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&v=4
url: https://github.com/dmontagu
- login: cassiobotaro
count: 22
count: 23
avatarUrl: https://avatars.githubusercontent.com/u/3127847?u=b0a652331da17efeb85cd6e3a4969182e5004804&v=4
url: https://github.com/cassiobotaro
- login: komtaki
@@ -336,6 +360,10 @@ top_reviewers:
count: 16
avatarUrl: https://avatars.githubusercontent.com/u/21978760?v=4
url: https://github.com/yanever
- login: lsglucas
count: 16
avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4
url: https://github.com/lsglucas
- login: SwftAlpc
count: 16
avatarUrl: https://avatars.githubusercontent.com/u/52768429?u=6a3aa15277406520ad37f6236e89466ed44bc5b8&v=4
@@ -356,26 +384,22 @@ top_reviewers:
count: 15
avatarUrl: https://avatars.githubusercontent.com/u/63476957?u=6c86e59b48e0394d4db230f37fc9ad4d7e2c27c7&v=4
url: https://github.com/delhi09
- login: lsglucas
count: 14
avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4
url: https://github.com/lsglucas
- login: rjNemo
count: 13
count: 14
avatarUrl: https://avatars.githubusercontent.com/u/56785022?u=d5c3a02567c8649e146fcfc51b6060ccaf8adef8&v=4
url: https://github.com/rjNemo
- login: RunningIkkyu
count: 12
avatarUrl: https://avatars.githubusercontent.com/u/31848542?u=706e1ee3f248245f2d68b976d149d06fd5a2010d&v=4
url: https://github.com/RunningIkkyu
- login: yezz123
count: 12
avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=636b4f79645176df4527dd45c12d5dbb5a4193cf&v=4
url: https://github.com/yezz123
- login: sh0nk
count: 12
avatarUrl: https://avatars.githubusercontent.com/u/6478810?u=af15d724875cec682ed8088a86d36b2798f981c0&v=4
url: https://github.com/sh0nk
- login: yezz123
count: 11
avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=636b4f79645176df4527dd45c12d5dbb5a4193cf&v=4
url: https://github.com/yezz123
- login: mariacamilagl
count: 10
avatarUrl: https://avatars.githubusercontent.com/u/11489395?u=4adb6986bf3debfc2b8216ae701f2bd47d73da7d&v=4
@@ -400,6 +424,10 @@ top_reviewers:
count: 9
avatarUrl: https://avatars.githubusercontent.com/u/49435654?v=4
url: https://github.com/kty4119
- login: zy7y
count: 9
avatarUrl: https://avatars.githubusercontent.com/u/67154681?u=5d634834cc514028ea3f9115f7030b99a1f4d5a4&v=4
url: https://github.com/zy7y
- login: bezaca
count: 9
avatarUrl: https://avatars.githubusercontent.com/u/69092910?u=4ac58eab99bd37d663f3d23551df96d4fbdbf760&v=4
@@ -444,39 +472,31 @@ top_reviewers:
count: 7
avatarUrl: https://avatars.githubusercontent.com/u/34248814?v=4
url: https://github.com/krocdort
- login: dimaqq
count: 7
avatarUrl: https://avatars.githubusercontent.com/u/662249?v=4
url: https://github.com/dimaqq
- login: jovicon
count: 6
avatarUrl: https://avatars.githubusercontent.com/u/21287303?u=b049eac3e51a4c0473c2efe66b4d28a7d8f2b572&v=4
url: https://github.com/jovicon
- login: NinaHwang
count: 6
avatarUrl: https://avatars.githubusercontent.com/u/79563565?u=1741703bd6c8f491503354b363a86e879b4c1cab&v=4
url: https://github.com/NinaHwang
- login: diogoduartec
count: 5
avatarUrl: https://avatars.githubusercontent.com/u/31852339?u=b50fc11c531e9b77922e19edfc9e7233d4d7b92e&v=4
url: https://github.com/diogoduartec
- login: nimctl
- login: n25a
count: 5
avatarUrl: https://avatars.githubusercontent.com/u/49960770?u=e39b11d47188744ee07b2a1c7ce1a1bdf3c80760&v=4
url: https://github.com/nimctl
avatarUrl: https://avatars.githubusercontent.com/u/49960770?u=eb3c95338741c78fff7d9d5d7ace9617e53eee4a&v=4
url: https://github.com/n25a
- login: izaguerreiro
count: 5
avatarUrl: https://avatars.githubusercontent.com/u/2241504?v=4
url: https://github.com/izaguerreiro
- login: israteneda
count: 5
avatarUrl: https://avatars.githubusercontent.com/u/20668624?u=d7b2961d330aca65fbce5bdb26a0800a3d23ed2d&v=4
url: https://github.com/israteneda
- login: juntatalor
count: 5
avatarUrl: https://avatars.githubusercontent.com/u/8134632?v=4
url: https://github.com/juntatalor
- login: SnkSynthesis
count: 5
avatarUrl: https://avatars.githubusercontent.com/u/63564282?u=0078826509dbecb2fdb543f4e881c9cd06157893&v=4
url: https://github.com/SnkSynthesis
- login: anthonycepeda
count: 5
avatarUrl: https://avatars.githubusercontent.com/u/72019805?u=892f700c79f9732211bd5221bf16eec32356a732&v=4
url: https://github.com/anthonycepeda
- login: oandersonmagalhaes
count: 5
avatarUrl: https://avatars.githubusercontent.com/u/83456692?v=4
url: https://github.com/oandersonmagalhaes
- login: qysfblog
count: 5
avatarUrl: https://avatars.githubusercontent.com/u/52229895?v=4
url: https://github.com/qysfblog

View File

@@ -5,6 +5,12 @@ gold:
- url: https://cryptapi.io/
title: "CryptAPI: Your easy to use, secure and privacy oriented payment gateway."
img: https://fastapi.tiangolo.com/img/sponsors/cryptapi.svg
- url: https://www.dropbase.io/careers
title: Dropbase - seamlessly collect, clean, and centralize data.
img: https://fastapi.tiangolo.com/img/sponsors/dropbase.svg
- url: https://striveworks.us/careers?utm_source=fastapi&utm_medium=sponsor_banner&utm_campaign=feb_march#openings
title: https://striveworks.us/careers
img: https://fastapi.tiangolo.com/img/sponsors/striveworks.png
silver:
- url: https://www.deta.sh/?ref=fastapi
title: The launchpad for all your (team's) ideas

View File

@@ -7,3 +7,5 @@ logins:
- koaning
- deepset-ai
- cryptapi
- DropbaseHQ
- Striveworks

View File

@@ -0,0 +1,267 @@
# Generate Clients
As **FastAPI** is based on the OpenAPI specification, you get automatic compatibility with many tools, including the automatic API docs (provided by Swagger UI).
One particular advantage that is not necessarily obvious is that you can **generate clients** (sometimes called <abbr title="Software Development Kits">**SDKs**</abbr> ) for your API, for many different **programming languages**.
## OpenAPI Client Generators
There are many tools to generate clients from **OpenAPI**.
A common tool is <a href="https://openapi-generator.tech/" class="external-link" target="_blank">OpenAPI Generator</a>.
If you are building a **frontend**, a very interesting alternative is <a href="https://github.com/ferdikoomen/openapi-typescript-codegen" class="external-link" target="_blank">openapi-typescript-codegen</a>.
## Generate a TypeScript Frontend Client
Let's start with a simple FastAPI application:
=== "Python 3.6 and above"
```Python hl_lines="9-11 14-15 18 19 23"
{!> ../../../docs_src/generate_clients/tutorial001.py!}
```
=== "Python 3.9 and above"
```Python hl_lines="7-9 12-13 16-17 21"
{!> ../../../docs_src/generate_clients/tutorial001_py39.py!}
```
Notice that the *path operations* define the models they use for request payload and response payload, using the models `Item` and `ResponseMessage`.
### API Docs
If you go to the API docs, you will see that it has the **schemas** for the data to be sent in requests and received in responses:
<img src="/img/tutorial/generate-clients/image01.png">
You can see those schemas because they were declared with the models in the app.
That information is available in the app's **OpenAPI schema**, and then shown in the API docs (by Swagger UI).
And that same information from the models that is included in OpenAPI is what can be used to **generate the client code**.
### Generate a TypeScript Client
Now that we have the app with the models, we can generate the client code for the frontend.
#### Install `openapi-typescript-codegen`
You can install `openapi-typescript-codegen` in your frontend code with:
<div class="termy">
```console
$ npm install openapi-typescript-codegen --save-dev
---> 100%
```
</div>
#### Generate Client Code
To generate the client code you can use the command line application `openapi` that would now be installed.
Because it is installed in the local project, you probably wouldn't be able to call that command directly, but you would put it on your `package.json` file.
It could look like this:
```JSON hl_lines="7"
{
"name": "frontend-app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"generate-client": "openapi --input http://localhost:8000/openapi.json --output ./src/client --client axios"
},
"author": "",
"license": "",
"devDependencies": {
"openapi-typescript-codegen": "^0.20.1",
"typescript": "^4.6.2"
}
}
```
After having that NPM `generate-client` script there, you can run it with:
<div class="termy">
```console
$ npm run generate-client
frontend-app@1.0.0 generate-client /home/user/code/frontend-app
> openapi --input http://localhost:8000/openapi.json --output ./src/client --client axios
```
</div>
That command will generate code in `./src/client` and will use `axios` (the frontend HTTP library) internally.
### Try Out the Client Code
Now you can import and use the client code, it could look like this, notice that you get autocompletion for the methods:
<img src="/img/tutorial/generate-clients/image02.png">
You will also get autocompletion for the payload to send:
<img src="/img/tutorial/generate-clients/image03.png">
!!! tip
Notice the autocompletion for `name` and `price`, that was defined in the FastAPI application, in the `Item` model.
You will have inline errors for the data that you send:
<img src="/img/tutorial/generate-clients/image04.png">
The response object will also have autocompletion:
<img src="/img/tutorial/generate-clients/image05.png">
## FastAPI App with Tags
In many cases your FastAPI app will be bigger, and you will probably use tags to separate different groups of *path operations*.
For example, you could have a section for **items** and another section for **users**, and they could be separated by tags:
=== "Python 3.6 and above"
```Python hl_lines="23 28 36"
{!> ../../../docs_src/generate_clients/tutorial002.py!}
```
=== "Python 3.9 and above"
```Python hl_lines="21 26 34"
{!> ../../../docs_src/generate_clients/tutorial002_py39.py!}
```
### Generate a TypeScript Client with Tags
If you generate a client for a FastAPI app using tags, it will normally also separate the client code based on the tags.
This way you will be able to have things ordered and grouped correctly for the client code:
<img src="/img/tutorial/generate-clients/image06.png">
In this case you have:
* `ItemsService`
* `UsersService`
### Client Method Names
Right now the generated method names like `createItemItemsPost` don't look very clean:
```TypeScript
ItemsService.createItemItemsPost({name: "Plumbus", price: 5})
```
...that's because the client generator uses the OpenAPI internal **operation ID** for each *path operation*.
OpenAPI requires that each operation ID is unique across all the *path operations*, so FastAPI uses the **function name**, the **path**, and the **HTTP method/operation** to generate that operation ID, because that way it can make sure that the operation IDs are unique.
But I'll show you how to improve that next. 🤓
## Custom Operation IDs and Better Method Names
You can **modify** the way these operation IDs are **generated** to make them simpler and have **simpler method names** in the clients.
In this case you will have to ensure that each operation ID is **unique** in some other way.
For example, you could make sure that each *path operation* has a tag, and then generate the operation ID based on the **tag** and the *path operation* **name** (the function name).
### Custom Generate Unique ID Function
FastAPI uses a **unique ID** for each *path operation*, it is used for the **operation ID** and also for the names of any needed custom models, for requests or responses.
You can customize that function. It takes an `APIRoute` and outputs a string.
For example, here it is using the first tag (you will probably have only one tag) and the *path operation* name (the function name).
You can then pass that custom function to **FastAPI** as the `generate_unique_id_function` parameter:
=== "Python 3.6 and above"
```Python hl_lines="8-9 12"
{!> ../../../docs_src/generate_clients/tutorial003.py!}
```
=== "Python 3.9 and above"
```Python hl_lines="6-7 10"
{!> ../../../docs_src/generate_clients/tutorial003_py39.py!}
```
### Generate a TypeScript Client with Custom Operation IDs
Now if you generate the client again, you will see that it has the improved method names:
<img src="/img/tutorial/generate-clients/image07.png">
As you see, the method names now have the tag and then the function name, now they don't include information from the URL path and the HTTP operation.
### Preprocess the OpenAPI Specification for the Client Generator
The generated code still has some **duplicated information**.
We already know that this method is related to the **items** because that word is in the `ItemsService` (taken from the tag), but we still have the tag name prefixed in the method name too. 😕
We will probably still want to keep it for OpenAPI in general, as that will ensure that the operation IDs are **unique**.
But for the generated client we could **modify** the OpenAPI operation IDs right before generating the clients, just to make those method names nicer and **cleaner**.
We could download the OpenAPI JSON to a file `openapi.json` and then we could **remove that prefixed tag** with a script like this:
```Python
{!../../../docs_src/generate_clients/tutorial004.py!}
```
With that, the operation IDs would be renamed from things like `items-get_items` to just `get_items`, that way the client generator can generate simpler method names.
### Generate a TypeScript Client with the Preprocessed OpenAPI
Now as the end result is in a file `openapi.json`, you would modify the `package.json` to use that local file, for example:
```JSON hl_lines="7"
{
"name": "frontend-app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"generate-client": "openapi --input ./openapi.json --output ./src/client --client axios"
},
"author": "",
"license": "",
"devDependencies": {
"openapi-typescript-codegen": "^0.20.1",
"typescript": "^4.6.2"
}
}
```
After generating the new client, you would now have **clean method names**, with all the **autocompletion**, **inline errors**, etc:
<img src="/img/tutorial/generate-clients/image08.png">
## Benefits
When using the automatically generated clients you would **autocompletion** for:
* Methods.
* Request payloads in the body, query parameters, etc.
* Response payloads.
You would also have **inline errors** for everything.
And whenever you update the backend code, and **regenerate** the frontend, it would have any new *path operations* available as methods, the old ones removed, and any other change would be reflected on the generated code. 🤓
This also means that if something changed it will be **reflected** on the client code automatically. And if you **build** the client it will error out if you have any **mismatch** in the data used.
So, you would **detect many errors** very early in the development cycle instead of having to wait for the errors to show up to your final users in production and then trying to debug where the problem is. ✨

View File

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -0,0 +1,124 @@
<?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"
width="240.00002"
height="100"
viewBox="0 0 240.00002 100"
fill="none"
version="1.1"
id="svg35">
<metadata
id="metadata39">
<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>
<path
d="M 0,0 H 240.00002 V 100 H 0 Z"
fill="#ffffff"
id="path2"
style="stroke-width:0.0406002" />
<g
id="g908"
transform="translate(0,8.4984581)">
<path
d="m 89.098346,53.723081 c 6.816739,0 10.858339,-4.216615 10.858339,-11.208001 0,-6.969268 -4.0416,-11.164025 -10.705478,-11.164025 h -7.733908 v 22.372026 z m -3.52837,-3.506585 V 34.857603 h 3.451939 c 4.544493,0 6.914955,2.5344 6.914955,7.657477 0,5.145232 -2.370462,7.701416 -7.034955,7.701416 z m 18.062774,3.506585 h 3.95446 v -9.86437 c 0,-2.130092 1.60578,-3.637661 3.77945,-3.637661 0.66646,0 1.49649,0.120369 1.83507,0.229661 v -3.637736 c -0.36037,-0.06554 -0.98289,-0.109255 -1.42006,-0.109255 -1.92258,0 -3.52837,1.092222 -4.14018,3.03696 h -0.17465 v -2.796554 h -3.83409 z m 18.82449,0.327877 c 4.91557,0 8.04,-3.463015 8.04,-8.651816 0,-5.199877 -3.12443,-8.673564 -8.04,-8.673564 -4.91594,0 -8.04,3.473687 -8.04,8.673564 0,5.188801 3.12406,8.651816 8.04,8.651816 z m 0.0218,-3.168 c -2.72013,0 -4.05268,-2.425108 -4.05268,-5.494893 0,-3.069415 1.33255,-5.527385 4.05268,-5.527385 2.67618,0 4.0091,2.45797 4.0091,5.527385 0,3.069785 -1.33292,5.494893 -4.0091,5.494893 z m 11.37452,9.132185 h 3.95446 v -8.935754 h 0.16357 c 0.62289,1.223631 1.92259,2.938708 4.80665,2.938708 3.95446,0 6.91458,-3.135139 6.91458,-8.662524 0,-5.593108 -3.04763,-8.629995 -6.92566,-8.629995 -2.96012,0 -4.19446,1.780764 -4.79557,2.993318 h -0.22929 v -2.77477 h -3.88874 z m 3.87766,-14.681724 c 0,-3.255139 1.39828,-5.363446 3.94376,-5.363446 2.63261,0 3.98695,2.239384 3.98695,5.363446 0,3.146216 -1.37612,5.440247 -3.98695,5.440247 -2.52333,0 -3.94376,-2.184739 -3.94376,-5.440247 z m 15.52283,8.389662 h 3.88911 v -2.643692 h 0.22929 c 0.62253,1.223631 1.92259,2.938708 4.80665,2.938708 3.95446,0 6.91459,-3.135139 6.91459,-8.662524 0,-5.593108 -3.04764,-8.629995 -6.92567,-8.629995 -2.96049,0 -4.19483,1.780764 -4.79557,2.993318 h -0.16393 v -8.367841 h -3.95447 z m 3.87803,-8.389662 c 0,-3.255139 1.39828,-5.363446 3.94339,-5.363446 2.63298,0 3.98732,2.239384 3.98732,5.363446 0,3.146216 -1.37649,5.440247 -3.98732,5.440247 -2.52332,0 -3.94339,-2.184739 -3.94339,-5.440247 z m 20.15188,8.728247 c 2.63262,0 4.20554,-1.234339 4.92665,-2.643692 h 0.13108 v 2.305107 h 3.8016 V 42.493296 c 0,-4.434831 -3.61588,-5.767718 -6.81674,-5.767718 -3.52837,0 -6.23742,1.572887 -7.11139,4.631595 l 3.69231,0.524307 c 0.39323,-1.14683 1.50757,-2.130092 3.44086,-2.130092 1.83545,0 2.8405,0.939692 2.8405,2.589046 v 0.06572 c 0,1.135754 -1.19077,1.1904 -4.15127,1.507201 -3.25514,0.349661 -6.36849,1.321846 -6.36849,5.101661 0,3.298708 2.41403,5.046647 5.61489,5.046647 z m 1.02683,-2.905846 c -1.64972,0 -2.82941,-0.7536 -2.82941,-2.206524 0,-1.518277 1.32184,-2.151877 3.09157,-2.403323 1.03754,-0.141785 3.11335,-0.403938 3.62658,-0.819323 v 1.977231 c 0,1.868308 -1.50757,3.451939 -3.88874,3.451939 z m 25.07004,-9.776863 c -0.5461,-2.840123 -2.81834,-4.653379 -6.75065,-4.653379 -4.04197,0 -6.79495,1.988271 -6.78388,5.090549 -0.0111,2.446892 1.4965,4.063754 4.71914,4.729846 l 2.86191,0.601108 c 1.54043,0.338584 2.26117,0.961108 2.26117,1.911508 0,1.146831 -1.24505,2.010092 -3.12406,2.010092 -1.8133,0 -2.99299,-0.786461 -3.33194,-2.294031 l -3.85588,0.371446 c 0.49145,3.080493 3.08049,4.904862 7.19889,4.904862 4.19447,0 7.15496,-2.174031 7.16604,-5.352739 -0.0111,-2.392246 -1.55114,-3.856246 -4.71914,-4.544492 l -2.86228,-0.611816 c -1.704,-0.382154 -2.38117,-0.972184 -2.37046,-1.944369 -0.0107,-1.136123 1.24541,-1.922585 2.89477,-1.922585 1.82437,0 2.78584,0.993969 3.09157,2.0976 z m 11.0496,12.672001 c 3.91089,0 6.59815,-1.911877 7.29711,-4.828431 l -3.69231,-0.415015 c -0.53539,1.420061 -1.84616,2.162585 -3.55016,2.162585 -2.55618,0 -4.24947,-1.682216 -4.28233,-4.555201 h 11.68873 v -1.212554 c 0,-5.887754 -3.53944,-8.476764 -7.66855,-8.476764 -4.80665,0 -7.94179,3.528333 -7.94179,8.706426 0,5.265231 3.09157,8.618954 8.1493,8.618954 z m -4.21662,-10.30117 c 0.12,-2.141169 1.704,-3.943754 4.06376,-3.943754 2.27224,0 3.8016,1.660431 3.82338,3.943754 z"
fill="#161616"
id="path4"
style="stroke-width:0.0369231" />
<g
id="g898">
<g
filter="url(#filter0_d_2_165)"
id="g8"
transform="scale(0.03692308)">
<rect
x="454"
y="401"
width="1438"
height="1438"
rx="100"
fill="#ffffff"
id="rect6" />
</g>
<rect
x="21.821541"
y="19.864616"
width="42.978466"
height="42.978466"
rx="2.9887683"
fill="#161616"
id="rect10"
style="stroke-width:0.0369231" />
<path
d="m 41.202465,54.873605 h -7.07077 c -2.332837,0 -4.224,-1.891201 -4.224,-4.224001 V 32.064003 c 0,-2.332837 1.891163,-4.224001 4.224,-4.224001 h 7.07077 z"
fill="#ffffff"
id="path12"
style="stroke-width:0.0369231" />
<path
d="m 45.437542,29.205086 c 0,-0.583201 0.472616,-1.056001 1.056,-1.056001 h 7.07077 c 1.749785,0 3.168,1.418364 3.168,3.168001 v 7.919963 c 0,0.583385 -0.472615,1.056001 -1.056,1.056001 h -7.070769 c -1.749785,0 -3.168001,-1.418216 -3.168001,-3.168001 z"
fill="#ffffff"
id="path14"
style="stroke-width:0.0369231" />
<path
d="m 45.437542,43.77785 c 0,-0.583016 0.472616,-1.056 1.056,-1.056 h 7.07077 c 1.749785,0 3.168,1.418215 3.168,3.168 v 7.920001 c 0,0.583384 -0.472615,1.056 -1.056,1.056 h -7.070769 c -1.749785,0 -3.168001,-1.418216 -3.168001,-3.168001 z"
fill="#ffffff"
id="path16"
style="stroke-width:0.0369231" />
</g>
</g>
<defs
id="defs33">
<filter
id="filter0_d_2_165"
x="408"
y="355"
width="1538"
height="1538"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB">
<feFlood
flood-opacity="0"
result="BackgroundImageFix"
id="feFlood18" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
id="feColorMatrix20" />
<feOffset
dx="4"
dy="4"
id="feOffset22" />
<feGaussianBlur
stdDeviation="25"
id="feGaussianBlur24" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0"
id="feColorMatrix26" />
<feBlend
mode="normal"
in2="BackgroundImageFix"
result="effect1_dropShadow_2_165"
id="feBlend28" />
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_2_165"
result="shape"
id="feBlend30" />
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -3,6 +3,111 @@
## Latest Changes
## 0.75.0
### Features
* ✨ Add support for custom `generate_unique_id_function` and docs for generating clients. New docs: [Advanced - Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/). PR [#4650](https://github.com/tiangolo/fastapi/pull/4650) by [@tiangolo](https://github.com/tiangolo).
## 0.74.1
### Features
* ✨ Include route in scope to allow middleware and other tools to extract its information. PR [#4603](https://github.com/tiangolo/fastapi/pull/4603) by [@tiangolo](https://github.com/tiangolo).
## 0.74.0
### Breaking Changes
* ✨ Update internal `AsyncExitStack` to fix context for dependencies with `yield`. PR [#4575](https://github.com/tiangolo/fastapi/pull/4575) by [@tiangolo](https://github.com/tiangolo).
Dependencies with `yield` can now catch `HTTPException` and custom exceptions. For example:
```Python
async def get_database():
with Session() as session:
try:
yield session
except HTTPException:
session.rollback()
raise
finally:
session.close()
```
After the dependency with `yield` handles the exception (or not) the exception is raised again. So that any exception handlers can catch it, or ultimately the default internal `ServerErrorMiddleware`.
If you depended on exceptions not being received by dependencies with `yield`, and receiving an exception breaks the code after `yield`, you can use a block with `try` and `finally`:
```Python
async def do_something():
try:
yield something
finally:
some_cleanup()
```
...that way the `finally` block is run regardless of any exception that might happen.
### Features
* The same PR [#4575](https://github.com/tiangolo/fastapi/pull/4575) from above also fixes the `contextvars` context for the code before and after `yield`. This was the main objective of that PR.
This means that now, if you set a value in a context variable before `yield`, the value would still be available after `yield` (as you would intuitively expect). And it also means that you can reset the context variable with a token afterwards.
For example, this works correctly now:
```Python
from contextvars import ContextVar
from typing import Any, Dict, Optional
legacy_request_state_context_var: ContextVar[Optional[Dict[str, Any]]] = ContextVar(
"legacy_request_state_context_var", default=None
)
async def set_up_request_state_dependency():
request_state = {"user": "deadpond"}
contextvar_token = legacy_request_state_context_var.set(request_state)
yield request_state
legacy_request_state_context_var.reset(contextvar_token)
```
...before this change it would raise an error when resetting the context variable, because the `contextvars` context was different, because of the way it was implemented.
**Note**: You probably don't need `contextvars`, and you should probably avoid using them. But they are powerful and useful in some advanced scenarios, for example, migrating from code that used Flask's `g` semi-global variable.
**Technical Details**: If you want to know more of the technical details you can check out the PR description [#4575](https://github.com/tiangolo/fastapi/pull/4575).
### Internal
* 🔧 Add Striveworks sponsor. PR [#4596](https://github.com/tiangolo/fastapi/pull/4596) by [@tiangolo](https://github.com/tiangolo).
* 💚 Only build docs on push when on master to avoid duplicate runs from PRs. PR [#4564](https://github.com/tiangolo/fastapi/pull/4564) by [@tiangolo](https://github.com/tiangolo).
* 👥 Update FastAPI People. PR [#4502](https://github.com/tiangolo/fastapi/pull/4502) by [@github-actions[bot]](https://github.com/apps/github-actions).
## 0.73.0
### Features
* ✨ Add support for declaring `UploadFile` parameters without explicit `File()`. PR [#4469](https://github.com/tiangolo/fastapi/pull/4469) by [@tiangolo](https://github.com/tiangolo). New docs: [Request Files - File Parameters with UploadFile](https://fastapi.tiangolo.com/tutorial/request-files/#file-parameters-with-uploadfile).
* ✨ Add support for tags with Enums. PR [#4468](https://github.com/tiangolo/fastapi/pull/4468) by [@tiangolo](https://github.com/tiangolo). New docs: [Path Operation Configuration - Tags with Enums](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#tags-with-enums).
* ✨ Allow hiding from OpenAPI (and Swagger UI) `Query`, `Cookie`, `Header`, and `Path` parameters. PR [#3144](https://github.com/tiangolo/fastapi/pull/3144) by [@astraldawn](https://github.com/astraldawn). New docs: [Query Parameters and String Validations - Exclude from OpenAPI](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi).
### Docs
* 📝 Tweak and improve docs for Request Files. PR [#4470](https://github.com/tiangolo/fastapi/pull/4470) by [@tiangolo](https://github.com/tiangolo).
### Fixes
* 🐛 Fix bug preventing to use OpenAPI when using tuples. PR [#3874](https://github.com/tiangolo/fastapi/pull/3874) by [@victorbenichoux](https://github.com/victorbenichoux).
* 🐛 Prefer custom encoder over defaults if specified in `jsonable_encoder`. PR [#2061](https://github.com/tiangolo/fastapi/pull/2061) by [@viveksunder](https://github.com/viveksunder).
* 💚 Duplicate PR to trigger CI. PR [#4467](https://github.com/tiangolo/fastapi/pull/4467) by [@tiangolo](https://github.com/tiangolo).
### Internal
* 🐛 Fix docs dependencies cache, to get the latest Material for MkDocs. PR [#4466](https://github.com/tiangolo/fastapi/pull/4466) by [@tiangolo](https://github.com/tiangolo).
* 🔧 Add sponsor Dropbase. PR [#4465](https://github.com/tiangolo/fastapi/pull/4465) by [@tiangolo](https://github.com/tiangolo).
## 0.72.0
### Features

View File

@@ -99,7 +99,7 @@ You saw that you can use dependencies with `yield` and have `try` blocks that ca
It might be tempting to raise an `HTTPException` or similar in the exit code, after the `yield`. But **it won't work**.
The exit code in dependencies with `yield` is executed *after* [Exception Handlers](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}. There's nothing catching exceptions thrown by your dependencies in the exit code (after the `yield`).
The exit code in dependencies with `yield` is executed *after* the response is sent, so [Exception Handlers](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} will have already run. There's nothing catching exceptions thrown by your dependencies in the exit code (after the `yield`).
So, if you raise an `HTTPException` after the `yield`, the default (or any custom) exception handler that catches `HTTPException`s and returns an HTTP 400 response won't be there to catch that exception anymore.
@@ -138,9 +138,11 @@ participant tasks as Background tasks
end
dep ->> operation: Run dependency, e.g. DB session
opt raise
operation -->> handler: Raise HTTPException
operation -->> dep: Raise HTTPException
dep -->> handler: Auto forward exception
handler -->> client: HTTP error response
operation -->> dep: Raise other exception
dep -->> handler: Auto forward exception
end
operation ->> client: Return response to client
Note over client,operation: Response is already sent, can't change it anymore
@@ -162,9 +164,9 @@ participant tasks as Background tasks
After one of those responses is sent, no other response can be sent.
!!! tip
This diagram shows `HTTPException`, but you could also raise any other exception for which you create a [Custom Exception Handler](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}. And that exception would be handled by that custom exception handler instead of the dependency exit code.
This diagram shows `HTTPException`, but you could also raise any other exception for which you create a [Custom Exception Handler](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}.
But if you raise an exception that is not handled by the exception handlers, it will be handled by the exit code of the dependency.
If you raise any exception, it will be passed to the dependencies with yield, including `HTTPException`, and then **again** to the exception handlers. If there's no exception handler for that exception, it will then be handled by the default internal `ServerErrorMiddleware`, returning a 500 HTTP status code, to let the client know that there was an error in the server.
## Context Managers

View File

@@ -64,6 +64,18 @@ They will be added to the OpenAPI schema and used by the automatic documentation
<img src="/img/tutorial/path-operation-configuration/image01.png">
### Tags with Enums
If you have a big application, you might end up accumulating **several tags**, and you would want to make sure you always use the **same tag** for related *path operations*.
In these cases, it could make sense to store the tags in an `Enum`.
**FastAPI** supports that the same way as with plain strings:
```Python hl_lines="1 8-10 13 18"
{!../../../docs_src/path_operation_configuration/tutorial002b.py!}
```
## Summary and description
You can add a `summary` and `description`:

View File

@@ -387,6 +387,22 @@ The docs will show it like this:
<img src="/img/tutorial/query-params-str-validations/image01.png">
## Exclude from OpenAPI
To exclude a query parameter from the generated OpenAPI schema (and thus, from the automatic documentation systems), set the parameter `include_in_schema` of `Query` to `False`:
=== "Python 3.6 and above"
```Python hl_lines="10"
{!> ../../../docs_src/query_params_str_validations/tutorial014.py!}
```
=== "Python 3.10 and above"
```Python hl_lines="7"
{!> ../../../docs_src/query_params_str_validations/tutorial014_py310.py!}
```
## Recap
You can declare additional validations and metadata for your parameters.

View File

@@ -17,7 +17,7 @@ Import `File` and `UploadFile` from `fastapi`:
{!../../../docs_src/request_files/tutorial001.py!}
```
## Define `File` parameters
## Define `File` Parameters
Create file parameters the same way you would for `Body` or `Form`:
@@ -41,9 +41,9 @@ Have in mind that this means that the whole contents will be stored in memory. T
But there are several cases in which you might benefit from using `UploadFile`.
## `File` parameters with `UploadFile`
## File Parameters with `UploadFile`
Define a `File` parameter with a type of `UploadFile`:
Define a file parameter with a type of `UploadFile`:
```Python hl_lines="12"
{!../../../docs_src/request_files/tutorial001.py!}
@@ -51,6 +51,7 @@ Define a `File` parameter with a type of `UploadFile`:
Using `UploadFile` has several advantages over `bytes`:
* You don't have to use `File()` in the default value of the parameter.
* It uses a "spooled" file:
* A file stored in memory up to a maximum size limit, and after passing this limit it will be stored in disk.
* This means that it will work well for large files like images, videos, large binaries, etc. without consuming all the memory.
@@ -113,7 +114,31 @@ The way HTML forms (`<form></form>`) sends the data to the server normally uses
This is not a limitation of **FastAPI**, it's part of the HTTP protocol.
## Multiple file uploads
## Optional File Upload
You can make a file optional by using standard type annotations and setting a default value of `None`:
=== "Python 3.6 and above"
```Python hl_lines="9 17"
{!> ../../../docs_src/request_files/tutorial001_02.py!}
```
=== "Python 3.9 and above"
```Python hl_lines="7 14"
{!> ../../../docs_src/request_files/tutorial001_02_py310.py!}
```
## `UploadFile` with Additional Metadata
You can also use `File()` with `UploadFile`, for example, to set additional metadata:
```Python hl_lines="13"
{!../../../docs_src/request_files/tutorial001_03.py!}
```
## Multiple File Uploads
It's possible to upload several files at the same time.
@@ -140,6 +165,22 @@ You will receive, as declared, a `list` of `bytes` or `UploadFile`s.
**FastAPI** provides the same `starlette.responses` as `fastapi.responses` just as a convenience for you, the developer. But most of the available responses come directly from Starlette.
### Multiple File Uploads with Additional Metadata
And the same way as before, you can use `File()` to set additional parameters, even for `UploadFile`:
=== "Python 3.6 and above"
```Python hl_lines="18"
{!> ../../../docs_src/request_files/tutorial003.py!}
```
=== "Python 3.9 and above"
```Python hl_lines="16"
{!> ../../../docs_src/request_files/tutorial003_py39.py!}
```
## Recap
Use `File` to declare files to be uploaded as input parameters (as form data).
Use `File`, `bytes`, and `UploadFile` to declare files to be uploaded in the request, sent as form data.

View File

@@ -140,6 +140,7 @@ nav:
- advanced/extending-openapi.md
- advanced/openapi-callbacks.md
- advanced/wsgi.md
- advanced/generate-clients.md
- async.md
- Deployment:
- deployment/index.md

View File

@@ -46,6 +46,18 @@
<img class="sponsor-image" src="/img/sponsors/cryptapi-banner.svg" />
</a>
</div>
<div class="item">
<a title="Dropbase - seamlessly collect, clean, and centralize data." style="display: block; position: relative;" href="https://www.dropbase.io/careers" target="_blank">
<span class="sponsor-badge">sponsor</span>
<img class="sponsor-image" src="/img/sponsors/dropbase-banner.svg" />
</a>
</div>
<div class="item">
<a title="https://striveworks.us/careers" style="display: block; position: relative;" href="https://striveworks.us/careers?utm_source=fastapi&utm_medium=small_banner&utm_campaign=feb_march#openings" target="_blank">
<span class="sponsor-badge">sponsor</span>
<img class="sponsor-image" src="/img/sponsors/striveworks-banner.png" />
</a>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,28 @@
from typing import List
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
price: float
class ResponseMessage(BaseModel):
message: str
@app.post("/items/", response_model=ResponseMessage)
async def create_item(item: Item):
return {"message": "item received"}
@app.get("/items/", response_model=List[Item])
async def get_items():
return [
{"name": "Plumbus", "price": 3},
{"name": "Portal Gun", "price": 9001},
]

View File

@@ -0,0 +1,26 @@
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
price: float
class ResponseMessage(BaseModel):
message: str
@app.post("/items/", response_model=ResponseMessage)
async def create_item(item: Item):
return {"message": "item received"}
@app.get("/items/", response_model=list[Item])
async def get_items():
return [
{"name": "Plumbus", "price": 3},
{"name": "Portal Gun", "price": 9001},
]

View File

@@ -0,0 +1,38 @@
from typing import List
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
price: float
class ResponseMessage(BaseModel):
message: str
class User(BaseModel):
username: str
email: str
@app.post("/items/", response_model=ResponseMessage, tags=["items"])
async def create_item(item: Item):
return {"message": "Item received"}
@app.get("/items/", response_model=List[Item], tags=["items"])
async def get_items():
return [
{"name": "Plumbus", "price": 3},
{"name": "Portal Gun", "price": 9001},
]
@app.post("/users/", response_model=ResponseMessage, tags=["users"])
async def create_user(user: User):
return {"message": "User received"}

View File

@@ -0,0 +1,36 @@
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
price: float
class ResponseMessage(BaseModel):
message: str
class User(BaseModel):
username: str
email: str
@app.post("/items/", response_model=ResponseMessage, tags=["items"])
async def create_item(item: Item):
return {"message": "Item received"}
@app.get("/items/", response_model=list[Item], tags=["items"])
async def get_items():
return [
{"name": "Plumbus", "price": 3},
{"name": "Portal Gun", "price": 9001},
]
@app.post("/users/", response_model=ResponseMessage, tags=["users"])
async def create_user(user: User):
return {"message": "User received"}

View File

@@ -0,0 +1,44 @@
from typing import List
from fastapi import FastAPI
from fastapi.routing import APIRoute
from pydantic import BaseModel
def custom_generate_unique_id(route: APIRoute):
return f"{route.tags[0]}-{route.name}"
app = FastAPI(generate_unique_id_function=custom_generate_unique_id)
class Item(BaseModel):
name: str
price: float
class ResponseMessage(BaseModel):
message: str
class User(BaseModel):
username: str
email: str
@app.post("/items/", response_model=ResponseMessage, tags=["items"])
async def create_item(item: Item):
return {"message": "Item received"}
@app.get("/items/", response_model=List[Item], tags=["items"])
async def get_items():
return [
{"name": "Plumbus", "price": 3},
{"name": "Portal Gun", "price": 9001},
]
@app.post("/users/", response_model=ResponseMessage, tags=["users"])
async def create_user(user: User):
return {"message": "User received"}

View File

@@ -0,0 +1,42 @@
from fastapi import FastAPI
from fastapi.routing import APIRoute
from pydantic import BaseModel
def custom_generate_unique_id(route: APIRoute):
return f"{route.tags[0]}-{route.name}"
app = FastAPI(generate_unique_id_function=custom_generate_unique_id)
class Item(BaseModel):
name: str
price: float
class ResponseMessage(BaseModel):
message: str
class User(BaseModel):
username: str
email: str
@app.post("/items/", response_model=ResponseMessage, tags=["items"])
async def create_item(item: Item):
return {"message": "Item received"}
@app.get("/items/", response_model=list[Item], tags=["items"])
async def get_items():
return [
{"name": "Plumbus", "price": 3},
{"name": "Portal Gun", "price": 9001},
]
@app.post("/users/", response_model=ResponseMessage, tags=["users"])
async def create_user(user: User):
return {"message": "User received"}

View File

@@ -0,0 +1,15 @@
import json
from pathlib import Path
file_path = Path("./openapi.json")
openapi_content = json.loads(file_path.read_text())
for path_data in openapi_content["paths"].values():
for operation in path_data.values():
tag = operation["tags"][0]
operation_id = operation["operationId"]
to_remove = f"{tag}-"
new_operation_id = operation_id[len(to_remove) :]
operation["operationId"] = new_operation_id
file_path.write_text(json.dumps(openapi_content))

View File

@@ -0,0 +1,20 @@
from enum import Enum
from fastapi import FastAPI
app = FastAPI()
class Tags(Enum):
items = "items"
users = "users"
@app.get("/items/", tags=[Tags.items])
async def get_items():
return ["Portal gun", "Plumbus"]
@app.get("/users/", tags=[Tags.users])
async def read_users():
return ["Rick", "Morty"]

View File

@@ -0,0 +1,15 @@
from typing import Optional
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/")
async def read_items(
hidden_query: Optional[str] = Query(None, include_in_schema=False)
):
if hidden_query:
return {"hidden_query": hidden_query}
else:
return {"hidden_query": "Not found"}

View File

@@ -0,0 +1,11 @@
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/")
async def read_items(hidden_query: str | None = Query(None, include_in_schema=False)):
if hidden_query:
return {"hidden_query": hidden_query}
else:
return {"hidden_query": "Not found"}

View File

@@ -9,5 +9,5 @@ async def create_file(file: bytes = File(...)):
@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile = File(...)):
async def create_upload_file(file: UploadFile):
return {"filename": file.filename}

View File

@@ -0,0 +1,21 @@
from typing import Optional
from fastapi import FastAPI, File, UploadFile
app = FastAPI()
@app.post("/files/")
async def create_file(file: Optional[bytes] = File(None)):
if not file:
return {"message": "No file sent"}
else:
return {"file_size": len(file)}
@app.post("/uploadfile/")
async def create_upload_file(file: Optional[UploadFile] = None):
if not file:
return {"message": "No upload file sent"}
else:
return {"filename": file.filename}

View File

@@ -0,0 +1,19 @@
from fastapi import FastAPI, File, UploadFile
app = FastAPI()
@app.post("/files/")
async def create_file(file: bytes | None = File(None)):
if not file:
return {"message": "No file sent"}
else:
return {"file_size": len(file)}
@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile | None = None):
if not file:
return {"message": "No upload file sent"}
else:
return {"filename": file.filename}

View File

@@ -0,0 +1,15 @@
from fastapi import FastAPI, File, UploadFile
app = FastAPI()
@app.post("/files/")
async def create_file(file: bytes = File(..., description="A file read as bytes")):
return {"file_size": len(file)}
@app.post("/uploadfile/")
async def create_upload_file(
file: UploadFile = File(..., description="A file read as UploadFile")
):
return {"filename": file.filename}

View File

@@ -12,7 +12,7 @@ async def create_files(files: List[bytes] = File(...)):
@app.post("/uploadfiles/")
async def create_upload_files(files: List[UploadFile] = File(...)):
async def create_upload_files(files: List[UploadFile]):
return {"filenames": [file.filename for file in files]}

View File

@@ -10,7 +10,7 @@ async def create_files(files: list[bytes] = File(...)):
@app.post("/uploadfiles/")
async def create_upload_files(files: list[UploadFile] = File(...)):
async def create_upload_files(files: list[UploadFile]):
return {"filenames": [file.filename for file in files]}

View File

@@ -0,0 +1,37 @@
from typing import List
from fastapi import FastAPI, File, UploadFile
from fastapi.responses import HTMLResponse
app = FastAPI()
@app.post("/files/")
async def create_files(
files: List[bytes] = File(..., description="Multiple files as bytes")
):
return {"file_sizes": [len(file) for file in files]}
@app.post("/uploadfiles/")
async def create_upload_files(
files: List[UploadFile] = File(..., description="Multiple files as UploadFile")
):
return {"filenames": [file.filename for file in files]}
@app.get("/")
async def main():
content = """
<body>
<form action="/files/" enctype="multipart/form-data" method="post">
<input name="files" type="file" multiple>
<input type="submit">
</form>
<form action="/uploadfiles/" enctype="multipart/form-data" method="post">
<input name="files" type="file" multiple>
<input type="submit">
</form>
</body>
"""
return HTMLResponse(content=content)

View File

@@ -0,0 +1,35 @@
from fastapi import FastAPI, File, UploadFile
from fastapi.responses import HTMLResponse
app = FastAPI()
@app.post("/files/")
async def create_files(
files: list[bytes] = File(..., description="Multiple files as bytes")
):
return {"file_sizes": [len(file) for file in files]}
@app.post("/uploadfiles/")
async def create_upload_files(
files: list[UploadFile] = File(..., description="Multiple files as UploadFile")
):
return {"filenames": [file.filename for file in files]}
@app.get("/")
async def main():
content = """
<body>
<form action="/files/" enctype="multipart/form-data" method="post">
<input name="files" type="file" multiple>
<input type="submit">
</form>
<form action="/uploadfiles/" enctype="multipart/form-data" method="post">
<input name="files" type="file" multiple>
<input type="submit">
</form>
</body>
"""
return HTMLResponse(content=content)

View File

@@ -1,6 +1,6 @@
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
__version__ = "0.72.0"
__version__ = "0.75.0"
from starlette import status as status

View File

@@ -1,7 +1,7 @@
from enum import Enum
from typing import Any, Callable, Coroutine, Dict, List, Optional, Sequence, Type, Union
from fastapi import routing
from fastapi.concurrency import AsyncExitStack
from fastapi.datastructures import Default, DefaultPlaceholder
from fastapi.encoders import DictIntStrAny, SetIntStr
from fastapi.exception_handlers import (
@@ -10,6 +10,7 @@ from fastapi.exception_handlers import (
)
from fastapi.exceptions import RequestValidationError
from fastapi.logger import logger
from fastapi.middleware.asyncexitstack import AsyncExitStackMiddleware
from fastapi.openapi.docs import (
get_redoc_html,
get_swagger_ui_html,
@@ -18,10 +19,12 @@ from fastapi.openapi.docs import (
from fastapi.openapi.utils import get_openapi
from fastapi.params import Depends
from fastapi.types import DecoratedCallable
from fastapi.utils import generate_unique_id
from starlette.applications import Starlette
from starlette.datastructures import State
from starlette.exceptions import HTTPException
from starlette.exceptions import ExceptionMiddleware, HTTPException
from starlette.middleware import Middleware
from starlette.middleware.errors import ServerErrorMiddleware
from starlette.requests import Request
from starlette.responses import HTMLResponse, JSONResponse, Response
from starlette.routing import BaseRoute
@@ -66,10 +69,44 @@ class FastAPI(Starlette):
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
swagger_ui_parameters: Optional[Dict[str, Any]] = None,
generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
generate_unique_id
),
**extra: Any,
) -> None:
self._debug: bool = debug
self.title = title
self.description = description
self.version = version
self.terms_of_service = terms_of_service
self.contact = contact
self.license_info = license_info
self.openapi_url = openapi_url
self.openapi_tags = openapi_tags
self.root_path_in_servers = root_path_in_servers
self.docs_url = docs_url
self.redoc_url = redoc_url
self.swagger_ui_oauth2_redirect_url = swagger_ui_oauth2_redirect_url
self.swagger_ui_init_oauth = swagger_ui_init_oauth
self.swagger_ui_parameters = swagger_ui_parameters
self.servers = servers or []
self.extra = extra
self.openapi_version = "3.0.2"
self.openapi_schema: Optional[Dict[str, Any]] = None
if self.openapi_url:
assert self.title, "A title must be provided for OpenAPI, e.g.: 'My API'"
assert self.version, "A version must be provided for OpenAPI, e.g.: '2.1.0'"
# TODO: remove when discarding the openapi_prefix parameter
if openapi_prefix:
logger.warning(
'"openapi_prefix" has been deprecated in favor of "root_path", which '
"follows more closely the ASGI standard, is simpler, and more "
"automatic. Check the docs at "
"https://fastapi.tiangolo.com/advanced/sub-applications/"
)
self.root_path = root_path or openapi_prefix
self.state: State = State()
self.dependency_overrides: Dict[Callable[..., Any], Callable[..., Any]] = {}
self.router: routing.APIRouter = routing.APIRouter(
routes=routes,
dependency_overrides_provider=self,
@@ -81,6 +118,7 @@ class FastAPI(Starlette):
deprecated=deprecated,
include_in_schema=include_in_schema,
responses=responses,
generate_unique_id_function=generate_unique_id_function,
)
self.exception_handlers: Dict[
Union[int, Type[Exception]],
@@ -97,42 +135,57 @@ class FastAPI(Starlette):
[] if middleware is None else list(middleware)
)
self.middleware_stack: ASGIApp = self.build_middleware_stack()
self.title = title
self.description = description
self.version = version
self.terms_of_service = terms_of_service
self.contact = contact
self.license_info = license_info
self.servers = servers or []
self.openapi_url = openapi_url
self.openapi_tags = openapi_tags
# TODO: remove when discarding the openapi_prefix parameter
if openapi_prefix:
logger.warning(
'"openapi_prefix" has been deprecated in favor of "root_path", which '
"follows more closely the ASGI standard, is simpler, and more "
"automatic. Check the docs at "
"https://fastapi.tiangolo.com/advanced/sub-applications/"
)
self.root_path = root_path or openapi_prefix
self.root_path_in_servers = root_path_in_servers
self.docs_url = docs_url
self.redoc_url = redoc_url
self.swagger_ui_oauth2_redirect_url = swagger_ui_oauth2_redirect_url
self.swagger_ui_init_oauth = swagger_ui_init_oauth
self.swagger_ui_parameters = swagger_ui_parameters
self.extra = extra
self.dependency_overrides: Dict[Callable[..., Any], Callable[..., Any]] = {}
self.openapi_version = "3.0.2"
if self.openapi_url:
assert self.title, "A title must be provided for OpenAPI, e.g.: 'My API'"
assert self.version, "A version must be provided for OpenAPI, e.g.: '2.1.0'"
self.openapi_schema: Optional[Dict[str, Any]] = None
self.setup()
def build_middleware_stack(self) -> ASGIApp:
# Duplicate/override from Starlette to add AsyncExitStackMiddleware
# inside of ExceptionMiddleware, inside of custom user middlewares
debug = self.debug
error_handler = None
exception_handlers = {}
for key, value in self.exception_handlers.items():
if key in (500, Exception):
error_handler = value
else:
exception_handlers[key] = value
middleware = (
[Middleware(ServerErrorMiddleware, handler=error_handler, debug=debug)]
+ self.user_middleware
+ [
Middleware(
ExceptionMiddleware, handlers=exception_handlers, debug=debug
),
# Add FastAPI-specific AsyncExitStackMiddleware for dependencies with
# contextvars.
# This needs to happen after user middlewares because those create a
# new contextvars context copy by using a new AnyIO task group.
# The initial part of dependencies with yield is executed in the
# FastAPI code, inside all the middlewares, but the teardown part
# (after yield) is executed in the AsyncExitStack in this middleware,
# if the AsyncExitStack lived outside of the custom middlewares and
# contextvars were set in a dependency with yield in that internal
# contextvars context, the values would not be available in the
# outside context of the AsyncExitStack.
# By putting the middleware and the AsyncExitStack here, inside all
# user middlewares, the code before and after yield in dependencies
# with yield is executed in the same contextvars context, so all values
# set in contextvars before yield is still available after yield as
# would be expected.
# Additionally, by having this AsyncExitStack here, after the
# ExceptionMiddleware, now dependencies can catch handled exceptions,
# e.g. HTTPException, to customize the teardown code (e.g. DB session
# rollback).
Middleware(AsyncExitStackMiddleware),
]
)
app = self.router
for cls, options in reversed(middleware):
app = cls(app=app, **options)
return app
def openapi(self) -> Dict[str, Any]:
if not self.openapi_schema:
self.openapi_schema = get_openapi(
@@ -205,12 +258,7 @@ class FastAPI(Starlette):
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if self.root_path:
scope["root_path"] = self.root_path
if AsyncExitStack:
async with AsyncExitStack() as stack:
scope["fastapi_astack"] = stack
await super().__call__(scope, receive, send)
else:
await super().__call__(scope, receive, send) # pragma: no cover
await super().__call__(scope, receive, send)
def add_api_route(
self,
@@ -219,7 +267,7 @@ class FastAPI(Starlette):
*,
response_model: Optional[Type[Any]] = None,
status_code: Optional[int] = None,
tags: Optional[List[str]] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[Depends]] = None,
summary: Optional[str] = None,
description: Optional[str] = None,
@@ -240,6 +288,9 @@ class FastAPI(Starlette):
),
name: Optional[str] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
generate_unique_id
),
) -> None:
self.router.add_api_route(
path,
@@ -265,6 +316,7 @@ class FastAPI(Starlette):
response_class=response_class,
name=name,
openapi_extra=openapi_extra,
generate_unique_id_function=generate_unique_id_function,
)
def api_route(
@@ -273,7 +325,7 @@ class FastAPI(Starlette):
*,
response_model: Optional[Type[Any]] = None,
status_code: Optional[int] = None,
tags: Optional[List[str]] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[Depends]] = None,
summary: Optional[str] = None,
description: Optional[str] = None,
@@ -292,6 +344,9 @@ class FastAPI(Starlette):
response_class: Type[Response] = Default(JSONResponse),
name: Optional[str] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
generate_unique_id
),
) -> Callable[[DecoratedCallable], DecoratedCallable]:
def decorator(func: DecoratedCallable) -> DecoratedCallable:
self.router.add_api_route(
@@ -318,6 +373,7 @@ class FastAPI(Starlette):
response_class=response_class,
name=name,
openapi_extra=openapi_extra,
generate_unique_id_function=generate_unique_id_function,
)
return func
@@ -342,13 +398,16 @@ class FastAPI(Starlette):
router: routing.APIRouter,
*,
prefix: str = "",
tags: Optional[List[str]] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[Depends]] = None,
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
default_response_class: Type[Response] = Default(JSONResponse),
callbacks: Optional[List[BaseRoute]] = None,
generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
generate_unique_id
),
) -> None:
self.router.include_router(
router,
@@ -360,6 +419,7 @@ class FastAPI(Starlette):
include_in_schema=include_in_schema,
default_response_class=default_response_class,
callbacks=callbacks,
generate_unique_id_function=generate_unique_id_function,
)
def get(
@@ -368,7 +428,7 @@ class FastAPI(Starlette):
*,
response_model: Optional[Type[Any]] = None,
status_code: Optional[int] = None,
tags: Optional[List[str]] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[Depends]] = None,
summary: Optional[str] = None,
description: Optional[str] = None,
@@ -387,6 +447,9 @@ class FastAPI(Starlette):
name: Optional[str] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
generate_unique_id
),
) -> Callable[[DecoratedCallable], DecoratedCallable]:
return self.router.get(
path,
@@ -411,6 +474,7 @@ class FastAPI(Starlette):
name=name,
callbacks=callbacks,
openapi_extra=openapi_extra,
generate_unique_id_function=generate_unique_id_function,
)
def put(
@@ -419,7 +483,7 @@ class FastAPI(Starlette):
*,
response_model: Optional[Type[Any]] = None,
status_code: Optional[int] = None,
tags: Optional[List[str]] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[Depends]] = None,
summary: Optional[str] = None,
description: Optional[str] = None,
@@ -438,6 +502,9 @@ class FastAPI(Starlette):
name: Optional[str] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
generate_unique_id
),
) -> Callable[[DecoratedCallable], DecoratedCallable]:
return self.router.put(
path,
@@ -462,6 +529,7 @@ class FastAPI(Starlette):
name=name,
callbacks=callbacks,
openapi_extra=openapi_extra,
generate_unique_id_function=generate_unique_id_function,
)
def post(
@@ -470,7 +538,7 @@ class FastAPI(Starlette):
*,
response_model: Optional[Type[Any]] = None,
status_code: Optional[int] = None,
tags: Optional[List[str]] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[Depends]] = None,
summary: Optional[str] = None,
description: Optional[str] = None,
@@ -489,6 +557,9 @@ class FastAPI(Starlette):
name: Optional[str] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
generate_unique_id
),
) -> Callable[[DecoratedCallable], DecoratedCallable]:
return self.router.post(
path,
@@ -513,6 +584,7 @@ class FastAPI(Starlette):
name=name,
callbacks=callbacks,
openapi_extra=openapi_extra,
generate_unique_id_function=generate_unique_id_function,
)
def delete(
@@ -521,7 +593,7 @@ class FastAPI(Starlette):
*,
response_model: Optional[Type[Any]] = None,
status_code: Optional[int] = None,
tags: Optional[List[str]] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[Depends]] = None,
summary: Optional[str] = None,
description: Optional[str] = None,
@@ -540,6 +612,9 @@ class FastAPI(Starlette):
name: Optional[str] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
generate_unique_id
),
) -> Callable[[DecoratedCallable], DecoratedCallable]:
return self.router.delete(
path,
@@ -564,6 +639,7 @@ class FastAPI(Starlette):
name=name,
callbacks=callbacks,
openapi_extra=openapi_extra,
generate_unique_id_function=generate_unique_id_function,
)
def options(
@@ -572,7 +648,7 @@ class FastAPI(Starlette):
*,
response_model: Optional[Type[Any]] = None,
status_code: Optional[int] = None,
tags: Optional[List[str]] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[Depends]] = None,
summary: Optional[str] = None,
description: Optional[str] = None,
@@ -591,6 +667,9 @@ class FastAPI(Starlette):
name: Optional[str] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
generate_unique_id
),
) -> Callable[[DecoratedCallable], DecoratedCallable]:
return self.router.options(
path,
@@ -615,6 +694,7 @@ class FastAPI(Starlette):
name=name,
callbacks=callbacks,
openapi_extra=openapi_extra,
generate_unique_id_function=generate_unique_id_function,
)
def head(
@@ -623,7 +703,7 @@ class FastAPI(Starlette):
*,
response_model: Optional[Type[Any]] = None,
status_code: Optional[int] = None,
tags: Optional[List[str]] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[Depends]] = None,
summary: Optional[str] = None,
description: Optional[str] = None,
@@ -642,6 +722,9 @@ class FastAPI(Starlette):
name: Optional[str] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
generate_unique_id
),
) -> Callable[[DecoratedCallable], DecoratedCallable]:
return self.router.head(
path,
@@ -666,6 +749,7 @@ class FastAPI(Starlette):
name=name,
callbacks=callbacks,
openapi_extra=openapi_extra,
generate_unique_id_function=generate_unique_id_function,
)
def patch(
@@ -674,7 +758,7 @@ class FastAPI(Starlette):
*,
response_model: Optional[Type[Any]] = None,
status_code: Optional[int] = None,
tags: Optional[List[str]] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[Depends]] = None,
summary: Optional[str] = None,
description: Optional[str] = None,
@@ -693,6 +777,9 @@ class FastAPI(Starlette):
name: Optional[str] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
generate_unique_id
),
) -> Callable[[DecoratedCallable], DecoratedCallable]:
return self.router.patch(
path,
@@ -717,6 +804,7 @@ class FastAPI(Starlette):
name=name,
callbacks=callbacks,
openapi_extra=openapi_extra,
generate_unique_id_function=generate_unique_id_function,
)
def trace(
@@ -725,7 +813,7 @@ class FastAPI(Starlette):
*,
response_model: Optional[Type[Any]] = None,
status_code: Optional[int] = None,
tags: Optional[List[str]] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[Depends]] = None,
summary: Optional[str] = None,
description: Optional[str] = None,
@@ -744,6 +832,9 @@ class FastAPI(Starlette):
name: Optional[str] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
generate_unique_id
),
) -> Callable[[DecoratedCallable], DecoratedCallable]:
return self.router.trace(
path,
@@ -768,4 +859,5 @@ class FastAPI(Starlette):
name=name,
callbacks=callbacks,
openapi_extra=openapi_extra,
generate_unique_id_function=generate_unique_id_function,
)

View File

@@ -1,4 +1,4 @@
from typing import Any, Callable, Iterable, Type, TypeVar
from typing import Any, Callable, Dict, Iterable, Type, TypeVar
from starlette.datastructures import URL as URL # noqa: F401
from starlette.datastructures import Address as Address # noqa: F401
@@ -20,6 +20,10 @@ class UploadFile(StarletteUploadFile):
raise ValueError(f"Expected UploadFile, received: {type(v)}")
return v
@classmethod
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
field_schema.update({"type": "string", "format": "binary"})
class DefaultPlaceholder:
"""

View File

@@ -390,6 +390,8 @@ def get_param_field(
field.required = required
if not had_schema and not is_scalar_field(field=field):
field.field_info = params.Body(field_info.default)
if not had_schema and lenient_issubclass(field.type_, UploadFile):
field.field_info = params.File(field_info.default)
return field
@@ -701,25 +703,6 @@ def get_missing_field_error(loc: Tuple[str, ...]) -> ErrorWrapper:
return missing_field_error
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 = create_response_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,
field_info=field.field_info,
)
return out_field
def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]:
flat_dependant = get_flat_dependant(dependant)
if not flat_dependant.body_params:
@@ -729,9 +712,8 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]:
embed = getattr(field_info, "embed", None)
body_param_names_set = {param.name for param in flat_dependant.body_params}
if len(body_param_names_set) == 1 and not embed:
final_field = get_schema_compatible_field(field=first_param)
check_file_field(final_field)
return final_field
check_file_field(first_param)
return first_param
# If one field requires to embed, all have to be embedded
# in case a sub-dependency is evaluated with a single unique body field
# That is combined (embedded) with other body fields
@@ -740,7 +722,7 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]:
model_name = "Body_" + name
BodyModel: Type[BaseModel] = create_model(model_name)
for f in flat_dependant.body_params:
BodyModel.__fields__[f.name] = get_schema_compatible_field(field=f)
BodyModel.__fields__[f.name] = f
required = any(True for f in flat_dependant.body_params if f.required)
BodyFieldInfo_kwargs: Dict[str, Any] = dict(default=None)

View File

@@ -34,9 +34,17 @@ def jsonable_encoder(
exclude_unset: bool = False,
exclude_defaults: bool = False,
exclude_none: bool = False,
custom_encoder: Dict[Any, Callable[[Any], Any]] = {},
custom_encoder: Optional[Dict[Any, Callable[[Any], Any]]] = None,
sqlalchemy_safe: bool = True,
) -> Any:
custom_encoder = custom_encoder or {}
if custom_encoder:
if type(obj) in custom_encoder:
return custom_encoder[type(obj)](obj)
else:
for encoder_type, encoder_instance in custom_encoder.items():
if isinstance(obj, encoder_type):
return encoder_instance(obj)
if include is not None and not isinstance(include, (set, dict)):
include = set(include)
if exclude is not None and not isinstance(exclude, (set, dict)):
@@ -118,14 +126,6 @@ def jsonable_encoder(
)
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():

View File

@@ -0,0 +1,28 @@
from typing import Optional
from fastapi.concurrency import AsyncExitStack
from starlette.types import ASGIApp, Receive, Scope, Send
class AsyncExitStackMiddleware:
def __init__(self, app: ASGIApp, context_name: str = "fastapi_astack") -> None:
self.app = app
self.context_name = context_name
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if AsyncExitStack:
dependency_exception: Optional[Exception] = None
async with AsyncExitStack() as stack:
scope[self.context_name] = stack
try:
await self.app(scope, receive, send)
except Exception as e:
dependency_exception = e
raise e
if dependency_exception:
# This exception was possibly handled by the dependency but it should
# still bubble up so that the ServerErrorMiddleware can return a 500
# or the ExceptionMiddleware can catch and handle any other exceptions
raise dependency_exception
else:
await self.app(scope, receive, send) # pragma: no cover

View File

@@ -123,7 +123,7 @@ class Schema(BaseModel):
oneOf: Optional[List["Schema"]] = None
anyOf: Optional[List["Schema"]] = None
not_: Optional["Schema"] = Field(None, alias="not")
items: Optional["Schema"] = None
items: Optional[Union["Schema", List["Schema"]]] = None
properties: Optional[Dict[str, "Schema"]] = None
additionalProperties: Optional[Union["Schema", Reference, bool]] = None
description: Optional[str] = None

View File

@@ -1,5 +1,6 @@
import http.client
import inspect
import warnings
from enum import Enum
from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, Type, Union, cast
@@ -92,6 +93,8 @@ def get_openapi_operation_parameters(
for param in all_route_params:
field_info = param.field_info
field_info = cast(Param, field_info)
if not field_info.include_in_schema:
continue
parameter = {
"name": param.alias,
"in": field_info.in_.value,
@@ -138,7 +141,15 @@ def get_openapi_operation_request_body(
return request_body_oai
def generate_operation_id(*, route: routing.APIRoute, method: str) -> str:
def generate_operation_id(
*, route: routing.APIRoute, method: str
) -> str: # pragma: nocover
warnings.warn(
"fastapi.openapi.utils.generate_operation_id() was deprecated, "
"it is not used internally, and will be removed soon",
DeprecationWarning,
stacklevel=2,
)
if route.operation_id:
return route.operation_id
path: str = route.path_format
@@ -152,7 +163,7 @@ def generate_operation_summary(*, route: routing.APIRoute, method: str) -> str:
def get_openapi_operation_metadata(
*, route: routing.APIRoute, method: str
*, route: routing.APIRoute, method: str, operation_ids: Set[str]
) -> Dict[str, Any]:
operation: Dict[str, Any] = {}
if route.tags:
@@ -160,14 +171,25 @@ def get_openapi_operation_metadata(
operation["summary"] = generate_operation_summary(route=route, method=method)
if route.description:
operation["description"] = route.description
operation["operationId"] = generate_operation_id(route=route, method=method)
operation_id = route.operation_id or route.unique_id
if operation_id in operation_ids:
message = (
f"Duplicate Operation ID {operation_id} for function "
+ f"{route.endpoint.__name__}"
)
file_name = getattr(route.endpoint, "__globals__", {}).get("__file__")
if file_name:
message += f" at {file_name}"
warnings.warn(message)
operation_ids.add(operation_id)
operation["operationId"] = operation_id
if route.deprecated:
operation["deprecated"] = route.deprecated
return operation
def get_openapi_path(
*, route: routing.APIRoute, model_name_map: Dict[type, str]
*, route: routing.APIRoute, model_name_map: Dict[type, str], operation_ids: Set[str]
) -> Tuple[Dict[str, Any], Dict[str, Any], Dict[str, Any]]:
path = {}
security_schemes: Dict[str, Any] = {}
@@ -181,7 +203,9 @@ def get_openapi_path(
route_response_media_type: Optional[str] = current_response_class.media_type
if route.include_in_schema:
for method in route.methods:
operation = get_openapi_operation_metadata(route=route, method=method)
operation = get_openapi_operation_metadata(
route=route, method=method, operation_ids=operation_ids
)
parameters: List[Dict[str, Any]] = []
flat_dependant = get_flat_dependant(route.dependant, skip_repeats=True)
security_definitions, operation_security = get_openapi_security_definitions(
@@ -215,7 +239,9 @@ def get_openapi_path(
cb_security_schemes,
cb_definitions,
) = get_openapi_path(
route=callback, model_name_map=model_name_map
route=callback,
model_name_map=model_name_map,
operation_ids=operation_ids,
)
callbacks[callback.name] = {callback.path: cb_path}
operation["callbacks"] = callbacks
@@ -382,6 +408,7 @@ def get_openapi(
output["servers"] = servers
components: Dict[str, Dict[str, Any]] = {}
paths: Dict[str, Dict[str, Any]] = {}
operation_ids: Set[str] = set()
flat_models = get_flat_models_from_routes(routes)
model_name_map = get_model_name_map(flat_models)
definitions = get_model_definitions(
@@ -389,7 +416,9 @@ def get_openapi(
)
for route in routes:
if isinstance(route, routing.APIRoute):
result = get_openapi_path(route=route, model_name_map=model_name_map)
result = get_openapi_path(
route=route, model_name_map=model_name_map, operation_ids=operation_ids
)
if result:
path, security_schemes, path_definitions = result
if path:

View File

@@ -20,6 +20,7 @@ def Path( # noqa: N802
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
**extra: Any,
) -> Any:
return params.Path(
@@ -37,6 +38,7 @@ def Path( # noqa: N802
example=example,
examples=examples,
deprecated=deprecated,
include_in_schema=include_in_schema,
**extra,
)
@@ -57,6 +59,7 @@ def Query( # noqa: N802
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
**extra: Any,
) -> Any:
return params.Query(
@@ -74,6 +77,7 @@ def Query( # noqa: N802
example=example,
examples=examples,
deprecated=deprecated,
include_in_schema=include_in_schema,
**extra,
)
@@ -95,6 +99,7 @@ def Header( # noqa: N802
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
**extra: Any,
) -> Any:
return params.Header(
@@ -113,6 +118,7 @@ def Header( # noqa: N802
example=example,
examples=examples,
deprecated=deprecated,
include_in_schema=include_in_schema,
**extra,
)
@@ -133,6 +139,7 @@ def Cookie( # noqa: N802
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
**extra: Any,
) -> Any:
return params.Cookie(
@@ -150,6 +157,7 @@ def Cookie( # noqa: N802
example=example,
examples=examples,
deprecated=deprecated,
include_in_schema=include_in_schema,
**extra,
)

View File

@@ -31,11 +31,13 @@ class Param(FieldInfo):
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
**extra: Any,
):
self.deprecated = deprecated
self.example = example
self.examples = examples
self.include_in_schema = include_in_schema
super().__init__(
default,
alias=alias,
@@ -75,6 +77,7 @@ class Path(Param):
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
**extra: Any,
):
self.in_ = self.in_
@@ -93,6 +96,7 @@ class Path(Param):
deprecated=deprecated,
example=example,
examples=examples,
include_in_schema=include_in_schema,
**extra,
)
@@ -117,6 +121,7 @@ class Query(Param):
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
**extra: Any,
):
super().__init__(
@@ -134,6 +139,7 @@ class Query(Param):
deprecated=deprecated,
example=example,
examples=examples,
include_in_schema=include_in_schema,
**extra,
)
@@ -159,6 +165,7 @@ class Header(Param):
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
**extra: Any,
):
self.convert_underscores = convert_underscores
@@ -177,6 +184,7 @@ class Header(Param):
deprecated=deprecated,
example=example,
examples=examples,
include_in_schema=include_in_schema,
**extra,
)
@@ -201,6 +209,7 @@ class Cookie(Param):
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
**extra: Any,
):
super().__init__(
@@ -218,6 +227,7 @@ class Cookie(Param):
deprecated=deprecated,
example=example,
examples=examples,
include_in_schema=include_in_schema,
**extra,
)

View File

@@ -1,9 +1,9 @@
import asyncio
import dataclasses
import email.message
import enum
import inspect
import json
from enum import Enum, IntEnum
from typing import (
Any,
Callable,
@@ -13,6 +13,7 @@ from typing import (
Optional,
Sequence,
Set,
Tuple,
Type,
Union,
)
@@ -33,7 +34,7 @@ from fastapi.types import DecoratedCallable
from fastapi.utils import (
create_cloned_field,
create_response_field,
generate_operation_id_for_path,
generate_unique_id,
get_value_or_default,
)
from pydantic import BaseModel
@@ -44,7 +45,7 @@ from starlette.concurrency import run_in_threadpool
from starlette.exceptions import HTTPException
from starlette.requests import Request
from starlette.responses import JSONResponse, Response
from starlette.routing import BaseRoute
from starlette.routing import BaseRoute, Match
from starlette.routing import Mount as Mount # noqa
from starlette.routing import (
compile_path,
@@ -53,7 +54,7 @@ from starlette.routing import (
websocket_session,
)
from starlette.status import WS_1008_POLICY_VIOLATION
from starlette.types import ASGIApp
from starlette.types import ASGIApp, Scope
from starlette.websockets import WebSocket
@@ -296,6 +297,12 @@ class APIWebSocketRoute(routing.WebSocketRoute):
)
self.path_regex, self.path_format, self.param_convertors = compile_path(path)
def matches(self, scope: Scope) -> Tuple[Match, Scope]:
match, child_scope = super().matches(scope)
if match != Match.NONE:
child_scope["route"] = self
return match, child_scope
class APIRoute(routing.Route):
def __init__(
@@ -305,7 +312,7 @@ class APIRoute(routing.Route):
*,
response_model: Optional[Type[Any]] = None,
status_code: Optional[int] = None,
tags: Optional[List[str]] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[params.Depends]] = None,
summary: Optional[str] = None,
description: Optional[str] = None,
@@ -328,21 +335,47 @@ class APIRoute(routing.Route):
dependency_overrides_provider: Optional[Any] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
generate_unique_id_function: Union[
Callable[["APIRoute"], str], DefaultPlaceholder
] = Default(generate_unique_id),
) -> None:
# normalise enums e.g. http.HTTPStatus
if isinstance(status_code, enum.IntEnum):
status_code = int(status_code)
self.path = path
self.endpoint = endpoint
self.response_model = response_model
self.summary = summary
self.response_description = response_description
self.deprecated = deprecated
self.operation_id = operation_id
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_exclude_unset = response_model_exclude_unset
self.response_model_exclude_defaults = response_model_exclude_defaults
self.response_model_exclude_none = response_model_exclude_none
self.include_in_schema = include_in_schema
self.response_class = response_class
self.dependency_overrides_provider = dependency_overrides_provider
self.callbacks = callbacks
self.openapi_extra = openapi_extra
self.generate_unique_id_function = generate_unique_id_function
self.tags = tags or []
self.responses = responses or {}
self.name = get_name(endpoint) if name is None else name
self.path_regex, self.path_format, self.param_convertors = compile_path(path)
if methods is None:
methods = ["GET"]
self.methods: Set[str] = set([method.upper() for method in methods])
self.unique_id = generate_operation_id_for_path(
name=self.name, path=self.path_format, method=list(methods)[0]
)
self.response_model = response_model
if isinstance(generate_unique_id_function, DefaultPlaceholder):
current_generate_unique_id: Callable[
["APIRoute"], str
] = generate_unique_id_function.value
else:
current_generate_unique_id = generate_unique_id_function
self.unique_id = self.operation_id or current_generate_unique_id(self)
# normalize enums e.g. http.HTTPStatus
if isinstance(status_code, IntEnum):
status_code = int(status_code)
self.status_code = status_code
if self.response_model:
assert (
status_code not in STATUS_CODES_WITH_NO_BODY
@@ -364,19 +397,14 @@ class APIRoute(routing.Route):
else:
self.response_field = None # type: ignore
self.secure_cloned_response_field = None
self.status_code = status_code
self.tags = tags or []
if dependencies:
self.dependencies = list(dependencies)
else:
self.dependencies = []
self.summary = summary
self.description = description or inspect.cleandoc(self.endpoint.__doc__ or "")
# if a "form feed" character (page break) is found in the description text,
# truncate description text to the content preceding the first "form feed"
self.description = self.description.split("\f")[0]
self.response_description = response_description
self.responses = responses or {}
response_fields = {}
for additional_status_code, response in self.responses.items():
assert isinstance(response, dict), "An additional response must be a dict"
@@ -392,16 +420,6 @@ class APIRoute(routing.Route):
self.response_fields: Dict[Union[int, str], ModelField] = response_fields
else:
self.response_fields = {}
self.deprecated = deprecated
self.operation_id = operation_id
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_exclude_unset = response_model_exclude_unset
self.response_model_exclude_defaults = response_model_exclude_defaults
self.response_model_exclude_none = response_model_exclude_none
self.include_in_schema = include_in_schema
self.response_class = response_class
assert callable(endpoint), "An endpoint must be a callable"
self.dependant = get_dependant(path=self.path_format, call=self.endpoint)
@@ -411,10 +429,7 @@ class APIRoute(routing.Route):
get_parameterless_sub_dependant(depends=depends, path=self.path_format),
)
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())
self.openapi_extra = openapi_extra
def get_route_handler(self) -> Callable[[Request], Coroutine[Any, Any, Response]]:
return get_request_handler(
@@ -432,13 +447,19 @@ class APIRoute(routing.Route):
dependency_overrides_provider=self.dependency_overrides_provider,
)
def matches(self, scope: Scope) -> Tuple[Match, Scope]:
match, child_scope = super().matches(scope)
if match != Match.NONE:
child_scope["route"] = self
return match, child_scope
class APIRouter(routing.Router):
def __init__(
self,
*,
prefix: str = "",
tags: Optional[List[str]] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[params.Depends]] = None,
default_response_class: Type[Response] = Default(JSONResponse),
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
@@ -452,6 +473,9 @@ class APIRouter(routing.Router):
on_shutdown: Optional[Sequence[Callable[[], Any]]] = None,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
generate_unique_id_function: Callable[[APIRoute], str] = Default(
generate_unique_id
),
) -> None:
super().__init__(
routes=routes, # type: ignore # in Starlette
@@ -466,7 +490,7 @@ class APIRouter(routing.Router):
"/"
), "A path prefix must not end with '/', as the routes will start with '/'"
self.prefix = prefix
self.tags: List[str] = tags or []
self.tags: List[Union[str, Enum]] = tags or []
self.dependencies = list(dependencies or []) or []
self.deprecated = deprecated
self.include_in_schema = include_in_schema
@@ -475,6 +499,7 @@ class APIRouter(routing.Router):
self.dependency_overrides_provider = dependency_overrides_provider
self.route_class = route_class
self.default_response_class = default_response_class
self.generate_unique_id_function = generate_unique_id_function
def add_api_route(
self,
@@ -483,7 +508,7 @@ class APIRouter(routing.Router):
*,
response_model: Optional[Type[Any]] = None,
status_code: Optional[int] = None,
tags: Optional[List[str]] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[params.Depends]] = None,
summary: Optional[str] = None,
description: Optional[str] = None,
@@ -506,6 +531,9 @@ class APIRouter(routing.Router):
route_class_override: Optional[Type[APIRoute]] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
generate_unique_id_function: Union[
Callable[[APIRoute], str], DefaultPlaceholder
] = Default(generate_unique_id),
) -> None:
route_class = route_class_override or self.route_class
responses = responses or {}
@@ -522,6 +550,9 @@ class APIRouter(routing.Router):
current_callbacks = self.callbacks.copy()
if callbacks:
current_callbacks.extend(callbacks)
current_generate_unique_id = get_value_or_default(
generate_unique_id_function, self.generate_unique_id_function
)
route = route_class(
self.prefix + path,
endpoint=endpoint,
@@ -548,6 +579,7 @@ class APIRouter(routing.Router):
dependency_overrides_provider=self.dependency_overrides_provider,
callbacks=current_callbacks,
openapi_extra=openapi_extra,
generate_unique_id_function=current_generate_unique_id,
)
self.routes.append(route)
@@ -557,7 +589,7 @@ class APIRouter(routing.Router):
*,
response_model: Optional[Type[Any]] = None,
status_code: Optional[int] = None,
tags: Optional[List[str]] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[params.Depends]] = None,
summary: Optional[str] = None,
description: Optional[str] = None,
@@ -577,6 +609,9 @@ class APIRouter(routing.Router):
name: Optional[str] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
generate_unique_id_function: Callable[[APIRoute], str] = Default(
generate_unique_id
),
) -> Callable[[DecoratedCallable], DecoratedCallable]:
def decorator(func: DecoratedCallable) -> DecoratedCallable:
self.add_api_route(
@@ -604,6 +639,7 @@ class APIRouter(routing.Router):
name=name,
callbacks=callbacks,
openapi_extra=openapi_extra,
generate_unique_id_function=generate_unique_id_function,
)
return func
@@ -634,13 +670,16 @@ class APIRouter(routing.Router):
router: "APIRouter",
*,
prefix: str = "",
tags: Optional[List[str]] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[params.Depends]] = None,
default_response_class: Type[Response] = Default(JSONResponse),
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
callbacks: Optional[List[BaseRoute]] = None,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
generate_unique_id_function: Callable[[APIRoute], str] = Default(
generate_unique_id
),
) -> None:
if prefix:
assert prefix.startswith("/"), "A path prefix must start with '/'"
@@ -681,6 +720,12 @@ class APIRouter(routing.Router):
current_callbacks.extend(callbacks)
if route.callbacks:
current_callbacks.extend(route.callbacks)
current_generate_unique_id = get_value_or_default(
route.generate_unique_id_function,
router.generate_unique_id_function,
generate_unique_id_function,
self.generate_unique_id_function,
)
self.add_api_route(
prefix + route.path,
route.endpoint,
@@ -709,6 +754,7 @@ class APIRouter(routing.Router):
route_class_override=type(route),
callbacks=current_callbacks,
openapi_extra=route.openapi_extra,
generate_unique_id_function=current_generate_unique_id,
)
elif isinstance(route, routing.Route):
methods = list(route.methods or []) # type: ignore # in Starlette
@@ -738,7 +784,7 @@ class APIRouter(routing.Router):
*,
response_model: Optional[Type[Any]] = None,
status_code: Optional[int] = None,
tags: Optional[List[str]] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[params.Depends]] = None,
summary: Optional[str] = None,
description: Optional[str] = None,
@@ -757,6 +803,9 @@ class APIRouter(routing.Router):
name: Optional[str] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
generate_unique_id_function: Callable[[APIRoute], str] = Default(
generate_unique_id
),
) -> Callable[[DecoratedCallable], DecoratedCallable]:
return self.api_route(
path=path,
@@ -782,6 +831,7 @@ class APIRouter(routing.Router):
name=name,
callbacks=callbacks,
openapi_extra=openapi_extra,
generate_unique_id_function=generate_unique_id_function,
)
def put(
@@ -790,7 +840,7 @@ class APIRouter(routing.Router):
*,
response_model: Optional[Type[Any]] = None,
status_code: Optional[int] = None,
tags: Optional[List[str]] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[params.Depends]] = None,
summary: Optional[str] = None,
description: Optional[str] = None,
@@ -809,6 +859,9 @@ class APIRouter(routing.Router):
name: Optional[str] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
generate_unique_id_function: Callable[[APIRoute], str] = Default(
generate_unique_id
),
) -> Callable[[DecoratedCallable], DecoratedCallable]:
return self.api_route(
path=path,
@@ -834,6 +887,7 @@ class APIRouter(routing.Router):
name=name,
callbacks=callbacks,
openapi_extra=openapi_extra,
generate_unique_id_function=generate_unique_id_function,
)
def post(
@@ -842,7 +896,7 @@ class APIRouter(routing.Router):
*,
response_model: Optional[Type[Any]] = None,
status_code: Optional[int] = None,
tags: Optional[List[str]] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[params.Depends]] = None,
summary: Optional[str] = None,
description: Optional[str] = None,
@@ -861,6 +915,9 @@ class APIRouter(routing.Router):
name: Optional[str] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
generate_unique_id_function: Callable[[APIRoute], str] = Default(
generate_unique_id
),
) -> Callable[[DecoratedCallable], DecoratedCallable]:
return self.api_route(
path=path,
@@ -886,6 +943,7 @@ class APIRouter(routing.Router):
name=name,
callbacks=callbacks,
openapi_extra=openapi_extra,
generate_unique_id_function=generate_unique_id_function,
)
def delete(
@@ -894,7 +952,7 @@ class APIRouter(routing.Router):
*,
response_model: Optional[Type[Any]] = None,
status_code: Optional[int] = None,
tags: Optional[List[str]] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[params.Depends]] = None,
summary: Optional[str] = None,
description: Optional[str] = None,
@@ -913,6 +971,9 @@ class APIRouter(routing.Router):
name: Optional[str] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
generate_unique_id_function: Callable[[APIRoute], str] = Default(
generate_unique_id
),
) -> Callable[[DecoratedCallable], DecoratedCallable]:
return self.api_route(
path=path,
@@ -938,6 +999,7 @@ class APIRouter(routing.Router):
name=name,
callbacks=callbacks,
openapi_extra=openapi_extra,
generate_unique_id_function=generate_unique_id_function,
)
def options(
@@ -946,7 +1008,7 @@ class APIRouter(routing.Router):
*,
response_model: Optional[Type[Any]] = None,
status_code: Optional[int] = None,
tags: Optional[List[str]] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[params.Depends]] = None,
summary: Optional[str] = None,
description: Optional[str] = None,
@@ -965,6 +1027,9 @@ class APIRouter(routing.Router):
name: Optional[str] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
generate_unique_id_function: Callable[[APIRoute], str] = Default(
generate_unique_id
),
) -> Callable[[DecoratedCallable], DecoratedCallable]:
return self.api_route(
path=path,
@@ -990,6 +1055,7 @@ class APIRouter(routing.Router):
name=name,
callbacks=callbacks,
openapi_extra=openapi_extra,
generate_unique_id_function=generate_unique_id_function,
)
def head(
@@ -998,7 +1064,7 @@ class APIRouter(routing.Router):
*,
response_model: Optional[Type[Any]] = None,
status_code: Optional[int] = None,
tags: Optional[List[str]] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[params.Depends]] = None,
summary: Optional[str] = None,
description: Optional[str] = None,
@@ -1017,6 +1083,9 @@ class APIRouter(routing.Router):
name: Optional[str] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
generate_unique_id_function: Callable[[APIRoute], str] = Default(
generate_unique_id
),
) -> Callable[[DecoratedCallable], DecoratedCallable]:
return self.api_route(
path=path,
@@ -1042,6 +1111,7 @@ class APIRouter(routing.Router):
name=name,
callbacks=callbacks,
openapi_extra=openapi_extra,
generate_unique_id_function=generate_unique_id_function,
)
def patch(
@@ -1050,7 +1120,7 @@ class APIRouter(routing.Router):
*,
response_model: Optional[Type[Any]] = None,
status_code: Optional[int] = None,
tags: Optional[List[str]] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[params.Depends]] = None,
summary: Optional[str] = None,
description: Optional[str] = None,
@@ -1069,6 +1139,9 @@ class APIRouter(routing.Router):
name: Optional[str] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
generate_unique_id_function: Callable[[APIRoute], str] = Default(
generate_unique_id
),
) -> Callable[[DecoratedCallable], DecoratedCallable]:
return self.api_route(
path=path,
@@ -1094,6 +1167,7 @@ class APIRouter(routing.Router):
name=name,
callbacks=callbacks,
openapi_extra=openapi_extra,
generate_unique_id_function=generate_unique_id_function,
)
def trace(
@@ -1102,7 +1176,7 @@ class APIRouter(routing.Router):
*,
response_model: Optional[Type[Any]] = None,
status_code: Optional[int] = None,
tags: Optional[List[str]] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[params.Depends]] = None,
summary: Optional[str] = None,
description: Optional[str] = None,
@@ -1121,6 +1195,9 @@ class APIRouter(routing.Router):
name: Optional[str] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
generate_unique_id_function: Callable[[APIRoute], str] = Default(
generate_unique_id
),
) -> Callable[[DecoratedCallable], DecoratedCallable]:
return self.api_route(
@@ -1147,4 +1224,5 @@ class APIRouter(routing.Router):
name=name,
callbacks=callbacks,
openapi_extra=openapi_extra,
generate_unique_id_function=generate_unique_id_function,
)

View File

@@ -1,8 +1,9 @@
import functools
import re
import warnings
from dataclasses import is_dataclass
from enum import Enum
from typing import Any, Dict, Optional, Set, Type, Union, cast
from typing import TYPE_CHECKING, Any, Dict, Optional, Set, Type, Union, cast
import fastapi
from fastapi.datastructures import DefaultPlaceholder, DefaultType
@@ -13,6 +14,9 @@ from pydantic.fields import FieldInfo, ModelField, UndefinedType
from pydantic.schema import model_process_schema
from pydantic.utils import lenient_issubclass
if TYPE_CHECKING: # pragma: nocover
from .routing import APIRoute
def get_model_definitions(
*,
@@ -119,13 +123,29 @@ def create_cloned_field(
return new_field
def generate_operation_id_for_path(*, name: str, path: str, method: str) -> str:
def generate_operation_id_for_path(
*, name: str, path: str, method: str
) -> str: # pragma: nocover
warnings.warn(
"fastapi.utils.generate_operation_id_for_path() was deprecated, "
"it is not used internally, and will be removed soon",
DeprecationWarning,
stacklevel=2,
)
operation_id = name + path
operation_id = re.sub("[^0-9a-zA-Z_]", "_", operation_id)
operation_id = operation_id + "_" + method.lower()
return operation_id
def generate_unique_id(route: "APIRoute") -> str:
operation_id = route.name + route.path_format
operation_id = re.sub("[^0-9a-zA-Z_]", "_", operation_id)
assert route.methods
operation_id = operation_id + "_" + list(route.methods)[0].lower()
return operation_id
def deep_dict_update(main_dict: Dict[Any, Any], update_dict: Dict[Any, Any]) -> None:
for key in update_dict:
if (

View File

@@ -235,7 +235,16 @@ def test_sync_raise_other():
assert "/sync_raise" not in errors
def test_async_raise():
def test_async_raise_raises():
with pytest.raises(AsyncDependencyError):
client.get("/async_raise")
assert state["/async_raise"] == "asyncgen raise finalized"
assert "/async_raise" in errors
errors.clear()
def test_async_raise_server_error():
client = TestClient(app, raise_server_exceptions=False)
response = client.get("/async_raise")
assert response.status_code == 500, response.text
assert state["/async_raise"] == "asyncgen raise finalized"
@@ -270,7 +279,16 @@ def test_background_tasks():
assert state["bg"] == "bg set - b: started b - a: started a"
def test_sync_raise():
def test_sync_raise_raises():
with pytest.raises(SyncDependencyError):
client.get("/sync_raise")
assert state["/sync_raise"] == "generator raise finalized"
assert "/sync_raise" in errors
errors.clear()
def test_sync_raise_server_error():
client = TestClient(app, raise_server_exceptions=False)
response = client.get("/sync_raise")
assert response.status_code == 500, response.text
assert state["/sync_raise"] == "generator raise finalized"
@@ -306,7 +324,16 @@ def test_sync_sync_raise_other():
assert "/sync_raise" not in errors
def test_sync_async_raise():
def test_sync_async_raise_raises():
with pytest.raises(AsyncDependencyError):
client.get("/sync_async_raise")
assert state["/async_raise"] == "asyncgen raise finalized"
assert "/async_raise" in errors
errors.clear()
def test_sync_async_raise_server_error():
client = TestClient(app, raise_server_exceptions=False)
response = client.get("/sync_async_raise")
assert response.status_code == 500, response.text
assert state["/async_raise"] == "asyncgen raise finalized"
@@ -314,7 +341,16 @@ def test_sync_async_raise():
errors.clear()
def test_sync_sync_raise():
def test_sync_sync_raise_raises():
with pytest.raises(SyncDependencyError):
client.get("/sync_sync_raise")
assert state["/sync_raise"] == "generator raise finalized"
assert "/sync_raise" in errors
errors.clear()
def test_sync_sync_raise_server_error():
client = TestClient(app, raise_server_exceptions=False)
response = client.get("/sync_sync_raise")
assert response.status_code == 500, response.text
assert state["/sync_raise"] == "generator raise finalized"

View File

@@ -0,0 +1,51 @@
from contextvars import ContextVar
from typing import Any, Awaitable, Callable, Dict, Optional
from fastapi import Depends, FastAPI, Request, Response
from fastapi.testclient import TestClient
legacy_request_state_context_var: ContextVar[Optional[Dict[str, Any]]] = ContextVar(
"legacy_request_state_context_var", default=None
)
app = FastAPI()
async def set_up_request_state_dependency():
request_state = {"user": "deadpond"}
contextvar_token = legacy_request_state_context_var.set(request_state)
yield request_state
legacy_request_state_context_var.reset(contextvar_token)
@app.middleware("http")
async def custom_middleware(
request: Request, call_next: Callable[[Request], Awaitable[Response]]
):
response = await call_next(request)
response.headers["custom"] = "foo"
return response
@app.get("/user", dependencies=[Depends(set_up_request_state_dependency)])
def get_user():
request_state = legacy_request_state_context_var.get()
assert request_state
return request_state["user"]
client = TestClient(app)
def test_dependency_contextvars():
"""
Check that custom middlewares don't affect the contextvar context for dependencies.
The code before yield and the code after yield should be run in the same contextvar
context, so that request_state_context_var.reset(contextvar_token).
If they are run in a different context, that raises an error.
"""
response = client.get("/user")
assert response.json() == "deadpond"
assert response.headers["custom"] == "foo"

View File

@@ -0,0 +1,71 @@
import pytest
from fastapi import Body, Depends, FastAPI, HTTPException
from fastapi.testclient import TestClient
initial_fake_database = {"rick": "Rick Sanchez"}
fake_database = initial_fake_database.copy()
initial_state = {"except": False, "finally": False}
state = initial_state.copy()
app = FastAPI()
async def get_database():
temp_database = fake_database.copy()
try:
yield temp_database
fake_database.update(temp_database)
except HTTPException:
state["except"] = True
finally:
state["finally"] = True
@app.put("/invalid-user/{user_id}")
def put_invalid_user(
user_id: str, name: str = Body(...), db: dict = Depends(get_database)
):
db[user_id] = name
raise HTTPException(status_code=400, detail="Invalid user")
@app.put("/user/{user_id}")
def put_user(user_id: str, name: str = Body(...), db: dict = Depends(get_database)):
db[user_id] = name
return {"message": "OK"}
@pytest.fixture(autouse=True)
def reset_state_and_db():
global fake_database
global state
fake_database = initial_fake_database.copy()
state = initial_state.copy()
client = TestClient(app)
def test_dependency_gets_exception():
assert state["except"] is False
assert state["finally"] is False
response = client.put("/invalid-user/rick", json="Morty")
assert response.status_code == 400, response.text
assert response.json() == {"detail": "Invalid user"}
assert state["except"] is True
assert state["finally"] is True
assert fake_database["rick"] == "Rick Sanchez"
def test_dependency_no_exception():
assert state["except"] is False
assert state["finally"] is False
response = client.put("/user/rick", json="Morty")
assert response.status_code == 200, response.text
assert response.json() == {"message": "OK"}
assert state["except"] is False
assert state["finally"] is True
assert fake_database["rick"] == "Morty"

View File

@@ -1,3 +1,4 @@
import pytest
from fastapi import FastAPI, HTTPException
from fastapi.exceptions import RequestValidationError
from fastapi.testclient import TestClient
@@ -12,10 +13,15 @@ def request_validation_exception_handler(request, exception):
return JSONResponse({"exception": "request-validation"})
def server_error_exception_handler(request, exception):
return JSONResponse(status_code=500, content={"exception": "server-error"})
app = FastAPI(
exception_handlers={
HTTPException: http_exception_handler,
RequestValidationError: request_validation_exception_handler,
Exception: server_error_exception_handler,
}
)
@@ -32,6 +38,11 @@ def route_with_request_validation_exception(param: int):
pass # pragma: no cover
@app.get("/server-error")
def route_with_server_error():
raise RuntimeError("Oops!")
def test_override_http_exception():
response = client.get("/http-exception")
assert response.status_code == 200
@@ -42,3 +53,15 @@ def test_override_request_validation_exception():
response = client.get("/request-validation/invalid")
assert response.status_code == 200
assert response.json() == {"exception": "request-validation"}
def test_override_server_error_exception_raises():
with pytest.raises(RuntimeError):
client.get("/server-error")
def test_override_server_error_exception_response():
client = TestClient(app, raise_server_exceptions=False)
response = client.get("/server-error")
assert response.status_code == 500
assert response.json() == {"exception": "server-error"}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,5 @@
import warnings
import pytest
from fastapi import APIRouter, Depends, FastAPI, Response
from fastapi.responses import JSONResponse
@@ -343,7 +345,11 @@ client = TestClient(app)
def test_openapi():
client = TestClient(app)
response = client.get("/openapi.json")
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
response = client.get("/openapi.json")
assert issubclass(w[-1].category, UserWarning)
assert "Duplicate Operation ID" in str(w[-1].message)
assert response.json() == openapi_schema

View File

@@ -161,6 +161,21 @@ def test_custom_encoders():
assert encoded_instance["dt_field"] == instance.dt_field.isoformat()
def test_custom_enum_encoders():
def custom_enum_encoder(v: Enum):
return v.value.lower()
class MyEnum(Enum):
ENUM_VAL_1 = "ENUM_VAL_1"
instance = MyEnum.ENUM_VAL_1
encoded_instance = jsonable_encoder(
instance, custom_encoder={MyEnum: custom_enum_encoder}
)
assert encoded_instance == custom_enum_encoder(instance)
def test_encode_model_with_path(model_with_path):
if isinstance(model_with_path.path, PureWindowsPath):
expected = "\\foo\\bar"

View File

@@ -0,0 +1,239 @@
from typing import Optional
import pytest
from fastapi import Cookie, FastAPI, Header, Path, Query
from fastapi.testclient import TestClient
app = FastAPI()
@app.get("/hidden_cookie")
async def hidden_cookie(
hidden_cookie: Optional[str] = Cookie(None, include_in_schema=False)
):
return {"hidden_cookie": hidden_cookie}
@app.get("/hidden_header")
async def hidden_header(
hidden_header: Optional[str] = Header(None, include_in_schema=False)
):
return {"hidden_header": hidden_header}
@app.get("/hidden_path/{hidden_path}")
async def hidden_path(hidden_path: str = Path(..., include_in_schema=False)):
return {"hidden_path": hidden_path}
@app.get("/hidden_query")
async def hidden_query(
hidden_query: Optional[str] = Query(None, include_in_schema=False)
):
return {"hidden_query": hidden_query}
client = TestClient(app)
openapi_shema = {
"openapi": "3.0.2",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/hidden_cookie": {
"get": {
"summary": "Hidden Cookie",
"operationId": "hidden_cookie_hidden_cookie_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/hidden_header": {
"get": {
"summary": "Hidden Header",
"operationId": "hidden_header_hidden_header_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/hidden_path/{hidden_path}": {
"get": {
"summary": "Hidden Path",
"operationId": "hidden_path_hidden_path__hidden_path__get",
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/hidden_query": {
"get": {
"summary": "Hidden Query",
"operationId": "hidden_query_hidden_query_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"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"},
}
},
},
"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_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_shema
@pytest.mark.parametrize(
"path,cookies,expected_status,expected_response",
[
(
"/hidden_cookie",
{},
200,
{"hidden_cookie": None},
),
(
"/hidden_cookie",
{"hidden_cookie": "somevalue"},
200,
{"hidden_cookie": "somevalue"},
),
],
)
def test_hidden_cookie(path, cookies, expected_status, expected_response):
response = client.get(path, cookies=cookies)
assert response.status_code == expected_status
assert response.json() == expected_response
@pytest.mark.parametrize(
"path,headers,expected_status,expected_response",
[
(
"/hidden_header",
{},
200,
{"hidden_header": None},
),
(
"/hidden_header",
{"Hidden-Header": "somevalue"},
200,
{"hidden_header": "somevalue"},
),
],
)
def test_hidden_header(path, headers, expected_status, expected_response):
response = client.get(path, headers=headers)
assert response.status_code == expected_status
assert response.json() == expected_response
def test_hidden_path():
response = client.get("/hidden_path/hidden_path")
assert response.status_code == 200
assert response.json() == {"hidden_path": "hidden_path"}
@pytest.mark.parametrize(
"path,expected_status,expected_response",
[
(
"/hidden_query",
200,
{"hidden_query": None},
),
(
"/hidden_query?hidden_query=somevalue",
200,
{"hidden_query": "somevalue"},
),
],
)
def test_hidden_query(path, expected_status, expected_response):
response = client.get(path)
assert response.status_code == expected_status
assert response.json() == expected_response

50
tests/test_route_scope.py Normal file
View File

@@ -0,0 +1,50 @@
import pytest
from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
from fastapi.routing import APIRoute, APIWebSocketRoute
from fastapi.testclient import TestClient
app = FastAPI()
@app.get("/users/{user_id}")
async def get_user(user_id: str, request: Request):
route: APIRoute = request.scope["route"]
return {"user_id": user_id, "path": route.path}
@app.websocket("/items/{item_id}")
async def websocket_item(item_id: str, websocket: WebSocket):
route: APIWebSocketRoute = websocket.scope["route"]
await websocket.accept()
await websocket.send_json({"item_id": item_id, "path": route.path})
client = TestClient(app)
def test_get():
response = client.get("/users/rick")
assert response.status_code == 200, response.text
assert response.json() == {"user_id": "rick", "path": "/users/{user_id}"}
def test_invalid_method_doesnt_match():
response = client.post("/users/rick")
assert response.status_code == 405, response.text
def test_invalid_path_doesnt_match():
response = client.post("/usersx/rick")
assert response.status_code == 404, response.text
def test_websocket():
with client.websocket_connect("/items/portal-gun") as websocket:
data = websocket.receive_json()
assert data == {"item_id": "portal-gun", "path": "/items/{item_id}"}
def test_websocket_invalid_path_doesnt_match():
with pytest.raises(WebSocketDisconnect):
with client.websocket_connect("/itemsx/portal-gun") as websocket:
websocket.receive_json()

267
tests/test_tuples.py Normal file
View File

@@ -0,0 +1,267 @@
from typing import List, Tuple
from fastapi import FastAPI, Form
from fastapi.testclient import TestClient
from pydantic import BaseModel
app = FastAPI()
class ItemGroup(BaseModel):
items: List[Tuple[str, str]]
class Coordinate(BaseModel):
x: float
y: float
@app.post("/model-with-tuple/")
def post_model_with_tuple(item_group: ItemGroup):
return item_group
@app.post("/tuple-of-models/")
def post_tuple_of_models(square: Tuple[Coordinate, Coordinate]):
return square
@app.post("/tuple-form/")
def hello(values: Tuple[int, int] = Form(...)):
return values
client = TestClient(app)
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/model-with-tuple/": {
"post": {
"summary": "Post Model With Tuple",
"operationId": "post_model_with_tuple_model_with_tuple__post",
"requestBody": {
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/ItemGroup"}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/tuple-of-models/": {
"post": {
"summary": "Post Tuple Of Models",
"operationId": "post_tuple_of_models_tuple_of_models__post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"title": "Square",
"maxItems": 2,
"minItems": 2,
"type": "array",
"items": [
{"$ref": "#/components/schemas/Coordinate"},
{"$ref": "#/components/schemas/Coordinate"},
],
}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/tuple-form/": {
"post": {
"summary": "Hello",
"operationId": "hello_tuple_form__post",
"requestBody": {
"content": {
"application/x-www-form-urlencoded": {
"schema": {
"$ref": "#/components/schemas/Body_hello_tuple_form__post"
}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
},
"components": {
"schemas": {
"Body_hello_tuple_form__post": {
"title": "Body_hello_tuple_form__post",
"required": ["values"],
"type": "object",
"properties": {
"values": {
"title": "Values",
"maxItems": 2,
"minItems": 2,
"type": "array",
"items": [{"type": "integer"}, {"type": "integer"}],
}
},
},
"Coordinate": {
"title": "Coordinate",
"required": ["x", "y"],
"type": "object",
"properties": {
"x": {"title": "X", "type": "number"},
"y": {"title": "Y", "type": "number"},
},
},
"HTTPValidationError": {
"title": "HTTPValidationError",
"type": "object",
"properties": {
"detail": {
"title": "Detail",
"type": "array",
"items": {"$ref": "#/components/schemas/ValidationError"},
}
},
},
"ItemGroup": {
"title": "ItemGroup",
"required": ["items"],
"type": "object",
"properties": {
"items": {
"title": "Items",
"type": "array",
"items": {
"maxItems": 2,
"minItems": 2,
"type": "array",
"items": [{"type": "string"}, {"type": "string"}],
},
}
},
},
"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_schema():
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == openapi_schema
def test_model_with_tuple_valid():
data = {"items": [["foo", "bar"], ["baz", "whatelse"]]}
response = client.post("/model-with-tuple/", json=data)
assert response.status_code == 200, response.text
assert response.json() == data
def test_model_with_tuple_invalid():
data = {"items": [["foo", "bar"], ["baz", "whatelse", "too", "much"]]}
response = client.post("/model-with-tuple/", json=data)
assert response.status_code == 422, response.text
data = {"items": [["foo", "bar"], ["baz"]]}
response = client.post("/model-with-tuple/", json=data)
assert response.status_code == 422, response.text
def test_tuple_with_model_valid():
data = [{"x": 1, "y": 2}, {"x": 3, "y": 4}]
response = client.post("/tuple-of-models/", json=data)
assert response.status_code == 200, response.text
assert response.json() == data
def test_tuple_with_model_invalid():
data = [{"x": 1, "y": 2}, {"x": 3, "y": 4}, {"x": 5, "y": 6}]
response = client.post("/tuple-of-models/", json=data)
assert response.status_code == 422, response.text
data = [{"x": 1, "y": 2}]
response = client.post("/tuple-of-models/", json=data)
assert response.status_code == 422, response.text
def test_tuple_form_valid():
response = client.post("/tuple-form/", data=[("values", "1"), ("values", "2")])
assert response.status_code == 200, response.text
assert response.json() == [1, 2]
def test_tuple_form_invalid():
response = client.post(
"/tuple-form/", data=[("values", "1"), ("values", "2"), ("values", "3")]
)
assert response.status_code == 422, response.text
response = client.post("/tuple-form/", data=[("values", "1")])
assert response.status_code == 422, response.text

View File

View File

@@ -0,0 +1,188 @@
from fastapi.testclient import TestClient
from docs_src.generate_clients.tutorial003 import app
client = TestClient(app)
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/items/": {
"get": {
"tags": ["items"],
"summary": "Get Items",
"operationId": "items-get_items",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"title": "Response Items-Get Items",
"type": "array",
"items": {"$ref": "#/components/schemas/Item"},
}
}
},
}
},
},
"post": {
"tags": ["items"],
"summary": "Create Item",
"operationId": "items-create_item",
"requestBody": {
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/Item"}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ResponseMessage"
}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
},
},
"/users/": {
"post": {
"tags": ["users"],
"summary": "Create User",
"operationId": "users-create_user",
"requestBody": {
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/User"}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ResponseMessage"
}
}
},
},
"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"},
}
},
},
"Item": {
"title": "Item",
"required": ["name", "price"],
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
"price": {"title": "Price", "type": "number"},
},
},
"ResponseMessage": {
"title": "ResponseMessage",
"required": ["message"],
"type": "object",
"properties": {"message": {"title": "Message", "type": "string"}},
},
"User": {
"title": "User",
"required": ["username", "email"],
"type": "object",
"properties": {
"username": {"title": "Username", "type": "string"},
"email": {"title": "Email", "type": "string"},
},
},
"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_post_items():
response = client.post("/items/", json={"name": "Foo", "price": 5})
assert response.status_code == 200, response.text
assert response.json() == {"message": "Item received"}
def test_post_users():
response = client.post(
"/users/", json={"username": "Foo", "email": "foo@example.com"}
)
assert response.status_code == 200, response.text
assert response.json() == {"message": "User received"}
def test_get_items():
response = client.get("/items/")
assert response.status_code == 200, response.text
assert response.json() == [
{"name": "Plumbus", "price": 3},
{"name": "Portal Gun", "price": 9001},
]

View File

@@ -0,0 +1,56 @@
from fastapi.testclient import TestClient
from docs_src.path_operation_configuration.tutorial002b import app
client = TestClient(app)
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/items/": {
"get": {
"tags": ["items"],
"summary": "Get Items",
"operationId": "get_items_items__get",
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
}
},
}
},
"/users/": {
"get": {
"tags": ["users"],
"summary": "Read Users",
"operationId": "read_users_users__get",
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
}
},
}
},
},
}
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == openapi_schema
def test_get_items():
response = client.get("/items/")
assert response.status_code == 200, response.text
assert response.json() == ["Portal gun", "Plumbus"]
def test_get_users():
response = client.get("/users/")
assert response.status_code == 200, response.text
assert response.json() == ["Rick", "Morty"]

View File

@@ -0,0 +1,82 @@
from fastapi.testclient import TestClient
from docs_src.query_params_str_validations.tutorial014 import app
client = TestClient(app)
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/items/": {
"get": {
"summary": "Read Items",
"operationId": "read_items_items__get",
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"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"},
}
},
},
"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_schema():
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == openapi_schema
def test_hidden_query():
response = client.get("/items?hidden_query=somevalue")
assert response.status_code == 200, response.text
assert response.json() == {"hidden_query": "somevalue"}
def test_no_hidden_query():
response = client.get("/items")
assert response.status_code == 200, response.text
assert response.json() == {"hidden_query": "Not found"}

View File

@@ -0,0 +1,91 @@
import pytest
from fastapi.testclient import TestClient
from ...utils import needs_py310
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/items/": {
"get": {
"summary": "Read Items",
"operationId": "read_items_items__get",
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"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"},
}
},
},
"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"},
},
},
}
},
}
@pytest.fixture(name="client")
def get_client():
from docs_src.query_params_str_validations.tutorial014_py310 import app
client = TestClient(app)
return client
@needs_py310
def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == openapi_schema
@needs_py310
def test_hidden_query(client: TestClient):
response = client.get("/items?hidden_query=somevalue")
assert response.status_code == 200, response.text
assert response.json() == {"hidden_query": "somevalue"}
@needs_py310
def test_no_hidden_query(client: TestClient):
response = client.get("/items")
assert response.status_code == 200, response.text
assert response.json() == {"hidden_query": "Not found"}

View File

@@ -0,0 +1,157 @@
from fastapi.testclient import TestClient
from docs_src.request_files.tutorial001_02 import app
client = TestClient(app)
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/files/": {
"post": {
"summary": "Create File",
"operationId": "create_file_files__post",
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"$ref": "#/components/schemas/Body_create_file_files__post"
}
}
}
},
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/uploadfile/": {
"post": {
"summary": "Create Upload File",
"operationId": "create_upload_file_uploadfile__post",
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post"
}
}
}
},
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
},
"components": {
"schemas": {
"Body_create_file_files__post": {
"title": "Body_create_file_files__post",
"type": "object",
"properties": {
"file": {"title": "File", "type": "string", "format": "binary"}
},
},
"Body_create_upload_file_uploadfile__post": {
"title": "Body_create_upload_file_uploadfile__post",
"type": "object",
"properties": {
"file": {"title": "File", "type": "string", "format": "binary"}
},
},
"HTTPValidationError": {
"title": "HTTPValidationError",
"type": "object",
"properties": {
"detail": {
"title": "Detail",
"type": "array",
"items": {"$ref": "#/components/schemas/ValidationError"},
}
},
},
"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_schema():
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == openapi_schema
def test_post_form_no_body():
response = client.post("/files/")
assert response.status_code == 200, response.text
assert response.json() == {"message": "No file sent"}
def test_post_uploadfile_no_body():
response = client.post("/uploadfile/")
assert response.status_code == 200, response.text
assert response.json() == {"message": "No upload file sent"}
def test_post_file(tmp_path):
path = tmp_path / "test.txt"
path.write_bytes(b"<file content>")
client = TestClient(app)
with path.open("rb") as file:
response = client.post("/files/", files={"file": file})
assert response.status_code == 200, response.text
assert response.json() == {"file_size": 14}
def test_post_upload_file(tmp_path):
path = tmp_path / "test.txt"
path.write_bytes(b"<file content>")
client = TestClient(app)
with path.open("rb") as file:
response = client.post("/uploadfile/", files={"file": file})
assert response.status_code == 200, response.text
assert response.json() == {"filename": "test.txt"}

View File

@@ -0,0 +1,169 @@
from pathlib import Path
import pytest
from fastapi.testclient import TestClient
from ...utils import needs_py310
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/files/": {
"post": {
"summary": "Create File",
"operationId": "create_file_files__post",
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"$ref": "#/components/schemas/Body_create_file_files__post"
}
}
}
},
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/uploadfile/": {
"post": {
"summary": "Create Upload File",
"operationId": "create_upload_file_uploadfile__post",
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post"
}
}
}
},
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
},
"components": {
"schemas": {
"Body_create_file_files__post": {
"title": "Body_create_file_files__post",
"type": "object",
"properties": {
"file": {"title": "File", "type": "string", "format": "binary"}
},
},
"Body_create_upload_file_uploadfile__post": {
"title": "Body_create_upload_file_uploadfile__post",
"type": "object",
"properties": {
"file": {"title": "File", "type": "string", "format": "binary"}
},
},
"HTTPValidationError": {
"title": "HTTPValidationError",
"type": "object",
"properties": {
"detail": {
"title": "Detail",
"type": "array",
"items": {"$ref": "#/components/schemas/ValidationError"},
}
},
},
"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"},
},
},
}
},
}
@pytest.fixture(name="client")
def get_client():
from docs_src.request_files.tutorial001_02_py310 import app
client = TestClient(app)
return client
@needs_py310
def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == openapi_schema
@needs_py310
def test_post_form_no_body(client: TestClient):
response = client.post("/files/")
assert response.status_code == 200, response.text
assert response.json() == {"message": "No file sent"}
@needs_py310
def test_post_uploadfile_no_body(client: TestClient):
response = client.post("/uploadfile/")
assert response.status_code == 200, response.text
assert response.json() == {"message": "No upload file sent"}
@needs_py310
def test_post_file(tmp_path: Path, client: TestClient):
path = tmp_path / "test.txt"
path.write_bytes(b"<file content>")
with path.open("rb") as file:
response = client.post("/files/", files={"file": file})
assert response.status_code == 200, response.text
assert response.json() == {"file_size": 14}
@needs_py310
def test_post_upload_file(tmp_path: Path, client: TestClient):
path = tmp_path / "test.txt"
path.write_bytes(b"<file content>")
with path.open("rb") as file:
response = client.post("/uploadfile/", files={"file": file})
assert response.status_code == 200, response.text
assert response.json() == {"filename": "test.txt"}

View File

@@ -0,0 +1,159 @@
from fastapi.testclient import TestClient
from docs_src.request_files.tutorial001_03 import app
client = TestClient(app)
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/files/": {
"post": {
"summary": "Create File",
"operationId": "create_file_files__post",
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"$ref": "#/components/schemas/Body_create_file_files__post"
}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/uploadfile/": {
"post": {
"summary": "Create Upload File",
"operationId": "create_upload_file_uploadfile__post",
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post"
}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
},
"components": {
"schemas": {
"Body_create_file_files__post": {
"title": "Body_create_file_files__post",
"required": ["file"],
"type": "object",
"properties": {
"file": {
"title": "File",
"type": "string",
"description": "A file read as bytes",
"format": "binary",
}
},
},
"Body_create_upload_file_uploadfile__post": {
"title": "Body_create_upload_file_uploadfile__post",
"required": ["file"],
"type": "object",
"properties": {
"file": {
"title": "File",
"type": "string",
"description": "A file read as UploadFile",
"format": "binary",
}
},
},
"HTTPValidationError": {
"title": "HTTPValidationError",
"type": "object",
"properties": {
"detail": {
"title": "Detail",
"type": "array",
"items": {"$ref": "#/components/schemas/ValidationError"},
}
},
},
"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_schema():
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == openapi_schema
def test_post_file(tmp_path):
path = tmp_path / "test.txt"
path.write_bytes(b"<file content>")
client = TestClient(app)
with path.open("rb") as file:
response = client.post("/files/", files={"file": file})
assert response.status_code == 200, response.text
assert response.json() == {"file_size": 14}
def test_post_upload_file(tmp_path):
path = tmp_path / "test.txt"
path.write_bytes(b"<file content>")
client = TestClient(app)
with path.open("rb") as file:
response = client.post("/uploadfile/", files={"file": file})
assert response.status_code == 200, response.text
assert response.json() == {"filename": "test.txt"}

View File

@@ -0,0 +1,194 @@
from fastapi.testclient import TestClient
from docs_src.request_files.tutorial003 import app
client = TestClient(app)
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/files/": {
"post": {
"summary": "Create Files",
"operationId": "create_files_files__post",
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"$ref": "#/components/schemas/Body_create_files_files__post"
}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/uploadfiles/": {
"post": {
"summary": "Create Upload Files",
"operationId": "create_upload_files_uploadfiles__post",
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"$ref": "#/components/schemas/Body_create_upload_files_uploadfiles__post"
}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/": {
"get": {
"summary": "Main",
"operationId": "main__get",
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
}
},
}
},
},
"components": {
"schemas": {
"Body_create_files_files__post": {
"title": "Body_create_files_files__post",
"required": ["files"],
"type": "object",
"properties": {
"files": {
"title": "Files",
"type": "array",
"items": {"type": "string", "format": "binary"},
"description": "Multiple files as bytes",
}
},
},
"Body_create_upload_files_uploadfiles__post": {
"title": "Body_create_upload_files_uploadfiles__post",
"required": ["files"],
"type": "object",
"properties": {
"files": {
"title": "Files",
"type": "array",
"items": {"type": "string", "format": "binary"},
"description": "Multiple files as UploadFile",
}
},
},
"HTTPValidationError": {
"title": "HTTPValidationError",
"type": "object",
"properties": {
"detail": {
"title": "Detail",
"type": "array",
"items": {"$ref": "#/components/schemas/ValidationError"},
}
},
},
"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_schema():
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == openapi_schema
def test_post_files(tmp_path):
path = tmp_path / "test.txt"
path.write_bytes(b"<file content>")
path2 = tmp_path / "test2.txt"
path2.write_bytes(b"<file content2>")
client = TestClient(app)
with path.open("rb") as file, path2.open("rb") as file2:
response = client.post(
"/files/",
files=(
("files", ("test.txt", file)),
("files", ("test2.txt", file2)),
),
)
assert response.status_code == 200, response.text
assert response.json() == {"file_sizes": [14, 15]}
def test_post_upload_file(tmp_path):
path = tmp_path / "test.txt"
path.write_bytes(b"<file content>")
path2 = tmp_path / "test2.txt"
path2.write_bytes(b"<file content2>")
client = TestClient(app)
with path.open("rb") as file, path2.open("rb") as file2:
response = client.post(
"/uploadfiles/",
files=(
("files", ("test.txt", file)),
("files", ("test2.txt", file2)),
),
)
assert response.status_code == 200, response.text
assert response.json() == {"filenames": ["test.txt", "test2.txt"]}
def test_get_root():
client = TestClient(app)
response = client.get("/")
assert response.status_code == 200, response.text
assert b"<form" in response.content

View File

@@ -0,0 +1,223 @@
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from ...utils import needs_py39
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/files/": {
"post": {
"summary": "Create Files",
"operationId": "create_files_files__post",
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"$ref": "#/components/schemas/Body_create_files_files__post"
}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/uploadfiles/": {
"post": {
"summary": "Create Upload Files",
"operationId": "create_upload_files_uploadfiles__post",
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"$ref": "#/components/schemas/Body_create_upload_files_uploadfiles__post"
}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/": {
"get": {
"summary": "Main",
"operationId": "main__get",
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
}
},
}
},
},
"components": {
"schemas": {
"Body_create_files_files__post": {
"title": "Body_create_files_files__post",
"required": ["files"],
"type": "object",
"properties": {
"files": {
"title": "Files",
"type": "array",
"items": {"type": "string", "format": "binary"},
"description": "Multiple files as bytes",
}
},
},
"Body_create_upload_files_uploadfiles__post": {
"title": "Body_create_upload_files_uploadfiles__post",
"required": ["files"],
"type": "object",
"properties": {
"files": {
"title": "Files",
"type": "array",
"items": {"type": "string", "format": "binary"},
"description": "Multiple files as UploadFile",
}
},
},
"HTTPValidationError": {
"title": "HTTPValidationError",
"type": "object",
"properties": {
"detail": {
"title": "Detail",
"type": "array",
"items": {"$ref": "#/components/schemas/ValidationError"},
}
},
},
"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"},
},
},
}
},
}
@pytest.fixture(name="app")
def get_app():
from docs_src.request_files.tutorial003_py39 import app
return app
@pytest.fixture(name="client")
def get_client(app: FastAPI):
client = TestClient(app)
return client
@needs_py39
def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == openapi_schema
file_required = {
"detail": [
{
"loc": ["body", "files"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
@needs_py39
def test_post_files(tmp_path, app: FastAPI):
path = tmp_path / "test.txt"
path.write_bytes(b"<file content>")
path2 = tmp_path / "test2.txt"
path2.write_bytes(b"<file content2>")
client = TestClient(app)
with path.open("rb") as file, path2.open("rb") as file2:
response = client.post(
"/files/",
files=(
("files", ("test.txt", file)),
("files", ("test2.txt", file2)),
),
)
assert response.status_code == 200, response.text
assert response.json() == {"file_sizes": [14, 15]}
@needs_py39
def test_post_upload_file(tmp_path, app: FastAPI):
path = tmp_path / "test.txt"
path.write_bytes(b"<file content>")
path2 = tmp_path / "test2.txt"
path2.write_bytes(b"<file content2>")
client = TestClient(app)
with path.open("rb") as file, path2.open("rb") as file2:
response = client.post(
"/uploadfiles/",
files=(
("files", ("test.txt", file)),
("files", ("test2.txt", file2)),
),
)
assert response.status_code == 200, response.text
assert response.json() == {"filenames": ["test.txt", "test2.txt"]}
@needs_py39
def test_get_root(app: FastAPI):
client = TestClient(app)
response = client.get("/")
assert response.status_code == 200, response.text
assert b"<form" in response.content