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):