diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 38df75928..bf88d59b1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -35,7 +35,7 @@ jobs: TIANGOLO_BUILD_PACKAGE: ${{ matrix.package }} run: python -m build - name: Publish - uses: pypa/gh-action-pypi-publish@v1.12.3 + uses: pypa/gh-action-pypi-publish@v1.12.4 - name: Dump GitHub context env: GITHUB_CONTEXT: ${{ toJson(github) }} diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 933b7271e..f25648b9a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,8 +7,30 @@ hide: ## Latest Changes +### Translations + +* 🌐 Update Russian translation for `docs/ru/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md`. PR [#13252](https://github.com/fastapi/fastapi/pull/13252) by [@Rishat-F](https://github.com/Rishat-F). +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/bigger-applications.md`. PR [#13154](https://github.com/fastapi/fastapi/pull/13154) by [@alv2017](https://github.com/alv2017). + +### Internal + +* ⬆ Bump pypa/gh-action-pypi-publish from 1.12.3 to 1.12.4. PR [#13251](https://github.com/fastapi/fastapi/pull/13251) by [@dependabot[bot]](https://github.com/apps/dependabot). + +## 0.115.7 + +### Upgrades + +* ⬆️ Upgrade `python-multipart` to >=0.0.18. PR [#13219](https://github.com/fastapi/fastapi/pull/13219) by [@DanielKusyDev](https://github.com/DanielKusyDev). +* ⬆️ Bump Starlette to allow up to 0.45.0: `>=0.40.0,<0.46.0`. PR [#13117](https://github.com/fastapi/fastapi/pull/13117) by [@Kludex](https://github.com/Kludex). +* ⬆️ Upgrade `jinja2` to >=3.1.5. PR [#13194](https://github.com/fastapi/fastapi/pull/13194) by [@DanielKusyDev](https://github.com/DanielKusyDev). + ### Refactors +* βœ… Simplify tests for websockets. PR [#13202](https://github.com/fastapi/fastapi/pull/13202) by [@alejsdev](https://github.com/alejsdev). +* βœ… Simplify tests for request_form_models . PR [#13183](https://github.com/fastapi/fastapi/pull/13183) by [@alejsdev](https://github.com/alejsdev). +* βœ… Simplify tests for separate_openapi_schemas. PR [#13201](https://github.com/fastapi/fastapi/pull/13201) by [@alejsdev](https://github.com/alejsdev). +* βœ… Simplify tests for security. PR [#13200](https://github.com/fastapi/fastapi/pull/13200) by [@alejsdev](https://github.com/alejsdev). +* βœ… Simplify tests for schema_extra_example. PR [#13197](https://github.com/fastapi/fastapi/pull/13197) by [@alejsdev](https://github.com/alejsdev). * βœ… Simplify tests for request_model. PR [#13195](https://github.com/fastapi/fastapi/pull/13195) by [@alejsdev](https://github.com/alejsdev). * βœ… Simplify tests for request_forms_and_files. PR [#13185](https://github.com/fastapi/fastapi/pull/13185) by [@alejsdev](https://github.com/alejsdev). * βœ… Simplify tests for request_forms. PR [#13184](https://github.com/fastapi/fastapi/pull/13184) by [@alejsdev](https://github.com/alejsdev). @@ -44,6 +66,7 @@ hide: ### Translations +* 🌐 Update Portuguese Translation for `docs/pt/docs/tutorial/request-forms.md`. PR [#13216](https://github.com/fastapi/fastapi/pull/13216) by [@Joao-Pedro-P-Holanda](https://github.com/Joao-Pedro-P-Holanda). * 🌐 Update Portuguese translation for `docs/pt/docs/advanced/settings.md`. PR [#13209](https://github.com/fastapi/fastapi/pull/13209) by [@ceb10n](https://github.com/ceb10n). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/security/oauth2-jwt.md`. PR [#13205](https://github.com/fastapi/fastapi/pull/13205) by [@ceb10n](https://github.com/ceb10n). * 🌐 Add Indonesian translation for `docs/id/docs/index.md`. PR [#13191](https://github.com/fastapi/fastapi/pull/13191) by [@gerry-sabar](https://github.com/gerry-sabar). @@ -98,6 +121,7 @@ hide: ### Internal +* πŸ”§ Add Pydantic 2 trove classifier. PR [#13199](https://github.com/fastapi/fastapi/pull/13199) by [@johnthagen](https://github.com/johnthagen). * πŸ‘₯ Update FastAPI People - Sponsors. PR [#13231](https://github.com/fastapi/fastapi/pull/13231) by [@tiangolo](https://github.com/tiangolo). * πŸ‘· Refactor FastAPI People Sponsors to use 2 tokens. PR [#13228](https://github.com/fastapi/fastapi/pull/13228) by [@tiangolo](https://github.com/tiangolo). * πŸ‘· Update token for FastAPI People - Sponsors. PR [#13225](https://github.com/fastapi/fastapi/pull/13225) by [@tiangolo](https://github.com/tiangolo). diff --git a/docs/pt/docs/tutorial/request-forms.md b/docs/pt/docs/tutorial/request-forms.md index 756ceb581..572ddf003 100644 --- a/docs/pt/docs/tutorial/request-forms.md +++ b/docs/pt/docs/tutorial/request-forms.md @@ -6,7 +6,11 @@ Quando vocΓͺ precisar receber campos de formulΓ‘rio ao invΓ©s de JSON, vocΓͺ pod Para usar formulΓ‘rios, primeiro instale `python-multipart`. -Ex: `pip install python-multipart`. +Lembre-se de criar um [ambiente virtual](../virtual-environments.md){.internal-link target=_blank}, ativΓ‘-lo e entΓ£o instalar a dependΓͺncia, por exemplo: + +```console +$ pip install python-multipart +``` /// diff --git a/docs/ru/docs/tutorial/bigger-applications.md b/docs/ru/docs/tutorial/bigger-applications.md new file mode 100644 index 000000000..7c3dc288f --- /dev/null +++ b/docs/ru/docs/tutorial/bigger-applications.md @@ -0,0 +1,556 @@ +# Π‘ΠΎΠ»ΡŒΡˆΠΈΠ΅ прилоТСния, Π² ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Ρ… ΠΌΠ½ΠΎΠ³ΠΎ Ρ„Π°ΠΉΠ»ΠΎΠ² + +ΠŸΡ€ΠΈ построСнии прилоТСния ΠΈΠ»ΠΈ Π²Π΅Π±-API Π½Π°ΠΌ Ρ€Π΅Π΄ΠΊΠΎ удаСтся ΠΏΠΎΠΌΠ΅ΡΡ‚ΠΈΡ‚ΡŒ всё Π² ΠΎΠ΄ΠΈΠ½ Ρ„Π°ΠΉΠ». + +**FastAPI** прСдоставляСт ΡƒΠ΄ΠΎΠ±Π½Ρ‹ΠΉ инструмСнтарий, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ позволяСт Π½Π°ΠΌ ΡΡ‚Ρ€ΡƒΠΊΡ‚ΡƒΡ€ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅, сохраняя ΠΏΡ€ΠΈ этом всю Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌΡƒΡŽ Π³ΠΈΠ±ΠΊΠΎΡΡ‚ΡŒ. + +/// info | ΠŸΡ€ΠΈΠΌΠ΅Ρ‡Π°Π½ΠΈΠ΅ + +Если Π²Ρ‹ Ρ€Π°Π½ΡŒΡˆΠ΅ использовали Flask, Ρ‚ΠΎ это Π°Π½Π°Π»ΠΎΠ³ шаблонов Flask (Flask's Blueprints). + +/// + +## ΠŸΡ€ΠΈΠΌΠ΅Ρ€ структуры прилоТСния + +Π”Π°Π²Π°ΠΉΡ‚Π΅ ΠΏΡ€Π΅Π΄ΠΏΠΎΠ»ΠΎΠΆΠΈΠΌ, Ρ‡Ρ‚ΠΎ нашС ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ ΠΈΠΌΠ΅Π΅Ρ‚ ΡΠ»Π΅Π΄ΡƒΡŽΡ‰ΡƒΡŽ структуру: + +``` +. +β”œβ”€β”€ app +β”‚Β Β  β”œβ”€β”€ __init__.py +β”‚Β Β  β”œβ”€β”€ main.py +β”‚Β Β  β”œβ”€β”€ dependencies.py +β”‚Β Β  └── routers +β”‚Β Β  β”‚ β”œβ”€β”€ __init__.py +β”‚Β Β  β”‚ β”œβ”€β”€ items.py +β”‚Β Β  β”‚ └── users.py +β”‚Β Β  └── internal +β”‚Β Β  β”œβ”€β”€ __init__.py +β”‚Β Β  └── admin.py +``` + +/// tip | Подсказка + +ΠžΠ±Ρ€Π°Ρ‚ΠΈΡ‚Π΅ Π²Π½ΠΈΠΌΠ°Π½ΠΈΠ΅, Ρ‡Ρ‚ΠΎ Π² ΠΊΠ°ΠΆΠ΄ΠΎΠΌ ΠΊΠ°Ρ‚Π°Π»ΠΎΠ³Π΅ ΠΈ ΠΏΠΎΠ΄ΠΊΠ°Ρ‚Π°Π»ΠΎΠ³Π΅ имССтся Ρ„Π°ΠΉΠ» `__init__.py` + +Π­Ρ‚ΠΎ ΠΊΠ°ΠΊ Ρ€Π°Π· Ρ‚ΠΎ, Ρ‡Ρ‚ΠΎ позволяСт ΠΈΠΌΠΏΠΎΡ€Ρ‚ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ ΠΊΠΎΠ΄ ΠΈΠ· ΠΎΠ΄Π½ΠΎΠ³ΠΎ Ρ„Π°ΠΉΠ»Π° Π² Π΄Ρ€ΡƒΠ³ΠΎΠΉ. + +НапримСр, Π² Ρ„Π°ΠΉΠ»Π΅ `app/main.py` ΠΌΠΎΠΆΠ΅Ρ‚ Π±Ρ‹Ρ‚ΡŒ ΡΠ»Π΅Π΄ΡƒΡŽΡ‰Π°Ρ строка: + +``` +from app.routers import items +``` + +/// + +* Всё помСщаСтся Π² ΠΊΠ°Ρ‚Π°Π»ΠΎΠ³Π΅ `app`. Π’ Π½Ρ‘ΠΌ Ρ‚Π°ΠΊΠΆΠ΅ находится пустой Ρ„Π°ΠΉΠ» `app/__init__.py`. Π’Π°ΠΊΠΈΠΌ ΠΎΠ±Ρ€Π°Π·ΠΎΠΌ, `app` являСтся "Python-ΠΏΠ°ΠΊΠ΅Ρ‚ΠΎΠΌ" (ΠΊΠΎΠ»Π»Π΅ΠΊΡ†ΠΈΠ΅ΠΉ ΠΌΠΎΠ΄ΡƒΠ»Π΅ΠΉ Python). +* Он содСрТит Ρ„Π°ΠΉΠ» `app/main.py`. Π”Π°Π½Π½Ρ‹ΠΉ Ρ„Π°ΠΉΠ» являСтся Ρ‡Π°ΡΡ‚ΡŒΡŽ ΠΏΠ°ΠΊΠ΅Ρ‚Π° (Ρ‚.Π΅. находится Π²Π½ΡƒΡ‚Ρ€ΠΈ ΠΊΠ°Ρ‚Π°Π»ΠΎΠ³Π°, содСрТащСго Ρ„Π°ΠΉΠ» `__init__.py`), ΠΈ, соотвСтствСнно, ΠΎΠ½ являСтся ΠΌΠΎΠ΄ΡƒΠ»Π΅ΠΌ ΠΏΠ°ΠΊΠ΅Ρ‚Π°: `app.main`. +* Он Ρ‚Π°ΠΊΠΆΠ΅ содСрТит Ρ„Π°ΠΉΠ» `app/dependencies.py`, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ Ρ‚Π°ΠΊΠΆΠ΅, ΠΊΠ°ΠΊ ΠΈ `app/main.py`, являСтся ΠΌΠΎΠ΄ΡƒΠ»Π΅ΠΌ: `app.dependencies`. +* Π—Π΄Π΅ΡΡŒ Ρ‚Π°ΠΊΠΆΠ΅ находится ΠΏΠΎΠ΄ΠΊΠ°Ρ‚Π°Π»ΠΎΠ³ `app/routers/`, содСрТащий `__init__.py`. Он являСтся суб-ΠΏΠ°ΠΊΠ΅Ρ‚ΠΎΠΌ: `app.routers`. +* Π€Π°ΠΉΠ» `app/routers/items.py` находится Π²Π½ΡƒΡ‚Ρ€ΠΈ ΠΏΠ°ΠΊΠ΅Ρ‚Π° `app/routers/`. Π’Π°ΠΊΠΈΠΌ ΠΎΠ±Ρ€Π°Π·ΠΎΠΌ, ΠΎΠ½ являСтся суб-ΠΌΠΎΠ΄ΡƒΠ»Π΅ΠΌ: `app.routers.items`. +* Π’ΠΎΡ‡Π½ΠΎ Ρ‚Π°ΠΊΠΆΠ΅ `app/routers/users.py` являСтся Π΅Ρ‰Ρ‘ ΠΎΠ΄Π½ΠΈΠΌ суб-ΠΌΠΎΠ΄ΡƒΠ»Π΅ΠΌ: `app.routers.users`. +* ΠŸΠΎΠ΄ΠΊΠ°Ρ‚Π°Π»ΠΎΠ³ `app/internal/`, содСрТащий Ρ„Π°ΠΉΠ» `__init__.py`, являСтся Π΅Ρ‰Ρ‘ ΠΎΠ΄Π½ΠΈΠΌ суб-ΠΏΠ°ΠΊΠ΅Ρ‚ΠΎΠΌ: `app.internal`. +* А Ρ„Π°ΠΉΠ» `app/internal/admin.py` являСтся Π΅Ρ‰Ρ‘ ΠΎΠ΄Π½ΠΈΠΌ суб-ΠΌΠΎΠ΄ΡƒΠ»Π΅ΠΌ: `app.internal.admin`. + + + +Π’Π° ΠΆΠ΅ самая файловая структура прилоТСния, Π½ΠΎ с коммСнтариями: + +``` +. +β”œβ”€β”€ app # "app" ΠΏΠ°ΠΊΠ΅Ρ‚ +β”‚Β Β  β”œβ”€β”€ __init__.py # этот Ρ„Π°ΠΉΠ» ΠΏΡ€Π΅Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚ "app" Π² "Python-ΠΏΠ°ΠΊΠ΅Ρ‚" +β”‚Β Β  β”œβ”€β”€ main.py # ΠΌΠΎΠ΄ΡƒΠ»ΡŒ "main", Π½Π°ΠΏΡ€.: import app.main +β”‚Β Β  β”œβ”€β”€ dependencies.py # ΠΌΠΎΠ΄ΡƒΠ»ΡŒ "dependencies", Π½Π°ΠΏΡ€.: import app.dependencies +β”‚Β Β  └── routers # суб-ΠΏΠ°ΠΊΠ΅Ρ‚ "routers" +β”‚Β Β  β”‚ β”œβ”€β”€ __init__.py # ΠΏΡ€Π΅Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚ "routers" Π² суб-ΠΏΠ°ΠΊΠ΅Ρ‚ +β”‚Β Β  β”‚ β”œβ”€β”€ items.py # суб-ΠΌΠΎΠ΄ΡƒΠ»ΡŒ "items", Π½Π°ΠΏΡ€.: import app.routers.items +β”‚Β Β  β”‚ └── users.py # суб-ΠΌΠΎΠ΄ΡƒΠ»ΡŒ "users", Π½Π°ΠΏΡ€.: import app.routers.users +β”‚Β Β  └── internal # суб-ΠΏΠ°ΠΊΠ΅Ρ‚ "internal" +β”‚Β Β  β”œβ”€β”€ __init__.py # ΠΏΡ€Π΅Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚ "internal" Π² суб-ΠΏΠ°ΠΊΠ΅Ρ‚ +β”‚Β Β  └── admin.py # суб-ΠΌΠΎΠ΄ΡƒΠ»ΡŒ "admin", Π½Π°ΠΏΡ€.: import app.internal.admin +``` + +## `APIRouter` + +Π”Π°Π²Π°ΠΉΡ‚Π΅ ΠΏΡ€Π΅Π΄ΠΏΠΎΠ»ΠΎΠΆΠΈΠΌ, Ρ‡Ρ‚ΠΎ для Ρ€Π°Π±ΠΎΡ‚Ρ‹ с ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡΠΌΠΈ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ΡΡ ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½Ρ‹ΠΉ Ρ„Π°ΠΉΠ» (суб-ΠΌΠΎΠ΄ΡƒΠ»ΡŒ) `/app/routers/users.py`. + +Для Π»ΡƒΡ‡ΡˆΠ΅ΠΉ ΠΎΡ€Π³Π°Π½ΠΈΠ·Π°Ρ†ΠΈΠΈ прилоТСния, Π²Ρ‹ Ρ…ΠΎΡ‚ΠΈΡ‚Π΅ ΠΎΡ‚Π΄Π΅Π»ΠΈΡ‚ΡŒ ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΈ ΠΏΡƒΡ‚ΠΈ, связанныС с ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡΠΌΠΈ, ΠΎΡ‚ ΠΎΡΡ‚Π°Π»ΡŒΠ½ΠΎΠ³ΠΎ ΠΊΠΎΠ΄Π°. + +Но Ρ‚Π°ΠΊ, Ρ‡Ρ‚ΠΎΠ±Ρ‹ эти ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΈ ΠΏΠΎ-ΠΏΡ€Π΅ΠΆΠ½Π΅ΠΌΡƒ ΠΎΡΡ‚Π°Π²Π°Π»ΠΈΡΡŒ Ρ‡Π°ΡΡ‚ΡŒΡŽ **FastAPI** прилоТСния/Π²Π΅Π±-API (Ρ‡Π°ΡΡ‚ΡŒΡŽ ΠΎΠ΄Π½ΠΎΠ³ΠΎ ΠΏΠ°ΠΊΠ΅Ρ‚Π°) + +Π‘ ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ `APIRouter` Π²Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ ΡΠΎΠ·Π΄Π°Ρ‚ΡŒ *ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΈ ΠΏΡƒΡ‚ΠΈ* (*эндпоинты*) для Π΄Π°Π½Π½ΠΎΠ³ΠΎ модуля. + + +### Π˜ΠΌΠΏΠΎΡ€Ρ‚ `APIRouter` + +Π’ΠΎΡ‡Π½ΠΎ Ρ‚Π°ΠΊΠΆΠ΅, ΠΊΠ°ΠΊ ΠΈ Π² случаС с классом `FastAPI`, Π²Π°ΠΌ Π½ΡƒΠΆΠ½ΠΎ ΠΈΠΌΠΏΠΎΡ€Ρ‚ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ ΠΈ ΡΠΎΠ·Π΄Π°Ρ‚ΡŒ ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ класса `APIRouter`. + +```Python hl_lines="1 3" title="app/routers/users.py" +{!../../docs_src/bigger_applications/app/routers/users.py!} +``` + +### Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ *эндпоинтов* с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ `APIRouter` + +Π’ дальнСйшСм ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ `APIRouter` для объявлСния *эндпоинтов*, Ρ‚ΠΎΡ‡Π½ΠΎ Ρ‚Π°ΠΊΠΆΠ΅, ΠΊΠ°ΠΊ Π²Ρ‹ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚Π΅ класс `FastAPI`: + +```Python hl_lines="6 11 16" title="app/routers/users.py" +{!../../docs_src/bigger_applications/app/routers/users.py!} +``` + +Π’Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ Π΄ΡƒΠΌΠ°Ρ‚ΡŒ ΠΎΠ± `APIRouter` ΠΊΠ°ΠΊ ΠΎΠ± "ΡƒΠΌΠ΅Π½ΡŒΡˆΠ΅Π½Π½ΠΎΠΉ вСрсии" класса FastAPI`. + +`APIRouter` ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°Π΅Ρ‚ всС Ρ‚Π΅ ΠΆΠ΅ самыС ΠΎΠΏΡ†ΠΈΠΈ. + +`APIRouter` ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°Π΅Ρ‚ всС Ρ‚Π΅ ΠΆΠ΅ самыС ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Ρ‹, Ρ‚Π°ΠΊΠΈΠ΅ ΠΊΠ°ΠΊ `parameters`, `responses`, `dependencies`, `tags`, ΠΈ Ρ‚. Π΄. + +/// tip | Подсказка + +Π’ Π΄Π°Π½Π½ΠΎΠΌ ΠΏΡ€ΠΈΠΌΠ΅Ρ€Π΅, Π² качСствС названия ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½ΠΎΠΉ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ΡΡ `router`, Π½ΠΎ Π²Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ любоС Π΄Ρ€ΡƒΠ³ΠΎΠ΅ имя. + +/// + +ΠœΡ‹ собираСмся ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡ΠΈΡ‚ΡŒ Π΄Π°Π½Π½Ρ‹ΠΉ `APIRouter` ΠΊ Π½Π°ΡˆΠ΅ΠΌΡƒ основному ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΡŽ Π½Π° `FastAPI`, Π½ΠΎ сначала Π΄Π°Π²Π°ΠΉΡ‚Π΅ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΈΠΌ зависимости ΠΈ создадим Π΅Ρ‰Ρ‘ ΠΎΠ΄ΠΈΠ½ ΠΌΠΎΠ΄ΡƒΠ»ΡŒ с `APIRouter`. + +## Зависимости + +Нам понадобятся Π½Π΅ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ зависимости, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ ΠΌΡ‹ Π±ΡƒΠ΄Π΅ΠΌ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ Π² Ρ€Π°Π·Π½Ρ‹Ρ… мСстах нашСго прилоТСния. + +ΠœΡ‹ помСстим ΠΈΡ… Π² ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½Ρ‹ΠΉ ΠΌΠΎΠ΄ΡƒΠ»ΡŒ `dependencies` (`app/dependencies.py`). + +Π’Π΅ΠΏΠ΅Ρ€ΡŒ ΠΌΡ‹ Π²ΠΎΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌΡΡ простой Π·Π°Π²ΠΈΡΠΈΠΌΠΎΡΡ‚ΡŒΡŽ, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΏΡ€ΠΎΡ‡ΠΈΡ‚Π°Ρ‚ΡŒ кастомизированный `X-Token` ΠΈΠ· Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΊΠ°: + +//// tab | Python 3.9+ + +```Python hl_lines="3 6-8" title="app/dependencies.py" +{!> ../../docs_src/bigger_applications/app_an_py39/dependencies.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="1 5-7" title="app/dependencies.py" +{!> ../../docs_src/bigger_applications/app_an/dependencies.py!} +``` + +//// + +//// tab | Python 3.8+ non-Annotated + +/// tip | Подсказка + +ΠœΡ‹ Ρ€Π΅ΠΊΠΎΠΌΠ΅Π½Π΄ΡƒΠ΅ΠΌ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ Π²Π΅Ρ€ΡΠΈΡŽ `Annotated`, ΠΊΠΎΠ³Π΄Π° это Π²ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎ. + +/// + +```Python hl_lines="1 4-6" title="app/dependencies.py" +{!> ../../docs_src/bigger_applications/app/dependencies.py!} +``` + +//// + +/// tip | Подсказка + +Для простоты ΠΌΡ‹ воспользовались Π½Π΅ΠΊΠΈΠΌ Π²ΠΎΠΎΠ±Ρ€Π°ΠΆΠ°Π΅ΠΌΡ‹ΠΌ Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊΠΎΠΌ. + +Π’ Ρ€Π΅Π°Π»ΡŒΠ½Ρ‹Ρ… случаях для получСния Π½Π°ΠΈΠ»ΡƒΡ‡ΡˆΠΈΡ… Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚ΠΎΠ² ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ ΠΈΠ½Ρ‚Π΅Π³Ρ€ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹Π΅ ΡƒΡ‚ΠΈΠ»ΠΈΡ‚Ρ‹ обСспСчСния бСзопасности [Security utilities](security/index.md){.internal-link target=_blank}. + +/// + +## Π•Ρ‰Ρ‘ ΠΎΠ΄ΠΈΠ½ ΠΌΠΎΠ΄ΡƒΠ»ΡŒ с `APIRouter` + +Π”Π°Π²Π°ΠΉΡ‚Π΅ Ρ‚Π°ΠΊΠΆΠ΅ ΠΏΡ€Π΅Π΄ΠΏΠΎΠ»ΠΎΠΆΠΈΠΌ, Ρ‡Ρ‚ΠΎ Ρƒ вас Π΅ΡΡ‚ΡŒ *эндпоинты*, ΠΎΡ‚Π²Π΅Ρ‡Π°ΡŽΡ‰ΠΈΠ΅ Π·Π° ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΡƒ "items", ΠΈ ΠΎΠ½ΠΈ находятся Π² ΠΌΠΎΠ΄ΡƒΠ»Π΅ `app/routers/items.py`. + +Π£ вас ΠΎΠΏΡ€Π΅Π΄Π΅Π»Π΅Π½Ρ‹ ΡΠ»Π΅Π΄ΡƒΡŽΡ‰ΠΈΠ΅ *ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΈ ΠΏΡƒΡ‚ΠΈ* (*эндпоинты*): + +* `/items/` +* `/items/{item_id}` + +Π’ΡƒΡ‚ всё Ρ‚ΠΎΡ‡Π½ΠΎ Ρ‚Π°ΠΊΠΆΠ΅, ΠΊΠ°ΠΊ ΠΈ Π² ситуации с `app/routers/users.py`. + +Но Ρ‚Π΅ΠΏΠ΅Ρ€ΡŒ ΠΌΡ‹ Ρ…ΠΎΡ‚ΠΈΠΌ ΠΏΠΎΡΡ‚ΡƒΠΏΠΈΡ‚ΡŒ Π½Π΅ΠΌΠ½ΠΎΠ³ΠΎ ΡƒΠΌΠ½Π΅Π΅ ΠΈ слСгка ΡƒΠΏΡ€ΠΎΡΡ‚ΠΈΡ‚ΡŒ ΠΊΠΎΠ΄. + +ΠœΡ‹ Π·Π½Π°Π΅ΠΌ, Ρ‡Ρ‚ΠΎ всС *эндпоинты* Π΄Π°Π½Π½ΠΎΠ³ΠΎ модуля ΠΈΠΌΠ΅ΡŽΡ‚ Π½Π΅ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ ΠΎΠ±Ρ‰ΠΈΠ΅ свойства: + +* ΠŸΡ€Π΅Ρ„ΠΈΠΊΡ ΠΏΡƒΡ‚ΠΈ: `/items`. +* Π’Π΅Π³ΠΈ: (ΠΎΠ΄ΠΈΠ½ СдинствСнный Ρ‚Π΅Π³: `items`). +* Π”ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ ΠΎΡ‚Π²Π΅Ρ‚Ρ‹ (responses) +* Зависимости: использованиС созданной Π½Π°ΠΌΠΈ зависимости `X-token` + +Π’Π°ΠΊΠΈΠΌ ΠΎΠ±Ρ€Π°Π·ΠΎΠΌ, вмСсто Ρ‚ΠΎΠ³ΠΎ Ρ‡Ρ‚ΠΎΠ±Ρ‹ Π΄ΠΎΠ±Π°Π²Π»ΡΡ‚ΡŒ всС эти свойства Π² Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½ΠΎΠ³ΠΎ *эндпоинта*, +ΠΌΡ‹ Π΄ΠΎΠ±Π°Π²ΠΈΠΌ ΠΈΡ… Π² `APIRouter`. + +```Python hl_lines="5-10 16 21" title="app/routers/items.py" +{!../../docs_src/bigger_applications/app/routers/items.py!} +``` + +Π’Π°ΠΊ ΠΊΠ°ΠΊ ΠΊΠ°ΠΆΠ΄Ρ‹ΠΉ *эндпоинт* начинаСтся с символа `/`: + +```Python hl_lines="1" +@router.get("/{item_id}") +async def read_item(item_id: str): + ... +``` + +...Ρ‚ΠΎ прСфикс Π½Π΅ Π΄ΠΎΠ»ΠΆΠ΅Π½ Π·Π°ΠΊΠ°Π½Ρ‡ΠΈΠ²Π°Ρ‚ΡŒΡΡ символом `/`. + +Π’ нашСм случаС прСфиксом являСтся `/items`. + +ΠœΡ‹ Ρ‚Π°ΠΊΠΆΠ΅ ΠΌΠΎΠΆΠ΅ΠΌ Π΄ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ Π² наш ΠΌΠ°Ρ€ΡˆΡ€ΡƒΡ‚ΠΈΠ·Π°Ρ‚ΠΎΡ€ (router) список `Ρ‚Π΅Π³ΠΎΠ²` (`tags`) ΠΈ Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Ρ… `ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ²` (`responses`), ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ ΡΠ²Π»ΡΡŽΡ‚ΡΡ ΠΎΠ±Ρ‰ΠΈΠΌΠΈ для ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ *эндпоинта*. + +И Π΅Ρ‰Ρ‘ ΠΌΡ‹ ΠΌΠΎΠΆΠ΅ΠΌ Π΄ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ Π² наш ΠΌΠ°Ρ€ΡˆΡ€ΡƒΡ‚ΠΈΠ·Π°Ρ‚ΠΎΡ€ список `зависимостСй`, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π²Ρ‹Π·Ρ‹Π²Π°Ρ‚ΡŒΡΡ ΠΏΡ€ΠΈ ΠΊΠ°ΠΆΠ΄ΠΎΠΌ ΠΎΠ±Ρ€Π°Ρ‰Π΅Π½ΠΈΠΈ ΠΊ *эндпоинтам*. + +/// tip | Подсказка + +ΠžΠ±Ρ€Π°Ρ‚ΠΈΡ‚Π΅ Π²Π½ΠΈΠΌΠ°Π½ΠΈΠ΅, Ρ‡Ρ‚ΠΎ Ρ‚Π°ΠΊΠΆΠ΅, ΠΊΠ°ΠΊ ΠΈ Π² случаС с зависимостями Π² Π΄Π΅ΠΊΠΎΡ€Π°Ρ‚ΠΎΡ€Π°Ρ… *эндпоинтов* ([dependencies in *path operation decorators*](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}), Π½ΠΈΠΊΠ°ΠΊΠΎΠ³ΠΎ значСния Π² *Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ эндпоинта* ΠΏΠ΅Ρ€Π΅Π΄Π°Π½ΠΎ Π½Π΅ Π±ΡƒΠ΄Π΅Ρ‚. + +/// + +Π’ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚Π΅ ΠΌΡ‹ ΠΏΠΎΠ»ΡƒΡ‡ΠΈΠΌ ΡΠ»Π΅Π΄ΡƒΡŽΡ‰ΠΈΠ΅ эндпоинты: + +* `/items/` +* `/items/{item_id}` + +...ΠΊΠ°ΠΊ ΠΌΡ‹ ΠΈ ΠΏΠ»Π°Π½ΠΈΡ€ΠΎΠ²Π°Π»ΠΈ. + +* Они Π±ΡƒΠ΄ΡƒΡ‚ ΠΏΠΎΠΌΠ΅Ρ‡Π΅Π½Ρ‹ Ρ‚Π΅Π³Π°ΠΌΠΈ ΠΈΠ· Π·Π°Π΄Π°Π½Π½ΠΎΠ³ΠΎ списка, Π² нашСм случаС это `"items"`. + * Π­Ρ‚ΠΈ Ρ‚Π΅Π³ΠΈ особСнно ΠΏΠΎΠ»Π΅Π·Π½Ρ‹ для систСмы автоматичСской ΠΈΠ½Ρ‚Π΅Ρ€Π°ΠΊΡ‚ΠΈΠ²Π½ΠΎΠΉ Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚Π°Ρ†ΠΈΠΈ (с использованиСм OpenAPI). +* ΠšΠ°ΠΆΠ΄Ρ‹ΠΉ ΠΈΠ· Π½ΠΈΡ… Π±ΡƒΠ΄Π΅Ρ‚ Π²ΠΊΠ»ΡŽΡ‡Π°Ρ‚ΡŒ ΠΏΡ€Π΅Π΄ΠΎΠΏΡ€Π΅Π΄Π΅Π»Π΅Π½Π½Ρ‹Π΅ ΠΎΡ‚Π²Π΅Ρ‚Ρ‹ `responses`. +* ΠšΠ°ΠΆΠ΄Ρ‹ΠΉ *эндпоинт* Π±ΡƒΠ΄Π΅Ρ‚ ΠΈΠΌΠ΅Ρ‚ΡŒ список зависимостСй (`dependencies`), исполняСмых ΠΏΠ΅Ρ€Π΅Π΄ Π²Ρ‹Π·ΠΎΠ²ΠΎΠΌ *эндпоинта*. + * Если Π²Ρ‹ ΠΎΠΏΡ€Π΅Π΄Π΅Π»ΠΈΠ»ΠΈ зависимости Π² самой ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΈ ΠΏΡƒΡ‚ΠΈ, **Ρ‚ΠΎ ΠΎΠ½Π° Ρ‚Π°ΠΊΠΆΠ΅ Π±ΡƒΠ΄Π΅Ρ‚ Π²Ρ‹ΠΏΠΎΠ»Π½Π΅Π½Π°**. + * Π‘Π½Π°Ρ‡Π°Π»Π° Π²Ρ‹ΠΏΠΎΠ»Π½ΡΡŽΡ‚ΡΡ зависимости ΠΌΠ°Ρ€ΡˆΡ€ΡƒΡ‚ΠΈΠ·Π°Ρ‚ΠΎΡ€Π°, Π·Π°Ρ‚Π΅ΠΌ Π²Ρ‹Π·Ρ‹Π²Π°ΡŽΡ‚ΡΡ зависимости, ΠΎΠΏΡ€Π΅Π΄Π΅Π»Π΅Π½Π½Ρ‹Π΅ Π² Π΄Π΅ΠΊΠΎΡ€Π°Ρ‚ΠΎΡ€Π΅ *эндпоинта* ([`dependencies` in the decorator](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}), ΠΈ, Π½Π°ΠΊΠΎΠ½Π΅Ρ†, ΠΎΠ±Ρ‹Ρ‡Π½Ρ‹Π΅ парамСтричСскиС зависимости. + * Π’Ρ‹ Ρ‚Π°ΠΊΠΆΠ΅ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ Π΄ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ зависимости бСзопасности с областями видимости (`scopes`) [`Security` dependencies with `scopes`](../advanced/security/oauth2-scopes.md){.internal-link target=_blank}. + +/// tip | Подсказка + +НапримСр, с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ зависимостСй Π² `APIRouter` ΠΌΡ‹ ΠΌΠΎΠΆΠ΅ΠΌ ΠΏΠΎΡ‚Ρ€Π΅Π±ΠΎΠ²Π°Ρ‚ΡŒ Π°ΡƒΡ‚Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ†ΠΈΠΈ для доступа ΠΊΠΎ всСй Π³Ρ€ΡƒΠΏΠΏΠ΅ *эндпоинтов*, Π½Π΅ указывая зависимости для ΠΊΠ°ΠΆΠ΄ΠΎΠΉ ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½ΠΎΠΉ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ *эндпоинта*. + +/// + +/// check | Π—Π°ΠΌΠ΅Ρ‚ΠΊΠ° + +ΠŸΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Ρ‹ `prefix`, `tags`, `responses` ΠΈ `dependencies` относятся ΠΊ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΎΠ½Π°Π»Ρƒ **FastAPI**, ΠΏΠΎΠΌΠΎΠ³Π°ΡŽΡ‰Π΅ΠΌΡƒ ΠΈΠ·Π±Π΅ΠΆΠ°Ρ‚ΡŒ дублирования ΠΊΠΎΠ΄Π°. + +/// + +### Π˜ΠΌΠΏΠΎΡ€Ρ‚ зависимостСй + +Наш ΠΊΠΎΠ΄ находится Π² ΠΌΠΎΠ΄ΡƒΠ»Π΅ `app.routers.items` (Ρ„Π°ΠΉΠ» `app/routers/items.py`). + +И Π½Π°ΠΌ Π½ΡƒΠΆΠ½ΠΎ Π²Ρ‹Π·Π²Π°Ρ‚ΡŒ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ зависимости ΠΈΠ· модуля `app.dependencies` (Ρ„Π°ΠΉΠ» `app/dependencies.py`). + +ΠœΡ‹ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌ ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΡŽ ΠΎΡ‚Π½ΠΎΡΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΠ³ΠΎ ΠΈΠΌΠΏΠΎΡ€Ρ‚Π° `..` для ΠΈΠΌΠΏΠΎΡ€Ρ‚Π° зависимости: + +```Python hl_lines="3" title="app/routers/items.py" +{!../../docs_src/bigger_applications/app/routers/items.py!} +``` + +#### Как Ρ€Π°Π±ΠΎΡ‚Π°Π΅Ρ‚ ΠΎΡ‚Π½ΠΎΡΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹ΠΉ ΠΈΠΌΠΏΠΎΡ€Ρ‚? + +/// tip | Подсказка + +Если Π²Ρ‹ прСкрасно Π·Π½Π°Π΅Ρ‚Π΅, ΠΊΠ°ΠΊ Ρ€Π°Π±ΠΎΡ‚Π°Π΅Ρ‚ ΠΈΠΌΠΏΠΎΡ€Ρ‚ Π² Python, Ρ‚ΠΎ ΠΏΠ΅Ρ€Π΅Ρ…ΠΎΠ΄ΠΈΡ‚Π΅ ΠΊ ΡΠ»Π΅Π΄ΡƒΡŽΡ‰Π΅ΠΌΡƒ Ρ€Π°Π·Π΄Π΅Π»Ρƒ. + +/// + +Одна Ρ‚ΠΎΡ‡ΠΊΠ° `.`, ΠΊΠ°ΠΊ Π² Π΄Π°Π½Π½ΠΎΠΌ ΠΏΡ€ΠΈΠΌΠ΅Ρ€Π΅: + +```Python +from .dependencies import get_token_header +``` +ΠΎΠ·Π½Π°Ρ‡Π°Π΅Ρ‚: + +* НачнитС с ΠΏΠ°ΠΊΠ΅Ρ‚Π°, Π² ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠΌ находится Π΄Π°Π½Π½Ρ‹ΠΉ ΠΌΠΎΠ΄ΡƒΠ»ΡŒ (Ρ„Π°ΠΉΠ» `app/routers/items.py` располоТСн Π² ΠΊΠ°Ρ‚Π°Π»ΠΎΠ³Π΅ `app/routers/`)... +* ... Π½Π°ΠΉΠ΄ΠΈΡ‚Π΅ ΠΌΠΎΠ΄ΡƒΠ»ΡŒ `dependencies` (Ρ„Π°ΠΉΠ» `app/routers/dependencies.py`)... +* ... ΠΈ ΠΈΠΌΠΏΠΎΡ€Ρ‚ΠΈΡ€ΡƒΠΉΡ‚Π΅ ΠΈΠ· Π½Π΅Π³ΠΎ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ `get_token_header`. + +К соТалСнию, Ρ‚Π°ΠΊΠΎΠ³ΠΎ Ρ„Π°ΠΉΠ»Π° Π½Π΅ сущСствуСт, ΠΈ наши зависимости находятся Π² Ρ„Π°ΠΉΠ»Π΅ `app/dependencies.py`. + +ВспомнитС, ΠΊΠ°ΠΊ выглядит файловая структура нашСго прилоТСния: + + + +--- + +Π”Π²Π΅ Ρ‚ΠΎΡ‡ΠΊΠΈ `..`, ΠΊΠ°ΠΊ Π² Π΄Π°Π½Π½ΠΎΠΌ ΠΏΡ€ΠΈΠΌΠ΅Ρ€Π΅: + +```Python +from ..dependencies import get_token_header +``` + +ΠΎΠ·Π½Π°Ρ‡Π°ΡŽΡ‚: + +* НачнитС с ΠΏΠ°ΠΊΠ΅Ρ‚Π°, Π² ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠΌ находится Π΄Π°Π½Π½Ρ‹ΠΉ ΠΌΠΎΠ΄ΡƒΠ»ΡŒ (Ρ„Π°ΠΉΠ» `app/routers/items.py` находится Π² ΠΊΠ°Ρ‚Π°Π»ΠΎΠ³Π΅ `app/routers/`)... +* ... ΠΏΠ΅Ρ€Π΅ΠΉΠ΄ΠΈΡ‚Π΅ Π² Ρ€ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΡΠΊΠΈΠΉ ΠΏΠ°ΠΊΠ΅Ρ‚ (ΠΊΠ°Ρ‚Π°Π»ΠΎΠ³ `app/`)... +* ... Π½Π°ΠΉΠ΄ΠΈΡ‚Π΅ Π² Π½Ρ‘ΠΌ ΠΌΠΎΠ΄ΡƒΠ»ΡŒ `dependencies` (Ρ„Π°ΠΉΠ» `app/dependencies.py`)... +* ... ΠΈ ΠΈΠΌΠΏΠΎΡ€Ρ‚ΠΈΡ€ΡƒΠΉΡ‚Π΅ ΠΈΠ· Π½Π΅Π³ΠΎ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ `get_token_header`. + +Π­Ρ‚ΠΎ Ρ€Π°Π±ΠΎΡ‚Π°Π΅Ρ‚ Π²Π΅Ρ€Π½ΠΎ! πŸŽ‰ + +--- + +Аналогично, Ссли Π±Ρ‹ ΠΌΡ‹ использовали Ρ‚Ρ€ΠΈ Ρ‚ΠΎΡ‡ΠΊΠΈ `...`, ΠΊΠ°ΠΊ здСсь: + +```Python +from ...dependencies import get_token_header +``` + +Ρ‚ΠΎ это Π±Ρ‹ ΠΎΠ·Π½Π°Ρ‡Π°Π»ΠΎ: + +* НачнитС с ΠΏΠ°ΠΊΠ΅Ρ‚Π°, Π² ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠΌ находится Π΄Π°Π½Π½Ρ‹ΠΉ ΠΌΠΎΠ΄ΡƒΠ»ΡŒ (Ρ„Π°ΠΉΠ» `app/routers/items.py` находится Π² ΠΊΠ°Ρ‚Π°Π»ΠΎΠ³Π΅ `app/routers/`)... +* ... ΠΏΠ΅Ρ€Π΅ΠΉΠ΄ΠΈΡ‚Π΅ Π² Ρ€ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΡΠΊΠΈΠΉ ΠΏΠ°ΠΊΠ΅Ρ‚ (ΠΊΠ°Ρ‚Π°Π»ΠΎΠ³ `app/`)... +* ... Π·Π°Ρ‚Π΅ΠΌ ΠΏΠ΅Ρ€Π΅ΠΉΠ΄ΠΈΡ‚Π΅ Π² Ρ€ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΡΠΊΠΈΠΉ ΠΏΠ°ΠΊΠ΅Ρ‚ Ρ‚Π΅ΠΊΡƒΡ‰Π΅Π³ΠΎ ΠΏΠ°ΠΊΠ΅Ρ‚Π° (Ρ‚Π°ΠΊΠΎΠ³ΠΎ ΠΏΠ°ΠΊΠ΅Ρ‚Π° Π½Π΅ сущСствуСт, `app` находится Π½Π° самом Π²Π΅Ρ€Ρ…Π½Π΅ΠΌ ΡƒΡ€ΠΎΠ²Π½Π΅ 😱)... +* ... Π½Π°ΠΉΠ΄ΠΈΡ‚Π΅ Π² Π½Ρ‘ΠΌ ΠΌΠΎΠ΄ΡƒΠ»ΡŒ `dependencies` (Ρ„Π°ΠΉΠ» `app/dependencies.py`)... +* ... ΠΈ ΠΈΠΌΠΏΠΎΡ€Ρ‚ΠΈΡ€ΡƒΠΉΡ‚Π΅ ΠΈΠ· Π½Π΅Π³ΠΎ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ `get_token_header`. + +Π­Ρ‚ΠΎ Π±ΡƒΠ΄Π΅Ρ‚ ΠΎΡ‚Π½ΠΎΡΠΈΡ‚ΡŒΡΡ ΠΊ Π½Π΅ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠΌΡƒ ΠΏΠ°ΠΊΠ΅Ρ‚Ρƒ, находящСмуся Π½Π° ΠΎΠ΄ΠΈΠ½ ΡƒΡ€ΠΎΠ²Π΅Π½ΡŒ Π²Ρ‹ΡˆΠ΅ Ρ‡Π΅ΠΌ `app/` ΠΈ содСрТащСму свой собствСнный Ρ„Π°ΠΉΠ» `__init__.py`. Но Π½ΠΈΡ‡Π΅Π³ΠΎ Ρ‚Π°ΠΊΠΎΠ³ΠΎ Ρƒ нас Π½Π΅Ρ‚. ΠŸΠΎΡΡ‚ΠΎΠΌΡƒ это ΠΏΡ€ΠΈΠ²Π΅Π΄Π΅Ρ‚ ΠΊ ошибкС Π² нашСм ΠΏΡ€ΠΈΠΌΠ΅Ρ€Π΅. 🚨 + +Π’Π΅ΠΏΠ΅Ρ€ΡŒ Π²Ρ‹ Π·Π½Π°Π΅Ρ‚Π΅, ΠΊΠ°ΠΊ Ρ€Π°Π±ΠΎΡ‚Π°Π΅Ρ‚ ΠΈΠΌΠΏΠΎΡ€Ρ‚ Π² Python, ΠΈ смоТСтС ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ ΠΎΡ‚Π½ΠΎΡΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΠ΅ ΠΈΠΌΠΏΠΎΡ€Ρ‚ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ Π² своих собствСнных прилоТСниях любого уровня слоТности. πŸ€“ + +### Π”ΠΎΠ±Π°Π²Π»Π΅Π½ΠΈΠ΅ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒΡΠΊΠΈΡ… Ρ‚Π΅Π³ΠΎΠ² (`tags`), ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ² (`responses`) ΠΈ зависимостСй (`dependencies`) + +ΠœΡ‹ Π½Π΅ Π±ΡƒΠ΄Π΅ΠΌ Π΄ΠΎΠ±Π°Π²Π»ΡΡ‚ΡŒ прСфикс `/items` ΠΈ список Ρ‚Π΅Π³ΠΎΠ² `tags=["items"]` для ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ *эндпоинта*, Ρ‚.ΠΊ. ΠΌΡ‹ ΡƒΠΆΠ΅ ΠΈΡ… Π΄ΠΎΠ±Π°Π²ΠΈΠ»ΠΈ с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ `APIRouter`. + +Но ΠΏΠΎΠΌΠΈΠΌΠΎ этого ΠΌΡ‹ ΠΌΠΎΠΆΠ΅ΠΌ Π΄ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ Π½ΠΎΠ²Ρ‹Π΅ Ρ‚Π΅Π³ΠΈ для ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½ΠΎΠ³ΠΎ *эндпоинта*, Π° Ρ‚Π°ΠΊΠΆΠ΅ Π½Π΅ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ ΠΎΡ‚Π²Π΅Ρ‚Ρ‹ (`responses`), Ρ…Π°Ρ€Π°ΠΊΡ‚Π΅Ρ€Π½Ρ‹Π΅ для Π΄Π°Π½Π½ΠΎΠ³ΠΎ *эндпоинта*: + +```Python hl_lines="30-31" title="app/routers/items.py" +{!../../docs_src/bigger_applications/app/routers/items.py!} +``` + +/// tip | Подсказка + +ПослСдний *эндпоинт* Π±ΡƒΠ΄Π΅Ρ‚ ΠΈΠΌΠ΅Ρ‚ΡŒ ΡΠ»Π΅Π΄ΡƒΡŽΡ‰ΡƒΡŽ ΠΊΠΎΠΌΠ±ΠΈΠ½Π°Ρ†ΠΈΡŽ Ρ‚Π΅Π³ΠΎΠ²: `["items", "custom"]`. + +А Ρ‚Π°ΠΊΠΆΠ΅ Π² Π΅Π³ΠΎ Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚Π°Ρ†ΠΈΠΈ Π±ΡƒΠ΄ΡƒΡ‚ ΡΠΎΠ΄Π΅Ρ€ΠΆΠ°Ρ‚ΡŒΡΡ ΠΎΠ±Π° ΠΎΡ‚Π²Π΅Ρ‚Π°: ΠΎΠ΄ΠΈΠ½ для `404` ΠΈ Π΄Ρ€ΡƒΠ³ΠΎΠΉ для `403`. + +/// + +## ΠœΠΎΠ΄ΡƒΠ»ΡŒ main Π² `FastAPI` + +Π’Π΅ΠΏΠ΅Ρ€ΡŒ Π΄Π°Π²Π°ΠΉΡ‚Π΅ посмотрим Π½Π° ΠΌΠΎΠ΄ΡƒΠ»ΡŒ `app/main.py`. + +ИмСнно сюда Π²Ρ‹ ΠΈΠΌΠΏΠΎΡ€Ρ‚ΠΈΡ€ΡƒΠ΅Ρ‚Π΅ ΠΈ ΠΈΠΌΠ΅Π½Π½ΠΎ здСсь Π²Ρ‹ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚Π΅ класс `FastAPI`. + +Π­Ρ‚ΠΎ основной Ρ„Π°ΠΉΠ» вашСго прилоТСния, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ ΠΎΠ±ΡŠΠ΅Π΄ΠΈΠ½ΡΠ΅Ρ‚ всё Π² ΠΎΠ΄Π½ΠΎ Ρ†Π΅Π»ΠΎΠ΅. + +И Ρ‚Π΅ΠΏΠ΅Ρ€ΡŒ, ΠΊΠΎΠ³Π΄Π° большая Ρ‡Π°ΡΡ‚ΡŒ Π»ΠΎΠ³ΠΈΠΊΠΈ прилоТСния Ρ€Π°Π·Π΄Π΅Π»Π΅Π½Π° Π½Π° ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½Ρ‹Π΅ ΠΌΠΎΠ΄ΡƒΠ»ΠΈ, основной Ρ„Π°ΠΉΠ» `app/main.py` Π±ΡƒΠ΄Π΅Ρ‚ достаточно простым. + +### Π˜ΠΌΠΏΠΎΡ€Ρ‚ `FastAPI` + +Π’Ρ‹ ΠΈΠΌΠΏΠΎΡ€Ρ‚ΠΈΡ€ΡƒΠ΅Ρ‚Π΅ ΠΈ создаСтС класс `FastAPI` ΠΊΠ°ΠΊ ΠΎΠ±Ρ‹Ρ‡Π½ΠΎ. + +ΠœΡ‹ Π΄Π°ΠΆΠ΅ ΠΌΠΎΠΆΠ΅ΠΌ ΠΎΠ±ΡŠΡΠ²ΠΈΡ‚ΡŒ Π³Π»ΠΎΠ±Π°Π»ΡŒΠ½Ρ‹Π΅ зависимости [global dependencies](dependencies/global-dependencies.md){.internal-link target=_blank}, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ Π±ΡƒΠ΄ΡƒΡ‚ ΠΎΠ±ΡŠΠ΅Π΄ΠΈΠ½Π΅Π½Ρ‹ с зависимостями для ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½ΠΎΠ³ΠΎ ΠΌΠ°Ρ€ΡˆΡ€ΡƒΡ‚ΠΈΠ·Π°Ρ‚ΠΎΡ€Π°: + +```Python hl_lines="1 3 7" title="app/main.py" +{!../../docs_src/bigger_applications/app/main.py!} +``` + +### Π˜ΠΌΠΏΠΎΡ€Ρ‚ `APIRouter` + +Π’Π΅ΠΏΠ΅Ρ€ΡŒ ΠΌΡ‹ ΠΈΠΌΠΏΠΎΡ€Ρ‚ΠΈΡ€ΡƒΠ΅ΠΌ Π΄Ρ€ΡƒΠ³ΠΈΠ΅ суб-ΠΌΠΎΠ΄ΡƒΠ»ΠΈ, содСрТащиС `APIRouter`: + +```Python hl_lines="4-5" title="app/main.py" +{!../../docs_src/bigger_applications/app/main.py!} +``` + +Π’Π°ΠΊ ΠΊΠ°ΠΊ Ρ„Π°ΠΉΠ»Ρ‹ `app/routers/users.py` ΠΈ `app/routers/items.py` ΡΠ²Π»ΡΡŽΡ‚ΡΡ суб-модулями ΠΎΠ΄Π½ΠΎΠ³ΠΎ ΠΈ Ρ‚ΠΎΠ³ΠΎ ΠΆΠ΅ Python-ΠΏΠ°ΠΊΠ΅Ρ‚Π° `app`, Ρ‚ΠΎ ΠΌΡ‹ смоТСм ΠΈΡ… ΠΈΠΌΠΏΠΎΡ€Ρ‚ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ, воспользовавшись ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠ΅ΠΉ ΠΎΡ‚Π½ΠΎΡΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΠ³ΠΎ ΠΈΠΌΠΏΠΎΡ€Ρ‚Π° `.`. + +### Как Ρ€Π°Π±ΠΎΡ‚Π°Π΅Ρ‚ ΠΈΠΌΠΏΠΎΡ€Ρ‚? + +Данная строка ΠΊΠΎΠ΄Π°: + +```Python +from .routers import items, users +``` + +ΠΎΠ·Π½Π°Ρ‡Π°Π΅Ρ‚: + +* НачнитС с ΠΏΠ°ΠΊΠ΅Ρ‚Π°, Π² ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠΌ содСрТится Π΄Π°Π½Π½Ρ‹ΠΉ ΠΌΠΎΠ΄ΡƒΠ»ΡŒ (Ρ„Π°ΠΉΠ» `app/main.py` содСрТится Π² ΠΊΠ°Ρ‚Π°Π»ΠΎΠ³Π΅ `app/`)... +* ... Π½Π°ΠΉΠ΄ΠΈΡ‚Π΅ суб-ΠΏΠ°ΠΊΠ΅Ρ‚ `routers` (ΠΊΠ°Ρ‚Π°Π»ΠΎΠ³ `app/routers/`)... +* ... ΠΈ ΠΈΠ· Π½Π΅Π³ΠΎ ΠΈΠΌΠΏΠΎΡ€Ρ‚ΠΈΡ€ΡƒΠΉΡ‚Π΅ суб-ΠΌΠΎΠ΄ΡƒΠ»ΠΈ `items` (Ρ„Π°ΠΉΠ» `app/routers/items.py`) ΠΈ `users` (Ρ„Π°ΠΉΠ» `app/routers/users.py`)... + +Π’ ΠΌΠΎΠ΄ΡƒΠ»Π΅ `items` содСрТится пСрСмСнная `router` (`items.router`), Ρ‚Π° самая, ΠΊΠΎΡ‚ΠΎΡ€ΡƒΡŽ ΠΌΡ‹ создали Π² Ρ„Π°ΠΉΠ»Π΅ `app/routers/items.py`, ΠΎΠ½Π° являСтся ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ΠΎΠΌ класса `APIRouter`. + +И Π·Π°Ρ‚Π΅ΠΌ ΠΌΡ‹ сдСлаСм Ρ‚ΠΎ ΠΆΠ΅ самоС для модуля `users`. + +ΠœΡ‹ Ρ‚Π°ΠΊΠΆΠ΅ ΠΌΠΎΠ³Π»ΠΈ Π±Ρ‹ ΠΈΠΌΠΏΠΎΡ€Ρ‚ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ ΠΈ Π΄Ρ€ΡƒΠ³ΠΈΠΌ ΠΌΠ΅Ρ‚ΠΎΠ΄ΠΎΠΌ: + +```Python +from app.routers import items, users +``` + +/// info | ΠŸΡ€ΠΈΠΌΠ΅Ρ‡Π°Π½ΠΈΠ΅ + +ΠŸΠ΅Ρ€Π²Π°Ρ вСрсия являСтся ΠΏΡ€ΠΈΠΌΠ΅Ρ€ΠΎΠΌ ΠΎΡ‚Π½ΠΎΡΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΠ³ΠΎ ΠΈΠΌΠΏΠΎΡ€Ρ‚Π°: + +```Python +from .routers import items, users +``` + +Вторая вСрсия являСтся ΠΏΡ€ΠΈΠΌΠ΅Ρ€ΠΎΠΌ Π°Π±ΡΠΎΠ»ΡŽΡ‚Π½ΠΎΠ³ΠΎ ΠΈΠΌΠΏΠΎΡ€Ρ‚Π°: + +```Python +from app.routers import items, users +``` + +Π£Π·Π½Π°Ρ‚ΡŒ большС ΠΎ ΠΏΠ°ΠΊΠ΅Ρ‚Π°Ρ… ΠΈ модулях Π² Python Π²Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ ΠΈΠ· ΠΎΡ„ΠΈΡ†ΠΈΠ°Π»ΡŒΠ½ΠΎΠΉ Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚Π°Ρ†ΠΈΠΈ Python ΠΎ модулях + +/// + +### Π˜Π·Π±Π΅Π³Π°ΠΉΡ‚Π΅ ΠΊΠΎΠ½Ρ„Π»ΠΈΠΊΡ‚ΠΎΠ² ΠΈΠΌΠ΅Π½ + +ВмСсто Ρ‚ΠΎΠ³ΠΎ Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΈΠΌΠΏΠΎΡ€Ρ‚ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½ΡƒΡŽ `router`, ΠΌΡ‹ ΠΈΠΌΠΏΠΎΡ€Ρ‚ΠΈΡ€ΡƒΠ΅ΠΌ нСпосрСдствСнно суб-ΠΌΠΎΠ΄ΡƒΠ»ΡŒ `items`. + +ΠœΡ‹ Π΄Π΅Π»Π°Π΅ΠΌ это ΠΏΠΎΡ‚ΠΎΠΌΡƒ, Ρ‡Ρ‚ΠΎ Ρƒ нас Π΅ΡΡ‚ΡŒ Π΅Ρ‰Ρ‘ ΠΎΠ΄Π½Π° пСрСмСнная `router` Π² суб-ΠΌΠΎΠ΄ΡƒΠ»Π΅ `users`. + +Если Π±Ρ‹ ΠΌΡ‹ ΠΈΠΌΠΏΠΎΡ€Ρ‚ΠΈΡ€ΠΎΠ²Π°Π»ΠΈ ΠΈΡ… ΠΎΠ΄Π½Ρƒ Π·Π° Π΄Ρ€ΡƒΠ³ΠΎΠΉ, ΠΊΠ°ΠΊ ΠΏΠΎΠΊΠ°Π·Π°Π½ΠΎ Π² ΠΏΡ€ΠΈΠΌΠ΅Ρ€Π΅: + +```Python +from .routers.items import router +from .routers.users import router +``` + +Ρ‚ΠΎ пСрСмСнная `router` ΠΈΠ· `users` пСрСписал Π±Ρ‹ ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½ΡƒΡŽ `router` ΠΈΠ· `items`, ΠΈ Ρƒ нас Π½Π΅ Π±Ρ‹Π»ΠΎ Π±Ρ‹ возмоТности ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ ΠΈΡ… ΠΎΠ΄Π½ΠΎΠ²Ρ€Π΅ΠΌΠ΅Π½Π½ΠΎ. + +ΠŸΠΎΡΡ‚ΠΎΠΌΡƒ, для Ρ‚ΠΎΠ³ΠΎ Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ ΠΎΠ±Π΅ эти ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Π΅ Π² ΠΎΠ΄Π½ΠΎΠΌ Ρ„Π°ΠΉΠ»Π΅, ΠΌΡ‹ ΠΈΠΌΠΏΠΎΡ€Ρ‚ΠΈΡ€ΠΎΠ²Π°Π»ΠΈ ΡΠΎΠΎΡ‚Π²Π΅Ρ‚ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΠ΅ суб-ΠΌΠΎΠ΄ΡƒΠ»ΠΈ: + +```Python hl_lines="5" title="app/main.py" +{!../../docs_src/bigger_applications/app/main.py!} +``` + +### ΠŸΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ ΠΌΠ°Ρ€ΡˆΡ€ΡƒΡ‚ΠΈΠ·Π°Ρ‚ΠΎΡ€ΠΎΠ² (`APIRouter`) для `users` ΠΈ для `items` + +Π”Π°Π²Π°ΠΉΡ‚Π΅ ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡ΠΈΠΌ ΠΌΠ°Ρ€ΡˆΡ€ΡƒΡ‚ΠΈΠ·Π°Ρ‚ΠΎΡ€Ρ‹ (`router`) ΠΈΠ· суб-ΠΌΠΎΠ΄ΡƒΠ»Π΅ΠΉ `users` ΠΈ `items`: + +```Python hl_lines="10-11" title="app/main.py" +{!../../docs_src/bigger_applications/app/main.py!} +``` + +/// info | ΠŸΡ€ΠΈΠΌΠ΅Ρ‡Π°Π½ΠΈΠ΅ + +`users.router` содСрТит `APIRouter` ΠΈΠ· Ρ„Π°ΠΉΠ»Π° `app/routers/users.py`. + +А `items.router` содСрТит `APIRouter` ΠΈΠ· Ρ„Π°ΠΉΠ»Π° `app/routers/items.py`. + +/// + +Π‘ ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ `app.include_router()` ΠΌΡ‹ ΠΌΠΎΠΆΠ΅ΠΌ Π΄ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ ΠΊΠ°ΠΆΠ΄Ρ‹ΠΉ ΠΈΠ· ΠΌΠ°Ρ€ΡˆΡ€ΡƒΡ‚ΠΈΠ·Π°Ρ‚ΠΎΡ€ΠΎΠ² (`APIRouter`) Π² основноС ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ `FastAPI`. + +Он ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡ΠΈΡ‚ всС ΠΌΠ°Ρ€ΡˆΡ€ΡƒΡ‚Ρ‹ Π·Π°Π΄Π°Π½Π½ΠΎΠ³ΠΎ ΠΌΠ°Ρ€ΡˆΡ€ΡƒΡ‚ΠΈΠ·Π°Ρ‚ΠΎΡ€Π° ΠΊ Π½Π°ΡˆΠ΅ΠΌΡƒ ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΡŽ. + +/// note | ВСхничСскиС Π΄Π΅Ρ‚Π°Π»ΠΈ + +ЀактичСски, Π²Π½ΡƒΡ‚Ρ€ΠΈ ΠΎΠ½ создаст всС *ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΈ ΠΏΡƒΡ‚ΠΈ* для ΠΊΠ°ΠΆΠ΄ΠΎΠΉ ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΈ ΠΏΡƒΡ‚ΠΈ объявлСнной Π² `APIRouter`. + +И ΠΏΠΎΠ΄ ΠΊΠ°ΠΏΠΎΡ‚ΠΎΠΌ всё Π±ΡƒΠ΄Π΅Ρ‚ Ρ€Π°Π±ΠΎΡ‚Π°Ρ‚ΡŒ Ρ‚Π°ΠΊ, ΠΊΠ°ΠΊ Π±ΡƒΠ΄Ρ‚ΠΎ Π±Ρ‹ ΠΌΡ‹ ΠΈΠΌΠ΅Π΅ΠΌ Π΄Π΅Π»ΠΎ с ΠΎΠ΄Π½ΠΈΠΌ Ρ„Π°ΠΉΠ»ΠΎΠΌ прилоТСния. + +/// + +/// check | Π—Π°ΠΌΠ΅Ρ‚ΠΊΠ° + +ΠŸΡ€ΠΈ ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠΈ ΠΌΠ°Ρ€ΡˆΡ€ΡƒΡ‚ΠΈΠ·Π°Ρ‚ΠΎΡ€ΠΎΠ² Π½Π΅ стоит Π±Π΅ΡΠΏΠΎΠΊΠΎΠΈΡ‚ΡŒΡΡ ΠΎ ΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΠΈ. + +ΠžΠΏΠ΅Ρ€Π°Ρ†ΠΈΡ ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΡ Π·Π°ΠΉΠΌΡ‘Ρ‚ микросСкунды ΠΈ понадобится Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΏΡ€ΠΈ запускС прилоТСния. + +Π’Π°ΠΊΠΈΠΌ ΠΎΠ±Ρ€Π°Π·ΠΎΠΌ, это Π½Π΅ повлияСт Π½Π° ΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΡŒ. ⚑ + +/// + +### ΠŸΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ `APIRouter` с ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒΡΠΊΠΈΠΌΠΈ прСфиксом (`prefix`), Ρ‚Π΅Π³Π°ΠΌΠΈ (`tags`), ΠΎΡ‚Π²Π΅Ρ‚Π°ΠΌΠΈ (`responses`), ΠΈ зависимостями (`dependencies`) + +Π’Π΅ΠΏΠ΅Ρ€ΡŒ Π΄Π°Π²Π°ΠΉΡ‚Π΅ прСдставим, Ρ‡Ρ‚ΠΎ ваша организация ΠΏΠ΅Ρ€Π΅Π΄Π°Π»Π° Π²Π°ΠΌ Ρ„Π°ΠΉΠ» `app/internal/admin.py`. + +Он содСрТит `APIRouter` с Π½Π΅ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΌΠΈ *эндпоитами* администрирования, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ ваша организация ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ для Π½Π΅ΡΠΊΠΎΠ»ΡŒΠΊΠΈΡ… ΠΏΡ€ΠΎΠ΅ΠΊΡ‚ΠΎΠ². + +Π’ Π΄Π°Π½Π½ΠΎΠΌ ΠΏΡ€ΠΈΠΌΠ΅Ρ€Π΅ это ΡΠ΄Π΅Π»Π°Ρ‚ΡŒ ΠΎΡ‡Π΅Π½ΡŒ просто. Но Π΄Π°Π²Π°ΠΉΡ‚Π΅ ΠΏΡ€Π΅Π΄ΠΏΠΎΠ»ΠΎΠΆΠΈΠΌ, Ρ‡Ρ‚ΠΎ ΠΏΠΎΡΠΊΠΎΠ»ΡŒΠΊΡƒ Ρ„Π°ΠΉΠ» ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ΡΡ для Π½Π΅ΡΠΊΠΎΠ»ΡŒΠΊΠΈΡ… ΠΏΡ€ΠΎΠ΅ΠΊΡ‚ΠΎΠ², +Ρ‚ΠΎ ΠΌΡ‹ Π½Π΅ ΠΌΠΎΠΆΠ΅ΠΌ ΠΌΠΎΠ΄ΠΈΡ„ΠΈΡ†ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ Π΅Π³ΠΎ, добавляя прСфиксы (`prefix`), зависимости (`dependencies`), Ρ‚Π΅Π³ΠΈ (`tags`), ΠΈ Ρ‚.Π΄. нСпосрСдствСнно Π² `APIRouter`: + +```Python hl_lines="3" title="app/internal/admin.py" +{!../../docs_src/bigger_applications/app/internal/admin.py!} +``` + +Но, нСсмотря Π½Π° это, ΠΌΡ‹ Ρ…ΠΎΡ‚ΠΈΠΌ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ кастомный прСфикс (`prefix`) для ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½Π½ΠΎΠ³ΠΎ ΠΌΠ°Ρ€ΡˆΡ€ΡƒΡ‚ΠΈΠ·Π°Ρ‚ΠΎΡ€Π° (`APIRouter`), Π² Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚Π΅ Ρ‡Π΅Π³ΠΎ, каТдая *опСрация ΠΏΡƒΡ‚ΠΈ* Π±ΡƒΠ΄Π΅Ρ‚ Π½Π°Ρ‡ΠΈΠ½Π°Ρ‚ΡŒΡΡ с `/admin`. Π’Π°ΠΊΠΆΠ΅ ΠΌΡ‹ Ρ…ΠΎΡ‚ΠΈΠΌ Π·Π°Ρ‰ΠΈΡ‚ΠΈΡ‚ΡŒ наш ΠΌΠ°Ρ€ΡˆΡ€ΡƒΡ‚ΠΈΠ·Π°Ρ‚ΠΎΡ€ с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ зависимостСй, созданных для нашСго ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π°. И Π΅Ρ‰Ρ‘ ΠΌΡ‹ Ρ…ΠΎΡ‚ΠΈΠΌ Π²ΠΊΠ»ΡŽΡ‡ΠΈΡ‚ΡŒ Ρ‚Π΅Π³ΠΈ (`tags`) ΠΈ ΠΎΡ‚Π²Π΅Ρ‚Ρ‹ (`responses`). + +ΠœΡ‹ ΠΌΠΎΠΆΠ΅ΠΌ ΠΏΡ€ΠΈΠΌΠ΅Π½ΠΈΡ‚ΡŒ всС Π²Ρ‹ΡˆΠ΅ΠΏΠ΅Ρ€Π΅Ρ‡ΠΈΡΠ»Π΅Π½Π½Ρ‹Π΅ настройки, Π½Π΅ измСняя Π½Π°Ρ‡Π°Π»ΡŒΠ½Ρ‹ΠΉ `APIRouter`. Нам всСго лишь Π½ΡƒΠΆΠ½ΠΎ ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‚ΡŒ Π½ΡƒΠΆΠ½Ρ‹Π΅ ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Ρ‹ Π² `app.include_router()`. + +```Python hl_lines="14-17" title="app/main.py" +{!../../docs_src/bigger_applications/app/main.py!} +``` + +Π’Π°ΠΊΠΈΠΌ ΠΎΠ±Ρ€Π°Π·ΠΎΠΌ, ΠΎΡ€ΠΈΠ³ΠΈΠ½Π°Π»ΡŒΠ½Ρ‹ΠΉ `APIRouter` Π½Π΅ Π±ΡƒΠ΄Π΅Ρ‚ ΠΌΠΎΠ΄ΠΈΡ„ΠΈΡ†ΠΈΡ€ΠΎΠ²Π°Π½, ΠΈ ΠΌΡ‹ смоТСм ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ Ρ„Π°ΠΉΠ» `app/internal/admin.py` сразу Π² Π½Π΅ΡΠΊΠΎΠ»ΡŒΠΊΠΈΡ… ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π°Ρ… ΠΎΡ€Π³Π°Π½ΠΈΠ·Π°Ρ†ΠΈΠΈ. + +Π’ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚Π΅, Π² нашСм ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΈ ΠΊΠ°ΠΆΠ΄Ρ‹ΠΉ *эндпоинт* модуля `admin` Π±ΡƒΠ΄Π΅Ρ‚ ΠΈΠΌΠ΅Ρ‚ΡŒ: + +* ΠŸΡ€Π΅Ρ„ΠΈΠΊΡ `/admin`. +* Π’Π΅Π³ `admin`. +* Π—Π°Π²ΠΈΡΠΈΠΌΠΎΡΡ‚ΡŒ `get_token_header`. +* ΠžΡ‚Π²Π΅Ρ‚ `418`. 🍡 + +Π­Ρ‚ΠΎ Π±ΡƒΠ΄Π΅Ρ‚ ΠΈΠΌΠ΅Ρ‚ΡŒ мСсто ΠΈΡΠΊΠ»ΡŽΡ‡ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎ для `APIRouter` Π² нашСм ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΈ, ΠΈ Π½Π΅ Π·Π°Ρ‚Ρ€ΠΎΠ½Π΅Ρ‚ любой Π΄Ρ€ΡƒΠ³ΠΎΠΉ ΠΊΠΎΠ΄, ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΡŽΡ‰ΠΈΠΉ Π΅Π³ΠΎ. + +НапримСр, Π΄Ρ€ΡƒΠ³ΠΈΠ΅ ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Ρ‹, ΠΌΠΎΠ³ΡƒΡ‚ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ Ρ‚ΠΎΡ‚ ΠΆΠ΅ самый `APIRouter` с Π΄Ρ€ΡƒΠ³ΠΈΠΌΠΈ ΠΌΠ΅Ρ‚ΠΎΠ΄Π°ΠΌΠΈ Π°ΡƒΡ‚Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ†ΠΈΠΈ. + +### ΠŸΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½ΠΎΠ³ΠΎ *эндпоинта* + +ΠœΡ‹ Ρ‚Π°ΠΊΠΆΠ΅ ΠΌΠΎΠΆΠ΅ΠΌ Π΄ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ *эндпоинт* нСпосрСдствСнно Π² основноС ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ `FastAPI`. + +Π—Π΄Π΅ΡΡŒ ΠΌΡ‹ это Π΄Π΅Π»Π°Π΅ΠΌ ... просто, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΏΠΎΠΊΠ°Π·Π°Ρ‚ΡŒ, Ρ‡Ρ‚ΠΎ это Π²ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎ 🀷: + +```Python hl_lines="21-23" title="app/main.py" +{!../../docs_src/bigger_applications/app/main.py!} +``` + +ΠΈ это Π±ΡƒΠ΄Π΅Ρ‚ Ρ€Π°Π±ΠΎΡ‚Π°Ρ‚ΡŒ ΠΊΠΎΡ€Ρ€Π΅ΠΊΡ‚Π½ΠΎ вмСстС с Π΄Ρ€ΡƒΠ³ΠΈΠΌΠΈ *эндпоинтами*, Π΄ΠΎΠ±Π°Π²Π»Π΅Π½Π½Ρ‹ΠΌΠΈ с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ `app.include_router()`. + +/// info | Π‘Π»ΠΎΠΆΠ½Ρ‹Π΅ тСхничСскиС Π΄Π΅Ρ‚Π°Π»ΠΈ + +**ΠŸΡ€ΠΈΠΌΠ΅Ρ‡Π°Π½ΠΈΠ΅**: это слоТная тСхничСская Π΄Π΅Ρ‚Π°Π»ΡŒ, ΠΊΠΎΡ‚ΠΎΡ€ΡƒΡŽ, скорСС всСго, **Π²Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ ΠΏΡ€ΠΎΠΏΡƒΡΡ‚ΠΈΡ‚ΡŒ**. + +--- + +ΠœΠ°Ρ€ΡˆΡ€ΡƒΡ‚ΠΈΠ·Π°Ρ‚ΠΎΡ€Ρ‹ (`APIRouter`) Π½Π΅ "ΠΌΠΎΠ½Ρ‚ΠΈΡ€ΡƒΡŽΡ‚ΡΡ" ΠΏΠΎ-ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΠΈ ΠΈ Π½Π΅ ΠΈΠ·ΠΎΠ»ΠΈΡ€ΡƒΡŽΡ‚ΡΡ ΠΎΡ‚ ΠΎΡΡ‚Π°Π»ΡŒΠ½ΠΎΠ³ΠΎ прилоТСния. + +Π­Ρ‚ΠΎ происходит ΠΏΠΎΡ‚ΠΎΠΌΡƒ, Ρ‡Ρ‚ΠΎ Π½ΡƒΠΆΠ½ΠΎ Π²ΠΊΠ»ΡŽΡ‡ΠΈΡ‚ΡŒ ΠΈΡ… *эндпоинты* Π² OpenAPI схСму ΠΈ Π² интСрфСйс ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ. + +Π’ силу Ρ‚ΠΎΠ³ΠΎ, Ρ‡Ρ‚ΠΎ ΠΌΡ‹ Π½Π΅ ΠΌΠΎΠΆΠ΅ΠΌ ΠΈΡ… ΠΈΠ·ΠΎΠ»ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ ΠΈ "ΠΏΡ€ΠΈΠΌΠΎΠ½Ρ‚ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ" нСзависимо ΠΎΡ‚ ΠΎΡΡ‚Π°Π»ΡŒΠ½Ρ‹Ρ…, *эндпоинты* ΠΊΠ»ΠΎΠ½ΠΈΡ€ΡƒΡŽΡ‚ΡΡ (ΠΏΠ΅Ρ€Π΅ΡΠΎΠ·Π΄Π°ΡŽΡ‚ΡΡ) ΠΈ Π½Π΅ ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π°ΡŽΡ‚ΡΡ Π½Π°ΠΏΡ€ΡΠΌΡƒΡŽ. + +/// + +## ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° автоматичСской Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚Π°Ρ†ΠΈΠΈ API + +Π’Π΅ΠΏΠ΅Ρ€ΡŒ запуститС ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅: + +
+ +```console +$ fastapi dev app/main.py + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +ΠžΡ‚ΠΊΡ€ΠΎΠΉΡ‚Π΅ Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚Π°Ρ†ΠΈΡŽ ΠΏΠΎ адрСсу http://127.0.0.1:8000/docs. + +Π’Ρ‹ ΡƒΠ²ΠΈΠ΄ΠΈΡ‚Π΅ Π°Π²Ρ‚ΠΎΠΌΠ°Ρ‚ΠΈΡ‡Π΅ΡΠΊΡƒΡŽ API Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚Π°Ρ†ΠΈΡŽ. Она Π²ΠΊΠ»ΡŽΡ‡Π°Π΅Ρ‚ Π² сСбя ΠΌΠ°Ρ€ΡˆΡ€ΡƒΡ‚Ρ‹ ΠΈΠ· суб-ΠΌΠΎΠ΄ΡƒΠ»Π΅ΠΉ, ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΡ Π²Π΅Ρ€Π½Ρ‹Π΅ ΠΌΠ°Ρ€ΡˆΡ€ΡƒΡ‚Ρ‹, прСфиксы ΠΈ Ρ‚Π΅Π³ΠΈ: + + + +## ΠŸΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ ΡΡƒΡ‰Π΅ΡΡ‚Π²ΡƒΡŽΡ‰Π΅Π³ΠΎ ΠΌΠ°Ρ€ΡˆΡ€ΡƒΡ‚Π° Ρ‡Π΅Ρ€Π΅Π· Π½ΠΎΠ²Ρ‹ΠΉ прСфикс (`prefix`) + +Π’Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ `.include_router()` нСсколько Ρ€Π°Π· с ΠΎΠ΄Π½ΠΈΠΌ ΠΈ Ρ‚Π΅ΠΌ ΠΆΠ΅ ΠΌΠ°Ρ€ΡˆΡ€ΡƒΡ‚ΠΎΠΌ, ΠΏΡ€ΠΈΠΌΠ΅Π½ΠΈΠ² Ρ€Π°Π·Π»ΠΈΡ‡Π½Ρ‹Π΅ прСфиксы. + +Π­Ρ‚ΠΎ ΠΌΠΎΠΆΠ΅Ρ‚ Π±Ρ‹Ρ‚ΡŒ ΠΏΠΎΠ»Π΅Π·Π½Ρ‹ΠΌ, Ссли Π½ΡƒΠΆΠ½ΠΎ ΠΏΡ€Π΅Π΄ΠΎΡΡ‚Π°Π²ΠΈΡ‚ΡŒ доступ ΠΊ ΠΎΠ΄Π½ΠΎΠΌΡƒ ΠΈ Ρ‚ΠΎΠΌΡƒ ΠΆΠ΅ API Ρ‡Π΅Ρ€Π΅Π· Ρ€Π°Π·Π»ΠΈΡ‡Π½Ρ‹Π΅ прСфиксы, Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€, `/api/v1` ΠΈ `/api/latest`. + +Π­Ρ‚ΠΎ ΠΏΡ€ΠΎΠ΄Π²ΠΈΠ½ΡƒΡ‚Ρ‹ΠΉ способ, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ Π²Π°ΠΌ ΠΌΠΎΠΆΠ΅Ρ‚ ΠΈ Π½Π΅ пригодится. ΠœΡ‹ ΠΏΡ€ΠΈΠ²ΠΎΠ΄ΠΈΠΌ Π΅Π³ΠΎ Π½Π° случай, Ссли Π²Π΄Ρ€ΡƒΠ³ Π²Π°ΠΌ это понадобится. + +## Π’ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ ΠΎΠ΄Π½ΠΎΠ³ΠΎ ΠΌΠ°Ρ€ΡˆΡ€ΡƒΡ‚ΠΈΠ·Π°Ρ‚ΠΎΡ€Π° (`APIRouter`) Π² Π΄Ρ€ΡƒΠ³ΠΎΠΉ + +Π’ΠΎΡ‡Π½ΠΎ Ρ‚Π°ΠΊ ΠΆΠ΅, ΠΊΠ°ΠΊ Π²Ρ‹ Π²ΠΊΠ»ΡŽΡ‡Π°Π΅Ρ‚Π΅ `APIRouter` Π² ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ `FastAPI`, Π²Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ Π²ΠΊΠ»ΡŽΡ‡ΠΈΡ‚ΡŒ `APIRouter` Π² Π΄Ρ€ΡƒΠ³ΠΎΠΉ `APIRouter`: + +```Python +router.include_router(other_router) +``` + +Π£Π΄ΠΎΡΡ‚ΠΎΠ²Π΅Ρ€ΡŒΡ‚Π΅ΡΡŒ, Ρ‡Ρ‚ΠΎ Π²Ρ‹ сдСлали это Π΄ΠΎ Ρ‚ΠΎΠ³ΠΎ, ΠΊΠ°ΠΊ ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡ΠΈΡ‚ΡŒ ΠΌΠ°Ρ€ΡˆΡ€ΡƒΡ‚ΠΈΠ·Π°Ρ‚ΠΎΡ€ (`router`) ΠΊ Π²Π°ΡˆΠ΅ΠΌΡƒ `FastAPI` ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΡŽ, ΠΈ *эндпоинты* ΠΌΠ°Ρ€ΡˆΡ€ΡƒΡ‚ΠΈΠ·Π°Ρ‚ΠΎΡ€Π° `other_router` Π±Ρ‹Π»ΠΈ Ρ‚Π°ΠΊΠΆΠ΅ ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½Ρ‹. diff --git a/docs/ru/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md b/docs/ru/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md index f9b9dec25..0e4eb95be 100644 --- a/docs/ru/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md +++ b/docs/ru/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md @@ -18,7 +18,7 @@ Зависимости ΠΈΠ· dependencies выполнятся Ρ‚Π°ΠΊ ΠΆΠ΅, ΠΊΠ°ΠΊ ΠΈ ΠΎΠ±Ρ‹Ρ‡Π½Ρ‹Π΅ зависимости. Но ΠΈΡ… значСния (Ссли ΠΎΠ½ΠΈ Π±Ρ‹Π»ΠΈ) Π½Π΅ Π±ΡƒΠ΄ΡƒΡ‚ ΠΏΠ΅Ρ€Π΅Π΄Π°Π½Ρ‹ Π² *Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΈ ΠΏΡƒΡ‚ΠΈ*. -/// подсказка +/// tip | Подсказка НСкоторыС Ρ€Π΅Π΄Π°ΠΊΡ‚ΠΎΡ€Ρ‹ ΠΊΠΎΠ΄Π° ΠΎΠΏΡ€Π΅Π΄Π΅Π»ΡΡŽΡ‚ Π½Π΅ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌΡ‹Π΅ ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Ρ‹ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΉ ΠΈ ΠΏΠΎΠ΄ΡΠ²Π΅Ρ‡ΠΈΠ²Π°ΡŽΡ‚ ΠΈΡ… ΠΊΠ°ΠΊ ΠΎΡˆΠΈΠ±ΠΊΡƒ. @@ -28,7 +28,7 @@ /// -/// Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½Π°Ρ | информация +/// info | ΠŸΡ€ΠΈΠΌΠ΅Ρ‡Π°Π½ΠΈΠ΅ Π’ этом ΠΏΡ€ΠΈΠΌΠ΅Ρ€Π΅ ΠΌΡ‹ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌ Π²Ρ‹Π΄ΡƒΠΌΠ°Π½Π½Ρ‹Π΅ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒΡΠΊΠΈΠ΅ Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΊΠΈ `X-Key` ΠΈ `X-Token`. diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 823957822..c92279cfd 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.115.6" +__version__ = "0.115.7" from starlette import status as status diff --git a/pyproject.toml b/pyproject.toml index edfa81522..381eb50bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ classifiers = [ "Framework :: FastAPI", "Framework :: Pydantic", "Framework :: Pydantic :: 1", + "Framework :: Pydantic :: 2", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3 :: Only", @@ -41,7 +42,7 @@ classifiers = [ "Topic :: Internet :: WWW/HTTP", ] dependencies = [ - "starlette>=0.40.0,<0.42.0", + "starlette>=0.40.0,<0.46.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", ] @@ -60,9 +61,9 @@ standard = [ # For the test client "httpx >=0.23.0", # For templates - "jinja2 >=2.11.2", + "jinja2 >=3.1.5", # For forms and file uploads - "python-multipart >=0.0.7", + "python-multipart >=0.0.18", # To validate email fields "email-validator >=2.0.0", # Uvicorn with uvloop @@ -79,9 +80,9 @@ all = [ # # For the test client "httpx >=0.23.0", # For templates - "jinja2 >=2.11.2", + "jinja2 >=3.1.5", # For forms and file uploads - "python-multipart >=0.0.7", + "python-multipart >=0.0.18", # For Starlette's SessionMiddleware, not commonly used with FastAPI "itsdangerous >=1.1.0", # For Starlette's schema generation, would not be used with FastAPI diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial001.py b/tests/test_tutorial/test_request_form_models/test_tutorial001.py index 46c130ee8..1ca3c96d3 100644 --- a/tests/test_tutorial/test_request_form_models/test_tutorial001.py +++ b/tests/test_tutorial/test_request_form_models/test_tutorial001.py @@ -1,13 +1,24 @@ +import importlib + import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient +from ...utils import needs_py39 + -@pytest.fixture(name="client") -def get_client(): - from docs_src.request_form_models.tutorial001 import app +@pytest.fixture( + name="client", + params=[ + "tutorial001", + "tutorial001_an", + pytest.param("tutorial001_an_py39", marks=needs_py39), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.request_form_models.{request.param}") - client = TestClient(app) + client = TestClient(mod.app) return client diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial001_an.py b/tests/test_tutorial/test_request_form_models/test_tutorial001_an.py deleted file mode 100644 index 4e14d89c8..000000000 --- a/tests/test_tutorial/test_request_form_models/test_tutorial001_an.py +++ /dev/null @@ -1,232 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.request_form_models.tutorial001_an import app - - client = TestClient(app) - return client - - -def test_post_body_form(client: TestClient): - response = client.post("/login/", data={"username": "Foo", "password": "secret"}) - assert response.status_code == 200 - assert response.json() == {"username": "Foo", "password": "secret"} - - -def test_post_body_form_no_password(client: TestClient): - response = client.post("/login/", data={"username": "Foo"}) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {"username": "Foo"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -def test_post_body_form_no_username(client: TestClient): - response = client.post("/login/", data={"password": "secret"}) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {"password": "secret"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -def test_post_body_form_no_data(client: TestClient): - response = client.post("/login/") - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {}, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {}, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -def test_post_body_json(client: TestClient): - response = client.post("/login/", json={"username": "Foo", "password": "secret"}) - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {}, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {}, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/login/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login", - "operationId": "login_login__post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": {"$ref": "#/components/schemas/FormData"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "FormData": { - "properties": { - "username": {"type": "string", "title": "Username"}, - "password": {"type": "string", "title": "Password"}, - }, - "type": "object", - "required": ["username", "password"], - "title": "FormData", - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial001_an_py39.py b/tests/test_tutorial/test_request_form_models/test_tutorial001_an_py39.py deleted file mode 100644 index 2e6426aa7..000000000 --- a/tests/test_tutorial/test_request_form_models/test_tutorial001_an_py39.py +++ /dev/null @@ -1,240 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from tests.utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.request_form_models.tutorial001_an_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_post_body_form(client: TestClient): - response = client.post("/login/", data={"username": "Foo", "password": "secret"}) - assert response.status_code == 200 - assert response.json() == {"username": "Foo", "password": "secret"} - - -@needs_py39 -def test_post_body_form_no_password(client: TestClient): - response = client.post("/login/", data={"username": "Foo"}) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {"username": "Foo"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -@needs_py39 -def test_post_body_form_no_username(client: TestClient): - response = client.post("/login/", data={"password": "secret"}) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {"password": "secret"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -@needs_py39 -def test_post_body_form_no_data(client: TestClient): - response = client.post("/login/") - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {}, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {}, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -@needs_py39 -def test_post_body_json(client: TestClient): - response = client.post("/login/", json={"username": "Foo", "password": "secret"}) - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {}, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {}, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/login/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login", - "operationId": "login_login__post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": {"$ref": "#/components/schemas/FormData"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "FormData": { - "properties": { - "username": {"type": "string", "title": "Username"}, - "password": {"type": "string", "title": "Password"}, - }, - "type": "object", - "required": ["username", "password"], - "title": "FormData", - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial002.py b/tests/test_tutorial/test_request_form_models/test_tutorial002.py index 76f480001..b3f6be63a 100644 --- a/tests/test_tutorial/test_request_form_models/test_tutorial002.py +++ b/tests/test_tutorial/test_request_form_models/test_tutorial002.py @@ -1,14 +1,23 @@ +import importlib + import pytest from fastapi.testclient import TestClient -from tests.utils import needs_pydanticv2 +from ...utils import needs_py39, needs_pydanticv2 -@pytest.fixture(name="client") -def get_client(): - from docs_src.request_form_models.tutorial002 import app +@pytest.fixture( + name="client", + params=[ + "tutorial002", + "tutorial002_an", + pytest.param("tutorial002_an_py39", marks=needs_py39), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.request_form_models.{request.param}") - client = TestClient(app) + client = TestClient(mod.app) return client diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial002_an.py b/tests/test_tutorial/test_request_form_models/test_tutorial002_an.py deleted file mode 100644 index 179b2977d..000000000 --- a/tests/test_tutorial/test_request_form_models/test_tutorial002_an.py +++ /dev/null @@ -1,196 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from tests.utils import needs_pydanticv2 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.request_form_models.tutorial002_an import app - - client = TestClient(app) - return client - - -@needs_pydanticv2 -def test_post_body_form(client: TestClient): - response = client.post("/login/", data={"username": "Foo", "password": "secret"}) - assert response.status_code == 200 - assert response.json() == {"username": "Foo", "password": "secret"} - - -@needs_pydanticv2 -def test_post_body_extra_form(client: TestClient): - response = client.post( - "/login/", data={"username": "Foo", "password": "secret", "extra": "extra"} - ) - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "extra_forbidden", - "loc": ["body", "extra"], - "msg": "Extra inputs are not permitted", - "input": "extra", - } - ] - } - - -@needs_pydanticv2 -def test_post_body_form_no_password(client: TestClient): - response = client.post("/login/", data={"username": "Foo"}) - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {"username": "Foo"}, - } - ] - } - - -@needs_pydanticv2 -def test_post_body_form_no_username(client: TestClient): - response = client.post("/login/", data={"password": "secret"}) - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {"password": "secret"}, - } - ] - } - - -@needs_pydanticv2 -def test_post_body_form_no_data(client: TestClient): - response = client.post("/login/") - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {}, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {}, - }, - ] - } - - -@needs_pydanticv2 -def test_post_body_json(client: TestClient): - response = client.post("/login/", json={"username": "Foo", "password": "secret"}) - assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {}, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {}, - }, - ] - } - - -@needs_pydanticv2 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/login/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login", - "operationId": "login_login__post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": {"$ref": "#/components/schemas/FormData"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "FormData": { - "properties": { - "username": {"type": "string", "title": "Username"}, - "password": {"type": "string", "title": "Password"}, - }, - "additionalProperties": False, - "type": "object", - "required": ["username", "password"], - "title": "FormData", - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial002_an_py39.py b/tests/test_tutorial/test_request_form_models/test_tutorial002_an_py39.py deleted file mode 100644 index 510ad9d7c..000000000 --- a/tests/test_tutorial/test_request_form_models/test_tutorial002_an_py39.py +++ /dev/null @@ -1,203 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from tests.utils import needs_py39, needs_pydanticv2 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.request_form_models.tutorial002_an_py39 import app - - client = TestClient(app) - return client - - -@needs_pydanticv2 -@needs_py39 -def test_post_body_form(client: TestClient): - response = client.post("/login/", data={"username": "Foo", "password": "secret"}) - assert response.status_code == 200 - assert response.json() == {"username": "Foo", "password": "secret"} - - -@needs_pydanticv2 -@needs_py39 -def test_post_body_extra_form(client: TestClient): - response = client.post( - "/login/", data={"username": "Foo", "password": "secret", "extra": "extra"} - ) - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "extra_forbidden", - "loc": ["body", "extra"], - "msg": "Extra inputs are not permitted", - "input": "extra", - } - ] - } - - -@needs_pydanticv2 -@needs_py39 -def test_post_body_form_no_password(client: TestClient): - response = client.post("/login/", data={"username": "Foo"}) - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {"username": "Foo"}, - } - ] - } - - -@needs_pydanticv2 -@needs_py39 -def test_post_body_form_no_username(client: TestClient): - response = client.post("/login/", data={"password": "secret"}) - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {"password": "secret"}, - } - ] - } - - -@needs_pydanticv2 -@needs_py39 -def test_post_body_form_no_data(client: TestClient): - response = client.post("/login/") - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {}, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {}, - }, - ] - } - - -@needs_pydanticv2 -@needs_py39 -def test_post_body_json(client: TestClient): - response = client.post("/login/", json={"username": "Foo", "password": "secret"}) - assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {}, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {}, - }, - ] - } - - -@needs_pydanticv2 -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/login/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login", - "operationId": "login_login__post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": {"$ref": "#/components/schemas/FormData"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "FormData": { - "properties": { - "username": {"type": "string", "title": "Username"}, - "password": {"type": "string", "title": "Password"}, - }, - "additionalProperties": False, - "type": "object", - "required": ["username", "password"], - "title": "FormData", - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py b/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py index 249b9379d..b503f23a5 100644 --- a/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py +++ b/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py @@ -1,17 +1,27 @@ +import importlib + import pytest from fastapi.testclient import TestClient -from tests.utils import needs_pydanticv1 +from ...utils import needs_py39, needs_pydanticv1 -@pytest.fixture(name="client") -def get_client(): - from docs_src.request_form_models.tutorial002_pv1 import app +@pytest.fixture( + name="client", + params=[ + "tutorial002_pv1", + "tutorial002_pv1_an", + pytest.param("tutorial002_pv1_an_py39", marks=needs_py39), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.request_form_models.{request.param}") - client = TestClient(app) + client = TestClient(mod.app) return client +# TODO: remove when deprecating Pydantic v1 @needs_pydanticv1 def test_post_body_form(client: TestClient): response = client.post("/login/", data={"username": "Foo", "password": "secret"}) @@ -19,6 +29,7 @@ def test_post_body_form(client: TestClient): assert response.json() == {"username": "Foo", "password": "secret"} +# TODO: remove when deprecating Pydantic v1 @needs_pydanticv1 def test_post_body_extra_form(client: TestClient): response = client.post( @@ -36,6 +47,7 @@ def test_post_body_extra_form(client: TestClient): } +# TODO: remove when deprecating Pydantic v1 @needs_pydanticv1 def test_post_body_form_no_password(client: TestClient): response = client.post("/login/", data={"username": "Foo"}) @@ -51,6 +63,7 @@ def test_post_body_form_no_password(client: TestClient): } +# TODO: remove when deprecating Pydantic v1 @needs_pydanticv1 def test_post_body_form_no_username(client: TestClient): response = client.post("/login/", data={"password": "secret"}) @@ -66,6 +79,7 @@ def test_post_body_form_no_username(client: TestClient): } +# TODO: remove when deprecating Pydantic v1 @needs_pydanticv1 def test_post_body_form_no_data(client: TestClient): response = client.post("/login/") @@ -86,6 +100,7 @@ def test_post_body_form_no_data(client: TestClient): } +# TODO: remove when deprecating Pydantic v1 @needs_pydanticv1 def test_post_body_json(client: TestClient): response = client.post("/login/", json={"username": "Foo", "password": "secret"}) @@ -106,6 +121,7 @@ def test_post_body_json(client: TestClient): } +# TODO: remove when deprecating Pydantic v1 @needs_pydanticv1 def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an.py b/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an.py deleted file mode 100644 index 44cb3c32b..000000000 --- a/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an.py +++ /dev/null @@ -1,196 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from tests.utils import needs_pydanticv1 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.request_form_models.tutorial002_pv1_an import app - - client = TestClient(app) - return client - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_post_body_form(client: TestClient): - response = client.post("/login/", data={"username": "Foo", "password": "secret"}) - assert response.status_code == 200 - assert response.json() == {"username": "Foo", "password": "secret"} - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_post_body_extra_form(client: TestClient): - response = client.post( - "/login/", data={"username": "Foo", "password": "secret", "extra": "extra"} - ) - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "value_error.extra", - "loc": ["body", "extra"], - "msg": "extra fields not permitted", - } - ] - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_post_body_form_no_password(client: TestClient): - response = client.post("/login/", data={"username": "Foo"}) - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "value_error.missing", - "loc": ["body", "password"], - "msg": "field required", - } - ] - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_post_body_form_no_username(client: TestClient): - response = client.post("/login/", data={"password": "secret"}) - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "value_error.missing", - "loc": ["body", "username"], - "msg": "field required", - } - ] - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_post_body_form_no_data(client: TestClient): - response = client.post("/login/") - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "value_error.missing", - "loc": ["body", "username"], - "msg": "field required", - }, - { - "type": "value_error.missing", - "loc": ["body", "password"], - "msg": "field required", - }, - ] - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_post_body_json(client: TestClient): - response = client.post("/login/", json={"username": "Foo", "password": "secret"}) - assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "type": "value_error.missing", - "loc": ["body", "username"], - "msg": "field required", - }, - { - "type": "value_error.missing", - "loc": ["body", "password"], - "msg": "field required", - }, - ] - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/login/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login", - "operationId": "login_login__post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": {"$ref": "#/components/schemas/FormData"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "FormData": { - "properties": { - "username": {"type": "string", "title": "Username"}, - "password": {"type": "string", "title": "Password"}, - }, - "additionalProperties": False, - "type": "object", - "required": ["username", "password"], - "title": "FormData", - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an_p39.py b/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an_p39.py deleted file mode 100644 index 899549e40..000000000 --- a/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an_p39.py +++ /dev/null @@ -1,203 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from tests.utils import needs_py39, needs_pydanticv1 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.request_form_models.tutorial002_pv1_an_py39 import app - - client = TestClient(app) - return client - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -@needs_py39 -def test_post_body_form(client: TestClient): - response = client.post("/login/", data={"username": "Foo", "password": "secret"}) - assert response.status_code == 200 - assert response.json() == {"username": "Foo", "password": "secret"} - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -@needs_py39 -def test_post_body_extra_form(client: TestClient): - response = client.post( - "/login/", data={"username": "Foo", "password": "secret", "extra": "extra"} - ) - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "value_error.extra", - "loc": ["body", "extra"], - "msg": "extra fields not permitted", - } - ] - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -@needs_py39 -def test_post_body_form_no_password(client: TestClient): - response = client.post("/login/", data={"username": "Foo"}) - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "value_error.missing", - "loc": ["body", "password"], - "msg": "field required", - } - ] - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -@needs_py39 -def test_post_body_form_no_username(client: TestClient): - response = client.post("/login/", data={"password": "secret"}) - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "value_error.missing", - "loc": ["body", "username"], - "msg": "field required", - } - ] - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -@needs_py39 -def test_post_body_form_no_data(client: TestClient): - response = client.post("/login/") - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "value_error.missing", - "loc": ["body", "username"], - "msg": "field required", - }, - { - "type": "value_error.missing", - "loc": ["body", "password"], - "msg": "field required", - }, - ] - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -@needs_py39 -def test_post_body_json(client: TestClient): - response = client.post("/login/", json={"username": "Foo", "password": "secret"}) - assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "type": "value_error.missing", - "loc": ["body", "username"], - "msg": "field required", - }, - { - "type": "value_error.missing", - "loc": ["body", "password"], - "msg": "field required", - }, - ] - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/login/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login", - "operationId": "login_login__post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": {"$ref": "#/components/schemas/FormData"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "FormData": { - "properties": { - "username": {"type": "string", "title": "Username"}, - "password": {"type": "string", "title": "Password"}, - }, - "additionalProperties": False, - "type": "object", - "required": ["username", "password"], - "title": "FormData", - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial001.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial001.py index 98b187355..c21cbb4bc 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial001.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial001.py @@ -1,14 +1,22 @@ +import importlib + import pytest from fastapi.testclient import TestClient -from ...utils import needs_pydanticv2 +from ...utils import needs_py310, needs_pydanticv2 -@pytest.fixture(name="client") -def get_client(): - from docs_src.schema_extra_example.tutorial001 import app +@pytest.fixture( + name="client", + params=[ + "tutorial001", + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.schema_extra_example.{request.param}") - client = TestClient(app) + client = TestClient(mod.app) return client diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py index 3520ef61d..b79f42e64 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py @@ -1,14 +1,22 @@ +import importlib + import pytest from fastapi.testclient import TestClient -from ...utils import needs_pydanticv1 +from ...utils import needs_py310, needs_pydanticv1 -@pytest.fixture(name="client") -def get_client(): - from docs_src.schema_extra_example.tutorial001_pv1 import app +@pytest.fixture( + name="client", + params=[ + "tutorial001_pv1", + pytest.param("tutorial001_pv1_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.schema_extra_example.{request.param}") - client = TestClient(app) + client = TestClient(mod.app) return client diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1_py310.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1_py310.py deleted file mode 100644 index b2a4d15b1..000000000 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1_py310.py +++ /dev/null @@ -1,129 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_py310, needs_pydanticv1 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.schema_extra_example.tutorial001_pv1_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -@needs_pydanticv1 -def test_post_body_example(client: TestClient): - response = client.put( - "/items/5", - json={ - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - ) - assert response.status_code == 200 - - -@needs_py310 -@needs_pydanticv1 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - # insert_assert(response.json()) - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"type": "integer", "title": "Item Id"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": {"$ref": "#/components/schemas/ValidationError"}, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "description": {"type": "string", "title": "Description"}, - "price": {"type": "number", "title": "Price"}, - "tax": {"type": "number", "title": "Tax"}, - }, - "type": "object", - "required": ["name", "price"], - "title": "Item", - "examples": [ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - } - ], - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial001_py310.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial001_py310.py deleted file mode 100644 index e63e33cda..000000000 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial001_py310.py +++ /dev/null @@ -1,135 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_py310, needs_pydanticv2 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.schema_extra_example.tutorial001_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -@needs_pydanticv2 -def test_post_body_example(client: TestClient): - response = client.put( - "/items/5", - json={ - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - ) - assert response.status_code == 200 - - -@needs_py310 -@needs_pydanticv2 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - # insert_assert(response.json()) - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "name": "item_id", - "in": "path", - "required": True, - "schema": {"type": "integer", "title": "Item Id"}, - } - ], - "requestBody": { - "required": True, - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": {"$ref": "#/components/schemas/ValidationError"}, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Description", - }, - "price": {"type": "number", "title": "Price"}, - "tax": { - "anyOf": [{"type": "number"}, {"type": "null"}], - "title": "Tax", - }, - }, - "type": "object", - "required": ["name", "price"], - "title": "Item", - "examples": [ - { - "description": "A very nice Item", - "name": "Foo", - "price": 35.4, - "tax": 3.2, - } - ], - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py index eac0d1e29..61aefd12a 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py @@ -1,13 +1,30 @@ +import importlib + +import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient -from docs_src.schema_extra_example.tutorial004 import app +from ...utils import needs_py39, needs_py310 + + +@pytest.fixture( + name="client", + params=[ + "tutorial004", + pytest.param("tutorial004_py310", marks=needs_py310), + "tutorial004_an", + pytest.param("tutorial004_an_py39", marks=needs_py39), + pytest.param("tutorial004_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.schema_extra_example.{request.param}") -client = TestClient(app) + client = TestClient(mod.app) + return client -# Test required and embedded body parameters with no bodies sent -def test_post_body_example(): +def test_post_body_example(client: TestClient): response = client.put( "/items/5", json={ @@ -20,7 +37,7 @@ def test_post_body_example(): assert response.status_code == 200 -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py deleted file mode 100644 index a9cecd098..000000000 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py +++ /dev/null @@ -1,168 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from docs_src.schema_extra_example.tutorial004_an import app - -client = TestClient(app) - - -# Test required and embedded body parameters with no bodies sent -def test_post_body_example(): - response = client.put( - "/items/5", - json={ - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - ) - assert response.status_code == 200 - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": IsDict( - { - "$ref": "#/components/schemas/Item", - "examples": [ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - {"name": "Bar", "price": "35.4"}, - { - "name": "Baz", - "price": "thirty five point four", - }, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - "examples": [ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - {"name": "Bar", "price": "35.4"}, - { - "name": "Baz", - "price": "thirty five point four", - }, - ], - } - ) - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py deleted file mode 100644 index b6a735599..000000000 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py +++ /dev/null @@ -1,177 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.schema_extra_example.tutorial004_an_py310 import app - - client = TestClient(app) - return client - - -# Test required and embedded body parameters with no bodies sent -@needs_py310 -def test_post_body_example(client: TestClient): - response = client.put( - "/items/5", - json={ - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - ) - assert response.status_code == 200 - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": IsDict( - { - "$ref": "#/components/schemas/Item", - "examples": [ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - {"name": "Bar", "price": "35.4"}, - { - "name": "Baz", - "price": "thirty five point four", - }, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - "examples": [ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - {"name": "Bar", "price": "35.4"}, - { - "name": "Baz", - "price": "thirty five point four", - }, - ], - } - ) - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py deleted file mode 100644 index 2493194a0..000000000 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py +++ /dev/null @@ -1,177 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.schema_extra_example.tutorial004_an_py39 import app - - client = TestClient(app) - return client - - -# Test required and embedded body parameters with no bodies sent -@needs_py39 -def test_post_body_example(client: TestClient): - response = client.put( - "/items/5", - json={ - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - ) - assert response.status_code == 200 - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": IsDict( - { - "$ref": "#/components/schemas/Item", - "examples": [ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - {"name": "Bar", "price": "35.4"}, - { - "name": "Baz", - "price": "thirty five point four", - }, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - "examples": [ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - {"name": "Bar", "price": "35.4"}, - { - "name": "Baz", - "price": "thirty five point four", - }, - ], - } - ) - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_py310.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_py310.py deleted file mode 100644 index 15f54bd5a..000000000 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_py310.py +++ /dev/null @@ -1,177 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.schema_extra_example.tutorial004_py310 import app - - client = TestClient(app) - return client - - -# Test required and embedded body parameters with no bodies sent -@needs_py310 -def test_post_body_example(client: TestClient): - response = client.put( - "/items/5", - json={ - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - ) - assert response.status_code == 200 - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": IsDict( - { - "$ref": "#/components/schemas/Item", - "examples": [ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - {"name": "Bar", "price": "35.4"}, - { - "name": "Baz", - "price": "thirty five point four", - }, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - "examples": [ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - {"name": "Bar", "price": "35.4"}, - { - "name": "Baz", - "price": "thirty five point four", - }, - ], - } - ) - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial005.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial005.py index 94a40ed5a..12859227b 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial005.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial005.py @@ -1,13 +1,26 @@ +import importlib + import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient +from ...utils import needs_py39, needs_py310 + -@pytest.fixture(name="client") -def get_client(): - from docs_src.schema_extra_example.tutorial005 import app +@pytest.fixture( + name="client", + params=[ + "tutorial005", + pytest.param("tutorial005_py310", marks=needs_py310), + "tutorial005_an", + pytest.param("tutorial005_an_py39", marks=needs_py39), + pytest.param("tutorial005_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.schema_extra_example.{request.param}") - client = TestClient(app) + client = TestClient(mod.app) return client diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an.py deleted file mode 100644 index da92f98f6..000000000 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an.py +++ /dev/null @@ -1,166 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.schema_extra_example.tutorial005_an import app - - client = TestClient(app) - return client - - -def test_post_body_example(client: TestClient): - response = client.put( - "/items/5", - json={ - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - ) - assert response.status_code == 200 - - -def test_openapi_schema(client: TestClient) -> None: - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": IsDict({"$ref": "#/components/schemas/Item"}) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - } - ), - "examples": { - "normal": { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - }, - "converted": { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": {"name": "Bar", "price": "35.4"}, - }, - "invalid": { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", - }, - }, - }, - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an_py310.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an_py310.py deleted file mode 100644 index 9109cb14e..000000000 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an_py310.py +++ /dev/null @@ -1,170 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.schema_extra_example.tutorial005_an_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_post_body_example(client: TestClient): - response = client.put( - "/items/5", - json={ - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - ) - assert response.status_code == 200 - - -@needs_py310 -def test_openapi_schema(client: TestClient) -> None: - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": IsDict({"$ref": "#/components/schemas/Item"}) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - } - ), - "examples": { - "normal": { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - }, - "converted": { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": {"name": "Bar", "price": "35.4"}, - }, - "invalid": { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", - }, - }, - }, - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an_py39.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an_py39.py deleted file mode 100644 index fd4ec0575..000000000 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an_py39.py +++ /dev/null @@ -1,170 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.schema_extra_example.tutorial005_an_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_post_body_example(client: TestClient): - response = client.put( - "/items/5", - json={ - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - ) - assert response.status_code == 200 - - -@needs_py39 -def test_openapi_schema(client: TestClient) -> None: - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": IsDict({"$ref": "#/components/schemas/Item"}) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - } - ), - "examples": { - "normal": { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - }, - "converted": { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": {"name": "Bar", "price": "35.4"}, - }, - "invalid": { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", - }, - }, - }, - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial005_py310.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial005_py310.py deleted file mode 100644 index 05df53422..000000000 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial005_py310.py +++ /dev/null @@ -1,170 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.schema_extra_example.tutorial005_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_post_body_example(client: TestClient): - response = client.put( - "/items/5", - json={ - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - ) - assert response.status_code == 200 - - -@needs_py310 -def test_openapi_schema(client: TestClient) -> None: - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": IsDict({"$ref": "#/components/schemas/Item"}) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - } - ), - "examples": { - "normal": { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - }, - "converted": { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": {"name": "Bar", "price": "35.4"}, - }, - "invalid": { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", - }, - }, - }, - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_security/test_tutorial001.py b/tests/test_tutorial/test_security/test_tutorial001.py index 417bed8f7..f572d6e3e 100644 --- a/tests/test_tutorial/test_security/test_tutorial001.py +++ b/tests/test_tutorial/test_security/test_tutorial001.py @@ -1,31 +1,47 @@ +import importlib + +import pytest from fastapi.testclient import TestClient -from docs_src.security.tutorial001 import app +from ...utils import needs_py39 + + +@pytest.fixture( + name="client", + params=[ + "tutorial001", + "tutorial001_an", + pytest.param("tutorial001_an_py39", marks=needs_py39), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.security.{request.param}") -client = TestClient(app) + client = TestClient(mod.app) + return client -def test_no_token(): +def test_no_token(client: TestClient): response = client.get("/items") assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} assert response.headers["WWW-Authenticate"] == "Bearer" -def test_token(): +def test_token(client: TestClient): response = client.get("/items", headers={"Authorization": "Bearer testtoken"}) assert response.status_code == 200, response.text assert response.json() == {"token": "testtoken"} -def test_incorrect_token(): +def test_incorrect_token(client: TestClient): response = client.get("/items", headers={"Authorization": "Notexistent testtoken"}) assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} assert response.headers["WWW-Authenticate"] == "Bearer" -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_security/test_tutorial001_an.py b/tests/test_tutorial/test_security/test_tutorial001_an.py deleted file mode 100644 index 59460da7f..000000000 --- a/tests/test_tutorial/test_security/test_tutorial001_an.py +++ /dev/null @@ -1,57 +0,0 @@ -from fastapi.testclient import TestClient - -from docs_src.security.tutorial001_an import app - -client = TestClient(app) - - -def test_no_token(): - response = client.get("/items") - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -def test_token(): - response = client.get("/items", headers={"Authorization": "Bearer testtoken"}) - assert response.status_code == 200, response.text - assert response.json() == {"token": "testtoken"} - - -def test_incorrect_token(): - response = client.get("/items", headers={"Authorization": "Notexistent testtoken"}) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "security": [{"OAuth2PasswordBearer": []}], - } - } - }, - "components": { - "securitySchemes": { - "OAuth2PasswordBearer": { - "type": "oauth2", - "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, - } - } - }, - } diff --git a/tests/test_tutorial/test_security/test_tutorial001_an_py39.py b/tests/test_tutorial/test_security/test_tutorial001_an_py39.py deleted file mode 100644 index d8e712773..000000000 --- a/tests/test_tutorial/test_security/test_tutorial001_an_py39.py +++ /dev/null @@ -1,68 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.security.tutorial001_an_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_no_token(client: TestClient): - response = client.get("/items") - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py39 -def test_token(client: TestClient): - response = client.get("/items", headers={"Authorization": "Bearer testtoken"}) - assert response.status_code == 200, response.text - assert response.json() == {"token": "testtoken"} - - -@needs_py39 -def test_incorrect_token(client: TestClient): - response = client.get("/items", headers={"Authorization": "Notexistent testtoken"}) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "security": [{"OAuth2PasswordBearer": []}], - } - } - }, - "components": { - "securitySchemes": { - "OAuth2PasswordBearer": { - "type": "oauth2", - "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, - } - } - }, - } diff --git a/tests/test_tutorial/test_security/test_tutorial003.py b/tests/test_tutorial/test_security/test_tutorial003.py index 18d4680f6..7a4c99401 100644 --- a/tests/test_tutorial/test_security/test_tutorial003.py +++ b/tests/test_tutorial/test_security/test_tutorial003.py @@ -1,18 +1,36 @@ +import importlib + +import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient -from docs_src.security.tutorial003 import app +from ...utils import needs_py39, needs_py310 + + +@pytest.fixture( + name="client", + params=[ + "tutorial003", + pytest.param("tutorial003_py310", marks=needs_py310), + "tutorial003_an", + pytest.param("tutorial003_an_py39", marks=needs_py39), + pytest.param("tutorial003_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.security.{request.param}") -client = TestClient(app) + client = TestClient(mod.app) + return client -def test_login(): +def test_login(client: TestClient): response = client.post("/token", data={"username": "johndoe", "password": "secret"}) assert response.status_code == 200, response.text assert response.json() == {"access_token": "johndoe", "token_type": "bearer"} -def test_login_incorrect_password(): +def test_login_incorrect_password(client: TestClient): response = client.post( "/token", data={"username": "johndoe", "password": "incorrect"} ) @@ -20,20 +38,20 @@ def test_login_incorrect_password(): assert response.json() == {"detail": "Incorrect username or password"} -def test_login_incorrect_username(): +def test_login_incorrect_username(client: TestClient): response = client.post("/token", data={"username": "foo", "password": "secret"}) assert response.status_code == 400, response.text assert response.json() == {"detail": "Incorrect username or password"} -def test_no_token(): +def test_no_token(client: TestClient): response = client.get("/users/me") assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} assert response.headers["WWW-Authenticate"] == "Bearer" -def test_token(): +def test_token(client: TestClient): response = client.get("/users/me", headers={"Authorization": "Bearer johndoe"}) assert response.status_code == 200, response.text assert response.json() == { @@ -45,14 +63,14 @@ def test_token(): } -def test_incorrect_token(): +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.headers["WWW-Authenticate"] == "Bearer" -def test_incorrect_token_type(): +def test_incorrect_token_type(client: TestClient): response = client.get( "/users/me", headers={"Authorization": "Notexistent testtoken"} ) @@ -61,13 +79,13 @@ def test_incorrect_token_type(): assert response.headers["WWW-Authenticate"] == "Bearer" -def test_inactive_user(): +def test_inactive_user(client: TestClient): response = client.get("/users/me", headers={"Authorization": "Bearer alice"}) assert response.status_code == 400, response.text assert response.json() == {"detail": "Inactive user"} -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_security/test_tutorial003_an.py b/tests/test_tutorial/test_security/test_tutorial003_an.py deleted file mode 100644 index a8f64d0c6..000000000 --- a/tests/test_tutorial/test_security/test_tutorial003_an.py +++ /dev/null @@ -1,207 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from docs_src.security.tutorial003_an import app - -client = TestClient(app) - - -def test_login(): - response = client.post("/token", data={"username": "johndoe", "password": "secret"}) - assert response.status_code == 200, response.text - assert response.json() == {"access_token": "johndoe", "token_type": "bearer"} - - -def test_login_incorrect_password(): - response = client.post( - "/token", data={"username": "johndoe", "password": "incorrect"} - ) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Incorrect username or password"} - - -def test_login_incorrect_username(): - response = client.post("/token", data={"username": "foo", "password": "secret"}) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Incorrect username or password"} - - -def test_no_token(): - response = client.get("/users/me") - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -def test_token(): - response = client.get("/users/me", headers={"Authorization": "Bearer johndoe"}) - assert response.status_code == 200, response.text - assert response.json() == { - "username": "johndoe", - "full_name": "John Doe", - "email": "johndoe@example.com", - "hashed_password": "fakehashedsecret", - "disabled": False, - } - - -def test_incorrect_token(): - 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.headers["WWW-Authenticate"] == "Bearer" - - -def test_incorrect_token_type(): - response = client.get( - "/users/me", headers={"Authorization": "Notexistent testtoken"} - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -def test_inactive_user(): - response = client.get("/users/me", headers={"Authorization": "Bearer alice"}) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Inactive user"} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/token": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login", - "operationId": "login_token_post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_token_post" - } - } - }, - "required": True, - }, - } - }, - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Users Me", - "operationId": "read_users_me_users_me_get", - "security": [{"OAuth2PasswordBearer": []}], - } - }, - }, - "components": { - "schemas": { - "Body_login_token_post": { - "title": "Body_login_token_post", - "required": ["username", "password"], - "type": "object", - "properties": { - "grant_type": IsDict( - { - "title": "Grant Type", - "anyOf": [ - {"pattern": "password", "type": "string"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Grant Type", - "pattern": "password", - "type": "string", - } - ), - "username": {"title": "Username", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": IsDict( - { - "title": "Client Id", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Id", "type": "string"} - ), - "client_secret": IsDict( - { - "title": "Client Secret", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Secret", "type": "string"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - }, - "securitySchemes": { - "OAuth2PasswordBearer": { - "type": "oauth2", - "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, - } - }, - }, - } diff --git a/tests/test_tutorial/test_security/test_tutorial003_an_py310.py b/tests/test_tutorial/test_security/test_tutorial003_an_py310.py deleted file mode 100644 index 7cbbcee2f..000000000 --- a/tests/test_tutorial/test_security/test_tutorial003_an_py310.py +++ /dev/null @@ -1,223 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.security.tutorial003_an_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_login(client: TestClient): - response = client.post("/token", data={"username": "johndoe", "password": "secret"}) - assert response.status_code == 200, response.text - assert response.json() == {"access_token": "johndoe", "token_type": "bearer"} - - -@needs_py310 -def test_login_incorrect_password(client: TestClient): - response = client.post( - "/token", data={"username": "johndoe", "password": "incorrect"} - ) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Incorrect username or password"} - - -@needs_py310 -def test_login_incorrect_username(client: TestClient): - response = client.post("/token", data={"username": "foo", "password": "secret"}) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Incorrect username or password"} - - -@needs_py310 -def test_no_token(client: TestClient): - response = client.get("/users/me") - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py310 -def test_token(client: TestClient): - response = client.get("/users/me", headers={"Authorization": "Bearer johndoe"}) - assert response.status_code == 200, response.text - assert response.json() == { - "username": "johndoe", - "full_name": "John Doe", - "email": "johndoe@example.com", - "hashed_password": "fakehashedsecret", - "disabled": False, - } - - -@needs_py310 -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.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py310 -def test_incorrect_token_type(client: TestClient): - response = client.get( - "/users/me", headers={"Authorization": "Notexistent testtoken"} - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py310 -def test_inactive_user(client: TestClient): - response = client.get("/users/me", headers={"Authorization": "Bearer alice"}) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Inactive user"} - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/token": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login", - "operationId": "login_token_post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_token_post" - } - } - }, - "required": True, - }, - } - }, - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Users Me", - "operationId": "read_users_me_users_me_get", - "security": [{"OAuth2PasswordBearer": []}], - } - }, - }, - "components": { - "schemas": { - "Body_login_token_post": { - "title": "Body_login_token_post", - "required": ["username", "password"], - "type": "object", - "properties": { - "grant_type": IsDict( - { - "title": "Grant Type", - "anyOf": [ - {"pattern": "password", "type": "string"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Grant Type", - "pattern": "password", - "type": "string", - } - ), - "username": {"title": "Username", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": IsDict( - { - "title": "Client Id", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Id", "type": "string"} - ), - "client_secret": IsDict( - { - "title": "Client Secret", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Secret", "type": "string"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - }, - "securitySchemes": { - "OAuth2PasswordBearer": { - "type": "oauth2", - "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, - } - }, - }, - } diff --git a/tests/test_tutorial/test_security/test_tutorial003_an_py39.py b/tests/test_tutorial/test_security/test_tutorial003_an_py39.py deleted file mode 100644 index 7b21fbcc9..000000000 --- a/tests/test_tutorial/test_security/test_tutorial003_an_py39.py +++ /dev/null @@ -1,223 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.security.tutorial003_an_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_login(client: TestClient): - response = client.post("/token", data={"username": "johndoe", "password": "secret"}) - assert response.status_code == 200, response.text - assert response.json() == {"access_token": "johndoe", "token_type": "bearer"} - - -@needs_py39 -def test_login_incorrect_password(client: TestClient): - response = client.post( - "/token", data={"username": "johndoe", "password": "incorrect"} - ) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Incorrect username or password"} - - -@needs_py39 -def test_login_incorrect_username(client: TestClient): - response = client.post("/token", data={"username": "foo", "password": "secret"}) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Incorrect username or password"} - - -@needs_py39 -def test_no_token(client: TestClient): - response = client.get("/users/me") - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py39 -def test_token(client: TestClient): - response = client.get("/users/me", headers={"Authorization": "Bearer johndoe"}) - assert response.status_code == 200, response.text - assert response.json() == { - "username": "johndoe", - "full_name": "John Doe", - "email": "johndoe@example.com", - "hashed_password": "fakehashedsecret", - "disabled": False, - } - - -@needs_py39 -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.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py39 -def test_incorrect_token_type(client: TestClient): - response = client.get( - "/users/me", headers={"Authorization": "Notexistent testtoken"} - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py39 -def test_inactive_user(client: TestClient): - response = client.get("/users/me", headers={"Authorization": "Bearer alice"}) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Inactive user"} - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/token": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login", - "operationId": "login_token_post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_token_post" - } - } - }, - "required": True, - }, - } - }, - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Users Me", - "operationId": "read_users_me_users_me_get", - "security": [{"OAuth2PasswordBearer": []}], - } - }, - }, - "components": { - "schemas": { - "Body_login_token_post": { - "title": "Body_login_token_post", - "required": ["username", "password"], - "type": "object", - "properties": { - "grant_type": IsDict( - { - "title": "Grant Type", - "anyOf": [ - {"pattern": "password", "type": "string"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Grant Type", - "pattern": "password", - "type": "string", - } - ), - "username": {"title": "Username", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": IsDict( - { - "title": "Client Id", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Id", "type": "string"} - ), - "client_secret": IsDict( - { - "title": "Client Secret", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Secret", "type": "string"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - }, - "securitySchemes": { - "OAuth2PasswordBearer": { - "type": "oauth2", - "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, - } - }, - }, - } diff --git a/tests/test_tutorial/test_security/test_tutorial003_py310.py b/tests/test_tutorial/test_security/test_tutorial003_py310.py deleted file mode 100644 index 512504534..000000000 --- a/tests/test_tutorial/test_security/test_tutorial003_py310.py +++ /dev/null @@ -1,223 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.security.tutorial003_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_login(client: TestClient): - response = client.post("/token", data={"username": "johndoe", "password": "secret"}) - assert response.status_code == 200, response.text - assert response.json() == {"access_token": "johndoe", "token_type": "bearer"} - - -@needs_py310 -def test_login_incorrect_password(client: TestClient): - response = client.post( - "/token", data={"username": "johndoe", "password": "incorrect"} - ) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Incorrect username or password"} - - -@needs_py310 -def test_login_incorrect_username(client: TestClient): - response = client.post("/token", data={"username": "foo", "password": "secret"}) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Incorrect username or password"} - - -@needs_py310 -def test_no_token(client: TestClient): - response = client.get("/users/me") - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py310 -def test_token(client: TestClient): - response = client.get("/users/me", headers={"Authorization": "Bearer johndoe"}) - assert response.status_code == 200, response.text - assert response.json() == { - "username": "johndoe", - "full_name": "John Doe", - "email": "johndoe@example.com", - "hashed_password": "fakehashedsecret", - "disabled": False, - } - - -@needs_py310 -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.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py310 -def test_incorrect_token_type(client: TestClient): - response = client.get( - "/users/me", headers={"Authorization": "Notexistent testtoken"} - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py310 -def test_inactive_user(client: TestClient): - response = client.get("/users/me", headers={"Authorization": "Bearer alice"}) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Inactive user"} - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/token": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login", - "operationId": "login_token_post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_token_post" - } - } - }, - "required": True, - }, - } - }, - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Users Me", - "operationId": "read_users_me_users_me_get", - "security": [{"OAuth2PasswordBearer": []}], - } - }, - }, - "components": { - "schemas": { - "Body_login_token_post": { - "title": "Body_login_token_post", - "required": ["username", "password"], - "type": "object", - "properties": { - "grant_type": IsDict( - { - "title": "Grant Type", - "anyOf": [ - {"pattern": "password", "type": "string"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Grant Type", - "pattern": "password", - "type": "string", - } - ), - "username": {"title": "Username", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": IsDict( - { - "title": "Client Id", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Id", "type": "string"} - ), - "client_secret": IsDict( - { - "title": "Client Secret", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Secret", "type": "string"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - }, - "securitySchemes": { - "OAuth2PasswordBearer": { - "type": "oauth2", - "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, - } - }, - }, - } diff --git a/tests/test_tutorial/test_security/test_tutorial005.py b/tests/test_tutorial/test_security/test_tutorial005.py index 2e580dbb3..c7f791b03 100644 --- a/tests/test_tutorial/test_security/test_tutorial005.py +++ b/tests/test_tutorial/test_security/test_tutorial005.py @@ -1,18 +1,33 @@ +import importlib +from types import ModuleType + +import pytest from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient -from docs_src.security.tutorial005 import ( - app, - create_access_token, - fake_users_db, - get_password_hash, - verify_password, +from ...utils import needs_py39, needs_py310 + + +@pytest.fixture( + name="mod", + params=[ + "tutorial005", + pytest.param("tutorial005_py310", marks=needs_py310), + "tutorial005_an", + pytest.param("tutorial005_py39", marks=needs_py39), + pytest.param("tutorial005_an_py39", marks=needs_py39), + pytest.param("tutorial005_an_py310", marks=needs_py310), + ], ) +def get_mod(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.security.{request.param}") -client = TestClient(app) + return mod -def get_access_token(username="johndoe", password="secret", scope=None): +def get_access_token( + *, username="johndoe", password="secret", scope=None, client: TestClient +): data = {"username": username, "password": password} if scope: data["scope"] = scope @@ -22,7 +37,8 @@ def get_access_token(username="johndoe", password="secret", scope=None): return access_token -def test_login(): +def test_login(mod: ModuleType): + client = TestClient(mod.app) response = client.post("/token", data={"username": "johndoe", "password": "secret"}) assert response.status_code == 200, response.text content = response.json() @@ -30,7 +46,8 @@ def test_login(): assert content["token_type"] == "bearer" -def test_login_incorrect_password(): +def test_login_incorrect_password(mod: ModuleType): + client = TestClient(mod.app) response = client.post( "/token", data={"username": "johndoe", "password": "incorrect"} ) @@ -38,21 +55,24 @@ def test_login_incorrect_password(): assert response.json() == {"detail": "Incorrect username or password"} -def test_login_incorrect_username(): +def test_login_incorrect_username(mod: ModuleType): + client = TestClient(mod.app) response = client.post("/token", data={"username": "foo", "password": "secret"}) assert response.status_code == 400, response.text assert response.json() == {"detail": "Incorrect username or password"} -def test_no_token(): +def test_no_token(mod: ModuleType): + client = TestClient(mod.app) response = client.get("/users/me") assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} assert response.headers["WWW-Authenticate"] == "Bearer" -def test_token(): - access_token = get_access_token(scope="me") +def test_token(mod: ModuleType): + client = TestClient(mod.app) + access_token = get_access_token(scope="me", client=client) response = client.get( "/users/me", headers={"Authorization": f"Bearer {access_token}"} ) @@ -65,14 +85,16 @@ def test_token(): } -def test_incorrect_token(): +def test_incorrect_token(mod: ModuleType): + client = TestClient(mod.app) response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) assert response.status_code == 401, response.text assert response.json() == {"detail": "Could not validate credentials"} assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' -def test_incorrect_token_type(): +def test_incorrect_token_type(mod: ModuleType): + client = TestClient(mod.app) response = client.get( "/users/me", headers={"Authorization": "Notexistent testtoken"} ) @@ -81,20 +103,24 @@ def test_incorrect_token_type(): assert response.headers["WWW-Authenticate"] == "Bearer" -def test_verify_password(): - assert verify_password("secret", fake_users_db["johndoe"]["hashed_password"]) +def test_verify_password(mod: ModuleType): + assert mod.verify_password( + "secret", mod.fake_users_db["johndoe"]["hashed_password"] + ) -def test_get_password_hash(): - assert get_password_hash("secretalice") +def test_get_password_hash(mod: ModuleType): + assert mod.get_password_hash("secretalice") -def test_create_access_token(): - access_token = create_access_token(data={"data": "foo"}) +def test_create_access_token(mod: ModuleType): + access_token = mod.create_access_token(data={"data": "foo"}) assert access_token -def test_token_no_sub(): +def test_token_no_sub(mod: ModuleType): + client = TestClient(mod.app) + response = client.get( "/users/me", headers={ @@ -106,7 +132,9 @@ def test_token_no_sub(): assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' -def test_token_no_username(): +def test_token_no_username(mod: ModuleType): + client = TestClient(mod.app) + response = client.get( "/users/me", headers={ @@ -118,8 +146,10 @@ def test_token_no_username(): assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' -def test_token_no_scope(): - access_token = get_access_token() +def test_token_no_scope(mod: ModuleType): + client = TestClient(mod.app) + + access_token = get_access_token(client=client) response = client.get( "/users/me", headers={"Authorization": f"Bearer {access_token}"} ) @@ -128,7 +158,9 @@ def test_token_no_scope(): assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' -def test_token_nonexistent_user(): +def test_token_nonexistent_user(mod: ModuleType): + client = TestClient(mod.app) + response = client.get( "/users/me", headers={ @@ -140,9 +172,11 @@ def test_token_nonexistent_user(): assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' -def test_token_inactive_user(): +def test_token_inactive_user(mod: ModuleType): + client = TestClient(mod.app) + access_token = get_access_token( - username="alice", password="secretalice", scope="me" + username="alice", password="secretalice", scope="me", client=client ) response = client.get( "/users/me", headers={"Authorization": f"Bearer {access_token}"} @@ -151,8 +185,9 @@ def test_token_inactive_user(): assert response.json() == {"detail": "Inactive user"} -def test_read_items(): - access_token = get_access_token(scope="me items") +def test_read_items(mod: ModuleType): + client = TestClient(mod.app) + access_token = get_access_token(scope="me items", client=client) response = client.get( "/users/me/items/", headers={"Authorization": f"Bearer {access_token}"} ) @@ -160,8 +195,9 @@ def test_read_items(): assert response.json() == [{"item_id": "Foo", "owner": "johndoe"}] -def test_read_system_status(): - access_token = get_access_token() +def test_read_system_status(mod: ModuleType): + client = TestClient(mod.app) + access_token = get_access_token(client=client) response = client.get( "/status/", headers={"Authorization": f"Bearer {access_token}"} ) @@ -169,14 +205,16 @@ def test_read_system_status(): assert response.json() == {"status": "ok"} -def test_read_system_status_no_token(): +def test_read_system_status_no_token(mod: ModuleType): + client = TestClient(mod.app) response = client.get("/status/") assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} assert response.headers["WWW-Authenticate"] == "Bearer" -def test_openapi_schema(): +def test_openapi_schema(mod: ModuleType): + client = TestClient(mod.app) response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_security/test_tutorial005_an.py b/tests/test_tutorial/test_security/test_tutorial005_an.py deleted file mode 100644 index 04c7d60bc..000000000 --- a/tests/test_tutorial/test_security/test_tutorial005_an.py +++ /dev/null @@ -1,409 +0,0 @@ -from dirty_equals import IsDict, IsOneOf -from fastapi.testclient import TestClient - -from docs_src.security.tutorial005_an import ( - app, - create_access_token, - fake_users_db, - get_password_hash, - verify_password, -) - -client = TestClient(app) - - -def get_access_token(username="johndoe", password="secret", scope=None): - data = {"username": username, "password": password} - if scope: - data["scope"] = scope - response = client.post("/token", data=data) - content = response.json() - access_token = content.get("access_token") - return access_token - - -def test_login(): - response = client.post("/token", data={"username": "johndoe", "password": "secret"}) - assert response.status_code == 200, response.text - content = response.json() - assert "access_token" in content - assert content["token_type"] == "bearer" - - -def test_login_incorrect_password(): - response = client.post( - "/token", data={"username": "johndoe", "password": "incorrect"} - ) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Incorrect username or password"} - - -def test_login_incorrect_username(): - response = client.post("/token", data={"username": "foo", "password": "secret"}) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Incorrect username or password"} - - -def test_no_token(): - response = client.get("/users/me") - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -def test_token(): - access_token = get_access_token(scope="me") - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 200, response.text - assert response.json() == { - "username": "johndoe", - "full_name": "John Doe", - "email": "johndoe@example.com", - "disabled": False, - } - - -def test_incorrect_token(): - response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -def test_incorrect_token_type(): - response = client.get( - "/users/me", headers={"Authorization": "Notexistent testtoken"} - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -def test_verify_password(): - assert verify_password("secret", fake_users_db["johndoe"]["hashed_password"]) - - -def test_get_password_hash(): - assert get_password_hash("secretalice") - - -def test_create_access_token(): - access_token = create_access_token(data={"data": "foo"}) - assert access_token - - -def test_token_no_sub(): - response = client.get( - "/users/me", - headers={ - "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiZm9vIn0.9ynBhuYb4e6aW3oJr_K_TBgwcMTDpRToQIE25L57rOE" - }, - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -def test_token_no_username(): - response = client.get( - "/users/me", - headers={ - "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmb28ifQ.NnExK_dlNAYyzACrXtXDrcWOgGY2JuPbI4eDaHdfK5Y" - }, - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -def test_token_no_scope(): - access_token = get_access_token() - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not enough permissions"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -def test_token_nonexistent_user(): - response = client.get( - "/users/me", - headers={ - "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VybmFtZTpib2IifQ.HcfCW67Uda-0gz54ZWTqmtgJnZeNem0Q757eTa9EZuw" - }, - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -def test_token_inactive_user(): - access_token = get_access_token( - username="alice", password="secretalice", scope="me" - ) - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Inactive user"} - - -def test_read_items(): - access_token = get_access_token(scope="me items") - response = client.get( - "/users/me/items/", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 200, response.text - assert response.json() == [{"item_id": "Foo", "owner": "johndoe"}] - - -def test_read_system_status(): - access_token = get_access_token() - response = client.get( - "/status/", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 200, response.text - assert response.json() == {"status": "ok"} - - -def test_read_system_status_no_token(): - response = client.get("/status/") - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/token": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Token"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login For Access Token", - "operationId": "login_for_access_token_token_post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_for_access_token_token_post" - } - } - }, - "required": True, - }, - } - }, - "/users/me/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - "summary": "Read Users Me", - "operationId": "read_users_me_users_me__get", - "security": [{"OAuth2PasswordBearer": ["me"]}], - } - }, - "/users/me/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Own Items", - "operationId": "read_own_items_users_me_items__get", - "security": [{"OAuth2PasswordBearer": ["items", "me"]}], - } - }, - "/status/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read System Status", - "operationId": "read_system_status_status__get", - "security": [{"OAuth2PasswordBearer": []}], - } - }, - }, - "components": { - "schemas": { - "User": { - "title": "User", - "required": IsOneOf( - ["username", "email", "full_name", "disabled"], - # TODO: remove when deprecating Pydantic v1 - ["username"], - ), - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "email": IsDict( - { - "title": "Email", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Email", "type": "string"} - ), - "full_name": IsDict( - { - "title": "Full Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Full Name", "type": "string"} - ), - "disabled": IsDict( - { - "title": "Disabled", - "anyOf": [{"type": "boolean"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Disabled", "type": "boolean"} - ), - }, - }, - "Token": { - "title": "Token", - "required": ["access_token", "token_type"], - "type": "object", - "properties": { - "access_token": {"title": "Access Token", "type": "string"}, - "token_type": {"title": "Token Type", "type": "string"}, - }, - }, - "Body_login_for_access_token_token_post": { - "title": "Body_login_for_access_token_token_post", - "required": ["username", "password"], - "type": "object", - "properties": { - "grant_type": IsDict( - { - "title": "Grant Type", - "anyOf": [ - {"pattern": "password", "type": "string"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Grant Type", - "pattern": "password", - "type": "string", - } - ), - "username": {"title": "Username", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": IsDict( - { - "title": "Client Id", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Id", "type": "string"} - ), - "client_secret": IsDict( - { - "title": "Client Secret", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Secret", "type": "string"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - }, - "securitySchemes": { - "OAuth2PasswordBearer": { - "type": "oauth2", - "flows": { - "password": { - "scopes": { - "me": "Read information about the current user.", - "items": "Read items.", - }, - "tokenUrl": "token", - } - }, - } - }, - }, - } diff --git a/tests/test_tutorial/test_security/test_tutorial005_an_py310.py b/tests/test_tutorial/test_security/test_tutorial005_an_py310.py deleted file mode 100644 index 9c7f83ed2..000000000 --- a/tests/test_tutorial/test_security/test_tutorial005_an_py310.py +++ /dev/null @@ -1,437 +0,0 @@ -import pytest -from dirty_equals import IsDict, IsOneOf -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.security.tutorial005_an_py310 import app - - client = TestClient(app) - return client - - -def get_access_token( - *, username="johndoe", password="secret", scope=None, client: TestClient -): - data = {"username": username, "password": password} - if scope: - data["scope"] = scope - response = client.post("/token", data=data) - content = response.json() - access_token = content.get("access_token") - return access_token - - -@needs_py310 -def test_login(client: TestClient): - response = client.post("/token", data={"username": "johndoe", "password": "secret"}) - assert response.status_code == 200, response.text - content = response.json() - assert "access_token" in content - assert content["token_type"] == "bearer" - - -@needs_py310 -def test_login_incorrect_password(client: TestClient): - response = client.post( - "/token", data={"username": "johndoe", "password": "incorrect"} - ) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Incorrect username or password"} - - -@needs_py310 -def test_login_incorrect_username(client: TestClient): - response = client.post("/token", data={"username": "foo", "password": "secret"}) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Incorrect username or password"} - - -@needs_py310 -def test_no_token(client: TestClient): - response = client.get("/users/me") - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py310 -def test_token(client: TestClient): - access_token = get_access_token(scope="me", client=client) - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 200, response.text - assert response.json() == { - "username": "johndoe", - "full_name": "John Doe", - "email": "johndoe@example.com", - "disabled": False, - } - - -@needs_py310 -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": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py310 -def test_incorrect_token_type(client: TestClient): - response = client.get( - "/users/me", headers={"Authorization": "Notexistent testtoken"} - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py310 -def test_verify_password(): - from docs_src.security.tutorial005_an_py310 import fake_users_db, verify_password - - assert verify_password("secret", fake_users_db["johndoe"]["hashed_password"]) - - -@needs_py310 -def test_get_password_hash(): - from docs_src.security.tutorial005_an_py310 import get_password_hash - - assert get_password_hash("secretalice") - - -@needs_py310 -def test_create_access_token(): - from docs_src.security.tutorial005_an_py310 import create_access_token - - access_token = create_access_token(data={"data": "foo"}) - assert access_token - - -@needs_py310 -def test_token_no_sub(client: TestClient): - response = client.get( - "/users/me", - headers={ - "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiZm9vIn0.9ynBhuYb4e6aW3oJr_K_TBgwcMTDpRToQIE25L57rOE" - }, - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py310 -def test_token_no_username(client: TestClient): - response = client.get( - "/users/me", - headers={ - "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmb28ifQ.NnExK_dlNAYyzACrXtXDrcWOgGY2JuPbI4eDaHdfK5Y" - }, - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py310 -def test_token_no_scope(client: TestClient): - access_token = get_access_token(client=client) - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not enough permissions"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py310 -def test_token_nonexistent_user(client: TestClient): - response = client.get( - "/users/me", - headers={ - "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VybmFtZTpib2IifQ.HcfCW67Uda-0gz54ZWTqmtgJnZeNem0Q757eTa9EZuw" - }, - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py310 -def test_token_inactive_user(client: TestClient): - access_token = get_access_token( - username="alice", password="secretalice", scope="me", client=client - ) - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Inactive user"} - - -@needs_py310 -def test_read_items(client: TestClient): - access_token = get_access_token(scope="me items", client=client) - response = client.get( - "/users/me/items/", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 200, response.text - assert response.json() == [{"item_id": "Foo", "owner": "johndoe"}] - - -@needs_py310 -def test_read_system_status(client: TestClient): - access_token = get_access_token(client=client) - response = client.get( - "/status/", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 200, response.text - assert response.json() == {"status": "ok"} - - -@needs_py310 -def test_read_system_status_no_token(client: TestClient): - response = client.get("/status/") - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/token": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Token"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login For Access Token", - "operationId": "login_for_access_token_token_post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_for_access_token_token_post" - } - } - }, - "required": True, - }, - } - }, - "/users/me/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - "summary": "Read Users Me", - "operationId": "read_users_me_users_me__get", - "security": [{"OAuth2PasswordBearer": ["me"]}], - } - }, - "/users/me/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Own Items", - "operationId": "read_own_items_users_me_items__get", - "security": [{"OAuth2PasswordBearer": ["items", "me"]}], - } - }, - "/status/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read System Status", - "operationId": "read_system_status_status__get", - "security": [{"OAuth2PasswordBearer": []}], - } - }, - }, - "components": { - "schemas": { - "User": { - "title": "User", - "required": IsOneOf( - ["username", "email", "full_name", "disabled"], - # TODO: remove when deprecating Pydantic v1 - ["username"], - ), - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "email": IsDict( - { - "title": "Email", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Email", "type": "string"} - ), - "full_name": IsDict( - { - "title": "Full Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Full Name", "type": "string"} - ), - "disabled": IsDict( - { - "title": "Disabled", - "anyOf": [{"type": "boolean"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Disabled", "type": "boolean"} - ), - }, - }, - "Token": { - "title": "Token", - "required": ["access_token", "token_type"], - "type": "object", - "properties": { - "access_token": {"title": "Access Token", "type": "string"}, - "token_type": {"title": "Token Type", "type": "string"}, - }, - }, - "Body_login_for_access_token_token_post": { - "title": "Body_login_for_access_token_token_post", - "required": ["username", "password"], - "type": "object", - "properties": { - "grant_type": IsDict( - { - "title": "Grant Type", - "anyOf": [ - {"pattern": "password", "type": "string"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Grant Type", - "pattern": "password", - "type": "string", - } - ), - "username": {"title": "Username", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": IsDict( - { - "title": "Client Id", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Id", "type": "string"} - ), - "client_secret": IsDict( - { - "title": "Client Secret", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Secret", "type": "string"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - }, - "securitySchemes": { - "OAuth2PasswordBearer": { - "type": "oauth2", - "flows": { - "password": { - "scopes": { - "me": "Read information about the current user.", - "items": "Read items.", - }, - "tokenUrl": "token", - } - }, - } - }, - }, - } diff --git a/tests/test_tutorial/test_security/test_tutorial005_an_py39.py b/tests/test_tutorial/test_security/test_tutorial005_an_py39.py deleted file mode 100644 index 04cc1b014..000000000 --- a/tests/test_tutorial/test_security/test_tutorial005_an_py39.py +++ /dev/null @@ -1,437 +0,0 @@ -import pytest -from dirty_equals import IsDict, IsOneOf -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.security.tutorial005_an_py39 import app - - client = TestClient(app) - return client - - -def get_access_token( - *, username="johndoe", password="secret", scope=None, client: TestClient -): - data = {"username": username, "password": password} - if scope: - data["scope"] = scope - response = client.post("/token", data=data) - content = response.json() - access_token = content.get("access_token") - return access_token - - -@needs_py39 -def test_login(client: TestClient): - response = client.post("/token", data={"username": "johndoe", "password": "secret"}) - assert response.status_code == 200, response.text - content = response.json() - assert "access_token" in content - assert content["token_type"] == "bearer" - - -@needs_py39 -def test_login_incorrect_password(client: TestClient): - response = client.post( - "/token", data={"username": "johndoe", "password": "incorrect"} - ) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Incorrect username or password"} - - -@needs_py39 -def test_login_incorrect_username(client: TestClient): - response = client.post("/token", data={"username": "foo", "password": "secret"}) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Incorrect username or password"} - - -@needs_py39 -def test_no_token(client: TestClient): - response = client.get("/users/me") - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py39 -def test_token(client: TestClient): - access_token = get_access_token(scope="me", client=client) - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 200, response.text - assert response.json() == { - "username": "johndoe", - "full_name": "John Doe", - "email": "johndoe@example.com", - "disabled": False, - } - - -@needs_py39 -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": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py39 -def test_incorrect_token_type(client: TestClient): - response = client.get( - "/users/me", headers={"Authorization": "Notexistent testtoken"} - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py39 -def test_verify_password(): - from docs_src.security.tutorial005_an_py39 import fake_users_db, verify_password - - assert verify_password("secret", fake_users_db["johndoe"]["hashed_password"]) - - -@needs_py39 -def test_get_password_hash(): - from docs_src.security.tutorial005_an_py39 import get_password_hash - - assert get_password_hash("secretalice") - - -@needs_py39 -def test_create_access_token(): - from docs_src.security.tutorial005_an_py39 import create_access_token - - access_token = create_access_token(data={"data": "foo"}) - assert access_token - - -@needs_py39 -def test_token_no_sub(client: TestClient): - response = client.get( - "/users/me", - headers={ - "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiZm9vIn0.9ynBhuYb4e6aW3oJr_K_TBgwcMTDpRToQIE25L57rOE" - }, - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py39 -def test_token_no_username(client: TestClient): - response = client.get( - "/users/me", - headers={ - "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmb28ifQ.NnExK_dlNAYyzACrXtXDrcWOgGY2JuPbI4eDaHdfK5Y" - }, - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py39 -def test_token_no_scope(client: TestClient): - access_token = get_access_token(client=client) - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not enough permissions"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py39 -def test_token_nonexistent_user(client: TestClient): - response = client.get( - "/users/me", - headers={ - "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VybmFtZTpib2IifQ.HcfCW67Uda-0gz54ZWTqmtgJnZeNem0Q757eTa9EZuw" - }, - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py39 -def test_token_inactive_user(client: TestClient): - access_token = get_access_token( - username="alice", password="secretalice", scope="me", client=client - ) - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Inactive user"} - - -@needs_py39 -def test_read_items(client: TestClient): - access_token = get_access_token(scope="me items", client=client) - response = client.get( - "/users/me/items/", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 200, response.text - assert response.json() == [{"item_id": "Foo", "owner": "johndoe"}] - - -@needs_py39 -def test_read_system_status(client: TestClient): - access_token = get_access_token(client=client) - response = client.get( - "/status/", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 200, response.text - assert response.json() == {"status": "ok"} - - -@needs_py39 -def test_read_system_status_no_token(client: TestClient): - response = client.get("/status/") - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/token": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Token"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login For Access Token", - "operationId": "login_for_access_token_token_post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_for_access_token_token_post" - } - } - }, - "required": True, - }, - } - }, - "/users/me/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - "summary": "Read Users Me", - "operationId": "read_users_me_users_me__get", - "security": [{"OAuth2PasswordBearer": ["me"]}], - } - }, - "/users/me/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Own Items", - "operationId": "read_own_items_users_me_items__get", - "security": [{"OAuth2PasswordBearer": ["items", "me"]}], - } - }, - "/status/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read System Status", - "operationId": "read_system_status_status__get", - "security": [{"OAuth2PasswordBearer": []}], - } - }, - }, - "components": { - "schemas": { - "User": { - "title": "User", - "required": IsOneOf( - ["username", "email", "full_name", "disabled"], - # TODO: remove when deprecating Pydantic v1 - ["username"], - ), - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "email": IsDict( - { - "title": "Email", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Email", "type": "string"} - ), - "full_name": IsDict( - { - "title": "Full Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Full Name", "type": "string"} - ), - "disabled": IsDict( - { - "title": "Disabled", - "anyOf": [{"type": "boolean"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Disabled", "type": "boolean"} - ), - }, - }, - "Token": { - "title": "Token", - "required": ["access_token", "token_type"], - "type": "object", - "properties": { - "access_token": {"title": "Access Token", "type": "string"}, - "token_type": {"title": "Token Type", "type": "string"}, - }, - }, - "Body_login_for_access_token_token_post": { - "title": "Body_login_for_access_token_token_post", - "required": ["username", "password"], - "type": "object", - "properties": { - "grant_type": IsDict( - { - "title": "Grant Type", - "anyOf": [ - {"pattern": "password", "type": "string"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Grant Type", - "pattern": "password", - "type": "string", - } - ), - "username": {"title": "Username", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": IsDict( - { - "title": "Client Id", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Id", "type": "string"} - ), - "client_secret": IsDict( - { - "title": "Client Secret", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Secret", "type": "string"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - }, - "securitySchemes": { - "OAuth2PasswordBearer": { - "type": "oauth2", - "flows": { - "password": { - "scopes": { - "me": "Read information about the current user.", - "items": "Read items.", - }, - "tokenUrl": "token", - } - }, - } - }, - }, - } diff --git a/tests/test_tutorial/test_security/test_tutorial005_py310.py b/tests/test_tutorial/test_security/test_tutorial005_py310.py deleted file mode 100644 index 98c60c1c2..000000000 --- a/tests/test_tutorial/test_security/test_tutorial005_py310.py +++ /dev/null @@ -1,437 +0,0 @@ -import pytest -from dirty_equals import IsDict, IsOneOf -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.security.tutorial005_py310 import app - - client = TestClient(app) - return client - - -def get_access_token( - *, username="johndoe", password="secret", scope=None, client: TestClient -): - data = {"username": username, "password": password} - if scope: - data["scope"] = scope - response = client.post("/token", data=data) - content = response.json() - access_token = content.get("access_token") - return access_token - - -@needs_py310 -def test_login(client: TestClient): - response = client.post("/token", data={"username": "johndoe", "password": "secret"}) - assert response.status_code == 200, response.text - content = response.json() - assert "access_token" in content - assert content["token_type"] == "bearer" - - -@needs_py310 -def test_login_incorrect_password(client: TestClient): - response = client.post( - "/token", data={"username": "johndoe", "password": "incorrect"} - ) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Incorrect username or password"} - - -@needs_py310 -def test_login_incorrect_username(client: TestClient): - response = client.post("/token", data={"username": "foo", "password": "secret"}) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Incorrect username or password"} - - -@needs_py310 -def test_no_token(client: TestClient): - response = client.get("/users/me") - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py310 -def test_token(client: TestClient): - access_token = get_access_token(scope="me", client=client) - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 200, response.text - assert response.json() == { - "username": "johndoe", - "full_name": "John Doe", - "email": "johndoe@example.com", - "disabled": False, - } - - -@needs_py310 -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": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py310 -def test_incorrect_token_type(client: TestClient): - response = client.get( - "/users/me", headers={"Authorization": "Notexistent testtoken"} - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py310 -def test_verify_password(): - from docs_src.security.tutorial005_py310 import fake_users_db, verify_password - - assert verify_password("secret", fake_users_db["johndoe"]["hashed_password"]) - - -@needs_py310 -def test_get_password_hash(): - from docs_src.security.tutorial005_py310 import get_password_hash - - assert get_password_hash("secretalice") - - -@needs_py310 -def test_create_access_token(): - from docs_src.security.tutorial005_py310 import create_access_token - - access_token = create_access_token(data={"data": "foo"}) - assert access_token - - -@needs_py310 -def test_token_no_sub(client: TestClient): - response = client.get( - "/users/me", - headers={ - "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiZm9vIn0.9ynBhuYb4e6aW3oJr_K_TBgwcMTDpRToQIE25L57rOE" - }, - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py310 -def test_token_no_username(client: TestClient): - response = client.get( - "/users/me", - headers={ - "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmb28ifQ.NnExK_dlNAYyzACrXtXDrcWOgGY2JuPbI4eDaHdfK5Y" - }, - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py310 -def test_token_no_scope(client: TestClient): - access_token = get_access_token(client=client) - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not enough permissions"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py310 -def test_token_nonexistent_user(client: TestClient): - response = client.get( - "/users/me", - headers={ - "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VybmFtZTpib2IifQ.HcfCW67Uda-0gz54ZWTqmtgJnZeNem0Q757eTa9EZuw" - }, - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py310 -def test_token_inactive_user(client: TestClient): - access_token = get_access_token( - username="alice", password="secretalice", scope="me", client=client - ) - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Inactive user"} - - -@needs_py310 -def test_read_items(client: TestClient): - access_token = get_access_token(scope="me items", client=client) - response = client.get( - "/users/me/items/", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 200, response.text - assert response.json() == [{"item_id": "Foo", "owner": "johndoe"}] - - -@needs_py310 -def test_read_system_status(client: TestClient): - access_token = get_access_token(client=client) - response = client.get( - "/status/", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 200, response.text - assert response.json() == {"status": "ok"} - - -@needs_py310 -def test_read_system_status_no_token(client: TestClient): - response = client.get("/status/") - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/token": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Token"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login For Access Token", - "operationId": "login_for_access_token_token_post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_for_access_token_token_post" - } - } - }, - "required": True, - }, - } - }, - "/users/me/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - "summary": "Read Users Me", - "operationId": "read_users_me_users_me__get", - "security": [{"OAuth2PasswordBearer": ["me"]}], - } - }, - "/users/me/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Own Items", - "operationId": "read_own_items_users_me_items__get", - "security": [{"OAuth2PasswordBearer": ["items", "me"]}], - } - }, - "/status/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read System Status", - "operationId": "read_system_status_status__get", - "security": [{"OAuth2PasswordBearer": []}], - } - }, - }, - "components": { - "schemas": { - "User": { - "title": "User", - "required": IsOneOf( - ["username", "email", "full_name", "disabled"], - # TODO: remove when deprecating Pydantic v1 - ["username"], - ), - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "email": IsDict( - { - "title": "Email", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Email", "type": "string"} - ), - "full_name": IsDict( - { - "title": "Full Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Full Name", "type": "string"} - ), - "disabled": IsDict( - { - "title": "Disabled", - "anyOf": [{"type": "boolean"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Disabled", "type": "boolean"} - ), - }, - }, - "Token": { - "title": "Token", - "required": ["access_token", "token_type"], - "type": "object", - "properties": { - "access_token": {"title": "Access Token", "type": "string"}, - "token_type": {"title": "Token Type", "type": "string"}, - }, - }, - "Body_login_for_access_token_token_post": { - "title": "Body_login_for_access_token_token_post", - "required": ["username", "password"], - "type": "object", - "properties": { - "grant_type": IsDict( - { - "title": "Grant Type", - "anyOf": [ - {"pattern": "password", "type": "string"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Grant Type", - "pattern": "password", - "type": "string", - } - ), - "username": {"title": "Username", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": IsDict( - { - "title": "Client Id", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Id", "type": "string"} - ), - "client_secret": IsDict( - { - "title": "Client Secret", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Secret", "type": "string"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - }, - "securitySchemes": { - "OAuth2PasswordBearer": { - "type": "oauth2", - "flows": { - "password": { - "scopes": { - "me": "Read information about the current user.", - "items": "Read items.", - }, - "tokenUrl": "token", - } - }, - } - }, - }, - } diff --git a/tests/test_tutorial/test_security/test_tutorial005_py39.py b/tests/test_tutorial/test_security/test_tutorial005_py39.py deleted file mode 100644 index cd2157d54..000000000 --- a/tests/test_tutorial/test_security/test_tutorial005_py39.py +++ /dev/null @@ -1,437 +0,0 @@ -import pytest -from dirty_equals import IsDict, IsOneOf -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.security.tutorial005_py39 import app - - client = TestClient(app) - return client - - -def get_access_token( - *, username="johndoe", password="secret", scope=None, client: TestClient -): - data = {"username": username, "password": password} - if scope: - data["scope"] = scope - response = client.post("/token", data=data) - content = response.json() - access_token = content.get("access_token") - return access_token - - -@needs_py39 -def test_login(client: TestClient): - response = client.post("/token", data={"username": "johndoe", "password": "secret"}) - assert response.status_code == 200, response.text - content = response.json() - assert "access_token" in content - assert content["token_type"] == "bearer" - - -@needs_py39 -def test_login_incorrect_password(client: TestClient): - response = client.post( - "/token", data={"username": "johndoe", "password": "incorrect"} - ) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Incorrect username or password"} - - -@needs_py39 -def test_login_incorrect_username(client: TestClient): - response = client.post("/token", data={"username": "foo", "password": "secret"}) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Incorrect username or password"} - - -@needs_py39 -def test_no_token(client: TestClient): - response = client.get("/users/me") - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py39 -def test_token(client: TestClient): - access_token = get_access_token(scope="me", client=client) - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 200, response.text - assert response.json() == { - "username": "johndoe", - "full_name": "John Doe", - "email": "johndoe@example.com", - "disabled": False, - } - - -@needs_py39 -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": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py39 -def test_incorrect_token_type(client: TestClient): - response = client.get( - "/users/me", headers={"Authorization": "Notexistent testtoken"} - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py39 -def test_verify_password(): - from docs_src.security.tutorial005_py39 import fake_users_db, verify_password - - assert verify_password("secret", fake_users_db["johndoe"]["hashed_password"]) - - -@needs_py39 -def test_get_password_hash(): - from docs_src.security.tutorial005_py39 import get_password_hash - - assert get_password_hash("secretalice") - - -@needs_py39 -def test_create_access_token(): - from docs_src.security.tutorial005_py39 import create_access_token - - access_token = create_access_token(data={"data": "foo"}) - assert access_token - - -@needs_py39 -def test_token_no_sub(client: TestClient): - response = client.get( - "/users/me", - headers={ - "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiZm9vIn0.9ynBhuYb4e6aW3oJr_K_TBgwcMTDpRToQIE25L57rOE" - }, - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py39 -def test_token_no_username(client: TestClient): - response = client.get( - "/users/me", - headers={ - "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmb28ifQ.NnExK_dlNAYyzACrXtXDrcWOgGY2JuPbI4eDaHdfK5Y" - }, - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py39 -def test_token_no_scope(client: TestClient): - access_token = get_access_token(client=client) - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not enough permissions"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py39 -def test_token_nonexistent_user(client: TestClient): - response = client.get( - "/users/me", - headers={ - "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VybmFtZTpib2IifQ.HcfCW67Uda-0gz54ZWTqmtgJnZeNem0Q757eTa9EZuw" - }, - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py39 -def test_token_inactive_user(client: TestClient): - access_token = get_access_token( - username="alice", password="secretalice", scope="me", client=client - ) - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Inactive user"} - - -@needs_py39 -def test_read_items(client: TestClient): - access_token = get_access_token(scope="me items", client=client) - response = client.get( - "/users/me/items/", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 200, response.text - assert response.json() == [{"item_id": "Foo", "owner": "johndoe"}] - - -@needs_py39 -def test_read_system_status(client: TestClient): - access_token = get_access_token(client=client) - response = client.get( - "/status/", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 200, response.text - assert response.json() == {"status": "ok"} - - -@needs_py39 -def test_read_system_status_no_token(client: TestClient): - response = client.get("/status/") - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/token": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Token"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login For Access Token", - "operationId": "login_for_access_token_token_post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_for_access_token_token_post" - } - } - }, - "required": True, - }, - } - }, - "/users/me/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - "summary": "Read Users Me", - "operationId": "read_users_me_users_me__get", - "security": [{"OAuth2PasswordBearer": ["me"]}], - } - }, - "/users/me/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Own Items", - "operationId": "read_own_items_users_me_items__get", - "security": [{"OAuth2PasswordBearer": ["items", "me"]}], - } - }, - "/status/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read System Status", - "operationId": "read_system_status_status__get", - "security": [{"OAuth2PasswordBearer": []}], - } - }, - }, - "components": { - "schemas": { - "User": { - "title": "User", - "required": IsOneOf( - ["username", "email", "full_name", "disabled"], - # TODO: remove when deprecating Pydantic v1 - ["username"], - ), - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "email": IsDict( - { - "title": "Email", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Email", "type": "string"} - ), - "full_name": IsDict( - { - "title": "Full Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Full Name", "type": "string"} - ), - "disabled": IsDict( - { - "title": "Disabled", - "anyOf": [{"type": "boolean"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Disabled", "type": "boolean"} - ), - }, - }, - "Token": { - "title": "Token", - "required": ["access_token", "token_type"], - "type": "object", - "properties": { - "access_token": {"title": "Access Token", "type": "string"}, - "token_type": {"title": "Token Type", "type": "string"}, - }, - }, - "Body_login_for_access_token_token_post": { - "title": "Body_login_for_access_token_token_post", - "required": ["username", "password"], - "type": "object", - "properties": { - "grant_type": IsDict( - { - "title": "Grant Type", - "anyOf": [ - {"pattern": "password", "type": "string"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Grant Type", - "pattern": "password", - "type": "string", - } - ), - "username": {"title": "Username", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": IsDict( - { - "title": "Client Id", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Id", "type": "string"} - ), - "client_secret": IsDict( - { - "title": "Client Secret", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Secret", "type": "string"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - }, - "securitySchemes": { - "OAuth2PasswordBearer": { - "type": "oauth2", - "flows": { - "password": { - "scopes": { - "me": "Read information about the current user.", - "items": "Read items.", - }, - "tokenUrl": "token", - } - }, - } - }, - }, - } diff --git a/tests/test_tutorial/test_security/test_tutorial006.py b/tests/test_tutorial/test_security/test_tutorial006.py index dc459b6fd..40b413806 100644 --- a/tests/test_tutorial/test_security/test_tutorial006.py +++ b/tests/test_tutorial/test_security/test_tutorial006.py @@ -1,26 +1,41 @@ +import importlib from base64 import b64encode +import pytest from fastapi.testclient import TestClient -from docs_src.security.tutorial006 import app +from ...utils import needs_py39 -client = TestClient(app) +@pytest.fixture( + name="client", + params=[ + "tutorial006", + "tutorial006_an", + pytest.param("tutorial006_an_py39", marks=needs_py39), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.security.{request.param}") -def test_security_http_basic(): + client = TestClient(mod.app) + return client + + +def test_security_http_basic(client: TestClient): response = client.get("/users/me", auth=("john", "secret")) assert response.status_code == 200, response.text assert response.json() == {"username": "john", "password": "secret"} -def test_security_http_basic_no_credentials(): +def test_security_http_basic_no_credentials(client: TestClient): response = client.get("/users/me") assert response.json() == {"detail": "Not authenticated"} assert response.status_code == 401, response.text assert response.headers["WWW-Authenticate"] == "Basic" -def test_security_http_basic_invalid_credentials(): +def test_security_http_basic_invalid_credentials(client: TestClient): response = client.get( "/users/me", headers={"Authorization": "Basic notabase64token"} ) @@ -29,7 +44,7 @@ def test_security_http_basic_invalid_credentials(): assert response.json() == {"detail": "Invalid authentication credentials"} -def test_security_http_basic_non_basic_credentials(): +def test_security_http_basic_non_basic_credentials(client: TestClient): payload = b64encode(b"johnsecret").decode("ascii") auth_header = f"Basic {payload}" response = client.get("/users/me", headers={"Authorization": auth_header}) @@ -38,7 +53,7 @@ def test_security_http_basic_non_basic_credentials(): assert response.json() == {"detail": "Invalid authentication credentials"} -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_security/test_tutorial006_an.py b/tests/test_tutorial/test_security/test_tutorial006_an.py deleted file mode 100644 index 52ddd938f..000000000 --- a/tests/test_tutorial/test_security/test_tutorial006_an.py +++ /dev/null @@ -1,65 +0,0 @@ -from base64 import b64encode - -from fastapi.testclient import TestClient - -from docs_src.security.tutorial006_an import app - -client = TestClient(app) - - -def test_security_http_basic(): - response = client.get("/users/me", auth=("john", "secret")) - assert response.status_code == 200, response.text - assert response.json() == {"username": "john", "password": "secret"} - - -def test_security_http_basic_no_credentials(): - response = client.get("/users/me") - assert response.json() == {"detail": "Not authenticated"} - assert response.status_code == 401, response.text - assert response.headers["WWW-Authenticate"] == "Basic" - - -def test_security_http_basic_invalid_credentials(): - response = client.get( - "/users/me", headers={"Authorization": "Basic notabase64token"} - ) - assert response.status_code == 401, response.text - assert response.headers["WWW-Authenticate"] == "Basic" - assert response.json() == {"detail": "Invalid authentication credentials"} - - -def test_security_http_basic_non_basic_credentials(): - payload = b64encode(b"johnsecret").decode("ascii") - auth_header = f"Basic {payload}" - 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"} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPBasic": []}], - } - } - }, - "components": { - "securitySchemes": {"HTTPBasic": {"type": "http", "scheme": "basic"}} - }, - } diff --git a/tests/test_tutorial/test_security/test_tutorial006_an_py39.py b/tests/test_tutorial/test_security/test_tutorial006_an_py39.py deleted file mode 100644 index 52b22e573..000000000 --- a/tests/test_tutorial/test_security/test_tutorial006_an_py39.py +++ /dev/null @@ -1,77 +0,0 @@ -from base64 import b64encode - -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.security.tutorial006_an import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_security_http_basic(client: TestClient): - response = client.get("/users/me", auth=("john", "secret")) - assert response.status_code == 200, response.text - assert response.json() == {"username": "john", "password": "secret"} - - -@needs_py39 -def test_security_http_basic_no_credentials(client: TestClient): - response = client.get("/users/me") - assert response.json() == {"detail": "Not authenticated"} - assert response.status_code == 401, response.text - assert response.headers["WWW-Authenticate"] == "Basic" - - -@needs_py39 -def test_security_http_basic_invalid_credentials(client: TestClient): - response = client.get( - "/users/me", headers={"Authorization": "Basic notabase64token"} - ) - assert response.status_code == 401, response.text - assert response.headers["WWW-Authenticate"] == "Basic" - assert response.json() == {"detail": "Invalid authentication credentials"} - - -@needs_py39 -def test_security_http_basic_non_basic_credentials(client: TestClient): - payload = b64encode(b"johnsecret").decode("ascii") - auth_header = f"Basic {payload}" - 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"} - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPBasic": []}], - } - } - }, - "components": { - "securitySchemes": {"HTTPBasic": {"type": "http", "scheme": "basic"}} - }, - } diff --git a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001.py index cdfae9f8c..059fb889b 100644 --- a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001.py +++ b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001.py @@ -1,14 +1,23 @@ +import importlib + import pytest from fastapi.testclient import TestClient -from ...utils import needs_pydanticv2 +from ...utils import needs_py39, needs_py310, needs_pydanticv2 -@pytest.fixture(name="client") -def get_client() -> TestClient: - from docs_src.separate_openapi_schemas.tutorial001 import app +@pytest.fixture( + name="client", + params=[ + "tutorial001", + pytest.param("tutorial001_py310", marks=needs_py310), + pytest.param("tutorial001_py39", marks=needs_py39), + ], +) +def get_client(request: pytest.FixtureRequest) -> TestClient: + mod = importlib.import_module(f"docs_src.separate_openapi_schemas.{request.param}") - client = TestClient(app) + client = TestClient(mod.app) return client diff --git a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py310.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py310.py deleted file mode 100644 index 3b22146f6..000000000 --- a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py310.py +++ /dev/null @@ -1,136 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_py310, needs_pydanticv2 - - -@pytest.fixture(name="client") -def get_client() -> TestClient: - from docs_src.separate_openapi_schemas.tutorial001_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_create_item(client: TestClient) -> None: - response = client.post("/items/", json={"name": "Foo"}) - assert response.status_code == 200, response.text - assert response.json() == {"name": "Foo", "description": None} - - -@needs_py310 -def test_read_items(client: TestClient) -> None: - response = client.get("/items/") - assert response.status_code == 200, response.text - assert response.json() == [ - { - "name": "Portal Gun", - "description": "Device to travel through the multi-rick-verse", - }, - {"name": "Plumbus", "description": None}, - ] - - -@needs_py310 -@needs_pydanticv2 -def test_openapi_schema(client: TestClient) -> None: - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": {"$ref": "#/components/schemas/Item"}, - "type": "array", - "title": "Response Read Items Items Get", - } - } - }, - } - }, - }, - "post": { - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": {"$ref": "#/components/schemas/ValidationError"}, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Description", - }, - }, - "type": "object", - "required": ["name"], - "title": "Item", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } diff --git a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py39.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py39.py deleted file mode 100644 index 991abe811..000000000 --- a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py39.py +++ /dev/null @@ -1,136 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_py39, needs_pydanticv2 - - -@pytest.fixture(name="client") -def get_client() -> TestClient: - from docs_src.separate_openapi_schemas.tutorial001_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_create_item(client: TestClient) -> None: - response = client.post("/items/", json={"name": "Foo"}) - assert response.status_code == 200, response.text - assert response.json() == {"name": "Foo", "description": None} - - -@needs_py39 -def test_read_items(client: TestClient) -> None: - response = client.get("/items/") - assert response.status_code == 200, response.text - assert response.json() == [ - { - "name": "Portal Gun", - "description": "Device to travel through the multi-rick-verse", - }, - {"name": "Plumbus", "description": None}, - ] - - -@needs_py39 -@needs_pydanticv2 -def test_openapi_schema(client: TestClient) -> None: - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": {"$ref": "#/components/schemas/Item"}, - "type": "array", - "title": "Response Read Items Items Get", - } - } - }, - } - }, - }, - "post": { - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": {"$ref": "#/components/schemas/ValidationError"}, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Description", - }, - }, - "type": "object", - "required": ["name"], - "title": "Item", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } diff --git a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002.py index d2cf7945b..cc9afeab7 100644 --- a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002.py +++ b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002.py @@ -1,14 +1,23 @@ +import importlib + import pytest from fastapi.testclient import TestClient -from ...utils import needs_pydanticv2 +from ...utils import needs_py39, needs_py310, needs_pydanticv2 -@pytest.fixture(name="client") -def get_client() -> TestClient: - from docs_src.separate_openapi_schemas.tutorial002 import app +@pytest.fixture( + name="client", + params=[ + "tutorial002", + pytest.param("tutorial002_py310", marks=needs_py310), + pytest.param("tutorial002_py39", marks=needs_py39), + ], +) +def get_client(request: pytest.FixtureRequest) -> TestClient: + mod = importlib.import_module(f"docs_src.separate_openapi_schemas.{request.param}") - client = TestClient(app) + client = TestClient(mod.app) return client diff --git a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002_py310.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002_py310.py deleted file mode 100644 index 89c9ce977..000000000 --- a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002_py310.py +++ /dev/null @@ -1,136 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_py310, needs_pydanticv2 - - -@pytest.fixture(name="client") -def get_client() -> TestClient: - from docs_src.separate_openapi_schemas.tutorial002_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_create_item(client: TestClient) -> None: - response = client.post("/items/", json={"name": "Foo"}) - assert response.status_code == 200, response.text - assert response.json() == {"name": "Foo", "description": None} - - -@needs_py310 -def test_read_items(client: TestClient) -> None: - response = client.get("/items/") - assert response.status_code == 200, response.text - assert response.json() == [ - { - "name": "Portal Gun", - "description": "Device to travel through the multi-rick-verse", - }, - {"name": "Plumbus", "description": None}, - ] - - -@needs_py310 -@needs_pydanticv2 -def test_openapi_schema(client: TestClient) -> None: - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": {"$ref": "#/components/schemas/Item"}, - "type": "array", - "title": "Response Read Items Items Get", - } - } - }, - } - }, - }, - "post": { - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": {"$ref": "#/components/schemas/ValidationError"}, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Description", - }, - }, - "type": "object", - "required": ["name"], - "title": "Item", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } diff --git a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002_py39.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002_py39.py deleted file mode 100644 index 6ac3d8f79..000000000 --- a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002_py39.py +++ /dev/null @@ -1,136 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_py39, needs_pydanticv2 - - -@pytest.fixture(name="client") -def get_client() -> TestClient: - from docs_src.separate_openapi_schemas.tutorial002_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_create_item(client: TestClient) -> None: - response = client.post("/items/", json={"name": "Foo"}) - assert response.status_code == 200, response.text - assert response.json() == {"name": "Foo", "description": None} - - -@needs_py39 -def test_read_items(client: TestClient) -> None: - response = client.get("/items/") - assert response.status_code == 200, response.text - assert response.json() == [ - { - "name": "Portal Gun", - "description": "Device to travel through the multi-rick-verse", - }, - {"name": "Plumbus", "description": None}, - ] - - -@needs_py39 -@needs_pydanticv2 -def test_openapi_schema(client: TestClient) -> None: - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": {"$ref": "#/components/schemas/Item"}, - "type": "array", - "title": "Response Read Items Items Get", - } - } - }, - } - }, - }, - "post": { - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": {"$ref": "#/components/schemas/ValidationError"}, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Description", - }, - }, - "type": "object", - "required": ["name"], - "title": "Item", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } diff --git a/tests/test_tutorial/test_websockets/test_tutorial002.py b/tests/test_tutorial/test_websockets/test_tutorial002.py index bb5ccbf8e..51aa5752a 100644 --- a/tests/test_tutorial/test_websockets/test_tutorial002.py +++ b/tests/test_tutorial/test_websockets/test_tutorial002.py @@ -1,18 +1,37 @@ +import importlib + import pytest +from fastapi import FastAPI from fastapi.testclient import TestClient from fastapi.websockets import WebSocketDisconnect -from docs_src.websockets.tutorial002 import app +from ...utils import needs_py39, needs_py310 + + +@pytest.fixture( + name="app", + params=[ + "tutorial002", + pytest.param("tutorial002_py310", marks=needs_py310), + "tutorial002_an", + pytest.param("tutorial002_an_py39", marks=needs_py39), + pytest.param("tutorial002_an_py310", marks=needs_py310), + ], +) +def get_app(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.websockets.{request.param}") + + return mod.app -def test_main(): +def test_main(app: FastAPI): client = TestClient(app) response = client.get("/") assert response.status_code == 200, response.text assert b"" in response.content -def test_websocket_with_cookie(): +def test_websocket_with_cookie(app: FastAPI): client = TestClient(app, cookies={"session": "fakesession"}) with pytest.raises(WebSocketDisconnect): with client.websocket_connect("/items/foo/ws") as websocket: @@ -30,7 +49,7 @@ def test_websocket_with_cookie(): assert data == f"Message text was: {message}, for item ID: foo" -def test_websocket_with_header(): +def test_websocket_with_header(app: FastAPI): client = TestClient(app) with pytest.raises(WebSocketDisconnect): with client.websocket_connect("/items/bar/ws?token=some-token") as websocket: @@ -48,7 +67,7 @@ def test_websocket_with_header(): assert data == f"Message text was: {message}, for item ID: bar" -def test_websocket_with_header_and_query(): +def test_websocket_with_header_and_query(app: FastAPI): client = TestClient(app) with pytest.raises(WebSocketDisconnect): with client.websocket_connect("/items/2/ws?q=3&token=some-token") as websocket: @@ -70,7 +89,7 @@ def test_websocket_with_header_and_query(): assert data == f"Message text was: {message}, for item ID: 2" -def test_websocket_no_credentials(): +def test_websocket_no_credentials(app: FastAPI): client = TestClient(app) with pytest.raises(WebSocketDisconnect): with client.websocket_connect("/items/foo/ws"): @@ -79,7 +98,7 @@ def test_websocket_no_credentials(): ) # pragma: no cover -def test_websocket_invalid_data(): +def test_websocket_invalid_data(app: FastAPI): client = TestClient(app) with pytest.raises(WebSocketDisconnect): with client.websocket_connect("/items/foo/ws?q=bar&token=some-token"): diff --git a/tests/test_tutorial/test_websockets/test_tutorial002_an.py b/tests/test_tutorial/test_websockets/test_tutorial002_an.py deleted file mode 100644 index ec78d70d3..000000000 --- a/tests/test_tutorial/test_websockets/test_tutorial002_an.py +++ /dev/null @@ -1,88 +0,0 @@ -import pytest -from fastapi.testclient import TestClient -from fastapi.websockets import WebSocketDisconnect - -from docs_src.websockets.tutorial002_an import app - - -def test_main(): - client = TestClient(app) - response = client.get("/") - assert response.status_code == 200, response.text - assert b"" in response.content - - -def test_websocket_with_cookie(): - client = TestClient(app, cookies={"session": "fakesession"}) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/foo/ws") as websocket: - message = "Message one" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: fakesession" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: foo" - message = "Message two" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: fakesession" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: foo" - - -def test_websocket_with_header(): - client = TestClient(app) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/bar/ws?token=some-token") as websocket: - message = "Message one" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: some-token" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: bar" - message = "Message two" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: some-token" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: bar" - - -def test_websocket_with_header_and_query(): - client = TestClient(app) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/2/ws?q=3&token=some-token") as websocket: - message = "Message one" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: some-token" - data = websocket.receive_text() - assert data == "Query parameter q is: 3" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: 2" - message = "Message two" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: some-token" - data = websocket.receive_text() - assert data == "Query parameter q is: 3" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: 2" - - -def test_websocket_no_credentials(): - client = TestClient(app) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/foo/ws"): - pytest.fail( - "did not raise WebSocketDisconnect on __enter__" - ) # pragma: no cover - - -def test_websocket_invalid_data(): - client = TestClient(app) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/foo/ws?q=bar&token=some-token"): - pytest.fail( - "did not raise WebSocketDisconnect on __enter__" - ) # pragma: no cover diff --git a/tests/test_tutorial/test_websockets/test_tutorial002_an_py310.py b/tests/test_tutorial/test_websockets/test_tutorial002_an_py310.py deleted file mode 100644 index 23b4bcb78..000000000 --- a/tests/test_tutorial/test_websockets/test_tutorial002_an_py310.py +++ /dev/null @@ -1,102 +0,0 @@ -import pytest -from fastapi import FastAPI -from fastapi.testclient import TestClient -from fastapi.websockets import WebSocketDisconnect - -from ...utils import needs_py310 - - -@pytest.fixture(name="app") -def get_app(): - from docs_src.websockets.tutorial002_an_py310 import app - - return app - - -@needs_py310 -def test_main(app: FastAPI): - client = TestClient(app) - response = client.get("/") - assert response.status_code == 200, response.text - assert b"" in response.content - - -@needs_py310 -def test_websocket_with_cookie(app: FastAPI): - client = TestClient(app, cookies={"session": "fakesession"}) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/foo/ws") as websocket: - message = "Message one" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: fakesession" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: foo" - message = "Message two" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: fakesession" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: foo" - - -@needs_py310 -def test_websocket_with_header(app: FastAPI): - client = TestClient(app) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/bar/ws?token=some-token") as websocket: - message = "Message one" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: some-token" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: bar" - message = "Message two" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: some-token" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: bar" - - -@needs_py310 -def test_websocket_with_header_and_query(app: FastAPI): - client = TestClient(app) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/2/ws?q=3&token=some-token") as websocket: - message = "Message one" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: some-token" - data = websocket.receive_text() - assert data == "Query parameter q is: 3" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: 2" - message = "Message two" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: some-token" - data = websocket.receive_text() - assert data == "Query parameter q is: 3" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: 2" - - -@needs_py310 -def test_websocket_no_credentials(app: FastAPI): - client = TestClient(app) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/foo/ws"): - pytest.fail( - "did not raise WebSocketDisconnect on __enter__" - ) # pragma: no cover - - -@needs_py310 -def test_websocket_invalid_data(app: FastAPI): - client = TestClient(app) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/foo/ws?q=bar&token=some-token"): - pytest.fail( - "did not raise WebSocketDisconnect on __enter__" - ) # pragma: no cover diff --git a/tests/test_tutorial/test_websockets/test_tutorial002_an_py39.py b/tests/test_tutorial/test_websockets/test_tutorial002_an_py39.py deleted file mode 100644 index 2d77f05b3..000000000 --- a/tests/test_tutorial/test_websockets/test_tutorial002_an_py39.py +++ /dev/null @@ -1,102 +0,0 @@ -import pytest -from fastapi import FastAPI -from fastapi.testclient import TestClient -from fastapi.websockets import WebSocketDisconnect - -from ...utils import needs_py39 - - -@pytest.fixture(name="app") -def get_app(): - from docs_src.websockets.tutorial002_an_py39 import app - - return app - - -@needs_py39 -def test_main(app: FastAPI): - client = TestClient(app) - response = client.get("/") - assert response.status_code == 200, response.text - assert b"" in response.content - - -@needs_py39 -def test_websocket_with_cookie(app: FastAPI): - client = TestClient(app, cookies={"session": "fakesession"}) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/foo/ws") as websocket: - message = "Message one" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: fakesession" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: foo" - message = "Message two" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: fakesession" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: foo" - - -@needs_py39 -def test_websocket_with_header(app: FastAPI): - client = TestClient(app) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/bar/ws?token=some-token") as websocket: - message = "Message one" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: some-token" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: bar" - message = "Message two" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: some-token" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: bar" - - -@needs_py39 -def test_websocket_with_header_and_query(app: FastAPI): - client = TestClient(app) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/2/ws?q=3&token=some-token") as websocket: - message = "Message one" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: some-token" - data = websocket.receive_text() - assert data == "Query parameter q is: 3" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: 2" - message = "Message two" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: some-token" - data = websocket.receive_text() - assert data == "Query parameter q is: 3" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: 2" - - -@needs_py39 -def test_websocket_no_credentials(app: FastAPI): - client = TestClient(app) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/foo/ws"): - pytest.fail( - "did not raise WebSocketDisconnect on __enter__" - ) # pragma: no cover - - -@needs_py39 -def test_websocket_invalid_data(app: FastAPI): - client = TestClient(app) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/foo/ws?q=bar&token=some-token"): - pytest.fail( - "did not raise WebSocketDisconnect on __enter__" - ) # pragma: no cover diff --git a/tests/test_tutorial/test_websockets/test_tutorial002_py310.py b/tests/test_tutorial/test_websockets/test_tutorial002_py310.py deleted file mode 100644 index 03bc27bdf..000000000 --- a/tests/test_tutorial/test_websockets/test_tutorial002_py310.py +++ /dev/null @@ -1,102 +0,0 @@ -import pytest -from fastapi import FastAPI -from fastapi.testclient import TestClient -from fastapi.websockets import WebSocketDisconnect - -from ...utils import needs_py310 - - -@pytest.fixture(name="app") -def get_app(): - from docs_src.websockets.tutorial002_py310 import app - - return app - - -@needs_py310 -def test_main(app: FastAPI): - client = TestClient(app) - response = client.get("/") - assert response.status_code == 200, response.text - assert b"" in response.content - - -@needs_py310 -def test_websocket_with_cookie(app: FastAPI): - client = TestClient(app, cookies={"session": "fakesession"}) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/foo/ws") as websocket: - message = "Message one" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: fakesession" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: foo" - message = "Message two" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: fakesession" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: foo" - - -@needs_py310 -def test_websocket_with_header(app: FastAPI): - client = TestClient(app) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/bar/ws?token=some-token") as websocket: - message = "Message one" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: some-token" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: bar" - message = "Message two" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: some-token" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: bar" - - -@needs_py310 -def test_websocket_with_header_and_query(app: FastAPI): - client = TestClient(app) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/2/ws?q=3&token=some-token") as websocket: - message = "Message one" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: some-token" - data = websocket.receive_text() - assert data == "Query parameter q is: 3" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: 2" - message = "Message two" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: some-token" - data = websocket.receive_text() - assert data == "Query parameter q is: 3" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: 2" - - -@needs_py310 -def test_websocket_no_credentials(app: FastAPI): - client = TestClient(app) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/foo/ws"): - pytest.fail( - "did not raise WebSocketDisconnect on __enter__" - ) # pragma: no cover - - -@needs_py310 -def test_websocket_invalid_data(app: FastAPI): - client = TestClient(app) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/foo/ws?q=bar&token=some-token"): - pytest.fail( - "did not raise WebSocketDisconnect on __enter__" - ) # pragma: no cover