Browse Source

Merge branch 'master' into master

pull/10589/head
Jan Vollmer 7 months ago
committed by GitHub
parent
commit
dc150ea1ac
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      .pre-commit-config.yaml
  2. BIN
      docs/en/docs/img/tutorial/cookie-param-models/image01.png
  3. BIN
      docs/en/docs/img/tutorial/header-param-models/image01.png
  4. BIN
      docs/en/docs/img/tutorial/query-param-models/image01.png
  5. 157
      docs/en/docs/release-notes.md
  6. 154
      docs/en/docs/tutorial/cookie-param-models.md
  7. 184
      docs/en/docs/tutorial/header-param-models.md
  8. 196
      docs/en/docs/tutorial/query-param-models.md
  9. 3
      docs/en/mkdocs.yml
  10. 597
      docs/nl/docs/python-types.md
  11. 192
      docs/pt/docs/advanced/security/http-basic-auth.md
  12. 134
      docs/pt/docs/tutorial/request-form-models.md
  13. 112
      docs/zh/docs/project-generation.md
  14. 17
      docs_src/cookie_param_models/tutorial001.py
  15. 18
      docs_src/cookie_param_models/tutorial001_an.py
  16. 17
      docs_src/cookie_param_models/tutorial001_an_py310.py
  17. 17
      docs_src/cookie_param_models/tutorial001_an_py39.py
  18. 15
      docs_src/cookie_param_models/tutorial001_py310.py
  19. 19
      docs_src/cookie_param_models/tutorial002.py
  20. 20
      docs_src/cookie_param_models/tutorial002_an.py
  21. 19
      docs_src/cookie_param_models/tutorial002_an_py310.py
  22. 19
      docs_src/cookie_param_models/tutorial002_an_py39.py
  23. 20
      docs_src/cookie_param_models/tutorial002_pv1.py
  24. 21
      docs_src/cookie_param_models/tutorial002_pv1_an.py
  25. 20
      docs_src/cookie_param_models/tutorial002_pv1_an_py310.py
  26. 20
      docs_src/cookie_param_models/tutorial002_pv1_an_py39.py
  27. 18
      docs_src/cookie_param_models/tutorial002_pv1_py310.py
  28. 17
      docs_src/cookie_param_models/tutorial002_py310.py
  29. 19
      docs_src/header_param_models/tutorial001.py
  30. 20
      docs_src/header_param_models/tutorial001_an.py
  31. 19
      docs_src/header_param_models/tutorial001_an_py310.py
  32. 19
      docs_src/header_param_models/tutorial001_an_py39.py
  33. 17
      docs_src/header_param_models/tutorial001_py310.py
  34. 19
      docs_src/header_param_models/tutorial001_py39.py
  35. 21
      docs_src/header_param_models/tutorial002.py
  36. 22
      docs_src/header_param_models/tutorial002_an.py
  37. 21
      docs_src/header_param_models/tutorial002_an_py310.py
  38. 21
      docs_src/header_param_models/tutorial002_an_py39.py
  39. 22
      docs_src/header_param_models/tutorial002_pv1.py
  40. 23
      docs_src/header_param_models/tutorial002_pv1_an.py
  41. 22
      docs_src/header_param_models/tutorial002_pv1_an_py310.py
  42. 22
      docs_src/header_param_models/tutorial002_pv1_an_py39.py
  43. 20
      docs_src/header_param_models/tutorial002_pv1_py310.py
  44. 22
      docs_src/header_param_models/tutorial002_pv1_py39.py
  45. 19
      docs_src/header_param_models/tutorial002_py310.py
  46. 21
      docs_src/header_param_models/tutorial002_py39.py
  47. 19
      docs_src/query_param_models/tutorial001.py
  48. 19
      docs_src/query_param_models/tutorial001_an.py
  49. 18
      docs_src/query_param_models/tutorial001_an_py310.py
  50. 17
      docs_src/query_param_models/tutorial001_an_py39.py
  51. 18
      docs_src/query_param_models/tutorial001_py310.py
  52. 17
      docs_src/query_param_models/tutorial001_py39.py
  53. 21
      docs_src/query_param_models/tutorial002.py
  54. 21
      docs_src/query_param_models/tutorial002_an.py
  55. 20
      docs_src/query_param_models/tutorial002_an_py310.py
  56. 19
      docs_src/query_param_models/tutorial002_an_py39.py
  57. 22
      docs_src/query_param_models/tutorial002_pv1.py
  58. 22
      docs_src/query_param_models/tutorial002_pv1_an.py
  59. 21
      docs_src/query_param_models/tutorial002_pv1_an_py310.py
  60. 20
      docs_src/query_param_models/tutorial002_pv1_an_py39.py
  61. 21
      docs_src/query_param_models/tutorial002_pv1_py310.py
  62. 20
      docs_src/query_param_models/tutorial002_pv1_py39.py
  63. 20
      docs_src/query_param_models/tutorial002_py310.py
  64. 19
      docs_src/query_param_models/tutorial002_py39.py
  65. 2
      fastapi/__init__.py
  66. 6
      fastapi/_compat.py
  67. 96
      fastapi/dependencies/utils.py
  68. 86
      fastapi/openapi/utils.py
  69. 4
      pyproject.toml
  70. 2
      requirements-tests.txt
  71. 39
      scripts/playwright/cookie_param_models/image01.py
  72. 38
      scripts/playwright/header_param_models/image01.py
  73. 41
      scripts/playwright/query_param_models/image01.py
  74. 4
      scripts/playwright/request_form_models/image01.py
  75. 3
      scripts/playwright/separate_openapi_schemas/image01.py
  76. 3
      scripts/playwright/separate_openapi_schemas/image02.py
  77. 3
      scripts/playwright/separate_openapi_schemas/image03.py
  78. 3
      scripts/playwright/separate_openapi_schemas/image04.py
  79. 3
      scripts/playwright/separate_openapi_schemas/image05.py
  80. 16
      tests/test_compat.py
  81. 10
      tests/test_forms_single_model.py
  82. 0
      tests/test_tutorial/test_cookie_param_models/__init__.py
  83. 205
      tests/test_tutorial/test_cookie_param_models/test_tutorial001.py
  84. 233
      tests/test_tutorial/test_cookie_param_models/test_tutorial002.py
  85. 0
      tests/test_tutorial/test_header_param_models/__init__.py
  86. 238
      tests/test_tutorial/test_header_param_models/test_tutorial001.py
  87. 249
      tests/test_tutorial/test_header_param_models/test_tutorial002.py
  88. 0
      tests/test_tutorial/test_query_param_models/__init__.py
  89. 260
      tests/test_tutorial/test_query_param_models/test_tutorial001.py
  90. 282
      tests/test_tutorial/test_query_param_models/test_tutorial002.py

2
.pre-commit-config.yaml

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

BIN
docs/en/docs/img/tutorial/cookie-param-models/image01.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
docs/en/docs/img/tutorial/header-param-models/image01.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

BIN
docs/en/docs/img/tutorial/query-param-models/image01.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

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

