Browse Source

Merge remote-tracking branch 'upstream/master' into fix-duplicate-special-dependency-handling

pull/12406/head
Yurii Motov 7 months ago
parent
commit
eada7d618f
  1. 1
      .github/labeler.yml
  2. 22
      .github/workflows/build-docs.yml
  3. 2
      .github/workflows/contributors.yml
  4. 2
      .github/workflows/deploy-docs.yml
  5. 2
      .github/workflows/label-approved.yml
  6. 4
      .github/workflows/latest-changes.yml
  7. 2
      .github/workflows/notify-translations.yml
  8. 2
      .github/workflows/people.yml
  9. 88
      .github/workflows/pre-commit.yml
  10. 2
      .github/workflows/publish.yml
  11. 2
      .github/workflows/smokeshow.yml
  12. 2
      .github/workflows/sponsors.yml
  13. 2
      .github/workflows/test-redistribute.yml
  14. 6
      .github/workflows/test.yml
  15. 2
      .github/workflows/topic-repos.yml
  16. 2
      .github/workflows/translate.yml
  17. 3
      .gitignore
  18. 34
      .pre-commit-config.yaml
  19. 45
      docs/en/data/contributors.yml
  20. 128
      docs/en/data/github_sponsors.yml
  21. 16
      docs/en/data/translation_reviewers.yml
  22. 24
      docs/en/data/translators.yml
  23. 15
      docs/en/docs/css/custom.css
  24. 2
      docs/en/docs/css/termynal.css
  25. 17
      docs/en/docs/how-to/authentication-error-status-code.md
  26. 44
      docs/en/docs/release-notes.md
  27. 1
      docs/en/mkdocs.env.yml
  28. 10
      docs/en/mkdocs.insiders.yml
  29. 11
      docs/en/mkdocs.yml
  30. 20
      docs_src/authentication_error_status_code/tutorial001_an.py
  31. 21
      docs_src/authentication_error_status_code/tutorial001_an_py39.py
  32. 2
      docs_src/security/tutorial003.py
  33. 2
      docs_src/security/tutorial003_an.py
  34. 2
      docs_src/security/tutorial003_an_py310.py
  35. 2
      docs_src/security/tutorial003_an_py39.py
  36. 2
      docs_src/security/tutorial003_py310.py
  37. 2
      fastapi/__init__.py
  38. 28
      fastapi/dependencies/models.py
  39. 32
      fastapi/dependencies/utils.py
  40. 76
      fastapi/security/api_key.py
  41. 74
      fastapi/security/http.py
  42. 40
      fastapi/security/oauth2.py
  43. 18
      fastapi/security/open_id_connect_url.py
  44. 3
      requirements-docs-insiders.txt
  45. 6
      requirements-docs.txt
  46. 2
      requirements.txt
  47. 130
      scripts/docs.py
  48. 3
      tests/test_security_api_key_cookie.py
  49. 3
      tests/test_security_api_key_cookie_description.py
  50. 3
      tests/test_security_api_key_header.py
  51. 3
      tests/test_security_api_key_header_description.py
  52. 3
      tests/test_security_api_key_query.py
  53. 3
      tests/test_security_api_key_query_description.py
  54. 3
      tests/test_security_http_base.py
  55. 3
      tests/test_security_http_base_description.py
  56. 4
      tests/test_security_http_basic_optional.py
  57. 4
      tests/test_security_http_basic_realm.py
  58. 4
      tests/test_security_http_basic_realm_description.py
  59. 8
      tests/test_security_http_bearer.py
  60. 8
      tests/test_security_http_bearer_description.py
  61. 8
      tests/test_security_http_digest.py
  62. 8
      tests/test_security_http_digest_description.py
  63. 3
      tests/test_security_oauth2.py
  64. 3
      tests/test_security_openid_connect.py
  65. 3
      tests/test_security_openid_connect_description.py
  66. 46
      tests/test_security_scopes.py
  67. 45
      tests/test_security_scopes_dont_propagate.py
  68. 107
      tests/test_security_scopes_sub_dependency.py
  69. 2
      tests/test_top_level_security_scheme_in_openapi.py
  70. 0
      tests/test_tutorial/test_authentication_error_status_code/__init__.py
  71. 69
      tests/test_tutorial/test_authentication_error_status_code/test_tutorial001.py
  72. 2
      tests/test_tutorial/test_security/test_tutorial003.py
  73. 4
      tests/test_tutorial/test_security/test_tutorial006.py

