Browse Source

Merge branch 'fastapi:master' into master

pull/12201/head
Guillaume Fassot 9 months ago
committed by GitHub
parent
commit
dd8feab037
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 9
      .github/workflows/issue-manager.yml
  2. 4
      .github/workflows/test.yml
  3. 2
      .pre-commit-config.yaml
  4. 2
      README.md
  5. 2
      docs/en/data/sponsors.yml
  6. 2
      docs/en/docs/deployment/cloud.md
  7. BIN
      docs/en/docs/img/tutorial/request-form-models/image01.png
  8. 133
      docs/en/docs/release-notes.md
  9. 134
      docs/en/docs/tutorial/request-form-models.md
  10. 1
      docs/en/mkdocs.yml
  11. 2
      docs/en/overrides/main.html
  12. 28
      docs/ko/docs/project-generation.md
  13. 597
      docs/nl/docs/python-types.md
  14. 298
      docs/pt/docs/environment-variables.md
  15. 115
      docs/pt/docs/tutorial/debugging.md
  16. 134
      docs/pt/docs/tutorial/request-form-models.md
  17. 249
      docs/pt/docs/tutorial/testing.md
  18. 844
      docs/pt/docs/virtual-environments.md
  19. 112
      docs/zh/docs/project-generation.md
  20. 14
      docs_src/request_form_models/tutorial001.py
  21. 15
      docs_src/request_form_models/tutorial001_an.py
  22. 16
      docs_src/request_form_models/tutorial001_an_py39.py
  23. 15
      docs_src/request_form_models/tutorial002.py
  24. 16
      docs_src/request_form_models/tutorial002_an.py
  25. 17
      docs_src/request_form_models/tutorial002_an_py39.py
  26. 17
      docs_src/request_form_models/tutorial002_pv1.py
  27. 18
      docs_src/request_form_models/tutorial002_pv1_an.py
  28. 19
      docs_src/request_form_models/tutorial002_pv1_an_py39.py
  29. 2
      fastapi/__init__.py
  30. 21
      fastapi/_compat.py
  31. 314
      fastapi/dependencies/utils.py
  32. 4
      fastapi/param_functions.py
  33. 5
      fastapi/params.py
  34. 26
      fastapi/routing.py
  35. 4
      pyproject.toml
  36. 4
      requirements-tests.txt
  37. 38
      scripts/playwright/request_form_models/image01.py
  38. 3
      scripts/playwright/separate_openapi_schemas/image01.py
  39. 3
      scripts/playwright/separate_openapi_schemas/image02.py
  40. 3
      scripts/playwright/separate_openapi_schemas/image03.py
  41. 3
      scripts/playwright/separate_openapi_schemas/image04.py
  42. 3
      scripts/playwright/separate_openapi_schemas/image05.py
  43. 29
      tests/test_compat.py
  44. 133
      tests/test_forms_single_model.py
  45. 99
      tests/test_forms_single_param.py
  46. 27
      tests/test_openapi_examples.py
  47. 0
      tests/test_tutorial/test_request_form_models/__init__.py
  48. 232
      tests/test_tutorial/test_request_form_models/test_tutorial001.py
  49. 232
      tests/test_tutorial/test_request_form_models/test_tutorial001_an.py
  50. 240
      tests/test_tutorial/test_request_form_models/test_tutorial001_an_py39.py
  51. 196
      tests/test_tutorial/test_request_form_models/test_tutorial002.py
  52. 196
      tests/test_tutorial/test_request_form_models/test_tutorial002_an.py
  53. 203
      tests/test_tutorial/test_request_form_models/test_tutorial002_an_py39.py
  54. 189
      tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py
  55. 196
      tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an.py
  56. 203
      tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an_p39.py

9
.github/workflows/issue-manager.yml

@ -2,7 +2,7 @@ name: Issue Manager
on: on:
schedule: schedule:
- cron: "10 3 * * *" - cron: "13 22 * * *"
issue_comment: issue_comment:
types: types:
- created - created
@ -16,6 +16,7 @@ on:
permissions: permissions:
issues: write issues: write
pull-requests: write
jobs: jobs:
issue-manager: issue-manager:
@ -26,7 +27,7 @@ jobs:
env: env:
GITHUB_CONTEXT: ${{ toJson(github) }} GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT" run: echo "$GITHUB_CONTEXT"
- uses: tiangolo/[email protected].0 - uses: tiangolo/[email protected].1
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
config: > config: >
@ -35,8 +36,8 @@ jobs:
"delay": 864000, "delay": 864000,
"message": "Assuming the original need was handled, this will be automatically closed now. But feel free to add more comments or create new issues or PRs." "message": "Assuming the original need was handled, this will be automatically closed now. But feel free to add more comments or create new issues or PRs."
}, },
"changes-requested": { "waiting": {
"delay": 2628000, "delay": 2628000,
"message": "As this PR had requested changes to be applied but has been inactive for a while, it's now going to be closed. But if there's anyone interested, feel free to create a new PR." "message": "As this PR has been waiting for the original user for a while but seems to be inactive, it's now going to be closed. But if there's anyone interested, feel free to create a new PR."
} }
} }

4
.github/workflows/test.yml

@ -37,7 +37,7 @@ jobs:
if: steps.cache.outputs.cache-hit != 'true' if: steps.cache.outputs.cache-hit != 'true'
run: pip install -r requirements-tests.txt run: pip install -r requirements-tests.txt
- name: Install Pydantic v2 - name: Install Pydantic v2
run: pip install "pydantic>=2.0.2,<3.0.0" run: pip install --upgrade "pydantic>=2.0.2,<3.0.0"
- name: Lint - name: Lint
run: bash scripts/lint.sh run: bash scripts/lint.sh
@ -79,7 +79,7 @@ jobs:
run: pip install "pydantic>=1.10.0,<2.0.0" run: pip install "pydantic>=1.10.0,<2.0.0"
- name: Install Pydantic v2 - name: Install Pydantic v2
if: matrix.pydantic-version == 'pydantic-v2' if: matrix.pydantic-version == 'pydantic-v2'
run: pip install "pydantic>=2.0.2,<3.0.0" run: pip install --upgrade "pydantic>=2.0.2,<3.0.0"
- run: mkdir coverage - run: mkdir coverage
- name: Test - name: Test
run: bash scripts/test.sh run: bash scripts/test.sh

2
.pre-commit-config.yaml

@ -14,7 +14,7 @@ repos:
- id: end-of-file-fixer - id: end-of-file-fixer
- id: trailing-whitespace - id: trailing-whitespace
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.2 rev: v0.6.4
hooks: hooks:
- id: ruff - id: ruff
args: args:

2
README.md

@ -52,7 +52,7 @@ The key features are:
<a href="https://bump.sh/fastapi?utm_source=fastapi&utm_medium=referral&utm_campaign=sponsor" target="_blank" title="Automate FastAPI documentation generation with Bump.sh"><img src="https://fastapi.tiangolo.com/img/sponsors/bump-sh.svg"></a> <a href="https://bump.sh/fastapi?utm_source=fastapi&utm_medium=referral&utm_campaign=sponsor" target="_blank" title="Automate FastAPI documentation generation with Bump.sh"><img src="https://fastapi.tiangolo.com/img/sponsors/bump-sh.svg"></a>
<a href="https://github.com/scalar/scalar/?utm_source=fastapi&utm_medium=website&utm_campaign=main-badge" target="_blank" title="Scalar: Beautiful Open-Source API References from Swagger/OpenAPI files"><img src="https://fastapi.tiangolo.com/img/sponsors/scalar.svg"></a> <a href="https://github.com/scalar/scalar/?utm_source=fastapi&utm_medium=website&utm_campaign=main-badge" target="_blank" title="Scalar: Beautiful Open-Source API References from Swagger/OpenAPI files"><img src="https://fastapi.tiangolo.com/img/sponsors/scalar.svg"></a>
<a href="https://www.propelauth.com/?utm_source=fastapi&utm_campaign=1223&utm_medium=mainbadge" target="_blank" title="Auth, user management and more for your B2B product"><img src="https://fastapi.tiangolo.com/img/sponsors/propelauth.png"></a> <a href="https://www.propelauth.com/?utm_source=fastapi&utm_campaign=1223&utm_medium=mainbadge" target="_blank" title="Auth, user management and more for your B2B product"><img src="https://fastapi.tiangolo.com/img/sponsors/propelauth.png"></a>
<a href="https://docs.withcoherence.com/coherence-templates/full-stack-template/#fastapi?utm_medium=advertising&utm_source=fastapi&utm_campaign=docs" target="_blank" title="Coherence"><img src="https://fastapi.tiangolo.com/img/sponsors/coherence.png"></a> <a href="https://www.withcoherence.com/?utm_medium=advertising&utm_source=fastapi&utm_campaign=website" target="_blank" title="Coherence"><img src="https://fastapi.tiangolo.com/img/sponsors/coherence.png"></a>
<a href="https://www.mongodb.com/developer/languages/python/python-quickstart-fastapi/?utm_campaign=fastapi_framework&utm_source=fastapi_sponsorship&utm_medium=web_referral" target="_blank" title="Simplify Full Stack Development with FastAPI & MongoDB"><img src="https://fastapi.tiangolo.com/img/sponsors/mongodb.png"></a> <a href="https://www.mongodb.com/developer/languages/python/python-quickstart-fastapi/?utm_campaign=fastapi_framework&utm_source=fastapi_sponsorship&utm_medium=web_referral" target="_blank" title="Simplify Full Stack Development with FastAPI & MongoDB"><img src="https://fastapi.tiangolo.com/img/sponsors/mongodb.png"></a>
<a href="https://zuplo.link/fastapi-gh" target="_blank" title="Zuplo: Scale, Protect, Document, and Monetize your FastAPI"><img src="https://fastapi.tiangolo.com/img/sponsors/zuplo.png"></a> <a href="https://zuplo.link/fastapi-gh" target="_blank" title="Zuplo: Scale, Protect, Document, and Monetize your FastAPI"><img src="https://fastapi.tiangolo.com/img/sponsors/zuplo.png"></a>
<a href="https://fine.dev?ref=fastapibadge" target="_blank" title="Fine's AI FastAPI Workflow: Effortlessly Deploy and Integrate FastAPI into Your Project"><img src="https://fastapi.tiangolo.com/img/sponsors/fine.png"></a> <a href="https://fine.dev?ref=fastapibadge" target="_blank" title="Fine's AI FastAPI Workflow: Effortlessly Deploy and Integrate FastAPI into Your Project"><img src="https://fastapi.tiangolo.com/img/sponsors/fine.png"></a>

2
docs/en/data/sponsors.yml

@ -17,7 +17,7 @@ gold:
- url: https://www.propelauth.com/?utm_source=fastapi&utm_campaign=1223&utm_medium=mainbadge - url: https://www.propelauth.com/?utm_source=fastapi&utm_campaign=1223&utm_medium=mainbadge
title: Auth, user management and more for your B2B product title: Auth, user management and more for your B2B product
img: https://fastapi.tiangolo.com/img/sponsors/propelauth.png img: https://fastapi.tiangolo.com/img/sponsors/propelauth.png
- url: https://docs.withcoherence.com/coherence-templates/full-stack-template/#fastapi?utm_medium=advertising&utm_source=fastapi&utm_campaign=docs - url: https://www.withcoherence.com/?utm_medium=advertising&utm_source=fastapi&utm_campaign=website
title: Coherence title: Coherence
img: https://fastapi.tiangolo.com/img/sponsors/coherence.png img: https://fastapi.tiangolo.com/img/sponsors/coherence.png
- url: https://www.mongodb.com/developer/languages/python/python-quickstart-fastapi/?utm_campaign=fastapi_framework&utm_source=fastapi_sponsorship&utm_medium=web_referral - url: https://www.mongodb.com/developer/languages/python/python-quickstart-fastapi/?utm_campaign=fastapi_framework&utm_source=fastapi_sponsorship&utm_medium=web_referral

2
docs/en/docs/deployment/cloud.md

@ -14,4 +14,4 @@ You might want to try their services and follow their guides:
* <a href="https://docs.platform.sh/languages/python.html?utm_source=fastapi-signup&utm_medium=banner&utm_campaign=FastAPI-signup-June-2023" class="external-link" target="_blank">Platform.sh</a> * <a href="https://docs.platform.sh/languages/python.html?utm_source=fastapi-signup&utm_medium=banner&utm_campaign=FastAPI-signup-June-2023" class="external-link" target="_blank">Platform.sh</a>
* <a href="https://docs.porter.run/language-specific-guides/fastapi" class="external-link" target="_blank">Porter</a> * <a href="https://docs.porter.run/language-specific-guides/fastapi" class="external-link" target="_blank">Porter</a>
* <a href="https://docs.withcoherence.com/coherence-templates/full-stack-template/#fastapi?utm_medium=advertising&utm_source=fastapi&utm_campaign=docs" class="external-link" target="_blank">Coherence</a> * <a href="https://www.withcoherence.com/?utm_medium=advertising&utm_source=fastapi&utm_campaign=website" class="external-link" target="_blank">Coherence</a>

BIN
docs/en/docs/img/tutorial/request-form-models/image01.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

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