@ -7,6 +7,163 @@ hide:
## Latest Changes
## 0.115.0
### Highlights
Now you can declare `Query`, `Header`, and `Cookie` parameters with Pydantic models. 🎉
#### `Query` Parameter Models
Use Pydantic models for `Query` parameters:
```python
from typing import Annotated, Literal
from fastapi import FastAPI, Query
from pydantic import BaseModel, Field
app = FastAPI()
class FilterParams(BaseModel):
limit: int = Field(100, gt=0, le=100)
offset: int = Field(0, ge=0)
order_by: Literal["created_at", "updated_at"] = "created_at"
tags: list[str] = []
@app.get("/items/")
async def read_items(filter_query: Annotated[FilterParams, Query()]):
return filter_query
```
Read the new docs: [Query Parameter Models](https://fastapi.tiangolo.com/tutorial/query-param-models/).
#### `Header` Parameter Models
Use Pydantic models for `Header` parameters:
```python
from typing import Annotated
from fastapi import FastAPI, Header
from pydantic import BaseModel
app = FastAPI()
class CommonHeaders(BaseModel):
host: str
save_data: bool
if_modified_since: str | None = None
traceparent: str | None = None
x_tag: list[str] = []
@app.get("/items/")
async def read_items(headers: Annotated[CommonHeaders, Header()]):
return headers
```
Read the new docs: [Header Parameter Models](https://fastapi.tiangolo.com/tutorial/header-param-models/).
#### `Cookie` Parameter Models
Use Pydantic models for `Cookie` parameters:
```python
from typing import Annotated
from fastapi import Cookie, FastAPI
from pydantic import BaseModel
app = FastAPI()
class Cookies(BaseModel):
session_id: str
fatebook_tracker: str | None = None
googall_tracker: str | None = None
@app.get("/items/")
async def read_items(cookies: Annotated[Cookies, Cookie()]):
return cookies
```
Read the new docs: [Cookie Parameter Models](https://fastapi.tiangolo.com/tutorial/cookie-param-models/).
#### Forbid Extra Query (Cookie, Header) Parameters
Use Pydantic models to restrict extra values for `Query` parameters (also applies to `Header` and `Cookie` parameters).
To achieve it, use Pydantic's `model_config = {"extra": "forbid"}`:
```python
from typing import Annotated, Literal
from fastapi import FastAPI, Query
from pydantic import BaseModel, Field
app = FastAPI()
class FilterParams(BaseModel):
model_config = {"extra": "forbid"}
limit: int = Field(100, gt=0, le=100)
offset: int = Field(0, ge=0)
order_by: Literal["created_at", "updated_at"] = "created_at"
tags: list[str] = []
@app.get("/items/")
async def read_items(filter_query: Annotated[FilterParams, Query()]):
return filter_query
```
This applies to `Query`, `Header`, and `Cookie` parameters, read the new docs:
* [Forbid Extra Query Parameters](https://fastapi.tiangolo.com/tutorial/query-param-models/#forbid-extra-query-parameters)
* [Forbid Extra Headers](https://fastapi.tiangolo.com/tutorial/header-param-models/#forbid-extra-headers)
* [Forbid Extra Cookies](https://fastapi.tiangolo.com/tutorial/cookie-param-models/#forbid-extra-cookies)
### Features
* ✨ Add support for Pydantic models for parameters using `Query`, `Cookie`, `Header`. PR [#12199](https://github.com/fastapi/fastapi/pull/12199) by [@tiangolo](https://github.com/tiangolo).
### Translations
* 🌐 Add Portuguese translation for `docs/pt/docs/advanced/security/http-basic-auth.md`. PR [#12195](https://github.com/fastapi/fastapi/pull/12195) by [@ceb10n](https://github.com/ceb10n).
### Internal
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#12204](https://github.com/fastapi/fastapi/pull/12204) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
## 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).

154
docs/en/docs/tutorial/cookie-param-models.md

@ -0,0 +1,154 @@
# Cookie Parameter Models
If you have a group of **cookies** that are related, you can create a **Pydantic model** to declare them. 🍪
This would allow you to **re-use the model** in **multiple places** and also to declare validations and metadata for all the parameters at once. 😎
/// note
This is supported since FastAPI version `0.115.0`. 🤓
///
/// tip
This same technique applies to `Query`, `Cookie`, and `Header`. 😎
///
## Cookies with a Pydantic Model
Declare the **cookie** parameters that you need in a **Pydantic model**, and then declare the parameter as `Cookie`:
//// tab | Python 3.10+
```Python hl_lines="9-12 16"
{!> ../../../docs_src/cookie_param_models/tutorial001_an_py310.py!}
```
////
//// tab | Python 3.9+
```Python hl_lines="9-12 16"
{!> ../../../docs_src/cookie_param_models/tutorial001_an_py39.py!}
```
////
//// tab | Python 3.8+
```Python hl_lines="10-13 17"
{!> ../../../docs_src/cookie_param_models/tutorial001_an.py!}
```
////
//// tab | Python 3.10+ non-Annotated
/// tip
Prefer to use the `Annotated` version if possible.
///
```Python hl_lines="7-10 14"
{!> ../../../docs_src/cookie_param_models/tutorial001_py310.py!}
```
////
//// tab | Python 3.8+ non-Annotated
/// tip
Prefer to use the `Annotated` version if possible.
///
```Python hl_lines="9-12 16"
{!> ../../../docs_src/cookie_param_models/tutorial001.py!}
```
////
**FastAPI** will **extract** the data for **each field** from the **cookies** received in the request and give you the Pydantic model you defined.
## Check the Docs
You can see the defined cookies in the docs UI at `/docs`:
<div class="screenshot">
<img src="/img/tutorial/cookie-param-models/image01.png">
</div>
/// info
Have in mind that, as **browsers handle cookies** in special ways and behind the scenes, they **don't** easily allow **JavaScript** to touch them.
If you go to the **API docs UI** at `/docs` you will be able to see the **documentation** for cookies for your *path operations*.
But even if you **fill the data** and click "Execute", because the docs UI works with **JavaScript**, the cookies won't be sent, and you will see an **error** message as if you didn't write any values.
///
## Forbid Extra Cookies
In some special use cases (probably not very common), you might want to **restrict** the cookies that you want to receive.
Your API now has the power to control its own <abbr title="This is a joke, just in case. It has nothing to do with cookie consents, but it's funny that even the API can now reject the poor cookies. Have a cookie. 🍪">cookie consent</abbr>. 🤪🍪
You can use Pydantic's model configuration to `forbid` any `extra` fields:
//// tab | Python 3.9+
```Python hl_lines="10"
{!> ../../../docs_src/cookie_param_models/tutorial002_an_py39.py!}
```
////
//// tab | Python 3.8+
```Python hl_lines="11"
{!> ../../../docs_src/cookie_param_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/cookie_param_models/tutorial002.py!}
```
////
If a client tries to send some **extra cookies**, they will receive an **error** response.
Poor cookie banners with all their effort to get your consent for the <abbr title="This is another joke. Don't pay attention to me. Have some coffee for your cookie. ☕">API to reject it</abbr>. 🍪
For example, if the client tries to send a `santa_tracker` cookie with a value of `good-list-please`, the client will receive an **error** response telling them that the `santa_tracker` <abbr title="Santa disapproves the lack of cookies. 🎅 Okay, no more cookie jokes.">cookie is not allowed</abbr>:
```json
{
"detail": [
{
"type": "extra_forbidden",
"loc": ["cookie", "santa_tracker"],
"msg": "Extra inputs are not permitted",
"input": "good-list-please",
}
]
}
```
## Summary
You can use **Pydantic models** to declare <abbr title="Have a last cookie before you go. 🍪">**cookies**</abbr> in **FastAPI**. 😎

184
docs/en/docs/tutorial/header-param-models.md

@ -0,0 +1,184 @@
# Header Parameter Models
If you have a group of related **header parameters**, you can create a **Pydantic model** to declare them.
This would allow you to **re-use the model** in **multiple places** and also to declare validations and metadata for all the parameters at once. 😎
/// note
This is supported since FastAPI version `0.115.0`. 🤓
///
## Header Parameters with a Pydantic Model
Declare the **header parameters** that you need in a **Pydantic model**, and then declare the parameter as `Header`:
//// tab | Python 3.10+
```Python hl_lines="9-14 18"
{!> ../../../docs_src/header_param_models/tutorial001_an_py310.py!}
```
////
//// tab | Python 3.9+
```Python hl_lines="9-14 18"
{!> ../../../docs_src/header_param_models/tutorial001_an_py39.py!}
```
////
//// tab | Python 3.8+
```Python hl_lines="10-15 19"
{!> ../../../docs_src/header_param_models/tutorial001_an.py!}
```
////
//// tab | Python 3.10+ non-Annotated
/// tip
Prefer to use the `Annotated` version if possible.
///
```Python hl_lines="7-12 16"
{!> ../../../docs_src/header_param_models/tutorial001_py310.py!}
```
////
//// tab | Python 3.9+ non-Annotated
/// tip
Prefer to use the `Annotated` version if possible.
///
```Python hl_lines="9-14 18"
{!> ../../../docs_src/header_param_models/tutorial001_py39.py!}
```
////
//// tab | Python 3.8+ non-Annotated
/// tip
Prefer to use the `Annotated` version if possible.
///
```Python hl_lines="7-12 16"
{!> ../../../docs_src/header_param_models/tutorial001_py310.py!}
```
////
**FastAPI** will **extract** the data for **each field** from the **headers** in the request and give you the Pydantic model you defined.
## Check the Docs
You can see the required headers in the docs UI at `/docs`:
<div class="screenshot">
<img src="/img/tutorial/header-param-models/image01.png">
</div>
## Forbid Extra Headers
In some special use cases (probably not very common), you might want to **restrict** the headers that you want to receive.
You can use Pydantic's model configuration to `forbid` any `extra` fields:
//// tab | Python 3.10+
```Python hl_lines="10"
{!> ../../../docs_src/header_param_models/tutorial002_an_py310.py!}
```
////
//// tab | Python 3.9+
```Python hl_lines="10"
{!> ../../../docs_src/header_param_models/tutorial002_an_py39.py!}
```
////
//// tab | Python 3.8+
```Python hl_lines="11"
{!> ../../../docs_src/header_param_models/tutorial002_an.py!}
```
////
//// tab | Python 3.10+ non-Annotated
/// tip
Prefer to use the `Annotated` version if possible.
///
```Python hl_lines="8"
{!> ../../../docs_src/header_param_models/tutorial002_py310.py!}
```
////
//// tab | Python 3.9+ non-Annotated
/// tip
Prefer to use the `Annotated` version if possible.
///
```Python hl_lines="10"
{!> ../../../docs_src/header_param_models/tutorial002_py39.py!}
```
////
//// tab | Python 3.8+ non-Annotated
/// tip
Prefer to use the `Annotated` version if possible.
///
```Python hl_lines="10"
{!> ../../../docs_src/header_param_models/tutorial002.py!}
```
////
If a client tries to send some **extra headers**, they will receive an **error** response.
For example, if the client tries to send a `tool` header with a value of `plumbus`, they will receive an **error** response telling them that the header parameter `tool` is not allowed:
```json
{
"detail": [
{
"type": "extra_forbidden",
"loc": ["header", "tool"],
"msg": "Extra inputs are not permitted",
"input": "plumbus",
}
]
}
```
## Summary
You can use **Pydantic models** to declare **headers** in **FastAPI**. 😎

196
docs/en/docs/tutorial/query-param-models.md

@ -0,0 +1,196 @@
# Query Parameter Models
If you have a group of **query parameters** that are related, you can create a **Pydantic model** to declare them.
This would allow you to **re-use the model** in **multiple places** and also to declare validations and metadata for all the parameters at once. 😎
/// note
This is supported since FastAPI version `0.115.0`. 🤓
///
## Query Parameters with a Pydantic Model
Declare the **query parameters** that you need in a **Pydantic model**, and then declare the parameter as `Query`:
//// tab | Python 3.10+
```Python hl_lines="9-13 17"
{!> ../../../docs_src/query_param_models/tutorial001_an_py310.py!}
```
////
//// tab | Python 3.9+
```Python hl_lines="8-12 16"
{!> ../../../docs_src/query_param_models/tutorial001_an_py39.py!}
```
////
//// tab | Python 3.8+
```Python hl_lines="10-14 18"
{!> ../../../docs_src/query_param_models/tutorial001_an.py!}
```
////
//// tab | Python 3.10+ non-Annotated
/// tip
Prefer to use the `Annotated` version if possible.
///
```Python hl_lines="9-13 17"
{!> ../../../docs_src/query_param_models/tutorial001_py310.py!}
```
////
//// tab | Python 3.9+ non-Annotated
/// tip
Prefer to use the `Annotated` version if possible.
///
```Python hl_lines="8-12 16"
{!> ../../../docs_src/query_param_models/tutorial001_py39.py!}
```
////
//// tab | Python 3.8+ non-Annotated
/// tip
Prefer to use the `Annotated` version if possible.
///
```Python hl_lines="9-13 17"
{!> ../../../docs_src/query_param_models/tutorial001_py310.py!}
```
////
**FastAPI** will **extract** the data for **each field** from the **query parameters** in the request and give you the Pydantic model you defined.
## Check the Docs
You can see the query parameters in the docs UI at `/docs`:
<div class="screenshot">
<img src="/img/tutorial/query-param-models/image01.png">
</div>
## Forbid Extra Query Parameters
In some special use cases (probably not very common), you might want to **restrict** the query parameters that you want to receive.
You can use Pydantic's model configuration to `forbid` any `extra` fields:
//// tab | Python 3.10+
```Python hl_lines="10"
{!> ../../../docs_src/query_param_models/tutorial002_an_py310.py!}
```
////
//// tab | Python 3.9+
```Python hl_lines="9"
{!> ../../../docs_src/query_param_models/tutorial002_an_py39.py!}
```
////
//// tab | Python 3.8+
```Python hl_lines="11"
{!> ../../../docs_src/query_param_models/tutorial002_an.py!}
```
////
//// tab | Python 3.10+ non-Annotated
/// tip
Prefer to use the `Annotated` version if possible.
///
```Python hl_lines="10"
{!> ../../../docs_src/query_param_models/tutorial002_py310.py!}
```
////
//// tab | Python 3.9+ non-Annotated
/// tip
Prefer to use the `Annotated` version if possible.
///
```Python hl_lines="9"
{!> ../../../docs_src/query_param_models/tutorial002_py39.py!}
```
////
//// tab | Python 3.8+ non-Annotated
/// tip
Prefer to use the `Annotated` version if possible.
///
```Python hl_lines="11"
{!> ../../../docs_src/query_param_models/tutorial002.py!}
```
////
If a client tries to send some **extra** data in the **query parameters**, they will receive an **error** response.
For example, if the client tries to send a `tool` query parameter with a value of `plumbus`, like:
```http
https://example.com/items/?limit=10&tool=plumbus
```
They will receive an **error** response telling them that the query parameter `tool` is not allowed:
```json
{
"detail": [
{
"type": "extra_forbidden",
"loc": ["query", "tool"],
"msg": "Extra inputs are not permitted",
"input": "plumbus"
}
]
}
```
## Summary
You can use **Pydantic models** to declare **query parameters** in **FastAPI**. 😎
/// tip
Spoiler alert: you can also use Pydantic models to declare cookies and headers, but you will read about that later in the tutorial. 🤫
///

3
docs/en/mkdocs.yml

@ -118,6 +118,7 @@ nav:
- tutorial/body.md
- tutorial/query-params-str-validations.md
- tutorial/path-params-numeric-validations.md
- tutorial/query-param-models.md
- tutorial/body-multiple-params.md
- tutorial/body-fields.md
- tutorial/body-nested-models.md
@ -125,6 +126,8 @@ nav:
- tutorial/extra-data-types.md
- tutorial/cookie-params.md
- tutorial/header-params.md
- tutorial/cookie-param-models.md
- tutorial/header-param-models.md
- tutorial/response-model.md
- tutorial/extra-models.md
- tutorial/response-status-code.md

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>.
///

192
docs/pt/docs/advanced/security/http-basic-auth.md

@ -0,0 +1,192 @@
# HTTP Basic Auth
Para os casos mais simples, você pode utilizar o HTTP Basic Auth.
No HTTP Basic Auth, a aplicação espera um cabeçalho que contém um usuário e uma senha.
Caso ela não receba, ela retorna um erro HTTP 401 "Unauthorized" (*Não Autorizado*).
E retorna um cabeçalho `WWW-Authenticate` com o valor `Basic`, e um parâmetro opcional `realm`.
Isso sinaliza ao navegador para mostrar o prompt integrado para um usuário e senha.
Então, quando você digitar o usuário e senha, o navegador os envia automaticamente no cabeçalho.
## HTTP Basic Auth Simples
* Importe `HTTPBasic` e `HTTPBasicCredentials`.
* Crie um "esquema `security`" utilizando `HTTPBasic`.
* Utilize o `security` com uma dependência em sua *operação de rota*.
* Isso retorna um objeto do tipo `HTTPBasicCredentials`:
* Isto contém o `username` e o `password` enviado.
//// tab | Python 3.9+
```Python hl_lines="4 8 12"
{!> ../../../docs_src/security/tutorial006_an_py39.py!}
```
////
//// tab | Python 3.8+
```Python hl_lines="2 7 11"
{!> ../../../docs_src/security/tutorial006_an.py!}
```
////
//// tab | Python 3.8+ non-Annotated
/// tip | "Dica"
Prefira utilizar a versão `Annotated` se possível.
///
```Python hl_lines="2 6 10"
{!> ../../../docs_src/security/tutorial006.py!}
```
////
Quando você tentar abrir a URL pela primeira vez (ou clicar no botão "Executar" nos documentos) o navegador vai pedir pelo seu usuário e senha:
<img src="/img/tutorial/security/image12.png">
## Verifique o usuário
Aqui está um exemplo mais completo.
Utilize uma dependência para verificar se o usuário e a senha estão corretos.
Para isso, utilize o módulo padrão do Python <a href="https://docs.python.org/3/library/secrets.html" class="external-link" target="_blank">`secrets`</a> para verificar o usuário e senha.
O `secrets.compare_digest()` necessita receber `bytes` ou `str` que possuem apenas caracteres ASCII (os em Inglês). Isso significa que não funcionaria com caracteres como o `á`, como em `Sebastián`.
Para lidar com isso, primeiramente nós convertemos o `username` e o `password` para `bytes`, codificando-os com UTF-8.
Então nós podemos utilizar o `secrets.compare_digest()` para garantir que o `credentials.username` é `"stanleyjobson"`, e que o `credentials.password` é `"swordfish"`.
//// tab | Python 3.9+
```Python hl_lines="1 12-24"
{!> ../../../docs_src/security/tutorial007_an_py39.py!}
```
////
//// tab | Python 3.8+
```Python hl_lines="1 12-24"
{!> ../../../docs_src/security/tutorial007_an.py!}
```
////
//// tab | Python 3.8+ non-Annotated
/// tip | "Dica"
Prefira utilizar a versão `Annotated` se possível.
///
```Python hl_lines="1 11-21"
{!> ../../../docs_src/security/tutorial007.py!}
```
////
Isso seria parecido com:
```Python
if not (credentials.username == "stanleyjobson") or not (credentials.password == "swordfish"):
# Return some error
...
```
Porém ao utilizar o `secrets.compare_digest()`, isso estará seguro contra um tipo de ataque chamado "ataque de temporização (timing attacks)".
### Ataques de Temporização
Mas o que é um "ataque de temporização"?
Vamos imaginar que alguns invasores estão tentando adivinhar o usuário e a senha.
E eles enviam uma requisição com um usuário `johndoe` e uma senha `love123`.
Então o código Python em sua aplicação seria equivalente a algo como:
```Python
if "johndoe" == "stanleyjobson" and "love123" == "swordfish":
...
```
Mas no exato momento que o Python compara o primeiro `j` em `johndoe` contra o primeiro `s` em `stanleyjobson`, ele retornará `False`, porque ele já sabe que aquelas duas strings não são a mesma, pensando que "não existe a necessidade de desperdiçar mais poder computacional comparando o resto das letras". E a sua aplicação dirá "Usuário ou senha incorretos".
Mas então os invasores vão tentar com o usuário `stanleyjobsox` e a senha `love123`.
E a sua aplicação faz algo como:
```Python
if "stanleyjobsox" == "stanleyjobson" and "love123" == "swordfish":
...
```
O Python terá que comparar todo o `stanleyjobso` tanto em `stanleyjobsox` como em `stanleyjobson` antes de perceber que as strings não são a mesma. Então isso levará alguns microsegundos a mais para retornar "Usuário ou senha incorretos".
#### O tempo para responder ajuda os invasores
Neste ponto, ao perceber que o servidor demorou alguns microsegundos a mais para enviar o retorno "Usuário ou senha incorretos", os invasores irão saber que eles acertaram _alguma coisa_, algumas das letras iniciais estavam certas.
E eles podem tentar de novo sabendo que provavelmente é algo mais parecido com `stanleyjobsox` do que com `johndoe`.
#### Um ataque "profissional"
Claro, os invasores não tentariam tudo isso de forma manual, eles escreveriam um programa para fazer isso, possivelmente com milhares ou milhões de testes por segundo. E obteriam apenas uma letra a mais por vez.
Mas fazendo isso, em alguns minutos ou horas os invasores teriam adivinhado o usuário e senha corretos, com a "ajuda" da nossa aplicação, apenas usando o tempo levado para responder.
#### Corrija com o `secrets.compare_digest()`
Mas em nosso código nós estamos utilizando o `secrets.compare_digest()`.
Resumindo, levará o mesmo tempo para comparar `stanleyjobsox` com `stanleyjobson` do que comparar `johndoe` com `stanleyjobson`. E o mesmo para a senha.
Deste modo, ao utilizar `secrets.compare_digest()` no código de sua aplicação, ela esterá a salvo contra toda essa gama de ataques de segurança.
### Retorne o erro
Depois de detectar que as credenciais estão incorretas, retorne um `HTTPException` com o status 401 (o mesmo retornado quando nenhuma credencial foi informada) e adicione o cabeçalho `WWW-Authenticate` para fazer com que o navegador mostre o prompt de login novamente:
//// tab | Python 3.9+
```Python hl_lines="26-30"
{!> ../../../docs_src/security/tutorial007_an_py39.py!}
```
////
//// tab | Python 3.8+
```Python hl_lines="26-30"
{!> ../../../docs_src/security/tutorial007_an.py!}
```
////
//// tab | Python 3.8+ non-Annotated
/// tip | "Dica"
Prefira utilizar a versão `Annotated` se possível.
///
```Python hl_lines="23-27"
{!> ../../../docs_src/security/tutorial007.py!}
```
////

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. 😎

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

@ -1,84 +1,28 @@
# 项目生成 - 模板
项目生成器一般都会提供很多初始设置、安全措施、数据库,甚至还准备好了第一个 API 端点,能帮助您快速上手。
项目生成器的设置通常都很主观,您可以按需更新或修改,但对于您的项目来说,它是非常好的起点。
## 全栈 FastAPI + PostgreSQL
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 + PostgreSQL - 功能
* 完整的 **Docker** 集成(基于 Docker)
* Docker Swarm 开发模式
* **Docker Compose** 本地开发集成与优化
* **生产可用**的 Python 网络服务器,使用 Uvicorn 或 Gunicorn
* Python <a href="https://github.com/fastapi/fastapi" class="external-link" target="_blank">**FastAPI**</a> 后端:
* * **速度快**:可与 **NodeJS****Go** 比肩的极高性能(归功于 Starlette 和 Pydantic)
* **直观**:强大的编辑器支持,处处皆可<abbr title="也叫自动完成、智能感知">自动补全</abbr>,减少调试时间
* **简单**:易学、易用,阅读文档所需时间更短
* **简短**:代码重复最小化,每次参数声明都可以实现多个功能
* **健壮**: 生产级别的代码,还有自动交互文档
* **基于标准**:完全兼容并基于 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>
* <a href="https://fastapi.tiangolo.com/features/" class="external-link" target="_blank">**更多功能**</a>包括自动验证、序列化、交互文档、OAuth2 JWT 令牌身份验证等
* **安全密码**,默认使用密码哈希
* **JWT 令牌**身份验证
* **SQLAlchemy** 模型(独立于 Flask 扩展,可直接用于 Celery Worker)
* 基础的用户模型(可按需修改或删除)
* **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)
# FastAPI全栈模板
模板通常带有特定的设置,而且被设计为灵活和可定制的。这允许您根据项目的需求修改和调整它们,使它们成为一个很好的起点。🏁
您可以使用此模板开始,因为它包含了许多已经为您完成的初始设置、安全性、数据库和一些API端点。
代码仓: <a href="https://github.com/fastapi/full-stack-fastapi-template" class="external-link" target="_blank">Full Stack FastAPI Template</a>
## 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) 用于前端组件。
- 🤖 一个自动化生成的前端客户端。
- 🧪 Playwright用于端到端测试。
- 🦇 支持暗黑主题(Dark mode)。
- 🐋 [Docker Compose](https://www.docker.com) 用于开发环境和生产环境。
- 🔒 默认使用密码哈希来保证安全。
- 🔑 JWT令牌用于权限验证。
- 📫 使用邮箱来进行密码恢复。
- ✅ 单元测试用了[Pytest](https://pytest.org).
- 📞 [Traefik](https://traefik.io) 用于反向代理和负载均衡。
- 🚢 部署指南(Docker Compose)包含了如何起一个Traefik前端代理来自动化HTTPS认证。
- 🏭 CI(持续集成)和 CD(持续部署)基于GitHub Actions。

17
docs_src/cookie_param_models/tutorial001.py

@ -0,0 +1,17 @@
from typing import Union
from fastapi import Cookie, FastAPI
from pydantic import BaseModel
app = FastAPI()
class Cookies(BaseModel):
session_id: str
fatebook_tracker: Union[str, None] = None
googall_tracker: Union[str, None] = None
@app.get("/items/")
async def read_items(cookies: Cookies = Cookie()):
return cookies

18
docs_src/cookie_param_models/tutorial001_an.py

@ -0,0 +1,18 @@
from typing import Union
from fastapi import Cookie, FastAPI
from pydantic import BaseModel
from typing_extensions import Annotated
app = FastAPI()
class Cookies(BaseModel):
session_id: str
fatebook_tracker: Union[str, None] = None
googall_tracker: Union[str, None] = None
@app.get("/items/")
async def read_items(cookies: Annotated[Cookies, Cookie()]):
return cookies

17
docs_src/cookie_param_models/tutorial001_an_py310.py

@ -0,0 +1,17 @@
from typing import Annotated
from fastapi import Cookie, FastAPI
from pydantic import BaseModel
app = FastAPI()
class Cookies(BaseModel):
session_id: str
fatebook_tracker: str | None = None
googall_tracker: str | None = None
@app.get("/items/")
async def read_items(cookies: Annotated[Cookies, Cookie()]):
return cookies

17
docs_src/cookie_param_models/tutorial001_an_py39.py

@ -0,0 +1,17 @@
from typing import Annotated, Union
from fastapi import Cookie, FastAPI
from pydantic import BaseModel
app = FastAPI()
class Cookies(BaseModel):
session_id: str
fatebook_tracker: Union[str, None] = None
googall_tracker: Union[str, None] = None
@app.get("/items/")
async def read_items(cookies: Annotated[Cookies, Cookie()]):
return cookies

15
docs_src/cookie_param_models/tutorial001_py310.py

@ -0,0 +1,15 @@
from fastapi import Cookie, FastAPI
from pydantic import BaseModel
app = FastAPI()
class Cookies(BaseModel):
session_id: str
fatebook_tracker: str | None = None
googall_tracker: str | None = None
@app.get("/items/")
async def read_items(cookies: Cookies = Cookie()):
return cookies

19
docs_src/cookie_param_models/tutorial002.py

@ -0,0 +1,19 @@
from typing import Union
from fastapi import Cookie, FastAPI
from pydantic import BaseModel
app = FastAPI()
class Cookies(BaseModel):
model_config = {"extra": "forbid"}
session_id: str
fatebook_tracker: Union[str, None] = None
googall_tracker: Union[str, None] = None
@app.get("/items/")
async def read_items(cookies: Cookies = Cookie()):
return cookies

20
docs_src/cookie_param_models/tutorial002_an.py

@ -0,0 +1,20 @@
from typing import Union
from fastapi import Cookie, FastAPI
from pydantic import BaseModel
from typing_extensions import Annotated
app = FastAPI()
class Cookies(BaseModel):
model_config = {"extra": "forbid"}
session_id: str
fatebook_tracker: Union[str, None] = None
googall_tracker: Union[str, None] = None
@app.get("/items/")
async def read_items(cookies: Annotated[Cookies, Cookie()]):
return cookies

19
docs_src/cookie_param_models/tutorial002_an_py310.py

@ -0,0 +1,19 @@
from typing import Annotated
from fastapi import Cookie, FastAPI
from pydantic import BaseModel
app = FastAPI()
class Cookies(BaseModel):
model_config = {"extra": "forbid"}
session_id: str
fatebook_tracker: str | None = None
googall_tracker: str | None = None
@app.get("/items/")
async def read_items(cookies: Annotated[Cookies, Cookie()]):
return cookies

19
docs_src/cookie_param_models/tutorial002_an_py39.py

@ -0,0 +1,19 @@
from typing import Annotated, Union
from fastapi import Cookie, FastAPI
from pydantic import BaseModel
app = FastAPI()
class Cookies(BaseModel):
model_config = {"extra": "forbid"}
session_id: str
fatebook_tracker: Union[str, None] = None
googall_tracker: Union[str, None] = None
@app.get("/items/")
async def read_items(cookies: Annotated[Cookies, Cookie()]):
return cookies

20
docs_src/cookie_param_models/tutorial002_pv1.py

@ -0,0 +1,20 @@
from typing import Union
from fastapi import Cookie, FastAPI
from pydantic import BaseModel
app = FastAPI()
class Cookies(BaseModel):
class Config:
extra = "forbid"
session_id: str
fatebook_tracker: Union[str, None] = None
googall_tracker: Union[str, None] = None
@app.get("/items/")
async def read_items(cookies: Cookies = Cookie()):
return cookies

21
docs_src/cookie_param_models/tutorial002_pv1_an.py

@ -0,0 +1,21 @@
from typing import Union
from fastapi import Cookie, FastAPI
from pydantic import BaseModel
from typing_extensions import Annotated
app = FastAPI()
class Cookies(BaseModel):
class Config:
extra = "forbid"
session_id: str
fatebook_tracker: Union[str, None] = None
googall_tracker: Union[str, None] = None
@app.get("/items/")
async def read_items(cookies: Annotated[Cookies, Cookie()]):
return cookies

20
docs_src/cookie_param_models/tutorial002_pv1_an_py310.py

@ -0,0 +1,20 @@
from typing import Annotated
from fastapi import Cookie, FastAPI
from pydantic import BaseModel
app = FastAPI()
class Cookies(BaseModel):
class Config:
extra = "forbid"
session_id: str
fatebook_tracker: str | None = None
googall_tracker: str | None = None
@app.get("/items/")
async def read_items(cookies: Annotated[Cookies, Cookie()]):
return cookies

20
docs_src/cookie_param_models/tutorial002_pv1_an_py39.py

@ -0,0 +1,20 @@
from typing import Annotated, Union
from fastapi import Cookie, FastAPI
from pydantic import BaseModel
app = FastAPI()
class Cookies(BaseModel):
class Config:
extra = "forbid"
session_id: str
fatebook_tracker: Union[str, None] = None
googall_tracker: Union[str, None] = None
@app.get("/items/")
async def read_items(cookies: Annotated[Cookies, Cookie()]):
return cookies

18
docs_src/cookie_param_models/tutorial002_pv1_py310.py

@ -0,0 +1,18 @@
from fastapi import Cookie, FastAPI
from pydantic import BaseModel
app = FastAPI()
class Cookies(BaseModel):
class Config:
extra = "forbid"
session_id: str
fatebook_tracker: str | None = None
googall_tracker: str | None = None
@app.get("/items/")
async def read_items(cookies: Cookies = Cookie()):
return cookies

17
docs_src/cookie_param_models/tutorial002_py310.py

@ -0,0 +1,17 @@
from fastapi import Cookie, FastAPI
from pydantic import BaseModel
app = FastAPI()
class Cookies(BaseModel):
model_config = {"extra": "forbid"}
session_id: str
fatebook_tracker: str | None = None
googall_tracker: str | None = None
@app.get("/items/")
async def read_items(cookies: Cookies = Cookie()):
return cookies

19
docs_src/header_param_models/tutorial001.py

@ -0,0 +1,19 @@
from typing import List, Union
from fastapi import FastAPI, Header
from pydantic import BaseModel
app = FastAPI()
class CommonHeaders(BaseModel):
host: str
save_data: bool
if_modified_since: Union[str, None] = None
traceparent: Union[str, None] = None
x_tag: List[str] = []
@app.get("/items/")
async def read_items(headers: CommonHeaders = Header()):
return headers

20
docs_src/header_param_models/tutorial001_an.py

@ -0,0 +1,20 @@
from typing import List, Union
from fastapi import FastAPI, Header
from pydantic import BaseModel
from typing_extensions import Annotated
app = FastAPI()
class CommonHeaders(BaseModel):
host: str
save_data: bool
if_modified_since: Union[str, None] = None
traceparent: Union[str, None] = None
x_tag: List[str] = []
@app.get("/items/")
async def read_items(headers: Annotated[CommonHeaders, Header()]):
return headers

19
docs_src/header_param_models/tutorial001_an_py310.py

@ -0,0 +1,19 @@
from typing import Annotated
from fastapi import FastAPI, Header
from pydantic import BaseModel
app = FastAPI()
class CommonHeaders(BaseModel):
host: str
save_data: bool
if_modified_since: str | None = None
traceparent: str | None = None
x_tag: list[str] = []
@app.get("/items/")
async def read_items(headers: Annotated[CommonHeaders, Header()]):
return headers

19
docs_src/header_param_models/tutorial001_an_py39.py

@ -0,0 +1,19 @@
from typing import Annotated, Union
from fastapi import FastAPI, Header
from pydantic import BaseModel
app = FastAPI()
class CommonHeaders(BaseModel):
host: str
save_data: bool
if_modified_since: Union[str, None] = None
traceparent: Union[str, None] = None
x_tag: list[str] = []
@app.get("/items/")
async def read_items(headers: Annotated[CommonHeaders, Header()]):
return headers

17
docs_src/header_param_models/tutorial001_py310.py

@ -0,0 +1,17 @@
from fastapi import FastAPI, Header
from pydantic import BaseModel
app = FastAPI()
class CommonHeaders(BaseModel):
host: str
save_data: bool
if_modified_since: str | None = None
traceparent: str | None = None
x_tag: list[str] = []
@app.get("/items/")
async def read_items(headers: CommonHeaders = Header()):
return headers

19
docs_src/header_param_models/tutorial001_py39.py

@ -0,0 +1,19 @@
from typing import Union
from fastapi import FastAPI, Header
from pydantic import BaseModel
app = FastAPI()
class CommonHeaders(BaseModel):
host: str
save_data: bool
if_modified_since: Union[str, None] = None
traceparent: Union[str, None] = None
x_tag: list[str] = []
@app.get("/items/")
async def read_items(headers: CommonHeaders = Header()):
return headers

21
docs_src/header_param_models/tutorial002.py

@ -0,0 +1,21 @@
from typing import List, Union
from fastapi import FastAPI, Header
from pydantic import BaseModel
app = FastAPI()
class CommonHeaders(BaseModel):
model_config = {"extra": "forbid"}
host: str
save_data: bool
if_modified_since: Union[str, None] = None
traceparent: Union[str, None] = None
x_tag: List[str] = []
@app.get("/items/")
async def read_items(headers: CommonHeaders = Header()):
return headers

22
docs_src/header_param_models/tutorial002_an.py

@ -0,0 +1,22 @@
from typing import List, Union
from fastapi import FastAPI, Header
from pydantic import BaseModel
from typing_extensions import Annotated
app = FastAPI()
class CommonHeaders(BaseModel):
model_config = {"extra": "forbid"}
host: str
save_data: bool
if_modified_since: Union[str, None] = None
traceparent: Union[str, None] = None
x_tag: List[str] = []
@app.get("/items/")
async def read_items(headers: Annotated[CommonHeaders, Header()]):
return headers

21
docs_src/header_param_models/tutorial002_an_py310.py

@ -0,0 +1,21 @@
from typing import Annotated
from fastapi import FastAPI, Header
from pydantic import BaseModel
app = FastAPI()
class CommonHeaders(BaseModel):
model_config = {"extra": "forbid"}
host: str
save_data: bool
if_modified_since: str | None = None
traceparent: str | None = None
x_tag: list[str] = []
@app.get("/items/")
async def read_items(headers: Annotated[CommonHeaders, Header()]):
return headers

21
docs_src/header_param_models/tutorial002_an_py39.py

@ -0,0 +1,21 @@
from typing import Annotated, Union
from fastapi import FastAPI, Header
from pydantic import BaseModel
app = FastAPI()
class CommonHeaders(BaseModel):
model_config = {"extra": "forbid"}
host: str
save_data: bool
if_modified_since: Union[str, None] = None
traceparent: Union[str, None] = None
x_tag: list[str] = []
@app.get("/items/")
async def read_items(headers: Annotated[CommonHeaders, Header()]):
return headers

22
docs_src/header_param_models/tutorial002_pv1.py

@ -0,0 +1,22 @@
from typing import List, Union
from fastapi import FastAPI, Header
from pydantic import BaseModel
app = FastAPI()
class CommonHeaders(BaseModel):
class Config:
extra = "forbid"
host: str
save_data: bool
if_modified_since: Union[str, None] = None
traceparent: Union[str, None] = None
x_tag: List[str] = []
@app.get("/items/")
async def read_items(headers: CommonHeaders = Header()):
return headers

23
docs_src/header_param_models/tutorial002_pv1_an.py

@ -0,0 +1,23 @@
from typing import List, Union
from fastapi import FastAPI, Header
from pydantic import BaseModel
from typing_extensions import Annotated
app = FastAPI()
class CommonHeaders(BaseModel):
class Config:
extra = "forbid"
host: str
save_data: bool
if_modified_since: Union[str, None] = None
traceparent: Union[str, None] = None
x_tag: List[str] = []
@app.get("/items/")
async def read_items(headers: Annotated[CommonHeaders, Header()]):
return headers

22
docs_src/header_param_models/tutorial002_pv1_an_py310.py

@ -0,0 +1,22 @@
from typing import Annotated
from fastapi import FastAPI, Header
from pydantic import BaseModel
app = FastAPI()
class CommonHeaders(BaseModel):
class Config:
extra = "forbid"
host: str
save_data: bool
if_modified_since: str | None = None
traceparent: str | None = None
x_tag: list[str] = []
@app.get("/items/")
async def read_items(headers: Annotated[CommonHeaders, Header()]):
return headers

22
docs_src/header_param_models/tutorial002_pv1_an_py39.py

@ -0,0 +1,22 @@
from typing import Annotated, Union
from fastapi import FastAPI, Header
from pydantic import BaseModel
app = FastAPI()
class CommonHeaders(BaseModel):
class Config:
extra = "forbid"
host: str
save_data: bool
if_modified_since: Union[str, None] = None
traceparent: Union[str, None] = None
x_tag: list[str] = []
@app.get("/items/")
async def read_items(headers: Annotated[CommonHeaders, Header()]):
return headers

20
docs_src/header_param_models/tutorial002_pv1_py310.py

@ -0,0 +1,20 @@
from fastapi import FastAPI, Header
from pydantic import BaseModel
app = FastAPI()
class CommonHeaders(BaseModel):
class Config:
extra = "forbid"
host: str
save_data: bool
if_modified_since: str | None = None
traceparent: str | None = None
x_tag: list[str] = []
@app.get("/items/")
async def read_items(headers: CommonHeaders = Header()):
return headers

22
docs_src/header_param_models/tutorial002_pv1_py39.py

@ -0,0 +1,22 @@
from typing import Union
from fastapi import FastAPI, Header
from pydantic import BaseModel
app = FastAPI()
class CommonHeaders(BaseModel):
class Config:
extra = "forbid"
host: str
save_data: bool
if_modified_since: Union[str, None] = None
traceparent: Union[str, None] = None
x_tag: list[str] = []
@app.get("/items/")
async def read_items(headers: CommonHeaders = Header()):
return headers

19
docs_src/header_param_models/tutorial002_py310.py

@ -0,0 +1,19 @@
from fastapi import FastAPI, Header
from pydantic import BaseModel
app = FastAPI()
class CommonHeaders(BaseModel):
model_config = {"extra": "forbid"}
host: str
save_data: bool
if_modified_since: str | None = None
traceparent: str | None = None
x_tag: list[str] = []
@app.get("/items/")
async def read_items(headers: CommonHeaders = Header()):
return headers

21
docs_src/header_param_models/tutorial002_py39.py

@ -0,0 +1,21 @@
from typing import Union
from fastapi import FastAPI, Header
from pydantic import BaseModel
app = FastAPI()
class CommonHeaders(BaseModel):
model_config = {"extra": "forbid"}
host: str
save_data: bool
if_modified_since: Union[str, None] = None
traceparent: Union[str, None] = None
x_tag: list[str] = []
@app.get("/items/")
async def read_items(headers: CommonHeaders = Header()):
return headers

19
docs_src/query_param_models/tutorial001.py

@ -0,0 +1,19 @@
from typing import List
from fastapi import FastAPI, Query
from pydantic import BaseModel, Field
from typing_extensions import Literal
app = FastAPI()
class FilterParams(BaseModel):
limit: int = Field(100, gt=0, le=100)
offset: int = Field(0, ge=0)
order_by: Literal["created_at", "updated_at"] = "created_at"
tags: List[str] = []
@app.get("/items/")
async def read_items(filter_query: FilterParams = Query()):
return filter_query

19
docs_src/query_param_models/tutorial001_an.py

@ -0,0 +1,19 @@
from typing import List
from fastapi import FastAPI, Query
from pydantic import BaseModel, Field
from typing_extensions import Annotated, Literal
app = FastAPI()
class FilterParams(BaseModel):
limit: int = Field(100, gt=0, le=100)
offset: int = Field(0, ge=0)
order_by: Literal["created_at", "updated_at"] = "created_at"
tags: List[str] = []
@app.get("/items/")
async def read_items(filter_query: Annotated[FilterParams, Query()]):
return filter_query

18
docs_src/query_param_models/tutorial001_an_py310.py

@ -0,0 +1,18 @@
from typing import Annotated, Literal
from fastapi import FastAPI, Query
from pydantic import BaseModel, Field
app = FastAPI()
class FilterParams(BaseModel):
limit: int = Field(100, gt=0, le=100)
offset: int = Field(0, ge=0)
order_by: Literal["created_at", "updated_at"] = "created_at"
tags: list[str] = []
@app.get("/items/")
async def read_items(filter_query: Annotated[FilterParams, Query()]):
return filter_query

17
docs_src/query_param_models/tutorial001_an_py39.py

@ -0,0 +1,17 @@
from fastapi import FastAPI, Query
from pydantic import BaseModel, Field
from typing_extensions import Annotated, Literal
app = FastAPI()
class FilterParams(BaseModel):
limit: int = Field(100, gt=0, le=100)
offset: int = Field(0, ge=0)
order_by: Literal["created_at", "updated_at"] = "created_at"
tags: list[str] = []
@app.get("/items/")
async def read_items(filter_query: Annotated[FilterParams, Query()]):
return filter_query

18
docs_src/query_param_models/tutorial001_py310.py

@ -0,0 +1,18 @@
from typing import Literal
from fastapi import FastAPI, Query
from pydantic import BaseModel, Field
app = FastAPI()
class FilterParams(BaseModel):
limit: int = Field(100, gt=0, le=100)
offset: int = Field(0, ge=0)
order_by: Literal["created_at", "updated_at"] = "created_at"
tags: list[str] = []
@app.get("/items/")
async def read_items(filter_query: FilterParams = Query()):
return filter_query

17
docs_src/query_param_models/tutorial001_py39.py

@ -0,0 +1,17 @@
from fastapi import FastAPI, Query
from pydantic import BaseModel, Field
from typing_extensions import Literal
app = FastAPI()
class FilterParams(BaseModel):
limit: int = Field(100, gt=0, le=100)
offset: int = Field(0, ge=0)
order_by: Literal["created_at", "updated_at"] = "created_at"
tags: list[str] = []
@app.get("/items/")
async def read_items(filter_query: FilterParams = Query()):
return filter_query

21
docs_src/query_param_models/tutorial002.py

@ -0,0 +1,21 @@
from typing import List
from fastapi import FastAPI, Query
from pydantic import BaseModel, Field
from typing_extensions import Literal
app = FastAPI()
class FilterParams(BaseModel):
model_config = {"extra": "forbid"}
limit: int = Field(100, gt=0, le=100)
offset: int = Field(0, ge=0)
order_by: Literal["created_at", "updated_at"] = "created_at"
tags: List[str] = []
@app.get("/items/")
async def read_items(filter_query: FilterParams = Query()):
return filter_query

21
docs_src/query_param_models/tutorial002_an.py

@ -0,0 +1,21 @@
from typing import List
from fastapi import FastAPI, Query
from pydantic import BaseModel, Field
from typing_extensions import Annotated, Literal
app = FastAPI()
class FilterParams(BaseModel):
model_config = {"extra": "forbid"}
limit: int = Field(100, gt=0, le=100)
offset: int = Field(0, ge=0)
order_by: Literal["created_at", "updated_at"] = "created_at"
tags: List[str] = []
@app.get("/items/")
async def read_items(filter_query: Annotated[FilterParams, Query()]):
return filter_query

20
docs_src/query_param_models/tutorial002_an_py310.py

@ -0,0 +1,20 @@
from typing import Annotated, Literal
from fastapi import FastAPI, Query
from pydantic import BaseModel, Field
app = FastAPI()
class FilterParams(BaseModel):
model_config = {"extra": "forbid"}
limit: int = Field(100, gt=0, le=100)
offset: int = Field(0, ge=0)
order_by: Literal["created_at", "updated_at"] = "created_at"
tags: list[str] = []
@app.get("/items/")
async def read_items(filter_query: Annotated[FilterParams, Query()]):
return filter_query

19
docs_src/query_param_models/tutorial002_an_py39.py

@ -0,0 +1,19 @@
from fastapi import FastAPI, Query
from pydantic import BaseModel, Field
from typing_extensions import Annotated, Literal
app = FastAPI()
class FilterParams(BaseModel):
model_config = {"extra": "forbid"}
limit: int = Field(100, gt=0, le=100)
offset: int = Field(0, ge=0)
order_by: Literal["created_at", "updated_at"] = "created_at"
tags: list[str] = []
@app.get("/items/")
async def read_items(filter_query: Annotated[FilterParams, Query()]):
return filter_query

22
docs_src/query_param_models/tutorial002_pv1.py

@ -0,0 +1,22 @@
from typing import List
from fastapi import FastAPI, Query
from pydantic import BaseModel, Field
from typing_extensions import Literal
app = FastAPI()
class FilterParams(BaseModel):
class Config:
extra = "forbid"
limit: int = Field(100, gt=0, le=100)
offset: int = Field(0, ge=0)
order_by: Literal["created_at", "updated_at"] = "created_at"
tags: List[str] = []
@app.get("/items/")
async def read_items(filter_query: FilterParams = Query()):
return filter_query

22
docs_src/query_param_models/tutorial002_pv1_an.py

@ -0,0 +1,22 @@
from typing import List
from fastapi import FastAPI, Query
from pydantic import BaseModel, Field
from typing_extensions import Annotated, Literal
app = FastAPI()
class FilterParams(BaseModel):
class Config:
extra = "forbid"
limit: int = Field(100, gt=0, le=100)
offset: int = Field(0, ge=0)
order_by: Literal["created_at", "updated_at"] = "created_at"
tags: List[str] = []
@app.get("/items/")
async def read_items(filter_query: Annotated[FilterParams, Query()]):
return filter_query

21
docs_src/query_param_models/tutorial002_pv1_an_py310.py

@ -0,0 +1,21 @@
from typing import Annotated, Literal
from fastapi import FastAPI, Query
from pydantic import BaseModel, Field
app = FastAPI()
class FilterParams(BaseModel):
class Config:
extra = "forbid"
limit: int = Field(100, gt=0, le=100)
offset: int = Field(0, ge=0)
order_by: Literal["created_at", "updated_at"] = "created_at"
tags: list[str] = []
@app.get("/items/")
async def read_items(filter_query: Annotated[FilterParams, Query()]):
return filter_query

20
docs_src/query_param_models/tutorial002_pv1_an_py39.py

@ -0,0 +1,20 @@
from fastapi import FastAPI, Query
from pydantic import BaseModel, Field
from typing_extensions import Annotated, Literal
app = FastAPI()
class FilterParams(BaseModel):
class Config:
extra = "forbid"
limit: int = Field(100, gt=0, le=100)
offset: int = Field(0, ge=0)
order_by: Literal["created_at", "updated_at"] = "created_at"
tags: list[str] = []
@app.get("/items/")
async def read_items(filter_query: Annotated[FilterParams, Query()]):
return filter_query

21
docs_src/query_param_models/tutorial002_pv1_py310.py

@ -0,0 +1,21 @@
from typing import Literal
from fastapi import FastAPI, Query
from pydantic import BaseModel, Field
app = FastAPI()
class FilterParams(BaseModel):
class Config:
extra = "forbid"
limit: int = Field(100, gt=0, le=100)
offset: int = Field(0, ge=0)
order_by: Literal["created_at", "updated_at"] = "created_at"
tags: list[str] = []
@app.get("/items/")
async def read_items(filter_query: FilterParams = Query()):
return filter_query

20
docs_src/query_param_models/tutorial002_pv1_py39.py

@ -0,0 +1,20 @@
from fastapi import FastAPI, Query
from pydantic import BaseModel, Field
from typing_extensions import Literal
app = FastAPI()
class FilterParams(BaseModel):
class Config:
extra = "forbid"
limit: int = Field(100, gt=0, le=100)
offset: int = Field(0, ge=0)
order_by: Literal["created_at", "updated_at"] = "created_at"
tags: list[str] = []
@app.get("/items/")
async def read_items(filter_query: FilterParams = Query()):
return filter_query

20
docs_src/query_param_models/tutorial002_py310.py

@ -0,0 +1,20 @@
from typing import Literal
from fastapi import FastAPI, Query
from pydantic import BaseModel, Field
app = FastAPI()
class FilterParams(BaseModel):
model_config = {"extra": "forbid"}
limit: int = Field(100, gt=0, le=100)
offset: int = Field(0, ge=0)
order_by: Literal["created_at", "updated_at"] = "created_at"
tags: list[str] = []
@app.get("/items/")
async def read_items(filter_query: FilterParams = Query()):
return filter_query

19
docs_src/query_param_models/tutorial002_py39.py

@ -0,0 +1,19 @@
from fastapi import FastAPI, Query
from pydantic import BaseModel, Field
from typing_extensions import Literal
app = FastAPI()
class FilterParams(BaseModel):
model_config = {"extra": "forbid"}
limit: int = Field(100, gt=0, le=100)
offset: int = Field(0, ge=0)
order_by: Literal["created_at", "updated_at"] = "created_at"
tags: list[str] = []
@app.get("/items/")
async def read_items(filter_query: FilterParams = Query()):
return filter_query

2
fastapi/__init__.py

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

6
fastapi/_compat.py

@ -2,6 +2,7 @@ from collections import deque
from copy import copy
from dataclasses import dataclass, is_dataclass
from enum import Enum
from functools import lru_cache
from typing import (
Any,
Callable,
@ -649,3 +650,8 @@ def is_uploadfile_sequence_annotation(annotation: Any) -> bool:
is_uploadfile_or_nonable_uploadfile_annotation(sub_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)

96
fastapi/dependencies/utils.py

@ -32,8 +32,8 @@ from fastapi._compat import (
evaluate_forwardref,
field_annotation_is_scalar,
get_annotation_from_field_info,
get_cached_model_fields,
get_missing_field_error,
get_model_fields,
is_bytes_field,
is_bytes_sequence_field,
is_scalar_field,
@ -201,14 +201,23 @@ def get_flat_dependant(
return flat_dependant
def _get_flat_fields_from_params(fields: List[ModelField]) -> List[ModelField]:
if not fields:
return fields
first_field = fields[0]
if len(fields) == 1 and lenient_issubclass(first_field.type_, BaseModel):
fields_to_extract = get_cached_model_fields(first_field.type_)
return fields_to_extract
return fields
def get_flat_params(dependant: Dependant) -> List[ModelField]:
flat_dependant = get_flat_dependant(dependant, skip_repeats=True)
return (
flat_dependant.path_params
+ flat_dependant.query_params
+ flat_dependant.header_params
+ flat_dependant.cookie_params
)
path_params = _get_flat_fields_from_params(flat_dependant.path_params)
query_params = _get_flat_fields_from_params(flat_dependant.query_params)
header_params = _get_flat_fields_from_params(flat_dependant.header_params)
cookie_params = _get_flat_fields_from_params(flat_dependant.cookie_params)
return path_params + query_params + header_params + cookie_params
def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
@ -479,7 +488,15 @@ def analyze_param(
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)
assert (
is_scalar_field(field)
or is_scalar_sequence_field(field)
or (
lenient_issubclass(field.type_, BaseModel)
# For Pydantic v1
and getattr(field, "shape", 1) == 1
)
)
return ParamDetails(type_annotation=type_annotation, depends=depends, field=field)
@ -686,11 +703,14 @@ def _validate_value_with_model_field(
return v_, []
def _get_multidict_value(field: ModelField, values: Mapping[str, Any]) -> Any:
def _get_multidict_value(
field: ModelField, values: Mapping[str, Any], alias: Union[str, None] = None
) -> Any:
alias = alias or field.alias
if is_sequence_field(field) and isinstance(values, (ImmutableMultiDict, Headers)):
value = values.getlist(field.alias)
value = values.getlist(alias)
else:
value = values.get(field.alias, None)
value = values.get(alias, None)
if (
value is None
or (
@ -712,7 +732,55 @@ def request_params_to_args(
received_params: Union[Mapping[str, Any], QueryParams, Headers],
) -> Tuple[Dict[str, Any], List[Any]]:
values: Dict[str, Any] = {}
errors = []
errors: List[Dict[str, Any]] = []
if not fields:
return values, errors
first_field = fields[0]
fields_to_extract = fields
single_not_embedded_field = False
if len(fields) == 1 and lenient_issubclass(first_field.type_, BaseModel):
fields_to_extract = get_cached_model_fields(first_field.type_)
single_not_embedded_field = True
params_to_process: Dict[str, Any] = {}
processed_keys = set()
for field in fields_to_extract:
alias = None
if isinstance(received_params, Headers):
# Handle fields extracted from a Pydantic Model for a header, each field
# doesn't have a FieldInfo of type Header with the default convert_underscores=True
convert_underscores = getattr(field.field_info, "convert_underscores", True)
if convert_underscores:
alias = (
field.alias
if field.alias != field.name
else field.name.replace("_", "-")
)
value = _get_multidict_value(field, received_params, alias=alias)
if value is not None:
params_to_process[field.name] = value
processed_keys.add(alias or field.alias)
processed_keys.add(field.name)
for key, value in received_params.items():
if key not in processed_keys:
params_to_process[key] = value
if single_not_embedded_field:
field_info = first_field.field_info
assert isinstance(
field_info, params.Param
), "Params must be subclasses of Param"
loc: Tuple[str, ...] = (field_info.in_.value,)
v_, errors_ = _validate_value_with_model_field(
field=first_field, value=params_to_process, values=values, loc=loc
)
return {first_field.name: v_}, errors_
for field in fields:
value = _get_multidict_value(field, received_params)
field_info = field.field_info
@ -788,7 +856,7 @@ async def _extract_form_body(
tg.start_soon(process_fn, sub_value.read)
value = serialize_sequence_value(field=field, value=results)
if value is not None:
values[field.name] = value
values[field.alias] = value
for key, value in received_body.items():
if key not in values:
values[key] = value
@ -810,7 +878,7 @@ async def request_body_to_args(
fields_to_extract: List[ModelField] = body_fields
if single_not_embedded_field and lenient_issubclass(first_field.type_, BaseModel):
fields_to_extract = get_model_fields(first_field.type_)
fields_to_extract = get_cached_model_fields(first_field.type_)
if isinstance(received_body, FormData):
body_to_process = await _extract_form_body(fields_to_extract, received_body)

86
fastapi/openapi/utils.py

@ -16,11 +16,15 @@ from fastapi._compat import (
)
from fastapi.datastructures import DefaultPlaceholder
from fastapi.dependencies.models import Dependant
from fastapi.dependencies.utils import get_flat_dependant, get_flat_params
from fastapi.dependencies.utils import (
_get_flat_fields_from_params,
get_flat_dependant,
get_flat_params,
)
from fastapi.encoders import jsonable_encoder
from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX, REF_TEMPLATE
from fastapi.openapi.models import OpenAPI
from fastapi.params import Body, Param
from fastapi.params import Body, ParamTypes
from fastapi.responses import Response
from fastapi.types import ModelNameMap
from fastapi.utils import (
@ -87,9 +91,9 @@ def get_openapi_security_definitions(
return security_definitions, operation_security
def get_openapi_operation_parameters(
def _get_openapi_operation_parameters(
*,
all_route_params: Sequence[ModelField],
dependant: Dependant,
schema_generator: GenerateJsonSchema,
model_name_map: ModelNameMap,
field_mapping: Dict[
@ -98,33 +102,47 @@ def get_openapi_operation_parameters(
separate_input_output_schemas: bool = True,
) -> List[Dict[str, Any]]:
parameters = []
for param in all_route_params:
field_info = param.field_info
field_info = cast(Param, field_info)
if not field_info.include_in_schema:
continue
param_schema = get_schema_from_model_field(
field=param,
schema_generator=schema_generator,
model_name_map=model_name_map,
field_mapping=field_mapping,
separate_input_output_schemas=separate_input_output_schemas,
)
parameter = {
"name": param.alias,
"in": field_info.in_.value,
"required": param.required,
"schema": param_schema,
}
if field_info.description:
parameter["description"] = field_info.description
if field_info.openapi_examples:
parameter["examples"] = jsonable_encoder(field_info.openapi_examples)
elif field_info.example != Undefined:
parameter["example"] = jsonable_encoder(field_info.example)
if field_info.deprecated:
parameter["deprecated"] = True
parameters.append(parameter)
flat_dependant = get_flat_dependant(dependant, skip_repeats=True)
path_params = _get_flat_fields_from_params(flat_dependant.path_params)
query_params = _get_flat_fields_from_params(flat_dependant.query_params)
header_params = _get_flat_fields_from_params(flat_dependant.header_params)
cookie_params = _get_flat_fields_from_params(flat_dependant.cookie_params)
parameter_groups = [
(ParamTypes.path, path_params),
(ParamTypes.query, query_params),
(ParamTypes.header, header_params),
(ParamTypes.cookie, cookie_params),
]
for param_type, param_group in parameter_groups:
for param in param_group:
field_info = param.field_info
# field_info = cast(Param, field_info)
if not getattr(field_info, "include_in_schema", True):
continue
param_schema = get_schema_from_model_field(
field=param,
schema_generator=schema_generator,
model_name_map=model_name_map,
field_mapping=field_mapping,
separate_input_output_schemas=separate_input_output_schemas,
)
parameter = {
"name": param.alias,
"in": param_type.value,
"required": param.required,
"schema": param_schema,
}
if field_info.description:
parameter["description"] = field_info.description
openapi_examples = getattr(field_info, "openapi_examples", None)
example = getattr(field_info, "example", None)
if openapi_examples:
parameter["examples"] = jsonable_encoder(openapi_examples)
elif example != Undefined:
parameter["example"] = jsonable_encoder(example)
if getattr(field_info, "deprecated", None):
parameter["deprecated"] = True
parameters.append(parameter)
return parameters
@ -247,9 +265,8 @@ def get_openapi_path(
operation.setdefault("security", []).extend(operation_security)
if security_definitions:
security_schemes.update(security_definitions)
all_route_params = get_flat_params(route.dependant)
operation_parameters = get_openapi_operation_parameters(
all_route_params=all_route_params,
operation_parameters = _get_openapi_operation_parameters(
dependant=route.dependant,
schema_generator=schema_generator,
model_name_map=model_name_map,
field_mapping=field_mapping,
@ -379,6 +396,7 @@ def get_openapi_path(
deep_dict_update(openapi_response, process_response)
openapi_response["description"] = description
http422 = str(HTTP_422_UNPROCESSABLE_ENTITY)
all_route_params = get_flat_params(route.dependant)
if (all_route_params or route.body_field) and not any(
status in operation["responses"]
for status in [http422, "4XX", "default"]

4
pyproject.toml

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

2
requirements-tests.txt

@ -14,7 +14,7 @@ anyio[trio] >=3.2.1,<4.0.0
PyJWT==2.8.0
pyyaml >=5.3.1,<7.0.0
passlib[bcrypt] >=1.7.2,<2.0.0
inline-snapshot==0.13.0
# types
types-ujson ==5.7.0.1
types-orjson ==3.6.2

39
scripts/playwright/cookie_param_models/image01.py

@ -0,0 +1,39 @@
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})
browser = playwright.chromium.launch(headless=False)
context = browser.new_context()
page = context.new_page()
page.goto("http://localhost:8000/docs")
page.get_by_role("link", name="/items/").click()
# Manually add the screenshot
page.screenshot(path="docs/en/docs/img/tutorial/cookie-param-models/image01.png")
# ---------------------
context.close()
browser.close()
process = subprocess.Popen(
["fastapi", "run", "docs_src/cookie_param_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()

38
scripts/playwright/header_param_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="GET /items/ Read Items").click()
page.get_by_role("button", name="Try it out").click()
# Manually add the screenshot
page.screenshot(path="docs/en/docs/img/tutorial/header-param-models/image01.png")
# ---------------------
context.close()
browser.close()
process = subprocess.Popen(
["fastapi", "run", "docs_src/header_param_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()

41
scripts/playwright/query_param_models/image01.py

@ -0,0 +1,41 @@
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})
browser = playwright.chromium.launch(headless=False)
context = browser.new_context()
page = context.new_page()
page.goto("http://localhost:8000/docs")
page.get_by_role("button", name="GET /items/ Read Items").click()
page.get_by_role("button", name="Try it out").click()
page.get_by_role("heading", name="Servers").click()
# Manually add the screenshot
page.screenshot(path="docs/en/docs/img/tutorial/query-param-models/image01.png")
# ---------------------
context.close()
browser.close()
process = subprocess.Popen(
["fastapi", "run", "docs_src/query_param_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()

4
scripts/playwright/request_form_models/image01.py

@ -8,11 +8,13 @@ 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)
context = browser.new_context()
# 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")
# ---------------------

3
scripts/playwright/separate_openapi_schemas/image01.py

@ -3,13 +3,16 @@ import subprocess
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_text("POST/items/Create Item").click()
page.get_by_role("tab", name="Schema").first.click()
# Manually add the screenshot
page.screenshot(
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
# 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_text("GET/items/Read Items").click()
page.get_by_role("button", name="Try it out").click()
page.get_by_role("button", name="Execute").click()
# Manually add the screenshot
page.screenshot(
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
# 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_text("GET/items/Read Items").click()
page.get_by_role("tab", name="Schema").click()
page.get_by_label("Schema").get_by_role("button", name="Expand all").click()
# Manually add the screenshot
page.screenshot(
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
# 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="Item-Input").click()
page.get_by_role("button", name="Item-Output").click()
page.set_viewport_size({"width": 960, "height": 820})
# Manually add the screenshot
page.screenshot(
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
# 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="Item", exact=True).click()
page.set_viewport_size({"width": 960, "height": 700})
# Manually add the screenshot
page.screenshot(
path="docs/en/docs/img/tutorial/separate-openapi-schemas/image05.png"
)

16
tests/test_compat.py

@ -5,6 +5,7 @@ from fastapi._compat import (
ModelField,
Undefined,
_get_model_config,
get_cached_model_fields,
get_model_fields,
is_bytes_sequence_annotation,
is_scalar_field,
@ -102,3 +103,18 @@ def test_is_pv1_scalar_field():
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

10
tests/test_forms_single_model.py

@ -3,7 +3,7 @@ from typing import List, Optional
from dirty_equals import IsDict
from fastapi import FastAPI, Form
from fastapi.testclient import TestClient
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing_extensions import Annotated
app = FastAPI()
@ -14,6 +14,7 @@ class FormModel(BaseModel):
lastname: str
age: Optional[int] = None
tags: List[str] = ["foo", "bar"]
alias_with: str = Field(alias="with", default="nothing")
@app.post("/form/")
@ -32,6 +33,7 @@ def test_send_all_data():
"lastname": "Sanchez",
"age": "70",
"tags": ["plumbus", "citadel"],
"with": "something",
},
)
assert response.status_code == 200, response.text
@ -40,6 +42,7 @@ def test_send_all_data():
"lastname": "Sanchez",
"age": 70,
"tags": ["plumbus", "citadel"],
"with": "something",
}
@ -51,6 +54,7 @@ def test_defaults():
"lastname": "Sanchez",
"age": None,
"tags": ["foo", "bar"],
"with": "nothing",
}
@ -100,13 +104,13 @@ def test_no_data():
"type": "missing",
"loc": ["body", "username"],
"msg": "Field required",
"input": {"tags": ["foo", "bar"]},
"input": {"tags": ["foo", "bar"], "with": "nothing"},
},
{
"type": "missing",
"loc": ["body", "lastname"],
"msg": "Field required",
"input": {"tags": ["foo", "bar"]},
"input": {"tags": ["foo", "bar"], "with": "nothing"},
},
]
}

0
tests/test_tutorial/test_cookie_param_models/__init__.py

205
tests/test_tutorial/test_cookie_param_models/test_tutorial001.py

@ -0,0 +1,205 @@
import importlib
import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient
from inline_snapshot import snapshot
from tests.utils import needs_py39, needs_py310
@pytest.fixture(
name="client",
params=[
"tutorial001",
pytest.param("tutorial001_py310", marks=needs_py310),
"tutorial001_an",
pytest.param("tutorial001_an_py39", marks=needs_py39),
pytest.param("tutorial001_an_py310", marks=needs_py310),
],
)
def get_client(request: pytest.FixtureRequest):
mod = importlib.import_module(f"docs_src.cookie_param_models.{request.param}")
client = TestClient(mod.app)
return client
def test_cookie_param_model(client: TestClient):
with client as c:
c.cookies.set("session_id", "123")
c.cookies.set("fatebook_tracker", "456")
c.cookies.set("googall_tracker", "789")
response = c.get("/items/")
assert response.status_code == 200
assert response.json() == {
"session_id": "123",
"fatebook_tracker": "456",
"googall_tracker": "789",
}
def test_cookie_param_model_defaults(client: TestClient):
with client as c:
c.cookies.set("session_id", "123")
response = c.get("/items/")
assert response.status_code == 200
assert response.json() == {
"session_id": "123",
"fatebook_tracker": None,
"googall_tracker": None,
}
def test_cookie_param_model_invalid(client: TestClient):
response = client.get("/items/")
assert response.status_code == 422
assert response.json() == snapshot(
IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["cookie", "session_id"],
"msg": "Field required",
"input": {},
}
]
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"type": "value_error.missing",
"loc": ["cookie", "session_id"],
"msg": "field required",
}
]
}
)
)
def test_cookie_param_model_extra(client: TestClient):
with client as c:
c.cookies.set("session_id", "123")
c.cookies.set("extra", "track-me-here-too")
response = c.get("/items/")
assert response.status_code == 200
assert response.json() == snapshot(
{"session_id": "123", "fatebook_tracker": None, "googall_tracker": None}
)
def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == snapshot(
{
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/items/": {
"get": {
"summary": "Read Items",
"operationId": "read_items_items__get",
"parameters": [
{
"name": "session_id",
"in": "cookie",
"required": True,
"schema": {"type": "string", "title": "Session Id"},
},
{
"name": "fatebook_tracker",
"in": "cookie",
"required": False,
"schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Fatebook Tracker",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"type": "string",
"title": "Fatebook Tracker",
}
),
},
{
"name": "googall_tracker",
"in": "cookie",
"required": False,
"schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Googall Tracker",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"type": "string",
"title": "Googall Tracker",
}
),
},
],
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
}
},
"components": {
"schemas": {
"HTTPValidationError": {
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ValidationError"
},
"type": "array",
"title": "Detail",
}
},
"type": "object",
"title": "HTTPValidationError",
},
"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",
},
}
},
}
)

233
tests/test_tutorial/test_cookie_param_models/test_tutorial002.py

@ -0,0 +1,233 @@
import importlib
import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient
from inline_snapshot import snapshot
from tests.utils import needs_py39, needs_py310, needs_pydanticv1, needs_pydanticv2
@pytest.fixture(
name="client",
params=[
pytest.param("tutorial002", marks=needs_pydanticv2),
pytest.param("tutorial002_py310", marks=[needs_py310, needs_pydanticv2]),
pytest.param("tutorial002_an", marks=needs_pydanticv2),
pytest.param("tutorial002_an_py39", marks=[needs_py39, needs_pydanticv2]),
pytest.param("tutorial002_an_py310", marks=[needs_py310, needs_pydanticv2]),
pytest.param("tutorial002_pv1", marks=[needs_pydanticv1, needs_pydanticv1]),
pytest.param("tutorial002_pv1_py310", marks=[needs_py310, needs_pydanticv1]),
pytest.param("tutorial002_pv1_an", marks=[needs_pydanticv1]),
pytest.param("tutorial002_pv1_an_py39", marks=[needs_py39, needs_pydanticv1]),
pytest.param("tutorial002_pv1_an_py310", marks=[needs_py310, needs_pydanticv1]),
],
)
def get_client(request: pytest.FixtureRequest):
mod = importlib.import_module(f"docs_src.cookie_param_models.{request.param}")
client = TestClient(mod.app)
return client
def test_cookie_param_model(client: TestClient):
with client as c:
c.cookies.set("session_id", "123")
c.cookies.set("fatebook_tracker", "456")
c.cookies.set("googall_tracker", "789")
response = c.get("/items/")
assert response.status_code == 200
assert response.json() == {
"session_id": "123",
"fatebook_tracker": "456",
"googall_tracker": "789",
}
def test_cookie_param_model_defaults(client: TestClient):
with client as c:
c.cookies.set("session_id", "123")
response = c.get("/items/")
assert response.status_code == 200
assert response.json() == {
"session_id": "123",
"fatebook_tracker": None,
"googall_tracker": None,
}
def test_cookie_param_model_invalid(client: TestClient):
response = client.get("/items/")
assert response.status_code == 422
assert response.json() == snapshot(
IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["cookie", "session_id"],
"msg": "Field required",
"input": {},
}
]
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"type": "value_error.missing",
"loc": ["cookie", "session_id"],
"msg": "field required",
}
]
}
)
)
def test_cookie_param_model_extra(client: TestClient):
with client as c:
c.cookies.set("session_id", "123")
c.cookies.set("extra", "track-me-here-too")
response = c.get("/items/")
assert response.status_code == 422
assert response.json() == snapshot(
IsDict(
{
"detail": [
{
"type": "extra_forbidden",
"loc": ["cookie", "extra"],
"msg": "Extra inputs are not permitted",
"input": "track-me-here-too",
}
]
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"type": "value_error.extra",
"loc": ["cookie", "extra"],
"msg": "extra fields not permitted",
}
]
}
)
)
def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == snapshot(
{
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/items/": {
"get": {
"summary": "Read Items",
"operationId": "read_items_items__get",
"parameters": [
{
"name": "session_id",
"in": "cookie",
"required": True,
"schema": {"type": "string", "title": "Session Id"},
},
{
"name": "fatebook_tracker",
"in": "cookie",
"required": False,
"schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Fatebook Tracker",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"type": "string",
"title": "Fatebook Tracker",
}
),
},
{
"name": "googall_tracker",
"in": "cookie",
"required": False,
"schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Googall Tracker",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"type": "string",
"title": "Googall Tracker",
}
),
},
],
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
}
},
"components": {
"schemas": {
"HTTPValidationError": {
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ValidationError"
},
"type": "array",
"title": "Detail",
}
},
"type": "object",
"title": "HTTPValidationError",
},
"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",
},
}
},
}
)