1
.github/labeler.yml

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

22
.github/workflows/build-docs.yml

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

2
.github/workflows/contributors.yml

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

2
.github/workflows/deploy-docs.yml

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

2
.github/workflows/label-approved.yml

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

4
.github/workflows/latest-changes.yml

@ -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/[email protected].0 - uses: tiangolo/[email protected].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

2
.github/workflows/notify-translations.yml

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

2
.github/workflows/people.yml

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

@ -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/[email protected]
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) }}

2
.github/workflows/publish.yml

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

2
.github/workflows/smokeshow.yml

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

2
.github/workflows/sponsors.yml

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

2
.github/workflows/test-redistribute.yml

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

6
.github/workflows/test.yml

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

2
.github/workflows/topic-repos.yml

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

2
.github/workflows/translate.yml

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

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

34
.pre-commit-config.yaml

@ -1,25 +1,29 @@
# 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
- id: check-toml - id: check-toml
- id: check-yaml - id: check-yaml
args: args:
- --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$

45
docs/en/data/contributors.yml

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

128
docs/en/data/github_sponsors.yml

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

16
docs/en/data/translation_reviewers.yml

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

24
docs/en/data/translators.yml

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

15
docs/en/docs/css/custom.css

@ -1,3 +1,18 @@
/* Fira Code, including characters used by Rich output, like the "heavy right-pointing angle bracket ornament", not included in Google Fonts */
@import url(https://cdn.jsdelivr.net/npm/firacode@6.2.0/distr/fira_code.css);
/* Noto Color Emoji for emoji support with the same font everywhere */
@import url(https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&display=swap);
/* Override default code font in Material for MkDocs to Fira Code */
:root {
--md-code-font: "Fira Code", monospace, "Noto Color Emoji";
}
/* Override default regular font in Material for MkDocs to include Noto Color Emoji */
:root {
--md-text-font: "Roboto", "Noto Color Emoji";
}
.termynal-comment { .termynal-comment {
color: #4a968f; color: #4a968f;
font-style: italic; font-style: italic;

2
docs/en/docs/css/termynal.css

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

17
docs/en/docs/how-to/authentication-error-status-code.md

@ -0,0 +1,17 @@
# Use Old 403 Authentication Error Status Codes { #use-old-403-authentication-error-status-codes }
Before FastAPI version `0.122.0`, when the integrated security utilities returned an error to the client after a failed authentication, they used the HTTP status code `403 Forbidden`.
Starting with FastAPI version `0.122.0`, they use the more appropriate HTTP status code `401 Unauthorized`, and return a sensible `WWW-Authenticate` header in the response, following the HTTP specifications, <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.
///

44
docs/en/docs/release-notes.md

@ -7,6 +7,50 @@ 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 ## 0.121.3
### Refactors ### Refactors

1
docs/en/mkdocs.maybe-insiders.yml → docs/en/mkdocs.env.yml

@ -1,6 +1,5 @@
# Define this here and not in the main mkdocs.yml file because that one is auto # 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]

10
docs/en/mkdocs.insiders.yml

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

11
docs/en/mkdocs.yml

@ -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:
@ -211,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
@ -253,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

20
docs_src/authentication_error_status_code/tutorial001_an.py

@ -0,0 +1,20 @@
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from typing_extensions import Annotated
app = FastAPI()
class HTTPBearer403(HTTPBearer):
def make_not_authenticated_error(self) -> HTTPException:
return HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="Not authenticated"
)
CredentialsDep = Annotated[HTTPAuthorizationCredentials, Depends(HTTPBearer403())]
@app.get("/me")
def read_me(credentials: CredentialsDep):
return {"message": "You are authenticated", "token": credentials.credentials}

21
docs_src/authentication_error_status_code/tutorial001_an_py39.py

@ -0,0 +1,21 @@
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
app = FastAPI()
class HTTPBearer403(HTTPBearer):
def make_not_authenticated_error(self) -> HTTPException:
return HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="Not authenticated"
)
CredentialsDep = Annotated[HTTPAuthorizationCredentials, Depends(HTTPBearer403())]
@app.get("/me")
def read_me(credentials: CredentialsDep):
return {"message": "You are authenticated", "token": credentials.credentials}

2
docs_src/security/tutorial003.py

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

2
docs_src/security/tutorial003_an.py

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

2
docs_src/security/tutorial003_an_py310.py

@ -60,7 +60,7 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
if not user: 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

2
docs_src/security/tutorial003_an_py39.py

@ -60,7 +60,7 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
if not user: 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

2
docs_src/security/tutorial003_py310.py

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

2
fastapi/__init__.py

@ -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.3" __version__ = "0.123.0"
from starlette import status as status from starlette import status as status

28
fastapi/dependencies/models.py

@ -38,19 +38,43 @@ class Dependant:
response_param_names: Set[str] = field(default_factory=set) response_param_names: Set[str] = field(default_factory=set)
background_tasks_param_names: Set[str] = field(default_factory=set) background_tasks_param_names: Set[str] = field(default_factory=set)
security_scopes_param_names: Set[str] = field(default_factory=set) security_scopes_param_names: Set[str] = field(default_factory=set)
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):