@ -7,6 +7,138 @@ hide:
## Latest Changes ## Latest Changes
## 0.114.2
### Fixes
* 🐛 Fix form field regression with `alias`. PR [#12194](https://github.com/fastapi/fastapi/pull/12194) by [@Wurstnase](https://github.com/Wurstnase).
### Translations
* 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/request-form-models.md`. PR [#12175](https://github.com/fastapi/fastapi/pull/12175) by [@ceb10n](https://github.com/ceb10n).
* 🌐 Add Chinese translation for `docs/zh/docs/project-generation.md`. PR [#12170](https://github.com/fastapi/fastapi/pull/12170) by [@waketzheng](https://github.com/waketzheng).
* 🌐 Add Dutch translation for `docs/nl/docs/python-types.md`. PR [#12158](https://github.com/fastapi/fastapi/pull/12158) by [@maxscheijen](https://github.com/maxscheijen).
### Internal
* 💡 Add comments with instructions for Playwright screenshot scripts. PR [#12193](https://github.com/fastapi/fastapi/pull/12193) by [@tiangolo](https://github.com/tiangolo).
* ➕ Add inline-snapshot for tests. PR [#12189](https://github.com/fastapi/fastapi/pull/12189) by [@tiangolo](https://github.com/tiangolo).
## 0.114.1
### Refactors
* ⚡️ Improve performance in request body parsing with a cache for internal model fields. PR [#12184](https://github.com/fastapi/fastapi/pull/12184) by [@tiangolo](https://github.com/tiangolo).
### Docs
* 📝 Remove duplicate line in docs for `docs/en/docs/environment-variables.md`. PR [#12169](https://github.com/fastapi/fastapi/pull/12169) by [@prometek](https://github.com/prometek).
### Translations
* 🌐 Add Portuguese translation for `docs/pt/docs/virtual-environments.md`. PR [#12163](https://github.com/fastapi/fastapi/pull/12163) by [@marcelomarkus](https://github.com/marcelomarkus).
* 🌐 Add Portuguese translation for `docs/pt/docs/environment-variables.md`. PR [#12162](https://github.com/fastapi/fastapi/pull/12162) by [@marcelomarkus](https://github.com/marcelomarkus).
* 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/testing.md`. PR [#12164](https://github.com/fastapi/fastapi/pull/12164) by [@marcelomarkus](https://github.com/marcelomarkus).
* 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/debugging.md`. PR [#12165](https://github.com/fastapi/fastapi/pull/12165) by [@marcelomarkus](https://github.com/marcelomarkus).
* 🌐 Add Korean translation for `docs/ko/docs/project-generation.md`. PR [#12157](https://github.com/fastapi/fastapi/pull/12157) by [@BORA040126](https://github.com/BORA040126).
### Internal
* ⬆ Bump tiangolo/issue-manager from 0.5.0 to 0.5.1. PR [#12173](https://github.com/fastapi/fastapi/pull/12173) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#12176](https://github.com/fastapi/fastapi/pull/12176) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* 👷 Update `issue-manager.yml`. PR [#12159](https://github.com/fastapi/fastapi/pull/12159) by [@tiangolo](https://github.com/tiangolo).
* ✏️ Fix typo in `fastapi/params.py`. PR [#12143](https://github.com/fastapi/fastapi/pull/12143) by [@surreal30](https://github.com/surreal30).
## 0.114.0
You can restrict form fields to only include those declared in a Pydantic model and forbid any extra field sent in the request using Pydantic's `model_config = {"extra": "forbid"}`:
```python
from typing import Annotated
from fastapi import FastAPI, Form
from pydantic import BaseModel
app = FastAPI()
class FormData(BaseModel):
username: str
password: str
model_config = {"extra": "forbid"}
@app.post("/login/")
async def login(data: Annotated[FormData, Form()]):
return data
```
Read the new docs: [Form Models - Forbid Extra Form Fields](https://fastapi.tiangolo.com/tutorial/request-form-models/#forbid-extra-form-fields).
### Features
* ✨ Add support for forbidding extra form fields with Pydantic models. PR [#12134](https://github.com/fastapi/fastapi/pull/12134) by [@tiangolo](https://github.com/tiangolo).
### Docs
* 📝 Update docs, Form Models section title, to match config name. PR [#12152](https://github.com/fastapi/fastapi/pull/12152) by [@tiangolo](https://github.com/tiangolo).
### Internal
* ✅ Update internal tests for latest Pydantic, including CI tweaks to install the latest Pydantic. PR [#12147](https://github.com/fastapi/fastapi/pull/12147) by [@tiangolo](https://github.com/tiangolo).
## 0.113.0
Now you can declare form fields with Pydantic models:
```python
from typing import Annotated
from fastapi import FastAPI, Form
from pydantic import BaseModel
app = FastAPI()
class FormData(BaseModel):
username: str
password: str
@app.post("/login/")
async def login(data: Annotated[FormData, Form()]):
return data
```
Read the new docs: [Form Models](https://fastapi.tiangolo.com/tutorial/request-form-models/).
### Features
* ✨ Add support for Pydantic models in `Form` parameters. PR [#12129](https://github.com/fastapi/fastapi/pull/12129) by [@tiangolo](https://github.com/tiangolo).
### Internal
* 🔧 Update sponsors: Coherence link. PR [#12130](https://github.com/fastapi/fastapi/pull/12130) by [@tiangolo](https://github.com/tiangolo).
## 0.112.4
This release is mainly a big internal refactor to enable adding support for Pydantic models for `Form` fields, but that feature comes in the next release.
This release shouldn't affect apps using FastAPI in any way. You don't even have to upgrade to this version yet. It's just a checkpoint. 🤓
### Refactors
* ♻️ Refactor deciding if `embed` body fields, do not overwrite fields, compute once per router, refactor internals in preparation for Pydantic models in `Form`, `Query` and others. PR [#12117](https://github.com/fastapi/fastapi/pull/12117) by [@tiangolo](https://github.com/tiangolo).
### Internal
* ⏪️ Temporarily revert "✨ Add support for Pydantic models in `Form` parameters" to make a checkpoint release. PR [#12128](https://github.com/fastapi/fastapi/pull/12128) by [@tiangolo](https://github.com/tiangolo). Restored by PR [#12129](https://github.com/fastapi/fastapi/pull/12129).
* ✨ Add support for Pydantic models in `Form` parameters. PR [#12127](https://github.com/fastapi/fastapi/pull/12127) by [@tiangolo](https://github.com/tiangolo). Reverted by PR [#12128](https://github.com/fastapi/fastapi/pull/12128) to make a checkpoint release with only refactors. Restored by PR [#12129](https://github.com/fastapi/fastapi/pull/12129).
## 0.112.3
This release is mainly internal refactors, it shouldn't affect apps using FastAPI in any way. You don't even have to upgrade to this version yet. There are a few bigger releases coming right after. 🚀
### Refactors ### Refactors
* ♻️ Refactor internal `check_file_field()`, rename to `ensure_multipart_is_installed()` to clarify its purpose. PR [#12106](https://github.com/fastapi/fastapi/pull/12106) by [@tiangolo](https://github.com/tiangolo). * ♻️ Refactor internal `check_file_field()`, rename to `ensure_multipart_is_installed()` to clarify its purpose. PR [#12106](https://github.com/fastapi/fastapi/pull/12106) by [@tiangolo](https://github.com/tiangolo).
@ -39,6 +171,7 @@ hide:
### Internal ### Internal
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#12115](https://github.com/fastapi/fastapi/pull/12115) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Bump pypa/gh-action-pypi-publish from 1.10.0 to 1.10.1. PR [#12120](https://github.com/fastapi/fastapi/pull/12120) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump pypa/gh-action-pypi-publish from 1.10.0 to 1.10.1. PR [#12120](https://github.com/fastapi/fastapi/pull/12120) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump pillow from 10.3.0 to 10.4.0. PR [#12105](https://github.com/fastapi/fastapi/pull/12105) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump pillow from 10.3.0 to 10.4.0. PR [#12105](https://github.com/fastapi/fastapi/pull/12105) by [@dependabot[bot]](https://github.com/apps/dependabot).
* 💚 Set `include-hidden-files` to `True` when using the `upload-artifact` GH action. PR [#12118](https://github.com/fastapi/fastapi/pull/12118) by [@svlandeg](https://github.com/svlandeg). * 💚 Set `include-hidden-files` to `True` when using the `upload-artifact` GH action. PR [#12118](https://github.com/fastapi/fastapi/pull/12118) by [@svlandeg](https://github.com/svlandeg).

134
docs/en/docs/tutorial/request-form-models.md

@ -0,0 +1,134 @@
# Form Models
You can use **Pydantic models** to declare **form fields** in FastAPI.
/// info
To use forms, first install <a href="https://github.com/Kludex/python-multipart" class="external-link" target="_blank">`python-multipart`</a>.
Make sure you create a [virtual environment](../virtual-environments.md){.internal-link target=_blank}, activate it, and then install it, for example:
```console
$ pip install python-multipart
```
///
/// note
This is supported since FastAPI version `0.113.0`. 🤓
///
## Pydantic Models for Forms
You just need to declare a **Pydantic model** with the fields you want to receive as **form fields**, and then declare the parameter as `Form`:
//// tab | Python 3.9+
```Python hl_lines="9-11 15"
{!> ../../../docs_src/request_form_models/tutorial001_an_py39.py!}
```
////
//// tab | Python 3.8+
```Python hl_lines="8-10 14"
{!> ../../../docs_src/request_form_models/tutorial001_an.py!}
```
////
//// tab | Python 3.8+ non-Annotated
/// tip
Prefer to use the `Annotated` version if possible.
///
```Python hl_lines="7-9 13"
{!> ../../../docs_src/request_form_models/tutorial001.py!}
```
////
**FastAPI** will **extract** the data for **each field** from the **form data** in the request and give you the Pydantic model you defined.
## Check the Docs
You can verify it in the docs UI at `/docs`:
<div class="screenshot">
<img src="/img/tutorial/request-form-models/image01.png">
</div>
## Forbid Extra Form Fields
In some special use cases (probably not very common), you might want to **restrict** the form fields to only those declared in the Pydantic model. And **forbid** any **extra** fields.
/// note
This is supported since FastAPI version `0.114.0`. 🤓
///
You can use Pydantic's model configuration to `forbid` any `extra` fields:
//// tab | Python 3.9+
```Python hl_lines="12"
{!> ../../../docs_src/request_form_models/tutorial002_an_py39.py!}
```
////
//// tab | Python 3.8+
```Python hl_lines="11"
{!> ../../../docs_src/request_form_models/tutorial002_an.py!}
```
////
//// tab | Python 3.8+ non-Annotated
/// tip
Prefer to use the `Annotated` version if possible.
///
```Python hl_lines="10"
{!> ../../../docs_src/request_form_models/tutorial002.py!}
```
////
If a client tries to send some extra data, they will receive an **error** response.
For example, if the client tries to send the form fields:
* `username`: `Rick`
* `password`: `Portal Gun`
* `extra`: `Mr. Poopybutthole`
They will receive an error response telling them that the field `extra` is not allowed:
```json
{
"detail": [
{
"type": "extra_forbidden",
"loc": ["body", "extra"],
"msg": "Extra inputs are not permitted",
"input": "Mr. Poopybutthole"
}
]
}
```
## Summary
You can use Pydantic models to declare form fields in FastAPI. 😎

1
docs/en/mkdocs.yml

@ -129,6 +129,7 @@ nav:
- tutorial/extra-models.md - tutorial/extra-models.md
- tutorial/response-status-code.md - tutorial/response-status-code.md
- tutorial/request-forms.md - tutorial/request-forms.md
- tutorial/request-form-models.md
- tutorial/request-files.md - tutorial/request-files.md
- tutorial/request-forms-and-files.md - tutorial/request-forms-and-files.md
- tutorial/handling-errors.md - tutorial/handling-errors.md

2
docs/en/overrides/main.html

@ -59,7 +59,7 @@
</a> </a>
</div> </div>
<div class="item"> <div class="item">
<a title="Coherence" style="display: block; position: relative;" href="https://docs.withcoherence.com/coherence-templates/full-stack-template/#fastapi?utm_medium=advertising&utm_source=fastapi&utm_campaign=docs" target="_blank"> <a title="Coherence" style="display: block; position: relative;" href="https://www.withcoherence.com/?utm_medium=advertising&utm_source=fastapi&utm_campaign=website" target="_blank">
<span class="sponsor-badge">sponsor</span> <span class="sponsor-badge">sponsor</span>
<img class="sponsor-image" src="/img/sponsors/coherence-banner.png" /> <img class="sponsor-image" src="/img/sponsors/coherence-banner.png" />
</a> </a>

28
docs/ko/docs/project-generation.md

@ -0,0 +1,28 @@
# Full Stack FastAPI 템플릿
템플릿은 일반적으로 특정 설정과 함께 제공되지만, 유연하고 커스터마이징이 가능하게 디자인 되었습니다. 이 특성들은 여러분이 프로젝트의 요구사항에 맞춰 수정, 적용을 할 수 있게 해주고, 템플릿이 완벽한 시작점이 되게 해줍니다. 🏁
많은 초기 설정, 보안, 데이터베이스 및 일부 API 엔드포인트가 이미 준비되어 있으므로, 여러분은 이 템플릿을 (프로젝트를) 시작하는 데 사용할 수 있습니다.
GitHub 저장소: <a href="https://github.com/tiangolo/full-stack-fastapi-template" class="external-link" target="_blank">Full Stack FastAPI 템플릿</a>
## Full Stack FastAPI 템플릿 - 기술 스택과 기능들
- ⚡ [**FastAPI**](https://fastapi.tiangolo.com): Python 백엔드 API.
- 🧰 [SQLModel](https://sqlmodel.tiangolo.com): Python SQL 데이터 상호작용을 위한 (ORM).
- 🔍 [Pydantic](https://docs.pydantic.dev): FastAPI에 의해 사용되는, 데이터 검증과 설정관리.
- 💾 [PostgreSQL](https://www.postgresql.org): SQL 데이터베이스.
- 🚀 [React](https://react.dev): 프론트엔드.
- 💃 TypeScript, hooks, Vite 및 기타 현대적인 프론트엔드 스택을 사용.
- 🎨 [Chakra UI](https://chakra-ui.com): 프론트엔드 컴포넌트.
- 🤖 자동으로 생성된 프론트엔드 클라이언트.
- 🧪 E2E 테스트를 위한 Playwright.
- 🦇 다크 모드 지원.
- 🐋 [Docker Compose](https://www.docker.com): 개발 환경과 프로덕션(운영).
- 🔒 기본으로 지원되는 안전한 비밀번호 해싱.
- 🔑 JWT 토큰 인증.
- 📫 이메일 기반 비밀번호 복구.
- ✅ [Pytest]를 이용한 테스트(https://pytest.org).
- 📞 [Traefik](https://traefik.io): 리버스 프록시 / 로드 밸런서.
- 🚢 Docker Compose를 이용한 배포 지침: 자동 HTTPS 인증서를 처리하기 위한 프론트엔드 Traefik 프록시 설정 방법을 포함.
- 🏭 GitHub Actions를 기반으로 CI (지속적인 통합) 및 CD (지속적인 배포).

597
docs/nl/docs/python-types.md

@ -0,0 +1,597 @@
# Introductie tot Python Types
Python biedt ondersteuning voor optionele "type hints" (ook wel "type annotaties" genoemd).
Deze **"type hints"** of annotaties zijn een speciale syntax waarmee het <abbr title="bijvoorbeeld: str, int, float, bool">type</abbr> van een variabele kan worden gedeclareerd.
Door types voor je variabelen te declareren, kunnen editors en hulpmiddelen je beter ondersteunen.
Dit is slechts een **korte tutorial/opfrisser** over Python type hints. Het behandelt enkel het minimum dat nodig is om ze te gebruiken met **FastAPI**... en dat is relatief weinig.
**FastAPI** is helemaal gebaseerd op deze type hints, ze geven veel voordelen.
Maar zelfs als je **FastAPI** nooit gebruikt, heb je er baat bij om er iets over te leren.
/// note
Als je een Python expert bent en alles al weet over type hints, sla dan dit hoofdstuk over.
///
## Motivatie
Laten we beginnen met een eenvoudig voorbeeld:
```Python
{!../../../docs_src/python_types/tutorial001.py!}
```
Het aanroepen van dit programma leidt tot het volgende resultaat:
```
John Doe
```
De functie voert het volgende uit:
* Neem een `first_name` en een `last_name`
* Converteer de eerste letter van elk naar een hoofdletter met `title()`.
``
* <abbr title="Voegt ze samen, als één. Met de inhoud van de een na de ander.">Voeg samen</abbr> met een spatie in het midden.
```Python hl_lines="2"
{!../../../docs_src/python_types/tutorial001.py!}
```
### Bewerk het
Dit is een heel eenvoudig programma.
Maar stel je nu voor dat je het vanaf nul zou moeten maken.
Op een gegeven moment zou je aan de definitie van de functie zijn begonnen, je had de parameters klaar...
Maar dan moet je “die methode die de eerste letter naar hoofdletters converteert” aanroepen.
Was het `upper`? Was het `uppercase`? `first_uppercase`? `capitalize`?
Dan roep je de hulp in van je oude programmeursvriend, (automatische) code aanvulling in je editor.
Je typt de eerste parameter van de functie, `first_name`, dan een punt (`.`) en drukt dan op `Ctrl+Spatie` om de aanvulling te activeren.
Maar helaas krijg je niets bruikbaars:
<img src="/img/python-types/image01.png">
### Types toevoegen
Laten we een enkele regel uit de vorige versie aanpassen.
We zullen precies dit fragment, de parameters van de functie, wijzigen van:
```Python
first_name, last_name
```
naar:
```Python
first_name: str, last_name: str
```
Dat is alles.
Dat zijn de "type hints":
```Python hl_lines="1"
{!../../../docs_src/python_types/tutorial002.py!}
```
Dit is niet hetzelfde als het declareren van standaardwaarden zoals bij:
```Python
first_name="john", last_name="doe"
```
Het is iets anders.
We gebruiken dubbele punten (`:`), geen gelijkheidstekens (`=`).
Het toevoegen van type hints verandert normaal gesproken niet wat er gebeurt in je programma t.o.v. wat er zonder type hints zou gebeuren.
Maar stel je voor dat je weer bezig bent met het maken van een functie, maar deze keer met type hints.
Op hetzelfde moment probeer je de automatische aanvulling te activeren met `Ctrl+Spatie` en je ziet:
<img src="/img/python-types/image02.png">
Nu kun je de opties bekijken en er doorheen scrollen totdat je de optie vindt die “een belletje doet rinkelen”:
<img src="/img/python-types/image03.png">
### Meer motivatie
Bekijk deze functie, deze heeft al type hints:
```Python hl_lines="1"
{!../../../docs_src/python_types/tutorial003.py!}
```
Omdat de editor de types van de variabelen kent, krijgt u niet alleen aanvulling, maar ook controles op fouten:
<img src="/img/python-types/image04.png">
Nu weet je hoe je het moet oplossen, converteer `age` naar een string met `str(age)`:
```Python hl_lines="2"
{!../../../docs_src/python_types/tutorial004.py!}
```
## Types declareren
Je hebt net de belangrijkste plek om type hints te declareren gezien. Namelijk als functieparameters.
Dit is ook de belangrijkste plek waar je ze gebruikt met **FastAPI**.
### Eenvoudige types
Je kunt alle standaard Python types declareren, niet alleen `str`.
Je kunt bijvoorbeeld het volgende gebruiken:
* `int`
* `float`
* `bool`
* `bytes`
```Python hl_lines="1"
{!../../../docs_src/python_types/tutorial005.py!}
```
### Generieke types met typeparameters
Er zijn enkele datastructuren die andere waarden kunnen bevatten, zoals `dict`, `list`, `set` en `tuple` en waar ook de interne waarden hun eigen type kunnen hebben.
Deze types die interne types hebben worden “**generieke**” types genoemd. Het is mogelijk om ze te declareren, zelfs met hun interne types.
Om deze types en de interne types te declareren, kun je de standaard Python module `typing` gebruiken. Deze module is speciaal gemaakt om deze type hints te ondersteunen.
#### Nieuwere versies van Python
De syntax met `typing` is **verenigbaar** met alle versies, van Python 3.6 tot aan de nieuwste, inclusief Python 3.9, Python 3.10, enz.
Naarmate Python zich ontwikkelt, worden **nieuwere versies**, met verbeterde ondersteuning voor deze type annotaties, beschikbaar. In veel gevallen hoef je niet eens de `typing` module te importeren en te gebruiken om de type annotaties te declareren.
Als je een recentere versie van Python kunt kiezen voor je project, kun je profiteren van die extra eenvoud.
In alle documentatie staan voorbeelden die compatibel zijn met elke versie van Python (als er een verschil is).
Bijvoorbeeld “**Python 3.6+**” betekent dat het compatibel is met Python 3.6 of hoger (inclusief 3.7, 3.8, 3.9, 3.10, etc). En “**Python 3.9+**” betekent dat het compatibel is met Python 3.9 of hoger (inclusief 3.10, etc).
Als je de **laatste versies van Python** kunt gebruiken, gebruik dan de voorbeelden voor de laatste versie, die hebben de **beste en eenvoudigste syntax**, bijvoorbeeld “**Python 3.10+**”.
#### List
Laten we bijvoorbeeld een variabele definiëren als een `list` van `str`.
//// tab | Python 3.9+
Declareer de variabele met dezelfde dubbele punt (`:`) syntax.
Als type, vul `list` in.
Doordat de list een type is dat enkele interne types bevat, zet je ze tussen vierkante haakjes:
```Python hl_lines="1"
{!> ../../../docs_src/python_types/tutorial006_py39.py!}
```
////
//// tab | Python 3.8+
Van `typing`, importeer `List` (met een hoofdletter `L`):
```Python hl_lines="1"
{!> ../../../docs_src/python_types/tutorial006.py!}
```
Declareer de variabele met dezelfde dubbele punt (`:`) syntax.
Zet als type de `List` die je hebt geïmporteerd uit `typing`.
Doordat de list een type is dat enkele interne types bevat, zet je ze tussen vierkante haakjes:
```Python hl_lines="4"
{!> ../../../docs_src/python_types/tutorial006.py!}
```
////
/// info
De interne types tussen vierkante haakjes worden “typeparameters” genoemd.
In dit geval is `str` de typeparameter die wordt doorgegeven aan `List` (of `list` in Python 3.9 en hoger).
///
Dat betekent: “de variabele `items` is een `list`, en elk van de items in deze list is een `str`”.
/// tip
Als je Python 3.9 of hoger gebruikt, hoef je `List` niet te importeren uit `typing`, je kunt in plaats daarvan hetzelfde reguliere `list` type gebruiken.
///
Door dat te doen, kan je editor ondersteuning bieden, zelfs tijdens het verwerken van items uit de list:
<img src="/img/python-types/image05.png">
Zonder types is dat bijna onmogelijk om te bereiken.
Merk op dat de variabele `item` een van de elementen is in de lijst `items`.
Toch weet de editor dat het een `str` is, en biedt daar vervolgens ondersteuning voor aan.
#### Tuple en Set
Je kunt hetzelfde doen om `tuple`s en `set`s te declareren:
//// tab | Python 3.9+
```Python hl_lines="1"
{!> ../../../docs_src/python_types/tutorial007_py39.py!}
```
////
//// tab | Python 3.8+
```Python hl_lines="1 4"
{!> ../../../docs_src/python_types/tutorial007.py!}
```
////
Dit betekent:
* De variabele `items_t` is een `tuple` met 3 items, een `int`, nog een `int`, en een `str`.
* De variabele `items_s` is een `set`, en elk van de items is van het type `bytes`.
#### Dict
Om een `dict` te definiëren, geef je 2 typeparameters door, gescheiden door komma's.
De eerste typeparameter is voor de sleutels (keys) van de `dict`.
De tweede typeparameter is voor de waarden (values) van het `dict`:
//// tab | Python 3.9+
```Python hl_lines="1"
{!> ../../../docs_src/python_types/tutorial008_py39.py!}
```
////
//// tab | Python 3.8+
```Python hl_lines="1 4"
{!> ../../../docs_src/python_types/tutorial008.py!}
```
////
Dit betekent:
* De variabele `prices` is een `dict`:
* De sleutels van dit `dict` zijn van het type `str` (bijvoorbeeld de naam van elk item).
* De waarden van dit `dict` zijn van het type `float` (bijvoorbeeld de prijs van elk item).
#### Union
Je kunt een variable declareren die van **verschillende types** kan zijn, bijvoorbeeld een `int` of een `str`.
In Python 3.6 en hoger (inclusief Python 3.10) kun je het `Union`-type van `typing` gebruiken en de mogelijke types die je wilt accepteren, tussen de vierkante haakjes zetten.
In Python 3.10 is er ook een **nieuwe syntax** waarin je de mogelijke types kunt scheiden door een <abbr title='ook wel "bitwise of operator" genoemd, maar die betekenis is hier niet relevant'>verticale balk (`|`)</abbr>.
//// tab | Python 3.10+
```Python hl_lines="1"
{!> ../../../docs_src/python_types/tutorial008b_py310.py!}
```
////
//// tab | Python 3.8+
```Python hl_lines="1 4"
{!> ../../../docs_src/python_types/tutorial008b.py!}
```
////
In beide gevallen betekent dit dat `item` een `int` of een `str` kan zijn.
#### Mogelijk `None`
Je kunt declareren dat een waarde een type kan hebben, zoals `str`, maar dat het ook `None` kan zijn.
In Python 3.6 en hoger (inclusief Python 3.10) kun je het declareren door `Optional` te importeren en te gebruiken vanuit de `typing`-module.
```Python hl_lines="1 4"
{!../../../docs_src/python_types/tutorial009.py!}
```
Door `Optional[str]` te gebruiken in plaats van alleen `str`, kan de editor je helpen fouten te detecteren waarbij je ervan uit zou kunnen gaan dat een waarde altijd een `str` is, terwijl het in werkelijkheid ook `None` zou kunnen zijn.
`Optional[EenType]` is eigenlijk een snelkoppeling voor `Union[EenType, None]`, ze zijn equivalent.
Dit betekent ook dat je in Python 3.10 `EenType | None` kunt gebruiken:
//// tab | Python 3.10+
```Python hl_lines="1"
{!> ../../../docs_src/python_types/tutorial009_py310.py!}
```
////
//// tab | Python 3.8+
```Python hl_lines="1 4"
{!> ../../../docs_src/python_types/tutorial009.py!}
```
////
//// tab | Python 3.8+ alternative
```Python hl_lines="1 4"
{!> ../../../docs_src/python_types/tutorial009b.py!}
```
////
#### Gebruik van `Union` of `Optional`
Als je een Python versie lager dan 3.10 gebruikt, is dit een tip vanuit mijn **subjectieve** standpunt:
* 🚨 Vermijd het gebruik van `Optional[EenType]`.
* Gebruik in plaats daarvan **`Union[EenType, None]`** ✨.
Beide zijn gelijkwaardig en onderliggend zijn ze hetzelfde, maar ik zou `Union` aanraden in plaats van `Optional` omdat het woord “**optional**” lijkt te impliceren dat de waarde optioneel is, en het eigenlijk betekent “het kan `None` zijn”, zelfs als het niet optioneel is en nog steeds vereist is.
Ik denk dat `Union[SomeType, None]` explicieter is over wat het betekent.
Het gaat alleen om de woorden en naamgeving. Maar die naamgeving kan invloed hebben op hoe jij en je teamgenoten over de code denken.
Laten we als voorbeeld deze functie nemen:
```Python hl_lines="1 4"
{!../../../docs_src/python_types/tutorial009c.py!}
```
De parameter `name` is gedefinieerd als `Optional[str]`, maar is **niet optioneel**, je kunt de functie niet aanroepen zonder de parameter:
```Python
say_hi() # Oh, nee, dit geeft een foutmelding! 😱
```
De `name` parameter is **nog steeds vereist** (niet *optioneel*) omdat het geen standaardwaarde heeft. Toch accepteert `name` `None` als waarde:
```Python
say_hi(name=None) # Dit werkt, None is geldig 🎉
```
Het goede nieuws is dat als je eenmaal Python 3.10 gebruikt, je je daar geen zorgen meer over hoeft te maken, omdat je dan gewoon `|` kunt gebruiken om unions van types te definiëren:
```Python hl_lines="1 4"
{!../../../docs_src/python_types/tutorial009c_py310.py!}
```
Dan hoef je je geen zorgen te maken over namen als `Optional` en `Union`. 😎
#### Generieke typen
De types die typeparameters in vierkante haakjes gebruiken, worden **Generieke types** of **Generics** genoemd, bijvoorbeeld:
//// tab | Python 3.10+
Je kunt dezelfde ingebouwde types gebruiken als generics (met vierkante haakjes en types erin):
* `list`
* `tuple`
* `set`
* `dict`
Hetzelfde als bij Python 3.8, uit de `typing`-module:
* `Union`
* `Optional` (hetzelfde als bij Python 3.8)
* ...en anderen.
In Python 3.10 kun je , als alternatief voor de generieke `Union` en `Optional`, de <abbr title='ook wel "bitwise or operator" genoemd, maar die betekenis is hier niet relevant'>verticale lijn (`|`)</abbr> gebruiken om unions van typen te voorzien, dat is veel beter en eenvoudiger.
////
//// tab | Python 3.9+
Je kunt dezelfde ingebouwde types gebruiken als generieke types (met vierkante haakjes en types erin):
* `list`
* `tuple`
* `set`
* `dict`
En hetzelfde als met Python 3.8, vanuit de `typing`-module:
* `Union`
* `Optional`
* ...en anderen.
////
//// tab | Python 3.8+
* `List`
* `Tuple`
* `Set`
* `Dict`
* `Union`
* `Optional`
* ...en anderen.
////
### Klassen als types
Je kunt een klasse ook declareren als het type van een variabele.
Stel dat je een klasse `Person` hebt, met een naam:
```Python hl_lines="1-3"
{!../../../docs_src/python_types/tutorial010.py!}
```
Vervolgens kun je een variabele van het type `Persoon` declareren:
```Python hl_lines="6"
{!../../../docs_src/python_types/tutorial010.py!}
```
Dan krijg je ook nog eens volledige editorondersteuning:
<img src="/img/python-types/image06.png">
Merk op dat dit betekent dat "`one_person` een **instantie** is van de klasse `Person`".
Dit betekent niet dat `one_person` de **klasse** is met de naam `Person`.
## Pydantic modellen
<a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic</a> is een Python-pakket voor het uitvoeren van datavalidatie.
Je declareert de "vorm" van de data als klassen met attributen.
Elk attribuut heeft een type.
Vervolgens maak je een instantie van die klasse met een aantal waarden en het valideert de waarden, converteert ze naar het juiste type (als dat het geval is) en geeft je een object met alle data terug.
Daarnaast krijg je volledige editorondersteuning met dat resulterende object.
Een voorbeeld uit de officiële Pydantic-documentatie:
//// tab | Python 3.10+
```Python
{!> ../../../docs_src/python_types/tutorial011_py310.py!}
```
////
//// tab | Python 3.9+
```Python
{!> ../../../docs_src/python_types/tutorial011_py39.py!}
```
////
//// tab | Python 3.8+
```Python
{!> ../../../docs_src/python_types/tutorial011.py!}
```
////
/// info
Om meer te leren over <a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic, bekijk de documentatie</a>.
///
**FastAPI** is volledig gebaseerd op Pydantic.
Je zult veel meer van dit alles in de praktijk zien in de [Tutorial - Gebruikershandleiding](tutorial/index.md){.internal-link target=_blank}.
/// tip
Pydantic heeft een speciaal gedrag wanneer je `Optional` of `Union[EenType, None]` gebruikt zonder een standaardwaarde, je kunt er meer over lezen in de Pydantic-documentatie over <a href="https://docs.pydantic.dev/2.3/usage/models/#required-fields" class="external-link" target="_blank">Verplichte optionele velden</a>.
///
## Type Hints met Metadata Annotaties
Python heeft ook een functie waarmee je **extra <abbr title="Data over de data, in dit geval informatie over het type, bijvoorbeeld een beschrijving.">metadata</abbr>** in deze type hints kunt toevoegen met behulp van `Annotated`.
//// tab | Python 3.9+
In Python 3.9 is `Annotated` onderdeel van de standaardpakket, dus je kunt het importeren vanuit `typing`.
```Python hl_lines="1 4"
{!> ../../../docs_src/python_types/tutorial013_py39.py!}
```
////
//// tab | Python 3.8+
In versies lager dan Python 3.9 importeer je `Annotated` vanuit `typing_extensions`.
Het wordt al geïnstalleerd met **FastAPI**.
```Python hl_lines="1 4"
{!> ../../../docs_src/python_types/tutorial013.py!}
```
////
Python zelf doet niets met deze `Annotated` en voor editors en andere hulpmiddelen is het type nog steeds een `str`.
Maar je kunt deze ruimte in `Annotated` gebruiken om **FastAPI** te voorzien van extra metadata over hoe je wilt dat je applicatie zich gedraagt.
Het belangrijkste om te onthouden is dat **de eerste *typeparameter*** die je doorgeeft aan `Annotated` het **werkelijke type** is. De rest is gewoon metadata voor andere hulpmiddelen.
Voor nu hoef je alleen te weten dat `Annotated` bestaat en dat het standaard Python is. 😎
Later zul je zien hoe **krachtig** het kan zijn.
/// tip
Het feit dat dit **standaard Python** is, betekent dat je nog steeds de **best mogelijke ontwikkelaarservaring** krijgt in je editor, met de hulpmiddelen die je gebruikt om je code te analyseren en te refactoren, enz. ✨
Daarnaast betekent het ook dat je code zeer verenigbaar zal zijn met veel andere Python-hulpmiddelen en -pakketten. 🚀
///
## Type hints in **FastAPI**
**FastAPI** maakt gebruik van type hints om verschillende dingen te doen.
Met **FastAPI** declareer je parameters met type hints en krijg je:
* **Editor ondersteuning**.
* **Type checks**.
...en **FastAPI** gebruikt dezelfde declaraties om:
* **Vereisten te definïeren **: van request pad parameters, query parameters, headers, bodies, dependencies, enz.
* **Data te converteren**: van de request naar het vereiste type.
* **Data te valideren**: afkomstig van elke request:
* **Automatische foutmeldingen** te genereren die naar de client worden geretourneerd wanneer de data ongeldig is.
* De API met OpenAPI te **documenteren**:
* die vervolgens wordt gebruikt door de automatische interactieve documentatie gebruikersinterfaces.
Dit klinkt misschien allemaal abstract. Maak je geen zorgen. Je ziet dit allemaal in actie in de [Tutorial - Gebruikershandleiding](tutorial/index.md){.internal-link target=_blank}.
Het belangrijkste is dat door standaard Python types te gebruiken, op één plek (in plaats van meer klassen, decorators, enz. toe te voegen), **FastAPI** een groot deel van het werk voor je doet.
/// info
Als je de hele tutorial al hebt doorgenomen en terug bent gekomen om meer te weten te komen over types, is een goede bron <a href="https://mypy.readthedocs.io/en/latest/cheat_sheet_py3.html" class="external-link" target="_blank">het "cheat sheet" van `mypy`</a>.
///

298
docs/pt/docs/environment-variables.md

@ -0,0 +1,298 @@
# Variáveis de Ambiente
/// tip | "Dica"
Se você já sabe o que são "variáveis de ambiente" e como usá-las, pode pular esta seção.
///
Uma variável de ambiente (também conhecida como "**env var**") é uma variável que existe **fora** do código Python, no **sistema operacional**, e pode ser lida pelo seu código Python (ou por outros programas também).
Variáveis de ambiente podem ser úteis para lidar com **configurações** do aplicativo, como parte da **instalação** do Python, etc.
## Criar e Usar Variáveis de Ambiente
Você pode **criar** e usar variáveis de ambiente no **shell (terminal)**, sem precisar do Python:
//// tab | Linux, macOS, Windows Bash
<div class="termy">
```console
// Você pode criar uma variável de ambiente MY_NAME com
$ export MY_NAME="Wade Wilson"
// Então você pode usá-la com outros programas, como
$ echo "Hello $MY_NAME"
Hello Wade Wilson
```
</div>
////
//// tab | Windows PowerShell
<div class="termy">
```console
// Criar uma variável de ambiente MY_NAME
$ $Env:MY_NAME = "Wade Wilson"
// Usá-la com outros programas, como
$ echo "Hello $Env:MY_NAME"
Hello Wade Wilson
```
</div>
////
## Ler Variáveis de Ambiente no Python
Você também pode criar variáveis de ambiente **fora** do Python, no terminal (ou com qualquer outro método) e depois **lê-las no Python**.
Por exemplo, você poderia ter um arquivo `main.py` com:
```Python hl_lines="3"
import os
name = os.getenv("MY_NAME", "World")
print(f"Hello {name} from Python")
```
/// tip | "Dica"
O segundo argumento para <a href="https://docs.python.org/3.8/library/os.html#os.getenv" class="external-link" target="_blank">`os.getenv()`</a> é o valor padrão a ser retornado.
Se não for fornecido, é `None` por padrão, Aqui fornecemos `"World"` como o valor padrão a ser usado.
///
Então você poderia chamar esse programa Python:
//// tab | Linux, macOS, Windows Bash
<div class="termy">
```console
// Aqui ainda não definimos a variável de ambiente
$ python main.py
// Como não definimos a variável de ambiente, obtemos o valor padrão
Hello World from Python
// Mas se criarmos uma variável de ambiente primeiro
$ export MY_NAME="Wade Wilson"
// E então chamar o programa novamente
$ python main.py
// Agora ele pode ler a variável de ambiente
Hello Wade Wilson from Python
```
</div>
////
//// tab | Windows PowerShell
<div class="termy">
```console
// Aqui ainda não definimos a variável de ambiente
$ python main.py
// Como não definimos a variável de ambiente, obtemos o valor padrão
Hello World from Python
// Mas se criarmos uma variável de ambiente primeiro
$ $Env:MY_NAME = "Wade Wilson"
// E então chamar o programa novamente
$ python main.py
// Agora ele pode ler a variável de ambiente
Hello Wade Wilson from Python
```
</div>
////
Como as variáveis de ambiente podem ser definidas fora do código, mas podem ser lidas pelo código e não precisam ser armazenadas (com versão no `git`) com o restante dos arquivos, é comum usá-las para configurações ou **definições**.
Você também pode criar uma variável de ambiente apenas para uma **invocação específica do programa**, que só está disponível para aquele programa e apenas pela duração dele.
Para fazer isso, crie-a na mesma linha, antes do próprio programa:
<div class="termy">
```console
// Criar uma variável de ambiente MY_NAME para esta chamada de programa
$ MY_NAME="Wade Wilson" python main.py
// Agora ele pode ler a variável de ambiente
Hello Wade Wilson from Python
// A variável de ambiente não existe mais depois
$ python main.py
Hello World from Python
```
</div>
/// tip | "Dica"
Você pode ler mais sobre isso em <a href="https://12factor.net/config" class="external-link" target="_blank">The Twelve-Factor App: Config</a>.
///
## Tipos e Validação
Essas variáveis de ambiente só podem lidar com **strings de texto**, pois são externas ao Python e precisam ser compatíveis com outros programas e com o resto do sistema (e até mesmo com diferentes sistemas operacionais, como Linux, Windows, macOS).
Isso significa que **qualquer valor** lido em Python de uma variável de ambiente **será uma `str`**, e qualquer conversão para um tipo diferente ou qualquer validação precisa ser feita no código.
Você aprenderá mais sobre como usar variáveis de ambiente para lidar com **configurações do aplicativo** no [Guia do Usuário Avançado - Configurações e Variáveis de Ambiente](./advanced/settings.md){.internal-link target=_blank}.
## Variável de Ambiente `PATH`
Existe uma variável de ambiente **especial** chamada **`PATH`** que é usada pelos sistemas operacionais (Linux, macOS, Windows) para encontrar programas para executar.
O valor da variável `PATH` é uma longa string composta por diretórios separados por dois pontos `:` no Linux e macOS, e por ponto e vírgula `;` no Windows.
Por exemplo, a variável de ambiente `PATH` poderia ter esta aparência:
//// tab | Linux, macOS
```plaintext
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
```
Isso significa que o sistema deve procurar programas nos diretórios:
* `/usr/local/bin`
* `/usr/bin`
* `/bin`
* `/usr/sbin`
* `/sbin`
////
//// tab | Windows
```plaintext
C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32
```
Isso significa que o sistema deve procurar programas nos diretórios:
* `C:\Program Files\Python312\Scripts`
* `C:\Program Files\Python312`
* `C:\Windows\System32`
////
Quando você digita um **comando** no terminal, o sistema operacional **procura** o programa em **cada um dos diretórios** listados na variável de ambiente `PATH`.
Por exemplo, quando você digita `python` no terminal, o sistema operacional procura um programa chamado `python` no **primeiro diretório** dessa lista.
Se ele o encontrar, então ele o **usará**. Caso contrário, ele continua procurando nos **outros diretórios**.
### Instalando o Python e Atualizando o `PATH`
Durante a instalação do Python, você pode ser questionado sobre a atualização da variável de ambiente `PATH`.
//// tab | Linux, macOS
Vamos supor que você instale o Python e ele fique em um diretório `/opt/custompython/bin`.
Se você concordar em atualizar a variável de ambiente `PATH`, o instalador adicionará `/opt/custompython/bin` para a variável de ambiente `PATH`.
Poderia parecer assim:
```plaintext
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/custompython/bin
```
Dessa forma, ao digitar `python` no terminal, o sistema encontrará o programa Python em `/opt/custompython/bin` (último diretório) e o utilizará.
////
//// tab | Windows
Digamos que você instala o Python e ele acaba em um diretório `C:\opt\custompython\bin`.
Se você disser sim para atualizar a variável de ambiente `PATH`, o instalador adicionará `C:\opt\custompython\bin` à variável de ambiente `PATH`.
```plaintext
C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32;C:\opt\custompython\bin
```
Dessa forma, quando você digitar `python` no terminal, o sistema encontrará o programa Python em `C:\opt\custompython\bin` (o último diretório) e o utilizará.
////
Então, se você digitar:
<div class="termy">
```console
$ python
```
</div>
//// tab | Linux, macOS
O sistema **encontrará** o programa `python` em `/opt/custompython/bin` e o executará.
Seria aproximadamente equivalente a digitar:
<div class="termy">
```console
$ /opt/custompython/bin/python
```
</div>
////
//// tab | Windows
O sistema **encontrará** o programa `python` em `C:\opt\custompython\bin\python` e o executará.
Seria aproximadamente equivalente a digitar:
<div class="termy">
```console
$ C:\opt\custompython\bin\python
```
</div>
////
Essas informações serão úteis ao aprender sobre [Ambientes Virtuais](virtual-environments.md){.internal-link target=_blank}.
## Conclusão
Com isso, você deve ter uma compreensão básica do que são **variáveis ​​de ambiente** e como usá-las em Python.
Você também pode ler mais sobre elas na <a href="https://en.wikipedia.org/wiki/Environment_variable" class="external-link" target="_blank">Wikipedia para Variáveis ​​de Ambiente</a>.
Em muitos casos, não é muito óbvio como as variáveis ​​de ambiente seriam úteis e aplicáveis ​​imediatamente. Mas elas continuam aparecendo em muitos cenários diferentes quando você está desenvolvendo, então é bom saber sobre elas.
Por exemplo, você precisará dessas informações na próxima seção, sobre [Ambientes Virtuais](virtual-environments.md).

115
docs/pt/docs/tutorial/debugging.md

@ -0,0 +1,115 @@
# Depuração
Você pode conectar o depurador no seu editor, por exemplo, com o Visual Studio Code ou PyCharm.
## Chamar `uvicorn`
Em seu aplicativo FastAPI, importe e execute `uvicorn` diretamente:
```Python hl_lines="1 15"
{!../../../docs_src/debugging/tutorial001.py!}
```
### Sobre `__name__ == "__main__"`
O objetivo principal de `__name__ == "__main__"` é ter algum código que seja executado quando seu arquivo for chamado com:
<div class="termy">
```console
$ python myapp.py
```
</div>
mas não é chamado quando outro arquivo o importa, como em:
```Python
from myapp import app
```
#### Mais detalhes
Digamos que seu arquivo se chama `myapp.py`.
Se você executá-lo com:
<div class="termy">
```console
$ python myapp.py
```
</div>
então a variável interna `__name__` no seu arquivo, criada automaticamente pelo Python, terá como valor a string `"__main__"`.
Então, a seção:
```Python
uvicorn.run(app, host="0.0.0.0", port=8000)
```
vai executar.
---
Isso não acontecerá se você importar esse módulo (arquivo).
Então, se você tiver outro arquivo `importer.py` com:
```Python
from myapp import app
# Mais um pouco de código
```
nesse caso, a variável criada automaticamente dentro de `myapp.py` não terá a variável `__name__` com o valor `"__main__"`.
Então, a linha:
```Python
uvicorn.run(app, host="0.0.0.0", port=8000)
```
não será executada.
/// info | "Informação"
Para mais informações, consulte <a href="https://docs.python.org/3/library/__main__.html" class="external-link" target="_blank">a documentação oficial do Python</a>.
///
## Execute seu código com seu depurador
Como você está executando o servidor Uvicorn diretamente do seu código, você pode chamar seu programa Python (seu aplicativo FastAPI) diretamente do depurador.
---
Por exemplo, no Visual Studio Code, você pode:
* Ir para o painel "Debug".
* "Add configuration...".
* Selecionar "Python"
* Executar o depurador com a opção "`Python: Current File (Integrated Terminal)`".
Em seguida, ele iniciará o servidor com seu código **FastAPI**, parará em seus pontos de interrupção, etc.
Veja como pode parecer:
<img src="/img/tutorial/debugging/image01.png">
---
Se você usar o Pycharm, você pode:
* Abrir o menu "Executar".
* Selecionar a opção "Depurar...".
* Então um menu de contexto aparece.
* Selecionar o arquivo para depurar (neste caso, `main.py`).
Em seguida, ele iniciará o servidor com seu código **FastAPI**, parará em seus pontos de interrupção, etc.
Veja como pode parecer:
<img src="/img/tutorial/debugging/image02.png">

134
docs/pt/docs/tutorial/request-form-models.md

@ -0,0 +1,134 @@
# Modelos de Formulários
Você pode utilizar **Modelos Pydantic** para declarar **campos de formulários** no FastAPI.
/// info | "Informação"
Para utilizar formulários, instale primeiramente o <a href="https://github.com/Kludex/python-multipart" class="external-link" target="_blank">`python-multipart`</a>.
Certifique-se de criar um [ambiente virtual](../virtual-environments.md){.internal-link target=_blank}, ativá-lo, e então instalar. Por exemplo:
```console
$ pip install python-multipart
```
///
/// note | "Nota"
Isto é suportado desde a versão `0.113.0` do FastAPI. 🤓
///
## Modelos Pydantic para Formulários
Você precisa apenas declarar um **modelo Pydantic** com os campos que deseja receber como **campos de formulários**, e então declarar o parâmetro como um `Form`:
//// tab | Python 3.9+
```Python hl_lines="9-11 15"
{!> ../../../docs_src/request_form_models/tutorial001_an_py39.py!}
```
////
//// tab | Python 3.8+
```Python hl_lines="8-10 14"
{!> ../../../docs_src/request_form_models/tutorial001_an.py!}
```
////
//// tab | Python 3.8+ non-Annotated
/// tip | "Dica"
Prefira utilizar a versão `Annotated` se possível.
///
```Python hl_lines="7-9 13"
{!> ../../../docs_src/request_form_models/tutorial001.py!}
```
////
O **FastAPI** irá **extrair** as informações para **cada campo** dos **dados do formulário** na requisição e dar para você o modelo Pydantic que você definiu.
## Confira os Documentos
Você pode verificar na UI de documentação em `/docs`:
<div class="screenshot">
<img src="/img/tutorial/request-form-models/image01.png">
</div>
## Proibir Campos Extras de Formulários
Em alguns casos de uso especiais (provavelmente não muito comum), você pode desejar **restringir** os campos do formulário para aceitar apenas os declarados no modelo Pydantic. E **proibir** qualquer campo **extra**.
/// note | "Nota"
Isso é suportado deste a versão `0.114.0` do FastAPI. 🤓
///
Você pode utilizar a configuração de modelo do Pydantic para `proibir` qualquer campo `extra`:
//// tab | Python 3.9+
```Python hl_lines="12"
{!> ../../../docs_src/request_form_models/tutorial002_an_py39.py!}
```
////
//// tab | Python 3.8+
```Python hl_lines="11"
{!> ../../../docs_src/request_form_models/tutorial002_an.py!}
```
////
//// tab | Python 3.8+ non-Annotated
/// tip
Prefira utilizar a versão `Annotated` se possível.
///
```Python hl_lines="10"
{!> ../../../docs_src/request_form_models/tutorial002.py!}
```
////
Caso um cliente tente enviar informações adicionais, ele receberá um retorno de **erro**.
Por exemplo, se o cliente tentar enviar os campos de formulário:
* `username`: `Rick`
* `password`: `Portal Gun`
* `extra`: `Mr. Poopybutthole`
Ele receberá um retorno de erro informando-o que o campo `extra` não é permitido:
```json
{
"detail": [
{
"type": "extra_forbidden",
"loc": ["body", "extra"],
"msg": "Extra inputs are not permitted",
"input": "Mr. Poopybutthole"
}
]
}
```
## Resumo
Você pode utilizar modelos Pydantic para declarar campos de formulários no FastAPI. 😎

249
docs/pt/docs/tutorial/testing.md

@ -0,0 +1,249 @@
# Testando
Graças ao <a href="https://www.starlette.io/testclient/" class="external-link" target="_blank">Starlette</a>, testar aplicativos **FastAPI** é fácil e agradável.
Ele é baseado no <a href="https://www.python-httpx.org" class="external-link" target="_blank">HTTPX</a>, que por sua vez é projetado com base em Requests, por isso é muito familiar e intuitivo.
Com ele, você pode usar o <a href="https://docs.pytest.org/" class="external-link" target="_blank">pytest</a> diretamente com **FastAPI**.
## Usando `TestClient`
/// info | "Informação"
Para usar o `TestClient`, primeiro instale o <a href="https://www.python-httpx.org" class="external-link" target="_blank">`httpx`</a>.
Certifique-se de criar um [ambiente virtual](../virtual-environments.md){.internal-link target=_blank}, ativá-lo e instalá-lo, por exemplo:
```console
$ pip install httpx
```
///
Importe `TestClient`.
Crie um `TestClient` passando seu aplicativo **FastAPI** para ele.
Crie funções com um nome que comece com `test_` (essa é a convenção padrão do `pytest`).
Use o objeto `TestClient` da mesma forma que você faz com `httpx`.
Escreva instruções `assert` simples com as expressões Python padrão que você precisa verificar (novamente, `pytest` padrão).
```Python hl_lines="2 12 15-18"
{!../../../docs_src/app_testing/tutorial001.py!}
```
/// tip | "Dica"
Observe que as funções de teste são `def` normais, não `async def`.
E as chamadas para o cliente também são chamadas normais, não usando `await`.
Isso permite que você use `pytest` diretamente sem complicações.
///
/// note | "Detalhes técnicos"
Você também pode usar `from starlette.testclient import TestClient`.
**FastAPI** fornece o mesmo `starlette.testclient` que `fastapi.testclient` apenas como uma conveniência para você, o desenvolvedor. Mas ele vem diretamente da Starlette.
///
/// tip | "Dica"
Se você quiser chamar funções `async` em seus testes além de enviar solicitações ao seu aplicativo FastAPI (por exemplo, funções de banco de dados assíncronas), dê uma olhada em [Testes assíncronos](../advanced/async-tests.md){.internal-link target=_blank} no tutorial avançado.
///
## Separando testes
Em uma aplicação real, você provavelmente teria seus testes em um arquivo diferente.
E seu aplicativo **FastAPI** também pode ser composto de vários arquivos/módulos, etc.
### Arquivo do aplicativo **FastAPI**
Digamos que você tenha uma estrutura de arquivo conforme descrito em [Aplicativos maiores](bigger-applications.md){.internal-link target=_blank}:
```
.
├── app
│   ├── __init__.py
│   └── main.py
```
No arquivo `main.py` você tem seu aplicativo **FastAPI**:
```Python
{!../../../docs_src/app_testing/main.py!}
```
### Arquivo de teste
Então você poderia ter um arquivo `test_main.py` com seus testes. Ele poderia estar no mesmo pacote Python (o mesmo diretório com um arquivo `__init__.py`):
``` hl_lines="5"
.
├── app
│   ├── __init__.py
│   ├── main.py
│   └── test_main.py
```
Como esse arquivo está no mesmo pacote, você pode usar importações relativas para importar o objeto `app` do módulo `main` (`main.py`):
```Python hl_lines="3"
{!../../../docs_src/app_testing/test_main.py!}
```
...e ter o código para os testes como antes.
## Testando: exemplo estendido
Agora vamos estender este exemplo e adicionar mais detalhes para ver como testar diferentes partes.
### Arquivo de aplicativo **FastAPI** estendido
Vamos continuar com a mesma estrutura de arquivo de antes:
```
.
├── app
│   ├── __init__.py
│   ├── main.py
│   └── test_main.py
```
Digamos que agora o arquivo `main.py` com seu aplicativo **FastAPI** tenha algumas outras **operações de rotas**.
Ele tem uma operação `GET` que pode retornar um erro.
Ele tem uma operação `POST` que pode retornar vários erros.
Ambas as *operações de rotas* requerem um cabeçalho `X-Token`.
//// tab | Python 3.10+
```Python
{!> ../../../docs_src/app_testing/app_b_an_py310/main.py!}
```
////
//// tab | Python 3.9+
```Python
{!> ../../../docs_src/app_testing/app_b_an_py39/main.py!}
```
////
//// tab | Python 3.8+
```Python
{!> ../../../docs_src/app_testing/app_b_an/main.py!}
```
////
//// tab | Python 3.10+ non-Annotated
/// tip | "Dica"
Prefira usar a versão `Annotated` se possível.
///
```Python
{!> ../../../docs_src/app_testing/app_b_py310/main.py!}
```
////
//// tab | Python 3.8+ non-Annotated
/// tip | "Dica"
Prefira usar a versão `Annotated` se possível.
///
```Python
{!> ../../../docs_src/app_testing/app_b/main.py!}
```
////
### Arquivo de teste estendido
Você pode então atualizar `test_main.py` com os testes estendidos:
```Python
{!> ../../../docs_src/app_testing/app_b/test_main.py!}
```
Sempre que você precisar que o cliente passe informações na requisição e não souber como, você pode pesquisar (no Google) como fazer isso no `httpx`, ou até mesmo como fazer isso com `requests`, já que o design do HTTPX é baseado no design do Requests.
Depois é só fazer o mesmo nos seus testes.
Por exemplo:
* Para passar um parâmetro *path* ou *query*, adicione-o à própria URL.
* Para passar um corpo JSON, passe um objeto Python (por exemplo, um `dict`) para o parâmetro `json`.
* Se você precisar enviar *Dados de Formulário* em vez de JSON, use o parâmetro `data`.
* Para passar *headers*, use um `dict` no parâmetro `headers`.
* Para *cookies*, um `dict` no parâmetro `cookies`.
Para mais informações sobre como passar dados para o backend (usando `httpx` ou `TestClient`), consulte a <a href="https://www.python-httpx.org" class="external-link" target="_blank">documentação do HTTPX</a>.
/// info | "Informação"
Observe que o `TestClient` recebe dados que podem ser convertidos para JSON, não para modelos Pydantic.
Se você tiver um modelo Pydantic em seu teste e quiser enviar seus dados para o aplicativo durante o teste, poderá usar o `jsonable_encoder` descrito em [Codificador compatível com JSON](encoder.md){.internal-link target=_blank}.
///
## Execute-o
Depois disso, você só precisa instalar o `pytest`.
Certifique-se de criar um [ambiente virtual](../virtual-environments.md){.internal-link target=_blank}, ativá-lo e instalá-lo, por exemplo:
<div class="termy">
```console
$ pip install pytest
---> 100%
```
</div>
Ele detectará os arquivos e os testes automaticamente, os executará e informará os resultados para você.
Execute os testes com:
<div class="termy">
```console
$ pytest
================ test session starts ================
platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1
rootdir: /home/user/code/superawesome-cli/app
plugins: forked-1.1.3, xdist-1.31.0, cov-2.8.1
collected 6 items
---> 100%
test_main.py <span style="color: green; white-space: pre;">...... [100%]</span>
<span style="color: green;">================= 1 passed in 0.03s =================</span>
```
</div>

844
docs/pt/docs/virtual-environments.md

@ -0,0 +1,844 @@
# Ambientes Virtuais
Ao trabalhar em projetos Python, você provavelmente deve usar um **ambiente virtual** (ou um mecanismo similar) para isolar os pacotes que você instala para cada projeto.
/// info | "Informação"
Se você já sabe sobre ambientes virtuais, como criá-los e usá-los, talvez seja melhor pular esta seção. 🤓
///
/// tip | "Dica"
Um **ambiente virtual** é diferente de uma **variável de ambiente**.
Uma **variável de ambiente** é uma variável no sistema que pode ser usada por programas.
Um **ambiente virtual** é um diretório com alguns arquivos.
///
/// info | "Informação"
Esta página lhe ensinará como usar **ambientes virtuais** e como eles funcionam.
Se você estiver pronto para adotar uma **ferramenta que gerencia tudo** para você (incluindo a instalação do Python), experimente <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">uv</a>.
///
## Criar um Projeto
Primeiro, crie um diretório para seu projeto.
O que normalmente faço é criar um diretório chamado `code` dentro do meu diretório home/user.
E dentro disso eu crio um diretório por projeto.
<div class="termy">
```console
// Vá para o diretório inicial
$ cd
// Crie um diretório para todos os seus projetos de código
$ mkdir code
// Entre nesse diretório de código
$ cd code
// Crie um diretório para este projeto
$ mkdir awesome-project
// Entre no diretório do projeto
$ cd awesome-project
```
</div>
## Crie um ambiente virtual
Ao começar a trabalhar em um projeto Python **pela primeira vez**, crie um ambiente virtual **<abbr title="existem outras opções, esta é uma diretriz simples">dentro do seu projeto</abbr>**.
/// tip | "Dica"
Você só precisa fazer isso **uma vez por projeto**, não toda vez que trabalhar.
///
//// tab | `venv`
Para criar um ambiente virtual, você pode usar o módulo `venv` que vem com o Python.
<div class="termy">
```console
$ python -m venv .venv
```
</div>
/// details | O que esse comando significa
* `python`: usa o programa chamado `python`
* `-m`: chama um módulo como um script, nós diremos a ele qual módulo vem em seguida
* `venv`: usa o módulo chamado `venv` que normalmente vem instalado com o Python
* `.venv`: cria o ambiente virtual no novo diretório `.venv`
///
////
//// tab | `uv`
Se você tiver o <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">`uv`</a> instalado, poderá usá-lo para criar um ambiente virtual.
<div class="termy">
```console
$ uv venv
```
</div>
/// tip | "Dica"
Por padrão, `uv` criará um ambiente virtual em um diretório chamado `.venv`.
Mas você pode personalizá-lo passando um argumento adicional com o nome do diretório.
///
////
Esse comando cria um novo ambiente virtual em um diretório chamado `.venv`.
/// details | `.venv` ou outro nome
Você pode criar o ambiente virtual em um diretório diferente, mas há uma convenção para chamá-lo de `.venv`.
///
## Ative o ambiente virtual
Ative o novo ambiente virtual para que qualquer comando Python que você executar ou pacote que você instalar o utilize.
/// tip | "Dica"
Faça isso **toda vez** que iniciar uma **nova sessão de terminal** para trabalhar no projeto.
///
//// tab | Linux, macOS
<div class="termy">
```console
$ source .venv/bin/activate
```
</div>
////
//// tab | Windows PowerShell
<div class="termy">
```console
$ .venv\Scripts\Activate.ps1
```
</div>
////
//// tab | Windows Bash
Ou se você usa o Bash para Windows (por exemplo, <a href="https://gitforwindows.org/" class="external-link" target="_blank">Git Bash</a>):
<div class="termy">
```console
$ source .venv/Scripts/activate
```
</div>
////
/// tip | "Dica"
Toda vez que você instalar um **novo pacote** naquele ambiente, **ative** o ambiente novamente.
Isso garante que, se você usar um **programa de terminal (<abbr title="interface de linha de comando">CLI</abbr>)** instalado por esse pacote, você usará aquele do seu ambiente virtual e não qualquer outro que possa ser instalado globalmente, provavelmente com uma versão diferente do que você precisa.
///
## Verifique se o ambiente virtual está ativo
Verifique se o ambiente virtual está ativo (o comando anterior funcionou).
/// tip | "Dica"
Isso é **opcional**, mas é uma boa maneira de **verificar** se tudo está funcionando conforme o esperado e se você está usando o ambiente virtual pretendido.
///
//// tab | Linux, macOS, Windows Bash
<div class="termy">
```console
$ which python
/home/user/code/awesome-project/.venv/bin/python
```
</div>
Se ele mostrar o binário `python` em `.venv/bin/python`, dentro do seu projeto (neste caso `awesome-project`), então funcionou. 🎉
////
//// tab | Windows PowerShell
<div class="termy">
```console
$ Get-Command python
C:\Users\user\code\awesome-project\.venv\Scripts\python
```
</div>
Se ele mostrar o binário `python` em `.venv\Scripts\python`, dentro do seu projeto (neste caso `awesome-project`), então funcionou. 🎉
////
## Atualizar `pip`
/// tip | "Dica"
Se você usar <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">`uv`</a>, você o usará para instalar coisas em vez do `pip`, então não precisará atualizar o `pip`. 😎
///
Se você estiver usando `pip` para instalar pacotes (ele vem por padrão com o Python), você deve **atualizá-lo** para a versão mais recente.
Muitos erros exóticos durante a instalação de um pacote são resolvidos apenas atualizando o `pip` primeiro.
/// tip | "Dica"
Normalmente, você faria isso **uma vez**, logo após criar o ambiente virtual.
///
Certifique-se de que o ambiente virtual esteja ativo (com o comando acima) e execute:
<div class="termy">
```console
$ python -m pip install --upgrade pip
---> 100%
```
</div>
## Adicionar `.gitignore`
Se você estiver usando **Git** (você deveria), adicione um arquivo `.gitignore` para excluir tudo em seu `.venv` do Git.
/// tip | "Dica"
Se você usou <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">`uv`</a> para criar o ambiente virtual, ele já fez isso para você, você pode pular esta etapa. 😎
///
/// tip | "Dica"
Faça isso **uma vez**, logo após criar o ambiente virtual.
///
<div class="termy">
```console
$ echo "*" > .venv/.gitignore
```
</div>
/// details | O que esse comando significa
* `echo "*"`: irá "imprimir" o texto `*` no terminal (a próxima parte muda isso um pouco)
* `>`: qualquer coisa impressa no terminal pelo comando à esquerda de `>` não deve ser impressa, mas sim escrita no arquivo que vai à direita de `>`
* `.gitignore`: o nome do arquivo onde o texto deve ser escrito
E `*` para Git significa "tudo". Então, ele ignorará tudo no diretório `.venv`.
Esse comando criará um arquivo `.gitignore` com o conteúdo:
```gitignore
*
```
///
## Instalar Pacotes
Após ativar o ambiente, você pode instalar pacotes nele.
/// tip | "Dica"
Faça isso **uma vez** ao instalar ou atualizar os pacotes que seu projeto precisa.
Se precisar atualizar uma versão ou adicionar um novo pacote, você **fará isso novamente**.
///
### Instalar pacotes diretamente
Se estiver com pressa e não quiser usar um arquivo para declarar os requisitos de pacote do seu projeto, você pode instalá-los diretamente.
/// tip | "Dica"
É uma (muito) boa ideia colocar os pacotes e versões que seu programa precisa em um arquivo (por exemplo `requirements.txt` ou `pyproject.toml`).
///
//// tab | `pip`
<div class="termy">
```console
$ pip install "fastapi[standard]"
---> 100%
```
</div>
////
//// tab | `uv`
Se você tem o <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">`uv`</a>:
<div class="termy">
```console
$ uv pip install "fastapi[standard]"
---> 100%
```
</div>
////
### Instalar a partir de `requirements.txt`
Se você tiver um `requirements.txt`, agora poderá usá-lo para instalar seus pacotes.
//// tab | `pip`
<div class="termy">
```console
$ pip install -r requirements.txt
---> 100%
```
</div>
////
//// tab | `uv`
Se você tem o <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">`uv`</a>:
<div class="termy">
```console
$ uv pip install -r requirements.txt
---> 100%
```
</div>
////
/// details | `requirements.txt`
Um `requirements.txt` com alguns pacotes poderia se parecer com:
```requirements.txt
fastapi[standard]==0.113.0
pydantic==2.8.0
```
///
## Execute seu programa
Depois de ativar o ambiente virtual, você pode executar seu programa, e ele usará o Python dentro do seu ambiente virtual com os pacotes que você instalou lá.
<div class="termy">
```console
$ python main.py
Hello World
```
</div>
## Configure seu editor
Você provavelmente usaria um editor. Certifique-se de configurá-lo para usar o mesmo ambiente virtual que você criou (ele provavelmente o detectará automaticamente) para que você possa obter erros de preenchimento automático e em linha.
Por exemplo:
* <a href="https://code.visualstudio.com/docs/python/environments#_select-and-activate-an-environment" class="external-link" target="_blank">VS Code</a>
* <a href="https://www.jetbrains.com/help/pycharm/creating-virtual-environment.html" class="external-link" target="_blank">PyCharm</a>
/// tip | "Dica"
Normalmente, você só precisa fazer isso **uma vez**, ao criar o ambiente virtual.
///
## Desativar o ambiente virtual
Quando terminar de trabalhar no seu projeto, você pode **desativar** o ambiente virtual.
<div class="termy">
```console
$ deactivate
```
</div>
Dessa forma, quando você executar `python`, ele não tentará executá-lo naquele ambiente virtual com os pacotes instalados nele.
## Pronto para trabalhar
Agora você está pronto para começar a trabalhar no seu projeto.
/// tip | "Dica"
Você quer entender o que é tudo isso acima?
Continue lendo. 👇🤓
///
## Por que ambientes virtuais
Para trabalhar com o FastAPI, você precisa instalar o <a href="https://www.python.org/" class="external-link" target="_blank">Python</a>.
Depois disso, você precisará **instalar** o FastAPI e quaisquer outros **pacotes** que queira usar.
Para instalar pacotes, você normalmente usaria o comando `pip` que vem com o Python (ou alternativas semelhantes).
No entanto, se você usar `pip` diretamente, os pacotes serão instalados no seu **ambiente Python global** (a instalação global do Python).
### O Problema
Então, qual é o problema em instalar pacotes no ambiente global do Python?
Em algum momento, você provavelmente acabará escrevendo muitos programas diferentes que dependem de **pacotes diferentes**. E alguns desses projetos em que você trabalha dependerão de **versões diferentes** do mesmo pacote. 😱
Por exemplo, você pode criar um projeto chamado `philosophers-stone`, este programa depende de outro pacote chamado **`harry`, usando a versão `1`**. Então, você precisa instalar `harry`.
```mermaid
flowchart LR
stone(philosophers-stone) -->|requires| harry-1[harry v1]
```
Então, em algum momento depois, você cria outro projeto chamado `prisoner-of-azkaban`, e esse projeto também depende de `harry`, mas esse projeto precisa do **`harry` versão `3`**.
```mermaid
flowchart LR
azkaban(prisoner-of-azkaban) --> |requires| harry-3[harry v3]
```
Mas agora o problema é que, se você instalar os pacotes globalmente (no ambiente global) em vez de em um **ambiente virtual** local, você terá que escolher qual versão do `harry` instalar.
Se você quiser executar `philosophers-stone`, precisará primeiro instalar `harry` versão `1`, por exemplo com:
<div class="termy">
```console
$ pip install "harry==1"
```
</div>
E então você acabaria com `harry` versão `1` instalado em seu ambiente Python global.
```mermaid
flowchart LR
subgraph global[global env]
harry-1[harry v1]
end
subgraph stone-project[philosophers-stone project]
stone(philosophers-stone) -->|requires| harry-1
end
```
Mas se você quiser executar `prisoner-of-azkaban`, você precisará desinstalar `harry` versão `1` e instalar `harry` versão `3` (ou apenas instalar a versão `3` desinstalaria automaticamente a versão `1`).
<div class="termy">
```console
$ pip install "harry==3"
```
</div>
E então você acabaria com `harry` versão `3` instalado em seu ambiente Python global.
E se você tentar executar `philosophers-stone` novamente, há uma chance de que **não funcione** porque ele precisa de `harry` versão `1`.
```mermaid
flowchart LR
subgraph global[global env]
harry-1[<strike>harry v1</strike>]
style harry-1 fill:#ccc,stroke-dasharray: 5 5
harry-3[harry v3]
end
subgraph stone-project[philosophers-stone project]
stone(philosophers-stone) -.-x|⛔️| harry-1
end
subgraph azkaban-project[prisoner-of-azkaban project]
azkaban(prisoner-of-azkaban) --> |requires| harry-3
end
```
/// tip | "Dica"
É muito comum em pacotes Python tentar ao máximo **evitar alterações drásticas** em **novas versões**, mas é melhor prevenir do que remediar e instalar versões mais recentes intencionalmente e, quando possível, executar os testes para verificar se tudo está funcionando corretamente.
///
Agora, imagine isso com **muitos** outros **pacotes** dos quais todos os seus **projetos dependem**. Isso é muito difícil de gerenciar. E você provavelmente acabaria executando alguns projetos com algumas **versões incompatíveis** dos pacotes, e não saberia por que algo não está funcionando.
Além disso, dependendo do seu sistema operacional (por exemplo, Linux, Windows, macOS), ele pode ter vindo com o Python já instalado. E, nesse caso, provavelmente tinha alguns pacotes pré-instalados com algumas versões específicas **necessárias para o seu sistema**. Se você instalar pacotes no ambiente global do Python, poderá acabar **quebrando** alguns dos programas que vieram com seu sistema operacional.
## Onde os pacotes são instalados
Quando você instala o Python, ele cria alguns diretórios com alguns arquivos no seu computador.
Alguns desses diretórios são os responsáveis ​​por ter todos os pacotes que você instala.
Quando você executa:
<div class="termy">
```console
// Não execute isso agora, é apenas um exemplo 🤓
$ pip install "fastapi[standard]"
---> 100%
```
</div>
Isso fará o download de um arquivo compactado com o código FastAPI, normalmente do <a href="https://pypi.org/project/fastapi/" class="external-link" target="_blank">PyPI</a>.
Ele também fará o **download** de arquivos para outros pacotes dos quais o FastAPI depende.
Em seguida, ele **extrairá** todos esses arquivos e os colocará em um diretório no seu computador.
Por padrão, ele colocará os arquivos baixados e extraídos no diretório que vem com a instalação do Python, que é o **ambiente global**.
## O que são ambientes virtuais
A solução para os problemas de ter todos os pacotes no ambiente global é usar um **ambiente virtual para cada projeto** em que você trabalha.
Um ambiente virtual é um **diretório**, muito semelhante ao global, onde você pode instalar os pacotes para um projeto.
Dessa forma, cada projeto terá seu próprio ambiente virtual (diretório `.venv`) com seus próprios pacotes.
```mermaid
flowchart TB
subgraph stone-project[philosophers-stone project]
stone(philosophers-stone) --->|requires| harry-1
subgraph venv1[.venv]
harry-1[harry v1]
end
end
subgraph azkaban-project[prisoner-of-azkaban project]
azkaban(prisoner-of-azkaban) --->|requires| harry-3
subgraph venv2[.venv]
harry-3[harry v3]
end
end
stone-project ~~~ azkaban-project
```
## O que significa ativar um ambiente virtual
Quando você ativa um ambiente virtual, por exemplo com:
//// tab | Linux, macOS
<div class="termy">
```console
$ source .venv/bin/activate
```
</div>
////
//// tab | Windows PowerShell
<div class="termy">
```console
$ .venv\Scripts\Activate.ps1
```
</div>
////
//// tab | Windows Bash
Ou se você usa o Bash para Windows (por exemplo, <a href="https://gitforwindows.org/" class="external-link" target="_blank">Git Bash</a>):
<div class="termy">
```console
$ source .venv/Scripts/activate
```
</div>
////
Esse comando criará ou modificará algumas [variáveis ​​de ambiente](environment-variables.md){.internal-link target=_blank} que estarão disponíveis para os próximos comandos.
Uma dessas variáveis ​​é a variável `PATH`.
/// tip | "Dica"
Você pode aprender mais sobre a variável de ambiente `PATH` na seção [Variáveis ​​de ambiente](environment-variables.md#path-environment-variable){.internal-link target=_blank}.
///
A ativação de um ambiente virtual adiciona seu caminho `.venv/bin` (no Linux e macOS) ou `.venv\Scripts` (no Windows) à variável de ambiente `PATH`.
Digamos que antes de ativar o ambiente, a variável `PATH` estava assim:
//// tab | Linux, macOS
```plaintext
/usr/bin:/bin:/usr/sbin:/sbin
```
Isso significa que o sistema procuraria programas em:
* `/usr/bin`
* `/bin`
* `/usr/sbin`
* `/sbin`
////
//// tab | Windows
```plaintext
C:\Windows\System32
```
Isso significa que o sistema procuraria programas em:
* `C:\Windows\System32`
////
Após ativar o ambiente virtual, a variável `PATH` ficaria mais ou menos assim:
//// tab | Linux, macOS
```plaintext
/home/user/code/awesome-project/.venv/bin:/usr/bin:/bin:/usr/sbin:/sbin
```
Isso significa que o sistema agora começará a procurar primeiro por programas em:
```plaintext
/home/user/code/awesome-project/.venv/bin
```
antes de procurar nos outros diretórios.
Então, quando você digita `python` no terminal, o sistema encontrará o programa Python em
```plaintext
/home/user/code/awesome-project/.venv/bin/python
```
e usa esse.
////
//// tab | Windows
```plaintext
C:\Users\user\code\awesome-project\.venv\Scripts;C:\Windows\System32
```
Isso significa que o sistema agora começará a procurar primeiro por programas em:
```plaintext
C:\Users\user\code\awesome-project\.venv\Scripts
```
antes de procurar nos outros diretórios.
Então, quando você digita `python` no terminal, o sistema encontrará o programa Python em
```plaintext
C:\Users\user\code\awesome-project\.venv\Scripts\python
```
e usa esse.
////
Um detalhe importante é que ele colocará o caminho do ambiente virtual no **início** da variável `PATH`. O sistema o encontrará **antes** de encontrar qualquer outro Python disponível. Dessa forma, quando você executar `python`, ele usará o Python **do ambiente virtual** em vez de qualquer outro `python` (por exemplo, um `python` de um ambiente global).
Ativar um ambiente virtual também muda algumas outras coisas, mas esta é uma das mais importantes.
## Verificando um ambiente virtual
Ao verificar se um ambiente virtual está ativo, por exemplo com:
//// tab | Linux, macOS, Windows Bash
<div class="termy">
```console
$ which python
/home/user/code/awesome-project/.venv/bin/python
```
</div>
////
//// tab | Windows PowerShell
<div class="termy">
```console
$ Get-Command python
C:\Users\user\code\awesome-project\.venv\Scripts\python
```
</div>
////
Isso significa que o programa `python` que será usado é aquele **no ambiente virtual**.
você usa `which` no Linux e macOS e `Get-Command` no Windows PowerShell.
A maneira como esse comando funciona é que ele vai e verifica na variável de ambiente `PATH`, passando por **cada caminho em ordem**, procurando pelo programa chamado `python`. Uma vez que ele o encontre, ele **mostrará o caminho** para esse programa.
A parte mais importante é que quando você chama ``python`, esse é exatamente o "`python`" que será executado.
Assim, você pode confirmar se está no ambiente virtual correto.
/// tip | "Dica"
É fácil ativar um ambiente virtual, obter um Python e então **ir para outro projeto**.
E o segundo projeto **não funcionaria** porque você está usando o **Python incorreto**, de um ambiente virtual para outro projeto.
É útil poder verificar qual `python` está sendo usado. 🤓
///
## Por que desativar um ambiente virtual
Por exemplo, você pode estar trabalhando em um projeto `philosophers-stone`, **ativar esse ambiente virtual**, instalar pacotes e trabalhar com esse ambiente.
E então você quer trabalhar em **outro projeto** `prisoner-of-azkaban`.
Você vai para aquele projeto:
<div class="termy">
```console
$ cd ~/code/prisoner-of-azkaban
```
</div>
Se você não desativar o ambiente virtual para `philosophers-stone`, quando você executar `python` no terminal, ele tentará usar o Python de `philosophers-stone`.
<div class="termy">
```console
$ cd ~/code/prisoner-of-azkaban
$ python main.py
// Erro ao importar o Sirius, ele não está instalado 😱
Traceback (most recent call last):
File "main.py", line 1, in <module>
import sirius
```
</div>
Mas se você desativar o ambiente virtual e ativar o novo para `prisoner-of-askaban`, quando você executar `python`, ele usará o Python do ambiente virtual em `prisoner-of-azkaban`.
<div class="termy">
```console
$ cd ~/code/prisoner-of-azkaban
// Você não precisa estar no diretório antigo para desativar, você pode fazer isso de onde estiver, mesmo depois de ir para o outro projeto 😎
$ deactivate
// Ative o ambiente virtual em prisoner-of-azkaban/.venv 🚀
$ source .venv/bin/activate
// Agora, quando você executar o python, ele encontrará o pacote sirius instalado neste ambiente virtual ✨
$ python main.py
Eu juro solenemente 🐺
```
</div>
## Alternativas
Este é um guia simples para você começar e lhe ensinar como tudo funciona **por baixo**.
Existem muitas **alternativas** para gerenciar ambientes virtuais, dependências de pacotes (requisitos) e projetos.
Quando estiver pronto e quiser usar uma ferramenta para **gerenciar todo o projeto**, dependências de pacotes, ambientes virtuais, etc., sugiro que você experimente o <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">uv</a>.
`uv` pode fazer muitas coisas, ele pode:
* **Instalar o Python** para você, incluindo versões diferentes
* Gerenciar o **ambiente virtual** para seus projetos
* Instalar **pacotes**
* Gerenciar **dependências e versões** de pacotes para seu projeto
* Certifique-se de ter um conjunto **exato** de pacotes e versões para instalar, incluindo suas dependências, para que você possa ter certeza de que pode executar seu projeto em produção exatamente da mesma forma que em seu computador durante o desenvolvimento, isso é chamado de **bloqueio**
* E muitas outras coisas
## Conclusão
Se você leu e entendeu tudo isso, agora **você sabe muito mais** sobre ambientes virtuais do que muitos desenvolvedores por aí. 🤓
Saber esses detalhes provavelmente será útil no futuro, quando você estiver depurando algo que parece complexo, mas você saberá **como tudo funciona**. 😎

112
docs/zh/docs/project-generation.md

@ -1,84 +1,28 @@
# 项目生成 - 模板 # FastAPI全栈模板
项目生成器一般都会提供很多初始设置、安全措施、数据库,甚至还准备好了第一个 API 端点,能帮助您快速上手。 模板通常带有特定的设置,而且被设计为灵活和可定制的。这允许您根据项目的需求修改和调整它们,使它们成为一个很好的起点。🏁
项目生成器的设置通常都很主观,您可以按需更新或修改,但对于您的项目来说,它是非常好的起点。 您可以使用此模板开始,因为它包含了许多已经为您完成的初始设置、安全性、数据库和一些API端点。
## 全栈 FastAPI + PostgreSQL 代码仓: <a href="https://github.com/fastapi/full-stack-fastapi-template" class="external-link" target="_blank">Full Stack FastAPI Template</a>
GitHub:<a href="https://github.com/tiangolo/full-stack-fastapi-postgresql" class="external-link" target="_blank">https://github.com/tiangolo/full-stack-fastapi-postgresql</a> ## FastAPI全栈模板 - 技术栈和特性
### 全栈 FastAPI + PostgreSQL - 功能 - ⚡ [**FastAPI**](https://fastapi.tiangolo.com) 用于Python后端API.
- 🧰 [SQLModel](https://sqlmodel.tiangolo.com) 用于Python和SQL数据库的集成(ORM)。
* 完整的 **Docker** 集成(基于 Docker) - 🔍 [Pydantic](https://docs.pydantic.dev) FastAPI的依赖项之一,用于数据验证和配置管理。
* Docker Swarm 开发模式 - 💾 [PostgreSQL](https://www.postgresql.org) 作为SQL数据库。
* **Docker Compose** 本地开发集成与优化 - 🚀 [React](https://react.dev) 用于前端。
* **生产可用**的 Python 网络服务器,使用 Uvicorn 或 Gunicorn - 💃 使用了TypeScript、hooks、Vite和其他一些现代化的前端技术栈。
* Python <a href="https://github.com/fastapi/fastapi" class="external-link" target="_blank">**FastAPI**</a> 后端: - 🎨 [Chakra UI](https://chakra-ui.com) 用于前端组件。
* * **速度快**:可与 **NodeJS****Go** 比肩的极高性能(归功于 Starlette 和 Pydantic) - 🤖 一个自动化生成的前端客户端。
* **直观**:强大的编辑器支持,处处皆可<abbr title="也叫自动完成、智能感知">自动补全</abbr>,减少调试时间 - 🧪 Playwright用于端到端测试。
* **简单**:易学、易用,阅读文档所需时间更短 - 🦇 支持暗黑主题(Dark mode)。
* **简短**:代码重复最小化,每次参数声明都可以实现多个功能 - 🐋 [Docker Compose](https://www.docker.com) 用于开发环境和生产环境。
* **健壮**: 生产级别的代码,还有自动交互文档 - 🔒 默认使用密码哈希来保证安全。
* **基于标准**:完全兼容并基于 API 开放标准:<a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a><a href="https://json-schema.org/" class="external-link" target="_blank">JSON Schema</a> - 🔑 JWT令牌用于权限验证。
* <a href="https://fastapi.tiangolo.com/features/" class="external-link" target="_blank">**更多功能**</a>包括自动验证、序列化、交互文档、OAuth2 JWT 令牌身份验证等 - 📫 使用邮箱来进行密码恢复。
* **安全密码**,默认使用密码哈希 - ✅ 单元测试用了[Pytest](https://pytest.org).
* **JWT 令牌**身份验证 - 📞 [Traefik](https://traefik.io) 用于反向代理和负载均衡。
* **SQLAlchemy** 模型(独立于 Flask 扩展,可直接用于 Celery Worker) - 🚢 部署指南(Docker Compose)包含了如何起一个Traefik前端代理来自动化HTTPS认证。
* 基础的用户模型(可按需修改或删除) - 🏭 CI(持续集成)和 CD(持续部署)基于GitHub Actions。
* **Alembic** 迁移
* **CORS**(跨域资源共享)
* **Celery** Worker 可从后端其它部分有选择地导入并使用模型和代码
* REST 后端测试基于 Pytest,并与 Docker 集成,可独立于数据库实现完整的 API 交互测试。因为是在 Docker 中运行,每次都可从头构建新的数据存储(使用 ElasticSearch、MongoDB、CouchDB 等数据库,仅测试 API 运行)
* Python 与 **Jupyter Kernels** 集成,用于远程或 Docker 容器内部开发,使用 Atom Hydrogen 或 Visual Studio Code 的 Jupyter 插件
* **Vue** 前端:
* 由 Vue CLI 生成
* **JWT 身份验证**处理
* 登录视图
* 登录后显示主仪表盘视图
* 主仪表盘支持用户创建与编辑
* 用户信息编辑
* **Vuex**
* **Vue-router**
* **Vuetify** 美化组件
* **TypeScript**
* 基于 **Nginx** 的 Docker 服务器(优化了 Vue-router 配置)
* Docker 多阶段构建,无需保存或提交编译的代码
* 在构建时运行前端测试(可禁用)
* 尽量模块化,开箱即用,但仍可使用 Vue CLI 重新生成或创建所需项目,或复用所需内容
* 使用 **PGAdmin** 管理 PostgreSQL 数据库,可轻松替换为 PHPMyAdmin 或 MySQL
* 使用 **Flower** 监控 Celery 任务
* 使用 **Traefik** 处理前后端负载平衡,可把前后端放在同一个域下,按路径分隔,但在不同容器中提供服务
* Traefik 集成,包括自动生成 Let's Encrypt **HTTPS** 凭证
* GitLab **CI**(持续集成),包括前后端测试
## 全栈 FastAPI + Couchbase
GitHub:<a href="https://github.com/tiangolo/full-stack-fastapi-couchbase" class="external-link" target="_blank">https://github.com/tiangolo/full-stack-fastapi-couchbase</a>
⚠️ **警告** ⚠️
如果您想从头开始创建新项目,建议使用以下备选方案。
例如,项目生成器<a href="https://github.com/tiangolo/full-stack-fastapi-postgresql" class="external-link" target="_blank">全栈 FastAPI + PostgreSQL </a>会更适用,这个项目的维护积极,用的人也多,还包括了所有新功能和改进内容。
当然,您也可以放心使用这个基于 Couchbase 的生成器,它也能正常使用。就算用它生成项目也没有任何问题(为了更好地满足需求,您可以自行更新这个项目)。
详见资源仓库中的文档。
## 全栈 FastAPI + MongoDB
……敬请期待,得看我有没有时间做这个项目。😅 🎉
## FastAPI + spaCy 机器学习模型
GitHub:<a href="https://github.com/microsoft/cookiecutter-spacy-fastapi" class="external-link" target="_blank">https://github.com/microsoft/cookiecutter-spacy-fastapi</a>
### FastAPI + spaCy 机器学习模型 - 功能
* 集成 **spaCy** NER 模型
* 内置 **Azure 认知搜索**请求格式
* **生产可用**的 Python 网络服务器,使用 Uvicorn 与 Gunicorn
* 内置 **Azure DevOps** Kubernetes (AKS) CI/CD 开发
* **多语**支持,可在项目设置时选择 spaCy 内置的语言
* 不仅局限于 spaCy,可**轻松扩展**至其它模型框架(Pytorch、TensorFlow)

14
docs_src/request_form_models/tutorial001.py

@ -0,0 +1,14 @@
from fastapi import FastAPI, Form
from pydantic import BaseModel
app = FastAPI()
class FormData(BaseModel):
username: str
password: str
@app.post("/login/")
async def login(data: FormData = Form()):
return data

15
docs_src/request_form_models/tutorial001_an.py

@ -0,0 +1,15 @@
from fastapi import FastAPI, Form
from pydantic import BaseModel
from typing_extensions import Annotated
app = FastAPI()
class FormData(BaseModel):
username: str
password: str
@app.post("/login/")
async def login(data: Annotated[FormData, Form()]):
return data

16
docs_src/request_form_models/tutorial001_an_py39.py

@ -0,0 +1,16 @@
from typing import Annotated
from fastapi import FastAPI, Form
from pydantic import BaseModel
app = FastAPI()
class FormData(BaseModel):
username: str
password: str
@app.post("/login/")
async def login(data: Annotated[FormData, Form()]):
return data

15
docs_src/request_form_models/tutorial002.py

@ -0,0 +1,15 @@
from fastapi import FastAPI, Form
from pydantic import BaseModel
app = FastAPI()
class FormData(BaseModel):
username: str
password: str
model_config = {"extra": "forbid"}
@app.post("/login/")
async def login(data: FormData = Form()):
return data

16
docs_src/request_form_models/tutorial002_an.py

@ -0,0 +1,16 @@
from fastapi import FastAPI, Form
from pydantic import BaseModel
from typing_extensions import Annotated
app = FastAPI()
class FormData(BaseModel):
username: str
password: str
model_config = {"extra": "forbid"}
@app.post("/login/")
async def login(data: Annotated[FormData, Form()]):
return data

17
docs_src/request_form_models/tutorial002_an_py39.py

@ -0,0 +1,17 @@
from typing import Annotated
from fastapi import FastAPI, Form
from pydantic import BaseModel
app = FastAPI()
class FormData(BaseModel):
username: str
password: str
model_config = {"extra": "forbid"}
@app.post("/login/")
async def login(data: Annotated[FormData, Form()]):
return data

17
docs_src/request_form_models/tutorial002_pv1.py

@ -0,0 +1,17 @@
from fastapi import FastAPI, Form
from pydantic import BaseModel
app = FastAPI()
class FormData(BaseModel):
username: str
password: str
class Config:
extra = "forbid"
@app.post("/login/")
async def login(data: FormData = Form()):
return data

18
docs_src/request_form_models/tutorial002_pv1_an.py

@ -0,0 +1,18 @@
from fastapi import FastAPI, Form
from pydantic import BaseModel
from typing_extensions import Annotated
app = FastAPI()
class FormData(BaseModel):
username: str
password: str
class Config:
extra = "forbid"
@app.post("/login/")
async def login(data: Annotated[FormData, Form()]):
return data

19
docs_src/request_form_models/tutorial002_pv1_an_py39.py

@ -0,0 +1,19 @@
from typing import Annotated
from fastapi import FastAPI, Form
from pydantic import BaseModel
app = FastAPI()
class FormData(BaseModel):
username: str
password: str
class Config:
extra = "forbid"
@app.post("/login/")
async def login(data: Annotated[FormData, Form()]):
return data

2
fastapi/__init__.py

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

21
fastapi/_compat.py

@ -2,6 +2,7 @@ from collections import deque
from copy import copy from copy import copy
from dataclasses import dataclass, is_dataclass from dataclasses import dataclass, is_dataclass
from enum import Enum from enum import Enum
from functools import lru_cache
from typing import ( from typing import (
Any, Any,
Callable, Callable,
@ -279,6 +280,12 @@ if PYDANTIC_V2:
BodyModel: Type[BaseModel] = create_model(model_name, **field_params) # type: ignore[call-overload] BodyModel: Type[BaseModel] = create_model(model_name, **field_params) # type: ignore[call-overload]
return BodyModel return BodyModel
def get_model_fields(model: Type[BaseModel]) -> List[ModelField]:
return [
ModelField(field_info=field_info, name=name)
for name, field_info in model.model_fields.items()
]
else: else:
from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX
from pydantic import AnyUrl as Url # noqa: F401 from pydantic import AnyUrl as Url # noqa: F401
@ -513,6 +520,9 @@ else:
BodyModel.__fields__[f.name] = f # type: ignore[index] BodyModel.__fields__[f.name] = f # type: ignore[index]
return BodyModel return BodyModel
def get_model_fields(model: Type[BaseModel]) -> List[ModelField]:
return list(model.__fields__.values()) # type: ignore[attr-defined]
def _regenerate_error_with_loc( def _regenerate_error_with_loc(
*, errors: Sequence[Any], loc_prefix: Tuple[Union[str, int], ...] *, errors: Sequence[Any], loc_prefix: Tuple[Union[str, int], ...]
@ -532,6 +542,12 @@ def _annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool:
def field_annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool: def field_annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool:
origin = get_origin(annotation)
if origin is Union or origin is UnionType:
for arg in get_args(annotation):
if field_annotation_is_sequence(arg):
return True
return False
return _annotation_is_sequence(annotation) or _annotation_is_sequence( return _annotation_is_sequence(annotation) or _annotation_is_sequence(
get_origin(annotation) get_origin(annotation)
) )
@ -634,3 +650,8 @@ def is_uploadfile_sequence_annotation(annotation: Any) -> bool:
is_uploadfile_or_nonable_uploadfile_annotation(sub_annotation) is_uploadfile_or_nonable_uploadfile_annotation(sub_annotation)
for sub_annotation in get_args(annotation) for sub_annotation in get_args(annotation)
) )
@lru_cache
def get_cached_model_fields(model: Type[BaseModel]) -> List[ModelField]:
return get_model_fields(model)

314
fastapi/dependencies/utils.py

@ -32,6 +32,7 @@ from fastapi._compat import (
evaluate_forwardref, evaluate_forwardref,
field_annotation_is_scalar, field_annotation_is_scalar,
get_annotation_from_field_info, get_annotation_from_field_info,
get_cached_model_fields,
get_missing_field_error, get_missing_field_error,
is_bytes_field, is_bytes_field,
is_bytes_sequence_field, is_bytes_sequence_field,
@ -56,10 +57,17 @@ from fastapi.security.base import SecurityBase
from fastapi.security.oauth2 import OAuth2, SecurityScopes from fastapi.security.oauth2 import OAuth2, SecurityScopes
from fastapi.security.open_id_connect_url import OpenIdConnect from fastapi.security.open_id_connect_url import OpenIdConnect
from fastapi.utils import create_model_field, get_path_param_names from fastapi.utils import create_model_field, get_path_param_names
from pydantic import BaseModel
from pydantic.fields import FieldInfo from pydantic.fields import FieldInfo
from starlette.background import BackgroundTasks as StarletteBackgroundTasks from starlette.background import BackgroundTasks as StarletteBackgroundTasks
from starlette.concurrency import run_in_threadpool from starlette.concurrency import run_in_threadpool
from starlette.datastructures import FormData, Headers, QueryParams, UploadFile from starlette.datastructures import (
FormData,
Headers,
ImmutableMultiDict,
QueryParams,
UploadFile,
)
from starlette.requests import HTTPConnection, Request from starlette.requests import HTTPConnection, Request
from starlette.responses import Response from starlette.responses import Response
from starlette.websockets import WebSocket from starlette.websockets import WebSocket
@ -282,7 +290,7 @@ def get_dependant(
), f"Cannot specify multiple FastAPI annotations for {param_name!r}" ), f"Cannot specify multiple FastAPI annotations for {param_name!r}"
continue continue
assert param_details.field is not None assert param_details.field is not None
if is_body_param(param_field=param_details.field, is_path_param=is_path_param): if isinstance(param_details.field.field_info, params.Body):
dependant.body_params.append(param_details.field) dependant.body_params.append(param_details.field)
else: else:
add_param_to_fields(field=param_details.field, dependant=dependant) add_param_to_fields(field=param_details.field, dependant=dependant)
@ -466,29 +474,16 @@ def analyze_param(
required=field_info.default in (Required, Undefined), required=field_info.default in (Required, Undefined),
field_info=field_info, field_info=field_info,
) )
if is_path_param:
assert is_scalar_field(
field=field
), "Path params must be of one of the supported types"
elif isinstance(field_info, params.Query):
assert is_scalar_field(field) or is_scalar_sequence_field(field)
return ParamDetails(type_annotation=type_annotation, depends=depends, field=field) return ParamDetails(type_annotation=type_annotation, depends=depends, field=field)
def is_body_param(*, param_field: ModelField, is_path_param: bool) -> bool:
if is_path_param:
assert is_scalar_field(
field=param_field
), "Path params must be of one of the supported types"
return False
elif is_scalar_field(field=param_field):
return False
elif isinstance(
param_field.field_info, (params.Query, params.Header)
) and is_scalar_sequence_field(param_field):
return False
else:
assert isinstance(
param_field.field_info, params.Body
), f"Param: {param_field.name} can only be a request body, using Body()"
return True
def add_param_to_fields(*, field: ModelField, dependant: Dependant) -> None: def add_param_to_fields(*, field: ModelField, dependant: Dependant) -> None:
field_info = field.field_info field_info = field.field_info
field_info_in = getattr(field_info, "in_", None) field_info_in = getattr(field_info, "in_", None)
@ -557,6 +552,7 @@ async def solve_dependencies(
dependency_overrides_provider: Optional[Any] = None, dependency_overrides_provider: Optional[Any] = None,
dependency_cache: Optional[Dict[Tuple[Callable[..., Any], Tuple[str]], Any]] = None, dependency_cache: Optional[Dict[Tuple[Callable[..., Any], Tuple[str]], Any]] = None,
async_exit_stack: AsyncExitStack, async_exit_stack: AsyncExitStack,
embed_body_fields: bool,
) -> SolvedDependency: ) -> SolvedDependency:
values: Dict[str, Any] = {} values: Dict[str, Any] = {}
errors: List[Any] = [] errors: List[Any] = []
@ -598,6 +594,7 @@ async def solve_dependencies(
dependency_overrides_provider=dependency_overrides_provider, dependency_overrides_provider=dependency_overrides_provider,
dependency_cache=dependency_cache, dependency_cache=dependency_cache,
async_exit_stack=async_exit_stack, async_exit_stack=async_exit_stack,
embed_body_fields=embed_body_fields,
) )
background_tasks = solved_result.background_tasks background_tasks = solved_result.background_tasks
dependency_cache.update(solved_result.dependency_cache) dependency_cache.update(solved_result.dependency_cache)
@ -640,7 +637,9 @@ async def solve_dependencies(
body_values, body_values,
body_errors, body_errors,
) = await request_body_to_args( # body_params checked above ) = await request_body_to_args( # body_params checked above
required_params=dependant.body_params, received_body=body body_fields=dependant.body_params,
received_body=body,
embed_body_fields=embed_body_fields,
) )
values.update(body_values) values.update(body_values)
errors.extend(body_errors) errors.extend(body_errors)
@ -669,138 +668,197 @@ async def solve_dependencies(
) )
def _validate_value_with_model_field(
*, field: ModelField, value: Any, values: Dict[str, Any], loc: Tuple[str, ...]
) -> Tuple[Any, List[Any]]:
if value is None:
if field.required:
return None, [get_missing_field_error(loc=loc)]
else:
return deepcopy(field.default), []
v_, errors_ = field.validate(value, values, loc=loc)
if isinstance(errors_, ErrorWrapper):
return None, [errors_]
elif isinstance(errors_, list):
new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=())
return None, new_errors
else:
return v_, []
def _get_multidict_value(field: ModelField, values: Mapping[str, Any]) -> Any:
if is_sequence_field(field) and isinstance(values, (ImmutableMultiDict, Headers)):
value = values.getlist(field.alias)
else:
value = values.get(field.alias, None)
if (
value is None
or (
isinstance(field.field_info, params.Form)
and isinstance(value, str) # For type checks
and value == ""
)
or (is_sequence_field(field) and len(value) == 0)
):
if field.required:
return
else:
return deepcopy(field.default)
return value
def request_params_to_args( def request_params_to_args(
required_params: Sequence[ModelField], fields: Sequence[ModelField],
received_params: Union[Mapping[str, Any], QueryParams, Headers], received_params: Union[Mapping[str, Any], QueryParams, Headers],
) -> Tuple[Dict[str, Any], List[Any]]: ) -> Tuple[Dict[str, Any], List[Any]]:
values = {} values: Dict[str, Any] = {}
errors = [] errors = []
for field in required_params: for field in fields:
if is_scalar_sequence_field(field) and isinstance( value = _get_multidict_value(field, received_params)
received_params, (QueryParams, Headers)
):
value = received_params.getlist(field.alias) or field.default
else:
value = received_params.get(field.alias)
field_info = field.field_info field_info = field.field_info
assert isinstance( assert isinstance(
field_info, params.Param field_info, params.Param
), "Params must be subclasses of Param" ), "Params must be subclasses of Param"
loc = (field_info.in_.value, field.alias) loc = (field_info.in_.value, field.alias)
if value is None: v_, errors_ = _validate_value_with_model_field(
if field.required: field=field, value=value, values=values, loc=loc
errors.append(get_missing_field_error(loc=loc)) )
else: if errors_:
values[field.name] = deepcopy(field.default) errors.extend(errors_)
continue
v_, errors_ = field.validate(value, values, loc=loc)
if isinstance(errors_, ErrorWrapper):
errors.append(errors_)
elif isinstance(errors_, list):
new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=())
errors.extend(new_errors)
else: else:
values[field.name] = v_ values[field.name] = v_
return values, errors return values, errors
def _should_embed_body_fields(fields: List[ModelField]) -> bool:
if not fields:
return False
# More than one dependency could have the same field, it would show up as multiple
# fields but it's the same one, so count them by name
body_param_names_set = {field.name for field in fields}
# A top level field has to be a single field, not multiple
if len(body_param_names_set) > 1:
return True
first_field = fields[0]
# If it explicitly specifies it is embedded, it has to be embedded
if getattr(first_field.field_info, "embed", None):
return True
# If it's a Form (or File) field, it has to be a BaseModel to be top level
# otherwise it has to be embedded, so that the key value pair can be extracted
if isinstance(first_field.field_info, params.Form) and not lenient_issubclass(
first_field.type_, BaseModel
):
return True
return False
async def _extract_form_body(
body_fields: List[ModelField],
received_body: FormData,
) -> Dict[str, Any]:
values = {}
first_field = body_fields[0]
first_field_info = first_field.field_info
for field in body_fields:
value = _get_multidict_value(field, received_body)
if (
isinstance(first_field_info, params.File)
and is_bytes_field(field)
and isinstance(value, UploadFile)
):
value = await value.read()
elif (
is_bytes_sequence_field(field)
and isinstance(first_field_info, params.File)
and value_is_sequence(value)
):
# For types
assert isinstance(value, sequence_types) # type: ignore[arg-type]
results: List[Union[bytes, str]] = []
async def process_fn(
fn: Callable[[], Coroutine[Any, Any, Any]],
) -> None:
result = await fn()
results.append(result) # noqa: B023
async with anyio.create_task_group() as tg:
for sub_value in value:
tg.start_soon(process_fn, sub_value.read)
value = serialize_sequence_value(field=field, value=results)
if value is not None:
values[field.alias] = value
for key, value in received_body.items():
if key not in values:
values[key] = value
return values
async def request_body_to_args( async def request_body_to_args(
required_params: List[ModelField], body_fields: List[ModelField],
received_body: Optional[Union[Dict[str, Any], FormData]], received_body: Optional[Union[Dict[str, Any], FormData]],
embed_body_fields: bool,
) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]: ) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]:
values = {} values: Dict[str, Any] = {}
errors: List[Dict[str, Any]] = [] errors: List[Dict[str, Any]] = []
if required_params: assert body_fields, "request_body_to_args() should be called with fields"
field = required_params[0] single_not_embedded_field = len(body_fields) == 1 and not embed_body_fields
field_info = field.field_info first_field = body_fields[0]
embed = getattr(field_info, "embed", None) body_to_process = received_body
field_alias_omitted = len(required_params) == 1 and not embed
if field_alias_omitted: fields_to_extract: List[ModelField] = body_fields
received_body = {field.alias: received_body}
if single_not_embedded_field and lenient_issubclass(first_field.type_, BaseModel):
for field in required_params: fields_to_extract = get_cached_model_fields(first_field.type_)
loc: Tuple[str, ...]
if field_alias_omitted: if isinstance(received_body, FormData):
loc = ("body",) body_to_process = await _extract_form_body(fields_to_extract, received_body)
else:
loc = ("body", field.alias) if single_not_embedded_field:
loc: Tuple[str, ...] = ("body",)
value: Optional[Any] = None v_, errors_ = _validate_value_with_model_field(
if received_body is not None: field=first_field, value=body_to_process, values=values, loc=loc
if (is_sequence_field(field)) and isinstance(received_body, FormData): )
value = received_body.getlist(field.alias) return {first_field.name: v_}, errors_
else: for field in body_fields:
try: loc = ("body", field.alias)
value = received_body.get(field.alias) value: Optional[Any] = None
except AttributeError: if body_to_process is not None:
errors.append(get_missing_field_error(loc)) try:
continue value = body_to_process.get(field.alias)
if ( # If the received body is a list, not a dict
value is None except AttributeError:
or (isinstance(field_info, params.Form) and value == "") errors.append(get_missing_field_error(loc))
or (
isinstance(field_info, params.Form)
and is_sequence_field(field)
and len(value) == 0
)
):
if field.required:
errors.append(get_missing_field_error(loc))
else:
values[field.name] = deepcopy(field.default)
continue continue
if ( v_, errors_ = _validate_value_with_model_field(
isinstance(field_info, params.File) field=field, value=value, values=values, loc=loc
and is_bytes_field(field) )
and isinstance(value, UploadFile) if errors_:
): errors.extend(errors_)
value = await value.read() else:
elif ( values[field.name] = v_
is_bytes_sequence_field(field)
and isinstance(field_info, params.File)
and value_is_sequence(value)
):
# For types
assert isinstance(value, sequence_types) # type: ignore[arg-type]
results: List[Union[bytes, str]] = []
async def process_fn(
fn: Callable[[], Coroutine[Any, Any, Any]],
) -> None:
result = await fn()
results.append(result) # noqa: B023
async with anyio.create_task_group() as tg:
for sub_value in value:
tg.start_soon(process_fn, sub_value.read)
value = serialize_sequence_value(field=field, value=results)
v_, errors_ = field.validate(value, values, loc=loc)
if isinstance(errors_, list):
errors.extend(errors_)
elif errors_:
errors.append(errors_)
else:
values[field.name] = v_
return values, errors return values, errors
def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]: def get_body_field(
flat_dependant = get_flat_dependant(dependant) *, flat_dependant: Dependant, name: str, embed_body_fields: bool
) -> Optional[ModelField]:
"""
Get a ModelField representing the request body for a path operation, combining
all body parameters into a single field if necessary.
Used to check if it's form data (with `isinstance(body_field, params.Form)`)
or JSON and to generate the JSON Schema for a request body.
This is **not** used to validate/parse the request body, that's done with each
individual body parameter.
"""
if not flat_dependant.body_params: if not flat_dependant.body_params:
return None return None
first_param = flat_dependant.body_params[0] first_param = flat_dependant.body_params[0]
field_info = first_param.field_info if not embed_body_fields:
embed = getattr(field_info, "embed", None)
body_param_names_set = {param.name for param in flat_dependant.body_params}
if len(body_param_names_set) == 1 and not embed:
return first_param return first_param
# If one field requires to embed, all have to be embedded
# in case a sub-dependency is evaluated with a single unique body field
# That is combined (embedded) with other body fields
for param in flat_dependant.body_params:
setattr(param.field_info, "embed", True) # noqa: B010
model_name = "Body_" + name model_name = "Body_" + name
BodyModel = create_body_model( BodyModel = create_body_model(
fields=flat_dependant.body_params, model_name=model_name fields=flat_dependant.body_params, model_name=model_name

4
fastapi/param_functions.py

@ -1282,7 +1282,7 @@ def Body( # noqa: N802
), ),
] = _Unset, ] = _Unset,
embed: Annotated[ embed: Annotated[
bool, Union[bool, None],
Doc( Doc(
""" """
When `embed` is `True`, the parameter will be expected in a JSON body as a When `embed` is `True`, the parameter will be expected in a JSON body as a
@ -1294,7 +1294,7 @@ def Body( # noqa: N802
[FastAPI docs for Body - Multiple Parameters](https://fastapi.tiangolo.com/tutorial/body-multiple-params/#embed-a-single-body-parameter). [FastAPI docs for Body - Multiple Parameters](https://fastapi.tiangolo.com/tutorial/body-multiple-params/#embed-a-single-body-parameter).
""" """
), ),
] = False, ] = None,
media_type: Annotated[ media_type: Annotated[
str, str,
Doc( Doc(

5
fastapi/params.py

@ -479,7 +479,7 @@ class Body(FieldInfo):
*, *,
default_factory: Union[Callable[[], Any], None] = _Unset, default_factory: Union[Callable[[], Any], None] = _Unset,
annotation: Optional[Any] = None, annotation: Optional[Any] = None,
embed: bool = False, embed: Union[bool, None] = None,
media_type: str = "application/json", media_type: str = "application/json",
alias: Optional[str] = None, alias: Optional[str] = None,
alias_priority: Union[int, None] = _Unset, alias_priority: Union[int, None] = _Unset,
@ -556,7 +556,7 @@ class Body(FieldInfo):
kwargs["examples"] = examples kwargs["examples"] = examples
if regex is not None: if regex is not None:
warnings.warn( warnings.warn(
"`regex` has been depreacated, please use `pattern` instead", "`regex` has been deprecated, please use `pattern` instead",
category=DeprecationWarning, category=DeprecationWarning,
stacklevel=4, stacklevel=4,
) )
@ -642,7 +642,6 @@ class Form(Body):
default=default, default=default,
default_factory=default_factory, default_factory=default_factory,
annotation=annotation, annotation=annotation,
embed=True,
media_type=media_type, media_type=media_type,
alias=alias, alias=alias,
alias_priority=alias_priority, alias_priority=alias_priority,

26
fastapi/routing.py

@ -33,8 +33,10 @@ from fastapi._compat import (
from fastapi.datastructures import Default, DefaultPlaceholder from fastapi.datastructures import Default, DefaultPlaceholder
from fastapi.dependencies.models import Dependant from fastapi.dependencies.models import Dependant
from fastapi.dependencies.utils import ( from fastapi.dependencies.utils import (
_should_embed_body_fields,
get_body_field, get_body_field,
get_dependant, get_dependant,
get_flat_dependant,
get_parameterless_sub_dependant, get_parameterless_sub_dependant,
get_typed_return_annotation, get_typed_return_annotation,
solve_dependencies, solve_dependencies,
@ -225,6 +227,7 @@ def get_request_handler(
response_model_exclude_defaults: bool = False, response_model_exclude_defaults: bool = False,
response_model_exclude_none: bool = False, response_model_exclude_none: bool = False,
dependency_overrides_provider: Optional[Any] = None, dependency_overrides_provider: Optional[Any] = None,
embed_body_fields: bool = False,
) -> Callable[[Request], Coroutine[Any, Any, Response]]: ) -> Callable[[Request], Coroutine[Any, Any, Response]]:
assert dependant.call is not None, "dependant.call must be a function" assert dependant.call is not None, "dependant.call must be a function"
is_coroutine = asyncio.iscoroutinefunction(dependant.call) is_coroutine = asyncio.iscoroutinefunction(dependant.call)
@ -291,6 +294,7 @@ def get_request_handler(
body=body, body=body,
dependency_overrides_provider=dependency_overrides_provider, dependency_overrides_provider=dependency_overrides_provider,
async_exit_stack=async_exit_stack, async_exit_stack=async_exit_stack,
embed_body_fields=embed_body_fields,
) )
errors = solved_result.errors errors = solved_result.errors
if not errors: if not errors:
@ -354,7 +358,9 @@ def get_request_handler(
def get_websocket_app( def get_websocket_app(
dependant: Dependant, dependency_overrides_provider: Optional[Any] = None dependant: Dependant,
dependency_overrides_provider: Optional[Any] = None,
embed_body_fields: bool = False,
) -> Callable[[WebSocket], Coroutine[Any, Any, Any]]: ) -> Callable[[WebSocket], Coroutine[Any, Any, Any]]:
async def app(websocket: WebSocket) -> None: async def app(websocket: WebSocket) -> None:
async with AsyncExitStack() as async_exit_stack: async with AsyncExitStack() as async_exit_stack:
@ -367,6 +373,7 @@ def get_websocket_app(
dependant=dependant, dependant=dependant,
dependency_overrides_provider=dependency_overrides_provider, dependency_overrides_provider=dependency_overrides_provider,
async_exit_stack=async_exit_stack, async_exit_stack=async_exit_stack,
embed_body_fields=embed_body_fields,
) )
if solved_result.errors: if solved_result.errors:
raise WebSocketRequestValidationError( raise WebSocketRequestValidationError(
@ -399,11 +406,15 @@ class APIWebSocketRoute(routing.WebSocketRoute):
0, 0,
get_parameterless_sub_dependant(depends=depends, path=self.path_format), get_parameterless_sub_dependant(depends=depends, path=self.path_format),
) )
self._flat_dependant = get_flat_dependant(self.dependant)
self._embed_body_fields = _should_embed_body_fields(
self._flat_dependant.body_params
)
self.app = websocket_session( self.app = websocket_session(
get_websocket_app( get_websocket_app(
dependant=self.dependant, dependant=self.dependant,
dependency_overrides_provider=dependency_overrides_provider, dependency_overrides_provider=dependency_overrides_provider,
embed_body_fields=self._embed_body_fields,
) )
) )
@ -544,7 +555,15 @@ class APIRoute(routing.Route):
0, 0,
get_parameterless_sub_dependant(depends=depends, path=self.path_format), get_parameterless_sub_dependant(depends=depends, path=self.path_format),
) )
self.body_field = get_body_field(dependant=self.dependant, name=self.unique_id) self._flat_dependant = get_flat_dependant(self.dependant)
self._embed_body_fields = _should_embed_body_fields(
self._flat_dependant.body_params
)
self.body_field = get_body_field(
flat_dependant=self._flat_dependant,
name=self.unique_id,
embed_body_fields=self._embed_body_fields,
)
self.app = request_response(self.get_route_handler()) self.app = request_response(self.get_route_handler())
def get_route_handler(self) -> Callable[[Request], Coroutine[Any, Any, Response]]: def get_route_handler(self) -> Callable[[Request], Coroutine[Any, Any, Response]]:
@ -561,6 +580,7 @@ class APIRoute(routing.Route):
response_model_exclude_defaults=self.response_model_exclude_defaults, response_model_exclude_defaults=self.response_model_exclude_defaults,
response_model_exclude_none=self.response_model_exclude_none, response_model_exclude_none=self.response_model_exclude_none,
dependency_overrides_provider=self.dependency_overrides_provider, dependency_overrides_provider=self.dependency_overrides_provider,
embed_body_fields=self._embed_body_fields,
) )
def matches(self, scope: Scope) -> Tuple[Match, Scope]: def matches(self, scope: Scope) -> Tuple[Match, Scope]:

4
pyproject.toml

@ -241,3 +241,7 @@ known-third-party = ["fastapi", "pydantic", "starlette"]
[tool.ruff.lint.pyupgrade] [tool.ruff.lint.pyupgrade]
# Preserve types, even if a file imports `from __future__ import annotations`. # Preserve types, even if a file imports `from __future__ import annotations`.
keep-runtime-typing = true keep-runtime-typing = true
[tool.inline-snapshot]
# default-flags=["fix"]
# default-flags=["create"]

4
requirements-tests.txt

@ -3,7 +3,7 @@
pytest >=7.1.3,<8.0.0 pytest >=7.1.3,<8.0.0
coverage[toml] >= 6.5.0,< 8.0 coverage[toml] >= 6.5.0,< 8.0
mypy ==1.8.0 mypy ==1.8.0
ruff ==0.6.1 ruff ==0.6.4
dirty-equals ==0.6.0 dirty-equals ==0.6.0
# TODO: once removing databases from tutorial, upgrade SQLAlchemy # TODO: once removing databases from tutorial, upgrade SQLAlchemy
# probably when including SQLModel # probably when including SQLModel
@ -14,7 +14,7 @@ anyio[trio] >=3.2.1,<4.0.0
PyJWT==2.8.0 PyJWT==2.8.0
pyyaml >=5.3.1,<7.0.0 pyyaml >=5.3.1,<7.0.0
passlib[bcrypt] >=1.7.2,<2.0.0 passlib[bcrypt] >=1.7.2,<2.0.0
inline-snapshot==0.13.0
# types # types
types-ujson ==5.7.0.1 types-ujson ==5.7.0.1
types-orjson ==3.6.2 types-orjson ==3.6.2

38
scripts/playwright/request_form_models/image01.py

@ -0,0 +1,38 @@
import subprocess
import time
import httpx
from playwright.sync_api import Playwright, sync_playwright
# Run playwright codegen to generate the code below, copy paste the sections in run()
def run(playwright: Playwright) -> None:
browser = playwright.chromium.launch(headless=False)
# Update the viewport manually
context = browser.new_context(viewport={"width": 960, "height": 1080})
page = context.new_page()
page.goto("http://localhost:8000/docs")
page.get_by_role("button", name="POST /login/ Login").click()
page.get_by_role("button", name="Try it out").click()
# Manually add the screenshot
page.screenshot(path="docs/en/docs/img/tutorial/request-form-models/image01.png")
# ---------------------
context.close()
browser.close()
process = subprocess.Popen(
["fastapi", "run", "docs_src/request_form_models/tutorial001.py"]
)
try:
for _ in range(3):
try:
response = httpx.get("http://localhost:8000/docs")
except httpx.ConnectError:
time.sleep(1)
break
with sync_playwright() as playwright:
run(playwright)
finally:
process.terminate()

3
scripts/playwright/separate_openapi_schemas/image01.py

@ -3,13 +3,16 @@ import subprocess
from playwright.sync_api import Playwright, sync_playwright from playwright.sync_api import Playwright, sync_playwright
# Run playwright codegen to generate the code below, copy paste the sections in run()
def run(playwright: Playwright) -> None: def run(playwright: Playwright) -> None:
browser = playwright.chromium.launch(headless=False) browser = playwright.chromium.launch(headless=False)
# Update the viewport manually
context = browser.new_context(viewport={"width": 960, "height": 1080}) context = browser.new_context(viewport={"width": 960, "height": 1080})
page = context.new_page() page = context.new_page()
page.goto("http://localhost:8000/docs") page.goto("http://localhost:8000/docs")
page.get_by_text("POST/items/Create Item").click() page.get_by_text("POST/items/Create Item").click()
page.get_by_role("tab", name="Schema").first.click() page.get_by_role("tab", name="Schema").first.click()
# Manually add the screenshot
page.screenshot( page.screenshot(
path="docs/en/docs/img/tutorial/separate-openapi-schemas/image01.png" path="docs/en/docs/img/tutorial/separate-openapi-schemas/image01.png"
) )

3
scripts/playwright/separate_openapi_schemas/image02.py

@ -3,14 +3,17 @@ import subprocess
from playwright.sync_api import Playwright, sync_playwright from playwright.sync_api import Playwright, sync_playwright
# Run playwright codegen to generate the code below, copy paste the sections in run()
def run(playwright: Playwright) -> None: def run(playwright: Playwright) -> None:
browser = playwright.chromium.launch(headless=False) browser = playwright.chromium.launch(headless=False)
# Update the viewport manually
context = browser.new_context(viewport={"width": 960, "height": 1080}) context = browser.new_context(viewport={"width": 960, "height": 1080})
page = context.new_page() page = context.new_page()
page.goto("http://localhost:8000/docs") page.goto("http://localhost:8000/docs")
page.get_by_text("GET/items/Read Items").click() page.get_by_text("GET/items/Read Items").click()
page.get_by_role("button", name="Try it out").click() page.get_by_role("button", name="Try it out").click()
page.get_by_role("button", name="Execute").click() page.get_by_role("button", name="Execute").click()
# Manually add the screenshot
page.screenshot( page.screenshot(
path="docs/en/docs/img/tutorial/separate-openapi-schemas/image02.png" path="docs/en/docs/img/tutorial/separate-openapi-schemas/image02.png"
) )

3
scripts/playwright/separate_openapi_schemas/image03.py

@ -3,14 +3,17 @@ import subprocess
from playwright.sync_api import Playwright, sync_playwright from playwright.sync_api import Playwright, sync_playwright
# Run playwright codegen to generate the code below, copy paste the sections in run()
def run(playwright: Playwright) -> None: def run(playwright: Playwright) -> None:
browser = playwright.chromium.launch(headless=False) browser = playwright.chromium.launch(headless=False)
# Update the viewport manually
context = browser.new_context(viewport={"width": 960, "height": 1080}) context = browser.new_context(viewport={"width": 960, "height": 1080})
page = context.new_page() page = context.new_page()
page.goto("http://localhost:8000/docs") page.goto("http://localhost:8000/docs")
page.get_by_text("GET/items/Read Items").click() page.get_by_text("GET/items/Read Items").click()
page.get_by_role("tab", name="Schema").click() page.get_by_role("tab", name="Schema").click()
page.get_by_label("Schema").get_by_role("button", name="Expand all").click() page.get_by_label("Schema").get_by_role("button", name="Expand all").click()
# Manually add the screenshot
page.screenshot( page.screenshot(
path="docs/en/docs/img/tutorial/separate-openapi-schemas/image03.png" path="docs/en/docs/img/tutorial/separate-openapi-schemas/image03.png"
) )

3
scripts/playwright/separate_openapi_schemas/image04.py

@ -3,14 +3,17 @@ import subprocess
from playwright.sync_api import Playwright, sync_playwright from playwright.sync_api import Playwright, sync_playwright
# Run playwright codegen to generate the code below, copy paste the sections in run()
def run(playwright: Playwright) -> None: def run(playwright: Playwright) -> None:
browser = playwright.chromium.launch(headless=False) browser = playwright.chromium.launch(headless=False)
# Update the viewport manually
context = browser.new_context(viewport={"width": 960, "height": 1080}) context = browser.new_context(viewport={"width": 960, "height": 1080})
page = context.new_page() page = context.new_page()
page.goto("http://localhost:8000/docs") page.goto("http://localhost:8000/docs")
page.get_by_role("button", name="Item-Input").click() page.get_by_role("button", name="Item-Input").click()
page.get_by_role("button", name="Item-Output").click() page.get_by_role("button", name="Item-Output").click()
page.set_viewport_size({"width": 960, "height": 820}) page.set_viewport_size({"width": 960, "height": 820})
# Manually add the screenshot
page.screenshot( page.screenshot(
path="docs/en/docs/img/tutorial/separate-openapi-schemas/image04.png" path="docs/en/docs/img/tutorial/separate-openapi-schemas/image04.png"
) )

3
scripts/playwright/separate_openapi_schemas/image05.py

@ -3,13 +3,16 @@ import subprocess
from playwright.sync_api import Playwright, sync_playwright from playwright.sync_api import Playwright, sync_playwright
# Run playwright codegen to generate the code below, copy paste the sections in run()
def run(playwright: Playwright) -> None: def run(playwright: Playwright) -> None:
browser = playwright.chromium.launch(headless=False) browser = playwright.chromium.launch(headless=False)
# Update the viewport manually
context = browser.new_context(viewport={"width": 960, "height": 1080}) context = browser.new_context(viewport={"width": 960, "height": 1080})
page = context.new_page() page = context.new_page()
page.goto("http://localhost:8000/docs") page.goto("http://localhost:8000/docs")
page.get_by_role("button", name="Item", exact=True).click() page.get_by_role("button", name="Item", exact=True).click()
page.set_viewport_size({"width": 960, "height": 700}) page.set_viewport_size({"width": 960, "height": 700})
# Manually add the screenshot
page.screenshot( page.screenshot(
path="docs/en/docs/img/tutorial/separate-openapi-schemas/image05.png" path="docs/en/docs/img/tutorial/separate-openapi-schemas/image05.png"
) )

29
tests/test_compat.py

@ -1,11 +1,14 @@
from typing import List, Union from typing import Any, Dict, List, Union
from fastapi import FastAPI, UploadFile from fastapi import FastAPI, UploadFile
from fastapi._compat import ( from fastapi._compat import (
ModelField, ModelField,
Undefined, Undefined,
_get_model_config, _get_model_config,
get_cached_model_fields,
get_model_fields,
is_bytes_sequence_annotation, is_bytes_sequence_annotation,
is_scalar_field,
is_uploadfile_sequence_annotation, is_uploadfile_sequence_annotation,
) )
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
@ -91,3 +94,27 @@ def test_is_uploadfile_sequence_annotation():
# and other types, but I'm not even sure it's a good idea to support it as a first # and other types, but I'm not even sure it's a good idea to support it as a first
# class "feature" # class "feature"
assert is_uploadfile_sequence_annotation(Union[List[str], List[UploadFile]]) assert is_uploadfile_sequence_annotation(Union[List[str], List[UploadFile]])
def test_is_pv1_scalar_field():
# For coverage
class Model(BaseModel):
foo: Union[str, Dict[str, Any]]
fields = get_model_fields(Model)
assert not is_scalar_field(fields[0])
def test_get_model_fields_cached():
class Model(BaseModel):
foo: str
non_cached_fields = get_model_fields(Model)
non_cached_fields2 = get_model_fields(Model)
cached_fields = get_cached_model_fields(Model)
cached_fields2 = get_cached_model_fields(Model)
for f1, f2 in zip(cached_fields, cached_fields2):
assert f1 is f2
assert non_cached_fields is not non_cached_fields2
assert cached_fields is cached_fields2

133
tests/test_forms_single_model.py

@ -0,0 +1,133 @@
from typing import List, Optional
from dirty_equals import IsDict
from fastapi import FastAPI, Form
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import Annotated
app = FastAPI()
class FormModel(BaseModel):
username: str
lastname: str
age: Optional[int] = None
tags: List[str] = ["foo", "bar"]
alias_with: str = Field(alias="with", default="nothing")
@app.post("/form/")
def post_form(user: Annotated[FormModel, Form()]):
return user
client = TestClient(app)
def test_send_all_data():
response = client.post(
"/form/",
data={
"username": "Rick",
"lastname": "Sanchez",
"age": "70",
"tags": ["plumbus", "citadel"],
"with": "something",
},
)
assert response.status_code == 200, response.text
assert response.json() == {
"username": "Rick",
"lastname": "Sanchez",
"age": 70,
"tags": ["plumbus", "citadel"],
"with": "something",
}
def test_defaults():
response = client.post("/form/", data={"username": "Rick", "lastname": "Sanchez"})
assert response.status_code == 200, response.text
assert response.json() == {
"username": "Rick",
"lastname": "Sanchez",
"age": None,
"tags": ["foo", "bar"],
"with": "nothing",
}
def test_invalid_data():
response = client.post(
"/form/",
data={
"username": "Rick",
"lastname": "Sanchez",
"age": "seventy",
"tags": ["plumbus", "citadel"],
},
)
assert response.status_code == 422, response.text
assert response.json() == IsDict(
{
"detail": [
{
"type": "int_parsing",
"loc": ["body", "age"],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "seventy",
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "age"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
}
]
}
)
def test_no_data():
response = client.post("/form/")
assert response.status_code == 422, response.text
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "username"],
"msg": "Field required",
"input": {"tags": ["foo", "bar"], "with": "nothing"},
},
{
"type": "missing",
"loc": ["body", "lastname"],
"msg": "Field required",
"input": {"tags": ["foo", "bar"], "with": "nothing"},
},
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "username"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", "lastname"],
"msg": "field required",
"type": "value_error.missing",
},
]
}
)