0
tests/test_tutorial/test_header_param_models/__init__.py

238
tests/test_tutorial/test_header_param_models/test_tutorial001.py

@ -0,0 +1,238 @@
import importlib
import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient
from inline_snapshot import snapshot
from tests.utils import needs_py39, needs_py310
@pytest.fixture(
name="client",
params=[
"tutorial001",
pytest.param("tutorial001_py39", marks=needs_py39),
pytest.param("tutorial001_py310", marks=needs_py310),
"tutorial001_an",
pytest.param("tutorial001_an_py39", marks=needs_py39),
pytest.param("tutorial001_an_py310", marks=needs_py310),
],
)
def get_client(request: pytest.FixtureRequest):
mod = importlib.import_module(f"docs_src.header_param_models.{request.param}")
client = TestClient(mod.app)
return client
def test_header_param_model(client: TestClient):
response = client.get(
"/items/",
headers=[
("save-data", "true"),
("if-modified-since", "yesterday"),
("traceparent", "123"),
("x-tag", "one"),
("x-tag", "two"),
],
)
assert response.status_code == 200
assert response.json() == {
"host": "testserver",
"save_data": True,
"if_modified_since": "yesterday",
"traceparent": "123",
"x_tag": ["one", "two"],
}
def test_header_param_model_defaults(client: TestClient):
response = client.get("/items/", headers=[("save-data", "true")])
assert response.status_code == 200
assert response.json() == {
"host": "testserver",
"save_data": True,
"if_modified_since": None,
"traceparent": None,
"x_tag": [],
}
def test_header_param_model_invalid(client: TestClient):
response = client.get("/items/")
assert response.status_code == 422
assert response.json() == snapshot(
{
"detail": [
IsDict(
{
"type": "missing",
"loc": ["header", "save_data"],
"msg": "Field required",
"input": {
"x_tag": [],
"host": "testserver",
"accept": "*/*",
"accept-encoding": "gzip, deflate",
"connection": "keep-alive",
"user-agent": "testclient",
},
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"type": "value_error.missing",
"loc": ["header", "save_data"],
"msg": "field required",
}
)
]
}
)
def test_header_param_model_extra(client: TestClient):
response = client.get(
"/items/", headers=[("save-data", "true"), ("tool", "plumbus")]
)
assert response.status_code == 200, response.text
assert response.json() == snapshot(
{
"host": "testserver",
"save_data": True,
"if_modified_since": None,
"traceparent": None,
"x_tag": [],
}
)
def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == snapshot(
{
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/items/": {
"get": {
"summary": "Read Items",
"operationId": "read_items_items__get",
"parameters": [
{
"name": "host",
"in": "header",
"required": True,
"schema": {"type": "string", "title": "Host"},
},
{
"name": "save_data",
"in": "header",
"required": True,
"schema": {"type": "boolean", "title": "Save Data"},
},
{
"name": "if_modified_since",
"in": "header",
"required": False,
"schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "If Modified Since",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"type": "string",
"title": "If Modified Since",
}
),
},
{
"name": "traceparent",
"in": "header",
"required": False,
"schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Traceparent",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"type": "string",
"title": "Traceparent",
}
),
},
{
"name": "x_tag",
"in": "header",
"required": False,
"schema": {
"type": "array",
"items": {"type": "string"},
"default": [],
"title": "X Tag",
},
},
],
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
}
},
"components": {
"schemas": {
"HTTPValidationError": {
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ValidationError"
},
"type": "array",
"title": "Detail",
}
},
"type": "object",
"title": "HTTPValidationError",
},
"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",
},
}
},
}
)

