Compare commits

..

60 Commits

Author SHA1 Message Date
Sebastián Ramírez
4721405ef7 🔖 Release version 0.98.0 2023-06-22 19:58:22 +02:00
Sebastián Ramírez
8066f85b3f 📝 Update release notes 2023-06-22 19:57:25 +02:00
github-actions
2ffb08d0bc 📝 Update release notes 2023-06-22 17:52:55 +00:00
dependabot[bot]
d1805ef466 ⬆ Bump ruff from 0.0.272 to 0.0.275 (#9721)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-22 19:52:20 +02:00
github-actions
41d774ed6d 📝 Update release notes 2023-06-22 17:44:21 +00:00
dependabot[bot]
836ac56203 ⬆ Update uvicorn[standard] requirement from <0.21.0,>=0.12.0 to >=0.12.0,<0.23.0 (#9463)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-22 19:43:44 +02:00
github-actions
a01c2ca3dd 📝 Update release notes 2023-06-22 17:43:29 +00:00
dependabot[bot]
6553243dbf ⬆ Bump mypy from 1.3.0 to 1.4.0 (#9719)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-22 19:42:53 +02:00
github-actions
fdc713428e 📝 Update release notes 2023-06-22 17:26:46 +00:00
dependabot[bot]
60343161ea ⬆ Update pre-commit requirement from <3.0.0,>=2.17.0 to >=2.17.0,<4.0.0 (#9251)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-22 19:26:01 +02:00
github-actions
586de94ca1 📝 Update release notes 2023-06-22 17:12:59 +00:00
dependabot[bot]
56bc75372f ⬆ Bump pypa/gh-action-pypi-publish from 1.8.5 to 1.8.6 (#9482)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-22 19:12:24 +02:00
github-actions
4842dfadcf 📝 Update release notes 2023-06-22 17:07:05 +00:00
я котик пур-пур
cfc06a3a3d 📝 Update docs on Pydantic using ujson internally (#5804)
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2023-06-22 17:06:25 +00:00
github-actions
c812b42293 📝 Update release notes 2023-06-22 17:04:50 +00:00
ivan-abc
68ce5b37dc ✏ Rewording in docs/en/docs/tutorial/debugging.md (#9581)
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2023-06-22 17:04:16 +00:00
github-actions
0dc9a377dc 📝 Update release notes 2023-06-22 17:02:08 +00:00
Pankaj Kumar
d82700c96d ✏️ Fix tooltips for light/dark theme toggler in docs (#9588) 2023-06-22 19:01:28 +02:00
github-actions
fafe670db6 📝 Update release notes 2023-06-22 16:53:00 +00:00
TabarakoAkula
47342cdd18 🌐 Add Russian translation for docs/ru/docs/tutorial/metadata.md (#9681)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: ivan-abc <36765187+ivan-abc@users.noreply.github.com>
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2023-06-22 18:52:24 +02:00
github-actions
e76dd3e70d 📝 Update release notes 2023-06-22 16:44:41 +00:00
Marcel Sander
e5f3d6a5eb 📝 Add german blog post (Domain-driven Design mit Python und FastAPI) (#9261) 2023-06-22 18:44:05 +02:00
github-actions
7217f167d4 📝 Update release notes 2023-06-22 16:41:05 +00:00
github-actions
762ede2bec 📝 Update release notes 2023-06-22 16:40:50 +00:00
jyothish-mohan
a3b1478221 ✏️ Tweak wording in docs/en/docs/tutorial/security/index.md (#9561) 2023-06-22 18:40:32 +02:00
Lili_DL
41ff599d4b 🌐 Fix typo in Spanish translation for docs/es/docs/tutorial/first-steps.md (#9571)
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2023-06-22 18:40:17 +02:00
github-actions
b1f27c96c4 📝 Update release notes 2023-06-22 16:35:04 +00:00
TabarakoAkula
4c401aef0f 🌐 Add Russian translation for docs/tutorial/path-operation-configuration.md (#9696)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: ivan-abc <36765187+ivan-abc@users.noreply.github.com>
Co-authored-by: Alexandr <alexandrhub@vk.com>
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2023-06-22 16:33:47 +00:00
github-actions
c7dad1bb59 📝 Update release notes 2023-06-22 16:33:28 +00:00
Alexandr
0ef164e1ee 📝 Update Annotated notes in docs/en/docs/tutorial/schema-extra-example.md (#9620)
Update for docs/tutorial/schema-extra-example.md

When working on the translation, I noticed that this page is missing the annotated tips that can be found in the rest of the documentation (I checked, and it's the only page where they're missing).
2023-06-22 18:32:53 +02:00
github-actions
fd6a78cbfe 📝 Update release notes 2023-06-22 16:20:40 +00:00
lordqyxz
fa7474b2e8 🌐 Add Chinese translation for docs/zh/docs/advanced/security/index.md (#9666)
Co-authored-by: shiyz <shiyz@finchina.com>
2023-06-22 18:19:49 +02:00
github-actions
2f0541f17a 📝 Update release notes 2023-06-22 16:18:54 +00:00
雨过初晴
804a0a90cf 🌐 Add Chinese translations for docs/zh/docs/advanced/settings.md (#9652) 2023-06-22 18:18:04 +02:00
github-actions
847befdc1d 📝 Update release notes 2023-06-22 16:17:50 +00:00
雨过初晴
4a7b21483b 🌐 Add Chinese translations for docs/zh/docs/advanced/websockets.md (#9651) 2023-06-22 18:17:12 +02:00
github-actions
1182b36362 📝 Update release notes 2023-06-22 16:16:43 +00:00
吴定焕
e17cacfee4 🌐 Add Chinese translation for docs/zh/docs/tutorial/testing.md (#9641)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-06-22 18:16:06 +02:00
github-actions
234cecb5bf 📝 Update release notes 2023-06-22 16:14:54 +00:00
ivan-abc
612cbee165 🌐 Add Russian translation for docs/tutorial/extra-models.md (#9619)
Co-authored-by: Alexandr <alexandrhub@vk.com>
2023-06-22 18:14:16 +02:00
github-actions
223ed67682 📝 Update release notes 2023-06-22 14:30:35 +00:00
ivan-abc
a2a0119c14 🌐 Add Russian translation for docs/tutorial/cors.md (#9608)
Co-authored-by: Alexandr <alexandrhub@vk.com>
Co-authored-by: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com>
2023-06-22 16:29:56 +02:00
github-actions
09319d6271 📝 Update release notes 2023-06-22 14:29:41 +00:00
Michał Brotoń
a92e9c957a 🌐 Add Polish translation for docs/pl/docs/features.md (#5348)
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2023-06-22 16:29:05 +02:00
github-actions
7505f24f2e 📝 Update release notes 2023-06-22 11:47:12 +00:00
Alexandr
57727fa4e0 🌐 Add Russian translation for docs/ru/docs/tutorial/body-nested-models.md (#9605)
Co-authored-by: ivan-abc <36765187+ivan-abc@users.noreply.github.com>
Co-authored-by: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com>
2023-06-22 13:46:36 +02:00
github-actions
a2aede32b4 📝 Update release notes 2023-06-22 11:43:21 +00:00
Ricardo Castro
7c66ec8a8b ✏️ Fix typo Annotation -> Annotated in docs/en/docs/tutorial/query-params-str-validations.md (#9625)
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2023-06-22 11:42:48 +00:00
github-actions
d47eea9bb6 📝 Update release notes 2023-06-22 11:35:49 +00:00
Michał Górny
74de9a7b15 🔧 Set minimal hatchling version needed to build the package (#9240)
Set minimal hatchling version needed to build the package

Set the minimal hatchling version that is needed to build fastapi to
1.13.0.  Older versions fail to build because they do not recognize
the trove classifiers used, e.g. 1.12.2 yields:

    ValueError: Unknown classifier in field `project.classifiers`: Framework :: Pydantic

Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2023-06-22 13:35:12 +02:00
github-actions
3279f0ba63 📝 Update release notes 2023-06-22 11:32:46 +00:00
Jacob Coffee
428376d285 📝 Add repo link to PyPI (#9559) 2023-06-22 13:32:09 +02:00
github-actions
05c5ce3689 📝 Update release notes 2023-06-22 11:26:45 +00:00
Ryan Russell
b4b39d3359 ✏️ Fix typos in data for tests (#4958)
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2023-06-22 11:26:11 +00:00
github-actions
2f048f7199 📝 Update release notes 2023-06-22 11:20:49 +00:00
Harsha Laxman
2cef119cd7 📝 Use in memory database for testing SQL in docs (#1223)
Co-authored-by: Harsha Laxman <harsh@Harshas-MacBook-Pro.local>
Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-06-22 11:20:12 +00:00
github-actions
dd1c2018dc 📝 Update release notes 2023-06-22 10:38:27 +00:00
cyberlis
e94c13ce74 Add allow disabling redirect_slashes at the FastAPI app level (#3432)
Co-authored-by: Denis Lisovik <ckyberlis@gmail.com>
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2023-06-22 10:37:50 +00:00
github-actions
b7ce10079e 📝 Update release notes 2023-06-19 12:34:13 +00:00
Sebastián Ramírez
87d5870314 🔧 Update sponsors, add Flint (#9699)
* 🔧 Set up sponsor Flint

* 🔧 Add configs for Flint sponsor
2023-06-19 12:33:32 +00:00
275 changed files with 6666 additions and 14554 deletions

View File

@@ -32,7 +32,7 @@ jobs:
- name: Build distribution
run: python -m build
- name: Publish
uses: pypa/gh-action-pypi-publish@v1.8.5
uses: pypa/gh-action-pypi-publish@v1.8.6
with:
password: ${{ secrets.PYPI_API_TOKEN }}
- name: Dump GitHub context

View File

@@ -25,12 +25,10 @@ jobs:
id: cache
with:
path: ${{ env.pythonLocation }}
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-pydantic-v2-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v03
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v03
- name: Install Dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: pip install -r requirements-tests.txt
- name: Install Pydantic v2
run: pip install --pre "pydantic>=2.0.0b2,<3.0.0"
- name: Lint
run: bash scripts/lint.sh
@@ -39,7 +37,6 @@ jobs:
strategy:
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
pydantic-version: ["pydantic-v1", "pydantic-v2"]
fail-fast: false
steps:
- uses: actions/checkout@v3
@@ -54,16 +51,10 @@ jobs:
id: cache
with:
path: ${{ env.pythonLocation }}
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ matrix.pydantic-version }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v03
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v03
- name: Install Dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: pip install -r requirements-tests.txt
- name: Install Pydantic v1
if: matrix.pydantic-version == 'pydantic-v1'
run: pip install "pydantic>=1.10.0,<2.0.0"
- name: Install Pydantic v2
if: matrix.pydantic-version == 'pydantic-v2'
run: pip install --pre "pydantic>=2.0.0b2,<3.0.0"
- run: mkdir coverage
- name: Test
run: bash scripts/test.sh

View File

@@ -446,7 +446,6 @@ To understand more about it, see the section <a href="https://fastapi.tiangolo.c
Used by Pydantic:
* <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - for faster JSON <abbr title="converting the string that comes from an HTTP request into Python data">"parsing"</abbr>.
* <a href="https://github.com/JoshData/python-email-validator" target="_blank"><code>email_validator</code></a> - for email validation.
Used by Starlette:

View File

@@ -441,7 +441,6 @@ To understand more about it, see the section <a href="https://fastapi.tiangolo.c
Used by Pydantic:
* <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - for faster JSON <abbr title="converting the string that comes from an HTTP request into Python data">"parsing"</abbr>.
* <a href="https://github.com/JoshData/python-email-validator" target="_blank"><code>email_validator</code></a> - for email validation.
Used by Starlette:

View File

@@ -440,7 +440,6 @@ To understand more about it, see the section <a href="https://fastapi.tiangolo.c
Used by Pydantic:
* <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - for faster JSON <abbr title="converting the string that comes from an HTTP request into Python data">"parsing"</abbr>.
* <a href="https://github.com/JoshData/python-email-validator" target="_blank"><code>email_validator</code></a> - for email validation.
Used by Starlette:

View File

@@ -233,6 +233,10 @@ articles:
link: https://medium.com/@krishnardt365/fastapi-docker-and-postgres-91943e71be92
title: Fastapi, Docker(Docker compose) and Postgres
german:
- author: Marcel Sander (actidoo)
author_link: https://www.actidoo.com
link: https://www.actidoo.com/de/blog/python-fastapi-domain-driven-design
title: Domain-driven Design mit Python und FastAPI
- author: Nico Axtmann
author_link: https://twitter.com/_nicoax
link: https://blog.codecentric.de/2019/08/inbetriebnahme-eines-scikit-learn-modells-mit-onnx-und-fastapi/

View File

@@ -31,3 +31,6 @@ bronze:
- url: https://www.exoflare.com/open-source/?utm_source=FastAPI&utm_campaign=open_source
title: Biosecurity risk assessments made easy.
img: https://fastapi.tiangolo.com/img/sponsors/exoflare.png
- url: https://www.flint.sh
title: IT expertise, consulting and development by passionate people
img: https://fastapi.tiangolo.com/img/sponsors/flint.png

View File

@@ -16,3 +16,4 @@ logins:
- armand-sauzay
- databento-bot
- nanram22
- Flint-company

View File

@@ -44,7 +44,7 @@ So the new file structure looks like:
First, we create a new database session with the new database.
For the tests we'll use a file `test.db` instead of `sql_app.db`.
We'll use an in-memory database that persists during the tests instead of the local file `sql_app.db`.
But the rest of the session code is more or less the same, we just copy it.

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -445,7 +445,6 @@ To understand more about it, see the section <a href="https://fastapi.tiangolo.c
Used by Pydantic:
* <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - for faster JSON <abbr title="converting the string that comes from an HTTP request into Python data">"parsing"</abbr>.
* <a href="https://github.com/JoshData/python-email-validator" target="_blank"><code>email_validator</code></a> - for email validation.
Used by Starlette:

View File

@@ -2,6 +2,49 @@
## Latest Changes
## 0.98.0
### Features
* ✨ Allow disabling `redirect_slashes` at the FastAPI app level. PR [#3432](https://github.com/tiangolo/fastapi/pull/3432) by [@cyberlis](https://github.com/cyberlis).
### Docs
* 📝 Update docs on Pydantic using ujson internally. PR [#5804](https://github.com/tiangolo/fastapi/pull/5804) by [@mvasilkov](https://github.com/mvasilkov).
* ✏ Rewording in `docs/en/docs/tutorial/debugging.md`. PR [#9581](https://github.com/tiangolo/fastapi/pull/9581) by [@ivan-abc](https://github.com/ivan-abc).
* 📝 Add german blog post (Domain-driven Design mit Python und FastAPI). PR [#9261](https://github.com/tiangolo/fastapi/pull/9261) by [@msander](https://github.com/msander).
* ✏️ Tweak wording in `docs/en/docs/tutorial/security/index.md`. PR [#9561](https://github.com/tiangolo/fastapi/pull/9561) by [@jyothish-mohan](https://github.com/jyothish-mohan).
* 📝 Update `Annotated` notes in `docs/en/docs/tutorial/schema-extra-example.md`. PR [#9620](https://github.com/tiangolo/fastapi/pull/9620) by [@Alexandrhub](https://github.com/Alexandrhub).
* ✏️ Fix typo `Annotation` -> `Annotated` in `docs/en/docs/tutorial/query-params-str-validations.md`. PR [#9625](https://github.com/tiangolo/fastapi/pull/9625) by [@mccricardo](https://github.com/mccricardo).
* 📝 Use in memory database for testing SQL in docs. PR [#1223](https://github.com/tiangolo/fastapi/pull/1223) by [@HarshaLaxman](https://github.com/HarshaLaxman).
### Translations
* 🌐 Add Russian translation for `docs/ru/docs/tutorial/metadata.md`. PR [#9681](https://github.com/tiangolo/fastapi/pull/9681) by [@TabarakoAkula](https://github.com/TabarakoAkula).
* 🌐 Fix typo in Spanish translation for `docs/es/docs/tutorial/first-steps.md`. PR [#9571](https://github.com/tiangolo/fastapi/pull/9571) by [@lilidl-nft](https://github.com/lilidl-nft).
* 🌐 Add Russian translation for `docs/tutorial/path-operation-configuration.md`. PR [#9696](https://github.com/tiangolo/fastapi/pull/9696) by [@TabarakoAkula](https://github.com/TabarakoAkula).
* 🌐 Add Chinese translation for `docs/zh/docs/advanced/security/index.md`. PR [#9666](https://github.com/tiangolo/fastapi/pull/9666) by [@lordqyxz](https://github.com/lordqyxz).
* 🌐 Add Chinese translations for `docs/zh/docs/advanced/settings.md`. PR [#9652](https://github.com/tiangolo/fastapi/pull/9652) by [@ChoyeonChern](https://github.com/ChoyeonChern).
* 🌐 Add Chinese translations for `docs/zh/docs/advanced/websockets.md`. PR [#9651](https://github.com/tiangolo/fastapi/pull/9651) by [@ChoyeonChern](https://github.com/ChoyeonChern).
* 🌐 Add Chinese translation for `docs/zh/docs/tutorial/testing.md`. PR [#9641](https://github.com/tiangolo/fastapi/pull/9641) by [@wdh99](https://github.com/wdh99).
* 🌐 Add Russian translation for `docs/tutorial/extra-models.md`. PR [#9619](https://github.com/tiangolo/fastapi/pull/9619) by [@ivan-abc](https://github.com/ivan-abc).
* 🌐 Add Russian translation for `docs/tutorial/cors.md`. PR [#9608](https://github.com/tiangolo/fastapi/pull/9608) by [@ivan-abc](https://github.com/ivan-abc).
* 🌐 Add Polish translation for `docs/pl/docs/features.md`. PR [#5348](https://github.com/tiangolo/fastapi/pull/5348) by [@mbroton](https://github.com/mbroton).
* 🌐 Add Russian translation for `docs/ru/docs/tutorial/body-nested-models.md`. PR [#9605](https://github.com/tiangolo/fastapi/pull/9605) by [@Alexandrhub](https://github.com/Alexandrhub).
### Internal
* ⬆ Bump ruff from 0.0.272 to 0.0.275. PR [#9721](https://github.com/tiangolo/fastapi/pull/9721) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Update uvicorn[standard] requirement from <0.21.0,>=0.12.0 to >=0.12.0,<0.23.0. PR [#9463](https://github.com/tiangolo/fastapi/pull/9463) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump mypy from 1.3.0 to 1.4.0. PR [#9719](https://github.com/tiangolo/fastapi/pull/9719) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Update pre-commit requirement from <3.0.0,>=2.17.0 to >=2.17.0,<4.0.0. PR [#9251](https://github.com/tiangolo/fastapi/pull/9251) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump pypa/gh-action-pypi-publish from 1.8.5 to 1.8.6. PR [#9482](https://github.com/tiangolo/fastapi/pull/9482) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ✏️ Fix tooltips for light/dark theme toggler in docs. PR [#9588](https://github.com/tiangolo/fastapi/pull/9588) by [@pankaj1707k](https://github.com/pankaj1707k).
* 🔧 Set minimal hatchling version needed to build the package. PR [#9240](https://github.com/tiangolo/fastapi/pull/9240) by [@mgorny](https://github.com/mgorny).
* 📝 Add repo link to PyPI. PR [#9559](https://github.com/tiangolo/fastapi/pull/9559) by [@JacobCoffee](https://github.com/JacobCoffee).
* ✏️ Fix typos in data for tests. PR [#4958](https://github.com/tiangolo/fastapi/pull/4958) by [@ryanrussell](https://github.com/ryanrussell).
* 🔧 Update sponsors, add Flint. PR [#9699](https://github.com/tiangolo/fastapi/pull/9699) by [@tiangolo](https://github.com/tiangolo).
* 👷 Lint in CI only once, only with one version of Python, run tests with all of them. PR [#9686](https://github.com/tiangolo/fastapi/pull/9686) by [@tiangolo](https://github.com/tiangolo).
## 0.97.0

View File

@@ -64,7 +64,7 @@ from myapp import app
# Some more code
```
in that case, the automatic variable inside of `myapp.py` will not have the variable `__name__` with a value of `"__main__"`.
in that case, the automatically created variable inside of `myapp.py` will not have the variable `__name__` with a value of `"__main__"`.
So, the line:

View File

@@ -44,7 +44,7 @@ To achieve that, first import:
=== "Python 3.6+"
In versions of Python below Python 3.9 you import `Annotation` from `typing_extensions`.
In versions of Python below Python 3.9 you import `Annotated` from `typing_extensions`.
It will already be installed with FastAPI.

View File

@@ -86,6 +86,9 @@ Here we pass an `example` of the data expected in `Body()`:
=== "Python 3.10+ non-Annotated"
!!! tip
Prefer to use the `Annotated` version if possible.
```Python hl_lines="18-23"
{!> ../../../docs_src/schema_extra_example/tutorial003_py310.py!}
```
@@ -138,6 +141,9 @@ Each specific example `dict` in the `examples` can contain:
=== "Python 3.10+ non-Annotated"
!!! tip
Prefer to use the `Annotated` version if possible.
```Python hl_lines="19-45"
{!> ../../../docs_src/schema_extra_example/tutorial004_py310.py!}
```

View File

@@ -26,7 +26,7 @@ That's what all the systems with "login with Facebook, Google, Twitter, GitHub"
### OAuth 1
There was an OAuth 1, which is very different from OAuth2, and more complex, as it included directly specifications on how to encrypt the communication.
There was an OAuth 1, which is very different from OAuth2, and more complex, as it included direct specifications on how to encrypt the communication.
It is not very popular or used nowadays.

View File

@@ -11,14 +11,14 @@ theme:
accent: amber
toggle:
icon: material/lightbulb
name: Switch to light mode
name: Switch to dark mode
- media: '(prefers-color-scheme: dark)'
scheme: slate
primary: teal
accent: amber
toggle:
icon: material/lightbulb-outline
name: Switch to dark mode
name: Switch to light mode
features:
- search.suggest
- search.highlight

View File

@@ -433,7 +433,6 @@ Para entender más al respecto revisa la sección <a href="https://fastapi.tiang
Usadas por Pydantic:
* <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - para <abbr title="convertir el string que viene de un HTTP request a datos de Python">"parsing"</abbr> de JSON más rápido.
* <a href="https://github.com/JoshData/python-email-validator" target="_blank"><code>email_validator</code></a> - para validación de emails.
Usados por Starlette:

View File

@@ -181,7 +181,7 @@ $ uvicorn main:my_awesome_api --reload
</div>
### Paso 3: crea un *operación de path*
### Paso 3: crea una *operación de path*
#### Path

View File

@@ -436,7 +436,6 @@ item: Item
استفاده شده توسط Pydantic:
* <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - برای <abbr title="تبدیل داده‌های موجود در درخواست‌های HTTP به داده پایتونی">"تجزیه (parse)"</abbr> سریع‌تر JSON .
* <a href="https://github.com/JoshData/python-email-validator" target="_blank"><code>email_validator</code></a> - برای اعتبارسنجی آدرس‌های ایمیل.
استفاده شده توسط Starlette:

View File

@@ -445,7 +445,6 @@ Pour en savoir plus, consultez la section <a href="https://fastapi.tiangolo.com/
Utilisées par Pydantic:
* <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - pour un <abbr title="convertit la chaine de caractère d'une requête HTTP en donnée Python">"décodage" <abbr title="JavaScript Object Notation">JSON</abbr></abbr> plus rapide.
* <a href="https://github.com/JoshData/python-email-validator" target="_blank"><code>email_validator</code></a> - pour la validation des adresses email.
Utilisées par Starlette :

View File

@@ -440,7 +440,6 @@ item: Item
בשימוש Pydantic:
- <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - <abbr title="המרת המחרוזת שמגיעה מבקשת HTTP למידע פייתון">"פרסור"</abbr> JSON.
- <a href="https://github.com/JoshData/python-email-validator" target="_blank"><code>email_validator</code></a> - לאימות כתובות אימייל.
בשימוש Starlette:

View File

@@ -441,7 +441,6 @@ To understand more about it, see the section <a href="https://fastapi.tiangolo.c
Used by Pydantic:
* <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - for faster JSON <abbr title="converting the string that comes from an HTTP request into Python data">"parsing"</abbr>.
* <a href="https://github.com/JoshData/python-email-validator" target="_blank"><code>email_validator</code></a> - for email validation.
Used by Starlette:

View File

@@ -438,7 +438,6 @@ To understand more about it, see the section <a href="https://fastapi.tiangolo.c
Used by Pydantic:
* <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - for faster JSON <abbr title="converting the string that comes from an HTTP request into Python data">"parsing"</abbr>.
* <a href="https://github.com/JoshData/python-email-validator" target="_blank"><code>email_validator</code></a> - for email validation.
Used by Starlette:

View File

@@ -431,7 +431,6 @@ item: Item
Pydantic によって使用されるもの:
- <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - より速い JSON への<abbr title="converting the string that comes from an HTTP request into Python data">"変換"</abbr>.
- <a href="https://github.com/JoshData/python-email-validator" target="_blank"><code>email_validator</code></a> - E メールの検証
Starlette によって使用されるもの:

View File

@@ -437,7 +437,6 @@ item: Item
Pydantic이 사용하는:
* <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - 더 빠른 JSON <abbr title="HTTP 요청에서 파이썬 데이터로 가는 문자열 변환">"파싱"</abbr>.
* <a href="https://github.com/JoshData/python-email-validator" target="_blank"><code>email_validator</code></a> - 이메일 유효성 검사.
Starlette이 사용하는:

View File

@@ -444,7 +444,6 @@ To understand more about it, see the section <a href="https://fastapi.tiangolo.c
Used by Pydantic:
* <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - for faster JSON <abbr title="converting the string that comes from an HTTP request into Python data">"parsing"</abbr>.
* <a href="https://github.com/JoshData/python-email-validator" target="_blank"><code>email_validator</code></a> - for email validation.
Used by Starlette:

200
docs/pl/docs/features.md Normal file
View File

@@ -0,0 +1,200 @@
# Cechy
## Cechy FastAPI
**FastAPI** zapewnia Ci następujące korzyści:
### Oparcie o standardy open
* <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank"><strong>OpenAPI</strong></a> do tworzenia API, w tym deklaracji <abbr title="znane również jako: paths, endpoints, routes">ścieżek</abbr> <abbr title="znane również jako metody HTTP, takie jak POST, GET, PUT, DELETE">operacji</abbr>, parametrów, <abbr title="po angielsku: body requests">ciał zapytań</abbr>, bezpieczeństwa, itp.
* Automatyczna dokumentacja modelu danych za pomocą <a href="https://json-schema.org/" class="external-link" target="_blank"><strong>JSON Schema</strong></a> (ponieważ OpenAPI bazuje na JSON Schema).
* Zaprojektowane z myślą o zgodności z powyższymi standardami zamiast dodawania ich obsługi po fakcie.
* Możliwość automatycznego **generowania kodu klienta** w wielu językach.
### Automatyczna dokumentacja
Interaktywna dokumentacja i webowe interfejsy do eksploracji API. Z racji tego, że framework bazuje na OpenAPI, istnieje wiele opcji, z czego 2 są domyślnie dołączone.
* <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank"><strong>Swagger UI</strong></a>, z interaktywnym interfejsem - odpytuj i testuj swoje API bezpośrednio z przeglądarki.
![Swagger UI interakcja](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png)
* Alternatywna dokumentacja API z <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank"><strong>ReDoc</strong></a>.
![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png)
### Nowoczesny Python
Wszystko opiera się na standardowych deklaracjach typu **Python 3.6** (dzięki Pydantic). Brak nowej składni do uczenia. Po prostu standardowy, współczesny Python.
Jeśli potrzebujesz szybkiego przypomnienia jak używać deklaracji typów w Pythonie (nawet jeśli nie używasz FastAPI), sprawdź krótki samouczek: [Python Types](python-types.md){.internal-link target=_blank}.
Wystarczy, że napiszesz standardowe deklaracje typów Pythona:
```Python
from datetime import date
from pydantic import BaseModel
# Zadeklaruj parametr jako str
# i uzyskaj wsparcie edytora wewnątrz funkcji
def main(user_id: str):
return user_id
# Model Pydantic
class User(BaseModel):
id: int
name: str
joined: date
```
A one będą mogły zostać później użyte w następujący sposób:
```Python
my_user: User = User(id=3, name="John Doe", joined="2018-07-19")
second_user_data = {
"id": 4,
"name": "Mary",
"joined": "2018-11-30",
}
my_second_user: User = User(**second_user_data)
```
!!! info
`**second_user_data` oznacza:
Przekaż klucze i wartości słownika `second_user_data` bezpośrednio jako argumenty klucz-wartość, co jest równoznaczne z: `User(id=4, name="Mary", joined="2018-11-30")`
### Wsparcie edytora
Cały framework został zaprojektowany tak, aby był łatwy i intuicyjny w użyciu. Wszystkie pomysły zostały przetestowane na wielu edytorach jeszcze przed rozpoczęciem procesu tworzenia, aby zapewnić najlepsze wrażenia programistyczne.
Ostatnia ankieta <abbr title="coroczna ankieta przeprowadza w środowisku programistów języka Python">Python developer survey</abbr> jasno wskazuje, że <a href="https://www.jetbrains.com/research/python-developers-survey-2017/#tools-and-features" class="external-link" target="_blank">najczęściej używaną funkcjonalnością jest autouzupełnianie w edytorze</a>.
Cała struktura frameworku **FastAPI** jest na tym oparta. Autouzupełnianie działa wszędzie.
Rzadko będziesz musiał wracać do dokumentacji.
Oto, jak twój edytor może Ci pomóc:
* <a href="https://code.visualstudio.com/" class="external-link" target="_blank">Visual Studio Code</a>:
![wsparcie edytora](https://fastapi.tiangolo.com/img/vscode-completion.png)
* <a href="https://www.jetbrains.com/pycharm/" class="external-link" target="_blank">PyCharm</a>:
![wsparcie edytora](https://fastapi.tiangolo.com/img/pycharm-completion.png)
Otrzymasz uzupełnienie nawet w miejscach, w których normalnie uzupełnienia nie ma. Na przykład klucz "price" w treści JSON (który mógł być zagnieżdżony), który pochodzi z zapytania.
Koniec z wpisywaniem błędnych nazw kluczy, przechodzeniem tam i z powrotem w dokumentacji lub przewijaniem w górę i w dół, aby sprawdzić, czy w końcu użyłeś nazwy `username` czy `user_name`.
### Zwięzłość
Wszystko posiada sensowne **domyślne wartości**. Wszędzie znajdziesz opcjonalne konfiguracje. Wszystkie parametry możesz dostroić, aby zrobić to co potrzebujesz do zdefiniowania API.
Ale domyślnie wszystko **"po prostu działa"**.
### Walidacja
* Walidacja większości (lub wszystkich?) **typów danych** Pythona, w tym:
* Obiektów JSON (`dict`).
* Tablic JSON (`list`) ze zdefiniowanym typem elementów.
* Pól tekstowych (`str`) z określeniem minimalnej i maksymalnej długości.
* Liczb (`int`, `float`) z wartościami minimalnymi, maksymalnymi, itp.
* Walidacja bardziej egzotycznych typów danych, takich jak:
* URL.
* Email.
* UUID.
* ...i inne.
Cała walidacja jest obsługiwana przez ugruntowaną i solidną bibliotekę **Pydantic**.
### Bezpieczeństwo i uwierzytelnianie
Bezpieczeństwo i uwierzytelnianie jest zintegrowane. Bez żadnych kompromisów z bazami czy modelami danych.
Wszystkie schematy bezpieczeństwa zdefiniowane w OpenAPI, w tym:
* Podstawowy protokół HTTP.
* **OAuth2** (również z **tokenami JWT**). Sprawdź samouczek [OAuth2 with JWT](tutorial/security/oauth2-jwt.md){.internal-link target=_blank}.
* Klucze API w:
* Nagłówkach.
* Parametrach zapytań.
* Ciasteczkach, itp.
Plus wszystkie funkcje bezpieczeństwa Starlette (włączając w to **<abbr title="po angielsku: session cookies">ciasteczka sesyjne</abbr>**).
Wszystko zbudowane jako narzędzia i komponenty wielokrotnego użytku, które można łatwo zintegrować z systemami, magazynami oraz bazami danych - relacyjnymi, NoSQL, itp.
### Wstrzykiwanie Zależności
FastAPI zawiera niezwykle łatwy w użyciu, ale niezwykle potężny system <abbr title='Po angielsku: Dependency Injection. Znane również jako "components", "resources", "services", "providers"'><strong>Wstrzykiwania Zależności</strong></abbr>.
* Nawet zależności mogą mieć zależności, tworząc hierarchię lub **"graf" zależności**.
* Wszystko jest **obsługiwane automatycznie** przez framework.
* Wszystkie zależności mogą wymagać danych w żądaniach oraz rozszerzać ograniczenia i automatyczną dokumentację **<abbr title="po angielsku: path operations">operacji na ścieżce</abbr>**.
* **Automatyczna walidacja** parametrów *operacji na ścieżce* zdefiniowanych w zależnościach.
* Obsługa złożonych systemów uwierzytelniania użytkowników, **połączeń z bazami danych**, itp.
* Bazy danych, front end, itp. **bez kompromisów**, ale wciąż łatwe do integracji.
### Nieograniczone "wtyczki"
Lub ujmując to inaczej - brak potrzeby wtyczek. Importuj i używaj kod, który potrzebujesz.
Każda integracja została zaprojektowana tak, aby była tak prosta w użyciu (z zależnościami), że możesz utworzyć "wtyczkę" dla swojej aplikacji w 2 liniach kodu, używając tej samej struktury i składni, które są używane w *operacjach na ścieżce*.
### Testy
* 100% <abbr title="Ilość kodu, który jest automatycznie testowany">pokrycia kodu testami</abbr>.
* 100% <abbr title="Deklaracje typów Python - dzięki nim twój edytor i zewnętrzne narzędzia mogą zapewnić Ci lepsze wsparcie ">adnotacji typów</abbr>.
* Używany w aplikacjach produkcyjnych.
## Cechy Starlette
**FastAPI** jest w pełni kompatybilny z (oraz bazuje na) <a href="https://www.starlette.io/" class="external-link" target="_blank"><strong>Starlette</strong></a>. Tak więc każdy dodatkowy kod Starlette, który posiadasz, również będzie działał.
`FastAPI` jest w rzeczywistości podklasą `Starlette`, więc jeśli już znasz lub używasz Starlette, większość funkcji będzie działać w ten sam sposób.
Dzięki **FastAPI** otrzymujesz wszystkie funkcje **Starlette** (ponieważ FastAPI to po prostu Starlette na sterydach):
* Bardzo imponująca wydajność. Jest to <a href="https://github.com/encode/starlette#performance" class="external-link" target="_blank">jeden z najszybszych dostępnych frameworków Pythona, na równi z **NodeJS** i **Go**</a>.
* Wsparcie dla **WebSocket**.
* <abbr title='Zadania wykonywane w tle, bez zatrzymywania żądań, w tym samym procesie. Po angielsku: In-process background tasks'>Zadania w tle</abbr>.
* Eventy startup i shutdown.
* Klient testowy zbudowany na bazie biblioteki `requests`.
* **CORS**, GZip, pliki statyczne, streamy.
* Obsługa **sesji i ciasteczek**.
* 100% pokrycie testami.
* 100% adnotacji typów.
## Cechy Pydantic
**FastAPI** jest w pełni kompatybilny z (oraz bazuje na) <a href="https://pydantic-docs.helpmanual.io" class="external-link" target="_blank"><strong>Pydantic</strong></a>. Tak więc każdy dodatkowy kod Pydantic, który posiadasz, również będzie działał.
Wliczając w to zewnętrzne biblioteki, również oparte o Pydantic, takie jak <abbr title="Mapowanie obiektowo-relacyjne. Po angielsku: Object-Relational Mapper">ORM</abbr>, <abbr title="Object-Document Mapper">ODM</abbr> dla baz danych.
Oznacza to, że w wielu przypadkach możesz przekazać ten sam obiekt, który otrzymasz z żądania **bezpośrednio do bazy danych**, ponieważ wszystko jest walidowane automatycznie.
Działa to również w drugą stronę, w wielu przypadkach możesz po prostu przekazać obiekt otrzymany z bazy danych **bezpośrednio do klienta**.
Dzięki **FastAPI** otrzymujesz wszystkie funkcje **Pydantic** (ponieważ FastAPI bazuje na Pydantic do obsługi wszystkich danych):
* **Bez prania mózgu**:
* Brak nowego mikrojęzyka do definiowania schematu, którego trzeba się nauczyć.
* Jeśli znasz adnotacje typów Pythona to wiesz jak używać Pydantic.
* Dobrze współpracuje z Twoim **<abbr title='Skrót od "Integrated Development Environment", podobne do edytora kodu'>IDE</abbr>/<abbr title="Program, który sprawdza Twój kod pod kątem błędów">linterem</abbr>/mózgiem**:
* Ponieważ struktury danych Pydantic to po prostu instancje klas, które definiujesz; autouzupełnianie, linting, mypy i twoja intuicja powinny działać poprawnie z Twoimi zwalidowanymi danymi.
* **Szybkość**:
* w <a href="https://pydantic-docs.helpmanual.io/benchmarks/" class="external-link" target="_blank">benchmarkach</a> Pydantic jest szybszy niż wszystkie inne testowane biblioteki.
* Walidacja **złożonych struktur**:
* Wykorzystanie hierarchicznych modeli Pydantic, Pythonowego modułu `typing` zawierającego `List`, `Dict`, itp.
* Walidatory umożliwiają jasne i łatwe definiowanie, sprawdzanie złożonych struktur danych oraz dokumentowanie ich jako JSON Schema.
* Możesz mieć głęboko **zagnieżdżone obiekty JSON** i wszystkie je poddać walidacji i adnotować.
* **Rozszerzalność**:
* Pydantic umożliwia zdefiniowanie niestandardowych typów danych lub rozszerzenie walidacji o metody na modelu, na których użyty jest dekorator walidatora.
* 100% pokrycie testami.

View File

@@ -435,7 +435,6 @@ Aby dowiedzieć się o tym więcej, zobacz sekcję <a href="https://fastapi.tian
Używane przez Pydantic:
* <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - dla szybszego <abbr title="przetwarzania stringa który przychodzi z żądaniem HTTP na dane używane przez Pythona">"parsowania"</abbr> danych JSON.
* <a href="https://github.com/JoshData/python-email-validator" target="_blank"><code>email_validator</code></a> - dla walidacji adresów email.
Używane przez Starlette:

View File

@@ -63,6 +63,7 @@ nav:
- tr: /tr/
- uk: /uk/
- zh: /zh/
- features.md
- Samouczek:
- tutorial/index.md
- tutorial/first-steps.md

View File

@@ -430,7 +430,6 @@ Para entender mais sobre performance, veja a seção <a href="https://fastapi.ti
Usados por Pydantic:
* <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - para JSON mais rápido <abbr title="converte uma string que chega de uma requisição HTTP para dados Python">"parsing"</abbr>.
* <a href="https://github.com/JoshData/python-email-validator" target="_blank"><code>email_validator</code></a> - para validação de email.
Usados por Starlette:

View File

@@ -439,7 +439,6 @@ item: Item
Используется Pydantic:
* <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - для более быстрого JSON <abbr title="преобразования строки, полученной из HTTP-запроса, в данные Python">"парсинга"</abbr>.
* <a href="https://github.com/JoshData/python-email-validator" target="_blank"><code>email_validator</code></a> - для проверки электронной почты.
Используется Starlette:

View File

@@ -0,0 +1,382 @@
# Body - Вложенные модели
С помощью **FastAPI**, вы можете определять, валидировать, документировать и использовать модели произвольной вложенности (благодаря библиотеке Pydantic).
## Определение полей содержащих списки
Вы можете определять атрибут как подтип. Например, тип `list` в Python:
=== "Python 3.10+"
```Python hl_lines="12"
{!> ../../../docs_src/body_nested_models/tutorial001_py310.py!}
```
=== "Python 3.6+"
```Python hl_lines="14"
{!> ../../../docs_src/body_nested_models/tutorial001.py!}
```
Это приведёт к тому, что обьект `tags` преобразуется в список, несмотря на то что тип его элементов не объявлен.
## Определение полей содержащих список с определением типов его элементов
Однако в Python есть способ объявления списков с указанием типов для вложенных элементов:
### Импортируйте `List` из модуля typing
В Python 3.9 и выше вы можете использовать стандартный тип `list` для объявления аннотаций типов, как мы увидим ниже. 💡
Но в версиях Python до 3.9 (начиная с 3.6) сначала вам необходимо импортировать `List` из стандартного модуля `typing` в Python:
```Python hl_lines="1"
{!> ../../../docs_src/body_nested_models/tutorial002.py!}
```
### Объявление `list` с указанием типов для вложенных элементов
Объявление типов для элементов (внутренних типов) вложенных в такие типы как `list`, `dict`, `tuple`:
* Если у вас Python версии ниже чем 3.9, импортируйте их аналог из модуля `typing`
* Передайте внутренний(ие) тип(ы) как "параметры типа", используя квадратные скобки: `[` и `]`
В Python версии 3.9 это будет выглядеть так:
```Python
my_list: list[str]
```
В версиях Python до 3.9 это будет выглядеть так:
```Python
from typing import List
my_list: List[str]
```
Это всё стандартный синтаксис Python для объявления типов.
Используйте этот же стандартный синтаксис для атрибутов модели с внутренними типами.
Таким образом, в нашем примере мы можем явно указать тип данных для поля `tags` как "список строк":
=== "Python 3.10+"
```Python hl_lines="12"
{!> ../../../docs_src/body_nested_models/tutorial002_py310.py!}
```
=== "Python 3.9+"
```Python hl_lines="14"
{!> ../../../docs_src/body_nested_models/tutorial002_py39.py!}
```
=== "Python 3.6+"
```Python hl_lines="14"
{!> ../../../docs_src/body_nested_models/tutorial002.py!}
```
## Типы множеств
Но затем мы подумали и поняли, что теги не должны повторяться и, вероятно, они должны быть уникальными строками.
И в Python есть специальный тип данных для множеств уникальных элементов - `set`.
Тогда мы может обьявить поле `tags` как множество строк:
=== "Python 3.10+"
```Python hl_lines="12"
{!> ../../../docs_src/body_nested_models/tutorial003_py310.py!}
```
=== "Python 3.9+"
```Python hl_lines="14"
{!> ../../../docs_src/body_nested_models/tutorial003_py39.py!}
```
=== "Python 3.6+"
```Python hl_lines="1 14"
{!> ../../../docs_src/body_nested_models/tutorial003.py!}
```
С помощью этого, даже если вы получите запрос с повторяющимися данными, они будут преобразованы в множество уникальных элементов.
И когда вы выводите эти данные, даже если исходный набор содержал дубликаты, они будут выведены в виде множества уникальных элементов.
И они также будут соответствующим образом аннотированы / задокументированы.
## Вложенные Модели
У каждого атрибута Pydantic-модели есть тип.
Но этот тип может сам быть другой моделью Pydantic.
Таким образом вы можете объявлять глубоко вложенные JSON "объекты" с определёнными именами атрибутов, типами и валидацией.
Всё это может быть произвольно вложенным.
### Определение подмодели
Например, мы можем определить модель `Image`:
=== "Python 3.10+"
```Python hl_lines="7-9"
{!> ../../../docs_src/body_nested_models/tutorial004_py310.py!}
```
=== "Python 3.9+"
```Python hl_lines="9-11"
{!> ../../../docs_src/body_nested_models/tutorial004_py39.py!}
```
=== "Python 3.6+"
```Python hl_lines="9-11"
{!> ../../../docs_src/body_nested_models/tutorial004.py!}
```
### Использование вложенной модели в качестве типа
Также мы можем использовать эту модель как тип атрибута:
=== "Python 3.10+"
```Python hl_lines="18"
{!> ../../../docs_src/body_nested_models/tutorial004_py310.py!}
```
=== "Python 3.9+"
```Python hl_lines="20"
{!> ../../../docs_src/body_nested_models/tutorial004_py39.py!}
```
=== "Python 3.6+"
```Python hl_lines="20"
{!> ../../../docs_src/body_nested_models/tutorial004.py!}
```
Это означает, что **FastAPI** будет ожидать тело запроса, аналогичное этому:
```JSON
{
"name": "Foo",
"description": "The pretender",
"price": 42.0,
"tax": 3.2,
"tags": ["rock", "metal", "bar"],
"image": {
"url": "http://example.com/baz.jpg",
"name": "The Foo live"
}
}
```
Ещё раз: сделав такое объявление, с помощью **FastAPI** вы получите:
* Поддержку редакторов IDE (автодополнение и т.д), даже для вложенных моделей
* Преобразование данных
* Валидацию данных
* Автоматическую документацию
## Особые типы и валидация
Помимо обычных простых типов, таких как `str`, `int`, `float`, и т.д. Вы можете использовать более сложные базовые типы, которые наследуются от типа `str`.
Чтобы увидеть все варианты, которые у вас есть, ознакомьтесь с документацией <a href="https://pydantic-docs.helpmanual.io/usage/types/" class="external-link" target="_blank">по необычным типам Pydantic</a>. Вы увидите некоторые примеры в следующей главе.
Например, так как в модели `Image` у нас есть поле `url`, то мы можем объявить его как тип `HttpUrl` из модуля Pydantic вместо типа `str`:
=== "Python 3.10+"
```Python hl_lines="2 8"
{!> ../../../docs_src/body_nested_models/tutorial005_py310.py!}
```
=== "Python 3.9+"
```Python hl_lines="4 10"
{!> ../../../docs_src/body_nested_models/tutorial005_py39.py!}
```
=== "Python 3.6+"
```Python hl_lines="4 10"
{!> ../../../docs_src/body_nested_models/tutorial005.py!}
```
Строка будет проверена на соответствие допустимому URL-адресу и задокументирована в JSON схему / OpenAPI.
## Атрибуты, содержащие списки подмоделей
Вы также можете использовать модели Pydantic в качестве типов вложенных в `list`, `set` и т.д:
=== "Python 3.10+"
```Python hl_lines="18"
{!> ../../../docs_src/body_nested_models/tutorial006_py310.py!}
```
=== "Python 3.9+"
```Python hl_lines="20"
{!> ../../../docs_src/body_nested_models/tutorial006_py39.py!}
```
=== "Python 3.6+"
```Python hl_lines="20"
{!> ../../../docs_src/body_nested_models/tutorial006.py!}
```
Такая реализация будет ожидать (конвертировать, валидировать, документировать и т.д) JSON-содержимое в следующем формате:
```JSON hl_lines="11"
{
"name": "Foo",
"description": "The pretender",
"price": 42.0,
"tax": 3.2,
"tags": [
"rock",
"metal",
"bar"
],
"images": [
{
"url": "http://example.com/baz.jpg",
"name": "The Foo live"
},
{
"url": "http://example.com/dave.jpg",
"name": "The Baz"
}
]
}
```
!!! info "Информация"
Заметьте, что теперь у ключа `images` есть список объектов изображений.
## Глубоко вложенные модели
Вы можете определять модели с произвольным уровнем вложенности:
=== "Python 3.10+"
```Python hl_lines="7 12 18 21 25"
{!> ../../../docs_src/body_nested_models/tutorial007_py310.py!}
```
=== "Python 3.9+"
```Python hl_lines="9 14 20 23 27"
{!> ../../../docs_src/body_nested_models/tutorial007_py39.py!}
```
=== "Python 3.6+"
```Python hl_lines="9 14 20 23 27"
{!> ../../../docs_src/body_nested_models/tutorial007.py!}
```
!!! info "Информация"
Заметьте, что у объекта `Offer` есть список объектов `Item`, которые, в свою очередь, могут содержать необязательный список объектов `Image`
## Тела с чистыми списками элементов
Если верхний уровень значения тела JSON-объекта представляет собой JSON `array` (в Python - `list`), вы можете объявить тип в параметре функции, так же, как в моделях Pydantic:
```Python
images: List[Image]
```
в Python 3.9 и выше:
```Python
images: list[Image]
```
например так:
=== "Python 3.9+"
```Python hl_lines="13"
{!> ../../../docs_src/body_nested_models/tutorial008_py39.py!}
```
=== "Python 3.6+"
```Python hl_lines="15"
{!> ../../../docs_src/body_nested_models/tutorial008.py!}
```
## Универсальная поддержка редактора
И вы получаете поддержку редактора везде.
Даже для элементов внутри списков:
<img src="/img/tutorial/body-nested-models/image01.png">
Вы не могли бы получить такую поддержку редактора, если бы работали напрямую с `dict`, а не с моделями Pydantic.
Но вы также не должны беспокоиться об этом, входящие словари автоматически конвертируются, а ваш вывод также автоматически преобразуется в формат JSON.
## Тела запросов с произвольными словарями (`dict` )
Вы также можете объявить тело запроса как `dict` с ключами определенного типа и значениями другого типа данных.
Без необходимости знать заранее, какие значения являются допустимыми для имён полей/атрибутов (как это было бы в случае с моделями Pydantic).
Это было бы полезно, если вы хотите получить ключи, которые вы еще не знаете.
---
Другой полезный случай - когда вы хотите чтобы ключи были другого типа данных, например, `int`.
Именно это мы сейчас и увидим здесь.
В этом случае вы принимаете `dict`, пока у него есть ключи типа `int` со значениями типа `float`:
=== "Python 3.9+"
```Python hl_lines="7"
{!> ../../../docs_src/body_nested_models/tutorial009_py39.py!}
```
=== "Python 3.6+"
```Python hl_lines="9"
{!> ../../../docs_src/body_nested_models/tutorial009.py!}
```
!!! tip "Совет"
Имейте в виду, что JSON поддерживает только ключи типа `str`.
Но Pydantic обеспечивает автоматическое преобразование данных.
Это значит, что даже если пользователи вашего API могут отправлять только строки в качестве ключей, при условии, что эти строки содержат целые числа, Pydantic автоматический преобразует и валидирует эти данные.
А `dict`, с именем `weights`, который вы получите в качестве ответа Pydantic, действительно будет иметь ключи типа `int` и значения типа `float`.
## Резюме
С помощью **FastAPI** вы получаете максимальную гибкость, предоставляемую моделями Pydantic, сохраняя при этом простоту, краткость и элегантность вашего кода.
И дополнительно вы получаете:
* Поддержку редактора (автодополнение доступно везде!)
* Преобразование данных (также известно как парсинг / сериализация)
* Валидацию данных
* Документацию схемы данных
* Автоматическую генерацию документации

View File

@@ -0,0 +1,84 @@
# CORS (Cross-Origin Resource Sharing)
<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" class="external-link" target="_blank">Понятие CORS или "Cross-Origin Resource Sharing"</a> относится к ситуациям, при которых запущенный в браузере фронтенд содержит JavaScript-код, который взаимодействует с бэкендом, находящимся на другом "источнике" ("origin").
## Источник
Источник - это совокупность протокола (`http`, `https`), домена (`myapp.com`, `localhost`, `localhost.tiangolo.com`) и порта (`80`, `443`, `8080`).
Поэтому это три разных источника:
* `http://localhost`
* `https://localhost`
* `http://localhost:8080`
Даже если они все расположены в `localhost`, они используют разные протоколы и порты, а значит, являются разными источниками.
## Шаги
Допустим, у вас есть фронтенд, запущенный в браузере по адресу `http://localhost:8080`, и его JavaScript-код пытается взаимодействовать с бэкендом, запущенным по адресу `http://localhost` (поскольку мы не указали порт, браузер по умолчанию будет использовать порт `80`).
Затем браузер отправит бэкенду HTTP-запрос `OPTIONS`, и если бэкенд вернёт соответствующие заголовки для авторизации взаимодействия с другим источником (`http://localhost:8080`), то браузер разрешит JavaScript-коду на фронтенде отправить запрос на этот бэкенд.
Чтобы это работало, у бэкенда должен быть список "разрешённых источников" ("allowed origins").
В таком случае этот список должен содержать `http://localhost:8080`, чтобы фронтенд работал корректно.
## Подстановочный символ `"*"`
В качестве списка источников можно указать подстановочный символ `"*"` ("wildcard"), чтобы разрешить любые источники.
Но тогда не будут разрешены некоторые виды взаимодействия, включая всё связанное с учётными данными: куки, заголовки Authorization с Bearer-токенами наподобие тех, которые мы использовали ранее и т.п.
Поэтому, чтобы всё работало корректно, лучше явно указывать список разрешённых источников.
## Использование `CORSMiddleware`
Вы можете настроить этот механизм в вашем **FastAPI** приложении, используя `CORSMiddleware`.
* Импортируйте `CORSMiddleware`.
* Создайте список разрешённых источников (в виде строк).
* Добавьте его как "middleware" к вашему **FastAPI** приложению.
Вы также можете указать, разрешает ли ваш бэкенд использование:
* Учётных данных (включая заголовки Authorization, куки и т.п.).
* Отдельных HTTP-методов (`POST`, `PUT`) или всех вместе, используя `"*"`.
* Отдельных HTTP-заголовков или всех вместе, используя `"*"`.
```Python hl_lines="2 6-11 13-19"
{!../../../docs_src/cors/tutorial001.py!}
```
`CORSMiddleware` использует для параметров "запрещающие" значения по умолчанию, поэтому вам нужно явным образом разрешить использование отдельных источников, методов или заголовков, чтобы браузеры могли использовать их в кросс-доменном контексте.
Поддерживаются следующие аргументы:
* `allow_origins` - Список источников, на которые разрешено выполнять кросс-доменные запросы. Например, `['https://example.org', 'https://www.example.org']`. Можно использовать `['*']`, чтобы разрешить любые источники.
* `allow_origin_regex` - Регулярное выражение для определения источников, на которые разрешено выполнять кросс-доменные запросы. Например, `'https://.*\.example\.org'`.
* `allow_methods` - Список HTTP-методов, которые разрешены для кросс-доменных запросов. По умолчанию равно `['GET']`. Можно использовать `['*']`, чтобы разрешить все стандартные методы.
* `allow_headers` - Список HTTP-заголовков, которые должны поддерживаться при кросс-доменных запросах. По умолчанию равно `[]`. Можно использовать `['*']`, чтобы разрешить все заголовки. Заголовки `Accept`, `Accept-Language`, `Content-Language` и `Content-Type` всегда разрешены для <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests" class="external-link" rel="noopener" target="_blank">простых CORS-запросов</a>.
* `allow_credentials` - указывает, что куки разрешены в кросс-доменных запросах. По умолчанию равно `False`. Также, `allow_origins` нельзя присвоить `['*']`, если разрешено использование учётных данных. В таком случае должен быть указан список источников.
* `expose_headers` - Указывает любые заголовки ответа, которые должны быть доступны браузеру. По умолчанию равно `[]`.
* `max_age` - Устанавливает максимальное время в секундах, в течение которого браузер кэширует CORS-ответы. По умолчанию равно `600`.
`CORSMiddleware` отвечает на два типа HTTP-запросов...
### CORS-запросы с предварительной проверкой
Это любые `OPTIONS` запросы с заголовками `Origin` и `Access-Control-Request-Method`.
В этом случае middleware перехватит входящий запрос и отправит соответствующие CORS-заголовки в ответе, а также ответ `200` или `400` в информационных целях.
### Простые запросы
Любые запросы с заголовком `Origin`. В этом случае middleware передаст запрос дальше как обычно, но добавит соответствующие CORS-заголовки к ответу.
## Больше информации
Для получения более подробной информации о <abbr title="Cross-Origin Resource Sharing">CORS</abbr>, обратитесь к <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" class="external-link" target="_blank">Документации CORS от Mozilla</a>.
!!! note "Технические детали"
Вы также можете использовать `from starlette.middleware.cors import CORSMiddleware`.
**FastAPI** предоставляет несколько middleware в `fastapi.middleware` только для вашего удобства как разработчика. Но большинство доступных middleware взяты напрямую из Starlette.

View File

@@ -0,0 +1,252 @@
# Дополнительные модели
В продолжение прошлого примера будет уже обычным делом иметь несколько связанных между собой моделей.
Это особенно применимо в случае моделей пользователя, потому что:
* **Модель для ввода** должна иметь возможность содержать пароль.
* **Модель для вывода** не должна содержать пароль.
* **Модель для базы данных**, возможно, должна содержать хэшированный пароль.
!!! danger "Внимание"
Никогда не храните пароли пользователей в чистом виде. Всегда храните "безопасный хэш", который вы затем сможете проверить.
Если вам это не знакомо, вы можете узнать про "хэш пароля" в [главах о безопасности](security/simple-oauth2.md#password-hashing){.internal-link target=_blank}.
## Множественные модели
Ниже изложена основная идея того, как могут выглядеть эти модели с полями для паролей, а также описаны места, где они используются:
=== "Python 3.10+"
```Python hl_lines="7 9 14 20 22 27-28 31-33 38-39"
{!> ../../../docs_src/extra_models/tutorial001_py310.py!}
```
=== "Python 3.6+"
```Python hl_lines="9 11 16 22 24 29-30 33-35 40-41"
{!> ../../../docs_src/extra_models/tutorial001.py!}
```
### Про `**user_in.dict()`
#### `.dict()` из Pydantic
`user_in` - это Pydantic-модель класса `UserIn`.
У Pydantic-моделей есть метод `.dict()`, который возвращает `dict` с данными модели.
Поэтому, если мы создадим Pydantic-объект `user_in` таким способом:
```Python
user_in = UserIn(username="john", password="secret", email="john.doe@example.com")
```
и затем вызовем:
```Python
user_dict = user_in.dict()
```
то теперь у нас есть `dict` с данными модели в переменной `user_dict` (это `dict` вместо объекта Pydantic-модели).
И если мы вызовем:
```Python
print(user_dict)
```
мы можем получить `dict` с такими данными:
```Python
{
'username': 'john',
'password': 'secret',
'email': 'john.doe@example.com',
'full_name': None,
}
```
#### Распаковка `dict`
Если мы возьмём `dict` наподобие `user_dict` и передадим его в функцию (или класс), используя `**user_dict`, Python распакует его. Он передаст ключи и значения `user_dict` напрямую как аргументы типа ключ-значение.
Поэтому, продолжая описанный выше пример с `user_dict`, написание такого кода:
```Python
UserInDB(**user_dict)
```
Будет работать так же, как примерно такой код:
```Python
UserInDB(
username="john",
password="secret",
email="john.doe@example.com",
full_name=None,
)
```
Или, если для большей точности мы напрямую используем `user_dict` с любым потенциальным содержимым, то этот пример будет выглядеть так:
```Python
UserInDB(
username = user_dict["username"],
password = user_dict["password"],
email = user_dict["email"],
full_name = user_dict["full_name"],
)
```
#### Pydantic-модель из содержимого другой модели
Как в примере выше мы получили `user_dict` из `user_in.dict()`, этот код:
```Python
user_dict = user_in.dict()
UserInDB(**user_dict)
```
будет равнозначен такому:
```Python
UserInDB(**user_in.dict())
```
...потому что `user_in.dict()` - это `dict`, и затем мы указываем, чтобы Python его "распаковал", когда передаём его в `UserInDB` и ставим перед ним `**`.
Таким образом мы получаем Pydantic-модель на основе данных из другой Pydantic-модели.
#### Распаковка `dict` и дополнительные именованные аргументы
И затем, если мы добавим дополнительный именованный аргумент `hashed_password=hashed_password` как здесь:
```Python
UserInDB(**user_in.dict(), hashed_password=hashed_password)
```
... то мы получим что-то подобное:
```Python
UserInDB(
username = user_dict["username"],
password = user_dict["password"],
email = user_dict["email"],
full_name = user_dict["full_name"],
hashed_password = hashed_password,
)
```
!!! warning "Предупреждение"
Цель использованных в примере вспомогательных функций - не более чем демонстрация возможных операций с данными, но, конечно, они не обеспечивают настоящую безопасность.
## Сократите дублирование
Сокращение дублирования кода - это одна из главных идей **FastAPI**.
Поскольку дублирование кода повышает риск появления багов, проблем с безопасностью, проблем десинхронизации кода (когда вы обновляете код в одном месте, но не обновляете в другом), и т.д.
А все описанные выше модели используют много общих данных и дублируют названия атрибутов и типов.
Мы можем это улучшить.
Мы можем определить модель `UserBase`, которая будет базовой для остальных моделей. И затем мы можем создать подклассы этой модели, которые будут наследовать её атрибуты (объявления типов, валидацию, и т.п.).
Все операции конвертации, валидации, документации, и т.п. будут по-прежнему работать нормально.
В этом случае мы можем определить только различия между моделями (с `password` в чистом виде, с `hashed_password` и без пароля):
=== "Python 3.10+"
```Python hl_lines="7 13-14 17-18 21-22"
{!> ../../../docs_src/extra_models/tutorial002_py310.py!}
```
=== "Python 3.6+"
```Python hl_lines="9 15-16 19-20 23-24"
{!> ../../../docs_src/extra_models/tutorial002.py!}
```
## `Union` или `anyOf`
Вы можете определить ответ как `Union` из двух типов. Это означает, что ответ должен соответствовать одному из них.
Он будет определён в OpenAPI как `anyOf`.
Для этого используйте стандартные аннотации типов в Python <a href="https://docs.python.org/3/library/typing.html#typing.Union" class="external-link" target="_blank">`typing.Union`</a>:
!!! note "Примечание"
При объявлении <a href="https://pydantic-docs.helpmanual.io/usage/types/#unions" class="external-link" target="_blank">`Union`</a>, сначала указывайте наиболее детальные типы, затем менее детальные. В примере ниже более детальный `PlaneItem` стоит перед `CarItem` в `Union[PlaneItem, CarItem]`.
=== "Python 3.10+"
```Python hl_lines="1 14-15 18-20 33"
{!> ../../../docs_src/extra_models/tutorial003_py310.py!}
```
=== "Python 3.6+"
```Python hl_lines="1 14-15 18-20 33"
{!> ../../../docs_src/extra_models/tutorial003.py!}
```
### `Union` в Python 3.10
В этом примере мы передаём `Union[PlaneItem, CarItem]` в качестве значения аргумента `response_model`.
Поскольку мы передаём его как **значение аргумента** вместо того, чтобы поместить его в **аннотацию типа**, нам придётся использовать `Union` даже в Python 3.10.
Если оно было бы указано в аннотации типа, то мы могли бы использовать вертикальную черту как в примере:
```Python
some_variable: PlaneItem | CarItem
```
Но если мы помещаем его в `response_model=PlaneItem | CarItem` мы получим ошибку, потому что Python попытается произвести **некорректную операцию** между `PlaneItem` и `CarItem` вместо того, чтобы интерпретировать это как аннотацию типа.
## Список моделей
Таким же образом вы можете определять ответы как списки объектов.
Для этого используйте `typing.List` из стандартной библиотеки Python (или просто `list` в Python 3.9 и выше):
=== "Python 3.9+"
```Python hl_lines="18"
{!> ../../../docs_src/extra_models/tutorial004_py39.py!}
```
=== "Python 3.6+"
```Python hl_lines="1 20"
{!> ../../../docs_src/extra_models/tutorial004.py!}
```
## Ответ с произвольным `dict`
Вы также можете определить ответ, используя произвольный одноуровневый `dict` и определяя только типы ключей и значений без использования Pydantic-моделей.
Это полезно, если вы заранее не знаете корректных названий полей/атрибутов (которые будут нужны при использовании Pydantic-модели).
В этом случае вы можете использовать `typing.Dict` (или просто `dict` в Python 3.9 и выше):
=== "Python 3.9+"
```Python hl_lines="6"
{!> ../../../docs_src/extra_models/tutorial005_py39.py!}
```
=== "Python 3.6+"
```Python hl_lines="1 8"
{!> ../../../docs_src/extra_models/tutorial005.py!}
```
## Резюме
Используйте несколько Pydantic-моделей и свободно применяйте наследование для каждой из них.
Вам не обязательно иметь единственную модель данных для каждой сущности, если эта сущность должна иметь возможность быть в разных "состояниях". Как в случае с "сущностью" пользователя, у которого есть состояния с полями `password`, `password_hash` и без пароля.

View File

@@ -0,0 +1,111 @@
# URL-адреса метаданных и документации
Вы можете настроить несколько конфигураций метаданных в вашем **FastAPI** приложении.
## Метаданные для API
Вы можете задать следующие поля, которые используются в спецификации OpenAPI и в UI автоматической документации API:
| Параметр | Тип | Описание |
|------------|--|-------------|
| `title` | `str` | Заголовок API. |
| `description` | `str` | Краткое описание API. Может быть использован Markdown. |
| `version` | `string` | Версия API. Версия вашего собственного приложения, а не OpenAPI. К примеру `2.5.0`. |
| `terms_of_service` | `str` | Ссылка к условиям пользования API. Если указано, то это должен быть URL-адрес. |
| `contact` | `dict` | Контактная информация для открытого API. Может содержать несколько полей. <details><summary>поля <code>contact</code></summary><table><thead><tr><th>Параметр</th><th>Тип</th><th>Описание</th></tr></thead><tbody><tr><td><code>name</code></td><td><code>str</code></td><td>Идентификационное имя контактного лица/организации.</td></tr><tr><td><code>url</code></td><td><code>str</code></td><td>URL указывающий на контактную информацию. ДОЛЖЕН быть в формате URL.</td></tr><tr><td><code>email</code></td><td><code>str</code></td><td>Email адрес контактного лица/организации. ДОЛЖЕН быть в формате email адреса.</td></tr></tbody></table></details> |
| `license_info` | `dict` | Информация о лицензии открытого API. Может содержать несколько полей. <details><summary>поля <code>license_info</code></summary><table><thead><tr><th>Параметр</th><th>Тип</th><th>Описание</th></tr></thead><tbody><tr><td><code>name</code></td><td><code>str</code></td><td><strong>ОБЯЗАТЕЛЬНО</strong> (если установлен параметр <code>license_info</code>). Название лицензии, используемой для API</td></tr><tr><td><code>url</code></td><td><code>str</code></td><td>URL, указывающий на лицензию, используемую для API. ДОЛЖЕН быть в формате URL.</td></tr></tbody></table></details> |
Вы можете задать их следующим образом:
```Python hl_lines="3-16 19-31"
{!../../../docs_src/metadata/tutorial001.py!}
```
!!! tip "Подсказка"
Вы можете использовать Markdown в поле `description`, и оно будет отображено в выводе.
С этой конфигурацией автоматическая документация API будут выглядеть так:
<img src="/img/tutorial/metadata/image01.png">
## Метаданные для тегов
Вы также можете добавить дополнительные метаданные для различных тегов, используемых для группировки ваших операций пути с помощью параметра `openapi_tags`.
Он принимает список, содержащий один словарь для каждого тега.
Каждый словарь может содержать в себе:
* `name` (**обязательно**): `str`-значение с тем же именем тега, которое вы используете в параметре `tags` в ваших *операциях пути* и `APIRouter`ах.
* `description`: `str`-значение с кратким описанием для тега. Может содержать Markdown и будет отображаться в UI документации.
* `externalDocs`: `dict`-значение описывающее внешнюю документацию. Включает в себя:
* `description`: `str`-значение с кратким описанием для внешней документации.
* `url` (**обязательно**): `str`-значение с URL-адресом для внешней документации.
### Создание метаданных для тегов
Давайте попробуем сделать это на примере с тегами для `users` и `items`.
Создайте метаданные для ваших тегов и передайте их в параметре `openapi_tags`:
```Python hl_lines="3-16 18"
{!../../../docs_src/metadata/tutorial004.py!}
```
Помните, что вы можете использовать Markdown внутри описания, к примеру "login" будет отображен жирным шрифтом (**login**) и "fancy" будет отображаться курсивом (_fancy_).
!!! tip "Подсказка"
Вам необязательно добавлять метаданные для всех используемых тегов
### Используйте собственные теги
Используйте параметр `tags` с вашими *операциями пути* (и `APIRouter`ами), чтобы присвоить им различные теги:
```Python hl_lines="21 26"
{!../../../docs_src/metadata/tutorial004.py!}
```
!!! info "Дополнительная информация"
Узнайте больше о тегах в [Конфигурации операции пути](../path-operation-configuration/#tags){.internal-link target=_blank}.
### Проверьте документацию
Теперь, если вы проверите документацию, вы увидите всю дополнительную информацию:
<img src="/img/tutorial/metadata/image02.png">
### Порядок расположения тегов
Порядок расположения словарей метаданных для каждого тега определяет также порядок, отображаемый в документах UI
К примеру, несмотря на то, что `users` будут идти после `items` в алфавитном порядке, они отображаются раньше, потому что мы добавляем свои метаданные в качестве первого словаря в списке.
## URL-адреса OpenAPI
По умолчанию схема OpenAPI отображена по адресу `/openapi.json`.
Но вы можете изменить это с помощью параметра `openapi_url`.
К примеру, чтобы задать её отображение по адресу `/api/v1/openapi.json`:
```Python hl_lines="3"
{!../../../docs_src/metadata/tutorial002.py!}
```
Если вы хотите отключить схему OpenAPI полностью, вы можете задать `openapi_url=None`, это также отключит пользовательские интерфейсы документации, которые его использует.
## URL-адреса документации
Вы можете изменить конфигурацию двух пользовательских интерфейсов документации, среди которых
* **Swagger UI**: отображаемый по адресу `/docs`.
* Вы можете задать его URL с помощью параметра `docs_url`.
* Вы можете отключить это с помощью настройки `docs_url=None`.
* **ReDoc**: отображаемый по адресу `/redoc`.
* Вы можете задать его URL с помощью параметра `redoc_url`.
* Вы можете отключить это с помощью настройки `redoc_url=None`.
К примеру, чтобы задать отображение Swagger UI по адресу `/documentation` и отключить ReDoc:
```Python hl_lines="3"
{!../../../docs_src/metadata/tutorial003.py!}
```

View File

@@ -0,0 +1,179 @@
# Конфигурация операций пути
Существует несколько параметров, которые вы можете передать вашему *декоратору операций пути* для его настройки.
!!! warning "Внимание"
Помните, что эти параметры передаются непосредственно *декоратору операций пути*, а не вашей *функции-обработчику операций пути*.
## Коды состояния
Вы можете определить (HTTP) `status_code`, который будет использован в ответах вашей *операции пути*.
Вы можете передать только `int`-значение кода, например `404`.
Но если вы не помните, для чего нужен каждый числовой код, вы можете использовать сокращенные константы в параметре `status`:
=== "Python 3.10+"
```Python hl_lines="1 15"
{!> ../../../docs_src/path_operation_configuration/tutorial001_py310.py!}
```
=== "Python 3.9+"
```Python hl_lines="3 17"
{!> ../../../docs_src/path_operation_configuration/tutorial001_py39.py!}
```
=== "Python 3.6+"
```Python hl_lines="3 17"
{!> ../../../docs_src/path_operation_configuration/tutorial001.py!}
```
Этот код состояния будет использован в ответе и будет добавлен в схему OpenAPI.
!!! note "Технические детали"
Вы также можете использовать `from starlette import status`.
**FastAPI** предоставляет тот же `starlette.status` под псевдонимом `fastapi.status` для удобства разработчика. Но его источник - это непосредственно Starlette.
## Теги
Вы можете добавлять теги к вашим *операциям пути*, добавив параметр `tags` с `list` заполненным `str`-значениями (обычно в нём только одна строка):
=== "Python 3.10+"
```Python hl_lines="15 20 25"
{!> ../../../docs_src/path_operation_configuration/tutorial002_py310.py!}
```
=== "Python 3.9+"
```Python hl_lines="17 22 27"
{!> ../../../docs_src/path_operation_configuration/tutorial002_py39.py!}
```
=== "Python 3.6+"
```Python hl_lines="17 22 27"
{!> ../../../docs_src/path_operation_configuration/tutorial002.py!}
```
Они будут добавлены в схему OpenAPI и будут использованы в автоматической документации интерфейса:
<img src="/img/tutorial/path-operation-configuration/image01.png">
### Теги с перечислениями
Если у вас большое приложение, вы можете прийти к необходимости добавить **несколько тегов**, и возможно, вы захотите убедиться в том, что всегда используете **один и тот же тег** для связанных *операций пути*.
В этих случаях, имеет смысл хранить теги в классе `Enum`.
**FastAPI** поддерживает это так же, как и в случае с обычными строками:
```Python hl_lines="1 8-10 13 18"
{!../../../docs_src/path_operation_configuration/tutorial002b.py!}
```
## Краткое и развёрнутое содержание
Вы можете добавить параметры `summary` и `description`:
=== "Python 3.10+"
```Python hl_lines="18-19"
{!> ../../../docs_src/path_operation_configuration/tutorial003_py310.py!}
```
=== "Python 3.9+"
```Python hl_lines="20-21"
{!> ../../../docs_src/path_operation_configuration/tutorial003_py39.py!}
```
=== "Python 3.6+"
```Python hl_lines="20-21"
{!> ../../../docs_src/path_operation_configuration/tutorial003.py!}
```
## Описание из строк документации
Так как описания обычно длинные и содержат много строк, вы можете объявить описание *операции пути* в функции <abbr title="многострочный текст, первое выражение внутри функции (не присвоенный какой-либо переменной), используемый для документации">строки документации</abbr> и **FastAPI** прочитает её отсюда.
Вы можете использовать <a href="https://en.wikipedia.org/wiki/Markdown" class="external-link" target="_blank">Markdown</a> в строке документации, и он будет интерпретирован и отображён корректно (с учетом отступа в строке документации).
=== "Python 3.10+"
```Python hl_lines="17-25"
{!> ../../../docs_src/path_operation_configuration/tutorial004_py310.py!}
```
=== "Python 3.9+"
```Python hl_lines="19-27"
{!> ../../../docs_src/path_operation_configuration/tutorial004_py39.py!}
```
=== "Python 3.6+"
```Python hl_lines="19-27"
{!> ../../../docs_src/path_operation_configuration/tutorial004.py!}
```
Он будет использован в интерактивной документации:
<img src="/img/tutorial/path-operation-configuration/image02.png">
## Описание ответа
Вы можете указать описание ответа с помощью параметра `response_description`:
=== "Python 3.10+"
```Python hl_lines="19"
{!> ../../../docs_src/path_operation_configuration/tutorial005_py310.py!}
```
=== "Python 3.9+"
```Python hl_lines="21"
{!> ../../../docs_src/path_operation_configuration/tutorial005_py39.py!}
```
=== "Python 3.6+"
```Python hl_lines="21"
{!> ../../../docs_src/path_operation_configuration/tutorial005.py!}
```
!!! info "Дополнительная информация"
Помните, что `response_description` относится конкретно к ответу, а `description` относится к *операции пути* в целом.
!!! check "Технические детали"
OpenAPI указывает, что каждой *операции пути* необходимо описание ответа.
Если вдруг вы не укажете его, то **FastAPI** автоматически сгенерирует это описание с текстом "Successful response".
<img src="/img/tutorial/path-operation-configuration/image03.png">
## Обозначение *операции пути* как устаревшей
Если вам необходимо пометить *операцию пути* как <abbr title="устаревшее, не рекомендовано к использованию">устаревшую</abbr>, при этом не удаляя её, передайте параметр `deprecated`:
```Python hl_lines="16"
{!../../../docs_src/path_operation_configuration/tutorial006.py!}
```
Он будет четко помечен как устаревший в интерактивной документации:
<img src="/img/tutorial/path-operation-configuration/image04.png">
Проверьте, как будут выглядеть устаревшие и не устаревшие *операции пути*:
<img src="/img/tutorial/path-operation-configuration/image05.png">
## Резюме
Вы можете легко конфигурировать и добавлять метаданные в ваши *операции пути*, передавая параметры *декораторам операций пути*.

View File

@@ -77,12 +77,17 @@ nav:
- tutorial/extra-data-types.md
- tutorial/cookie-params.md
- tutorial/testing.md
- tutorial/extra-models.md
- tutorial/response-status-code.md
- tutorial/query-params.md
- tutorial/body-multiple-params.md
- tutorial/metadata.md
- tutorial/path-operation-configuration.md
- tutorial/cors.md
- tutorial/static-files.md
- tutorial/debugging.md
- tutorial/schema-extra-example.md
- tutorial/body-nested-models.md
- async.md
- Развёртывание:
- deployment/index.md

View File

@@ -441,7 +441,6 @@ To understand more about it, see the section <a href="https://fastapi.tiangolo.c
Used by Pydantic:
* <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - for faster JSON <abbr title="converting the string that comes from an HTTP request into Python data">"parsing"</abbr>.
* <a href="https://github.com/JoshData/python-email-validator" target="_blank"><code>email_validator</code></a> - for email validation.
Used by Starlette:

View File

@@ -444,7 +444,6 @@ To understand more about it, see the section <a href="https://fastapi.tiangolo.c
Used by Pydantic:
* <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - for faster JSON <abbr title="converting the string that comes from an HTTP request into Python data">"parsing"</abbr>.
* <a href="https://github.com/JoshData/python-email-validator" target="_blank"><code>email_validator</code></a> - for email validation.
Used by Starlette:

View File

@@ -449,7 +449,6 @@ Daha fazla bilgi için, bu bölüme bir göz at <a href="https://fastapi.tiangol
Pydantic tarafında kullanılan:
* <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - daha hızlı JSON <abbr title="HTTP bağlantısından gelen stringi Python objesine çevirmek için">"dönüşümü"</abbr> için.
* <a href="https://github.com/JoshData/python-email-validator" target="_blank"><code>email_validator</code></a> - email doğrulaması için.
Starlette tarafında kullanılan:

View File

@@ -441,7 +441,6 @@ To understand more about it, see the section <a href="https://fastapi.tiangolo.c
Used by Pydantic:
* <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - for faster JSON <abbr title="converting the string that comes from an HTTP request into Python data">"parsing"</abbr>.
* <a href="https://github.com/JoshData/python-email-validator" target="_blank"><code>email_validator</code></a> - for email validation.
Used by Starlette:

View File

@@ -0,0 +1,16 @@
# 高级安全 - 介绍
## 附加特性
除 [教程 - 用户指南: 安全性](../../tutorial/security/){.internal-link target=_blank} 中涵盖的功能之外,还有一些额外的功能来处理安全性.
!!! tip "小贴士"
接下来的章节 **并不一定是 "高级的"**.
而且对于你的使用场景来说,解决方案很可能就在其中。
## 先阅读教程
接下来的部分假设你已经阅读了主要的 [教程 - 用户指南: 安全性](../../tutorial/security/){.internal-link target=_blank}.
它们都基于相同的概念,但支持一些额外的功能.

View File

@@ -0,0 +1,433 @@
# 设置和环境变量
在许多情况下,您的应用程序可能需要一些外部设置或配置,例如密钥、数据库凭据、电子邮件服务的凭据等等。
这些设置中的大多数是可变的(可以更改的),比如数据库的 URL。而且许多设置可能是敏感的比如密钥。
因此,通常会将它们提供为由应用程序读取的环境变量。
## 环境变量
!!! tip
如果您已经知道什么是"环境变量"以及如何使用它们,请随意跳到下面的下一节。
环境变量(也称为"env var")是一种存在于 Python 代码之外、存在于操作系统中的变量,可以被您的 Python 代码(或其他程序)读取。
您可以在 shell 中创建和使用环境变量,而无需使用 Python
=== "Linux、macOS、Windows Bash"
<div class="termy">
```console
// 您可以创建一个名为 MY_NAME 的环境变量
$ export MY_NAME="Wade Wilson"
// 然后您可以与其他程序一起使用它,例如
$ echo "Hello $MY_NAME"
Hello Wade Wilson
```
</div>
=== "Windows PowerShell"
<div class="termy">
```console
// 创建一个名为 MY_NAME 的环境变量
$ $Env:MY_NAME = "Wade Wilson"
// 与其他程序一起使用它,例如
$ echo "Hello $Env:MY_NAME"
Hello Wade Wilson
```
</div>
### 在 Python 中读取环境变量
您还可以在 Python 之外的地方(例如终端中或使用任何其他方法)创建环境变量,然后在 Python 中读取它们。
例如,您可以有一个名为 `main.py` 的文件,其中包含以下内容:
```Python hl_lines="3"
import os
name = os.getenv("MY_NAME", "World")
print(f"Hello {name} from Python")
```
!!! tip
<a href="https://docs.python.org/3.8/library/os.html#os.getenv" class="external-link" target="_blank">`os.getenv()`</a> 的第二个参数是要返回的默认值。
如果没有提供默认值,默认为 `None`,此处我们提供了 `"World"` 作为要使用的默认值。
然后,您可以调用该 Python 程序:
<div class="termy">
```console
// 这里我们还没有设置环境变量
$ python main.py
// 因为我们没有设置环境变量,所以我们得到默认值
Hello World from Python
// 但是如果我们先创建一个环境变量
$ export MY_NAME="Wade Wilson"
// 然后再次调用程序
$ python main.py
// 现在它可以读取环境变量
Hello Wade Wilson from Python
```
</div>
由于环境变量可以在代码之外设置,但可以由代码读取,并且不需要与其他文件一起存储(提交到 `git`),因此通常将它们用于配置或设置。
您还可以仅为特定程序调用创建一个环境变量,该环境变量仅对该程序可用,并且仅在其运行期间有效。
要做到这一点,在程序本身之前的同一行创建它:
<div class="termy">
```console
// 在此程序调用行中创建一个名为 MY_NAME 的环境变量
$ MY_NAME="Wade Wilson" python main.py
// 现在它可以读取环境变量
Hello Wade Wilson from Python
// 之后环境变量不再存在
$ python main.py
Hello World from Python
```
</div>
!!! tip
您可以在 <a href="https://12factor.net/config" class="external-link" target="_blank">Twelve-Factor App: Config</a> 中阅读更多相关信息。
### 类型和验证
这些环境变量只能处理文本字符串,因为它们是外部于 Python 的,并且必须与其他程序和整个系统兼容(甚至与不同的操作系统,如 Linux、Windows、macOS
这意味着从环境变量中在 Python 中读取的任何值都将是 `str` 类型,任何类型的转换或验证都必须在代码中完成。
## Pydantic 的 `Settings`
幸运的是Pydantic 提供了一个很好的工具来处理来自环境变量的设置,即<a href="https://pydantic-docs.helpmanual.io/usage/settings/" class="external-link" target="_blank">Pydantic: Settings management</a>。
### 创建 `Settings` 对象
从 Pydantic 导入 `BaseSettings` 并创建一个子类,与 Pydantic 模型非常相似。
与 Pydantic 模型一样,您使用类型注释声明类属性,还可以指定默认值。
您可以使用与 Pydantic 模型相同的验证功能和工具,比如不同的数据类型和使用 `Field()` 进行附加验证。
```Python hl_lines="2 5-8 11"
{!../../../docs_src/settings/tutorial001.py!}
```
!!! tip
如果您需要一个快速的复制粘贴示例,请不要使用此示例,而应使用下面的最后一个示例。
然后,当您创建该 `Settings` 类的实例(在此示例中是 `settings` 对象Pydantic 将以不区分大小写的方式读取环境变量,因此,大写的变量 `APP_NAME` 仍将为属性 `app_name` 读取。
然后,它将转换和验证数据。因此,当您使用该 `settings` 对象时,您将获得您声明的类型的数据(例如 `items_per_user` 将为 `int` 类型)。
### 使用 `settings`
然后,您可以在应用程序中使用新的 `settings` 对象:
```Python hl_lines="18-20"
{!../../../docs_src/settings/tutorial001.py!}
```
### 运行服务器
接下来,您将运行服务器,并将配置作为环境变量传递。例如,您可以设置一个 `ADMIN_EMAIL` 和 `APP_NAME`,如下所示:
<div class="termy">
```console
$ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp"uvicorn main:app
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
```
</div>
!!! tip
要为单个命令设置多个环境变量,只需用空格分隔它们,并将它们全部放在命令之前。
然后,`admin_email` 设置将为 `"deadpool@example.com"`。
`app_name` 将为 `"ChimichangApp"`。
而 `items_per_user` 将保持其默认值为 `50`。
## 在另一个模块中设置
您可以将这些设置放在另一个模块文件中,就像您在[Bigger Applications - Multiple Files](../tutorial/bigger-applications.md){.internal-link target=_blank}中所见的那样。
例如,您可以创建一个名为 `config.py` 的文件,其中包含以下内容:
```Python
{!../../../docs_src/settings/app01/config.py!}
```
然后在一个名为 `main.py` 的文件中使用它:
```Python hl_lines="3 11-13"
{!../../../docs_src/settings/app01/main.py!}
```
!!! tip
您还需要一个名为 `__init__.py` 的文件,就像您在[Bigger Applications - Multiple Files](../tutorial/bigger-applications.md){.internal-link target=_blank}中看到的那样。
## 在依赖项中使用设置
在某些情况下,从依赖项中提供设置可能比在所有地方都使用全局对象 `settings` 更有用。
这在测试期间尤其有用,因为很容易用自定义设置覆盖依赖项。
### 配置文件
根据前面的示例,您的 `config.py` 文件可能如下所示:
```Python hl_lines="10"
{!../../../docs_src/settings/app02/config.py!}
```
请注意,现在我们不创建默认实例 `settings = Settings()`。
### 主应用程序文件
现在我们创建一个依赖项,返回一个新的 `config.Settings()`。
=== "Python 3.9+"
```Python hl_lines="6 12-13"
{!> ../../../docs_src/settings/app02_an_py39/main.py!}
```
=== "Python 3.6+"
```Python hl_lines="6 12-13"
{!> ../../../docs_src/settings/app02_an/main.py!}
```
=== "Python 3.6+ 非注解版本"
!!! tip
如果可能,请尽量使用 `Annotated` 版本。
```Python hl_lines="5 11-12"
{!> ../../../docs_src/settings/app02/main.py!}
```
!!! tip
我们稍后会讨论 `@lru_cache()`。
目前,您可以将 `get_settings()` 视为普通函数。
然后,我们可以将其作为依赖项从“路径操作函数”中引入,并在需要时使用它。
=== "Python 3.9+"
```Python hl_lines="17 19-21"
{!> ../../../docs_src/settings/app02_an_py39/main.py!}
```
=== "Python 3.6+"
```Python hl_lines="17 19-21"
{!> ../../../docs_src/settings/app02_an/main.py!}
```
=== "Python 3.6+ 非注解版本"
!!! tip
如果可能,请尽量使用 `Annotated` 版本。
```Python hl_lines="16 18-20"
{!> ../../../docs_src/settings/app02/main.py!}
```
### 设置和测试
然后,在测试期间,通过创建 `get_settings` 的依赖项覆盖,很容易提供一个不同的设置对象:
```Python hl_lines="9-10 13 21"
{!../../../docs_src/settings/app02/test_main.py!}
```
在依赖项覆盖中,我们在创建新的 `Settings` 对象时为 `admin_email` 设置了一个新值,然后返回该新对象。
然后,我们可以测试它是否被使用。
## 从 `.env` 文件中读取设置
如果您有许多可能经常更改的设置,可能在不同的环境中,将它们放在一个文件中,然后从该文件中读取它们,就像它们是环境变量一样,可能非常有用。
这种做法相当常见,有一个名称,这些环境变量通常放在一个名为 `.env` 的文件中该文件被称为“dotenv”。
!!! tip
以点 (`.`) 开头的文件是 Unix-like 系统(如 Linux 和 macOS中的隐藏文件。
但是dotenv 文件实际上不一定要具有确切的文件名。
Pydantic 支持使用外部库从这些类型的文件中读取。您可以在<a href="https://pydantic-docs.helpmanual.io/usage/settings/#dotenv-env-support" class="external-link" target="_blank">Pydantic 设置: Dotenv (.env) 支持</a>中阅读更多相关信息。
!!! tip
要使其工作,您需要执行 `pip install python-dotenv`。
### `.env` 文件
您可以使用以下内容创建一个名为 `.env` 的文件:
```bash
ADMIN_EMAIL="deadpool@example.com"
APP_NAME="ChimichangApp"
```
### 从 `.env` 文件中读取设置
然后,您可以使用以下方式更新您的 `config.py`
```Python hl_lines="9-10"
{!../../../docs_src/settings/app03/config.py!}
```
在这里,我们在 Pydantic 的 `Settings` 类中创建了一个名为 `Config` 的类,并将 `env_file` 设置为我们想要使用的 dotenv 文件的文件名。
!!! tip
`Config` 类仅用于 Pydantic 配置。您可以在<a href="https://pydantic-docs.helpmanual.io/usage/model_config/" class="external-link" target="_blank">Pydantic Model Config</a>中阅读更多相关信息。
### 使用 `lru_cache` 仅创建一次 `Settings`
从磁盘中读取文件通常是一项耗时的(慢)操作,因此您可能希望仅在首次读取后并重复使用相同的设置对象,而不是为每个请求都读取它。
但是,每次执行以下操作:
```Python
Settings()
```
都会创建一个新的 `Settings` 对象,并且在创建时会再次读取 `.env` 文件。
如果依赖项函数只是这样的:
```Python
def get_settings():
return Settings()
```
我们将为每个请求创建该对象,并且将在每个请求中读取 `.env` 文件。 ⚠️
但是,由于我们在顶部使用了 `@lru_cache()` 装饰器,因此只有在第一次调用它时,才会创建 `Settings` 对象一次。 ✔️
=== "Python 3.9+"
```Python hl_lines="1 11"
{!> ../../../docs_src/settings/app03_an_py39/main.py!}
```
=== "Python 3.6+"
```Python hl_lines="1 11"
{!> ../../../docs_src/settings/app03_an/main.py!}
```
=== "Python 3.6+ 非注解版本"
!!! tip
如果可能,请尽量使用 `Annotated` 版本。
```Python hl_lines="1 10"
{!> ../../../docs_src/settings/app03/main.py!}
```
然后,在下一次请求的依赖项中对 `get_settings()` 进行任何后续调用时,它不会执行 `get_settings()` 的内部代码并创建新的 `Settings` 对象,而是返回在第一次调用时返回的相同对象,一次又一次。
#### `lru_cache` 技术细节
`@lru_cache()` 修改了它所装饰的函数,以返回第一次返回的相同值,而不是再次计算它,每次都执行函数的代码。
因此,下面的函数将对每个参数组合执行一次。然后,每个参数组合返回的值将在使用完全相同的参数组合调用函数时再次使用。
例如,如果您有一个函数:
```Python
@lru_cache()
def say_hi(name: str, salutation: str = "Ms."):
return f"Hello {salutation} {name}"
```
您的程序可以像这样执行:
```mermaid
sequenceDiagram
participant code as Code
participant function as say_hi()
participant execute as Execute function
rect rgba(0, 255, 0, .1)
code ->> function: say_hi(name="Camila")
function ->> execute: 执行函数代码
execute ->> code: 返回结果
end
rect rgba(0, 255, 255, .1)
code ->> function: say_hi(name="Camila")
function ->> code: 返回存储的结果
end
rect rgba(0, 255, 0, .1)
code ->> function: say_hi(name="Rick")
function ->> execute: 执行函数代码
execute ->> code: 返回结果
end
rect rgba(0, 255, 0, .1)
code ->> function: say_hi(name="Rick", salutation="Mr.")
function ->> execute: 执行函数代码
execute ->> code: 返回结果
end
rect rgba(0, 255, 255, .1)
code ->> function: say_hi(name="Rick")
function ->> code: 返回存储的结果
end
rect rgba(0, 255, 255, .1)
code ->> function: say_hi(name="Camila")
function ->> code: 返回存储的结果
end
```
对于我们的依赖项 `get_settings()`,该函数甚至不接受任何参数,因此它始终返回相同的值。
这样,它的行为几乎就像是一个全局变量。但是由于它使用了依赖项函数,因此我们可以轻松地进行测试时的覆盖。
`@lru_cache()` 是 `functools` 的一部分,它是 Python 标准库的一部分,您可以在<a href="https://docs.python.org/3/library/functools.html#functools.lru_cache" class="external-link" target="_blank">Python 文档中了解有关 `@lru_cache()` 的更多信息</a>。
## 小结
您可以使用 Pydantic 设置处理应用程序的设置或配置,利用 Pydantic 模型的所有功能。
* 通过使用依赖项,您可以简化测试。
* 您可以使用 `.env` 文件。
* 使用 `@lru_cache()` 可以避免为每个请求重复读取 dotenv 文件,同时允许您在测试时进行覆盖。

View File

@@ -0,0 +1,214 @@
# WebSockets
您可以在 **FastAPI** 中使用 [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API)。
## 安装 `WebSockets`
首先,您需要安装 `WebSockets`
```console
$ pip install websockets
---> 100%
```
## WebSockets 客户端
### 在生产环境中
在您的生产系统中您可能使用现代框架如React、Vue.js或Angular创建了一个前端。
要使用 WebSockets 与后端进行通信,您可能会使用前端的工具。
或者,您可能有一个原生移动应用程序,直接使用原生代码与 WebSocket 后端通信。
或者,您可能有其他与 WebSocket 终端通信的方式。
---
但是在本示例中我们将使用一个非常简单的HTML文档其中包含一些JavaScript全部放在一个长字符串中。
当然,这并不是最优的做法,您不应该在生产环境中使用它。
在生产环境中,您应该选择上述任一选项。
但这是一种专注于 WebSockets 的服务器端并提供一个工作示例的最简单方式:
```Python hl_lines="2 6-38 41-43"
{!../../../docs_src/websockets/tutorial001.py!}
```
## 创建 `websocket`
在您的 **FastAPI** 应用程序中,创建一个 `websocket`
```Python hl_lines="1 46-47"
{!../../../docs_src/websockets/tutorial001.py!}
```
!!! note "技术细节"
您也可以使用 `from starlette.websockets import WebSocket`。
**FastAPI** 直接提供了相同的 `WebSocket`,只是为了方便开发人员。但它直接来自 Starlette。
## 等待消息并发送消息
在您的 WebSocket 路由中,您可以使用 `await` 等待消息并发送消息。
```Python hl_lines="48-52"
{!../../../docs_src/websockets/tutorial001.py!}
```
您可以接收和发送二进制、文本和 JSON 数据。
## 尝试一下
如果您的文件名为 `main.py`,请使用以下命令运行应用程序:
```console
$ uvicorn main:app --reload
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
```
在浏览器中打开 <a href="http://127.0.0.1:8000" class="external-link" target="_blank">http://127.0.0.1:8000</a>。
您将看到一个简单的页面,如下所示:
<img src="/img/tutorial/websockets/image01.png">
您可以在输入框中输入消息并发送:
<img src="/img/tutorial/websockets/image02.png">
您的 **FastAPI** 应用程序将回复:
<img src="/img/tutorial/websockets/image03.png">
您可以发送(和接收)多条消息:
<img src="/img/tutorial/websockets/image04.png">
所有这些消息都将使用同一个 WebSocket 连
接。
## 使用 `Depends` 和其他依赖项
在 WebSocket 端点中,您可以从 `fastapi` 导入并使用以下内容:
* `Depends`
* `Security`
* `Cookie`
* `Header`
* `Path`
* `Query`
它们的工作方式与其他 FastAPI 端点/ *路径操作* 相同:
=== "Python 3.10+"
```Python hl_lines="68-69 82"
{!> ../../../docs_src/websockets/tutorial002_an_py310.py!}
```
=== "Python 3.9+"
```Python hl_lines="68-69 82"
{!> ../../../docs_src/websockets/tutorial002_an_py39.py!}
```
=== "Python 3.6+"
```Python hl_lines="69-70 83"
{!> ../../../docs_src/websockets/tutorial002_an.py!}
```
=== "Python 3.10+ 非带注解版本"
!!! tip
如果可能,请尽量使用 `Annotated` 版本。
```Python hl_lines="66-67 79"
{!> ../../../docs_src/websockets/tutorial002_py310.py!}
```
=== "Python 3.6+ 非带注解版本"
!!! tip
如果可能,请尽量使用 `Annotated` 版本。
```Python hl_lines="68-69 81"
{!> ../../../docs_src/websockets/tutorial002.py!}
```
!!! info
由于这是一个 WebSocket抛出 `HTTPException` 并不是很合理,而是抛出 `WebSocketException`。
您可以使用<a href="https://tools.ietf.org/html/rfc6455#section-7.4.1" class="external-link" target="_blank">规范中定义的有效代码</a>。
### 尝试带有依赖项的 WebSockets
如果您的文件名为 `main.py`,请使用以下命令运行应用程序:
```console
$ uvicorn main:app --reload
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
```
在浏览器中打开 <a href="http://127.0.0.1:8000" class="external-link" target="_blank">http://127.0.0.1:8000</a>。
在页面中,您可以设置:
* "Item ID",用于路径。
* "Token",作为查询参数。
!!! tip
注意,查询参数 `token` 将由依赖项处理。
通过这样,您可以连接 WebSocket然后发送和接收消息
<img src="/img/tutorial/websockets/image05.png">
## 处理断开连接和多个客户端
当 WebSocket 连接关闭时,`await websocket.receive_text()` 将引发 `WebSocketDisconnect` 异常,您可以捕获并处理该异常,就像本示例中的示例一样。
=== "Python 3.9+"
```Python hl_lines="79-81"
{!> ../../../docs_src/websockets/tutorial003_py39.py!}
```
=== "Python 3.6+"
```Python hl_lines="81-83"
{!> ../../../docs_src/websockets/tutorial003.py!}
```
尝试以下操作:
* 使用多个浏览器选项卡打开应用程序。
* 从这些选项卡中发送消息。
* 然后关闭其中一个选项卡。
这将引发 `WebSocketDisconnect` 异常,并且所有其他客户端都会收到类似以下的消息:
```
Client #1596980209979 left the chat
```
!!! tip
上面的应用程序是一个最小和简单的示例,用于演示如何处理和向多个 WebSocket 连接广播消息。
但请记住,由于所有内容都在内存中以单个列表的形式处理,因此它只能在进程运行时工作,并且只能使用单个进程。
如果您需要与 FastAPI 集成更简单但更强大的功能,支持 Redis、PostgreSQL 或其他功能,请查看 [encode/broadcaster](https://github.com/encode/broadcaster)。
## 更多信息
要了解更多选项,请查看 Starlette 的文档:
* [WebSocket 类](https://www.starlette.io/websockets/)
* [基于类的 WebSocket 处理](https://www.starlette.io/endpoints/#websocketendpoint)。

View File

@@ -437,7 +437,6 @@ item: Item
用于 Pydantic
* <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - 更快的 JSON <abbr title="将来自 HTTP 请求中的字符串转换为 Python 数据类型">「解析」</abbr>。
* <a href="https://github.com/JoshData/python-email-validator" target="_blank"><code>email_validator</code></a> - 用于 email 校验。
用于 Starlette

View File

@@ -0,0 +1,212 @@
# 测试
感谢 <a href="https://www.starlette.io/testclient/" class="external-link" target="_blank">Starlette</a>,测试**FastAPI** 应用轻松又愉快。
它基于 <a href="https://www.python-httpx.org" class="external-link" target="_blank">HTTPX</a> 而HTTPX又是基于Requests设计的所以很相似且易懂。
有了它,你可以直接与**FastAPI**一起使用 <a href="https://docs.pytest.org/" class="external-link" target="_blank">pytest</a>。
## 使用 `TestClient`
!!! 信息
要使用 `TestClient`,先要安装 <a href="https://www.python-httpx.org" class="external-link" target="_blank">`httpx`</a>.
例:`pip install httpx`.
导入 `TestClient`.
通过传入你的**FastAPI**应用创建一个 `TestClient`
创建名字以 `test_` 开头的函数(这是标准的 `pytest` 约定)。
像使用 `httpx` 那样使用 `TestClient` 对象。
为你需要检查的地方用标准的Python表达式写个简单的 `assert` 语句(重申,标准的`pytest`)。
```Python hl_lines="2 12 15-18"
{!../../../docs_src/app_testing/tutorial001.py!}
```
!!! 提示
注意测试函数是普通的 `def`,不是 `async def`。
还有client的调用也是普通的调用不是用 `await`。
这让你可以直接使用 `pytest` 而不会遇到麻烦。
!!! note "技术细节"
你也可以用 `from starlette.testclient import TestClient`。
**FastAPI** 提供了和 `starlette.testclient` 一样的 `fastapi.testclient`只是为了方便开发者。但它直接来自Starlette。
!!! 提示
除了发送请求之外如果你还想测试时在FastAPI应用中调用 `async` 函数(例如异步数据库函数), 可以在高级教程中看下 [Async Tests](../advanced/async-tests.md){.internal-link target=_blank} 。
## 分离测试
在实际应用中,你可能会把你的测试放在另一个文件里。
您的**FastAPI**应用程序也可能由一些文件/模块组成等等。
### **FastAPI** app 文件
假设你有一个像 [更大的应用](./bigger-applications.md){.internal-link target=_blank} 中所描述的文件结构:
```
.
├── app
│   ├── __init__.py
│   └── main.py
```
在 `main.py` 文件中你有一个 **FastAPI** app:
```Python
{!../../../docs_src/app_testing/main.py!}
```
### 测试文件
然后你会有一个包含测试的文件 `test_main.py` 。app可以像Python包那样存在一样是目录但有个 `__init__.py` 文件):
``` hl_lines="5"
.
├── app
│   ├── __init__.py
│   ├── main.py
│   └── test_main.py
```
因为这文件在同一个包中,所以你可以通过相对导入从 `main` 模块(`main.py`)导入`app`对象:
```Python hl_lines="3"
{!../../../docs_src/app_testing/test_main.py!}
```
...然后测试代码和之前一样的。
## 测试:扩展示例
现在让我们扩展这个例子,并添加更多细节,看下如何测试不同部分。
### 扩展后的 **FastAPI** app 文件
让我们继续之前的文件结构:
```
.
├── app
│   ├── __init__.py
│   ├── main.py
│   └── test_main.py
```
假设现在包含**FastAPI** app的文件 `main.py` 有些其他**路径操作**。
有个 `GET` 操作会返回错误。
有个 `POST` 操作会返回一些错误。
所有*路径操作* 都需要一个`X-Token` 头。
=== "Python 3.10+"
```Python
{!> ../../../docs_src/app_testing/app_b_an_py310/main.py!}
```
=== "Python 3.9+"
```Python
{!> ../../../docs_src/app_testing/app_b_an_py39/main.py!}
```
=== "Python 3.6+"
```Python
{!> ../../../docs_src/app_testing/app_b_an/main.py!}
```
=== "Python 3.10+ non-Annotated"
!!! tip
Prefer to use the `Annotated` version if possible.
```Python
{!> ../../../docs_src/app_testing/app_b_py310/main.py!}
```
=== "Python 3.6+ non-Annotated"
!!! tip
Prefer to use the `Annotated` version if possible.
```Python
{!> ../../../docs_src/app_testing/app_b/main.py!}
```
### 扩展后的测试文件
然后您可以使用扩展后的测试更新`test_main.py`
```Python
{!> ../../../docs_src/app_testing/app_b/test_main.py!}
```
每当你需要客户端在请求中传递信息,但你不知道如何传递时,你可以通过搜索(谷歌)如何用 `httpx`做,或者是用 `requests` 做毕竟HTTPX的设计是基于Requests的设计的。
接着只需在测试中同样操作。
示例:
* 传一个*路径* 或*查询* 参数添加到URL上。
* 传一个JSON体传一个Python对象(例如一个`dict`)到参数 `json`。
* 如果你需要发送 *Form Data* 而不是 JSON使用 `data` 参数。
* 要发送 *headers*,传 `dict` 给 `headers` 参数。
* 对于 *cookies*,传 `dict` 给 `cookies` 参数。
关于如何传数据给后端的更多信息 (使用`httpx` 或 `TestClient`),请查阅 <a href="https://www.python-httpx.org" class="external-link" target="_blank">HTTPX 文档</a>.
!!! 信息
注意 `TestClient` 接收可以被转化为JSON的数据而不是Pydantic模型。
如果你在测试中有一个Pydantic模型并且你想在测试时发送它的数据给应用你可以使用在[JSON Compatible Encoder](encoder.md){.internal-link target=_blank}介绍的`jsonable_encoder` 。
## 运行起来
之后,你只需要安装 `pytest`:
<div class="termy">
```console
$ pip install pytest
---> 100%
```
</div>
他会自动检测文件和测试,执行测试,然后向你报告结果。
执行测试:
<div class="termy">
```console
$ pytest
================ test session starts ================
platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1
rootdir: /home/user/code/superawesome-cli/app
plugins: forked-1.1.3, xdist-1.31.0, cov-2.8.1
collected 6 items
---> 100%
test_main.py <span style="color: green; white-space: pre;">...... [100%]</span>
<span style="color: green;">================= 1 passed in 0.03s =================</span>
```
</div>

View File

@@ -109,6 +109,7 @@ nav:
- tutorial/bigger-applications.md
- tutorial/metadata.md
- tutorial/static-files.md
- tutorial/testing.md
- tutorial/debugging.md
- 高级用户指南:
- advanced/index.md
@@ -118,8 +119,12 @@ nav:
- advanced/custom-response.md
- advanced/response-cookies.md
- advanced/response-change-status-code.md
- advanced/settings.md
- advanced/response-headers.md
- advanced/websockets.md
- advanced/wsgi.md
- 高级安全:
- advanced/security/index.md
- contributing.md
- help-fastapi.md
- benchmarks.md

View File

@@ -12,11 +12,11 @@ class BaseItem(BaseModel):
class CarItem(BaseItem):
type: str = "car"
type = "car"
class PlaneItem(BaseItem):
type: str = "plane"
type = "plane"
size: int

View File

@@ -12,11 +12,11 @@ class BaseItem(BaseModel):
class CarItem(BaseItem):
type: str = "car"
type = "car"
class PlaneItem(BaseItem):
type: str = "plane"
type = "plane"
size: int

View File

@@ -1,14 +1,17 @@
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool
from ..database import Base
from ..main import app, get_db
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
SQLALCHEMY_DATABASE_URL = "sqlite://"
engine = create_engine(
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
SQLALCHEMY_DATABASE_URL,
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

View File

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

View File

@@ -1,595 +0,0 @@
from collections import deque
from copy import copy
from dataclasses import dataclass, is_dataclass
from enum import Enum
from typing import (
Any,
Callable,
Deque,
Dict,
FrozenSet,
List,
Mapping,
Sequence,
Set,
Tuple,
Type,
Union,
)
from fastapi.exceptions import RequestErrorModel
from fastapi.types import IncEx, ModelNameMap, UnionType
from pydantic import BaseModel, create_model
from pydantic.version import VERSION as PYDANTIC_VERSION
from starlette.datastructures import UploadFile
from typing_extensions import Annotated, Literal, get_args, get_origin
PYDANTIC_V2 = PYDANTIC_VERSION.startswith("2.")
sequence_annotation_to_type = {
Sequence: list,
List: list,
list: list,
Tuple: tuple,
tuple: tuple,
Set: set,
set: set,
FrozenSet: frozenset,
frozenset: frozenset,
Deque: deque,
deque: deque,
}
sequence_types = tuple(sequence_annotation_to_type.keys())
if PYDANTIC_V2:
from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError
from pydantic import TypeAdapter
from pydantic import ValidationError as ValidationError
from pydantic._internal._schema_generation_shared import ( # type: ignore[attr-defined]
GetJsonSchemaHandler as GetJsonSchemaHandler,
)
from pydantic._internal._typing_extra import eval_type_lenient
from pydantic._internal._utils import lenient_issubclass as lenient_issubclass
from pydantic.fields import FieldInfo
from pydantic.json_schema import GenerateJsonSchema as GenerateJsonSchema
from pydantic.json_schema import JsonSchemaValue as JsonSchemaValue
from pydantic_core import CoreSchema as CoreSchema
from pydantic_core import MultiHostUrl as MultiHostUrl
from pydantic_core import PydanticUndefined, PydanticUndefinedType
from pydantic_core import Url as Url
from pydantic_core.core_schema import (
general_plain_validator_function as general_plain_validator_function,
)
Required = PydanticUndefined
Undefined = PydanticUndefined
UndefinedType = PydanticUndefinedType
evaluate_forwardref = eval_type_lenient
Validator = Any
class BaseConfig:
pass
class ErrorWrapper(Exception):
pass
@dataclass
class ModelField:
field_info: FieldInfo
name: str
@property
def alias(self) -> str:
a = self.field_info.alias
return a if a is not None else self.name
@property
def required(self) -> bool:
return self.field_info.is_required()
@property
def default(self) -> Any:
return self.get_default()
@property
def type_(self) -> Any:
return self.field_info.annotation
def __post_init__(self) -> None:
self._type_adapter: TypeAdapter[Any] = TypeAdapter(
Annotated[self.field_info.annotation, self.field_info]
)
def get_default(self) -> Any:
if self.field_info.is_required():
return Undefined
return self.field_info.get_default(call_default_factory=True)
def validate(
self,
value: Any,
values: Dict[str, Any] = {}, # noqa: B006
*,
loc: Tuple[Union[int, str], ...] = (),
) -> Tuple[Any, Union[List[Dict[str, Any]], None]]:
try:
return (
self._type_adapter.validate_python(value, from_attributes=True),
None,
)
except ValidationError as exc:
return None, _regenerate_error_with_loc(
errors=exc.errors(), loc_prefix=loc
)
def serialize(
self,
value: Any,
*,
mode: Literal["json", "python"] = "json",
include: Union[IncEx, None] = None,
exclude: Union[IncEx, None] = None,
by_alias: bool = True,
exclude_unset: bool = False,
exclude_defaults: bool = False,
exclude_none: bool = False,
) -> Any:
# What calls this code passes a value that already called
# self._type_adapter.validate_python(value)
return self._type_adapter.dump_python(
value,
mode=mode,
include=include,
exclude=exclude,
by_alias=by_alias,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
)
def __hash__(self) -> int:
# Each ModelField is unique for our purposes, to allow making a dict from
# ModelField to its JSON Schema.
return id(self)
def get_annotation_from_field_info(
annotation: Any, field_info: FieldInfo, field_name: str
) -> Any:
return annotation
def _normalize_errors(errors: Sequence[Any]) -> List[Dict[str, Any]]:
return errors # type: ignore[return-value]
def _model_rebuild(model: Type[BaseModel]) -> None:
model.model_rebuild()
def _model_dump(
model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any
) -> Any:
return model.model_dump(mode=mode, **kwargs)
def _get_model_config(model: BaseModel) -> Any:
return model.model_config
def get_schema_from_model_field(
*,
field: ModelField,
schema_generator: GenerateJsonSchema,
model_name_map: ModelNameMap,
) -> Dict[str, Any]:
# This expects that GenerateJsonSchema was already used to generate the definitions
json_schema = schema_generator.generate_inner(field._type_adapter.core_schema)
if "$ref" not in json_schema:
# TODO remove when deprecating Pydantic v1
# Ref: https://github.com/pydantic/pydantic/blob/d61792cc42c80b13b23e3ffa74bc37ec7c77f7d1/pydantic/schema.py#L207
json_schema[
"title"
] = field.field_info.title or field.alias.title().replace("_", " ")
return json_schema
def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap:
return {}
def get_definitions(
*,
fields: List[ModelField],
schema_generator: GenerateJsonSchema,
model_name_map: ModelNameMap,
) -> Dict[str, Dict[str, Any]]:
inputs = [
(field, "validation", field._type_adapter.core_schema) for field in fields
]
_, definitions = schema_generator.generate_definitions(inputs=inputs) # type: ignore[arg-type]
return definitions # type: ignore[return-value]
def is_scalar_field(field: ModelField) -> bool:
from fastapi import params
return field_annotation_is_scalar(
field.field_info.annotation
) and not isinstance(field.field_info, params.Body)
def is_sequence_field(field: ModelField) -> bool:
return field_annotation_is_sequence(field.field_info.annotation)
def is_scalar_sequence_field(field: ModelField) -> bool:
return field_annotation_is_scalar_sequence(field.field_info.annotation)
def is_bytes_field(field: ModelField) -> bool:
return is_bytes_or_nonable_bytes_annotation(field.type_)
def is_bytes_sequence_field(field: ModelField) -> bool:
return is_bytes_sequence_annotation(field.type_)
def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:
return type(field_info).from_annotation(annotation)
def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:
origin_type = (
get_origin(field.field_info.annotation) or field.field_info.annotation
)
assert issubclass(origin_type, sequence_types) # type: ignore[arg-type]
return sequence_annotation_to_type[origin_type](value) # type: ignore[no-any-return]
def get_missing_field_error(loc: Tuple[str, ...]) -> Dict[str, Any]:
error = ValidationError.from_exception_data(
"Field required", [{"type": "missing", "loc": loc, "input": {}}]
).errors()[0]
error["input"] = None
return error # type: ignore[return-value]
def create_body_model(
*, fields: Sequence[ModelField], model_name: str
) -> Type[BaseModel]:
field_params = {f.name: (f.field_info.annotation, f.field_info) for f in fields}
BodyModel: Type[BaseModel] = create_model(model_name, **field_params) # type: ignore[call-overload]
return BodyModel
else:
from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX
from pydantic import AnyUrl as Url # noqa: F401
from pydantic import ( # type: ignore[assignment]
BaseConfig as BaseConfig, # noqa: F401
)
from pydantic import ValidationError as ValidationError # noqa: F401
from pydantic.class_validators import ( # type: ignore[no-redef]
Validator as Validator, # noqa: F401
)
from pydantic.error_wrappers import ( # type: ignore[no-redef]
ErrorWrapper as ErrorWrapper, # noqa: F401
)
from pydantic.errors import MissingError
from pydantic.fields import ( # type: ignore[attr-defined]
SHAPE_FROZENSET,
SHAPE_LIST,
SHAPE_SEQUENCE,
SHAPE_SET,
SHAPE_SINGLETON,
SHAPE_TUPLE,
SHAPE_TUPLE_ELLIPSIS,
)
from pydantic.fields import FieldInfo as FieldInfo
from pydantic.fields import ( # type: ignore[no-redef,attr-defined]
ModelField as ModelField, # noqa: F401
)
from pydantic.fields import ( # type: ignore[no-redef,attr-defined]
Required as Required, # noqa: F401
)
from pydantic.fields import ( # type: ignore[no-redef,attr-defined]
Undefined as Undefined,
)
from pydantic.fields import ( # type: ignore[no-redef, attr-defined]
UndefinedType as UndefinedType, # noqa: F401
)
from pydantic.networks import ( # type: ignore[no-redef]
MultiHostDsn as MultiHostUrl, # noqa: F401
)
from pydantic.schema import (
field_schema,
get_flat_models_from_fields,
get_model_name_map,
model_process_schema,
)
from pydantic.schema import ( # type: ignore[no-redef] # noqa: F401
get_annotation_from_field_info as get_annotation_from_field_info,
)
from pydantic.typing import ( # type: ignore[no-redef]
evaluate_forwardref as evaluate_forwardref, # noqa: F401
)
from pydantic.utils import ( # type: ignore[no-redef]
lenient_issubclass as lenient_issubclass, # noqa: F401
)
GetJsonSchemaHandler = Any # type: ignore[assignment,misc]
JsonSchemaValue = Dict[str, Any] # type: ignore[misc]
CoreSchema = Any # type: ignore[assignment,misc]
sequence_shapes = {
SHAPE_LIST,
SHAPE_SET,
SHAPE_FROZENSET,
SHAPE_TUPLE,
SHAPE_SEQUENCE,
SHAPE_TUPLE_ELLIPSIS,
}
sequence_shape_to_type = {
SHAPE_LIST: list,
SHAPE_SET: set,
SHAPE_TUPLE: tuple,
SHAPE_SEQUENCE: list,
SHAPE_TUPLE_ELLIPSIS: list,
}
@dataclass
class GenerateJsonSchema: # type: ignore[no-redef]
ref_template: str
class PydanticSchemaGenerationError(Exception): # type: ignore[no-redef]
pass
def general_plain_validator_function( # type: ignore[misc]
function: Callable[..., Any],
*,
ref: Union[str, None] = None,
metadata: Any = None,
serialization: Any = None,
) -> Any:
return {}
def get_model_definitions(
*,
flat_models: Set[Union[Type[BaseModel], Type[Enum]]],
model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str],
) -> Dict[str, Any]:
definitions: Dict[str, Dict[str, Any]] = {}
for model in flat_models:
m_schema, m_definitions, m_nested_models = model_process_schema(
model, model_name_map=model_name_map, ref_prefix=REF_PREFIX
)
definitions.update(m_definitions)
model_name = model_name_map[model]
if "description" in m_schema:
m_schema["description"] = m_schema["description"].split("\f")[0]
definitions[model_name] = m_schema
return definitions
def is_pv1_scalar_field(field: ModelField) -> bool:
from fastapi import params
field_info = field.field_info
if not (
field.shape == SHAPE_SINGLETON # type: ignore[attr-defined]
and not lenient_issubclass(field.type_, BaseModel)
and not lenient_issubclass(field.type_, dict)
and not field_annotation_is_sequence(field.type_)
and not is_dataclass(field.type_)
and not isinstance(field_info, params.Body)
):
return False
if field.sub_fields: # type: ignore[attr-defined]
if not all(
is_pv1_scalar_field(f)
for f in field.sub_fields # type: ignore[attr-defined]
):
return False
return True
def is_pv1_scalar_sequence_field(field: ModelField) -> bool:
if (field.shape in sequence_shapes) and not lenient_issubclass( # type: ignore[attr-defined]
field.type_, BaseModel
):
if field.sub_fields is not None: # type: ignore[attr-defined]
for sub_field in field.sub_fields: # type: ignore[attr-defined]
if not is_pv1_scalar_field(sub_field):
return False
return True
if _annotation_is_sequence(field.type_):
return True
return False
def _normalize_errors(errors: Sequence[Any]) -> List[Dict[str, Any]]:
use_errors: List[Any] = []
for error in errors:
if isinstance(error, ErrorWrapper):
new_errors = ValidationError( # type: ignore[call-arg]
errors=[error], model=RequestErrorModel
).errors()
use_errors.extend(new_errors)
elif isinstance(error, list):
use_errors.extend(_normalize_errors(error))
else:
use_errors.append(error)
return use_errors
def _model_rebuild(model: Type[BaseModel]) -> None:
model.update_forward_refs()
def _model_dump(
model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any
) -> Any:
return model.dict(**kwargs)
def _get_model_config(model: BaseModel) -> Any:
return model.__config__ # type: ignore[attr-defined]
def get_schema_from_model_field(
*,
field: ModelField,
schema_generator: GenerateJsonSchema,
model_name_map: ModelNameMap,
) -> Dict[str, Any]:
# This expects that GenerateJsonSchema was already used to generate the definitions
return field_schema( # type: ignore[no-any-return]
field, model_name_map=model_name_map, ref_prefix=REF_PREFIX
)[0]
def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap:
models = get_flat_models_from_fields(fields, known_models=set())
return get_model_name_map(models) # type: ignore[no-any-return]
def get_definitions(
*,
fields: List[ModelField],
schema_generator: GenerateJsonSchema,
model_name_map: ModelNameMap,
) -> Dict[str, Dict[str, Any]]:
models = get_flat_models_from_fields(fields, known_models=set())
return get_model_definitions(flat_models=models, model_name_map=model_name_map)
def is_scalar_field(field: ModelField) -> bool:
return is_pv1_scalar_field(field)
def is_sequence_field(field: ModelField) -> bool:
return field.shape in sequence_shapes or _annotation_is_sequence(field.type_) # type: ignore[attr-defined]
def is_scalar_sequence_field(field: ModelField) -> bool:
return is_pv1_scalar_sequence_field(field)
def is_bytes_field(field: ModelField) -> bool:
return lenient_issubclass(field.type_, bytes)
def is_bytes_sequence_field(field: ModelField) -> bool:
return field.shape in sequence_shapes and lenient_issubclass(field.type_, bytes) # type: ignore[attr-defined]
def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:
return copy(field_info)
def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:
return sequence_shape_to_type[field.shape](value) # type: ignore[no-any-return,attr-defined]
def get_missing_field_error(loc: Tuple[str, ...]) -> Dict[str, Any]:
missing_field_error = ErrorWrapper(MissingError(), loc=loc) # type: ignore[call-arg]
new_error = ValidationError([missing_field_error], RequestErrorModel)
return new_error.errors()[0] # type: ignore[return-value]
def create_body_model(
*, fields: Sequence[ModelField], model_name: str
) -> Type[BaseModel]:
BodyModel = create_model(model_name)
for f in fields:
BodyModel.__fields__[f.name] = f # type: ignore[index]
return BodyModel
def _regenerate_error_with_loc(
*, errors: Sequence[Any], loc_prefix: Tuple[Union[str, int], ...]
) -> List[Dict[str, Any]]:
updated_loc_errors: List[Any] = [
{**err, "loc": loc_prefix + err.get("loc", ())}
for err in _normalize_errors(errors)
]
return updated_loc_errors
def _annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool:
if lenient_issubclass(annotation, (str, bytes)):
return False
return lenient_issubclass(annotation, sequence_types)
def field_annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool:
return _annotation_is_sequence(annotation) or _annotation_is_sequence(
get_origin(annotation)
)
def value_is_sequence(value: Any) -> bool:
return isinstance(value, sequence_types) and not isinstance(value, (str, bytes)) # type: ignore[arg-type]
def _annotation_is_complex(annotation: Union[Type[Any], None]) -> bool:
return (
lenient_issubclass(annotation, (BaseModel, Mapping, UploadFile))
or _annotation_is_sequence(annotation)
or is_dataclass(annotation)
)
def field_annotation_is_complex(annotation: Union[Type[Any], None]) -> bool:
origin = get_origin(annotation)
if origin is Union or origin is UnionType:
return any(field_annotation_is_complex(arg) for arg in get_args(annotation))
return (
_annotation_is_complex(annotation)
or _annotation_is_complex(origin)
or hasattr(origin, "__pydantic_core_schema__")
or hasattr(origin, "__get_pydantic_core_schema__")
)
def field_annotation_is_scalar(annotation: Any) -> bool:
# handle Ellipsis here to make tuple[int, ...] work nicely
return annotation is Ellipsis or not field_annotation_is_complex(annotation)
def field_annotation_is_scalar_sequence(annotation: Union[Type[Any], None]) -> bool:
origin = get_origin(annotation)
if origin is Union or origin is UnionType:
at_least_one_scalar_sequence = False
for arg in get_args(annotation):
if field_annotation_is_scalar_sequence(arg):
at_least_one_scalar_sequence = True
continue
elif not field_annotation_is_scalar(arg):
return False
return at_least_one_scalar_sequence
return field_annotation_is_sequence(annotation) and all(
field_annotation_is_scalar(sub_annotation)
for sub_annotation in get_args(annotation)
)
def is_bytes_or_nonable_bytes_annotation(annotation: Any) -> bool:
if lenient_issubclass(annotation, bytes):
return True
origin = get_origin(annotation)
if origin is Union or origin is UnionType:
for arg in get_args(annotation):
if lenient_issubclass(arg, bytes):
return True
return False
def is_uploadfile_or_nonable_uploadfile_annotation(annotation: Any) -> bool:
if lenient_issubclass(annotation, UploadFile):
return True
origin = get_origin(annotation)
if origin is Union or origin is UnionType:
for arg in get_args(annotation):
if lenient_issubclass(arg, UploadFile):
return True
return False
def is_bytes_sequence_annotation(annotation: Any) -> bool:
origin = get_origin(annotation)
if origin is Union or origin is UnionType:
at_least_one = False
for arg in get_args(annotation):
if is_bytes_sequence_annotation(arg):
at_least_one = True
continue
return at_least_one
return field_annotation_is_sequence(annotation) and all(
is_bytes_or_nonable_bytes_annotation(sub_annotation)
for sub_annotation in get_args(annotation)
)
def is_uploadfile_sequence_annotation(annotation: Any) -> bool:
origin = get_origin(annotation)
if origin is Union or origin is UnionType:
at_least_one = False
for arg in get_args(annotation):
if is_uploadfile_sequence_annotation(arg):
at_least_one = True
continue
return at_least_one
return field_annotation_is_sequence(annotation) and all(
is_uploadfile_or_nonable_uploadfile_annotation(sub_annotation)
for sub_annotation in get_args(annotation)
)

View File

@@ -15,6 +15,7 @@ from typing import (
from fastapi import routing
from fastapi.datastructures import Default, DefaultPlaceholder
from fastapi.encoders import DictIntStrAny, SetIntStr
from fastapi.exception_handlers import (
http_exception_handler,
request_validation_exception_handler,
@@ -30,7 +31,7 @@ from fastapi.openapi.docs import (
)
from fastapi.openapi.utils import get_openapi
from fastapi.params import Depends
from fastapi.types import DecoratedCallable, IncEx
from fastapi.types import DecoratedCallable
from fastapi.utils import generate_unique_id
from starlette.applications import Starlette
from starlette.datastructures import State
@@ -61,6 +62,7 @@ class FastAPI(Starlette):
servers: Optional[List[Dict[str, Union[str, Any]]]] = None,
dependencies: Optional[Sequence[Depends]] = None,
default_response_class: Type[Response] = Default(JSONResponse),
redirect_slashes: bool = True,
docs_url: Optional[str] = "/docs",
redoc_url: Optional[str] = "/redoc",
swagger_ui_oauth2_redirect_url: Optional[str] = "/docs/oauth2-redirect",
@@ -126,6 +128,7 @@ class FastAPI(Starlette):
self.dependency_overrides: Dict[Callable[..., Any], Callable[..., Any]] = {}
self.router: routing.APIRouter = routing.APIRouter(
routes=routes,
redirect_slashes=redirect_slashes,
dependency_overrides_provider=self,
on_startup=on_startup,
on_shutdown=on_shutdown,
@@ -296,8 +299,8 @@ class FastAPI(Starlette):
deprecated: Optional[bool] = None,
methods: Optional[List[str]] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -354,8 +357,8 @@ class FastAPI(Starlette):
deprecated: Optional[bool] = None,
methods: Optional[List[str]] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -475,8 +478,8 @@ class FastAPI(Starlette):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -530,8 +533,8 @@ class FastAPI(Starlette):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -585,8 +588,8 @@ class FastAPI(Starlette):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -640,8 +643,8 @@ class FastAPI(Starlette):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -695,8 +698,8 @@ class FastAPI(Starlette):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -750,8 +753,8 @@ class FastAPI(Starlette):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -805,8 +808,8 @@ class FastAPI(Starlette):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -860,8 +863,8 @@ class FastAPI(Starlette):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,

View File

@@ -1,12 +1,5 @@
from typing import Any, Callable, Dict, Iterable, Type, TypeVar, cast
from typing import Any, Callable, Dict, Iterable, Type, TypeVar
from fastapi._compat import (
PYDANTIC_V2,
CoreSchema,
GetJsonSchemaHandler,
JsonSchemaValue,
general_plain_validator_function,
)
from starlette.datastructures import URL as URL # noqa: F401
from starlette.datastructures import Address as Address # noqa: F401
from starlette.datastructures import FormData as FormData # noqa: F401
@@ -28,28 +21,8 @@ class UploadFile(StarletteUploadFile):
return v
@classmethod
def _validate(cls, __input_value: Any, _: Any) -> "UploadFile":
if not isinstance(__input_value, StarletteUploadFile):
raise ValueError(f"Expected UploadFile, received: {type(__input_value)}")
return cast(UploadFile, __input_value)
if not PYDANTIC_V2:
@classmethod
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
field_schema.update({"type": "string", "format": "binary"})
@classmethod
def __get_pydantic_json_schema__(
cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler
) -> JsonSchemaValue:
return {"type": "string", "format": "binary"}
@classmethod
def __get_pydantic_core_schema__(
cls, source: Type[Any], handler: Callable[[Any], CoreSchema]
) -> CoreSchema:
return general_plain_validator_function(cls._validate)
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
field_schema.update({"type": "string", "format": "binary"})
class DefaultPlaceholder:

View File

@@ -1,7 +1,7 @@
from typing import Any, Callable, List, Optional, Sequence
from fastapi._compat import ModelField
from fastapi.security.base import SecurityBase
from pydantic.fields import ModelField
class SecurityRequirement:

View File

@@ -1,6 +1,7 @@
import dataclasses
import inspect
from contextlib import contextmanager
from copy import deepcopy
from copy import copy, deepcopy
from typing import (
Any,
Callable,
@@ -19,31 +20,6 @@ from typing import (
import anyio
from fastapi import params
from fastapi._compat import (
PYDANTIC_V2,
ErrorWrapper,
ModelField,
Required,
Undefined,
_regenerate_error_with_loc,
copy_field_info,
create_body_model,
evaluate_forwardref,
field_annotation_is_scalar,
get_annotation_from_field_info,
get_missing_field_error,
is_bytes_field,
is_bytes_sequence_field,
is_scalar_field,
is_scalar_sequence_field,
is_sequence_field,
is_uploadfile_or_nonable_uploadfile_annotation,
is_uploadfile_sequence_annotation,
lenient_issubclass,
sequence_types,
serialize_sequence_value,
value_is_sequence,
)
from fastapi.concurrency import (
AsyncExitStack,
asynccontextmanager,
@@ -55,14 +31,50 @@ from fastapi.security.base import SecurityBase
from fastapi.security.oauth2 import OAuth2, SecurityScopes
from fastapi.security.open_id_connect_url import OpenIdConnect
from fastapi.utils import create_response_field, get_path_param_names
from pydantic.fields import FieldInfo
from pydantic import BaseModel, create_model
from pydantic.error_wrappers import ErrorWrapper
from pydantic.errors import MissingError
from pydantic.fields import (
SHAPE_FROZENSET,
SHAPE_LIST,
SHAPE_SEQUENCE,
SHAPE_SET,
SHAPE_SINGLETON,
SHAPE_TUPLE,
SHAPE_TUPLE_ELLIPSIS,
FieldInfo,
ModelField,
Required,
Undefined,
)
from pydantic.schema import get_annotation_from_field_info
from pydantic.typing import evaluate_forwardref, get_args, get_origin
from pydantic.utils import lenient_issubclass
from starlette.background import BackgroundTasks
from starlette.concurrency import run_in_threadpool
from starlette.datastructures import FormData, Headers, QueryParams, UploadFile
from starlette.requests import HTTPConnection, Request
from starlette.responses import Response
from starlette.websockets import WebSocket
from typing_extensions import Annotated, get_args, get_origin
from typing_extensions import Annotated
sequence_shapes = {
SHAPE_LIST,
SHAPE_SET,
SHAPE_FROZENSET,
SHAPE_TUPLE,
SHAPE_SEQUENCE,
SHAPE_TUPLE_ELLIPSIS,
}
sequence_types = (list, set, tuple)
sequence_shape_to_type = {
SHAPE_LIST: list,
SHAPE_SET: set,
SHAPE_TUPLE: tuple,
SHAPE_SEQUENCE: list,
SHAPE_TUPLE_ELLIPSIS: list,
}
multipart_not_installed_error = (
'Form data requires "python-multipart" to be installed. \n'
@@ -204,6 +216,36 @@ def get_flat_params(dependant: Dependant) -> List[ModelField]:
)
def is_scalar_field(field: ModelField) -> bool:
field_info = field.field_info
if not (
field.shape == SHAPE_SINGLETON
and not lenient_issubclass(field.type_, BaseModel)
and not lenient_issubclass(field.type_, sequence_types + (dict,))
and not dataclasses.is_dataclass(field.type_)
and not isinstance(field_info, params.Body)
):
return False
if field.sub_fields:
if not all(is_scalar_field(f) for f in field.sub_fields):
return False
return True
def is_scalar_sequence_field(field: ModelField) -> bool:
if (field.shape in sequence_shapes) and not lenient_issubclass(
field.type_, BaseModel
):
if field.sub_fields is not None:
for sub_field in field.sub_fields:
if not is_scalar_field(sub_field):
return False
return True
if lenient_issubclass(field.type_, sequence_types):
return True
return False
def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
signature = inspect.signature(call)
globalns = getattr(call, "__globals__", {})
@@ -322,11 +364,12 @@ def analyze_param(
is_path_param: bool,
) -> Tuple[Any, Optional[params.Depends], Optional[ModelField]]:
field_info = None
used_default_field_info = False
depends = None
type_annotation: Any = Any
if (
annotation is not inspect.Signature.empty
and get_origin(annotation) is Annotated
and get_origin(annotation) is Annotated # type: ignore[comparison-overlap]
):
annotated_args = get_args(annotation)
type_annotation = annotated_args[0]
@@ -341,9 +384,7 @@ def analyze_param(
fastapi_annotation = next(iter(fastapi_annotations), None)
if isinstance(fastapi_annotation, FieldInfo):
# Copy `field_info` because we mutate `field_info.default` below.
field_info = copy_field_info(
field_info=fastapi_annotation, annotation=annotation
)
field_info = copy(fastapi_annotation)
assert field_info.default is Undefined or field_info.default is Required, (
f"`{field_info.__class__.__name__}` default value cannot be set in"
f" `Annotated` for {param_name!r}. Set the default value with `=` instead."
@@ -374,8 +415,6 @@ def analyze_param(
f" together for {param_name!r}"
)
field_info = value
if PYDANTIC_V2:
field_info.annotation = type_annotation
if depends is not None and depends.dependency is None:
depends.dependency = type_annotation
@@ -394,15 +433,10 @@ def analyze_param(
# We might check here that `default_value is Required`, but the fact is that the same
# parameter might sometimes be a path parameter and sometimes not. See
# `tests/test_infer_param_optionality.py` for an example.
field_info = params.Path(annotation=type_annotation)
elif is_uploadfile_or_nonable_uploadfile_annotation(
type_annotation
) or is_uploadfile_sequence_annotation(type_annotation):
field_info = params.File(annotation=type_annotation, default=default_value)
elif not field_annotation_is_scalar(annotation=type_annotation):
field_info = params.Body(annotation=type_annotation, default=default_value)
field_info = params.Path()
else:
field_info = params.Query(annotation=type_annotation, default=default_value)
field_info = params.Query(default=default_value)
used_default_field_info = True
field = None
if field_info is not None:
@@ -416,8 +450,8 @@ def analyze_param(
and getattr(field_info, "in_", None) is None
):
field_info.in_ = params.ParamTypes.query
use_annotation = get_annotation_from_field_info(
type_annotation,
annotation = get_annotation_from_field_info(
annotation if annotation is not inspect.Signature.empty else Any,
field_info,
param_name,
)
@@ -425,15 +459,19 @@ def analyze_param(
alias = param_name.replace("_", "-")
else:
alias = field_info.alias or param_name
field_info.alias = alias
field = create_response_field(
name=param_name,
type_=use_annotation,
type_=annotation,
default=field_info.default,
alias=alias,
required=field_info.default in (Required, Undefined),
field_info=field_info,
)
if used_default_field_info:
if lenient_issubclass(field.type_, UploadFile):
field.field_info = params.File(field_info.default)
elif not is_scalar_field(field=field):
field.field_info = params.Body(field_info.default)
return type_annotation, depends, field
@@ -516,13 +554,13 @@ async def solve_dependencies(
dependency_cache: Optional[Dict[Tuple[Callable[..., Any], Tuple[str]], Any]] = None,
) -> Tuple[
Dict[str, Any],
List[Any],
List[ErrorWrapper],
Optional[BackgroundTasks],
Response,
Dict[Tuple[Callable[..., Any], Tuple[str]], Any],
]:
values: Dict[str, Any] = {}
errors: List[Any] = []
errors: List[ErrorWrapper] = []
if response is None:
response = Response()
del response.headers["content-length"]
@@ -636,7 +674,7 @@ async def solve_dependencies(
def request_params_to_args(
required_params: Sequence[ModelField],
received_params: Union[Mapping[str, Any], QueryParams, Headers],
) -> Tuple[Dict[str, Any], List[Any]]:
) -> Tuple[Dict[str, Any], List[ErrorWrapper]]:
values = {}
errors = []
for field in required_params:
@@ -650,19 +688,23 @@ def request_params_to_args(
assert isinstance(
field_info, params.Param
), "Params must be subclasses of Param"
loc = (field_info.in_.value, field.alias)
if value is None:
if field.required:
errors.append(get_missing_field_error(loc=loc))
errors.append(
ErrorWrapper(
MissingError(), loc=(field_info.in_.value, field.alias)
)
)
else:
values[field.name] = deepcopy(field.default)
continue
v_, errors_ = field.validate(value, values, loc=loc)
v_, errors_ = field.validate(
value, values, loc=(field_info.in_.value, field.alias)
)
if isinstance(errors_, ErrorWrapper):
errors.append(errors_)
elif isinstance(errors_, list):
new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=())
errors.extend(new_errors)
errors.extend(errors_)
else:
values[field.name] = v_
return values, errors
@@ -671,9 +713,9 @@ def request_params_to_args(
async def request_body_to_args(
required_params: List[ModelField],
received_body: Optional[Union[Dict[str, Any], FormData]],
) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]:
) -> Tuple[Dict[str, Any], List[ErrorWrapper]]:
values = {}
errors: List[Dict[str, Any]] = []
errors = []
if required_params:
field = required_params[0]
field_info = field.field_info
@@ -691,7 +733,9 @@ async def request_body_to_args(
value: Optional[Any] = None
if received_body is not None:
if (is_sequence_field(field)) and isinstance(received_body, FormData):
if (
field.shape in sequence_shapes or field.type_ in sequence_types
) and isinstance(received_body, FormData):
value = received_body.getlist(field.alias)
else:
try:
@@ -704,7 +748,7 @@ async def request_body_to_args(
or (isinstance(field_info, params.Form) and value == "")
or (
isinstance(field_info, params.Form)
and is_sequence_field(field)
and field.shape in sequence_shapes
and len(value) == 0
)
):
@@ -715,17 +759,16 @@ async def request_body_to_args(
continue
if (
isinstance(field_info, params.File)
and is_bytes_field(field)
and lenient_issubclass(field.type_, bytes)
and isinstance(value, UploadFile)
):
value = await value.read()
elif (
is_bytes_sequence_field(field)
field.shape in sequence_shapes
and isinstance(field_info, params.File)
and value_is_sequence(value)
and lenient_issubclass(field.type_, bytes)
and isinstance(value, sequence_types)
):
# For types
assert isinstance(value, sequence_types) # type: ignore[arg-type]
results: List[Union[bytes, str]] = []
async def process_fn(
@@ -737,19 +780,24 @@ async def request_body_to_args(
async with anyio.create_task_group() as tg:
for sub_value in value:
tg.start_soon(process_fn, sub_value.read)
value = serialize_sequence_value(field=field, value=results)
value = sequence_shape_to_type[field.shape](results)
v_, errors_ = field.validate(value, values, loc=loc)
if isinstance(errors_, list):
errors.extend(errors_)
elif errors_:
if isinstance(errors_, ErrorWrapper):
errors.append(errors_)
elif isinstance(errors_, list):
errors.extend(errors_)
else:
values[field.name] = v_
return values, errors
def get_missing_field_error(loc: Tuple[str, ...]) -> ErrorWrapper:
missing_field_error = ErrorWrapper(MissingError(), loc=loc)
return missing_field_error
def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]:
flat_dependant = get_flat_dependant(dependant)
if not flat_dependant.body_params:
@@ -767,16 +815,12 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]:
for param in flat_dependant.body_params:
setattr(param.field_info, "embed", True) # noqa: B010
model_name = "Body_" + name
BodyModel = create_body_model(
fields=flat_dependant.body_params, model_name=model_name
)
BodyModel: Type[BaseModel] = create_model(model_name)
for f in flat_dependant.body_params:
BodyModel.__fields__[f.name] = f
required = any(True for f in flat_dependant.body_params if f.required)
BodyFieldInfo_kwargs: Dict[str, Any] = {
"annotation": BodyModel,
"alias": "body",
}
if not required:
BodyFieldInfo_kwargs["default"] = None
BodyFieldInfo_kwargs: Dict[str, Any] = {"default": None}
if any(isinstance(f.field_info, params.File) for f in flat_dependant.body_params):
BodyFieldInfo: Type[params.Body] = params.File
elif any(isinstance(f.field_info, params.Form) for f in flat_dependant.body_params):

View File

@@ -1,87 +1,15 @@
import dataclasses
import datetime
from collections import defaultdict, deque
from decimal import Decimal
from collections import defaultdict
from enum import Enum
from ipaddress import (
IPv4Address,
IPv4Interface,
IPv4Network,
IPv6Address,
IPv6Interface,
IPv6Network,
)
from pathlib import Path, PurePath
from re import Pattern
from pathlib import PurePath
from types import GeneratorType
from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union
from uuid import UUID
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
from fastapi.types import IncEx
from pydantic import BaseModel
from pydantic.color import Color
from pydantic.networks import NameEmail
from pydantic.types import SecretBytes, SecretStr
from pydantic.json import ENCODERS_BY_TYPE
from ._compat import PYDANTIC_V2, MultiHostUrl, Url, _model_dump
# Taken from Pydantic v1 as is
def isoformat(o: Union[datetime.date, datetime.time]) -> str:
return o.isoformat()
# Taken from Pydantic v1 as is
# TODO: pv2 should this return strings instead?
def decimal_encoder(dec_value: Decimal) -> Union[int, float]:
"""
Encodes a Decimal as int of there's no exponent, otherwise float
This is useful when we use ConstrainedDecimal to represent Numeric(x,0)
where a integer (but not int typed) is used. Encoding this as a float
results in failed round-tripping between encode and parse.
Our Id type is a prime example of this.
>>> decimal_encoder(Decimal("1.0"))
1.0
>>> decimal_encoder(Decimal("1"))
1
"""
if dec_value.as_tuple().exponent >= 0: # type: ignore[operator]
return int(dec_value)
else:
return float(dec_value)
ENCODERS_BY_TYPE: Dict[Type[Any], Callable[[Any], Any]] = {
bytes: lambda o: o.decode(),
Color: str,
datetime.date: isoformat,
datetime.datetime: isoformat,
datetime.time: isoformat,
datetime.timedelta: lambda td: td.total_seconds(),
Decimal: decimal_encoder,
Enum: lambda o: o.value,
frozenset: list,
deque: list,
GeneratorType: list,
IPv4Address: str,
IPv4Interface: str,
IPv4Network: str,
IPv6Address: str,
IPv6Interface: str,
IPv6Network: str,
NameEmail: str,
Path: str,
Pattern: lambda o: o.pattern,
SecretBytes: str,
SecretStr: str,
set: list,
UUID: str,
Url: str,
MultiHostUrl: str,
}
SetIntStr = Set[Union[int, str]]
DictIntStrAny = Dict[Union[int, str], Any]
def generate_encoders_by_class_tuples(
@@ -100,8 +28,8 @@ encoders_by_class_tuples = generate_encoders_by_class_tuples(ENCODERS_BY_TYPE)
def jsonable_encoder(
obj: Any,
include: Optional[IncEx] = None,
exclude: Optional[IncEx] = None,
include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
by_alias: bool = True,
exclude_unset: bool = False,
exclude_defaults: bool = False,
@@ -122,15 +50,10 @@ def jsonable_encoder(
if exclude is not None and not isinstance(exclude, (set, dict)):
exclude = set(exclude)
if isinstance(obj, BaseModel):
# TODO: remove when deprecating Pydantic v1
encoders: Dict[Any, Any] = {}
if not PYDANTIC_V2:
encoders = getattr(obj.__config__, "json_encoders", {}) # type: ignore[attr-defined]
if custom_encoder:
encoders.update(custom_encoder)
obj_dict = _model_dump(
obj,
mode="json",
encoder = getattr(obj.__config__, "json_encoders", {})
if custom_encoder:
encoder.update(custom_encoder)
obj_dict = obj.dict(
include=include,
exclude=exclude,
by_alias=by_alias,
@@ -144,8 +67,7 @@ def jsonable_encoder(
obj_dict,
exclude_none=exclude_none,
exclude_defaults=exclude_defaults,
# TODO: remove when deprecating Pydantic v1
custom_encoder=encoders,
custom_encoder=encoder,
sqlalchemy_safe=sqlalchemy_safe,
)
if dataclasses.is_dataclass(obj):

View File

@@ -1,6 +1,7 @@
from typing import Any, Dict, Optional, Sequence, Type
from pydantic import BaseModel, create_model
from pydantic import BaseModel, ValidationError, create_model
from pydantic.error_wrappers import ErrorList
from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.exceptions import WebSocketException as WebSocketException # noqa: F401
@@ -25,25 +26,12 @@ class FastAPIError(RuntimeError):
"""
class ValidationException(Exception):
def __init__(self, errors: Sequence[Any]) -> None:
self._errors = errors
def errors(self) -> Sequence[Any]:
return self._errors
class RequestValidationError(ValidationException):
def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None:
super().__init__(errors)
class RequestValidationError(ValidationError):
def __init__(self, errors: Sequence[ErrorList], *, body: Any = None) -> None:
self.body = body
super().__init__(errors, RequestErrorModel)
class WebSocketRequestValidationError(ValidationException):
pass
class ResponseValidationError(ValidationException):
def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None:
super().__init__(errors)
self.body = body
class WebSocketRequestValidationError(ValidationError):
def __init__(self, errors: Sequence[ErrorList]) -> None:
super().__init__(errors, WebSocketErrorModel)

View File

@@ -1,3 +1,2 @@
METHODS_WITH_BODY = {"GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"}
REF_PREFIX = "#/components/schemas/"
REF_TEMPLATE = "#/components/schemas/{model}"

View File

@@ -1,20 +1,12 @@
from enum import Enum
from typing import Any, Callable, Dict, Iterable, List, Optional, Type, Union
from typing import Any, Callable, Dict, Iterable, List, Optional, Union
from fastapi._compat import (
PYDANTIC_V2,
CoreSchema,
GetJsonSchemaHandler,
JsonSchemaValue,
_model_rebuild,
general_plain_validator_function,
)
from fastapi.logger import logger
from pydantic import AnyUrl, BaseModel, Field
from typing_extensions import Literal
try:
import email_validator
import email_validator # type: ignore
assert email_validator # make autoflake ignore the unused import
from pydantic import EmailStr
@@ -33,52 +25,22 @@ except ImportError: # pragma: no cover
)
return str(v)
@classmethod
def _validate(cls, __input_value: Any, _: Any) -> str:
logger.warning(
"email-validator not installed, email fields will be treated as str.\n"
"To install, run: pip install email-validator"
)
return str(__input_value)
@classmethod
def __get_pydantic_json_schema__(
cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler
) -> JsonSchemaValue:
return {"type": "string", "format": "email"}
@classmethod
def __get_pydantic_core_schema__(
cls, source: Type[Any], handler: Callable[[Any], CoreSchema]
) -> CoreSchema:
return general_plain_validator_function(cls._validate)
class Contact(BaseModel):
name: Optional[str] = None
url: Optional[AnyUrl] = None
email: Optional[EmailStr] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class License(BaseModel):
name: str
url: Optional[AnyUrl] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class Info(BaseModel):
@@ -89,13 +51,8 @@ class Info(BaseModel):
license: Optional[License] = None
version: str
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class ServerVariable(BaseModel):
@@ -103,13 +60,8 @@ class ServerVariable(BaseModel):
default: str
description: Optional[str] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class Server(BaseModel):
@@ -117,13 +69,8 @@ class Server(BaseModel):
description: Optional[str] = None
variables: Optional[Dict[str, ServerVariable]] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class Reference(BaseModel):
@@ -142,26 +89,16 @@ class XML(BaseModel):
attribute: Optional[bool] = None
wrapped: Optional[bool] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class ExternalDocumentation(BaseModel):
description: Optional[str] = None
url: AnyUrl
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class Schema(BaseModel):
@@ -202,13 +139,8 @@ class Schema(BaseModel):
example: Optional[Any] = None
deprecated: Optional[bool] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra: str = "allow"
class Example(BaseModel):
@@ -217,13 +149,8 @@ class Example(BaseModel):
value: Optional[Any] = None
externalValue: Optional[AnyUrl] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class ParameterInType(Enum):
@@ -240,13 +167,8 @@ class Encoding(BaseModel):
explode: Optional[bool] = None
allowReserved: Optional[bool] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class MediaType(BaseModel):
@@ -255,13 +177,8 @@ class MediaType(BaseModel):
examples: Optional[Dict[str, Union[Example, Reference]]] = None
encoding: Optional[Dict[str, Encoding]] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class ParameterBase(BaseModel):
@@ -278,13 +195,8 @@ class ParameterBase(BaseModel):
# Serialization rules for more complex scenarios
content: Optional[Dict[str, MediaType]] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class Parameter(ParameterBase):
@@ -301,13 +213,8 @@ class RequestBody(BaseModel):
content: Dict[str, MediaType]
required: Optional[bool] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class Link(BaseModel):
@@ -318,13 +225,8 @@ class Link(BaseModel):
description: Optional[str] = None
server: Optional[Server] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class Response(BaseModel):
@@ -333,13 +235,8 @@ class Response(BaseModel):
content: Optional[Dict[str, MediaType]] = None
links: Optional[Dict[str, Union[Link, Reference]]] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class Operation(BaseModel):
@@ -357,13 +254,8 @@ class Operation(BaseModel):
security: Optional[List[Dict[str, List[str]]]] = None
servers: Optional[List[Server]] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class PathItem(BaseModel):
@@ -381,13 +273,8 @@ class PathItem(BaseModel):
servers: Optional[List[Server]] = None
parameters: Optional[List[Union[Parameter, Reference]]] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class SecuritySchemeType(Enum):
@@ -401,13 +288,8 @@ class SecurityBase(BaseModel):
type_: SecuritySchemeType = Field(alias="type")
description: Optional[str] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class APIKeyIn(Enum):
@@ -436,13 +318,8 @@ class OAuthFlow(BaseModel):
refreshUrl: Optional[str] = None
scopes: Dict[str, str] = {}
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class OAuthFlowImplicit(OAuthFlow):
@@ -468,13 +345,8 @@ class OAuthFlows(BaseModel):
clientCredentials: Optional[OAuthFlowClientCredentials] = None
authorizationCode: Optional[OAuthFlowAuthorizationCode] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class OAuth2(SecurityBase):
@@ -504,13 +376,8 @@ class Components(BaseModel):
# Using Any for Specification Extensions
callbacks: Optional[Dict[str, Union[Dict[str, PathItem], Reference, Any]]] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class Tag(BaseModel):
@@ -518,13 +385,8 @@ class Tag(BaseModel):
description: Optional[str] = None
externalDocs: Optional[ExternalDocumentation] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class OpenAPI(BaseModel):
@@ -538,15 +400,10 @@ class OpenAPI(BaseModel):
tags: Optional[List[Tag]] = None
externalDocs: Optional[ExternalDocumentation] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
_model_rebuild(Schema)
_model_rebuild(Operation)
_model_rebuild(Encoding)
Schema.update_forward_refs()
Operation.update_forward_refs()
Encoding.update_forward_refs()

View File

@@ -1,32 +1,32 @@
import http.client
import inspect
import warnings
from enum import Enum
from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, Type, Union, cast
from fastapi import routing
from fastapi._compat import (
GenerateJsonSchema,
ModelField,
Undefined,
get_compat_model_name_map,
get_definitions,
get_schema_from_model_field,
lenient_issubclass,
)
from fastapi.datastructures import DefaultPlaceholder
from fastapi.dependencies.models import Dependant
from fastapi.dependencies.utils import 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.constants import METHODS_WITH_BODY, REF_PREFIX
from fastapi.openapi.models import OpenAPI
from fastapi.params import Body, Param
from fastapi.responses import Response
from fastapi.types import ModelNameMap
from fastapi.utils import (
deep_dict_update,
generate_operation_id_for_path,
get_model_definitions,
is_body_allowed_for_status_code,
)
from pydantic import BaseModel
from pydantic.fields import ModelField, Undefined
from pydantic.schema import (
field_schema,
get_flat_models_from_fields,
get_model_name_map,
)
from pydantic.utils import lenient_issubclass
from starlette.responses import JSONResponse
from starlette.routing import BaseRoute
from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY
@@ -88,8 +88,7 @@ def get_openapi_security_definitions(
def get_openapi_operation_parameters(
*,
all_route_params: Sequence[ModelField],
schema_generator: GenerateJsonSchema,
model_name_map: ModelNameMap,
model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str],
) -> List[Dict[str, Any]]:
parameters = []
for param in all_route_params:
@@ -97,16 +96,13 @@ def get_openapi_operation_parameters(
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,
)
parameter = {
"name": param.alias,
"in": field_info.in_.value,
"required": param.required,
"schema": param_schema,
"schema": field_schema(
param, model_name_map=model_name_map, ref_prefix=REF_PREFIX
)[0],
}
if field_info.description:
parameter["description"] = field_info.description
@@ -123,16 +119,13 @@ def get_openapi_operation_parameters(
def get_openapi_operation_request_body(
*,
body_field: Optional[ModelField],
schema_generator: GenerateJsonSchema,
model_name_map: ModelNameMap,
model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str],
) -> Optional[Dict[str, Any]]:
if not body_field:
return None
assert isinstance(body_field, ModelField)
body_schema = get_schema_from_model_field(
field=body_field,
schema_generator=schema_generator,
model_name_map=model_name_map,
body_schema, _, _ = field_schema(
body_field, model_name_map=model_name_map, ref_prefix=REF_PREFIX
)
field_info = cast(Body, body_field.field_info)
request_media_type = field_info.media_type
@@ -197,11 +190,7 @@ def get_openapi_operation_metadata(
def get_openapi_path(
*,
route: routing.APIRoute,
operation_ids: Set[str],
schema_generator: GenerateJsonSchema,
model_name_map: ModelNameMap,
*, route: routing.APIRoute, model_name_map: Dict[type, str], operation_ids: Set[str]
) -> Tuple[Dict[str, Any], Dict[str, Any], Dict[str, Any]]:
path = {}
security_schemes: Dict[str, Any] = {}
@@ -229,9 +218,7 @@ def get_openapi_path(
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,
schema_generator=schema_generator,
model_name_map=model_name_map,
all_route_params=all_route_params, model_name_map=model_name_map
)
parameters.extend(operation_parameters)
if parameters:
@@ -249,9 +236,7 @@ def get_openapi_path(
operation["parameters"] = list(all_parameters.values())
if method in METHODS_WITH_BODY:
request_body_oai = get_openapi_operation_request_body(
body_field=route.body_field,
schema_generator=schema_generator,
model_name_map=model_name_map,
body_field=route.body_field, model_name_map=model_name_map
)
if request_body_oai:
operation["requestBody"] = request_body_oai
@@ -265,9 +250,8 @@ def get_openapi_path(
cb_definitions,
) = get_openapi_path(
route=callback,
operation_ids=operation_ids,
schema_generator=schema_generator,
model_name_map=model_name_map,
operation_ids=operation_ids,
)
callbacks[callback.name] = {callback.path: cb_path}
operation["callbacks"] = callbacks
@@ -293,10 +277,10 @@ def get_openapi_path(
response_schema = {"type": "string"}
if lenient_issubclass(current_response_class, JSONResponse):
if route.response_field:
response_schema = get_schema_from_model_field(
field=route.response_field,
schema_generator=schema_generator,
response_schema, _, _ = field_schema(
route.response_field,
model_name_map=model_name_map,
ref_prefix=REF_PREFIX,
)
else:
response_schema = {}
@@ -325,10 +309,8 @@ def get_openapi_path(
field = route.response_fields.get(additional_status_code)
additional_field_schema: Optional[Dict[str, Any]] = None
if field:
additional_field_schema = get_schema_from_model_field(
field=field,
schema_generator=schema_generator,
model_name_map=model_name_map,
additional_field_schema, _, _ = field_schema(
field, model_name_map=model_name_map, ref_prefix=REF_PREFIX
)
media_type = route_response_media_type or "application/json"
additional_schema = (
@@ -374,13 +356,13 @@ def get_openapi_path(
return path, security_schemes, definitions
def get_fields_from_routes(
def get_flat_models_from_routes(
routes: Sequence[BaseRoute],
) -> List[ModelField]:
) -> Set[Union[Type[BaseModel], Type[Enum]]]:
body_fields_from_routes: List[ModelField] = []
responses_from_routes: List[ModelField] = []
request_fields_from_routes: List[ModelField] = []
callback_flat_models: List[ModelField] = []
callback_flat_models: Set[Union[Type[BaseModel], Type[Enum]]] = set()
for route in routes:
if getattr(route, "include_in_schema", None) and isinstance(
route, routing.APIRoute
@@ -395,12 +377,13 @@ def get_fields_from_routes(
if route.response_fields:
responses_from_routes.extend(route.response_fields.values())
if route.callbacks:
callback_flat_models.extend(get_fields_from_routes(route.callbacks))
callback_flat_models |= get_flat_models_from_routes(route.callbacks)
params = get_flat_params(route.dependant)
request_fields_from_routes.extend(params)
flat_models = callback_flat_models + list(
body_fields_from_routes + responses_from_routes + request_fields_from_routes
flat_models = callback_flat_models | get_flat_models_from_fields(
body_fields_from_routes + responses_from_routes + request_fields_from_routes,
known_models=set(),
)
return flat_models
@@ -433,21 +416,15 @@ def get_openapi(
components: Dict[str, Dict[str, Any]] = {}
paths: Dict[str, Dict[str, Any]] = {}
operation_ids: Set[str] = set()
all_fields = get_fields_from_routes(routes)
model_name_map = get_compat_model_name_map(all_fields)
schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE)
definitions = get_definitions(
fields=all_fields,
schema_generator=schema_generator,
model_name_map=model_name_map,
flat_models = get_flat_models_from_routes(routes)
model_name_map = get_model_name_map(flat_models)
definitions = get_model_definitions(
flat_models=flat_models, model_name_map=model_name_map
)
for route in routes:
if isinstance(route, routing.APIRoute):
result = get_openapi_path(
route=route,
operation_ids=operation_ids,
schema_generator=schema_generator,
model_name_map=model_name_map,
route=route, model_name_map=model_name_map, operation_ids=operation_ids
)
if result:
path, security_schemes, path_definitions = result

View File

@@ -1,7 +1,7 @@
from typing import Any, Callable, Dict, Optional, Sequence
from fastapi import params
from fastapi._compat import Undefined
from pydantic.fields import Undefined
def Path( # noqa: N802
@@ -16,7 +16,6 @@ def Path( # noqa: N802
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
@@ -35,7 +34,6 @@ def Path( # noqa: N802
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
example=example,
examples=examples,
@@ -57,7 +55,6 @@ def Query( # noqa: N802
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
@@ -76,7 +73,6 @@ def Query( # noqa: N802
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
example=example,
examples=examples,
@@ -99,7 +95,6 @@ def Header( # noqa: N802
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
@@ -119,7 +114,6 @@ def Header( # noqa: N802
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
example=example,
examples=examples,
@@ -141,7 +135,6 @@ def Cookie( # noqa: N802
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
@@ -160,7 +153,6 @@ def Cookie( # noqa: N802
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
example=example,
examples=examples,
@@ -184,7 +176,6 @@ def Body( # noqa: N802
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
@@ -203,7 +194,6 @@ def Body( # noqa: N802
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
example=example,
examples=examples,
@@ -224,7 +214,6 @@ def Form( # noqa: N802
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
@@ -242,7 +231,6 @@ def Form( # noqa: N802
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
example=example,
examples=examples,
@@ -263,7 +251,6 @@ def File( # noqa: N802
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
@@ -281,7 +268,6 @@ def File( # noqa: N802
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
example=example,
examples=examples,

View File

@@ -1,9 +1,7 @@
from enum import Enum
from typing import Any, Callable, Dict, Optional, Sequence, Type
from typing import Any, Callable, Dict, Optional, Sequence
from pydantic.fields import FieldInfo
from ._compat import PYDANTIC_V2, Undefined
from pydantic.fields import FieldInfo, Undefined
class ParamTypes(Enum):
@@ -20,7 +18,6 @@ class Param(FieldInfo):
self,
default: Any = Undefined,
*,
annotation: Optional[Type[Any]] = None,
alias: Optional[str] = None,
title: Optional[str] = None,
description: Optional[str] = None,
@@ -30,7 +27,6 @@ class Param(FieldInfo):
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
@@ -40,8 +36,9 @@ class Param(FieldInfo):
):
self.deprecated = deprecated
self.example = example
self.examples = examples
self.include_in_schema = include_in_schema
kwargs = dict(
super().__init__(
default=default,
alias=alias,
title=title,
@@ -52,19 +49,9 @@ class Param(FieldInfo):
le=le,
min_length=min_length,
max_length=max_length,
regex=regex,
**extra,
)
if PYDANTIC_V2:
kwargs["annotation"] = annotation
kwargs["pattern"] = pattern or regex
else:
# TODO: pv2 figure out how to deprecate regex
kwargs["regex"] = pattern or regex
super().__init__(**kwargs)
# TODO: pv2 decide how to handle OpenAPI examples vs JSON Schema examples
# and how to deprecate OpenAPI examples
self.examples = examples # type: ignore[assignment]
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.default})"
@@ -77,7 +64,6 @@ class Path(Param):
self,
default: Any = ...,
*,
annotation: Optional[Type[Any]] = None,
alias: Optional[str] = None,
title: Optional[str] = None,
description: Optional[str] = None,
@@ -87,7 +73,6 @@ class Path(Param):
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
@@ -99,7 +84,6 @@ class Path(Param):
self.in_ = self.in_
super().__init__(
default=default,
annotation=annotation,
alias=alias,
title=title,
description=description,
@@ -109,7 +93,6 @@ class Path(Param):
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
deprecated=deprecated,
example=example,
@@ -126,7 +109,6 @@ class Query(Param):
self,
default: Any = Undefined,
*,
annotation: Optional[Type[Any]] = None,
alias: Optional[str] = None,
title: Optional[str] = None,
description: Optional[str] = None,
@@ -136,7 +118,6 @@ class Query(Param):
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
@@ -146,7 +127,6 @@ class Query(Param):
):
super().__init__(
default=default,
annotation=annotation,
alias=alias,
title=title,
description=description,
@@ -156,7 +136,6 @@ class Query(Param):
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
deprecated=deprecated,
example=example,
@@ -173,7 +152,6 @@ class Header(Param):
self,
default: Any = Undefined,
*,
annotation: Optional[Type[Any]] = None,
alias: Optional[str] = None,
convert_underscores: bool = True,
title: Optional[str] = None,
@@ -184,7 +162,6 @@ class Header(Param):
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
@@ -195,7 +172,6 @@ class Header(Param):
self.convert_underscores = convert_underscores
super().__init__(
default=default,
annotation=annotation,
alias=alias,
title=title,
description=description,
@@ -205,7 +181,6 @@ class Header(Param):
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
deprecated=deprecated,
example=example,
@@ -222,7 +197,6 @@ class Cookie(Param):
self,
default: Any = Undefined,
*,
annotation: Optional[Type[Any]] = None,
alias: Optional[str] = None,
title: Optional[str] = None,
description: Optional[str] = None,
@@ -232,7 +206,6 @@ class Cookie(Param):
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
@@ -242,7 +215,6 @@ class Cookie(Param):
):
super().__init__(
default=default,
annotation=annotation,
alias=alias,
title=title,
description=description,
@@ -252,7 +224,6 @@ class Cookie(Param):
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
deprecated=deprecated,
example=example,
@@ -267,7 +238,6 @@ class Body(FieldInfo):
self,
default: Any = Undefined,
*,
annotation: Optional[Type[Any]] = None,
embed: bool = False,
media_type: str = "application/json",
alias: Optional[str] = None,
@@ -279,7 +249,6 @@ class Body(FieldInfo):
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
@@ -288,7 +257,8 @@ class Body(FieldInfo):
self.embed = embed
self.media_type = media_type
self.example = example
kwargs = dict(
self.examples = examples
super().__init__(
default=default,
alias=alias,
title=title,
@@ -299,20 +269,9 @@ class Body(FieldInfo):
le=le,
min_length=min_length,
max_length=max_length,
regex=regex,
**extra,
)
if PYDANTIC_V2:
kwargs["annotation"] = annotation
kwargs["pattern"] = pattern or regex
else:
# TODO: pv2 figure out how to deprecate regex
kwargs["regex"] = pattern or regex
super().__init__(
**kwargs,
)
# TODO: pv2 decide how to handle OpenAPI examples vs JSON Schema examples
# and how to deprecate OpenAPI examples
self.examples = examples # type: ignore[assignment]
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.default})"
@@ -323,7 +282,6 @@ class Form(Body):
self,
default: Any = Undefined,
*,
annotation: Optional[Type[Any]] = None,
media_type: str = "application/x-www-form-urlencoded",
alias: Optional[str] = None,
title: Optional[str] = None,
@@ -334,7 +292,6 @@ class Form(Body):
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
@@ -342,7 +299,6 @@ class Form(Body):
):
super().__init__(
default=default,
annotation=annotation,
embed=True,
media_type=media_type,
alias=alias,
@@ -354,7 +310,6 @@ class Form(Body):
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
example=example,
examples=examples,
@@ -367,7 +322,6 @@ class File(Form):
self,
default: Any = Undefined,
*,
annotation: Optional[Type[Any]] = None,
media_type: str = "multipart/form-data",
alias: Optional[str] = None,
title: Optional[str] = None,
@@ -378,7 +332,6 @@ class File(Form):
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
@@ -386,7 +339,6 @@ class File(Form):
):
super().__init__(
default=default,
annotation=annotation,
media_type=media_type,
alias=alias,
title=title,
@@ -397,7 +349,6 @@ class File(Form):
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
example=example,
examples=examples,

View File

@@ -20,14 +20,6 @@ from typing import (
)
from fastapi import params
from fastapi._compat import (
ModelField,
Undefined,
_get_model_config,
_model_dump,
_normalize_errors,
lenient_issubclass,
)
from fastapi.datastructures import Default, DefaultPlaceholder
from fastapi.dependencies.models import Dependant
from fastapi.dependencies.utils import (
@@ -37,14 +29,13 @@ from fastapi.dependencies.utils import (
get_typed_return_annotation,
solve_dependencies,
)
from fastapi.encoders import jsonable_encoder
from fastapi.encoders import DictIntStrAny, SetIntStr, jsonable_encoder
from fastapi.exceptions import (
FastAPIError,
RequestValidationError,
ResponseValidationError,
WebSocketRequestValidationError,
)
from fastapi.types import DecoratedCallable, IncEx
from fastapi.types import DecoratedCallable
from fastapi.utils import (
create_cloned_field,
create_response_field,
@@ -53,6 +44,9 @@ from fastapi.utils import (
is_body_allowed_for_status_code,
)
from pydantic import BaseModel
from pydantic.error_wrappers import ErrorWrapper, ValidationError
from pydantic.fields import ModelField, Undefined
from pydantic.utils import lenient_issubclass
from starlette import routing
from starlette.concurrency import run_in_threadpool
from starlette.exceptions import HTTPException
@@ -79,15 +73,14 @@ def _prepare_response_content(
exclude_none: bool = False,
) -> Any:
if isinstance(res, BaseModel):
read_with_orm_mode = getattr(_get_model_config(res), "read_with_orm_mode", None)
read_with_orm_mode = getattr(res.__config__, "read_with_orm_mode", None)
if read_with_orm_mode:
# Let from_orm extract the data from this model instead of converting
# it now to a dict.
# Otherwise there's no way to extract lazy data that requires attribute
# access instead of dict iteration, e.g. lazy relationships.
return res
return _model_dump(
res,
return res.dict(
by_alias=True,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
@@ -122,8 +115,8 @@ async def serialize_response(
*,
field: Optional[ModelField] = None,
response_content: Any,
include: Optional[IncEx] = None,
exclude: Optional[IncEx] = None,
include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
by_alias: bool = True,
exclude_unset: bool = False,
exclude_defaults: bool = False,
@@ -132,40 +125,24 @@ async def serialize_response(
) -> Any:
if field:
errors = []
if not hasattr(field, "serialize"):
# pydantic v1
response_content = _prepare_response_content(
response_content,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
)
response_content = _prepare_response_content(
response_content,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
)
if is_coroutine:
value, errors_ = field.validate(response_content, {}, loc=("response",))
else:
value, errors_ = await run_in_threadpool(
field.validate, response_content, {}, loc=("response",)
)
if isinstance(errors_, list):
errors.extend(errors_)
elif errors_:
if isinstance(errors_, ErrorWrapper):
errors.append(errors_)
elif isinstance(errors_, list):
errors.extend(errors_)
if errors:
raise ResponseValidationError(
errors=_normalize_errors(errors), body=response_content
)
if hasattr(field, "serialize"):
return field.serialize(
value,
include=include,
exclude=exclude,
by_alias=by_alias,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
)
raise ValidationError(errors, field.type_)
return jsonable_encoder(
value,
include=include,
@@ -198,8 +175,8 @@ def get_request_handler(
status_code: Optional[int] = None,
response_class: Union[Type[Response], DefaultPlaceholder] = Default(JSONResponse),
response_field: Optional[ModelField] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -243,16 +220,7 @@ def get_request_handler(
body = body_bytes
except json.JSONDecodeError as e:
raise RequestValidationError(
[
{
"type": "json_invalid",
"loc": ("body", e.pos),
"msg": "JSON decode error",
"input": {},
"ctx": {"error": e.msg},
}
],
body=e.doc,
[ErrorWrapper(e, ("body", e.pos))], body=e.doc
) from e
except HTTPException:
raise
@@ -268,7 +236,7 @@ def get_request_handler(
)
values, errors, background_tasks, sub_response, _ = solved_result
if errors:
raise RequestValidationError(_normalize_errors(errors), body=body)
raise RequestValidationError(errors, body=body)
else:
raw_response = await run_endpoint_function(
dependant=dependant, values=values, is_coroutine=is_coroutine
@@ -319,7 +287,7 @@ def get_websocket_app(
)
values, errors, _, _2, _3 = solved_result
if errors:
raise WebSocketRequestValidationError(_normalize_errors(errors))
raise WebSocketRequestValidationError(errors)
assert dependant.call is not None, "dependant.call must be a function"
await dependant.call(**values)
@@ -380,8 +348,8 @@ class APIRoute(routing.Route):
name: Optional[str] = None,
methods: Optional[Union[Set[str], List[str]]] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -455,7 +423,6 @@ class APIRoute(routing.Route):
# would pass the validation and be returned as is.
# By being a new field, no inheritance will be passed as is. A new model
# will be always created.
# TODO: remove when deprecating Pydantic v1
self.secure_cloned_response_field: Optional[
ModelField
] = create_cloned_field(self.response_field)
@@ -602,8 +569,8 @@ class APIRouter(routing.Router):
deprecated: Optional[bool] = None,
methods: Optional[Union[Set[str], List[str]]] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -683,8 +650,8 @@ class APIRouter(routing.Router):
deprecated: Optional[bool] = None,
methods: Optional[List[str]] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -910,8 +877,8 @@ class APIRouter(routing.Router):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -966,8 +933,8 @@ class APIRouter(routing.Router):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -1022,8 +989,8 @@ class APIRouter(routing.Router):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -1078,8 +1045,8 @@ class APIRouter(routing.Router):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -1134,8 +1101,8 @@ class APIRouter(routing.Router):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -1190,8 +1157,8 @@ class APIRouter(routing.Router):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -1246,8 +1213,8 @@ class APIRouter(routing.Router):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -1302,8 +1269,8 @@ class APIRouter(routing.Router):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,

View File

@@ -9,9 +9,6 @@ from fastapi.security.utils import get_authorization_scheme_param
from starlette.requests import Request
from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN
# TODO: import from typing when deprecating Python 3.9
from typing_extensions import Annotated
class OAuth2PasswordRequestForm:
"""
@@ -48,13 +45,12 @@ class OAuth2PasswordRequestForm:
def __init__(
self,
*,
grant_type: Annotated[Union[str, None], Form(pattern="password")] = None,
username: Annotated[str, Form()],
password: Annotated[str, Form()],
scope: Annotated[str, Form()] = "",
client_id: Annotated[Union[str, None], Form()] = None,
client_secret: Annotated[Union[str, None], Form()] = None,
grant_type: str = Form(default=None, regex="password"),
username: str = Form(),
password: str = Form(),
scope: str = Form(default=""),
client_id: Optional[str] = Form(default=None),
client_secret: Optional[str] = Form(default=None),
):
self.grant_type = grant_type
self.username = username
@@ -99,12 +95,12 @@ class OAuth2PasswordRequestFormStrict(OAuth2PasswordRequestForm):
def __init__(
self,
grant_type: Annotated[str, Form(pattern="password")],
username: Annotated[str, Form()],
password: Annotated[str, Form()],
scope: Annotated[str, Form()] = "",
client_id: Annotated[Union[str, None], Form()] = None,
client_secret: Annotated[Union[str, None], Form()] = None,
grant_type: str = Form(regex="password"),
username: str = Form(),
password: str = Form(),
scope: str = Form(default=""),
client_id: Optional[str] = Form(default=None),
client_secret: Optional[str] = Form(default=None),
):
super().__init__(
grant_type=grant_type,

View File

@@ -1,11 +1,3 @@
import types
from enum import Enum
from typing import Any, Callable, Dict, Set, Type, TypeVar, Union
from pydantic import BaseModel
from typing import Any, Callable, TypeVar
DecoratedCallable = TypeVar("DecoratedCallable", bound=Callable[..., Any])
UnionType = getattr(types, "UnionType", Union)
NoneType = getattr(types, "UnionType", None)
ModelNameMap = Dict[Union[Type[BaseModel], Type[Enum]], str]
IncEx = Union[Set[int], Set[str], Dict[int, Any], Dict[str, Any]]

View File

@@ -1,6 +1,7 @@
import re
import warnings
from dataclasses import is_dataclass
from enum import Enum
from typing import (
TYPE_CHECKING,
Any,
@@ -15,19 +16,13 @@ from typing import (
from weakref import WeakKeyDictionary
import fastapi
from fastapi._compat import (
PYDANTIC_V2,
BaseConfig,
ModelField,
PydanticSchemaGenerationError,
Undefined,
UndefinedType,
Validator,
lenient_issubclass,
)
from fastapi.datastructures import DefaultPlaceholder, DefaultType
from pydantic import BaseModel, create_model
from pydantic.fields import FieldInfo
from fastapi.openapi.constants import REF_PREFIX
from pydantic import BaseConfig, BaseModel, create_model
from pydantic.class_validators import Validator
from pydantic.fields import FieldInfo, ModelField, UndefinedType
from pydantic.schema import model_process_schema
from pydantic.utils import lenient_issubclass
if TYPE_CHECKING: # pragma: nocover
from .routing import APIRoute
@@ -55,6 +50,24 @@ def is_body_allowed_for_status_code(status_code: Union[int, str, None]) -> bool:
return not (current_status_code < 200 or current_status_code in {204, 304})
def get_model_definitions(
*,
flat_models: Set[Union[Type[BaseModel], Type[Enum]]],
model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str],
) -> Dict[str, Any]:
definitions: Dict[str, Dict[str, Any]] = {}
for model in flat_models:
m_schema, m_definitions, m_nested_models = model_process_schema(
model, model_name_map=model_name_map, ref_prefix=REF_PREFIX
)
definitions.update(m_definitions)
model_name = model_name_map[model]
if "description" in m_schema:
m_schema["description"] = m_schema["description"].split("\f")[0]
definitions[model_name] = m_schema
return definitions
def get_path_param_names(path: str) -> Set[str]:
return set(re.findall("{(.*?)}", path))
@@ -63,8 +76,8 @@ def create_response_field(
name: str,
type_: Type[Any],
class_validators: Optional[Dict[str, Validator]] = None,
default: Optional[Any] = Undefined,
required: Union[bool, UndefinedType] = Undefined,
default: Optional[Any] = None,
required: Union[bool, UndefinedType] = True,
model_config: Type[BaseConfig] = BaseConfig,
field_info: Optional[FieldInfo] = None,
alias: Optional[str] = None,
@@ -73,27 +86,20 @@ def create_response_field(
Create a new response field. Raises if type_ is invalid.
"""
class_validators = class_validators or {}
if PYDANTIC_V2:
field_info = field_info or FieldInfo(
annotation=type_, default=default, alias=alias
)
else:
field_info = field_info or FieldInfo()
kwargs = {"name": name, "field_info": field_info}
if not PYDANTIC_V2:
kwargs.update(
{
"type_": type_,
"class_validators": class_validators,
"default": default,
"required": required,
"model_config": model_config,
"alias": alias,
}
)
field_info = field_info or FieldInfo()
try:
return ModelField(**kwargs) # type: ignore[arg-type]
except (RuntimeError, PydanticSchemaGenerationError):
return ModelField(
name=name,
type_=type_,
class_validators=class_validators,
default=default,
required=required,
model_config=model_config,
alias=alias,
field_info=field_info,
)
except RuntimeError:
raise fastapi.exceptions.FastAPIError(
"Invalid args for response field! Hint: "
f"check that {type_} is a valid Pydantic field type. "
@@ -110,8 +116,6 @@ def create_cloned_field(
*,
cloned_types: Optional[MutableMapping[Type[BaseModel], Type[BaseModel]]] = None,
) -> ModelField:
if PYDANTIC_V2:
return field
# cloned_types caches already cloned types to support recursive models and improve
# performance by avoiding unecessary cloning
if cloned_types is None:
@@ -132,30 +136,30 @@ def create_cloned_field(
f, cloned_types=cloned_types
)
new_field = create_response_field(name=field.name, type_=use_type)
new_field.has_alias = field.has_alias # type: ignore[attr-defined]
new_field.alias = field.alias # type: ignore[misc]
new_field.class_validators = field.class_validators # type: ignore[attr-defined]
new_field.default = field.default # type: ignore[misc]
new_field.required = field.required # type: ignore[misc]
new_field.model_config = field.model_config # type: ignore[attr-defined]
new_field.has_alias = field.has_alias
new_field.alias = field.alias
new_field.class_validators = field.class_validators
new_field.default = field.default
new_field.required = field.required
new_field.model_config = field.model_config
new_field.field_info = field.field_info
new_field.allow_none = field.allow_none # type: ignore[attr-defined]
new_field.validate_always = field.validate_always # type: ignore[attr-defined]
if field.sub_fields: # type: ignore[attr-defined]
new_field.sub_fields = [ # type: ignore[attr-defined]
new_field.allow_none = field.allow_none
new_field.validate_always = field.validate_always
if field.sub_fields:
new_field.sub_fields = [
create_cloned_field(sub_field, cloned_types=cloned_types)
for sub_field in field.sub_fields # type: ignore[attr-defined]
for sub_field in field.sub_fields
]
if field.key_field: # type: ignore[attr-defined]
new_field.key_field = create_cloned_field( # type: ignore[attr-defined]
field.key_field, cloned_types=cloned_types # type: ignore[attr-defined]
if field.key_field:
new_field.key_field = create_cloned_field(
field.key_field, cloned_types=cloned_types
)
new_field.validators = field.validators # type: ignore[attr-defined]
new_field.pre_validators = field.pre_validators # type: ignore[attr-defined]
new_field.post_validators = field.post_validators # type: ignore[attr-defined]
new_field.parse_json = field.parse_json # type: ignore[attr-defined]
new_field.shape = field.shape # type: ignore[attr-defined]
new_field.populate_validators() # type: ignore[attr-defined]
new_field.validators = field.validators
new_field.pre_validators = field.pre_validators
new_field.post_validators = field.post_validators
new_field.parse_json = field.parse_json
new_field.shape = field.shape
new_field.populate_validators()
return new_field
@@ -216,9 +220,3 @@ def get_value_or_default(
if not isinstance(item, DefaultPlaceholder):
return item
return first_item
def match_pydantic_error_url(error_type: str) -> Any:
from dirty_equals import IsStr
return IsStr(regex=rf"^https://errors\.pydantic\.dev/.*/v/{error_type}")

View File

@@ -1,5 +1,5 @@
[build-system]
requires = ["hatchling"]
requires = ["hatchling >= 1.13.0"]
build-backend = "hatchling.build"
[project]
@@ -42,13 +42,14 @@ classifiers = [
]
dependencies = [
"starlette>=0.27.0,<0.28.0",
"pydantic>=1.7.4,!=1.8,!=1.8.1,<3.0.0",
"pydantic>=1.7.4,!=1.8,!=1.8.1,<2.0.0",
]
dynamic = ["version"]
[project.urls]
Homepage = "https://github.com/tiangolo/fastapi"
Documentation = "https://fastapi.tiangolo.com/"
Repository = "https://github.com/tiangolo/fastapi"
[project.optional-dependencies]
all = [
@@ -59,7 +60,7 @@ all = [
"pyyaml >=5.3.1",
"ujson >=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0",
"orjson >=3.2.1",
"email_validator >=2.0.0",
"email_validator >=1.1.1",
"uvicorn[standard] >=0.12.0",
]
@@ -83,7 +84,6 @@ check_untyped_defs = true
addopts = [
"--strict-config",
"--strict-markers",
"--ignore=docs_src",
]
xfail_strict = true
junit_family = "xunit2"

View File

@@ -1,13 +1,11 @@
-e .
pytest >=7.1.3,<8.0.0
coverage[toml] >= 6.5.0,< 8.0
dirty-equals >= 0.6.0
mypy ==1.3.0
ruff ==0.0.272
mypy ==1.4.0
ruff ==0.0.275
black == 23.3.0
httpx >=0.23.0,<0.24.0
email_validator >=2.0.0,<3.0.0
email_validator >=1.1.1,<2.0.0
# TODO: once removing databases from tutorial, upgrade SQLAlchemy
# probably when including SQLModel
sqlalchemy >=1.3.18,<1.4.43

View File

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

View File

@@ -1,4 +1,3 @@
from dirty_equals import IsDict
from fastapi import APIRouter, FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, HttpUrl
@@ -43,24 +42,13 @@ def test_openapi_schema():
"parameters": [
{
"required": True,
"schema": IsDict(
{
"title": "Callback Url",
"minLength": 1,
"type": "string",
"format": "uri",
}
)
# TODO: remove when deprecating Pydantic v1
| IsDict(
{
"title": "Callback Url",
"maxLength": 2083,
"minLength": 1,
"type": "string",
"format": "uri",
}
),
"schema": {
"title": "Callback Url",
"maxLength": 2083,
"minLength": 1,
"type": "string",
"format": "uri",
},
"name": "callback_url",
"in": "query",
}

View File

@@ -1,8 +1,6 @@
import pytest
from dirty_equals import IsDict
from fastapi import APIRouter, FastAPI, Query
from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from typing_extensions import Annotated
app = FastAPI()
@@ -32,46 +30,21 @@ client = TestClient(app)
foo_is_missing = {
"detail": [
IsDict(
{
"loc": ["query", "foo"],
"msg": "Field required",
"type": "missing",
"input": None,
"url": match_pydantic_error_url("missing"),
}
)
# TODO: remove when deprecating Pydantic v1
| IsDict(
{
"loc": ["query", "foo"],
"msg": "field required",
"type": "value_error.missing",
}
)
{
"loc": ["query", "foo"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
foo_is_short = {
"detail": [
IsDict(
{
"ctx": {"min_length": 1},
"loc": ["query", "foo"],
"msg": "String should have at least 1 characters",
"type": "string_too_short",
"input": "",
"url": match_pydantic_error_url("string_too_short"),
}
)
# TODO: remove when deprecating Pydantic v1
| IsDict(
{
"ctx": {"limit_value": 1},
"loc": ["query", "foo"],
"msg": "ensure this value has at least 1 characters",
"type": "value_error.any_str.min_length",
}
)
{
"ctx": {"limit_value": 1},
"loc": ["query", "foo"],
"msg": "ensure this value has at least 1 characters",
"type": "value_error.any_str.min_length",
}
]
}

View File

@@ -1,5 +1,4 @@
import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient
from .main import app
@@ -267,17 +266,10 @@ def test_openapi_schema():
"operationId": "get_path_param_id_path_param__item_id__get",
"parameters": [
{
"required": True,
"schema": {"title": "Item Id", "type": "string"},
"name": "item_id",
"in": "path",
"required": True,
"schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Item Id",
}
)
# TODO: remove when deprecating Pydantic v1
| IsDict({"title": "Item Id", "type": "string"}),
}
],
}
@@ -977,17 +969,10 @@ def test_openapi_schema():
"operationId": "get_query_type_optional_query_int_optional_get",
"parameters": [
{
"required": False,
"schema": {"title": "Query", "type": "integer"},
"name": "query",
"in": "query",
"required": False,
"schema": IsDict(
{
"anyOf": [{"type": "integer"}, {"type": "null"}],
"title": "Query",
}
)
# TODO: remove when deprecating Pydantic v1
| IsDict({"title": "Query", "type": "integer"}),
}
],
}

View File

@@ -1,93 +0,0 @@
from typing import List, Union
from fastapi import FastAPI, UploadFile
from fastapi._compat import (
ModelField,
Undefined,
_get_model_config,
is_bytes_sequence_annotation,
is_uploadfile_sequence_annotation,
)
from fastapi.testclient import TestClient
from pydantic import BaseConfig, BaseModel, ConfigDict
from pydantic.fields import FieldInfo
from .utils import needs_pydanticv1, needs_pydanticv2
@needs_pydanticv2
def test_model_field_default_required():
# For coverage
field_info = FieldInfo(annotation=str)
field = ModelField(name="foo", field_info=field_info)
assert field.default is Undefined
@needs_pydanticv1
def test_upload_file_dummy_general_plain_validator_function():
# For coverage
assert UploadFile.__get_pydantic_core_schema__(str, lambda x: None) == {}
@needs_pydanticv1
def test_union_scalar_list():
# For coverage
# TODO: there might not be a current valid code path that uses this, it would
# potentially enable query parameters defined as both a scalar and a list
# but that would require more refactors, also not sure it's really useful
from fastapi._compat import is_pv1_scalar_field
field_info = FieldInfo()
field = ModelField(
name="foo",
field_info=field_info,
type_=Union[str, List[int]],
class_validators={},
model_config=BaseConfig,
)
assert not is_pv1_scalar_field(field)
@needs_pydanticv2
def test_get_model_config():
# For coverage in Pydantic v2
class Foo(BaseModel):
model_config = ConfigDict(from_attributes=True)
foo = Foo()
config = _get_model_config(foo)
assert config == {"from_attributes": True}
def test_complex():
app = FastAPI()
@app.post("/")
def foo(foo: Union[str, List[int]]):
return foo
client = TestClient(app)
response = client.post("/", json="bar")
assert response.status_code == 200, response.text
assert response.json() == "bar"
response2 = client.post("/", json=[1, 2])
assert response2.status_code == 200, response2.text
assert response2.json() == [1, 2]
def test_is_bytes_sequence_annotation_union():
# For coverage
# TODO: in theory this would allow declaring types that could be lists of bytes
# to be read from files and other types, but I'm not even sure it's a good idea
# to support it as a first class "feature"
assert is_bytes_sequence_annotation(Union[List[str], List[bytes]])
def test_is_uploadfile_sequence_annotation():
# For coverage
# TODO: in theory this would allow declaring types that could be lists of UploadFile
# and other types, but I'm not even sure it's a good idea to support it as a first
# class "feature"
assert is_uploadfile_sequence_annotation(Union[List[str], List[UploadFile]])

View File

@@ -1,5 +1,4 @@
from fastapi import FastAPI
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
from pydantic import BaseModel
@@ -9,18 +8,10 @@ app = FastAPI()
class Item(BaseModel):
name: str
if PYDANTIC_V2:
model_config = {
"json_schema_extra": {
"x-something-internal": {"level": 4},
}
class Config:
schema_extra = {
"x-something-internal": {"level": 4},
}
else:
class Config:
schema_extra = {
"x-something-internal": {"level": 4},
}
@app.get("/foo", response_model=Item)

View File

@@ -7,17 +7,11 @@ from fastapi.datastructures import Default
from fastapi.testclient import TestClient
# TODO: remove when deprecating Pydantic v1
def test_upload_file_invalid():
with pytest.raises(ValueError):
UploadFile.validate("not a Starlette UploadFile")
def test_upload_file_invalid_pydantic_v2():
with pytest.raises(ValueError):
UploadFile._validate("not a Starlette UploadFile", {})
def test_default_placeholder_equals():
placeholder_1 = Default("a")
placeholder_2 = Default("a")

View File

@@ -4,54 +4,31 @@ from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel
from .utils import needs_pydanticv1, needs_pydanticv2
class ModelWithDatetimeField(BaseModel):
dt_field: datetime
class Config:
json_encoders = {
datetime: lambda dt: dt.replace(
microsecond=0, tzinfo=timezone.utc
).isoformat()
}
@needs_pydanticv2
def test_pydanticv2():
from pydantic import field_serializer
app = FastAPI()
model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8))
class ModelWithDatetimeField(BaseModel):
dt_field: datetime
@field_serializer("dt_field")
def serialize_datetime(self, dt_field: datetime):
return dt_field.replace(microsecond=0, tzinfo=timezone.utc).isoformat()
@app.get("/model", response_model=ModelWithDatetimeField)
def get_model():
return model
app = FastAPI()
model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8))
@app.get("/model", response_model=ModelWithDatetimeField)
def get_model():
return model
client = TestClient(app)
client = TestClient(app)
with client:
response = client.get("/model")
assert response.json() == {"dt_field": "2019-01-01T08:00:00+00:00"}
# TODO: remove when deprecating Pydantic v1
@needs_pydanticv1
def test_pydanticv1():
class ModelWithDatetimeField(BaseModel):
dt_field: datetime
class Config:
json_encoders = {
datetime: lambda dt: dt.replace(
microsecond=0, tzinfo=timezone.utc
).isoformat()
}
app = FastAPI()
model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8))
@app.get("/model", response_model=ModelWithDatetimeField)
def get_model():
return model
client = TestClient(app)
def test_dt():
with client:
response = client.get("/model")
assert response.json() == {"dt_field": "2019-01-01T08:00:00+00:00"}

View File

@@ -1,9 +1,7 @@
from typing import List
from dirty_equals import IsDict
from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from pydantic import BaseModel
app = FastAPI()
@@ -49,30 +47,15 @@ async def no_duplicates_sub(
def test_no_duplicates_invalid():
response = client.post("/no-duplicates", json={"item": {"data": "myitem"}})
assert response.status_code == 422, response.text
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "item2"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "item2"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
assert response.json() == {
"detail": [
{
"loc": ["body", "item2"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
def test_no_duplicates():

View File

@@ -1,10 +1,8 @@
from typing import Optional
import pytest
from dirty_equals import IsDict
from fastapi import APIRouter, Depends, FastAPI
from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
app = FastAPI()
@@ -52,180 +50,99 @@ async def overrider_dependency_with_sub(msg: dict = Depends(overrider_sub_depend
return msg
def test_main_depends():
response = client.get("/main-depends/")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "q"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "q"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
def test_main_depends_q_foo():
response = client.get("/main-depends/?q=foo")
assert response.status_code == 200
assert response.json() == {
"in": "main-depends",
"params": {"q": "foo", "skip": 0, "limit": 100},
}
def test_main_depends_q_foo_skip_100_limit_200():
response = client.get("/main-depends/?q=foo&skip=100&limit=200")
assert response.status_code == 200
assert response.json() == {
"in": "main-depends",
"params": {"q": "foo", "skip": 100, "limit": 200},
}
def test_decorator_depends():
response = client.get("/decorator-depends/")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "q"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "q"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
def test_decorator_depends_q_foo():
response = client.get("/decorator-depends/?q=foo")
assert response.status_code == 200
assert response.json() == {"in": "decorator-depends"}
def test_decorator_depends_q_foo_skip_100_limit_200():
response = client.get("/decorator-depends/?q=foo&skip=100&limit=200")
assert response.status_code == 200
assert response.json() == {"in": "decorator-depends"}
def test_router_depends():
response = client.get("/router-depends/")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "q"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "q"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
def test_router_depends_q_foo():
response = client.get("/router-depends/?q=foo")
assert response.status_code == 200
assert response.json() == {
"in": "router-depends",
"params": {"q": "foo", "skip": 0, "limit": 100},
}
def test_router_depends_q_foo_skip_100_limit_200():
response = client.get("/router-depends/?q=foo&skip=100&limit=200")
assert response.status_code == 200
assert response.json() == {
"in": "router-depends",
"params": {"q": "foo", "skip": 100, "limit": 200},
}
def test_router_decorator_depends():
response = client.get("/router-decorator-depends/")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "q"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "q"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
def test_router_decorator_depends_q_foo():
response = client.get("/router-decorator-depends/?q=foo")
assert response.status_code == 200
assert response.json() == {"in": "router-decorator-depends"}
def test_router_decorator_depends_q_foo_skip_100_limit_200():
response = client.get("/router-decorator-depends/?q=foo&skip=100&limit=200")
assert response.status_code == 200
assert response.json() == {"in": "router-decorator-depends"}
@pytest.mark.parametrize(
"url,status_code,expected",
[
(
"/main-depends/",
422,
{
"detail": [
{
"loc": ["query", "q"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
),
(
"/main-depends/?q=foo",
200,
{"in": "main-depends", "params": {"q": "foo", "skip": 0, "limit": 100}},
),
(
"/main-depends/?q=foo&skip=100&limit=200",
200,
{"in": "main-depends", "params": {"q": "foo", "skip": 100, "limit": 200}},
),
(
"/decorator-depends/",
422,
{
"detail": [
{
"loc": ["query", "q"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
),
("/decorator-depends/?q=foo", 200, {"in": "decorator-depends"}),
(
"/decorator-depends/?q=foo&skip=100&limit=200",
200,
{"in": "decorator-depends"},
),
(
"/router-depends/",
422,
{
"detail": [
{
"loc": ["query", "q"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
),
(
"/router-depends/?q=foo",
200,
{"in": "router-depends", "params": {"q": "foo", "skip": 0, "limit": 100}},
),
(
"/router-depends/?q=foo&skip=100&limit=200",
200,
{"in": "router-depends", "params": {"q": "foo", "skip": 100, "limit": 200}},
),
(
"/router-decorator-depends/",
422,
{
"detail": [
{
"loc": ["query", "q"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
),
("/router-decorator-depends/?q=foo", 200, {"in": "router-decorator-depends"}),
(
"/router-decorator-depends/?q=foo&skip=100&limit=200",
200,
{"in": "router-decorator-depends"},
),
],
)
def test_normal_app(url, status_code, expected):
response = client.get(url)
assert response.status_code == status_code
assert response.json() == expected
@pytest.mark.parametrize(
@@ -273,281 +190,126 @@ def test_override_simple(url, status_code, expected):
app.dependency_overrides = {}
def test_override_with_sub_main_depends():
@pytest.mark.parametrize(
"url,status_code,expected",
[
(
"/main-depends/",
422,
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
),
(
"/main-depends/?q=foo",
422,
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
),
("/main-depends/?k=bar", 200, {"in": "main-depends", "params": {"k": "bar"}}),
(
"/decorator-depends/",
422,
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
),
(
"/decorator-depends/?q=foo",
422,
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
),
("/decorator-depends/?k=bar", 200, {"in": "decorator-depends"}),
(
"/router-depends/",
422,
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
),
(
"/router-depends/?q=foo",
422,
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
),
(
"/router-depends/?k=bar",
200,
{"in": "router-depends", "params": {"k": "bar"}},
),
(
"/router-decorator-depends/",
422,
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
),
(
"/router-decorator-depends/?q=foo",
422,
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
),
("/router-decorator-depends/?k=bar", 200, {"in": "router-decorator-depends"}),
],
)
def test_override_with_sub(url, status_code, expected):
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
response = client.get("/main-depends/")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "k"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
app.dependency_overrides = {}
def test_override_with_sub__main_depends_q_foo():
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
response = client.get("/main-depends/?q=foo")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "k"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
app.dependency_overrides = {}
def test_override_with_sub_main_depends_k_bar():
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
response = client.get("/main-depends/?k=bar")
assert response.status_code == 200
assert response.json() == {"in": "main-depends", "params": {"k": "bar"}}
app.dependency_overrides = {}
def test_override_with_sub_decorator_depends():
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
response = client.get("/decorator-depends/")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "k"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
app.dependency_overrides = {}
def test_override_with_sub_decorator_depends_q_foo():
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
response = client.get("/decorator-depends/?q=foo")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "k"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
app.dependency_overrides = {}
def test_override_with_sub_decorator_depends_k_bar():
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
response = client.get("/decorator-depends/?k=bar")
assert response.status_code == 200
assert response.json() == {"in": "decorator-depends"}
app.dependency_overrides = {}
def test_override_with_sub_router_depends():
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
response = client.get("/router-depends/")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "k"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
app.dependency_overrides = {}
def test_override_with_sub_router_depends_q_foo():
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
response = client.get("/router-depends/?q=foo")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "k"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
app.dependency_overrides = {}
def test_override_with_sub_router_depends_k_bar():
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
response = client.get("/router-depends/?k=bar")
assert response.status_code == 200
assert response.json() == {"in": "router-depends", "params": {"k": "bar"}}
app.dependency_overrides = {}
def test_override_with_sub_router_decorator_depends():
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
response = client.get("/router-decorator-depends/")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "k"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
app.dependency_overrides = {}
def test_override_with_sub_router_decorator_depends_q_foo():
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
response = client.get("/router-decorator-depends/?q=foo")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "k"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
app.dependency_overrides = {}
def test_override_with_sub_router_decorator_depends_k_bar():
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
response = client.get("/router-decorator-depends/?k=bar")
assert response.status_code == 200
assert response.json() == {"in": "router-decorator-depends"}
response = client.get(url)
assert response.status_code == status_code
assert response.json() == expected
app.dependency_overrides = {}

View File

@@ -1,6 +1,5 @@
from typing import Optional
from dirty_equals import IsDict
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from fastapi.testclient import TestClient
@@ -328,14 +327,7 @@ def test_openapi_schema():
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
"price": IsDict(
{
"title": "Price",
"anyOf": [{"type": "number"}, {"type": "null"}],
}
)
# TODO: remove when deprecating Pydantic v1
| IsDict({"title": "Price", "type": "number"}),
"price": {"title": "Price", "type": "number"},
},
},
"ValidationError": {

View File

@@ -1,20 +1,46 @@
from typing import Optional
import pytest
from fastapi.exceptions import ResponseValidationError
from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, ValidationError, validator
from ..utils import needs_pydanticv1
app = FastAPI()
@pytest.fixture(name="client")
def get_client():
from .app_pv1 import app
client = TestClient(app)
return client
class ModelB(BaseModel):
username: str
@needs_pydanticv1
def test_filter_sub_model(client: TestClient):
class ModelC(ModelB):
password: str
class ModelA(BaseModel):
name: str
description: Optional[str] = None
model_b: ModelB
@validator("name")
def lower_username(cls, name: str, values):
if not name.endswith("A"):
raise ValueError("name must end in A")
return name
async def get_model_c() -> ModelC:
return ModelC(username="test-user", password="test-password")
@app.get("/model/{name}", response_model=ModelA)
async def get_model_a(name: str, model_c=Depends(get_model_c)):
return {"name": name, "description": "model-a-desc", "model_b": model_c}
client = TestClient(app)
def test_filter_sub_model():
response = client.get("/model/modelA")
assert response.status_code == 200, response.text
assert response.json() == {
@@ -24,9 +50,8 @@ def test_filter_sub_model(client: TestClient):
}
@needs_pydanticv1
def test_validator_is_cloned(client: TestClient):
with pytest.raises(ResponseValidationError) as err:
def test_validator_is_cloned():
with pytest.raises(ValidationError) as err:
client.get("/model/modelX")
assert err.value.errors() == [
{
@@ -37,8 +62,7 @@ def test_validator_is_cloned(client: TestClient):
]
@needs_pydanticv1
def test_openapi_schema(client: TestClient):
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {

View File

@@ -1,35 +0,0 @@
from typing import Optional
from fastapi import Depends, FastAPI
from pydantic import BaseModel, validator
app = FastAPI()
class ModelB(BaseModel):
username: str
class ModelC(ModelB):
password: str
class ModelA(BaseModel):
name: str
description: Optional[str] = None
model_b: ModelB
@validator("name")
def lower_username(cls, name: str, values):
if not name.endswith("A"):
raise ValueError("name must end in A")
return name
async def get_model_c() -> ModelC:
return ModelC(username="test-user", password="test-password")
@app.get("/model/{name}", response_model=ModelA)
async def get_model_a(name: str, model_c=Depends(get_model_c)):
return {"name": name, "description": "model-a-desc", "model_b": model_c}

View File

@@ -1,182 +0,0 @@
from typing import Optional
import pytest
from dirty_equals import IsDict
from fastapi import Depends, FastAPI
from fastapi.exceptions import ResponseValidationError
from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from .utils import needs_pydanticv2
@pytest.fixture(name="client")
def get_client():
from pydantic import BaseModel, FieldValidationInfo, field_validator
app = FastAPI()
class ModelB(BaseModel):
username: str
class ModelC(ModelB):
password: str
class ModelA(BaseModel):
name: str
description: Optional[str] = None
foo: ModelB
@field_validator("name")
def lower_username(cls, name: str, info: FieldValidationInfo):
if not name.endswith("A"):
raise ValueError("name must end in A")
return name
async def get_model_c() -> ModelC:
return ModelC(username="test-user", password="test-password")
@app.get("/model/{name}", response_model=ModelA)
async def get_model_a(name: str, model_c=Depends(get_model_c)):
return {"name": name, "description": "model-a-desc", "foo": model_c}
client = TestClient(app)
return client
@needs_pydanticv2
def test_filter_sub_model(client: TestClient):
response = client.get("/model/modelA")
assert response.status_code == 200, response.text
assert response.json() == {
"name": "modelA",
"description": "model-a-desc",
"foo": {"username": "test-user"},
}
@needs_pydanticv2
def test_validator_is_cloned(client: TestClient):
with pytest.raises(ResponseValidationError) as err:
client.get("/model/modelX")
assert err.value.errors() == [
IsDict(
{
"type": "value_error",
"loc": ("response", "name"),
"msg": "Value error, name must end in A",
"input": "modelX",
"ctx": {"error": "name must end in A"},
"url": match_pydantic_error_url("value_error"),
}
)
| IsDict(
# TODO remove when deprecating Pydantic v1
{
"loc": ("response", "name"),
"msg": "name must end in A",
"type": "value_error",
}
)
]
@needs_pydanticv2
def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
"openapi": "3.0.2",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/model/{name}": {
"get": {
"summary": "Get Model A",
"operationId": "get_model_a_model__name__get",
"parameters": [
{
"required": True,
"schema": {"title": "Name", "type": "string"},
"name": "name",
"in": "path",
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/ModelA"}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
}
},
"components": {
"schemas": {
"HTTPValidationError": {
"title": "HTTPValidationError",
"type": "object",
"properties": {
"detail": {
"title": "Detail",
"type": "array",
"items": {"$ref": "#/components/schemas/ValidationError"},
}
},
},
"ModelA": {
"title": "ModelA",
"required": ["name", "foo"],
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
"description": IsDict(
{
"title": "Description",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
|
# TODO remove when deprecating Pydantic v1
IsDict({"title": "Description", "type": "string"}),
"foo": {"$ref": "#/components/schemas/ModelB"},
},
},
"ModelB": {
"title": "ModelB",
"required": ["username"],
"type": "object",
"properties": {"username": {"title": "Username", "type": "string"}},
},
"ValidationError": {
"title": "ValidationError",
"required": ["loc", "msg", "type"],
"type": "object",
"properties": {
"loc": {
"title": "Location",
"type": "array",
"items": {
"anyOf": [{"type": "string"}, {"type": "integer"}]
},
},
"msg": {"title": "Message", "type": "string"},
"type": {"title": "Error Type", "type": "string"},
},
},
}
},
}

View File

@@ -1,6 +1,5 @@
from typing import Optional
from dirty_equals import IsDict
from fastapi import APIRouter, FastAPI
from fastapi.testclient import TestClient
@@ -105,253 +104,35 @@ def test_get_users_item():
assert response.json() == {"item_id": "item01", "user_id": "abc123"}
def test_openapi_schema():
def test_schema_1():
"""Check that the user_id is a required path parameter under /users"""
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
"openapi": "3.0.2",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/users/": {
"get": {
"summary": "Get Users",
"operationId": "get_users_users__get",
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
}
},
}
},
"/users/{user_id}": {
"get": {
"summary": "Get User",
"operationId": "get_user_users__user_id__get",
"parameters": [
{
"required": True,
"schema": {"title": "User Id", "type": "string"},
"name": "user_id",
"in": "path",
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/items/": {
"get": {
"summary": "Get Items",
"operationId": "get_items_items__get",
"parameters": [
{
"required": False,
"name": "user_id",
"in": "query",
"schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "User Id",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "User Id", "type": "string"}
),
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/items/{item_id}": {
"get": {
"summary": "Get Item",
"operationId": "get_item_items__item_id__get",
"parameters": [
{
"required": True,
"schema": {"title": "Item Id", "type": "string"},
"name": "item_id",
"in": "path",
},
{
"required": False,
"name": "user_id",
"in": "query",
"schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "User Id",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "User Id", "type": "string"}
),
},
],
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/users/{user_id}/items/": {
"get": {
"summary": "Get Items",
"operationId": "get_items_users__user_id__items__get",
"parameters": [
{
"required": True,
"name": "user_id",
"in": "path",
"schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "User Id",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "User Id", "type": "string"}
),
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/users/{user_id}/items/{item_id}": {
"get": {
"summary": "Get Item",
"operationId": "get_item_users__user_id__items__item_id__get",
"parameters": [
{
"required": True,
"schema": {"title": "Item Id", "type": "string"},
"name": "item_id",
"in": "path",
},
{
"required": True,
"name": "user_id",
"in": "path",
"schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "User Id",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "User Id", "type": "string"}
),
},
],
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
},
"components": {
"schemas": {
"HTTPValidationError": {
"title": "HTTPValidationError",
"type": "object",
"properties": {
"detail": {
"title": "Detail",
"type": "array",
"items": {"$ref": "#/components/schemas/ValidationError"},
}
},
},
"ValidationError": {
"title": "ValidationError",
"required": ["loc", "msg", "type"],
"type": "object",
"properties": {
"loc": {
"title": "Location",
"type": "array",
"items": {
"anyOf": [{"type": "string"}, {"type": "integer"}]
},
},
"msg": {"title": "Message", "type": "string"},
"type": {"title": "Error Type", "type": "string"},
},
},
}
},
r = response.json()
d = {
"required": True,
"schema": {"title": "User Id", "type": "string"},
"name": "user_id",
"in": "path",
}
assert d in r["paths"]["/users/{user_id}"]["get"]["parameters"]
assert d in r["paths"]["/users/{user_id}/items/"]["get"]["parameters"]
def test_schema_2():
"""Check that the user_id is an optional query parameter under /items"""
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
r = response.json()
d = {
"required": False,
"schema": {"title": "User Id", "type": "string"},
"name": "user_id",
"in": "query",
}
assert d in r["paths"]["/items/{item_id}"]["get"]["parameters"]
assert d in r["paths"]["/items/"]["get"]["parameters"]

View File

@@ -5,7 +5,7 @@ from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel
from .utils import needs_pydanticv1, needs_pydanticv2
app = FastAPI()
class MyUuid:
@@ -26,78 +26,40 @@ class MyUuid:
raise TypeError("vars() argument must have __dict__ attribute")
@needs_pydanticv2
def test_pydanticv2():
from pydantic import field_serializer
@app.get("/fast_uuid")
def return_fast_uuid():
# I don't want to import asyncpg for this test so I made my own UUID
# Import asyncpg and uncomment the two lines below for the actual bug
app = FastAPI()
# from asyncpg.pgproto import pgproto
# asyncpg_uuid = pgproto.UUID("a10ff360-3b1e-4984-a26f-d3ab460bdb51")
@app.get("/fast_uuid")
def return_fast_uuid():
asyncpg_uuid = MyUuid("a10ff360-3b1e-4984-a26f-d3ab460bdb51")
assert isinstance(asyncpg_uuid, uuid.UUID)
assert type(asyncpg_uuid) != uuid.UUID
with pytest.raises(TypeError):
vars(asyncpg_uuid)
return {"fast_uuid": asyncpg_uuid}
asyncpg_uuid = MyUuid("a10ff360-3b1e-4984-a26f-d3ab460bdb51")
assert isinstance(asyncpg_uuid, uuid.UUID)
assert type(asyncpg_uuid) != uuid.UUID
with pytest.raises(TypeError):
vars(asyncpg_uuid)
return {"fast_uuid": asyncpg_uuid}
class SomeCustomClass(BaseModel):
model_config = {"arbitrary_types_allowed": True}
a_uuid: MyUuid
class SomeCustomClass(BaseModel):
class Config:
arbitrary_types_allowed = True
json_encoders = {uuid.UUID: str}
@field_serializer("a_uuid")
def serialize_a_uuid(self, v):
return str(v)
a_uuid: MyUuid
@app.get("/get_custom_class")
def return_some_user():
# Test that the fix also works for custom pydantic classes
return SomeCustomClass(a_uuid=MyUuid("b8799909-f914-42de-91bc-95c819218d01"))
client = TestClient(app)
with client:
response_simple = client.get("/fast_uuid")
response_pydantic = client.get("/get_custom_class")
assert response_simple.json() == {
"fast_uuid": "a10ff360-3b1e-4984-a26f-d3ab460bdb51"
}
assert response_pydantic.json() == {
"a_uuid": "b8799909-f914-42de-91bc-95c819218d01"
}
# TODO: remove when deprecating Pydantic v1
@needs_pydanticv1
def test_pydanticv1():
app = FastAPI()
@app.get("/fast_uuid")
def return_fast_uuid():
asyncpg_uuid = MyUuid("a10ff360-3b1e-4984-a26f-d3ab460bdb51")
assert isinstance(asyncpg_uuid, uuid.UUID)
assert type(asyncpg_uuid) != uuid.UUID
with pytest.raises(TypeError):
vars(asyncpg_uuid)
return {"fast_uuid": asyncpg_uuid}
class SomeCustomClass(BaseModel):
class Config:
arbitrary_types_allowed = True
json_encoders = {uuid.UUID: str}
a_uuid: MyUuid
@app.get("/get_custom_class")
def return_some_user():
# Test that the fix also works for custom pydantic classes
return SomeCustomClass(a_uuid=MyUuid("b8799909-f914-42de-91bc-95c819218d01"))
client = TestClient(app)
@app.get("/get_custom_class")
def return_some_user():
# Test that the fix also works for custom pydantic classes
return SomeCustomClass(a_uuid=MyUuid("b8799909-f914-42de-91bc-95c819218d01"))
client = TestClient(app)
def test_dt():
with client:
response_simple = client.get("/fast_uuid")
response_pydantic = client.get("/get_custom_class")

View File

@@ -1,16 +1,12 @@
from dataclasses import dataclass
from datetime import datetime, timezone
from decimal import Decimal
from enum import Enum
from pathlib import PurePath, PurePosixPath, PureWindowsPath
from typing import Optional
import pytest
from fastapi._compat import PYDANTIC_V2
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel, Field, ValidationError
from .utils import needs_pydanticv1, needs_pydanticv2
from pydantic import BaseModel, Field, ValidationError, create_model
class Person:
@@ -49,6 +45,22 @@ class Unserializable:
raise NotImplementedError()
class ModelWithCustomEncoder(BaseModel):
dt_field: datetime
class Config:
json_encoders = {
datetime: lambda dt: dt.replace(
microsecond=0, tzinfo=timezone.utc
).isoformat()
}
class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder):
class Config:
pass
class RoleEnum(Enum):
admin = "admin"
normal = "normal"
@@ -57,12 +69,8 @@ class RoleEnum(Enum):
class ModelWithConfig(BaseModel):
role: Optional[RoleEnum] = None
if PYDANTIC_V2:
model_config = {"use_enum_values": True}
else:
class Config:
use_enum_values = True
class Config:
use_enum_values = True
class ModelWithAlias(BaseModel):
@@ -75,6 +83,23 @@ class ModelWithDefault(BaseModel):
bla: str = "bla"
class ModelWithRoot(BaseModel):
__root__: str
@pytest.fixture(
name="model_with_path", params=[PurePath, PurePosixPath, PureWindowsPath]
)
def fixture_model_with_path(request):
class Config:
arbitrary_types_allowed = True
ModelWithPath = create_model(
"ModelWithPath", path=(request.param, ...), __config__=Config # type: ignore
)
return ModelWithPath(path=request.param("/foo", "bar"))
def test_encode_dict():
pet = {"name": "Firulais", "owner": {"name": "Foo"}}
assert jsonable_encoder(pet) == {"name": "Firulais", "owner": {"name": "Foo"}}
@@ -128,47 +153,14 @@ def test_encode_unsupported():
jsonable_encoder(unserializable)
@needs_pydanticv2
def test_encode_custom_json_encoders_model_pydanticv2():
from pydantic import field_serializer
class ModelWithCustomEncoder(BaseModel):
dt_field: datetime
@field_serializer("dt_field")
def serialize_dt_field(self, dt):
return dt.replace(microsecond=0, tzinfo=timezone.utc).isoformat()
class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder):
pass
def test_encode_custom_json_encoders_model():
model = ModelWithCustomEncoder(dt_field=datetime(2019, 1, 1, 8))
assert jsonable_encoder(model) == {"dt_field": "2019-01-01T08:00:00+00:00"}
subclass_model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8))
assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"}
# TODO: remove when deprecating Pydantic v1
@needs_pydanticv1
def test_encode_custom_json_encoders_model_pydanticv1():
class ModelWithCustomEncoder(BaseModel):
dt_field: datetime
class Config:
json_encoders = {
datetime: lambda dt: dt.replace(
microsecond=0, tzinfo=timezone.utc
).isoformat()
}
class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder):
class Config:
pass
model = ModelWithCustomEncoder(dt_field=datetime(2019, 1, 1, 8))
def test_encode_custom_json_encoders_model_subclass():
model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8))
assert jsonable_encoder(model) == {"dt_field": "2019-01-01T08:00:00+00:00"}
subclass_model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8))
assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"}
def test_encode_model_with_config():
@@ -204,7 +196,6 @@ def test_encode_model_with_default():
}
@needs_pydanticv1
def test_custom_encoders():
class safe_datetime(datetime):
pass
@@ -235,67 +226,14 @@ def test_custom_enum_encoders():
assert encoded_instance == custom_enum_encoder(instance)
def test_encode_model_with_pure_path():
class ModelWithPath(BaseModel):
path: PurePath
if PYDANTIC_V2:
model_config = {"arbitrary_types_allowed": True}
else:
class Config:
arbitrary_types_allowed = True
obj = ModelWithPath(path=PurePath("/foo", "bar"))
assert jsonable_encoder(obj) == {"path": "/foo/bar"}
def test_encode_model_with_path(model_with_path):
if isinstance(model_with_path.path, PureWindowsPath):
expected = "\\foo\\bar"
else:
expected = "/foo/bar"
assert jsonable_encoder(model_with_path) == {"path": expected}
def test_encode_model_with_pure_posix_path():
class ModelWithPath(BaseModel):
path: PurePosixPath
if PYDANTIC_V2:
model_config = {"arbitrary_types_allowed": True}
else:
class Config:
arbitrary_types_allowed = True
obj = ModelWithPath(path=PurePosixPath("/foo", "bar"))
assert jsonable_encoder(obj) == {"path": "/foo/bar"}
def test_encode_model_with_pure_windows_path():
class ModelWithPath(BaseModel):
path: PureWindowsPath
if PYDANTIC_V2:
model_config = {"arbitrary_types_allowed": True}
else:
class Config:
arbitrary_types_allowed = True
obj = ModelWithPath(path=PureWindowsPath("/foo", "bar"))
assert jsonable_encoder(obj) == {"path": "\\foo\\bar"}
@needs_pydanticv1
def test_encode_root():
class ModelWithRoot(BaseModel):
__root__: str
model = ModelWithRoot(__root__="Foo")
assert jsonable_encoder(model) == "Foo"
@needs_pydanticv2
def test_decimal_encoder_float():
data = {"value": Decimal(1.23)}
assert jsonable_encoder(data) == {"value": 1.23}
@needs_pydanticv2
def test_decimal_encoder_int():
data = {"value": Decimal(2)}
assert jsonable_encoder(data) == {"value": 2}

View File

@@ -1,10 +1,8 @@
from decimal import Decimal
from typing import List
from dirty_equals import IsDict, IsOneOf
from fastapi import FastAPI
from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from pydantic import BaseModel, condecimal
app = FastAPI()
@@ -23,115 +21,59 @@ def save_item_no_body(item: List[Item]):
client = TestClient(app)
single_error = {
"detail": [
{
"ctx": {"limit_value": 0.0},
"loc": ["body", 0, "age"],
"msg": "ensure this value is greater than 0",
"type": "value_error.number.not_gt",
}
]
}
multiple_errors = {
"detail": [
{
"loc": ["body", 0, "name"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", 0, "age"],
"msg": "value is not a valid decimal",
"type": "type_error.decimal",
},
{
"loc": ["body", 1, "name"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", 1, "age"],
"msg": "value is not a valid decimal",
"type": "type_error.decimal",
},
]
}
def test_put_correct_body():
response = client.post("/items/", json=[{"name": "Foo", "age": 5}])
assert response.status_code == 200, response.text
assert response.json() == {
"item": [
{
"name": "Foo",
"age": IsOneOf(
5,
# TODO: remove when deprecating Pydantic v1
"5",
),
}
]
}
assert response.json() == {"item": [{"name": "Foo", "age": 5}]}
def test_jsonable_encoder_requiring_error():
response = client.post("/items/", json=[{"name": "Foo", "age": -1.0}])
assert response.status_code == 422, response.text
assert response.json() == IsDict(
{
"detail": [
{
"type": "greater_than",
"loc": ["body", 0, "age"],
"msg": "Input should be greater than 0",
"input": -1.0,
"ctx": {"gt": 0.0},
"url": match_pydantic_error_url("greater_than"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"ctx": {"limit_value": 0.0},
"loc": ["body", 0, "age"],
"msg": "ensure this value is greater than 0",
"type": "value_error.number.not_gt",
}
]
}
)
assert response.json() == single_error
def test_put_incorrect_body_multiple():
response = client.post("/items/", json=[{"age": "five"}, {"age": "six"}])
assert response.status_code == 422, response.text
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", 0, "name"],
"msg": "Field required",
"input": {"age": "five"},
"url": match_pydantic_error_url("missing"),
},
{
"type": "decimal_parsing",
"loc": ["body", 0, "age"],
"msg": "Input should be a valid decimal",
"input": "five",
},
{
"type": "missing",
"loc": ["body", 1, "name"],
"msg": "Field required",
"input": {"age": "six"},
"url": match_pydantic_error_url("missing"),
},
{
"type": "decimal_parsing",
"loc": ["body", 1, "age"],
"msg": "Input should be a valid decimal",
"input": "six",
},
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", 0, "name"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", 0, "age"],
"msg": "value is not a valid decimal",
"type": "type_error.decimal",
},
{
"loc": ["body", 1, "name"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", 1, "age"],
"msg": "value is not a valid decimal",
"type": "type_error.decimal",
},
]
}
)
assert response.json() == multiple_errors
def test_openapi_schema():
@@ -184,23 +126,11 @@ def test_openapi_schema():
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
"age": IsDict(
{
"title": "Age",
"anyOf": [
{"exclusiveMinimum": 0.0, "type": "number"},
{"type": "string"},
],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"title": "Age",
"exclusiveMinimum": 0.0,
"type": "number",
}
),
"age": {
"title": "Age",
"exclusiveMinimum": 0.0,
"type": "number",
},
},
},
"ValidationError": {

View File

@@ -1,9 +1,7 @@
from typing import List
from dirty_equals import IsDict
from fastapi import FastAPI, Query
from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
app = FastAPI()
@@ -16,6 +14,22 @@ def read_items(q: List[int] = Query(default=None)):
client = TestClient(app)
multiple_errors = {
"detail": [
{
"loc": ["query", "q", 0],
"msg": "value is not a valid integer",
"type": "type_error.integer",
},
{
"loc": ["query", "q", 1],
"msg": "value is not a valid integer",
"type": "type_error.integer",
},
]
}
def test_multi_query():
response = client.get("/items/?q=5&q=6")
assert response.status_code == 200, response.text
@@ -25,42 +39,7 @@ def test_multi_query():
def test_multi_query_incorrect():
response = client.get("/items/?q=five&q=six")
assert response.status_code == 422, response.text
assert response.json() == IsDict(
{
"detail": [
{
"type": "int_parsing",
"loc": ["query", "q", 0],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "five",
"url": match_pydantic_error_url("int_parsing"),
},
{
"type": "int_parsing",
"loc": ["query", "q", 1],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "six",
"url": match_pydantic_error_url("int_parsing"),
},
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "q", 0],
"msg": "value is not a valid integer",
"type": "type_error.integer",
},
{
"loc": ["query", "q", 1],
"msg": "value is not a valid integer",
"type": "type_error.integer",
},
]
}
)
assert response.json() == multiple_errors
def test_openapi_schema():

View File

@@ -1,6 +1,5 @@
from typing import Optional
from dirty_equals import IsDict
from fastapi import FastAPI
from fastapi.testclient import TestClient
@@ -53,21 +52,11 @@ def test_openapi():
"parameters": [
{
"required": False,
"schema": IsDict(
{
"anyOf": [{"type": "integer"}, {"type": "null"}],
"default": 50,
"title": "Standard Query Param",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"title": "Standard Query Param",
"type": "integer",
"default": 50,
}
),
"schema": {
"title": "Standard Query Param",
"type": "integer",
"default": 50,
},
"name": "standard_query_param",
"in": "query",
},

View File

@@ -1,4 +1,3 @@
from dirty_equals import IsOneOf
from fastapi import FastAPI
from fastapi.testclient import TestClient
@@ -36,20 +35,10 @@ def test_openapi_schema():
"servers": [
{"url": "/", "description": "Default, relative server"},
{
"url": IsOneOf(
"http://staging.localhost.tiangolo.com:8000/",
# TODO: remove when deprecating Pydantic v1
"http://staging.localhost.tiangolo.com:8000",
),
"url": "http://staging.localhost.tiangolo.com:8000",
"description": "Staging but actually localhost still",
},
{
"url": IsOneOf(
"https://prod.example.com/",
# TODO: remove when deprecating Pydantic v1
"https://prod.example.com",
)
},
{"url": "https://prod.example.com"},
],
"paths": {
"/foo": {

View File

@@ -33,7 +33,7 @@ async def hidden_query(
return {"hidden_query": hidden_query}
openapi_shema = {
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
@@ -162,7 +162,7 @@ def test_openapi_schema():
client = TestClient(app)
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_shema
assert response.json() == openapi_schema
@pytest.mark.parametrize(

View File

@@ -1,6 +1,6 @@
from typing import Any, List
from dirty_equals import IsOneOf
import pytest
from fastapi.params import Body, Cookie, Depends, Header, Param, Path, Query
test_data: List[Any] = ["teststr", None, ..., 1, []]
@@ -10,137 +10,34 @@ def get_user():
return {} # pragma: no cover
def test_param_repr_str():
assert repr(Param("teststr")) == "Param(teststr)"
@pytest.fixture(scope="function", params=test_data)
def params(request):
return request.param
def test_param_repr_none():
assert repr(Param(None)) == "Param(None)"
def test_param_repr_ellipsis():
assert repr(Param(...)) == IsOneOf(
"Param(PydanticUndefined)",
# TODO: remove when deprecating Pydantic v1
"Param(Ellipsis)",
)
def test_param_repr_number():
assert repr(Param(1)) == "Param(1)"
def test_param_repr_list():
assert repr(Param([])) == "Param([])"
def test_param_repr(params):
assert repr(Param(params)) == "Param(" + str(params) + ")"
def test_path_repr():
assert repr(Path()) == IsOneOf(
"Path(PydanticUndefined)",
# TODO: remove when deprecating Pydantic v1
"Path(Ellipsis)",
)
assert repr(Path(...)) == IsOneOf(
"Path(PydanticUndefined)",
# TODO: remove when deprecating Pydantic v1
"Path(Ellipsis)",
)
assert repr(Path()) == "Path(Ellipsis)"
assert repr(Path(...)) == "Path(Ellipsis)"
def test_query_repr_str():
assert repr(Query("teststr")) == "Query(teststr)"
def test_query_repr(params):
assert repr(Query(params)) == "Query(" + str(params) + ")"
def test_query_repr_none():
assert repr(Query(None)) == "Query(None)"
def test_header_repr(params):
assert repr(Header(params)) == "Header(" + str(params) + ")"
def test_query_repr_ellipsis():
assert repr(Query(...)) == IsOneOf(
"Query(PydanticUndefined)",
# TODO: remove when deprecating Pydantic v1
"Query(Ellipsis)",
)
def test_cookie_repr(params):
assert repr(Cookie(params)) == "Cookie(" + str(params) + ")"
def test_query_repr_number():
assert repr(Query(1)) == "Query(1)"
def test_query_repr_list():
assert repr(Query([])) == "Query([])"
def test_header_repr_str():
assert repr(Header("teststr")) == "Header(teststr)"
def test_header_repr_none():
assert repr(Header(None)) == "Header(None)"
def test_header_repr_ellipsis():
assert repr(Header(...)) == IsOneOf(
"Header(PydanticUndefined)",
# TODO: remove when deprecating Pydantic v1
"Header(Ellipsis)",
)
def test_header_repr_number():
assert repr(Header(1)) == "Header(1)"
def test_header_repr_list():
assert repr(Header([])) == "Header([])"
def test_cookie_repr_str():
assert repr(Cookie("teststr")) == "Cookie(teststr)"
def test_cookie_repr_none():
assert repr(Cookie(None)) == "Cookie(None)"
def test_cookie_repr_ellipsis():
assert repr(Cookie(...)) == IsOneOf(
"Cookie(PydanticUndefined)",
# TODO: remove when deprecating Pydantic v1
"Cookie(Ellipsis)",
)
def test_cookie_repr_number():
assert repr(Cookie(1)) == "Cookie(1)"
def test_cookie_repr_list():
assert repr(Cookie([])) == "Cookie([])"
def test_body_repr_str():
assert repr(Body("teststr")) == "Body(teststr)"
def test_body_repr_none():
assert repr(Body(None)) == "Body(None)"
def test_body_repr_ellipsis():
assert repr(Body(...)) == IsOneOf(
"Body(PydanticUndefined)",
# TODO: remove when deprecating Pydantic v1
"Body(Ellipsis)",
)
def test_body_repr_number():
assert repr(Body(1)) == "Body(1)"
def test_body_repr_list():
assert repr(Body([])) == "Body([])"
def test_body_repr(params):
assert repr(Body(params)) == "Body(" + str(params) + ")"
def test_depends_repr():

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,410 +1,62 @@
from dirty_equals import IsDict
import pytest
from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from .main import app
client = TestClient(app)
def test_query():
response = client.get("/query")
assert response.status_code == 422
assert response.json() == IsDict(
response_missing = {
"detail": [
{
"detail": [
{
"type": "missing",
"loc": ["query", "query"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
"loc": ["query", "query"],
"msg": "field required",
"type": "value_error.missing",
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
]
}
response_not_valid_int = {
"detail": [
{
"detail": [
{
"loc": ["query", "query"],
"msg": "field required",
"type": "value_error.missing",
}
]
"loc": ["query", "query"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
}
)
]
}
def test_query_query_baz():
response = client.get("/query?query=baz")
assert response.status_code == 200
assert response.json() == "foo bar baz"
def test_query_not_declared_baz():
response = client.get("/query?not_declared=baz")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "query"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "query"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
def test_query_optional():
response = client.get("/query/optional")
assert response.status_code == 200
assert response.json() == "foo bar"
def test_query_optional_query_baz():
response = client.get("/query/optional?query=baz")
assert response.status_code == 200
assert response.json() == "foo bar baz"
def test_query_optional_not_declared_baz():
response = client.get("/query/optional?not_declared=baz")
assert response.status_code == 200
assert response.json() == "foo bar"
def test_query_int():
response = client.get("/query/int")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "query"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "query"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
def test_query_int_query_42():
response = client.get("/query/int?query=42")
assert response.status_code == 200
assert response.json() == "foo bar 42"
def test_query_int_query_42_5():
response = client.get("/query/int?query=42.5")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "int_parsing",
"loc": ["query", "query"],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "42.5",
"url": match_pydantic_error_url("int_parsing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "query"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
}
]
}
)
def test_query_int_query_baz():
response = client.get("/query/int?query=baz")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "int_parsing",
"loc": ["query", "query"],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "baz",
"url": match_pydantic_error_url("int_parsing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "query"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
}
]
}
)
def test_query_int_not_declared_baz():
response = client.get("/query/int?not_declared=baz")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "query"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "query"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
def test_query_int_optional():
response = client.get("/query/int/optional")
assert response.status_code == 200
assert response.json() == "foo bar"
def test_query_int_optional_query_50():
response = client.get("/query/int/optional?query=50")
assert response.status_code == 200
assert response.json() == "foo bar 50"
def test_query_int_optional_query_foo():
response = client.get("/query/int/optional?query=foo")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "int_parsing",
"loc": ["query", "query"],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "foo",
"url": match_pydantic_error_url("int_parsing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "query"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
}
]
}
)
def test_query_int_default():
response = client.get("/query/int/default")
assert response.status_code == 200
assert response.json() == "foo bar 10"
def test_query_int_default_query_50():
response = client.get("/query/int/default?query=50")
assert response.status_code == 200
assert response.json() == "foo bar 50"
def test_query_int_default_query_foo():
response = client.get("/query/int/default?query=foo")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "int_parsing",
"loc": ["query", "query"],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "foo",
"url": match_pydantic_error_url("int_parsing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "query"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
}
]
}
)
def test_query_param():
response = client.get("/query/param")
assert response.status_code == 200
assert response.json() == "foo bar"
def test_query_param_query_50():
response = client.get("/query/param?query=50")
assert response.status_code == 200
assert response.json() == "foo bar 50"
def test_query_param_required():
response = client.get("/query/param-required")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "query"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "query"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
def test_query_param_required_query_50():
response = client.get("/query/param-required?query=50")
assert response.status_code == 200
assert response.json() == "foo bar 50"
def test_query_param_required_int():
response = client.get("/query/param-required/int")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "query"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "query"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
def test_query_param_required_int_query_50():
response = client.get("/query/param-required/int?query=50")
assert response.status_code == 200
assert response.json() == "foo bar 50"
def test_query_param_required_int_query_foo():
response = client.get("/query/param-required/int?query=foo")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "int_parsing",
"loc": ["query", "query"],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "foo",
"url": match_pydantic_error_url("int_parsing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "query"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
}
]
}
)
def test_query_frozenset_query_1_query_1_query_2():
response = client.get("/query/frozenset/?query=1&query=1&query=2")
assert response.status_code == 200
assert response.json() == "1,2"
@pytest.mark.parametrize(
"path,expected_status,expected_response",
[
("/query", 422, response_missing),
("/query?query=baz", 200, "foo bar baz"),
("/query?not_declared=baz", 422, response_missing),
("/query/optional", 200, "foo bar"),
("/query/optional?query=baz", 200, "foo bar baz"),
("/query/optional?not_declared=baz", 200, "foo bar"),
("/query/int", 422, response_missing),
("/query/int?query=42", 200, "foo bar 42"),
("/query/int?query=42.5", 422, response_not_valid_int),
("/query/int?query=baz", 422, response_not_valid_int),
("/query/int?not_declared=baz", 422, response_missing),
("/query/int/optional", 200, "foo bar"),
("/query/int/optional?query=50", 200, "foo bar 50"),
("/query/int/optional?query=foo", 422, response_not_valid_int),
("/query/int/default", 200, "foo bar 10"),
("/query/int/default?query=50", 200, "foo bar 50"),
("/query/int/default?query=foo", 422, response_not_valid_int),
("/query/param", 200, "foo bar"),
("/query/param?query=50", 200, "foo bar 50"),
("/query/param-required", 422, response_missing),
("/query/param-required?query=50", 200, "foo bar 50"),
("/query/param-required/int", 422, response_missing),
("/query/param-required/int?query=50", 200, "foo bar 50"),
("/query/param-required/int?query=foo", 422, response_not_valid_int),
("/query/frozenset/?query=1&query=1&query=2", 200, "1,2"),
],
)
def test_get_path(path, expected_status, expected_response):
response = client.get(path)
assert response.status_code == expected_status
assert response.json() == expected_response

View File

@@ -2,83 +2,48 @@ from typing import Any
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, ConfigDict
from .utils import needs_pydanticv1, needs_pydanticv2
from pydantic import BaseModel
class PersonBase(BaseModel):
name: str
lastname: str
class Person(PersonBase):
@property
def full_name(self) -> str:
return f"{self.name} {self.lastname}"
class Config:
orm_mode = True
read_with_orm_mode = True
class PersonCreate(PersonBase):
pass
class PersonRead(PersonBase):
full_name: str
class Config:
orm_mode = True
app = FastAPI()
@app.post("/people/", response_model=PersonRead)
def create_person(person: PersonCreate) -> Any:
db_person = Person.from_orm(person)
return db_person
client = TestClient(app)
@needs_pydanticv2
def test_read_with_orm_mode() -> None:
class PersonBase(BaseModel):
name: str
lastname: str
class Person(PersonBase):
@property
def full_name(self) -> str:
return f"{self.name} {self.lastname}"
model_config = ConfigDict(from_attributes=True)
class PersonCreate(PersonBase):
pass
class PersonRead(PersonBase):
full_name: str
model_config = {"from_attributes": True}
app = FastAPI()
@app.post("/people/", response_model=PersonRead)
def create_person(person: PersonCreate) -> Any:
db_person = Person.model_validate(person)
return db_person
client = TestClient(app)
person_data = {"name": "Dive", "lastname": "Wilson"}
response = client.post("/people/", json=person_data)
data = response.json()
assert response.status_code == 200, response.text
assert data["name"] == person_data["name"]
assert data["lastname"] == person_data["lastname"]
assert data["full_name"] == person_data["name"] + " " + person_data["lastname"]
@needs_pydanticv1
def test_read_with_orm_mode_pv1() -> None:
class PersonBase(BaseModel):
name: str
lastname: str
class Person(PersonBase):
@property
def full_name(self) -> str:
return f"{self.name} {self.lastname}"
class Config:
orm_mode = True
read_with_orm_mode = True
class PersonCreate(PersonBase):
pass
class PersonRead(PersonBase):
full_name: str
class Config:
orm_mode = True
app = FastAPI()
@app.post("/people/", response_model=PersonRead)
def create_person(person: PersonCreate) -> Any:
db_person = Person.from_orm(person)
return db_person
client = TestClient(app)
person_data = {"name": "Dive", "lastname": "Wilson"}
response = client.post("/people/", json=person_data)
data = response.json()

View File

@@ -39,6 +39,7 @@ client = TestClient(app)
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
# insert_assert(response.json())
assert response.json() == {
"openapi": "3.0.2",
"info": {"title": "FastAPI", "version": "0.1.0"},

View File

@@ -1,9 +1,8 @@
from typing import List
from fastapi import FastAPI
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
from pydantic import BaseModel, ConfigDict, Field
from pydantic import BaseModel, Field
app = FastAPI()
@@ -15,24 +14,13 @@ class Model(BaseModel):
class ModelNoAlias(BaseModel):
name: str
if PYDANTIC_V2:
model_config = ConfigDict(
json_schema_extra={
"description": (
"response_model_by_alias=False is basically a quick hack, to support "
"proper OpenAPI use another model with the correct field names"
)
}
)
else:
class Config:
schema_extra = {
"description": (
"response_model_by_alias=False is basically a quick hack, to support "
"proper OpenAPI use another model with the correct field names"
)
}
class Config:
schema_extra = {
"description": (
"response_model_by_alias=False is basically a quick hack, to support "
"proper OpenAPI use another model with the correct field names"
)
}
@app.get("/dict", response_model=Model, response_model_by_alias=False)

View File

@@ -2,10 +2,10 @@ from typing import List, Union
import pytest
from fastapi import FastAPI
from fastapi.exceptions import FastAPIError, ResponseValidationError
from fastapi.exceptions import FastAPIError
from fastapi.responses import JSONResponse, Response
from fastapi.testclient import TestClient
from pydantic import BaseModel
from pydantic import BaseModel, ValidationError
class BaseUser(BaseModel):
@@ -277,12 +277,12 @@ def test_response_model_no_annotation_return_exact_dict():
def test_response_model_no_annotation_return_invalid_dict():
with pytest.raises(ResponseValidationError):
with pytest.raises(ValidationError):
client.get("/response_model-no_annotation-return_invalid_dict")
def test_response_model_no_annotation_return_invalid_model():
with pytest.raises(ResponseValidationError):
with pytest.raises(ValidationError):
client.get("/response_model-no_annotation-return_invalid_model")
@@ -313,12 +313,12 @@ def test_no_response_model_annotation_return_exact_dict():
def test_no_response_model_annotation_return_invalid_dict():
with pytest.raises(ResponseValidationError):
with pytest.raises(ValidationError):
client.get("/no_response_model-annotation-return_invalid_dict")
def test_no_response_model_annotation_return_invalid_model():
with pytest.raises(ResponseValidationError):
with pytest.raises(ValidationError):
client.get("/no_response_model-annotation-return_invalid_model")
@@ -395,12 +395,12 @@ def test_response_model_model1_annotation_model2_return_exact_dict():
def test_response_model_model1_annotation_model2_return_invalid_dict():
with pytest.raises(ResponseValidationError):
with pytest.raises(ValidationError):
client.get("/response_model_model1-annotation_model2-return_invalid_dict")
def test_response_model_model1_annotation_model2_return_invalid_model():
with pytest.raises(ResponseValidationError):
with pytest.raises(ValidationError):
client.get("/response_model_model1-annotation_model2-return_invalid_model")

Some files were not shown because too many files have changed in this diff Show More