99
tests/test_forms_single_param.py

@ -0,0 +1,99 @@
from fastapi import FastAPI, Form
from fastapi.testclient import TestClient
from typing_extensions import Annotated
app = FastAPI()
@app.post("/form/")
def post_form(username: Annotated[str, Form()]):
return username
client = TestClient(app)
def test_single_form_field():
response = client.post("/form/", data={"username": "Rick"})
assert response.status_code == 200, response.text
assert response.json() == "Rick"
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": {
"/form/": {
"post": {
"summary": "Post Form",
"operationId": "post_form_form__post",
"requestBody": {
"content": {
"application/x-www-form-urlencoded": {
"schema": {
"$ref": "#/components/schemas/Body_post_form_form__post"
}
}
},
"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": {
"Body_post_form_form__post": {
"properties": {"username": {"type": "string", "title": "Username"}},
"type": "object",
"required": ["username"],
"title": "Body_post_form_form__post",
},
"HTTPValidationError": {
"properties": {
"detail": {
"items": {"$ref": "#/components/schemas/ValidationError"},
"type": "array",
"title": "Detail",
}
},
"type": "object",
"title": "HTTPValidationError",
},
"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",
},
}
},
}

27
tests/test_openapi_examples.py

@ -155,13 +155,26 @@ def test_openapi_schema():
"requestBody": { "requestBody": {
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": IsDict(
"allOf": [{"$ref": "#/components/schemas/Item"}], {
"title": "Item", "$ref": "#/components/schemas/Item",
"examples": [ "examples": [
{"data": "Data in Body examples, example1"} {"data": "Data in Body examples, example1"}
], ],
}, }
)
| IsDict(
{
# TODO: remove when deprecating Pydantic v1
"allOf": [
{"$ref": "#/components/schemas/Item"}
],
"title": "Item",
"examples": [
{"data": "Data in Body examples, example1"}
],
}
),
"examples": { "examples": {
"Example One": { "Example One": {
"summary": "Example One Summary", "summary": "Example One Summary",

0
tests/test_tutorial/test_request_form_models/__init__.py

232
tests/test_tutorial/test_request_form_models/test_tutorial001.py

@ -0,0 +1,232 @@
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 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"},
}
},
},
}
},
}