249
tests/test_tutorial/test_header_param_models/test_tutorial002.py

@ -0,0 +1,249 @@
import importlib
import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient
from inline_snapshot import snapshot
from tests.utils import needs_py39, needs_py310, needs_pydanticv1, needs_pydanticv2
@pytest.fixture(
name="client",
params=[
pytest.param("tutorial002", marks=needs_pydanticv2),
pytest.param("tutorial002_py310", marks=[needs_py310, needs_pydanticv2]),
pytest.param("tutorial002_an", marks=needs_pydanticv2),
pytest.param("tutorial002_an_py39", marks=[needs_py39, needs_pydanticv2]),
pytest.param("tutorial002_an_py310", marks=[needs_py310, needs_pydanticv2]),
pytest.param("tutorial002_pv1", marks=[needs_pydanticv1, needs_pydanticv1]),
pytest.param("tutorial002_pv1_py310", marks=[needs_py310, needs_pydanticv1]),
pytest.param("tutorial002_pv1_an", marks=[needs_pydanticv1]),
pytest.param("tutorial002_pv1_an_py39", marks=[needs_py39, needs_pydanticv1]),
pytest.param("tutorial002_pv1_an_py310", marks=[needs_py310, needs_pydanticv1]),
],
)
def get_client(request: pytest.FixtureRequest):
mod = importlib.import_module(f"docs_src.header_param_models.{request.param}")
client = TestClient(mod.app)
client.headers.clear()
return client
def test_header_param_model(client: TestClient):
response = client.get(
"/items/",
headers=[
("save-data", "true"),
("if-modified-since", "yesterday"),
("traceparent", "123"),
("x-tag", "one"),
("x-tag", "two"),
],
)
assert response.status_code == 200, response.text
assert response.json() == {
"host": "testserver",
"save_data": True,
"if_modified_since": "yesterday",
"traceparent": "123",
"x_tag": ["one", "two"],
}
def test_header_param_model_defaults(client: TestClient):
response = client.get("/items/", headers=[("save-data", "true")])
assert response.status_code == 200
assert response.json() == {
"host": "testserver",
"save_data": True,
"if_modified_since": None,
"traceparent": None,
"x_tag": [],
}
def test_header_param_model_invalid(client: TestClient):
response = client.get("/items/")
assert response.status_code == 422
assert response.json() == snapshot(
{
"detail": [
IsDict(
{
"type": "missing",
"loc": ["header", "save_data"],
"msg": "Field required",
"input": {"x_tag": [], "host": "testserver"},
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"type": "value_error.missing",
"loc": ["header", "save_data"],
"msg": "field required",
}
)
]
}
)
def test_header_param_model_extra(client: TestClient):
response = client.get(
"/items/", headers=[("save-data", "true"), ("tool", "plumbus")]
)
assert response.status_code == 422, response.text
assert response.json() == snapshot(
{
"detail": [
IsDict(
{
"type": "extra_forbidden",
"loc": ["header", "tool"],
"msg": "Extra inputs are not permitted",
"input": "plumbus",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"type": "value_error.extra",
"loc": ["header", "tool"],
"msg": "extra fields not permitted",
}
)
]
}
)
def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == snapshot(
{
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/items/": {
"get": {
"summary": "Read Items",
"operationId": "read_items_items__get",
"parameters": [
{
"name": "host",
"in": "header",
"required": True,
"schema": {"type": "string", "title": "Host"},
},
{
"name": "save_data",
"in": "header",
"required": True,
"schema": {"type": "boolean", "title": "Save Data"},
},
{
"name": "if_modified_since",
"in": "header",
"required": False,
"schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "If Modified Since",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"type": "string",
"title": "If Modified Since",
}
),
},
{
"name": "traceparent",
"in": "header",
"required": False,
"schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Traceparent",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"type": "string",
"title": "Traceparent",
}
),
},
{
"name": "x_tag",
"in": "header",
"required": False,
"schema": {
"type": "array",
"items": {"type": "string"},
"default": [],
"title": "X Tag",
},
},
],
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
}
},
"components": {
"schemas": {
"HTTPValidationError": {
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ValidationError"
},
"type": "array",
"title": "Detail",
}
},
"type": "object",
"title": "HTTPValidationError",
},
"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",
},
}
},
}
)

