Browse Source

Merge branch 'master' into bugfix/subapp-custom-openapi

pull/4657/head
Lorhan Sohaky 2 years ago
committed by GitHub
parent
commit
488bd15f10
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      .github/workflows/build-docs.yml
  2. 8
      .github/workflows/test.yml
  3. 15
      .pre-commit-config.yaml
  4. 1
      README.md
  5. 2
      docs/em/docs/contributing.md
  6. 3
      docs/en/data/sponsors.yml
  7. 1
      docs/en/data/sponsors_badge.yml
  8. 9
      docs/en/docs/contributing.md
  9. 2
      docs/en/docs/features.md
  10. BIN
      docs/en/docs/img/sponsors/platform-sh-banner.png
  11. BIN
      docs/en/docs/img/sponsors/platform-sh.png
  12. 57
      docs/en/docs/release-notes.md
  13. 6
      docs/en/overrides/main.html
  14. 16
      docs/id/docs/tutorial/index.md
  15. 2
      docs/ja/docs/contributing.md
  16. 2
      docs/pt/docs/contributing.md
  17. 2
      docs/ru/docs/contributing.md
  18. 80
      docs/ru/docs/tutorial/index.md
  19. 189
      docs/ru/docs/tutorial/schema-extra-example.md
  20. 2
      docs/ru/mkdocs.yml
  21. 31
      docs/zh/docs/advanced/response-change-status-code.md
  22. 39
      docs/zh/docs/advanced/response-headers.md
  23. 2
      docs/zh/docs/contributing.md
  24. 2
      docs/zh/mkdocs.yml
  25. 2
      fastapi/__init__.py
  26. 35
      fastapi/applications.py
  27. 13
      fastapi/exception_handlers.py
  28. 2
      fastapi/exceptions.py
  29. 29
      fastapi/middleware/asyncexitstack.py
  30. 13
      fastapi/openapi/models.py
  31. 8
      fastapi/openapi/utils.py
  32. 2
      fastapi/responses.py
  33. 62
      fastapi/routing.py
  34. 12
      fastapi/security/api_key.py
  35. 25
      fastapi/security/oauth2.py
  36. 47
      pyproject.toml
  37. 8
      requirements-docs.txt
  38. 25
      requirements-tests.txt
  39. 5
      requirements.txt
  40. 2
      scripts/build-docs.sh
  41. 1
      scripts/format.sh
  42. 1
      scripts/lint.sh
  43. 2
      scripts/test.sh
  44. 3
      tests/test_empty_router.py
  45. 73
      tests/test_ws_dependencies.py
  46. 152
      tests/test_ws_router.py

4
.github/workflows/build-docs.yml

@ -22,10 +22,10 @@ jobs:
id: cache
with:
path: ${{ env.pythonLocation }}
key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-v03
key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-docs.txt') }}-v03
- name: Install docs extras
if: steps.cache.outputs.cache-hit != 'true'
run: pip install .[doc]
run: pip install -r requirements-docs.txt
- name: Install Material for MkDocs Insiders
if: ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false ) && steps.cache.outputs.cache-hit != 'true'
run: pip install git+https://${{ secrets.ACTIONS_TOKEN }}@github.com/squidfunk/mkdocs-material-insiders.git

8
.github/workflows/test.yml

@ -23,15 +23,15 @@ jobs:
python-version: ${{ matrix.python-version }}
# Issue ref: https://github.com/actions/setup-python/issues/436
# cache: "pip"
cache-dependency-path: pyproject.toml
# cache-dependency-path: pyproject.toml
- uses: actions/cache@v3
id: cache
with:
path: ${{ env.pythonLocation }}
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-test-v03
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v03
- name: Install Dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: pip install -e .[all,dev,doc,test]
run: pip install -r requirements-tests.txt
- name: Lint
run: bash scripts/lint.sh
- run: mkdir coverage
@ -57,7 +57,7 @@ jobs:
python-version: '3.8'
# Issue ref: https://github.com/actions/setup-python/issues/436
# cache: "pip"
cache-dependency-path: pyproject.toml
# cache-dependency-path: pyproject.toml
- name: Get coverage files
uses: actions/download-artifact@v3

15
.pre-commit-config.yaml

@ -21,24 +21,13 @@ repos:
- --py3-plus
- --keep-runtime-typing
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.254
rev: v0.0.272
hooks:
- id: ruff
args:
- --fix
- repo: https://github.com/pycqa/isort
rev: 5.12.0
hooks:
- id: isort
name: isort (python)
- id: isort
name: isort (cython)
types: [cython]
- id: isort
name: isort (pyi)
types: [pyi]
- repo: https://github.com/psf/black
rev: 23.1.0
rev: 23.3.0
hooks:
- id: black
ci:

1
README.md

@ -47,6 +47,7 @@ The key features are:
<!-- sponsors -->
<a href="https://cryptapi.io/" target="_blank" title="CryptAPI: Your easy to use, secure and privacy oriented payment gateway."><img src="https://fastapi.tiangolo.com/img/sponsors/cryptapi.svg"></a>
<a href="https://platform.sh/try-it-now/?utm_source=fastapi-signup&utm_medium=banner&utm_campaign=FastAPI-signup-June-2023" target="_blank" title="Build, run and scale your apps on a modern, reliable, and secure PaaS."><img src="https://fastapi.tiangolo.com/img/sponsors/platform-sh.png"></a>
<a href="https://www.deta.sh/?ref=fastapi" target="_blank" title="The launchpad for all your (team's) ideas"><img src="https://fastapi.tiangolo.com/img/sponsors/deta.svg"></a>
<a href="https://training.talkpython.fm/fastapi-courses" target="_blank" title="FastAPI video courses on demand from people you trust"><img src="https://fastapi.tiangolo.com/img/sponsors/talkpython.png"></a>
<a href="https://testdriven.io/courses/tdd-fastapi/" target="_blank" title="Learn to build high-quality web apps with best practices"><img src="https://fastapi.tiangolo.com/img/sponsors/testdriven.svg"></a>

2
docs/em/docs/contributing.md

@ -108,7 +108,7 @@ $ python -m pip install --upgrade pip
<div class="termy">
```console
$ pip install -e ."[dev,doc,test]"
$ pip install -r requirements.txt
---> 100%
```

3
docs/en/data/sponsors.yml

