Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c89dc736bd | ||
|
|
19769d0483 | ||
|
|
586d17bfb5 | ||
|
|
786b5858e5 | ||
|
|
8a0d4c79c1 | ||
|
|
440d2d2d1a | ||
|
|
e953059054 | ||
|
|
f5d7df3c6c | ||
|
|
1ce16c2f40 | ||
|
|
03d4d4c38e | ||
|
|
2d2f1dee5d | ||
|
|
59e36481dc | ||
|
|
4fcb00328c | ||
|
|
9d56a3cb59 | ||
|
|
78b07cb809 | ||
|
|
6034f80687 | ||
|
|
b93f8a709a | ||
|
|
618c99d774 |
2
.github/workflows/build-docs.yml
vendored
@@ -1,6 +1,8 @@
|
||||
name: Build Docs
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
jobs:
|
||||
|
||||
@@ -50,6 +50,7 @@ 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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,6 +8,9 @@ gold:
|
||||
- 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
|
||||
|
||||
@@ -8,3 +8,4 @@ logins:
|
||||
- deepset-ai
|
||||
- cryptapi
|
||||
- DropbaseHQ
|
||||
- Striveworks
|
||||
|
||||
267
docs/en/docs/advanced/generate-clients.md
Normal file
@@ -0,0 +1,267 @@
|
||||
# Generate Clients
|
||||
|
||||
As **FastAPI** is based on the OpenAPI specification, you get automatic compatibility with many tools, including the automatic API docs (provided by Swagger UI).
|
||||
|
||||
One particular advantage that is not necessarily obvious is that you can **generate clients** (sometimes called <abbr title="Software Development Kits">**SDKs**</abbr> ) for your API, for many different **programming languages**.
|
||||
|
||||
## OpenAPI Client Generators
|
||||
|
||||
There are many tools to generate clients from **OpenAPI**.
|
||||
|
||||
A common tool is <a href="https://openapi-generator.tech/" class="external-link" target="_blank">OpenAPI Generator</a>.
|
||||
|
||||
If you are building a **frontend**, a very interesting alternative is <a href="https://github.com/ferdikoomen/openapi-typescript-codegen" class="external-link" target="_blank">openapi-typescript-codegen</a>.
|
||||
|
||||
## Generate a TypeScript Frontend Client
|
||||
|
||||
Let's start with a simple FastAPI application:
|
||||
|
||||
=== "Python 3.6 and above"
|
||||
|
||||
```Python hl_lines="9-11 14-15 18 19 23"
|
||||
{!> ../../../docs_src/generate_clients/tutorial001.py!}
|
||||
```
|
||||
|
||||
=== "Python 3.9 and above"
|
||||
|
||||
```Python hl_lines="7-9 12-13 16-17 21"
|
||||
{!> ../../../docs_src/generate_clients/tutorial001_py39.py!}
|
||||
```
|
||||
|
||||
Notice that the *path operations* define the models they use for request payload and response payload, using the models `Item` and `ResponseMessage`.
|
||||
|
||||
### API Docs
|
||||
|
||||
If you go to the API docs, you will see that it has the **schemas** for the data to be sent in requests and received in responses:
|
||||
|
||||
<img src="/img/tutorial/generate-clients/image01.png">
|
||||
|
||||
You can see those schemas because they were declared with the models in the app.
|
||||
|
||||
That information is available in the app's **OpenAPI schema**, and then shown in the API docs (by Swagger UI).
|
||||
|
||||
And that same information from the models that is included in OpenAPI is what can be used to **generate the client code**.
|
||||
|
||||
### Generate a TypeScript Client
|
||||
|
||||
Now that we have the app with the models, we can generate the client code for the frontend.
|
||||
|
||||
#### Install `openapi-typescript-codegen`
|
||||
|
||||
You can install `openapi-typescript-codegen` in your frontend code with:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ npm install openapi-typescript-codegen --save-dev
|
||||
|
||||
---> 100%
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
#### Generate Client Code
|
||||
|
||||
To generate the client code you can use the command line application `openapi` that would now be installed.
|
||||
|
||||
Because it is installed in the local project, you probably wouldn't be able to call that command directly, but you would put it on your `package.json` file.
|
||||
|
||||
It could look like this:
|
||||
|
||||
```JSON hl_lines="7"
|
||||
{
|
||||
"name": "frontend-app",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"generate-client": "openapi --input http://localhost:8000/openapi.json --output ./src/client --client axios"
|
||||
},
|
||||
"author": "",
|
||||
"license": "",
|
||||
"devDependencies": {
|
||||
"openapi-typescript-codegen": "^0.20.1",
|
||||
"typescript": "^4.6.2"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
After having that NPM `generate-client` script there, you can run it with:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ npm run generate-client
|
||||
|
||||
frontend-app@1.0.0 generate-client /home/user/code/frontend-app
|
||||
> openapi --input http://localhost:8000/openapi.json --output ./src/client --client axios
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
That command will generate code in `./src/client` and will use `axios` (the frontend HTTP library) internally.
|
||||
|
||||
### Try Out the Client Code
|
||||
|
||||
Now you can import and use the client code, it could look like this, notice that you get autocompletion for the methods:
|
||||
|
||||
<img src="/img/tutorial/generate-clients/image02.png">
|
||||
|
||||
You will also get autocompletion for the payload to send:
|
||||
|
||||
<img src="/img/tutorial/generate-clients/image03.png">
|
||||
|
||||
!!! tip
|
||||
Notice the autocompletion for `name` and `price`, that was defined in the FastAPI application, in the `Item` model.
|
||||
|
||||
You will have inline errors for the data that you send:
|
||||
|
||||
<img src="/img/tutorial/generate-clients/image04.png">
|
||||
|
||||
The response object will also have autocompletion:
|
||||
|
||||
<img src="/img/tutorial/generate-clients/image05.png">
|
||||
|
||||
## FastAPI App with Tags
|
||||
|
||||
In many cases your FastAPI app will be bigger, and you will probably use tags to separate different groups of *path operations*.
|
||||
|
||||
For example, you could have a section for **items** and another section for **users**, and they could be separated by tags:
|
||||
|
||||
|
||||
=== "Python 3.6 and above"
|
||||
|
||||
```Python hl_lines="23 28 36"
|
||||
{!> ../../../docs_src/generate_clients/tutorial002.py!}
|
||||
```
|
||||
|
||||
=== "Python 3.9 and above"
|
||||
|
||||
```Python hl_lines="21 26 34"
|
||||
{!> ../../../docs_src/generate_clients/tutorial002_py39.py!}
|
||||
```
|
||||
|
||||
### Generate a TypeScript Client with Tags
|
||||
|
||||
If you generate a client for a FastAPI app using tags, it will normally also separate the client code based on the tags.
|
||||
|
||||
This way you will be able to have things ordered and grouped correctly for the client code:
|
||||
|
||||
<img src="/img/tutorial/generate-clients/image06.png">
|
||||
|
||||
In this case you have:
|
||||
|
||||
* `ItemsService`
|
||||
* `UsersService`
|
||||
|
||||
### Client Method Names
|
||||
|
||||
Right now the generated method names like `createItemItemsPost` don't look very clean:
|
||||
|
||||
```TypeScript
|
||||
ItemsService.createItemItemsPost({name: "Plumbus", price: 5})
|
||||
```
|
||||
|
||||
...that's because the client generator uses the OpenAPI internal **operation ID** for each *path operation*.
|
||||
|
||||
OpenAPI requires that each operation ID is unique across all the *path operations*, so FastAPI uses the **function name**, the **path**, and the **HTTP method/operation** to generate that operation ID, because that way it can make sure that the operation IDs are unique.
|
||||
|
||||
But I'll show you how to improve that next. 🤓
|
||||
|
||||
## Custom Operation IDs and Better Method Names
|
||||
|
||||
You can **modify** the way these operation IDs are **generated** to make them simpler and have **simpler method names** in the clients.
|
||||
|
||||
In this case you will have to ensure that each operation ID is **unique** in some other way.
|
||||
|
||||
For example, you could make sure that each *path operation* has a tag, and then generate the operation ID based on the **tag** and the *path operation* **name** (the function name).
|
||||
|
||||
### Custom Generate Unique ID Function
|
||||
|
||||
FastAPI uses a **unique ID** for each *path operation*, it is used for the **operation ID** and also for the names of any needed custom models, for requests or responses.
|
||||
|
||||
You can customize that function. It takes an `APIRoute` and outputs a string.
|
||||
|
||||
For example, here it is using the first tag (you will probably have only one tag) and the *path operation* name (the function name).
|
||||
|
||||
You can then pass that custom function to **FastAPI** as the `generate_unique_id_function` parameter:
|
||||
|
||||
=== "Python 3.6 and above"
|
||||
|
||||
```Python hl_lines="8-9 12"
|
||||
{!> ../../../docs_src/generate_clients/tutorial003.py!}
|
||||
```
|
||||
|
||||
=== "Python 3.9 and above"
|
||||
|
||||
```Python hl_lines="6-7 10"
|
||||
{!> ../../../docs_src/generate_clients/tutorial003_py39.py!}
|
||||
```
|
||||
|
||||
### Generate a TypeScript Client with Custom Operation IDs
|
||||
|
||||
Now if you generate the client again, you will see that it has the improved method names:
|
||||
|
||||
<img src="/img/tutorial/generate-clients/image07.png">
|
||||
|
||||
As you see, the method names now have the tag and then the function name, now they don't include information from the URL path and the HTTP operation.
|
||||
|
||||
### Preprocess the OpenAPI Specification for the Client Generator
|
||||
|
||||
The generated code still has some **duplicated information**.
|
||||
|
||||
We already know that this method is related to the **items** because that word is in the `ItemsService` (taken from the tag), but we still have the tag name prefixed in the method name too. 😕
|
||||
|
||||
We will probably still want to keep it for OpenAPI in general, as that will ensure that the operation IDs are **unique**.
|
||||
|
||||
But for the generated client we could **modify** the OpenAPI operation IDs right before generating the clients, just to make those method names nicer and **cleaner**.
|
||||
|
||||
We could download the OpenAPI JSON to a file `openapi.json` and then we could **remove that prefixed tag** with a script like this:
|
||||
|
||||
```Python
|
||||
{!../../../docs_src/generate_clients/tutorial004.py!}
|
||||
```
|
||||
|
||||
With that, the operation IDs would be renamed from things like `items-get_items` to just `get_items`, that way the client generator can generate simpler method names.
|
||||
|
||||
### Generate a TypeScript Client with the Preprocessed OpenAPI
|
||||
|
||||
Now as the end result is in a file `openapi.json`, you would modify the `package.json` to use that local file, for example:
|
||||
|
||||
```JSON hl_lines="7"
|
||||
{
|
||||
"name": "frontend-app",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"generate-client": "openapi --input ./openapi.json --output ./src/client --client axios"
|
||||
},
|
||||
"author": "",
|
||||
"license": "",
|
||||
"devDependencies": {
|
||||
"openapi-typescript-codegen": "^0.20.1",
|
||||
"typescript": "^4.6.2"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
After generating the new client, you would now have **clean method names**, with all the **autocompletion**, **inline errors**, etc:
|
||||
|
||||
<img src="/img/tutorial/generate-clients/image08.png">
|
||||
|
||||
## Benefits
|
||||
|
||||
When using the automatically generated clients you would **autocompletion** for:
|
||||
|
||||
* Methods.
|
||||
* Request payloads in the body, query parameters, etc.
|
||||
* Response payloads.
|
||||
|
||||
You would also have **inline errors** for everything.
|
||||
|
||||
And whenever you update the backend code, and **regenerate** the frontend, it would have any new *path operations* available as methods, the old ones removed, and any other change would be reflected on the generated code. 🤓
|
||||
|
||||
This also means that if something changed it will be **reflected** on the client code automatically. And if you **build** the client it will error out if you have any **mismatch** in the data used.
|
||||
|
||||
So, you would **detect many errors** very early in the development cycle instead of having to wait for the errors to show up to your final users in production and then trying to debug where the problem is. ✨
|
||||
BIN
docs/en/docs/img/sponsors/striveworks-banner.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
docs/en/docs/img/sponsors/striveworks.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
docs/en/docs/img/tutorial/generate-clients/image01.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
docs/en/docs/img/tutorial/generate-clients/image02.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
docs/en/docs/img/tutorial/generate-clients/image03.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
docs/en/docs/img/tutorial/generate-clients/image04.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
docs/en/docs/img/tutorial/generate-clients/image05.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
docs/en/docs/img/tutorial/generate-clients/image06.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
docs/en/docs/img/tutorial/generate-clients/image07.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
docs/en/docs/img/tutorial/generate-clients/image08.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
@@ -2,6 +2,89 @@
|
||||
|
||||
## Latest Changes
|
||||
|
||||
|
||||
## 0.75.0
|
||||
|
||||
### Features
|
||||
|
||||
* ✨ Add support for custom `generate_unique_id_function` and docs for generating clients. New docs: [Advanced - Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/). PR [#4650](https://github.com/tiangolo/fastapi/pull/4650) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
## 0.74.1
|
||||
|
||||
### Features
|
||||
|
||||
* ✨ Include route in scope to allow middleware and other tools to extract its information. PR [#4603](https://github.com/tiangolo/fastapi/pull/4603) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
## 0.74.0
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* ✨ Update internal `AsyncExitStack` to fix context for dependencies with `yield`. PR [#4575](https://github.com/tiangolo/fastapi/pull/4575) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
Dependencies with `yield` can now catch `HTTPException` and custom exceptions. For example:
|
||||
|
||||
```Python
|
||||
async def get_database():
|
||||
with Session() as session:
|
||||
try:
|
||||
yield session
|
||||
except HTTPException:
|
||||
session.rollback()
|
||||
raise
|
||||
finally:
|
||||
session.close()
|
||||
```
|
||||
|
||||
After the dependency with `yield` handles the exception (or not) the exception is raised again. So that any exception handlers can catch it, or ultimately the default internal `ServerErrorMiddleware`.
|
||||
|
||||
If you depended on exceptions not being received by dependencies with `yield`, and receiving an exception breaks the code after `yield`, you can use a block with `try` and `finally`:
|
||||
|
||||
```Python
|
||||
async def do_something():
|
||||
try:
|
||||
yield something
|
||||
finally:
|
||||
some_cleanup()
|
||||
```
|
||||
|
||||
...that way the `finally` block is run regardless of any exception that might happen.
|
||||
|
||||
### Features
|
||||
|
||||
* The same PR [#4575](https://github.com/tiangolo/fastapi/pull/4575) from above also fixes the `contextvars` context for the code before and after `yield`. This was the main objective of that PR.
|
||||
|
||||
This means that now, if you set a value in a context variable before `yield`, the value would still be available after `yield` (as you would intuitively expect). And it also means that you can reset the context variable with a token afterwards.
|
||||
|
||||
For example, this works correctly now:
|
||||
|
||||
```Python
|
||||
from contextvars import ContextVar
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
|
||||
legacy_request_state_context_var: ContextVar[Optional[Dict[str, Any]]] = ContextVar(
|
||||
"legacy_request_state_context_var", default=None
|
||||
)
|
||||
|
||||
async def set_up_request_state_dependency():
|
||||
request_state = {"user": "deadpond"}
|
||||
contextvar_token = legacy_request_state_context_var.set(request_state)
|
||||
yield request_state
|
||||
legacy_request_state_context_var.reset(contextvar_token)
|
||||
```
|
||||
|
||||
...before this change it would raise an error when resetting the context variable, because the `contextvars` context was different, because of the way it was implemented.
|
||||
|
||||
**Note**: You probably don't need `contextvars`, and you should probably avoid using them. But they are powerful and useful in some advanced scenarios, for example, migrating from code that used Flask's `g` semi-global variable.
|
||||
|
||||
**Technical Details**: If you want to know more of the technical details you can check out the PR description [#4575](https://github.com/tiangolo/fastapi/pull/4575).
|
||||
|
||||
### Internal
|
||||
|
||||
* 🔧 Add Striveworks sponsor. PR [#4596](https://github.com/tiangolo/fastapi/pull/4596) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 💚 Only build docs on push when on master to avoid duplicate runs from PRs. PR [#4564](https://github.com/tiangolo/fastapi/pull/4564) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 👥 Update FastAPI People. PR [#4502](https://github.com/tiangolo/fastapi/pull/4502) by [@github-actions[bot]](https://github.com/apps/github-actions).
|
||||
|
||||
## 0.73.0
|
||||
|
||||
### Features
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -140,6 +140,7 @@ nav:
|
||||
- advanced/extending-openapi.md
|
||||
- advanced/openapi-callbacks.md
|
||||
- advanced/wsgi.md
|
||||
- advanced/generate-clients.md
|
||||
- async.md
|
||||
- Deployment:
|
||||
- deployment/index.md
|
||||
|
||||
@@ -52,6 +52,12 @@
|
||||
<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 %}
|
||||
|
||||
28
docs_src/generate_clients/tutorial001.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from typing import List
|
||||
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
price: float
|
||||
|
||||
|
||||
class ResponseMessage(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
@app.post("/items/", response_model=ResponseMessage)
|
||||
async def create_item(item: Item):
|
||||
return {"message": "item received"}
|
||||
|
||||
|
||||
@app.get("/items/", response_model=List[Item])
|
||||
async def get_items():
|
||||
return [
|
||||
{"name": "Plumbus", "price": 3},
|
||||
{"name": "Portal Gun", "price": 9001},
|
||||
]
|
||||
26
docs_src/generate_clients/tutorial001_py39.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
price: float
|
||||
|
||||
|
||||
class ResponseMessage(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
@app.post("/items/", response_model=ResponseMessage)
|
||||
async def create_item(item: Item):
|
||||
return {"message": "item received"}
|
||||
|
||||
|
||||
@app.get("/items/", response_model=list[Item])
|
||||
async def get_items():
|
||||
return [
|
||||
{"name": "Plumbus", "price": 3},
|
||||
{"name": "Portal Gun", "price": 9001},
|
||||
]
|
||||
38
docs_src/generate_clients/tutorial002.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from typing import List
|
||||
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
price: float
|
||||
|
||||
|
||||
class ResponseMessage(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
username: str
|
||||
email: str
|
||||
|
||||
|
||||
@app.post("/items/", response_model=ResponseMessage, tags=["items"])
|
||||
async def create_item(item: Item):
|
||||
return {"message": "Item received"}
|
||||
|
||||
|
||||
@app.get("/items/", response_model=List[Item], tags=["items"])
|
||||
async def get_items():
|
||||
return [
|
||||
{"name": "Plumbus", "price": 3},
|
||||
{"name": "Portal Gun", "price": 9001},
|
||||
]
|
||||
|
||||
|
||||
@app.post("/users/", response_model=ResponseMessage, tags=["users"])
|
||||
async def create_user(user: User):
|
||||
return {"message": "User received"}
|
||||
36
docs_src/generate_clients/tutorial002_py39.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
price: float
|
||||
|
||||
|
||||
class ResponseMessage(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
username: str
|
||||
email: str
|
||||
|
||||
|
||||
@app.post("/items/", response_model=ResponseMessage, tags=["items"])
|
||||
async def create_item(item: Item):
|
||||
return {"message": "Item received"}
|
||||
|
||||
|
||||
@app.get("/items/", response_model=list[Item], tags=["items"])
|
||||
async def get_items():
|
||||
return [
|
||||
{"name": "Plumbus", "price": 3},
|
||||
{"name": "Portal Gun", "price": 9001},
|
||||
]
|
||||
|
||||
|
||||
@app.post("/users/", response_model=ResponseMessage, tags=["users"])
|
||||
async def create_user(user: User):
|
||||
return {"message": "User received"}
|
||||
44
docs_src/generate_clients/tutorial003.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from typing import List
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.routing import APIRoute
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
def custom_generate_unique_id(route: APIRoute):
|
||||
return f"{route.tags[0]}-{route.name}"
|
||||
|
||||
|
||||
app = FastAPI(generate_unique_id_function=custom_generate_unique_id)
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
price: float
|
||||
|
||||
|
||||
class ResponseMessage(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
username: str
|
||||
email: str
|
||||
|
||||
|
||||
@app.post("/items/", response_model=ResponseMessage, tags=["items"])
|
||||
async def create_item(item: Item):
|
||||
return {"message": "Item received"}
|
||||
|
||||
|
||||
@app.get("/items/", response_model=List[Item], tags=["items"])
|
||||
async def get_items():
|
||||
return [
|
||||
{"name": "Plumbus", "price": 3},
|
||||
{"name": "Portal Gun", "price": 9001},
|
||||
]
|
||||
|
||||
|
||||
@app.post("/users/", response_model=ResponseMessage, tags=["users"])
|
||||
async def create_user(user: User):
|
||||
return {"message": "User received"}
|
||||
42
docs_src/generate_clients/tutorial003_py39.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.routing import APIRoute
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
def custom_generate_unique_id(route: APIRoute):
|
||||
return f"{route.tags[0]}-{route.name}"
|
||||
|
||||
|
||||
app = FastAPI(generate_unique_id_function=custom_generate_unique_id)
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
price: float
|
||||
|
||||
|
||||
class ResponseMessage(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
username: str
|
||||
email: str
|
||||
|
||||
|
||||
@app.post("/items/", response_model=ResponseMessage, tags=["items"])
|
||||
async def create_item(item: Item):
|
||||
return {"message": "Item received"}
|
||||
|
||||
|
||||
@app.get("/items/", response_model=list[Item], tags=["items"])
|
||||
async def get_items():
|
||||
return [
|
||||
{"name": "Plumbus", "price": 3},
|
||||
{"name": "Portal Gun", "price": 9001},
|
||||
]
|
||||
|
||||
|
||||
@app.post("/users/", response_model=ResponseMessage, tags=["users"])
|
||||
async def create_user(user: User):
|
||||
return {"message": "User received"}
|
||||
15
docs_src/generate_clients/tutorial004.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
file_path = Path("./openapi.json")
|
||||
openapi_content = json.loads(file_path.read_text())
|
||||
|
||||
for path_data in openapi_content["paths"].values():
|
||||
for operation in path_data.values():
|
||||
tag = operation["tags"][0]
|
||||
operation_id = operation["operationId"]
|
||||
to_remove = f"{tag}-"
|
||||
new_operation_id = operation_id[len(to_remove) :]
|
||||
operation["operationId"] = new_operation_id
|
||||
|
||||
file_path.write_text(json.dumps(openapi_content))
|
||||
@@ -1,6 +1,6 @@
|
||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.73.0"
|
||||
__version__ = "0.75.0"
|
||||
|
||||
from starlette import status as status
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ 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 (
|
||||
@@ -11,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,
|
||||
@@ -19,10 +19,12 @@ from fastapi.openapi.docs import (
|
||||
from fastapi.openapi.utils import get_openapi
|
||||
from fastapi.params import Depends
|
||||
from fastapi.types import DecoratedCallable
|
||||
from fastapi.utils import generate_unique_id
|
||||
from starlette.applications import Starlette
|
||||
from starlette.datastructures import State
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.exceptions import ExceptionMiddleware, HTTPException
|
||||
from starlette.middleware import Middleware
|
||||
from starlette.middleware.errors import ServerErrorMiddleware
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import HTMLResponse, JSONResponse, Response
|
||||
from starlette.routing import BaseRoute
|
||||
@@ -67,10 +69,44 @@ class FastAPI(Starlette):
|
||||
deprecated: Optional[bool] = None,
|
||||
include_in_schema: bool = True,
|
||||
swagger_ui_parameters: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
**extra: Any,
|
||||
) -> None:
|
||||
self._debug: bool = debug
|
||||
self.title = title
|
||||
self.description = description
|
||||
self.version = version
|
||||
self.terms_of_service = terms_of_service
|
||||
self.contact = contact
|
||||
self.license_info = license_info
|
||||
self.openapi_url = openapi_url
|
||||
self.openapi_tags = openapi_tags
|
||||
self.root_path_in_servers = root_path_in_servers
|
||||
self.docs_url = docs_url
|
||||
self.redoc_url = redoc_url
|
||||
self.swagger_ui_oauth2_redirect_url = swagger_ui_oauth2_redirect_url
|
||||
self.swagger_ui_init_oauth = swagger_ui_init_oauth
|
||||
self.swagger_ui_parameters = swagger_ui_parameters
|
||||
self.servers = servers or []
|
||||
self.extra = extra
|
||||
self.openapi_version = "3.0.2"
|
||||
self.openapi_schema: Optional[Dict[str, Any]] = None
|
||||
if self.openapi_url:
|
||||
assert self.title, "A title must be provided for OpenAPI, e.g.: 'My API'"
|
||||
assert self.version, "A version must be provided for OpenAPI, e.g.: '2.1.0'"
|
||||
# TODO: remove when discarding the openapi_prefix parameter
|
||||
if openapi_prefix:
|
||||
logger.warning(
|
||||
'"openapi_prefix" has been deprecated in favor of "root_path", which '
|
||||
"follows more closely the ASGI standard, is simpler, and more "
|
||||
"automatic. Check the docs at "
|
||||
"https://fastapi.tiangolo.com/advanced/sub-applications/"
|
||||
)
|
||||
self.root_path = root_path or openapi_prefix
|
||||
self.state: State = State()
|
||||
self.dependency_overrides: Dict[Callable[..., Any], Callable[..., Any]] = {}
|
||||
self.router: routing.APIRouter = routing.APIRouter(
|
||||
routes=routes,
|
||||
dependency_overrides_provider=self,
|
||||
@@ -82,6 +118,7 @@ class FastAPI(Starlette):
|
||||
deprecated=deprecated,
|
||||
include_in_schema=include_in_schema,
|
||||
responses=responses,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
self.exception_handlers: Dict[
|
||||
Union[int, Type[Exception]],
|
||||
@@ -98,42 +135,57 @@ class FastAPI(Starlette):
|
||||
[] if middleware is None else list(middleware)
|
||||
)
|
||||
self.middleware_stack: ASGIApp = self.build_middleware_stack()
|
||||
|
||||
self.title = title
|
||||
self.description = description
|
||||
self.version = version
|
||||
self.terms_of_service = terms_of_service
|
||||
self.contact = contact
|
||||
self.license_info = license_info
|
||||
self.servers = servers or []
|
||||
self.openapi_url = openapi_url
|
||||
self.openapi_tags = openapi_tags
|
||||
# TODO: remove when discarding the openapi_prefix parameter
|
||||
if openapi_prefix:
|
||||
logger.warning(
|
||||
'"openapi_prefix" has been deprecated in favor of "root_path", which '
|
||||
"follows more closely the ASGI standard, is simpler, and more "
|
||||
"automatic. Check the docs at "
|
||||
"https://fastapi.tiangolo.com/advanced/sub-applications/"
|
||||
)
|
||||
self.root_path = root_path or openapi_prefix
|
||||
self.root_path_in_servers = root_path_in_servers
|
||||
self.docs_url = docs_url
|
||||
self.redoc_url = redoc_url
|
||||
self.swagger_ui_oauth2_redirect_url = swagger_ui_oauth2_redirect_url
|
||||
self.swagger_ui_init_oauth = swagger_ui_init_oauth
|
||||
self.swagger_ui_parameters = swagger_ui_parameters
|
||||
self.extra = extra
|
||||
self.dependency_overrides: Dict[Callable[..., Any], Callable[..., Any]] = {}
|
||||
|
||||
self.openapi_version = "3.0.2"
|
||||
|
||||
if self.openapi_url:
|
||||
assert self.title, "A title must be provided for OpenAPI, e.g.: 'My API'"
|
||||
assert self.version, "A version must be provided for OpenAPI, e.g.: '2.1.0'"
|
||||
self.openapi_schema: Optional[Dict[str, Any]] = None
|
||||
self.setup()
|
||||
|
||||
def build_middleware_stack(self) -> ASGIApp:
|
||||
# Duplicate/override from Starlette to add AsyncExitStackMiddleware
|
||||
# inside of ExceptionMiddleware, inside of custom user middlewares
|
||||
debug = self.debug
|
||||
error_handler = None
|
||||
exception_handlers = {}
|
||||
|
||||
for key, value in self.exception_handlers.items():
|
||||
if key in (500, Exception):
|
||||
error_handler = value
|
||||
else:
|
||||
exception_handlers[key] = value
|
||||
|
||||
middleware = (
|
||||
[Middleware(ServerErrorMiddleware, handler=error_handler, debug=debug)]
|
||||
+ self.user_middleware
|
||||
+ [
|
||||
Middleware(
|
||||
ExceptionMiddleware, handlers=exception_handlers, debug=debug
|
||||
),
|
||||
# Add FastAPI-specific AsyncExitStackMiddleware for dependencies with
|
||||
# contextvars.
|
||||
# This needs to happen after user middlewares because those create a
|
||||
# new contextvars context copy by using a new AnyIO task group.
|
||||
# The initial part of dependencies with yield is executed in the
|
||||
# FastAPI code, inside all the middlewares, but the teardown part
|
||||
# (after yield) is executed in the AsyncExitStack in this middleware,
|
||||
# if the AsyncExitStack lived outside of the custom middlewares and
|
||||
# contextvars were set in a dependency with yield in that internal
|
||||
# contextvars context, the values would not be available in the
|
||||
# outside context of the AsyncExitStack.
|
||||
# By putting the middleware and the AsyncExitStack here, inside all
|
||||
# user middlewares, the code before and after yield in dependencies
|
||||
# with yield is executed in the same contextvars context, so all values
|
||||
# set in contextvars before yield is still available after yield as
|
||||
# would be expected.
|
||||
# Additionally, by having this AsyncExitStack here, after the
|
||||
# ExceptionMiddleware, now dependencies can catch handled exceptions,
|
||||
# e.g. HTTPException, to customize the teardown code (e.g. DB session
|
||||
# rollback).
|
||||
Middleware(AsyncExitStackMiddleware),
|
||||
]
|
||||
)
|
||||
|
||||
app = self.router
|
||||
for cls, options in reversed(middleware):
|
||||
app = cls(app=app, **options)
|
||||
return app
|
||||
|
||||
def openapi(self) -> Dict[str, Any]:
|
||||
if not self.openapi_schema:
|
||||
self.openapi_schema = get_openapi(
|
||||
@@ -206,12 +258,7 @@ class FastAPI(Starlette):
|
||||
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
||||
if self.root_path:
|
||||
scope["root_path"] = self.root_path
|
||||
if AsyncExitStack:
|
||||
async with AsyncExitStack() as stack:
|
||||
scope["fastapi_astack"] = stack
|
||||
await super().__call__(scope, receive, send)
|
||||
else:
|
||||
await super().__call__(scope, receive, send) # pragma: no cover
|
||||
await super().__call__(scope, receive, send)
|
||||
|
||||
def add_api_route(
|
||||
self,
|
||||
@@ -241,6 +288,9 @@ class FastAPI(Starlette):
|
||||
),
|
||||
name: Optional[str] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> None:
|
||||
self.router.add_api_route(
|
||||
path,
|
||||
@@ -266,6 +316,7 @@ class FastAPI(Starlette):
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
|
||||
def api_route(
|
||||
@@ -293,6 +344,9 @@ class FastAPI(Starlette):
|
||||
response_class: Type[Response] = Default(JSONResponse),
|
||||
name: Optional[str] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
def decorator(func: DecoratedCallable) -> DecoratedCallable:
|
||||
self.router.add_api_route(
|
||||
@@ -319,6 +373,7 @@ class FastAPI(Starlette):
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
return func
|
||||
|
||||
@@ -350,6 +405,9 @@ class FastAPI(Starlette):
|
||||
include_in_schema: bool = True,
|
||||
default_response_class: Type[Response] = Default(JSONResponse),
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> None:
|
||||
self.router.include_router(
|
||||
router,
|
||||
@@ -361,6 +419,7 @@ class FastAPI(Starlette):
|
||||
include_in_schema=include_in_schema,
|
||||
default_response_class=default_response_class,
|
||||
callbacks=callbacks,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
|
||||
def get(
|
||||
@@ -388,6 +447,9 @@ class FastAPI(Starlette):
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
return self.router.get(
|
||||
path,
|
||||
@@ -412,6 +474,7 @@ class FastAPI(Starlette):
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
|
||||
def put(
|
||||
@@ -439,6 +502,9 @@ class FastAPI(Starlette):
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
return self.router.put(
|
||||
path,
|
||||
@@ -463,6 +529,7 @@ class FastAPI(Starlette):
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
|
||||
def post(
|
||||
@@ -490,6 +557,9 @@ class FastAPI(Starlette):
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
return self.router.post(
|
||||
path,
|
||||
@@ -514,6 +584,7 @@ class FastAPI(Starlette):
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
|
||||
def delete(
|
||||
@@ -541,6 +612,9 @@ class FastAPI(Starlette):
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
return self.router.delete(
|
||||
path,
|
||||
@@ -565,6 +639,7 @@ class FastAPI(Starlette):
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
|
||||
def options(
|
||||
@@ -592,6 +667,9 @@ class FastAPI(Starlette):
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
return self.router.options(
|
||||
path,
|
||||
@@ -616,6 +694,7 @@ class FastAPI(Starlette):
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
|
||||
def head(
|
||||
@@ -643,6 +722,9 @@ class FastAPI(Starlette):
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
return self.router.head(
|
||||
path,
|
||||
@@ -667,6 +749,7 @@ class FastAPI(Starlette):
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
|
||||
def patch(
|
||||
@@ -694,6 +777,9 @@ class FastAPI(Starlette):
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
return self.router.patch(
|
||||
path,
|
||||
@@ -718,6 +804,7 @@ class FastAPI(Starlette):
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
|
||||
def trace(
|
||||
@@ -745,6 +832,9 @@ class FastAPI(Starlette):
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
return self.router.trace(
|
||||
path,
|
||||
@@ -769,4 +859,5 @@ class FastAPI(Starlette):
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
|
||||
28
fastapi/middleware/asyncexitstack.py
Normal 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
|
||||
@@ -1,5 +1,6 @@
|
||||
import http.client
|
||||
import inspect
|
||||
import warnings
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, Type, Union, cast
|
||||
|
||||
@@ -140,7 +141,15 @@ def get_openapi_operation_request_body(
|
||||
return request_body_oai
|
||||
|
||||
|
||||
def generate_operation_id(*, route: routing.APIRoute, method: str) -> str:
|
||||
def generate_operation_id(
|
||||
*, route: routing.APIRoute, method: str
|
||||
) -> str: # pragma: nocover
|
||||
warnings.warn(
|
||||
"fastapi.openapi.utils.generate_operation_id() was deprecated, "
|
||||
"it is not used internally, and will be removed soon",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
if route.operation_id:
|
||||
return route.operation_id
|
||||
path: str = route.path_format
|
||||
@@ -154,7 +163,7 @@ def generate_operation_summary(*, route: routing.APIRoute, method: str) -> str:
|
||||
|
||||
|
||||
def get_openapi_operation_metadata(
|
||||
*, route: routing.APIRoute, method: str
|
||||
*, route: routing.APIRoute, method: str, operation_ids: Set[str]
|
||||
) -> Dict[str, Any]:
|
||||
operation: Dict[str, Any] = {}
|
||||
if route.tags:
|
||||
@@ -162,14 +171,25 @@ def get_openapi_operation_metadata(
|
||||
operation["summary"] = generate_operation_summary(route=route, method=method)
|
||||
if route.description:
|
||||
operation["description"] = route.description
|
||||
operation["operationId"] = generate_operation_id(route=route, method=method)
|
||||
operation_id = route.operation_id or route.unique_id
|
||||
if operation_id in operation_ids:
|
||||
message = (
|
||||
f"Duplicate Operation ID {operation_id} for function "
|
||||
+ f"{route.endpoint.__name__}"
|
||||
)
|
||||
file_name = getattr(route.endpoint, "__globals__", {}).get("__file__")
|
||||
if file_name:
|
||||
message += f" at {file_name}"
|
||||
warnings.warn(message)
|
||||
operation_ids.add(operation_id)
|
||||
operation["operationId"] = operation_id
|
||||
if route.deprecated:
|
||||
operation["deprecated"] = route.deprecated
|
||||
return operation
|
||||
|
||||
|
||||
def get_openapi_path(
|
||||
*, route: routing.APIRoute, model_name_map: Dict[type, str]
|
||||
*, route: routing.APIRoute, model_name_map: Dict[type, str], operation_ids: Set[str]
|
||||
) -> Tuple[Dict[str, Any], Dict[str, Any], Dict[str, Any]]:
|
||||
path = {}
|
||||
security_schemes: Dict[str, Any] = {}
|
||||
@@ -183,7 +203,9 @@ def get_openapi_path(
|
||||
route_response_media_type: Optional[str] = current_response_class.media_type
|
||||
if route.include_in_schema:
|
||||
for method in route.methods:
|
||||
operation = get_openapi_operation_metadata(route=route, method=method)
|
||||
operation = get_openapi_operation_metadata(
|
||||
route=route, method=method, operation_ids=operation_ids
|
||||
)
|
||||
parameters: List[Dict[str, Any]] = []
|
||||
flat_dependant = get_flat_dependant(route.dependant, skip_repeats=True)
|
||||
security_definitions, operation_security = get_openapi_security_definitions(
|
||||
@@ -217,7 +239,9 @@ def get_openapi_path(
|
||||
cb_security_schemes,
|
||||
cb_definitions,
|
||||
) = get_openapi_path(
|
||||
route=callback, model_name_map=model_name_map
|
||||
route=callback,
|
||||
model_name_map=model_name_map,
|
||||
operation_ids=operation_ids,
|
||||
)
|
||||
callbacks[callback.name] = {callback.path: cb_path}
|
||||
operation["callbacks"] = callbacks
|
||||
@@ -384,6 +408,7 @@ def get_openapi(
|
||||
output["servers"] = servers
|
||||
components: Dict[str, Dict[str, Any]] = {}
|
||||
paths: Dict[str, Dict[str, Any]] = {}
|
||||
operation_ids: Set[str] = set()
|
||||
flat_models = get_flat_models_from_routes(routes)
|
||||
model_name_map = get_model_name_map(flat_models)
|
||||
definitions = get_model_definitions(
|
||||
@@ -391,7 +416,9 @@ def get_openapi(
|
||||
)
|
||||
for route in routes:
|
||||
if isinstance(route, routing.APIRoute):
|
||||
result = get_openapi_path(route=route, model_name_map=model_name_map)
|
||||
result = get_openapi_path(
|
||||
route=route, model_name_map=model_name_map, operation_ids=operation_ids
|
||||
)
|
||||
if result:
|
||||
path, security_schemes, path_definitions = result
|
||||
if path:
|
||||
|
||||
@@ -13,6 +13,7 @@ from typing import (
|
||||
Optional,
|
||||
Sequence,
|
||||
Set,
|
||||
Tuple,
|
||||
Type,
|
||||
Union,
|
||||
)
|
||||
@@ -33,7 +34,7 @@ from fastapi.types import DecoratedCallable
|
||||
from fastapi.utils import (
|
||||
create_cloned_field,
|
||||
create_response_field,
|
||||
generate_operation_id_for_path,
|
||||
generate_unique_id,
|
||||
get_value_or_default,
|
||||
)
|
||||
from pydantic import BaseModel
|
||||
@@ -44,7 +45,7 @@ from starlette.concurrency import run_in_threadpool
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import JSONResponse, Response
|
||||
from starlette.routing import BaseRoute
|
||||
from starlette.routing import BaseRoute, Match
|
||||
from starlette.routing import Mount as Mount # noqa
|
||||
from starlette.routing import (
|
||||
compile_path,
|
||||
@@ -53,7 +54,7 @@ from starlette.routing import (
|
||||
websocket_session,
|
||||
)
|
||||
from starlette.status import WS_1008_POLICY_VIOLATION
|
||||
from starlette.types import ASGIApp
|
||||
from starlette.types import ASGIApp, Scope
|
||||
from starlette.websockets import WebSocket
|
||||
|
||||
|
||||
@@ -296,6 +297,12 @@ class APIWebSocketRoute(routing.WebSocketRoute):
|
||||
)
|
||||
self.path_regex, self.path_format, self.param_convertors = compile_path(path)
|
||||
|
||||
def matches(self, scope: Scope) -> Tuple[Match, Scope]:
|
||||
match, child_scope = super().matches(scope)
|
||||
if match != Match.NONE:
|
||||
child_scope["route"] = self
|
||||
return match, child_scope
|
||||
|
||||
|
||||
class APIRoute(routing.Route):
|
||||
def __init__(
|
||||
@@ -328,21 +335,47 @@ class APIRoute(routing.Route):
|
||||
dependency_overrides_provider: Optional[Any] = None,
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Union[
|
||||
Callable[["APIRoute"], str], DefaultPlaceholder
|
||||
] = Default(generate_unique_id),
|
||||
) -> None:
|
||||
# normalise enums e.g. http.HTTPStatus
|
||||
if isinstance(status_code, IntEnum):
|
||||
status_code = int(status_code)
|
||||
self.path = path
|
||||
self.endpoint = endpoint
|
||||
self.response_model = response_model
|
||||
self.summary = summary
|
||||
self.response_description = response_description
|
||||
self.deprecated = deprecated
|
||||
self.operation_id = operation_id
|
||||
self.response_model_include = response_model_include
|
||||
self.response_model_exclude = response_model_exclude
|
||||
self.response_model_by_alias = response_model_by_alias
|
||||
self.response_model_exclude_unset = response_model_exclude_unset
|
||||
self.response_model_exclude_defaults = response_model_exclude_defaults
|
||||
self.response_model_exclude_none = response_model_exclude_none
|
||||
self.include_in_schema = include_in_schema
|
||||
self.response_class = response_class
|
||||
self.dependency_overrides_provider = dependency_overrides_provider
|
||||
self.callbacks = callbacks
|
||||
self.openapi_extra = openapi_extra
|
||||
self.generate_unique_id_function = generate_unique_id_function
|
||||
self.tags = tags or []
|
||||
self.responses = responses or {}
|
||||
self.name = get_name(endpoint) if name is None else name
|
||||
self.path_regex, self.path_format, self.param_convertors = compile_path(path)
|
||||
if methods is None:
|
||||
methods = ["GET"]
|
||||
self.methods: Set[str] = set([method.upper() for method in methods])
|
||||
self.unique_id = generate_operation_id_for_path(
|
||||
name=self.name, path=self.path_format, method=list(methods)[0]
|
||||
)
|
||||
self.response_model = response_model
|
||||
if isinstance(generate_unique_id_function, DefaultPlaceholder):
|
||||
current_generate_unique_id: Callable[
|
||||
["APIRoute"], str
|
||||
] = generate_unique_id_function.value
|
||||
else:
|
||||
current_generate_unique_id = generate_unique_id_function
|
||||
self.unique_id = self.operation_id or current_generate_unique_id(self)
|
||||
# normalize enums e.g. http.HTTPStatus
|
||||
if isinstance(status_code, IntEnum):
|
||||
status_code = int(status_code)
|
||||
self.status_code = status_code
|
||||
if self.response_model:
|
||||
assert (
|
||||
status_code not in STATUS_CODES_WITH_NO_BODY
|
||||
@@ -364,19 +397,14 @@ class APIRoute(routing.Route):
|
||||
else:
|
||||
self.response_field = None # type: ignore
|
||||
self.secure_cloned_response_field = None
|
||||
self.status_code = status_code
|
||||
self.tags = tags or []
|
||||
if dependencies:
|
||||
self.dependencies = list(dependencies)
|
||||
else:
|
||||
self.dependencies = []
|
||||
self.summary = summary
|
||||
self.description = description or inspect.cleandoc(self.endpoint.__doc__ or "")
|
||||
# if a "form feed" character (page break) is found in the description text,
|
||||
# truncate description text to the content preceding the first "form feed"
|
||||
self.description = self.description.split("\f")[0]
|
||||
self.response_description = response_description
|
||||
self.responses = responses or {}
|
||||
response_fields = {}
|
||||
for additional_status_code, response in self.responses.items():
|
||||
assert isinstance(response, dict), "An additional response must be a dict"
|
||||
@@ -392,16 +420,6 @@ class APIRoute(routing.Route):
|
||||
self.response_fields: Dict[Union[int, str], ModelField] = response_fields
|
||||
else:
|
||||
self.response_fields = {}
|
||||
self.deprecated = deprecated
|
||||
self.operation_id = operation_id
|
||||
self.response_model_include = response_model_include
|
||||
self.response_model_exclude = response_model_exclude
|
||||
self.response_model_by_alias = response_model_by_alias
|
||||
self.response_model_exclude_unset = response_model_exclude_unset
|
||||
self.response_model_exclude_defaults = response_model_exclude_defaults
|
||||
self.response_model_exclude_none = response_model_exclude_none
|
||||
self.include_in_schema = include_in_schema
|
||||
self.response_class = response_class
|
||||
|
||||
assert callable(endpoint), "An endpoint must be a callable"
|
||||
self.dependant = get_dependant(path=self.path_format, call=self.endpoint)
|
||||
@@ -411,10 +429,7 @@ class APIRoute(routing.Route):
|
||||
get_parameterless_sub_dependant(depends=depends, path=self.path_format),
|
||||
)
|
||||
self.body_field = get_body_field(dependant=self.dependant, name=self.unique_id)
|
||||
self.dependency_overrides_provider = dependency_overrides_provider
|
||||
self.callbacks = callbacks
|
||||
self.app = request_response(self.get_route_handler())
|
||||
self.openapi_extra = openapi_extra
|
||||
|
||||
def get_route_handler(self) -> Callable[[Request], Coroutine[Any, Any, Response]]:
|
||||
return get_request_handler(
|
||||
@@ -432,6 +447,12 @@ class APIRoute(routing.Route):
|
||||
dependency_overrides_provider=self.dependency_overrides_provider,
|
||||
)
|
||||
|
||||
def matches(self, scope: Scope) -> Tuple[Match, Scope]:
|
||||
match, child_scope = super().matches(scope)
|
||||
if match != Match.NONE:
|
||||
child_scope["route"] = self
|
||||
return match, child_scope
|
||||
|
||||
|
||||
class APIRouter(routing.Router):
|
||||
def __init__(
|
||||
@@ -452,6 +473,9 @@ class APIRouter(routing.Router):
|
||||
on_shutdown: Optional[Sequence[Callable[[], Any]]] = None,
|
||||
deprecated: Optional[bool] = None,
|
||||
include_in_schema: bool = True,
|
||||
generate_unique_id_function: Callable[[APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> None:
|
||||
super().__init__(
|
||||
routes=routes, # type: ignore # in Starlette
|
||||
@@ -475,6 +499,7 @@ class APIRouter(routing.Router):
|
||||
self.dependency_overrides_provider = dependency_overrides_provider
|
||||
self.route_class = route_class
|
||||
self.default_response_class = default_response_class
|
||||
self.generate_unique_id_function = generate_unique_id_function
|
||||
|
||||
def add_api_route(
|
||||
self,
|
||||
@@ -506,6 +531,9 @@ class APIRouter(routing.Router):
|
||||
route_class_override: Optional[Type[APIRoute]] = None,
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Union[
|
||||
Callable[[APIRoute], str], DefaultPlaceholder
|
||||
] = Default(generate_unique_id),
|
||||
) -> None:
|
||||
route_class = route_class_override or self.route_class
|
||||
responses = responses or {}
|
||||
@@ -522,6 +550,9 @@ class APIRouter(routing.Router):
|
||||
current_callbacks = self.callbacks.copy()
|
||||
if callbacks:
|
||||
current_callbacks.extend(callbacks)
|
||||
current_generate_unique_id = get_value_or_default(
|
||||
generate_unique_id_function, self.generate_unique_id_function
|
||||
)
|
||||
route = route_class(
|
||||
self.prefix + path,
|
||||
endpoint=endpoint,
|
||||
@@ -548,6 +579,7 @@ class APIRouter(routing.Router):
|
||||
dependency_overrides_provider=self.dependency_overrides_provider,
|
||||
callbacks=current_callbacks,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=current_generate_unique_id,
|
||||
)
|
||||
self.routes.append(route)
|
||||
|
||||
@@ -577,6 +609,9 @@ class APIRouter(routing.Router):
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
def decorator(func: DecoratedCallable) -> DecoratedCallable:
|
||||
self.add_api_route(
|
||||
@@ -604,6 +639,7 @@ class APIRouter(routing.Router):
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
return func
|
||||
|
||||
@@ -641,6 +677,9 @@ class APIRouter(routing.Router):
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
deprecated: Optional[bool] = None,
|
||||
include_in_schema: bool = True,
|
||||
generate_unique_id_function: Callable[[APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> None:
|
||||
if prefix:
|
||||
assert prefix.startswith("/"), "A path prefix must start with '/'"
|
||||
@@ -681,6 +720,12 @@ class APIRouter(routing.Router):
|
||||
current_callbacks.extend(callbacks)
|
||||
if route.callbacks:
|
||||
current_callbacks.extend(route.callbacks)
|
||||
current_generate_unique_id = get_value_or_default(
|
||||
route.generate_unique_id_function,
|
||||
router.generate_unique_id_function,
|
||||
generate_unique_id_function,
|
||||
self.generate_unique_id_function,
|
||||
)
|
||||
self.add_api_route(
|
||||
prefix + route.path,
|
||||
route.endpoint,
|
||||
@@ -709,6 +754,7 @@ class APIRouter(routing.Router):
|
||||
route_class_override=type(route),
|
||||
callbacks=current_callbacks,
|
||||
openapi_extra=route.openapi_extra,
|
||||
generate_unique_id_function=current_generate_unique_id,
|
||||
)
|
||||
elif isinstance(route, routing.Route):
|
||||
methods = list(route.methods or []) # type: ignore # in Starlette
|
||||
@@ -757,6 +803,9 @@ class APIRouter(routing.Router):
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
return self.api_route(
|
||||
path=path,
|
||||
@@ -782,6 +831,7 @@ class APIRouter(routing.Router):
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
|
||||
def put(
|
||||
@@ -809,6 +859,9 @@ class APIRouter(routing.Router):
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
return self.api_route(
|
||||
path=path,
|
||||
@@ -834,6 +887,7 @@ class APIRouter(routing.Router):
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
|
||||
def post(
|
||||
@@ -861,6 +915,9 @@ class APIRouter(routing.Router):
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
return self.api_route(
|
||||
path=path,
|
||||
@@ -886,6 +943,7 @@ class APIRouter(routing.Router):
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
|
||||
def delete(
|
||||
@@ -913,6 +971,9 @@ class APIRouter(routing.Router):
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
return self.api_route(
|
||||
path=path,
|
||||
@@ -938,6 +999,7 @@ class APIRouter(routing.Router):
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
|
||||
def options(
|
||||
@@ -965,6 +1027,9 @@ class APIRouter(routing.Router):
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
return self.api_route(
|
||||
path=path,
|
||||
@@ -990,6 +1055,7 @@ class APIRouter(routing.Router):
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
|
||||
def head(
|
||||
@@ -1017,6 +1083,9 @@ class APIRouter(routing.Router):
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
return self.api_route(
|
||||
path=path,
|
||||
@@ -1042,6 +1111,7 @@ class APIRouter(routing.Router):
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
|
||||
def patch(
|
||||
@@ -1069,6 +1139,9 @@ class APIRouter(routing.Router):
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
return self.api_route(
|
||||
path=path,
|
||||
@@ -1094,6 +1167,7 @@ class APIRouter(routing.Router):
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
|
||||
def trace(
|
||||
@@ -1121,6 +1195,9 @@ class APIRouter(routing.Router):
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
|
||||
return self.api_route(
|
||||
@@ -1147,4 +1224,5 @@ class APIRouter(routing.Router):
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import functools
|
||||
import re
|
||||
import warnings
|
||||
from dataclasses import is_dataclass
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, Optional, Set, Type, Union, cast
|
||||
from typing import TYPE_CHECKING, Any, Dict, Optional, Set, Type, Union, cast
|
||||
|
||||
import fastapi
|
||||
from fastapi.datastructures import DefaultPlaceholder, DefaultType
|
||||
@@ -13,6 +14,9 @@ from pydantic.fields import FieldInfo, ModelField, UndefinedType
|
||||
from pydantic.schema import model_process_schema
|
||||
from pydantic.utils import lenient_issubclass
|
||||
|
||||
if TYPE_CHECKING: # pragma: nocover
|
||||
from .routing import APIRoute
|
||||
|
||||
|
||||
def get_model_definitions(
|
||||
*,
|
||||
@@ -119,13 +123,29 @@ def create_cloned_field(
|
||||
return new_field
|
||||
|
||||
|
||||
def generate_operation_id_for_path(*, name: str, path: str, method: str) -> str:
|
||||
def generate_operation_id_for_path(
|
||||
*, name: str, path: str, method: str
|
||||
) -> str: # pragma: nocover
|
||||
warnings.warn(
|
||||
"fastapi.utils.generate_operation_id_for_path() was deprecated, "
|
||||
"it is not used internally, and will be removed soon",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
operation_id = name + path
|
||||
operation_id = re.sub("[^0-9a-zA-Z_]", "_", operation_id)
|
||||
operation_id = operation_id + "_" + method.lower()
|
||||
return operation_id
|
||||
|
||||
|
||||
def generate_unique_id(route: "APIRoute") -> str:
|
||||
operation_id = route.name + route.path_format
|
||||
operation_id = re.sub("[^0-9a-zA-Z_]", "_", operation_id)
|
||||
assert route.methods
|
||||
operation_id = operation_id + "_" + list(route.methods)[0].lower()
|
||||
return operation_id
|
||||
|
||||
|
||||
def deep_dict_update(main_dict: Dict[Any, Any], update_dict: Dict[Any, Any]) -> None:
|
||||
for key in update_dict:
|
||||
if (
|
||||
|
||||
@@ -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"
|
||||
|
||||
51
tests/test_dependency_contextvars.py
Normal 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"
|
||||
71
tests/test_dependency_normal_exceptions.py
Normal 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"
|
||||
@@ -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"}
|
||||
|
||||
1617
tests/test_generate_unique_id_function.py
Normal file
@@ -1,3 +1,5 @@
|
||||
import warnings
|
||||
|
||||
import pytest
|
||||
from fastapi import APIRouter, Depends, FastAPI, Response
|
||||
from fastapi.responses import JSONResponse
|
||||
@@ -343,7 +345,11 @@ client = TestClient(app)
|
||||
|
||||
def test_openapi():
|
||||
client = TestClient(app)
|
||||
response = client.get("/openapi.json")
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
response = client.get("/openapi.json")
|
||||
assert issubclass(w[-1].category, UserWarning)
|
||||
assert "Duplicate Operation ID" in str(w[-1].message)
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
|
||||
50
tests/test_route_scope.py
Normal file
@@ -0,0 +1,50 @@
|
||||
import pytest
|
||||
from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
|
||||
from fastapi.routing import APIRoute, APIWebSocketRoute
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/users/{user_id}")
|
||||
async def get_user(user_id: str, request: Request):
|
||||
route: APIRoute = request.scope["route"]
|
||||
return {"user_id": user_id, "path": route.path}
|
||||
|
||||
|
||||
@app.websocket("/items/{item_id}")
|
||||
async def websocket_item(item_id: str, websocket: WebSocket):
|
||||
route: APIWebSocketRoute = websocket.scope["route"]
|
||||
await websocket.accept()
|
||||
await websocket.send_json({"item_id": item_id, "path": route.path})
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_get():
|
||||
response = client.get("/users/rick")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"user_id": "rick", "path": "/users/{user_id}"}
|
||||
|
||||
|
||||
def test_invalid_method_doesnt_match():
|
||||
response = client.post("/users/rick")
|
||||
assert response.status_code == 405, response.text
|
||||
|
||||
|
||||
def test_invalid_path_doesnt_match():
|
||||
response = client.post("/usersx/rick")
|
||||
assert response.status_code == 404, response.text
|
||||
|
||||
|
||||
def test_websocket():
|
||||
with client.websocket_connect("/items/portal-gun") as websocket:
|
||||
data = websocket.receive_json()
|
||||
assert data == {"item_id": "portal-gun", "path": "/items/{item_id}"}
|
||||
|
||||
|
||||
def test_websocket_invalid_path_doesnt_match():
|
||||
with pytest.raises(WebSocketDisconnect):
|
||||
with client.websocket_connect("/itemsx/portal-gun") as websocket:
|
||||
websocket.receive_json()
|
||||
188
tests/test_tutorial/test_generate_clients/test_tutorial003.py
Normal file
@@ -0,0 +1,188 @@
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from docs_src.generate_clients.tutorial003 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/items/": {
|
||||
"get": {
|
||||
"tags": ["items"],
|
||||
"summary": "Get Items",
|
||||
"operationId": "items-get_items",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"title": "Response Items-Get Items",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/Item"},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
"post": {
|
||||
"tags": ["items"],
|
||||
"summary": "Create Item",
|
||||
"operationId": "items-create_item",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/Item"}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ResponseMessage"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"/users/": {
|
||||
"post": {
|
||||
"tags": ["users"],
|
||||
"summary": "Create User",
|
||||
"operationId": "users-create_user",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/User"}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ResponseMessage"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"HTTPValidationError": {
|
||||
"title": "HTTPValidationError",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"detail": {
|
||||
"title": "Detail",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||
}
|
||||
},
|
||||
},
|
||||
"Item": {
|
||||
"title": "Item",
|
||||
"required": ["name", "price"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"price": {"title": "Price", "type": "number"},
|
||||
},
|
||||
},
|
||||
"ResponseMessage": {
|
||||
"title": "ResponseMessage",
|
||||
"required": ["message"],
|
||||
"type": "object",
|
||||
"properties": {"message": {"title": "Message", "type": "string"}},
|
||||
},
|
||||
"User": {
|
||||
"title": "User",
|
||||
"required": ["username", "email"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"username": {"title": "Username", "type": "string"},
|
||||
"email": {"title": "Email", "type": "string"},
|
||||
},
|
||||
},
|
||||
"ValidationError": {
|
||||
"title": "ValidationError",
|
||||
"required": ["loc", "msg", "type"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"loc": {
|
||||
"title": "Location",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"msg": {"title": "Message", "type": "string"},
|
||||
"type": {"title": "Error Type", "type": "string"},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi():
|
||||
with client:
|
||||
response = client.get("/openapi.json")
|
||||
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_post_items():
|
||||
response = client.post("/items/", json={"name": "Foo", "price": 5})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"message": "Item received"}
|
||||
|
||||
|
||||
def test_post_users():
|
||||
response = client.post(
|
||||
"/users/", json={"username": "Foo", "email": "foo@example.com"}
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"message": "User received"}
|
||||
|
||||
|
||||
def test_get_items():
|
||||
response = client.get("/items/")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == [
|
||||
{"name": "Plumbus", "price": 3},
|
||||
{"name": "Portal Gun", "price": 9001},
|
||||
]
|
||||