0
tests/test_tutorial/test_query_param_models/__init__.py

260
tests/test_tutorial/test_query_param_models/test_tutorial001.py

@ -0,0 +1,260 @@
import importlib
import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient
from inline_snapshot import snapshot
from tests.utils import needs_py39, needs_py310
@pytest.fixture(
name="client",
params=[
"tutorial001",
pytest.param("tutorial001_py39", marks=needs_py39),
pytest.param("tutorial001_py310", marks=needs_py310),
"tutorial001_an",
pytest.param("tutorial001_an_py39", marks=needs_py39),
pytest.param("tutorial001_an_py310", marks=needs_py310),
],
)
def get_client(request: pytest.FixtureRequest):
mod = importlib.import_module(f"docs_src.query_param_models.{request.param}")
client = TestClient(mod.app)
return client
def test_query_param_model(client: TestClient):
response = client.get(
"/items/",
params={
"limit": 10,
"offset": 5,
"order_by": "updated_at",
"tags": ["tag1", "tag2"],
},
)
assert response.status_code == 200
assert response.json() == {
"limit": 10,
"offset": 5,
"order_by": "updated_at",
"tags": ["tag1", "tag2"],
}
def test_query_param_model_defaults(client: TestClient):
response = client.get("/items/")
assert response.status_code == 200
assert response.json() == {
"limit": 100,
"offset": 0,
"order_by": "created_at",
"tags": [],
}
def test_query_param_model_invalid(client: TestClient):
response = client.get(
"/items/",
params={
"limit": 150,
"offset": -1,
"order_by": "invalid",
},
)
assert response.status_code == 422
assert response.json() == snapshot(
IsDict(
{
"detail": [
{
"type": "less_than_equal",
"loc": ["query", "limit"],
"msg": "Input should be less than or equal to 100",
"input": "150",
"ctx": {"le": 100},
},
{
"type": "greater_than_equal",
"loc": ["query", "offset"],
"msg": "Input should be greater than or equal to 0",
"input": "-1",
"ctx": {"ge": 0},
},
{
"type": "literal_error",
"loc": ["query", "order_by"],
"msg": "Input should be 'created_at' or 'updated_at'",
"input": "invalid",
"ctx": {"expected": "'created_at' or 'updated_at'"},
},
]
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"type": "value_error.number.not_le",
"loc": ["query", "limit"],
"msg": "ensure this value is less than or equal to 100",
"ctx": {"limit_value": 100},
},
{
"type": "value_error.number.not_ge",
"loc": ["query", "offset"],
"msg": "ensure this value is greater than or equal to 0",
"ctx": {"limit_value": 0},
},
{
"type": "value_error.const",
"loc": ["query", "order_by"],
"msg": "unexpected value; permitted: 'created_at', 'updated_at'",
"ctx": {
"given": "invalid",
"permitted": ["created_at", "updated_at"],
},
},
]
}
)
)
def test_query_param_model_extra(client: TestClient):
response = client.get(
"/items/",
params={
"limit": 10,
"offset": 5,
"order_by": "updated_at",
"tags": ["tag1", "tag2"],
"tool": "plumbus",
},
)
assert response.status_code == 200
assert response.json() == {
"limit": 10,
"offset": 5,
"order_by": "updated_at",
"tags": ["tag1", "tag2"],
}
def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == snapshot(
{
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/items/": {
"get": {
"summary": "Read Items",
"operationId": "read_items_items__get",
"parameters": [
{
"name": "limit",
"in": "query",
"required": False,
"schema": {
"type": "integer",
"maximum": 100,
"exclusiveMinimum": 0,
"default": 100,
"title": "Limit",
},
},
{
"name": "offset",
"in": "query",
"required": False,
"schema": {
"type": "integer",
"minimum": 0,
"default": 0,
"title": "Offset",
},
},
{
"name": "order_by",
"in": "query",
"required": False,
"schema": {
"enum": ["created_at", "updated_at"],
"type": "string",
"default": "created_at",
"title": "Order By",
},
},
{
"name": "tags",
"in": "query",
"required": False,
"schema": {
"type": "array",
"items": {"type": "string"},
"default": [],
"title": "Tags",
},
},
],
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
}
},
"components": {
"schemas": {
"HTTPValidationError": {
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ValidationError"
},
"type": "array",
"title": "Detail",
}
},
"type": "object",
"title": "HTTPValidationError",
},
"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",
},
}
},
}
)

