diff --git a/.github/labeler.yml b/.github/labeler.yml
index c5b1f84f3..cdaefbf2d 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -17,6 +17,7 @@ lang-all:
- docs/*/docs/**
- all-globs-to-all-files:
- '!docs/en/docs/**'
+ - '!docs/*/**/_*.md'
- '!fastapi/**'
- '!pyproject.toml'
diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml
index f78b6730e..73e1c6b67 100644
--- a/.github/workflows/build-docs.yml
+++ b/.github/workflows/build-docs.yml
@@ -21,7 +21,7 @@ jobs:
outputs:
docs: ${{ steps.filter.outputs.docs }}
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
# For pull requests it's not necessary to checkout the code but for the main branch it is
- uses: dorny/paths-filter@v3
id: filter
@@ -32,12 +32,9 @@ jobs:
- docs/**
- docs_src/**
- requirements-docs.txt
- - requirements-docs-insiders.txt
- pyproject.toml
- mkdocs.yml
- - mkdocs.insiders.yml
- - mkdocs.maybe-insiders.yml
- - mkdocs.no-insiders.yml
+ - mkdocs.env.yml
- .github/workflows/build-docs.yml
- .github/workflows/deploy-docs.yml
- scripts/mkdocs_hooks.py
@@ -48,7 +45,7 @@ jobs:
outputs:
langs: ${{ steps.show-langs.outputs.langs }}
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
@@ -63,12 +60,6 @@ jobs:
pyproject.toml
- name: Install docs extras
run: uv pip install -r requirements-docs.txt
- # Install MkDocs Material Insiders here just to put it in the cache for the rest of the steps
- - name: Install Material for MkDocs Insiders
- if: ( github.event_name != 'pull_request' || github.secret_source == 'Actions' )
- run: uv pip install -r requirements-docs-insiders.txt
- env:
- TOKEN: ${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}
- name: Verify Docs
run: python ./scripts/docs.py verify-docs
- name: Export Language Codes
@@ -90,7 +81,7 @@ jobs:
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
@@ -105,11 +96,6 @@ jobs:
pyproject.toml
- name: Install docs extras
run: uv pip install -r requirements-docs.txt
- - name: Install Material for MkDocs Insiders
- if: ( github.event_name != 'pull_request' || github.secret_source == 'Actions' )
- run: uv pip install -r requirements-docs-insiders.txt
- env:
- TOKEN: ${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}
- name: Update Languages
run: python ./scripts/docs.py update-languages
- uses: actions/cache@v4
diff --git a/.github/workflows/contributors.yml b/.github/workflows/contributors.yml
index 7d5449c6a..2abd2fdcf 100644
--- a/.github/workflows/contributors.yml
+++ b/.github/workflows/contributors.yml
@@ -24,7 +24,7 @@ jobs:
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml
index aa4fd6b65..50662a190 100644
--- a/.github/workflows/deploy-docs.yml
+++ b/.github/workflows/deploy-docs.yml
@@ -23,7 +23,7 @@ jobs:
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
diff --git a/.github/workflows/label-approved.yml b/.github/workflows/label-approved.yml
index e6ae3d963..7f16254db 100644
--- a/.github/workflows/label-approved.yml
+++ b/.github/workflows/label-approved.yml
@@ -20,7 +20,7 @@ jobs:
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
diff --git a/.github/workflows/latest-changes.yml b/.github/workflows/latest-changes.yml
index 2fa832fab..b9e45ea62 100644
--- a/.github/workflows/latest-changes.yml
+++ b/.github/workflows/latest-changes.yml
@@ -24,6 +24,8 @@ jobs:
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
+ # pin to actions/checkout@v5 for compatibility with latest-changes
+ # Ref: https://github.com/actions/checkout/issues/2313
- uses: actions/checkout@v5
with:
# To allow latest-changes to commit to the main branch
@@ -34,7 +36,7 @@ jobs:
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }}
with:
limit-access-to-actor: true
- - uses: tiangolo/latest-changes@0.4.0
+ - uses: tiangolo/latest-changes@0.4.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
latest_changes_file: docs/en/docs/release-notes.md
diff --git a/.github/workflows/notify-translations.yml b/.github/workflows/notify-translations.yml
index 04beeb64e..971e6bbd8 100644
--- a/.github/workflows/notify-translations.yml
+++ b/.github/workflows/notify-translations.yml
@@ -28,7 +28,7 @@ jobs:
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
diff --git a/.github/workflows/people.yml b/.github/workflows/people.yml
index f15b92137..9b35a3d7e 100644
--- a/.github/workflows/people.yml
+++ b/.github/workflows/people.yml
@@ -24,7 +24,7 @@ jobs:
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml
new file mode 100644
index 000000000..fa0574d7d
--- /dev/null
+++ b/.github/workflows/pre-commit.yml
@@ -0,0 +1,88 @@
+name: pre-commit
+
+on:
+ pull_request:
+ types:
+ - opened
+ - synchronize
+
+env:
+ IS_FORK: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
+
+jobs:
+ pre-commit:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Dump GitHub context
+ env:
+ GITHUB_CONTEXT: ${{ toJson(github) }}
+ run: echo "$GITHUB_CONTEXT"
+ - uses: actions/checkout@v5
+ name: Checkout PR for own repo
+ if: env.IS_FORK == 'false'
+ with:
+ # To be able to commit it needs more than the last commit
+ ref: ${{ github.head_ref }}
+ # A token other than the default GITHUB_TOKEN is needed to be able to trigger CI
+ token: ${{ secrets.PRE_COMMIT }}
+ # pre-commit lite ci needs the default checkout configs to work
+ - uses: actions/checkout@v5
+ name: Checkout PR for fork
+ if: env.IS_FORK == 'true'
+ - name: Set up Python
+ uses: actions/setup-python@v6
+ with:
+ python-version: "3.14"
+ - name: Setup uv
+ uses: astral-sh/setup-uv@v7
+ with:
+ cache-dependency-glob: |
+ requirements**.txt
+ pyproject.toml
+ uv.lock
+ - name: Install Dependencies
+ run: |
+ uv venv
+ uv pip install -r requirements.txt
+ - name: Run pre-commit
+ id: precommit
+ run: |
+ # Fetch the base branch for comparison
+ git fetch origin ${{ github.base_ref }}
+ uvx pre-commit run --from-ref origin/${{ github.base_ref }} --to-ref HEAD --show-diff-on-failure
+ continue-on-error: true
+ - name: Commit and push changes
+ if: env.IS_FORK == 'false'
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+ git add -A
+ if git diff --staged --quiet; then
+ echo "No changes to commit"
+ else
+ git commit -m "🎨 Auto format"
+ git push
+ fi
+ - uses: pre-commit-ci/lite-action@v1.1.0
+ if: env.IS_FORK == 'true'
+ with:
+ msg: 🎨 Auto format
+ - name: Error out on pre-commit errors
+ if: steps.precommit.outcome == 'failure'
+ run: exit 1
+
+ # https://github.com/marketplace/actions/alls-green#why
+ pre-commit-alls-green: # This job does nothing and is only used for the branch protection
+ if: always()
+ needs:
+ - pre-commit
+ runs-on: ubuntu-latest
+ steps:
+ - name: Dump GitHub context
+ env:
+ GITHUB_CONTEXT: ${{ toJson(github) }}
+ run: echo "$GITHUB_CONTEXT"
+ - name: Decide whether the needed jobs succeeded or failed
+ uses: re-actors/alls-green@release/v1
+ with:
+ jobs: ${{ toJSON(needs) }}
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 441eb4560..6d9a00b49 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -20,7 +20,7 @@ jobs:
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml
index eed5fbec0..84c743019 100644
--- a/.github/workflows/smokeshow.yml
+++ b/.github/workflows/smokeshow.yml
@@ -21,7 +21,7 @@ jobs:
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.9'
diff --git a/.github/workflows/sponsors.yml b/.github/workflows/sponsors.yml
index 7d29469a5..8b0249001 100644
--- a/.github/workflows/sponsors.yml
+++ b/.github/workflows/sponsors.yml
@@ -24,7 +24,7 @@ jobs:
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
diff --git a/.github/workflows/test-redistribute.yml b/.github/workflows/test-redistribute.yml
index a44f0b681..653ab2a74 100644
--- a/.github/workflows/test-redistribute.yml
+++ b/.github/workflows/test-redistribute.yml
@@ -22,7 +22,7 @@ jobs:
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 9c3e2218b..8157e364b 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -23,7 +23,7 @@ jobs:
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
@@ -65,7 +65,7 @@ jobs:
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
@@ -111,7 +111,7 @@ jobs:
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.8'
diff --git a/.github/workflows/topic-repos.yml b/.github/workflows/topic-repos.yml
index 22b37d59d..41dabee1e 100644
--- a/.github/workflows/topic-repos.yml
+++ b/.github/workflows/topic-repos.yml
@@ -19,7 +19,7 @@ jobs:
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
diff --git a/.github/workflows/translate.yml b/.github/workflows/translate.yml
index a7fcf84df..6506b8e28 100644
--- a/.github/workflows/translate.yml
+++ b/.github/workflows/translate.yml
@@ -42,7 +42,7 @@ jobs:
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
diff --git a/.gitignore b/.gitignore
index ef6364a9a..6016ffa59 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,3 +28,6 @@ archive.zip
# macOS
.DS_Store
+
+# Ignore while the setup still depends on requirements.txt files
+uv.lock
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 8e5eba4c4..8e6d93fb7 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,25 +1,29 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
-default_language_version:
- python: python3.10
repos:
-- repo: https://github.com/pre-commit/pre-commit-hooks
+ - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- - id: check-added-large-files
- - id: check-toml
- - id: check-yaml
+ - id: check-added-large-files
+ - id: check-toml
+ - id: check-yaml
args:
- - --unsafe
- - id: end-of-file-fixer
- - id: trailing-whitespace
-- repo: https://github.com/astral-sh/ruff-pre-commit
+ - --unsafe
+ - id: end-of-file-fixer
+ - id: trailing-whitespace
+ - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.3
hooks:
- - id: ruff
+ - id: ruff
args:
- --fix
- - id: ruff-format
-ci:
- autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
- autoupdate_commit_msg: ⬆ [pre-commit.ci] pre-commit autoupdate
+ - id: ruff-format
+ - repo: local
+ hooks:
+ - id: local-script
+ language: unsupported
+ name: local script
+ entry: uv run ./scripts/docs.py add-permalinks-pages
+ args:
+ - --update-existing
+ files: ^docs/en/docs/.*\.md$
diff --git a/README.md b/README.md
index 09cd38da1..a42cedae6 100644
--- a/README.md
+++ b/README.md
@@ -40,11 +40,16 @@ The key features are:
* **Robust**: Get production-ready code. With automatic interactive documentation.
* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema.
-* estimation based on tests on an internal development team, building production applications.
+* estimation based on tests conducted by an internal development team, building production applications.
## Sponsors
+### Keystone Sponsor
+
+
+
+### Gold and Silver Sponsors
@@ -56,6 +61,7 @@ The key features are:
+
@@ -447,6 +453,58 @@ For a more complete example including more features, see the FastAPI Cloud, go and join the waiting list if you haven't. 🚀
+
+If you already have a **FastAPI Cloud** account (we invited you from the waiting list 😉), you can deploy your application with one command.
+
+Before deploying, make sure you are logged in:
+
+
+
+```console
+$ fastapi login
+
+You are logged in to FastAPI Cloud 🚀
+```
+
+
+
+Then deploy your app:
+
+
+
+```console
+$ fastapi deploy
+
+Deploying to FastAPI Cloud...
+
+✅ Deployment successful!
+
+🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev
+```
+
+
+
+That's it! Now you can access your app at that URL. ✨
+
+#### About FastAPI Cloud
+
+**FastAPI Cloud** is built by the same author and team behind **FastAPI**.
+
+It streamlines the process of **building**, **deploying**, and **accessing** an API with minimal effort.
+
+It brings the same **developer experience** of building apps with FastAPI to **deploying** them to the cloud. 🎉
+
+FastAPI Cloud is the primary sponsor and funding provider for the *FastAPI and friends* open source projects. ✨
+
+#### Deploy to other cloud providers
+
+FastAPI is open source and based on standards. You can deploy FastAPI apps to any cloud provider you choose.
+
+Follow your cloud provider's guides to deploy FastAPI apps with them. 🤓
+
## Performance
Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*)
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/sponsors.yml b/docs/en/data/sponsors.yml
index 943b92adb..50b114530 100644
--- a/docs/en/data/sponsors.yml
+++ b/docs/en/data/sponsors.yml
@@ -1,3 +1,7 @@
+keystone:
+ - url: https://fastapicloud.com
+ title: FastAPI Cloud. By the same team behind FastAPI. You code. We Cloud.
+ img: https://fastapi.tiangolo.com/img/sponsors/fastapicloud.png
gold:
- url: https://blockbee.io?ref=fastapi
title: BlockBee Cryptocurrency Payment Gateway
@@ -29,6 +33,9 @@ gold:
- url: https://serpapi.com/?utm_source=fastapi_website
title: "SerpApi: Web Search API"
img: https://fastapi.tiangolo.com/img/sponsors/serpapi.png
+ - url: https://www.greptile.com/?utm_source=fastapi&utm_medium=sponsorship&utm_campaign=fastapi_sponsor_page
+ title: "Greptile: The AI Code Reviewer"
+ img: https://fastapi.tiangolo.com/img/sponsors/greptile.png
silver:
- url: https://databento.com/?utm_source=fastapi&utm_medium=sponsor&utm_content=display
title: Pay as you go for market data
diff --git a/docs/en/data/sponsors_badge.yml b/docs/en/data/sponsors_badge.yml
index 14f55805c..d648be5fc 100644
--- a/docs/en/data/sponsors_badge.yml
+++ b/docs/en/data/sponsors_badge.yml
@@ -47,3 +47,4 @@ logins:
- railwayapp
- subtotal
- requestly
+ - greptileai
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 a38df772f..87111ff64 100644
--- a/docs/en/docs/css/custom.css
+++ b/docs/en/docs/css/custom.css
@@ -1,3 +1,18 @@
+/* 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, "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 {
color: #4a968f;
font-style: italic;
diff --git a/docs/en/docs/css/termynal.css b/docs/en/docs/css/termynal.css
index 8534f9102..a2564e286 100644
--- a/docs/en/docs/css/termynal.css
+++ b/docs/en/docs/css/termynal.css
@@ -20,7 +20,7 @@
/* font-size: 18px; */
font-size: 15px;
/* font-family: 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; */
- font-family: 'Roboto Mono', 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace;
+ font-family: var(--md-code-font-family), 'Roboto Mono', 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace;
border-radius: 4px;
padding: 75px 45px 35px;
position: relative;
diff --git a/docs/en/docs/deployment/cloud.md b/docs/en/docs/deployment/cloud.md
index c88c4b51a..4f5c23e4b 100644
--- a/docs/en/docs/deployment/cloud.md
+++ b/docs/en/docs/deployment/cloud.md
@@ -4,13 +4,21 @@ You can use virtually **any cloud provider** to deploy your FastAPI application.
In most of the cases, the main cloud providers have guides to deploy FastAPI with them.
-## Cloud Providers - Sponsors { #cloud-providers-sponsors }
+## FastAPI Cloud { #fastapi-cloud }
+
+**FastAPI Cloud** is built by the same author and team behind **FastAPI**.
+
+It streamlines the process of **building**, **deploying**, and **accessing** an API with minimal effort.
-Some cloud providers ✨ [**sponsor FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨, this ensures the continued and healthy **development** of FastAPI and its **ecosystem**.
+It brings the same **developer experience** of building apps with FastAPI to **deploying** them to the cloud. 🎉
+
+FastAPI Cloud is the primary sponsor and funding provider for the *FastAPI and friends* open source projects. ✨
+
+## Cloud Providers - Sponsors { #cloud-providers-sponsors }
-And it shows their true commitment to FastAPI and its **community** (you), as they not only want to provide you a **good service** but also want to make sure you have a **good and healthy framework**, FastAPI. 🙇
+Some other cloud providers ✨ [**sponsor FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨ too. 🙇
-You might want to try their services and follow their guides:
+You might also want to consider them to follow their guides and try their services:
* Render
* Railway
diff --git a/docs/en/docs/deployment/fastapicloud.md b/docs/en/docs/deployment/fastapicloud.md
new file mode 100644
index 000000000..b0889974f
--- /dev/null
+++ b/docs/en/docs/deployment/fastapicloud.md
@@ -0,0 +1,65 @@
+# FastAPI Cloud { #fastapi-cloud }
+
+You can deploy your FastAPI app to FastAPI Cloud with **one command**, go and join the waiting list if you haven't. 🚀
+
+## Login { #login }
+
+Make sure you already have a **FastAPI Cloud** account (we invited you from the waiting list 😉).
+
+Then log in:
+
+
+
+```console
+$ fastapi login
+
+You are logged in to FastAPI Cloud 🚀
+```
+
+
+
+## Deploy { #deploy }
+
+Now deploy your app, with **one command**:
+
+
+
+```console
+$ fastapi deploy
+
+Deploying to FastAPI Cloud...
+
+✅ Deployment successful!
+
+🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev
+```
+
+
+
+That's it! Now you can access your app at that URL. ✨
+
+## About FastAPI Cloud { #about-fastapi-cloud }
+
+**FastAPI Cloud** is built by the same author and team behind **FastAPI**.
+
+It streamlines the process of **building**, **deploying**, and **accessing** an API with minimal effort.
+
+It brings the same **developer experience** of building apps with FastAPI to **deploying** them to the cloud. 🎉
+
+It will also take care of most of the things you would need when deploying an app, like:
+
+* HTTPS
+* Replication, with autoscaling based on requests
+* etc.
+
+FastAPI Cloud is the primary sponsor and funding provider for the *FastAPI and friends* open source projects. ✨
+
+## Deploy to other cloud providers { #deploy-to-other-cloud-providers }
+
+FastAPI is open source and based on standards. You can deploy FastAPI apps to any cloud provider you choose.
+
+Follow your cloud provider's guides to deploy FastAPI apps with them. 🤓
+
+## Deploy your own server { #deploy-your-own-server }
+
+I will also teach you later in this **Deployment** guide all the details, so you can understand what is going on, what needs to happen, or how to deploy FastAPI apps on your own, also with your own servers. 🤓
diff --git a/docs/en/docs/deployment/index.md b/docs/en/docs/deployment/index.md
index 2364791a7..8d7521e73 100644
--- a/docs/en/docs/deployment/index.md
+++ b/docs/en/docs/deployment/index.md
@@ -16,6 +16,8 @@ There are several ways to do it depending on your specific use case and the tool
You could **deploy a server** yourself using a combination of tools, you could use a **cloud service** that does part of the work for you, or other possible options.
+For example, we, the team behind FastAPI, built **FastAPI Cloud**, to make deploying FastAPI apps to the cloud as streamlined as possible, with the same developer experience of working with FastAPI.
+
I will show you some of the main concepts you should probably keep in mind when deploying a **FastAPI** application (although most of it applies to any other type of web application).
You will see more details to keep in mind and some of the techniques to do it in the next sections. ✨
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/img/sponsors/fastapicloud.png b/docs/en/docs/img/sponsors/fastapicloud.png
new file mode 100644
index 000000000..c23dec220
Binary files /dev/null and b/docs/en/docs/img/sponsors/fastapicloud.png differ
diff --git a/docs/en/docs/img/sponsors/greptile-banner.png b/docs/en/docs/img/sponsors/greptile-banner.png
new file mode 100644
index 000000000..e0909b39d
Binary files /dev/null and b/docs/en/docs/img/sponsors/greptile-banner.png differ
diff --git a/docs/en/docs/img/sponsors/greptile.png b/docs/en/docs/img/sponsors/greptile.png
new file mode 100644
index 000000000..ae3d78cbd
Binary files /dev/null and b/docs/en/docs/img/sponsors/greptile.png differ
diff --git a/docs/en/docs/index.md b/docs/en/docs/index.md
index 35c46d15f..df03b7675 100644
--- a/docs/en/docs/index.md
+++ b/docs/en/docs/index.md
@@ -46,20 +46,26 @@ The key features are:
* **Robust**: Get production-ready code. With automatic interactive documentation.
* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema.
-* estimation based on tests on an internal development team, building production applications.
+* estimation based on tests conducted by an internal development team, building production applications.
## Sponsors { #sponsors }
-{% if sponsors %}
+### Keystone Sponsor
+
+{% for sponsor in sponsors.keystone -%}
+
+{% endfor -%}
+
+### Gold and Silver Sponsors
+
{% for sponsor in sponsors.gold -%}
{% endfor -%}
{%- for sponsor in sponsors.silver -%}
{% endfor %}
-{% endif %}
@@ -444,6 +450,58 @@ For a more complete example including more features, see the FastAPI Cloud, go and join the waiting list if you haven't. 🚀
+
+If you already have a **FastAPI Cloud** account (we invited you from the waiting list 😉), you can deploy your application with one command.
+
+Before deploying, make sure you are logged in:
+
+
+
+```console
+$ fastapi login
+
+You are logged in to FastAPI Cloud 🚀
+```
+
+
+
+Then deploy your app:
+
+
+
+```console
+$ fastapi deploy
+
+Deploying to FastAPI Cloud...
+
+✅ Deployment successful!
+
+🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev
+```
+
+
+
+That's it! Now you can access your app at that URL. ✨
+
+#### About FastAPI Cloud { #about-fastapi-cloud }
+
+**FastAPI Cloud** is built by the same author and team behind **FastAPI**.
+
+It streamlines the process of **building**, **deploying**, and **accessing** an API with minimal effort.
+
+It brings the same **developer experience** of building apps with FastAPI to **deploying** them to the cloud. 🎉
+
+FastAPI Cloud is the primary sponsor and funding provider for the *FastAPI and friends* open source projects. ✨
+
+#### Deploy to other cloud providers { #deploy-to-other-cloud-providers }
+
+FastAPI is open source and based on standards. You can deploy FastAPI apps to any cloud provider you choose.
+
+Follow your cloud provider's guides to deploy FastAPI apps with them. 🤓
+
## Performance { #performance }
Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*)
diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md
index a3e051a30..c0090b49a 100644
--- a/docs/en/docs/release-notes.md
+++ b/docs/en/docs/release-notes.md
@@ -7,6 +7,95 @@ hide:
## Latest Changes
+### Fixes
+
+* 🐛 Fix optional sequence handling in `serialize sequence value` with Pydantic V2. PR [#14297](https://github.com/fastapi/fastapi/pull/14297) by [@YuriiMotov](https://github.com/YuriiMotov).
+
+## 0.123.2
+
+### Fixes
+
+* 🐛 Fix unformatted `{type_}` in FastAPIError. PR [#14416](https://github.com/fastapi/fastapi/pull/14416) by [@Just-Helpful](https://github.com/Just-Helpful).
+* 🐛 Fix parsing extra non-body parameter list. PR [#14356](https://github.com/fastapi/fastapi/pull/14356) by [@YuriiMotov](https://github.com/YuriiMotov).
+* 🐛 Fix parsing extra `Form` parameter list. PR [#14303](https://github.com/fastapi/fastapi/pull/14303) by [@YuriiMotov](https://github.com/YuriiMotov).
+* 🐛 Fix support for form values with empty strings interpreted as missing (`None` if that's the default), for compatibility with HTML forms. PR [#13537](https://github.com/fastapi/fastapi/pull/13537) by [@MarinPostma](https://github.com/MarinPostma).
+
+### Docs
+
+* 📝 Add tip on how to install `pip` in case of `No module named pip` error in `virtual-environments.md`. PR [#14211](https://github.com/fastapi/fastapi/pull/14211) by [@zadevhub](https://github.com/zadevhub).
+* 📝 Update Primary Key notes for the SQL databases tutorial to avoid confusion. PR [#14120](https://github.com/fastapi/fastapi/pull/14120) by [@FlaviusRaducu](https://github.com/FlaviusRaducu).
+* 📝 Clarify estimation note in documentation. PR [#14070](https://github.com/fastapi/fastapi/pull/14070) by [@SaisakthiM](https://github.com/SaisakthiM).
+
+## 0.123.1
+
+### Fixes
+
+* 🐛 Avoid accessing non-existing "$ref" key for Pydantic v2 compat remapping. PR [#14361](https://github.com/fastapi/fastapi/pull/14361) by [@svlandeg](https://github.com/svlandeg).
+* 🐛 Fix `TypeError` when encoding a decimal with a `NaN` or `Infinity` value. PR [#12935](https://github.com/fastapi/fastapi/pull/12935) by [@kentwelcome](https://github.com/kentwelcome).
+
+### Internal
+
+* 🐛 Fix Windows UnicodeEncodeError in CLI test. PR [#14295](https://github.com/fastapi/fastapi/pull/14295) by [@hemanth-thirthahalli](https://github.com/hemanth-thirthahalli).
+* 🔧 Update sponsors: add Greptile. PR [#14429](https://github.com/fastapi/fastapi/pull/14429) by [@tiangolo](https://github.com/tiangolo).
+* 👥 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).
+* 👷 Add custom pre-commit CI. PR [#14397](https://github.com/fastapi/fastapi/pull/14397) by [@tiangolo](https://github.com/tiangolo).
+* ⬆ Bump actions/checkout from 5 to 6. PR [#14381](https://github.com/fastapi/fastapi/pull/14381) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* 👷 Upgrade `latest-changes` GitHub Action and pin `actions/checkout@v5`. PR [#14403](https://github.com/fastapi/fastapi/pull/14403) by [@svlandeg](https://github.com/svlandeg).
+* 🛠️ Add `add-permalinks` and `add-permalinks-page` to `scripts/docs.py`. PR [#14033](https://github.com/fastapi/fastapi/pull/14033) by [@YuriiMotov](https://github.com/YuriiMotov).
+* 🔧 Upgrade Material for MkDocs and remove insiders. PR [#14375](https://github.com/fastapi/fastapi/pull/14375) by [@tiangolo](https://github.com/tiangolo).
+
+## 0.121.3
+
+### Refactors
+
+* ♻️ Make the result of `Depends()` and `Security()` hashable, as a workaround for other tools interacting with these internal parts. PR [#14372](https://github.com/fastapi/fastapi/pull/14372) by [@tiangolo](https://github.com/tiangolo).
+
+### Upgrades
+
+* ⬆️ Bump Starlette to <`0.51.0`. PR [#14282](https://github.com/fastapi/fastapi/pull/14282) by [@musicinmybrain](https://github.com/musicinmybrain).
+
+### Docs
+
+* 📝 Add missing hash part. PR [#14369](https://github.com/fastapi/fastapi/pull/14369) by [@nilslindemann](https://github.com/nilslindemann).
+* 📝 Fix typos in code comments. PR [#14364](https://github.com/fastapi/fastapi/pull/14364) by [@Edge-Seven](https://github.com/Edge-Seven).
+* 📝 Add docs for using FastAPI Cloud. PR [#14359](https://github.com/fastapi/fastapi/pull/14359) by [@tiangolo](https://github.com/tiangolo).
+
## 0.121.2
### Fixes
diff --git a/docs/en/docs/tutorial/first-steps.md b/docs/en/docs/tutorial/first-steps.md
index 7d4c12de8..b88ff6a18 100644
--- a/docs/en/docs/tutorial/first-steps.md
+++ b/docs/en/docs/tutorial/first-steps.md
@@ -143,6 +143,42 @@ And there are dozens of alternatives, all based on OpenAPI. You could easily add
You could also use it to generate code automatically, for clients that communicate with your API. For example, frontend, mobile or IoT applications.
+### Deploy your app (optional) { #deploy-your-app-optional }
+
+You can optionally deploy your FastAPI app to FastAPI Cloud, go and join the waiting list if you haven't. 🚀
+
+If you already have a **FastAPI Cloud** account (we invited you from the waiting list 😉), you can deploy your application with one command.
+
+Before deploying, make sure you are logged in:
+
+
+
+```console
+$ fastapi login
+
+You are logged in to FastAPI Cloud 🚀
+```
+
+
+
+Then deploy your app:
+
+
+
+```console
+$ fastapi deploy
+
+Deploying to FastAPI Cloud...
+
+✅ Deployment successful!
+
+🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev
+```
+
+
+
+That's it! Now you can access your app at that URL. ✨
+
## Recap, step by step { #recap-step-by-step }
### Step 1: import `FastAPI` { #step-1-import-fastapi }
@@ -314,6 +350,26 @@ You can also return Pydantic models (you'll see more about that later).
There are many other objects and models that will be automatically converted to JSON (including ORMs, etc). Try using your favorite ones, it's highly probable that they are already supported.
+### Step 6: Deploy it { #step-6-deploy-it }
+
+Deploy your app to **FastAPI Cloud** with one command: `fastapi deploy`. 🎉
+
+#### About FastAPI Cloud { #about-fastapi-cloud }
+
+**FastAPI Cloud** is built by the same author and team behind **FastAPI**.
+
+It streamlines the process of **building**, **deploying**, and **accessing** an API with minimal effort.
+
+It brings the same **developer experience** of building apps with FastAPI to **deploying** them to the cloud. 🎉
+
+FastAPI Cloud is the primary sponsor and funding provider for the *FastAPI and friends* open source projects. ✨
+
+#### Deploy to other cloud providers { #deploy-to-other-cloud-providers }
+
+FastAPI is open source and based on standards. You can deploy FastAPI apps to any cloud provider you choose.
+
+Follow your cloud provider's guides to deploy FastAPI apps with them. 🤓
+
## Recap { #recap }
* Import `FastAPI`.
@@ -321,3 +377,4 @@ There are many other objects and models that will be automatically converted to
* Write a **path operation decorator** using decorators like `@app.get("/")`.
* Define a **path operation function**; for example, `def root(): ...`.
* Run the development server using the command `fastapi dev`.
+* Optionally deploy your app with `fastapi deploy`.
diff --git a/docs/en/docs/tutorial/sql-databases.md b/docs/en/docs/tutorial/sql-databases.md
index cfa1c9073..b42e9ba2f 100644
--- a/docs/en/docs/tutorial/sql-databases.md
+++ b/docs/en/docs/tutorial/sql-databases.md
@@ -65,7 +65,7 @@ There are a few differences:
* `Field(primary_key=True)` tells SQLModel that the `id` is the **primary key** in the SQL database (you can learn more about SQL primary keys in the SQLModel docs).
- By having the type as `int | None`, SQLModel will know that this column should be an `INTEGER` in the SQL database and that it should be `NULLABLE`.
+ **Note:** We use `int | None` for the primary key field so that in Python code we can *create an object without an `id`* (`id=None`), assuming the database will *generate it when saving*. SQLModel understands that the database will provide the `id` and *defines the column as a non-null `INTEGER`* in the database schema. See SQLModel docs on primary keys for details.
* `Field(index=True)` tells SQLModel that it should create a **SQL index** for this column, that would allow faster lookups in the database when reading data filtered by this column.
diff --git a/docs/en/docs/virtual-environments.md b/docs/en/docs/virtual-environments.md
index e9b0a9fc4..c02e43ab9 100644
--- a/docs/en/docs/virtual-environments.md
+++ b/docs/en/docs/virtual-environments.md
@@ -242,6 +242,26 @@ $ python -m pip install --upgrade pip
+/// tip
+
+Sometimes, you might get a **`No module named pip`** error when trying to upgrade pip.
+
+If this happens, install and upgrade pip using the command below:
+
+
+
+```console
+$ python -m ensurepip --upgrade
+
+---> 100%
+```
+
+
+
+This command will install pip if it is not already installed and also ensures that the installed version of pip is at least as recent as the one available in `ensurepip`.
+
+///
+
## Add `.gitignore` { #add-gitignore }
If you are using **Git** (you should), add a `.gitignore` file to exclude everything in your `.venv` from Git.
diff --git a/docs/en/mkdocs.maybe-insiders.yml b/docs/en/mkdocs.env.yml
similarity index 78%
rename from docs/en/mkdocs.maybe-insiders.yml
rename to docs/en/mkdocs.env.yml
index 37fd9338e..c5f6e07d7 100644
--- a/docs/en/mkdocs.maybe-insiders.yml
+++ b/docs/en/mkdocs.env.yml
@@ -1,6 +1,5 @@
# Define this here and not in the main mkdocs.yml file because that one is auto
# updated and written, and the script would remove the env var
-INHERIT: !ENV [INSIDERS_FILE, '../en/mkdocs.no-insiders.yml']
markdown_extensions:
pymdownx.highlight:
linenums: !ENV [LINENUMS, false]
diff --git a/docs/en/mkdocs.insiders.yml b/docs/en/mkdocs.insiders.yml
deleted file mode 100644
index 8d6d26e17..000000000
--- a/docs/en/mkdocs.insiders.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-plugins:
- social:
- cards_layout_options:
- logo: ../en/docs/img/icon-white.svg
- typeset:
-markdown_extensions:
- material.extensions.preview:
- targets:
- include:
- - "*"
diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml
index 323035240..fd346a3d3 100644
--- a/docs/en/mkdocs.yml
+++ b/docs/en/mkdocs.yml
@@ -1,4 +1,4 @@
-INHERIT: ../en/mkdocs.maybe-insiders.yml
+INHERIT: ../en/mkdocs.env.yml
site_name: FastAPI
site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production
site_url: https://fastapi.tiangolo.com/
@@ -52,6 +52,10 @@ theme:
repo_name: fastapi/fastapi
repo_url: https://github.com/fastapi/fastapi
plugins:
+ social:
+ cards_layout_options:
+ logo: ../en/docs/img/icon-white.svg
+ typeset:
search: null
macros:
include_yaml:
@@ -192,6 +196,7 @@ nav:
- Deployment:
- deployment/index.md
- deployment/versions.md
+ - deployment/fastapicloud.md
- deployment/https.md
- deployment/manually.md
- deployment/concepts.md
@@ -210,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
@@ -252,6 +258,10 @@ nav:
- management.md
- release-notes.md
markdown_extensions:
+ material.extensions.preview:
+ targets:
+ include:
+ - "*"
abbr: null
attr_list: null
footnotes: null
diff --git a/docs/en/overrides/main.html b/docs/en/overrides/main.html
index be31bd75c..a37ebf0a4 100644
--- a/docs/en/overrides/main.html
+++ b/docs/en/overrides/main.html
@@ -3,6 +3,13 @@
{% block announce %}
{% endblock %}
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 0672423cf..7a6ee14f2 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.2"
+__version__ = "0.123.2"
from starlette import status as status
diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py
index 5cd49343b..3d91814c0 100644
--- a/fastapi/_compat/v2.py
+++ b/fastapi/_compat/v2.py
@@ -304,7 +304,7 @@ def _remap_definitions_and_field_mappings(
old_name_to_new_name_map = {}
for field_key, schema in field_mapping.items():
model = field_key[0].type_
- if model not in model_name_map:
+ if model not in model_name_map or "$ref" not in schema:
continue
new_name = model_name_map[model]
old_name = schema["$ref"].split("/")[-1]
@@ -371,6 +371,13 @@ def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:
def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:
origin_type = get_origin(field.field_info.annotation) or field.field_info.annotation
+ if origin_type is Union: # Handle optional sequences
+ union_args = get_args(field.field_info.annotation)
+ for union_arg in union_args:
+ if union_arg is type(None):
+ continue
+ origin_type = get_origin(union_arg) or union_arg
+ break
assert issubclass(origin_type, shared.sequence_types) # type: ignore[arg-type]
return shared.sequence_annotation_to_type[origin_type](value) # type: ignore[no-any-return]
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 e0010f97d..ef3f56417 100644
--- a/fastapi/dependencies/utils.py
+++ b/fastapi/dependencies/utils.py
@@ -1,3 +1,4 @@
+import dataclasses
import inspect
from contextlib import AsyncExitStack, contextmanager
from copy import copy, deepcopy
@@ -57,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
@@ -125,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,
)
@@ -231,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:
@@ -239,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():
@@ -274,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,
)
@@ -428,7 +429,7 @@ def analyze_param(
if depends is not None and depends.dependency is None:
# Copy `depends` before mutating it
depends = copy(depends)
- depends.dependency = type_annotation
+ depends = dataclasses.replace(depends, dependency=type_annotation)
# Handle non-param type annotations like Request
if lenient_issubclass(
@@ -608,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,
)
@@ -690,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,
@@ -789,9 +790,16 @@ def request_params_to_args(
params_to_process[field.alias] = value
processed_keys.add(alias or field.alias)
- for key, value in received_params.items():
+ for key in received_params.keys():
if key not in processed_keys:
- params_to_process[key] = value
+ if hasattr(received_params, "getlist"):
+ value = received_params.getlist(key)
+ if isinstance(value, list) and (len(value) == 1):
+ params_to_process[key] = value[0]
+ else:
+ params_to_process[key] = value
+ else:
+ params_to_process[key] = received_params.get(key)
if single_not_embedded_field:
field_info = first_field.field_info
@@ -900,9 +908,14 @@ async def _extract_form_body(
value = serialize_sequence_value(field=field, value=results)
if value is not None:
values[field.alias] = value
- for key, value in received_body.items():
- if key not in values:
- values[key] = value
+ field_aliases = {field.alias for field in body_fields}
+ for key in received_body.keys():
+ if key not in field_aliases:
+ param_values = received_body.getlist(key)
+ if len(param_values) == 1:
+ values[key] = param_values[0]
+ else:
+ values[key] = param_values
return values
diff --git a/fastapi/encoders.py b/fastapi/encoders.py
index 6fc6228e1..793951089 100644
--- a/fastapi/encoders.py
+++ b/fastapi/encoders.py
@@ -34,14 +34,14 @@ def isoformat(o: Union[datetime.date, datetime.time]) -> str:
return o.isoformat()
-# Taken from Pydantic v1 as is
+# Adapted from Pydantic v1
# TODO: pv2 should this return strings instead?
def decimal_encoder(dec_value: Decimal) -> Union[int, float]:
"""
- Encodes a Decimal as int of there's no exponent, otherwise float
+ Encodes a Decimal as int if there's no exponent, otherwise float
This is useful when we use ConstrainedDecimal to represent Numeric(x,0)
- where a integer (but not int typed) is used. Encoding this as a float
+ where an integer (but not int typed) is used. Encoding this as a float
results in failed round-tripping between encode and parse.
Our Id type is a prime example of this.
@@ -50,8 +50,12 @@ def decimal_encoder(dec_value: Decimal) -> Union[int, float]:
>>> decimal_encoder(Decimal("1"))
1
+
+ >>> decimal_encoder(Decimal("NaN"))
+ nan
"""
- if dec_value.as_tuple().exponent >= 0: # type: ignore[operator]
+ exponent = dec_value.as_tuple().exponent
+ if isinstance(exponent, int) and exponent >= 0:
return int(dec_value)
else:
return float(dec_value)
diff --git a/fastapi/params.py b/fastapi/params.py
index 6a58d5808..6d07df35e 100644
--- a/fastapi/params.py
+++ b/fastapi/params.py
@@ -762,13 +762,13 @@ class File(Form): # type: ignore[misc]
)
-@dataclass
+@dataclass(frozen=True)
class Depends:
dependency: Optional[Callable[..., Any]] = None
use_cache: bool = True
scope: Union[Literal["function", "request"], None] = None
-@dataclass
+@dataclass(frozen=True)
class Security(Depends):
scopes: Optional[Sequence[str]] = None
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/fastapi/utils.py b/fastapi/utils.py
index 2e79ee6b1..b3b89ed2b 100644
--- a/fastapi/utils.py
+++ b/fastapi/utils.py
@@ -110,7 +110,9 @@ def create_model_field(
try:
return v1.ModelField(**v1_kwargs) # type: ignore[no-any-return]
except RuntimeError:
- raise fastapi.exceptions.FastAPIError(_invalid_args_message) from None
+ raise fastapi.exceptions.FastAPIError(
+ _invalid_args_message.format(type_=type_)
+ ) from None
elif PYDANTIC_V2:
from ._compat import v2
@@ -121,7 +123,9 @@ def create_model_field(
try:
return v2.ModelField(**kwargs) # type: ignore[return-value,arg-type]
except PydanticSchemaGenerationError:
- raise fastapi.exceptions.FastAPIError(_invalid_args_message) from None
+ raise fastapi.exceptions.FastAPIError(
+ _invalid_args_message.format(type_=type_)
+ ) from None
# Pydantic v2 is not installed, but it's not a Pydantic v1 ModelField, it could be
# a Pydantic v1 type, like a constrained int
from fastapi._compat import v1
@@ -129,7 +133,9 @@ def create_model_field(
try:
return v1.ModelField(**v1_kwargs) # type: ignore[no-any-return]
except RuntimeError:
- raise fastapi.exceptions.FastAPIError(_invalid_args_message) from None
+ raise fastapi.exceptions.FastAPIError(
+ _invalid_args_message.format(type_=type_)
+ ) from None
def create_cloned_field(
diff --git a/pyproject.toml b/pyproject.toml
index 7d2be0074..cafcf65c6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -45,7 +45,7 @@ classifiers = [
"Topic :: Internet :: WWW/HTTP",
]
dependencies = [
- "starlette>=0.40.0,<0.50.0",
+ "starlette>=0.40.0,<0.51.0",
"pydantic>=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0",
"typing-extensions>=4.8.0",
"annotated-doc>=0.0.2",
diff --git a/requirements-docs-insiders.txt b/requirements-docs-insiders.txt
deleted file mode 100644
index d8d3c37a9..000000000
--- a/requirements-docs-insiders.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-git+https://${TOKEN}@github.com/squidfunk/mkdocs-material-insiders.git@9.5.30-insiders-4.53.11
-git+https://${TOKEN}@github.com/pawamoy-insiders/griffe-typing-deprecated.git
-git+https://${TOKEN}@github.com/pawamoy-insiders/mkdocstrings-python.git
diff --git a/requirements-docs.txt b/requirements-docs.txt
index 696eb2a33..4f1863a4a 100644
--- a/requirements-docs.txt
+++ b/requirements-docs.txt
@@ -1,6 +1,6 @@
-e .
-r requirements-docs-tests.txt
-mkdocs-material==9.6.16
+mkdocs-material==9.7.0
mdx-include >=1.4.1,<2.0.0
mkdocs-redirects>=1.2.1,<1.3.0
typer == 0.16.0
@@ -13,7 +13,9 @@ pillow==11.3.0
cairosvg==2.8.2
mkdocstrings[python]==0.30.1
griffe-typingdoc==0.3.0
+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/requirements.txt b/requirements.txt
index 9180bf1be..5d9f97b75 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,6 @@
-e .[all]
-r requirements-tests.txt
-r requirements-docs.txt
-pre-commit >=2.17.0,<5.0.0
+pre-commit >=4.5.0,<5.0.0
# For generating screenshots
playwright
diff --git a/scripts/docs.py b/scripts/docs.py
index 56ffb9d36..73f60e68c 100644
--- a/scripts/docs.py
+++ b/scripts/docs.py
@@ -4,9 +4,8 @@ import os
import re
import shutil
import subprocess
-from functools import lru_cache
+from html.parser import HTMLParser
from http.server import HTTPServer, SimpleHTTPRequestHandler
-from importlib import metadata
from multiprocessing import Pool
from pathlib import Path
from typing import Any, Dict, List, Optional, Union
@@ -16,6 +15,7 @@ import typer
import yaml
from jinja2 import Template
from ruff.__main__ import find_ruff_bin
+from slugify import slugify as py_slugify
logging.basicConfig(level=logging.INFO)
@@ -27,8 +27,8 @@ missing_translation_snippet = """
{!../../docs/missing-translation.md!}
"""
-non_translated_sections = [
- "reference/",
+non_translated_sections = (
+ f"reference{os.sep}",
"release-notes.md",
"fastapi-people.md",
"external-links.md",
@@ -36,7 +36,7 @@ non_translated_sections = [
"management-tasks.md",
"management.md",
"contributing.md",
-]
+)
docs_path = Path("docs")
en_docs_path = Path("docs/en")
@@ -44,13 +44,39 @@ en_config_path: Path = en_docs_path / mkdocs_name
site_path = Path("site").absolute()
build_site_path = Path("site_build").absolute()
+header_pattern = re.compile(r"^(#{1,6}) (.+?)(?:\s*\{\s*(#.*)\s*\})?\s*$")
header_with_permalink_pattern = re.compile(r"^(#{1,6}) (.+?)(\s*\{\s*#.*\s*\})\s*$")
+code_block3_pattern = re.compile(r"^\s*```")
+code_block4_pattern = re.compile(r"^\s*````")
-@lru_cache
-def is_mkdocs_insiders() -> bool:
- version = metadata.version("mkdocs-material")
- return "insiders" in version
+class VisibleTextExtractor(HTMLParser):
+ """Extract visible text from a string with HTML tags."""
+
+ def __init__(self):
+ super().__init__()
+ self.text_parts = []
+
+ def handle_data(self, data):
+ self.text_parts.append(data)
+
+ def extract_visible_text(self, html: str) -> str:
+ self.reset()
+ self.text_parts = []
+ self.feed(html)
+ return "".join(self.text_parts).strip()
+
+
+def slugify(text: str) -> str:
+ return py_slugify(
+ text,
+ replacements=[
+ ("`", ""), # `dict`s -> dicts
+ ("'s", "s"), # it's -> its
+ ("'t", "t"), # don't -> dont
+ ("**", ""), # **FastAPI**s -> FastAPIs
+ ],
+ )
def get_en_config() -> Dict[str, Any]:
@@ -77,9 +103,7 @@ def complete_existing_lang(incomplete: str):
@app.callback()
def callback() -> None:
- if is_mkdocs_insiders():
- os.environ["INSIDERS_FILE"] = "../en/mkdocs.insiders.yml"
- # For MacOS with insiders and Cairo
+ # For MacOS with Cairo
os.environ["DYLD_FALLBACK_LIBRARY_PATH"] = "/opt/homebrew/lib"
@@ -115,10 +139,6 @@ def build_lang(
"""
Build the docs for a language.
"""
- insiders_env_file = os.environ.get("INSIDERS_FILE")
- print(f"Insiders file {insiders_env_file}")
- if is_mkdocs_insiders():
- print("Using insiders")
lang_path: Path = Path("docs") / lang
if not lang_path.is_dir():
typer.echo(f"The language translation doesn't seem to exist yet: {lang}")
@@ -145,14 +165,20 @@ def build_lang(
index_sponsors_template = """
-{% if sponsors %}
+### Keystone Sponsor
+
+{% for sponsor in sponsors.keystone -%}
+
+{% endfor %}
+### Gold and Silver Sponsors
+
{% for sponsor in sponsors.gold -%}
{% endfor -%}
{%- for sponsor in sponsors.silver -%}
{% endfor %}
-{% endif %}
+
"""
@@ -434,5 +460,83 @@ def generate_docs_src_versions_for_file(file_path: Path) -> None:
version_file.write_text(content_format, encoding="utf-8")
+@app.command()
+def add_permalinks_page(path: Path, update_existing: bool = False):
+ """
+ Add or update header permalinks in specific page of En docs.
+ """
+
+ if not path.is_relative_to(en_docs_path / "docs"):
+ raise RuntimeError(f"Path must be inside {en_docs_path}")
+ rel_path = path.relative_to(en_docs_path / "docs")
+
+ # Skip excluded sections
+ if str(rel_path).startswith(non_translated_sections):
+ return
+
+ visible_text_extractor = VisibleTextExtractor()
+ updated_lines = []
+ in_code_block3 = False
+ in_code_block4 = False
+ permalinks = set()
+
+ with path.open("r", encoding="utf-8") as f:
+ lines = f.readlines()
+
+ for line in lines:
+ # Handle codeblocks start and end
+ if not (in_code_block3 or in_code_block4):
+ if code_block4_pattern.match(line):
+ in_code_block4 = True
+ elif code_block3_pattern.match(line):
+ in_code_block3 = True
+ else:
+ if in_code_block4 and code_block4_pattern.match(line):
+ in_code_block4 = False
+ elif in_code_block3 and code_block3_pattern.match(line):
+ in_code_block3 = False
+
+ # Process Headers only outside codeblocks
+ if not (in_code_block3 or in_code_block4):
+ match = header_pattern.match(line)
+ if match:
+ hashes, title, _permalink = match.groups()
+ if (not _permalink) or update_existing:
+ slug = slugify(visible_text_extractor.extract_visible_text(title))
+ if slug in permalinks:
+ # If the slug is already used, append a number to make it unique
+ count = 1
+ original_slug = slug
+ while slug in permalinks:
+ slug = f"{original_slug}_{count}"
+ count += 1
+ permalinks.add(slug)
+
+ line = f"{hashes} {title} {{ #{slug} }}\n"
+
+ updated_lines.append(line)
+
+ with path.open("w", encoding="utf-8") as f:
+ f.writelines(updated_lines)
+
+
+@app.command()
+def add_permalinks_pages(pages: List[Path], update_existing: bool = False) -> None:
+ """
+ Add or update header permalinks in specific pages of En docs.
+ """
+ for md_file in pages:
+ add_permalinks_page(md_file, update_existing=update_existing)
+
+
+@app.command()
+def add_permalinks(update_existing: bool = False) -> None:
+ """
+ Add or update header permalinks in all pages of En docs.
+ """
+ for md_file in en_docs_path.rglob("*.md"):
+ add_permalinks_page(md_file, update_existing=update_existing)
+
+
if __name__ == "__main__":
app()
diff --git a/tests/test_compat.py b/tests/test_compat.py
index 0184c9a2e..c3a97209a 100644
--- a/tests/test_compat.py
+++ b/tests/test_compat.py
@@ -136,6 +136,30 @@ def test_is_uploadfile_sequence_annotation():
assert is_uploadfile_sequence_annotation(Union[List[str], List[UploadFile]])
+@needs_pydanticv2
+def test_serialize_sequence_value_with_optional_list():
+ """Test that serialize_sequence_value handles optional lists correctly."""
+ from fastapi._compat import v2
+
+ field_info = FieldInfo(annotation=Union[List[str], None])
+ field = v2.ModelField(name="items", field_info=field_info)
+ result = v2.serialize_sequence_value(field=field, value=["a", "b", "c"])
+ assert result == ["a", "b", "c"]
+ assert isinstance(result, list)
+
+
+@needs_pydanticv2
+def test_serialize_sequence_value_with_none_first_in_union():
+ """Test that serialize_sequence_value handles Union[None, List[...]] correctly."""
+ from fastapi._compat import v2
+
+ field_info = FieldInfo(annotation=Union[None, List[str]])
+ field = v2.ModelField(name="items", field_info=field_info)
+ result = v2.serialize_sequence_value(field=field, value=["x", "y"])
+ assert result == ["x", "y"]
+ assert isinstance(result, list)
+
+
@needs_py_lt_314
def test_is_pv1_scalar_field():
from fastapi._compat import v1
diff --git a/tests/test_depends_hashable.py b/tests/test_depends_hashable.py
new file mode 100644
index 000000000..d57f2726e
--- /dev/null
+++ b/tests/test_depends_hashable.py
@@ -0,0 +1,25 @@
+# This is more or less a workaround to make Depends and Security hashable
+# as other tools that use them depend on that
+# Ref: https://github.com/fastapi/fastapi/pull/14320
+
+from fastapi import Depends, Security
+
+
+def dep():
+ pass
+
+
+def test_depends_hashable():
+ dep() # just for coverage
+ d1 = Depends(dep)
+ d2 = Depends(dep)
+ d3 = Depends(dep, scope="function")
+ d4 = Depends(dep, scope="function")
+
+ s1 = Security(dep)
+ s2 = Security(dep)
+
+ assert hash(d1) == hash(d2)
+ assert hash(s1) == hash(s2)
+ assert hash(d1) != hash(d3)
+ assert hash(d3) == hash(d4)
diff --git a/tests/test_fastapi_cli.py b/tests/test_fastapi_cli.py
index a5c10778a..78a49a1fb 100644
--- a/tests/test_fastapi_cli.py
+++ b/tests/test_fastapi_cli.py
@@ -1,3 +1,4 @@
+import os
import subprocess
import sys
from unittest.mock import patch
@@ -20,6 +21,7 @@ def test_fastapi_cli():
],
capture_output=True,
encoding="utf-8",
+ env={**os.environ, "PYTHONIOENCODING": "utf-8"},
)
assert result.returncode == 1, result.stdout
assert "Path does not exist non_existent_file.py" in result.stdout
diff --git a/tests/test_form_default.py b/tests/test_form_default.py
new file mode 100644
index 000000000..2a12049d1
--- /dev/null
+++ b/tests/test_form_default.py
@@ -0,0 +1,35 @@
+from typing import Optional
+
+from fastapi import FastAPI, File, Form
+from starlette.testclient import TestClient
+from typing_extensions import Annotated
+
+app = FastAPI()
+
+
+@app.post("/urlencoded")
+async def post_url_encoded(age: Annotated[Optional[int], Form()] = None):
+ return age
+
+
+@app.post("/multipart")
+async def post_multi_part(
+ age: Annotated[Optional[int], Form()] = None,
+ file: Annotated[Optional[bytes], File()] = None,
+):
+ return {"file": file, "age": age}
+
+
+client = TestClient(app)
+
+
+def test_form_default_url_encoded():
+ response = client.post("/urlencoded", data={"age": ""})
+ assert response.status_code == 200
+ assert response.text == "null"
+
+
+def test_form_default_multi_part():
+ response = client.post("/multipart", data={"age": ""})
+ assert response.status_code == 200
+ assert response.json() == {"file": None, "age": None}
diff --git a/tests/test_forms_single_model.py b/tests/test_forms_single_model.py
index 880ab3820..1db63f021 100644
--- a/tests/test_forms_single_model.py
+++ b/tests/test_forms_single_model.py
@@ -2,6 +2,7 @@ from typing import List, Optional
from dirty_equals import IsDict
from fastapi import FastAPI, Form
+from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import Annotated
@@ -17,11 +18,27 @@ class FormModel(BaseModel):
alias_with: str = Field(alias="with", default="nothing")
+class FormModelExtraAllow(BaseModel):
+ param: str
+
+ if PYDANTIC_V2:
+ model_config = {"extra": "allow"}
+ else:
+
+ class Config:
+ extra = "allow"
+
+
@app.post("/form/")
def post_form(user: Annotated[FormModel, Form()]):
return user
+@app.post("/form-extra-allow/")
+def post_form_extra_allow(params: Annotated[FormModelExtraAllow, Form()]):
+ return params
+
+
client = TestClient(app)
@@ -131,3 +148,33 @@ def test_no_data():
]
}
)
+
+
+def test_extra_param_single():
+ response = client.post(
+ "/form-extra-allow/",
+ data={
+ "param": "123",
+ "extra_param": "456",
+ },
+ )
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "param": "123",
+ "extra_param": "456",
+ }
+
+
+def test_extra_param_list():
+ response = client.post(
+ "/form-extra-allow/",
+ data={
+ "param": "123",
+ "extra_params": ["456", "789"],
+ },
+ )
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "param": "123",
+ "extra_params": ["456", "789"],
+ }
diff --git a/tests/test_jsonable_encoder.py b/tests/test_jsonable_encoder.py
index 447c5b4d6..3b6513e27 100644
--- a/tests/test_jsonable_encoder.py
+++ b/tests/test_jsonable_encoder.py
@@ -3,6 +3,7 @@ from dataclasses import dataclass
from datetime import datetime, timezone
from decimal import Decimal
from enum import Enum
+from math import isinf, isnan
from pathlib import PurePath, PurePosixPath, PureWindowsPath
from typing import Optional
@@ -306,6 +307,20 @@ def test_decimal_encoder_int():
assert jsonable_encoder(data) == {"value": 2}
+@needs_pydanticv2
+def test_decimal_encoder_nan():
+ data = {"value": Decimal("NaN")}
+ assert isnan(jsonable_encoder(data)["value"])
+
+
+@needs_pydanticv2
+def test_decimal_encoder_infinity():
+ data = {"value": Decimal("Infinity")}
+ assert isinf(jsonable_encoder(data)["value"])
+ data = {"value": Decimal("-Infinity")}
+ assert isinf(jsonable_encoder(data)["value"])
+
+
def test_encode_deque_encodes_child_models():
class Model(BaseModel):
test: str
diff --git a/tests/test_optional_file_list.py b/tests/test_optional_file_list.py
new file mode 100644
index 000000000..0228900cf
--- /dev/null
+++ b/tests/test_optional_file_list.py
@@ -0,0 +1,30 @@
+from typing import List, Optional
+
+from fastapi import FastAPI, File
+from fastapi.testclient import TestClient
+
+app = FastAPI()
+
+
+@app.post("/files")
+async def upload_files(files: Optional[List[bytes]] = File(None)):
+ if files is None:
+ return {"files_count": 0}
+ return {"files_count": len(files), "sizes": [len(f) for f in files]}
+
+
+def test_optional_bytes_list():
+ client = TestClient(app)
+ response = client.post(
+ "/files",
+ files=[("files", b"content1"), ("files", b"content2")],
+ )
+ assert response.status_code == 200
+ assert response.json() == {"files_count": 2, "sizes": [8, 8]}
+
+
+def test_optional_bytes_list_no_files():
+ client = TestClient(app)
+ response = client.post("/files")
+ assert response.status_code == 200
+ assert response.json() == {"files_count": 0}
diff --git a/tests/test_query_cookie_header_model_extra_params.py b/tests/test_query_cookie_header_model_extra_params.py
new file mode 100644
index 000000000..f4ebefb3f
--- /dev/null
+++ b/tests/test_query_cookie_header_model_extra_params.py
@@ -0,0 +1,111 @@
+from fastapi import Cookie, FastAPI, Header, Query
+from fastapi._compat import PYDANTIC_V2
+from fastapi.testclient import TestClient
+from pydantic import BaseModel
+
+app = FastAPI()
+
+
+class Model(BaseModel):
+ param: str
+
+ if PYDANTIC_V2:
+ model_config = {"extra": "allow"}
+ else:
+
+ class Config:
+ extra = "allow"
+
+
+@app.get("/query")
+async def query_model_with_extra(data: Model = Query()):
+ return data
+
+
+@app.get("/header")
+async def header_model_with_extra(data: Model = Header()):
+ return data
+
+
+@app.get("/cookie")
+async def cookies_model_with_extra(data: Model = Cookie()):
+ return data
+
+
+def test_query_pass_extra_list():
+ client = TestClient(app)
+ resp = client.get(
+ "/query",
+ params={
+ "param": "123",
+ "param2": ["456", "789"], # Pass a list of values as extra parameter
+ },
+ )
+ assert resp.status_code == 200
+ assert resp.json() == {
+ "param": "123",
+ "param2": ["456", "789"],
+ }
+
+
+def test_query_pass_extra_single():
+ client = TestClient(app)
+ resp = client.get(
+ "/query",
+ params={
+ "param": "123",
+ "param2": "456",
+ },
+ )
+ assert resp.status_code == 200
+ assert resp.json() == {
+ "param": "123",
+ "param2": "456",
+ }
+
+
+def test_header_pass_extra_list():
+ client = TestClient(app)
+
+ resp = client.get(
+ "/header",
+ headers=[
+ ("param", "123"),
+ ("param2", "456"), # Pass a list of values as extra parameter
+ ("param2", "789"),
+ ],
+ )
+ assert resp.status_code == 200
+ resp_json = resp.json()
+ assert "param2" in resp_json
+ assert resp_json["param2"] == ["456", "789"]
+
+
+def test_header_pass_extra_single():
+ client = TestClient(app)
+
+ resp = client.get(
+ "/header",
+ headers=[
+ ("param", "123"),
+ ("param2", "456"),
+ ],
+ )
+ assert resp.status_code == 200
+ resp_json = resp.json()
+ assert "param2" in resp_json
+ assert resp_json["param2"] == "456"
+
+
+def test_cookie_pass_extra_list():
+ client = TestClient(app)
+ client.cookies = [
+ ("param", "123"),
+ ("param2", "456"), # Pass a list of values as extra parameter
+ ("param2", "789"),
+ ]
+ resp = client.get("/cookie")
+ assert resp.status_code == 200
+ resp_json = resp.json()
+ assert "param2" in resp_json
+ assert resp_json["param2"] == "789" # Cookies only keep the last value
diff --git a/tests/test_schema_compat_pydantic_v2.py b/tests/test_schema_compat_pydantic_v2.py
new file mode 100644
index 000000000..39626c0ec
--- /dev/null
+++ b/tests/test_schema_compat_pydantic_v2.py
@@ -0,0 +1,92 @@
+import pytest
+from fastapi import FastAPI
+from fastapi.testclient import TestClient
+from inline_snapshot import snapshot
+from pydantic import BaseModel
+
+from tests.utils import needs_py310, needs_pydanticv2
+
+
+@pytest.fixture(name="client")
+def get_client():
+ from enum import Enum
+
+ app = FastAPI()
+
+ class PlatformRole(str, Enum):
+ admin = "admin"
+ user = "user"
+
+ class OtherRole(str, Enum): ...
+
+ class User(BaseModel):
+ username: str
+ role: PlatformRole | OtherRole
+
+ @app.get("/users")
+ async def get_user() -> User:
+ return {"username": "alice", "role": "admin"}
+
+ client = TestClient(app)
+ return client
+
+
+@needs_py310
+@needs_pydanticv2
+def test_get(client: TestClient):
+ response = client.get("/users")
+ assert response.json() == {"username": "alice", "role": "admin"}
+
+
+@needs_py310
+@needs_pydanticv2
+def test_openapi_schema(client: TestClient):
+ response = client.get("openapi.json")
+ assert response.json() == snapshot(
+ {
+ "openapi": "3.1.0",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/users": {
+ "get": {
+ "summary": "Get User",
+ "operationId": "get_user_users_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {"$ref": "#/components/schemas/User"}
+ }
+ },
+ }
+ },
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "PlatformRole": {
+ "type": "string",
+ "enum": ["admin", "user"],
+ "title": "PlatformRole",
+ },
+ "User": {
+ "properties": {
+ "username": {"type": "string", "title": "Username"},
+ "role": {
+ "anyOf": [
+ {"$ref": "#/components/schemas/PlatformRole"},
+ {"enum": [], "title": "OtherRole"},
+ ],
+ "title": "Role",
+ },
+ },
+ "type": "object",
+ "required": ["username", "role"],
+ "title": "User",
+ },
+ }
+ },
+ }
+ )
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/docs/en/mkdocs.no-insiders.yml b/tests/test_tutorial/test_authentication_error_status_code/__init__.py
similarity index 100%
rename from docs/en/mkdocs.no-insiders.yml
rename to tests/test_tutorial/test_authentication_error_status_code/__init__.py
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_header_param_models/test_tutorial003.py b/tests/test_tutorial/test_header_param_models/test_tutorial003.py
index 60940e1da..554a48d2e 100644
--- a/tests/test_tutorial/test_header_param_models/test_tutorial003.py
+++ b/tests/test_tutorial/test_header_param_models/test_tutorial003.py
@@ -77,7 +77,7 @@ def test_header_param_model_no_underscore(client: TestClient):
"user-agent": "testclient",
"save-data": "true",
"if-modified-since": "yesterday",
- "x-tag": "two",
+ "x-tag": ["one", "two"],
},
}
)
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):
diff --git a/tests/test_tutorial/test_sql_databases/test_tutorial001.py b/tests/test_tutorial/test_sql_databases/test_tutorial001.py
index 6604a2fd3..b45be4884 100644
--- a/tests/test_tutorial/test_sql_databases/test_tutorial001.py
+++ b/tests/test_tutorial/test_sql_databases/test_tutorial001.py
@@ -45,7 +45,7 @@ def get_client(request: pytest.FixtureRequest):
with TestClient(mod.app) as c:
yield c
- # Clean up connection explicitely to avoid resource warning
+ # Clean up connection explicitly to avoid resource warning
mod.engine.dispose()
diff --git a/tests/test_tutorial/test_sql_databases/test_tutorial002.py b/tests/test_tutorial/test_sql_databases/test_tutorial002.py
index 2c4e0988c..da0b8b7ce 100644
--- a/tests/test_tutorial/test_sql_databases/test_tutorial002.py
+++ b/tests/test_tutorial/test_sql_databases/test_tutorial002.py
@@ -45,7 +45,7 @@ def get_client(request: pytest.FixtureRequest):
with TestClient(mod.app) as c:
yield c
- # Clean up connection explicitely to avoid resource warning
+ # Clean up connection explicitly to avoid resource warning
mod.engine.dispose()