diff --git a/docs/en/data/contributors.yml b/docs/en/data/contributors.yml
index 592c79af0..163dc68e3 100644
--- a/docs/en/data/contributors.yml
+++ b/docs/en/data/contributors.yml
@@ -1,21 +1,21 @@
tiangolo:
login: tiangolo
- count: 794
+ count: 808
avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4
url: https://github.com/tiangolo
dependabot:
login: dependabot
- count: 126
+ count: 130
avatarUrl: https://avatars.githubusercontent.com/in/29110?v=4
url: https://github.com/apps/dependabot
alejsdev:
login: alejsdev
count: 52
- avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=447d12a1b347f466b35378bee4c7104cc9b2c571&v=4
+ avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=85ceac49fb87138aebe8d663912e359447329090&v=4
url: https://github.com/alejsdev
pre-commit-ci:
login: pre-commit-ci
- count: 49
+ count: 50
avatarUrl: https://avatars.githubusercontent.com/in/68672?v=4
url: https://github.com/apps/pre-commit-ci
github-actions:
@@ -28,31 +28,31 @@ Kludex:
count: 25
avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4
url: https://github.com/Kludex
+YuriiMotov:
+ login: YuriiMotov
+ count: 20
+ avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4
+ url: https://github.com/YuriiMotov
dmontagu:
login: dmontagu
count: 17
avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4
url: https://github.com/dmontagu
-YuriiMotov:
- login: YuriiMotov
- count: 15
- avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4
- url: https://github.com/YuriiMotov
nilslindemann:
login: nilslindemann
- count: 14
+ count: 15
avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4
url: https://github.com/nilslindemann
+svlandeg:
+ login: svlandeg
+ count: 14
+ avatarUrl: https://avatars.githubusercontent.com/u/8796347?u=556c97650c27021911b0b9447ec55e75987b0e8a&v=4
+ url: https://github.com/svlandeg
euri10:
login: euri10
count: 13
avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4
url: https://github.com/euri10
-svlandeg:
- login: svlandeg
- count: 13
- avatarUrl: https://avatars.githubusercontent.com/u/8796347?u=556c97650c27021911b0b9447ec55e75987b0e8a&v=4
- url: https://github.com/svlandeg
kantandane:
login: kantandane
count: 13
@@ -103,6 +103,11 @@ waynerv:
count: 5
avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4
url: https://github.com/waynerv
+musicinmybrain:
+ login: musicinmybrain
+ count: 5
+ avatarUrl: https://avatars.githubusercontent.com/u/6898909?u=9010312053e7141383b9bdf538036c7f37fbaba0&v=4
+ url: https://github.com/musicinmybrain
krishnamadhavan:
login: krishnamadhavan
count: 5
@@ -133,11 +138,6 @@ iudeen:
count: 4
avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=f09cdd745e5bf16138f29b42732dd57c7f02bee1&v=4
url: https://github.com/iudeen
-musicinmybrain:
- login: musicinmybrain
- count: 4
- avatarUrl: https://avatars.githubusercontent.com/u/6898909?u=9010312053e7141383b9bdf538036c7f37fbaba0&v=4
- url: https://github.com/musicinmybrain
philipokiokio:
login: philipokiokio
count: 4
@@ -483,6 +483,11 @@ nzig:
count: 2
avatarUrl: https://avatars.githubusercontent.com/u/7372858?u=e769add36ed73c778cdb136eb10bf96b1e119671&v=4
url: https://github.com/nzig
+kristjanvalur:
+ login: kristjanvalur
+ count: 2
+ avatarUrl: https://avatars.githubusercontent.com/u/6009543?u=1419f20bbfff8f031be8cb470962e7e62de2595e&v=4
+ url: https://github.com/kristjanvalur
yezz123:
login: yezz123
count: 2
diff --git a/docs/en/data/github_sponsors.yml b/docs/en/data/github_sponsors.yml
index 3d8ecdb7a..24780603d 100644
--- a/docs/en/data/github_sponsors.yml
+++ b/docs/en/data/github_sponsors.yml
@@ -23,9 +23,6 @@ sponsors:
- login: railwayapp
avatarUrl: https://avatars.githubusercontent.com/u/66716858?v=4
url: https://github.com/railwayapp
- - login: scalar
- avatarUrl: https://avatars.githubusercontent.com/u/301879?v=4
- url: https://github.com/scalar
- - login: dribia
avatarUrl: https://avatars.githubusercontent.com/u/41189616?v=4
url: https://github.com/dribia
@@ -44,25 +41,25 @@ sponsors:
- login: permitio
avatarUrl: https://avatars.githubusercontent.com/u/71775833?v=4
url: https://github.com/permitio
-- - login: BoostryJP
- avatarUrl: https://avatars.githubusercontent.com/u/57932412?v=4
- url: https://github.com/BoostryJP
- - login: mercedes-benz
- avatarUrl: https://avatars.githubusercontent.com/u/34240465?v=4
- url: https://github.com/mercedes-benz
- - login: Ponte-Energy-Partners
+- - login: Ponte-Energy-Partners
avatarUrl: https://avatars.githubusercontent.com/u/114745848?v=4
url: https://github.com/Ponte-Energy-Partners
- login: LambdaTest-Inc
avatarUrl: https://avatars.githubusercontent.com/u/171592363?u=96606606a45fa170427206199014f2a5a2a4920b&v=4
url: https://github.com/LambdaTest-Inc
+ - login: BoostryJP
+ avatarUrl: https://avatars.githubusercontent.com/u/57932412?v=4
+ url: https://github.com/BoostryJP
- login: requestly
avatarUrl: https://avatars.githubusercontent.com/u/12287519?v=4
url: https://github.com/requestly
- login: acsone
avatarUrl: https://avatars.githubusercontent.com/u/7601056?v=4
url: https://github.com/acsone
-- - login: Trivie
+- - login: scalar
+ avatarUrl: https://avatars.githubusercontent.com/u/301879?v=4
+ url: https://github.com/scalar
+ - login: Trivie
avatarUrl: https://avatars.githubusercontent.com/u/8161763?v=4
url: https://github.com/Trivie
- - login: takashi-yoneya
@@ -71,42 +68,30 @@ sponsors:
- login: Doist
avatarUrl: https://avatars.githubusercontent.com/u/2565372?v=4
url: https://github.com/Doist
+ - login: bholagabbar
+ avatarUrl: https://avatars.githubusercontent.com/u/11693595?v=4
+ url: https://github.com/bholagabbar
- - login: mainframeindustries
avatarUrl: https://avatars.githubusercontent.com/u/55092103?v=4
url: https://github.com/mainframeindustries
- - login: alixlahuec
avatarUrl: https://avatars.githubusercontent.com/u/29543316?u=44357eb2a93bccf30fb9d389b8befe94a3d00985&v=4
url: https://github.com/alixlahuec
- - login: Partho
- avatarUrl: https://avatars.githubusercontent.com/u/2034301?u=ce195ac36835cca0cdfe6dd6e897bd38873a1524&v=4
- url: https://github.com/Partho
- - login: primer-io
avatarUrl: https://avatars.githubusercontent.com/u/62146168?v=4
url: https://github.com/primer-io
- - login: xsalagarcia
- avatarUrl: https://avatars.githubusercontent.com/u/66035908?v=4
- url: https://github.com/xsalagarcia
- - login: upciti
avatarUrl: https://avatars.githubusercontent.com/u/43346262?v=4
url: https://github.com/upciti
- - login: GonnaFlyMethod
- avatarUrl: https://avatars.githubusercontent.com/u/60840539?u=edf70b373fd4f1a83d3eb7c6802f4b6addb572cf&v=4
- url: https://github.com/GonnaFlyMethod
- login: ChargeStorm
avatarUrl: https://avatars.githubusercontent.com/u/26000165?v=4
url: https://github.com/ChargeStorm
- - login: DanielYang59
- avatarUrl: https://avatars.githubusercontent.com/u/80093591?u=63873f701c7c74aac83c906800a1dddc0bc8c92f&v=4
- url: https://github.com/DanielYang59
- login: nilslindemann
avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4
url: https://github.com/nilslindemann
- - login: samuelcolvin
avatarUrl: https://avatars.githubusercontent.com/u/4039449?u=42eb3b833047c8c4b4f647a031eaef148c16d93f&v=4
url: https://github.com/samuelcolvin
- - login: vincentkoc
- avatarUrl: https://avatars.githubusercontent.com/u/25068?u=fbd5b2d51142daa4bdbc21e21953a3b8b8188a4a&v=4
- url: https://github.com/vincentkoc
- login: otosky
avatarUrl: https://avatars.githubusercontent.com/u/42260747?u=69d089387c743d89427aa4ad8740cfb34045a9e0&v=4
url: https://github.com/otosky
@@ -137,9 +122,6 @@ sponsors:
- login: jugeeem
avatarUrl: https://avatars.githubusercontent.com/u/116043716?u=ae590d79c38ac79c91b9c5caa6887d061e865a3d&v=4
url: https://github.com/jugeeem
- - login: connorpark24
- avatarUrl: https://avatars.githubusercontent.com/u/142128990?u=09b84a4beb1f629b77287a837bcf3729785cdd89&v=4
- url: https://github.com/connorpark24
- login: patsatsia
avatarUrl: https://avatars.githubusercontent.com/u/61111267?u=3271b85f7a37b479c8d0ae0a235182e83c166edf&v=4
url: https://github.com/patsatsia
@@ -155,9 +137,9 @@ sponsors:
- login: kaoru0310
avatarUrl: https://avatars.githubusercontent.com/u/80977929?u=1b61d10142b490e56af932ddf08a390fae8ee94f&v=4
url: https://github.com/kaoru0310
- - login: DelfinaCare
- avatarUrl: https://avatars.githubusercontent.com/u/83734439?v=4
- url: https://github.com/DelfinaCare
+ - login: jstanden
+ avatarUrl: https://avatars.githubusercontent.com/u/63288?u=c3658d57d2862c607a0e19c2101c3c51876e36ad&v=4
+ url: https://github.com/jstanden
- login: knallgelb
avatarUrl: https://avatars.githubusercontent.com/u/2358812?u=c48cb6362b309d74cbf144bd6ad3aed3eb443e82&v=4
url: https://github.com/knallgelb
@@ -191,9 +173,6 @@ sponsors:
- login: oliverxchen
avatarUrl: https://avatars.githubusercontent.com/u/4471774?u=534191f25e32eeaadda22dfab4b0a428733d5489&v=4
url: https://github.com/oliverxchen
- - login: jstanden
- avatarUrl: https://avatars.githubusercontent.com/u/63288?u=c3658d57d2862c607a0e19c2101c3c51876e36ad&v=4
- url: https://github.com/jstanden
- login: paulcwatts
avatarUrl: https://avatars.githubusercontent.com/u/150269?u=1819e145d573b44f0ad74b87206d21cd60331d4e&v=4
url: https://github.com/paulcwatts
@@ -233,9 +212,6 @@ sponsors:
- login: mjohnsey
avatarUrl: https://avatars.githubusercontent.com/u/16784016?u=38fad2e6b411244560b3af99c5f5a4751bc81865&v=4
url: https://github.com/mjohnsey
- - login: enguy-hub
- avatarUrl: https://avatars.githubusercontent.com/u/16822912?u=2c45f9e7f427b2f2f3b023d7fdb0d44764c92ae8&v=4
- url: https://github.com/enguy-hub
- login: ashi-agrawal
avatarUrl: https://avatars.githubusercontent.com/u/17105294?u=99c7a854035e5398d8e7b674f2d42baae6c957f8&v=4
url: https://github.com/ashi-agrawal
@@ -260,10 +236,7 @@ sponsors:
- - login: manoelpqueiroz
avatarUrl: https://avatars.githubusercontent.com/u/23669137?u=b12e84b28a84369ab5b30bd5a79e5788df5a0756&v=4
url: https://github.com/manoelpqueiroz
-- - login: ceb10n
- avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4
- url: https://github.com/ceb10n
- - login: pawamoy
+- - login: pawamoy
avatarUrl: https://avatars.githubusercontent.com/u/3999221?u=b030e4c89df2f3a36bc4710b925bdeb6745c9856&v=4
url: https://github.com/pawamoy
- login: siavashyj
@@ -281,9 +254,9 @@ sponsors:
- login: hgalytoby
avatarUrl: https://avatars.githubusercontent.com/u/50397689?u=6cc9028f3db63f8f60ad21c17b1ce4b88c4e2e60&v=4
url: https://github.com/hgalytoby
- - login: johnl28
- avatarUrl: https://avatars.githubusercontent.com/u/54412955?u=47dd06082d1c39caa90c752eb55566e4f3813957&v=4
- url: https://github.com/johnl28
+ - login: nisutec
+ avatarUrl: https://avatars.githubusercontent.com/u/25281462?u=e562484c451fdfc59053163f64405f8eb262b8b0&v=4
+ url: https://github.com/nisutec
- login: hoenie-ams
avatarUrl: https://avatars.githubusercontent.com/u/25708487?u=cda07434f0509ac728d9edf5e681117c0f6b818b&v=4
url: https://github.com/hoenie-ams
@@ -299,21 +272,24 @@ sponsors:
- login: petercool
avatarUrl: https://avatars.githubusercontent.com/u/37613029?u=75aa8c6729e6e8f85a300561c4dbeef9d65c8797&v=4
url: https://github.com/petercool
+ - login: johnl28
+ avatarUrl: https://avatars.githubusercontent.com/u/54412955?u=47dd06082d1c39caa90c752eb55566e4f3813957&v=4
+ url: https://github.com/johnl28
- login: PunRabbit
avatarUrl: https://avatars.githubusercontent.com/u/70463212?u=1a835cfbc99295a60c8282f6aa6199d1b42241a5&v=4
url: https://github.com/PunRabbit
- login: PelicanQ
avatarUrl: https://avatars.githubusercontent.com/u/77930606?v=4
url: https://github.com/PelicanQ
+ - login: WillHogan
+ avatarUrl: https://avatars.githubusercontent.com/u/1661551?u=8a80356e3e7d5a417157aba7ea565dabc8678327&v=4
+ url: https://github.com/WillHogan
- login: my3
avatarUrl: https://avatars.githubusercontent.com/u/1825270?v=4
url: https://github.com/my3
- login: danielunderwood
avatarUrl: https://avatars.githubusercontent.com/u/4472301?v=4
url: https://github.com/danielunderwood
- - login: rangulvers
- avatarUrl: https://avatars.githubusercontent.com/u/5235430?u=e254d4af4ace5a05fa58372ae677c7d26f0d5a53&v=4
- url: https://github.com/rangulvers
- login: ddanier
avatarUrl: https://avatars.githubusercontent.com/u/113563?u=ed1dc79de72f93bd78581f88ebc6952b62f472da&v=4
url: https://github.com/ddanier
@@ -323,15 +299,21 @@ sponsors:
- login: slafs
avatarUrl: https://avatars.githubusercontent.com/u/210173?v=4
url: https://github.com/slafs
+ - login: ceb10n
+ avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4
+ url: https://github.com/ceb10n
- login: tochikuji
avatarUrl: https://avatars.githubusercontent.com/u/851759?v=4
url: https://github.com/tochikuji
- login: miguelgr
avatarUrl: https://avatars.githubusercontent.com/u/1484589?u=54556072b8136efa12ae3b6902032ea2a39ace4b&v=4
url: https://github.com/miguelgr
- - login: WillHogan
- avatarUrl: https://avatars.githubusercontent.com/u/1661551?u=8a80356e3e7d5a417157aba7ea565dabc8678327&v=4
- url: https://github.com/WillHogan
+ - login: xncbf
+ avatarUrl: https://avatars.githubusercontent.com/u/9462045?u=a80a7bb349555b277645632ed66639ff43400614&v=4
+ url: https://github.com/xncbf
+ - login: DMantis
+ avatarUrl: https://avatars.githubusercontent.com/u/9536869?u=652dd0d49717803c0cbcbf44f7740e53cf2d4892&v=4
+ url: https://github.com/DMantis
- login: hard-coders
avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4
url: https://github.com/hard-coders
@@ -347,9 +329,9 @@ sponsors:
- login: joshuatz
avatarUrl: https://avatars.githubusercontent.com/u/17817563?u=f1bf05b690d1fc164218f0b420cdd3acb7913e21&v=4
url: https://github.com/joshuatz
- - login: nisutec
- avatarUrl: https://avatars.githubusercontent.com/u/25281462?u=e562484c451fdfc59053163f64405f8eb262b8b0&v=4
- url: https://github.com/nisutec
+ - login: rangulvers
+ avatarUrl: https://avatars.githubusercontent.com/u/5235430?u=e254d4af4ace5a05fa58372ae677c7d26f0d5a53&v=4
+ url: https://github.com/rangulvers
- login: sdevkota
avatarUrl: https://avatars.githubusercontent.com/u/5250987?u=4ed9a120c89805a8aefda1cbdc0cf6512e64d1b4&v=4
url: https://github.com/sdevkota
@@ -368,19 +350,7 @@ sponsors:
- login: moonape1226
avatarUrl: https://avatars.githubusercontent.com/u/8532038?u=d9f8b855a429fff9397c3833c2ff83849ebf989d&v=4
url: https://github.com/moonape1226
- - login: xncbf
- avatarUrl: https://avatars.githubusercontent.com/u/9462045?u=a80a7bb349555b277645632ed66639ff43400614&v=4
- url: https://github.com/xncbf
- - login: DMantis
- avatarUrl: https://avatars.githubusercontent.com/u/9536869?u=652dd0d49717803c0cbcbf44f7740e53cf2d4892&v=4
- url: https://github.com/DMantis
-- - login: morzan1001
- avatarUrl: https://avatars.githubusercontent.com/u/47593005?u=c30ab7230f82a12a9b938dcb54f84a996931409a&v=4
- url: https://github.com/morzan1001
- - login: larsyngvelundin
- avatarUrl: https://avatars.githubusercontent.com/u/34173819?u=74958599695bf83ac9f1addd935a51548a10c6b0&v=4
- url: https://github.com/larsyngvelundin
- - login: andrecorumba
+- - login: andrecorumba
avatarUrl: https://avatars.githubusercontent.com/u/37807517?u=9b9be3b41da9bda60957da9ef37b50dbf65baa61&v=4
url: https://github.com/andrecorumba
- login: KOZ39
@@ -389,21 +359,30 @@ sponsors:
- login: rwxd
avatarUrl: https://avatars.githubusercontent.com/u/40308458?u=cd04a39e3655923be4f25c2ba8a5a07b3da3230a&v=4
url: https://github.com/rwxd
+ - login: morzan1001
+ avatarUrl: https://avatars.githubusercontent.com/u/47593005?u=c30ab7230f82a12a9b938dcb54f84a996931409a&v=4
+ url: https://github.com/morzan1001
+ - login: Olegt0rr
+ avatarUrl: https://avatars.githubusercontent.com/u/25399456?u=3e87b5239a2f4600975ba13be73054f8567c6060&v=4
+ url: https://github.com/Olegt0rr
+ - login: dinoz0rg
+ avatarUrl: https://avatars.githubusercontent.com/u/32940067?u=739cda1eb123a2dd5e1db45c361396f239e23f8b&v=4
+ url: https://github.com/dinoz0rg
+ - login: larsyngvelundin
+ avatarUrl: https://avatars.githubusercontent.com/u/34173819?u=74958599695bf83ac9f1addd935a51548a10c6b0&v=4
+ url: https://github.com/larsyngvelundin
- login: hippoley
avatarUrl: https://avatars.githubusercontent.com/u/135493401?u=1164ef48a645a7c12664fabc1638fbb7e1c459b0&v=4
url: https://github.com/hippoley
+ - login: 4anklee
+ avatarUrl: https://avatars.githubusercontent.com/u/144109238?u=a79c0d581b2a3d8f3897e7ef4c012640a6c1eb3a&v=4
+ url: https://github.com/4anklee
- login: CoderDeltaLAN
avatarUrl: https://avatars.githubusercontent.com/u/152043745?u=4ff541efffb7d134e60c5fcf2dd1e343f90bb782&v=4
url: https://github.com/CoderDeltaLAN
- - login: chris1ding1
- avatarUrl: https://avatars.githubusercontent.com/u/194386334?u=5500604b50e35ed8a5aeb82ce34aa5d3ee3f88c7&v=4
- url: https://github.com/chris1ding1
- login: onestn
avatarUrl: https://avatars.githubusercontent.com/u/62360849?u=746dd21c34e7e06eefb11b03e8bb01aaae3c2a4f&v=4
url: https://github.com/onestn
- - login: Rubinskiy
- avatarUrl: https://avatars.githubusercontent.com/u/62457878?u=f2e35ed3d196a99cfadb5a29a91950342af07e34&v=4
- url: https://github.com/Rubinskiy
- login: nayasinghania
avatarUrl: https://avatars.githubusercontent.com/u/74111380?u=752e99a5e139389fdc0a0677122adc08438eb076&v=4
url: https://github.com/nayasinghania
@@ -413,9 +392,6 @@ sponsors:
- login: andreagrandi
avatarUrl: https://avatars.githubusercontent.com/u/636391?u=13d90cb8ec313593a5b71fbd4e33b78d6da736f5&v=4
url: https://github.com/andreagrandi
- - login: Olegt0rr
- avatarUrl: https://avatars.githubusercontent.com/u/25399456?u=3e87b5239a2f4600975ba13be73054f8567c6060&v=4
- url: https://github.com/Olegt0rr
- login: msserpa
avatarUrl: https://avatars.githubusercontent.com/u/6334934?u=82c4489eb1559d88d2990d60001901b14f722bbb&v=4
url: https://github.com/msserpa
diff --git a/docs/en/data/topic_repos.yml b/docs/en/data/topic_repos.yml
index 1bb6fd70d..cb7e3c033 100644
--- a/docs/en/data/topic_repos.yml
+++ b/docs/en/data/topic_repos.yml
@@ -1,368 +1,388 @@
- name: full-stack-fastapi-template
html_url: https://github.com/fastapi/full-stack-fastapi-template
- stars: 38779
+ stars: 39475
owner_login: fastapi
owner_html_url: https://github.com/fastapi
- name: Hello-Python
html_url: https://github.com/mouredev/Hello-Python
- stars: 32726
+ stars: 33090
owner_login: mouredev
owner_html_url: https://github.com/mouredev
- name: serve
html_url: https://github.com/jina-ai/serve
- stars: 21779
+ stars: 21798
owner_login: jina-ai
owner_html_url: https://github.com/jina-ai
- name: HivisionIDPhotos
html_url: https://github.com/Zeyi-Lin/HivisionIDPhotos
- stars: 20028
+ stars: 20258
owner_login: Zeyi-Lin
owner_html_url: https://github.com/Zeyi-Lin
- name: sqlmodel
html_url: https://github.com/fastapi/sqlmodel
- stars: 17038
+ stars: 17212
owner_login: fastapi
owner_html_url: https://github.com/fastapi
- name: Douyin_TikTok_Download_API
html_url: https://github.com/Evil0ctal/Douyin_TikTok_Download_API
- stars: 14786
+ stars: 15145
owner_login: Evil0ctal
owner_html_url: https://github.com/Evil0ctal
- name: fastapi-best-practices
html_url: https://github.com/zhanymkanov/fastapi-best-practices
- stars: 13968
+ stars: 14644
owner_login: zhanymkanov
owner_html_url: https://github.com/zhanymkanov
- name: machine-learning-zoomcamp
html_url: https://github.com/DataTalksClub/machine-learning-zoomcamp
- stars: 12171
+ stars: 12320
owner_login: DataTalksClub
owner_html_url: https://github.com/DataTalksClub
- name: fastapi_mcp
html_url: https://github.com/tadata-org/fastapi_mcp
- stars: 10976
+ stars: 11174
owner_login: tadata-org
owner_html_url: https://github.com/tadata-org
-- name: awesome-fastapi
- html_url: https://github.com/mjhea0/awesome-fastapi
- stars: 10618
- owner_login: mjhea0
- owner_html_url: https://github.com/mjhea0
- name: SurfSense
html_url: https://github.com/MODSetter/SurfSense
- stars: 10243
+ stars: 10858
owner_login: MODSetter
owner_html_url: https://github.com/MODSetter
+- name: awesome-fastapi
+ html_url: https://github.com/mjhea0/awesome-fastapi
+ stars: 10758
+ owner_login: mjhea0
+ owner_html_url: https://github.com/mjhea0
- name: XHS-Downloader
html_url: https://github.com/JoeanAmier/XHS-Downloader
- stars: 9062
+ stars: 9313
owner_login: JoeanAmier
owner_html_url: https://github.com/JoeanAmier
- name: FastUI
html_url: https://github.com/pydantic/FastUI
- stars: 8892
+ stars: 8915
owner_login: pydantic
owner_html_url: https://github.com/pydantic
- name: polar
html_url: https://github.com/polarsource/polar
- stars: 8084
+ stars: 8339
owner_login: polarsource
owner_html_url: https://github.com/polarsource
- name: FileCodeBox
html_url: https://github.com/vastsa/FileCodeBox
- stars: 7494
+ stars: 7721
owner_login: vastsa
owner_html_url: https://github.com/vastsa
- name: nonebot2
html_url: https://github.com/nonebot/nonebot2
- stars: 7128
+ stars: 7170
owner_login: nonebot
owner_html_url: https://github.com/nonebot
- name: hatchet
html_url: https://github.com/hatchet-dev/hatchet
- stars: 6155
+ stars: 6253
owner_login: hatchet-dev
owner_html_url: https://github.com/hatchet-dev
-- name: serge
- html_url: https://github.com/serge-chat/serge
- stars: 5754
- owner_login: serge-chat
- owner_html_url: https://github.com/serge-chat
- name: fastapi-users
html_url: https://github.com/fastapi-users/fastapi-users
- stars: 5683
+ stars: 5849
owner_login: fastapi-users
owner_html_url: https://github.com/fastapi-users
+- name: serge
+ html_url: https://github.com/serge-chat/serge
+ stars: 5756
+ owner_login: serge-chat
+ owner_html_url: https://github.com/serge-chat
- name: strawberry
html_url: https://github.com/strawberry-graphql/strawberry
- stars: 4452
+ stars: 4569
owner_login: strawberry-graphql
owner_html_url: https://github.com/strawberry-graphql
- name: chatgpt-web-share
html_url: https://github.com/chatpire/chatgpt-web-share
- stars: 4296
+ stars: 4294
owner_login: chatpire
owner_html_url: https://github.com/chatpire
- name: poem
html_url: https://github.com/poem-web/poem
- stars: 4235
+ stars: 4276
owner_login: poem-web
owner_html_url: https://github.com/poem-web
- name: dynaconf
html_url: https://github.com/dynaconf/dynaconf
- stars: 4174
+ stars: 4202
owner_login: dynaconf
owner_html_url: https://github.com/dynaconf
- name: atrilabs-engine
html_url: https://github.com/Atri-Labs/atrilabs-engine
- stars: 4094
+ stars: 4093
owner_login: Atri-Labs
owner_html_url: https://github.com/Atri-Labs
- name: Kokoro-FastAPI
html_url: https://github.com/remsky/Kokoro-FastAPI
- stars: 3875
+ stars: 4019
owner_login: remsky
owner_html_url: https://github.com/remsky
- name: logfire
html_url: https://github.com/pydantic/logfire
- stars: 3717
+ stars: 3805
owner_login: pydantic
owner_html_url: https://github.com/pydantic
- name: LitServe
html_url: https://github.com/Lightning-AI/LitServe
- stars: 3615
+ stars: 3719
owner_login: Lightning-AI
owner_html_url: https://github.com/Lightning-AI
+- name: fastapi-admin
+ html_url: https://github.com/fastapi-admin/fastapi-admin
+ stars: 3632
+ owner_login: fastapi-admin
+ owner_html_url: https://github.com/fastapi-admin
- name: datamodel-code-generator
html_url: https://github.com/koxudaxi/datamodel-code-generator
- stars: 3554
+ stars: 3609
owner_login: koxudaxi
owner_html_url: https://github.com/koxudaxi
- name: huma
html_url: https://github.com/danielgtaylor/huma
- stars: 3521
+ stars: 3603
owner_login: danielgtaylor
owner_html_url: https://github.com/danielgtaylor
-- name: fastapi-admin
- html_url: https://github.com/fastapi-admin/fastapi-admin
- stars: 3497
- owner_login: fastapi-admin
- owner_html_url: https://github.com/fastapi-admin
- name: farfalle
html_url: https://github.com/rashadphz/farfalle
- stars: 3476
+ stars: 3490
owner_login: rashadphz
owner_html_url: https://github.com/rashadphz
- name: tracecat
html_url: https://github.com/TracecatHQ/tracecat
- stars: 3310
+ stars: 3379
owner_login: TracecatHQ
owner_html_url: https://github.com/TracecatHQ
- name: opyrator
html_url: https://github.com/ml-tooling/opyrator
- stars: 3134
+ stars: 3135
owner_login: ml-tooling
owner_html_url: https://github.com/ml-tooling
- name: docarray
html_url: https://github.com/docarray/docarray
- stars: 3108
+ stars: 3114
owner_login: docarray
owner_html_url: https://github.com/docarray
+- name: devpush
+ html_url: https://github.com/hunvreus/devpush
+ stars: 3097
+ owner_login: hunvreus
+ owner_html_url: https://github.com/hunvreus
- name: fastapi-realworld-example-app
html_url: https://github.com/nsidnev/fastapi-realworld-example-app
- stars: 2945
+ stars: 3050
owner_login: nsidnev
owner_html_url: https://github.com/nsidnev
- name: uvicorn-gunicorn-fastapi-docker
html_url: https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker
- stars: 2809
+ stars: 2911
owner_login: tiangolo
owner_html_url: https://github.com/tiangolo
-- name: devpush
- html_url: https://github.com/hunvreus/devpush
- stars: 2784
- owner_login: hunvreus
- owner_html_url: https://github.com/hunvreus
- name: mcp-context-forge
html_url: https://github.com/IBM/mcp-context-forge
- stars: 2763
+ stars: 2899
owner_login: IBM
owner_html_url: https://github.com/IBM
- name: best-of-web-python
html_url: https://github.com/ml-tooling/best-of-web-python
- stars: 2630
+ stars: 2648
owner_login: ml-tooling
owner_html_url: https://github.com/ml-tooling
-- name: fastapi-react
- html_url: https://github.com/Buuntu/fastapi-react
- stars: 2464
- owner_login: Buuntu
- owner_html_url: https://github.com/Buuntu
- name: FastAPI-template
html_url: https://github.com/s3rius/FastAPI-template
- stars: 2453
+ stars: 2637
owner_login: s3rius
owner_html_url: https://github.com/s3rius
-- name: RasaGPT
- html_url: https://github.com/paulpierre/RasaGPT
- stars: 2444
- owner_login: paulpierre
- owner_html_url: https://github.com/paulpierre
+- name: YC-Killer
+ html_url: https://github.com/sahibzada-allahyar/YC-Killer
+ stars: 2599
+ owner_login: sahibzada-allahyar
+ owner_html_url: https://github.com/sahibzada-allahyar
+- name: fastapi-react
+ html_url: https://github.com/Buuntu/fastapi-react
+ stars: 2569
+ owner_login: Buuntu
+ owner_html_url: https://github.com/Buuntu
+- name: Yuxi-Know
+ html_url: https://github.com/xerrors/Yuxi-Know
+ stars: 2563
+ owner_login: xerrors
+ owner_html_url: https://github.com/xerrors
- name: sqladmin
html_url: https://github.com/aminalaee/sqladmin
- stars: 2423
+ stars: 2558
owner_login: aminalaee
owner_html_url: https://github.com/aminalaee
-- name: nextpy
- html_url: https://github.com/dot-agent/nextpy
- stars: 2325
- owner_login: dot-agent
- owner_html_url: https://github.com/dot-agent
+- name: RasaGPT
+ html_url: https://github.com/paulpierre/RasaGPT
+ stars: 2451
+ owner_login: paulpierre
+ owner_html_url: https://github.com/paulpierre
- name: supabase-py
html_url: https://github.com/supabase/supabase-py
- stars: 2292
+ stars: 2344
owner_login: supabase
owner_html_url: https://github.com/supabase
+- name: nextpy
+ html_url: https://github.com/dot-agent/nextpy
+ stars: 2335
+ owner_login: dot-agent
+ owner_html_url: https://github.com/dot-agent
+- name: fastapi-utils
+ html_url: https://github.com/fastapiutils/fastapi-utils
+ stars: 2291
+ owner_login: fastapiutils
+ owner_html_url: https://github.com/fastapiutils
- name: 30-Days-of-Python
html_url: https://github.com/codingforentrepreneurs/30-Days-of-Python
- stars: 2214
+ stars: 2220
owner_login: codingforentrepreneurs
owner_html_url: https://github.com/codingforentrepreneurs
-- name: Yuxi-Know
- html_url: https://github.com/xerrors/Yuxi-Know
- stars: 2212
- owner_login: xerrors
- owner_html_url: https://github.com/xerrors
- name: langserve
html_url: https://github.com/langchain-ai/langserve
- stars: 2191
+ stars: 2215
owner_login: langchain-ai
owner_html_url: https://github.com/langchain-ai
-- name: fastapi-utils
- html_url: https://github.com/fastapiutils/fastapi-utils
- stars: 2185
- owner_login: fastapiutils
- owner_html_url: https://github.com/fastapiutils
- name: solara
html_url: https://github.com/widgetti/solara
- stars: 2111
+ stars: 2122
owner_login: widgetti
owner_html_url: https://github.com/widgetti
- name: mangum
html_url: https://github.com/Kludex/mangum
- stars: 2011
+ stars: 2029
owner_login: Kludex
owner_html_url: https://github.com/Kludex
- name: agentkit
html_url: https://github.com/BCG-X-Official/agentkit
- stars: 1826
+ stars: 1912
owner_login: BCG-X-Official
owner_html_url: https://github.com/BCG-X-Official
-- name: python-week-2022
- html_url: https://github.com/rochacbruno/python-week-2022
- stars: 1815
- owner_login: rochacbruno
- owner_html_url: https://github.com/rochacbruno
- name: manage-fastapi
html_url: https://github.com/ycd/manage-fastapi
- stars: 1787
+ stars: 1885
owner_login: ycd
owner_html_url: https://github.com/ycd
-- name: ormar
- html_url: https://github.com/collerek/ormar
- stars: 1780
- owner_login: collerek
- owner_html_url: https://github.com/collerek
-- name: vue-fastapi-admin
- html_url: https://github.com/mizhexiaoxiao/vue-fastapi-admin
- stars: 1758
- owner_login: mizhexiaoxiao
- owner_html_url: https://github.com/mizhexiaoxiao
- name: openapi-python-client
html_url: https://github.com/openapi-generators/openapi-python-client
- stars: 1731
+ stars: 1862
owner_login: openapi-generators
owner_html_url: https://github.com/openapi-generators
- name: piccolo
html_url: https://github.com/piccolo-orm/piccolo
- stars: 1711
+ stars: 1836
owner_login: piccolo-orm
owner_html_url: https://github.com/piccolo-orm
-- name: fastapi-cache
- html_url: https://github.com/long2ice/fastapi-cache
- stars: 1677
- owner_login: long2ice
- owner_html_url: https://github.com/long2ice
+- name: vue-fastapi-admin
+ html_url: https://github.com/mizhexiaoxiao/vue-fastapi-admin
+ stars: 1831
+ owner_login: mizhexiaoxiao
+ owner_html_url: https://github.com/mizhexiaoxiao
+- name: python-week-2022
+ html_url: https://github.com/rochacbruno/python-week-2022
+ stars: 1817
+ owner_login: rochacbruno
+ owner_html_url: https://github.com/rochacbruno
- name: slowapi
html_url: https://github.com/laurentS/slowapi
- stars: 1669
+ stars: 1798
owner_login: laurentS
owner_html_url: https://github.com/laurentS
-- name: langchain-serve
- html_url: https://github.com/jina-ai/langchain-serve
- stars: 1632
- owner_login: jina-ai
- owner_html_url: https://github.com/jina-ai
+- name: fastapi-cache
+ html_url: https://github.com/long2ice/fastapi-cache
+ stars: 1789
+ owner_login: long2ice
+ owner_html_url: https://github.com/long2ice
+- name: ormar
+ html_url: https://github.com/collerek/ormar
+ stars: 1783
+ owner_login: collerek
+ owner_html_url: https://github.com/collerek
- name: termpair
html_url: https://github.com/cs01/termpair
- stars: 1621
+ stars: 1716
owner_login: cs01
owner_html_url: https://github.com/cs01
- name: FastAPI-boilerplate
html_url: https://github.com/benavlabs/FastAPI-boilerplate
- stars: 1596
+ stars: 1660
owner_login: benavlabs
owner_html_url: https://github.com/benavlabs
-- name: coronavirus-tracker-api
- html_url: https://github.com/ExpDev07/coronavirus-tracker-api
- stars: 1573
- owner_login: ExpDev07
- owner_html_url: https://github.com/ExpDev07
-- name: fastapi-crudrouter
- html_url: https://github.com/awtkns/fastapi-crudrouter
- stars: 1553
- owner_login: awtkns
- owner_html_url: https://github.com/awtkns
+- name: fastapi-langgraph-agent-production-ready-template
+ html_url: https://github.com/wassim249/fastapi-langgraph-agent-production-ready-template
+ stars: 1638
+ owner_login: wassim249
+ owner_html_url: https://github.com/wassim249
+- name: langchain-serve
+ html_url: https://github.com/jina-ai/langchain-serve
+ stars: 1635
+ owner_login: jina-ai
+ owner_html_url: https://github.com/jina-ai
- name: awesome-fastapi-projects
html_url: https://github.com/Kludex/awesome-fastapi-projects
- stars: 1485
+ stars: 1589
owner_login: Kludex
owner_html_url: https://github.com/Kludex
- name: fastapi-pagination
html_url: https://github.com/uriyyo/fastapi-pagination
- stars: 1473
+ stars: 1585
owner_login: uriyyo
owner_html_url: https://github.com/uriyyo
+- name: coronavirus-tracker-api
+ html_url: https://github.com/ExpDev07/coronavirus-tracker-api
+ stars: 1574
+ owner_login: ExpDev07
+ owner_html_url: https://github.com/ExpDev07
+- name: fastapi-crudrouter
+ html_url: https://github.com/awtkns/fastapi-crudrouter
+ stars: 1559
+ owner_login: awtkns
+ owner_html_url: https://github.com/awtkns
- name: bracket
html_url: https://github.com/evroon/bracket
- stars: 1470
+ stars: 1489
owner_login: evroon
owner_html_url: https://github.com/evroon
-- name: fastapi-langgraph-agent-production-ready-template
- html_url: https://github.com/wassim249/fastapi-langgraph-agent-production-ready-template
- stars: 1456
- owner_login: wassim249
- owner_html_url: https://github.com/wassim249
+- name: fastapi-amis-admin
+ html_url: https://github.com/amisadmin/fastapi-amis-admin
+ stars: 1475
+ owner_login: amisadmin
+ owner_html_url: https://github.com/amisadmin
- name: fastapi-boilerplate
html_url: https://github.com/teamhide/fastapi-boilerplate
- stars: 1424
+ stars: 1436
owner_login: teamhide
owner_html_url: https://github.com/teamhide
- name: awesome-python-resources
html_url: https://github.com/DjangoEx/awesome-python-resources
- stars: 1420
+ stars: 1426
owner_login: DjangoEx
owner_html_url: https://github.com/DjangoEx
-- name: fastapi-amis-admin
- html_url: https://github.com/amisadmin/fastapi-amis-admin
- stars: 1363
- owner_login: amisadmin
- owner_html_url: https://github.com/amisadmin
- name: fastcrud
html_url: https://github.com/benavlabs/fastcrud
- stars: 1362
+ stars: 1414
owner_login: benavlabs
owner_html_url: https://github.com/benavlabs
+- name: prometheus-fastapi-instrumentator
+ html_url: https://github.com/trallnag/prometheus-fastapi-instrumentator
+ stars: 1388
+ owner_login: trallnag
+ owner_html_url: https://github.com/trallnag
+- name: fastapi_best_architecture
+ html_url: https://github.com/fastapi-practices/fastapi_best_architecture
+ stars: 1378
+ owner_login: fastapi-practices
+ owner_html_url: https://github.com/fastapi-practices
+- name: fastapi-code-generator
+ html_url: https://github.com/koxudaxi/fastapi-code-generator
+ stars: 1375
+ owner_login: koxudaxi
+ owner_html_url: https://github.com/koxudaxi
- name: budgetml
html_url: https://github.com/ebhy/budgetml
stars: 1345
@@ -370,126 +390,106 @@
owner_html_url: https://github.com/ebhy
- name: fastapi-tutorial
html_url: https://github.com/liaogx/fastapi-tutorial
- stars: 1315
+ stars: 1327
owner_login: liaogx
owner_html_url: https://github.com/liaogx
-- name: fastapi_best_architecture
- html_url: https://github.com/fastapi-practices/fastapi_best_architecture
- stars: 1311
- owner_login: fastapi-practices
- owner_html_url: https://github.com/fastapi-practices
-- name: fastapi-code-generator
- html_url: https://github.com/koxudaxi/fastapi-code-generator
- stars: 1270
- owner_login: koxudaxi
- owner_html_url: https://github.com/koxudaxi
-- name: prometheus-fastapi-instrumentator
- html_url: https://github.com/trallnag/prometheus-fastapi-instrumentator
- stars: 1264
- owner_login: trallnag
- owner_html_url: https://github.com/trallnag
+- name: fastapi-alembic-sqlmodel-async
+ html_url: https://github.com/jonra1993/fastapi-alembic-sqlmodel-async
+ stars: 1259
+ owner_login: jonra1993
+ owner_html_url: https://github.com/jonra1993
+- name: fastapi-scaff
+ html_url: https://github.com/atpuxiner/fastapi-scaff
+ stars: 1255
+ owner_login: atpuxiner
+ owner_html_url: https://github.com/atpuxiner
- name: bedrock-chat
html_url: https://github.com/aws-samples/bedrock-chat
- stars: 1243
+ stars: 1254
owner_login: aws-samples
owner_html_url: https://github.com/aws-samples
- name: bolt-python
html_url: https://github.com/slackapi/bolt-python
- stars: 1238
+ stars: 1253
owner_login: slackapi
owner_html_url: https://github.com/slackapi
- name: fastapi_production_template
html_url: https://github.com/zhanymkanov/fastapi_production_template
- stars: 1209
+ stars: 1217
owner_login: zhanymkanov
owner_html_url: https://github.com/zhanymkanov
-- name: fastapi-scaff
- html_url: https://github.com/atpuxiner/fastapi-scaff
- stars: 1200
- owner_login: atpuxiner
- owner_html_url: https://github.com/atpuxiner
- name: langchain-extract
html_url: https://github.com/langchain-ai/langchain-extract
- stars: 1173
+ stars: 1176
owner_login: langchain-ai
owner_html_url: https://github.com/langchain-ai
-- name: fastapi-alembic-sqlmodel-async
- html_url: https://github.com/jonra1993/fastapi-alembic-sqlmodel-async
- stars: 1162
- owner_login: jonra1993
- owner_html_url: https://github.com/jonra1993
-- name: odmantic
- html_url: https://github.com/art049/odmantic
- stars: 1137
- owner_login: art049
- owner_html_url: https://github.com/art049
- name: restish
html_url: https://github.com/rest-sh/restish
- stars: 1129
+ stars: 1140
owner_login: rest-sh
owner_html_url: https://github.com/rest-sh
-- name: kubetorch
- html_url: https://github.com/run-house/kubetorch
- stars: 1065
- owner_login: run-house
- owner_html_url: https://github.com/run-house
-- name: flock
- html_url: https://github.com/Onelevenvy/flock
- stars: 1039
- owner_login: Onelevenvy
- owner_html_url: https://github.com/Onelevenvy
+- name: odmantic
+ html_url: https://github.com/art049/odmantic
+ stars: 1138
+ owner_login: art049
+ owner_html_url: https://github.com/art049
- name: authx
html_url: https://github.com/yezz123/authx
- stars: 1017
+ stars: 1119
owner_login: yezz123
owner_html_url: https://github.com/yezz123
-- name: autollm
- html_url: https://github.com/viddexa/autollm
- stars: 997
- owner_login: viddexa
- owner_html_url: https://github.com/viddexa
-- name: lanarky
- html_url: https://github.com/ajndkr/lanarky
- stars: 993
- owner_login: ajndkr
- owner_html_url: https://github.com/ajndkr
-- name: RuoYi-Vue3-FastAPI
- html_url: https://github.com/insistence/RuoYi-Vue3-FastAPI
- stars: 974
- owner_login: insistence
- owner_html_url: https://github.com/insistence
+- name: NoteDiscovery
+ html_url: https://github.com/gamosoft/NoteDiscovery
+ stars: 1107
+ owner_login: gamosoft
+ owner_html_url: https://github.com/gamosoft
+- name: flock
+ html_url: https://github.com/Onelevenvy/flock
+ stars: 1055
+ owner_login: Onelevenvy
+ owner_html_url: https://github.com/Onelevenvy
+- name: fastapi-observability
+ html_url: https://github.com/blueswen/fastapi-observability
+ stars: 1038
+ owner_login: blueswen
+ owner_html_url: https://github.com/blueswen
- name: aktools
html_url: https://github.com/akfamily/aktools
- stars: 972
+ stars: 1027
owner_login: akfamily
owner_html_url: https://github.com/akfamily
+- name: RuoYi-Vue3-FastAPI
+ html_url: https://github.com/insistence/RuoYi-Vue3-FastAPI
+ stars: 1016
+ owner_login: insistence
+ owner_html_url: https://github.com/insistence
+- name: autollm
+ html_url: https://github.com/viddexa/autollm
+ stars: 1002
+ owner_login: viddexa
+ owner_html_url: https://github.com/viddexa
- name: titiler
html_url: https://github.com/developmentseed/titiler
- stars: 965
+ stars: 999
owner_login: developmentseed
owner_html_url: https://github.com/developmentseed
-- name: secure
- html_url: https://github.com/TypeError/secure
- stars: 953
- owner_login: TypeError
- owner_html_url: https://github.com/TypeError
-- name: energy-forecasting
- html_url: https://github.com/iusztinpaul/energy-forecasting
- stars: 949
- owner_login: iusztinpaul
- owner_html_url: https://github.com/iusztinpaul
+- name: lanarky
+ html_url: https://github.com/ajndkr/lanarky
+ stars: 994
+ owner_login: ajndkr
+ owner_html_url: https://github.com/ajndkr
- name: every-pdf
html_url: https://github.com/DDULDDUCK/every-pdf
- stars: 942
+ stars: 985
owner_login: DDULDDUCK
owner_html_url: https://github.com/DDULDDUCK
-- name: langcorn
- html_url: https://github.com/msoedov/langcorn
- stars: 933
- owner_login: msoedov
- owner_html_url: https://github.com/msoedov
-- name: fastapi-observability
- html_url: https://github.com/blueswen/fastapi-observability
- stars: 923
- owner_login: blueswen
- owner_html_url: https://github.com/blueswen
+- name: enterprise-deep-research
+ html_url: https://github.com/SalesforceAIResearch/enterprise-deep-research
+ stars: 973
+ owner_login: SalesforceAIResearch
+ owner_html_url: https://github.com/SalesforceAIResearch
+- name: fastapi-mail
+ html_url: https://github.com/sabuhish/fastapi-mail
+ stars: 964
+ owner_login: sabuhish
+ owner_html_url: https://github.com/sabuhish
diff --git a/docs/en/data/translation_reviewers.yml b/docs/en/data/translation_reviewers.yml
index 45aa55e5e..c3d3d0388 100644
--- a/docs/en/data/translation_reviewers.yml
+++ b/docs/en/data/translation_reviewers.yml
@@ -75,7 +75,7 @@ mattwang44:
url: https://github.com/mattwang44
tiangolo:
login: tiangolo
- count: 55
+ count: 56
avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4
url: https://github.com/tiangolo
Laineyzhang55:
@@ -136,7 +136,7 @@ JavierSanchezCastro:
alejsdev:
login: alejsdev
count: 37
- avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=447d12a1b347f466b35378bee4c7104cc9b2c571&v=4
+ avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=85ceac49fb87138aebe8d663912e359447329090&v=4
url: https://github.com/alejsdev
stlucasgarcia:
login: stlucasgarcia
@@ -436,7 +436,7 @@ jburckel:
peidrao:
login: peidrao
count: 13
- avatarUrl: https://avatars.githubusercontent.com/u/32584628?u=64c634bb10381905038ff7faf3c8c3df47fb799a&v=4
+ avatarUrl: https://avatars.githubusercontent.com/u/32584628?u=979c62398e16ff000cc0faa028e028efd679887c&v=4
url: https://github.com/peidrao
impocode:
login: impocode
@@ -1006,7 +1006,7 @@ takacs:
anton2yakovlev:
login: anton2yakovlev
count: 5
- avatarUrl: https://avatars.githubusercontent.com/u/44229180?u=bdd445ba99074b378e7298d23c4bf6d707d2c282&v=4
+ avatarUrl: https://avatars.githubusercontent.com/u/44229180?u=ac245e57bc834ff80f08ca8128000bb650a77a3d&v=4
url: https://github.com/anton2yakovlev
ILoveSorasakiHina:
login: ILoveSorasakiHina
@@ -1161,7 +1161,7 @@ cookie-byte217:
AbolfazlKameli:
login: AbolfazlKameli
count: 4
- avatarUrl: https://avatars.githubusercontent.com/u/120686133?u=e41743da3c1820efafc59c5870cacd4f4425334c&v=4
+ avatarUrl: https://avatars.githubusercontent.com/u/120686133?u=af8f025278cce0d489007071254e4055df60b78c&v=4
url: https://github.com/AbolfazlKameli
tyronedamasceno:
login: tyronedamasceno
@@ -1196,7 +1196,7 @@ Xaraxx:
Suyoung789:
login: Suyoung789
count: 3
- avatarUrl: https://avatars.githubusercontent.com/u/31277231?u=744bd3e641413e19bfad6b06a90bb0887c3f9332&v=4
+ avatarUrl: https://avatars.githubusercontent.com/u/31277231?u=1591aaf651eb860017231a36590050e154c026b6&v=4
url: https://github.com/Suyoung789
akagaeng:
login: akagaeng
@@ -1806,7 +1806,7 @@ MrL8199:
ivintoiu:
login: ivintoiu
count: 2
- avatarUrl: https://avatars.githubusercontent.com/u/1853336?u=b537c905ad08b69993de8796fb235c8d4d47f039&v=4
+ avatarUrl: https://avatars.githubusercontent.com/u/1853336?u=e3de5fd0ab17efc12256b4295285b504ca281440&v=4
url: https://github.com/ivintoiu
TechnoService2:
login: TechnoService2
@@ -1841,7 +1841,7 @@ NavesSapnis:
eqsdxr:
login: eqsdxr
count: 2
- avatarUrl: https://avatars.githubusercontent.com/u/157279130?u=d7aaffb29f542b647cf0f6b0e05722490863658a&v=4
+ avatarUrl: https://avatars.githubusercontent.com/u/157279130?u=7927dc0366995334f9a18c3204a41d3a34d6d96f&v=4
url: https://github.com/eqsdxr
syedasamina56:
login: syedasamina56
diff --git a/docs/en/data/translators.yml b/docs/en/data/translators.yml
index a4b87e1bf..c66eff4d4 100644
--- a/docs/en/data/translators.yml
+++ b/docs/en/data/translators.yml
@@ -1,6 +1,6 @@
nilslindemann:
login: nilslindemann
- count: 124
+ count: 125
avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4
url: https://github.com/nilslindemann
jaystone776:
@@ -8,16 +8,16 @@ jaystone776:
count: 46
avatarUrl: https://avatars.githubusercontent.com/u/11191137?u=299205a95e9b6817a43144a48b643346a5aac5cc&v=4
url: https://github.com/jaystone776
+ceb10n:
+ login: ceb10n
+ count: 29
+ avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4
+ url: https://github.com/ceb10n
valentinDruzhinin:
login: valentinDruzhinin
count: 29
avatarUrl: https://avatars.githubusercontent.com/u/12831905?u=aae1ebc675c91e8fa582df4fcc4fc4128106344d&v=4
url: https://github.com/valentinDruzhinin
-ceb10n:
- login: ceb10n
- count: 27
- avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4
- url: https://github.com/ceb10n
tokusumi:
login: tokusumi
count: 23
@@ -286,7 +286,7 @@ hsuanchi:
alejsdev:
login: alejsdev
count: 3
- avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=447d12a1b347f466b35378bee4c7104cc9b2c571&v=4
+ avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=85ceac49fb87138aebe8d663912e359447329090&v=4
url: https://github.com/alejsdev
riroan:
login: riroan
@@ -358,6 +358,11 @@ ruzia:
count: 3
avatarUrl: https://avatars.githubusercontent.com/u/24503?v=4
url: https://github.com/ruzia
+YuriiMotov:
+ login: YuriiMotov
+ count: 3
+ avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4
+ url: https://github.com/YuriiMotov
izaguerreiro:
login: izaguerreiro
count: 2
@@ -543,8 +548,3 @@ EdmilsonRodrigues:
count: 2
avatarUrl: https://avatars.githubusercontent.com/u/62777025?u=217d6f3cd6cc750bb8818a3af7726c8d74eb7c2d&v=4
url: https://github.com/EdmilsonRodrigues
-YuriiMotov:
- login: YuriiMotov
- count: 2
- avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4
- url: https://github.com/YuriiMotov
diff --git a/docs/en/docs/css/custom.css b/docs/en/docs/css/custom.css
index 8849d8741..87111ff64 100644
--- a/docs/en/docs/css/custom.css
+++ b/docs/en/docs/css/custom.css
@@ -1,9 +1,16 @@
/* Fira Code, including characters used by Rich output, like the "heavy right-pointing angle bracket ornament", not included in Google Fonts */
@import url(https://cdn.jsdelivr.net/npm/firacode@6.2.0/distr/fira_code.css);
+/* Noto Color Emoji for emoji support with the same font everywhere */
+@import url(https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&display=swap);
/* Override default code font in Material for MkDocs to Fira Code */
:root {
- --md-code-font: "Fira Code", monospace;
+ --md-code-font: "Fira Code", monospace, "Noto Color Emoji";
+}
+
+/* Override default regular font in Material for MkDocs to include Noto Color Emoji */
+:root {
+ --md-text-font: "Roboto", "Noto Color Emoji";
}
.termynal-comment {
diff --git a/docs/en/docs/how-to/authentication-error-status-code.md b/docs/en/docs/how-to/authentication-error-status-code.md
new file mode 100644
index 000000000..f9433e5dd
--- /dev/null
+++ b/docs/en/docs/how-to/authentication-error-status-code.md
@@ -0,0 +1,17 @@
+# Use Old 403 Authentication Error Status Codes { #use-old-403-authentication-error-status-codes }
+
+Before FastAPI version `0.122.0`, when the integrated security utilities returned an error to the client after a failed authentication, they used the HTTP status code `403 Forbidden`.
+
+Starting with FastAPI version `0.122.0`, they use the more appropriate HTTP status code `401 Unauthorized`, and return a sensible `WWW-Authenticate` header in the response, following the HTTP specifications, RFC 7235, RFC 9110.
+
+But if for some reason your clients depend on the old behavior, you can revert to it by overriding the method `make_not_authenticated_error` in your security classes.
+
+For example, you can create a subclass of `HTTPBearer` that returns a `403 Forbidden` error instead of the default `401 Unauthorized` error:
+
+{* ../../docs_src/authentication_error_status_code/tutorial001_an_py39.py hl[9:13] *}
+
+/// tip
+
+Notice that the function returns the exception instance, it doesn't raise it. The raising is done in the rest of the internal code.
+
+///
diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md
index 8ea745475..58781c691 100644
--- a/docs/en/docs/release-notes.md
+++ b/docs/en/docs/release-notes.md
@@ -9,6 +9,40 @@ hide:
### Internal
+* 👥 Update FastAPI GitHub topic repositories. PR [#14426](https://github.com/fastapi/fastapi/pull/14426) by [@tiangolo](https://github.com/tiangolo).
+* ⬆ Bump markdown-include-variants from 0.0.6 to 0.0.7. PR [#14423](https://github.com/fastapi/fastapi/pull/14423) by [@YuriiMotov](https://github.com/YuriiMotov).
+* 👥 Update FastAPI People - Sponsors. PR [#14422](https://github.com/fastapi/fastapi/pull/14422) by [@tiangolo](https://github.com/tiangolo).
+* 👥 Update FastAPI People - Contributors and Translators. PR [#14420](https://github.com/fastapi/fastapi/pull/14420) by [@tiangolo](https://github.com/tiangolo).
+
+## 0.123.0
+
+### Fixes
+
+* 🐛 Cache dependencies that don't use scopes and don't have sub-dependencies with scopes. PR [#14419](https://github.com/fastapi/fastapi/pull/14419) by [@tiangolo](https://github.com/tiangolo).
+
+## 0.122.1
+
+### Fixes
+
+* 🐛 Fix hierarchical security scope propagation. PR [#5624](https://github.com/fastapi/fastapi/pull/5624) by [@kristjanvalur](https://github.com/kristjanvalur).
+
+### Docs
+
+* 💅 Update CSS to explicitly use emoji font. PR [#14415](https://github.com/fastapi/fastapi/pull/14415) by [@tiangolo](https://github.com/tiangolo).
+
+### Internal
+
+* ⬆ Bump markdown-include-variants from 0.0.5 to 0.0.6. PR [#14418](https://github.com/fastapi/fastapi/pull/14418) by [@YuriiMotov](https://github.com/YuriiMotov).
+
+## 0.122.0
+
+### Fixes
+
+* 🐛 Use `401` status code in security classes when credentials are missing. PR [#13786](https://github.com/fastapi/fastapi/pull/13786) by [@YuriiMotov](https://github.com/YuriiMotov).
+ * If your code depended on these classes raising the old (less correct) `403` status code, check the new docs about how to override the classes, to use the same old behavior: [Use Old 403 Authentication Error Status Codes](https://fastapi.tiangolo.com/how-to/authentication-error-status-code/).
+
+### Internal
+
* 🔧 Configure labeler to exclude files that start from underscore for `lang-all` label. PR [#14213](https://github.com/fastapi/fastapi/pull/14213) by [@YuriiMotov](https://github.com/YuriiMotov).
* 👷 Add pre-commit config with local script for permalinks. PR [#14398](https://github.com/fastapi/fastapi/pull/14398) by [@tiangolo](https://github.com/tiangolo).
* 💄 Use font Fira Code to fix display of Rich panels in docs in Windows. PR [#14387](https://github.com/fastapi/fastapi/pull/14387) by [@tiangolo](https://github.com/tiangolo).
diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml
index 8be832f11..fd346a3d3 100644
--- a/docs/en/mkdocs.yml
+++ b/docs/en/mkdocs.yml
@@ -215,6 +215,7 @@ nav:
- how-to/custom-docs-ui-assets.md
- how-to/configure-swagger-ui.md
- how-to/testing-database.md
+ - how-to/authentication-error-status-code.md
- Reference (Code API):
- reference/index.md
- reference/fastapi.md
diff --git a/docs_src/authentication_error_status_code/tutorial001_an.py b/docs_src/authentication_error_status_code/tutorial001_an.py
new file mode 100644
index 000000000..40678e858
--- /dev/null
+++ b/docs_src/authentication_error_status_code/tutorial001_an.py
@@ -0,0 +1,20 @@
+from fastapi import Depends, FastAPI, HTTPException, status
+from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
+from typing_extensions import Annotated
+
+app = FastAPI()
+
+
+class HTTPBearer403(HTTPBearer):
+ def make_not_authenticated_error(self) -> HTTPException:
+ return HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN, detail="Not authenticated"
+ )
+
+
+CredentialsDep = Annotated[HTTPAuthorizationCredentials, Depends(HTTPBearer403())]
+
+
+@app.get("/me")
+def read_me(credentials: CredentialsDep):
+ return {"message": "You are authenticated", "token": credentials.credentials}
diff --git a/docs_src/authentication_error_status_code/tutorial001_an_py39.py b/docs_src/authentication_error_status_code/tutorial001_an_py39.py
new file mode 100644
index 000000000..7bbc2f717
--- /dev/null
+++ b/docs_src/authentication_error_status_code/tutorial001_an_py39.py
@@ -0,0 +1,21 @@
+from typing import Annotated
+
+from fastapi import Depends, FastAPI, HTTPException, status
+from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
+
+app = FastAPI()
+
+
+class HTTPBearer403(HTTPBearer):
+ def make_not_authenticated_error(self) -> HTTPException:
+ return HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN, detail="Not authenticated"
+ )
+
+
+CredentialsDep = Annotated[HTTPAuthorizationCredentials, Depends(HTTPBearer403())]
+
+
+@app.get("/me")
+def read_me(credentials: CredentialsDep):
+ return {"message": "You are authenticated", "token": credentials.credentials}
diff --git a/docs_src/security/tutorial003.py b/docs_src/security/tutorial003.py
index 4b324866f..ce7a71b68 100644
--- a/docs_src/security/tutorial003.py
+++ b/docs_src/security/tutorial003.py
@@ -60,7 +60,7 @@ async def get_current_user(token: str = Depends(oauth2_scheme)):
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
- detail="Invalid authentication credentials",
+ detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
return user
diff --git a/docs_src/security/tutorial003_an.py b/docs_src/security/tutorial003_an.py
index 8fb40dd4a..1b7056a20 100644
--- a/docs_src/security/tutorial003_an.py
+++ b/docs_src/security/tutorial003_an.py
@@ -61,7 +61,7 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
- detail="Invalid authentication credentials",
+ detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
return user
diff --git a/docs_src/security/tutorial003_an_py310.py b/docs_src/security/tutorial003_an_py310.py
index ced4a2fbc..4a2743f6f 100644
--- a/docs_src/security/tutorial003_an_py310.py
+++ b/docs_src/security/tutorial003_an_py310.py
@@ -60,7 +60,7 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
- detail="Invalid authentication credentials",
+ detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
return user
diff --git a/docs_src/security/tutorial003_an_py39.py b/docs_src/security/tutorial003_an_py39.py
index 068a3933e..b396210c8 100644
--- a/docs_src/security/tutorial003_an_py39.py
+++ b/docs_src/security/tutorial003_an_py39.py
@@ -60,7 +60,7 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
- detail="Invalid authentication credentials",
+ detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
return user
diff --git a/docs_src/security/tutorial003_py310.py b/docs_src/security/tutorial003_py310.py
index af935e997..081259b31 100644
--- a/docs_src/security/tutorial003_py310.py
+++ b/docs_src/security/tutorial003_py310.py
@@ -58,7 +58,7 @@ async def get_current_user(token: str = Depends(oauth2_scheme)):
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
- detail="Invalid authentication credentials",
+ detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
return user
diff --git a/fastapi/__init__.py b/fastapi/__init__.py
index 85a7ea7b5..25ed2bbeb 100644
--- a/fastapi/__init__.py
+++ b/fastapi/__init__.py
@@ -1,6 +1,6 @@
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
-__version__ = "0.121.3"
+__version__ = "0.123.0"
from starlette import status as status
diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py
index d6359c0f5..fbb666a7d 100644
--- a/fastapi/dependencies/models.py
+++ b/fastapi/dependencies/models.py
@@ -38,19 +38,43 @@ class Dependant:
response_param_name: Optional[str] = None
background_tasks_param_name: Optional[str] = None
security_scopes_param_name: Optional[str] = None
- security_scopes: Optional[List[str]] = None
+ own_oauth_scopes: Optional[List[str]] = None
+ parent_oauth_scopes: Optional[List[str]] = None
use_cache: bool = True
path: Optional[str] = None
scope: Union[Literal["function", "request"], None] = None
+ @cached_property
+ def oauth_scopes(self) -> List[str]:
+ scopes = self.parent_oauth_scopes.copy() if self.parent_oauth_scopes else []
+ # This doesn't use a set to preserve order, just in case
+ for scope in self.own_oauth_scopes or []:
+ if scope not in scopes:
+ scopes.append(scope)
+ return scopes
+
@cached_property
def cache_key(self) -> DependencyCacheKey:
+ scopes_for_cache = (
+ tuple(sorted(set(self.oauth_scopes or []))) if self._uses_scopes else ()
+ )
return (
self.call,
- tuple(sorted(set(self.security_scopes or []))),
+ scopes_for_cache,
self.computed_scope or "",
)
+ @cached_property
+ def _uses_scopes(self) -> bool:
+ if self.own_oauth_scopes:
+ return True
+ if self.security_scopes_param_name is not None:
+ return True
+ for sub_dep in self.dependencies:
+ if sub_dep._uses_scopes:
+ return True
+ return False
+
@cached_property
def is_gen_callable(self) -> bool:
if inspect.isgeneratorfunction(self.call):
diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py
index 1e92c1ba2..d43fa8a51 100644
--- a/fastapi/dependencies/utils.py
+++ b/fastapi/dependencies/utils.py
@@ -58,8 +58,7 @@ from fastapi.dependencies.models import Dependant, SecurityRequirement
from fastapi.exceptions import DependencyScopeError
from fastapi.logger import logger
from fastapi.security.base import SecurityBase
-from fastapi.security.oauth2 import OAuth2, SecurityScopes
-from fastapi.security.open_id_connect_url import OpenIdConnect
+from fastapi.security.oauth2 import SecurityScopes
from fastapi.types import DependencyCacheKey
from fastapi.utils import create_model_field, get_path_param_names
from pydantic import BaseModel
@@ -126,14 +125,14 @@ def get_parameterless_sub_dependant(*, depends: params.Depends, path: str) -> De
assert callable(depends.dependency), (
"A parameter-less dependency must have a callable dependency"
)
- use_security_scopes: List[str] = []
+ own_oauth_scopes: List[str] = []
if isinstance(depends, params.Security) and depends.scopes:
- use_security_scopes.extend(depends.scopes)
+ own_oauth_scopes.extend(depends.scopes)
return get_dependant(
path=path,
call=depends.dependency,
scope=depends.scope,
- security_scopes=use_security_scopes,
+ own_oauth_scopes=own_oauth_scopes,
)
@@ -232,7 +231,8 @@ def get_dependant(
path: str,
call: Callable[..., Any],
name: Optional[str] = None,
- security_scopes: Optional[List[str]] = None,
+ own_oauth_scopes: Optional[List[str]] = None,
+ parent_oauth_scopes: Optional[List[str]] = None,
use_cache: bool = True,
scope: Union[Literal["function", "request"], None] = None,
) -> Dependant:
@@ -240,19 +240,18 @@ def get_dependant(
call=call,
name=name,
path=path,
- security_scopes=security_scopes,
use_cache=use_cache,
scope=scope,
+ own_oauth_scopes=own_oauth_scopes,
+ parent_oauth_scopes=parent_oauth_scopes,
)
+ current_scopes = (parent_oauth_scopes or []) + (own_oauth_scopes or [])
path_param_names = get_path_param_names(path)
endpoint_signature = get_typed_signature(call)
signature_params = endpoint_signature.parameters
if isinstance(call, SecurityBase):
- use_scopes: List[str] = []
- if isinstance(call, (OAuth2, OpenIdConnect)):
- use_scopes = security_scopes or use_scopes
security_requirement = SecurityRequirement(
- security_scheme=call, scopes=use_scopes
+ security_scheme=call, scopes=current_scopes
)
dependant.security_requirements.append(security_requirement)
for param_name, param in signature_params.items():
@@ -275,15 +274,16 @@ def get_dependant(
f'The dependency "{dependant.call.__name__}" has a scope of '
'"request", it cannot depend on dependencies with scope "function".'
)
- use_security_scopes = security_scopes or []
+ sub_own_oauth_scopes: List[str] = []
if isinstance(param_details.depends, params.Security):
if param_details.depends.scopes:
- use_security_scopes.extend(param_details.depends.scopes)
+ sub_own_oauth_scopes = list(param_details.depends.scopes)
sub_dependant = get_dependant(
path=path,
call=param_details.depends.dependency,
name=param_name,
- security_scopes=use_security_scopes,
+ own_oauth_scopes=sub_own_oauth_scopes,
+ parent_oauth_scopes=current_scopes,
use_cache=param_details.depends.use_cache,
scope=param_details.depends.scope,
)
@@ -609,7 +609,7 @@ async def solve_dependencies(
path=use_path,
call=call,
name=sub_dependant.name,
- security_scopes=sub_dependant.security_scopes,
+ parent_oauth_scopes=sub_dependant.oauth_scopes,
scope=sub_dependant.scope,
)
@@ -691,7 +691,7 @@ async def solve_dependencies(
values[dependant.response_param_name] = response
if dependant.security_scopes_param_name:
values[dependant.security_scopes_param_name] = SecurityScopes(
- scopes=dependant.security_scopes
+ scopes=dependant.oauth_scopes
)
return SolvedDependency(
values=values,
diff --git a/fastapi/security/api_key.py b/fastapi/security/api_key.py
index 496c815a7..81c7be10d 100644
--- a/fastapi/security/api_key.py
+++ b/fastapi/security/api_key.py
@@ -1,22 +1,52 @@
-from typing import Optional
+from typing import Optional, Union
from annotated_doc import Doc
from fastapi.openapi.models import APIKey, APIKeyIn
from fastapi.security.base import SecurityBase
from starlette.exceptions import HTTPException
from starlette.requests import Request
-from starlette.status import HTTP_403_FORBIDDEN
+from starlette.status import HTTP_401_UNAUTHORIZED
from typing_extensions import Annotated
class APIKeyBase(SecurityBase):
- @staticmethod
- def check_api_key(api_key: Optional[str], auto_error: bool) -> Optional[str]:
+ def __init__(
+ self,
+ location: APIKeyIn,
+ name: str,
+ description: Union[str, None],
+ scheme_name: Union[str, None],
+ auto_error: bool,
+ ):
+ self.auto_error = auto_error
+
+ self.model: APIKey = APIKey(
+ **{"in": location},
+ name=name,
+ description=description,
+ )
+ self.scheme_name = scheme_name or self.__class__.__name__
+
+ def make_not_authenticated_error(self) -> HTTPException:
+ """
+ The WWW-Authenticate header is not standardized for API Key authentication but
+ the HTTP specification requires that an error of 401 "Unauthorized" must
+ include a WWW-Authenticate header.
+
+ Ref: https://datatracker.ietf.org/doc/html/rfc9110#name-401-unauthorized
+
+ For this, this method sends a custom challenge `APIKey`.
+ """
+ return HTTPException(
+ status_code=HTTP_401_UNAUTHORIZED,
+ detail="Not authenticated",
+ headers={"WWW-Authenticate": "APIKey"},
+ )
+
+ def check_api_key(self, api_key: Optional[str]) -> Optional[str]:
if not api_key:
- if auto_error:
- raise HTTPException(
- status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
- )
+ if self.auto_error:
+ raise self.make_not_authenticated_error()
return None
return api_key
@@ -100,17 +130,17 @@ class APIKeyQuery(APIKeyBase):
),
] = True,
):
- self.model: APIKey = APIKey(
- **{"in": APIKeyIn.query},
+ super().__init__(
+ location=APIKeyIn.query,
name=name,
+ scheme_name=scheme_name,
description=description,
+ auto_error=auto_error,
)
- self.scheme_name = scheme_name or self.__class__.__name__
- self.auto_error = auto_error
async def __call__(self, request: Request) -> Optional[str]:
api_key = request.query_params.get(self.model.name)
- return self.check_api_key(api_key, self.auto_error)
+ return self.check_api_key(api_key)
class APIKeyHeader(APIKeyBase):
@@ -188,17 +218,17 @@ class APIKeyHeader(APIKeyBase):
),
] = True,
):
- self.model: APIKey = APIKey(
- **{"in": APIKeyIn.header},
+ super().__init__(
+ location=APIKeyIn.header,
name=name,
+ scheme_name=scheme_name,
description=description,
+ auto_error=auto_error,
)
- self.scheme_name = scheme_name or self.__class__.__name__
- self.auto_error = auto_error
async def __call__(self, request: Request) -> Optional[str]:
api_key = request.headers.get(self.model.name)
- return self.check_api_key(api_key, self.auto_error)
+ return self.check_api_key(api_key)
class APIKeyCookie(APIKeyBase):
@@ -276,14 +306,14 @@ class APIKeyCookie(APIKeyBase):
),
] = True,
):
- self.model: APIKey = APIKey(
- **{"in": APIKeyIn.cookie},
+ super().__init__(
+ location=APIKeyIn.cookie,
name=name,
+ scheme_name=scheme_name,
description=description,
+ auto_error=auto_error,
)
- self.scheme_name = scheme_name or self.__class__.__name__
- self.auto_error = auto_error
async def __call__(self, request: Request) -> Optional[str]:
api_key = request.cookies.get(self.model.name)
- return self.check_api_key(api_key, self.auto_error)
+ return self.check_api_key(api_key)
diff --git a/fastapi/security/http.py b/fastapi/security/http.py
index 3a5985650..0d1bbba3a 100644
--- a/fastapi/security/http.py
+++ b/fastapi/security/http.py
@@ -1,6 +1,6 @@
import binascii
from base64 import b64decode
-from typing import Optional
+from typing import Dict, Optional
from annotated_doc import Doc
from fastapi.exceptions import HTTPException
@@ -10,7 +10,7 @@ from fastapi.security.base import SecurityBase
from fastapi.security.utils import get_authorization_scheme_param
from pydantic import BaseModel
from starlette.requests import Request
-from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN
+from starlette.status import HTTP_401_UNAUTHORIZED
from typing_extensions import Annotated
@@ -76,10 +76,22 @@ class HTTPBase(SecurityBase):
description: Optional[str] = None,
auto_error: bool = True,
):
- self.model = HTTPBaseModel(scheme=scheme, description=description)
+ self.model: HTTPBaseModel = HTTPBaseModel(
+ scheme=scheme, description=description
+ )
self.scheme_name = scheme_name or self.__class__.__name__
self.auto_error = auto_error
+ def make_authenticate_headers(self) -> Dict[str, str]:
+ return {"WWW-Authenticate": f"{self.model.scheme.title()}"}
+
+ def make_not_authenticated_error(self) -> HTTPException:
+ return HTTPException(
+ status_code=HTTP_401_UNAUTHORIZED,
+ detail="Not authenticated",
+ headers=self.make_authenticate_headers(),
+ )
+
async def __call__(
self, request: Request
) -> Optional[HTTPAuthorizationCredentials]:
@@ -87,9 +99,7 @@ class HTTPBase(SecurityBase):
scheme, credentials = get_authorization_scheme_param(authorization)
if not (authorization and scheme and credentials):
if self.auto_error:
- raise HTTPException(
- status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
- )
+ raise self.make_not_authenticated_error()
else:
return None
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
@@ -99,6 +109,8 @@ class HTTPBasic(HTTPBase):
"""
HTTP Basic authentication.
+ Ref: https://datatracker.ietf.org/doc/html/rfc7617
+
## Usage
Create an instance object and use that object as the dependency in `Depends()`.
@@ -185,36 +197,28 @@ class HTTPBasic(HTTPBase):
self.realm = realm
self.auto_error = auto_error
+ def make_authenticate_headers(self) -> Dict[str, str]:
+ if self.realm:
+ return {"WWW-Authenticate": f'Basic realm="{self.realm}"'}
+ return {"WWW-Authenticate": "Basic"}
+
async def __call__( # type: ignore
self, request: Request
) -> Optional[HTTPBasicCredentials]:
authorization = request.headers.get("Authorization")
scheme, param = get_authorization_scheme_param(authorization)
- if self.realm:
- unauthorized_headers = {"WWW-Authenticate": f'Basic realm="{self.realm}"'}
- else:
- unauthorized_headers = {"WWW-Authenticate": "Basic"}
if not authorization or scheme.lower() != "basic":
if self.auto_error:
- raise HTTPException(
- status_code=HTTP_401_UNAUTHORIZED,
- detail="Not authenticated",
- headers=unauthorized_headers,
- )
+ raise self.make_not_authenticated_error()
else:
return None
- invalid_user_credentials_exc = HTTPException(
- status_code=HTTP_401_UNAUTHORIZED,
- detail="Invalid authentication credentials",
- headers=unauthorized_headers,
- )
try:
data = b64decode(param).decode("ascii")
- except (ValueError, UnicodeDecodeError, binascii.Error):
- raise invalid_user_credentials_exc # noqa: B904
+ except (ValueError, UnicodeDecodeError, binascii.Error) as e:
+ raise self.make_not_authenticated_error() from e
username, separator, password = data.partition(":")
if not separator:
- raise invalid_user_credentials_exc
+ raise self.make_not_authenticated_error()
return HTTPBasicCredentials(username=username, password=password)
@@ -306,17 +310,12 @@ class HTTPBearer(HTTPBase):
scheme, credentials = get_authorization_scheme_param(authorization)
if not (authorization and scheme and credentials):
if self.auto_error:
- raise HTTPException(
- status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
- )
+ raise self.make_not_authenticated_error()
else:
return None
if scheme.lower() != "bearer":
if self.auto_error:
- raise HTTPException(
- status_code=HTTP_403_FORBIDDEN,
- detail="Invalid authentication credentials",
- )
+ raise self.make_not_authenticated_error()
else:
return None
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
@@ -326,6 +325,12 @@ class HTTPDigest(HTTPBase):
"""
HTTP Digest authentication.
+ **Warning**: this is only a stub to connect the components with OpenAPI in FastAPI,
+ but it doesn't implement the full Digest scheme, you would need to to subclass it
+ and implement it in your code.
+
+ Ref: https://datatracker.ietf.org/doc/html/rfc7616
+
## Usage
Create an instance object and use that object as the dependency in `Depends()`.
@@ -408,17 +413,12 @@ class HTTPDigest(HTTPBase):
scheme, credentials = get_authorization_scheme_param(authorization)
if not (authorization and scheme and credentials):
if self.auto_error:
- raise HTTPException(
- status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
- )
+ raise self.make_not_authenticated_error()
else:
return None
if scheme.lower() != "digest":
if self.auto_error:
- raise HTTPException(
- status_code=HTTP_403_FORBIDDEN,
- detail="Invalid authentication credentials",
- )
+ raise self.make_not_authenticated_error()
else:
return None
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
diff --git a/fastapi/security/oauth2.py b/fastapi/security/oauth2.py
index f8d97d762..b41b0f877 100644
--- a/fastapi/security/oauth2.py
+++ b/fastapi/security/oauth2.py
@@ -8,7 +8,7 @@ from fastapi.param_functions import Form
from fastapi.security.base import SecurityBase
from fastapi.security.utils import get_authorization_scheme_param
from starlette.requests import Request
-from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN
+from starlette.status import HTTP_401_UNAUTHORIZED
# TODO: import from typing when deprecating Python 3.9
from typing_extensions import Annotated
@@ -377,13 +377,33 @@ class OAuth2(SecurityBase):
self.scheme_name = scheme_name or self.__class__.__name__
self.auto_error = auto_error
+ def make_not_authenticated_error(self) -> HTTPException:
+ """
+ The OAuth 2 specification doesn't define the challenge that should be used,
+ because a `Bearer` token is not really the only option to authenticate.
+
+ But declaring any other authentication challenge would be application-specific
+ as it's not defined in the specification.
+
+ For practical reasons, this method uses the `Bearer` challenge by default, as
+ it's probably the most common one.
+
+ If you are implementing an OAuth2 authentication scheme other than the provided
+ ones in FastAPI (based on bearer tokens), you might want to override this.
+
+ Ref: https://datatracker.ietf.org/doc/html/rfc6749
+ """
+ return HTTPException(
+ status_code=HTTP_401_UNAUTHORIZED,
+ detail="Not authenticated",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+
async def __call__(self, request: Request) -> Optional[str]:
authorization = request.headers.get("Authorization")
if not authorization:
if self.auto_error:
- raise HTTPException(
- status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
- )
+ raise self.make_not_authenticated_error()
else:
return None
return authorization
@@ -491,11 +511,7 @@ class OAuth2PasswordBearer(OAuth2):
scheme, param = get_authorization_scheme_param(authorization)
if not authorization or scheme.lower() != "bearer":
if self.auto_error:
- raise HTTPException(
- status_code=HTTP_401_UNAUTHORIZED,
- detail="Not authenticated",
- headers={"WWW-Authenticate": "Bearer"},
- )
+ raise self.make_not_authenticated_error()
else:
return None
return param
@@ -601,11 +617,7 @@ class OAuth2AuthorizationCodeBearer(OAuth2):
scheme, param = get_authorization_scheme_param(authorization)
if not authorization or scheme.lower() != "bearer":
if self.auto_error:
- raise HTTPException(
- status_code=HTTP_401_UNAUTHORIZED,
- detail="Not authenticated",
- headers={"WWW-Authenticate": "Bearer"},
- )
+ raise self.make_not_authenticated_error()
else:
return None # pragma: nocover
return param
diff --git a/fastapi/security/open_id_connect_url.py b/fastapi/security/open_id_connect_url.py
index 5e99798e6..e574a56a8 100644
--- a/fastapi/security/open_id_connect_url.py
+++ b/fastapi/security/open_id_connect_url.py
@@ -5,7 +5,7 @@ from fastapi.openapi.models import OpenIdConnect as OpenIdConnectModel
from fastapi.security.base import SecurityBase
from starlette.exceptions import HTTPException
from starlette.requests import Request
-from starlette.status import HTTP_403_FORBIDDEN
+from starlette.status import HTTP_401_UNAUTHORIZED
from typing_extensions import Annotated
@@ -13,6 +13,11 @@ class OpenIdConnect(SecurityBase):
"""
OpenID Connect authentication class. An instance of it would be used as a
dependency.
+
+ **Warning**: this is only a stub to connect the components with OpenAPI in FastAPI,
+ but it doesn't implement the full OpenIdConnect scheme, for example, it doesn't use
+ the OpenIDConnect URL. You would need to to subclass it and implement it in your
+ code.
"""
def __init__(
@@ -73,13 +78,18 @@ class OpenIdConnect(SecurityBase):
self.scheme_name = scheme_name or self.__class__.__name__
self.auto_error = auto_error
+ def make_not_authenticated_error(self) -> HTTPException:
+ return HTTPException(
+ status_code=HTTP_401_UNAUTHORIZED,
+ detail="Not authenticated",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+
async def __call__(self, request: Request) -> Optional[str]:
authorization = request.headers.get("Authorization")
if not authorization:
if self.auto_error:
- raise HTTPException(
- status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
- )
+ raise self.make_not_authenticated_error()
else:
return None
return authorization
diff --git a/requirements-docs.txt b/requirements-docs.txt
index 05b47fe92..4f1863a4a 100644
--- a/requirements-docs.txt
+++ b/requirements-docs.txt
@@ -17,5 +17,5 @@ griffe-warnings-deprecated==1.1.0
# For griffe, it formats with black
black==25.1.0
mkdocs-macros-plugin==1.4.1
-markdown-include-variants==0.0.5
+markdown-include-variants==0.0.7
python-slugify==8.0.4
diff --git a/tests/test_security_api_key_cookie.py b/tests/test_security_api_key_cookie.py
index 4ddb8e2ee..9bacfc56e 100644
--- a/tests/test_security_api_key_cookie.py
+++ b/tests/test_security_api_key_cookie.py
@@ -32,8 +32,9 @@ def test_security_api_key():
def test_security_api_key_no_key():
client = TestClient(app)
response = client.get("/users/me")
- assert response.status_code == 403, response.text
+ assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"}
+ assert response.headers["WWW-Authenticate"] == "APIKey"
def test_openapi_schema():
diff --git a/tests/test_security_api_key_cookie_description.py b/tests/test_security_api_key_cookie_description.py
index d99d616e0..d0cab324e 100644
--- a/tests/test_security_api_key_cookie_description.py
+++ b/tests/test_security_api_key_cookie_description.py
@@ -32,8 +32,9 @@ def test_security_api_key():
def test_security_api_key_no_key():
client = TestClient(app)
response = client.get("/users/me")
- assert response.status_code == 403, response.text
+ assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"}
+ assert response.headers["WWW-Authenticate"] == "APIKey"
def test_openapi_schema():
diff --git a/tests/test_security_api_key_header.py b/tests/test_security_api_key_header.py
index 1ff883703..3e761b150 100644
--- a/tests/test_security_api_key_header.py
+++ b/tests/test_security_api_key_header.py
@@ -33,8 +33,9 @@ def test_security_api_key():
def test_security_api_key_no_key():
response = client.get("/users/me")
- assert response.status_code == 403, response.text
+ assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"}
+ assert response.headers["WWW-Authenticate"] == "APIKey"
def test_openapi_schema():
diff --git a/tests/test_security_api_key_header_description.py b/tests/test_security_api_key_header_description.py
index 27f9d0f29..38a1a8881 100644
--- a/tests/test_security_api_key_header_description.py
+++ b/tests/test_security_api_key_header_description.py
@@ -33,8 +33,9 @@ def test_security_api_key():
def test_security_api_key_no_key():
response = client.get("/users/me")
- assert response.status_code == 403, response.text
+ assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"}
+ assert response.headers["WWW-Authenticate"] == "APIKey"
def test_openapi_schema():
diff --git a/tests/test_security_api_key_query.py b/tests/test_security_api_key_query.py
index dc7a0a621..11ed19468 100644
--- a/tests/test_security_api_key_query.py
+++ b/tests/test_security_api_key_query.py
@@ -33,8 +33,9 @@ def test_security_api_key():
def test_security_api_key_no_key():
response = client.get("/users/me")
- assert response.status_code == 403, response.text
+ assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"}
+ assert response.headers["WWW-Authenticate"] == "APIKey"
def test_openapi_schema():
diff --git a/tests/test_security_api_key_query_description.py b/tests/test_security_api_key_query_description.py
index 35dc7743a..658798326 100644
--- a/tests/test_security_api_key_query_description.py
+++ b/tests/test_security_api_key_query_description.py
@@ -33,8 +33,9 @@ def test_security_api_key():
def test_security_api_key_no_key():
response = client.get("/users/me")
- assert response.status_code == 403, response.text
+ assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"}
+ assert response.headers["WWW-Authenticate"] == "APIKey"
def test_openapi_schema():
diff --git a/tests/test_security_http_base.py b/tests/test_security_http_base.py
index 51928bafd..8cf259a75 100644
--- a/tests/test_security_http_base.py
+++ b/tests/test_security_http_base.py
@@ -23,8 +23,9 @@ def test_security_http_base():
def test_security_http_base_no_credentials():
response = client.get("/users/me")
- assert response.status_code == 403, response.text
+ assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"}
+ assert response.headers["WWW-Authenticate"] == "Other"
def test_openapi_schema():
diff --git a/tests/test_security_http_base_description.py b/tests/test_security_http_base_description.py
index bc79f3242..791ea59f4 100644
--- a/tests/test_security_http_base_description.py
+++ b/tests/test_security_http_base_description.py
@@ -23,8 +23,9 @@ def test_security_http_base():
def test_security_http_base_no_credentials():
response = client.get("/users/me")
- assert response.status_code == 403, response.text
+ assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"}
+ assert response.headers["WWW-Authenticate"] == "Other"
def test_openapi_schema():
diff --git a/tests/test_security_http_basic_optional.py b/tests/test_security_http_basic_optional.py
index 9b6cb6c45..7071f381a 100644
--- a/tests/test_security_http_basic_optional.py
+++ b/tests/test_security_http_basic_optional.py
@@ -38,7 +38,7 @@ def test_security_http_basic_invalid_credentials():
)
assert response.status_code == 401, response.text
assert response.headers["WWW-Authenticate"] == "Basic"
- assert response.json() == {"detail": "Invalid authentication credentials"}
+ assert response.json() == {"detail": "Not authenticated"}
def test_security_http_basic_non_basic_credentials():
@@ -47,7 +47,7 @@ def test_security_http_basic_non_basic_credentials():
response = client.get("/users/me", headers={"Authorization": auth_header})
assert response.status_code == 401, response.text
assert response.headers["WWW-Authenticate"] == "Basic"
- assert response.json() == {"detail": "Invalid authentication credentials"}
+ assert response.json() == {"detail": "Not authenticated"}
def test_openapi_schema():
diff --git a/tests/test_security_http_basic_realm.py b/tests/test_security_http_basic_realm.py
index 9fc33971a..ec7371f90 100644
--- a/tests/test_security_http_basic_realm.py
+++ b/tests/test_security_http_basic_realm.py
@@ -36,7 +36,7 @@ def test_security_http_basic_invalid_credentials():
)
assert response.status_code == 401, response.text
assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"'
- assert response.json() == {"detail": "Invalid authentication credentials"}
+ assert response.json() == {"detail": "Not authenticated"}
def test_security_http_basic_non_basic_credentials():
@@ -45,7 +45,7 @@ def test_security_http_basic_non_basic_credentials():
response = client.get("/users/me", headers={"Authorization": auth_header})
assert response.status_code == 401, response.text
assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"'
- assert response.json() == {"detail": "Invalid authentication credentials"}
+ assert response.json() == {"detail": "Not authenticated"}
def test_openapi_schema():
diff --git a/tests/test_security_http_basic_realm_description.py b/tests/test_security_http_basic_realm_description.py
index 02122442e..a93d5fc86 100644
--- a/tests/test_security_http_basic_realm_description.py
+++ b/tests/test_security_http_basic_realm_description.py
@@ -36,7 +36,7 @@ def test_security_http_basic_invalid_credentials():
)
assert response.status_code == 401, response.text
assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"'
- assert response.json() == {"detail": "Invalid authentication credentials"}
+ assert response.json() == {"detail": "Not authenticated"}
def test_security_http_basic_non_basic_credentials():
@@ -45,7 +45,7 @@ def test_security_http_basic_non_basic_credentials():
response = client.get("/users/me", headers={"Authorization": auth_header})
assert response.status_code == 401, response.text
assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"'
- assert response.json() == {"detail": "Invalid authentication credentials"}
+ assert response.json() == {"detail": "Not authenticated"}
def test_openapi_schema():
diff --git a/tests/test_security_http_bearer.py b/tests/test_security_http_bearer.py
index 5b9e2d691..961b42f4d 100644
--- a/tests/test_security_http_bearer.py
+++ b/tests/test_security_http_bearer.py
@@ -23,14 +23,16 @@ def test_security_http_bearer():
def test_security_http_bearer_no_credentials():
response = client.get("/users/me")
- assert response.status_code == 403, response.text
+ assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"}
+ assert response.headers["WWW-Authenticate"] == "Bearer"
def test_security_http_bearer_incorrect_scheme_credentials():
response = client.get("/users/me", headers={"Authorization": "Basic notreally"})
- assert response.status_code == 403, response.text
- assert response.json() == {"detail": "Invalid authentication credentials"}
+ assert response.status_code == 401, response.text
+ assert response.json() == {"detail": "Not authenticated"}
+ assert response.headers["WWW-Authenticate"] == "Bearer"
def test_openapi_schema():
diff --git a/tests/test_security_http_bearer_description.py b/tests/test_security_http_bearer_description.py
index 2f11c3a14..e16994abc 100644
--- a/tests/test_security_http_bearer_description.py
+++ b/tests/test_security_http_bearer_description.py
@@ -23,14 +23,16 @@ def test_security_http_bearer():
def test_security_http_bearer_no_credentials():
response = client.get("/users/me")
- assert response.status_code == 403, response.text
+ assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"}
+ assert response.headers["WWW-Authenticate"] == "Bearer"
def test_security_http_bearer_incorrect_scheme_credentials():
response = client.get("/users/me", headers={"Authorization": "Basic notreally"})
- assert response.status_code == 403, response.text
- assert response.json() == {"detail": "Invalid authentication credentials"}
+ assert response.status_code == 401, response.text
+ assert response.json() == {"detail": "Not authenticated"}
+ assert response.headers["WWW-Authenticate"] == "Bearer"
def test_openapi_schema():
diff --git a/tests/test_security_http_digest.py b/tests/test_security_http_digest.py
index 133d35763..3fad4c7a5 100644
--- a/tests/test_security_http_digest.py
+++ b/tests/test_security_http_digest.py
@@ -23,16 +23,18 @@ def test_security_http_digest():
def test_security_http_digest_no_credentials():
response = client.get("/users/me")
- assert response.status_code == 403, response.text
+ assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"}
+ assert response.headers["WWW-Authenticate"] == "Digest"
def test_security_http_digest_incorrect_scheme_credentials():
response = client.get(
"/users/me", headers={"Authorization": "Other invalidauthorization"}
)
- assert response.status_code == 403, response.text
- assert response.json() == {"detail": "Invalid authentication credentials"}
+ assert response.status_code == 401, response.text
+ assert response.json() == {"detail": "Not authenticated"}
+ assert response.headers["WWW-Authenticate"] == "Digest"
def test_openapi_schema():
diff --git a/tests/test_security_http_digest_description.py b/tests/test_security_http_digest_description.py
index 4e31a0c00..319416a07 100644
--- a/tests/test_security_http_digest_description.py
+++ b/tests/test_security_http_digest_description.py
@@ -23,16 +23,18 @@ def test_security_http_digest():
def test_security_http_digest_no_credentials():
response = client.get("/users/me")
- assert response.status_code == 403, response.text
+ assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"}
+ assert response.headers["WWW-Authenticate"] == "Digest"
def test_security_http_digest_incorrect_scheme_credentials():
response = client.get(
"/users/me", headers={"Authorization": "Other invalidauthorization"}
)
- assert response.status_code == 403, response.text
- assert response.json() == {"detail": "Invalid authentication credentials"}
+ assert response.status_code == 401, response.text
+ assert response.json() == {"detail": "Not authenticated"}
+ assert response.headers["WWW-Authenticate"] == "Digest"
def test_openapi_schema():
diff --git a/tests/test_security_oauth2.py b/tests/test_security_oauth2.py
index 2b7e3457a..804e4152d 100644
--- a/tests/test_security_oauth2.py
+++ b/tests/test_security_oauth2.py
@@ -56,8 +56,9 @@ def test_security_oauth2_password_other_header():
def test_security_oauth2_password_bearer_no_header():
response = client.get("/users/me")
- assert response.status_code == 403, response.text
+ assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"}
+ assert response.headers["WWW-Authenticate"] == "Bearer"
def test_strict_login_no_data():
diff --git a/tests/test_security_openid_connect.py b/tests/test_security_openid_connect.py
index 1e322e640..c9a0a8db7 100644
--- a/tests/test_security_openid_connect.py
+++ b/tests/test_security_openid_connect.py
@@ -39,8 +39,9 @@ def test_security_oauth2_password_other_header():
def test_security_oauth2_password_bearer_no_header():
response = client.get("/users/me")
- assert response.status_code == 403, response.text
+ assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"}
+ assert response.headers["WWW-Authenticate"] == "Bearer"
def test_openapi_schema():
diff --git a/tests/test_security_openid_connect_description.py b/tests/test_security_openid_connect_description.py
index 44cf57f86..d008cbc63 100644
--- a/tests/test_security_openid_connect_description.py
+++ b/tests/test_security_openid_connect_description.py
@@ -41,8 +41,9 @@ def test_security_oauth2_password_other_header():
def test_security_oauth2_password_bearer_no_header():
response = client.get("/users/me")
- assert response.status_code == 403, response.text
+ assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"}
+ assert response.headers["WWW-Authenticate"] == "Bearer"
def test_openapi_schema():
diff --git a/tests/test_security_scopes.py b/tests/test_security_scopes.py
new file mode 100644
index 000000000..248fd2bcc
--- /dev/null
+++ b/tests/test_security_scopes.py
@@ -0,0 +1,46 @@
+from typing import Dict
+
+import pytest
+from fastapi import Depends, FastAPI, Security
+from fastapi.testclient import TestClient
+from typing_extensions import Annotated
+
+
+@pytest.fixture(name="call_counter")
+def call_counter_fixture():
+ return {"count": 0}
+
+
+@pytest.fixture(name="app")
+def app_fixture(call_counter: Dict[str, int]):
+ def get_db():
+ call_counter["count"] += 1
+ return f"db_{call_counter['count']}"
+
+ def get_user(db: Annotated[str, Depends(get_db)]):
+ return "user"
+
+ app = FastAPI()
+
+ @app.get("/")
+ def endpoint(
+ db: Annotated[str, Depends(get_db)],
+ user: Annotated[str, Security(get_user, scopes=["read"])],
+ ):
+ return {"db": db}
+
+ return app
+
+
+@pytest.fixture(name="client")
+def client_fixture(app: FastAPI):
+ return TestClient(app)
+
+
+def test_security_scopes_dependency_called_once(
+ client: TestClient, call_counter: Dict[str, int]
+):
+ response = client.get("/")
+
+ assert response.status_code == 200
+ assert call_counter["count"] == 1
diff --git a/tests/test_security_scopes_dont_propagate.py b/tests/test_security_scopes_dont_propagate.py
new file mode 100644
index 000000000..2bbcc749d
--- /dev/null
+++ b/tests/test_security_scopes_dont_propagate.py
@@ -0,0 +1,45 @@
+# Ref: https://github.com/tiangolo/fastapi/issues/5623
+
+from typing import Any, Dict, List
+
+from fastapi import FastAPI, Security
+from fastapi.security import SecurityScopes
+from fastapi.testclient import TestClient
+from typing_extensions import Annotated
+
+
+async def security1(scopes: SecurityScopes):
+ return scopes.scopes
+
+
+async def security2(scopes: SecurityScopes):
+ return scopes.scopes
+
+
+async def dep3(
+ dep1: Annotated[List[str], Security(security1, scopes=["scope1"])],
+ dep2: Annotated[List[str], Security(security2, scopes=["scope2"])],
+):
+ return {"dep1": dep1, "dep2": dep2}
+
+
+app = FastAPI()
+
+
+@app.get("/scopes")
+def get_scopes(
+ dep3: Annotated[Dict[str, Any], Security(dep3, scopes=["scope3"])],
+):
+ return dep3
+
+
+client = TestClient(app)
+
+
+def test_security_scopes_dont_propagate():
+ response = client.get("/scopes")
+ assert response.status_code == 200
+ assert response.json() == {
+ "dep1": ["scope3", "scope1"],
+ "dep2": ["scope3", "scope2"],
+ }
diff --git a/tests/test_security_scopes_sub_dependency.py b/tests/test_security_scopes_sub_dependency.py
new file mode 100644
index 000000000..9cc668d8e
--- /dev/null
+++ b/tests/test_security_scopes_sub_dependency.py
@@ -0,0 +1,107 @@
+# Ref: https://github.com/fastapi/fastapi/discussions/6024#discussioncomment-8541913
+
+from typing import Dict
+
+import pytest
+from fastapi import Depends, FastAPI, Security
+from fastapi.security import SecurityScopes
+from fastapi.testclient import TestClient
+from typing_extensions import Annotated
+
+
+@pytest.fixture(name="call_counts")
+def call_counts_fixture():
+ return {
+ "get_db_session": 0,
+ "get_current_user": 0,
+ "get_user_me": 0,
+ "get_user_items": 0,
+ }
+
+
+@pytest.fixture(name="app")
+def app_fixture(call_counts: Dict[str, int]):
+ def get_db_session():
+ call_counts["get_db_session"] += 1
+ return f"db_session_{call_counts['get_db_session']}"
+
+ def get_current_user(
+ security_scopes: SecurityScopes,
+ db_session: Annotated[str, Depends(get_db_session)],
+ ):
+ call_counts["get_current_user"] += 1
+ return {
+ "user": f"user_{call_counts['get_current_user']}",
+ "scopes": security_scopes.scopes,
+ "db_session": db_session,
+ }
+
+ def get_user_me(
+ current_user: Annotated[dict, Security(get_current_user, scopes=["me"])],
+ ):
+ call_counts["get_user_me"] += 1
+ return {
+ "user_me": f"user_me_{call_counts['get_user_me']}",
+ "current_user": current_user,
+ }
+
+ def get_user_items(
+ user_me: Annotated[dict, Depends(get_user_me)],
+ ):
+ call_counts["get_user_items"] += 1
+ return {
+ "user_items": f"user_items_{call_counts['get_user_items']}",
+ "user_me": user_me,
+ }
+
+ app = FastAPI()
+
+ @app.get("/")
+ def path_operation(
+ user_me: Annotated[dict, Depends(get_user_me)],
+ user_items: Annotated[dict, Security(get_user_items, scopes=["items"])],
+ ):
+ return {
+ "user_me": user_me,
+ "user_items": user_items,
+ }
+
+ return app
+
+
+@pytest.fixture(name="client")
+def client_fixture(app: FastAPI):
+ return TestClient(app)
+
+
+def test_security_scopes_sub_dependency_caching(
+ client: TestClient, call_counts: Dict[str, int]
+):
+ response = client.get("/")
+
+ assert response.status_code == 200
+ assert call_counts["get_db_session"] == 1
+ assert call_counts["get_current_user"] == 2
+ assert call_counts["get_user_me"] == 2
+ assert call_counts["get_user_items"] == 1
+ assert response.json() == {
+ "user_me": {
+ "user_me": "user_me_1",
+ "current_user": {
+ "user": "user_1",
+ "scopes": ["me"],
+ "db_session": "db_session_1",
+ },
+ },
+ "user_items": {
+ "user_items": "user_items_1",
+ "user_me": {
+ "user_me": "user_me_2",
+ "current_user": {
+ "user": "user_2",
+ "scopes": ["items", "me"],
+ "db_session": "db_session_1",
+ },
+ },
+ },
+ }
diff --git a/tests/test_top_level_security_scheme_in_openapi.py b/tests/test_top_level_security_scheme_in_openapi.py
index e2de31af5..a36c66d1a 100644
--- a/tests/test_top_level_security_scheme_in_openapi.py
+++ b/tests/test_top_level_security_scheme_in_openapi.py
@@ -27,7 +27,7 @@ def test_get_root():
def test_get_root_no_token():
response = client.get("/")
- assert response.status_code == 403, response.text
+ assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"}
diff --git a/tests/test_tutorial/test_authentication_error_status_code/__init__.py b/tests/test_tutorial/test_authentication_error_status_code/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/test_tutorial/test_authentication_error_status_code/test_tutorial001.py b/tests/test_tutorial/test_authentication_error_status_code/test_tutorial001.py
new file mode 100644
index 000000000..bbd7bff30
--- /dev/null
+++ b/tests/test_tutorial/test_authentication_error_status_code/test_tutorial001.py
@@ -0,0 +1,69 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+from inline_snapshot import snapshot
+
+from ...utils import needs_py39
+
+
+@pytest.fixture(
+ name="client",
+ params=[
+ "tutorial001_an",
+ pytest.param("tutorial001_an_py39", marks=needs_py39),
+ ],
+)
+def get_client(request: pytest.FixtureRequest):
+ mod = importlib.import_module(
+ f"docs_src.authentication_error_status_code.{request.param}"
+ )
+
+ client = TestClient(mod.app)
+ return client
+
+
+def test_get_me(client: TestClient):
+ response = client.get("/me", headers={"Authorization": "Bearer secrettoken"})
+ assert response.status_code == 200
+ assert response.json() == {
+ "message": "You are authenticated",
+ "token": "secrettoken",
+ }
+
+
+def test_get_me_no_credentials(client: TestClient):
+ response = client.get("/me")
+ assert response.status_code == 403
+ assert response.json() == {"detail": "Not authenticated"}
+
+
+def test_openapi_schema(client: TestClient):
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == snapshot(
+ {
+ "openapi": "3.1.0",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/me": {
+ "get": {
+ "summary": "Read Me",
+ "operationId": "read_me_me_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ }
+ },
+ "security": [{"HTTPBearer403": []}],
+ }
+ }
+ },
+ "components": {
+ "securitySchemes": {
+ "HTTPBearer403": {"type": "http", "scheme": "bearer"}
+ }
+ },
+ }
+ )
diff --git a/tests/test_tutorial/test_security/test_tutorial003.py b/tests/test_tutorial/test_security/test_tutorial003.py
index 2bbb2e851..6b8735113 100644
--- a/tests/test_tutorial/test_security/test_tutorial003.py
+++ b/tests/test_tutorial/test_security/test_tutorial003.py
@@ -66,7 +66,7 @@ def test_token(client: TestClient):
def test_incorrect_token(client: TestClient):
response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"})
assert response.status_code == 401, response.text
- assert response.json() == {"detail": "Invalid authentication credentials"}
+ assert response.json() == {"detail": "Not authenticated"}
assert response.headers["WWW-Authenticate"] == "Bearer"
diff --git a/tests/test_tutorial/test_security/test_tutorial006.py b/tests/test_tutorial/test_security/test_tutorial006.py
index 40b413806..9587159dc 100644
--- a/tests/test_tutorial/test_security/test_tutorial006.py
+++ b/tests/test_tutorial/test_security/test_tutorial006.py
@@ -41,7 +41,7 @@ def test_security_http_basic_invalid_credentials(client: TestClient):
)
assert response.status_code == 401, response.text
assert response.headers["WWW-Authenticate"] == "Basic"
- assert response.json() == {"detail": "Invalid authentication credentials"}
+ assert response.json() == {"detail": "Not authenticated"}
def test_security_http_basic_non_basic_credentials(client: TestClient):
@@ -50,7 +50,7 @@ def test_security_http_basic_non_basic_credentials(client: TestClient):
response = client.get("/users/me", headers={"Authorization": auth_header})
assert response.status_code == 401, response.text
assert response.headers["WWW-Authenticate"] == "Basic"
- assert response.json() == {"detail": "Invalid authentication credentials"}
+ assert response.json() == {"detail": "Not authenticated"}
def test_openapi_schema(client: TestClient):