282
tests/test_tutorial/test_query_param_models/test_tutorial002.py

@ -0,0 +1,282 @@
import importlib
import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient
from inline_snapshot import snapshot
from tests.utils import needs_py39, needs_py310, needs_pydanticv1, needs_pydanticv2
@pytest.fixture(
name="client",
params=[
pytest.param("tutorial002", marks=needs_pydanticv2),
pytest.param("tutorial002_py39", marks=[needs_py39, needs_pydanticv2]),
pytest.param("tutorial002_py310", marks=[needs_py310, needs_pydanticv2]),
pytest.param("tutorial002_an", marks=needs_pydanticv2),
pytest.param("tutorial002_an_py39", marks=[needs_py39, needs_pydanticv2]),
pytest.param("tutorial002_an_py310", marks=[needs_py310, needs_pydanticv2]),
pytest.param("tutorial002_pv1", marks=[needs_pydanticv1, needs_pydanticv1]),
pytest.param("tutorial002_pv1_py39", marks=[needs_py39, needs_pydanticv1]),
pytest.param("tutorial002_pv1_py310", marks=[needs_py310, needs_pydanticv1]),
pytest.param("tutorial002_pv1_an", marks=[needs_pydanticv1]),
pytest.param("tutorial002_pv1_an_py39", marks=[needs_py39, needs_pydanticv1]),
pytest.param("tutorial002_pv1_an_py310", marks=[needs_py310, needs_pydanticv1]),
],
)
def get_client(request: pytest.FixtureRequest):
mod = importlib.import_module(f"docs_src.query_param_models.{request.param}")
client = TestClient(mod.app)
return client
def test_query_param_model(client: TestClient):
response = client.get(
"/items/",
params={
"limit": 10,
"offset": 5,
"order_by": "updated_at",
"tags": ["tag1", "tag2"],
},
)
assert response.status_code == 200
assert response.json() == {
"limit": 10,
"offset": 5,
"order_by": "updated_at",
"tags": ["tag1", "tag2"],
}
def test_query_param_model_defaults(client: TestClient):
response = client.get("/items/")
assert response.status_code == 200
assert response.json() == {
"limit": 100,
"offset": 0,
"order_by": "created_at",
"tags": [],
}
def test_query_param_model_invalid(client: TestClient):
response = client.get(
"/items/",
params={
"limit": 150,
"offset": -1,
"order_by": "invalid",
},
)
assert response.status_code == 422
assert response.json() == snapshot(
IsDict(
{
"detail": [
{
"type": "less_than_equal",
"loc": ["query", "limit"],
"msg": "Input should be less than or equal to 100",
"input": "150",
"ctx": {"le": 100},
},
{
"type": "greater_than_equal",
"loc": ["query", "offset"],
"msg": "Input should be greater than or equal to 0",
"input": "-1",
"ctx": {"ge": 0},
},
{
"type": "literal_error",
"loc": ["query", "order_by"],
"msg": "Input should be 'created_at' or 'updated_at'",
"input": "invalid",
"ctx": {"expected": "'created_at' or 'updated_at'"},
},
]
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"type": "value_error.number.not_le",
"loc": ["query", "limit"],
"msg": "ensure this value is less than or equal to 100",
"ctx": {"limit_value": 100},
},
{
"type": "value_error.number.not_ge",
"loc": ["query", "offset"],
"msg": "ensure this value is greater than or equal to 0",
"ctx": {"limit_value": 0},
},
{
"type": "value_error.const",
"loc": ["query", "order_by"],
"msg": "unexpected value; permitted: 'created_at', 'updated_at'",
"ctx": {
"given": "invalid",
"permitted": ["created_at", "updated_at"],
},
},
]
}
)
)
def test_query_param_model_extra(client: TestClient):
response = client.get(
"/items/",
params={
"limit": 10,
"offset": 5,
"order_by": "updated_at",
"tags": ["tag1", "tag2"],
"tool": "plumbus",
},
)
assert response.status_code == 422
assert response.json() == snapshot(
{
"detail": [
IsDict(
{
"type": "extra_forbidden",
"loc": ["query", "tool"],
"msg": "Extra inputs are not permitted",
"input": "plumbus",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"type": "value_error.extra",
"loc": ["query", "tool"],
"msg": "extra fields not permitted",
}
)
]
}
)
def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == snapshot(
{
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/items/": {
"get": {
"summary": "Read Items",
"operationId": "read_items_items__get",
"parameters": [
{
"name": "limit",
"in": "query",
"required": False,
"schema": {
"type": "integer",
"maximum": 100,
"exclusiveMinimum": 0,
"default": 100,
"title": "Limit",
},
},
{
"name": "offset",
"in": "query",
"required": False,
"schema": {
"type": "integer",
"minimum": 0,
"default": 0,
"title": "Offset",
},
},
{
"name": "order_by",
"in": "query",
"required": False,
"schema": {
"enum": ["created_at", "updated_at"],
"type": "string",
"default": "created_at",
"title": "Order By",
},
},
{
"name": "tags",
"in": "query",
"required": False,
"schema": {
"type": "array",
"items": {"type": "string"},
"default": [],
"title": "Tags",
},
},
],
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
}
},
"components": {
"schemas": {
"HTTPValidationError": {
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ValidationError"
},
"type": "array",
"title": "Detail",
}
},
"type": "object",
"title": "HTTPValidationError",
},
"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",
},
}
},
}
)
Loading…
Cancel
Save