Merge remote-tracking branch 'upstream/master' into deprecate-scopes-parameter

This commit is contained in:
Yurii Motov 2025-12-01 09:31:43 +01:00
commit 4c6fd06027
87 changed files with 1294 additions and 348 deletions

1
.github/labeler.yml vendored
View File

@ -17,6 +17,7 @@ lang-all:
- docs/*/docs/** - docs/*/docs/**
- all-globs-to-all-files: - all-globs-to-all-files:
- '!docs/en/docs/**' - '!docs/en/docs/**'
- '!docs/*/**/_*.md'
- '!fastapi/**' - '!fastapi/**'
- '!pyproject.toml' - '!pyproject.toml'

View File

@ -21,7 +21,7 @@ jobs:
outputs: outputs:
docs: ${{ steps.filter.outputs.docs }} docs: ${{ steps.filter.outputs.docs }}
steps: 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 # For pull requests it's not necessary to checkout the code but for the main branch it is
- uses: dorny/paths-filter@v3 - uses: dorny/paths-filter@v3
id: filter id: filter
@ -32,12 +32,9 @@ jobs:
- docs/** - docs/**
- docs_src/** - docs_src/**
- requirements-docs.txt - requirements-docs.txt
- requirements-docs-insiders.txt
- pyproject.toml - pyproject.toml
- mkdocs.yml - mkdocs.yml
- mkdocs.insiders.yml - mkdocs.env.yml
- mkdocs.maybe-insiders.yml
- mkdocs.no-insiders.yml
- .github/workflows/build-docs.yml - .github/workflows/build-docs.yml
- .github/workflows/deploy-docs.yml - .github/workflows/deploy-docs.yml
- scripts/mkdocs_hooks.py - scripts/mkdocs_hooks.py
@ -48,7 +45,7 @@ jobs:
outputs: outputs:
langs: ${{ steps.show-langs.outputs.langs }} langs: ${{ steps.show-langs.outputs.langs }}
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v6
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:
@ -63,12 +60,6 @@ jobs:
pyproject.toml pyproject.toml
- name: Install docs extras - name: Install docs extras
run: uv pip install -r requirements-docs.txt 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 - name: Verify Docs
run: python ./scripts/docs.py verify-docs run: python ./scripts/docs.py verify-docs
- name: Export Language Codes - name: Export Language Codes
@ -90,7 +81,7 @@ jobs:
env: env:
GITHUB_CONTEXT: ${{ toJson(github) }} GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT" run: echo "$GITHUB_CONTEXT"
- uses: actions/checkout@v5 - uses: actions/checkout@v6
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:
@ -105,11 +96,6 @@ jobs:
pyproject.toml pyproject.toml
- name: Install docs extras - name: Install docs extras
run: uv pip install -r requirements-docs.txt 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 - name: Update Languages
run: python ./scripts/docs.py update-languages run: python ./scripts/docs.py update-languages
- uses: actions/cache@v4 - uses: actions/cache@v4

View File

@ -24,7 +24,7 @@ jobs:
env: env:
GITHUB_CONTEXT: ${{ toJson(github) }} GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT" run: echo "$GITHUB_CONTEXT"
- uses: actions/checkout@v5 - uses: actions/checkout@v6
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:

View File

@ -23,7 +23,7 @@ jobs:
env: env:
GITHUB_CONTEXT: ${{ toJson(github) }} GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT" run: echo "$GITHUB_CONTEXT"
- uses: actions/checkout@v5 - uses: actions/checkout@v6
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:

View File

@ -20,7 +20,7 @@ jobs:
env: env:
GITHUB_CONTEXT: ${{ toJson(github) }} GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT" run: echo "$GITHUB_CONTEXT"
- uses: actions/checkout@v5 - uses: actions/checkout@v6
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:

View File

@ -24,6 +24,8 @@ jobs:
env: env:
GITHUB_CONTEXT: ${{ toJson(github) }} GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT" 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 - uses: actions/checkout@v5
with: with:
# To allow latest-changes to commit to the main branch # 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' }} if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }}
with: with:
limit-access-to-actor: true limit-access-to-actor: true
- uses: tiangolo/latest-changes@0.4.0 - uses: tiangolo/latest-changes@0.4.1
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
latest_changes_file: docs/en/docs/release-notes.md latest_changes_file: docs/en/docs/release-notes.md

View File

@ -28,7 +28,7 @@ jobs:
env: env:
GITHUB_CONTEXT: ${{ toJson(github) }} GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT" run: echo "$GITHUB_CONTEXT"
- uses: actions/checkout@v5 - uses: actions/checkout@v6
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:

View File

@ -24,7 +24,7 @@ jobs:
env: env:
GITHUB_CONTEXT: ${{ toJson(github) }} GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT" run: echo "$GITHUB_CONTEXT"
- uses: actions/checkout@v5 - uses: actions/checkout@v6
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:

88
.github/workflows/pre-commit.yml vendored Normal file
View File

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

View File

@ -20,7 +20,7 @@ jobs:
env: env:
GITHUB_CONTEXT: ${{ toJson(github) }} GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT" run: echo "$GITHUB_CONTEXT"
- uses: actions/checkout@v5 - uses: actions/checkout@v6
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:

View File

@ -21,7 +21,7 @@ jobs:
env: env:
GITHUB_CONTEXT: ${{ toJson(github) }} GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT" run: echo "$GITHUB_CONTEXT"
- uses: actions/checkout@v5 - uses: actions/checkout@v6
- uses: actions/setup-python@v6 - uses: actions/setup-python@v6
with: with:
python-version: '3.9' python-version: '3.9'

View File

@ -24,7 +24,7 @@ jobs:
env: env:
GITHUB_CONTEXT: ${{ toJson(github) }} GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT" run: echo "$GITHUB_CONTEXT"
- uses: actions/checkout@v5 - uses: actions/checkout@v6
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:

View File

@ -22,7 +22,7 @@ jobs:
env: env:
GITHUB_CONTEXT: ${{ toJson(github) }} GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT" run: echo "$GITHUB_CONTEXT"
- uses: actions/checkout@v5 - uses: actions/checkout@v6
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:

View File

@ -23,7 +23,7 @@ jobs:
env: env:
GITHUB_CONTEXT: ${{ toJson(github) }} GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT" run: echo "$GITHUB_CONTEXT"
- uses: actions/checkout@v5 - uses: actions/checkout@v6
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:
@ -65,7 +65,7 @@ jobs:
env: env:
GITHUB_CONTEXT: ${{ toJson(github) }} GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT" run: echo "$GITHUB_CONTEXT"
- uses: actions/checkout@v5 - uses: actions/checkout@v6
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:
@ -111,7 +111,7 @@ jobs:
env: env:
GITHUB_CONTEXT: ${{ toJson(github) }} GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT" run: echo "$GITHUB_CONTEXT"
- uses: actions/checkout@v5 - uses: actions/checkout@v6
- uses: actions/setup-python@v6 - uses: actions/setup-python@v6
with: with:
python-version: '3.8' python-version: '3.8'

View File

@ -19,7 +19,7 @@ jobs:
env: env:
GITHUB_CONTEXT: ${{ toJson(github) }} GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT" run: echo "$GITHUB_CONTEXT"
- uses: actions/checkout@v5 - uses: actions/checkout@v6
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:

View File

@ -42,7 +42,7 @@ jobs:
env: env:
GITHUB_CONTEXT: ${{ toJson(github) }} GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT" run: echo "$GITHUB_CONTEXT"
- uses: actions/checkout@v5 - uses: actions/checkout@v6
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:

3
.gitignore vendored
View File

@ -28,3 +28,6 @@ archive.zip
# macOS # macOS
.DS_Store .DS_Store
# Ignore while the setup still depends on requirements.txt files
uv.lock

View File

@ -1,9 +1,7 @@
# See https://pre-commit.com for more information # See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks # See https://pre-commit.com/hooks.html for more hooks
default_language_version:
python: python3.10
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0 rev: v6.0.0
hooks: hooks:
- id: check-added-large-files - id: check-added-large-files
@ -13,13 +11,19 @@ repos:
- --unsafe - --unsafe
- id: end-of-file-fixer - id: end-of-file-fixer
- id: trailing-whitespace - id: trailing-whitespace
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.3 rev: v0.14.3
hooks: hooks:
- id: ruff - id: ruff
args: args:
- --fix - --fix
- id: ruff-format - id: ruff-format
ci: - repo: local
autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks hooks:
autoupdate_commit_msg: ⬆ [pre-commit.ci] pre-commit autoupdate - 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$

View File

@ -45,6 +45,11 @@ The key features are:
## Sponsors ## Sponsors
<!-- sponsors --> <!-- sponsors -->
### Keystone Sponsor
<a href="https://fastapicloud.com" target="_blank" title="FastAPI Cloud. By the same team behind FastAPI. You code. We Cloud."><img src="https://fastapi.tiangolo.com/img/sponsors/fastapicloud.png"></a>
### Gold and Silver Sponsors
<a href="https://blockbee.io?ref=fastapi" target="_blank" title="BlockBee Cryptocurrency Payment Gateway"><img src="https://fastapi.tiangolo.com/img/sponsors/blockbee.png"></a> <a href="https://blockbee.io?ref=fastapi" target="_blank" title="BlockBee Cryptocurrency Payment Gateway"><img src="https://fastapi.tiangolo.com/img/sponsors/blockbee.png"></a>
<a href="https://github.com/scalar/scalar/?utm_source=fastapi&utm_medium=website&utm_campaign=main-badge" target="_blank" title="Scalar: Beautiful Open-Source API References from Swagger/OpenAPI files"><img src="https://fastapi.tiangolo.com/img/sponsors/scalar.svg"></a> <a href="https://github.com/scalar/scalar/?utm_source=fastapi&utm_medium=website&utm_campaign=main-badge" target="_blank" title="Scalar: Beautiful Open-Source API References from Swagger/OpenAPI files"><img src="https://fastapi.tiangolo.com/img/sponsors/scalar.svg"></a>
@ -447,6 +452,58 @@ For a more complete example including more features, see the <a href="https://fa
* **Cookie Sessions** * **Cookie Sessions**
* ...and more. * ...and more.
### Deploy your app (optional)
You can optionally deploy your FastAPI app to <a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>, 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:
<div class="termy">
```console
$ fastapi login
You are logged in to FastAPI Cloud 🚀
```
</div>
Then deploy your app:
<div class="termy">
```console
$ fastapi deploy
Deploying to FastAPI Cloud...
✅ Deployment successful!
🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev
```
</div>
That's it! Now you can access your app at that URL. ✨
#### About FastAPI Cloud
**<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>** 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 ## Performance
Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">one of the fastest Python frameworks available</a>, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">one of the fastest Python frameworks available</a>, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*)

View File

@ -1,21 +1,21 @@
tiangolo: tiangolo:
login: tiangolo login: tiangolo
count: 794 count: 808
avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4
url: https://github.com/tiangolo url: https://github.com/tiangolo
dependabot: dependabot:
login: dependabot login: dependabot
count: 126 count: 130
avatarUrl: https://avatars.githubusercontent.com/in/29110?v=4 avatarUrl: https://avatars.githubusercontent.com/in/29110?v=4
url: https://github.com/apps/dependabot url: https://github.com/apps/dependabot
alejsdev: alejsdev:
login: alejsdev login: alejsdev
count: 52 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 url: https://github.com/alejsdev
pre-commit-ci: pre-commit-ci:
login: pre-commit-ci login: pre-commit-ci
count: 49 count: 50
avatarUrl: https://avatars.githubusercontent.com/in/68672?v=4 avatarUrl: https://avatars.githubusercontent.com/in/68672?v=4
url: https://github.com/apps/pre-commit-ci url: https://github.com/apps/pre-commit-ci
github-actions: github-actions:
@ -28,31 +28,31 @@ Kludex:
count: 25 count: 25
avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4
url: https://github.com/Kludex 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: dmontagu:
login: dmontagu login: dmontagu
count: 17 count: 17
avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4 avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4
url: https://github.com/dmontagu 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: nilslindemann:
login: nilslindemann login: nilslindemann
count: 14 count: 15
avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4
url: https://github.com/nilslindemann 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: euri10:
login: euri10 login: euri10
count: 13 count: 13
avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4 avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4
url: https://github.com/euri10 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: kantandane:
login: kantandane login: kantandane
count: 13 count: 13
@ -103,6 +103,11 @@ waynerv:
count: 5 count: 5
avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4 avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4
url: https://github.com/waynerv 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: krishnamadhavan:
login: krishnamadhavan login: krishnamadhavan
count: 5 count: 5
@ -133,11 +138,6 @@ iudeen:
count: 4 count: 4
avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=f09cdd745e5bf16138f29b42732dd57c7f02bee1&v=4 avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=f09cdd745e5bf16138f29b42732dd57c7f02bee1&v=4
url: https://github.com/iudeen 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: philipokiokio:
login: philipokiokio login: philipokiokio
count: 4 count: 4
@ -483,6 +483,11 @@ nzig:
count: 2 count: 2
avatarUrl: https://avatars.githubusercontent.com/u/7372858?u=e769add36ed73c778cdb136eb10bf96b1e119671&v=4 avatarUrl: https://avatars.githubusercontent.com/u/7372858?u=e769add36ed73c778cdb136eb10bf96b1e119671&v=4
url: https://github.com/nzig 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: yezz123:
login: yezz123 login: yezz123
count: 2 count: 2

View File

@ -23,9 +23,6 @@ sponsors:
- login: railwayapp - login: railwayapp
avatarUrl: https://avatars.githubusercontent.com/u/66716858?v=4 avatarUrl: https://avatars.githubusercontent.com/u/66716858?v=4
url: https://github.com/railwayapp url: https://github.com/railwayapp
- login: scalar
avatarUrl: https://avatars.githubusercontent.com/u/301879?v=4
url: https://github.com/scalar
- - login: dribia - - login: dribia
avatarUrl: https://avatars.githubusercontent.com/u/41189616?v=4 avatarUrl: https://avatars.githubusercontent.com/u/41189616?v=4
url: https://github.com/dribia url: https://github.com/dribia
@ -44,25 +41,25 @@ sponsors:
- login: permitio - login: permitio
avatarUrl: https://avatars.githubusercontent.com/u/71775833?v=4 avatarUrl: https://avatars.githubusercontent.com/u/71775833?v=4
url: https://github.com/permitio url: https://github.com/permitio
- - login: BoostryJP - - login: Ponte-Energy-Partners
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
avatarUrl: https://avatars.githubusercontent.com/u/114745848?v=4 avatarUrl: https://avatars.githubusercontent.com/u/114745848?v=4
url: https://github.com/Ponte-Energy-Partners url: https://github.com/Ponte-Energy-Partners
- login: LambdaTest-Inc - login: LambdaTest-Inc
avatarUrl: https://avatars.githubusercontent.com/u/171592363?u=96606606a45fa170427206199014f2a5a2a4920b&v=4 avatarUrl: https://avatars.githubusercontent.com/u/171592363?u=96606606a45fa170427206199014f2a5a2a4920b&v=4
url: https://github.com/LambdaTest-Inc url: https://github.com/LambdaTest-Inc
- login: BoostryJP
avatarUrl: https://avatars.githubusercontent.com/u/57932412?v=4
url: https://github.com/BoostryJP
- login: requestly - login: requestly
avatarUrl: https://avatars.githubusercontent.com/u/12287519?v=4 avatarUrl: https://avatars.githubusercontent.com/u/12287519?v=4
url: https://github.com/requestly url: https://github.com/requestly
- login: acsone - login: acsone
avatarUrl: https://avatars.githubusercontent.com/u/7601056?v=4 avatarUrl: https://avatars.githubusercontent.com/u/7601056?v=4
url: https://github.com/acsone 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 avatarUrl: https://avatars.githubusercontent.com/u/8161763?v=4
url: https://github.com/Trivie url: https://github.com/Trivie
- - login: takashi-yoneya - - login: takashi-yoneya
@ -71,42 +68,30 @@ sponsors:
- login: Doist - login: Doist
avatarUrl: https://avatars.githubusercontent.com/u/2565372?v=4 avatarUrl: https://avatars.githubusercontent.com/u/2565372?v=4
url: https://github.com/Doist url: https://github.com/Doist
- login: bholagabbar
avatarUrl: https://avatars.githubusercontent.com/u/11693595?v=4
url: https://github.com/bholagabbar
- - login: mainframeindustries - - login: mainframeindustries
avatarUrl: https://avatars.githubusercontent.com/u/55092103?v=4 avatarUrl: https://avatars.githubusercontent.com/u/55092103?v=4
url: https://github.com/mainframeindustries url: https://github.com/mainframeindustries
- - login: alixlahuec - - login: alixlahuec
avatarUrl: https://avatars.githubusercontent.com/u/29543316?u=44357eb2a93bccf30fb9d389b8befe94a3d00985&v=4 avatarUrl: https://avatars.githubusercontent.com/u/29543316?u=44357eb2a93bccf30fb9d389b8befe94a3d00985&v=4
url: https://github.com/alixlahuec 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 - - login: primer-io
avatarUrl: https://avatars.githubusercontent.com/u/62146168?v=4 avatarUrl: https://avatars.githubusercontent.com/u/62146168?v=4
url: https://github.com/primer-io url: https://github.com/primer-io
- login: xsalagarcia
avatarUrl: https://avatars.githubusercontent.com/u/66035908?v=4
url: https://github.com/xsalagarcia
- - login: upciti - - login: upciti
avatarUrl: https://avatars.githubusercontent.com/u/43346262?v=4 avatarUrl: https://avatars.githubusercontent.com/u/43346262?v=4
url: https://github.com/upciti 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 - login: ChargeStorm
avatarUrl: https://avatars.githubusercontent.com/u/26000165?v=4 avatarUrl: https://avatars.githubusercontent.com/u/26000165?v=4
url: https://github.com/ChargeStorm 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 - login: nilslindemann
avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4
url: https://github.com/nilslindemann url: https://github.com/nilslindemann
- - login: samuelcolvin - - login: samuelcolvin
avatarUrl: https://avatars.githubusercontent.com/u/4039449?u=42eb3b833047c8c4b4f647a031eaef148c16d93f&v=4 avatarUrl: https://avatars.githubusercontent.com/u/4039449?u=42eb3b833047c8c4b4f647a031eaef148c16d93f&v=4
url: https://github.com/samuelcolvin 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 - login: otosky
avatarUrl: https://avatars.githubusercontent.com/u/42260747?u=69d089387c743d89427aa4ad8740cfb34045a9e0&v=4 avatarUrl: https://avatars.githubusercontent.com/u/42260747?u=69d089387c743d89427aa4ad8740cfb34045a9e0&v=4
url: https://github.com/otosky url: https://github.com/otosky
@ -137,9 +122,6 @@ sponsors:
- login: jugeeem - login: jugeeem
avatarUrl: https://avatars.githubusercontent.com/u/116043716?u=ae590d79c38ac79c91b9c5caa6887d061e865a3d&v=4 avatarUrl: https://avatars.githubusercontent.com/u/116043716?u=ae590d79c38ac79c91b9c5caa6887d061e865a3d&v=4
url: https://github.com/jugeeem 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 - login: patsatsia
avatarUrl: https://avatars.githubusercontent.com/u/61111267?u=3271b85f7a37b479c8d0ae0a235182e83c166edf&v=4 avatarUrl: https://avatars.githubusercontent.com/u/61111267?u=3271b85f7a37b479c8d0ae0a235182e83c166edf&v=4
url: https://github.com/patsatsia url: https://github.com/patsatsia
@ -155,9 +137,9 @@ sponsors:
- login: kaoru0310 - login: kaoru0310
avatarUrl: https://avatars.githubusercontent.com/u/80977929?u=1b61d10142b490e56af932ddf08a390fae8ee94f&v=4 avatarUrl: https://avatars.githubusercontent.com/u/80977929?u=1b61d10142b490e56af932ddf08a390fae8ee94f&v=4
url: https://github.com/kaoru0310 url: https://github.com/kaoru0310
- login: DelfinaCare - login: jstanden
avatarUrl: https://avatars.githubusercontent.com/u/83734439?v=4 avatarUrl: https://avatars.githubusercontent.com/u/63288?u=c3658d57d2862c607a0e19c2101c3c51876e36ad&v=4
url: https://github.com/DelfinaCare url: https://github.com/jstanden
- login: knallgelb - login: knallgelb
avatarUrl: https://avatars.githubusercontent.com/u/2358812?u=c48cb6362b309d74cbf144bd6ad3aed3eb443e82&v=4 avatarUrl: https://avatars.githubusercontent.com/u/2358812?u=c48cb6362b309d74cbf144bd6ad3aed3eb443e82&v=4
url: https://github.com/knallgelb url: https://github.com/knallgelb
@ -191,9 +173,6 @@ sponsors:
- login: oliverxchen - login: oliverxchen
avatarUrl: https://avatars.githubusercontent.com/u/4471774?u=534191f25e32eeaadda22dfab4b0a428733d5489&v=4 avatarUrl: https://avatars.githubusercontent.com/u/4471774?u=534191f25e32eeaadda22dfab4b0a428733d5489&v=4
url: https://github.com/oliverxchen 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 - login: paulcwatts
avatarUrl: https://avatars.githubusercontent.com/u/150269?u=1819e145d573b44f0ad74b87206d21cd60331d4e&v=4 avatarUrl: https://avatars.githubusercontent.com/u/150269?u=1819e145d573b44f0ad74b87206d21cd60331d4e&v=4
url: https://github.com/paulcwatts url: https://github.com/paulcwatts
@ -233,9 +212,6 @@ sponsors:
- login: mjohnsey - login: mjohnsey
avatarUrl: https://avatars.githubusercontent.com/u/16784016?u=38fad2e6b411244560b3af99c5f5a4751bc81865&v=4 avatarUrl: https://avatars.githubusercontent.com/u/16784016?u=38fad2e6b411244560b3af99c5f5a4751bc81865&v=4
url: https://github.com/mjohnsey 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 - login: ashi-agrawal
avatarUrl: https://avatars.githubusercontent.com/u/17105294?u=99c7a854035e5398d8e7b674f2d42baae6c957f8&v=4 avatarUrl: https://avatars.githubusercontent.com/u/17105294?u=99c7a854035e5398d8e7b674f2d42baae6c957f8&v=4
url: https://github.com/ashi-agrawal url: https://github.com/ashi-agrawal
@ -260,10 +236,7 @@ sponsors:
- - login: manoelpqueiroz - - login: manoelpqueiroz
avatarUrl: https://avatars.githubusercontent.com/u/23669137?u=b12e84b28a84369ab5b30bd5a79e5788df5a0756&v=4 avatarUrl: https://avatars.githubusercontent.com/u/23669137?u=b12e84b28a84369ab5b30bd5a79e5788df5a0756&v=4
url: https://github.com/manoelpqueiroz url: https://github.com/manoelpqueiroz
- - login: ceb10n - - login: pawamoy
avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4
url: https://github.com/ceb10n
- login: pawamoy
avatarUrl: https://avatars.githubusercontent.com/u/3999221?u=b030e4c89df2f3a36bc4710b925bdeb6745c9856&v=4 avatarUrl: https://avatars.githubusercontent.com/u/3999221?u=b030e4c89df2f3a36bc4710b925bdeb6745c9856&v=4
url: https://github.com/pawamoy url: https://github.com/pawamoy
- login: siavashyj - login: siavashyj
@ -281,9 +254,9 @@ sponsors:
- login: hgalytoby - login: hgalytoby
avatarUrl: https://avatars.githubusercontent.com/u/50397689?u=6cc9028f3db63f8f60ad21c17b1ce4b88c4e2e60&v=4 avatarUrl: https://avatars.githubusercontent.com/u/50397689?u=6cc9028f3db63f8f60ad21c17b1ce4b88c4e2e60&v=4
url: https://github.com/hgalytoby url: https://github.com/hgalytoby
- login: johnl28 - login: nisutec
avatarUrl: https://avatars.githubusercontent.com/u/54412955?u=47dd06082d1c39caa90c752eb55566e4f3813957&v=4 avatarUrl: https://avatars.githubusercontent.com/u/25281462?u=e562484c451fdfc59053163f64405f8eb262b8b0&v=4
url: https://github.com/johnl28 url: https://github.com/nisutec
- login: hoenie-ams - login: hoenie-ams
avatarUrl: https://avatars.githubusercontent.com/u/25708487?u=cda07434f0509ac728d9edf5e681117c0f6b818b&v=4 avatarUrl: https://avatars.githubusercontent.com/u/25708487?u=cda07434f0509ac728d9edf5e681117c0f6b818b&v=4
url: https://github.com/hoenie-ams url: https://github.com/hoenie-ams
@ -299,21 +272,24 @@ sponsors:
- login: petercool - login: petercool
avatarUrl: https://avatars.githubusercontent.com/u/37613029?u=75aa8c6729e6e8f85a300561c4dbeef9d65c8797&v=4 avatarUrl: https://avatars.githubusercontent.com/u/37613029?u=75aa8c6729e6e8f85a300561c4dbeef9d65c8797&v=4
url: https://github.com/petercool 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 - login: PunRabbit
avatarUrl: https://avatars.githubusercontent.com/u/70463212?u=1a835cfbc99295a60c8282f6aa6199d1b42241a5&v=4 avatarUrl: https://avatars.githubusercontent.com/u/70463212?u=1a835cfbc99295a60c8282f6aa6199d1b42241a5&v=4
url: https://github.com/PunRabbit url: https://github.com/PunRabbit
- login: PelicanQ - login: PelicanQ
avatarUrl: https://avatars.githubusercontent.com/u/77930606?v=4 avatarUrl: https://avatars.githubusercontent.com/u/77930606?v=4
url: https://github.com/PelicanQ 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 - login: my3
avatarUrl: https://avatars.githubusercontent.com/u/1825270?v=4 avatarUrl: https://avatars.githubusercontent.com/u/1825270?v=4
url: https://github.com/my3 url: https://github.com/my3
- login: danielunderwood - login: danielunderwood
avatarUrl: https://avatars.githubusercontent.com/u/4472301?v=4 avatarUrl: https://avatars.githubusercontent.com/u/4472301?v=4
url: https://github.com/danielunderwood 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 - login: ddanier
avatarUrl: https://avatars.githubusercontent.com/u/113563?u=ed1dc79de72f93bd78581f88ebc6952b62f472da&v=4 avatarUrl: https://avatars.githubusercontent.com/u/113563?u=ed1dc79de72f93bd78581f88ebc6952b62f472da&v=4
url: https://github.com/ddanier url: https://github.com/ddanier
@ -323,15 +299,21 @@ sponsors:
- login: slafs - login: slafs
avatarUrl: https://avatars.githubusercontent.com/u/210173?v=4 avatarUrl: https://avatars.githubusercontent.com/u/210173?v=4
url: https://github.com/slafs 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 - login: tochikuji
avatarUrl: https://avatars.githubusercontent.com/u/851759?v=4 avatarUrl: https://avatars.githubusercontent.com/u/851759?v=4
url: https://github.com/tochikuji url: https://github.com/tochikuji
- login: miguelgr - login: miguelgr
avatarUrl: https://avatars.githubusercontent.com/u/1484589?u=54556072b8136efa12ae3b6902032ea2a39ace4b&v=4 avatarUrl: https://avatars.githubusercontent.com/u/1484589?u=54556072b8136efa12ae3b6902032ea2a39ace4b&v=4
url: https://github.com/miguelgr url: https://github.com/miguelgr
- login: WillHogan - login: xncbf
avatarUrl: https://avatars.githubusercontent.com/u/1661551?u=8a80356e3e7d5a417157aba7ea565dabc8678327&v=4 avatarUrl: https://avatars.githubusercontent.com/u/9462045?u=a80a7bb349555b277645632ed66639ff43400614&v=4
url: https://github.com/WillHogan 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 - login: hard-coders
avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4
url: https://github.com/hard-coders url: https://github.com/hard-coders
@ -347,9 +329,9 @@ sponsors:
- login: joshuatz - login: joshuatz
avatarUrl: https://avatars.githubusercontent.com/u/17817563?u=f1bf05b690d1fc164218f0b420cdd3acb7913e21&v=4 avatarUrl: https://avatars.githubusercontent.com/u/17817563?u=f1bf05b690d1fc164218f0b420cdd3acb7913e21&v=4
url: https://github.com/joshuatz url: https://github.com/joshuatz
- login: nisutec - login: rangulvers
avatarUrl: https://avatars.githubusercontent.com/u/25281462?u=e562484c451fdfc59053163f64405f8eb262b8b0&v=4 avatarUrl: https://avatars.githubusercontent.com/u/5235430?u=e254d4af4ace5a05fa58372ae677c7d26f0d5a53&v=4
url: https://github.com/nisutec url: https://github.com/rangulvers
- login: sdevkota - login: sdevkota
avatarUrl: https://avatars.githubusercontent.com/u/5250987?u=4ed9a120c89805a8aefda1cbdc0cf6512e64d1b4&v=4 avatarUrl: https://avatars.githubusercontent.com/u/5250987?u=4ed9a120c89805a8aefda1cbdc0cf6512e64d1b4&v=4
url: https://github.com/sdevkota url: https://github.com/sdevkota
@ -368,19 +350,7 @@ sponsors:
- login: moonape1226 - login: moonape1226
avatarUrl: https://avatars.githubusercontent.com/u/8532038?u=d9f8b855a429fff9397c3833c2ff83849ebf989d&v=4 avatarUrl: https://avatars.githubusercontent.com/u/8532038?u=d9f8b855a429fff9397c3833c2ff83849ebf989d&v=4
url: https://github.com/moonape1226 url: https://github.com/moonape1226
- login: xncbf - - login: andrecorumba
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
avatarUrl: https://avatars.githubusercontent.com/u/37807517?u=9b9be3b41da9bda60957da9ef37b50dbf65baa61&v=4 avatarUrl: https://avatars.githubusercontent.com/u/37807517?u=9b9be3b41da9bda60957da9ef37b50dbf65baa61&v=4
url: https://github.com/andrecorumba url: https://github.com/andrecorumba
- login: KOZ39 - login: KOZ39
@ -389,21 +359,30 @@ sponsors:
- login: rwxd - login: rwxd
avatarUrl: https://avatars.githubusercontent.com/u/40308458?u=cd04a39e3655923be4f25c2ba8a5a07b3da3230a&v=4 avatarUrl: https://avatars.githubusercontent.com/u/40308458?u=cd04a39e3655923be4f25c2ba8a5a07b3da3230a&v=4
url: https://github.com/rwxd 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 - login: hippoley
avatarUrl: https://avatars.githubusercontent.com/u/135493401?u=1164ef48a645a7c12664fabc1638fbb7e1c459b0&v=4 avatarUrl: https://avatars.githubusercontent.com/u/135493401?u=1164ef48a645a7c12664fabc1638fbb7e1c459b0&v=4
url: https://github.com/hippoley 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 - login: CoderDeltaLAN
avatarUrl: https://avatars.githubusercontent.com/u/152043745?u=4ff541efffb7d134e60c5fcf2dd1e343f90bb782&v=4 avatarUrl: https://avatars.githubusercontent.com/u/152043745?u=4ff541efffb7d134e60c5fcf2dd1e343f90bb782&v=4
url: https://github.com/CoderDeltaLAN 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 - login: onestn
avatarUrl: https://avatars.githubusercontent.com/u/62360849?u=746dd21c34e7e06eefb11b03e8bb01aaae3c2a4f&v=4 avatarUrl: https://avatars.githubusercontent.com/u/62360849?u=746dd21c34e7e06eefb11b03e8bb01aaae3c2a4f&v=4
url: https://github.com/onestn 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 - login: nayasinghania
avatarUrl: https://avatars.githubusercontent.com/u/74111380?u=752e99a5e139389fdc0a0677122adc08438eb076&v=4 avatarUrl: https://avatars.githubusercontent.com/u/74111380?u=752e99a5e139389fdc0a0677122adc08438eb076&v=4
url: https://github.com/nayasinghania url: https://github.com/nayasinghania
@ -413,9 +392,6 @@ sponsors:
- login: andreagrandi - login: andreagrandi
avatarUrl: https://avatars.githubusercontent.com/u/636391?u=13d90cb8ec313593a5b71fbd4e33b78d6da736f5&v=4 avatarUrl: https://avatars.githubusercontent.com/u/636391?u=13d90cb8ec313593a5b71fbd4e33b78d6da736f5&v=4
url: https://github.com/andreagrandi 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 - login: msserpa
avatarUrl: https://avatars.githubusercontent.com/u/6334934?u=82c4489eb1559d88d2990d60001901b14f722bbb&v=4 avatarUrl: https://avatars.githubusercontent.com/u/6334934?u=82c4489eb1559d88d2990d60001901b14f722bbb&v=4
url: https://github.com/msserpa url: https://github.com/msserpa

View File

@ -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: gold:
- url: https://blockbee.io?ref=fastapi - url: https://blockbee.io?ref=fastapi
title: BlockBee Cryptocurrency Payment Gateway title: BlockBee Cryptocurrency Payment Gateway

View File

@ -75,7 +75,7 @@ mattwang44:
url: https://github.com/mattwang44 url: https://github.com/mattwang44
tiangolo: tiangolo:
login: tiangolo login: tiangolo
count: 55 count: 56
avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4
url: https://github.com/tiangolo url: https://github.com/tiangolo
Laineyzhang55: Laineyzhang55:
@ -136,7 +136,7 @@ JavierSanchezCastro:
alejsdev: alejsdev:
login: alejsdev login: alejsdev
count: 37 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 url: https://github.com/alejsdev
stlucasgarcia: stlucasgarcia:
login: stlucasgarcia login: stlucasgarcia
@ -436,7 +436,7 @@ jburckel:
peidrao: peidrao:
login: peidrao login: peidrao
count: 13 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 url: https://github.com/peidrao
impocode: impocode:
login: impocode login: impocode
@ -1006,7 +1006,7 @@ takacs:
anton2yakovlev: anton2yakovlev:
login: anton2yakovlev login: anton2yakovlev
count: 5 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 url: https://github.com/anton2yakovlev
ILoveSorasakiHina: ILoveSorasakiHina:
login: ILoveSorasakiHina login: ILoveSorasakiHina
@ -1161,7 +1161,7 @@ cookie-byte217:
AbolfazlKameli: AbolfazlKameli:
login: AbolfazlKameli login: AbolfazlKameli
count: 4 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 url: https://github.com/AbolfazlKameli
tyronedamasceno: tyronedamasceno:
login: tyronedamasceno login: tyronedamasceno
@ -1196,7 +1196,7 @@ Xaraxx:
Suyoung789: Suyoung789:
login: Suyoung789 login: Suyoung789
count: 3 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 url: https://github.com/Suyoung789
akagaeng: akagaeng:
login: akagaeng login: akagaeng
@ -1806,7 +1806,7 @@ MrL8199:
ivintoiu: ivintoiu:
login: ivintoiu login: ivintoiu
count: 2 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 url: https://github.com/ivintoiu
TechnoService2: TechnoService2:
login: TechnoService2 login: TechnoService2
@ -1841,7 +1841,7 @@ NavesSapnis:
eqsdxr: eqsdxr:
login: eqsdxr login: eqsdxr
count: 2 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 url: https://github.com/eqsdxr
syedasamina56: syedasamina56:
login: syedasamina56 login: syedasamina56

View File

@ -1,6 +1,6 @@
nilslindemann: nilslindemann:
login: nilslindemann login: nilslindemann
count: 124 count: 125
avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4
url: https://github.com/nilslindemann url: https://github.com/nilslindemann
jaystone776: jaystone776:
@ -8,16 +8,16 @@ jaystone776:
count: 46 count: 46
avatarUrl: https://avatars.githubusercontent.com/u/11191137?u=299205a95e9b6817a43144a48b643346a5aac5cc&v=4 avatarUrl: https://avatars.githubusercontent.com/u/11191137?u=299205a95e9b6817a43144a48b643346a5aac5cc&v=4
url: https://github.com/jaystone776 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: valentinDruzhinin:
login: valentinDruzhinin login: valentinDruzhinin
count: 29 count: 29
avatarUrl: https://avatars.githubusercontent.com/u/12831905?u=aae1ebc675c91e8fa582df4fcc4fc4128106344d&v=4 avatarUrl: https://avatars.githubusercontent.com/u/12831905?u=aae1ebc675c91e8fa582df4fcc4fc4128106344d&v=4
url: https://github.com/valentinDruzhinin 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: tokusumi:
login: tokusumi login: tokusumi
count: 23 count: 23
@ -286,7 +286,7 @@ hsuanchi:
alejsdev: alejsdev:
login: alejsdev login: alejsdev
count: 3 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 url: https://github.com/alejsdev
riroan: riroan:
login: riroan login: riroan
@ -358,6 +358,11 @@ ruzia:
count: 3 count: 3
avatarUrl: https://avatars.githubusercontent.com/u/24503?v=4 avatarUrl: https://avatars.githubusercontent.com/u/24503?v=4
url: https://github.com/ruzia 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: izaguerreiro:
login: izaguerreiro login: izaguerreiro
count: 2 count: 2
@ -543,8 +548,3 @@ EdmilsonRodrigues:
count: 2 count: 2
avatarUrl: https://avatars.githubusercontent.com/u/62777025?u=217d6f3cd6cc750bb8818a3af7726c8d74eb7c2d&v=4 avatarUrl: https://avatars.githubusercontent.com/u/62777025?u=217d6f3cd6cc750bb8818a3af7726c8d74eb7c2d&v=4
url: https://github.com/EdmilsonRodrigues 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

View File

@ -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 { .termynal-comment {
color: #4a968f; color: #4a968f;
font-style: italic; font-style: italic;

View File

@ -20,7 +20,7 @@
/* font-size: 18px; */ /* font-size: 18px; */
font-size: 15px; font-size: 15px;
/* font-family: 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; */ /* 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; border-radius: 4px;
padding: 75px 45px 35px; padding: 75px 45px 35px;
position: relative; position: relative;

View File

@ -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. In most of the cases, the main cloud providers have guides to deploy FastAPI with them.
## FastAPI Cloud { #fastapi-cloud }
**<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>** 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. ✨
## Cloud Providers - Sponsors { #cloud-providers-sponsors } ## Cloud Providers - Sponsors { #cloud-providers-sponsors }
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**. Some other cloud providers ✨ [**sponsor FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨ too. 🙇
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. 🙇 You might also want to consider them to follow their guides and try their services:
You might want to try their services and follow their guides:
* <a href="https://docs.render.com/deploy-fastapi?utm_source=deploydoc&utm_medium=referral&utm_campaign=fastapi" class="external-link" target="_blank">Render</a> * <a href="https://docs.render.com/deploy-fastapi?utm_source=deploydoc&utm_medium=referral&utm_campaign=fastapi" class="external-link" target="_blank">Render</a>
* <a href="https://docs.railway.com/guides/fastapi?utm_medium=integration&utm_source=docs&utm_campaign=fastapi" class="external-link" target="_blank">Railway</a> * <a href="https://docs.railway.com/guides/fastapi?utm_medium=integration&utm_source=docs&utm_campaign=fastapi" class="external-link" target="_blank">Railway</a>

View File

@ -0,0 +1,65 @@
# FastAPI Cloud { #fastapi-cloud }
You can deploy your FastAPI app to <a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a> 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:
<div class="termy">
```console
$ fastapi login
You are logged in to FastAPI Cloud 🚀
```
</div>
## Deploy { #deploy }
Now deploy your app, with **one command**:
<div class="termy">
```console
$ fastapi deploy
Deploying to FastAPI Cloud...
✅ Deployment successful!
🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev
```
</div>
That's it! Now you can access your app at that URL. ✨
## About FastAPI Cloud { #about-fastapi-cloud }
**<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>** 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. 🤓

View File

@ -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. 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 <a href="https://fastapicloud.com" class="external-link" target="_blank">**FastAPI Cloud**</a>, 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). 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. ✨ You will see more details to keep in mind and some of the techniques to do it in the next sections. ✨

View File

@ -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, <a href="https://datatracker.ietf.org/doc/html/rfc7235#section-3.1" class="external-link" target="_blank">RFC 7235</a>, <a href="https://datatracker.ietf.org/doc/html/rfc9110#name-401-unauthorized" class="external-link" target="_blank">RFC 9110</a>.
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.
///

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -52,14 +52,20 @@ The key features are:
<!-- sponsors --> <!-- sponsors -->
{% if sponsors %} ### Keystone Sponsor
{% for sponsor in sponsors.keystone -%}
<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a>
{% endfor -%}
### Gold and Silver Sponsors
{% for sponsor in sponsors.gold -%} {% for sponsor in sponsors.gold -%}
<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a> <a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a>
{% endfor -%} {% endfor -%}
{%- for sponsor in sponsors.silver -%} {%- for sponsor in sponsors.silver -%}
<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a> <a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a>
{% endfor %} {% endfor %}
{% endif %}
<!-- /sponsors --> <!-- /sponsors -->
@ -444,6 +450,58 @@ For a more complete example including more features, see the <a href="https://fa
* **Cookie Sessions** * **Cookie Sessions**
* ...and more. * ...and more.
### Deploy your app (optional) { #deploy-your-app-optional }
You can optionally deploy your FastAPI app to <a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>, 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:
<div class="termy">
```console
$ fastapi login
You are logged in to FastAPI Cloud 🚀
```
</div>
Then deploy your app:
<div class="termy">
```console
$ fastapi deploy
Deploying to FastAPI Cloud...
✅ Deployment successful!
🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev
```
</div>
That's it! Now you can access your app at that URL. ✨
#### About FastAPI Cloud { #about-fastapi-cloud }
**<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>** 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 } ## Performance { #performance }
Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">one of the fastest Python frameworks available</a>, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">one of the fastest Python frameworks available</a>, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*)

View File

@ -7,6 +7,66 @@ hide:
## Latest Changes ## Latest Changes
### Internal
* ⬆ 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 ## 0.121.2
### Fixes ### Fixes

View File

@ -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. 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 <a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>, 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:
<div class="termy">
```console
$ fastapi login
You are logged in to FastAPI Cloud 🚀
```
</div>
Then deploy your app:
<div class="termy">
```console
$ fastapi deploy
Deploying to FastAPI Cloud...
✅ Deployment successful!
🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev
```
</div>
That's it! Now you can access your app at that URL. ✨
## Recap, step by step { #recap-step-by-step } ## Recap, step by step { #recap-step-by-step }
### Step 1: import `FastAPI` { #step-1-import-fastapi } ### 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. 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 **<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>** with one command: `fastapi deploy`. 🎉
#### About FastAPI Cloud { #about-fastapi-cloud }
**<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>** 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 } ## Recap { #recap }
* Import `FastAPI`. * 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("/")`. * Write a **path operation decorator** using decorators like `@app.get("/")`.
* Define a **path operation function**; for example, `def root(): ...`. * Define a **path operation function**; for example, `def root(): ...`.
* Run the development server using the command `fastapi dev`. * Run the development server using the command `fastapi dev`.
* Optionally deploy your app with `fastapi deploy`.

View File

@ -1,6 +1,5 @@
# Define this here and not in the main mkdocs.yml file because that one is auto # 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 # updated and written, and the script would remove the env var
INHERIT: !ENV [INSIDERS_FILE, '../en/mkdocs.no-insiders.yml']
markdown_extensions: markdown_extensions:
pymdownx.highlight: pymdownx.highlight:
linenums: !ENV [LINENUMS, false] linenums: !ENV [LINENUMS, false]

View File

@ -1,10 +0,0 @@
plugins:
social:
cards_layout_options:
logo: ../en/docs/img/icon-white.svg
typeset:
markdown_extensions:
material.extensions.preview:
targets:
include:
- "*"

View File

@ -1,4 +1,4 @@
INHERIT: ../en/mkdocs.maybe-insiders.yml INHERIT: ../en/mkdocs.env.yml
site_name: FastAPI site_name: FastAPI
site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production
site_url: https://fastapi.tiangolo.com/ site_url: https://fastapi.tiangolo.com/
@ -52,6 +52,10 @@ theme:
repo_name: fastapi/fastapi repo_name: fastapi/fastapi
repo_url: https://github.com/fastapi/fastapi repo_url: https://github.com/fastapi/fastapi
plugins: plugins:
social:
cards_layout_options:
logo: ../en/docs/img/icon-white.svg
typeset:
search: null search: null
macros: macros:
include_yaml: include_yaml:
@ -192,6 +196,7 @@ nav:
- Deployment: - Deployment:
- deployment/index.md - deployment/index.md
- deployment/versions.md - deployment/versions.md
- deployment/fastapicloud.md
- deployment/https.md - deployment/https.md
- deployment/manually.md - deployment/manually.md
- deployment/concepts.md - deployment/concepts.md
@ -210,6 +215,7 @@ nav:
- how-to/custom-docs-ui-assets.md - how-to/custom-docs-ui-assets.md
- how-to/configure-swagger-ui.md - how-to/configure-swagger-ui.md
- how-to/testing-database.md - how-to/testing-database.md
- how-to/authentication-error-status-code.md
- Reference (Code API): - Reference (Code API):
- reference/index.md - reference/index.md
- reference/fastapi.md - reference/fastapi.md
@ -252,6 +258,10 @@ nav:
- management.md - management.md
- release-notes.md - release-notes.md
markdown_extensions: markdown_extensions:
material.extensions.preview:
targets:
include:
- "*"
abbr: null abbr: null
attr_list: null attr_list: null
footnotes: null footnotes: null

View File

@ -3,6 +3,13 @@
{% block announce %} {% block announce %}
<div class="announce-wrapper"> <div class="announce-wrapper">
<div id="announce-left"> <div id="announce-left">
<div class="item">
<a class="announce-link" href="https://fastapicloud.com" target="_blank">
<span class="twemoji">
{% include ".icons/material/cloud-arrow-up.svg" %}
</span> Join the <strong>FastAPI Cloud</strong> waiting list 🚀
</a>
</div>
<div class="item"> <div class="item">
<a class="announce-link" href="https://x.com/fastapi" target="_blank"> <a class="announce-link" href="https://x.com/fastapi" target="_blank">
<span class="twemoji"> <span class="twemoji">

View File

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

View File

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

View File

@ -60,7 +60,7 @@ async def get_current_user(token: str = Depends(oauth2_scheme)):
if not user: if not user:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials", detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
return user return user

View File

@ -61,7 +61,7 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
if not user: if not user:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials", detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
return user return user

View File

@ -60,7 +60,7 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
if not user: if not user:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials", detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
return user return user

View File

@ -60,7 +60,7 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
if not user: if not user:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials", detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
return user return user

View File

@ -58,7 +58,7 @@ async def get_current_user(token: str = Depends(oauth2_scheme)):
if not user: if not user:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials", detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
return user return user

View File

@ -1,6 +1,6 @@
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production""" """FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
__version__ = "0.121.2" __version__ = "0.123.0"
from starlette import status as status from starlette import status as status

View File

@ -38,19 +38,43 @@ class Dependant:
response_param_name: Optional[str] = None response_param_name: Optional[str] = None
background_tasks_param_name: Optional[str] = None background_tasks_param_name: Optional[str] = None
security_scopes_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 use_cache: bool = True
path: Optional[str] = None path: Optional[str] = None
scope: Union[Literal["function", "request"], None] = 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 @cached_property
def cache_key(self) -> DependencyCacheKey: def cache_key(self) -> DependencyCacheKey:
scopes_for_cache = (
tuple(sorted(set(self.oauth_scopes or []))) if self._uses_scopes else ()
)
return ( return (
self.call, self.call,
tuple(sorted(set(self.security_scopes or []))), scopes_for_cache,
self.computed_scope or "", 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 @cached_property
def is_gen_callable(self) -> bool: def is_gen_callable(self) -> bool:
if inspect.isgeneratorfunction(self.call): if inspect.isgeneratorfunction(self.call):

View File

@ -1,3 +1,4 @@
import dataclasses
import inspect import inspect
from contextlib import AsyncExitStack, contextmanager from contextlib import AsyncExitStack, contextmanager
from copy import copy, deepcopy from copy import copy, deepcopy
@ -57,8 +58,7 @@ from fastapi.dependencies.models import Dependant, SecurityRequirement
from fastapi.exceptions import DependencyScopeError from fastapi.exceptions import DependencyScopeError
from fastapi.logger import logger from fastapi.logger import logger
from fastapi.security.base import SecurityBase from fastapi.security.base import SecurityBase
from fastapi.security.oauth2 import OAuth2, SecurityScopes from fastapi.security.oauth2 import SecurityScopes
from fastapi.security.open_id_connect_url import OpenIdConnect
from fastapi.types import DependencyCacheKey from fastapi.types import DependencyCacheKey
from fastapi.utils import create_model_field, get_path_param_names from fastapi.utils import create_model_field, get_path_param_names
from pydantic import BaseModel from pydantic import BaseModel
@ -125,14 +125,14 @@ def get_parameterless_sub_dependant(*, depends: params.Depends, path: str) -> De
assert callable(depends.dependency), ( assert callable(depends.dependency), (
"A parameter-less dependency must have a callable 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.oauth_scopes: if isinstance(depends, params.Security) and depends.oauth_scopes:
use_security_scopes.extend(depends.oauth_scopes) own_oauth_scopes.extend(depends.scopes)
return get_dependant( return get_dependant(
path=path, path=path,
call=depends.dependency, call=depends.dependency,
scope=depends.scope, scope=depends.scope,
security_scopes=use_security_scopes, own_oauth_scopes=own_oauth_scopes,
) )
@ -231,7 +231,8 @@ def get_dependant(
path: str, path: str,
call: Callable[..., Any], call: Callable[..., Any],
name: Optional[str] = None, 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, use_cache: bool = True,
scope: Union[Literal["function", "request"], None] = None, scope: Union[Literal["function", "request"], None] = None,
) -> Dependant: ) -> Dependant:
@ -239,19 +240,18 @@ def get_dependant(
call=call, call=call,
name=name, name=name,
path=path, path=path,
security_scopes=security_scopes,
use_cache=use_cache, use_cache=use_cache,
scope=scope, 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) path_param_names = get_path_param_names(path)
endpoint_signature = get_typed_signature(call) endpoint_signature = get_typed_signature(call)
signature_params = endpoint_signature.parameters signature_params = endpoint_signature.parameters
if isinstance(call, SecurityBase): if isinstance(call, SecurityBase):
use_scopes: List[str] = []
if isinstance(call, (OAuth2, OpenIdConnect)):
use_scopes = security_scopes or use_scopes
security_requirement = SecurityRequirement( security_requirement = SecurityRequirement(
security_scheme=call, scopes=use_scopes security_scheme=call, scopes=current_scopes
) )
dependant.security_requirements.append(security_requirement) dependant.security_requirements.append(security_requirement)
for param_name, param in signature_params.items(): 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 ' f'The dependency "{dependant.call.__name__}" has a scope of '
'"request", it cannot depend on dependencies with scope "function".' '"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 isinstance(param_details.depends, params.Security):
if param_details.depends.oauth_scopes: if param_details.depends.oauth_scopes:
use_security_scopes.extend(param_details.depends.oauth_scopes) sub_own_oauth_scopes = list(param_details.depends.oauth_scopes)
sub_dependant = get_dependant( sub_dependant = get_dependant(
path=path, path=path,
call=param_details.depends.dependency, call=param_details.depends.dependency,
name=param_name, 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, use_cache=param_details.depends.use_cache,
scope=param_details.depends.scope, scope=param_details.depends.scope,
) )
@ -428,7 +429,7 @@ def analyze_param(
if depends is not None and depends.dependency is None: if depends is not None and depends.dependency is None:
# Copy `depends` before mutating it # Copy `depends` before mutating it
depends = copy(depends) depends = copy(depends)
depends.dependency = type_annotation depends = dataclasses.replace(depends, dependency=type_annotation)
# Handle non-param type annotations like Request # Handle non-param type annotations like Request
if lenient_issubclass( if lenient_issubclass(
@ -608,7 +609,7 @@ async def solve_dependencies(
path=use_path, path=use_path,
call=call, call=call,
name=sub_dependant.name, name=sub_dependant.name,
security_scopes=sub_dependant.security_scopes, parent_oauth_scopes=sub_dependant.oauth_scopes,
scope=sub_dependant.scope, scope=sub_dependant.scope,
) )
@ -690,7 +691,7 @@ async def solve_dependencies(
values[dependant.response_param_name] = response values[dependant.response_param_name] = response
if dependant.security_scopes_param_name: if dependant.security_scopes_param_name:
values[dependant.security_scopes_param_name] = SecurityScopes( values[dependant.security_scopes_param_name] = SecurityScopes(
scopes=dependant.security_scopes scopes=dependant.oauth_scopes
) )
return SolvedDependency( return SolvedDependency(
values=values, values=values,

View File

@ -772,14 +772,14 @@ class File(Form): # type: ignore[misc]
) )
@dataclass @dataclass(frozen=True)
class Depends: class Depends:
dependency: Optional[Callable[..., Any]] = None dependency: Optional[Callable[..., Any]] = None
use_cache: bool = True use_cache: bool = True
scope: Union[Literal["function", "request"], None] = None scope: Union[Literal["function", "request"], None] = None
@dataclass @dataclass(frozen=True)
class Security(Depends): class Security(Depends):
oauth_scopes: Optional[ oauth_scopes: Optional[
Union[ Union[

View File

@ -1,22 +1,52 @@
from typing import Optional from typing import Optional, Union
from annotated_doc import Doc from annotated_doc import Doc
from fastapi.openapi.models import APIKey, APIKeyIn from fastapi.openapi.models import APIKey, APIKeyIn
from fastapi.security.base import SecurityBase from fastapi.security.base import SecurityBase
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
from starlette.requests import Request from starlette.requests import Request
from starlette.status import HTTP_403_FORBIDDEN from starlette.status import HTTP_401_UNAUTHORIZED
from typing_extensions import Annotated from typing_extensions import Annotated
class APIKeyBase(SecurityBase): class APIKeyBase(SecurityBase):
@staticmethod def __init__(
def check_api_key(api_key: Optional[str], auto_error: bool) -> Optional[str]: self,
if not api_key: location: APIKeyIn,
if auto_error: name: str,
raise HTTPException( description: Union[str, None],
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" 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 self.auto_error:
raise self.make_not_authenticated_error()
return None return None
return api_key return api_key
@ -100,17 +130,17 @@ class APIKeyQuery(APIKeyBase):
), ),
] = True, ] = True,
): ):
self.model: APIKey = APIKey( super().__init__(
**{"in": APIKeyIn.query}, location=APIKeyIn.query,
name=name, name=name,
scheme_name=scheme_name,
description=description, 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]: async def __call__(self, request: Request) -> Optional[str]:
api_key = request.query_params.get(self.model.name) 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): class APIKeyHeader(APIKeyBase):
@ -188,17 +218,17 @@ class APIKeyHeader(APIKeyBase):
), ),
] = True, ] = True,
): ):
self.model: APIKey = APIKey( super().__init__(
**{"in": APIKeyIn.header}, location=APIKeyIn.header,
name=name, name=name,
scheme_name=scheme_name,
description=description, 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]: async def __call__(self, request: Request) -> Optional[str]:
api_key = request.headers.get(self.model.name) 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): class APIKeyCookie(APIKeyBase):
@ -276,14 +306,14 @@ class APIKeyCookie(APIKeyBase):
), ),
] = True, ] = True,
): ):
self.model: APIKey = APIKey( super().__init__(
**{"in": APIKeyIn.cookie}, location=APIKeyIn.cookie,
name=name, name=name,
scheme_name=scheme_name,
description=description, 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]: async def __call__(self, request: Request) -> Optional[str]:
api_key = request.cookies.get(self.model.name) 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)

View File

@ -1,6 +1,6 @@
import binascii import binascii
from base64 import b64decode from base64 import b64decode
from typing import Optional from typing import Dict, Optional
from annotated_doc import Doc from annotated_doc import Doc
from fastapi.exceptions import HTTPException 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 fastapi.security.utils import get_authorization_scheme_param
from pydantic import BaseModel from pydantic import BaseModel
from starlette.requests import Request 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 from typing_extensions import Annotated
@ -76,10 +76,22 @@ class HTTPBase(SecurityBase):
description: Optional[str] = None, description: Optional[str] = None,
auto_error: bool = True, 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.scheme_name = scheme_name or self.__class__.__name__
self.auto_error = auto_error 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__( async def __call__(
self, request: Request self, request: Request
) -> Optional[HTTPAuthorizationCredentials]: ) -> Optional[HTTPAuthorizationCredentials]:
@ -87,9 +99,7 @@ class HTTPBase(SecurityBase):
scheme, credentials = get_authorization_scheme_param(authorization) scheme, credentials = get_authorization_scheme_param(authorization)
if not (authorization and scheme and credentials): if not (authorization and scheme and credentials):
if self.auto_error: if self.auto_error:
raise HTTPException( raise self.make_not_authenticated_error()
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
)
else: else:
return None return None
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials) return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
@ -99,6 +109,8 @@ class HTTPBasic(HTTPBase):
""" """
HTTP Basic authentication. HTTP Basic authentication.
Ref: https://datatracker.ietf.org/doc/html/rfc7617
## Usage ## Usage
Create an instance object and use that object as the dependency in `Depends()`. Create an instance object and use that object as the dependency in `Depends()`.
@ -185,36 +197,28 @@ class HTTPBasic(HTTPBase):
self.realm = realm self.realm = realm
self.auto_error = auto_error 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 async def __call__( # type: ignore
self, request: Request self, request: Request
) -> Optional[HTTPBasicCredentials]: ) -> Optional[HTTPBasicCredentials]:
authorization = request.headers.get("Authorization") authorization = request.headers.get("Authorization")
scheme, param = get_authorization_scheme_param(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 not authorization or scheme.lower() != "basic":
if self.auto_error: if self.auto_error:
raise HTTPException( raise self.make_not_authenticated_error()
status_code=HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers=unauthorized_headers,
)
else: else:
return None return None
invalid_user_credentials_exc = HTTPException(
status_code=HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers=unauthorized_headers,
)
try: try:
data = b64decode(param).decode("ascii") data = b64decode(param).decode("ascii")
except (ValueError, UnicodeDecodeError, binascii.Error): except (ValueError, UnicodeDecodeError, binascii.Error) as e:
raise invalid_user_credentials_exc # noqa: B904 raise self.make_not_authenticated_error() from e
username, separator, password = data.partition(":") username, separator, password = data.partition(":")
if not separator: if not separator:
raise invalid_user_credentials_exc raise self.make_not_authenticated_error()
return HTTPBasicCredentials(username=username, password=password) return HTTPBasicCredentials(username=username, password=password)
@ -306,17 +310,12 @@ class HTTPBearer(HTTPBase):
scheme, credentials = get_authorization_scheme_param(authorization) scheme, credentials = get_authorization_scheme_param(authorization)
if not (authorization and scheme and credentials): if not (authorization and scheme and credentials):
if self.auto_error: if self.auto_error:
raise HTTPException( raise self.make_not_authenticated_error()
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
)
else: else:
return None return None
if scheme.lower() != "bearer": if scheme.lower() != "bearer":
if self.auto_error: if self.auto_error:
raise HTTPException( raise self.make_not_authenticated_error()
status_code=HTTP_403_FORBIDDEN,
detail="Invalid authentication credentials",
)
else: else:
return None return None
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials) return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
@ -326,6 +325,12 @@ class HTTPDigest(HTTPBase):
""" """
HTTP Digest authentication. 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 ## Usage
Create an instance object and use that object as the dependency in `Depends()`. 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) scheme, credentials = get_authorization_scheme_param(authorization)
if not (authorization and scheme and credentials): if not (authorization and scheme and credentials):
if self.auto_error: if self.auto_error:
raise HTTPException( raise self.make_not_authenticated_error()
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
)
else: else:
return None return None
if scheme.lower() != "digest": if scheme.lower() != "digest":
if self.auto_error: if self.auto_error:
raise HTTPException( raise self.make_not_authenticated_error()
status_code=HTTP_403_FORBIDDEN,
detail="Invalid authentication credentials",
)
else: else:
return None return None
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials) return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)

View File

@ -8,7 +8,7 @@ from fastapi.param_functions import Form
from fastapi.security.base import SecurityBase from fastapi.security.base import SecurityBase
from fastapi.security.utils import get_authorization_scheme_param from fastapi.security.utils import get_authorization_scheme_param
from starlette.requests import Request 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 # TODO: import from typing when deprecating Python 3.9
from typing_extensions import Annotated from typing_extensions import Annotated
@ -377,13 +377,33 @@ class OAuth2(SecurityBase):
self.scheme_name = scheme_name or self.__class__.__name__ self.scheme_name = scheme_name or self.__class__.__name__
self.auto_error = auto_error 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]: async def __call__(self, request: Request) -> Optional[str]:
authorization = request.headers.get("Authorization") authorization = request.headers.get("Authorization")
if not authorization: if not authorization:
if self.auto_error: if self.auto_error:
raise HTTPException( raise self.make_not_authenticated_error()
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
)
else: else:
return None return None
return authorization return authorization
@ -491,11 +511,7 @@ class OAuth2PasswordBearer(OAuth2):
scheme, param = get_authorization_scheme_param(authorization) scheme, param = get_authorization_scheme_param(authorization)
if not authorization or scheme.lower() != "bearer": if not authorization or scheme.lower() != "bearer":
if self.auto_error: if self.auto_error:
raise HTTPException( raise self.make_not_authenticated_error()
status_code=HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
else: else:
return None return None
return param return param
@ -601,11 +617,7 @@ class OAuth2AuthorizationCodeBearer(OAuth2):
scheme, param = get_authorization_scheme_param(authorization) scheme, param = get_authorization_scheme_param(authorization)
if not authorization or scheme.lower() != "bearer": if not authorization or scheme.lower() != "bearer":
if self.auto_error: if self.auto_error:
raise HTTPException( raise self.make_not_authenticated_error()
status_code=HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
else: else:
return None # pragma: nocover return None # pragma: nocover
return param return param

View File

@ -5,7 +5,7 @@ from fastapi.openapi.models import OpenIdConnect as OpenIdConnectModel
from fastapi.security.base import SecurityBase from fastapi.security.base import SecurityBase
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
from starlette.requests import Request from starlette.requests import Request
from starlette.status import HTTP_403_FORBIDDEN from starlette.status import HTTP_401_UNAUTHORIZED
from typing_extensions import Annotated 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 OpenID Connect authentication class. An instance of it would be used as a
dependency. 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__( def __init__(
@ -73,13 +78,18 @@ class OpenIdConnect(SecurityBase):
self.scheme_name = scheme_name or self.__class__.__name__ self.scheme_name = scheme_name or self.__class__.__name__
self.auto_error = auto_error 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]: async def __call__(self, request: Request) -> Optional[str]:
authorization = request.headers.get("Authorization") authorization = request.headers.get("Authorization")
if not authorization: if not authorization:
if self.auto_error: if self.auto_error:
raise HTTPException( raise self.make_not_authenticated_error()
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
)
else: else:
return None return None
return authorization return authorization

View File

@ -45,7 +45,7 @@ classifiers = [
"Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP",
] ]
dependencies = [ 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", "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", "typing-extensions>=4.8.0",
"annotated-doc>=0.0.2", "annotated-doc>=0.0.2",

View File

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

View File

@ -1,6 +1,6 @@
-e . -e .
-r requirements-docs-tests.txt -r requirements-docs-tests.txt
mkdocs-material==9.6.16 mkdocs-material==9.7.0
mdx-include >=1.4.1,<2.0.0 mdx-include >=1.4.1,<2.0.0
mkdocs-redirects>=1.2.1,<1.3.0 mkdocs-redirects>=1.2.1,<1.3.0
typer == 0.16.0 typer == 0.16.0
@ -13,7 +13,9 @@ pillow==11.3.0
cairosvg==2.8.2 cairosvg==2.8.2
mkdocstrings[python]==0.30.1 mkdocstrings[python]==0.30.1
griffe-typingdoc==0.3.0 griffe-typingdoc==0.3.0
griffe-warnings-deprecated==1.1.0
# For griffe, it formats with black # For griffe, it formats with black
black==25.1.0 black==25.1.0
mkdocs-macros-plugin==1.4.1 mkdocs-macros-plugin==1.4.1
markdown-include-variants==0.0.5 markdown-include-variants==0.0.7
python-slugify==8.0.4

View File

@ -1,6 +1,6 @@
-e .[all] -e .[all]
-r requirements-tests.txt -r requirements-tests.txt
-r requirements-docs.txt -r requirements-docs.txt
pre-commit >=2.17.0,<5.0.0 pre-commit >=4.5.0,<5.0.0
# For generating screenshots # For generating screenshots
playwright playwright

View File

@ -4,9 +4,8 @@ import os
import re import re
import shutil import shutil
import subprocess import subprocess
from functools import lru_cache from html.parser import HTMLParser
from http.server import HTTPServer, SimpleHTTPRequestHandler from http.server import HTTPServer, SimpleHTTPRequestHandler
from importlib import metadata
from multiprocessing import Pool from multiprocessing import Pool
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional, Union from typing import Any, Dict, List, Optional, Union
@ -16,6 +15,7 @@ import typer
import yaml import yaml
from jinja2 import Template from jinja2 import Template
from ruff.__main__ import find_ruff_bin from ruff.__main__ import find_ruff_bin
from slugify import slugify as py_slugify
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
@ -27,8 +27,8 @@ missing_translation_snippet = """
{!../../docs/missing-translation.md!} {!../../docs/missing-translation.md!}
""" """
non_translated_sections = [ non_translated_sections = (
"reference/", f"reference{os.sep}",
"release-notes.md", "release-notes.md",
"fastapi-people.md", "fastapi-people.md",
"external-links.md", "external-links.md",
@ -36,7 +36,7 @@ non_translated_sections = [
"management-tasks.md", "management-tasks.md",
"management.md", "management.md",
"contributing.md", "contributing.md",
] )
docs_path = Path("docs") docs_path = Path("docs")
en_docs_path = Path("docs/en") en_docs_path = Path("docs/en")
@ -44,13 +44,39 @@ en_config_path: Path = en_docs_path / mkdocs_name
site_path = Path("site").absolute() site_path = Path("site").absolute()
build_site_path = Path("site_build").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*$") 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 class VisibleTextExtractor(HTMLParser):
def is_mkdocs_insiders() -> bool: """Extract visible text from a string with HTML tags."""
version = metadata.version("mkdocs-material")
return "insiders" in version 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]: def get_en_config() -> Dict[str, Any]:
@ -77,9 +103,7 @@ def complete_existing_lang(incomplete: str):
@app.callback() @app.callback()
def callback() -> None: def callback() -> None:
if is_mkdocs_insiders(): # For MacOS with Cairo
os.environ["INSIDERS_FILE"] = "../en/mkdocs.insiders.yml"
# For MacOS with insiders and Cairo
os.environ["DYLD_FALLBACK_LIBRARY_PATH"] = "/opt/homebrew/lib" os.environ["DYLD_FALLBACK_LIBRARY_PATH"] = "/opt/homebrew/lib"
@ -115,10 +139,6 @@ def build_lang(
""" """
Build the docs for a language. 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 lang_path: Path = Path("docs") / lang
if not lang_path.is_dir(): if not lang_path.is_dir():
typer.echo(f"The language translation doesn't seem to exist yet: {lang}") typer.echo(f"The language translation doesn't seem to exist yet: {lang}")
@ -145,14 +165,20 @@ def build_lang(
index_sponsors_template = """ index_sponsors_template = """
{% if sponsors %} ### Keystone Sponsor
{% for sponsor in sponsors.keystone -%}
<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}"></a>
{% endfor %}
### Gold and Silver Sponsors
{% for sponsor in sponsors.gold -%} {% for sponsor in sponsors.gold -%}
<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}"></a> <a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}"></a>
{% endfor -%} {% endfor -%}
{%- for sponsor in sponsors.silver -%} {%- for sponsor in sponsors.silver -%}
<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}"></a> <a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}"></a>
{% endfor %} {% 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") 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__": if __name__ == "__main__":
app() app()

View File

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

View File

@ -32,8 +32,9 @@ def test_security_api_key():
def test_security_api_key_no_key(): def test_security_api_key_no_key():
client = TestClient(app) client = TestClient(app)
response = client.get("/users/me") 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.json() == {"detail": "Not authenticated"}
assert response.headers["WWW-Authenticate"] == "APIKey"
def test_openapi_schema(): def test_openapi_schema():

View File

@ -32,8 +32,9 @@ def test_security_api_key():
def test_security_api_key_no_key(): def test_security_api_key_no_key():
client = TestClient(app) client = TestClient(app)
response = client.get("/users/me") 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.json() == {"detail": "Not authenticated"}
assert response.headers["WWW-Authenticate"] == "APIKey"
def test_openapi_schema(): def test_openapi_schema():

View File

@ -33,8 +33,9 @@ def test_security_api_key():
def test_security_api_key_no_key(): def test_security_api_key_no_key():
response = client.get("/users/me") 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.json() == {"detail": "Not authenticated"}
assert response.headers["WWW-Authenticate"] == "APIKey"
def test_openapi_schema(): def test_openapi_schema():

View File

@ -33,8 +33,9 @@ def test_security_api_key():
def test_security_api_key_no_key(): def test_security_api_key_no_key():
response = client.get("/users/me") 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.json() == {"detail": "Not authenticated"}
assert response.headers["WWW-Authenticate"] == "APIKey"
def test_openapi_schema(): def test_openapi_schema():

View File

@ -33,8 +33,9 @@ def test_security_api_key():
def test_security_api_key_no_key(): def test_security_api_key_no_key():
response = client.get("/users/me") 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.json() == {"detail": "Not authenticated"}
assert response.headers["WWW-Authenticate"] == "APIKey"
def test_openapi_schema(): def test_openapi_schema():

View File

@ -33,8 +33,9 @@ def test_security_api_key():
def test_security_api_key_no_key(): def test_security_api_key_no_key():
response = client.get("/users/me") 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.json() == {"detail": "Not authenticated"}
assert response.headers["WWW-Authenticate"] == "APIKey"
def test_openapi_schema(): def test_openapi_schema():

View File

@ -23,8 +23,9 @@ def test_security_http_base():
def test_security_http_base_no_credentials(): def test_security_http_base_no_credentials():
response = client.get("/users/me") 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.json() == {"detail": "Not authenticated"}
assert response.headers["WWW-Authenticate"] == "Other"
def test_openapi_schema(): def test_openapi_schema():

View File

@ -23,8 +23,9 @@ def test_security_http_base():
def test_security_http_base_no_credentials(): def test_security_http_base_no_credentials():
response = client.get("/users/me") 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.json() == {"detail": "Not authenticated"}
assert response.headers["WWW-Authenticate"] == "Other"
def test_openapi_schema(): def test_openapi_schema():

View File

@ -38,7 +38,7 @@ def test_security_http_basic_invalid_credentials():
) )
assert response.status_code == 401, response.text assert response.status_code == 401, response.text
assert response.headers["WWW-Authenticate"] == "Basic" 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(): 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}) response = client.get("/users/me", headers={"Authorization": auth_header})
assert response.status_code == 401, response.text assert response.status_code == 401, response.text
assert response.headers["WWW-Authenticate"] == "Basic" assert response.headers["WWW-Authenticate"] == "Basic"
assert response.json() == {"detail": "Invalid authentication credentials"} assert response.json() == {"detail": "Not authenticated"}
def test_openapi_schema(): def test_openapi_schema():

View File

@ -36,7 +36,7 @@ def test_security_http_basic_invalid_credentials():
) )
assert response.status_code == 401, response.text assert response.status_code == 401, response.text
assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"' 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(): 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}) response = client.get("/users/me", headers={"Authorization": auth_header})
assert response.status_code == 401, response.text assert response.status_code == 401, response.text
assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"' 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(): def test_openapi_schema():

View File

@ -36,7 +36,7 @@ def test_security_http_basic_invalid_credentials():
) )
assert response.status_code == 401, response.text assert response.status_code == 401, response.text
assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"' 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(): 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}) response = client.get("/users/me", headers={"Authorization": auth_header})
assert response.status_code == 401, response.text assert response.status_code == 401, response.text
assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"' 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(): def test_openapi_schema():

View File

@ -23,14 +23,16 @@ def test_security_http_bearer():
def test_security_http_bearer_no_credentials(): def test_security_http_bearer_no_credentials():
response = client.get("/users/me") 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.json() == {"detail": "Not authenticated"}
assert response.headers["WWW-Authenticate"] == "Bearer"
def test_security_http_bearer_incorrect_scheme_credentials(): def test_security_http_bearer_incorrect_scheme_credentials():
response = client.get("/users/me", headers={"Authorization": "Basic notreally"}) response = client.get("/users/me", headers={"Authorization": "Basic notreally"})
assert response.status_code == 403, response.text 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"
def test_openapi_schema(): def test_openapi_schema():

View File

@ -23,14 +23,16 @@ def test_security_http_bearer():
def test_security_http_bearer_no_credentials(): def test_security_http_bearer_no_credentials():
response = client.get("/users/me") 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.json() == {"detail": "Not authenticated"}
assert response.headers["WWW-Authenticate"] == "Bearer"
def test_security_http_bearer_incorrect_scheme_credentials(): def test_security_http_bearer_incorrect_scheme_credentials():
response = client.get("/users/me", headers={"Authorization": "Basic notreally"}) response = client.get("/users/me", headers={"Authorization": "Basic notreally"})
assert response.status_code == 403, response.text 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"
def test_openapi_schema(): def test_openapi_schema():

View File

@ -23,16 +23,18 @@ def test_security_http_digest():
def test_security_http_digest_no_credentials(): def test_security_http_digest_no_credentials():
response = client.get("/users/me") 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.json() == {"detail": "Not authenticated"}
assert response.headers["WWW-Authenticate"] == "Digest"
def test_security_http_digest_incorrect_scheme_credentials(): def test_security_http_digest_incorrect_scheme_credentials():
response = client.get( response = client.get(
"/users/me", headers={"Authorization": "Other invalidauthorization"} "/users/me", headers={"Authorization": "Other invalidauthorization"}
) )
assert response.status_code == 403, response.text 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"] == "Digest"
def test_openapi_schema(): def test_openapi_schema():

View File

@ -23,16 +23,18 @@ def test_security_http_digest():
def test_security_http_digest_no_credentials(): def test_security_http_digest_no_credentials():
response = client.get("/users/me") 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.json() == {"detail": "Not authenticated"}
assert response.headers["WWW-Authenticate"] == "Digest"
def test_security_http_digest_incorrect_scheme_credentials(): def test_security_http_digest_incorrect_scheme_credentials():
response = client.get( response = client.get(
"/users/me", headers={"Authorization": "Other invalidauthorization"} "/users/me", headers={"Authorization": "Other invalidauthorization"}
) )
assert response.status_code == 403, response.text 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"] == "Digest"
def test_openapi_schema(): def test_openapi_schema():

View File

@ -56,8 +56,9 @@ def test_security_oauth2_password_other_header():
def test_security_oauth2_password_bearer_no_header(): def test_security_oauth2_password_bearer_no_header():
response = client.get("/users/me") 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.json() == {"detail": "Not authenticated"}
assert response.headers["WWW-Authenticate"] == "Bearer"
def test_strict_login_no_data(): def test_strict_login_no_data():

View File

@ -39,8 +39,9 @@ def test_security_oauth2_password_other_header():
def test_security_oauth2_password_bearer_no_header(): def test_security_oauth2_password_bearer_no_header():
response = client.get("/users/me") 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.json() == {"detail": "Not authenticated"}
assert response.headers["WWW-Authenticate"] == "Bearer"
def test_openapi_schema(): def test_openapi_schema():

View File

@ -41,8 +41,9 @@ def test_security_oauth2_password_other_header():
def test_security_oauth2_password_bearer_no_header(): def test_security_oauth2_password_bearer_no_header():
response = client.get("/users/me") 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.json() == {"detail": "Not authenticated"}
assert response.headers["WWW-Authenticate"] == "Bearer"
def test_openapi_schema(): def test_openapi_schema():

View File

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

View File

@ -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"],
}

View File

@ -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",
},
},
},
}

View File

@ -27,7 +27,7 @@ def test_get_root():
def test_get_root_no_token(): def test_get_root_no_token():
response = client.get("/") response = client.get("/")
assert response.status_code == 403, response.text assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"} assert response.json() == {"detail": "Not authenticated"}

View File

@ -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"}
}
},
}
)

View File

@ -66,7 +66,7 @@ def test_token(client: TestClient):
def test_incorrect_token(client: TestClient): def test_incorrect_token(client: TestClient):
response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"})
assert response.status_code == 401, response.text 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" assert response.headers["WWW-Authenticate"] == "Bearer"

View File

@ -41,7 +41,7 @@ def test_security_http_basic_invalid_credentials(client: TestClient):
) )
assert response.status_code == 401, response.text assert response.status_code == 401, response.text
assert response.headers["WWW-Authenticate"] == "Basic" 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): 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}) response = client.get("/users/me", headers={"Authorization": auth_header})
assert response.status_code == 401, response.text assert response.status_code == 401, response.text
assert response.headers["WWW-Authenticate"] == "Basic" 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): def test_openapi_schema(client: TestClient):

View File

@ -45,7 +45,7 @@ def get_client(request: pytest.FixtureRequest):
with TestClient(mod.app) as c: with TestClient(mod.app) as c:
yield c yield c
# Clean up connection explicitely to avoid resource warning # Clean up connection explicitly to avoid resource warning
mod.engine.dispose() mod.engine.dispose()

View File

@ -45,7 +45,7 @@ def get_client(request: pytest.FixtureRequest):
with TestClient(mod.app) as c: with TestClient(mod.app) as c:
yield c yield c
# Clean up connection explicitely to avoid resource warning # Clean up connection explicitly to avoid resource warning
mod.engine.dispose() mod.engine.dispose()