mirror of https://github.com/tiangolo/fastapi.git
Merge remote-tracking branch 'upstream/master' into deprecate-scopes-parameter
This commit is contained in:
commit
4c6fd06027
|
|
@ -17,6 +17,7 @@ lang-all:
|
|||
- docs/*/docs/**
|
||||
- all-globs-to-all-files:
|
||||
- '!docs/en/docs/**'
|
||||
- '!docs/*/**/_*.md'
|
||||
- '!fastapi/**'
|
||||
- '!pyproject.toml'
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ jobs:
|
|||
outputs:
|
||||
docs: ${{ steps.filter.outputs.docs }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
# For pull requests it's not necessary to checkout the code but for the main branch it is
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
|
|
@ -32,12 +32,9 @@ jobs:
|
|||
- docs/**
|
||||
- docs_src/**
|
||||
- requirements-docs.txt
|
||||
- requirements-docs-insiders.txt
|
||||
- pyproject.toml
|
||||
- mkdocs.yml
|
||||
- mkdocs.insiders.yml
|
||||
- mkdocs.maybe-insiders.yml
|
||||
- mkdocs.no-insiders.yml
|
||||
- mkdocs.env.yml
|
||||
- .github/workflows/build-docs.yml
|
||||
- .github/workflows/deploy-docs.yml
|
||||
- scripts/mkdocs_hooks.py
|
||||
|
|
@ -48,7 +45,7 @@ jobs:
|
|||
outputs:
|
||||
langs: ${{ steps.show-langs.outputs.langs }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
|
|
@ -63,12 +60,6 @@ jobs:
|
|||
pyproject.toml
|
||||
- name: Install docs extras
|
||||
run: uv pip install -r requirements-docs.txt
|
||||
# Install MkDocs Material Insiders here just to put it in the cache for the rest of the steps
|
||||
- name: Install Material for MkDocs Insiders
|
||||
if: ( github.event_name != 'pull_request' || github.secret_source == 'Actions' )
|
||||
run: uv pip install -r requirements-docs-insiders.txt
|
||||
env:
|
||||
TOKEN: ${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}
|
||||
- name: Verify Docs
|
||||
run: python ./scripts/docs.py verify-docs
|
||||
- name: Export Language Codes
|
||||
|
|
@ -90,7 +81,7 @@ jobs:
|
|||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
|
|
@ -105,11 +96,6 @@ jobs:
|
|||
pyproject.toml
|
||||
- name: Install docs extras
|
||||
run: uv pip install -r requirements-docs.txt
|
||||
- name: Install Material for MkDocs Insiders
|
||||
if: ( github.event_name != 'pull_request' || github.secret_source == 'Actions' )
|
||||
run: uv pip install -r requirements-docs-insiders.txt
|
||||
env:
|
||||
TOKEN: ${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}
|
||||
- name: Update Languages
|
||||
run: python ./scripts/docs.py update-languages
|
||||
- uses: actions/cache@v4
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ jobs:
|
|||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ jobs:
|
|||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ jobs:
|
|||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ jobs:
|
|||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
# pin to actions/checkout@v5 for compatibility with latest-changes
|
||||
# Ref: https://github.com/actions/checkout/issues/2313
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
# To allow latest-changes to commit to the main branch
|
||||
|
|
@ -34,7 +36,7 @@ jobs:
|
|||
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }}
|
||||
with:
|
||||
limit-access-to-actor: true
|
||||
- uses: tiangolo/latest-changes@0.4.0
|
||||
- uses: tiangolo/latest-changes@0.4.1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
latest_changes_file: docs/en/docs/release-notes.md
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ jobs:
|
|||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ jobs:
|
|||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
name: pre-commit
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
|
||||
env:
|
||||
IS_FORK: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
|
||||
|
||||
jobs:
|
||||
pre-commit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Dump GitHub context
|
||||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v5
|
||||
name: Checkout PR for own repo
|
||||
if: env.IS_FORK == 'false'
|
||||
with:
|
||||
# To be able to commit it needs more than the last commit
|
||||
ref: ${{ github.head_ref }}
|
||||
# A token other than the default GITHUB_TOKEN is needed to be able to trigger CI
|
||||
token: ${{ secrets.PRE_COMMIT }}
|
||||
# pre-commit lite ci needs the default checkout configs to work
|
||||
- uses: actions/checkout@v5
|
||||
name: Checkout PR for fork
|
||||
if: env.IS_FORK == 'true'
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.14"
|
||||
- name: Setup uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
cache-dependency-glob: |
|
||||
requirements**.txt
|
||||
pyproject.toml
|
||||
uv.lock
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
uv venv
|
||||
uv pip install -r requirements.txt
|
||||
- name: Run pre-commit
|
||||
id: precommit
|
||||
run: |
|
||||
# Fetch the base branch for comparison
|
||||
git fetch origin ${{ github.base_ref }}
|
||||
uvx pre-commit run --from-ref origin/${{ github.base_ref }} --to-ref HEAD --show-diff-on-failure
|
||||
continue-on-error: true
|
||||
- name: Commit and push changes
|
||||
if: env.IS_FORK == 'false'
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add -A
|
||||
if git diff --staged --quiet; then
|
||||
echo "No changes to commit"
|
||||
else
|
||||
git commit -m "🎨 Auto format"
|
||||
git push
|
||||
fi
|
||||
- uses: pre-commit-ci/lite-action@v1.1.0
|
||||
if: env.IS_FORK == 'true'
|
||||
with:
|
||||
msg: 🎨 Auto format
|
||||
- name: Error out on pre-commit errors
|
||||
if: steps.precommit.outcome == 'failure'
|
||||
run: exit 1
|
||||
|
||||
# https://github.com/marketplace/actions/alls-green#why
|
||||
pre-commit-alls-green: # This job does nothing and is only used for the branch protection
|
||||
if: always()
|
||||
needs:
|
||||
- pre-commit
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Dump GitHub context
|
||||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- name: Decide whether the needed jobs succeeded or failed
|
||||
uses: re-actors/alls-green@release/v1
|
||||
with:
|
||||
jobs: ${{ toJSON(needs) }}
|
||||
|
|
@ -20,7 +20,7 @@ jobs:
|
|||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ jobs:
|
|||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.9'
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ jobs:
|
|||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ jobs:
|
|||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ jobs:
|
|||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
|
|
@ -65,7 +65,7 @@ jobs:
|
|||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
|
|
@ -111,7 +111,7 @@ jobs:
|
|||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.8'
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ jobs:
|
|||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ jobs:
|
|||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
|
|
|
|||
|
|
@ -28,3 +28,6 @@ archive.zip
|
|||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Ignore while the setup still depends on requirements.txt files
|
||||
uv.lock
|
||||
|
|
|
|||
|
|
@ -1,25 +1,29 @@
|
|||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
default_language_version:
|
||||
python: python3.10
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: check-added-large-files
|
||||
- id: check-toml
|
||||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
- id: check-toml
|
||||
- id: check-yaml
|
||||
args:
|
||||
- --unsafe
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
- --unsafe
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.14.3
|
||||
hooks:
|
||||
- id: ruff
|
||||
- id: ruff
|
||||
args:
|
||||
- --fix
|
||||
- id: ruff-format
|
||||
ci:
|
||||
autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
|
||||
autoupdate_commit_msg: ⬆ [pre-commit.ci] pre-commit autoupdate
|
||||
- id: ruff-format
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: local-script
|
||||
language: unsupported
|
||||
name: local script
|
||||
entry: uv run ./scripts/docs.py add-permalinks-pages
|
||||
args:
|
||||
- --update-existing
|
||||
files: ^docs/en/docs/.*\.md$
|
||||
|
|
|
|||
57
README.md
57
README.md
|
|
@ -45,6 +45,11 @@ The key features are:
|
|||
## Sponsors
|
||||
|
||||
<!-- sponsors -->
|
||||
### Keystone Sponsor
|
||||
|
||||
<a href="https://fastapicloud.com" target="_blank" title="FastAPI Cloud. By the same team behind FastAPI. You code. We Cloud."><img src="https://fastapi.tiangolo.com/img/sponsors/fastapicloud.png"></a>
|
||||
|
||||
### Gold and Silver Sponsors
|
||||
|
||||
<a href="https://blockbee.io?ref=fastapi" target="_blank" title="BlockBee Cryptocurrency Payment Gateway"><img src="https://fastapi.tiangolo.com/img/sponsors/blockbee.png"></a>
|
||||
<a href="https://github.com/scalar/scalar/?utm_source=fastapi&utm_medium=website&utm_campaign=main-badge" target="_blank" title="Scalar: Beautiful Open-Source API References from Swagger/OpenAPI files"><img src="https://fastapi.tiangolo.com/img/sponsors/scalar.svg"></a>
|
||||
|
|
@ -447,6 +452,58 @@ For a more complete example including more features, see the <a href="https://fa
|
|||
* **Cookie Sessions**
|
||||
* ...and more.
|
||||
|
||||
### Deploy your app (optional)
|
||||
|
||||
You can optionally deploy your FastAPI app to <a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>, go and join the waiting list if you haven't. 🚀
|
||||
|
||||
If you already have a **FastAPI Cloud** account (we invited you from the waiting list 😉), you can deploy your application with one command.
|
||||
|
||||
Before deploying, make sure you are logged in:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ fastapi login
|
||||
|
||||
You are logged in to FastAPI Cloud 🚀
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
Then deploy your app:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ fastapi deploy
|
||||
|
||||
Deploying to FastAPI Cloud...
|
||||
|
||||
✅ Deployment successful!
|
||||
|
||||
🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
That's it! Now you can access your app at that URL. ✨
|
||||
|
||||
#### About FastAPI Cloud
|
||||
|
||||
**<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>** is built by the same author and team behind **FastAPI**.
|
||||
|
||||
It streamlines the process of **building**, **deploying**, and **accessing** an API with minimal effort.
|
||||
|
||||
It brings the same **developer experience** of building apps with FastAPI to **deploying** them to the cloud. 🎉
|
||||
|
||||
FastAPI Cloud is the primary sponsor and funding provider for the *FastAPI and friends* open source projects. ✨
|
||||
|
||||
#### Deploy to other cloud providers
|
||||
|
||||
FastAPI is open source and based on standards. You can deploy FastAPI apps to any cloud provider you choose.
|
||||
|
||||
Follow your cloud provider's guides to deploy FastAPI apps with them. 🤓
|
||||
|
||||
## Performance
|
||||
|
||||
Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">one of the fastest Python frameworks available</a>, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*)
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
tiangolo:
|
||||
login: tiangolo
|
||||
count: 794
|
||||
count: 808
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4
|
||||
url: https://github.com/tiangolo
|
||||
dependabot:
|
||||
login: dependabot
|
||||
count: 126
|
||||
count: 130
|
||||
avatarUrl: https://avatars.githubusercontent.com/in/29110?v=4
|
||||
url: https://github.com/apps/dependabot
|
||||
alejsdev:
|
||||
login: alejsdev
|
||||
count: 52
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=447d12a1b347f466b35378bee4c7104cc9b2c571&v=4
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=85ceac49fb87138aebe8d663912e359447329090&v=4
|
||||
url: https://github.com/alejsdev
|
||||
pre-commit-ci:
|
||||
login: pre-commit-ci
|
||||
count: 49
|
||||
count: 50
|
||||
avatarUrl: https://avatars.githubusercontent.com/in/68672?v=4
|
||||
url: https://github.com/apps/pre-commit-ci
|
||||
github-actions:
|
||||
|
|
@ -28,31 +28,31 @@ Kludex:
|
|||
count: 25
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4
|
||||
url: https://github.com/Kludex
|
||||
YuriiMotov:
|
||||
login: YuriiMotov
|
||||
count: 20
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4
|
||||
url: https://github.com/YuriiMotov
|
||||
dmontagu:
|
||||
login: dmontagu
|
||||
count: 17
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4
|
||||
url: https://github.com/dmontagu
|
||||
YuriiMotov:
|
||||
login: YuriiMotov
|
||||
count: 15
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4
|
||||
url: https://github.com/YuriiMotov
|
||||
nilslindemann:
|
||||
login: nilslindemann
|
||||
count: 14
|
||||
count: 15
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4
|
||||
url: https://github.com/nilslindemann
|
||||
svlandeg:
|
||||
login: svlandeg
|
||||
count: 14
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/8796347?u=556c97650c27021911b0b9447ec55e75987b0e8a&v=4
|
||||
url: https://github.com/svlandeg
|
||||
euri10:
|
||||
login: euri10
|
||||
count: 13
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4
|
||||
url: https://github.com/euri10
|
||||
svlandeg:
|
||||
login: svlandeg
|
||||
count: 13
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/8796347?u=556c97650c27021911b0b9447ec55e75987b0e8a&v=4
|
||||
url: https://github.com/svlandeg
|
||||
kantandane:
|
||||
login: kantandane
|
||||
count: 13
|
||||
|
|
@ -103,6 +103,11 @@ waynerv:
|
|||
count: 5
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4
|
||||
url: https://github.com/waynerv
|
||||
musicinmybrain:
|
||||
login: musicinmybrain
|
||||
count: 5
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/6898909?u=9010312053e7141383b9bdf538036c7f37fbaba0&v=4
|
||||
url: https://github.com/musicinmybrain
|
||||
krishnamadhavan:
|
||||
login: krishnamadhavan
|
||||
count: 5
|
||||
|
|
@ -133,11 +138,6 @@ iudeen:
|
|||
count: 4
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=f09cdd745e5bf16138f29b42732dd57c7f02bee1&v=4
|
||||
url: https://github.com/iudeen
|
||||
musicinmybrain:
|
||||
login: musicinmybrain
|
||||
count: 4
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/6898909?u=9010312053e7141383b9bdf538036c7f37fbaba0&v=4
|
||||
url: https://github.com/musicinmybrain
|
||||
philipokiokio:
|
||||
login: philipokiokio
|
||||
count: 4
|
||||
|
|
@ -483,6 +483,11 @@ nzig:
|
|||
count: 2
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/7372858?u=e769add36ed73c778cdb136eb10bf96b1e119671&v=4
|
||||
url: https://github.com/nzig
|
||||
kristjanvalur:
|
||||
login: kristjanvalur
|
||||
count: 2
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/6009543?u=1419f20bbfff8f031be8cb470962e7e62de2595e&v=4
|
||||
url: https://github.com/kristjanvalur
|
||||
yezz123:
|
||||
login: yezz123
|
||||
count: 2
|
||||
|
|
|
|||
|
|
@ -23,9 +23,6 @@ sponsors:
|
|||
- login: railwayapp
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/66716858?v=4
|
||||
url: https://github.com/railwayapp
|
||||
- login: scalar
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/301879?v=4
|
||||
url: https://github.com/scalar
|
||||
- - login: dribia
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/41189616?v=4
|
||||
url: https://github.com/dribia
|
||||
|
|
@ -44,25 +41,25 @@ sponsors:
|
|||
- login: permitio
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/71775833?v=4
|
||||
url: https://github.com/permitio
|
||||
- - login: BoostryJP
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/57932412?v=4
|
||||
url: https://github.com/BoostryJP
|
||||
- login: mercedes-benz
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/34240465?v=4
|
||||
url: https://github.com/mercedes-benz
|
||||
- login: Ponte-Energy-Partners
|
||||
- - login: Ponte-Energy-Partners
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/114745848?v=4
|
||||
url: https://github.com/Ponte-Energy-Partners
|
||||
- login: LambdaTest-Inc
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/171592363?u=96606606a45fa170427206199014f2a5a2a4920b&v=4
|
||||
url: https://github.com/LambdaTest-Inc
|
||||
- login: BoostryJP
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/57932412?v=4
|
||||
url: https://github.com/BoostryJP
|
||||
- login: requestly
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/12287519?v=4
|
||||
url: https://github.com/requestly
|
||||
- login: acsone
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/7601056?v=4
|
||||
url: https://github.com/acsone
|
||||
- - login: Trivie
|
||||
- - login: scalar
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/301879?v=4
|
||||
url: https://github.com/scalar
|
||||
- login: Trivie
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/8161763?v=4
|
||||
url: https://github.com/Trivie
|
||||
- - login: takashi-yoneya
|
||||
|
|
@ -71,42 +68,30 @@ sponsors:
|
|||
- login: Doist
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/2565372?v=4
|
||||
url: https://github.com/Doist
|
||||
- login: bholagabbar
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/11693595?v=4
|
||||
url: https://github.com/bholagabbar
|
||||
- - login: mainframeindustries
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/55092103?v=4
|
||||
url: https://github.com/mainframeindustries
|
||||
- - login: alixlahuec
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/29543316?u=44357eb2a93bccf30fb9d389b8befe94a3d00985&v=4
|
||||
url: https://github.com/alixlahuec
|
||||
- login: Partho
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/2034301?u=ce195ac36835cca0cdfe6dd6e897bd38873a1524&v=4
|
||||
url: https://github.com/Partho
|
||||
- - login: primer-io
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/62146168?v=4
|
||||
url: https://github.com/primer-io
|
||||
- login: xsalagarcia
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/66035908?v=4
|
||||
url: https://github.com/xsalagarcia
|
||||
- - login: upciti
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/43346262?v=4
|
||||
url: https://github.com/upciti
|
||||
- login: GonnaFlyMethod
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/60840539?u=edf70b373fd4f1a83d3eb7c6802f4b6addb572cf&v=4
|
||||
url: https://github.com/GonnaFlyMethod
|
||||
- login: ChargeStorm
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/26000165?v=4
|
||||
url: https://github.com/ChargeStorm
|
||||
- login: DanielYang59
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/80093591?u=63873f701c7c74aac83c906800a1dddc0bc8c92f&v=4
|
||||
url: https://github.com/DanielYang59
|
||||
- login: nilslindemann
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4
|
||||
url: https://github.com/nilslindemann
|
||||
- - login: samuelcolvin
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/4039449?u=42eb3b833047c8c4b4f647a031eaef148c16d93f&v=4
|
||||
url: https://github.com/samuelcolvin
|
||||
- login: vincentkoc
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/25068?u=fbd5b2d51142daa4bdbc21e21953a3b8b8188a4a&v=4
|
||||
url: https://github.com/vincentkoc
|
||||
- login: otosky
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/42260747?u=69d089387c743d89427aa4ad8740cfb34045a9e0&v=4
|
||||
url: https://github.com/otosky
|
||||
|
|
@ -137,9 +122,6 @@ sponsors:
|
|||
- login: jugeeem
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/116043716?u=ae590d79c38ac79c91b9c5caa6887d061e865a3d&v=4
|
||||
url: https://github.com/jugeeem
|
||||
- login: connorpark24
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/142128990?u=09b84a4beb1f629b77287a837bcf3729785cdd89&v=4
|
||||
url: https://github.com/connorpark24
|
||||
- login: patsatsia
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/61111267?u=3271b85f7a37b479c8d0ae0a235182e83c166edf&v=4
|
||||
url: https://github.com/patsatsia
|
||||
|
|
@ -155,9 +137,9 @@ sponsors:
|
|||
- login: kaoru0310
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/80977929?u=1b61d10142b490e56af932ddf08a390fae8ee94f&v=4
|
||||
url: https://github.com/kaoru0310
|
||||
- login: DelfinaCare
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/83734439?v=4
|
||||
url: https://github.com/DelfinaCare
|
||||
- login: jstanden
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/63288?u=c3658d57d2862c607a0e19c2101c3c51876e36ad&v=4
|
||||
url: https://github.com/jstanden
|
||||
- login: knallgelb
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/2358812?u=c48cb6362b309d74cbf144bd6ad3aed3eb443e82&v=4
|
||||
url: https://github.com/knallgelb
|
||||
|
|
@ -191,9 +173,6 @@ sponsors:
|
|||
- login: oliverxchen
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/4471774?u=534191f25e32eeaadda22dfab4b0a428733d5489&v=4
|
||||
url: https://github.com/oliverxchen
|
||||
- login: jstanden
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/63288?u=c3658d57d2862c607a0e19c2101c3c51876e36ad&v=4
|
||||
url: https://github.com/jstanden
|
||||
- login: paulcwatts
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/150269?u=1819e145d573b44f0ad74b87206d21cd60331d4e&v=4
|
||||
url: https://github.com/paulcwatts
|
||||
|
|
@ -233,9 +212,6 @@ sponsors:
|
|||
- login: mjohnsey
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/16784016?u=38fad2e6b411244560b3af99c5f5a4751bc81865&v=4
|
||||
url: https://github.com/mjohnsey
|
||||
- login: enguy-hub
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/16822912?u=2c45f9e7f427b2f2f3b023d7fdb0d44764c92ae8&v=4
|
||||
url: https://github.com/enguy-hub
|
||||
- login: ashi-agrawal
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/17105294?u=99c7a854035e5398d8e7b674f2d42baae6c957f8&v=4
|
||||
url: https://github.com/ashi-agrawal
|
||||
|
|
@ -260,10 +236,7 @@ sponsors:
|
|||
- - login: manoelpqueiroz
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/23669137?u=b12e84b28a84369ab5b30bd5a79e5788df5a0756&v=4
|
||||
url: https://github.com/manoelpqueiroz
|
||||
- - login: ceb10n
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4
|
||||
url: https://github.com/ceb10n
|
||||
- login: pawamoy
|
||||
- - login: pawamoy
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/3999221?u=b030e4c89df2f3a36bc4710b925bdeb6745c9856&v=4
|
||||
url: https://github.com/pawamoy
|
||||
- login: siavashyj
|
||||
|
|
@ -281,9 +254,9 @@ sponsors:
|
|||
- login: hgalytoby
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/50397689?u=6cc9028f3db63f8f60ad21c17b1ce4b88c4e2e60&v=4
|
||||
url: https://github.com/hgalytoby
|
||||
- login: johnl28
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/54412955?u=47dd06082d1c39caa90c752eb55566e4f3813957&v=4
|
||||
url: https://github.com/johnl28
|
||||
- login: nisutec
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/25281462?u=e562484c451fdfc59053163f64405f8eb262b8b0&v=4
|
||||
url: https://github.com/nisutec
|
||||
- login: hoenie-ams
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/25708487?u=cda07434f0509ac728d9edf5e681117c0f6b818b&v=4
|
||||
url: https://github.com/hoenie-ams
|
||||
|
|
@ -299,21 +272,24 @@ sponsors:
|
|||
- login: petercool
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/37613029?u=75aa8c6729e6e8f85a300561c4dbeef9d65c8797&v=4
|
||||
url: https://github.com/petercool
|
||||
- login: johnl28
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/54412955?u=47dd06082d1c39caa90c752eb55566e4f3813957&v=4
|
||||
url: https://github.com/johnl28
|
||||
- login: PunRabbit
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/70463212?u=1a835cfbc99295a60c8282f6aa6199d1b42241a5&v=4
|
||||
url: https://github.com/PunRabbit
|
||||
- login: PelicanQ
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/77930606?v=4
|
||||
url: https://github.com/PelicanQ
|
||||
- login: WillHogan
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/1661551?u=8a80356e3e7d5a417157aba7ea565dabc8678327&v=4
|
||||
url: https://github.com/WillHogan
|
||||
- login: my3
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/1825270?v=4
|
||||
url: https://github.com/my3
|
||||
- login: danielunderwood
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/4472301?v=4
|
||||
url: https://github.com/danielunderwood
|
||||
- login: rangulvers
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/5235430?u=e254d4af4ace5a05fa58372ae677c7d26f0d5a53&v=4
|
||||
url: https://github.com/rangulvers
|
||||
- login: ddanier
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/113563?u=ed1dc79de72f93bd78581f88ebc6952b62f472da&v=4
|
||||
url: https://github.com/ddanier
|
||||
|
|
@ -323,15 +299,21 @@ sponsors:
|
|||
- login: slafs
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/210173?v=4
|
||||
url: https://github.com/slafs
|
||||
- login: ceb10n
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4
|
||||
url: https://github.com/ceb10n
|
||||
- login: tochikuji
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/851759?v=4
|
||||
url: https://github.com/tochikuji
|
||||
- login: miguelgr
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/1484589?u=54556072b8136efa12ae3b6902032ea2a39ace4b&v=4
|
||||
url: https://github.com/miguelgr
|
||||
- login: WillHogan
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/1661551?u=8a80356e3e7d5a417157aba7ea565dabc8678327&v=4
|
||||
url: https://github.com/WillHogan
|
||||
- login: xncbf
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/9462045?u=a80a7bb349555b277645632ed66639ff43400614&v=4
|
||||
url: https://github.com/xncbf
|
||||
- login: DMantis
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/9536869?u=652dd0d49717803c0cbcbf44f7740e53cf2d4892&v=4
|
||||
url: https://github.com/DMantis
|
||||
- login: hard-coders
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4
|
||||
url: https://github.com/hard-coders
|
||||
|
|
@ -347,9 +329,9 @@ sponsors:
|
|||
- login: joshuatz
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/17817563?u=f1bf05b690d1fc164218f0b420cdd3acb7913e21&v=4
|
||||
url: https://github.com/joshuatz
|
||||
- login: nisutec
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/25281462?u=e562484c451fdfc59053163f64405f8eb262b8b0&v=4
|
||||
url: https://github.com/nisutec
|
||||
- login: rangulvers
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/5235430?u=e254d4af4ace5a05fa58372ae677c7d26f0d5a53&v=4
|
||||
url: https://github.com/rangulvers
|
||||
- login: sdevkota
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/5250987?u=4ed9a120c89805a8aefda1cbdc0cf6512e64d1b4&v=4
|
||||
url: https://github.com/sdevkota
|
||||
|
|
@ -368,19 +350,7 @@ sponsors:
|
|||
- login: moonape1226
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/8532038?u=d9f8b855a429fff9397c3833c2ff83849ebf989d&v=4
|
||||
url: https://github.com/moonape1226
|
||||
- login: xncbf
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/9462045?u=a80a7bb349555b277645632ed66639ff43400614&v=4
|
||||
url: https://github.com/xncbf
|
||||
- login: DMantis
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/9536869?u=652dd0d49717803c0cbcbf44f7740e53cf2d4892&v=4
|
||||
url: https://github.com/DMantis
|
||||
- - login: morzan1001
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/47593005?u=c30ab7230f82a12a9b938dcb54f84a996931409a&v=4
|
||||
url: https://github.com/morzan1001
|
||||
- login: larsyngvelundin
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/34173819?u=74958599695bf83ac9f1addd935a51548a10c6b0&v=4
|
||||
url: https://github.com/larsyngvelundin
|
||||
- login: andrecorumba
|
||||
- - login: andrecorumba
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/37807517?u=9b9be3b41da9bda60957da9ef37b50dbf65baa61&v=4
|
||||
url: https://github.com/andrecorumba
|
||||
- login: KOZ39
|
||||
|
|
@ -389,21 +359,30 @@ sponsors:
|
|||
- login: rwxd
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/40308458?u=cd04a39e3655923be4f25c2ba8a5a07b3da3230a&v=4
|
||||
url: https://github.com/rwxd
|
||||
- login: morzan1001
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/47593005?u=c30ab7230f82a12a9b938dcb54f84a996931409a&v=4
|
||||
url: https://github.com/morzan1001
|
||||
- login: Olegt0rr
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/25399456?u=3e87b5239a2f4600975ba13be73054f8567c6060&v=4
|
||||
url: https://github.com/Olegt0rr
|
||||
- login: dinoz0rg
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/32940067?u=739cda1eb123a2dd5e1db45c361396f239e23f8b&v=4
|
||||
url: https://github.com/dinoz0rg
|
||||
- login: larsyngvelundin
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/34173819?u=74958599695bf83ac9f1addd935a51548a10c6b0&v=4
|
||||
url: https://github.com/larsyngvelundin
|
||||
- login: hippoley
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/135493401?u=1164ef48a645a7c12664fabc1638fbb7e1c459b0&v=4
|
||||
url: https://github.com/hippoley
|
||||
- login: 4anklee
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/144109238?u=a79c0d581b2a3d8f3897e7ef4c012640a6c1eb3a&v=4
|
||||
url: https://github.com/4anklee
|
||||
- login: CoderDeltaLAN
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/152043745?u=4ff541efffb7d134e60c5fcf2dd1e343f90bb782&v=4
|
||||
url: https://github.com/CoderDeltaLAN
|
||||
- login: chris1ding1
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/194386334?u=5500604b50e35ed8a5aeb82ce34aa5d3ee3f88c7&v=4
|
||||
url: https://github.com/chris1ding1
|
||||
- login: onestn
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/62360849?u=746dd21c34e7e06eefb11b03e8bb01aaae3c2a4f&v=4
|
||||
url: https://github.com/onestn
|
||||
- login: Rubinskiy
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/62457878?u=f2e35ed3d196a99cfadb5a29a91950342af07e34&v=4
|
||||
url: https://github.com/Rubinskiy
|
||||
- login: nayasinghania
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/74111380?u=752e99a5e139389fdc0a0677122adc08438eb076&v=4
|
||||
url: https://github.com/nayasinghania
|
||||
|
|
@ -413,9 +392,6 @@ sponsors:
|
|||
- login: andreagrandi
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/636391?u=13d90cb8ec313593a5b71fbd4e33b78d6da736f5&v=4
|
||||
url: https://github.com/andreagrandi
|
||||
- login: Olegt0rr
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/25399456?u=3e87b5239a2f4600975ba13be73054f8567c6060&v=4
|
||||
url: https://github.com/Olegt0rr
|
||||
- login: msserpa
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/6334934?u=82c4489eb1559d88d2990d60001901b14f722bbb&v=4
|
||||
url: https://github.com/msserpa
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
keystone:
|
||||
- url: https://fastapicloud.com
|
||||
title: FastAPI Cloud. By the same team behind FastAPI. You code. We Cloud.
|
||||
img: https://fastapi.tiangolo.com/img/sponsors/fastapicloud.png
|
||||
gold:
|
||||
- url: https://blockbee.io?ref=fastapi
|
||||
title: BlockBee Cryptocurrency Payment Gateway
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ mattwang44:
|
|||
url: https://github.com/mattwang44
|
||||
tiangolo:
|
||||
login: tiangolo
|
||||
count: 55
|
||||
count: 56
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4
|
||||
url: https://github.com/tiangolo
|
||||
Laineyzhang55:
|
||||
|
|
@ -136,7 +136,7 @@ JavierSanchezCastro:
|
|||
alejsdev:
|
||||
login: alejsdev
|
||||
count: 37
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=447d12a1b347f466b35378bee4c7104cc9b2c571&v=4
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=85ceac49fb87138aebe8d663912e359447329090&v=4
|
||||
url: https://github.com/alejsdev
|
||||
stlucasgarcia:
|
||||
login: stlucasgarcia
|
||||
|
|
@ -436,7 +436,7 @@ jburckel:
|
|||
peidrao:
|
||||
login: peidrao
|
||||
count: 13
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/32584628?u=64c634bb10381905038ff7faf3c8c3df47fb799a&v=4
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/32584628?u=979c62398e16ff000cc0faa028e028efd679887c&v=4
|
||||
url: https://github.com/peidrao
|
||||
impocode:
|
||||
login: impocode
|
||||
|
|
@ -1006,7 +1006,7 @@ takacs:
|
|||
anton2yakovlev:
|
||||
login: anton2yakovlev
|
||||
count: 5
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/44229180?u=bdd445ba99074b378e7298d23c4bf6d707d2c282&v=4
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/44229180?u=ac245e57bc834ff80f08ca8128000bb650a77a3d&v=4
|
||||
url: https://github.com/anton2yakovlev
|
||||
ILoveSorasakiHina:
|
||||
login: ILoveSorasakiHina
|
||||
|
|
@ -1161,7 +1161,7 @@ cookie-byte217:
|
|||
AbolfazlKameli:
|
||||
login: AbolfazlKameli
|
||||
count: 4
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/120686133?u=e41743da3c1820efafc59c5870cacd4f4425334c&v=4
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/120686133?u=af8f025278cce0d489007071254e4055df60b78c&v=4
|
||||
url: https://github.com/AbolfazlKameli
|
||||
tyronedamasceno:
|
||||
login: tyronedamasceno
|
||||
|
|
@ -1196,7 +1196,7 @@ Xaraxx:
|
|||
Suyoung789:
|
||||
login: Suyoung789
|
||||
count: 3
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/31277231?u=744bd3e641413e19bfad6b06a90bb0887c3f9332&v=4
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/31277231?u=1591aaf651eb860017231a36590050e154c026b6&v=4
|
||||
url: https://github.com/Suyoung789
|
||||
akagaeng:
|
||||
login: akagaeng
|
||||
|
|
@ -1806,7 +1806,7 @@ MrL8199:
|
|||
ivintoiu:
|
||||
login: ivintoiu
|
||||
count: 2
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/1853336?u=b537c905ad08b69993de8796fb235c8d4d47f039&v=4
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/1853336?u=e3de5fd0ab17efc12256b4295285b504ca281440&v=4
|
||||
url: https://github.com/ivintoiu
|
||||
TechnoService2:
|
||||
login: TechnoService2
|
||||
|
|
@ -1841,7 +1841,7 @@ NavesSapnis:
|
|||
eqsdxr:
|
||||
login: eqsdxr
|
||||
count: 2
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/157279130?u=d7aaffb29f542b647cf0f6b0e05722490863658a&v=4
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/157279130?u=7927dc0366995334f9a18c3204a41d3a34d6d96f&v=4
|
||||
url: https://github.com/eqsdxr
|
||||
syedasamina56:
|
||||
login: syedasamina56
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
nilslindemann:
|
||||
login: nilslindemann
|
||||
count: 124
|
||||
count: 125
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4
|
||||
url: https://github.com/nilslindemann
|
||||
jaystone776:
|
||||
|
|
@ -8,16 +8,16 @@ jaystone776:
|
|||
count: 46
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/11191137?u=299205a95e9b6817a43144a48b643346a5aac5cc&v=4
|
||||
url: https://github.com/jaystone776
|
||||
ceb10n:
|
||||
login: ceb10n
|
||||
count: 29
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4
|
||||
url: https://github.com/ceb10n
|
||||
valentinDruzhinin:
|
||||
login: valentinDruzhinin
|
||||
count: 29
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/12831905?u=aae1ebc675c91e8fa582df4fcc4fc4128106344d&v=4
|
||||
url: https://github.com/valentinDruzhinin
|
||||
ceb10n:
|
||||
login: ceb10n
|
||||
count: 27
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4
|
||||
url: https://github.com/ceb10n
|
||||
tokusumi:
|
||||
login: tokusumi
|
||||
count: 23
|
||||
|
|
@ -286,7 +286,7 @@ hsuanchi:
|
|||
alejsdev:
|
||||
login: alejsdev
|
||||
count: 3
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=447d12a1b347f466b35378bee4c7104cc9b2c571&v=4
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=85ceac49fb87138aebe8d663912e359447329090&v=4
|
||||
url: https://github.com/alejsdev
|
||||
riroan:
|
||||
login: riroan
|
||||
|
|
@ -358,6 +358,11 @@ ruzia:
|
|||
count: 3
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/24503?v=4
|
||||
url: https://github.com/ruzia
|
||||
YuriiMotov:
|
||||
login: YuriiMotov
|
||||
count: 3
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4
|
||||
url: https://github.com/YuriiMotov
|
||||
izaguerreiro:
|
||||
login: izaguerreiro
|
||||
count: 2
|
||||
|
|
@ -543,8 +548,3 @@ EdmilsonRodrigues:
|
|||
count: 2
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/62777025?u=217d6f3cd6cc750bb8818a3af7726c8d74eb7c2d&v=4
|
||||
url: https://github.com/EdmilsonRodrigues
|
||||
YuriiMotov:
|
||||
login: YuriiMotov
|
||||
count: 2
|
||||
avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4
|
||||
url: https://github.com/YuriiMotov
|
||||
|
|
|
|||
|
|
@ -1,3 +1,18 @@
|
|||
/* Fira Code, including characters used by Rich output, like the "heavy right-pointing angle bracket ornament", not included in Google Fonts */
|
||||
@import url(https://cdn.jsdelivr.net/npm/firacode@6.2.0/distr/fira_code.css);
|
||||
/* Noto Color Emoji for emoji support with the same font everywhere */
|
||||
@import url(https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&display=swap);
|
||||
|
||||
/* Override default code font in Material for MkDocs to Fira Code */
|
||||
:root {
|
||||
--md-code-font: "Fira Code", monospace, "Noto Color Emoji";
|
||||
}
|
||||
|
||||
/* Override default regular font in Material for MkDocs to include Noto Color Emoji */
|
||||
:root {
|
||||
--md-text-font: "Roboto", "Noto Color Emoji";
|
||||
}
|
||||
|
||||
.termynal-comment {
|
||||
color: #4a968f;
|
||||
font-style: italic;
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
/* font-size: 18px; */
|
||||
font-size: 15px;
|
||||
/* font-family: 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; */
|
||||
font-family: 'Roboto Mono', 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace;
|
||||
font-family: var(--md-code-font-family), 'Roboto Mono', 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace;
|
||||
border-radius: 4px;
|
||||
padding: 75px 45px 35px;
|
||||
position: relative;
|
||||
|
|
|
|||
|
|
@ -4,13 +4,21 @@ You can use virtually **any cloud provider** to deploy your FastAPI application.
|
|||
|
||||
In most of the cases, the main cloud providers have guides to deploy FastAPI with them.
|
||||
|
||||
## FastAPI Cloud { #fastapi-cloud }
|
||||
|
||||
**<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>** is built by the same author and team behind **FastAPI**.
|
||||
|
||||
It streamlines the process of **building**, **deploying**, and **accessing** an API with minimal effort.
|
||||
|
||||
It brings the same **developer experience** of building apps with FastAPI to **deploying** them to the cloud. 🎉
|
||||
|
||||
FastAPI Cloud is the primary sponsor and funding provider for the *FastAPI and friends* open source projects. ✨
|
||||
|
||||
## Cloud Providers - Sponsors { #cloud-providers-sponsors }
|
||||
|
||||
Some cloud providers ✨ [**sponsor FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨, this ensures the continued and healthy **development** of FastAPI and its **ecosystem**.
|
||||
Some other cloud providers ✨ [**sponsor FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨ too. 🙇
|
||||
|
||||
And it shows their true commitment to FastAPI and its **community** (you), as they not only want to provide you a **good service** but also want to make sure you have a **good and healthy framework**, FastAPI. 🙇
|
||||
|
||||
You might want to try their services and follow their guides:
|
||||
You might also want to consider them to follow their guides and try their services:
|
||||
|
||||
* <a href="https://docs.render.com/deploy-fastapi?utm_source=deploydoc&utm_medium=referral&utm_campaign=fastapi" class="external-link" target="_blank">Render</a>
|
||||
* <a href="https://docs.railway.com/guides/fastapi?utm_medium=integration&utm_source=docs&utm_campaign=fastapi" class="external-link" target="_blank">Railway</a>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
# FastAPI Cloud { #fastapi-cloud }
|
||||
|
||||
You can deploy your FastAPI app to <a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a> with **one command**, go and join the waiting list if you haven't. 🚀
|
||||
|
||||
## Login { #login }
|
||||
|
||||
Make sure you already have a **FastAPI Cloud** account (we invited you from the waiting list 😉).
|
||||
|
||||
Then log in:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ fastapi login
|
||||
|
||||
You are logged in to FastAPI Cloud 🚀
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## Deploy { #deploy }
|
||||
|
||||
Now deploy your app, with **one command**:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ fastapi deploy
|
||||
|
||||
Deploying to FastAPI Cloud...
|
||||
|
||||
✅ Deployment successful!
|
||||
|
||||
🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
That's it! Now you can access your app at that URL. ✨
|
||||
|
||||
## About FastAPI Cloud { #about-fastapi-cloud }
|
||||
|
||||
**<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>** is built by the same author and team behind **FastAPI**.
|
||||
|
||||
It streamlines the process of **building**, **deploying**, and **accessing** an API with minimal effort.
|
||||
|
||||
It brings the same **developer experience** of building apps with FastAPI to **deploying** them to the cloud. 🎉
|
||||
|
||||
It will also take care of most of the things you would need when deploying an app, like:
|
||||
|
||||
* HTTPS
|
||||
* Replication, with autoscaling based on requests
|
||||
* etc.
|
||||
|
||||
FastAPI Cloud is the primary sponsor and funding provider for the *FastAPI and friends* open source projects. ✨
|
||||
|
||||
## Deploy to other cloud providers { #deploy-to-other-cloud-providers }
|
||||
|
||||
FastAPI is open source and based on standards. You can deploy FastAPI apps to any cloud provider you choose.
|
||||
|
||||
Follow your cloud provider's guides to deploy FastAPI apps with them. 🤓
|
||||
|
||||
## Deploy your own server { #deploy-your-own-server }
|
||||
|
||||
I will also teach you later in this **Deployment** guide all the details, so you can understand what is going on, what needs to happen, or how to deploy FastAPI apps on your own, also with your own servers. 🤓
|
||||
|
|
@ -16,6 +16,8 @@ There are several ways to do it depending on your specific use case and the tool
|
|||
|
||||
You could **deploy a server** yourself using a combination of tools, you could use a **cloud service** that does part of the work for you, or other possible options.
|
||||
|
||||
For example, we, the team behind FastAPI, built <a href="https://fastapicloud.com" class="external-link" target="_blank">**FastAPI Cloud**</a>, to make deploying FastAPI apps to the cloud as streamlined as possible, with the same developer experience of working with FastAPI.
|
||||
|
||||
I will show you some of the main concepts you should probably keep in mind when deploying a **FastAPI** application (although most of it applies to any other type of web application).
|
||||
|
||||
You will see more details to keep in mind and some of the techniques to do it in the next sections. ✨
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
# Use Old 403 Authentication Error Status Codes { #use-old-403-authentication-error-status-codes }
|
||||
|
||||
Before FastAPI version `0.122.0`, when the integrated security utilities returned an error to the client after a failed authentication, they used the HTTP status code `403 Forbidden`.
|
||||
|
||||
Starting with FastAPI version `0.122.0`, they use the more appropriate HTTP status code `401 Unauthorized`, and return a sensible `WWW-Authenticate` header in the response, following the HTTP specifications, <a href="https://datatracker.ietf.org/doc/html/rfc7235#section-3.1" class="external-link" target="_blank">RFC 7235</a>, <a href="https://datatracker.ietf.org/doc/html/rfc9110#name-401-unauthorized" class="external-link" target="_blank">RFC 9110</a>.
|
||||
|
||||
But if for some reason your clients depend on the old behavior, you can revert to it by overriding the method `make_not_authenticated_error` in your security classes.
|
||||
|
||||
For example, you can create a subclass of `HTTPBearer` that returns a `403 Forbidden` error instead of the default `401 Unauthorized` error:
|
||||
|
||||
{* ../../docs_src/authentication_error_status_code/tutorial001_an_py39.py hl[9:13] *}
|
||||
|
||||
/// tip
|
||||
|
||||
Notice that the function returns the exception instance, it doesn't raise it. The raising is done in the rest of the internal code.
|
||||
|
||||
///
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
|
|
@ -52,14 +52,20 @@ The key features are:
|
|||
|
||||
<!-- sponsors -->
|
||||
|
||||
{% if sponsors %}
|
||||
### Keystone Sponsor
|
||||
|
||||
{% for sponsor in sponsors.keystone -%}
|
||||
<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a>
|
||||
{% endfor -%}
|
||||
|
||||
### Gold and Silver Sponsors
|
||||
|
||||
{% for sponsor in sponsors.gold -%}
|
||||
<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a>
|
||||
{% endfor -%}
|
||||
{%- for sponsor in sponsors.silver -%}
|
||||
<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<!-- /sponsors -->
|
||||
|
||||
|
|
@ -444,6 +450,58 @@ For a more complete example including more features, see the <a href="https://fa
|
|||
* **Cookie Sessions**
|
||||
* ...and more.
|
||||
|
||||
### Deploy your app (optional) { #deploy-your-app-optional }
|
||||
|
||||
You can optionally deploy your FastAPI app to <a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>, go and join the waiting list if you haven't. 🚀
|
||||
|
||||
If you already have a **FastAPI Cloud** account (we invited you from the waiting list 😉), you can deploy your application with one command.
|
||||
|
||||
Before deploying, make sure you are logged in:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ fastapi login
|
||||
|
||||
You are logged in to FastAPI Cloud 🚀
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
Then deploy your app:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ fastapi deploy
|
||||
|
||||
Deploying to FastAPI Cloud...
|
||||
|
||||
✅ Deployment successful!
|
||||
|
||||
🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
That's it! Now you can access your app at that URL. ✨
|
||||
|
||||
#### About FastAPI Cloud { #about-fastapi-cloud }
|
||||
|
||||
**<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>** is built by the same author and team behind **FastAPI**.
|
||||
|
||||
It streamlines the process of **building**, **deploying**, and **accessing** an API with minimal effort.
|
||||
|
||||
It brings the same **developer experience** of building apps with FastAPI to **deploying** them to the cloud. 🎉
|
||||
|
||||
FastAPI Cloud is the primary sponsor and funding provider for the *FastAPI and friends* open source projects. ✨
|
||||
|
||||
#### Deploy to other cloud providers { #deploy-to-other-cloud-providers }
|
||||
|
||||
FastAPI is open source and based on standards. You can deploy FastAPI apps to any cloud provider you choose.
|
||||
|
||||
Follow your cloud provider's guides to deploy FastAPI apps with them. 🤓
|
||||
|
||||
## Performance { #performance }
|
||||
|
||||
Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">one of the fastest Python frameworks available</a>, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,66 @@ hide:
|
|||
|
||||
## Latest Changes
|
||||
|
||||
### Internal
|
||||
|
||||
* ⬆ Bump markdown-include-variants from 0.0.6 to 0.0.7. PR [#14423](https://github.com/fastapi/fastapi/pull/14423) by [@YuriiMotov](https://github.com/YuriiMotov).
|
||||
* 👥 Update FastAPI People - Sponsors. PR [#14422](https://github.com/fastapi/fastapi/pull/14422) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 👥 Update FastAPI People - Contributors and Translators. PR [#14420](https://github.com/fastapi/fastapi/pull/14420) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
## 0.123.0
|
||||
|
||||
### Fixes
|
||||
|
||||
* 🐛 Cache dependencies that don't use scopes and don't have sub-dependencies with scopes. PR [#14419](https://github.com/fastapi/fastapi/pull/14419) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
## 0.122.1
|
||||
|
||||
### Fixes
|
||||
|
||||
* 🐛 Fix hierarchical security scope propagation. PR [#5624](https://github.com/fastapi/fastapi/pull/5624) by [@kristjanvalur](https://github.com/kristjanvalur).
|
||||
|
||||
### Docs
|
||||
|
||||
* 💅 Update CSS to explicitly use emoji font. PR [#14415](https://github.com/fastapi/fastapi/pull/14415) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
### Internal
|
||||
|
||||
* ⬆ Bump markdown-include-variants from 0.0.5 to 0.0.6. PR [#14418](https://github.com/fastapi/fastapi/pull/14418) by [@YuriiMotov](https://github.com/YuriiMotov).
|
||||
|
||||
## 0.122.0
|
||||
|
||||
### Fixes
|
||||
|
||||
* 🐛 Use `401` status code in security classes when credentials are missing. PR [#13786](https://github.com/fastapi/fastapi/pull/13786) by [@YuriiMotov](https://github.com/YuriiMotov).
|
||||
* If your code depended on these classes raising the old (less correct) `403` status code, check the new docs about how to override the classes, to use the same old behavior: [Use Old 403 Authentication Error Status Codes](https://fastapi.tiangolo.com/how-to/authentication-error-status-code/).
|
||||
|
||||
### Internal
|
||||
|
||||
* 🔧 Configure labeler to exclude files that start from underscore for `lang-all` label. PR [#14213](https://github.com/fastapi/fastapi/pull/14213) by [@YuriiMotov](https://github.com/YuriiMotov).
|
||||
* 👷 Add pre-commit config with local script for permalinks. PR [#14398](https://github.com/fastapi/fastapi/pull/14398) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 💄 Use font Fira Code to fix display of Rich panels in docs in Windows. PR [#14387](https://github.com/fastapi/fastapi/pull/14387) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 👷 Add custom pre-commit CI. PR [#14397](https://github.com/fastapi/fastapi/pull/14397) by [@tiangolo](https://github.com/tiangolo).
|
||||
* ⬆ Bump actions/checkout from 5 to 6. PR [#14381](https://github.com/fastapi/fastapi/pull/14381) by [@dependabot[bot]](https://github.com/apps/dependabot).
|
||||
* 👷 Upgrade `latest-changes` GitHub Action and pin `actions/checkout@v5`. PR [#14403](https://github.com/fastapi/fastapi/pull/14403) by [@svlandeg](https://github.com/svlandeg).
|
||||
* 🛠️ Add `add-permalinks` and `add-permalinks-page` to `scripts/docs.py`. PR [#14033](https://github.com/fastapi/fastapi/pull/14033) by [@YuriiMotov](https://github.com/YuriiMotov).
|
||||
* 🔧 Upgrade Material for MkDocs and remove insiders. PR [#14375](https://github.com/fastapi/fastapi/pull/14375) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
## 0.121.3
|
||||
|
||||
### Refactors
|
||||
|
||||
* ♻️ Make the result of `Depends()` and `Security()` hashable, as a workaround for other tools interacting with these internal parts. PR [#14372](https://github.com/fastapi/fastapi/pull/14372) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
### Upgrades
|
||||
|
||||
* ⬆️ Bump Starlette to <`0.51.0`. PR [#14282](https://github.com/fastapi/fastapi/pull/14282) by [@musicinmybrain](https://github.com/musicinmybrain).
|
||||
|
||||
### Docs
|
||||
|
||||
* 📝 Add missing hash part. PR [#14369](https://github.com/fastapi/fastapi/pull/14369) by [@nilslindemann](https://github.com/nilslindemann).
|
||||
* 📝 Fix typos in code comments. PR [#14364](https://github.com/fastapi/fastapi/pull/14364) by [@Edge-Seven](https://github.com/Edge-Seven).
|
||||
* 📝 Add docs for using FastAPI Cloud. PR [#14359](https://github.com/fastapi/fastapi/pull/14359) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
## 0.121.2
|
||||
|
||||
### Fixes
|
||||
|
|
|
|||
|
|
@ -143,6 +143,42 @@ And there are dozens of alternatives, all based on OpenAPI. You could easily add
|
|||
|
||||
You could also use it to generate code automatically, for clients that communicate with your API. For example, frontend, mobile or IoT applications.
|
||||
|
||||
### Deploy your app (optional) { #deploy-your-app-optional }
|
||||
|
||||
You can optionally deploy your FastAPI app to <a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>, go and join the waiting list if you haven't. 🚀
|
||||
|
||||
If you already have a **FastAPI Cloud** account (we invited you from the waiting list 😉), you can deploy your application with one command.
|
||||
|
||||
Before deploying, make sure you are logged in:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ fastapi login
|
||||
|
||||
You are logged in to FastAPI Cloud 🚀
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
Then deploy your app:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ fastapi deploy
|
||||
|
||||
Deploying to FastAPI Cloud...
|
||||
|
||||
✅ Deployment successful!
|
||||
|
||||
🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
That's it! Now you can access your app at that URL. ✨
|
||||
|
||||
## Recap, step by step { #recap-step-by-step }
|
||||
|
||||
### Step 1: import `FastAPI` { #step-1-import-fastapi }
|
||||
|
|
@ -314,6 +350,26 @@ You can also return Pydantic models (you'll see more about that later).
|
|||
|
||||
There are many other objects and models that will be automatically converted to JSON (including ORMs, etc). Try using your favorite ones, it's highly probable that they are already supported.
|
||||
|
||||
### Step 6: Deploy it { #step-6-deploy-it }
|
||||
|
||||
Deploy your app to **<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>** with one command: `fastapi deploy`. 🎉
|
||||
|
||||
#### About FastAPI Cloud { #about-fastapi-cloud }
|
||||
|
||||
**<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>** is built by the same author and team behind **FastAPI**.
|
||||
|
||||
It streamlines the process of **building**, **deploying**, and **accessing** an API with minimal effort.
|
||||
|
||||
It brings the same **developer experience** of building apps with FastAPI to **deploying** them to the cloud. 🎉
|
||||
|
||||
FastAPI Cloud is the primary sponsor and funding provider for the *FastAPI and friends* open source projects. ✨
|
||||
|
||||
#### Deploy to other cloud providers { #deploy-to-other-cloud-providers }
|
||||
|
||||
FastAPI is open source and based on standards. You can deploy FastAPI apps to any cloud provider you choose.
|
||||
|
||||
Follow your cloud provider's guides to deploy FastAPI apps with them. 🤓
|
||||
|
||||
## Recap { #recap }
|
||||
|
||||
* Import `FastAPI`.
|
||||
|
|
@ -321,3 +377,4 @@ There are many other objects and models that will be automatically converted to
|
|||
* Write a **path operation decorator** using decorators like `@app.get("/")`.
|
||||
* Define a **path operation function**; for example, `def root(): ...`.
|
||||
* Run the development server using the command `fastapi dev`.
|
||||
* Optionally deploy your app with `fastapi deploy`.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
# Define this here and not in the main mkdocs.yml file because that one is auto
|
||||
# updated and written, and the script would remove the env var
|
||||
INHERIT: !ENV [INSIDERS_FILE, '../en/mkdocs.no-insiders.yml']
|
||||
markdown_extensions:
|
||||
pymdownx.highlight:
|
||||
linenums: !ENV [LINENUMS, false]
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
plugins:
|
||||
social:
|
||||
cards_layout_options:
|
||||
logo: ../en/docs/img/icon-white.svg
|
||||
typeset:
|
||||
markdown_extensions:
|
||||
material.extensions.preview:
|
||||
targets:
|
||||
include:
|
||||
- "*"
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
INHERIT: ../en/mkdocs.maybe-insiders.yml
|
||||
INHERIT: ../en/mkdocs.env.yml
|
||||
site_name: FastAPI
|
||||
site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production
|
||||
site_url: https://fastapi.tiangolo.com/
|
||||
|
|
@ -52,6 +52,10 @@ theme:
|
|||
repo_name: fastapi/fastapi
|
||||
repo_url: https://github.com/fastapi/fastapi
|
||||
plugins:
|
||||
social:
|
||||
cards_layout_options:
|
||||
logo: ../en/docs/img/icon-white.svg
|
||||
typeset:
|
||||
search: null
|
||||
macros:
|
||||
include_yaml:
|
||||
|
|
@ -192,6 +196,7 @@ nav:
|
|||
- Deployment:
|
||||
- deployment/index.md
|
||||
- deployment/versions.md
|
||||
- deployment/fastapicloud.md
|
||||
- deployment/https.md
|
||||
- deployment/manually.md
|
||||
- deployment/concepts.md
|
||||
|
|
@ -210,6 +215,7 @@ nav:
|
|||
- how-to/custom-docs-ui-assets.md
|
||||
- how-to/configure-swagger-ui.md
|
||||
- how-to/testing-database.md
|
||||
- how-to/authentication-error-status-code.md
|
||||
- Reference (Code API):
|
||||
- reference/index.md
|
||||
- reference/fastapi.md
|
||||
|
|
@ -252,6 +258,10 @@ nav:
|
|||
- management.md
|
||||
- release-notes.md
|
||||
markdown_extensions:
|
||||
material.extensions.preview:
|
||||
targets:
|
||||
include:
|
||||
- "*"
|
||||
abbr: null
|
||||
attr_list: null
|
||||
footnotes: null
|
||||
|
|
|
|||
|
|
@ -3,6 +3,13 @@
|
|||
{% block announce %}
|
||||
<div class="announce-wrapper">
|
||||
<div id="announce-left">
|
||||
<div class="item">
|
||||
<a class="announce-link" href="https://fastapicloud.com" target="_blank">
|
||||
<span class="twemoji">
|
||||
{% include ".icons/material/cloud-arrow-up.svg" %}
|
||||
</span> Join the <strong>FastAPI Cloud</strong> waiting list 🚀
|
||||
</a>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a class="announce-link" href="https://x.com/fastapi" target="_blank">
|
||||
<span class="twemoji">
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
@ -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}
|
||||
|
|
@ -60,7 +60,7 @@ async def get_current_user(token: str = Depends(oauth2_scheme)):
|
|||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid authentication credentials",
|
||||
detail="Not authenticated",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
return user
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
|
|||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid authentication credentials",
|
||||
detail="Not authenticated",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
return user
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
|
|||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid authentication credentials",
|
||||
detail="Not authenticated",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
return user
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
|
|||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid authentication credentials",
|
||||
detail="Not authenticated",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
return user
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ async def get_current_user(token: str = Depends(oauth2_scheme)):
|
|||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid authentication credentials",
|
||||
detail="Not authenticated",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
return user
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.121.2"
|
||||
__version__ = "0.123.0"
|
||||
|
||||
from starlette import status as status
|
||||
|
||||
|
|
|
|||
|
|
@ -38,19 +38,43 @@ class Dependant:
|
|||
response_param_name: Optional[str] = None
|
||||
background_tasks_param_name: Optional[str] = None
|
||||
security_scopes_param_name: Optional[str] = None
|
||||
security_scopes: Optional[List[str]] = None
|
||||
own_oauth_scopes: Optional[List[str]] = None
|
||||
parent_oauth_scopes: Optional[List[str]] = None
|
||||
use_cache: bool = True
|
||||
path: Optional[str] = None
|
||||
scope: Union[Literal["function", "request"], None] = None
|
||||
|
||||
@cached_property
|
||||
def oauth_scopes(self) -> List[str]:
|
||||
scopes = self.parent_oauth_scopes.copy() if self.parent_oauth_scopes else []
|
||||
# This doesn't use a set to preserve order, just in case
|
||||
for scope in self.own_oauth_scopes or []:
|
||||
if scope not in scopes:
|
||||
scopes.append(scope)
|
||||
return scopes
|
||||
|
||||
@cached_property
|
||||
def cache_key(self) -> DependencyCacheKey:
|
||||
scopes_for_cache = (
|
||||
tuple(sorted(set(self.oauth_scopes or []))) if self._uses_scopes else ()
|
||||
)
|
||||
return (
|
||||
self.call,
|
||||
tuple(sorted(set(self.security_scopes or []))),
|
||||
scopes_for_cache,
|
||||
self.computed_scope or "",
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def _uses_scopes(self) -> bool:
|
||||
if self.own_oauth_scopes:
|
||||
return True
|
||||
if self.security_scopes_param_name is not None:
|
||||
return True
|
||||
for sub_dep in self.dependencies:
|
||||
if sub_dep._uses_scopes:
|
||||
return True
|
||||
return False
|
||||
|
||||
@cached_property
|
||||
def is_gen_callable(self) -> bool:
|
||||
if inspect.isgeneratorfunction(self.call):
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import dataclasses
|
||||
import inspect
|
||||
from contextlib import AsyncExitStack, contextmanager
|
||||
from copy import copy, deepcopy
|
||||
|
|
@ -57,8 +58,7 @@ from fastapi.dependencies.models import Dependant, SecurityRequirement
|
|||
from fastapi.exceptions import DependencyScopeError
|
||||
from fastapi.logger import logger
|
||||
from fastapi.security.base import SecurityBase
|
||||
from fastapi.security.oauth2 import OAuth2, SecurityScopes
|
||||
from fastapi.security.open_id_connect_url import OpenIdConnect
|
||||
from fastapi.security.oauth2 import SecurityScopes
|
||||
from fastapi.types import DependencyCacheKey
|
||||
from fastapi.utils import create_model_field, get_path_param_names
|
||||
from pydantic import BaseModel
|
||||
|
|
@ -125,14 +125,14 @@ def get_parameterless_sub_dependant(*, depends: params.Depends, path: str) -> De
|
|||
assert callable(depends.dependency), (
|
||||
"A parameter-less dependency must have a callable dependency"
|
||||
)
|
||||
use_security_scopes: List[str] = []
|
||||
own_oauth_scopes: List[str] = []
|
||||
if isinstance(depends, params.Security) and depends.oauth_scopes:
|
||||
use_security_scopes.extend(depends.oauth_scopes)
|
||||
own_oauth_scopes.extend(depends.scopes)
|
||||
return get_dependant(
|
||||
path=path,
|
||||
call=depends.dependency,
|
||||
scope=depends.scope,
|
||||
security_scopes=use_security_scopes,
|
||||
own_oauth_scopes=own_oauth_scopes,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -231,7 +231,8 @@ def get_dependant(
|
|||
path: str,
|
||||
call: Callable[..., Any],
|
||||
name: Optional[str] = None,
|
||||
security_scopes: Optional[List[str]] = None,
|
||||
own_oauth_scopes: Optional[List[str]] = None,
|
||||
parent_oauth_scopes: Optional[List[str]] = None,
|
||||
use_cache: bool = True,
|
||||
scope: Union[Literal["function", "request"], None] = None,
|
||||
) -> Dependant:
|
||||
|
|
@ -239,19 +240,18 @@ def get_dependant(
|
|||
call=call,
|
||||
name=name,
|
||||
path=path,
|
||||
security_scopes=security_scopes,
|
||||
use_cache=use_cache,
|
||||
scope=scope,
|
||||
own_oauth_scopes=own_oauth_scopes,
|
||||
parent_oauth_scopes=parent_oauth_scopes,
|
||||
)
|
||||
current_scopes = (parent_oauth_scopes or []) + (own_oauth_scopes or [])
|
||||
path_param_names = get_path_param_names(path)
|
||||
endpoint_signature = get_typed_signature(call)
|
||||
signature_params = endpoint_signature.parameters
|
||||
if isinstance(call, SecurityBase):
|
||||
use_scopes: List[str] = []
|
||||
if isinstance(call, (OAuth2, OpenIdConnect)):
|
||||
use_scopes = security_scopes or use_scopes
|
||||
security_requirement = SecurityRequirement(
|
||||
security_scheme=call, scopes=use_scopes
|
||||
security_scheme=call, scopes=current_scopes
|
||||
)
|
||||
dependant.security_requirements.append(security_requirement)
|
||||
for param_name, param in signature_params.items():
|
||||
|
|
@ -274,15 +274,16 @@ def get_dependant(
|
|||
f'The dependency "{dependant.call.__name__}" has a scope of '
|
||||
'"request", it cannot depend on dependencies with scope "function".'
|
||||
)
|
||||
use_security_scopes = security_scopes or []
|
||||
sub_own_oauth_scopes: List[str] = []
|
||||
if isinstance(param_details.depends, params.Security):
|
||||
if param_details.depends.oauth_scopes:
|
||||
use_security_scopes.extend(param_details.depends.oauth_scopes)
|
||||
sub_own_oauth_scopes = list(param_details.depends.oauth_scopes)
|
||||
sub_dependant = get_dependant(
|
||||
path=path,
|
||||
call=param_details.depends.dependency,
|
||||
name=param_name,
|
||||
security_scopes=use_security_scopes,
|
||||
own_oauth_scopes=sub_own_oauth_scopes,
|
||||
parent_oauth_scopes=current_scopes,
|
||||
use_cache=param_details.depends.use_cache,
|
||||
scope=param_details.depends.scope,
|
||||
)
|
||||
|
|
@ -428,7 +429,7 @@ def analyze_param(
|
|||
if depends is not None and depends.dependency is None:
|
||||
# Copy `depends` before mutating it
|
||||
depends = copy(depends)
|
||||
depends.dependency = type_annotation
|
||||
depends = dataclasses.replace(depends, dependency=type_annotation)
|
||||
|
||||
# Handle non-param type annotations like Request
|
||||
if lenient_issubclass(
|
||||
|
|
@ -608,7 +609,7 @@ async def solve_dependencies(
|
|||
path=use_path,
|
||||
call=call,
|
||||
name=sub_dependant.name,
|
||||
security_scopes=sub_dependant.security_scopes,
|
||||
parent_oauth_scopes=sub_dependant.oauth_scopes,
|
||||
scope=sub_dependant.scope,
|
||||
)
|
||||
|
||||
|
|
@ -690,7 +691,7 @@ async def solve_dependencies(
|
|||
values[dependant.response_param_name] = response
|
||||
if dependant.security_scopes_param_name:
|
||||
values[dependant.security_scopes_param_name] = SecurityScopes(
|
||||
scopes=dependant.security_scopes
|
||||
scopes=dependant.oauth_scopes
|
||||
)
|
||||
return SolvedDependency(
|
||||
values=values,
|
||||
|
|
|
|||
|
|
@ -772,14 +772,14 @@ class File(Form): # type: ignore[misc]
|
|||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(frozen=True)
|
||||
class Depends:
|
||||
dependency: Optional[Callable[..., Any]] = None
|
||||
use_cache: bool = True
|
||||
scope: Union[Literal["function", "request"], None] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(frozen=True)
|
||||
class Security(Depends):
|
||||
oauth_scopes: Optional[
|
||||
Union[
|
||||
|
|
|
|||
|
|
@ -1,22 +1,52 @@
|
|||
from typing import Optional
|
||||
from typing import Optional, Union
|
||||
|
||||
from annotated_doc import Doc
|
||||
from fastapi.openapi.models import APIKey, APIKeyIn
|
||||
from fastapi.security.base import SecurityBase
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.requests import Request
|
||||
from starlette.status import HTTP_403_FORBIDDEN
|
||||
from starlette.status import HTTP_401_UNAUTHORIZED
|
||||
from typing_extensions import Annotated
|
||||
|
||||
|
||||
class APIKeyBase(SecurityBase):
|
||||
@staticmethod
|
||||
def check_api_key(api_key: Optional[str], auto_error: bool) -> Optional[str]:
|
||||
def __init__(
|
||||
self,
|
||||
location: APIKeyIn,
|
||||
name: str,
|
||||
description: Union[str, None],
|
||||
scheme_name: Union[str, None],
|
||||
auto_error: bool,
|
||||
):
|
||||
self.auto_error = auto_error
|
||||
|
||||
self.model: APIKey = APIKey(
|
||||
**{"in": location},
|
||||
name=name,
|
||||
description=description,
|
||||
)
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
|
||||
def make_not_authenticated_error(self) -> HTTPException:
|
||||
"""
|
||||
The WWW-Authenticate header is not standardized for API Key authentication but
|
||||
the HTTP specification requires that an error of 401 "Unauthorized" must
|
||||
include a WWW-Authenticate header.
|
||||
|
||||
Ref: https://datatracker.ietf.org/doc/html/rfc9110#name-401-unauthorized
|
||||
|
||||
For this, this method sends a custom challenge `APIKey`.
|
||||
"""
|
||||
return HTTPException(
|
||||
status_code=HTTP_401_UNAUTHORIZED,
|
||||
detail="Not authenticated",
|
||||
headers={"WWW-Authenticate": "APIKey"},
|
||||
)
|
||||
|
||||
def check_api_key(self, api_key: Optional[str]) -> Optional[str]:
|
||||
if not api_key:
|
||||
if auto_error:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
if self.auto_error:
|
||||
raise self.make_not_authenticated_error()
|
||||
return None
|
||||
return api_key
|
||||
|
||||
|
|
@ -100,17 +130,17 @@ class APIKeyQuery(APIKeyBase):
|
|||
),
|
||||
] = True,
|
||||
):
|
||||
self.model: APIKey = APIKey(
|
||||
**{"in": APIKeyIn.query},
|
||||
super().__init__(
|
||||
location=APIKeyIn.query,
|
||||
name=name,
|
||||
scheme_name=scheme_name,
|
||||
description=description,
|
||||
auto_error=auto_error,
|
||||
)
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
self.auto_error = auto_error
|
||||
|
||||
async def __call__(self, request: Request) -> Optional[str]:
|
||||
api_key = request.query_params.get(self.model.name)
|
||||
return self.check_api_key(api_key, self.auto_error)
|
||||
return self.check_api_key(api_key)
|
||||
|
||||
|
||||
class APIKeyHeader(APIKeyBase):
|
||||
|
|
@ -188,17 +218,17 @@ class APIKeyHeader(APIKeyBase):
|
|||
),
|
||||
] = True,
|
||||
):
|
||||
self.model: APIKey = APIKey(
|
||||
**{"in": APIKeyIn.header},
|
||||
super().__init__(
|
||||
location=APIKeyIn.header,
|
||||
name=name,
|
||||
scheme_name=scheme_name,
|
||||
description=description,
|
||||
auto_error=auto_error,
|
||||
)
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
self.auto_error = auto_error
|
||||
|
||||
async def __call__(self, request: Request) -> Optional[str]:
|
||||
api_key = request.headers.get(self.model.name)
|
||||
return self.check_api_key(api_key, self.auto_error)
|
||||
return self.check_api_key(api_key)
|
||||
|
||||
|
||||
class APIKeyCookie(APIKeyBase):
|
||||
|
|
@ -276,14 +306,14 @@ class APIKeyCookie(APIKeyBase):
|
|||
),
|
||||
] = True,
|
||||
):
|
||||
self.model: APIKey = APIKey(
|
||||
**{"in": APIKeyIn.cookie},
|
||||
super().__init__(
|
||||
location=APIKeyIn.cookie,
|
||||
name=name,
|
||||
scheme_name=scheme_name,
|
||||
description=description,
|
||||
auto_error=auto_error,
|
||||
)
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
self.auto_error = auto_error
|
||||
|
||||
async def __call__(self, request: Request) -> Optional[str]:
|
||||
api_key = request.cookies.get(self.model.name)
|
||||
return self.check_api_key(api_key, self.auto_error)
|
||||
return self.check_api_key(api_key)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import binascii
|
||||
from base64 import b64decode
|
||||
from typing import Optional
|
||||
from typing import Dict, Optional
|
||||
|
||||
from annotated_doc import Doc
|
||||
from fastapi.exceptions import HTTPException
|
||||
|
|
@ -10,7 +10,7 @@ from fastapi.security.base import SecurityBase
|
|||
from fastapi.security.utils import get_authorization_scheme_param
|
||||
from pydantic import BaseModel
|
||||
from starlette.requests import Request
|
||||
from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN
|
||||
from starlette.status import HTTP_401_UNAUTHORIZED
|
||||
from typing_extensions import Annotated
|
||||
|
||||
|
||||
|
|
@ -76,10 +76,22 @@ class HTTPBase(SecurityBase):
|
|||
description: Optional[str] = None,
|
||||
auto_error: bool = True,
|
||||
):
|
||||
self.model = HTTPBaseModel(scheme=scheme, description=description)
|
||||
self.model: HTTPBaseModel = HTTPBaseModel(
|
||||
scheme=scheme, description=description
|
||||
)
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
self.auto_error = auto_error
|
||||
|
||||
def make_authenticate_headers(self) -> Dict[str, str]:
|
||||
return {"WWW-Authenticate": f"{self.model.scheme.title()}"}
|
||||
|
||||
def make_not_authenticated_error(self) -> HTTPException:
|
||||
return HTTPException(
|
||||
status_code=HTTP_401_UNAUTHORIZED,
|
||||
detail="Not authenticated",
|
||||
headers=self.make_authenticate_headers(),
|
||||
)
|
||||
|
||||
async def __call__(
|
||||
self, request: Request
|
||||
) -> Optional[HTTPAuthorizationCredentials]:
|
||||
|
|
@ -87,9 +99,7 @@ class HTTPBase(SecurityBase):
|
|||
scheme, credentials = get_authorization_scheme_param(authorization)
|
||||
if not (authorization and scheme and credentials):
|
||||
if self.auto_error:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
raise self.make_not_authenticated_error()
|
||||
else:
|
||||
return None
|
||||
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
|
||||
|
|
@ -99,6 +109,8 @@ class HTTPBasic(HTTPBase):
|
|||
"""
|
||||
HTTP Basic authentication.
|
||||
|
||||
Ref: https://datatracker.ietf.org/doc/html/rfc7617
|
||||
|
||||
## Usage
|
||||
|
||||
Create an instance object and use that object as the dependency in `Depends()`.
|
||||
|
|
@ -185,36 +197,28 @@ class HTTPBasic(HTTPBase):
|
|||
self.realm = realm
|
||||
self.auto_error = auto_error
|
||||
|
||||
def make_authenticate_headers(self) -> Dict[str, str]:
|
||||
if self.realm:
|
||||
return {"WWW-Authenticate": f'Basic realm="{self.realm}"'}
|
||||
return {"WWW-Authenticate": "Basic"}
|
||||
|
||||
async def __call__( # type: ignore
|
||||
self, request: Request
|
||||
) -> Optional[HTTPBasicCredentials]:
|
||||
authorization = request.headers.get("Authorization")
|
||||
scheme, param = get_authorization_scheme_param(authorization)
|
||||
if self.realm:
|
||||
unauthorized_headers = {"WWW-Authenticate": f'Basic realm="{self.realm}"'}
|
||||
else:
|
||||
unauthorized_headers = {"WWW-Authenticate": "Basic"}
|
||||
if not authorization or scheme.lower() != "basic":
|
||||
if self.auto_error:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_401_UNAUTHORIZED,
|
||||
detail="Not authenticated",
|
||||
headers=unauthorized_headers,
|
||||
)
|
||||
raise self.make_not_authenticated_error()
|
||||
else:
|
||||
return None
|
||||
invalid_user_credentials_exc = HTTPException(
|
||||
status_code=HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid authentication credentials",
|
||||
headers=unauthorized_headers,
|
||||
)
|
||||
try:
|
||||
data = b64decode(param).decode("ascii")
|
||||
except (ValueError, UnicodeDecodeError, binascii.Error):
|
||||
raise invalid_user_credentials_exc # noqa: B904
|
||||
except (ValueError, UnicodeDecodeError, binascii.Error) as e:
|
||||
raise self.make_not_authenticated_error() from e
|
||||
username, separator, password = data.partition(":")
|
||||
if not separator:
|
||||
raise invalid_user_credentials_exc
|
||||
raise self.make_not_authenticated_error()
|
||||
return HTTPBasicCredentials(username=username, password=password)
|
||||
|
||||
|
||||
|
|
@ -306,17 +310,12 @@ class HTTPBearer(HTTPBase):
|
|||
scheme, credentials = get_authorization_scheme_param(authorization)
|
||||
if not (authorization and scheme and credentials):
|
||||
if self.auto_error:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
raise self.make_not_authenticated_error()
|
||||
else:
|
||||
return None
|
||||
if scheme.lower() != "bearer":
|
||||
if self.auto_error:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN,
|
||||
detail="Invalid authentication credentials",
|
||||
)
|
||||
raise self.make_not_authenticated_error()
|
||||
else:
|
||||
return None
|
||||
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
|
||||
|
|
@ -326,6 +325,12 @@ class HTTPDigest(HTTPBase):
|
|||
"""
|
||||
HTTP Digest authentication.
|
||||
|
||||
**Warning**: this is only a stub to connect the components with OpenAPI in FastAPI,
|
||||
but it doesn't implement the full Digest scheme, you would need to to subclass it
|
||||
and implement it in your code.
|
||||
|
||||
Ref: https://datatracker.ietf.org/doc/html/rfc7616
|
||||
|
||||
## Usage
|
||||
|
||||
Create an instance object and use that object as the dependency in `Depends()`.
|
||||
|
|
@ -408,17 +413,12 @@ class HTTPDigest(HTTPBase):
|
|||
scheme, credentials = get_authorization_scheme_param(authorization)
|
||||
if not (authorization and scheme and credentials):
|
||||
if self.auto_error:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
raise self.make_not_authenticated_error()
|
||||
else:
|
||||
return None
|
||||
if scheme.lower() != "digest":
|
||||
if self.auto_error:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN,
|
||||
detail="Invalid authentication credentials",
|
||||
)
|
||||
raise self.make_not_authenticated_error()
|
||||
else:
|
||||
return None
|
||||
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from fastapi.param_functions import Form
|
|||
from fastapi.security.base import SecurityBase
|
||||
from fastapi.security.utils import get_authorization_scheme_param
|
||||
from starlette.requests import Request
|
||||
from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN
|
||||
from starlette.status import HTTP_401_UNAUTHORIZED
|
||||
|
||||
# TODO: import from typing when deprecating Python 3.9
|
||||
from typing_extensions import Annotated
|
||||
|
|
@ -377,13 +377,33 @@ class OAuth2(SecurityBase):
|
|||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
self.auto_error = auto_error
|
||||
|
||||
def make_not_authenticated_error(self) -> HTTPException:
|
||||
"""
|
||||
The OAuth 2 specification doesn't define the challenge that should be used,
|
||||
because a `Bearer` token is not really the only option to authenticate.
|
||||
|
||||
But declaring any other authentication challenge would be application-specific
|
||||
as it's not defined in the specification.
|
||||
|
||||
For practical reasons, this method uses the `Bearer` challenge by default, as
|
||||
it's probably the most common one.
|
||||
|
||||
If you are implementing an OAuth2 authentication scheme other than the provided
|
||||
ones in FastAPI (based on bearer tokens), you might want to override this.
|
||||
|
||||
Ref: https://datatracker.ietf.org/doc/html/rfc6749
|
||||
"""
|
||||
return HTTPException(
|
||||
status_code=HTTP_401_UNAUTHORIZED,
|
||||
detail="Not authenticated",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
async def __call__(self, request: Request) -> Optional[str]:
|
||||
authorization = request.headers.get("Authorization")
|
||||
if not authorization:
|
||||
if self.auto_error:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
raise self.make_not_authenticated_error()
|
||||
else:
|
||||
return None
|
||||
return authorization
|
||||
|
|
@ -491,11 +511,7 @@ class OAuth2PasswordBearer(OAuth2):
|
|||
scheme, param = get_authorization_scheme_param(authorization)
|
||||
if not authorization or scheme.lower() != "bearer":
|
||||
if self.auto_error:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_401_UNAUTHORIZED,
|
||||
detail="Not authenticated",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
raise self.make_not_authenticated_error()
|
||||
else:
|
||||
return None
|
||||
return param
|
||||
|
|
@ -601,11 +617,7 @@ class OAuth2AuthorizationCodeBearer(OAuth2):
|
|||
scheme, param = get_authorization_scheme_param(authorization)
|
||||
if not authorization or scheme.lower() != "bearer":
|
||||
if self.auto_error:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_401_UNAUTHORIZED,
|
||||
detail="Not authenticated",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
raise self.make_not_authenticated_error()
|
||||
else:
|
||||
return None # pragma: nocover
|
||||
return param
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from fastapi.openapi.models import OpenIdConnect as OpenIdConnectModel
|
|||
from fastapi.security.base import SecurityBase
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.requests import Request
|
||||
from starlette.status import HTTP_403_FORBIDDEN
|
||||
from starlette.status import HTTP_401_UNAUTHORIZED
|
||||
from typing_extensions import Annotated
|
||||
|
||||
|
||||
|
|
@ -13,6 +13,11 @@ class OpenIdConnect(SecurityBase):
|
|||
"""
|
||||
OpenID Connect authentication class. An instance of it would be used as a
|
||||
dependency.
|
||||
|
||||
**Warning**: this is only a stub to connect the components with OpenAPI in FastAPI,
|
||||
but it doesn't implement the full OpenIdConnect scheme, for example, it doesn't use
|
||||
the OpenIDConnect URL. You would need to to subclass it and implement it in your
|
||||
code.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
|
@ -73,13 +78,18 @@ class OpenIdConnect(SecurityBase):
|
|||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
self.auto_error = auto_error
|
||||
|
||||
def make_not_authenticated_error(self) -> HTTPException:
|
||||
return HTTPException(
|
||||
status_code=HTTP_401_UNAUTHORIZED,
|
||||
detail="Not authenticated",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
async def __call__(self, request: Request) -> Optional[str]:
|
||||
authorization = request.headers.get("Authorization")
|
||||
if not authorization:
|
||||
if self.auto_error:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
raise self.make_not_authenticated_error()
|
||||
else:
|
||||
return None
|
||||
return authorization
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ classifiers = [
|
|||
"Topic :: Internet :: WWW/HTTP",
|
||||
]
|
||||
dependencies = [
|
||||
"starlette>=0.40.0,<0.50.0",
|
||||
"starlette>=0.40.0,<0.51.0",
|
||||
"pydantic>=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0",
|
||||
"typing-extensions>=4.8.0",
|
||||
"annotated-doc>=0.0.2",
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
git+https://${TOKEN}@github.com/squidfunk/mkdocs-material-insiders.git@9.5.30-insiders-4.53.11
|
||||
git+https://${TOKEN}@github.com/pawamoy-insiders/griffe-typing-deprecated.git
|
||||
git+https://${TOKEN}@github.com/pawamoy-insiders/mkdocstrings-python.git
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
-e .
|
||||
-r requirements-docs-tests.txt
|
||||
mkdocs-material==9.6.16
|
||||
mkdocs-material==9.7.0
|
||||
mdx-include >=1.4.1,<2.0.0
|
||||
mkdocs-redirects>=1.2.1,<1.3.0
|
||||
typer == 0.16.0
|
||||
|
|
@ -13,7 +13,9 @@ pillow==11.3.0
|
|||
cairosvg==2.8.2
|
||||
mkdocstrings[python]==0.30.1
|
||||
griffe-typingdoc==0.3.0
|
||||
griffe-warnings-deprecated==1.1.0
|
||||
# For griffe, it formats with black
|
||||
black==25.1.0
|
||||
mkdocs-macros-plugin==1.4.1
|
||||
markdown-include-variants==0.0.5
|
||||
markdown-include-variants==0.0.7
|
||||
python-slugify==8.0.4
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
-e .[all]
|
||||
-r requirements-tests.txt
|
||||
-r requirements-docs.txt
|
||||
pre-commit >=2.17.0,<5.0.0
|
||||
pre-commit >=4.5.0,<5.0.0
|
||||
# For generating screenshots
|
||||
playwright
|
||||
|
|
|
|||
140
scripts/docs.py
140
scripts/docs.py
|
|
@ -4,9 +4,8 @@ import os
|
|||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from functools import lru_cache
|
||||
from html.parser import HTMLParser
|
||||
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
||||
from importlib import metadata
|
||||
from multiprocessing import Pool
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
|
@ -16,6 +15,7 @@ import typer
|
|||
import yaml
|
||||
from jinja2 import Template
|
||||
from ruff.__main__ import find_ruff_bin
|
||||
from slugify import slugify as py_slugify
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
|
|
@ -27,8 +27,8 @@ missing_translation_snippet = """
|
|||
{!../../docs/missing-translation.md!}
|
||||
"""
|
||||
|
||||
non_translated_sections = [
|
||||
"reference/",
|
||||
non_translated_sections = (
|
||||
f"reference{os.sep}",
|
||||
"release-notes.md",
|
||||
"fastapi-people.md",
|
||||
"external-links.md",
|
||||
|
|
@ -36,7 +36,7 @@ non_translated_sections = [
|
|||
"management-tasks.md",
|
||||
"management.md",
|
||||
"contributing.md",
|
||||
]
|
||||
)
|
||||
|
||||
docs_path = Path("docs")
|
||||
en_docs_path = Path("docs/en")
|
||||
|
|
@ -44,13 +44,39 @@ en_config_path: Path = en_docs_path / mkdocs_name
|
|||
site_path = Path("site").absolute()
|
||||
build_site_path = Path("site_build").absolute()
|
||||
|
||||
header_pattern = re.compile(r"^(#{1,6}) (.+?)(?:\s*\{\s*(#.*)\s*\})?\s*$")
|
||||
header_with_permalink_pattern = re.compile(r"^(#{1,6}) (.+?)(\s*\{\s*#.*\s*\})\s*$")
|
||||
code_block3_pattern = re.compile(r"^\s*```")
|
||||
code_block4_pattern = re.compile(r"^\s*````")
|
||||
|
||||
|
||||
@lru_cache
|
||||
def is_mkdocs_insiders() -> bool:
|
||||
version = metadata.version("mkdocs-material")
|
||||
return "insiders" in version
|
||||
class VisibleTextExtractor(HTMLParser):
|
||||
"""Extract visible text from a string with HTML tags."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.text_parts = []
|
||||
|
||||
def handle_data(self, data):
|
||||
self.text_parts.append(data)
|
||||
|
||||
def extract_visible_text(self, html: str) -> str:
|
||||
self.reset()
|
||||
self.text_parts = []
|
||||
self.feed(html)
|
||||
return "".join(self.text_parts).strip()
|
||||
|
||||
|
||||
def slugify(text: str) -> str:
|
||||
return py_slugify(
|
||||
text,
|
||||
replacements=[
|
||||
("`", ""), # `dict`s -> dicts
|
||||
("'s", "s"), # it's -> its
|
||||
("'t", "t"), # don't -> dont
|
||||
("**", ""), # **FastAPI**s -> FastAPIs
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def get_en_config() -> Dict[str, Any]:
|
||||
|
|
@ -77,9 +103,7 @@ def complete_existing_lang(incomplete: str):
|
|||
|
||||
@app.callback()
|
||||
def callback() -> None:
|
||||
if is_mkdocs_insiders():
|
||||
os.environ["INSIDERS_FILE"] = "../en/mkdocs.insiders.yml"
|
||||
# For MacOS with insiders and Cairo
|
||||
# For MacOS with Cairo
|
||||
os.environ["DYLD_FALLBACK_LIBRARY_PATH"] = "/opt/homebrew/lib"
|
||||
|
||||
|
||||
|
|
@ -115,10 +139,6 @@ def build_lang(
|
|||
"""
|
||||
Build the docs for a language.
|
||||
"""
|
||||
insiders_env_file = os.environ.get("INSIDERS_FILE")
|
||||
print(f"Insiders file {insiders_env_file}")
|
||||
if is_mkdocs_insiders():
|
||||
print("Using insiders")
|
||||
lang_path: Path = Path("docs") / lang
|
||||
if not lang_path.is_dir():
|
||||
typer.echo(f"The language translation doesn't seem to exist yet: {lang}")
|
||||
|
|
@ -145,14 +165,20 @@ def build_lang(
|
|||
|
||||
|
||||
index_sponsors_template = """
|
||||
{% if sponsors %}
|
||||
### Keystone Sponsor
|
||||
|
||||
{% for sponsor in sponsors.keystone -%}
|
||||
<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}"></a>
|
||||
{% endfor %}
|
||||
### Gold and Silver Sponsors
|
||||
|
||||
{% for sponsor in sponsors.gold -%}
|
||||
<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}"></a>
|
||||
{% endfor -%}
|
||||
{%- for sponsor in sponsors.silver -%}
|
||||
<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}"></a>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
"""
|
||||
|
||||
|
||||
|
|
@ -434,5 +460,83 @@ def generate_docs_src_versions_for_file(file_path: Path) -> None:
|
|||
version_file.write_text(content_format, encoding="utf-8")
|
||||
|
||||
|
||||
@app.command()
|
||||
def add_permalinks_page(path: Path, update_existing: bool = False):
|
||||
"""
|
||||
Add or update header permalinks in specific page of En docs.
|
||||
"""
|
||||
|
||||
if not path.is_relative_to(en_docs_path / "docs"):
|
||||
raise RuntimeError(f"Path must be inside {en_docs_path}")
|
||||
rel_path = path.relative_to(en_docs_path / "docs")
|
||||
|
||||
# Skip excluded sections
|
||||
if str(rel_path).startswith(non_translated_sections):
|
||||
return
|
||||
|
||||
visible_text_extractor = VisibleTextExtractor()
|
||||
updated_lines = []
|
||||
in_code_block3 = False
|
||||
in_code_block4 = False
|
||||
permalinks = set()
|
||||
|
||||
with path.open("r", encoding="utf-8") as f:
|
||||
lines = f.readlines()
|
||||
|
||||
for line in lines:
|
||||
# Handle codeblocks start and end
|
||||
if not (in_code_block3 or in_code_block4):
|
||||
if code_block4_pattern.match(line):
|
||||
in_code_block4 = True
|
||||
elif code_block3_pattern.match(line):
|
||||
in_code_block3 = True
|
||||
else:
|
||||
if in_code_block4 and code_block4_pattern.match(line):
|
||||
in_code_block4 = False
|
||||
elif in_code_block3 and code_block3_pattern.match(line):
|
||||
in_code_block3 = False
|
||||
|
||||
# Process Headers only outside codeblocks
|
||||
if not (in_code_block3 or in_code_block4):
|
||||
match = header_pattern.match(line)
|
||||
if match:
|
||||
hashes, title, _permalink = match.groups()
|
||||
if (not _permalink) or update_existing:
|
||||
slug = slugify(visible_text_extractor.extract_visible_text(title))
|
||||
if slug in permalinks:
|
||||
# If the slug is already used, append a number to make it unique
|
||||
count = 1
|
||||
original_slug = slug
|
||||
while slug in permalinks:
|
||||
slug = f"{original_slug}_{count}"
|
||||
count += 1
|
||||
permalinks.add(slug)
|
||||
|
||||
line = f"{hashes} {title} {{ #{slug} }}\n"
|
||||
|
||||
updated_lines.append(line)
|
||||
|
||||
with path.open("w", encoding="utf-8") as f:
|
||||
f.writelines(updated_lines)
|
||||
|
||||
|
||||
@app.command()
|
||||
def add_permalinks_pages(pages: List[Path], update_existing: bool = False) -> None:
|
||||
"""
|
||||
Add or update header permalinks in specific pages of En docs.
|
||||
"""
|
||||
for md_file in pages:
|
||||
add_permalinks_page(md_file, update_existing=update_existing)
|
||||
|
||||
|
||||
@app.command()
|
||||
def add_permalinks(update_existing: bool = False) -> None:
|
||||
"""
|
||||
Add or update header permalinks in all pages of En docs.
|
||||
"""
|
||||
for md_file in en_docs_path.rglob("*.md"):
|
||||
add_permalinks_page(md_file, update_existing=update_existing)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
# This is more or less a workaround to make Depends and Security hashable
|
||||
# as other tools that use them depend on that
|
||||
# Ref: https://github.com/fastapi/fastapi/pull/14320
|
||||
|
||||
from fastapi import Depends, Security
|
||||
|
||||
|
||||
def dep():
|
||||
pass
|
||||
|
||||
|
||||
def test_depends_hashable():
|
||||
dep() # just for coverage
|
||||
d1 = Depends(dep)
|
||||
d2 = Depends(dep)
|
||||
d3 = Depends(dep, scope="function")
|
||||
d4 = Depends(dep, scope="function")
|
||||
|
||||
s1 = Security(dep)
|
||||
s2 = Security(dep)
|
||||
|
||||
assert hash(d1) == hash(d2)
|
||||
assert hash(s1) == hash(s2)
|
||||
assert hash(d1) != hash(d3)
|
||||
assert hash(d3) == hash(d4)
|
||||
|
|
@ -32,8 +32,9 @@ def test_security_api_key():
|
|||
def test_security_api_key_no_key():
|
||||
client = TestClient(app)
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 403, response.text
|
||||
assert response.status_code == 401, response.text
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
assert response.headers["WWW-Authenticate"] == "APIKey"
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
|
|
|
|||
|
|
@ -32,8 +32,9 @@ def test_security_api_key():
|
|||
def test_security_api_key_no_key():
|
||||
client = TestClient(app)
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 403, response.text
|
||||
assert response.status_code == 401, response.text
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
assert response.headers["WWW-Authenticate"] == "APIKey"
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
|
|
|
|||
|
|
@ -33,8 +33,9 @@ def test_security_api_key():
|
|||
|
||||
def test_security_api_key_no_key():
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 403, response.text
|
||||
assert response.status_code == 401, response.text
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
assert response.headers["WWW-Authenticate"] == "APIKey"
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
|
|
|
|||
|
|
@ -33,8 +33,9 @@ def test_security_api_key():
|
|||
|
||||
def test_security_api_key_no_key():
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 403, response.text
|
||||
assert response.status_code == 401, response.text
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
assert response.headers["WWW-Authenticate"] == "APIKey"
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
|
|
|
|||
|
|
@ -33,8 +33,9 @@ def test_security_api_key():
|
|||
|
||||
def test_security_api_key_no_key():
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 403, response.text
|
||||
assert response.status_code == 401, response.text
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
assert response.headers["WWW-Authenticate"] == "APIKey"
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
|
|
|
|||
|
|
@ -33,8 +33,9 @@ def test_security_api_key():
|
|||
|
||||
def test_security_api_key_no_key():
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 403, response.text
|
||||
assert response.status_code == 401, response.text
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
assert response.headers["WWW-Authenticate"] == "APIKey"
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
|
|
|
|||
|
|
@ -23,8 +23,9 @@ def test_security_http_base():
|
|||
|
||||
def test_security_http_base_no_credentials():
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 403, response.text
|
||||
assert response.status_code == 401, response.text
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
assert response.headers["WWW-Authenticate"] == "Other"
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
|
|
|
|||
|
|
@ -23,8 +23,9 @@ def test_security_http_base():
|
|||
|
||||
def test_security_http_base_no_credentials():
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 403, response.text
|
||||
assert response.status_code == 401, response.text
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
assert response.headers["WWW-Authenticate"] == "Other"
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ def test_security_http_basic_invalid_credentials():
|
|||
)
|
||||
assert response.status_code == 401, response.text
|
||||
assert response.headers["WWW-Authenticate"] == "Basic"
|
||||
assert response.json() == {"detail": "Invalid authentication credentials"}
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
|
||||
|
||||
def test_security_http_basic_non_basic_credentials():
|
||||
|
|
@ -47,7 +47,7 @@ def test_security_http_basic_non_basic_credentials():
|
|||
response = client.get("/users/me", headers={"Authorization": auth_header})
|
||||
assert response.status_code == 401, response.text
|
||||
assert response.headers["WWW-Authenticate"] == "Basic"
|
||||
assert response.json() == {"detail": "Invalid authentication credentials"}
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ def test_security_http_basic_invalid_credentials():
|
|||
)
|
||||
assert response.status_code == 401, response.text
|
||||
assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"'
|
||||
assert response.json() == {"detail": "Invalid authentication credentials"}
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
|
||||
|
||||
def test_security_http_basic_non_basic_credentials():
|
||||
|
|
@ -45,7 +45,7 @@ def test_security_http_basic_non_basic_credentials():
|
|||
response = client.get("/users/me", headers={"Authorization": auth_header})
|
||||
assert response.status_code == 401, response.text
|
||||
assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"'
|
||||
assert response.json() == {"detail": "Invalid authentication credentials"}
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ def test_security_http_basic_invalid_credentials():
|
|||
)
|
||||
assert response.status_code == 401, response.text
|
||||
assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"'
|
||||
assert response.json() == {"detail": "Invalid authentication credentials"}
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
|
||||
|
||||
def test_security_http_basic_non_basic_credentials():
|
||||
|
|
@ -45,7 +45,7 @@ def test_security_http_basic_non_basic_credentials():
|
|||
response = client.get("/users/me", headers={"Authorization": auth_header})
|
||||
assert response.status_code == 401, response.text
|
||||
assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"'
|
||||
assert response.json() == {"detail": "Invalid authentication credentials"}
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
|
|
|
|||
|
|
@ -23,14 +23,16 @@ def test_security_http_bearer():
|
|||
|
||||
def test_security_http_bearer_no_credentials():
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 403, response.text
|
||||
assert response.status_code == 401, response.text
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
assert response.headers["WWW-Authenticate"] == "Bearer"
|
||||
|
||||
|
||||
def test_security_http_bearer_incorrect_scheme_credentials():
|
||||
response = client.get("/users/me", headers={"Authorization": "Basic notreally"})
|
||||
assert response.status_code == 403, response.text
|
||||
assert response.json() == {"detail": "Invalid authentication credentials"}
|
||||
assert response.status_code == 401, response.text
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
assert response.headers["WWW-Authenticate"] == "Bearer"
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
|
|
|
|||
|
|
@ -23,14 +23,16 @@ def test_security_http_bearer():
|
|||
|
||||
def test_security_http_bearer_no_credentials():
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 403, response.text
|
||||
assert response.status_code == 401, response.text
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
assert response.headers["WWW-Authenticate"] == "Bearer"
|
||||
|
||||
|
||||
def test_security_http_bearer_incorrect_scheme_credentials():
|
||||
response = client.get("/users/me", headers={"Authorization": "Basic notreally"})
|
||||
assert response.status_code == 403, response.text
|
||||
assert response.json() == {"detail": "Invalid authentication credentials"}
|
||||
assert response.status_code == 401, response.text
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
assert response.headers["WWW-Authenticate"] == "Bearer"
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
|
|
|
|||
|
|
@ -23,16 +23,18 @@ def test_security_http_digest():
|
|||
|
||||
def test_security_http_digest_no_credentials():
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 403, response.text
|
||||
assert response.status_code == 401, response.text
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
assert response.headers["WWW-Authenticate"] == "Digest"
|
||||
|
||||
|
||||
def test_security_http_digest_incorrect_scheme_credentials():
|
||||
response = client.get(
|
||||
"/users/me", headers={"Authorization": "Other invalidauthorization"}
|
||||
)
|
||||
assert response.status_code == 403, response.text
|
||||
assert response.json() == {"detail": "Invalid authentication credentials"}
|
||||
assert response.status_code == 401, response.text
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
assert response.headers["WWW-Authenticate"] == "Digest"
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
|
|
|
|||
|
|
@ -23,16 +23,18 @@ def test_security_http_digest():
|
|||
|
||||
def test_security_http_digest_no_credentials():
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 403, response.text
|
||||
assert response.status_code == 401, response.text
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
assert response.headers["WWW-Authenticate"] == "Digest"
|
||||
|
||||
|
||||
def test_security_http_digest_incorrect_scheme_credentials():
|
||||
response = client.get(
|
||||
"/users/me", headers={"Authorization": "Other invalidauthorization"}
|
||||
)
|
||||
assert response.status_code == 403, response.text
|
||||
assert response.json() == {"detail": "Invalid authentication credentials"}
|
||||
assert response.status_code == 401, response.text
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
assert response.headers["WWW-Authenticate"] == "Digest"
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
|
|
|
|||
|
|
@ -56,8 +56,9 @@ def test_security_oauth2_password_other_header():
|
|||
|
||||
def test_security_oauth2_password_bearer_no_header():
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 403, response.text
|
||||
assert response.status_code == 401, response.text
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
assert response.headers["WWW-Authenticate"] == "Bearer"
|
||||
|
||||
|
||||
def test_strict_login_no_data():
|
||||
|
|
|
|||
|
|
@ -39,8 +39,9 @@ def test_security_oauth2_password_other_header():
|
|||
|
||||
def test_security_oauth2_password_bearer_no_header():
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 403, response.text
|
||||
assert response.status_code == 401, response.text
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
assert response.headers["WWW-Authenticate"] == "Bearer"
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
|
|
|
|||
|
|
@ -41,8 +41,9 @@ def test_security_oauth2_password_other_header():
|
|||
|
||||
def test_security_oauth2_password_bearer_no_header():
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 403, response.text
|
||||
assert response.status_code == 401, response.text
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
assert response.headers["WWW-Authenticate"] == "Bearer"
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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"],
|
||||
}
|
||||
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@ def test_get_root():
|
|||
|
||||
def test_get_root_no_token():
|
||||
response = client.get("/")
|
||||
assert response.status_code == 403, response.text
|
||||
assert response.status_code == 401, response.text
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
|
@ -66,7 +66,7 @@ def test_token(client: TestClient):
|
|||
def test_incorrect_token(client: TestClient):
|
||||
response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"})
|
||||
assert response.status_code == 401, response.text
|
||||
assert response.json() == {"detail": "Invalid authentication credentials"}
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
assert response.headers["WWW-Authenticate"] == "Bearer"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ def test_security_http_basic_invalid_credentials(client: TestClient):
|
|||
)
|
||||
assert response.status_code == 401, response.text
|
||||
assert response.headers["WWW-Authenticate"] == "Basic"
|
||||
assert response.json() == {"detail": "Invalid authentication credentials"}
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
|
||||
|
||||
def test_security_http_basic_non_basic_credentials(client: TestClient):
|
||||
|
|
@ -50,7 +50,7 @@ def test_security_http_basic_non_basic_credentials(client: TestClient):
|
|||
response = client.get("/users/me", headers={"Authorization": auth_header})
|
||||
assert response.status_code == 401, response.text
|
||||
assert response.headers["WWW-Authenticate"] == "Basic"
|
||||
assert response.json() == {"detail": "Invalid authentication credentials"}
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
|
||||
|
||||
def test_openapi_schema(client: TestClient):
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ def get_client(request: pytest.FixtureRequest):
|
|||
|
||||
with TestClient(mod.app) as c:
|
||||
yield c
|
||||
# Clean up connection explicitely to avoid resource warning
|
||||
# Clean up connection explicitly to avoid resource warning
|
||||
mod.engine.dispose()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ def get_client(request: pytest.FixtureRequest):
|
|||
|
||||
with TestClient(mod.app) as c:
|
||||
yield c
|
||||
# Clean up connection explicitely to avoid resource warning
|
||||
# Clean up connection explicitly to avoid resource warning
|
||||
mod.engine.dispose()
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue