Compare commits

...

31 Commits

Author SHA1 Message Date
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
52 changed files with 2708 additions and 173 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

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

@@ -3,6 +3,99 @@
## Latest Changes
## 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

@@ -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,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.74.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,
@@ -20,8 +21,9 @@ from fastapi.params import Depends
from fastapi.types import DecoratedCallable
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
@@ -133,6 +135,55 @@ class FastAPI(Starlette):
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 +256,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 +265,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,
@@ -273,7 +319,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,
@@ -342,7 +388,7 @@ 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,
@@ -368,7 +414,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,
@@ -419,7 +465,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,
@@ -470,7 +516,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,
@@ -521,7 +567,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,
@@ -572,7 +618,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,
@@ -623,7 +669,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,
@@ -674,7 +720,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,
@@ -725,7 +771,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,

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

@@ -92,6 +92,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,

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,
@@ -305,7 +305,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,
@@ -330,7 +330,7 @@ class APIRoute(routing.Route):
openapi_extra: Optional[Dict[str, Any]] = None,
) -> None:
# normalise enums e.g. http.HTTPStatus
if isinstance(status_code, enum.IntEnum):
if isinstance(status_code, IntEnum):
status_code = int(status_code)
self.path = path
self.endpoint = endpoint
@@ -438,7 +438,7 @@ class APIRouter(routing.Router):
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,
@@ -466,7 +466,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
@@ -483,7 +483,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,
@@ -557,7 +557,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,
@@ -634,7 +634,7 @@ 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,
@@ -738,7 +738,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,
@@ -790,7 +790,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,
@@ -842,7 +842,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,
@@ -894,7 +894,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,
@@ -946,7 +946,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,
@@ -998,7 +998,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,
@@ -1050,7 +1050,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,
@@ -1102,7 +1102,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,

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

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

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

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