32
fastapi/dependencies/utils.py

@ -58,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
@ -126,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.scopes: if isinstance(depends, params.Security) and depends.scopes:
use_security_scopes.extend(depends.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,
) )
@ -232,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:
@ -240,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():
@ -275,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.scopes: if param_details.depends.scopes:
use_security_scopes.extend(param_details.depends.scopes) sub_own_oauth_scopes = list(param_details.depends.scopes)
sub_dependant = get_dependant( 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,
) )
@ -609,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,
) )
@ -693,7 +693,7 @@ async def solve_dependencies(
for name in dependant.response_param_names: for name in dependant.response_param_names:
values[name] = response values[name] = response
if dependant.security_scopes_param_names: if dependant.security_scopes_param_names:
security_scope = SecurityScopes(scopes=dependant.security_scopes) security_scope = SecurityScopes(scopes=dependant.oauth_scopes)
for name in dependant.security_scopes_param_names: for name in dependant.security_scopes_param_names:
values[name] = security_scope values[name] = security_scope
return SolvedDependency( return SolvedDependency(

76
fastapi/security/api_key.py

@ -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,
location: APIKeyIn,
name: str,
description: Union[str, None],
scheme_name: Union[str, None],
auto_error: bool,
):
self.auto_error = auto_error
self.model: APIKey = APIKey(
**{"in": location},
name=name,
description=description,
)
self.scheme_name = scheme_name or self.__class__.__name__
def make_not_authenticated_error(self) -> HTTPException:
"""
The WWW-Authenticate header is not standardized for API Key authentication but
the HTTP specification requires that an error of 401 "Unauthorized" must
include a WWW-Authenticate header.
Ref: https://datatracker.ietf.org/doc/html/rfc9110#name-401-unauthorized
For this, this method sends a custom challenge `APIKey`.
"""
return HTTPException(
status_code=HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers={"WWW-Authenticate": "APIKey"},
)
def check_api_key(self, api_key: Optional[str]) -> Optional[str]:
if not api_key: if not api_key:
if auto_error: if self.auto_error:
raise HTTPException( raise self.make_not_authenticated_error()
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
)
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)

74
fastapi/security/http.py

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

40
fastapi/security/oauth2.py

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

18
fastapi/security/open_id_connect_url.py

@ -5,7 +5,7 @@ from fastapi.openapi.models import OpenIdConnect as OpenIdConnectModel
from fastapi.security.base import SecurityBase from 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

3
requirements-docs-insiders.txt

@ -1,3 +0,0 @@
git+https://${TOKEN}@github.com/squidfunk/[email protected]
git+https://${TOKEN}@github.com/pawamoy-insiders/griffe-typing-deprecated.git
git+https://${TOKEN}@github.com/pawamoy-insiders/mkdocstrings-python.git

6
requirements-docs.txt

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

2
requirements.txt

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

130
scripts/docs.py

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

3
tests/test_security_api_key_cookie.py

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

3
tests/test_security_api_key_cookie_description.py

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

3
tests/test_security_api_key_header.py

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

3
tests/test_security_api_key_header_description.py

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

3
tests/test_security_api_key_query.py

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

3
tests/test_security_api_key_query_description.py

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

3
tests/test_security_http_base.py

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

3
tests/test_security_http_base_description.py

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

4
tests/test_security_http_basic_optional.py

@ -38,7 +38,7 @@ def test_security_http_basic_invalid_credentials():
) )
assert response.status_code == 401, response.text assert response.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():

