mirror of
https://github.com/fastapi/fastapi.git
synced 2026-01-01 10:37:47 -05:00
Compare commits
6 Commits
0.98.0
...
0.100.0-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6dc975da9d | ||
|
|
e7b3d47af3 | ||
|
|
cfb00b2119 | ||
|
|
c58e2b2d1e | ||
|
|
5301cfff52 | ||
|
|
bd32fecaf6 |
2
.github/workflows/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
- name: Build distribution
|
||||
run: python -m build
|
||||
- name: Publish
|
||||
uses: pypa/gh-action-pypi-publish@v1.8.6
|
||||
uses: pypa/gh-action-pypi-publish@v1.8.5
|
||||
with:
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
- name: Dump GitHub context
|
||||
|
||||
13
.github/workflows/test.yml
vendored
13
.github/workflows/test.yml
vendored
@@ -25,10 +25,12 @@ jobs:
|
||||
id: cache
|
||||
with:
|
||||
path: ${{ env.pythonLocation }}
|
||||
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v03
|
||||
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-pydantic-v2-${{ 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
|
||||
|
||||
@@ -37,6 +39,7 @@ 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
|
||||
@@ -51,10 +54,16 @@ jobs:
|
||||
id: cache
|
||||
with:
|
||||
path: ${{ env.pythonLocation }}
|
||||
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v03
|
||||
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ matrix.pydantic-version }}-${{ 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
|
||||
|
||||
@@ -446,6 +446,7 @@ 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:
|
||||
|
||||
@@ -441,6 +441,7 @@ 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:
|
||||
|
||||
@@ -440,6 +440,7 @@ 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:
|
||||
|
||||
@@ -233,10 +233,6 @@ 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/
|
||||
|
||||
@@ -31,6 +31,3 @@ 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
|
||||
|
||||
@@ -16,4 +16,3 @@ logins:
|
||||
- armand-sauzay
|
||||
- databento-bot
|
||||
- nanram22
|
||||
- Flint-company
|
||||
|
||||
@@ -44,7 +44,7 @@ So the new file structure looks like:
|
||||
|
||||
First, we create a new database session with the new database.
|
||||
|
||||
We'll use an in-memory database that persists during the tests instead of the local file `sql_app.db`.
|
||||
For the tests we'll use a file `test.db` instead of `sql_app.db`.
|
||||
|
||||
But the rest of the session code is more or less the same, we just copy it.
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 10 KiB |
@@ -445,6 +445,7 @@ 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:
|
||||
|
||||
@@ -2,49 +2,6 @@
|
||||
|
||||
## 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
|
||||
|
||||
@@ -64,7 +64,7 @@ from myapp import app
|
||||
# Some more code
|
||||
```
|
||||
|
||||
in that case, the automatically created variable inside of `myapp.py` will not have the variable `__name__` with a value of `"__main__"`.
|
||||
in that case, the automatic variable inside of `myapp.py` will not have the variable `__name__` with a value of `"__main__"`.
|
||||
|
||||
So, the line:
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ To achieve that, first import:
|
||||
|
||||
=== "Python 3.6+"
|
||||
|
||||
In versions of Python below Python 3.9 you import `Annotated` from `typing_extensions`.
|
||||
In versions of Python below Python 3.9 you import `Annotation` from `typing_extensions`.
|
||||
|
||||
It will already be installed with FastAPI.
|
||||
|
||||
|
||||
@@ -86,9 +86,6 @@ 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!}
|
||||
```
|
||||
@@ -141,9 +138,6 @@ 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!}
|
||||
```
|
||||
|
||||
@@ -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 direct specifications on how to encrypt the communication.
|
||||
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.
|
||||
|
||||
It is not very popular or used nowadays.
|
||||
|
||||
|
||||
@@ -11,14 +11,14 @@ theme:
|
||||
accent: amber
|
||||
toggle:
|
||||
icon: material/lightbulb
|
||||
name: Switch to dark mode
|
||||
name: Switch to light mode
|
||||
- media: '(prefers-color-scheme: dark)'
|
||||
scheme: slate
|
||||
primary: teal
|
||||
accent: amber
|
||||
toggle:
|
||||
icon: material/lightbulb-outline
|
||||
name: Switch to light mode
|
||||
name: Switch to dark mode
|
||||
features:
|
||||
- search.suggest
|
||||
- search.highlight
|
||||
|
||||
@@ -433,6 +433,7 @@ 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:
|
||||
|
||||
@@ -181,7 +181,7 @@ $ uvicorn main:my_awesome_api --reload
|
||||
|
||||
</div>
|
||||
|
||||
### Paso 3: crea una *operación de path*
|
||||
### Paso 3: crea un *operación de path*
|
||||
|
||||
#### Path
|
||||
|
||||
|
||||
@@ -436,6 +436,7 @@ 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:
|
||||
|
||||
@@ -445,6 +445,7 @@ 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 :
|
||||
|
||||
@@ -440,6 +440,7 @@ 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:
|
||||
|
||||
@@ -441,6 +441,7 @@ 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:
|
||||
|
||||
@@ -438,6 +438,7 @@ 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:
|
||||
|
||||
@@ -431,6 +431,7 @@ 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 によって使用されるもの:
|
||||
|
||||
@@ -437,6 +437,7 @@ 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이 사용하는:
|
||||
|
||||
@@ -444,6 +444,7 @@ 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:
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
# 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.
|
||||
|
||||

|
||||
|
||||
* Alternatywna dokumentacja API z <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank"><strong>ReDoc</strong></a>.
|
||||
|
||||

|
||||
|
||||
### 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>:
|
||||
|
||||

|
||||
|
||||
* <a href="https://www.jetbrains.com/pycharm/" class="external-link" target="_blank">PyCharm</a>:
|
||||
|
||||

|
||||
|
||||
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.
|
||||
@@ -435,6 +435,7 @@ 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:
|
||||
|
||||
@@ -63,7 +63,6 @@ nav:
|
||||
- tr: /tr/
|
||||
- uk: /uk/
|
||||
- zh: /zh/
|
||||
- features.md
|
||||
- Samouczek:
|
||||
- tutorial/index.md
|
||||
- tutorial/first-steps.md
|
||||
|
||||
@@ -430,6 +430,7 @@ 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:
|
||||
|
||||
@@ -439,6 +439,7 @@ 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:
|
||||
|
||||
@@ -1,382 +0,0 @@
|
||||
# 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, сохраняя при этом простоту, краткость и элегантность вашего кода.
|
||||
|
||||
И дополнительно вы получаете:
|
||||
|
||||
* Поддержку редактора (автодополнение доступно везде!)
|
||||
* Преобразование данных (также известно как парсинг / сериализация)
|
||||
* Валидацию данных
|
||||
* Документацию схемы данных
|
||||
* Автоматическую генерацию документации
|
||||
@@ -1,84 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,252 +0,0 @@
|
||||
# Дополнительные модели
|
||||
|
||||
В продолжение прошлого примера будет уже обычным делом иметь несколько связанных между собой моделей.
|
||||
|
||||
Это особенно применимо в случае моделей пользователя, потому что:
|
||||
|
||||
* **Модель для ввода** должна иметь возможность содержать пароль.
|
||||
* **Модель для вывода** не должна содержать пароль.
|
||||
* **Модель для базы данных**, возможно, должна содержать хэшированный пароль.
|
||||
|
||||
!!! 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` и без пароля.
|
||||
@@ -1,111 +0,0 @@
|
||||
# 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!}
|
||||
```
|
||||
@@ -1,179 +0,0 @@
|
||||
# Конфигурация операций пути
|
||||
|
||||
Существует несколько параметров, которые вы можете передать вашему *декоратору операций пути* для его настройки.
|
||||
|
||||
!!! 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">
|
||||
|
||||
## Резюме
|
||||
|
||||
Вы можете легко конфигурировать и добавлять метаданные в ваши *операции пути*, передавая параметры *декораторам операций пути*.
|
||||
@@ -77,17 +77,12 @@ 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
|
||||
|
||||
@@ -441,6 +441,7 @@ 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:
|
||||
|
||||
@@ -444,6 +444,7 @@ 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:
|
||||
|
||||
@@ -449,6 +449,7 @@ 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:
|
||||
|
||||
@@ -441,6 +441,7 @@ 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:
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
# 高级安全 - 介绍
|
||||
|
||||
## 附加特性
|
||||
|
||||
除 [教程 - 用户指南: 安全性](../../tutorial/security/){.internal-link target=_blank} 中涵盖的功能之外,还有一些额外的功能来处理安全性.
|
||||
|
||||
!!! tip "小贴士"
|
||||
接下来的章节 **并不一定是 "高级的"**.
|
||||
|
||||
而且对于你的使用场景来说,解决方案很可能就在其中。
|
||||
|
||||
## 先阅读教程
|
||||
|
||||
接下来的部分假设你已经阅读了主要的 [教程 - 用户指南: 安全性](../../tutorial/security/){.internal-link target=_blank}.
|
||||
|
||||
它们都基于相同的概念,但支持一些额外的功能.
|
||||
@@ -1,433 +0,0 @@
|
||||
# 设置和环境变量
|
||||
|
||||
在许多情况下,您的应用程序可能需要一些外部设置或配置,例如密钥、数据库凭据、电子邮件服务的凭据等等。
|
||||
|
||||
这些设置中的大多数是可变的(可以更改的),比如数据库的 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 文件,同时允许您在测试时进行覆盖。
|
||||
@@ -1,214 +0,0 @@
|
||||
# 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)。
|
||||
@@ -437,6 +437,7 @@ 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:
|
||||
|
||||
@@ -1,212 +0,0 @@
|
||||
# 测试
|
||||
|
||||
感谢 <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>
|
||||
@@ -109,7 +109,6 @@ nav:
|
||||
- tutorial/bigger-applications.md
|
||||
- tutorial/metadata.md
|
||||
- tutorial/static-files.md
|
||||
- tutorial/testing.md
|
||||
- tutorial/debugging.md
|
||||
- 高级用户指南:
|
||||
- advanced/index.md
|
||||
@@ -119,12 +118,8 @@ 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
|
||||
|
||||
@@ -12,11 +12,11 @@ class BaseItem(BaseModel):
|
||||
|
||||
|
||||
class CarItem(BaseItem):
|
||||
type = "car"
|
||||
type: str = "car"
|
||||
|
||||
|
||||
class PlaneItem(BaseItem):
|
||||
type = "plane"
|
||||
type: str = "plane"
|
||||
size: int
|
||||
|
||||
|
||||
|
||||
@@ -12,11 +12,11 @@ class BaseItem(BaseModel):
|
||||
|
||||
|
||||
class CarItem(BaseItem):
|
||||
type = "car"
|
||||
type: str = "car"
|
||||
|
||||
|
||||
class PlaneItem(BaseItem):
|
||||
type = "plane"
|
||||
type: str = "plane"
|
||||
size: int
|
||||
|
||||
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
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://"
|
||||
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
|
||||
|
||||
engine = create_engine(
|
||||
SQLALCHEMY_DATABASE_URL,
|
||||
connect_args={"check_same_thread": False},
|
||||
poolclass=StaticPool,
|
||||
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
|
||||
)
|
||||
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.98.0"
|
||||
__version__ = "0.100.0-beta1"
|
||||
|
||||
from starlette import status as status
|
||||
|
||||
|
||||
597
fastapi/_compat.py
Normal file
597
fastapi/_compat.py
Normal file
@@ -0,0 +1,597 @@
|
||||
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._fields import Undefined as Undefined
|
||||
from pydantic._internal._fields import _UndefinedType
|
||||
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 ErrorDetails
|
||||
from pydantic_core import MultiHostUrl as MultiHostUrl
|
||||
from pydantic_core import Url as Url
|
||||
from pydantic_core.core_schema import (
|
||||
general_plain_validator_function as general_plain_validator_function,
|
||||
)
|
||||
|
||||
Required = Undefined
|
||||
UndefinedType = _UndefinedType
|
||||
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
|
||||
)
|
||||
|
||||
ErrorDetails = Dict[str, Any] # type: ignore[assignment,misc]
|
||||
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)
|
||||
)
|
||||
@@ -15,7 +15,6 @@ 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,
|
||||
@@ -31,7 +30,7 @@ from fastapi.openapi.docs import (
|
||||
)
|
||||
from fastapi.openapi.utils import get_openapi
|
||||
from fastapi.params import Depends
|
||||
from fastapi.types import DecoratedCallable
|
||||
from fastapi.types import DecoratedCallable, IncEx
|
||||
from fastapi.utils import generate_unique_id
|
||||
from starlette.applications import Starlette
|
||||
from starlette.datastructures import State
|
||||
@@ -62,7 +61,6 @@ 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",
|
||||
@@ -128,7 +126,6 @@ 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,
|
||||
@@ -299,8 +296,8 @@ class FastAPI(Starlette):
|
||||
deprecated: Optional[bool] = None,
|
||||
methods: Optional[List[str]] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -357,8 +354,8 @@ class FastAPI(Starlette):
|
||||
deprecated: Optional[bool] = None,
|
||||
methods: Optional[List[str]] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -478,8 +475,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[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -533,8 +530,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[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -588,8 +585,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[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -643,8 +640,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[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -698,8 +695,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[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -753,8 +750,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[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -808,8 +805,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[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -863,8 +860,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[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
from typing import Any, Callable, Dict, Iterable, Type, TypeVar
|
||||
from typing import Any, Callable, Dict, Iterable, Type, TypeVar, cast
|
||||
|
||||
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
|
||||
@@ -21,8 +28,28 @@ class UploadFile(StarletteUploadFile):
|
||||
return v
|
||||
|
||||
@classmethod
|
||||
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
|
||||
field_schema.update({"type": "string", "format": "binary"})
|
||||
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)
|
||||
|
||||
|
||||
class DefaultPlaceholder:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import dataclasses
|
||||
import inspect
|
||||
from contextlib import contextmanager
|
||||
from copy import copy, deepcopy
|
||||
from copy import deepcopy
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
@@ -20,6 +19,31 @@ 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,
|
||||
@@ -31,50 +55,14 @@ 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 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 pydantic.fields import FieldInfo
|
||||
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
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
from typing_extensions import Annotated, get_args, get_origin
|
||||
|
||||
multipart_not_installed_error = (
|
||||
'Form data requires "python-multipart" to be installed. \n'
|
||||
@@ -216,36 +204,6 @@ 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__", {})
|
||||
@@ -364,12 +322,11 @@ 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 # type: ignore[comparison-overlap]
|
||||
and get_origin(annotation) is Annotated
|
||||
):
|
||||
annotated_args = get_args(annotation)
|
||||
type_annotation = annotated_args[0]
|
||||
@@ -384,7 +341,9 @@ 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(fastapi_annotation)
|
||||
field_info = copy_field_info(
|
||||
field_info=fastapi_annotation, annotation=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."
|
||||
@@ -415,6 +374,8 @@ 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
|
||||
@@ -433,10 +394,15 @@ 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()
|
||||
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)
|
||||
else:
|
||||
field_info = params.Query(default=default_value)
|
||||
used_default_field_info = True
|
||||
field_info = params.Query(annotation=type_annotation, default=default_value)
|
||||
|
||||
field = None
|
||||
if field_info is not None:
|
||||
@@ -450,8 +416,8 @@ def analyze_param(
|
||||
and getattr(field_info, "in_", None) is None
|
||||
):
|
||||
field_info.in_ = params.ParamTypes.query
|
||||
annotation = get_annotation_from_field_info(
|
||||
annotation if annotation is not inspect.Signature.empty else Any,
|
||||
use_annotation = get_annotation_from_field_info(
|
||||
type_annotation,
|
||||
field_info,
|
||||
param_name,
|
||||
)
|
||||
@@ -459,19 +425,15 @@ 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_=annotation,
|
||||
type_=use_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
|
||||
|
||||
@@ -554,13 +516,13 @@ async def solve_dependencies(
|
||||
dependency_cache: Optional[Dict[Tuple[Callable[..., Any], Tuple[str]], Any]] = None,
|
||||
) -> Tuple[
|
||||
Dict[str, Any],
|
||||
List[ErrorWrapper],
|
||||
List[Any],
|
||||
Optional[BackgroundTasks],
|
||||
Response,
|
||||
Dict[Tuple[Callable[..., Any], Tuple[str]], Any],
|
||||
]:
|
||||
values: Dict[str, Any] = {}
|
||||
errors: List[ErrorWrapper] = []
|
||||
errors: List[Any] = []
|
||||
if response is None:
|
||||
response = Response()
|
||||
del response.headers["content-length"]
|
||||
@@ -674,7 +636,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[ErrorWrapper]]:
|
||||
) -> Tuple[Dict[str, Any], List[Any]]:
|
||||
values = {}
|
||||
errors = []
|
||||
for field in required_params:
|
||||
@@ -688,23 +650,19 @@ 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(
|
||||
ErrorWrapper(
|
||||
MissingError(), loc=(field_info.in_.value, field.alias)
|
||||
)
|
||||
)
|
||||
errors.append(get_missing_field_error(loc=loc))
|
||||
else:
|
||||
values[field.name] = deepcopy(field.default)
|
||||
continue
|
||||
v_, errors_ = field.validate(
|
||||
value, values, loc=(field_info.in_.value, field.alias)
|
||||
)
|
||||
v_, errors_ = field.validate(value, values, loc=loc)
|
||||
if isinstance(errors_, ErrorWrapper):
|
||||
errors.append(errors_)
|
||||
elif isinstance(errors_, list):
|
||||
errors.extend(errors_)
|
||||
new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=())
|
||||
errors.extend(new_errors)
|
||||
else:
|
||||
values[field.name] = v_
|
||||
return values, errors
|
||||
@@ -713,9 +671,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[ErrorWrapper]]:
|
||||
) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]:
|
||||
values = {}
|
||||
errors = []
|
||||
errors: List[Dict[str, Any]] = []
|
||||
if required_params:
|
||||
field = required_params[0]
|
||||
field_info = field.field_info
|
||||
@@ -733,9 +691,7 @@ async def request_body_to_args(
|
||||
|
||||
value: Optional[Any] = None
|
||||
if received_body is not None:
|
||||
if (
|
||||
field.shape in sequence_shapes or field.type_ in sequence_types
|
||||
) and isinstance(received_body, FormData):
|
||||
if (is_sequence_field(field)) and isinstance(received_body, FormData):
|
||||
value = received_body.getlist(field.alias)
|
||||
else:
|
||||
try:
|
||||
@@ -748,7 +704,7 @@ async def request_body_to_args(
|
||||
or (isinstance(field_info, params.Form) and value == "")
|
||||
or (
|
||||
isinstance(field_info, params.Form)
|
||||
and field.shape in sequence_shapes
|
||||
and is_sequence_field(field)
|
||||
and len(value) == 0
|
||||
)
|
||||
):
|
||||
@@ -759,16 +715,17 @@ async def request_body_to_args(
|
||||
continue
|
||||
if (
|
||||
isinstance(field_info, params.File)
|
||||
and lenient_issubclass(field.type_, bytes)
|
||||
and is_bytes_field(field)
|
||||
and isinstance(value, UploadFile)
|
||||
):
|
||||
value = await value.read()
|
||||
elif (
|
||||
field.shape in sequence_shapes
|
||||
is_bytes_sequence_field(field)
|
||||
and isinstance(field_info, params.File)
|
||||
and lenient_issubclass(field.type_, bytes)
|
||||
and isinstance(value, sequence_types)
|
||||
and value_is_sequence(value)
|
||||
):
|
||||
# For types
|
||||
assert isinstance(value, sequence_types) # type: ignore[arg-type]
|
||||
results: List[Union[bytes, str]] = []
|
||||
|
||||
async def process_fn(
|
||||
@@ -780,24 +737,19 @@ 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 = sequence_shape_to_type[field.shape](results)
|
||||
value = serialize_sequence_value(field=field, value=results)
|
||||
|
||||
v_, errors_ = field.validate(value, values, loc=loc)
|
||||
|
||||
if isinstance(errors_, ErrorWrapper):
|
||||
errors.append(errors_)
|
||||
elif isinstance(errors_, list):
|
||||
if isinstance(errors_, list):
|
||||
errors.extend(errors_)
|
||||
elif errors_:
|
||||
errors.append(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:
|
||||
@@ -815,12 +767,16 @@ 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: Type[BaseModel] = create_model(model_name)
|
||||
for f in flat_dependant.body_params:
|
||||
BodyModel.__fields__[f.name] = f
|
||||
BodyModel = create_body_model(
|
||||
fields=flat_dependant.body_params, model_name=model_name
|
||||
)
|
||||
required = any(True for f in flat_dependant.body_params if f.required)
|
||||
|
||||
BodyFieldInfo_kwargs: Dict[str, Any] = {"default": None}
|
||||
BodyFieldInfo_kwargs: Dict[str, Any] = {
|
||||
"annotation": BodyModel,
|
||||
"alias": "body",
|
||||
}
|
||||
if not required:
|
||||
BodyFieldInfo_kwargs["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):
|
||||
|
||||
@@ -1,15 +1,87 @@
|
||||
import dataclasses
|
||||
from collections import defaultdict
|
||||
import datetime
|
||||
from collections import defaultdict, deque
|
||||
from decimal import Decimal
|
||||
from enum import Enum
|
||||
from pathlib import PurePath
|
||||
from ipaddress import (
|
||||
IPv4Address,
|
||||
IPv4Interface,
|
||||
IPv4Network,
|
||||
IPv6Address,
|
||||
IPv6Interface,
|
||||
IPv6Network,
|
||||
)
|
||||
from pathlib import Path, PurePath
|
||||
from re import Pattern
|
||||
from types import GeneratorType
|
||||
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
|
||||
from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi.types import IncEx
|
||||
from pydantic import BaseModel
|
||||
from pydantic.json import ENCODERS_BY_TYPE
|
||||
from pydantic.color import Color
|
||||
from pydantic.networks import NameEmail
|
||||
from pydantic.types import SecretBytes, SecretStr
|
||||
|
||||
SetIntStr = Set[Union[int, str]]
|
||||
DictIntStrAny = Dict[Union[int, str], Any]
|
||||
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,
|
||||
}
|
||||
|
||||
|
||||
def generate_encoders_by_class_tuples(
|
||||
@@ -28,8 +100,8 @@ encoders_by_class_tuples = generate_encoders_by_class_tuples(ENCODERS_BY_TYPE)
|
||||
|
||||
def jsonable_encoder(
|
||||
obj: Any,
|
||||
include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
include: Optional[IncEx] = None,
|
||||
exclude: Optional[IncEx] = None,
|
||||
by_alias: bool = True,
|
||||
exclude_unset: bool = False,
|
||||
exclude_defaults: bool = False,
|
||||
@@ -50,10 +122,15 @@ def jsonable_encoder(
|
||||
if exclude is not None and not isinstance(exclude, (set, dict)):
|
||||
exclude = set(exclude)
|
||||
if isinstance(obj, BaseModel):
|
||||
encoder = getattr(obj.__config__, "json_encoders", {})
|
||||
if custom_encoder:
|
||||
encoder.update(custom_encoder)
|
||||
obj_dict = obj.dict(
|
||||
# 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",
|
||||
include=include,
|
||||
exclude=exclude,
|
||||
by_alias=by_alias,
|
||||
@@ -67,7 +144,8 @@ def jsonable_encoder(
|
||||
obj_dict,
|
||||
exclude_none=exclude_none,
|
||||
exclude_defaults=exclude_defaults,
|
||||
custom_encoder=encoder,
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
custom_encoder=encoders,
|
||||
sqlalchemy_safe=sqlalchemy_safe,
|
||||
)
|
||||
if dataclasses.is_dataclass(obj):
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from typing import Any, Dict, Optional, Sequence, Type
|
||||
|
||||
from pydantic import BaseModel, ValidationError, create_model
|
||||
from pydantic.error_wrappers import ErrorList
|
||||
from pydantic import BaseModel, create_model
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
from starlette.exceptions import WebSocketException as WebSocketException # noqa: F401
|
||||
|
||||
@@ -26,12 +25,25 @@ class FastAPIError(RuntimeError):
|
||||
"""
|
||||
|
||||
|
||||
class RequestValidationError(ValidationError):
|
||||
def __init__(self, errors: Sequence[ErrorList], *, body: Any = None) -> None:
|
||||
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)
|
||||
self.body = body
|
||||
super().__init__(errors, RequestErrorModel)
|
||||
|
||||
|
||||
class WebSocketRequestValidationError(ValidationError):
|
||||
def __init__(self, errors: Sequence[ErrorList]) -> None:
|
||||
super().__init__(errors, WebSocketErrorModel)
|
||||
class WebSocketRequestValidationError(ValidationException):
|
||||
pass
|
||||
|
||||
|
||||
class ResponseValidationError(ValidationException):
|
||||
def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None:
|
||||
super().__init__(errors)
|
||||
self.body = body
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
METHODS_WITH_BODY = {"GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"}
|
||||
REF_PREFIX = "#/components/schemas/"
|
||||
REF_TEMPLATE = "#/components/schemas/{model}"
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
from enum import Enum
|
||||
from typing import Any, Callable, Dict, Iterable, List, Optional, Union
|
||||
from typing import Any, Callable, Dict, Iterable, List, Optional, Type, 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 # type: ignore
|
||||
import email_validator
|
||||
|
||||
assert email_validator # make autoflake ignore the unused import
|
||||
from pydantic import EmailStr
|
||||
@@ -25,22 +33,52 @@ 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
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class License(BaseModel):
|
||||
name: str
|
||||
url: Optional[AnyUrl] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class Info(BaseModel):
|
||||
@@ -51,8 +89,13 @@ class Info(BaseModel):
|
||||
license: Optional[License] = None
|
||||
version: str
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class ServerVariable(BaseModel):
|
||||
@@ -60,8 +103,13 @@ class ServerVariable(BaseModel):
|
||||
default: str
|
||||
description: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class Server(BaseModel):
|
||||
@@ -69,8 +117,13 @@ class Server(BaseModel):
|
||||
description: Optional[str] = None
|
||||
variables: Optional[Dict[str, ServerVariable]] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class Reference(BaseModel):
|
||||
@@ -89,16 +142,26 @@ class XML(BaseModel):
|
||||
attribute: Optional[bool] = None
|
||||
wrapped: Optional[bool] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class ExternalDocumentation(BaseModel):
|
||||
description: Optional[str] = None
|
||||
url: AnyUrl
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class Schema(BaseModel):
|
||||
@@ -139,8 +202,13 @@ class Schema(BaseModel):
|
||||
example: Optional[Any] = None
|
||||
deprecated: Optional[bool] = None
|
||||
|
||||
class Config:
|
||||
extra: str = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class Example(BaseModel):
|
||||
@@ -149,8 +217,13 @@ class Example(BaseModel):
|
||||
value: Optional[Any] = None
|
||||
externalValue: Optional[AnyUrl] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class ParameterInType(Enum):
|
||||
@@ -167,8 +240,13 @@ class Encoding(BaseModel):
|
||||
explode: Optional[bool] = None
|
||||
allowReserved: Optional[bool] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class MediaType(BaseModel):
|
||||
@@ -177,8 +255,13 @@ class MediaType(BaseModel):
|
||||
examples: Optional[Dict[str, Union[Example, Reference]]] = None
|
||||
encoding: Optional[Dict[str, Encoding]] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class ParameterBase(BaseModel):
|
||||
@@ -195,8 +278,13 @@ class ParameterBase(BaseModel):
|
||||
# Serialization rules for more complex scenarios
|
||||
content: Optional[Dict[str, MediaType]] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class Parameter(ParameterBase):
|
||||
@@ -213,8 +301,13 @@ class RequestBody(BaseModel):
|
||||
content: Dict[str, MediaType]
|
||||
required: Optional[bool] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class Link(BaseModel):
|
||||
@@ -225,8 +318,13 @@ class Link(BaseModel):
|
||||
description: Optional[str] = None
|
||||
server: Optional[Server] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class Response(BaseModel):
|
||||
@@ -235,8 +333,13 @@ class Response(BaseModel):
|
||||
content: Optional[Dict[str, MediaType]] = None
|
||||
links: Optional[Dict[str, Union[Link, Reference]]] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class Operation(BaseModel):
|
||||
@@ -254,8 +357,13 @@ class Operation(BaseModel):
|
||||
security: Optional[List[Dict[str, List[str]]]] = None
|
||||
servers: Optional[List[Server]] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class PathItem(BaseModel):
|
||||
@@ -273,8 +381,13 @@ class PathItem(BaseModel):
|
||||
servers: Optional[List[Server]] = None
|
||||
parameters: Optional[List[Union[Parameter, Reference]]] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class SecuritySchemeType(Enum):
|
||||
@@ -288,8 +401,13 @@ class SecurityBase(BaseModel):
|
||||
type_: SecuritySchemeType = Field(alias="type")
|
||||
description: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class APIKeyIn(Enum):
|
||||
@@ -318,8 +436,13 @@ class OAuthFlow(BaseModel):
|
||||
refreshUrl: Optional[str] = None
|
||||
scopes: Dict[str, str] = {}
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class OAuthFlowImplicit(OAuthFlow):
|
||||
@@ -345,8 +468,13 @@ class OAuthFlows(BaseModel):
|
||||
clientCredentials: Optional[OAuthFlowClientCredentials] = None
|
||||
authorizationCode: Optional[OAuthFlowAuthorizationCode] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class OAuth2(SecurityBase):
|
||||
@@ -376,8 +504,13 @@ class Components(BaseModel):
|
||||
# Using Any for Specification Extensions
|
||||
callbacks: Optional[Dict[str, Union[Dict[str, PathItem], Reference, Any]]] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class Tag(BaseModel):
|
||||
@@ -385,8 +518,13 @@ class Tag(BaseModel):
|
||||
description: Optional[str] = None
|
||||
externalDocs: Optional[ExternalDocumentation] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class OpenAPI(BaseModel):
|
||||
@@ -400,10 +538,15 @@ class OpenAPI(BaseModel):
|
||||
tags: Optional[List[Tag]] = None
|
||||
externalDocs: Optional[ExternalDocumentation] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
Schema.update_forward_refs()
|
||||
Operation.update_forward_refs()
|
||||
Encoding.update_forward_refs()
|
||||
_model_rebuild(Schema)
|
||||
_model_rebuild(Operation)
|
||||
_model_rebuild(Encoding)
|
||||
|
||||
@@ -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
|
||||
from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX, REF_TEMPLATE
|
||||
from fastapi.openapi.models import OpenAPI
|
||||
from fastapi.params import Body, Param
|
||||
from fastapi.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,7 +88,8 @@ def get_openapi_security_definitions(
|
||||
def get_openapi_operation_parameters(
|
||||
*,
|
||||
all_route_params: Sequence[ModelField],
|
||||
model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str],
|
||||
schema_generator: GenerateJsonSchema,
|
||||
model_name_map: ModelNameMap,
|
||||
) -> List[Dict[str, Any]]:
|
||||
parameters = []
|
||||
for param in all_route_params:
|
||||
@@ -96,13 +97,16 @@ 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": field_schema(
|
||||
param, model_name_map=model_name_map, ref_prefix=REF_PREFIX
|
||||
)[0],
|
||||
"schema": param_schema,
|
||||
}
|
||||
if field_info.description:
|
||||
parameter["description"] = field_info.description
|
||||
@@ -119,13 +123,16 @@ def get_openapi_operation_parameters(
|
||||
def get_openapi_operation_request_body(
|
||||
*,
|
||||
body_field: Optional[ModelField],
|
||||
model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str],
|
||||
schema_generator: GenerateJsonSchema,
|
||||
model_name_map: ModelNameMap,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
if not body_field:
|
||||
return None
|
||||
assert isinstance(body_field, ModelField)
|
||||
body_schema, _, _ = field_schema(
|
||||
body_field, model_name_map=model_name_map, ref_prefix=REF_PREFIX
|
||||
body_schema = get_schema_from_model_field(
|
||||
field=body_field,
|
||||
schema_generator=schema_generator,
|
||||
model_name_map=model_name_map,
|
||||
)
|
||||
field_info = cast(Body, body_field.field_info)
|
||||
request_media_type = field_info.media_type
|
||||
@@ -190,7 +197,11 @@ def get_openapi_operation_metadata(
|
||||
|
||||
|
||||
def get_openapi_path(
|
||||
*, route: routing.APIRoute, model_name_map: Dict[type, str], operation_ids: Set[str]
|
||||
*,
|
||||
route: routing.APIRoute,
|
||||
operation_ids: Set[str],
|
||||
schema_generator: GenerateJsonSchema,
|
||||
model_name_map: ModelNameMap,
|
||||
) -> Tuple[Dict[str, Any], Dict[str, Any], Dict[str, Any]]:
|
||||
path = {}
|
||||
security_schemes: Dict[str, Any] = {}
|
||||
@@ -218,7 +229,9 @@ 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, model_name_map=model_name_map
|
||||
all_route_params=all_route_params,
|
||||
schema_generator=schema_generator,
|
||||
model_name_map=model_name_map,
|
||||
)
|
||||
parameters.extend(operation_parameters)
|
||||
if parameters:
|
||||
@@ -236,7 +249,9 @@ 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, model_name_map=model_name_map
|
||||
body_field=route.body_field,
|
||||
schema_generator=schema_generator,
|
||||
model_name_map=model_name_map,
|
||||
)
|
||||
if request_body_oai:
|
||||
operation["requestBody"] = request_body_oai
|
||||
@@ -250,8 +265,9 @@ def get_openapi_path(
|
||||
cb_definitions,
|
||||
) = get_openapi_path(
|
||||
route=callback,
|
||||
model_name_map=model_name_map,
|
||||
operation_ids=operation_ids,
|
||||
schema_generator=schema_generator,
|
||||
model_name_map=model_name_map,
|
||||
)
|
||||
callbacks[callback.name] = {callback.path: cb_path}
|
||||
operation["callbacks"] = callbacks
|
||||
@@ -277,10 +293,10 @@ def get_openapi_path(
|
||||
response_schema = {"type": "string"}
|
||||
if lenient_issubclass(current_response_class, JSONResponse):
|
||||
if route.response_field:
|
||||
response_schema, _, _ = field_schema(
|
||||
route.response_field,
|
||||
response_schema = get_schema_from_model_field(
|
||||
field=route.response_field,
|
||||
schema_generator=schema_generator,
|
||||
model_name_map=model_name_map,
|
||||
ref_prefix=REF_PREFIX,
|
||||
)
|
||||
else:
|
||||
response_schema = {}
|
||||
@@ -309,8 +325,10 @@ 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, _, _ = field_schema(
|
||||
field, model_name_map=model_name_map, ref_prefix=REF_PREFIX
|
||||
additional_field_schema = get_schema_from_model_field(
|
||||
field=field,
|
||||
schema_generator=schema_generator,
|
||||
model_name_map=model_name_map,
|
||||
)
|
||||
media_type = route_response_media_type or "application/json"
|
||||
additional_schema = (
|
||||
@@ -356,13 +374,13 @@ def get_openapi_path(
|
||||
return path, security_schemes, definitions
|
||||
|
||||
|
||||
def get_flat_models_from_routes(
|
||||
def get_fields_from_routes(
|
||||
routes: Sequence[BaseRoute],
|
||||
) -> Set[Union[Type[BaseModel], Type[Enum]]]:
|
||||
) -> List[ModelField]:
|
||||
body_fields_from_routes: List[ModelField] = []
|
||||
responses_from_routes: List[ModelField] = []
|
||||
request_fields_from_routes: List[ModelField] = []
|
||||
callback_flat_models: Set[Union[Type[BaseModel], Type[Enum]]] = set()
|
||||
callback_flat_models: List[ModelField] = []
|
||||
for route in routes:
|
||||
if getattr(route, "include_in_schema", None) and isinstance(
|
||||
route, routing.APIRoute
|
||||
@@ -377,13 +395,12 @@ def get_flat_models_from_routes(
|
||||
if route.response_fields:
|
||||
responses_from_routes.extend(route.response_fields.values())
|
||||
if route.callbacks:
|
||||
callback_flat_models |= get_flat_models_from_routes(route.callbacks)
|
||||
callback_flat_models.extend(get_fields_from_routes(route.callbacks))
|
||||
params = get_flat_params(route.dependant)
|
||||
request_fields_from_routes.extend(params)
|
||||
|
||||
flat_models = callback_flat_models | get_flat_models_from_fields(
|
||||
body_fields_from_routes + responses_from_routes + request_fields_from_routes,
|
||||
known_models=set(),
|
||||
flat_models = callback_flat_models + list(
|
||||
body_fields_from_routes + responses_from_routes + request_fields_from_routes
|
||||
)
|
||||
return flat_models
|
||||
|
||||
@@ -416,15 +433,21 @@ def get_openapi(
|
||||
components: Dict[str, Dict[str, Any]] = {}
|
||||
paths: Dict[str, Dict[str, Any]] = {}
|
||||
operation_ids: Set[str] = set()
|
||||
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
|
||||
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,
|
||||
)
|
||||
for route in routes:
|
||||
if isinstance(route, routing.APIRoute):
|
||||
result = get_openapi_path(
|
||||
route=route, model_name_map=model_name_map, operation_ids=operation_ids
|
||||
route=route,
|
||||
operation_ids=operation_ids,
|
||||
schema_generator=schema_generator,
|
||||
model_name_map=model_name_map,
|
||||
)
|
||||
if result:
|
||||
path, security_schemes, path_definitions = result
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from typing import Any, Callable, Dict, Optional, Sequence
|
||||
|
||||
from fastapi import params
|
||||
from pydantic.fields import Undefined
|
||||
from fastapi._compat import Undefined
|
||||
|
||||
|
||||
def Path( # noqa: N802
|
||||
@@ -16,6 +16,7 @@ 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,
|
||||
@@ -34,6 +35,7 @@ def Path( # noqa: N802
|
||||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
example=example,
|
||||
examples=examples,
|
||||
@@ -55,6 +57,7 @@ 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,
|
||||
@@ -73,6 +76,7 @@ def Query( # noqa: N802
|
||||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
example=example,
|
||||
examples=examples,
|
||||
@@ -95,6 +99,7 @@ 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,
|
||||
@@ -114,6 +119,7 @@ def Header( # noqa: N802
|
||||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
example=example,
|
||||
examples=examples,
|
||||
@@ -135,6 +141,7 @@ 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,
|
||||
@@ -153,6 +160,7 @@ def Cookie( # noqa: N802
|
||||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
example=example,
|
||||
examples=examples,
|
||||
@@ -176,6 +184,7 @@ 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,
|
||||
@@ -194,6 +203,7 @@ def Body( # noqa: N802
|
||||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
example=example,
|
||||
examples=examples,
|
||||
@@ -214,6 +224,7 @@ 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,
|
||||
@@ -231,6 +242,7 @@ def Form( # noqa: N802
|
||||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
example=example,
|
||||
examples=examples,
|
||||
@@ -251,6 +263,7 @@ 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,
|
||||
@@ -268,6 +281,7 @@ def File( # noqa: N802
|
||||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
example=example,
|
||||
examples=examples,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from enum import Enum
|
||||
from typing import Any, Callable, Dict, Optional, Sequence
|
||||
from typing import Any, Callable, Dict, Optional, Sequence, Type
|
||||
|
||||
from pydantic.fields import FieldInfo, Undefined
|
||||
from pydantic.fields import FieldInfo
|
||||
|
||||
from ._compat import PYDANTIC_V2, Undefined
|
||||
|
||||
|
||||
class ParamTypes(Enum):
|
||||
@@ -18,6 +20,7 @@ class Param(FieldInfo):
|
||||
self,
|
||||
default: Any = Undefined,
|
||||
*,
|
||||
annotation: Optional[Type[Any]] = None,
|
||||
alias: Optional[str] = None,
|
||||
title: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
@@ -27,6 +30,7 @@ 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,
|
||||
@@ -36,9 +40,8 @@ class Param(FieldInfo):
|
||||
):
|
||||
self.deprecated = deprecated
|
||||
self.example = example
|
||||
self.examples = examples
|
||||
self.include_in_schema = include_in_schema
|
||||
super().__init__(
|
||||
kwargs = dict(
|
||||
default=default,
|
||||
alias=alias,
|
||||
title=title,
|
||||
@@ -49,9 +52,19 @@ 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})"
|
||||
@@ -64,6 +77,7 @@ class Path(Param):
|
||||
self,
|
||||
default: Any = ...,
|
||||
*,
|
||||
annotation: Optional[Type[Any]] = None,
|
||||
alias: Optional[str] = None,
|
||||
title: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
@@ -73,6 +87,7 @@ 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,
|
||||
@@ -84,6 +99,7 @@ class Path(Param):
|
||||
self.in_ = self.in_
|
||||
super().__init__(
|
||||
default=default,
|
||||
annotation=annotation,
|
||||
alias=alias,
|
||||
title=title,
|
||||
description=description,
|
||||
@@ -93,6 +109,7 @@ class Path(Param):
|
||||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
deprecated=deprecated,
|
||||
example=example,
|
||||
@@ -109,6 +126,7 @@ class Query(Param):
|
||||
self,
|
||||
default: Any = Undefined,
|
||||
*,
|
||||
annotation: Optional[Type[Any]] = None,
|
||||
alias: Optional[str] = None,
|
||||
title: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
@@ -118,6 +136,7 @@ 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,
|
||||
@@ -127,6 +146,7 @@ class Query(Param):
|
||||
):
|
||||
super().__init__(
|
||||
default=default,
|
||||
annotation=annotation,
|
||||
alias=alias,
|
||||
title=title,
|
||||
description=description,
|
||||
@@ -136,6 +156,7 @@ class Query(Param):
|
||||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
deprecated=deprecated,
|
||||
example=example,
|
||||
@@ -152,6 +173,7 @@ class Header(Param):
|
||||
self,
|
||||
default: Any = Undefined,
|
||||
*,
|
||||
annotation: Optional[Type[Any]] = None,
|
||||
alias: Optional[str] = None,
|
||||
convert_underscores: bool = True,
|
||||
title: Optional[str] = None,
|
||||
@@ -162,6 +184,7 @@ 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,
|
||||
@@ -172,6 +195,7 @@ class Header(Param):
|
||||
self.convert_underscores = convert_underscores
|
||||
super().__init__(
|
||||
default=default,
|
||||
annotation=annotation,
|
||||
alias=alias,
|
||||
title=title,
|
||||
description=description,
|
||||
@@ -181,6 +205,7 @@ class Header(Param):
|
||||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
deprecated=deprecated,
|
||||
example=example,
|
||||
@@ -197,6 +222,7 @@ class Cookie(Param):
|
||||
self,
|
||||
default: Any = Undefined,
|
||||
*,
|
||||
annotation: Optional[Type[Any]] = None,
|
||||
alias: Optional[str] = None,
|
||||
title: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
@@ -206,6 +232,7 @@ 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,
|
||||
@@ -215,6 +242,7 @@ class Cookie(Param):
|
||||
):
|
||||
super().__init__(
|
||||
default=default,
|
||||
annotation=annotation,
|
||||
alias=alias,
|
||||
title=title,
|
||||
description=description,
|
||||
@@ -224,6 +252,7 @@ class Cookie(Param):
|
||||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
deprecated=deprecated,
|
||||
example=example,
|
||||
@@ -238,6 +267,7 @@ class Body(FieldInfo):
|
||||
self,
|
||||
default: Any = Undefined,
|
||||
*,
|
||||
annotation: Optional[Type[Any]] = None,
|
||||
embed: bool = False,
|
||||
media_type: str = "application/json",
|
||||
alias: Optional[str] = None,
|
||||
@@ -249,6 +279,7 @@ 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,
|
||||
@@ -257,8 +288,7 @@ class Body(FieldInfo):
|
||||
self.embed = embed
|
||||
self.media_type = media_type
|
||||
self.example = example
|
||||
self.examples = examples
|
||||
super().__init__(
|
||||
kwargs = dict(
|
||||
default=default,
|
||||
alias=alias,
|
||||
title=title,
|
||||
@@ -269,9 +299,20 @@ 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})"
|
||||
@@ -282,6 +323,7 @@ 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,
|
||||
@@ -292,6 +334,7 @@ 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,
|
||||
@@ -299,6 +342,7 @@ class Form(Body):
|
||||
):
|
||||
super().__init__(
|
||||
default=default,
|
||||
annotation=annotation,
|
||||
embed=True,
|
||||
media_type=media_type,
|
||||
alias=alias,
|
||||
@@ -310,6 +354,7 @@ class Form(Body):
|
||||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
example=example,
|
||||
examples=examples,
|
||||
@@ -322,6 +367,7 @@ 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,
|
||||
@@ -332,6 +378,7 @@ 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,
|
||||
@@ -339,6 +386,7 @@ class File(Form):
|
||||
):
|
||||
super().__init__(
|
||||
default=default,
|
||||
annotation=annotation,
|
||||
media_type=media_type,
|
||||
alias=alias,
|
||||
title=title,
|
||||
@@ -349,6 +397,7 @@ class File(Form):
|
||||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
example=example,
|
||||
examples=examples,
|
||||
|
||||
@@ -20,6 +20,14 @@ 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 (
|
||||
@@ -29,13 +37,14 @@ from fastapi.dependencies.utils import (
|
||||
get_typed_return_annotation,
|
||||
solve_dependencies,
|
||||
)
|
||||
from fastapi.encoders import DictIntStrAny, SetIntStr, jsonable_encoder
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.exceptions import (
|
||||
FastAPIError,
|
||||
RequestValidationError,
|
||||
ResponseValidationError,
|
||||
WebSocketRequestValidationError,
|
||||
)
|
||||
from fastapi.types import DecoratedCallable
|
||||
from fastapi.types import DecoratedCallable, IncEx
|
||||
from fastapi.utils import (
|
||||
create_cloned_field,
|
||||
create_response_field,
|
||||
@@ -44,9 +53,6 @@ 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
|
||||
@@ -73,14 +79,15 @@ def _prepare_response_content(
|
||||
exclude_none: bool = False,
|
||||
) -> Any:
|
||||
if isinstance(res, BaseModel):
|
||||
read_with_orm_mode = getattr(res.__config__, "read_with_orm_mode", None)
|
||||
read_with_orm_mode = getattr(_get_model_config(res), "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 res.dict(
|
||||
return _model_dump(
|
||||
res,
|
||||
by_alias=True,
|
||||
exclude_unset=exclude_unset,
|
||||
exclude_defaults=exclude_defaults,
|
||||
@@ -115,8 +122,8 @@ async def serialize_response(
|
||||
*,
|
||||
field: Optional[ModelField] = None,
|
||||
response_content: Any,
|
||||
include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
include: Optional[IncEx] = None,
|
||||
exclude: Optional[IncEx] = None,
|
||||
by_alias: bool = True,
|
||||
exclude_unset: bool = False,
|
||||
exclude_defaults: bool = False,
|
||||
@@ -125,24 +132,40 @@ async def serialize_response(
|
||||
) -> Any:
|
||||
if field:
|
||||
errors = []
|
||||
response_content = _prepare_response_content(
|
||||
response_content,
|
||||
exclude_unset=exclude_unset,
|
||||
exclude_defaults=exclude_defaults,
|
||||
exclude_none=exclude_none,
|
||||
)
|
||||
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,
|
||||
)
|
||||
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_, ErrorWrapper):
|
||||
errors.append(errors_)
|
||||
elif isinstance(errors_, list):
|
||||
if isinstance(errors_, list):
|
||||
errors.extend(errors_)
|
||||
elif errors_:
|
||||
errors.append(errors_)
|
||||
if errors:
|
||||
raise ValidationError(errors, field.type_)
|
||||
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,
|
||||
)
|
||||
|
||||
return jsonable_encoder(
|
||||
value,
|
||||
include=include,
|
||||
@@ -175,8 +198,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[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -220,7 +243,16 @@ def get_request_handler(
|
||||
body = body_bytes
|
||||
except json.JSONDecodeError as e:
|
||||
raise RequestValidationError(
|
||||
[ErrorWrapper(e, ("body", e.pos))], body=e.doc
|
||||
[
|
||||
{
|
||||
"type": "json_invalid",
|
||||
"loc": ("body", e.pos),
|
||||
"msg": "JSON decode error",
|
||||
"input": {},
|
||||
"ctx": {"error": e.msg},
|
||||
}
|
||||
],
|
||||
body=e.doc,
|
||||
) from e
|
||||
except HTTPException:
|
||||
raise
|
||||
@@ -236,7 +268,7 @@ def get_request_handler(
|
||||
)
|
||||
values, errors, background_tasks, sub_response, _ = solved_result
|
||||
if errors:
|
||||
raise RequestValidationError(errors, body=body)
|
||||
raise RequestValidationError(_normalize_errors(errors), body=body)
|
||||
else:
|
||||
raw_response = await run_endpoint_function(
|
||||
dependant=dependant, values=values, is_coroutine=is_coroutine
|
||||
@@ -287,7 +319,7 @@ def get_websocket_app(
|
||||
)
|
||||
values, errors, _, _2, _3 = solved_result
|
||||
if errors:
|
||||
raise WebSocketRequestValidationError(errors)
|
||||
raise WebSocketRequestValidationError(_normalize_errors(errors))
|
||||
assert dependant.call is not None, "dependant.call must be a function"
|
||||
await dependant.call(**values)
|
||||
|
||||
@@ -348,8 +380,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[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -423,6 +455,7 @@ 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)
|
||||
@@ -569,8 +602,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[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -650,8 +683,8 @@ class APIRouter(routing.Router):
|
||||
deprecated: Optional[bool] = None,
|
||||
methods: Optional[List[str]] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -877,8 +910,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[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -933,8 +966,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[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -989,8 +1022,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[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -1045,8 +1078,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[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -1101,8 +1134,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[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -1157,8 +1190,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[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -1213,8 +1246,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[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -1269,8 +1302,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[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
|
||||
@@ -9,6 +9,9 @@ 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:
|
||||
"""
|
||||
@@ -45,12 +48,13 @@ class OAuth2PasswordRequestForm:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
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),
|
||||
*,
|
||||
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,
|
||||
):
|
||||
self.grant_type = grant_type
|
||||
self.username = username
|
||||
@@ -95,12 +99,12 @@ class OAuth2PasswordRequestFormStrict(OAuth2PasswordRequestForm):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
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),
|
||||
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,
|
||||
):
|
||||
super().__init__(
|
||||
grant_type=grant_type,
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
from typing import Any, Callable, TypeVar
|
||||
import types
|
||||
from enum import Enum
|
||||
from typing import Any, Callable, Dict, Set, Type, TypeVar, Union
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
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]]
|
||||
|
||||
122
fastapi/utils.py
122
fastapi/utils.py
@@ -1,7 +1,6 @@
|
||||
import re
|
||||
import warnings
|
||||
from dataclasses import is_dataclass
|
||||
from enum import Enum
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
@@ -16,13 +15,19 @@ 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 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
|
||||
from pydantic import BaseModel, create_model
|
||||
from pydantic.fields import FieldInfo
|
||||
|
||||
if TYPE_CHECKING: # pragma: nocover
|
||||
from .routing import APIRoute
|
||||
@@ -50,24 +55,6 @@ 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))
|
||||
|
||||
@@ -76,8 +63,8 @@ def create_response_field(
|
||||
name: str,
|
||||
type_: Type[Any],
|
||||
class_validators: Optional[Dict[str, Validator]] = None,
|
||||
default: Optional[Any] = None,
|
||||
required: Union[bool, UndefinedType] = True,
|
||||
default: Optional[Any] = Undefined,
|
||||
required: Union[bool, UndefinedType] = Undefined,
|
||||
model_config: Type[BaseConfig] = BaseConfig,
|
||||
field_info: Optional[FieldInfo] = None,
|
||||
alias: Optional[str] = None,
|
||||
@@ -86,20 +73,27 @@ def create_response_field(
|
||||
Create a new response field. Raises if type_ is invalid.
|
||||
"""
|
||||
class_validators = class_validators or {}
|
||||
field_info = field_info or FieldInfo()
|
||||
|
||||
try:
|
||||
return ModelField(
|
||||
name=name,
|
||||
type_=type_,
|
||||
class_validators=class_validators,
|
||||
default=default,
|
||||
required=required,
|
||||
model_config=model_config,
|
||||
alias=alias,
|
||||
field_info=field_info,
|
||||
if PYDANTIC_V2:
|
||||
field_info = field_info or FieldInfo(
|
||||
annotation=type_, default=default, alias=alias
|
||||
)
|
||||
except RuntimeError:
|
||||
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,
|
||||
}
|
||||
)
|
||||
try:
|
||||
return ModelField(**kwargs) # type: ignore[arg-type]
|
||||
except (RuntimeError, PydanticSchemaGenerationError):
|
||||
raise fastapi.exceptions.FastAPIError(
|
||||
"Invalid args for response field! Hint: "
|
||||
f"check that {type_} is a valid Pydantic field type. "
|
||||
@@ -116,6 +110,8 @@ 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:
|
||||
@@ -136,30 +132,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
|
||||
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.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.field_info = field.field_info
|
||||
new_field.allow_none = field.allow_none
|
||||
new_field.validate_always = field.validate_always
|
||||
if field.sub_fields:
|
||||
new_field.sub_fields = [
|
||||
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]
|
||||
create_cloned_field(sub_field, cloned_types=cloned_types)
|
||||
for sub_field in field.sub_fields
|
||||
for sub_field in field.sub_fields # type: ignore[attr-defined]
|
||||
]
|
||||
if field.key_field:
|
||||
new_field.key_field = create_cloned_field(
|
||||
field.key_field, cloned_types=cloned_types
|
||||
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]
|
||||
)
|
||||
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()
|
||||
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]
|
||||
return new_field
|
||||
|
||||
|
||||
@@ -220,3 +216,9 @@ 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}")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[build-system]
|
||||
requires = ["hatchling >= 1.13.0"]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
@@ -42,14 +42,13 @@ classifiers = [
|
||||
]
|
||||
dependencies = [
|
||||
"starlette>=0.27.0,<0.28.0",
|
||||
"pydantic>=1.7.4,!=1.8,!=1.8.1,<2.0.0",
|
||||
"pydantic>=1.7.4,!=1.8,!=1.8.1,<3.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 = [
|
||||
@@ -60,7 +59,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 >=1.1.1",
|
||||
"email_validator >=2.0.0",
|
||||
"uvicorn[standard] >=0.12.0",
|
||||
]
|
||||
|
||||
@@ -84,6 +83,7 @@ check_untyped_defs = true
|
||||
addopts = [
|
||||
"--strict-config",
|
||||
"--strict-markers",
|
||||
"--ignore=docs_src",
|
||||
]
|
||||
xfail_strict = true
|
||||
junit_family = "xunit2"
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
-e .
|
||||
pytest >=7.1.3,<8.0.0
|
||||
coverage[toml] >= 6.5.0,< 8.0
|
||||
mypy ==1.4.0
|
||||
ruff ==0.0.275
|
||||
dirty-equals >= 0.6.0
|
||||
|
||||
mypy ==1.3.0
|
||||
ruff ==0.0.272
|
||||
black == 23.3.0
|
||||
httpx >=0.23.0,<0.24.0
|
||||
email_validator >=1.1.1,<2.0.0
|
||||
email_validator >=2.0.0,<3.0.0
|
||||
# TODO: once removing databases from tutorial, upgrade SQLAlchemy
|
||||
# probably when including SQLModel
|
||||
sqlalchemy >=1.3.18,<1.4.43
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
-e .[all]
|
||||
-r requirements-tests.txt
|
||||
-r requirements-docs.txt
|
||||
uvicorn[standard] >=0.12.0,<0.23.0
|
||||
pre-commit >=2.17.0,<4.0.0
|
||||
uvicorn[standard] >=0.12.0,<0.21.0
|
||||
pre-commit >=2.17.0,<3.0.0
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, HttpUrl
|
||||
@@ -42,13 +43,24 @@ def test_openapi_schema():
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {
|
||||
"title": "Callback Url",
|
||||
"maxLength": 2083,
|
||||
"minLength": 1,
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
},
|
||||
"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",
|
||||
}
|
||||
),
|
||||
"name": "callback_url",
|
||||
"in": "query",
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
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()
|
||||
@@ -30,21 +32,46 @@ client = TestClient(app)
|
||||
|
||||
foo_is_missing = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "foo"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
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",
|
||||
}
|
||||
)
|
||||
]
|
||||
}
|
||||
foo_is_short = {
|
||||
"detail": [
|
||||
{
|
||||
"ctx": {"limit_value": 1},
|
||||
"loc": ["query", "foo"],
|
||||
"msg": "ensure this value has at least 1 characters",
|
||||
"type": "value_error.any_str.min_length",
|
||||
}
|
||||
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",
|
||||
}
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from .main import app
|
||||
@@ -266,10 +267,17 @@ 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"}),
|
||||
}
|
||||
],
|
||||
}
|
||||
@@ -969,10 +977,17 @@ 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"}),
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
93
tests/test_compat.py
Normal file
93
tests/test_compat.py
Normal file
@@ -0,0 +1,93 @@
|
||||
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]])
|
||||
@@ -1,4 +1,5 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi._compat import PYDANTIC_V2
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel
|
||||
|
||||
@@ -8,10 +9,18 @@ app = FastAPI()
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"x-something-internal": {"level": 4},
|
||||
if PYDANTIC_V2:
|
||||
model_config = {
|
||||
"json_schema_extra": {
|
||||
"x-something-internal": {"level": 4},
|
||||
}
|
||||
}
|
||||
else:
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"x-something-internal": {"level": 4},
|
||||
}
|
||||
|
||||
|
||||
@app.get("/foo", response_model=Item)
|
||||
|
||||
@@ -7,11 +7,17 @@ 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")
|
||||
|
||||
@@ -4,31 +4,54 @@ from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ModelWithDatetimeField(BaseModel):
|
||||
dt_field: datetime
|
||||
|
||||
class Config:
|
||||
json_encoders = {
|
||||
datetime: lambda dt: dt.replace(
|
||||
microsecond=0, tzinfo=timezone.utc
|
||||
).isoformat()
|
||||
}
|
||||
from .utils import needs_pydanticv1, needs_pydanticv2
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8))
|
||||
@needs_pydanticv2
|
||||
def test_pydanticv2():
|
||||
from pydantic import field_serializer
|
||||
|
||||
class ModelWithDatetimeField(BaseModel):
|
||||
dt_field: datetime
|
||||
|
||||
@app.get("/model", response_model=ModelWithDatetimeField)
|
||||
def get_model():
|
||||
return model
|
||||
@field_serializer("dt_field")
|
||||
def serialize_datetime(self, dt_field: datetime):
|
||||
return dt_field.replace(microsecond=0, tzinfo=timezone.utc).isoformat()
|
||||
|
||||
app = FastAPI()
|
||||
model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8))
|
||||
|
||||
client = TestClient(app)
|
||||
@app.get("/model", response_model=ModelWithDatetimeField)
|
||||
def get_model():
|
||||
return model
|
||||
|
||||
|
||||
def test_dt():
|
||||
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)
|
||||
with client:
|
||||
response = client.get("/model")
|
||||
assert response.json() == {"dt_field": "2019-01-01T08:00:00+00:00"}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
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()
|
||||
@@ -47,15 +49,30 @@ 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() == {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "item2"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
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",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_no_duplicates():
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
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()
|
||||
|
||||
@@ -50,99 +52,180 @@ async def overrider_dependency_with_sub(msg: dict = Depends(overrider_sub_depend
|
||||
return msg
|
||||
|
||||
|
||||
@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
|
||||
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(
|
||||
@@ -190,126 +273,281 @@ def test_override_simple(url, status_code, expected):
|
||||
app.dependency_overrides = {}
|
||||
|
||||
|
||||
@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):
|
||||
def test_override_with_sub_main_depends():
|
||||
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
|
||||
response = client.get(url)
|
||||
assert response.status_code == status_code
|
||||
assert response.json() == expected
|
||||
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"}
|
||||
app.dependency_overrides = {}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from typing import Optional
|
||||
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.testclient import TestClient
|
||||
@@ -327,7 +328,14 @@ def test_openapi_schema():
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"price": {"title": "Price", "type": "number"},
|
||||
"price": IsDict(
|
||||
{
|
||||
"title": "Price",
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
| IsDict({"title": "Price", "type": "number"}),
|
||||
},
|
||||
},
|
||||
"ValidationError": {
|
||||
|
||||
0
tests/test_filter_pydantic_sub_model/__init__.py
Normal file
0
tests/test_filter_pydantic_sub_model/__init__.py
Normal file
35
tests/test_filter_pydantic_sub_model/app_pv1.py
Normal file
35
tests/test_filter_pydantic_sub_model/app_pv1.py
Normal file
@@ -0,0 +1,35 @@
|
||||
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}
|
||||
@@ -1,46 +1,20 @@
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
from fastapi import Depends, FastAPI
|
||||
from fastapi.exceptions import ResponseValidationError
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, ValidationError, validator
|
||||
|
||||
app = FastAPI()
|
||||
from ..utils import needs_pydanticv1
|
||||
|
||||
|
||||
class ModelB(BaseModel):
|
||||
username: str
|
||||
@pytest.fixture(name="client")
|
||||
def get_client():
|
||||
from .app_pv1 import app
|
||||
|
||||
client = TestClient(app)
|
||||
return client
|
||||
|
||||
|
||||
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():
|
||||
@needs_pydanticv1
|
||||
def test_filter_sub_model(client: TestClient):
|
||||
response = client.get("/model/modelA")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
@@ -50,8 +24,9 @@ def test_filter_sub_model():
|
||||
}
|
||||
|
||||
|
||||
def test_validator_is_cloned():
|
||||
with pytest.raises(ValidationError) as err:
|
||||
@needs_pydanticv1
|
||||
def test_validator_is_cloned(client: TestClient):
|
||||
with pytest.raises(ResponseValidationError) as err:
|
||||
client.get("/model/modelX")
|
||||
assert err.value.errors() == [
|
||||
{
|
||||
@@ -62,7 +37,8 @@ def test_validator_is_cloned():
|
||||
]
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
@needs_pydanticv1
|
||||
def test_openapi_schema(client: TestClient):
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
182
tests/test_filter_pydantic_sub_model_pv2.py
Normal file
182
tests/test_filter_pydantic_sub_model_pv2.py
Normal file
@@ -0,0 +1,182 @@
|
||||
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"},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
from typing import Optional
|
||||
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
@@ -104,35 +105,253 @@ def test_get_users_item():
|
||||
assert response.json() == {"item_id": "item01", "user_id": "abc123"}
|
||||
|
||||
|
||||
def test_schema_1():
|
||||
"""Check that the user_id is a required path parameter under /users"""
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
r = response.json()
|
||||
|
||||
d = {
|
||||
"required": True,
|
||||
"schema": {"title": "User Id", "type": "string"},
|
||||
"name": "user_id",
|
||||
"in": "path",
|
||||
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"},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
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"]
|
||||
|
||||
@@ -5,7 +5,7 @@ from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
from .utils import needs_pydanticv1, needs_pydanticv2
|
||||
|
||||
|
||||
class MyUuid:
|
||||
@@ -26,40 +26,78 @@ class MyUuid:
|
||||
raise TypeError("vars() argument must have __dict__ attribute")
|
||||
|
||||
|
||||
@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
|
||||
@needs_pydanticv2
|
||||
def test_pydanticv2():
|
||||
from pydantic import field_serializer
|
||||
|
||||
# from asyncpg.pgproto import pgproto
|
||||
# asyncpg_uuid = pgproto.UUID("a10ff360-3b1e-4984-a26f-d3ab460bdb51")
|
||||
app = FastAPI()
|
||||
|
||||
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}
|
||||
@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):
|
||||
model_config = {"arbitrary_types_allowed": True}
|
||||
|
||||
class SomeCustomClass(BaseModel):
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
json_encoders = {uuid.UUID: str}
|
||||
a_uuid: MyUuid
|
||||
|
||||
a_uuid: MyUuid
|
||||
@field_serializer("a_uuid")
|
||||
def serialize_a_uuid(self, v):
|
||||
return str(v)
|
||||
|
||||
@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"))
|
||||
|
||||
@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)
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_dt():
|
||||
with client:
|
||||
response_simple = client.get("/fast_uuid")
|
||||
response_pydantic = client.get("/get_custom_class")
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
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, create_model
|
||||
from pydantic import BaseModel, Field, ValidationError
|
||||
|
||||
from .utils import needs_pydanticv1, needs_pydanticv2
|
||||
|
||||
|
||||
class Person:
|
||||
@@ -45,22 +49,6 @@ 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"
|
||||
@@ -69,8 +57,12 @@ class RoleEnum(Enum):
|
||||
class ModelWithConfig(BaseModel):
|
||||
role: Optional[RoleEnum] = None
|
||||
|
||||
class Config:
|
||||
use_enum_values = True
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"use_enum_values": True}
|
||||
else:
|
||||
|
||||
class Config:
|
||||
use_enum_values = True
|
||||
|
||||
|
||||
class ModelWithAlias(BaseModel):
|
||||
@@ -83,23 +75,6 @@ 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"}}
|
||||
@@ -153,14 +128,47 @@ def test_encode_unsupported():
|
||||
jsonable_encoder(unserializable)
|
||||
|
||||
|
||||
def test_encode_custom_json_encoders_model():
|
||||
@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
|
||||
|
||||
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"}
|
||||
|
||||
|
||||
def test_encode_custom_json_encoders_model_subclass():
|
||||
model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8))
|
||||
# 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))
|
||||
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():
|
||||
@@ -196,6 +204,7 @@ def test_encode_model_with_default():
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv1
|
||||
def test_custom_encoders():
|
||||
class safe_datetime(datetime):
|
||||
pass
|
||||
@@ -226,14 +235,67 @@ def test_custom_enum_encoders():
|
||||
assert encoded_instance == custom_enum_encoder(instance)
|
||||
|
||||
|
||||
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_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_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}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
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()
|
||||
@@ -21,59 +23,115 @@ 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": 5}]}
|
||||
assert response.json() == {
|
||||
"item": [
|
||||
{
|
||||
"name": "Foo",
|
||||
"age": IsOneOf(
|
||||
5,
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"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() == single_error
|
||||
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",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
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() == multiple_errors
|
||||
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",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
@@ -126,11 +184,23 @@ def test_openapi_schema():
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"age": {
|
||||
"title": "Age",
|
||||
"exclusiveMinimum": 0.0,
|
||||
"type": "number",
|
||||
},
|
||||
"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",
|
||||
}
|
||||
),
|
||||
},
|
||||
},
|
||||
"ValidationError": {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
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()
|
||||
|
||||
@@ -14,22 +16,6 @@ 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
|
||||
@@ -39,7 +25,42 @@ 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() == multiple_errors
|
||||
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",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from typing import Optional
|
||||
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
@@ -52,11 +53,21 @@ def test_openapi():
|
||||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"title": "Standard Query Param",
|
||||
"type": "integer",
|
||||
"default": 50,
|
||||
},
|
||||
"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,
|
||||
}
|
||||
),
|
||||
"name": "standard_query_param",
|
||||
"in": "query",
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from dirty_equals import IsOneOf
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
@@ -35,10 +36,20 @@ def test_openapi_schema():
|
||||
"servers": [
|
||||
{"url": "/", "description": "Default, relative server"},
|
||||
{
|
||||
"url": "http://staging.localhost.tiangolo.com:8000",
|
||||
"url": IsOneOf(
|
||||
"http://staging.localhost.tiangolo.com:8000/",
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"http://staging.localhost.tiangolo.com:8000",
|
||||
),
|
||||
"description": "Staging but actually localhost still",
|
||||
},
|
||||
{"url": "https://prod.example.com"},
|
||||
{
|
||||
"url": IsOneOf(
|
||||
"https://prod.example.com/",
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"https://prod.example.com",
|
||||
)
|
||||
},
|
||||
],
|
||||
"paths": {
|
||||
"/foo": {
|
||||
|
||||
@@ -33,7 +33,7 @@ async def hidden_query(
|
||||
return {"hidden_query": hidden_query}
|
||||
|
||||
|
||||
openapi_schema = {
|
||||
openapi_shema = {
|
||||
"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_schema
|
||||
assert response.json() == openapi_shema
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Any, List
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsOneOf
|
||||
from fastapi.params import Body, Cookie, Depends, Header, Param, Path, Query
|
||||
|
||||
test_data: List[Any] = ["teststr", None, ..., 1, []]
|
||||
@@ -10,34 +10,137 @@ def get_user():
|
||||
return {} # pragma: no cover
|
||||
|
||||
|
||||
@pytest.fixture(scope="function", params=test_data)
|
||||
def params(request):
|
||||
return request.param
|
||||
def test_param_repr_str():
|
||||
assert repr(Param("teststr")) == "Param(teststr)"
|
||||
|
||||
|
||||
def test_param_repr(params):
|
||||
assert repr(Param(params)) == "Param(" + str(params) + ")"
|
||||
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_path_repr():
|
||||
assert repr(Path()) == "Path(Ellipsis)"
|
||||
assert repr(Path(...)) == "Path(Ellipsis)"
|
||||
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)",
|
||||
)
|
||||
|
||||
|
||||
def test_query_repr(params):
|
||||
assert repr(Query(params)) == "Query(" + str(params) + ")"
|
||||
def test_query_repr_str():
|
||||
assert repr(Query("teststr")) == "Query(teststr)"
|
||||
|
||||
|
||||
def test_header_repr(params):
|
||||
assert repr(Header(params)) == "Header(" + str(params) + ")"
|
||||
def test_query_repr_none():
|
||||
assert repr(Query(None)) == "Query(None)"
|
||||
|
||||
|
||||
def test_cookie_repr(params):
|
||||
assert repr(Cookie(params)) == "Cookie(" + str(params) + ")"
|
||||
def test_query_repr_ellipsis():
|
||||
assert repr(Query(...)) == IsOneOf(
|
||||
"Query(PydanticUndefined)",
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"Query(Ellipsis)",
|
||||
)
|
||||
|
||||
|
||||
def test_body_repr(params):
|
||||
assert repr(Body(params)) == "Body(" + 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_depends_repr():
|
||||
|
||||
1413
tests/test_path.py
1413
tests/test_path.py
File diff suppressed because it is too large
Load Diff
@@ -1,62 +1,410 @@
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
|
||||
from .main import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
response_missing = {
|
||||
"detail": [
|
||||
|
||||
def test_query():
|
||||
response = client.get("/query")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"loc": ["query", "query"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "query"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
response_not_valid_int = {
|
||||
"detail": [
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"loc": ["query", "query"],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "query"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@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
|
||||
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"
|
||||
|
||||
@@ -2,48 +2,83 @@ from typing import Any
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
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)
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from .utils import needs_pydanticv1, needs_pydanticv2
|
||||
|
||||
|
||||
@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()
|
||||
|
||||
@@ -39,7 +39,6 @@ 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"},
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from typing import List
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi._compat import PYDANTIC_V2
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@@ -14,13 +15,24 @@ class Model(BaseModel):
|
||||
class ModelNoAlias(BaseModel):
|
||||
name: str
|
||||
|
||||
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"
|
||||
)
|
||||
}
|
||||
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"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@app.get("/dict", response_model=Model, response_model_by_alias=False)
|
||||
|
||||
@@ -2,10 +2,10 @@ from typing import List, Union
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.exceptions import FastAPIError
|
||||
from fastapi.exceptions import FastAPIError, ResponseValidationError
|
||||
from fastapi.responses import JSONResponse, Response
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
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(ValidationError):
|
||||
with pytest.raises(ResponseValidationError):
|
||||
client.get("/response_model-no_annotation-return_invalid_dict")
|
||||
|
||||
|
||||
def test_response_model_no_annotation_return_invalid_model():
|
||||
with pytest.raises(ValidationError):
|
||||
with pytest.raises(ResponseValidationError):
|
||||
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(ValidationError):
|
||||
with pytest.raises(ResponseValidationError):
|
||||
client.get("/no_response_model-annotation-return_invalid_dict")
|
||||
|
||||
|
||||
def test_no_response_model_annotation_return_invalid_model():
|
||||
with pytest.raises(ValidationError):
|
||||
with pytest.raises(ResponseValidationError):
|
||||
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(ValidationError):
|
||||
with pytest.raises(ResponseValidationError):
|
||||
client.get("/response_model_model1-annotation_model2-return_invalid_dict")
|
||||
|
||||
|
||||
def test_response_model_model1_annotation_model2_return_invalid_model():
|
||||
with pytest.raises(ValidationError):
|
||||
with pytest.raises(ResponseValidationError):
|
||||
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
Reference in New Issue
Block a user