@ -2,6 +2,9 @@ gold:
- url: https://cryptapi.io/
title: "CryptAPI: Your easy to use, secure and privacy oriented payment gateway."
img: https://fastapi.tiangolo.com/img/sponsors/cryptapi.svg
- url: https://platform.sh/try-it-now/?utm_source=fastapi-signup&utm_medium=banner&utm_campaign=FastAPI-signup-June-2023
title: "Build, run and scale your apps on a modern, reliable, and secure PaaS."
img: https://fastapi.tiangolo.com/img/sponsors/platform-sh.png
silver:
- url: https://www.deta.sh/?ref=fastapi
title: The launchpad for all your (team's) ideas

1
docs/en/data/sponsors_badge.yml

@ -15,3 +15,4 @@ logins:
- svix
- armand-sauzay
- databento-bot
- nanram22

9
docs/en/docs/contributing.md

@ -108,7 +108,7 @@ After activating the environment as described above:
<div class="termy">
```console
$ pip install -e ".[dev,doc,test]"
$ pip install -r requirements.txt
---> 100%
```
@ -121,10 +121,15 @@ It will install all the dependencies and your local FastAPI in your local enviro
If you create a Python file that imports and uses FastAPI, and run it with the Python from your local environment, it will use your local FastAPI source code.
And if you update that local FastAPI source code, as it is installed with `-e`, when you run that Python file again, it will use the fresh version of FastAPI you just edited.
And if you update that local FastAPI source code when you run that Python file again, it will use the fresh version of FastAPI you just edited.
That way, you don't have to "install" your local version to be able to test every change.
!!! note "Technical Details"
This only happens when you install using this included `requiements.txt` instead of installing `pip install fastapi` directly.
That is because inside of the `requirements.txt` file, the local version of FastAPI is marked to be installed in "editable" mode, with the `-e` option.
### Format
There is a script that you can run that will format and clean all your code:

2
docs/en/docs/features.md

@ -189,8 +189,6 @@ With **FastAPI** you get all of **Pydantic**'s features (as FastAPI is based on
* If you know Python types you know how to use Pydantic.
* Plays nicely with your **<abbr title="Integrated Development Environment, similar to a code editor">IDE</abbr>/<abbr title="A program that checks for code errors">linter</abbr>/brain**:
* Because pydantic data structures are just instances of classes you define; auto-completion, linting, mypy and your intuition should all work properly with your validated data.
* **Fast**:
* in <a href="https://pydantic-docs.helpmanual.io/benchmarks/" class="external-link" target="_blank">benchmarks</a> Pydantic is faster than all other tested libraries.
* Validate **complex structures**:
* Use of hierarchical Pydantic models, Python `typing`’s `List` and `Dict`, etc.
* And validators allow complex data schemas to be clearly and easily defined, checked and documented as JSON Schema.

BIN
docs/en/docs/img/sponsors/platform-sh-banner.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

BIN
docs/en/docs/img/sponsors/platform-sh.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

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

@ -2,11 +2,62 @@
## Latest Changes
* 👷 Add custom tokens for GitHub Actions to avoid rate limits. PR [#9647](https://github.com/tiangolo/fastapi/pull/9647) by [@tiangolo](https://github.com/tiangolo).
* ♻ Instantiate `HTTPException` only when needed, optimization refactor. PR [#5356](https://github.com/tiangolo/fastapi/pull/5356) by [@pawamoy](https://github.com/pawamoy).
* 📌 Update minimum version of Pydantic to >=1.7.4. PR [#9567](https://github.com/tiangolo/fastapi/pull/9567) by [@Kludex](https://github.com/Kludex).
## 0.97.0
### Features
* ✨ Add support for `dependencies` in WebSocket routes. PR [#4534](https://github.com/tiangolo/fastapi/pull/4534) by [@paulo-raca](https://github.com/paulo-raca).
* ✨ Add exception handler for `WebSocketRequestValidationError` (which also allows to override it). PR [#6030](https://github.com/tiangolo/fastapi/pull/6030) by [@kristjanvalur](https://github.com/kristjanvalur).
### Refactors
* ⬆️ Upgrade and fully migrate to Ruff, remove isort, includes a couple of tweaks suggested by the new version of Ruff. PR [#9660](https://github.com/tiangolo/fastapi/pull/9660) by [@tiangolo](https://github.com/tiangolo).
* ♻️ Update internal type annotations and upgrade mypy. PR [#9658](https://github.com/tiangolo/fastapi/pull/9658) by [@tiangolo](https://github.com/tiangolo).
* ♻️ Simplify `AsyncExitStackMiddleware` as without Python 3.6 `AsyncExitStack` is always available. PR [#9657](https://github.com/tiangolo/fastapi/pull/9657) by [@tiangolo](https://github.com/tiangolo).
### Upgrades
* ⬆️ Upgrade Black. PR [#9661](https://github.com/tiangolo/fastapi/pull/9661) by [@tiangolo](https://github.com/tiangolo).
### Internal
* 💚 Update CI cache to fix installs when dependencies change. PR [#9659](https://github.com/tiangolo/fastapi/pull/9659) by [@tiangolo](https://github.com/tiangolo).
* ⬇️ Separate requirements for development into their own requirements.txt files, they shouldn't be extras. PR [#9655](https://github.com/tiangolo/fastapi/pull/9655) by [@tiangolo](https://github.com/tiangolo).
## 0.96.1
### Fixes
* 🐛 Fix `HTTPException` header type annotations. PR [#9648](https://github.com/tiangolo/fastapi/pull/9648) by [@tiangolo](https://github.com/tiangolo).
* 🐛 Fix OpenAPI model fields int validations, `gte` to `ge`. PR [#9635](https://github.com/tiangolo/fastapi/pull/9635) by [@tiangolo](https://github.com/tiangolo).
### Upgrades
* 📌 Update minimum version of Pydantic to >=1.7.4. This fixes an issue when trying to use an old version of Pydantic. PR [#9567](https://github.com/tiangolo/fastapi/pull/9567) by [@Kludex](https://github.com/Kludex).
### Refactors
* ♻ Remove `media_type` from `ORJSONResponse` as it's inherited from the parent class. PR [#5805](https://github.com/tiangolo/fastapi/pull/5805) by [@Kludex](https://github.com/Kludex).
* ♻ Instantiate `HTTPException` only when needed, optimization refactor. PR [#5356](https://github.com/tiangolo/fastapi/pull/5356) by [@pawamoy](https://github.com/pawamoy).
### Docs
* 🔥 Remove link to Pydantic's benchmark, as it was removed there. PR [#5811](https://github.com/tiangolo/fastapi/pull/5811) by [@Kludex](https://github.com/Kludex).
### Translations
* 🌐 Fix spelling in Indonesian translation of `docs/id/docs/tutorial/index.md`. PR [#5635](https://github.com/tiangolo/fastapi/pull/5635) by [@purwowd](https://github.com/purwowd).
* 🌐 Add Russian translation for `docs/ru/docs/tutorial/index.md`. PR [#5896](https://github.com/tiangolo/fastapi/pull/5896) by [@Wilidon](https://github.com/Wilidon).
* 🌐 Add Chinese translations for `docs/zh/docs/advanced/response-change-status-code.md` and `docs/zh/docs/advanced/response-headers.md`. PR [#9544](https://github.com/tiangolo/fastapi/pull/9544) by [@ChoyeonChern](https://github.com/ChoyeonChern).
* 🌐 Add Russian translation for `docs/ru/docs/tutorial/schema-extra-example.md`. PR [#9621](https://github.com/tiangolo/fastapi/pull/9621) by [@Alexandrhub](https://github.com/Alexandrhub).
### Internal
* 🔧 Add sponsor Platform.sh. PR [#9650](https://github.com/tiangolo/fastapi/pull/9650) by [@tiangolo](https://github.com/tiangolo).
* 👷 Add custom token to Smokeshow and Preview Docs for download-artifact, to prevent API rate limits. PR [#9646](https://github.com/tiangolo/fastapi/pull/9646) by [@tiangolo](https://github.com/tiangolo).
* 👷 Add custom tokens for GitHub Actions to avoid rate limits. PR [#9647](https://github.com/tiangolo/fastapi/pull/9647) by [@tiangolo](https://github.com/tiangolo).
## 0.96.0
### Features

6
docs/en/overrides/main.html

@ -28,6 +28,12 @@
<img class="sponsor-image" src="/img/sponsors/cryptapi-banner.svg" />
</a>
</div>
<div class="item">
<a title="Build, run and scale your apps on a modern, reliable, and secure PaaS." style="display: block; position: relative;" href="https://platform.sh/try-it-now/?utm_source=fastapi-signup&utm_medium=banner&utm_campaign=FastAPI-signup-June-2023" target="_blank">
<span class="sponsor-badge">sponsor</span>
<img class="sponsor-image" src="/img/sponsors/platform-sh-banner.png" />
</a>
</div>
</div>
</div>
{% endblock %}

16
docs/id/docs/tutorial/index.md

@ -10,9 +10,9 @@ Sehingga kamu dapat kembali lagi dan mencari apa yang kamu butuhkan dengan tepat
## Jalankan kode
Semua blok-blok kode dapat dicopy dan digunakan langsung (Mereka semua sebenarnya adalah file python yang sudah teruji).
Semua blok-blok kode dapat disalin dan digunakan langsung (Mereka semua sebenarnya adalah file python yang sudah teruji).
Untuk menjalankan setiap contoh, copy kode ke file `main.py`, dan jalankan `uvicorn` dengan:
Untuk menjalankan setiap contoh, salin kode ke file `main.py`, dan jalankan `uvicorn` dengan:
<div class="termy">
@ -28,7 +28,7 @@ $ uvicorn main:app --reload
</div>
**SANGAT disarankan** agar kamu menulis atau meng-copy kode, meng-editnya dan menjalankannya secara lokal.
**SANGAT disarankan** agar kamu menulis atau menyalin kode, mengubahnya dan menjalankannya secara lokal.
Dengan menggunakannya di dalam editor, benar-benar memperlihatkan manfaat dari FastAPI, melihat bagaimana sedikitnya kode yang harus kamu tulis, semua pengecekan tipe, pelengkapan otomatis, dll.
@ -38,7 +38,7 @@ Dengan menggunakannya di dalam editor, benar-benar memperlihatkan manfaat dari F
Langkah pertama adalah dengan meng-install FastAPI.
Untuk tutorial, kamu mungkin hendak meng-instalnya dengan semua pilihan fitur dan dependensinya:
Untuk tutorial, kamu mungkin hendak meng-installnya dengan semua pilihan fitur dan dependensinya:
<div class="termy">
@ -53,15 +53,15 @@ $ pip install "fastapi[all]"
...yang juga termasuk `uvicorn`, yang dapat kamu gunakan sebagai server yang menjalankan kodemu.
!!! catatan
Kamu juga dapat meng-instalnya bagian demi bagian.
Kamu juga dapat meng-installnya bagian demi bagian.
Hal ini mungkin yang akan kamu lakukan ketika kamu hendak men-deploy aplikasimu ke tahap produksi:
Hal ini mungkin yang akan kamu lakukan ketika kamu hendak menyebarkan (men-deploy) aplikasimu ke tahap produksi:
```
pip install fastapi
```
Juga install `uvicorn` untk menjalankan server"
Juga install `uvicorn` untuk menjalankan server"
```
pip install "uvicorn[standard]"
@ -77,4 +77,4 @@ Tersedia juga **Pedoman Pengguna Lanjutan** yang dapat kamu baca nanti setelah *
Tetapi kamu harus membaca terlebih dahulu **Tutorial - Pedoman Pengguna** (apa yang sedang kamu baca sekarang).
Hal ini didesain sehingga kamu dapat membangun aplikasi lengkap dengan hanya **Tutorial - Pedoman Pengguna**, dan kemudian mengembangkannya ke banyak cara yang berbeda, tergantung dari kebutuhanmu, menggunakan beberapa ide-ide tambahan dari **Pedoman Pengguna Lanjutan**.
Hal ini dirancang supaya kamu dapat membangun aplikasi lengkap dengan hanya **Tutorial - Pedoman Pengguna**, dan kemudian mengembangkannya ke banyak cara yang berbeda, tergantung dari kebutuhanmu, menggunakan beberapa ide-ide tambahan dari **Pedoman Pengguna Lanjutan**.

2
docs/ja/docs/contributing.md

@ -97,7 +97,7 @@ $ python -m venv env
<div class="termy">
```console
$ pip install -e ."[dev,doc,test]"
$ pip install -r requirements.txt
---> 100%
```

2
docs/pt/docs/contributing.md

@ -98,7 +98,7 @@ Após ativar o ambiente como descrito acima:
<div class="termy">
```console
$ pip install -e ."[dev,doc,test]"
$ pip install -r requirements.txt
---> 100%
```

2
docs/ru/docs/contributing.md

@ -108,7 +108,7 @@ $ python -m pip install --upgrade pip
<div class="termy">
```console
$ pip install -e ."[dev,doc,test]"
$ pip install -r requirements.txt
---> 100%
```

80
docs/ru/docs/tutorial/index.md

@ -0,0 +1,80 @@
# Учебник - Руководство пользователя - Введение
В этом руководстве шаг за шагом показано, как использовать **FastApi** с большинством его функций.
Каждый раздел постепенно основывается на предыдущих, но он структурирован по отдельным темам, так что вы можете перейти непосредственно к конкретной теме для решения ваших конкретных потребностей в API.
Он также создан для использования в качестве будущего справочника.
Так что вы можете вернуться и посмотреть именно то, что вам нужно.
## Запустите код
Все блоки кода можно копировать и использовать напрямую (на самом деле это проверенные файлы Python).
Чтобы запустить любой из примеров, скопируйте код в файл `main.py` и запустите `uvicorn` с параметрами:
<div class="termy">
```console
$ uvicorn main:app --reload
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
<span style="color: green;">INFO</span>: Started reloader process [28720]
<span style="color: green;">INFO</span>: Started server process [28722]
<span style="color: green;">INFO</span>: Waiting for application startup.
<span style="color: green;">INFO</span>: Application startup complete.
```
</div>
**НАСТОЯТЕЛЬНО рекомендуется**, чтобы вы написали или скопировали код, отредактировали его и запустили локально.
Использование кода в вашем редакторе — это то, что действительно показывает вам преимущества FastAPI, видя, как мало кода вам нужно написать, все проверки типов, автодополнение и т.д.
---
## Установка FastAPI
Первый шаг — установить FastAPI.
Для руководства вы, возможно, захотите установить его со всеми дополнительными зависимостями и функциями:
<div class="termy">
```console
$ pip install "fastapi[all]"
---> 100%
```
</div>
...это также включает `uvicorn`, который вы можете использовать в качестве сервера, который запускает ваш код.
!!! note "Технические детали"
Вы также можете установить его по частям.
Это то, что вы, вероятно, сделаете, когда захотите развернуть свое приложение в рабочей среде:
```
pip install fastapi
```
Также установите `uvicorn` для работы в качестве сервера:
```
pip install "uvicorn[standard]"
```
И то же самое для каждой из необязательных зависимостей, которые вы хотите использовать.
## Продвинутое руководство пользователя
Существует также **Продвинутое руководство пользователя**, которое вы сможете прочитать после руководства **Учебник - Руководство пользователя**.
**Продвинутое руководство пользователя** основано на этом, использует те же концепции и учит вас некоторым дополнительным функциям.
Но вы должны сначала прочитать **Учебник - Руководство пользователя** (то, что вы читаете прямо сейчас).
Он разработан таким образом, что вы можете создать полноценное приложение, используя только **Учебник - Руководство пользователя**, а затем расширить его различными способами, в зависимости от ваших потребностей, используя некоторые дополнительные идеи из **Продвинутого руководства пользователя**.

189
docs/ru/docs/tutorial/schema-extra-example.md

@ -0,0 +1,189 @@
# Объявление примера запроса данных
Вы можете объявлять примеры данных, которые ваше приложение может получать.
Вот несколько способов, как это можно сделать.
## Pydantic `schema_extra`
Вы можете объявить ключ `example` для модели Pydantic, используя класс `Config` и переменную `schema_extra`, как описано в <a href="https://pydantic-docs.helpmanual.io/usage/schema/#schema-customization" class="external-link" target="_blank">Pydantic документации: Настройка схемы</a>:
=== "Python 3.10+"
```Python hl_lines="13-21"
{!> ../../../docs_src/schema_extra_example/tutorial001_py310.py!}
```
=== "Python 3.6+"
```Python hl_lines="15-23"
{!> ../../../docs_src/schema_extra_example/tutorial001.py!}
```
Эта дополнительная информация будет включена в **JSON Schema** выходных данных для этой модели, и она будет использоваться в документации к API.
!!! tip Подсказка
Вы можете использовать тот же метод для расширения JSON-схемы и добавления своей собственной дополнительной информации.
Например, вы можете использовать это для добавления дополнительной информации для пользовательского интерфейса в вашем веб-приложении и т.д.
## Дополнительные аргументы поля `Field`
При использовании `Field()` с моделями Pydantic, вы также можете объявлять дополнительную информацию для **JSON Schema**, передавая любые другие произвольные аргументы в функцию.
Вы можете использовать это, чтобы добавить аргумент `example` для каждого поля:
=== "Python 3.10+"
```Python hl_lines="2 8-11"
{!> ../../../docs_src/schema_extra_example/tutorial002_py310.py!}
```
=== "Python 3.6+"
```Python hl_lines="4 10-13"
{!> ../../../docs_src/schema_extra_example/tutorial002.py!}
```
!!! warning Внимание
Имейте в виду, что эти дополнительные переданные аргументы не добавляют никакой валидации, только дополнительную информацию для документации.
## Использование `example` и `examples` в OpenAPI
При использовании любой из этих функций:
* `Path()`
* `Query()`
* `Header()`
* `Cookie()`
* `Body()`
* `Form()`
* `File()`
вы также можете добавить аргумент, содержащий `example` или группу `examples` с дополнительной информацией, которая будет добавлена в **OpenAPI**.
### Параметр `Body` с аргументом `example`
Здесь мы передаём аргумент `example`, как пример данных ожидаемых в параметре `Body()`:
=== "Python 3.10+"
```Python hl_lines="22-27"
{!> ../../../docs_src/schema_extra_example/tutorial003_an_py310.py!}
```
=== "Python 3.9+"
```Python hl_lines="22-27"
{!> ../../../docs_src/schema_extra_example/tutorial003_an_py39.py!}
```
=== "Python 3.6+"
```Python hl_lines="23-28"
{!> ../../../docs_src/schema_extra_example/tutorial003_an.py!}
```
=== "Python 3.10+ non-Annotated"
!!! tip Заметка
Рекомендуется использовать версию с `Annotated`, если это возможно.
```Python hl_lines="18-23"
{!> ../../../docs_src/schema_extra_example/tutorial003_py310.py!}
```
=== "Python 3.6+ non-Annotated"
!!! tip Заметка
Рекомендуется использовать версию с `Annotated`, если это возможно.
```Python hl_lines="20-25"
{!> ../../../docs_src/schema_extra_example/tutorial003.py!}
```
### Аргумент "example" в UI документации
С любым из вышеуказанных методов это будет выглядеть так в `/docs`:
<img src="/img/tutorial/body-fields/image01.png">
### `Body` с аргументом `examples`
В качестве альтернативы одному аргументу `example`, вы можете передавать `examples` используя тип данных `dict` с **несколькими примерами**, каждый из которых содержит дополнительную информацию, которая также будет добавлена в **OpenAPI**.
Ключи `dict` указывают на каждый пример, а значения для каждого из них - на еще один тип `dict` с дополнительной информацией.
Каждый конкретный пример типа `dict` в аргументе `examples` может содержать:
* `summary`: Краткое описание для примера.
* `description`: Полное описание, которое может содержать текст в формате Markdown.
* `value`: Это конкретный пример, который отображается, например, в виде типа `dict`.
* `externalValue`: альтернатива параметру `value`, URL-адрес, указывающий на пример. Хотя это может не поддерживаться таким же количеством инструментов разработки и тестирования API, как параметр `value`.
=== "Python 3.10+"
```Python hl_lines="23-49"
{!> ../../../docs_src/schema_extra_example/tutorial004_an_py310.py!}
```
=== "Python 3.9+"
```Python hl_lines="23-49"
{!> ../../../docs_src/schema_extra_example/tutorial004_an_py39.py!}
```
=== "Python 3.6+"
```Python hl_lines="24-50"
{!> ../../../docs_src/schema_extra_example/tutorial004_an.py!}
```
=== "Python 3.10+ non-Annotated"
!!! tip Заметка
Рекомендуется использовать версию с `Annotated`, если это возможно.
```Python hl_lines="19-45"
{!> ../../../docs_src/schema_extra_example/tutorial004_py310.py!}
```
=== "Python 3.6+ non-Annotated"
!!! tip Заметка
Рекомендуется использовать версию с `Annotated`, если это возможно.
```Python hl_lines="21-47"
{!> ../../../docs_src/schema_extra_example/tutorial004.py!}
```
### Аргумент "examples" в UI документации
С аргументом `examples`, добавленным в `Body()`, страница документации `/docs` будет выглядеть так:
<img src="/img/tutorial/body-fields/image02.png">
## Технические Детали
!!! warning Внимание
Эти технические детали относятся к стандартам **JSON Schema** и **OpenAPI**.
Если предложенные выше идеи уже работают для вас, возможно этого будет достаточно и эти детали вам не потребуются, можете спокойно их пропустить.
Когда вы добавляете пример внутрь модели Pydantic, используя `schema_extra` или `Field(example="something")`, этот пример добавляется в **JSON Schema** для данной модели Pydantic.
И эта **JSON Schema** модели Pydantic включается в **OpenAPI** вашего API, а затем используется в UI документации.
Поля `example` как такового не существует в стандартах **JSON Schema**. В последних версиях JSON-схемы определено поле <a href="https://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.9.5" class="external-link" target="_blank">`examples`</a>, но OpenAPI 3.0.3 основан на более старой версии JSON-схемы, которая не имела поля `examples`.
Таким образом, OpenAPI 3.0.3 определяет своё собственное поле <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#fixed-fields-20" class="external-link" target="_blank">`example`</a> для модифицированной версии **JSON Schema**, которую он использует чтобы достичь той же цели (однако это именно поле `example`, а не `examples`), и именно это используется API в UI документации (с интеграцией Swagger UI).
Итак, хотя поле `example` не является частью JSON-схемы, оно является частью настраиваемой версии JSON-схемы в OpenAPI, и именно это поле будет использоваться в UI документации.
Однако, когда вы используете поле `example` или `examples` с любой другой функцией (`Query()`, `Body()`, и т.д.), эти примеры не добавляются в JSON-схему, которая описывает эти данные (даже в собственную версию JSON-схемы OpenAPI), они добавляются непосредственно в объявление *операции пути* в OpenAPI (вне частей OpenAPI, которые используют JSON-схему).
Для функций `Path()`, `Query()`, `Header()`, и `Cookie()`, аргументы `example` или `examples` добавляются в <a href="https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#parameter-object" class="external-link" target="_blank">определение OpenAPI, к объекту `Parameter Object` (в спецификации)</a>.
И для функций `Body()`, `File()` и `Form()` аргументы `example` или `examples` аналогично добавляются в <a href="https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#mediaTypeObject" class="external-link" target="_blank"> определение OpenAPI, к объекту `Request Body Object`, в поле `content` в объекте `Media Type Object` (в спецификации)</a>.
С другой стороны, существует более новая версия OpenAPI: **3.1.0**, недавно выпущенная. Она основана на последней версии JSON-схемы и большинство модификаций из OpenAPI JSON-схемы удалены в обмен на новые возможности из последней версии JSON-схемы, так что все эти мелкие отличия устранены. Тем не менее, Swagger UI в настоящее время не поддерживает OpenAPI 3.1.0, поэтому пока лучше продолжать использовать вышеупомянутые методы.

2
docs/ru/mkdocs.yml

@ -67,6 +67,7 @@ nav:
- fastapi-people.md
- python-types.md
- Учебник - руководство пользователя:
- tutorial/index.md
- tutorial/first-steps.md
- tutorial/path-params.md
- tutorial/query-params-str-validations.md
@ -81,6 +82,7 @@ nav:
- tutorial/body-multiple-params.md
- tutorial/static-files.md
- tutorial/debugging.md
- tutorial/schema-extra-example.md
- async.md
- Развёртывание:
- deployment/index.md

31
docs/zh/docs/advanced/response-change-status-code.md

@ -0,0 +1,31 @@
# 响应 - 更改状态码
你可能之前已经了解到,你可以设置默认的[响应状态码](../tutorial/response-status-code.md){.internal-link target=_blank}。
但在某些情况下,你需要返回一个不同于默认值的状态码。
## 使用场景
例如,假设你想默认返回一个HTTP状态码为“OK”`200`。
但如果数据不存在,你想创建它,并返回一个HTTP状态码为“CREATED”`201`。
但你仍然希望能够使用`response_model`过滤和转换你返回的数据。
对于这些情况,你可以使用一个`Response`参数。
## 使用 `Response` 参数
你可以在你的*路径操作函数*中声明一个`Response`类型的参数(就像你可以为cookies和头部做的那样)。
然后你可以在这个*临时*响应对象中设置`status_code`。
```Python hl_lines="1 9 12"
{!../../../docs_src/response_change_status_code/tutorial001.py!}
```
然后你可以像平常一样返回任何你需要的对象(例如一个`dict`或者一个数据库模型)。如果你声明了一个`response_model`,它仍然会被用来过滤和转换你返回的对象。
**FastAPI**将使用这个临时响应来提取状态码(也包括cookies和头部),并将它们放入包含你返回的值的最终响应中,该响应由任何`response_model`过滤。
你也可以在依赖项中声明`Response`参数,并在其中设置状态码。但请注意,最后设置的状态码将会生效。

39
docs/zh/docs/advanced/response-headers.md

@ -0,0 +1,39 @@
# 响应头
## 使用 `Response` 参数
你可以在你的*路径操作函数*中声明一个`Response`类型的参数(就像你可以为cookies做的那样)。
然后你可以在这个*临时*响应对象中设置头部。
```Python hl_lines="1 7-8"
{!../../../docs_src/response_headers/tutorial002.py!}
```
然后你可以像平常一样返回任何你需要的对象(例如一个`dict`或者一个数据库模型)。如果你声明了一个`response_model`,它仍然会被用来过滤和转换你返回的对象。
**FastAPI**将使用这个临时响应来提取头部(也包括cookies和状态码),并将它们放入包含你返回的值的最终响应中,该响应由任何`response_model`过滤。
你也可以在依赖项中声明`Response`参数,并在其中设置头部(和cookies)。
## 直接返回 `Response`
你也可以在直接返回`Response`时添加头部。
按照[直接返回响应](response-directly.md){.internal-link target=_blank}中所述创建响应,并将头部作为附加参数传递:
```Python hl_lines="10-12"
{!../../../docs_src/response_headers/tutorial001.py!}
```
!!! 注意 "技术细节"
你也可以使用`from starlette.responses import Response`或`from starlette.responses import JSONResponse`。
**FastAPI**提供了与`fastapi.responses`相同的`starlette.responses`,只是为了方便开发者。但是,大多数可用的响应都直接来自Starlette。
由于`Response`经常用于设置头部和cookies,因此**FastAPI**还在`fastapi.Response`中提供了它。
## 自定义头部
请注意,可以使用'X-'前缀添加自定义专有头部。
但是,如果你有自定义头部,你希望浏览器中的客户端能够看到它们,你需要将它们添加到你的CORS配置中(在[CORS(跨源资源共享)](../tutorial/cors.md){.internal-link target=_blank}中阅读更多),使用在<a href="https://www.starlette.io/middleware/#corsmiddleware" class="external-link" target="_blank">Starlette的CORS文档</a>中记录的`expose_headers`参数。

2
docs/zh/docs/contributing.md

@ -97,7 +97,7 @@ $ python -m venv env
<div class="termy">
```console
$ pip install -e ."[dev,doc,test]"
$ pip install -r requirements.txt
---> 100%
```

2
docs/zh/mkdocs.yml

@ -117,6 +117,8 @@ nav:
- advanced/response-directly.md
- advanced/custom-response.md
- advanced/response-cookies.md
- advanced/response-change-status-code.md
- advanced/response-headers.md
- advanced/wsgi.md
- contributing.md
- help-fastapi.md

2
fastapi/__init__.py

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

35
fastapi/applications.py

@ -19,8 +19,9 @@ from fastapi.encoders import DictIntStrAny, SetIntStr
from fastapi.exception_handlers import (
http_exception_handler,
request_validation_exception_handler,
websocket_request_validation_exception_handler,
)
from fastapi.exceptions import RequestValidationError
from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError
from fastapi.logger import logger
from fastapi.middleware.asyncexitstack import AsyncExitStackMiddleware
from fastapi.openapi.docs import (
@ -145,6 +146,11 @@ class FastAPI(Starlette):
self.exception_handlers.setdefault(
RequestValidationError, request_validation_exception_handler
)
self.exception_handlers.setdefault(
WebSocketRequestValidationError,
# Starlette still has incorrect type specification for the handlers
websocket_request_validation_exception_handler, # type: ignore
)
self.user_middleware: List[Middleware] = (
[] if middleware is None else list(middleware)
@ -395,15 +401,34 @@ class FastAPI(Starlette):
return decorator
def add_api_websocket_route(
self, path: str, endpoint: Callable[..., Any], name: Optional[str] = None
self,
path: str,
endpoint: Callable[..., Any],
name: Optional[str] = None,
*,
dependencies: Optional[Sequence[Depends]] = None,
) -> None:
self.router.add_api_websocket_route(path, endpoint, name=name)
self.router.add_api_websocket_route(
path,
endpoint,
name=name,
dependencies=dependencies,
)
def websocket(
self, path: str, name: Optional[str] = None
self,
path: str,
name: Optional[str] = None,
*,
dependencies: Optional[Sequence[Depends]] = None,
) -> Callable[[DecoratedCallable], DecoratedCallable]:
def decorator(func: DecoratedCallable) -> DecoratedCallable:
self.add_api_websocket_route(path, func, name=name)
self.add_api_websocket_route(
path,
func,
name=name,
dependencies=dependencies,
)
return func
return decorator

13
fastapi/exception_handlers.py

@ -1,10 +1,11 @@
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError
from fastapi.utils import is_body_allowed_for_status_code
from fastapi.websockets import WebSocket
from starlette.exceptions import HTTPException
from starlette.requests import Request
from starlette.responses import JSONResponse, Response
from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY
from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY, WS_1008_POLICY_VIOLATION
async def http_exception_handler(request: Request, exc: HTTPException) -> Response:
@ -23,3 +24,11 @@ async def request_validation_exception_handler(
status_code=HTTP_422_UNPROCESSABLE_ENTITY,
content={"detail": jsonable_encoder(exc.errors())},
)
async def websocket_request_validation_exception_handler(
websocket: WebSocket, exc: WebSocketRequestValidationError
) -> None:
await websocket.close(
code=WS_1008_POLICY_VIOLATION, reason=jsonable_encoder(exc.errors())
)

2
fastapi/exceptions.py

@ -11,7 +11,7 @@ class HTTPException(StarletteHTTPException):
self,
status_code: int,
detail: Any = None,
headers: Optional[Dict[str, Any]] = None,
headers: Optional[Dict[str, str]] = None,
) -> None:
super().__init__(status_code=status_code, detail=detail, headers=headers)

29
fastapi/middleware/asyncexitstack.py

@ -10,19 +10,16 @@ class AsyncExitStackMiddleware:
self.context_name = context_name
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if AsyncExitStack:
dependency_exception: Optional[Exception] = None
async with AsyncExitStack() as stack:
scope[self.context_name] = stack
try:
await self.app(scope, receive, send)
except Exception as e:
dependency_exception = e
raise e
if dependency_exception:
# This exception was possibly handled by the dependency but it should
# still bubble up so that the ServerErrorMiddleware can return a 500
# or the ExceptionMiddleware can catch and handle any other exceptions
raise dependency_exception
else:
await self.app(scope, receive, send) # pragma: no cover
dependency_exception: Optional[Exception] = None
async with AsyncExitStack() as stack:
scope[self.context_name] = stack
try:
await self.app(scope, receive, send)
except Exception as e:
dependency_exception = e
raise e
if dependency_exception:
# This exception was possibly handled by the dependency but it should
# still bubble up so that the ServerErrorMiddleware can return a 500
# or the ExceptionMiddleware can catch and handle any other exceptions
raise dependency_exception

13
fastapi/openapi/models.py

@ -3,6 +3,7 @@ from typing import Any, Callable, Dict, Iterable, List, Optional, Union
from fastapi.logger import logger
from pydantic import AnyUrl, BaseModel, Field
from typing_extensions import Literal
try:
import email_validator # type: ignore
@ -298,18 +299,18 @@ class APIKeyIn(Enum):
class APIKey(SecurityBase):
type_ = Field(SecuritySchemeType.apiKey, alias="type")
type_: SecuritySchemeType = Field(default=SecuritySchemeType.apiKey, alias="type")
in_: APIKeyIn = Field(alias="in")
name: str
class HTTPBase(SecurityBase):
type_ = Field(SecuritySchemeType.http, alias="type")
type_: SecuritySchemeType = Field(default=SecuritySchemeType.http, alias="type")
scheme: str
class HTTPBearer(HTTPBase):
scheme = "bearer"
scheme: Literal["bearer"] = "bearer"
bearerFormat: Optional[str] = None
@ -349,12 +350,14 @@ class OAuthFlows(BaseModel):
class OAuth2(SecurityBase):
type_ = Field(SecuritySchemeType.oauth2, alias="type")
type_: SecuritySchemeType = Field(default=SecuritySchemeType.oauth2, alias="type")
flows: OAuthFlows
class OpenIdConnect(SecurityBase):
type_ = Field(SecuritySchemeType.openIdConnect, alias="type")
type_: SecuritySchemeType = Field(
default=SecuritySchemeType.openIdConnect, alias="type"
)
openIdConnectUrl: str

8
fastapi/openapi/utils.py

@ -181,7 +181,7 @@ def get_openapi_operation_metadata(
file_name = getattr(route.endpoint, "__globals__", {}).get("__file__")
if file_name:
message += f" at {file_name}"
warnings.warn(message)
warnings.warn(message, stacklevel=1)
operation_ids.add(operation_id)
operation["operationId"] = operation_id
if route.deprecated:
@ -332,10 +332,8 @@ def get_openapi_path(
openapi_response["description"] = description
http422 = str(HTTP_422_UNPROCESSABLE_ENTITY)
if (all_route_params or route.body_field) and not any(
[
status in operation["responses"]
for status in [http422, "4XX", "default"]
]
status in operation["responses"]
for status in [http422, "4XX", "default"]
):
operation["responses"][http422] = {
"description": "Validation Error",

2
fastapi/responses.py

@ -27,8 +27,6 @@ class UJSONResponse(JSONResponse):
class ORJSONResponse(JSONResponse):
media_type = "application/json"
def render(self, content: Any) -> bytes:
assert orjson is not None, "orjson must be installed to use ORJSONResponse"
return orjson.dumps(

62
fastapi/routing.py

@ -30,7 +30,11 @@ from fastapi.dependencies.utils import (
solve_dependencies,
)
from fastapi.encoders import DictIntStrAny, SetIntStr, jsonable_encoder
from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError
from fastapi.exceptions import (
FastAPIError,
RequestValidationError,
WebSocketRequestValidationError,
)
from fastapi.types import DecoratedCallable
from fastapi.utils import (
create_cloned_field,
@ -48,15 +52,15 @@ from starlette.concurrency import run_in_threadpool
from starlette.exceptions import HTTPException
from starlette.requests import Request
from starlette.responses import JSONResponse, Response
from starlette.routing import BaseRoute, Match
from starlette.routing import Mount as Mount # noqa
from starlette.routing import (
BaseRoute,
Match,
compile_path,
get_name,
request_response,
websocket_session,
)
from starlette.status import WS_1008_POLICY_VIOLATION
from starlette.routing import Mount as Mount # noqa
from starlette.types import ASGIApp, Lifespan, Scope
from starlette.websockets import WebSocket
@ -283,7 +287,6 @@ def get_websocket_app(
)
values, errors, _, _2, _3 = solved_result
if errors:
await websocket.close(code=WS_1008_POLICY_VIOLATION)
raise WebSocketRequestValidationError(errors)
assert dependant.call is not None, "dependant.call must be a function"
await dependant.call(**values)
@ -298,13 +301,21 @@ class APIWebSocketRoute(routing.WebSocketRoute):
endpoint: Callable[..., Any],
*,
name: Optional[str] = None,
dependencies: Optional[Sequence[params.Depends]] = None,
dependency_overrides_provider: Optional[Any] = None,
) -> None:
self.path = path
self.endpoint = endpoint
self.name = get_name(endpoint) if name is None else name
self.dependencies = list(dependencies or [])
self.path_regex, self.path_format, self.param_convertors = compile_path(path)
self.dependant = get_dependant(path=self.path_format, call=self.endpoint)
for depends in self.dependencies[::-1]:
self.dependant.dependencies.insert(
0,
get_parameterless_sub_dependant(depends=depends, path=self.path_format),
)
self.app = websocket_session(
get_websocket_app(
dependant=self.dependant,
@ -418,10 +429,7 @@ class APIRoute(routing.Route):
else:
self.response_field = None # type: ignore
self.secure_cloned_response_field = None
if dependencies:
self.dependencies = list(dependencies)
else:
self.dependencies = []
self.dependencies = list(dependencies or [])
self.description = description or inspect.cleandoc(self.endpoint.__doc__ or "")
# if a "form feed" character (page break) is found in the description text,
# truncate description text to the content preceding the first "form feed"
@ -516,7 +524,7 @@ class APIRouter(routing.Router):
), "A path prefix must not end with '/', as the routes will start with '/'"
self.prefix = prefix
self.tags: List[Union[str, Enum]] = tags or []
self.dependencies = list(dependencies or []) or []
self.dependencies = list(dependencies or [])
self.deprecated = deprecated
self.include_in_schema = include_in_schema
self.responses = responses or {}
@ -690,21 +698,37 @@ class APIRouter(routing.Router):
return decorator
def add_api_websocket_route(
self, path: str, endpoint: Callable[..., Any], name: Optional[str] = None
self,
path: str,
endpoint: Callable[..., Any],
name: Optional[str] = None,
*,
dependencies: Optional[Sequence[params.Depends]] = None,
) -> None:
current_dependencies = self.dependencies.copy()
if dependencies:
current_dependencies.extend(dependencies)
route = APIWebSocketRoute(
self.prefix + path,
endpoint=endpoint,
name=name,
dependencies=current_dependencies,
dependency_overrides_provider=self.dependency_overrides_provider,
)
self.routes.append(route)
def websocket(
self, path: str, name: Optional[str] = None
self,
path: str,
name: Optional[str] = None,
*,
dependencies: Optional[Sequence[params.Depends]] = None,
) -> Callable[[DecoratedCallable], DecoratedCallable]:
def decorator(func: DecoratedCallable) -> DecoratedCallable:
self.add_api_websocket_route(path, func, name=name)
self.add_api_websocket_route(
path, func, name=name, dependencies=dependencies
)
return func
return decorator
@ -744,7 +768,7 @@ class APIRouter(routing.Router):
path = getattr(r, "path") # noqa: B009
name = getattr(r, "name", "unknown")
if path is not None and not path:
raise Exception(
raise FastAPIError(
f"Prefix and path cannot be both empty (path operation: {name})"
)
if responses is None:
@ -819,8 +843,16 @@ class APIRouter(routing.Router):
name=route.name,
)
elif isinstance(route, APIWebSocketRoute):
current_dependencies = []
if dependencies:
current_dependencies.extend(dependencies)
if route.dependencies:
current_dependencies.extend(route.dependencies)
self.add_api_websocket_route(
prefix + route.path, route.endpoint, name=route.name
prefix + route.path,
route.endpoint,
dependencies=current_dependencies,
name=route.name,
)
elif isinstance(route, routing.WebSocketRoute):
self.add_websocket_route(

12
fastapi/security/api_key.py

@ -21,7 +21,9 @@ class APIKeyQuery(APIKeyBase):
auto_error: bool = True,
):
self.model: APIKey = APIKey(
**{"in": APIKeyIn.query}, name=name, description=description
**{"in": APIKeyIn.query}, # type: ignore[arg-type]
name=name,
description=description,
)
self.scheme_name = scheme_name or self.__class__.__name__
self.auto_error = auto_error
@ -48,7 +50,9 @@ class APIKeyHeader(APIKeyBase):
auto_error: bool = True,
):
self.model: APIKey = APIKey(
**{"in": APIKeyIn.header}, name=name, description=description
**{"in": APIKeyIn.header}, # type: ignore[arg-type]
name=name,
description=description,
)
self.scheme_name = scheme_name or self.__class__.__name__
self.auto_error = auto_error
@ -75,7 +79,9 @@ class APIKeyCookie(APIKeyBase):
auto_error: bool = True,
):
self.model: APIKey = APIKey(
**{"in": APIKeyIn.cookie}, name=name, description=description
**{"in": APIKeyIn.cookie}, # type: ignore[arg-type]
name=name,
description=description,
)
self.scheme_name = scheme_name or self.__class__.__name__
self.auto_error = auto_error

25
fastapi/security/oauth2.py

@ -1,4 +1,4 @@
from typing import Any, Dict, List, Optional, Union
from typing import Any, Dict, List, Optional, Union, cast
from fastapi.exceptions import HTTPException
from fastapi.openapi.models import OAuth2 as OAuth2Model
@ -121,7 +121,9 @@ class OAuth2(SecurityBase):
description: Optional[str] = None,
auto_error: bool = True,
):
self.model = OAuth2Model(flows=flows, description=description)
self.model = OAuth2Model(
flows=cast(OAuthFlowsModel, flows), description=description
)
self.scheme_name = scheme_name or self.__class__.__name__
self.auto_error = auto_error
@ -148,7 +150,9 @@ class OAuth2PasswordBearer(OAuth2):
):
if not scopes:
scopes = {}
flows = OAuthFlowsModel(password={"tokenUrl": tokenUrl, "scopes": scopes})
flows = OAuthFlowsModel(
password=cast(Any, {"tokenUrl": tokenUrl, "scopes": scopes})
)
super().__init__(
flows=flows,
scheme_name=scheme_name,
@ -185,12 +189,15 @@ class OAuth2AuthorizationCodeBearer(OAuth2):
if not scopes:
scopes = {}
flows = OAuthFlowsModel(
authorizationCode={
"authorizationUrl": authorizationUrl,
"tokenUrl": tokenUrl,
"refreshUrl": refreshUrl,
"scopes": scopes,
}
authorizationCode=cast(
Any,
{
"authorizationUrl": authorizationUrl,
"tokenUrl": tokenUrl,
"refreshUrl": refreshUrl,
"scopes": scopes,
},
)
)
super().__init__(
flows=flows,

47
pyproject.toml

@ -51,47 +51,6 @@ Homepage = "https://github.com/tiangolo/fastapi"
Documentation = "https://fastapi.tiangolo.com/"
[project.optional-dependencies]
test = [
"pytest >=7.1.3,<8.0.0",
"coverage[toml] >= 6.5.0,< 8.0",
"mypy ==0.982",
"ruff ==0.0.138",
"black == 23.1.0",
"isort >=5.0.6,<6.0.0",
"httpx >=0.23.0,<0.24.0",
"email_validator >=1.1.1,<2.0.0",
# TODO: once removing databases from tutorial, upgrade SQLAlchemy
# probably when including SQLModel
"sqlalchemy >=1.3.18,<1.4.43",
"peewee >=3.13.3,<4.0.0",
"databases[sqlite] >=0.3.2,<0.7.0",
"orjson >=3.2.1,<4.0.0",
"ujson >=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0",
"python-multipart >=0.0.5,<0.0.7",
"flask >=1.1.2,<3.0.0",
"anyio[trio] >=3.2.1,<4.0.0",
"python-jose[cryptography] >=3.3.0,<4.0.0",
"pyyaml >=5.3.1,<7.0.0",
"passlib[bcrypt] >=1.7.2,<2.0.0",
# types
"types-ujson ==5.7.0.1",
"types-orjson ==3.6.2",
]
doc = [
"mkdocs >=1.1.2,<2.0.0",
"mkdocs-material >=8.1.4,<9.0.0",
"mdx-include >=1.4.1,<2.0.0",
"mkdocs-markdownextradata-plugin >=0.1.7,<0.3.0",
"typer-cli >=0.0.13,<0.0.14",
"typer[all] >=0.6.1,<0.8.0",
"pyyaml >=5.3.1,<7.0.0",
]
dev = [
"ruff ==0.0.138",
"uvicorn[standard] >=0.12.0,<0.21.0",
"pre-commit >=2.17.0,<3.0.0",
]
all = [
"httpx >=0.23.0",
"jinja2 >=2.11.2",
@ -107,10 +66,6 @@ all = [
[tool.hatch.version]
path = "fastapi/__init__.py"
[tool.isort]
profile = "black"
known_third_party = ["fastapi", "pydantic", "starlette"]
[tool.mypy]
strict = true
@ -166,7 +121,7 @@ select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
# "I", # isort
"I", # isort
"C", # flake8-comprehensions
"B", # flake8-bugbear
]

8
requirements-docs.txt

@ -0,0 +1,8 @@
-e .
mkdocs >=1.1.2,<2.0.0
mkdocs-material >=8.1.4,<9.0.0
mdx-include >=1.4.1,<2.0.0
mkdocs-markdownextradata-plugin >=0.1.7,<0.3.0
typer-cli >=0.0.13,<0.0.14
typer[all] >=0.6.1,<0.8.0
pyyaml >=5.3.1,<7.0.0

25
requirements-tests.txt

@ -0,0 +1,25 @@
-e .
pytest >=7.1.3,<8.0.0
coverage[toml] >= 6.5.0,< 8.0
mypy ==1.3.0
ruff ==0.0.272
black == 23.3.0
httpx >=0.23.0,<0.24.0
email_validator >=1.1.1,<2.0.0
# TODO: once removing databases from tutorial, upgrade SQLAlchemy
# probably when including SQLModel
sqlalchemy >=1.3.18,<1.4.43
peewee >=3.13.3,<4.0.0
databases[sqlite] >=0.3.2,<0.7.0
orjson >=3.2.1,<4.0.0
ujson >=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0
python-multipart >=0.0.5,<0.0.7
flask >=1.1.2,<3.0.0
anyio[trio] >=3.2.1,<4.0.0
python-jose[cryptography] >=3.3.0,<4.0.0
pyyaml >=5.3.1,<7.0.0
passlib[bcrypt] >=1.7.2,<2.0.0
# types
types-ujson ==5.7.0.1
types-orjson ==3.6.2

5
requirements.txt

@ -0,0 +1,5 @@
-e .[all]
-r requirements-tests.txt
-r requirements-docs.txt
uvicorn[standard] >=0.12.0,<0.21.0
pre-commit >=2.17.0,<3.0.0

2
scripts/build-docs.sh

@ -3,4 +3,6 @@
set -e
set -x
# Check README.md is up to date
python ./scripts/docs.py verify-readme
python ./scripts/docs.py build-all

1
scripts/format.sh

@ -3,4 +3,3 @@ set -x
ruff fastapi tests docs_src scripts --fix
black fastapi tests docs_src scripts
isort fastapi tests docs_src scripts

1
scripts/lint.sh

@ -6,4 +6,3 @@ set -x
mypy fastapi
ruff fastapi tests docs_src scripts
black fastapi tests --check
isort fastapi tests docs_src scripts --check-only

2
scripts/test.sh

@ -3,7 +3,5 @@
set -e
set -x
# Check README.md is up to date
python ./scripts/docs.py verify-readme
export PYTHONPATH=./docs_src
coverage run -m pytest tests ${@}

3
tests/test_empty_router.py

@ -1,5 +1,6 @@
import pytest
from fastapi import APIRouter, FastAPI
from fastapi.exceptions import FastAPIError
from fastapi.testclient import TestClient
app = FastAPI()
@ -31,5 +32,5 @@ def test_use_empty():
def test_include_empty():
# if both include and router.path are empty - it should raise exception
with pytest.raises(Exception):
with pytest.raises(FastAPIError):
app.include_router(router)

73
tests/test_ws_dependencies.py

@ -0,0 +1,73 @@
import json
from typing import List
from fastapi import APIRouter, Depends, FastAPI, WebSocket
from fastapi.testclient import TestClient
from typing_extensions import Annotated
def dependency_list() -> List[str]:
return []
DepList = Annotated[List[str], Depends(dependency_list)]
def create_dependency(name: str):
def fun(deps: DepList):
deps.append(name)
return Depends(fun)
router = APIRouter(dependencies=[create_dependency("router")])
prefix_router = APIRouter(dependencies=[create_dependency("prefix_router")])
app = FastAPI(dependencies=[create_dependency("app")])
@app.websocket("/", dependencies=[create_dependency("index")])
async def index(websocket: WebSocket, deps: DepList):
await websocket.accept()
await websocket.send_text(json.dumps(deps))
await websocket.close()
@router.websocket("/router", dependencies=[create_dependency("routerindex")])
async def routerindex(websocket: WebSocket, deps: DepList):
await websocket.accept()
await websocket.send_text(json.dumps(deps))
await websocket.close()
@prefix_router.websocket("/", dependencies=[create_dependency("routerprefixindex")])
async def routerprefixindex(websocket: WebSocket, deps: DepList):
await websocket.accept()
await websocket.send_text(json.dumps(deps))
await websocket.close()
app.include_router(router, dependencies=[create_dependency("router2")])
app.include_router(
prefix_router, prefix="/prefix", dependencies=[create_dependency("prefix_router2")]
)
def test_index():
client = TestClient(app)
with client.websocket_connect("/") as websocket:
data = json.loads(websocket.receive_text())
assert data == ["app", "index"]
def test_routerindex():
client = TestClient(app)
with client.websocket_connect("/router") as websocket:
data = json.loads(websocket.receive_text())
assert data == ["app", "router2", "router", "routerindex"]
def test_routerprefixindex():
client = TestClient(app)
with client.websocket_connect("/prefix/") as websocket:
data = json.loads(websocket.receive_text())
assert data == ["app", "prefix_router2", "prefix_router", "routerprefixindex"]

152
tests/test_ws_router.py

@ -1,4 +1,16 @@
from fastapi import APIRouter, Depends, FastAPI, WebSocket
import functools
import pytest
from fastapi import (
APIRouter,
Depends,
FastAPI,
Header,
WebSocket,
WebSocketDisconnect,
status,
)
from fastapi.middleware import Middleware
from fastapi.testclient import TestClient
router = APIRouter()
@ -63,9 +75,44 @@ async def router_native_prefix_ws(websocket: WebSocket):
await websocket.close()
app.include_router(router)
app.include_router(prefix_router, prefix="/prefix")
app.include_router(native_prefix_route)
async def ws_dependency_err():
raise NotImplementedError()
@router.websocket("/depends-err/")
async def router_ws_depends_err(websocket: WebSocket, data=Depends(ws_dependency_err)):
pass # pragma: no cover
async def ws_dependency_validate(x_missing: str = Header()):
pass # pragma: no cover
@router.websocket("/depends-validate/")
async def router_ws_depends_validate(
websocket: WebSocket, data=Depends(ws_dependency_validate)
):
pass # pragma: no cover
class CustomError(Exception):
pass
@router.websocket("/custom_error/")
async def router_ws_custom_error(websocket: WebSocket):
raise CustomError()
def make_app(app=None, **kwargs):
app = app or FastAPI(**kwargs)
app.include_router(router)
app.include_router(prefix_router, prefix="/prefix")
app.include_router(native_prefix_route)
return app
app = make_app(app)
def test_app():
@ -125,3 +172,100 @@ def test_router_with_params():
assert data == "path/to/file"
data = websocket.receive_text()
assert data == "a_query_param"
def test_wrong_uri():
"""
Verify that a websocket connection to a non-existent endpoing returns in a shutdown
"""
client = TestClient(app)
with pytest.raises(WebSocketDisconnect) as e:
with client.websocket_connect("/no-router/"):
pass # pragma: no cover
assert e.value.code == status.WS_1000_NORMAL_CLOSURE
def websocket_middleware(middleware_func):
"""
Helper to create a Starlette pure websocket middleware
"""
def middleware_constructor(app):
@functools.wraps(app)
async def wrapped_app(scope, receive, send):
if scope["type"] != "websocket":
return await app(scope, receive, send) # pragma: no cover
async def call_next():
return await app(scope, receive, send)
websocket = WebSocket(scope, receive=receive, send=send)
return await middleware_func(websocket, call_next)
return wrapped_app
return middleware_constructor
def test_depend_validation():
"""
Verify that a validation in a dependency invokes the correct exception handler
"""
caught = []
@websocket_middleware
async def catcher(websocket, call_next):
try:
return await call_next()
except Exception as e: # pragma: no cover
caught.append(e)
raise
myapp = make_app(middleware=[Middleware(catcher)])
client = TestClient(myapp)
with pytest.raises(WebSocketDisconnect) as e:
with client.websocket_connect("/depends-validate/"):
pass # pragma: no cover
# the validation error does produce a close message
assert e.value.code == status.WS_1008_POLICY_VIOLATION
# and no error is leaked
assert caught == []
def test_depend_err_middleware():
"""
Verify that it is possible to write custom WebSocket middleware to catch errors
"""
@websocket_middleware
async def errorhandler(websocket: WebSocket, call_next):
try:
return await call_next()
except Exception as e:
await websocket.close(code=status.WS_1006_ABNORMAL_CLOSURE, reason=repr(e))
myapp = make_app(middleware=[Middleware(errorhandler)])
client = TestClient(myapp)
with pytest.raises(WebSocketDisconnect) as e:
with client.websocket_connect("/depends-err/"):
pass # pragma: no cover
assert e.value.code == status.WS_1006_ABNORMAL_CLOSURE
assert "NotImplementedError" in e.value.reason
def test_depend_err_handler():
"""
Verify that it is possible to write custom WebSocket middleware to catch errors
"""
async def custom_handler(websocket: WebSocket, exc: CustomError) -> None:
await websocket.close(1002, "foo")
myapp = make_app(exception_handlers={CustomError: custom_handler})
client = TestClient(myapp)
with pytest.raises(WebSocketDisconnect) as e:
with client.websocket_connect("/custom_error/"):
pass # pragma: no cover
assert e.value.code == 1002
assert "foo" in e.value.reason

Loading…
Cancel
Save