4
tests/test_security_http_basic_realm.py

@ -36,7 +36,7 @@ def test_security_http_basic_invalid_credentials():
) )
assert response.status_code == 401, response.text assert response.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():

4
tests/test_security_http_basic_realm_description.py

@ -36,7 +36,7 @@ def test_security_http_basic_invalid_credentials():
) )
assert response.status_code == 401, response.text assert response.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():

8
tests/test_security_http_bearer.py

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

8
tests/test_security_http_bearer_description.py

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

8
tests/test_security_http_digest.py

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

8
tests/test_security_http_digest_description.py

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

3
tests/test_security_oauth2.py

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

3
tests/test_security_openid_connect.py

@ -39,8 +39,9 @@ def test_security_oauth2_password_other_header():
def test_security_oauth2_password_bearer_no_header(): 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():

3
tests/test_security_openid_connect_description.py

@ -41,8 +41,9 @@ def test_security_oauth2_password_other_header():
def test_security_oauth2_password_bearer_no_header(): 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():

46
tests/test_security_scopes.py

@ -0,0 +1,46 @@
from typing import Dict
import pytest
from fastapi import Depends, FastAPI, Security
from fastapi.testclient import TestClient
from typing_extensions import Annotated
@pytest.fixture(name="call_counter")
def call_counter_fixture():
return {"count": 0}
@pytest.fixture(name="app")
def app_fixture(call_counter: Dict[str, int]):
def get_db():
call_counter["count"] += 1
return f"db_{call_counter['count']}"
def get_user(db: Annotated[str, Depends(get_db)]):
return "user"
app = FastAPI()
@app.get("/")
def endpoint(
db: Annotated[str, Depends(get_db)],
user: Annotated[str, Security(get_user, scopes=["read"])],
):
return {"db": db}
return app
@pytest.fixture(name="client")
def client_fixture(app: FastAPI):
return TestClient(app)
def test_security_scopes_dependency_called_once(
client: TestClient, call_counter: Dict[str, int]
):
response = client.get("/")
assert response.status_code == 200
assert call_counter["count"] == 1

45
tests/test_security_scopes_dont_propagate.py

@ -0,0 +1,45 @@
# Ref: https://github.com/tiangolo/fastapi/issues/5623
from typing import Any, Dict, List
from fastapi import FastAPI, Security
from fastapi.security import SecurityScopes
from fastapi.testclient import TestClient
from typing_extensions import Annotated
async def security1(scopes: SecurityScopes):
return scopes.scopes
async def security2(scopes: SecurityScopes):
return scopes.scopes
async def dep3(
dep1: Annotated[List[str], Security(security1, scopes=["scope1"])],
dep2: Annotated[List[str], Security(security2, scopes=["scope2"])],
):
return {"dep1": dep1, "dep2": dep2}
app = FastAPI()
@app.get("/scopes")
def get_scopes(
dep3: Annotated[Dict[str, Any], Security(dep3, scopes=["scope3"])],
):
return dep3
client = TestClient(app)
def test_security_scopes_dont_propagate():
response = client.get("/scopes")
assert response.status_code == 200
assert response.json() == {
"dep1": ["scope3", "scope1"],
"dep2": ["scope3", "scope2"],
}

107
tests/test_security_scopes_sub_dependency.py