232
tests/test_tutorial/test_request_form_models/test_tutorial001_an.py

@ -0,0 +1,232 @@
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"},
}
},
},
}
},
}

240
tests/test_tutorial/test_request_form_models/test_tutorial001_an_py39.py

@ -0,0 +1,240 @@
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"},
}
},
},
}
},
}

196
tests/test_tutorial/test_request_form_models/test_tutorial002.py

@ -0,0 +1,196 @@
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 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"},
}
},
},
}
},
}

196
tests/test_tutorial/test_request_form_models/test_tutorial002_an.py

@ -0,0 +1,196 @@
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"},
}
},
},
}
},
}

203
tests/test_tutorial/test_request_form_models/test_tutorial002_an_py39.py

@ -0,0 +1,203 @@
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"},
}
},
},
}
},
}

189
tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py

@ -0,0 +1,189 @@
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 import app
client = TestClient(app)
return client
@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"}
@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",
}
]
}
@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",
}
]
}
@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",
}
]
}
@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",
},
]
}
@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",
},
]
}
@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"},
}
},
},
}
},
}

196
tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an.py

@ -0,0 +1,196 @@
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"},
}
},
},
}
},
}

203
tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an_p39.py

@ -0,0 +1,203 @@
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"},
}
},
},
}
},
}
Loading…
Cancel
Save