@ -0,0 +1,107 @@
# Ref: https://github.com/fastapi/fastapi/discussions/6024#discussioncomment-8541913
from typing import Dict
import pytest
from fastapi import Depends, FastAPI, Security
from fastapi.security import SecurityScopes
from fastapi.testclient import TestClient
from typing_extensions import Annotated
@pytest.fixture(name="call_counts")
def call_counts_fixture():
return {
"get_db_session": 0,
"get_current_user": 0,
"get_user_me": 0,
"get_user_items": 0,
}
@pytest.fixture(name="app")
def app_fixture(call_counts: Dict[str, int]):
def get_db_session():
call_counts["get_db_session"] += 1
return f"db_session_{call_counts['get_db_session']}"
def get_current_user(
security_scopes: SecurityScopes,
db_session: Annotated[str, Depends(get_db_session)],
):
call_counts["get_current_user"] += 1
return {
"user": f"user_{call_counts['get_current_user']}",
"scopes": security_scopes.scopes,
"db_session": db_session,
}
def get_user_me(
current_user: Annotated[dict, Security(get_current_user, scopes=["me"])],
):
call_counts["get_user_me"] += 1
return {
"user_me": f"user_me_{call_counts['get_user_me']}",
"current_user": current_user,
}
def get_user_items(
user_me: Annotated[dict, Depends(get_user_me)],
):
call_counts["get_user_items"] += 1
return {
"user_items": f"user_items_{call_counts['get_user_items']}",
"user_me": user_me,
}
app = FastAPI()
@app.get("/")
def path_operation(
user_me: Annotated[dict, Depends(get_user_me)],
user_items: Annotated[dict, Security(get_user_items, scopes=["items"])],
):
return {
"user_me": user_me,
"user_items": user_items,
}
return app
@pytest.fixture(name="client")
def client_fixture(app: FastAPI):
return TestClient(app)
def test_security_scopes_sub_dependency_caching(
client: TestClient, call_counts: Dict[str, int]
):
response = client.get("/")
assert response.status_code == 200
assert call_counts["get_db_session"] == 1
assert call_counts["get_current_user"] == 2
assert call_counts["get_user_me"] == 2
assert call_counts["get_user_items"] == 1
assert response.json() == {
"user_me": {
"user_me": "user_me_1",
"current_user": {
"user": "user_1",
"scopes": ["me"],
"db_session": "db_session_1",
},
},
"user_items": {
"user_items": "user_items_1",
"user_me": {
"user_me": "user_me_2",
"current_user": {
"user": "user_2",
"scopes": ["items", "me"],
"db_session": "db_session_1",
},
},
},
}

2
tests/test_top_level_security_scheme_in_openapi.py

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

0
docs/en/mkdocs.no-insiders.yml → tests/test_tutorial/test_authentication_error_status_code/__init__.py

69
tests/test_tutorial/test_authentication_error_status_code/test_tutorial001.py

@ -0,0 +1,69 @@
import importlib
import pytest
from fastapi.testclient import TestClient
from inline_snapshot import snapshot
from ...utils import needs_py39
@pytest.fixture(
name="client",
params=[
"tutorial001_an",
pytest.param("tutorial001_an_py39", marks=needs_py39),
],
)
def get_client(request: pytest.FixtureRequest):
mod = importlib.import_module(
f"docs_src.authentication_error_status_code.{request.param}"
)
client = TestClient(mod.app)
return client
def test_get_me(client: TestClient):
response = client.get("/me", headers={"Authorization": "Bearer secrettoken"})
assert response.status_code == 200
assert response.json() == {
"message": "You are authenticated",
"token": "secrettoken",
}
def test_get_me_no_credentials(client: TestClient):
response = client.get("/me")
assert response.status_code == 403
assert response.json() == {"detail": "Not authenticated"}
def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == snapshot(
{
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/me": {
"get": {
"summary": "Read Me",
"operationId": "read_me_me_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
}
},
"security": [{"HTTPBearer403": []}],
}
}
},
"components": {
"securitySchemes": {
"HTTPBearer403": {"type": "http", "scheme": "bearer"}
}
},
}
)

2
tests/test_tutorial/test_security/test_tutorial003.py

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

4
tests/test_tutorial/test_security/test_tutorial006.py

@ -41,7 +41,7 @@ def test_security_http_basic_invalid_credentials(client: TestClient):
) )
assert response.status_code == 401, response.text assert response.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):

Loading…
Cancel
Save