From 61596ebe5b270a619fb2a3e73ff55b71333f4760 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 6 Oct 2025 20:09:58 +0900 Subject: [PATCH 001/256] =?UTF-8?q?=F0=9F=94=A8=20Add=20Russian=20translat?= =?UTF-8?q?ions=20LLM=20prompt=20(#13936)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔨 Add Russian translations LLM prompt * 🔨 Tweak prompt with input from Yurii * 📝 Update LLM prompt * Update llm-prompt.md * Update llm-prompt.md * Update llm-prompt.md * 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks * Update llm-prompt.md * Update llm-prompt.md * Update llm-prompt.md * Update ru `llm-prompt.md` --------- Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Yurii Motov --- docs/ru/llm-prompt.md | 94 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 docs/ru/llm-prompt.md diff --git a/docs/ru/llm-prompt.md b/docs/ru/llm-prompt.md new file mode 100644 index 000000000..6a437bdd1 --- /dev/null +++ b/docs/ru/llm-prompt.md @@ -0,0 +1,94 @@ +Translate to Russian (русский язык). + +Language code: ru. + +--- + +Use a neutral tone (not overly formal or informal). + +Use correct Russian grammar — appropriate cases, suffixes, and endings depending on context. + +For the following technical terms, use these specific translations to ensure consistency and clarity across the documentation: + +* production (meaning production software or environment): продакшн (do not change the ending, for example, translate `in production` as `в продакшн` (not `в продакшене`)) +* completion (meaning code auto-completion): автозавершение +* editor (meaning component of IDE): редактор кода +* adopt (meaning start to use): использовать (or `начать использовать`) +* headers (meaning HTTP-headers): HTTP-заголовки +* cookie sessions: сессии с использованием cookie +* tested (adjective): протестированный +* middleware: middleware (don't translate, but add `промежуточный слой` if clarification is needed) +* path operation: операция пути (optionally clarify as `обработчик пути`) +* path operation function: функция-обработчик пути (or `функция обработки пути`) +* proprietary: проприетарный +* benchmark: бенчмарк (add (`тест производительности`) if clarification is needed or use just `тест производительности`) +* ASGI server: ASGI-сервер +* In a hurry? : Нет времени? +* response status code: статус-код ответа +* HTTP status code: HTTP статус-код +* issue (meaning GitHub issue): Issue (add `тикет\обращение` if clarification is needed) +* PR (meaning GitHub pull request): пулл-реквест (add `запрос на изменение` if clarification is needed) +* run (meaning run the code): запустить (or `прогнать` if it's about testing the program) +* to reach users: донести до пользователей (or `привлечь внимание пользователей` in the promotion context) +* body (meaning HTTP request body): тело запроса +* body (meaning HTTP response body): тело ответа +* body parameter : body-параметр (or `параметр тела запроса`) +* validate: валидировать (or `выполнить валидацию`) +* requirements (meaning dependencies): зависимости +* auto-reload: авто-перезагрузка (or `перезагрузить автоматически` if used as a verb) +* show (meaning show on the screen): отобразить +* parsing (noun): парсинг +* origin (in web development): origin (add `источник` if clarification is needed) +* include: включать (add `в себя` if it's appropriate, or use `содержать` as an alternative) +* virtual environment: виртуальное окружение +* framework: фреймворк +* path paremeter: path-параметр +* path (as in URL path): путь +* form (as in HTML form): форма +* media type: тип содержимого (or `медиа-тип`) +* request: HTTP-запрос +* response: HTTP-ответ +* type hints: аннотации типов +* type annotations: аннотации типов +* context manager: менеджер контекста +* code base: кодовая база +* instantiate: создать экземпляр (avoid "инстанцировать") +* load balancer: балансировщик нагрузки +* load balance: балансировка нагрузки +* worker process: воркер-процесс (or `процесс воркера`) +* worker: воркер +* lifespan: lifespan (do not translate when it's about lifespan events, but translate as `жизненный цикл` or `срок жизни` in other cases) +* mount (verb): монтировать +* mount (noun): точка монтирования / mount (keep in English if it's a FastAPI keyword) +* plugin: плагин +* plug-in: плагин +* full stack: full stack (do not translate) +* full-stack: full-stack (do not translate) +* loop (as in async loop): цикл событий +* Machine Learning: Машинное обучение +* Deep Learning: Глубокое обучение +* callback hell: callback hell (clarify as `ад обратных вызовов`) +* on the fly: на лету +* scratch the surface: поверхностно ознакомиться +* tip: совет (or `подсказка` depending on context) +* Pydantic model: Pydantic-модель (`модель Pydantic` and `Pydantic модель` are also fine) +* declare: объявить +* have the next best performance, after: быть на следующем месте по производительности после +* timing attack: тайминговая атака (clarify `атака по времени` if needed) +* OAuth2 scope: OAuth2 scope (clarify `область` if needed) +* TLS Termination Proxy: прокси-сервер TSL-терминации +* utilize (resources): использовать +* сontent: содержимое (or `контент`) +* raise exception: вызвать исключение (also possible to use `сгенерировать исключение` or `выбросить исключение`) +* password flow: password flow (clarify as `аутентификация по паролю` if needed) +* tutorial: руководство (or `учебник`) +* too long; didn't read: слишком длинно; не читал +* proxy with a stripped path prefix: прокси с функцией удаления префикса пути +* nerd: умник +* sub application: подприложение +* webhook request: вебхук-запрос +* serve (meaning providing access to something): «отдавать» (or `предоставлять доступ к`) +* recap (noun): резюме +* utility function: вспомогательная функция + +Do not add whitespace in `т.д.`, `т.п.`. From 56c0632f863b49b6224a8eea1b899ea8dacffa2e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 6 Oct 2025 11:10:19 +0000 Subject: [PATCH 002/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index cbbb173cc..3ae043416 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Translations +* 🔨 Add Russian translations LLM prompt. PR [#13936](https://github.com/fastapi/fastapi/pull/13936) by [@tiangolo](https://github.com/tiangolo). * 🌐 Sync German docs. PR [#14149](https://github.com/fastapi/fastapi/pull/14149) by [@nilslindemann](https://github.com/nilslindemann). * 🌐 Add Russian translations for missing pages (LLM-generated). PR [#14135](https://github.com/fastapi/fastapi/pull/14135) by [@YuriiMotov](https://github.com/YuriiMotov). * 🌐 Update Russian translations for existing pages (LLM-generated). PR [#14123](https://github.com/fastapi/fastapi/pull/14123) by [@YuriiMotov](https://github.com/YuriiMotov). From 6df0358b80ccb8d9c72a9fbae44d2ec8be978190 Mon Sep 17 00:00:00 2001 From: Simon Gurcke Date: Tue, 7 Oct 2025 05:08:20 +1000 Subject: [PATCH 003/256] =?UTF-8?q?=F0=9F=93=9D=20Add=20External=20Link:?= =?UTF-8?q?=20Getting=20started=20with=20logging=20in=20FastAPI=20(#14152)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add logging article to External links --- docs/en/data/external_links.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/data/external_links.yml b/docs/en/data/external_links.yml index c1191b460..b8a5fdb3a 100644 --- a/docs/en/data/external_links.yml +++ b/docs/en/data/external_links.yml @@ -1,5 +1,9 @@ Articles: English: + - author: Apitally + author_link: https://apitally.io + link: https://apitally.io/blog/getting-started-with-logging-in-fastapi + title: Getting started with logging in FastAPI - author: Balthazar Rouberol author_link: https://balthazar-rouberol.com link: https://blog.balthazar-rouberol.com/how-to-profile-a-fastapi-asynchronous-request From 1cd8717818e886d54a275f78305b5d8b61a46309 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 6 Oct 2025 19:08:44 +0000 Subject: [PATCH 004/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 3ae043416..9ca2680cb 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Docs + +* 📝 Add External Link: Getting started with logging in FastAPI. PR [#14152](https://github.com/fastapi/fastapi/pull/14152) by [@itssimon](https://github.com/itssimon). + ### Translations * 🔨 Add Russian translations LLM prompt. PR [#13936](https://github.com/fastapi/fastapi/pull/13936) by [@tiangolo](https://github.com/tiangolo). From bc5e877c9c6ce375a25c0b463e6ba5be92837e03 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 17:05:17 +0200 Subject: [PATCH 005/256] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commi?= =?UTF-8?q?t=20autoupdate=20(#14161)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.13.2 → v0.13.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.13.2...v0.13.3) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d9ab333ad..9c075f68e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.13.2 + rev: v0.13.3 hooks: - id: ruff args: From 22b38099ce5a4fba4f26cf8cae335691b26aab39 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 7 Oct 2025 15:05:44 +0000 Subject: [PATCH 006/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 9ca2680cb..b6e3aeef9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -21,6 +21,7 @@ hide: ### Internal +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#14161](https://github.com/fastapi/fastapi/pull/14161) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump griffe-typingdoc from 0.2.8 to 0.2.9. PR [#14144](https://github.com/fastapi/fastapi/pull/14144) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-macros-plugin from 1.3.9 to 1.4.0. PR [#14145](https://github.com/fastapi/fastapi/pull/14145) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump markdown-include-variants from 0.0.4 to 0.0.5. PR [#14146](https://github.com/fastapi/fastapi/pull/14146) by [@dependabot[bot]](https://github.com/apps/dependabot). From c970d8a735f75249ae7badb036bf0560c6c2d186 Mon Sep 17 00:00:00 2001 From: Colin Watson Date: Wed, 8 Oct 2025 09:57:37 +0100 Subject: [PATCH 007/256] =?UTF-8?q?=F0=9F=91=BD=EF=B8=8F=20Ensure=20compat?= =?UTF-8?q?ibility=20with=20Pydantic=202.12.0=20(#14036)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sofie Van Landeghem Co-authored-by: Victorien <65306057+Viicos@users.noreply.github.com> Co-authored-by: svlandeg Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Co-authored-by: Patrick Arminio --- fastapi/_compat.py | 18 +++++++++++++++--- fastapi/params.py | 16 ++++++++-------- tests/test_multi_body_errors.py | 10 +++++++++- 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/fastapi/_compat.py b/fastapi/_compat.py index 26b6638c8..8ea5bf253 100644 --- a/fastapi/_compat.py +++ b/fastapi/_compat.py @@ -1,3 +1,4 @@ +import warnings from collections import deque from copy import copy from dataclasses import dataclass, is_dataclass @@ -109,9 +110,20 @@ if PYDANTIC_V2: return self.field_info.annotation def __post_init__(self) -> None: - self._type_adapter: TypeAdapter[Any] = TypeAdapter( - Annotated[self.field_info.annotation, self.field_info] - ) + with warnings.catch_warnings(): + # Pydantic >= 2.12.0 warns about field specific metadata that is unused + # (e.g. `TypeAdapter(Annotated[int, Field(alias='b')])`). In some cases, we + # end up building the type adapter from a model field annotation so we + # need to ignore the warning: + if PYDANTIC_VERSION_MINOR_TUPLE >= (2, 12): + from pydantic.warnings import UnsupportedFieldAttributeWarning + + warnings.simplefilter( + "ignore", category=UnsupportedFieldAttributeWarning + ) + 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(): diff --git a/fastapi/params.py b/fastapi/params.py index 8f5601dd3..e85375018 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -22,7 +22,7 @@ class ParamTypes(Enum): cookie = "cookie" -class Param(FieldInfo): +class Param(FieldInfo): # type: ignore[misc] in_: ParamTypes def __init__( @@ -136,7 +136,7 @@ class Param(FieldInfo): return f"{self.__class__.__name__}({self.default})" -class Path(Param): +class Path(Param): # type: ignore[misc] in_ = ParamTypes.path def __init__( @@ -222,7 +222,7 @@ class Path(Param): ) -class Query(Param): +class Query(Param): # type: ignore[misc] in_ = ParamTypes.query def __init__( @@ -306,7 +306,7 @@ class Query(Param): ) -class Header(Param): +class Header(Param): # type: ignore[misc] in_ = ParamTypes.header def __init__( @@ -392,7 +392,7 @@ class Header(Param): ) -class Cookie(Param): +class Cookie(Param): # type: ignore[misc] in_ = ParamTypes.cookie def __init__( @@ -476,7 +476,7 @@ class Cookie(Param): ) -class Body(FieldInfo): +class Body(FieldInfo): # type: ignore[misc] def __init__( self, default: Any = Undefined, @@ -593,7 +593,7 @@ class Body(FieldInfo): return f"{self.__class__.__name__}({self.default})" -class Form(Body): +class Form(Body): # type: ignore[misc] def __init__( self, default: Any = Undefined, @@ -677,7 +677,7 @@ class Form(Body): ) -class File(Form): +class File(Form): # type: ignore[misc] def __init__( self, default: Any = Undefined, diff --git a/tests/test_multi_body_errors.py b/tests/test_multi_body_errors.py index 0102f0f1a..33304827a 100644 --- a/tests/test_multi_body_errors.py +++ b/tests/test_multi_body_errors.py @@ -185,7 +185,15 @@ def test_openapi_schema(): "title": "Age", "anyOf": [ {"exclusiveMinimum": 0.0, "type": "number"}, - {"type": "string"}, + IsOneOf( + # pydantic < 2.12.0 + {"type": "string"}, + # pydantic >= 2.12.0 + { + "type": "string", + "pattern": r"^(?!^[-+.]*$)[+-]?0*\d*\.?\d*$", + }, + ), ], } ) From 32b93b53fc76e21b19c0d17d4b77c3b19380b440 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 8 Oct 2025 08:58:49 +0000 Subject: [PATCH 008/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index b6e3aeef9..1b3106dae 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Upgrades + +* 👽️ Ensure compatibility with Pydantic 2.12.0. PR [#14036](https://github.com/fastapi/fastapi/pull/14036) by [@cjwatson](https://github.com/cjwatson). + ### Docs * 📝 Add External Link: Getting started with logging in FastAPI. PR [#14152](https://github.com/fastapi/fastapi/pull/14152) by [@itssimon](https://github.com/itssimon). From 485bfedf5d5d4a51c53b12010d685854174da92f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 8 Oct 2025 18:03:21 +0900 Subject: [PATCH 009/256] =?UTF-8?q?=F0=9F=94=A8=20Move=20local=20coverage?= =?UTF-8?q?=20logic=20to=20its=20own=20script=20(#14166)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/coverage.sh | 8 ++++++++ scripts/test-cov-html.sh | 4 +--- 2 files changed, 9 insertions(+), 3 deletions(-) create mode 100755 scripts/coverage.sh diff --git a/scripts/coverage.sh b/scripts/coverage.sh new file mode 100755 index 000000000..e07b51ec5 --- /dev/null +++ b/scripts/coverage.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e +set -x + +coverage combine +coverage report +coverage html diff --git a/scripts/test-cov-html.sh b/scripts/test-cov-html.sh index 517ac6422..f87f906dc 100755 --- a/scripts/test-cov-html.sh +++ b/scripts/test-cov-html.sh @@ -4,6 +4,4 @@ set -e set -x bash scripts/test.sh ${@} -coverage combine -coverage report -coverage html +bash scripts/coverage.sh From fca8564ea0767bebe6082622fc57df23686030b7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 8 Oct 2025 09:03:50 +0000 Subject: [PATCH 010/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 1b3106dae..f00f63a67 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -25,6 +25,7 @@ hide: ### Internal +* 🔨 Move local coverage logic to its own script. PR [#14166](https://github.com/fastapi/fastapi/pull/14166) by [@tiangolo](https://github.com/tiangolo). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#14161](https://github.com/fastapi/fastapi/pull/14161) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump griffe-typingdoc from 0.2.8 to 0.2.9. PR [#14144](https://github.com/fastapi/fastapi/pull/14144) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-macros-plugin from 1.3.9 to 1.4.0. PR [#14145](https://github.com/fastapi/fastapi/pull/14145) by [@dependabot[bot]](https://github.com/apps/dependabot). From 01be148429241adb7a9ba5eb8f6cd5e17230b646 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 8 Oct 2025 11:05:44 +0200 Subject: [PATCH 011/256] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.11?= =?UTF-8?q?8.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f00f63a67..7d7c7ec5a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.118.1 + ### Upgrades * 👽️ Ensure compatibility with Pydantic 2.12.0. PR [#14036](https://github.com/fastapi/fastapi/pull/14036) by [@cjwatson](https://github.com/cjwatson). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 03a5aaad5..12ebbaf9b 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.118.0" +__version__ = "0.118.1" from starlette import status as status From 05dbfebce51044b88b115222c635e50096f72947 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Oct 2025 16:36:25 +0200 Subject: [PATCH 012/256] =?UTF-8?q?=E2=AC=86=20Bump=20astral-sh/setup-uv?= =?UTF-8?q?=20from=206=20to=207=20(#14167)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 6 to 7. - [Release notes](https://github.com/astral-sh/setup-uv/releases) - [Commits](https://github.com/astral-sh/setup-uv/compare/v6...v7) --- updated-dependencies: - dependency-name: astral-sh/setup-uv dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-docs.yml | 4 ++-- .github/workflows/contributors.yml | 2 +- .github/workflows/deploy-docs.yml | 2 +- .github/workflows/label-approved.yml | 2 +- .github/workflows/notify-translations.yml | 2 +- .github/workflows/people.yml | 2 +- .github/workflows/smokeshow.yml | 2 +- .github/workflows/sponsors.yml | 2 +- .github/workflows/test.yml | 6 +++--- .github/workflows/topic-repos.yml | 2 +- .github/workflows/translate.yml | 2 +- 11 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index a5761361d..b3e53b91c 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -54,7 +54,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true @@ -96,7 +96,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/contributors.yml b/.github/workflows/contributors.yml index ee8bfafb4..7d5449c6a 100644 --- a/.github/workflows/contributors.yml +++ b/.github/workflows/contributors.yml @@ -30,7 +30,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 2c432da8c..0eb26cc4d 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -29,7 +29,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/label-approved.yml b/.github/workflows/label-approved.yml index 76ac77298..e6ae3d963 100644 --- a/.github/workflows/label-approved.yml +++ b/.github/workflows/label-approved.yml @@ -26,7 +26,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/notify-translations.yml b/.github/workflows/notify-translations.yml index ef3990d31..04beeb64e 100644 --- a/.github/workflows/notify-translations.yml +++ b/.github/workflows/notify-translations.yml @@ -34,7 +34,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/people.yml b/.github/workflows/people.yml index e6e56bf04..f15b92137 100644 --- a/.github/workflows/people.yml +++ b/.github/workflows/people.yml @@ -30,7 +30,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index cde0ca308..e42d79723 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -26,7 +26,7 @@ jobs: with: python-version: '3.9' - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/sponsors.yml b/.github/workflows/sponsors.yml index 1e245346d..7d29469a5 100644 --- a/.github/workflows/sponsors.yml +++ b/.github/workflows/sponsors.yml @@ -30,7 +30,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b76afe01e..ba69519b3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,7 +29,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true @@ -67,7 +67,7 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true @@ -112,7 +112,7 @@ jobs: with: python-version: '3.8' - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/topic-repos.yml b/.github/workflows/topic-repos.yml index cb98698d3..22b37d59d 100644 --- a/.github/workflows/topic-repos.yml +++ b/.github/workflows/topic-repos.yml @@ -25,7 +25,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/translate.yml b/.github/workflows/translate.yml index fa4e8f463..a7fcf84df 100644 --- a/.github/workflows/translate.yml +++ b/.github/workflows/translate.yml @@ -48,7 +48,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true From 27c0f7e75fc0ce26ca75295d455eb36f0ca033d3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 8 Oct 2025 14:37:16 +0000 Subject: [PATCH 013/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 7d7c7ec5a..37ffd3052 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Internal + +* ⬆ Bump astral-sh/setup-uv from 6 to 7. PR [#14167](https://github.com/fastapi/fastapi/pull/14167) by [@dependabot[bot]](https://github.com/apps/dependabot). + ## 0.118.1 ### Upgrades From 185cecd891ee9591fd0f3beb65b412339d152bf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Voron?= Date: Wed, 8 Oct 2025 16:48:30 +0200 Subject: [PATCH 014/256] =?UTF-8?q?=F0=9F=90=9B=20Fix=20tagged=20discrimin?= =?UTF-8?q?ated=20union=20not=20recognized=20as=20body=20field=20(#12942)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Co-authored-by: Patrick Arminio Co-authored-by: Sebastián Ramírez --- fastapi/_compat.py | 3 + tests/test_union_body_discriminator.py | 188 +++++++++++++++++++++++++ 2 files changed, 191 insertions(+) create mode 100644 tests/test_union_body_discriminator.py diff --git a/fastapi/_compat.py b/fastapi/_compat.py index 8ea5bf253..21ea1a237 100644 --- a/fastapi/_compat.py +++ b/fastapi/_compat.py @@ -590,6 +590,9 @@ def field_annotation_is_complex(annotation: Union[Type[Any], None]) -> bool: if origin is Union or origin is UnionType: return any(field_annotation_is_complex(arg) for arg in get_args(annotation)) + if origin is Annotated: + return field_annotation_is_complex(get_args(annotation)[0]) + return ( _annotation_is_complex(annotation) or _annotation_is_complex(origin) diff --git a/tests/test_union_body_discriminator.py b/tests/test_union_body_discriminator.py new file mode 100644 index 000000000..6af9e1d22 --- /dev/null +++ b/tests/test_union_body_discriminator.py @@ -0,0 +1,188 @@ +from typing import Any, Dict, Union + +from dirty_equals import IsDict +from fastapi import FastAPI +from fastapi.testclient import TestClient +from inline_snapshot import snapshot +from pydantic import BaseModel, Field +from typing_extensions import Annotated, Literal + +from .utils import needs_pydanticv2 + + +@needs_pydanticv2 +def test_discriminator_pydantic_v2() -> None: + from pydantic import Tag + + app = FastAPI() + + class FirstItem(BaseModel): + value: Literal["first"] + price: int + + class OtherItem(BaseModel): + value: Literal["other"] + price: float + + Item = Annotated[ + Union[Annotated[FirstItem, Tag("first")], Annotated[OtherItem, Tag("other")]], + Field(discriminator="value"), + ] + + @app.post("/items/") + def save_union_body_discriminator( + item: Item, q: Annotated[str, Field(description="Query string")] + ) -> Dict[str, Any]: + return {"item": item} + + client = TestClient(app) + response = client.post("/items/?q=first", json={"value": "first", "price": 100}) + assert response.status_code == 200, response.text + assert response.json() == {"item": {"value": "first", "price": 100}} + + response = client.post("/items/?q=other", json={"value": "other", "price": 100.5}) + assert response.status_code == 200, response.text + assert response.json() == {"item": {"value": "other", "price": 100.5}} + + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "summary": "Save Union Body Discriminator", + "operationId": "save_union_body_discriminator_items__post", + "parameters": [ + { + "name": "q", + "in": "query", + "required": True, + "schema": { + "type": "string", + "description": "Query string", + "title": "Q", + }, + } + ], + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": { + "oneOf": [ + {"$ref": "#/components/schemas/FirstItem"}, + {"$ref": "#/components/schemas/OtherItem"}, + ], + "discriminator": { + "propertyName": "value", + "mapping": { + "first": "#/components/schemas/FirstItem", + "other": "#/components/schemas/OtherItem", + }, + }, + "title": "Item", + } + } + }, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": IsDict( + { + # Pydantic 2.10, in Python 3.8 + # TODO: remove when dropping support for Python 3.8 + "type": "object", + "title": "Response Save Union Body Discriminator Items Post", + } + ) + | IsDict( + { + "type": "object", + "additionalProperties": True, + "title": "Response Save Union Body Discriminator Items Post", + } + ) + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "FirstItem": { + "properties": { + "value": { + "type": "string", + "const": "first", + "title": "Value", + }, + "price": {"type": "integer", "title": "Price"}, + }, + "type": "object", + "required": ["value", "price"], + "title": "FirstItem", + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "OtherItem": { + "properties": { + "value": { + "type": "string", + "const": "other", + "title": "Value", + }, + "price": {"type": "number", "title": "Price"}, + }, + "type": "object", + "required": ["value", "price"], + "title": "OtherItem", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } + ) From 13b067c9b677dc7c2aa692b5576768ee0253c4eb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 8 Oct 2025 14:48:57 +0000 Subject: [PATCH 015/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 37ffd3052..9df0a665d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Fixes + +* 🐛 Fix tagged discriminated union not recognized as body field. PR [#12942](https://github.com/fastapi/fastapi/pull/12942) by [@frankie567](https://github.com/frankie567). + ### Internal * ⬆ Bump astral-sh/setup-uv from 6 to 7. PR [#14167](https://github.com/fastapi/fastapi/pull/14167) by [@dependabot[bot]](https://github.com/apps/dependabot). From 942fce394bd66b841b9399cff815bb8a18cfd4c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 8 Oct 2025 16:49:59 +0200 Subject: [PATCH 016/256] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.11?= =?UTF-8?q?8.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 9df0a665d..6dfc92cbc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.118.2 + ### Fixes * 🐛 Fix tagged discriminated union not recognized as body field. PR [#12942](https://github.com/fastapi/fastapi/pull/12942) by [@frankie567](https://github.com/frankie567). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 12ebbaf9b..cd606969c 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.118.1" +__version__ = "0.118.2" from starlette import status as status From 3611c3fc5b827ad8e131726152865e71afe2b981 Mon Sep 17 00:00:00 2001 From: Sofie Van Landeghem Date: Fri, 10 Oct 2025 11:44:39 +0200 Subject: [PATCH 017/256] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Add=20support=20fo?= =?UTF-8?q?r=20Python=203.14=20(#14165)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 4 ++++ pyproject.toml | 1 + requirements-tests.txt | 2 +- tests/test_tutorial/test_sql_databases/test_tutorial001.py | 2 ++ tests/test_tutorial/test_sql_databases/test_tutorial002.py | 2 ++ 5 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ba69519b3..cbf1a8567 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -48,6 +48,7 @@ jobs: strategy: matrix: python-version: + - "3.14" - "3.13" - "3.12" - "3.11" @@ -55,6 +56,9 @@ jobs: - "3.9" - "3.8" pydantic-version: ["pydantic-v1", "pydantic-v2"] + exclude: + - python-version: "3.14" + pydantic-version: "pydantic-v1" fail-fast: false steps: - name: Dump GitHub context diff --git a/pyproject.toml b/pyproject.toml index 41ef1eb76..cac8059f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Internet :: WWW/HTTP", ] diff --git a/requirements-tests.txt b/requirements-tests.txt index 79aac7e7e..c5de4157e 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -4,7 +4,7 @@ pytest >=7.1.3,<9.0.0 coverage[toml] >= 6.5.0,< 8.0 mypy ==1.14.1 dirty-equals ==0.9.0 -sqlmodel==0.0.25 +sqlmodel==0.0.27 flask >=1.1.2,<4.0.0 anyio[trio] >=3.2.1,<5.0.0 PyJWT==2.9.0 diff --git a/tests/test_tutorial/test_sql_databases/test_tutorial001.py b/tests/test_tutorial/test_sql_databases/test_tutorial001.py index cc7e590df..6604a2fd3 100644 --- a/tests/test_tutorial/test_sql_databases/test_tutorial001.py +++ b/tests/test_tutorial/test_sql_databases/test_tutorial001.py @@ -45,6 +45,8 @@ def get_client(request: pytest.FixtureRequest): with TestClient(mod.app) as c: yield c + # Clean up connection explicitely to avoid resource warning + mod.engine.dispose() def test_crud_app(client: TestClient): diff --git a/tests/test_tutorial/test_sql_databases/test_tutorial002.py b/tests/test_tutorial/test_sql_databases/test_tutorial002.py index 8a98f9a2d..2c4e0988c 100644 --- a/tests/test_tutorial/test_sql_databases/test_tutorial002.py +++ b/tests/test_tutorial/test_sql_databases/test_tutorial002.py @@ -45,6 +45,8 @@ def get_client(request: pytest.FixtureRequest): with TestClient(mod.app) as c: yield c + # Clean up connection explicitely to avoid resource warning + mod.engine.dispose() def test_crud_app(client: TestClient): From 96e7d6eaa47e3ce421477188735a800b2dffa580 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 10 Oct 2025 09:45:05 +0000 Subject: [PATCH 018/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 6dfc92cbc..b6c74e87e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Upgrades + +* ⬆️ Add support for Python 3.14. PR [#14165](https://github.com/fastapi/fastapi/pull/14165) by [@svlandeg](https://github.com/svlandeg). + ## 0.118.2 ### Fixes From 352dbefc634b21cff94663be1ab0a1aa5dbd6ed5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 10 Oct 2025 12:34:39 +0200 Subject: [PATCH 019/256] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.11?= =?UTF-8?q?8.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index b6c74e87e..55a941535 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.118.3 + ### Upgrades * ⬆️ Add support for Python 3.14. PR [#14165](https://github.com/fastapi/fastapi/pull/14165) by [@svlandeg](https://github.com/svlandeg). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index cd606969c..297d993d6 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.118.2" +__version__ = "0.118.3" from starlette import status as status From d34918abf00c93ac592c5d1d1c55650d8c1b92ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 11 Oct 2025 18:45:54 +0200 Subject: [PATCH 020/256] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20`from?= =?UTF-8?q?=20pydantic.v1=20import=20BaseModel`,=20mixed=20Pydantic=20v1?= =?UTF-8?q?=20and=20v2=20models=20in=20the=20same=20app=20(#14168)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- ...migrate-from-pydantic-v1-to-pydantic-v2.md | 133 ++ docs/en/mkdocs.yml | 1 + docs_src/pydantic_v1_in_v2/tutorial001_an.py | 9 + .../pydantic_v1_in_v2/tutorial001_an_py310.py | 7 + docs_src/pydantic_v1_in_v2/tutorial002_an.py | 18 + .../pydantic_v1_in_v2/tutorial002_an_py310.py | 16 + docs_src/pydantic_v1_in_v2/tutorial003_an.py | 25 + .../pydantic_v1_in_v2/tutorial003_an_py310.py | 23 + docs_src/pydantic_v1_in_v2/tutorial004_an.py | 20 + .../pydantic_v1_in_v2/tutorial004_an_py310.py | 19 + .../pydantic_v1_in_v2/tutorial004_an_py39.py | 19 + fastapi/_compat.py | 680 -------- fastapi/_compat/__init__.py | 50 + fastapi/_compat/main.py | 305 ++++ fastapi/_compat/model_field.py | 53 + fastapi/_compat/shared.py | 209 +++ fastapi/_compat/v1.py | 334 ++++ fastapi/_compat/v2.py | 459 +++++ fastapi/datastructures.py | 13 +- fastapi/dependencies/utils.py | 105 +- fastapi/encoders.py | 15 +- fastapi/openapi/utils.py | 23 +- fastapi/routing.py | 6 +- fastapi/temp_pydantic_v1_params.py | 724 ++++++++ fastapi/utils.py | 98 +- tests/test_compat.py | 51 +- tests/test_compat_params_v1.py | 1122 ++++++++++++ ...t_get_model_definitions_formfeed_escape.py | 7 +- ...t_openapi_separate_input_output_schemas.py | 383 ++--- tests/test_pydantic_v1_v2_01.py | 475 ++++++ tests/test_pydantic_v1_v2_list.py | 701 ++++++++ tests/test_pydantic_v1_v2_mixed.py | 1499 +++++++++++++++++ .../test_pydantic_v1_v2_multifile/__init__.py | 0 tests/test_pydantic_v1_v2_multifile/main.py | 142 ++ .../test_pydantic_v1_v2_multifile/modelsv1.py | 19 + .../test_pydantic_v1_v2_multifile/modelsv2.py | 19 + .../modelsv2b.py | 19 + .../test_multifile.py | 1237 ++++++++++++++ tests/test_pydantic_v1_v2_noneable.py | 766 +++++++++ ...est_response_model_as_return_annotation.py | 18 + .../test_pydantic_v1_in_v2/__init__.py | 0 .../test_tutorial001.py | 37 + .../test_tutorial002.py | 140 ++ .../test_tutorial003.py | 154 ++ .../test_tutorial004.py | 153 ++ tests/utils.py | 9 + 46 files changed, 9314 insertions(+), 1001 deletions(-) create mode 100644 docs/en/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md create mode 100644 docs_src/pydantic_v1_in_v2/tutorial001_an.py create mode 100644 docs_src/pydantic_v1_in_v2/tutorial001_an_py310.py create mode 100644 docs_src/pydantic_v1_in_v2/tutorial002_an.py create mode 100644 docs_src/pydantic_v1_in_v2/tutorial002_an_py310.py create mode 100644 docs_src/pydantic_v1_in_v2/tutorial003_an.py create mode 100644 docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py create mode 100644 docs_src/pydantic_v1_in_v2/tutorial004_an.py create mode 100644 docs_src/pydantic_v1_in_v2/tutorial004_an_py310.py create mode 100644 docs_src/pydantic_v1_in_v2/tutorial004_an_py39.py delete mode 100644 fastapi/_compat.py create mode 100644 fastapi/_compat/__init__.py create mode 100644 fastapi/_compat/main.py create mode 100644 fastapi/_compat/model_field.py create mode 100644 fastapi/_compat/shared.py create mode 100644 fastapi/_compat/v1.py create mode 100644 fastapi/_compat/v2.py create mode 100644 fastapi/temp_pydantic_v1_params.py create mode 100644 tests/test_compat_params_v1.py create mode 100644 tests/test_pydantic_v1_v2_01.py create mode 100644 tests/test_pydantic_v1_v2_list.py create mode 100644 tests/test_pydantic_v1_v2_mixed.py create mode 100644 tests/test_pydantic_v1_v2_multifile/__init__.py create mode 100644 tests/test_pydantic_v1_v2_multifile/main.py create mode 100644 tests/test_pydantic_v1_v2_multifile/modelsv1.py create mode 100644 tests/test_pydantic_v1_v2_multifile/modelsv2.py create mode 100644 tests/test_pydantic_v1_v2_multifile/modelsv2b.py create mode 100644 tests/test_pydantic_v1_v2_multifile/test_multifile.py create mode 100644 tests/test_pydantic_v1_v2_noneable.py create mode 100644 tests/test_tutorial/test_pydantic_v1_in_v2/__init__.py create mode 100644 tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial001.py create mode 100644 tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial002.py create mode 100644 tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial003.py create mode 100644 tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial004.py diff --git a/docs/en/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md b/docs/en/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md new file mode 100644 index 000000000..e85d122be --- /dev/null +++ b/docs/en/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md @@ -0,0 +1,133 @@ +# Migrate from Pydantic v1 to Pydantic v2 { #migrate-from-pydantic-v1-to-pydantic-v2 } + +If you have an old FastAPI app, you might be using Pydantic version 1. + +FastAPI has had support for either Pydantic v1 or v2 since version 0.100.0. + +If you had installed Pydantic v2, it would use it. If instead you had Pydantic v1, it would use that. + +Pydantic v1 is now deprecated and support for it will be removed in the next versions of FastAPI, you should **migrate to Pydantic v2**. This way you will get the latest features, improvements, and fixes. + +/// warning + +Also, the Pydantic team stopped support for Pydantic v1 for the latest versions of Python, starting with **Python 3.14**. + +If you want to use the latest features of Python, you will need to make sure you use Pydantic v2. + +/// + +If you have an old FastAPI app with Pydantic v1, here I'll show you how to migrate it to Pydantic v2, and the **new features in FastAPI 0.119.0** to help you with a gradual migration. + +## Official Guide { #official-guide } + +Pydantic has an official Migration Guide from v1 to v2. + +It also includes what has changed, how validations are now more correct and strict, possible caveats, etc. + +You can read it to understand better what has changed. + +## Tests { #tests } + +Make sure you have [tests](../tutorial/testing.md){.internal-link target=_blank} for your app and you run them on continuous integration (CI). + +This way, you can do the upgrade and make sure everything is still working as expected. + +## `bump-pydantic` { #bump-pydantic } + +In many cases, when you use regular Pydantic models without customizations, you will be able to automate most of the process of migrating from Pydantic v1 to Pydantic v2. + +You can use `bump-pydantic` from the same Pydantic team. + +This tool will help you to automatically change most of the code that needs to be changed. + +After this, you can run the tests and check if everything works. If it does, you are done. 😎 + +## Pydantic v1 in v2 { #pydantic-v1-in-v2 } + +Pydantic v2 includes everything from Pydantic v1 as a submodule `pydantic.v1`. + +This means that you can install the latest version of Pydantic v2 and import and use the old Pydantic v1 components from this submodule, as if you had the old Pydantic v1 installed. + +{* ../../docs_src/pydantic_v1_in_v2/tutorial001_an_py310.py hl[1,4] *} + +### FastAPI support for Pydantic v1 in v2 { #fastapi-support-for-pydantic-v1-in-v2 } + +Since FastAPI 0.119.0, there's also partial support for Pydantic v1 from inside of Pydantic v2, to facilitate the migration to v2. + +So, you could upgrade Pydantic to the latest version 2, and change the imports to use the `pydantic.v1` submodule, and in many cases it would just work. + +{* ../../docs_src/pydantic_v1_in_v2/tutorial002_an_py310.py hl[2,5,15] *} + +/// warning + +Have in mind that as the Pydantic team no longer supports Pydantic v1 in recent versions of Python, starting from Python 3.14, using `pydantic.v1` is also not supported in Python 3.14 and above. + +/// + +### Pydantic v1 and v2 on the same app { #pydantic-v1-and-v2-on-the-same-app } + +It's **not supported** by Pydantic to have a model of Pydantic v2 with its own fields defined as Pydantic v1 models or vice versa. + +```mermaid +graph TB + subgraph "❌ Not Supported" + direction TB + subgraph V2["Pydantic v2 Model"] + V1Field["Pydantic v1 Model"] + end + subgraph V1["Pydantic v1 Model"] + V2Field["Pydantic v2 Model"] + end + end + + style V2 fill:#f9fff3 + style V1 fill:#fff6f0 + style V1Field fill:#fff6f0 + style V2Field fill:#f9fff3 +``` + +...but, you can have separated models using Pydantic v1 and v2 in the same app. + +```mermaid +graph TB + subgraph "✅ Supported" + direction TB + subgraph V2["Pydantic v2 Model"] + V2Field["Pydantic v2 Model"] + end + subgraph V1["Pydantic v1 Model"] + V1Field["Pydantic v1 Model"] + end + end + + style V2 fill:#f9fff3 + style V1 fill:#fff6f0 + style V1Field fill:#fff6f0 + style V2Field fill:#f9fff3 +``` + +In some cases, it's even possible to have both Pydantic v1 and v2 models in the same **path operation** in your FastAPI app: + +{* ../../docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py hl[2:3,6,12,21:22] *} + +In this example above, the input model is a Pydantic v1 model, and the output model (defined in `response_model=ItemV2`) is a Pydantic v2 model. + +### Pydantic v1 parameters { #pydantic-v1-parameters } + +If you need to use some of the FastAPI-specific tools for parameters like `Body`, `Query`, `Form`, etc. with Pydantic v1 models, you can import them from `fastapi.temp_pydantic_v1_params` while you finish the migration to Pydantic v2: + +{* ../../docs_src/pydantic_v1_in_v2/tutorial004_an_py310.py hl[4,18] *} + +### Migrate in steps { #migrate-in-steps } + +/// tip + +First try with `bump-pydantic`, if your tests pass and that works, then you're done in one command. ✨ + +/// + +If `bump-pydantic` doesn't work for your use case, you can use the support for both Pydantic v1 and v2 models in the same app to do the migration to Pydantic v2 gradually. + +You could fist upgrade Pydantic to use the latest version 2, and change the imports to use `pydantic.v1` for all your models. + +Then, you can start migrating your models from Pydantic v1 to v2 in groups, in gradual steps. 🚶 diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index e85f31102..323035240 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -201,6 +201,7 @@ nav: - How To - Recipes: - how-to/index.md - how-to/general.md + - how-to/migrate-from-pydantic-v1-to-pydantic-v2.md - how-to/graphql.md - how-to/custom-request-and-route.md - how-to/conditional-openapi.md diff --git a/docs_src/pydantic_v1_in_v2/tutorial001_an.py b/docs_src/pydantic_v1_in_v2/tutorial001_an.py new file mode 100644 index 000000000..62a4b2c21 --- /dev/null +++ b/docs_src/pydantic_v1_in_v2/tutorial001_an.py @@ -0,0 +1,9 @@ +from typing import Union + +from pydantic.v1 import BaseModel + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + size: float diff --git a/docs_src/pydantic_v1_in_v2/tutorial001_an_py310.py b/docs_src/pydantic_v1_in_v2/tutorial001_an_py310.py new file mode 100644 index 000000000..a8ec729b3 --- /dev/null +++ b/docs_src/pydantic_v1_in_v2/tutorial001_an_py310.py @@ -0,0 +1,7 @@ +from pydantic.v1 import BaseModel + + +class Item(BaseModel): + name: str + description: str | None = None + size: float diff --git a/docs_src/pydantic_v1_in_v2/tutorial002_an.py b/docs_src/pydantic_v1_in_v2/tutorial002_an.py new file mode 100644 index 000000000..3c6a06080 --- /dev/null +++ b/docs_src/pydantic_v1_in_v2/tutorial002_an.py @@ -0,0 +1,18 @@ +from typing import Union + +from fastapi import FastAPI +from pydantic.v1 import BaseModel + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + size: float + + +app = FastAPI() + + +@app.post("/items/") +async def create_item(item: Item) -> Item: + return item diff --git a/docs_src/pydantic_v1_in_v2/tutorial002_an_py310.py b/docs_src/pydantic_v1_in_v2/tutorial002_an_py310.py new file mode 100644 index 000000000..4934e7004 --- /dev/null +++ b/docs_src/pydantic_v1_in_v2/tutorial002_an_py310.py @@ -0,0 +1,16 @@ +from fastapi import FastAPI +from pydantic.v1 import BaseModel + + +class Item(BaseModel): + name: str + description: str | None = None + size: float + + +app = FastAPI() + + +@app.post("/items/") +async def create_item(item: Item) -> Item: + return item diff --git a/docs_src/pydantic_v1_in_v2/tutorial003_an.py b/docs_src/pydantic_v1_in_v2/tutorial003_an.py new file mode 100644 index 000000000..117d6f7a4 --- /dev/null +++ b/docs_src/pydantic_v1_in_v2/tutorial003_an.py @@ -0,0 +1,25 @@ +from typing import Union + +from fastapi import FastAPI +from pydantic import BaseModel as BaseModelV2 +from pydantic.v1 import BaseModel + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + size: float + + +class ItemV2(BaseModelV2): + name: str + description: Union[str, None] = None + size: float + + +app = FastAPI() + + +@app.post("/items/", response_model=ItemV2) +async def create_item(item: Item): + return item diff --git a/docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py b/docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py new file mode 100644 index 000000000..6e3013644 --- /dev/null +++ b/docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py @@ -0,0 +1,23 @@ +from fastapi import FastAPI +from pydantic import BaseModel as BaseModelV2 +from pydantic.v1 import BaseModel + + +class Item(BaseModel): + name: str + description: str | None = None + size: float + + +class ItemV2(BaseModelV2): + name: str + description: str | None = None + size: float + + +app = FastAPI() + + +@app.post("/items/", response_model=ItemV2) +async def create_item(item: Item): + return item diff --git a/docs_src/pydantic_v1_in_v2/tutorial004_an.py b/docs_src/pydantic_v1_in_v2/tutorial004_an.py new file mode 100644 index 000000000..cca8a9ea8 --- /dev/null +++ b/docs_src/pydantic_v1_in_v2/tutorial004_an.py @@ -0,0 +1,20 @@ +from typing import Union + +from fastapi import FastAPI +from fastapi.temp_pydantic_v1_params import Body +from pydantic.v1 import BaseModel +from typing_extensions import Annotated + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + size: float + + +app = FastAPI() + + +@app.post("/items/") +async def create_item(item: Annotated[Item, Body(embed=True)]) -> Item: + return item diff --git a/docs_src/pydantic_v1_in_v2/tutorial004_an_py310.py b/docs_src/pydantic_v1_in_v2/tutorial004_an_py310.py new file mode 100644 index 000000000..c251311e0 --- /dev/null +++ b/docs_src/pydantic_v1_in_v2/tutorial004_an_py310.py @@ -0,0 +1,19 @@ +from typing import Annotated + +from fastapi import FastAPI +from fastapi.temp_pydantic_v1_params import Body +from pydantic.v1 import BaseModel + + +class Item(BaseModel): + name: str + description: str | None = None + size: float + + +app = FastAPI() + + +@app.post("/items/") +async def create_item(item: Annotated[Item, Body(embed=True)]) -> Item: + return item diff --git a/docs_src/pydantic_v1_in_v2/tutorial004_an_py39.py b/docs_src/pydantic_v1_in_v2/tutorial004_an_py39.py new file mode 100644 index 000000000..150ab20ae --- /dev/null +++ b/docs_src/pydantic_v1_in_v2/tutorial004_an_py39.py @@ -0,0 +1,19 @@ +from typing import Annotated, Union + +from fastapi import FastAPI +from fastapi.temp_pydantic_v1_params import Body +from pydantic.v1 import BaseModel + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + size: float + + +app = FastAPI() + + +@app.post("/items/") +async def create_item(item: Annotated[Item, Body(embed=True)]) -> Item: + return item diff --git a/fastapi/_compat.py b/fastapi/_compat.py deleted file mode 100644 index 21ea1a237..000000000 --- a/fastapi/_compat.py +++ /dev/null @@ -1,680 +0,0 @@ -import warnings -from collections import deque -from copy import copy -from dataclasses import dataclass, is_dataclass -from enum import Enum -from functools import lru_cache -from typing import ( - Any, - Callable, - Deque, - Dict, - FrozenSet, - List, - Mapping, - Sequence, - Set, - Tuple, - Type, - Union, - cast, -) - -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_VERSION_MINOR_TUPLE = tuple(int(x) for x in PYDANTIC_VERSION.split(".")[:2]) -PYDANTIC_V2 = PYDANTIC_VERSION_MINOR_TUPLE[0] == 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()) - -Url: Type[Any] - -if PYDANTIC_V2: - from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError - from pydantic import TypeAdapter - from pydantic import ValidationError as ValidationError - from pydantic._internal._schema_generation_shared import ( # type: ignore[attr-defined] - GetJsonSchemaHandler as GetJsonSchemaHandler, - ) - from pydantic._internal._typing_extra import eval_type_lenient - from pydantic._internal._utils import lenient_issubclass as lenient_issubclass - from pydantic.fields import FieldInfo - from pydantic.json_schema import GenerateJsonSchema as GenerateJsonSchema - from pydantic.json_schema import JsonSchemaValue as JsonSchemaValue - from pydantic_core import CoreSchema as CoreSchema - from pydantic_core import PydanticUndefined, PydanticUndefinedType - from pydantic_core import Url as Url - - try: - from pydantic_core.core_schema import ( - with_info_plain_validator_function as with_info_plain_validator_function, - ) - except ImportError: # pragma: no cover - from pydantic_core.core_schema import ( - general_plain_validator_function as with_info_plain_validator_function, # noqa: F401 - ) - - RequiredParam = PydanticUndefined - Undefined = PydanticUndefined - UndefinedType = PydanticUndefinedType - evaluate_forwardref = eval_type_lenient - Validator = Any - - class BaseConfig: - pass - - class ErrorWrapper(Exception): - pass - - @dataclass - class ModelField: - field_info: FieldInfo - name: str - mode: Literal["validation", "serialization"] = "validation" - - @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: - with warnings.catch_warnings(): - # Pydantic >= 2.12.0 warns about field specific metadata that is unused - # (e.g. `TypeAdapter(Annotated[int, Field(alias='b')])`). In some cases, we - # end up building the type adapter from a model field annotation so we - # need to ignore the warning: - if PYDANTIC_VERSION_MINOR_TUPLE >= (2, 12): - from pydantic.warnings import UnsupportedFieldAttributeWarning - - warnings.simplefilter( - "ignore", category=UnsupportedFieldAttributeWarning - ) - 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(include_url=False), 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, - field_mapping: Dict[ - Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue - ], - separate_input_output_schemas: bool = True, - ) -> Dict[str, Any]: - override_mode: Union[Literal["validation"], None] = ( - None if separate_input_output_schemas else "validation" - ) - # This expects that GenerateJsonSchema was already used to generate the definitions - json_schema = field_mapping[(field, override_mode or field.mode)] - 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, - separate_input_output_schemas: bool = True, - ) -> Tuple[ - Dict[ - Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue - ], - Dict[str, Dict[str, Any]], - ]: - override_mode: Union[Literal["validation"], None] = ( - None if separate_input_output_schemas else "validation" - ) - inputs = [ - (field, override_mode or field.mode, field._type_adapter.core_schema) - for field in fields - ] - field_mapping, definitions = schema_generator.generate_definitions( - inputs=inputs - ) - for item_def in cast(Dict[str, Dict[str, Any]], definitions).values(): - if "description" in item_def: - item_description = cast(str, item_def["description"]).split("\f")[0] - item_def["description"] = item_description - return field_mapping, 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: - cls = type(field_info) - merged_field_info = cls.from_annotation(annotation) - new_field_info = copy(field_info) - new_field_info.metadata = merged_field_info.metadata - new_field_info.annotation = merged_field_info.annotation - return new_field_info - - 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(include_url=False)[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 - - def get_model_fields(model: Type[BaseModel]) -> List[ModelField]: - return [ - ModelField(field_info=field_info, name=name) - for name, field_info in model.model_fields.items() - ] - -else: - 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 - ) - - # Keeping old "Required" functionality from Pydantic V1, without - # shadowing typing.Required. - RequiredParam: Any = Ellipsis # type: ignore[no-redef] - 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.schema import ( - field_schema, - get_flat_models_from_fields, - get_model_name_map, - model_process_schema, - ) - from pydantic.schema import ( # type: ignore[no-redef] # noqa: F401 - get_annotation_from_field_info as get_annotation_from_field_info, - ) - from pydantic.typing import ( # type: ignore[no-redef] - evaluate_forwardref as evaluate_forwardref, # noqa: F401 - ) - from pydantic.utils import ( # type: ignore[no-redef] - lenient_issubclass as lenient_issubclass, # noqa: F401 - ) - - GetJsonSchemaHandler = Any # type: ignore[assignment,misc] - JsonSchemaValue = Dict[str, Any] # type: ignore[misc] - CoreSchema = Any # type: ignore[assignment,misc] - - sequence_shapes = { - SHAPE_LIST, - SHAPE_SET, - SHAPE_FROZENSET, - SHAPE_TUPLE, - SHAPE_SEQUENCE, - SHAPE_TUPLE_ELLIPSIS, - } - sequence_shape_to_type = { - SHAPE_LIST: list, - SHAPE_SET: set, - SHAPE_TUPLE: tuple, - SHAPE_SEQUENCE: list, - SHAPE_TUPLE_ELLIPSIS: list, - } - - @dataclass - class GenerateJsonSchema: # type: ignore[no-redef] - ref_template: str - - class PydanticSchemaGenerationError(Exception): # type: ignore[no-redef] - pass - - def with_info_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] - definitions[model_name] = m_schema - for m_schema in definitions.values(): - if "description" in m_schema: - m_schema["description"] = m_schema["description"].split("\f")[0] - 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, - field_mapping: Dict[ - Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue - ], - separate_input_output_schemas: bool = True, - ) -> 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, - separate_input_output_schemas: bool = True, - ) -> Tuple[ - Dict[ - Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue - ], - 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 get_model_fields(model: Type[BaseModel]) -> List[ModelField]: - return list(model.__fields__.values()) # type: ignore[attr-defined] - - -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: - origin = get_origin(annotation) - if origin is Union or origin is UnionType: - for arg in get_args(annotation): - if field_annotation_is_sequence(arg): - return True - return False - return _annotation_is_sequence(annotation) or _annotation_is_sequence( - 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)) - - if origin is Annotated: - return field_annotation_is_complex(get_args(annotation)[0]) - - 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) - ) - - -@lru_cache -def get_cached_model_fields(model: Type[BaseModel]) -> List[ModelField]: - return get_model_fields(model) diff --git a/fastapi/_compat/__init__.py b/fastapi/_compat/__init__.py new file mode 100644 index 000000000..b2ae5adc7 --- /dev/null +++ b/fastapi/_compat/__init__.py @@ -0,0 +1,50 @@ +from .main import BaseConfig as BaseConfig +from .main import PydanticSchemaGenerationError as PydanticSchemaGenerationError +from .main import RequiredParam as RequiredParam +from .main import Undefined as Undefined +from .main import UndefinedType as UndefinedType +from .main import Url as Url +from .main import Validator as Validator +from .main import _get_model_config as _get_model_config +from .main import _is_error_wrapper as _is_error_wrapper +from .main import _is_model_class as _is_model_class +from .main import _is_model_field as _is_model_field +from .main import _is_undefined as _is_undefined +from .main import _model_dump as _model_dump +from .main import _model_rebuild as _model_rebuild +from .main import copy_field_info as copy_field_info +from .main import create_body_model as create_body_model +from .main import evaluate_forwardref as evaluate_forwardref +from .main import get_annotation_from_field_info as get_annotation_from_field_info +from .main import get_cached_model_fields as get_cached_model_fields +from .main import get_compat_model_name_map as get_compat_model_name_map +from .main import get_definitions as get_definitions +from .main import get_missing_field_error as get_missing_field_error +from .main import get_schema_from_model_field as get_schema_from_model_field +from .main import is_bytes_field as is_bytes_field +from .main import is_bytes_sequence_field as is_bytes_sequence_field +from .main import is_scalar_field as is_scalar_field +from .main import is_scalar_sequence_field as is_scalar_sequence_field +from .main import is_sequence_field as is_sequence_field +from .main import serialize_sequence_value as serialize_sequence_value +from .main import ( + with_info_plain_validator_function as with_info_plain_validator_function, +) +from .model_field import ModelField as ModelField +from .shared import PYDANTIC_V2 as PYDANTIC_V2 +from .shared import PYDANTIC_VERSION_MINOR_TUPLE as PYDANTIC_VERSION_MINOR_TUPLE +from .shared import annotation_is_pydantic_v1 as annotation_is_pydantic_v1 +from .shared import field_annotation_is_scalar as field_annotation_is_scalar +from .shared import ( + is_uploadfile_or_nonable_uploadfile_annotation as is_uploadfile_or_nonable_uploadfile_annotation, +) +from .shared import ( + is_uploadfile_sequence_annotation as is_uploadfile_sequence_annotation, +) +from .shared import lenient_issubclass as lenient_issubclass +from .shared import sequence_types as sequence_types +from .shared import value_is_sequence as value_is_sequence +from .v1 import CoreSchema as CoreSchema +from .v1 import GetJsonSchemaHandler as GetJsonSchemaHandler +from .v1 import JsonSchemaValue as JsonSchemaValue +from .v1 import _normalize_errors as _normalize_errors diff --git a/fastapi/_compat/main.py b/fastapi/_compat/main.py new file mode 100644 index 000000000..3f758f072 --- /dev/null +++ b/fastapi/_compat/main.py @@ -0,0 +1,305 @@ +from functools import lru_cache +from typing import ( + Any, + Dict, + List, + Sequence, + Tuple, + Type, +) + +from fastapi._compat import v1 +from fastapi._compat.shared import PYDANTIC_V2, lenient_issubclass +from fastapi.types import ModelNameMap +from pydantic import BaseModel +from typing_extensions import Literal + +from .model_field import ModelField + +if PYDANTIC_V2: + from .v2 import BaseConfig as BaseConfig + from .v2 import FieldInfo as FieldInfo + from .v2 import PydanticSchemaGenerationError as PydanticSchemaGenerationError + from .v2 import RequiredParam as RequiredParam + from .v2 import Undefined as Undefined + from .v2 import UndefinedType as UndefinedType + from .v2 import Url as Url + from .v2 import Validator as Validator + from .v2 import evaluate_forwardref as evaluate_forwardref + from .v2 import get_missing_field_error as get_missing_field_error + from .v2 import ( + with_info_plain_validator_function as with_info_plain_validator_function, + ) +else: + from .v1 import BaseConfig as BaseConfig # type: ignore[assignment] + from .v1 import FieldInfo as FieldInfo + from .v1 import ( # type: ignore[assignment] + PydanticSchemaGenerationError as PydanticSchemaGenerationError, + ) + from .v1 import RequiredParam as RequiredParam + from .v1 import Undefined as Undefined + from .v1 import UndefinedType as UndefinedType + from .v1 import Url as Url # type: ignore[assignment] + from .v1 import Validator as Validator + from .v1 import evaluate_forwardref as evaluate_forwardref + from .v1 import get_missing_field_error as get_missing_field_error + from .v1 import ( # type: ignore[assignment] + with_info_plain_validator_function as with_info_plain_validator_function, + ) + + +@lru_cache +def get_cached_model_fields(model: Type[BaseModel]) -> List[ModelField]: + if lenient_issubclass(model, v1.BaseModel): + return v1.get_model_fields(model) + else: + from . import v2 + + return v2.get_model_fields(model) # type: ignore[return-value] + + +def _is_undefined(value: object) -> bool: + if isinstance(value, v1.UndefinedType): + return True + elif PYDANTIC_V2: + from . import v2 + + return isinstance(value, v2.UndefinedType) + return False + + +def _get_model_config(model: BaseModel) -> Any: + if isinstance(model, v1.BaseModel): + return v1._get_model_config(model) + elif PYDANTIC_V2: + from . import v2 + + return v2._get_model_config(model) + + +def _model_dump( + model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any +) -> Any: + if isinstance(model, v1.BaseModel): + return v1._model_dump(model, mode=mode, **kwargs) + elif PYDANTIC_V2: + from . import v2 + + return v2._model_dump(model, mode=mode, **kwargs) + + +def _is_error_wrapper(exc: Exception) -> bool: + if isinstance(exc, v1.ErrorWrapper): + return True + elif PYDANTIC_V2: + from . import v2 + + return isinstance(exc, v2.ErrorWrapper) + return False + + +def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: + if isinstance(field_info, v1.FieldInfo): + return v1.copy_field_info(field_info=field_info, annotation=annotation) + else: + assert PYDANTIC_V2 + from . import v2 + + return v2.copy_field_info(field_info=field_info, annotation=annotation) + + +def create_body_model( + *, fields: Sequence[ModelField], model_name: str +) -> Type[BaseModel]: + if fields and isinstance(fields[0], v1.ModelField): + return v1.create_body_model(fields=fields, model_name=model_name) + else: + assert PYDANTIC_V2 + from . import v2 + + return v2.create_body_model(fields=fields, model_name=model_name) # type: ignore[arg-type] + + +def get_annotation_from_field_info( + annotation: Any, field_info: FieldInfo, field_name: str +) -> Any: + if isinstance(field_info, v1.FieldInfo): + return v1.get_annotation_from_field_info( + annotation=annotation, field_info=field_info, field_name=field_name + ) + else: + assert PYDANTIC_V2 + from . import v2 + + return v2.get_annotation_from_field_info( + annotation=annotation, field_info=field_info, field_name=field_name + ) + + +def is_bytes_field(field: ModelField) -> bool: + if isinstance(field, v1.ModelField): + return v1.is_bytes_field(field) + else: + assert PYDANTIC_V2 + from . import v2 + + return v2.is_bytes_field(field) # type: ignore[arg-type] + + +def is_bytes_sequence_field(field: ModelField) -> bool: + if isinstance(field, v1.ModelField): + return v1.is_bytes_sequence_field(field) + else: + assert PYDANTIC_V2 + from . import v2 + + return v2.is_bytes_sequence_field(field) # type: ignore[arg-type] + + +def is_scalar_field(field: ModelField) -> bool: + if isinstance(field, v1.ModelField): + return v1.is_scalar_field(field) + else: + assert PYDANTIC_V2 + from . import v2 + + return v2.is_scalar_field(field) # type: ignore[arg-type] + + +def is_scalar_sequence_field(field: ModelField) -> bool: + if isinstance(field, v1.ModelField): + return v1.is_scalar_sequence_field(field) + else: + assert PYDANTIC_V2 + from . import v2 + + return v2.is_scalar_sequence_field(field) # type: ignore[arg-type] + + +def is_sequence_field(field: ModelField) -> bool: + if isinstance(field, v1.ModelField): + return v1.is_sequence_field(field) + else: + assert PYDANTIC_V2 + from . import v2 + + return v2.is_sequence_field(field) # type: ignore[arg-type] + + +def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: + if isinstance(field, v1.ModelField): + return v1.serialize_sequence_value(field=field, value=value) + else: + assert PYDANTIC_V2 + from . import v2 + + return v2.serialize_sequence_value(field=field, value=value) # type: ignore[arg-type] + + +def _model_rebuild(model: Type[BaseModel]) -> None: + if lenient_issubclass(model, v1.BaseModel): + v1._model_rebuild(model) + elif PYDANTIC_V2: + from . import v2 + + v2._model_rebuild(model) + + +def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap: + v1_model_fields = [field for field in fields if isinstance(field, v1.ModelField)] + v1_flat_models = v1.get_flat_models_from_fields(v1_model_fields, known_models=set()) # type: ignore[attr-defined] + all_flat_models = v1_flat_models + if PYDANTIC_V2: + from . import v2 + + v2_model_fields = [ + field for field in fields if isinstance(field, v2.ModelField) + ] + v2_flat_models = v2.get_flat_models_from_fields( + v2_model_fields, known_models=set() + ) + all_flat_models = all_flat_models.union(v2_flat_models) + + model_name_map = v2.get_model_name_map(all_flat_models) + return model_name_map + model_name_map = v1.get_model_name_map(all_flat_models) + return model_name_map + + +def get_definitions( + *, + fields: List[ModelField], + model_name_map: ModelNameMap, + separate_input_output_schemas: bool = True, +) -> Tuple[ + Dict[Tuple[ModelField, Literal["validation", "serialization"]], v1.JsonSchemaValue], + Dict[str, Dict[str, Any]], +]: + v1_fields = [field for field in fields if isinstance(field, v1.ModelField)] + v1_field_maps, v1_definitions = v1.get_definitions( + fields=v1_fields, + model_name_map=model_name_map, + separate_input_output_schemas=separate_input_output_schemas, + ) + if not PYDANTIC_V2: + return v1_field_maps, v1_definitions + else: + from . import v2 + + v2_fields = [field for field in fields if isinstance(field, v2.ModelField)] + v2_field_maps, v2_definitions = v2.get_definitions( + fields=v2_fields, + model_name_map=model_name_map, + separate_input_output_schemas=separate_input_output_schemas, + ) + all_definitions = {**v1_definitions, **v2_definitions} + all_field_maps = {**v1_field_maps, **v2_field_maps} + return all_field_maps, all_definitions + + +def get_schema_from_model_field( + *, + field: ModelField, + model_name_map: ModelNameMap, + field_mapping: Dict[ + Tuple[ModelField, Literal["validation", "serialization"]], v1.JsonSchemaValue + ], + separate_input_output_schemas: bool = True, +) -> Dict[str, Any]: + if isinstance(field, v1.ModelField): + return v1.get_schema_from_model_field( + field=field, + model_name_map=model_name_map, + field_mapping=field_mapping, + separate_input_output_schemas=separate_input_output_schemas, + ) + else: + assert PYDANTIC_V2 + from . import v2 + + return v2.get_schema_from_model_field( + field=field, # type: ignore[arg-type] + model_name_map=model_name_map, + field_mapping=field_mapping, # type: ignore[arg-type] + separate_input_output_schemas=separate_input_output_schemas, + ) + + +def _is_model_field(value: Any) -> bool: + if isinstance(value, v1.ModelField): + return True + elif PYDANTIC_V2: + from . import v2 + + return isinstance(value, v2.ModelField) + return False + + +def _is_model_class(value: Any) -> bool: + if lenient_issubclass(value, v1.BaseModel): + return True + elif PYDANTIC_V2: + from . import v2 + + return lenient_issubclass(value, v2.BaseModel) # type: ignore[attr-defined] + return False diff --git a/fastapi/_compat/model_field.py b/fastapi/_compat/model_field.py new file mode 100644 index 000000000..fa2008c5e --- /dev/null +++ b/fastapi/_compat/model_field.py @@ -0,0 +1,53 @@ +from typing import ( + Any, + Dict, + List, + Tuple, + Union, +) + +from fastapi.types import IncEx +from pydantic.fields import FieldInfo +from typing_extensions import Literal, Protocol + + +class ModelField(Protocol): + field_info: "FieldInfo" + name: str + mode: Literal["validation", "serialization"] = "validation" + _version: Literal["v1", "v2"] = "v1" + + @property + def alias(self) -> str: ... + + @property + def required(self) -> bool: ... + + @property + def default(self) -> Any: ... + + @property + def type_(self) -> Any: ... + + def get_default(self) -> Any: ... + + def validate( + self, + value: Any, + values: Dict[str, Any] = {}, # noqa: B006 + *, + loc: Tuple[Union[int, str], ...] = (), + ) -> Tuple[Any, Union[List[Dict[str, Any]], None]]: ... + + 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: ... diff --git a/fastapi/_compat/shared.py b/fastapi/_compat/shared.py new file mode 100644 index 000000000..495d5c5f7 --- /dev/null +++ b/fastapi/_compat/shared.py @@ -0,0 +1,209 @@ +import sys +import types +import typing +from collections import deque +from dataclasses import is_dataclass +from typing import ( + Any, + Deque, + FrozenSet, + List, + Mapping, + Sequence, + Set, + Tuple, + Type, + Union, +) + +from fastapi._compat import v1 +from fastapi.types import UnionType +from pydantic import BaseModel +from pydantic.version import VERSION as PYDANTIC_VERSION +from starlette.datastructures import UploadFile +from typing_extensions import Annotated, get_args, get_origin + +# Copy from Pydantic v2, compatible with v1 +if sys.version_info < (3, 9): + # Pydantic no longer supports Python 3.8, this might be incorrect, but the code + # this is used for is also never reached in this codebase, as it's a copy of + # Pydantic's lenient_issubclass, just for compatibility with v1 + # TODO: remove when dropping support for Python 3.8 + WithArgsTypes: Tuple[Any, ...] = () +elif sys.version_info < (3, 10): + WithArgsTypes: tuple[Any, ...] = (typing._GenericAlias, types.GenericAlias) # type: ignore[attr-defined] +else: + WithArgsTypes: tuple[Any, ...] = ( + typing._GenericAlias, # type: ignore[attr-defined] + types.GenericAlias, + types.UnionType, + ) # pyright: ignore[reportAttributeAccessIssue] + +PYDANTIC_VERSION_MINOR_TUPLE = tuple(int(x) for x in PYDANTIC_VERSION.split(".")[:2]) +PYDANTIC_V2 = PYDANTIC_VERSION_MINOR_TUPLE[0] == 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()) + +Url: Type[Any] + + +# Copy of Pydantic v2, compatible with v1 +def lenient_issubclass( + cls: Any, class_or_tuple: Union[Type[Any], Tuple[Type[Any], ...], None] +) -> bool: + try: + return isinstance(cls, type) and issubclass(cls, class_or_tuple) # type: ignore[arg-type] + except TypeError: # pragma: no cover + if isinstance(cls, WithArgsTypes): + return False + raise # pragma: no cover + + +def _annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool: + if lenient_issubclass(annotation, (str, bytes)): + return False + return lenient_issubclass(annotation, sequence_types) # type: ignore[arg-type] + + +def field_annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool: + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + for arg in get_args(annotation): + if field_annotation_is_sequence(arg): + return True + return False + return _annotation_is_sequence(annotation) or _annotation_is_sequence( + 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, v1.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)) + + if origin is Annotated: + return field_annotation_is_complex(get_args(annotation)[0]) + + 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) + ) + + +def annotation_is_pydantic_v1(annotation: Any) -> bool: + if lenient_issubclass(annotation, v1.BaseModel): + return True + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + for arg in get_args(annotation): + if lenient_issubclass(arg, v1.BaseModel): + return True + if field_annotation_is_sequence(annotation): + for sub_annotation in get_args(annotation): + if annotation_is_pydantic_v1(sub_annotation): + return True + return False diff --git a/fastapi/_compat/v1.py b/fastapi/_compat/v1.py new file mode 100644 index 000000000..f0ac51634 --- /dev/null +++ b/fastapi/_compat/v1.py @@ -0,0 +1,334 @@ +from copy import copy +from dataclasses import dataclass, is_dataclass +from enum import Enum +from typing import ( + Any, + Callable, + Dict, + List, + Sequence, + Set, + Tuple, + Type, + Union, +) + +from fastapi._compat import shared +from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX +from fastapi.types import ModelNameMap +from pydantic.version import VERSION as PYDANTIC_VERSION +from typing_extensions import Literal + +PYDANTIC_VERSION_MINOR_TUPLE = tuple(int(x) for x in PYDANTIC_VERSION.split(".")[:2]) +PYDANTIC_V2 = PYDANTIC_VERSION_MINOR_TUPLE[0] == 2 +# Keeping old "Required" functionality from Pydantic V1, without +# shadowing typing.Required. +RequiredParam: Any = Ellipsis + +if not PYDANTIC_V2: + from pydantic import BaseConfig as BaseConfig + from pydantic import BaseModel as BaseModel + from pydantic import ValidationError as ValidationError + from pydantic import create_model as create_model + from pydantic.class_validators import Validator as Validator + from pydantic.color import Color as Color + from pydantic.error_wrappers import ErrorWrapper as ErrorWrapper + 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 ModelField as ModelField # type: ignore[attr-defined] + from pydantic.fields import Undefined as Undefined # type: ignore[attr-defined] + from pydantic.fields import ( # type: ignore[attr-defined] + UndefinedType as UndefinedType, + ) + from pydantic.networks import AnyUrl as AnyUrl + from pydantic.networks import NameEmail as NameEmail + from pydantic.schema import TypeModelSet as TypeModelSet + from pydantic.schema import ( + field_schema, + get_flat_models_from_fields, + model_process_schema, + ) + from pydantic.schema import ( + get_annotation_from_field_info as get_annotation_from_field_info, + ) + from pydantic.schema import get_flat_models_from_field as get_flat_models_from_field + from pydantic.schema import get_model_name_map as get_model_name_map + from pydantic.types import SecretBytes as SecretBytes + from pydantic.types import SecretStr as SecretStr + from pydantic.typing import evaluate_forwardref as evaluate_forwardref + from pydantic.utils import lenient_issubclass as lenient_issubclass + + +else: + from pydantic.v1 import BaseConfig as BaseConfig # type: ignore[assignment] + from pydantic.v1 import BaseModel as BaseModel # type: ignore[assignment] + from pydantic.v1 import ( # type: ignore[assignment] + ValidationError as ValidationError, + ) + from pydantic.v1 import create_model as create_model # type: ignore[no-redef] + from pydantic.v1.class_validators import Validator as Validator + from pydantic.v1.color import Color as Color # type: ignore[assignment] + from pydantic.v1.error_wrappers import ErrorWrapper as ErrorWrapper + from pydantic.v1.errors import MissingError + from pydantic.v1.fields import ( + SHAPE_FROZENSET, + SHAPE_LIST, + SHAPE_SEQUENCE, + SHAPE_SET, + SHAPE_SINGLETON, + SHAPE_TUPLE, + SHAPE_TUPLE_ELLIPSIS, + ) + from pydantic.v1.fields import FieldInfo as FieldInfo # type: ignore[assignment] + from pydantic.v1.fields import ModelField as ModelField + from pydantic.v1.fields import Undefined as Undefined + from pydantic.v1.fields import UndefinedType as UndefinedType + from pydantic.v1.networks import AnyUrl as AnyUrl + from pydantic.v1.networks import ( # type: ignore[assignment] + NameEmail as NameEmail, + ) + from pydantic.v1.schema import TypeModelSet as TypeModelSet + from pydantic.v1.schema import ( + field_schema, + get_flat_models_from_fields, + model_process_schema, + ) + from pydantic.v1.schema import ( + get_annotation_from_field_info as get_annotation_from_field_info, + ) + from pydantic.v1.schema import ( + get_flat_models_from_field as get_flat_models_from_field, + ) + from pydantic.v1.schema import get_model_name_map as get_model_name_map + from pydantic.v1.types import ( # type: ignore[assignment] + SecretBytes as SecretBytes, + ) + from pydantic.v1.types import ( # type: ignore[assignment] + SecretStr as SecretStr, + ) + from pydantic.v1.typing import evaluate_forwardref as evaluate_forwardref + from pydantic.v1.utils import lenient_issubclass as lenient_issubclass + + +GetJsonSchemaHandler = Any +JsonSchemaValue = Dict[str, Any] +CoreSchema = Any +Url = AnyUrl + +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: + ref_template: str + + +class PydanticSchemaGenerationError(Exception): + pass + + +RequestErrorModel: Type[BaseModel] = create_model("Request") + + +def with_info_plain_validator_function( + 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] + definitions[model_name] = m_schema + for m_schema in definitions.values(): + if "description" in m_schema: + m_schema["description"] = m_schema["description"].split("\f")[0] + 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 + and not lenient_issubclass(field.type_, BaseModel) + and not lenient_issubclass(field.type_, dict) + and not shared.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: + if not all(is_pv1_scalar_field(f) for f in field.sub_fields): + return False + return True + + +def is_pv1_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_pv1_scalar_field(sub_field): + return False + return True + if shared._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 _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 _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, + model_name_map: ModelNameMap, + field_mapping: Dict[ + Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + ], + separate_input_output_schemas: bool = True, +) -> Dict[str, Any]: + 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], + model_name_map: ModelNameMap, + separate_input_output_schemas: bool = True, +) -> Tuple[ + Dict[Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue], + 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 shared._annotation_is_sequence(field.type_) + + +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) # type: ignore[no-any-return] + + +def is_bytes_sequence_field(field: ModelField) -> bool: + return field.shape in sequence_shapes and lenient_issubclass(field.type_, bytes) + + +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] + + +def get_missing_field_error(loc: Tuple[str, ...]) -> Dict[str, Any]: + missing_field_error = ErrorWrapper(MissingError(), loc=loc) + 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 get_model_fields(model: Type[BaseModel]) -> List[ModelField]: + return list(model.__fields__.values()) # type: ignore[attr-defined] diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py new file mode 100644 index 000000000..29606b9f3 --- /dev/null +++ b/fastapi/_compat/v2.py @@ -0,0 +1,459 @@ +import re +import warnings +from copy import copy, deepcopy +from dataclasses import dataclass +from enum import Enum +from typing import ( + Any, + Dict, + List, + Sequence, + Set, + Tuple, + Type, + Union, + cast, +) + +from fastapi._compat import shared, v1 +from fastapi.openapi.constants import REF_TEMPLATE +from fastapi.types import IncEx, ModelNameMap +from pydantic import BaseModel, TypeAdapter, create_model +from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError +from pydantic import PydanticUndefinedAnnotation as PydanticUndefinedAnnotation +from pydantic import ValidationError as ValidationError +from pydantic._internal._schema_generation_shared import ( # type: ignore[attr-defined] + GetJsonSchemaHandler as GetJsonSchemaHandler, +) +from pydantic._internal._typing_extra import eval_type_lenient +from pydantic._internal._utils import lenient_issubclass as lenient_issubclass +from pydantic.fields import FieldInfo as 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 PydanticUndefined, PydanticUndefinedType +from pydantic_core import Url as Url +from typing_extensions import Annotated, Literal, get_args, get_origin + +try: + from pydantic_core.core_schema import ( + with_info_plain_validator_function as with_info_plain_validator_function, + ) +except ImportError: # pragma: no cover + from pydantic_core.core_schema import ( + general_plain_validator_function as with_info_plain_validator_function, # noqa: F401 + ) + +RequiredParam = PydanticUndefined +Undefined = PydanticUndefined +UndefinedType = PydanticUndefinedType +evaluate_forwardref = eval_type_lenient +Validator = Any + + +class BaseConfig: + pass + + +class ErrorWrapper(Exception): + pass + + +@dataclass +class ModelField: + field_info: FieldInfo + name: str + mode: Literal["validation", "serialization"] = "validation" + + @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: + with warnings.catch_warnings(): + # Pydantic >= 2.12.0 warns about field specific metadata that is unused + # (e.g. `TypeAdapter(Annotated[int, Field(alias='b')])`). In some cases, we + # end up building the type adapter from a model field annotation so we + # need to ignore the warning: + if shared.PYDANTIC_VERSION_MINOR_TUPLE >= (2, 12): + from pydantic.warnings import UnsupportedFieldAttributeWarning + + warnings.simplefilter( + "ignore", category=UnsupportedFieldAttributeWarning + ) + 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, v1._regenerate_error_with_loc( + errors=exc.errors(include_url=False), 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 _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, + model_name_map: ModelNameMap, + field_mapping: Dict[ + Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + ], + separate_input_output_schemas: bool = True, +) -> Dict[str, Any]: + override_mode: Union[Literal["validation"], None] = ( + None if separate_input_output_schemas else "validation" + ) + # This expects that GenerateJsonSchema was already used to generate the definitions + json_schema = field_mapping[(field, override_mode or field.mode)] + 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_definitions( + *, + fields: Sequence[ModelField], + model_name_map: ModelNameMap, + separate_input_output_schemas: bool = True, +) -> Tuple[ + Dict[Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue], + Dict[str, Dict[str, Any]], +]: + schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE) + override_mode: Union[Literal["validation"], None] = ( + None if separate_input_output_schemas else "validation" + ) + flat_models = get_flat_models_from_fields(fields, known_models=set()) + flat_model_fields = [ + ModelField(field_info=FieldInfo(annotation=model), name=model.__name__) + for model in flat_models + ] + input_types = {f.type_ for f in fields} + unique_flat_model_fields = { + f for f in flat_model_fields if f.type_ not in input_types + } + + inputs = [ + (field, override_mode or field.mode, field._type_adapter.core_schema) + for field in list(fields) + list(unique_flat_model_fields) + ] + field_mapping, definitions = schema_generator.generate_definitions(inputs=inputs) + for item_def in cast(Dict[str, Dict[str, Any]], definitions).values(): + if "description" in item_def: + item_description = cast(str, item_def["description"]).split("\f")[0] + item_def["description"] = item_description + new_mapping, new_definitions = _remap_definitions_and_field_mappings( + model_name_map=model_name_map, + definitions=definitions, # type: ignore[arg-type] + field_mapping=field_mapping, + ) + return new_mapping, new_definitions + + +def _replace_refs( + *, + schema: Dict[str, Any], + old_name_to_new_name_map: Dict[str, str], +) -> Dict[str, Any]: + new_schema = deepcopy(schema) + for key, value in new_schema.items(): + if key == "$ref": + ref_name = schema["$ref"].split("/")[-1] + if ref_name in old_name_to_new_name_map: + new_name = old_name_to_new_name_map[ref_name] + new_schema["$ref"] = REF_TEMPLATE.format(model=new_name) + else: + new_schema["$ref"] = schema["$ref"] + continue + if isinstance(value, dict): + new_schema[key] = _replace_refs( + schema=value, + old_name_to_new_name_map=old_name_to_new_name_map, + ) + elif isinstance(value, list): + new_value = [] + for item in value: + if isinstance(item, dict): + new_item = _replace_refs( + schema=item, + old_name_to_new_name_map=old_name_to_new_name_map, + ) + new_value.append(new_item) + + else: + new_value.append(item) + new_schema[key] = new_value + return new_schema + + +def _remap_definitions_and_field_mappings( + *, + model_name_map: ModelNameMap, + definitions: Dict[str, Any], + field_mapping: Dict[ + Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + ], +) -> Tuple[ + Dict[Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue], + Dict[str, Any], +]: + old_name_to_new_name_map = {} + for field_key, schema in field_mapping.items(): + model = field_key[0].type_ + if model not in model_name_map: + continue + new_name = model_name_map[model] + old_name = schema["$ref"].split("/")[-1] + if old_name in {f"{new_name}-Input", f"{new_name}-Output"}: + continue + old_name_to_new_name_map[old_name] = new_name + + new_field_mapping: Dict[ + Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + ] = {} + for field_key, schema in field_mapping.items(): + new_schema = _replace_refs( + schema=schema, + old_name_to_new_name_map=old_name_to_new_name_map, + ) + new_field_mapping[field_key] = new_schema + + new_definitions = {} + for key, value in definitions.items(): + if key in old_name_to_new_name_map: + new_key = old_name_to_new_name_map[key] + else: + new_key = key + new_value = _replace_refs( + schema=value, + old_name_to_new_name_map=old_name_to_new_name_map, + ) + new_definitions[new_key] = new_value + return new_field_mapping, new_definitions + + +def is_scalar_field(field: ModelField) -> bool: + from fastapi import params + + return shared.field_annotation_is_scalar( + field.field_info.annotation + ) and not isinstance(field.field_info, params.Body) + + +def is_sequence_field(field: ModelField) -> bool: + return shared.field_annotation_is_sequence(field.field_info.annotation) + + +def is_scalar_sequence_field(field: ModelField) -> bool: + return shared.field_annotation_is_scalar_sequence(field.field_info.annotation) + + +def is_bytes_field(field: ModelField) -> bool: + return shared.is_bytes_or_nonable_bytes_annotation(field.type_) + + +def is_bytes_sequence_field(field: ModelField) -> bool: + return shared.is_bytes_sequence_annotation(field.type_) + + +def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: + cls = type(field_info) + merged_field_info = cls.from_annotation(annotation) + new_field_info = copy(field_info) + new_field_info.metadata = merged_field_info.metadata + new_field_info.annotation = merged_field_info.annotation + return new_field_info + + +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, shared.sequence_types) # type: ignore[arg-type] + return shared.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(include_url=False)[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 + + +def get_model_fields(model: Type[BaseModel]) -> List[ModelField]: + return [ + ModelField(field_info=field_info, name=name) + for name, field_info in model.model_fields.items() + ] + + +# Duplicate of several schema functions from Pydantic v1 to make them compatible with +# Pydantic v2 and allow mixing the models + +TypeModelOrEnum = Union[Type["BaseModel"], Type[Enum]] +TypeModelSet = Set[TypeModelOrEnum] + + +def normalize_name(name: str) -> str: + return re.sub(r"[^a-zA-Z0-9.\-_]", "_", name) + + +def get_model_name_map(unique_models: TypeModelSet) -> Dict[TypeModelOrEnum, str]: + name_model_map = {} + conflicting_names: Set[str] = set() + for model in unique_models: + model_name = normalize_name(model.__name__) + if model_name in conflicting_names: + model_name = get_long_model_name(model) + name_model_map[model_name] = model + elif model_name in name_model_map: + conflicting_names.add(model_name) + conflicting_model = name_model_map.pop(model_name) + name_model_map[get_long_model_name(conflicting_model)] = conflicting_model + name_model_map[get_long_model_name(model)] = model + else: + name_model_map[model_name] = model + return {v: k for k, v in name_model_map.items()} + + +def get_flat_models_from_model( + model: Type["BaseModel"], known_models: Union[TypeModelSet, None] = None +) -> TypeModelSet: + known_models = known_models or set() + fields = get_model_fields(model) + get_flat_models_from_fields(fields, known_models=known_models) + return known_models + + +def get_flat_models_from_annotation( + annotation: Any, known_models: TypeModelSet +) -> TypeModelSet: + origin = get_origin(annotation) + if origin is not None: + for arg in get_args(annotation): + if lenient_issubclass(arg, (BaseModel, Enum)) and arg not in known_models: + known_models.add(arg) + if lenient_issubclass(arg, BaseModel): + get_flat_models_from_model(arg, known_models=known_models) + else: + get_flat_models_from_annotation(arg, known_models=known_models) + return known_models + + +def get_flat_models_from_field( + field: ModelField, known_models: TypeModelSet +) -> TypeModelSet: + field_type = field.type_ + if lenient_issubclass(field_type, BaseModel): + if field_type in known_models: + return known_models + known_models.add(field_type) + get_flat_models_from_model(field_type, known_models=known_models) + elif lenient_issubclass(field_type, Enum): + known_models.add(field_type) + else: + get_flat_models_from_annotation(field_type, known_models=known_models) + return known_models + + +def get_flat_models_from_fields( + fields: Sequence[ModelField], known_models: TypeModelSet +) -> TypeModelSet: + for field in fields: + get_flat_models_from_field(field, known_models=known_models) + return known_models + + +def get_long_model_name(model: TypeModelOrEnum) -> str: + return f"{model.__module__}__{model.__qualname__}".replace(".", "__") diff --git a/fastapi/datastructures.py b/fastapi/datastructures.py index cf8406b0f..34185b96a 100644 --- a/fastapi/datastructures.py +++ b/fastapi/datastructures.py @@ -11,11 +11,9 @@ from typing import ( ) from fastapi._compat import ( - PYDANTIC_V2, CoreSchema, GetJsonSchemaHandler, JsonSchemaValue, - with_info_plain_validator_function, ) from starlette.datastructures import URL as URL # noqa: F401 from starlette.datastructures import Address as Address # noqa: F401 @@ -154,11 +152,10 @@ class UploadFile(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"}) + # TODO: remove when deprecating Pydantic v1 + @classmethod + def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: + field_schema.update({"type": "string", "format": "binary"}) @classmethod def __get_pydantic_json_schema__( @@ -170,6 +167,8 @@ class UploadFile(StarletteUploadFile): def __get_pydantic_core_schema__( cls, source: Type[Any], handler: Callable[[Any], CoreSchema] ) -> CoreSchema: + from ._compat.v2 import with_info_plain_validator_function + return with_info_plain_validator_function(cls._validate) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index e49380cb3..675ad6faf 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -23,11 +23,11 @@ import anyio from fastapi import params from fastapi._compat import ( PYDANTIC_V2, - ErrorWrapper, ModelField, RequiredParam, Undefined, - _regenerate_error_with_loc, + _is_error_wrapper, + _is_model_class, copy_field_info, create_body_model, evaluate_forwardref, @@ -45,8 +45,10 @@ from fastapi._compat import ( lenient_issubclass, sequence_types, serialize_sequence_value, + v1, value_is_sequence, ) +from fastapi._compat.shared import annotation_is_pydantic_v1 from fastapi.background import BackgroundTasks from fastapi.concurrency import ( asynccontextmanager, @@ -74,6 +76,8 @@ from starlette.responses import Response from starlette.websockets import WebSocket from typing_extensions import Annotated, get_args, get_origin +from .. import temp_pydantic_v1_params + if sys.version_info >= (3, 13): # pragma: no cover from inspect import iscoroutinefunction else: # pragma: no cover @@ -219,7 +223,7 @@ def _get_flat_fields_from_params(fields: List[ModelField]) -> List[ModelField]: if not fields: return fields first_field = fields[0] - if len(fields) == 1 and lenient_issubclass(first_field.type_, BaseModel): + if len(fields) == 1 and _is_model_class(first_field.type_): fields_to_extract = get_cached_model_fields(first_field.type_) return fields_to_extract return fields @@ -315,7 +319,9 @@ def get_dependant( ) continue assert param_details.field is not None - if isinstance(param_details.field.field_info, params.Body): + if isinstance( + param_details.field.field_info, (params.Body, temp_pydantic_v1_params.Body) + ): dependant.body_params.append(param_details.field) else: add_param_to_fields(field=param_details.field, dependant=dependant) @@ -374,28 +380,38 @@ def analyze_param( fastapi_annotations = [ arg for arg in annotated_args[1:] - if isinstance(arg, (FieldInfo, params.Depends)) + if isinstance(arg, (FieldInfo, v1.FieldInfo, params.Depends)) ] fastapi_specific_annotations = [ arg for arg in fastapi_annotations - if isinstance(arg, (params.Param, params.Body, params.Depends)) + if isinstance( + arg, + ( + params.Param, + temp_pydantic_v1_params.Param, + params.Body, + temp_pydantic_v1_params.Body, + params.Depends, + ), + ) ] if fastapi_specific_annotations: - fastapi_annotation: Union[FieldInfo, params.Depends, None] = ( + fastapi_annotation: Union[FieldInfo, v1.FieldInfo, params.Depends, None] = ( fastapi_specific_annotations[-1] ) else: fastapi_annotation = None # Set default for Annotated FieldInfo - if isinstance(fastapi_annotation, FieldInfo): + if isinstance(fastapi_annotation, (FieldInfo, v1.FieldInfo)): # Copy `field_info` because we mutate `field_info.default` below. field_info = copy_field_info( field_info=fastapi_annotation, annotation=use_annotation ) - assert ( - field_info.default is Undefined or field_info.default is RequiredParam - ), ( + assert field_info.default in { + Undefined, + v1.Undefined, + } or field_info.default in {RequiredParam, v1.RequiredParam}, ( f"`{field_info.__class__.__name__}` default value cannot be set in" f" `Annotated` for {param_name!r}. Set the default value with `=` instead." ) @@ -419,14 +435,15 @@ def analyze_param( ) depends = value # Get FieldInfo from default value - elif isinstance(value, FieldInfo): + elif isinstance(value, (FieldInfo, v1.FieldInfo)): assert field_info is None, ( "Cannot specify FastAPI annotations in `Annotated` and default value" f" together for {param_name!r}" ) field_info = value if PYDANTIC_V2: - field_info.annotation = type_annotation + if isinstance(field_info, FieldInfo): + field_info.annotation = type_annotation # Get Depends from type annotation if depends is not None and depends.dependency is None: @@ -463,7 +480,14 @@ def analyze_param( ) or is_uploadfile_sequence_annotation(type_annotation): field_info = params.File(annotation=use_annotation, default=default_value) elif not field_annotation_is_scalar(annotation=type_annotation): - field_info = params.Body(annotation=use_annotation, default=default_value) + if annotation_is_pydantic_v1(use_annotation): + field_info = temp_pydantic_v1_params.Body( + annotation=use_annotation, default=default_value + ) + else: + field_info = params.Body( + annotation=use_annotation, default=default_value + ) else: field_info = params.Query(annotation=use_annotation, default=default_value) @@ -472,12 +496,14 @@ def analyze_param( if field_info is not None: # Handle field_info.in_ if is_path_param: - assert isinstance(field_info, params.Path), ( + assert isinstance( + field_info, (params.Path, temp_pydantic_v1_params.Path) + ), ( f"Cannot use `{field_info.__class__.__name__}` for path param" f" {param_name!r}" ) elif ( - isinstance(field_info, params.Param) + isinstance(field_info, (params.Param, temp_pydantic_v1_params.Param)) and getattr(field_info, "in_", None) is None ): field_info.in_ = params.ParamTypes.query @@ -486,7 +512,7 @@ def analyze_param( field_info, param_name, ) - if isinstance(field_info, params.Form): + if isinstance(field_info, (params.Form, temp_pydantic_v1_params.Form)): ensure_multipart_is_installed() if not field_info.alias and getattr(field_info, "convert_underscores", None): alias = param_name.replace("_", "-") @@ -498,19 +524,19 @@ def analyze_param( type_=use_annotation_from_field_info, default=field_info.default, alias=alias, - required=field_info.default in (RequiredParam, Undefined), + required=field_info.default in (RequiredParam, v1.RequiredParam, Undefined), field_info=field_info, ) if is_path_param: assert is_scalar_field(field=field), ( "Path params must be of one of the supported types" ) - elif isinstance(field_info, params.Query): + elif isinstance(field_info, (params.Query, temp_pydantic_v1_params.Query)): assert ( is_scalar_field(field) or is_scalar_sequence_field(field) or ( - lenient_issubclass(field.type_, BaseModel) + _is_model_class(field.type_) # For Pydantic v1 and getattr(field, "shape", 1) == 1 ) @@ -712,10 +738,10 @@ def _validate_value_with_model_field( else: return deepcopy(field.default), [] v_, errors_ = field.validate(value, values, loc=loc) - if isinstance(errors_, ErrorWrapper): + if _is_error_wrapper(errors_): # type: ignore[arg-type] return None, [errors_] elif isinstance(errors_, list): - new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=()) + new_errors = v1._regenerate_error_with_loc(errors=errors_, loc_prefix=()) return None, new_errors else: return v_, [] @@ -732,7 +758,7 @@ def _get_multidict_value( if ( value is None or ( - isinstance(field.field_info, params.Form) + isinstance(field.field_info, (params.Form, temp_pydantic_v1_params.Form)) and isinstance(value, str) # For type checks and value == "" ) @@ -798,7 +824,7 @@ def request_params_to_args( if single_not_embedded_field: field_info = first_field.field_info - assert isinstance(field_info, params.Param), ( + assert isinstance(field_info, (params.Param, temp_pydantic_v1_params.Param)), ( "Params must be subclasses of Param" ) loc: Tuple[str, ...] = (field_info.in_.value,) @@ -810,7 +836,7 @@ def request_params_to_args( for field in fields: value = _get_multidict_value(field, received_params) field_info = field.field_info - assert isinstance(field_info, params.Param), ( + assert isinstance(field_info, (params.Param, temp_pydantic_v1_params.Param)), ( "Params must be subclasses of Param" ) loc = (field_info.in_.value, field.alias) @@ -837,7 +863,7 @@ def is_union_of_base_models(field_type: Any) -> bool: union_args = get_args(field_type) for arg in union_args: - if not lenient_issubclass(arg, BaseModel): + if not _is_model_class(arg): return False return True @@ -859,8 +885,8 @@ def _should_embed_body_fields(fields: List[ModelField]) -> bool: # If it's a Form (or File) field, it has to be a BaseModel (or a union of BaseModels) to be top level # otherwise it has to be embedded, so that the key value pair can be extracted if ( - isinstance(first_field.field_info, params.Form) - and not lenient_issubclass(first_field.type_, BaseModel) + isinstance(first_field.field_info, (params.Form, temp_pydantic_v1_params.Form)) + and not _is_model_class(first_field.type_) and not is_union_of_base_models(first_field.type_) ): return True @@ -877,14 +903,14 @@ async def _extract_form_body( value = _get_multidict_value(field, received_body) field_info = field.field_info if ( - isinstance(field_info, params.File) + isinstance(field_info, (params.File, temp_pydantic_v1_params.File)) and is_bytes_field(field) and isinstance(value, UploadFile) ): value = await value.read() elif ( is_bytes_sequence_field(field) - and isinstance(field_info, params.File) + and isinstance(field_info, (params.File, temp_pydantic_v1_params.File)) and value_is_sequence(value) ): # For types @@ -925,7 +951,7 @@ async def request_body_to_args( if ( single_not_embedded_field - and lenient_issubclass(first_field.type_, BaseModel) + and _is_model_class(first_field.type_) and isinstance(received_body, FormData) ): fields_to_extract = get_cached_model_fields(first_field.type_) @@ -990,15 +1016,28 @@ def get_body_field( 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, temp_pydantic_v1_params.File) + for f in flat_dependant.body_params + ): + BodyFieldInfo: Type[temp_pydantic_v1_params.Body] = temp_pydantic_v1_params.File # type: ignore[no-redef] elif any(isinstance(f.field_info, params.Form) for f in flat_dependant.body_params): BodyFieldInfo = params.Form + elif any( + isinstance(f.field_info, temp_pydantic_v1_params.Form) + for f in flat_dependant.body_params + ): + BodyFieldInfo = temp_pydantic_v1_params.Form # type: ignore[assignment] else: - BodyFieldInfo = params.Body + if annotation_is_pydantic_v1(BodyModel): + BodyFieldInfo = temp_pydantic_v1_params.Body # type: ignore[assignment] + else: + BodyFieldInfo = params.Body body_param_media_types = [ f.field_info.media_type for f in flat_dependant.body_params - if isinstance(f.field_info, params.Body) + if isinstance(f.field_info, (params.Body, temp_pydantic_v1_params.Body)) ] if len(set(body_param_media_types)) == 1: BodyFieldInfo_kwargs["media_type"] = body_param_media_types[0] diff --git a/fastapi/encoders.py b/fastapi/encoders.py index b037f8bb5..8ff7d58dd 100644 --- a/fastapi/encoders.py +++ b/fastapi/encoders.py @@ -17,6 +17,7 @@ from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union from uuid import UUID +from fastapi._compat import v1 from fastapi.types import IncEx from pydantic import BaseModel from pydantic.color import Color @@ -24,7 +25,7 @@ from pydantic.networks import AnyUrl, NameEmail from pydantic.types import SecretBytes, SecretStr from typing_extensions import Annotated, Doc -from ._compat import PYDANTIC_V2, UndefinedType, Url, _model_dump +from ._compat import Url, _is_undefined, _model_dump # Taken from Pydantic v1 as is @@ -58,6 +59,7 @@ def decimal_encoder(dec_value: Decimal) -> Union[int, float]: ENCODERS_BY_TYPE: Dict[Type[Any], Callable[[Any], Any]] = { bytes: lambda o: o.decode(), Color: str, + v1.Color: str, datetime.date: isoformat, datetime.datetime: isoformat, datetime.time: isoformat, @@ -74,14 +76,19 @@ ENCODERS_BY_TYPE: Dict[Type[Any], Callable[[Any], Any]] = { IPv6Interface: str, IPv6Network: str, NameEmail: str, + v1.NameEmail: str, Path: str, Pattern: lambda o: o.pattern, SecretBytes: str, + v1.SecretBytes: str, SecretStr: str, + v1.SecretStr: str, set: list, UUID: str, Url: str, + v1.Url: str, AnyUrl: str, + v1.AnyUrl: str, } @@ -213,10 +220,10 @@ def jsonable_encoder( include = set(include) if exclude is not None and not isinstance(exclude, (set, dict)): exclude = set(exclude) - if isinstance(obj, BaseModel): + if isinstance(obj, (BaseModel, v1.BaseModel)): # TODO: remove when deprecating Pydantic v1 encoders: Dict[Any, Any] = {} - if not PYDANTIC_V2: + if isinstance(obj, v1.BaseModel): encoders = getattr(obj.__config__, "json_encoders", {}) # type: ignore[attr-defined] if custom_encoder: encoders = {**encoders, **custom_encoder} @@ -260,7 +267,7 @@ def jsonable_encoder( return str(obj) if isinstance(obj, (str, int, float, type(None))): return obj - if isinstance(obj, UndefinedType): + if _is_undefined(obj): return None if isinstance(obj, dict): encoded_dict = {} diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index 21105cf65..dbc93d289 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -5,7 +5,6 @@ from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, Type, Union, from fastapi import routing from fastapi._compat import ( - GenerateJsonSchema, JsonSchemaValue, ModelField, Undefined, @@ -22,7 +21,7 @@ from fastapi.dependencies.utils import ( get_flat_params, ) from fastapi.encoders import jsonable_encoder -from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX, REF_TEMPLATE +from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX from fastapi.openapi.models import OpenAPI from fastapi.params import Body, ParamTypes from fastapi.responses import Response @@ -37,6 +36,8 @@ from starlette.responses import JSONResponse from starlette.routing import BaseRoute from typing_extensions import Literal +from .._compat import _is_model_field + validation_error_definition = { "title": "ValidationError", "type": "object", @@ -94,7 +95,6 @@ def get_openapi_security_definitions( def _get_openapi_operation_parameters( *, dependant: Dependant, - schema_generator: GenerateJsonSchema, model_name_map: ModelNameMap, field_mapping: Dict[ Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue @@ -128,7 +128,6 @@ def _get_openapi_operation_parameters( continue param_schema = get_schema_from_model_field( field=param, - schema_generator=schema_generator, model_name_map=model_name_map, field_mapping=field_mapping, separate_input_output_schemas=separate_input_output_schemas, @@ -169,7 +168,6 @@ def _get_openapi_operation_parameters( def get_openapi_operation_request_body( *, body_field: Optional[ModelField], - schema_generator: GenerateJsonSchema, model_name_map: ModelNameMap, field_mapping: Dict[ Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue @@ -178,10 +176,9 @@ def get_openapi_operation_request_body( ) -> Optional[Dict[str, Any]]: if not body_field: return None - assert isinstance(body_field, ModelField) + assert _is_model_field(body_field) body_schema = get_schema_from_model_field( field=body_field, - schema_generator=schema_generator, model_name_map=model_name_map, field_mapping=field_mapping, separate_input_output_schemas=separate_input_output_schemas, @@ -254,7 +251,6 @@ def get_openapi_path( *, route: routing.APIRoute, operation_ids: Set[str], - schema_generator: GenerateJsonSchema, model_name_map: ModelNameMap, field_mapping: Dict[ Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue @@ -287,7 +283,6 @@ def get_openapi_path( security_schemes.update(security_definitions) operation_parameters = _get_openapi_operation_parameters( dependant=route.dependant, - schema_generator=schema_generator, model_name_map=model_name_map, field_mapping=field_mapping, separate_input_output_schemas=separate_input_output_schemas, @@ -309,7 +304,6 @@ def get_openapi_path( if method in METHODS_WITH_BODY: request_body_oai = get_openapi_operation_request_body( body_field=route.body_field, - schema_generator=schema_generator, model_name_map=model_name_map, field_mapping=field_mapping, separate_input_output_schemas=separate_input_output_schemas, @@ -327,7 +321,6 @@ def get_openapi_path( ) = get_openapi_path( route=callback, operation_ids=operation_ids, - schema_generator=schema_generator, model_name_map=model_name_map, field_mapping=field_mapping, separate_input_output_schemas=separate_input_output_schemas, @@ -358,7 +351,6 @@ def get_openapi_path( if route.response_field: response_schema = get_schema_from_model_field( field=route.response_field, - schema_generator=schema_generator, model_name_map=model_name_map, field_mapping=field_mapping, separate_input_output_schemas=separate_input_output_schemas, @@ -392,7 +384,6 @@ def get_openapi_path( if field: additional_field_schema = get_schema_from_model_field( field=field, - schema_generator=schema_generator, model_name_map=model_name_map, field_mapping=field_mapping, separate_input_output_schemas=separate_input_output_schemas, @@ -454,7 +445,7 @@ def get_fields_from_routes( route, routing.APIRoute ): if route.body_field: - assert isinstance(route.body_field, ModelField), ( + assert _is_model_field(route.body_field), ( "A request body must be a Pydantic Field" ) body_fields_from_routes.append(route.body_field) @@ -510,10 +501,8 @@ def get_openapi( operation_ids: Set[str] = set() all_fields = get_fields_from_routes(list(routes or []) + list(webhooks or [])) model_name_map = get_compat_model_name_map(all_fields) - schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE) field_mapping, definitions = get_definitions( fields=all_fields, - schema_generator=schema_generator, model_name_map=model_name_map, separate_input_output_schemas=separate_input_output_schemas, ) @@ -522,7 +511,6 @@ def get_openapi( result = get_openapi_path( route=route, operation_ids=operation_ids, - schema_generator=schema_generator, model_name_map=model_name_map, field_mapping=field_mapping, separate_input_output_schemas=separate_input_output_schemas, @@ -542,7 +530,6 @@ def get_openapi( result = get_openapi_path( route=webhook, operation_ids=operation_ids, - schema_generator=schema_generator, model_name_map=model_name_map, field_mapping=field_mapping, separate_input_output_schemas=separate_input_output_schemas, diff --git a/fastapi/routing.py b/fastapi/routing.py index 65f739d95..fe25d7dec 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -24,7 +24,7 @@ from typing import ( Union, ) -from fastapi import params +from fastapi import params, temp_pydantic_v1_params from fastapi._compat import ( ModelField, Undefined, @@ -307,7 +307,9 @@ def get_request_handler( ) -> Callable[[Request], Coroutine[Any, Any, Response]]: assert dependant.call is not None, "dependant.call must be a function" is_coroutine = iscoroutinefunction(dependant.call) - is_body_form = body_field and isinstance(body_field.field_info, params.Form) + is_body_form = body_field and isinstance( + body_field.field_info, (params.Form, temp_pydantic_v1_params.Form) + ) if isinstance(response_class, DefaultPlaceholder): actual_response_class: Type[Response] = response_class.value else: diff --git a/fastapi/temp_pydantic_v1_params.py b/fastapi/temp_pydantic_v1_params.py new file mode 100644 index 000000000..0535ee727 --- /dev/null +++ b/fastapi/temp_pydantic_v1_params.py @@ -0,0 +1,724 @@ +import warnings +from typing import Any, Callable, Dict, List, Optional, Union + +from fastapi.openapi.models import Example +from fastapi.params import ParamTypes +from typing_extensions import Annotated, deprecated + +from ._compat.shared import PYDANTIC_VERSION_MINOR_TUPLE +from ._compat.v1 import FieldInfo, Undefined + +_Unset: Any = Undefined + + +class Param(FieldInfo): # type: ignore[misc] + in_: ParamTypes + + def __init__( + self, + default: Any = Undefined, + *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, + alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, + title: Optional[str] = None, + description: Optional[str] = None, + gt: Optional[float] = None, + ge: Optional[float] = None, + lt: Optional[float] = None, + le: Optional[float] = None, + min_length: Optional[int] = None, + max_length: Optional[int] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = _Unset, + openapi_examples: Optional[Dict[str, Example]] = None, + deprecated: Union[deprecated, str, bool, None] = None, + include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, + **extra: Any, + ): + if example is not _Unset: + warnings.warn( + "`example` has been deprecated, please use `examples` instead", + category=DeprecationWarning, + stacklevel=4, + ) + self.example = example + self.include_in_schema = include_in_schema + self.openapi_examples = openapi_examples + kwargs = dict( + default=default, + default_factory=default_factory, + alias=alias, + title=title, + description=description, + gt=gt, + ge=ge, + lt=lt, + le=le, + min_length=min_length, + max_length=max_length, + discriminator=discriminator, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, + **extra, + ) + if examples is not None: + kwargs["examples"] = examples + if regex is not None: + warnings.warn( + "`regex` has been deprecated, please use `pattern` instead", + category=DeprecationWarning, + stacklevel=4, + ) + current_json_schema_extra = json_schema_extra or extra + if PYDANTIC_VERSION_MINOR_TUPLE < (2, 7): + self.deprecated = deprecated + else: + kwargs["deprecated"] = deprecated + kwargs["regex"] = pattern or regex + kwargs.update(**current_json_schema_extra) + use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset} + + super().__init__(**use_kwargs) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.default})" + + +class Path(Param): # type: ignore[misc] + in_ = ParamTypes.path + + def __init__( + self, + default: Any = ..., + *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, + alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, + title: Optional[str] = None, + description: Optional[str] = None, + gt: Optional[float] = None, + ge: Optional[float] = None, + lt: Optional[float] = None, + le: Optional[float] = None, + min_length: Optional[int] = None, + max_length: Optional[int] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = _Unset, + openapi_examples: Optional[Dict[str, Example]] = None, + deprecated: Union[deprecated, str, bool, None] = None, + include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, + **extra: Any, + ): + assert default is ..., "Path parameters cannot have a default value" + self.in_ = self.in_ + super().__init__( + default=default, + default_factory=default_factory, + annotation=annotation, + alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, + title=title, + description=description, + gt=gt, + ge=ge, + lt=lt, + le=le, + min_length=min_length, + max_length=max_length, + pattern=pattern, + regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, + deprecated=deprecated, + example=example, + examples=examples, + openapi_examples=openapi_examples, + include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, + **extra, + ) + + +class Query(Param): # type: ignore[misc] + in_ = ParamTypes.query + + def __init__( + self, + default: Any = Undefined, + *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, + alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, + title: Optional[str] = None, + description: Optional[str] = None, + gt: Optional[float] = None, + ge: Optional[float] = None, + lt: Optional[float] = None, + le: Optional[float] = None, + min_length: Optional[int] = None, + max_length: Optional[int] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = _Unset, + openapi_examples: Optional[Dict[str, Example]] = None, + deprecated: Union[deprecated, str, bool, None] = None, + include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, + **extra: Any, + ): + super().__init__( + default=default, + default_factory=default_factory, + annotation=annotation, + alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, + title=title, + description=description, + gt=gt, + ge=ge, + lt=lt, + le=le, + min_length=min_length, + max_length=max_length, + pattern=pattern, + regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, + deprecated=deprecated, + example=example, + examples=examples, + openapi_examples=openapi_examples, + include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, + **extra, + ) + + +class Header(Param): # type: ignore[misc] + in_ = ParamTypes.header + + def __init__( + self, + default: Any = Undefined, + *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, + alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, + convert_underscores: bool = True, + title: Optional[str] = None, + description: Optional[str] = None, + gt: Optional[float] = None, + ge: Optional[float] = None, + lt: Optional[float] = None, + le: Optional[float] = None, + min_length: Optional[int] = None, + max_length: Optional[int] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = _Unset, + openapi_examples: Optional[Dict[str, Example]] = None, + deprecated: Union[deprecated, str, bool, None] = None, + include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, + **extra: Any, + ): + self.convert_underscores = convert_underscores + super().__init__( + default=default, + default_factory=default_factory, + annotation=annotation, + alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, + title=title, + description=description, + gt=gt, + ge=ge, + lt=lt, + le=le, + min_length=min_length, + max_length=max_length, + pattern=pattern, + regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, + deprecated=deprecated, + example=example, + examples=examples, + openapi_examples=openapi_examples, + include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, + **extra, + ) + + +class Cookie(Param): # type: ignore[misc] + in_ = ParamTypes.cookie + + def __init__( + self, + default: Any = Undefined, + *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, + alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, + title: Optional[str] = None, + description: Optional[str] = None, + gt: Optional[float] = None, + ge: Optional[float] = None, + lt: Optional[float] = None, + le: Optional[float] = None, + min_length: Optional[int] = None, + max_length: Optional[int] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = _Unset, + openapi_examples: Optional[Dict[str, Example]] = None, + deprecated: Union[deprecated, str, bool, None] = None, + include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, + **extra: Any, + ): + super().__init__( + default=default, + default_factory=default_factory, + annotation=annotation, + alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, + title=title, + description=description, + gt=gt, + ge=ge, + lt=lt, + le=le, + min_length=min_length, + max_length=max_length, + pattern=pattern, + regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, + deprecated=deprecated, + example=example, + examples=examples, + openapi_examples=openapi_examples, + include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, + **extra, + ) + + +class Body(FieldInfo): # type: ignore[misc] + def __init__( + self, + default: Any = Undefined, + *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, + embed: Union[bool, None] = None, + media_type: str = "application/json", + alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, + title: Optional[str] = None, + description: Optional[str] = None, + gt: Optional[float] = None, + ge: Optional[float] = None, + lt: Optional[float] = None, + le: Optional[float] = None, + min_length: Optional[int] = None, + max_length: Optional[int] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = _Unset, + openapi_examples: Optional[Dict[str, Example]] = None, + deprecated: Union[deprecated, str, bool, None] = None, + include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, + **extra: Any, + ): + self.embed = embed + self.media_type = media_type + if example is not _Unset: + warnings.warn( + "`example` has been deprecated, please use `examples` instead", + category=DeprecationWarning, + stacklevel=4, + ) + self.example = example + self.include_in_schema = include_in_schema + self.openapi_examples = openapi_examples + kwargs = dict( + default=default, + default_factory=default_factory, + alias=alias, + title=title, + description=description, + gt=gt, + ge=ge, + lt=lt, + le=le, + min_length=min_length, + max_length=max_length, + discriminator=discriminator, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, + **extra, + ) + if examples is not None: + kwargs["examples"] = examples + if regex is not None: + warnings.warn( + "`regex` has been deprecated, please use `pattern` instead", + category=DeprecationWarning, + stacklevel=4, + ) + current_json_schema_extra = json_schema_extra or extra + if PYDANTIC_VERSION_MINOR_TUPLE < (2, 7): + self.deprecated = deprecated + else: + kwargs["deprecated"] = deprecated + kwargs["regex"] = pattern or regex + kwargs.update(**current_json_schema_extra) + + use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset} + + super().__init__(**use_kwargs) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.default})" + + +class Form(Body): # type: ignore[misc] + def __init__( + self, + default: Any = Undefined, + *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, + media_type: str = "application/x-www-form-urlencoded", + alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, + title: Optional[str] = None, + description: Optional[str] = None, + gt: Optional[float] = None, + ge: Optional[float] = None, + lt: Optional[float] = None, + le: Optional[float] = None, + min_length: Optional[int] = None, + max_length: Optional[int] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = _Unset, + openapi_examples: Optional[Dict[str, Example]] = None, + deprecated: Union[deprecated, str, bool, None] = None, + include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, + **extra: Any, + ): + super().__init__( + default=default, + default_factory=default_factory, + annotation=annotation, + media_type=media_type, + alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, + title=title, + description=description, + gt=gt, + ge=ge, + lt=lt, + le=le, + min_length=min_length, + max_length=max_length, + pattern=pattern, + regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, + deprecated=deprecated, + example=example, + examples=examples, + openapi_examples=openapi_examples, + include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, + **extra, + ) + + +class File(Form): # type: ignore[misc] + def __init__( + self, + default: Any = Undefined, + *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, + media_type: str = "multipart/form-data", + alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, + title: Optional[str] = None, + description: Optional[str] = None, + gt: Optional[float] = None, + ge: Optional[float] = None, + lt: Optional[float] = None, + le: Optional[float] = None, + min_length: Optional[int] = None, + max_length: Optional[int] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = _Unset, + openapi_examples: Optional[Dict[str, Example]] = None, + deprecated: Union[deprecated, str, bool, None] = None, + include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, + **extra: Any, + ): + super().__init__( + default=default, + default_factory=default_factory, + annotation=annotation, + media_type=media_type, + alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, + title=title, + description=description, + gt=gt, + ge=ge, + lt=lt, + le=le, + min_length=min_length, + max_length=max_length, + pattern=pattern, + regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, + deprecated=deprecated, + example=example, + examples=examples, + openapi_examples=openapi_examples, + include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, + **extra, + ) diff --git a/fastapi/utils.py b/fastapi/utils.py index 98725ff19..3ea9271b1 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -23,10 +23,12 @@ from fastapi._compat import ( Undefined, UndefinedType, Validator, + annotation_is_pydantic_v1, lenient_issubclass, + v1, ) from fastapi.datastructures import DefaultPlaceholder, DefaultType -from pydantic import BaseModel, create_model +from pydantic import BaseModel from pydantic.fields import FieldInfo from typing_extensions import Literal @@ -60,50 +62,70 @@ def get_path_param_names(path: str) -> Set[str]: return set(re.findall("{(.*?)}", path)) +_invalid_args_message = ( + "Invalid args for response field! Hint: " + "check that {type_} is a valid Pydantic field type. " + "If you are using a return type annotation that is not a valid Pydantic " + "field (e.g. Union[Response, dict, None]) you can disable generating the " + "response model from the type annotation with the path operation decorator " + "parameter response_model=None. Read more: " + "https://fastapi.tiangolo.com/tutorial/response-model/" +) + + def create_model_field( name: str, type_: Any, class_validators: Optional[Dict[str, Validator]] = None, default: Optional[Any] = Undefined, required: Union[bool, UndefinedType] = Undefined, - model_config: Type[BaseConfig] = BaseConfig, + model_config: Union[Type[BaseConfig], None] = None, field_info: Optional[FieldInfo] = None, alias: Optional[str] = None, mode: Literal["validation", "serialization"] = "validation", + version: Literal["1", "auto"] = "auto", ) -> ModelField: class_validators = class_validators or {} - if PYDANTIC_V2: + + v1_model_config = v1.BaseConfig + v1_field_info = field_info or v1.FieldInfo() + v1_kwargs = { + "name": name, + "field_info": v1_field_info, + "type_": type_, + "class_validators": class_validators, + "default": default, + "required": required, + "model_config": v1_model_config, + "alias": alias, + } + + if ( + annotation_is_pydantic_v1(type_) + or isinstance(field_info, v1.FieldInfo) + or version == "1" + ): + try: + return v1.ModelField(**v1_kwargs) # type: ignore[no-any-return] + except RuntimeError: + raise fastapi.exceptions.FastAPIError(_invalid_args_message) from None + elif PYDANTIC_V2: + from ._compat import v2 + field_info = field_info or FieldInfo( annotation=type_, default=default, alias=alias ) - else: - field_info = field_info or FieldInfo() - kwargs = {"name": name, "field_info": field_info} - if PYDANTIC_V2: - kwargs.update({"mode": mode}) - else: - kwargs.update( - { - "type_": type_, - "class_validators": class_validators, - "default": default, - "required": required, - "model_config": model_config, - "alias": alias, - } - ) + kwargs = {"mode": mode, "name": name, "field_info": field_info} + try: + return v2.ModelField(**kwargs) # type: ignore[return-value,arg-type] + except PydanticSchemaGenerationError: + raise fastapi.exceptions.FastAPIError(_invalid_args_message) from None + # Pydantic v2 is not installed, but it's not a Pydantic v1 ModelField, it could be + # a Pydantic v1 type, like a constrained int 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. " - "If you are using a return type annotation that is not a valid Pydantic " - "field (e.g. Union[Response, dict, None]) you can disable generating the " - "response model from the type annotation with the path operation decorator " - "parameter response_model=None. Read more: " - "https://fastapi.tiangolo.com/tutorial/response-model/" - ) from None + return v1.ModelField(**v1_kwargs) # type: ignore[no-any-return] + except RuntimeError: + raise fastapi.exceptions.FastAPIError(_invalid_args_message) from None def create_cloned_field( @@ -112,7 +134,10 @@ def create_cloned_field( cloned_types: Optional[MutableMapping[Type[BaseModel], Type[BaseModel]]] = None, ) -> ModelField: if PYDANTIC_V2: - return field + from ._compat import v2 + + if isinstance(field, v2.ModelField): + return field # cloned_types caches already cloned types to support recursive models and improve # performance by avoiding unnecessary cloning if cloned_types is None: @@ -122,17 +147,18 @@ def create_cloned_field( if is_dataclass(original_type) and hasattr(original_type, "__pydantic_model__"): original_type = original_type.__pydantic_model__ use_type = original_type - if lenient_issubclass(original_type, BaseModel): - original_type = cast(Type[BaseModel], original_type) + if lenient_issubclass(original_type, v1.BaseModel): + original_type = cast(Type[v1.BaseModel], original_type) use_type = cloned_types.get(original_type) if use_type is None: - use_type = create_model(original_type.__name__, __base__=original_type) + use_type = v1.create_model(original_type.__name__, __base__=original_type) cloned_types[original_type] = use_type for f in original_type.__fields__.values(): use_type.__fields__[f.name] = create_cloned_field( - f, cloned_types=cloned_types + f, + cloned_types=cloned_types, ) - new_field = create_model_field(name=field.name, type_=use_type) + new_field = create_model_field(name=field.name, type_=use_type, version="1") 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] diff --git a/tests/test_compat.py b/tests/test_compat.py index 43c686489..f79dbdabc 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -2,53 +2,45 @@ from typing import Any, Dict, List, Union from fastapi import FastAPI, UploadFile from fastapi._compat import ( - ModelField, Undefined, _get_model_config, get_cached_model_fields, - get_model_fields, - is_bytes_sequence_annotation, is_scalar_field, is_uploadfile_sequence_annotation, + v1, ) +from fastapi._compat.shared import is_bytes_sequence_annotation from fastapi.testclient import TestClient -from pydantic import BaseConfig, BaseModel, ConfigDict +from pydantic import BaseModel, ConfigDict from pydantic.fields import FieldInfo -from .utils import needs_pydanticv1, needs_pydanticv2 +from .utils import needs_py_lt_314, needs_pydanticv2 @needs_pydanticv2 def test_model_field_default_required(): + from fastapi._compat import v2 + # For coverage field_info = FieldInfo(annotation=str) - field = ModelField(name="foo", field_info=field_info) + field = v2.ModelField(name="foo", field_info=field_info) assert field.default is Undefined -@needs_pydanticv1 -def test_upload_file_dummy_with_info_plain_validator_function(): +def test_v1_plain_validator_function(): # For coverage - assert UploadFile.__get_pydantic_core_schema__(str, lambda x: None) == {} + def func(v): # pragma: no cover + return v + + result = v1.with_info_plain_validator_function(func) + assert result == {} -@needs_pydanticv1 -def test_union_scalar_list(): +def test_is_model_field(): # 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 + from fastapi._compat import _is_model_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) + assert not _is_model_field(str) @needs_pydanticv2 @@ -141,21 +133,22 @@ def test_is_uploadfile_sequence_annotation(): assert is_uploadfile_sequence_annotation(Union[List[str], List[UploadFile]]) +@needs_py_lt_314 def test_is_pv1_scalar_field(): # For coverage - class Model(BaseModel): + class Model(v1.BaseModel): foo: Union[str, Dict[str, Any]] - fields = get_model_fields(Model) + fields = v1.get_model_fields(Model) assert not is_scalar_field(fields[0]) def test_get_model_fields_cached(): - class Model(BaseModel): + class Model(v1.BaseModel): foo: str - non_cached_fields = get_model_fields(Model) - non_cached_fields2 = get_model_fields(Model) + non_cached_fields = v1.get_model_fields(Model) + non_cached_fields2 = v1.get_model_fields(Model) cached_fields = get_cached_model_fields(Model) cached_fields2 = get_cached_model_fields(Model) for f1, f2 in zip(cached_fields, cached_fields2): diff --git a/tests/test_compat_params_v1.py b/tests/test_compat_params_v1.py new file mode 100644 index 000000000..7064761cb --- /dev/null +++ b/tests/test_compat_params_v1.py @@ -0,0 +1,1122 @@ +import sys +from typing import List, Optional + +import pytest + +from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314 + +if sys.version_info >= (3, 14): + skip_module_if_py_gte_314() + +from fastapi import FastAPI +from fastapi._compat.v1 import BaseModel +from fastapi.temp_pydantic_v1_params import ( + Body, + Cookie, + File, + Form, + Header, + Path, + Query, +) +from fastapi.testclient import TestClient +from inline_snapshot import snapshot +from typing_extensions import Annotated + + +class Item(BaseModel): + name: str + price: float + description: Optional[str] = None + + +app = FastAPI() + + +@app.get("/items/{item_id}") +def get_item_with_path( + item_id: Annotated[int, Path(title="The ID of the item", ge=1, le=1000)], +): + return {"item_id": item_id} + + +@app.get("/items/") +def get_items_with_query( + q: Annotated[ + Optional[str], Query(min_length=3, max_length=50, pattern="^[a-zA-Z0-9 ]+$") + ] = None, + skip: Annotated[int, Query(ge=0)] = 0, + limit: Annotated[int, Query(ge=1, le=100, examples=[5])] = 10, +): + return {"q": q, "skip": skip, "limit": limit} + + +@app.get("/users/") +def get_user_with_header( + x_custom: Annotated[Optional[str], Header()] = None, + x_token: Annotated[Optional[str], Header(convert_underscores=True)] = None, +): + return {"x_custom": x_custom, "x_token": x_token} + + +@app.get("/cookies/") +def get_cookies( + session_id: Annotated[Optional[str], Cookie()] = None, + tracking_id: Annotated[Optional[str], Cookie(min_length=10)] = None, +): + return {"session_id": session_id, "tracking_id": tracking_id} + + +@app.post("/items/") +def create_item( + item: Annotated[ + Item, + Body(examples=[{"name": "Foo", "price": 35.4, "description": "The Foo item"}]), + ], +): + return {"item": item} + + +@app.post("/items-embed/") +def create_item_embed( + item: Annotated[Item, Body(embed=True)], +): + return {"item": item} + + +@app.put("/items/{item_id}") +def update_item( + item_id: Annotated[int, Path(ge=1)], + item: Annotated[Item, Body()], + importance: Annotated[int, Body(gt=0, le=10)], +): + return {"item": item, "importance": importance} + + +@app.post("/form-data/") +def submit_form( + username: Annotated[str, Form(min_length=3, max_length=50)], + password: Annotated[str, Form(min_length=8)], + email: Annotated[Optional[str], Form()] = None, +): + return {"username": username, "password": password, "email": email} + + +@app.post("/upload/") +def upload_file( + file: Annotated[bytes, File()], + description: Annotated[Optional[str], Form()] = None, +): + return {"file_size": len(file), "description": description} + + +@app.post("/upload-multiple/") +def upload_multiple_files( + files: Annotated[List[bytes], File()], + note: Annotated[str, Form()] = "", +): + return { + "file_count": len(files), + "total_size": sum(len(f) for f in files), + "note": note, + } + + +client = TestClient(app) + + +# Path parameter tests +def test_path_param_valid(): + response = client.get("/items/50") + assert response.status_code == 200 + assert response.json() == {"item_id": 50} + + +def test_path_param_too_large(): + response = client.get("/items/1001") + assert response.status_code == 422 + error = response.json()["detail"][0] + assert error["loc"] == ["path", "item_id"] + + +def test_path_param_too_small(): + response = client.get("/items/0") + assert response.status_code == 422 + error = response.json()["detail"][0] + assert error["loc"] == ["path", "item_id"] + + +# Query parameter tests +def test_query_params_valid(): + response = client.get("/items/?q=test search&skip=5&limit=20") + assert response.status_code == 200 + assert response.json() == {"q": "test search", "skip": 5, "limit": 20} + + +def test_query_params_defaults(): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == {"q": None, "skip": 0, "limit": 10} + + +def test_query_param_too_short(): + response = client.get("/items/?q=ab") + assert response.status_code == 422 + error = response.json()["detail"][0] + assert error["loc"] == ["query", "q"] + + +def test_query_param_invalid_pattern(): + response = client.get("/items/?q=test@#$") + assert response.status_code == 422 + error = response.json()["detail"][0] + assert error["loc"] == ["query", "q"] + + +def test_query_param_limit_too_large(): + response = client.get("/items/?limit=101") + assert response.status_code == 422 + error = response.json()["detail"][0] + assert error["loc"] == ["query", "limit"] + + +# Header parameter tests +def test_header_params(): + response = client.get( + "/users/", + headers={"X-Custom": "Plumbus", "X-Token": "secret-token"}, + ) + assert response.status_code == 200 + assert response.json() == { + "x_custom": "Plumbus", + "x_token": "secret-token", + } + + +def test_header_underscore_conversion(): + response = client.get( + "/users/", + headers={"x-token": "secret-token-with-dash"}, + ) + assert response.status_code == 200 + assert response.json()["x_token"] == "secret-token-with-dash" + + +def test_header_params_none(): + response = client.get("/users/") + assert response.status_code == 200 + assert response.json() == {"x_custom": None, "x_token": None} + + +# Cookie parameter tests +def test_cookie_params(): + with TestClient(app) as client: + client.cookies.set("session_id", "abc123") + client.cookies.set("tracking_id", "1234567890abcdef") + response = client.get("/cookies/") + assert response.status_code == 200 + assert response.json() == { + "session_id": "abc123", + "tracking_id": "1234567890abcdef", + } + + +def test_cookie_tracking_id_too_short(): + with TestClient(app) as client: + client.cookies.set("tracking_id", "short") + response = client.get("/cookies/") + assert response.status_code == 422 + assert response.json() == snapshot( + { + "detail": [ + { + "loc": ["cookie", "tracking_id"], + "msg": "ensure this value has at least 10 characters", + "type": "value_error.any_str.min_length", + "ctx": {"limit_value": 10}, + } + ] + } + ) + + +def test_cookie_params_none(): + response = client.get("/cookies/") + assert response.status_code == 200 + assert response.json() == {"session_id": None, "tracking_id": None} + + +# Body parameter tests +def test_body_param(): + response = client.post( + "/items/", + json={"name": "Test Item", "price": 29.99, "description": "A test item"}, + ) + assert response.status_code == 200 + assert response.json() == { + "item": { + "name": "Test Item", + "price": 29.99, + "description": "A test item", + } + } + + +def test_body_param_minimal(): + response = client.post( + "/items/", + json={"name": "Minimal", "price": 9.99}, + ) + assert response.status_code == 200 + assert response.json() == { + "item": {"name": "Minimal", "price": 9.99, "description": None} + } + + +def test_body_param_missing_required(): + response = client.post( + "/items/", + json={"name": "Incomplete"}, + ) + assert response.status_code == 422 + error = response.json()["detail"][0] + assert error["loc"] == ["body", "price"] + + +def test_body_embed(): + response = client.post( + "/items-embed/", + json={"item": {"name": "Embedded", "price": 15.0}}, + ) + assert response.status_code == 200 + assert response.json() == { + "item": {"name": "Embedded", "price": 15.0, "description": None} + } + + +def test_body_embed_wrong_structure(): + response = client.post( + "/items-embed/", + json={"name": "Not Embedded", "price": 15.0}, + ) + assert response.status_code == 422 + + +# Multiple body parameters test +def test_multiple_body_params(): + response = client.put( + "/items/5", + json={ + "item": {"name": "Updated Item", "price": 49.99}, + "importance": 8, + }, + ) + assert response.status_code == 200 + assert response.json() == snapshot( + { + "item": {"name": "Updated Item", "price": 49.99, "description": None}, + "importance": 8, + } + ) + + +def test_multiple_body_params_importance_too_large(): + response = client.put( + "/items/5", + json={ + "item": {"name": "Item", "price": 10.0}, + "importance": 11, + }, + ) + assert response.status_code == 422 + assert response.json() == snapshot( + { + "detail": [ + { + "loc": ["body", "importance"], + "msg": "ensure this value is less than or equal to 10", + "type": "value_error.number.not_le", + "ctx": {"limit_value": 10}, + } + ] + } + ) + + +def test_multiple_body_params_importance_too_small(): + response = client.put( + "/items/5", + json={ + "item": {"name": "Item", "price": 10.0}, + "importance": 0, + }, + ) + assert response.status_code == 422 + assert response.json() == snapshot( + { + "detail": [ + { + "loc": ["body", "importance"], + "msg": "ensure this value is greater than 0", + "type": "value_error.number.not_gt", + "ctx": {"limit_value": 0}, + } + ] + } + ) + + +# Form parameter tests +def test_form_data_valid(): + response = client.post( + "/form-data/", + data={ + "username": "testuser", + "password": "password123", + "email": "test@example.com", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "testuser", + "password": "password123", + "email": "test@example.com", + } + + +def test_form_data_optional_field(): + response = client.post( + "/form-data/", + data={"username": "testuser", "password": "password123"}, + ) + assert response.status_code == 200 + assert response.json() == { + "username": "testuser", + "password": "password123", + "email": None, + } + + +def test_form_data_username_too_short(): + response = client.post( + "/form-data/", + data={"username": "ab", "password": "password123"}, + ) + assert response.status_code == 422 + assert response.json() == snapshot( + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "ensure this value has at least 3 characters", + "type": "value_error.any_str.min_length", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_form_data_password_too_short(): + response = client.post( + "/form-data/", + data={"username": "testuser", "password": "short"}, + ) + assert response.status_code == 422 + assert response.json() == snapshot( + { + "detail": [ + { + "loc": ["body", "password"], + "msg": "ensure this value has at least 8 characters", + "type": "value_error.any_str.min_length", + "ctx": {"limit_value": 8}, + } + ] + } + ) + + +# File upload tests +def test_upload_file(): + response = client.post( + "/upload/", + files={"file": ("test.txt", b"Hello, World!", "text/plain")}, + data={"description": "A test file"}, + ) + assert response.status_code == 200 + assert response.json() == { + "file_size": 13, + "description": "A test file", + } + + +def test_upload_file_without_description(): + response = client.post( + "/upload/", + files={"file": ("test.txt", b"Hello!", "text/plain")}, + ) + assert response.status_code == 200 + assert response.json() == { + "file_size": 6, + "description": None, + } + + +def test_upload_multiple_files(): + response = client.post( + "/upload-multiple/", + files=[ + ("files", ("file1.txt", b"Content 1", "text/plain")), + ("files", ("file2.txt", b"Content 2", "text/plain")), + ("files", ("file3.txt", b"Content 3", "text/plain")), + ], + data={"note": "Multiple files uploaded"}, + ) + assert response.status_code == 200 + assert response.json() == { + "file_count": 3, + "total_size": 27, + "note": "Multiple files uploaded", + } + + +def test_upload_multiple_files_empty_note(): + response = client.post( + "/upload-multiple/", + files=[ + ("files", ("file1.txt", b"Test", "text/plain")), + ], + ) + assert response.status_code == 200 + assert response.json()["file_count"] == 1 + assert response.json()["note"] == "" + + +# __repr__ tests +def test_query_repr(): + query_param = Query(default=None, min_length=3) + assert repr(query_param) == "Query(None)" + + +def test_body_repr(): + body_param = Body(default=None) + assert repr(body_param) == "Body(None)" + + +# Deprecation warning tests for regex parameter +def test_query_regex_deprecation_warning(): + with pytest.warns(DeprecationWarning, match="`regex` has been deprecated"): + Query(regex="^test$") + + +def test_body_regex_deprecation_warning(): + with pytest.warns(DeprecationWarning, match="`regex` has been deprecated"): + Body(regex="^test$") + + +# Deprecation warning tests for example parameter +def test_query_example_deprecation_warning(): + with pytest.warns(DeprecationWarning, match="`example` has been deprecated"): + Query(example="test example") + + +def test_body_example_deprecation_warning(): + with pytest.warns(DeprecationWarning, match="`example` has been deprecated"): + Body(example={"test": "example"}) + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "summary": "Get Item With Path", + "operationId": "get_item_with_path_items__item_id__get", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": True, + "schema": { + "title": "The ID of the item", + "minimum": 1, + "maximum": 1000, + "type": "integer", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": True, + "schema": { + "title": "Item Id", + "minimum": 1, + "type": "integer", + }, + } + ], + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": pydantic_snapshot( + v1=snapshot( + { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + ), + v2=snapshot( + { + "title": "Body", + "allOf": [ + { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + ], + } + ), + ), + } + }, + }, + "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 With Query", + "operationId": "get_items_with_query_items__get", + "parameters": [ + { + "name": "q", + "in": "query", + "required": False, + "schema": { + "title": "Q", + "maxLength": 50, + "minLength": 3, + "pattern": "^[a-zA-Z0-9 ]+$", + "type": "string", + }, + }, + { + "name": "skip", + "in": "query", + "required": False, + "schema": { + "title": "Skip", + "default": 0, + "minimum": 0, + "type": "integer", + }, + }, + { + "name": "limit", + "in": "query", + "required": False, + "schema": { + "title": "Limit", + "default": 10, + "minimum": 1, + "maximum": 100, + "examples": [5], + "type": "integer", + }, + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": { + "title": "Item", + "examples": [ + { + "name": "Foo", + "price": 35.4, + "description": "The Foo item", + } + ], + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + } + } + }, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/users/": { + "get": { + "summary": "Get User With Header", + "operationId": "get_user_with_header_users__get", + "parameters": [ + { + "name": "x-custom", + "in": "header", + "required": False, + "schema": {"title": "X-Custom", "type": "string"}, + }, + { + "name": "x-token", + "in": "header", + "required": False, + "schema": {"title": "X-Token", "type": "string"}, + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/cookies/": { + "get": { + "summary": "Get Cookies", + "operationId": "get_cookies_cookies__get", + "parameters": [ + { + "name": "session_id", + "in": "cookie", + "required": False, + "schema": {"title": "Session Id", "type": "string"}, + }, + { + "name": "tracking_id", + "in": "cookie", + "required": False, + "schema": { + "title": "Tracking Id", + "minLength": 10, + "type": "string", + }, + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/items-embed/": { + "post": { + "summary": "Create Item Embed", + "operationId": "create_item_embed_items_embed__post", + "requestBody": { + "content": { + "application/json": { + "schema": pydantic_snapshot( + v1=snapshot( + { + "$ref": "#/components/schemas/Body_create_item_embed_items_embed__post" + } + ), + v2=snapshot( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_create_item_embed_items_embed__post" + } + ], + "title": "Body", + } + ), + ), + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/form-data/": { + "post": { + "summary": "Submit Form", + "operationId": "submit_form_form_data__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": pydantic_snapshot( + v1=snapshot( + { + "$ref": "#/components/schemas/Body_submit_form_form_data__post" + } + ), + v2=snapshot( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_submit_form_form_data__post" + } + ], + "title": "Body", + } + ), + ), + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/upload/": { + "post": { + "summary": "Upload File", + "operationId": "upload_file_upload__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": pydantic_snapshot( + v1=snapshot( + { + "$ref": "#/components/schemas/Body_upload_file_upload__post" + } + ), + v2=snapshot( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_upload_file_upload__post" + } + ], + "title": "Body", + } + ), + ), + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/upload-multiple/": { + "post": { + "summary": "Upload Multiple Files", + "operationId": "upload_multiple_files_upload_multiple__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": pydantic_snapshot( + v1=snapshot( + { + "$ref": "#/components/schemas/Body_upload_multiple_files_upload_multiple__post" + } + ), + v2=snapshot( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_upload_multiple_files_upload_multiple__post" + } + ], + "title": "Body", + } + ), + ), + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_item_embed_items_embed__post": { + "properties": pydantic_snapshot( + v1=snapshot( + {"item": {"$ref": "#/components/schemas/Item"}} + ), + v2=snapshot( + { + "item": { + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + } + } + ), + ), + "type": "object", + "required": ["item"], + "title": "Body_create_item_embed_items_embed__post", + }, + "Body_submit_form_form_data__post": { + "properties": { + "username": { + "type": "string", + "maxLength": 50, + "minLength": 3, + "title": "Username", + }, + "password": { + "type": "string", + "minLength": 8, + "title": "Password", + }, + "email": {"type": "string", "title": "Email"}, + }, + "type": "object", + "required": ["username", "password"], + "title": "Body_submit_form_form_data__post", + }, + "Body_update_item_items__item_id__put": { + "properties": { + "item": pydantic_snapshot( + v1=snapshot({"$ref": "#/components/schemas/Item"}), + v2=snapshot( + { + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + } + ), + ), + "importance": { + "type": "integer", + "maximum": 10.0, + "exclusiveMinimum": 0.0, + "title": "Importance", + }, + }, + "type": "object", + "required": ["item", "importance"], + "title": "Body_update_item_items__item_id__put", + }, + "Body_upload_file_upload__post": { + "properties": { + "file": { + "type": "string", + "format": "binary", + "title": "File", + }, + "description": {"type": "string", "title": "Description"}, + }, + "type": "object", + "required": ["file"], + "title": "Body_upload_file_upload__post", + }, + "Body_upload_multiple_files_upload_multiple__post": { + "properties": { + "files": { + "items": {"type": "string", "format": "binary"}, + "type": "array", + "title": "Files", + }, + "note": {"type": "string", "title": "Note", "default": ""}, + }, + "type": "object", + "required": ["files"], + "title": "Body_upload_multiple_files_upload_multiple__post", + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "price": {"type": "number", "title": "Price"}, + "description": {"type": "string", "title": "Description"}, + }, + "type": "object", + "required": ["name", "price"], + "title": "Item", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } + ) diff --git a/tests/test_get_model_definitions_formfeed_escape.py b/tests/test_get_model_definitions_formfeed_escape.py index f77195dc5..439e6d448 100644 --- a/tests/test_get_model_definitions_formfeed_escape.py +++ b/tests/test_get_model_definitions_formfeed_escape.py @@ -5,6 +5,7 @@ import fastapi.openapi.utils import pydantic.schema import pytest from fastapi import FastAPI +from fastapi._compat import v1 from pydantic import BaseModel from starlette.testclient import TestClient @@ -166,14 +167,12 @@ def test_model_description_escaped_with_formfeed(sort_reversed: bool): """ all_fields = fastapi.openapi.utils.get_fields_from_routes(app.routes) - flat_models = fastapi._compat.get_flat_models_from_fields( - all_fields, known_models=set() - ) + flat_models = v1.get_flat_models_from_fields(all_fields, known_models=set()) model_name_map = pydantic.schema.get_model_name_map(flat_models) expected_address_description = "This is a public description of an Address\n" - models = fastapi._compat.get_model_definitions( + models = v1.get_model_definitions( flat_models=SortedTypeSet(flat_models, sort_reversed=sort_reversed), model_name_map=model_name_map, ) diff --git a/tests/test_openapi_separate_input_output_schemas.py b/tests/test_openapi_separate_input_output_schemas.py index f7e045259..fa73620ea 100644 --- a/tests/test_openapi_separate_input_output_schemas.py +++ b/tests/test_openapi_separate_input_output_schemas.py @@ -2,6 +2,7 @@ from typing import List, Optional from fastapi import FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel from .utils import PYDANTIC_V2, needs_pydanticv2 @@ -135,217 +136,223 @@ def test_openapi_schema(): client = get_app_client() response = client.get("/openapi.json") assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "responses": { - "200": { - "description": "Successful Response", + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Item-Output" + }, + "type": "array", + "title": "Response Read Items Items Get", + } + } + }, + } + }, + }, + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item-Input" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item-Output" + } + } + }, + }, + "402": { + "description": "Payment Required", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item-Output" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/items-list/": { + "post": { + "summary": "Create Item List", + "operationId": "create_item_list_items_list__post", + "requestBody": { "content": { "application/json": { "schema": { "items": { - "$ref": "#/components/schemas/Item-Output" + "$ref": "#/components/schemas/Item-Input" }, "type": "array", - "title": "Response Read Items Items Get", + "title": "Item", } } }, - } - }, - }, - "post": { - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item-Input"} - } + "required": True, }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Item-Output" + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } } - } + }, }, }, - "402": { - "description": "Payment Required", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Item-Output" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, + } }, }, - "/items-list/": { - "post": { - "summary": "Create Item List", - "operationId": "create_item_list_items_list__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/Item-Input" - }, - "type": "array", - "title": "Item", - } + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", } }, - "required": True, + "type": "object", + "title": "HTTPValidationError", }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } + "Item-Input": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + "sub": { + "anyOf": [ + {"$ref": "#/components/schemas/SubItem-Input"}, + {"type": "null"}, + ] }, }, + "type": "object", + "required": ["name"], + "title": "Item", + }, + "Item-Output": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + "sub": { + "anyOf": [ + {"$ref": "#/components/schemas/SubItem-Output"}, + {"type": "null"}, + ] + }, + }, + "type": "object", + "required": ["name", "description", "sub"], + "title": "Item", + }, + "SubItem-Input": { + "properties": { + "subname": {"type": "string", "title": "Subname"}, + "sub_description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Sub Description", + }, + "tags": { + "items": {"type": "string"}, + "type": "array", + "title": "Tags", + "default": [], + }, + }, + "type": "object", + "required": ["subname"], + "title": "SubItem", + }, + "SubItem-Output": { + "properties": { + "subname": {"type": "string", "title": "Subname"}, + "sub_description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Sub Description", + }, + "tags": { + "items": {"type": "string"}, + "type": "array", + "title": "Tags", + "default": [], + }, + }, + "type": "object", + "required": ["subname", "sub_description", "tags"], + "title": "SubItem", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", }, } }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": {"$ref": "#/components/schemas/ValidationError"}, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item-Input": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Description", - }, - "sub": { - "anyOf": [ - {"$ref": "#/components/schemas/SubItem-Input"}, - {"type": "null"}, - ] - }, - }, - "type": "object", - "required": ["name"], - "title": "Item", - }, - "Item-Output": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Description", - }, - "sub": { - "anyOf": [ - {"$ref": "#/components/schemas/SubItem-Output"}, - {"type": "null"}, - ] - }, - }, - "type": "object", - "required": ["name", "description", "sub"], - "title": "Item", - }, - "SubItem-Input": { - "properties": { - "subname": {"type": "string", "title": "Subname"}, - "sub_description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Sub Description", - }, - "tags": { - "items": {"type": "string"}, - "type": "array", - "title": "Tags", - "default": [], - }, - }, - "type": "object", - "required": ["subname"], - "title": "SubItem", - }, - "SubItem-Output": { - "properties": { - "subname": {"type": "string", "title": "Subname"}, - "sub_description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Sub Description", - }, - "tags": { - "items": {"type": "string"}, - "type": "array", - "title": "Tags", - "default": [], - }, - }, - "type": "object", - "required": ["subname", "sub_description", "tags"], - "title": "SubItem", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } + } + ) @needs_pydanticv2 diff --git a/tests/test_pydantic_v1_v2_01.py b/tests/test_pydantic_v1_v2_01.py new file mode 100644 index 000000000..769e5fab6 --- /dev/null +++ b/tests/test_pydantic_v1_v2_01.py @@ -0,0 +1,475 @@ +import sys +from typing import Any, List, Union + +from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314 + +if sys.version_info >= (3, 14): + skip_module_if_py_gte_314() + +from fastapi import FastAPI +from fastapi._compat.v1 import BaseModel +from fastapi.testclient import TestClient +from inline_snapshot import snapshot + + +class SubItem(BaseModel): + name: str + + +class Item(BaseModel): + title: str + size: int + description: Union[str, None] = None + sub: SubItem + multi: List[SubItem] = [] + + +app = FastAPI() + + +@app.post("/simple-model") +def handle_simple_model(data: SubItem) -> SubItem: + return data + + +@app.post("/simple-model-filter", response_model=SubItem) +def handle_simple_model_filter(data: SubItem) -> Any: + extended_data = data.dict() + extended_data.update({"secret_price": 42}) + return extended_data + + +@app.post("/item") +def handle_item(data: Item) -> Item: + return data + + +@app.post("/item-filter", response_model=Item) +def handle_item_filter(data: Item) -> Any: + extended_data = data.dict() + extended_data.update({"secret_data": "classified", "internal_id": 12345}) + extended_data["sub"].update({"internal_id": 67890}) + return extended_data + + +client = TestClient(app) + + +def test_old_simple_model(): + response = client.post( + "/simple-model", + json={"name": "Foo"}, + ) + assert response.status_code == 200, response.text + assert response.json() == {"name": "Foo"} + + +def test_old_simple_model_validation_error(): + response = client.post( + "/simple-model", + json={"wrong_name": "Foo"}, + ) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": [ + { + "loc": ["body", "name"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_old_simple_model_filter(): + response = client.post( + "/simple-model-filter", + json={"name": "Foo"}, + ) + assert response.status_code == 200, response.text + assert response.json() == {"name": "Foo"} + + +def test_item_model(): + response = client.post( + "/item", + json={ + "title": "Test Item", + "size": 100, + "description": "This is a test item", + "sub": {"name": "SubItem1"}, + "multi": [{"name": "Multi1"}, {"name": "Multi2"}], + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "title": "Test Item", + "size": 100, + "description": "This is a test item", + "sub": {"name": "SubItem1"}, + "multi": [{"name": "Multi1"}, {"name": "Multi2"}], + } + + +def test_item_model_minimal(): + response = client.post( + "/item", + json={"title": "Minimal Item", "size": 50, "sub": {"name": "SubMin"}}, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "title": "Minimal Item", + "size": 50, + "description": None, + "sub": {"name": "SubMin"}, + "multi": [], + } + + +def test_item_model_validation_errors(): + response = client.post( + "/item", + json={"title": "Missing fields"}, + ) + assert response.status_code == 422, response.text + error_detail = response.json()["detail"] + assert len(error_detail) == 2 + assert { + "loc": ["body", "size"], + "msg": "field required", + "type": "value_error.missing", + } in error_detail + assert { + "loc": ["body", "sub"], + "msg": "field required", + "type": "value_error.missing", + } in error_detail + + +def test_item_model_nested_validation_error(): + response = client.post( + "/item", + json={"title": "Test Item", "size": 100, "sub": {"wrong_field": "test"}}, + ) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": [ + { + "loc": ["body", "sub", "name"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_item_model_invalid_type(): + response = client.post( + "/item", + json={"title": "Test Item", "size": "not_a_number", "sub": {"name": "SubItem"}}, + ) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": [ + { + "loc": ["body", "size"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_item_filter(): + response = client.post( + "/item-filter", + json={ + "title": "Filtered Item", + "size": 200, + "description": "Test filtering", + "sub": {"name": "SubFiltered"}, + "multi": [], + }, + ) + assert response.status_code == 200, response.text + result = response.json() + assert result == { + "title": "Filtered Item", + "size": 200, + "description": "Test filtering", + "sub": {"name": "SubFiltered"}, + "multi": [], + } + assert "secret_data" not in result + assert "internal_id" not in result + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/simple-model": { + "post": { + "summary": "Handle Simple Model", + "operationId": "handle_simple_model_simple_model_post", + "requestBody": { + "content": { + "application/json": { + "schema": pydantic_snapshot( + v2=snapshot( + { + "allOf": [ + { + "$ref": "#/components/schemas/SubItem" + } + ], + "title": "Data", + } + ), + v1=snapshot( + {"$ref": "#/components/schemas/SubItem"} + ), + ) + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubItem" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/simple-model-filter": { + "post": { + "summary": "Handle Simple Model Filter", + "operationId": "handle_simple_model_filter_simple_model_filter_post", + "requestBody": { + "content": { + "application/json": { + "schema": pydantic_snapshot( + v2=snapshot( + { + "allOf": [ + { + "$ref": "#/components/schemas/SubItem" + } + ], + "title": "Data", + } + ), + v1=snapshot( + {"$ref": "#/components/schemas/SubItem"} + ), + ) + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubItem" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/item": { + "post": { + "summary": "Handle Item", + "operationId": "handle_item_item_post", + "requestBody": { + "content": { + "application/json": { + "schema": pydantic_snapshot( + v2=snapshot( + { + "allOf": [ + { + "$ref": "#/components/schemas/Item" + } + ], + "title": "Data", + } + ), + v1=snapshot( + {"$ref": "#/components/schemas/Item"} + ), + ) + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/item-filter": { + "post": { + "summary": "Handle Item Filter", + "operationId": "handle_item_filter_item_filter_post", + "requestBody": { + "content": { + "application/json": { + "schema": pydantic_snapshot( + v2=snapshot( + { + "allOf": [ + { + "$ref": "#/components/schemas/Item" + } + ], + "title": "Data", + } + ), + v1=snapshot( + {"$ref": "#/components/schemas/Item"} + ), + ) + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "title": {"type": "string", "title": "Title"}, + "size": {"type": "integer", "title": "Size"}, + "description": {"type": "string", "title": "Description"}, + "sub": {"$ref": "#/components/schemas/SubItem"}, + "multi": { + "items": {"$ref": "#/components/schemas/SubItem"}, + "type": "array", + "title": "Multi", + "default": [], + }, + }, + "type": "object", + "required": ["title", "size", "sub"], + "title": "Item", + }, + "SubItem": { + "properties": {"name": {"type": "string", "title": "Name"}}, + "type": "object", + "required": ["name"], + "title": "SubItem", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } + ) diff --git a/tests/test_pydantic_v1_v2_list.py b/tests/test_pydantic_v1_v2_list.py new file mode 100644 index 000000000..64f3dd344 --- /dev/null +++ b/tests/test_pydantic_v1_v2_list.py @@ -0,0 +1,701 @@ +import sys +from typing import Any, List, Union + +from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314 + +if sys.version_info >= (3, 14): + skip_module_if_py_gte_314() + +from fastapi import FastAPI +from fastapi._compat.v1 import BaseModel +from fastapi.testclient import TestClient +from inline_snapshot import snapshot + + +class SubItem(BaseModel): + name: str + + +class Item(BaseModel): + title: str + size: int + description: Union[str, None] = None + sub: SubItem + multi: List[SubItem] = [] + + +app = FastAPI() + + +@app.post("/item") +def handle_item(data: Item) -> List[Item]: + return [data, data] + + +@app.post("/item-filter", response_model=List[Item]) +def handle_item_filter(data: Item) -> Any: + extended_data = data.dict() + extended_data.update({"secret_data": "classified", "internal_id": 12345}) + extended_data["sub"].update({"internal_id": 67890}) + return [extended_data, extended_data] + + +@app.post("/item-list") +def handle_item_list(data: List[Item]) -> Item: + if data: + return data[0] + return Item(title="", size=0, sub=SubItem(name="")) + + +@app.post("/item-list-filter", response_model=Item) +def handle_item_list_filter(data: List[Item]) -> Any: + if data: + extended_data = data[0].dict() + extended_data.update({"secret_data": "classified", "internal_id": 12345}) + extended_data["sub"].update({"internal_id": 67890}) + return extended_data + return Item(title="", size=0, sub=SubItem(name="")) + + +@app.post("/item-list-to-list") +def handle_item_list_to_list(data: List[Item]) -> List[Item]: + return data + + +@app.post("/item-list-to-list-filter", response_model=List[Item]) +def handle_item_list_to_list_filter(data: List[Item]) -> Any: + if data: + extended_data = data[0].dict() + extended_data.update({"secret_data": "classified", "internal_id": 12345}) + extended_data["sub"].update({"internal_id": 67890}) + return [extended_data, extended_data] + return [] + + +client = TestClient(app) + + +def test_item_to_list(): + response = client.post( + "/item", + json={ + "title": "Test Item", + "size": 100, + "description": "This is a test item", + "sub": {"name": "SubItem1"}, + "multi": [{"name": "Multi1"}, {"name": "Multi2"}], + }, + ) + assert response.status_code == 200, response.text + result = response.json() + assert isinstance(result, list) + assert len(result) == 2 + for item in result: + assert item == { + "title": "Test Item", + "size": 100, + "description": "This is a test item", + "sub": {"name": "SubItem1"}, + "multi": [{"name": "Multi1"}, {"name": "Multi2"}], + } + + +def test_item_to_list_filter(): + response = client.post( + "/item-filter", + json={ + "title": "Filtered Item", + "size": 200, + "description": "Test filtering", + "sub": {"name": "SubFiltered"}, + "multi": [], + }, + ) + assert response.status_code == 200, response.text + result = response.json() + assert isinstance(result, list) + assert len(result) == 2 + for item in result: + assert item == { + "title": "Filtered Item", + "size": 200, + "description": "Test filtering", + "sub": {"name": "SubFiltered"}, + "multi": [], + } + # Verify secret fields are filtered out + assert "secret_data" not in item + assert "internal_id" not in item + assert "internal_id" not in item["sub"] + + +def test_list_to_item(): + response = client.post( + "/item-list", + json=[ + {"title": "First Item", "size": 50, "sub": {"name": "First Sub"}}, + {"title": "Second Item", "size": 75, "sub": {"name": "Second Sub"}}, + ], + ) + assert response.status_code == 200, response.text + assert response.json() == { + "title": "First Item", + "size": 50, + "description": None, + "sub": {"name": "First Sub"}, + "multi": [], + } + + +def test_list_to_item_empty(): + response = client.post( + "/item-list", + json=[], + ) + assert response.status_code == 200, response.text + assert response.json() == { + "title": "", + "size": 0, + "description": None, + "sub": {"name": ""}, + "multi": [], + } + + +def test_list_to_item_filter(): + response = client.post( + "/item-list-filter", + json=[ + { + "title": "First Item", + "size": 100, + "sub": {"name": "First Sub"}, + "multi": [{"name": "Multi1"}], + }, + {"title": "Second Item", "size": 200, "sub": {"name": "Second Sub"}}, + ], + ) + assert response.status_code == 200, response.text + result = response.json() + assert result == { + "title": "First Item", + "size": 100, + "description": None, + "sub": {"name": "First Sub"}, + "multi": [{"name": "Multi1"}], + } + # Verify secret fields are filtered out + assert "secret_data" not in result + assert "internal_id" not in result + + +def test_list_to_item_filter_no_data(): + response = client.post("/item-list-filter", json=[]) + assert response.status_code == 200, response.text + assert response.json() == { + "title": "", + "size": 0, + "description": None, + "sub": {"name": ""}, + "multi": [], + } + + +def test_list_to_list(): + input_items = [ + {"title": "Item 1", "size": 10, "sub": {"name": "Sub1"}}, + { + "title": "Item 2", + "size": 20, + "description": "Second item", + "sub": {"name": "Sub2"}, + "multi": [{"name": "M1"}, {"name": "M2"}], + }, + {"title": "Item 3", "size": 30, "sub": {"name": "Sub3"}}, + ] + response = client.post( + "/item-list-to-list", + json=input_items, + ) + assert response.status_code == 200, response.text + result = response.json() + assert isinstance(result, list) + assert len(result) == 3 + assert result[0] == { + "title": "Item 1", + "size": 10, + "description": None, + "sub": {"name": "Sub1"}, + "multi": [], + } + assert result[1] == { + "title": "Item 2", + "size": 20, + "description": "Second item", + "sub": {"name": "Sub2"}, + "multi": [{"name": "M1"}, {"name": "M2"}], + } + assert result[2] == { + "title": "Item 3", + "size": 30, + "description": None, + "sub": {"name": "Sub3"}, + "multi": [], + } + + +def test_list_to_list_filter(): + response = client.post( + "/item-list-to-list-filter", + json=[{"title": "Item 1", "size": 100, "sub": {"name": "Sub1"}}], + ) + assert response.status_code == 200, response.text + result = response.json() + assert isinstance(result, list) + assert len(result) == 2 + for item in result: + assert item == { + "title": "Item 1", + "size": 100, + "description": None, + "sub": {"name": "Sub1"}, + "multi": [], + } + # Verify secret fields are filtered out + assert "secret_data" not in item + assert "internal_id" not in item + + +def test_list_to_list_filter_no_data(): + response = client.post( + "/item-list-to-list-filter", + json=[], + ) + assert response.status_code == 200, response.text + assert response.json() == [] + + +def test_list_validation_error(): + response = client.post( + "/item-list", + json=[ + {"title": "Valid Item", "size": 100, "sub": {"name": "Sub1"}}, + { + "title": "Invalid Item" + # Missing required fields: size and sub + }, + ], + ) + assert response.status_code == 422, response.text + error_detail = response.json()["detail"] + assert len(error_detail) == 2 + assert { + "loc": ["body", 1, "size"], + "msg": "field required", + "type": "value_error.missing", + } in error_detail + assert { + "loc": ["body", 1, "sub"], + "msg": "field required", + "type": "value_error.missing", + } in error_detail + + +def test_list_nested_validation_error(): + response = client.post( + "/item-list", + json=[ + {"title": "Item with bad sub", "size": 100, "sub": {"wrong_field": "value"}} + ], + ) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": [ + { + "loc": ["body", 0, "sub", "name"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_list_type_validation_error(): + response = client.post( + "/item-list", + json=[{"title": "Item", "size": "not_a_number", "sub": {"name": "Sub"}}], + ) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": [ + { + "loc": ["body", 0, "size"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_invalid_list_structure(): + response = client.post( + "/item-list", + json={"title": "Not a list", "size": 100, "sub": {"name": "Sub"}}, + ) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": [ + { + "loc": ["body"], + "msg": "value is not a valid list", + "type": "type_error.list", + } + ] + } + ) + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/item": { + "post": { + "summary": "Handle Item", + "operationId": "handle_item_item_post", + "requestBody": { + "content": { + "application/json": { + "schema": pydantic_snapshot( + v2=snapshot( + { + "allOf": [ + { + "$ref": "#/components/schemas/Item" + } + ], + "title": "Data", + } + ), + v1=snapshot( + {"$ref": "#/components/schemas/Item"} + ), + ) + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Item" + }, + "type": "array", + "title": "Response Handle Item Item Post", + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/item-filter": { + "post": { + "summary": "Handle Item Filter", + "operationId": "handle_item_filter_item_filter_post", + "requestBody": { + "content": { + "application/json": { + "schema": pydantic_snapshot( + v2=snapshot( + { + "allOf": [ + { + "$ref": "#/components/schemas/Item" + } + ], + "title": "Data", + } + ), + v1=snapshot( + {"$ref": "#/components/schemas/Item"} + ), + ) + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Item" + }, + "type": "array", + "title": "Response Handle Item Filter Item Filter Post", + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/item-list": { + "post": { + "summary": "Handle Item List", + "operationId": "handle_item_list_item_list_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": {"$ref": "#/components/schemas/Item"}, + "type": "array", + "title": "Data", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/item-list-filter": { + "post": { + "summary": "Handle Item List Filter", + "operationId": "handle_item_list_filter_item_list_filter_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": {"$ref": "#/components/schemas/Item"}, + "type": "array", + "title": "Data", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/item-list-to-list": { + "post": { + "summary": "Handle Item List To List", + "operationId": "handle_item_list_to_list_item_list_to_list_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": {"$ref": "#/components/schemas/Item"}, + "type": "array", + "title": "Data", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Item" + }, + "type": "array", + "title": "Response Handle Item List To List Item List To List Post", + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/item-list-to-list-filter": { + "post": { + "summary": "Handle Item List To List Filter", + "operationId": "handle_item_list_to_list_filter_item_list_to_list_filter_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": {"$ref": "#/components/schemas/Item"}, + "type": "array", + "title": "Data", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Item" + }, + "type": "array", + "title": "Response Handle Item List To List Filter Item List To List Filter Post", + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "title": {"type": "string", "title": "Title"}, + "size": {"type": "integer", "title": "Size"}, + "description": {"type": "string", "title": "Description"}, + "sub": {"$ref": "#/components/schemas/SubItem"}, + "multi": { + "items": {"$ref": "#/components/schemas/SubItem"}, + "type": "array", + "title": "Multi", + "default": [], + }, + }, + "type": "object", + "required": ["title", "size", "sub"], + "title": "Item", + }, + "SubItem": { + "properties": {"name": {"type": "string", "title": "Name"}}, + "type": "object", + "required": ["name"], + "title": "SubItem", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } + ) diff --git a/tests/test_pydantic_v1_v2_mixed.py b/tests/test_pydantic_v1_v2_mixed.py new file mode 100644 index 000000000..54d408827 --- /dev/null +++ b/tests/test_pydantic_v1_v2_mixed.py @@ -0,0 +1,1499 @@ +import sys +from typing import Any, List, Union + +from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314 + +if sys.version_info >= (3, 14): + skip_module_if_py_gte_314() + +from fastapi import FastAPI +from fastapi._compat.v1 import BaseModel +from fastapi.testclient import TestClient +from inline_snapshot import snapshot +from pydantic import BaseModel as NewBaseModel + + +class SubItem(BaseModel): + name: str + + +class Item(BaseModel): + title: str + size: int + description: Union[str, None] = None + sub: SubItem + multi: List[SubItem] = [] + + +class NewSubItem(NewBaseModel): + new_sub_name: str + + +class NewItem(NewBaseModel): + new_title: str + new_size: int + new_description: Union[str, None] = None + new_sub: NewSubItem + new_multi: List[NewSubItem] = [] + + +app = FastAPI() + + +@app.post("/v1-to-v2/item") +def handle_v1_item_to_v2(data: Item) -> NewItem: + return NewItem( + new_title=data.title, + new_size=data.size, + new_description=data.description, + new_sub=NewSubItem(new_sub_name=data.sub.name), + new_multi=[NewSubItem(new_sub_name=s.name) for s in data.multi], + ) + + +@app.post("/v1-to-v2/item-filter", response_model=NewItem) +def handle_v1_item_to_v2_filter(data: Item) -> Any: + result = { + "new_title": data.title, + "new_size": data.size, + "new_description": data.description, + "new_sub": {"new_sub_name": data.sub.name, "new_sub_secret": "sub_hidden"}, + "new_multi": [ + {"new_sub_name": s.name, "new_sub_secret": "sub_hidden"} for s in data.multi + ], + "secret": "hidden_v1_to_v2", + } + return result + + +@app.post("/v2-to-v1/item") +def handle_v2_item_to_v1(data: NewItem) -> Item: + return Item( + title=data.new_title, + size=data.new_size, + description=data.new_description, + sub=SubItem(name=data.new_sub.new_sub_name), + multi=[SubItem(name=s.new_sub_name) for s in data.new_multi], + ) + + +@app.post("/v2-to-v1/item-filter", response_model=Item) +def handle_v2_item_to_v1_filter(data: NewItem) -> Any: + result = { + "title": data.new_title, + "size": data.new_size, + "description": data.new_description, + "sub": {"name": data.new_sub.new_sub_name, "sub_secret": "sub_hidden"}, + "multi": [ + {"name": s.new_sub_name, "sub_secret": "sub_hidden"} for s in data.new_multi + ], + "secret": "hidden_v2_to_v1", + } + return result + + +@app.post("/v1-to-v2/item-to-list") +def handle_v1_item_to_v2_list(data: Item) -> List[NewItem]: + converted = NewItem( + new_title=data.title, + new_size=data.size, + new_description=data.description, + new_sub=NewSubItem(new_sub_name=data.sub.name), + new_multi=[NewSubItem(new_sub_name=s.name) for s in data.multi], + ) + return [converted, converted] + + +@app.post("/v1-to-v2/list-to-list") +def handle_v1_list_to_v2_list(data: List[Item]) -> List[NewItem]: + result = [] + for item in data: + result.append( + NewItem( + new_title=item.title, + new_size=item.size, + new_description=item.description, + new_sub=NewSubItem(new_sub_name=item.sub.name), + new_multi=[NewSubItem(new_sub_name=s.name) for s in item.multi], + ) + ) + return result + + +@app.post("/v1-to-v2/list-to-list-filter", response_model=List[NewItem]) +def handle_v1_list_to_v2_list_filter(data: List[Item]) -> Any: + result = [] + for item in data: + converted = { + "new_title": item.title, + "new_size": item.size, + "new_description": item.description, + "new_sub": {"new_sub_name": item.sub.name, "new_sub_secret": "sub_hidden"}, + "new_multi": [ + {"new_sub_name": s.name, "new_sub_secret": "sub_hidden"} + for s in item.multi + ], + "secret": "hidden_v2_to_v1", + } + result.append(converted) + return result + + +@app.post("/v1-to-v2/list-to-item") +def handle_v1_list_to_v2_item(data: List[Item]) -> NewItem: + if data: + item = data[0] + return NewItem( + new_title=item.title, + new_size=item.size, + new_description=item.description, + new_sub=NewSubItem(new_sub_name=item.sub.name), + new_multi=[NewSubItem(new_sub_name=s.name) for s in item.multi], + ) + return NewItem(new_title="", new_size=0, new_sub=NewSubItem(new_sub_name="")) + + +@app.post("/v2-to-v1/item-to-list") +def handle_v2_item_to_v1_list(data: NewItem) -> List[Item]: + converted = Item( + title=data.new_title, + size=data.new_size, + description=data.new_description, + sub=SubItem(name=data.new_sub.new_sub_name), + multi=[SubItem(name=s.new_sub_name) for s in data.new_multi], + ) + return [converted, converted] + + +@app.post("/v2-to-v1/list-to-list") +def handle_v2_list_to_v1_list(data: List[NewItem]) -> List[Item]: + result = [] + for item in data: + result.append( + Item( + title=item.new_title, + size=item.new_size, + description=item.new_description, + sub=SubItem(name=item.new_sub.new_sub_name), + multi=[SubItem(name=s.new_sub_name) for s in item.new_multi], + ) + ) + return result + + +@app.post("/v2-to-v1/list-to-list-filter", response_model=List[Item]) +def handle_v2_list_to_v1_list_filter(data: List[NewItem]) -> Any: + result = [] + for item in data: + converted = { + "title": item.new_title, + "size": item.new_size, + "description": item.new_description, + "sub": {"name": item.new_sub.new_sub_name, "sub_secret": "sub_hidden"}, + "multi": [ + {"name": s.new_sub_name, "sub_secret": "sub_hidden"} + for s in item.new_multi + ], + "secret": "hidden_v2_to_v1", + } + result.append(converted) + return result + + +@app.post("/v2-to-v1/list-to-item") +def handle_v2_list_to_v1_item(data: List[NewItem]) -> Item: + if data: + item = data[0] + return Item( + title=item.new_title, + size=item.new_size, + description=item.new_description, + sub=SubItem(name=item.new_sub.new_sub_name), + multi=[SubItem(name=s.new_sub_name) for s in item.new_multi], + ) + return Item(title="", size=0, sub=SubItem(name="")) + + +client = TestClient(app) + + +def test_v1_to_v2_item(): + response = client.post( + "/v1-to-v2/item", + json={ + "title": "Old Item", + "size": 100, + "description": "V1 description", + "sub": {"name": "V1 Sub"}, + "multi": [{"name": "M1"}, {"name": "M2"}], + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "new_title": "Old Item", + "new_size": 100, + "new_description": "V1 description", + "new_sub": {"new_sub_name": "V1 Sub"}, + "new_multi": [{"new_sub_name": "M1"}, {"new_sub_name": "M2"}], + } + + +def test_v1_to_v2_item_minimal(): + response = client.post( + "/v1-to-v2/item", + json={"title": "Minimal", "size": 50, "sub": {"name": "MinSub"}}, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "new_title": "Minimal", + "new_size": 50, + "new_description": None, + "new_sub": {"new_sub_name": "MinSub"}, + "new_multi": [], + } + + +def test_v1_to_v2_item_filter(): + response = client.post( + "/v1-to-v2/item-filter", + json={ + "title": "Filtered Item", + "size": 50, + "sub": {"name": "Sub"}, + "multi": [{"name": "Multi1"}], + }, + ) + assert response.status_code == 200, response.text + result = response.json() + assert result == snapshot( + { + "new_title": "Filtered Item", + "new_size": 50, + "new_description": None, + "new_sub": {"new_sub_name": "Sub"}, + "new_multi": [{"new_sub_name": "Multi1"}], + } + ) + # Verify secret fields are filtered out + assert "secret" not in result + assert "new_sub_secret" not in result["new_sub"] + assert "new_sub_secret" not in result["new_multi"][0] + + +def test_v2_to_v1_item(): + response = client.post( + "/v2-to-v1/item", + json={ + "new_title": "New Item", + "new_size": 200, + "new_description": "V2 description", + "new_sub": {"new_sub_name": "V2 Sub"}, + "new_multi": [{"new_sub_name": "N1"}, {"new_sub_name": "N2"}], + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "title": "New Item", + "size": 200, + "description": "V2 description", + "sub": {"name": "V2 Sub"}, + "multi": [{"name": "N1"}, {"name": "N2"}], + } + + +def test_v2_to_v1_item_minimal(): + response = client.post( + "/v2-to-v1/item", + json={ + "new_title": "MinimalNew", + "new_size": 75, + "new_sub": {"new_sub_name": "MinNewSub"}, + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "title": "MinimalNew", + "size": 75, + "description": None, + "sub": {"name": "MinNewSub"}, + "multi": [], + } + + +def test_v2_to_v1_item_filter(): + response = client.post( + "/v2-to-v1/item-filter", + json={ + "new_title": "Filtered New", + "new_size": 75, + "new_sub": {"new_sub_name": "NewSub"}, + "new_multi": [], + }, + ) + assert response.status_code == 200, response.text + result = response.json() + assert result == snapshot( + { + "title": "Filtered New", + "size": 75, + "description": None, + "sub": {"name": "NewSub"}, + "multi": [], + } + ) + # Verify secret fields are filtered out + assert "secret" not in result + assert "sub_secret" not in result["sub"] + + +def test_v1_item_to_v2_list(): + response = client.post( + "/v1-to-v2/item-to-list", + json={ + "title": "Single to List", + "size": 150, + "description": "Convert to list", + "sub": {"name": "Sub1"}, + "multi": [], + }, + ) + assert response.status_code == 200, response.text + result = response.json() + assert result == [ + { + "new_title": "Single to List", + "new_size": 150, + "new_description": "Convert to list", + "new_sub": {"new_sub_name": "Sub1"}, + "new_multi": [], + }, + { + "new_title": "Single to List", + "new_size": 150, + "new_description": "Convert to list", + "new_sub": {"new_sub_name": "Sub1"}, + "new_multi": [], + }, + ] + + +def test_v1_list_to_v2_list(): + response = client.post( + "/v1-to-v2/list-to-list", + json=[ + {"title": "Item1", "size": 10, "sub": {"name": "Sub1"}}, + { + "title": "Item2", + "size": 20, + "description": "Second item", + "sub": {"name": "Sub2"}, + "multi": [{"name": "M1"}, {"name": "M2"}], + }, + {"title": "Item3", "size": 30, "sub": {"name": "Sub3"}}, + ], + ) + assert response.status_code == 200, response.text + assert response.json() == [ + { + "new_title": "Item1", + "new_size": 10, + "new_description": None, + "new_sub": {"new_sub_name": "Sub1"}, + "new_multi": [], + }, + { + "new_title": "Item2", + "new_size": 20, + "new_description": "Second item", + "new_sub": {"new_sub_name": "Sub2"}, + "new_multi": [{"new_sub_name": "M1"}, {"new_sub_name": "M2"}], + }, + { + "new_title": "Item3", + "new_size": 30, + "new_description": None, + "new_sub": {"new_sub_name": "Sub3"}, + "new_multi": [], + }, + ] + + +def test_v1_list_to_v2_list_filter(): + response = client.post( + "/v1-to-v2/list-to-list-filter", + json=[{"title": "FilterMe", "size": 30, "sub": {"name": "SubF"}}], + ) + assert response.status_code == 200, response.text + result = response.json() + assert result == snapshot( + [ + { + "new_title": "FilterMe", + "new_size": 30, + "new_description": None, + "new_sub": {"new_sub_name": "SubF"}, + "new_multi": [], + } + ] + ) + # Verify secret fields are filtered out + assert "secret" not in result[0] + assert "new_sub_secret" not in result[0]["new_sub"] + + +def test_v1_list_to_v2_item(): + response = client.post( + "/v1-to-v2/list-to-item", + json=[ + {"title": "First", "size": 100, "sub": {"name": "FirstSub"}}, + {"title": "Second", "size": 200, "sub": {"name": "SecondSub"}}, + ], + ) + assert response.status_code == 200, response.text + assert response.json() == { + "new_title": "First", + "new_size": 100, + "new_description": None, + "new_sub": {"new_sub_name": "FirstSub"}, + "new_multi": [], + } + + +def test_v1_list_to_v2_item_empty(): + response = client.post("/v1-to-v2/list-to-item", json=[]) + assert response.status_code == 200, response.text + assert response.json() == { + "new_title": "", + "new_size": 0, + "new_description": None, + "new_sub": {"new_sub_name": ""}, + "new_multi": [], + } + + +def test_v2_item_to_v1_list(): + response = client.post( + "/v2-to-v1/item-to-list", + json={ + "new_title": "Single New", + "new_size": 250, + "new_description": "New to list", + "new_sub": {"new_sub_name": "NewSub"}, + "new_multi": [], + }, + ) + assert response.status_code == 200, response.text + assert response.json() == [ + { + "title": "Single New", + "size": 250, + "description": "New to list", + "sub": {"name": "NewSub"}, + "multi": [], + }, + { + "title": "Single New", + "size": 250, + "description": "New to list", + "sub": {"name": "NewSub"}, + "multi": [], + }, + ] + + +def test_v2_list_to_v1_list(): + response = client.post( + "/v2-to-v1/list-to-list", + json=[ + {"new_title": "New1", "new_size": 15, "new_sub": {"new_sub_name": "NS1"}}, + { + "new_title": "New2", + "new_size": 25, + "new_description": "Second new", + "new_sub": {"new_sub_name": "NS2"}, + "new_multi": [{"new_sub_name": "NM1"}], + }, + ], + ) + assert response.status_code == 200, response.text + assert response.json() == [ + { + "title": "New1", + "size": 15, + "description": None, + "sub": {"name": "NS1"}, + "multi": [], + }, + { + "title": "New2", + "size": 25, + "description": "Second new", + "sub": {"name": "NS2"}, + "multi": [{"name": "NM1"}], + }, + ] + + +def test_v2_list_to_v1_list_filter(): + response = client.post( + "/v2-to-v1/list-to-list-filter", + json=[ + { + "new_title": "FilterNew", + "new_size": 35, + "new_sub": {"new_sub_name": "NSF"}, + } + ], + ) + assert response.status_code == 200, response.text + result = response.json() + assert result == snapshot( + [ + { + "title": "FilterNew", + "size": 35, + "description": None, + "sub": {"name": "NSF"}, + "multi": [], + } + ] + ) + # Verify secret fields are filtered out + assert "secret" not in result[0] + assert "sub_secret" not in result[0]["sub"] + + +def test_v2_list_to_v1_item(): + response = client.post( + "/v2-to-v1/list-to-item", + json=[ + { + "new_title": "FirstNew", + "new_size": 300, + "new_sub": {"new_sub_name": "FNS"}, + }, + { + "new_title": "SecondNew", + "new_size": 400, + "new_sub": {"new_sub_name": "SNS"}, + }, + ], + ) + assert response.status_code == 200, response.text + assert response.json() == { + "title": "FirstNew", + "size": 300, + "description": None, + "sub": {"name": "FNS"}, + "multi": [], + } + + +def test_v2_list_to_v1_item_empty(): + response = client.post("/v2-to-v1/list-to-item", json=[]) + assert response.status_code == 200, response.text + assert response.json() == { + "title": "", + "size": 0, + "description": None, + "sub": {"name": ""}, + "multi": [], + } + + +def test_v1_to_v2_validation_error(): + response = client.post("/v1-to-v2/item", json={"title": "Missing fields"}) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": [ + { + "loc": ["body", "size"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "sub"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_v1_to_v2_nested_validation_error(): + response = client.post( + "/v1-to-v2/item", + json={"title": "Bad sub", "size": 100, "sub": {"wrong_field": "value"}}, + ) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": [ + { + "loc": ["body", "sub", "name"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_v1_to_v2_type_validation_error(): + response = client.post( + "/v1-to-v2/item", + json={"title": "Bad type", "size": "not_a_number", "sub": {"name": "Sub"}}, + ) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": [ + { + "loc": ["body", "size"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_v2_to_v1_validation_error(): + response = client.post( + "/v2-to-v1/item", + json={"new_title": "Missing fields"}, + ) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": pydantic_snapshot( + v2=snapshot( + [ + { + "type": "missing", + "loc": ["body", "new_size"], + "msg": "Field required", + "input": {"new_title": "Missing fields"}, + }, + { + "type": "missing", + "loc": ["body", "new_sub"], + "msg": "Field required", + "input": {"new_title": "Missing fields"}, + }, + ] + ), + v1=snapshot( + [ + { + "loc": ["body", "new_size"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "new_sub"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + ), + ) + } + ) + + +def test_v2_to_v1_nested_validation_error(): + response = client.post( + "/v2-to-v1/item", + json={ + "new_title": "Bad sub", + "new_size": 200, + "new_sub": {"wrong_field": "value"}, + }, + ) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": [ + pydantic_snapshot( + v2=snapshot( + { + "type": "missing", + "loc": ["body", "new_sub", "new_sub_name"], + "msg": "Field required", + "input": {"wrong_field": "value"}, + } + ), + v1=snapshot( + { + "loc": ["body", "new_sub", "new_sub_name"], + "msg": "field required", + "type": "value_error.missing", + } + ), + ) + ] + } + ) + + +def test_v1_list_validation_error(): + response = client.post( + "/v1-to-v2/list-to-list", + json=[ + {"title": "Valid", "size": 10, "sub": {"name": "S"}}, + {"title": "Invalid"}, + ], + ) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": [ + { + "loc": ["body", 1, "size"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", 1, "sub"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_v2_list_validation_error(): + response = client.post( + "/v2-to-v1/list-to-list", + json=[ + {"new_title": "Valid", "new_size": 10, "new_sub": {"new_sub_name": "NS"}}, + {"new_title": "Invalid"}, + ], + ) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": pydantic_snapshot( + v2=snapshot( + [ + { + "type": "missing", + "loc": ["body", 1, "new_size"], + "msg": "Field required", + "input": {"new_title": "Invalid"}, + }, + { + "type": "missing", + "loc": ["body", 1, "new_sub"], + "msg": "Field required", + "input": {"new_title": "Invalid"}, + }, + ] + ), + v1=snapshot( + [ + { + "loc": ["body", 1, "new_size"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", 1, "new_sub"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + ), + ) + } + ) + + +def test_invalid_list_structure_v1(): + response = client.post( + "/v1-to-v2/list-to-list", + json={"title": "Not a list", "size": 100, "sub": {"name": "Sub"}}, + ) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": [ + { + "loc": ["body"], + "msg": "value is not a valid list", + "type": "type_error.list", + } + ] + } + ) + + +def test_invalid_list_structure_v2(): + response = client.post( + "/v2-to-v1/list-to-list", + json={ + "new_title": "Not a list", + "new_size": 100, + "new_sub": {"new_sub_name": "Sub"}, + }, + ) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": pydantic_snapshot( + v2=snapshot( + [ + { + "type": "list_type", + "loc": ["body"], + "msg": "Input should be a valid list", + "input": { + "new_title": "Not a list", + "new_size": 100, + "new_sub": {"new_sub_name": "Sub"}, + }, + } + ] + ), + v1=snapshot( + [ + { + "loc": ["body"], + "msg": "value is not a valid list", + "type": "type_error.list", + } + ] + ), + ) + } + ) + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/v1-to-v2/item": { + "post": { + "summary": "Handle V1 Item To V2", + "operationId": "handle_v1_item_to_v2_v1_to_v2_item_post", + "requestBody": { + "content": { + "application/json": { + "schema": pydantic_snapshot( + v2=snapshot( + { + "allOf": [ + { + "$ref": "#/components/schemas/Item" + } + ], + "title": "Data", + } + ), + v1=snapshot( + {"$ref": "#/components/schemas/Item"} + ), + ) + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewItem" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v1-to-v2/item-filter": { + "post": { + "summary": "Handle V1 Item To V2 Filter", + "operationId": "handle_v1_item_to_v2_filter_v1_to_v2_item_filter_post", + "requestBody": { + "content": { + "application/json": { + "schema": pydantic_snapshot( + v2=snapshot( + { + "allOf": [ + { + "$ref": "#/components/schemas/Item" + } + ], + "title": "Data", + } + ), + v1=snapshot( + {"$ref": "#/components/schemas/Item"} + ), + ) + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewItem" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v2-to-v1/item": { + "post": { + "summary": "Handle V2 Item To V1", + "operationId": "handle_v2_item_to_v1_v2_to_v1_item_post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/NewItem"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v2-to-v1/item-filter": { + "post": { + "summary": "Handle V2 Item To V1 Filter", + "operationId": "handle_v2_item_to_v1_filter_v2_to_v1_item_filter_post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/NewItem"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v1-to-v2/item-to-list": { + "post": { + "summary": "Handle V1 Item To V2 List", + "operationId": "handle_v1_item_to_v2_list_v1_to_v2_item_to_list_post", + "requestBody": { + "content": { + "application/json": { + "schema": pydantic_snapshot( + v2=snapshot( + { + "allOf": [ + { + "$ref": "#/components/schemas/Item" + } + ], + "title": "Data", + } + ), + v1=snapshot( + {"$ref": "#/components/schemas/Item"} + ), + ) + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/NewItem" + }, + "type": "array", + "title": "Response Handle V1 Item To V2 List V1 To V2 Item To List Post", + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v1-to-v2/list-to-list": { + "post": { + "summary": "Handle V1 List To V2 List", + "operationId": "handle_v1_list_to_v2_list_v1_to_v2_list_to_list_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": {"$ref": "#/components/schemas/Item"}, + "type": "array", + "title": "Data", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/NewItem" + }, + "type": "array", + "title": "Response Handle V1 List To V2 List V1 To V2 List To List Post", + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v1-to-v2/list-to-list-filter": { + "post": { + "summary": "Handle V1 List To V2 List Filter", + "operationId": "handle_v1_list_to_v2_list_filter_v1_to_v2_list_to_list_filter_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": {"$ref": "#/components/schemas/Item"}, + "type": "array", + "title": "Data", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/NewItem" + }, + "type": "array", + "title": "Response Handle V1 List To V2 List Filter V1 To V2 List To List Filter Post", + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v1-to-v2/list-to-item": { + "post": { + "summary": "Handle V1 List To V2 Item", + "operationId": "handle_v1_list_to_v2_item_v1_to_v2_list_to_item_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": {"$ref": "#/components/schemas/Item"}, + "type": "array", + "title": "Data", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewItem" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v2-to-v1/item-to-list": { + "post": { + "summary": "Handle V2 Item To V1 List", + "operationId": "handle_v2_item_to_v1_list_v2_to_v1_item_to_list_post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/NewItem"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Item" + }, + "type": "array", + "title": "Response Handle V2 Item To V1 List V2 To V1 Item To List Post", + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v2-to-v1/list-to-list": { + "post": { + "summary": "Handle V2 List To V1 List", + "operationId": "handle_v2_list_to_v1_list_v2_to_v1_list_to_list_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/NewItem" + }, + "type": "array", + "title": "Data", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Item" + }, + "type": "array", + "title": "Response Handle V2 List To V1 List V2 To V1 List To List Post", + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v2-to-v1/list-to-list-filter": { + "post": { + "summary": "Handle V2 List To V1 List Filter", + "operationId": "handle_v2_list_to_v1_list_filter_v2_to_v1_list_to_list_filter_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/NewItem" + }, + "type": "array", + "title": "Data", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Item" + }, + "type": "array", + "title": "Response Handle V2 List To V1 List Filter V2 To V1 List To List Filter Post", + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v2-to-v1/list-to-item": { + "post": { + "summary": "Handle V2 List To V1 Item", + "operationId": "handle_v2_list_to_v1_item_v2_to_v1_list_to_item_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/NewItem" + }, + "type": "array", + "title": "Data", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "title": {"type": "string", "title": "Title"}, + "size": {"type": "integer", "title": "Size"}, + "description": {"type": "string", "title": "Description"}, + "sub": {"$ref": "#/components/schemas/SubItem"}, + "multi": { + "items": {"$ref": "#/components/schemas/SubItem"}, + "type": "array", + "title": "Multi", + "default": [], + }, + }, + "type": "object", + "required": ["title", "size", "sub"], + "title": "Item", + }, + "NewItem": { + "properties": { + "new_title": {"type": "string", "title": "New Title"}, + "new_size": {"type": "integer", "title": "New Size"}, + "new_description": pydantic_snapshot( + v2=snapshot( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "New Description", + } + ), + v1=snapshot( + {"type": "string", "title": "New Description"} + ), + ), + "new_sub": {"$ref": "#/components/schemas/NewSubItem"}, + "new_multi": { + "items": {"$ref": "#/components/schemas/NewSubItem"}, + "type": "array", + "title": "New Multi", + "default": [], + }, + }, + "type": "object", + "required": ["new_title", "new_size", "new_sub"], + "title": "NewItem", + }, + "NewSubItem": { + "properties": { + "new_sub_name": {"type": "string", "title": "New Sub Name"} + }, + "type": "object", + "required": ["new_sub_name"], + "title": "NewSubItem", + }, + "SubItem": { + "properties": {"name": {"type": "string", "title": "Name"}}, + "type": "object", + "required": ["name"], + "title": "SubItem", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } + ) diff --git a/tests/test_pydantic_v1_v2_multifile/__init__.py b/tests/test_pydantic_v1_v2_multifile/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_pydantic_v1_v2_multifile/main.py b/tests/test_pydantic_v1_v2_multifile/main.py new file mode 100644 index 000000000..8985cb7b4 --- /dev/null +++ b/tests/test_pydantic_v1_v2_multifile/main.py @@ -0,0 +1,142 @@ +from typing import List + +from fastapi import FastAPI + +from . import modelsv1, modelsv2, modelsv2b + +app = FastAPI() + + +@app.post("/v1-to-v2/item") +def handle_v1_item_to_v2(data: modelsv1.Item) -> modelsv2.Item: + return modelsv2.Item( + new_title=data.title, + new_size=data.size, + new_description=data.description, + new_sub=modelsv2.SubItem(new_sub_name=data.sub.name), + new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in data.multi], + ) + + +@app.post("/v2-to-v1/item") +def handle_v2_item_to_v1(data: modelsv2.Item) -> modelsv1.Item: + return modelsv1.Item( + title=data.new_title, + size=data.new_size, + description=data.new_description, + sub=modelsv1.SubItem(name=data.new_sub.new_sub_name), + multi=[modelsv1.SubItem(name=s.new_sub_name) for s in data.new_multi], + ) + + +@app.post("/v1-to-v2/item-to-list") +def handle_v1_item_to_v2_list(data: modelsv1.Item) -> List[modelsv2.Item]: + converted = modelsv2.Item( + new_title=data.title, + new_size=data.size, + new_description=data.description, + new_sub=modelsv2.SubItem(new_sub_name=data.sub.name), + new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in data.multi], + ) + return [converted, converted] + + +@app.post("/v1-to-v2/list-to-list") +def handle_v1_list_to_v2_list(data: List[modelsv1.Item]) -> List[modelsv2.Item]: + result = [] + for item in data: + result.append( + modelsv2.Item( + new_title=item.title, + new_size=item.size, + new_description=item.description, + new_sub=modelsv2.SubItem(new_sub_name=item.sub.name), + new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in item.multi], + ) + ) + return result + + +@app.post("/v1-to-v2/list-to-item") +def handle_v1_list_to_v2_item(data: List[modelsv1.Item]) -> modelsv2.Item: + if data: + item = data[0] + return modelsv2.Item( + new_title=item.title, + new_size=item.size, + new_description=item.description, + new_sub=modelsv2.SubItem(new_sub_name=item.sub.name), + new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in item.multi], + ) + return modelsv2.Item( + new_title="", new_size=0, new_sub=modelsv2.SubItem(new_sub_name="") + ) + + +@app.post("/v2-to-v1/item-to-list") +def handle_v2_item_to_v1_list(data: modelsv2.Item) -> List[modelsv1.Item]: + converted = modelsv1.Item( + title=data.new_title, + size=data.new_size, + description=data.new_description, + sub=modelsv1.SubItem(name=data.new_sub.new_sub_name), + multi=[modelsv1.SubItem(name=s.new_sub_name) for s in data.new_multi], + ) + return [converted, converted] + + +@app.post("/v2-to-v1/list-to-list") +def handle_v2_list_to_v1_list(data: List[modelsv2.Item]) -> List[modelsv1.Item]: + result = [] + for item in data: + result.append( + modelsv1.Item( + title=item.new_title, + size=item.new_size, + description=item.new_description, + sub=modelsv1.SubItem(name=item.new_sub.new_sub_name), + multi=[modelsv1.SubItem(name=s.new_sub_name) for s in item.new_multi], + ) + ) + return result + + +@app.post("/v2-to-v1/list-to-item") +def handle_v2_list_to_v1_item(data: List[modelsv2.Item]) -> modelsv1.Item: + if data: + item = data[0] + return modelsv1.Item( + title=item.new_title, + size=item.new_size, + description=item.new_description, + sub=modelsv1.SubItem(name=item.new_sub.new_sub_name), + multi=[modelsv1.SubItem(name=s.new_sub_name) for s in item.new_multi], + ) + return modelsv1.Item(title="", size=0, sub=modelsv1.SubItem(name="")) + + +@app.post("/v2-to-v1/same-name") +def handle_v2_same_name_to_v1( + item1: modelsv2.Item, item2: modelsv2b.Item +) -> modelsv1.Item: + return modelsv1.Item( + title=item1.new_title, + size=item2.dup_size, + description=item1.new_description, + sub=modelsv1.SubItem(name=item1.new_sub.new_sub_name), + multi=[modelsv1.SubItem(name=s.dup_sub_name) for s in item2.dup_multi], + ) + + +@app.post("/v2-to-v1/list-of-items-to-list-of-items") +def handle_v2_items_in_list_to_v1_item_in_list( + data1: List[modelsv2.ItemInList], data2: List[modelsv2b.ItemInList] +) -> List[modelsv1.ItemInList]: + result = [] + item1 = data1[0] + item2 = data2[0] + result = [ + modelsv1.ItemInList(name1=item1.name2), + modelsv1.ItemInList(name1=item2.dup_name2), + ] + return result diff --git a/tests/test_pydantic_v1_v2_multifile/modelsv1.py b/tests/test_pydantic_v1_v2_multifile/modelsv1.py new file mode 100644 index 000000000..889291a1a --- /dev/null +++ b/tests/test_pydantic_v1_v2_multifile/modelsv1.py @@ -0,0 +1,19 @@ +from typing import List, Union + +from fastapi._compat.v1 import BaseModel + + +class SubItem(BaseModel): + name: str + + +class Item(BaseModel): + title: str + size: int + description: Union[str, None] = None + sub: SubItem + multi: List[SubItem] = [] + + +class ItemInList(BaseModel): + name1: str diff --git a/tests/test_pydantic_v1_v2_multifile/modelsv2.py b/tests/test_pydantic_v1_v2_multifile/modelsv2.py new file mode 100644 index 000000000..2c8c6ea35 --- /dev/null +++ b/tests/test_pydantic_v1_v2_multifile/modelsv2.py @@ -0,0 +1,19 @@ +from typing import List, Union + +from pydantic import BaseModel + + +class SubItem(BaseModel): + new_sub_name: str + + +class Item(BaseModel): + new_title: str + new_size: int + new_description: Union[str, None] = None + new_sub: SubItem + new_multi: List[SubItem] = [] + + +class ItemInList(BaseModel): + name2: str diff --git a/tests/test_pydantic_v1_v2_multifile/modelsv2b.py b/tests/test_pydantic_v1_v2_multifile/modelsv2b.py new file mode 100644 index 000000000..dc0c06c66 --- /dev/null +++ b/tests/test_pydantic_v1_v2_multifile/modelsv2b.py @@ -0,0 +1,19 @@ +from typing import List, Union + +from pydantic import BaseModel + + +class SubItem(BaseModel): + dup_sub_name: str + + +class Item(BaseModel): + dup_title: str + dup_size: int + dup_description: Union[str, None] = None + dup_sub: SubItem + dup_multi: List[SubItem] = [] + + +class ItemInList(BaseModel): + dup_name2: str diff --git a/tests/test_pydantic_v1_v2_multifile/test_multifile.py b/tests/test_pydantic_v1_v2_multifile/test_multifile.py new file mode 100644 index 000000000..4472bd73e --- /dev/null +++ b/tests/test_pydantic_v1_v2_multifile/test_multifile.py @@ -0,0 +1,1237 @@ +import sys + +from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314 + +if sys.version_info >= (3, 14): + skip_module_if_py_gte_314() + +from fastapi.testclient import TestClient +from inline_snapshot import snapshot + +from .main import app + +client = TestClient(app) + + +def test_v1_to_v2_item(): + response = client.post( + "/v1-to-v2/item", + json={"title": "Test", "size": 10, "sub": {"name": "SubTest"}}, + ) + assert response.status_code == 200 + assert response.json() == { + "new_title": "Test", + "new_size": 10, + "new_description": None, + "new_sub": {"new_sub_name": "SubTest"}, + "new_multi": [], + } + + +def test_v2_to_v1_item(): + response = client.post( + "/v2-to-v1/item", + json={ + "new_title": "NewTest", + "new_size": 20, + "new_sub": {"new_sub_name": "NewSubTest"}, + }, + ) + assert response.status_code == 200 + assert response.json() == { + "title": "NewTest", + "size": 20, + "description": None, + "sub": {"name": "NewSubTest"}, + "multi": [], + } + + +def test_v1_to_v2_item_to_list(): + response = client.post( + "/v1-to-v2/item-to-list", + json={"title": "ListTest", "size": 30, "sub": {"name": "SubListTest"}}, + ) + assert response.status_code == 200 + assert response.json() == [ + { + "new_title": "ListTest", + "new_size": 30, + "new_description": None, + "new_sub": {"new_sub_name": "SubListTest"}, + "new_multi": [], + }, + { + "new_title": "ListTest", + "new_size": 30, + "new_description": None, + "new_sub": {"new_sub_name": "SubListTest"}, + "new_multi": [], + }, + ] + + +def test_v1_to_v2_list_to_list(): + response = client.post( + "/v1-to-v2/list-to-list", + json=[ + {"title": "Item1", "size": 40, "sub": {"name": "Sub1"}}, + {"title": "Item2", "size": 50, "sub": {"name": "Sub2"}}, + ], + ) + assert response.status_code == 200 + assert response.json() == [ + { + "new_title": "Item1", + "new_size": 40, + "new_description": None, + "new_sub": {"new_sub_name": "Sub1"}, + "new_multi": [], + }, + { + "new_title": "Item2", + "new_size": 50, + "new_description": None, + "new_sub": {"new_sub_name": "Sub2"}, + "new_multi": [], + }, + ] + + +def test_v1_to_v2_list_to_item(): + response = client.post( + "/v1-to-v2/list-to-item", + json=[ + {"title": "FirstItem", "size": 60, "sub": {"name": "FirstSub"}}, + {"title": "SecondItem", "size": 70, "sub": {"name": "SecondSub"}}, + ], + ) + assert response.status_code == 200 + assert response.json() == { + "new_title": "FirstItem", + "new_size": 60, + "new_description": None, + "new_sub": {"new_sub_name": "FirstSub"}, + "new_multi": [], + } + + +def test_v2_to_v1_item_to_list(): + response = client.post( + "/v2-to-v1/item-to-list", + json={ + "new_title": "ListNew", + "new_size": 80, + "new_sub": {"new_sub_name": "SubListNew"}, + }, + ) + assert response.status_code == 200 + assert response.json() == [ + { + "title": "ListNew", + "size": 80, + "description": None, + "sub": {"name": "SubListNew"}, + "multi": [], + }, + { + "title": "ListNew", + "size": 80, + "description": None, + "sub": {"name": "SubListNew"}, + "multi": [], + }, + ] + + +def test_v2_to_v1_list_to_list(): + response = client.post( + "/v2-to-v1/list-to-list", + json=[ + { + "new_title": "New1", + "new_size": 90, + "new_sub": {"new_sub_name": "NewSub1"}, + }, + { + "new_title": "New2", + "new_size": 100, + "new_sub": {"new_sub_name": "NewSub2"}, + }, + ], + ) + assert response.status_code == 200 + assert response.json() == [ + { + "title": "New1", + "size": 90, + "description": None, + "sub": {"name": "NewSub1"}, + "multi": [], + }, + { + "title": "New2", + "size": 100, + "description": None, + "sub": {"name": "NewSub2"}, + "multi": [], + }, + ] + + +def test_v2_to_v1_list_to_item(): + response = client.post( + "/v2-to-v1/list-to-item", + json=[ + { + "new_title": "FirstNew", + "new_size": 110, + "new_sub": {"new_sub_name": "FirstNewSub"}, + }, + { + "new_title": "SecondNew", + "new_size": 120, + "new_sub": {"new_sub_name": "SecondNewSub"}, + }, + ], + ) + assert response.status_code == 200 + assert response.json() == { + "title": "FirstNew", + "size": 110, + "description": None, + "sub": {"name": "FirstNewSub"}, + "multi": [], + } + + +def test_v1_to_v2_list_to_item_empty(): + response = client.post("/v1-to-v2/list-to-item", json=[]) + assert response.status_code == 200 + assert response.json() == { + "new_title": "", + "new_size": 0, + "new_description": None, + "new_sub": {"new_sub_name": ""}, + "new_multi": [], + } + + +def test_v2_to_v1_list_to_item_empty(): + response = client.post("/v2-to-v1/list-to-item", json=[]) + assert response.status_code == 200 + assert response.json() == { + "title": "", + "size": 0, + "description": None, + "sub": {"name": ""}, + "multi": [], + } + + +def test_v2_same_name_to_v1(): + response = client.post( + "/v2-to-v1/same-name", + json={ + "item1": { + "new_title": "Title1", + "new_size": 100, + "new_description": "Description1", + "new_sub": {"new_sub_name": "Sub1"}, + "new_multi": [{"new_sub_name": "Multi1"}], + }, + "item2": { + "dup_title": "Title2", + "dup_size": 200, + "dup_description": "Description2", + "dup_sub": {"dup_sub_name": "Sub2"}, + "dup_multi": [ + {"dup_sub_name": "Multi2a"}, + {"dup_sub_name": "Multi2b"}, + ], + }, + }, + ) + assert response.status_code == 200 + assert response.json() == { + "title": "Title1", + "size": 200, + "description": "Description1", + "sub": {"name": "Sub1"}, + "multi": [{"name": "Multi2a"}, {"name": "Multi2b"}], + } + + +def test_v2_items_in_list_to_v1_item_in_list(): + response = client.post( + "/v2-to-v1/list-of-items-to-list-of-items", + json={ + "data1": [{"name2": "Item1"}, {"name2": "Item2"}], + "data2": [{"dup_name2": "Item3"}, {"dup_name2": "Item4"}], + }, + ) + assert response.status_code == 200, response.text + assert response.json() == [ + {"name1": "Item1"}, + {"name1": "Item3"}, + ] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/v1-to-v2/item": { + "post": { + "summary": "Handle V1 Item To V2", + "operationId": "handle_v1_item_to_v2_v1_to_v2_item_post", + "requestBody": { + "content": { + "application/json": { + "schema": pydantic_snapshot( + v2=snapshot( + { + "allOf": [ + { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" + } + ], + "title": "Data", + } + ), + v1=snapshot( + { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" + } + ), + ) + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v2-to-v1/item": { + "post": { + "summary": "Handle V2 Item To V1", + "operationId": "handle_v2_item_to_v1_v2_to_v1_item_post", + "requestBody": { + "content": { + "application/json": { + "schema": pydantic_snapshot( + v2=snapshot( + { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" + } + ), + v1=snapshot( + { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" + } + ), + ), + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v1-to-v2/item-to-list": { + "post": { + "summary": "Handle V1 Item To V2 List", + "operationId": "handle_v1_item_to_v2_list_v1_to_v2_item_to_list_post", + "requestBody": { + "content": { + "application/json": { + "schema": pydantic_snapshot( + v2=snapshot( + { + "allOf": [ + { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" + } + ], + "title": "Data", + } + ), + v1=snapshot( + { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" + } + ), + ) + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" + }, + "type": "array", + "title": "Response Handle V1 Item To V2 List V1 To V2 Item To List Post", + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v1-to-v2/list-to-list": { + "post": { + "summary": "Handle V1 List To V2 List", + "operationId": "handle_v1_list_to_v2_list_v1_to_v2_list_to_list_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" + }, + "type": "array", + "title": "Data", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" + }, + "type": "array", + "title": "Response Handle V1 List To V2 List V1 To V2 List To List Post", + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v1-to-v2/list-to-item": { + "post": { + "summary": "Handle V1 List To V2 Item", + "operationId": "handle_v1_list_to_v2_item_v1_to_v2_list_to_item_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" + }, + "type": "array", + "title": "Data", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v2-to-v1/item-to-list": { + "post": { + "summary": "Handle V2 Item To V1 List", + "operationId": "handle_v2_item_to_v1_list_v2_to_v1_item_to_list_post", + "requestBody": { + "content": { + "application/json": { + "schema": pydantic_snapshot( + v2=snapshot( + { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" + } + ), + v1=snapshot( + { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" + } + ), + ), + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" + }, + "type": "array", + "title": "Response Handle V2 Item To V1 List V2 To V1 Item To List Post", + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v2-to-v1/list-to-list": { + "post": { + "summary": "Handle V2 List To V1 List", + "operationId": "handle_v2_list_to_v1_list_v2_to_v1_list_to_list_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": pydantic_snapshot( + v2=snapshot( + { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" + } + ), + v1=snapshot( + { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" + } + ), + ), + "type": "array", + "title": "Data", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" + }, + "type": "array", + "title": "Response Handle V2 List To V1 List V2 To V1 List To List Post", + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v2-to-v1/list-to-item": { + "post": { + "summary": "Handle V2 List To V1 Item", + "operationId": "handle_v2_list_to_v1_item_v2_to_v1_list_to_item_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": pydantic_snapshot( + v2=snapshot( + { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" + } + ), + v1=snapshot( + { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" + } + ), + ), + "type": "array", + "title": "Data", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v2-to-v1/same-name": { + "post": { + "summary": "Handle V2 Same Name To V1", + "operationId": "handle_v2_same_name_to_v1_v2_to_v1_same_name_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_handle_v2_same_name_to_v1_v2_to_v1_same_name_post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v2-to-v1/list-of-items-to-list-of-items": { + "post": { + "summary": "Handle V2 Items In List To V1 Item In List", + "operationId": "handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__ItemInList" + }, + "type": "array", + "title": "Response Handle V2 Items In List To V1 Item In List V2 To V1 List Of Items To List Of Items Post", + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": pydantic_snapshot( + v1=snapshot( + { + "Body_handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post": { + "properties": { + "data1": { + "items": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__ItemInList" + }, + "type": "array", + "title": "Data1", + }, + "data2": { + "items": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__ItemInList" + }, + "type": "array", + "title": "Data2", + }, + }, + "type": "object", + "required": ["data1", "data2"], + "title": "Body_handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post", + }, + "Body_handle_v2_same_name_to_v1_v2_to_v1_same_name_post": { + "properties": { + "item1": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" + }, + "item2": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__Item" + }, + }, + "type": "object", + "required": ["item1", "item2"], + "title": "Body_handle_v2_same_name_to_v1_v2_to_v1_same_name_post", + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + {"type": "string"}, + {"type": "integer"}, + ] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + "tests__test_pydantic_v1_v2_multifile__modelsv1__Item": { + "properties": { + "title": {"type": "string", "title": "Title"}, + "size": {"type": "integer", "title": "Size"}, + "description": { + "type": "string", + "title": "Description", + }, + "sub": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem" + }, + "multi": { + "items": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem" + }, + "type": "array", + "title": "Multi", + "default": [], + }, + }, + "type": "object", + "required": ["title", "size", "sub"], + "title": "Item", + }, + "tests__test_pydantic_v1_v2_multifile__modelsv1__ItemInList": { + "properties": { + "name1": {"type": "string", "title": "Name1"} + }, + "type": "object", + "required": ["name1"], + "title": "ItemInList", + }, + "tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem": { + "properties": { + "name": {"type": "string", "title": "Name"} + }, + "type": "object", + "required": ["name"], + "title": "SubItem", + }, + "tests__test_pydantic_v1_v2_multifile__modelsv2__Item": { + "properties": { + "new_title": { + "type": "string", + "title": "New Title", + }, + "new_size": { + "type": "integer", + "title": "New Size", + }, + "new_description": { + "type": "string", + "title": "New Description", + }, + "new_sub": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem" + }, + "new_multi": { + "items": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem" + }, + "type": "array", + "title": "New Multi", + "default": [], + }, + }, + "type": "object", + "required": ["new_title", "new_size", "new_sub"], + "title": "Item", + }, + "tests__test_pydantic_v1_v2_multifile__modelsv2__ItemInList": { + "properties": { + "name2": {"type": "string", "title": "Name2"} + }, + "type": "object", + "required": ["name2"], + "title": "ItemInList", + }, + "tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem": { + "properties": { + "new_sub_name": { + "type": "string", + "title": "New Sub Name", + } + }, + "type": "object", + "required": ["new_sub_name"], + "title": "SubItem", + }, + "tests__test_pydantic_v1_v2_multifile__modelsv2b__Item": { + "properties": { + "dup_title": { + "type": "string", + "title": "Dup Title", + }, + "dup_size": { + "type": "integer", + "title": "Dup Size", + }, + "dup_description": { + "type": "string", + "title": "Dup Description", + }, + "dup_sub": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem" + }, + "dup_multi": { + "items": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem" + }, + "type": "array", + "title": "Dup Multi", + "default": [], + }, + }, + "type": "object", + "required": ["dup_title", "dup_size", "dup_sub"], + "title": "Item", + }, + "tests__test_pydantic_v1_v2_multifile__modelsv2b__ItemInList": { + "properties": { + "dup_name2": { + "type": "string", + "title": "Dup Name2", + } + }, + "type": "object", + "required": ["dup_name2"], + "title": "ItemInList", + }, + "tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem": { + "properties": { + "dup_sub_name": { + "type": "string", + "title": "Dup Sub Name", + } + }, + "type": "object", + "required": ["dup_sub_name"], + "title": "SubItem", + }, + } + ), + v2=snapshot( + { + "Body_handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post": { + "properties": { + "data1": { + "items": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__ItemInList" + }, + "type": "array", + "title": "Data1", + }, + "data2": { + "items": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__ItemInList" + }, + "type": "array", + "title": "Data2", + }, + }, + "type": "object", + "required": ["data1", "data2"], + "title": "Body_handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post", + }, + "Body_handle_v2_same_name_to_v1_v2_to_v1_same_name_post": { + "properties": { + "item1": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" + }, + "item2": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__Item" + }, + }, + "type": "object", + "required": ["item1", "item2"], + "title": "Body_handle_v2_same_name_to_v1_v2_to_v1_same_name_post", + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "SubItem-Output": { + "properties": { + "new_sub_name": { + "type": "string", + "title": "New Sub Name", + } + }, + "type": "object", + "required": ["new_sub_name"], + "title": "SubItem", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + {"type": "string"}, + {"type": "integer"}, + ] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + "tests__test_pydantic_v1_v2_multifile__modelsv1__Item": { + "properties": { + "title": {"type": "string", "title": "Title"}, + "size": {"type": "integer", "title": "Size"}, + "description": { + "type": "string", + "title": "Description", + }, + "sub": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem" + }, + "multi": { + "items": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem" + }, + "type": "array", + "title": "Multi", + "default": [], + }, + }, + "type": "object", + "required": ["title", "size", "sub"], + "title": "Item", + }, + "tests__test_pydantic_v1_v2_multifile__modelsv1__ItemInList": { + "properties": { + "name1": {"type": "string", "title": "Name1"} + }, + "type": "object", + "required": ["name1"], + "title": "ItemInList", + }, + "tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem": { + "properties": { + "name": {"type": "string", "title": "Name"} + }, + "type": "object", + "required": ["name"], + "title": "SubItem", + }, + "tests__test_pydantic_v1_v2_multifile__modelsv2__Item": { + "properties": { + "new_title": { + "type": "string", + "title": "New Title", + }, + "new_size": { + "type": "integer", + "title": "New Size", + }, + "new_description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "New Description", + }, + "new_sub": { + "$ref": "#/components/schemas/SubItem-Output" + }, + "new_multi": { + "items": { + "$ref": "#/components/schemas/SubItem-Output" + }, + "type": "array", + "title": "New Multi", + "default": [], + }, + }, + "type": "object", + "required": ["new_title", "new_size", "new_sub"], + "title": "Item", + }, + "tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input": { + "properties": { + "new_title": { + "type": "string", + "title": "New Title", + }, + "new_size": { + "type": "integer", + "title": "New Size", + }, + "new_description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "New Description", + }, + "new_sub": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem" + }, + "new_multi": { + "items": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem" + }, + "type": "array", + "title": "New Multi", + "default": [], + }, + }, + "type": "object", + "required": ["new_title", "new_size", "new_sub"], + "title": "Item", + }, + "tests__test_pydantic_v1_v2_multifile__modelsv2__ItemInList": { + "properties": { + "name2": {"type": "string", "title": "Name2"} + }, + "type": "object", + "required": ["name2"], + "title": "ItemInList", + }, + "tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem": { + "properties": { + "new_sub_name": { + "type": "string", + "title": "New Sub Name", + } + }, + "type": "object", + "required": ["new_sub_name"], + "title": "SubItem", + }, + "tests__test_pydantic_v1_v2_multifile__modelsv2b__Item": { + "properties": { + "dup_title": { + "type": "string", + "title": "Dup Title", + }, + "dup_size": { + "type": "integer", + "title": "Dup Size", + }, + "dup_description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Dup Description", + }, + "dup_sub": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem" + }, + "dup_multi": { + "items": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem" + }, + "type": "array", + "title": "Dup Multi", + "default": [], + }, + }, + "type": "object", + "required": ["dup_title", "dup_size", "dup_sub"], + "title": "Item", + }, + "tests__test_pydantic_v1_v2_multifile__modelsv2b__ItemInList": { + "properties": { + "dup_name2": { + "type": "string", + "title": "Dup Name2", + } + }, + "type": "object", + "required": ["dup_name2"], + "title": "ItemInList", + }, + "tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem": { + "properties": { + "dup_sub_name": { + "type": "string", + "title": "Dup Sub Name", + } + }, + "type": "object", + "required": ["dup_sub_name"], + "title": "SubItem", + }, + } + ), + ), + }, + } + ) diff --git a/tests/test_pydantic_v1_v2_noneable.py b/tests/test_pydantic_v1_v2_noneable.py new file mode 100644 index 000000000..d2d6f6635 --- /dev/null +++ b/tests/test_pydantic_v1_v2_noneable.py @@ -0,0 +1,766 @@ +import sys +from typing import Any, List, Union + +from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314 + +if sys.version_info >= (3, 14): + skip_module_if_py_gte_314() + +from fastapi import FastAPI +from fastapi._compat.v1 import BaseModel +from fastapi.testclient import TestClient +from inline_snapshot import snapshot +from pydantic import BaseModel as NewBaseModel + + +class SubItem(BaseModel): + name: str + + +class Item(BaseModel): + title: str + size: int + description: Union[str, None] = None + sub: SubItem + multi: List[SubItem] = [] + + +class NewSubItem(NewBaseModel): + new_sub_name: str + + +class NewItem(NewBaseModel): + new_title: str + new_size: int + new_description: Union[str, None] = None + new_sub: NewSubItem + new_multi: List[NewSubItem] = [] + + +app = FastAPI() + + +@app.post("/v1-to-v2/") +def handle_v1_item_to_v2(data: Item) -> Union[NewItem, None]: + if data.size < 0: + return None + return NewItem( + new_title=data.title, + new_size=data.size, + new_description=data.description, + new_sub=NewSubItem(new_sub_name=data.sub.name), + new_multi=[NewSubItem(new_sub_name=s.name) for s in data.multi], + ) + + +@app.post("/v1-to-v2/item-filter", response_model=Union[NewItem, None]) +def handle_v1_item_to_v2_filter(data: Item) -> Any: + if data.size < 0: + return None + result = { + "new_title": data.title, + "new_size": data.size, + "new_description": data.description, + "new_sub": {"new_sub_name": data.sub.name, "new_sub_secret": "sub_hidden"}, + "new_multi": [ + {"new_sub_name": s.name, "new_sub_secret": "sub_hidden"} for s in data.multi + ], + "secret": "hidden_v1_to_v2", + } + return result + + +@app.post("/v2-to-v1/item") +def handle_v2_item_to_v1(data: NewItem) -> Union[Item, None]: + if data.new_size < 0: + return None + return Item( + title=data.new_title, + size=data.new_size, + description=data.new_description, + sub=SubItem(name=data.new_sub.new_sub_name), + multi=[SubItem(name=s.new_sub_name) for s in data.new_multi], + ) + + +@app.post("/v2-to-v1/item-filter", response_model=Union[Item, None]) +def handle_v2_item_to_v1_filter(data: NewItem) -> Any: + if data.new_size < 0: + return None + result = { + "title": data.new_title, + "size": data.new_size, + "description": data.new_description, + "sub": {"name": data.new_sub.new_sub_name, "sub_secret": "sub_hidden"}, + "multi": [ + {"name": s.new_sub_name, "sub_secret": "sub_hidden"} for s in data.new_multi + ], + "secret": "hidden_v2_to_v1", + } + return result + + +client = TestClient(app) + + +def test_v1_to_v2_item_success(): + response = client.post( + "/v1-to-v2/", + json={ + "title": "Old Item", + "size": 100, + "description": "V1 description", + "sub": {"name": "V1 Sub"}, + "multi": [{"name": "M1"}, {"name": "M2"}], + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "new_title": "Old Item", + "new_size": 100, + "new_description": "V1 description", + "new_sub": {"new_sub_name": "V1 Sub"}, + "new_multi": [{"new_sub_name": "M1"}, {"new_sub_name": "M2"}], + } + + +def test_v1_to_v2_item_returns_none(): + response = client.post( + "/v1-to-v2/", + json={"title": "Invalid Item", "size": -10, "sub": {"name": "Sub"}}, + ) + assert response.status_code == 200, response.text + assert response.json() is None + + +def test_v1_to_v2_item_minimal(): + response = client.post( + "/v1-to-v2/", json={"title": "Minimal", "size": 50, "sub": {"name": "MinSub"}} + ) + assert response.status_code == 200, response.text + assert response.json() == { + "new_title": "Minimal", + "new_size": 50, + "new_description": None, + "new_sub": {"new_sub_name": "MinSub"}, + "new_multi": [], + } + + +def test_v1_to_v2_item_filter_success(): + response = client.post( + "/v1-to-v2/item-filter", + json={ + "title": "Filtered Item", + "size": 50, + "sub": {"name": "Sub"}, + "multi": [{"name": "Multi1"}], + }, + ) + assert response.status_code == 200, response.text + result = response.json() + assert result["new_title"] == "Filtered Item" + assert result["new_size"] == 50 + assert result["new_sub"]["new_sub_name"] == "Sub" + assert result["new_multi"][0]["new_sub_name"] == "Multi1" + # Verify secret fields are filtered out + assert "secret" not in result + assert "new_sub_secret" not in result["new_sub"] + assert "new_sub_secret" not in result["new_multi"][0] + + +def test_v1_to_v2_item_filter_returns_none(): + response = client.post( + "/v1-to-v2/item-filter", + json={"title": "Invalid", "size": -1, "sub": {"name": "Sub"}}, + ) + assert response.status_code == 200, response.text + assert response.json() is None + + +def test_v2_to_v1_item_success(): + response = client.post( + "/v2-to-v1/item", + json={ + "new_title": "New Item", + "new_size": 200, + "new_description": "V2 description", + "new_sub": {"new_sub_name": "V2 Sub"}, + "new_multi": [{"new_sub_name": "N1"}, {"new_sub_name": "N2"}], + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "title": "New Item", + "size": 200, + "description": "V2 description", + "sub": {"name": "V2 Sub"}, + "multi": [{"name": "N1"}, {"name": "N2"}], + } + + +def test_v2_to_v1_item_returns_none(): + response = client.post( + "/v2-to-v1/item", + json={ + "new_title": "Invalid New", + "new_size": -5, + "new_sub": {"new_sub_name": "NewSub"}, + }, + ) + assert response.status_code == 200, response.text + assert response.json() is None + + +def test_v2_to_v1_item_minimal(): + response = client.post( + "/v2-to-v1/item", + json={ + "new_title": "MinimalNew", + "new_size": 75, + "new_sub": {"new_sub_name": "MinNewSub"}, + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "title": "MinimalNew", + "size": 75, + "description": None, + "sub": {"name": "MinNewSub"}, + "multi": [], + } + + +def test_v2_to_v1_item_filter_success(): + response = client.post( + "/v2-to-v1/item-filter", + json={ + "new_title": "Filtered New", + "new_size": 75, + "new_sub": {"new_sub_name": "NewSub"}, + "new_multi": [], + }, + ) + assert response.status_code == 200, response.text + result = response.json() + assert result["title"] == "Filtered New" + assert result["size"] == 75 + assert result["sub"]["name"] == "NewSub" + # Verify secret fields are filtered out + assert "secret" not in result + assert "sub_secret" not in result["sub"] + + +def test_v2_to_v1_item_filter_returns_none(): + response = client.post( + "/v2-to-v1/item-filter", + json={ + "new_title": "Invalid Filtered", + "new_size": -100, + "new_sub": {"new_sub_name": "Sub"}, + }, + ) + assert response.status_code == 200, response.text + assert response.json() is None + + +def test_v1_to_v2_validation_error(): + response = client.post("/v1-to-v2/", json={"title": "Missing fields"}) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": [ + { + "loc": ["body", "size"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "sub"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_v1_to_v2_nested_validation_error(): + response = client.post( + "/v1-to-v2/", + json={"title": "Bad sub", "size": 100, "sub": {"wrong_field": "value"}}, + ) + assert response.status_code == 422, response.text + error_detail = response.json()["detail"] + assert len(error_detail) == 1 + assert error_detail[0]["loc"] == ["body", "sub", "name"] + + +def test_v1_to_v2_type_validation_error(): + response = client.post( + "/v1-to-v2/", + json={"title": "Bad type", "size": "not_a_number", "sub": {"name": "Sub"}}, + ) + assert response.status_code == 422, response.text + error_detail = response.json()["detail"] + assert len(error_detail) == 1 + assert error_detail[0]["loc"] == ["body", "size"] + + +def test_v2_to_v1_validation_error(): + response = client.post("/v2-to-v1/item", json={"new_title": "Missing fields"}) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": pydantic_snapshot( + v2=snapshot( + [ + { + "type": "missing", + "loc": ["body", "new_size"], + "msg": "Field required", + "input": {"new_title": "Missing fields"}, + }, + { + "type": "missing", + "loc": ["body", "new_sub"], + "msg": "Field required", + "input": {"new_title": "Missing fields"}, + }, + ] + ), + v1=snapshot( + [ + { + "loc": ["body", "new_size"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "new_sub"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + ), + ) + } + ) + + +def test_v2_to_v1_nested_validation_error(): + response = client.post( + "/v2-to-v1/item", + json={ + "new_title": "Bad sub", + "new_size": 200, + "new_sub": {"wrong_field": "value"}, + }, + ) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": [ + pydantic_snapshot( + v2=snapshot( + { + "type": "missing", + "loc": ["body", "new_sub", "new_sub_name"], + "msg": "Field required", + "input": {"wrong_field": "value"}, + } + ), + v1=snapshot( + { + "loc": ["body", "new_sub", "new_sub_name"], + "msg": "field required", + "type": "value_error.missing", + } + ), + ) + ] + } + ) + + +def test_v2_to_v1_type_validation_error(): + response = client.post( + "/v2-to-v1/item", + json={ + "new_title": "Bad type", + "new_size": "not_a_number", + "new_sub": {"new_sub_name": "Sub"}, + }, + ) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": [ + pydantic_snapshot( + v2=snapshot( + { + "type": "int_parsing", + "loc": ["body", "new_size"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "not_a_number", + } + ), + v1=snapshot( + { + "loc": ["body", "new_size"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ), + ) + ] + } + ) + + +def test_v1_to_v2_with_multi_items(): + response = client.post( + "/v1-to-v2/", + json={ + "title": "Complex Item", + "size": 300, + "description": "Item with multiple sub-items", + "sub": {"name": "Main Sub"}, + "multi": [{"name": "Sub1"}, {"name": "Sub2"}, {"name": "Sub3"}], + }, + ) + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "new_title": "Complex Item", + "new_size": 300, + "new_description": "Item with multiple sub-items", + "new_sub": {"new_sub_name": "Main Sub"}, + "new_multi": [ + {"new_sub_name": "Sub1"}, + {"new_sub_name": "Sub2"}, + {"new_sub_name": "Sub3"}, + ], + } + ) + + +def test_v2_to_v1_with_multi_items(): + response = client.post( + "/v2-to-v1/item", + json={ + "new_title": "Complex New Item", + "new_size": 400, + "new_description": "New item with multiple sub-items", + "new_sub": {"new_sub_name": "Main New Sub"}, + "new_multi": [{"new_sub_name": "NewSub1"}, {"new_sub_name": "NewSub2"}], + }, + ) + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "title": "Complex New Item", + "size": 400, + "description": "New item with multiple sub-items", + "sub": {"name": "Main New Sub"}, + "multi": [{"name": "NewSub1"}, {"name": "NewSub2"}], + } + ) + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/v1-to-v2/": { + "post": { + "summary": "Handle V1 Item To V2", + "operationId": "handle_v1_item_to_v2_v1_to_v2__post", + "requestBody": { + "content": { + "application/json": { + "schema": pydantic_snapshot( + v2=snapshot( + { + "allOf": [ + { + "$ref": "#/components/schemas/Item" + } + ], + "title": "Data", + } + ), + v1=snapshot( + {"$ref": "#/components/schemas/Item"} + ), + ) + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": pydantic_snapshot( + v2=snapshot( + { + "anyOf": [ + { + "$ref": "#/components/schemas/NewItem" + }, + {"type": "null"}, + ], + "title": "Response Handle V1 Item To V2 V1 To V2 Post", + } + ), + v1=snapshot( + {"$ref": "#/components/schemas/NewItem"} + ), + ) + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v1-to-v2/item-filter": { + "post": { + "summary": "Handle V1 Item To V2 Filter", + "operationId": "handle_v1_item_to_v2_filter_v1_to_v2_item_filter_post", + "requestBody": { + "content": { + "application/json": { + "schema": pydantic_snapshot( + v2=snapshot( + { + "allOf": [ + { + "$ref": "#/components/schemas/Item" + } + ], + "title": "Data", + } + ), + v1=snapshot( + {"$ref": "#/components/schemas/Item"} + ), + ) + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": pydantic_snapshot( + v2=snapshot( + { + "anyOf": [ + { + "$ref": "#/components/schemas/NewItem" + }, + {"type": "null"}, + ], + "title": "Response Handle V1 Item To V2 Filter V1 To V2 Item Filter Post", + } + ), + v1=snapshot( + {"$ref": "#/components/schemas/NewItem"} + ), + ) + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v2-to-v1/item": { + "post": { + "summary": "Handle V2 Item To V1", + "operationId": "handle_v2_item_to_v1_v2_to_v1_item_post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/NewItem"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v2-to-v1/item-filter": { + "post": { + "summary": "Handle V2 Item To V1 Filter", + "operationId": "handle_v2_item_to_v1_filter_v2_to_v1_item_filter_post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/NewItem"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "title": {"type": "string", "title": "Title"}, + "size": {"type": "integer", "title": "Size"}, + "description": {"type": "string", "title": "Description"}, + "sub": {"$ref": "#/components/schemas/SubItem"}, + "multi": { + "items": {"$ref": "#/components/schemas/SubItem"}, + "type": "array", + "title": "Multi", + "default": [], + }, + }, + "type": "object", + "required": ["title", "size", "sub"], + "title": "Item", + }, + "NewItem": { + "properties": { + "new_title": {"type": "string", "title": "New Title"}, + "new_size": {"type": "integer", "title": "New Size"}, + "new_description": pydantic_snapshot( + v2=snapshot( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "New Description", + } + ), + v1=snapshot( + {"type": "string", "title": "New Description"} + ), + ), + "new_sub": {"$ref": "#/components/schemas/NewSubItem"}, + "new_multi": { + "items": {"$ref": "#/components/schemas/NewSubItem"}, + "type": "array", + "title": "New Multi", + "default": [], + }, + }, + "type": "object", + "required": ["new_title", "new_size", "new_sub"], + "title": "NewItem", + }, + "NewSubItem": { + "properties": { + "new_sub_name": {"type": "string", "title": "New Sub Name"} + }, + "type": "object", + "required": ["new_sub_name"], + "title": "NewSubItem", + }, + "SubItem": { + "properties": {"name": {"type": "string", "title": "Name"}}, + "type": "object", + "required": ["name"], + "title": "SubItem", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } + ) diff --git a/tests/test_response_model_as_return_annotation.py b/tests/test_response_model_as_return_annotation.py index 6948430a1..c3c0ed6c4 100644 --- a/tests/test_response_model_as_return_annotation.py +++ b/tests/test_response_model_as_return_annotation.py @@ -2,6 +2,7 @@ from typing import List, Union import pytest from fastapi import FastAPI +from fastapi._compat import v1 from fastapi.exceptions import FastAPIError, ResponseValidationError from fastapi.responses import JSONResponse, Response from fastapi.testclient import TestClient @@ -509,6 +510,23 @@ def test_invalid_response_model_field(): assert "parameter response_model=None" in e.value.args[0] +# TODO: remove when dropping Pydantic v1 support +def test_invalid_response_model_field_pv1(): + app = FastAPI() + + class Model(v1.BaseModel): + foo: str + + with pytest.raises(FastAPIError) as e: + + @app.get("/") + def read_root() -> Union[Response, Model, None]: + return Response(content="Foo") # pragma: no cover + + assert "valid Pydantic field type" in e.value.args[0] + assert "parameter response_model=None" in e.value.args[0] + + def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text diff --git a/tests/test_tutorial/test_pydantic_v1_in_v2/__init__.py b/tests/test_tutorial/test_pydantic_v1_in_v2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial001.py b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial001.py new file mode 100644 index 000000000..3075a05f5 --- /dev/null +++ b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial001.py @@ -0,0 +1,37 @@ +import sys +from typing import Any + +import pytest +from fastapi._compat import PYDANTIC_V2 + +from tests.utils import skip_module_if_py_gte_314 + +if sys.version_info >= (3, 14): + skip_module_if_py_gte_314() + + +if not PYDANTIC_V2: + pytest.skip("This test is only for Pydantic v2", allow_module_level=True) + +import importlib + +import pytest + +from ...utils import needs_py310 + + +@pytest.fixture( + name="mod", + params=[ + "tutorial001_an", + pytest.param("tutorial001_an_py310", marks=needs_py310), + ], +) +def get_mod(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}") + return mod + + +def test_model(mod: Any): + item = mod.Item(name="Foo", size=3.4) + assert item.dict() == {"name": "Foo", "description": None, "size": 3.4} diff --git a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial002.py b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial002.py new file mode 100644 index 000000000..a402c663d --- /dev/null +++ b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial002.py @@ -0,0 +1,140 @@ +import sys + +import pytest +from fastapi._compat import PYDANTIC_V2 +from inline_snapshot import snapshot + +from tests.utils import skip_module_if_py_gte_314 + +if sys.version_info >= (3, 14): + skip_module_if_py_gte_314() + + +if not PYDANTIC_V2: + pytest.skip("This test is only for Pydantic v2", allow_module_level=True) + +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + "tutorial002_an", + pytest.param("tutorial002_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}") + + c = TestClient(mod.app) + return c + + +def test_call(client: TestClient): + response = client.post("/items/", json={"name": "Foo", "size": 3.4}) + assert response.status_code == 200, response.text + assert response.json() == { + "name": "Foo", + "description": None, + "size": 3.4, + } + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": {"type": "string", "title": "Description"}, + "size": {"type": "number", "title": "Size"}, + }, + "type": "object", + "required": ["name", "size"], + "title": "Item", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } + ) diff --git a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial003.py b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial003.py new file mode 100644 index 000000000..03155c924 --- /dev/null +++ b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial003.py @@ -0,0 +1,154 @@ +import sys + +import pytest +from fastapi._compat import PYDANTIC_V2 +from inline_snapshot import snapshot + +from tests.utils import skip_module_if_py_gte_314 + +if sys.version_info >= (3, 14): + skip_module_if_py_gte_314() + +if not PYDANTIC_V2: + pytest.skip("This test is only for Pydantic v2", allow_module_level=True) + + +import importlib + +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + "tutorial003_an", + pytest.param("tutorial003_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}") + + c = TestClient(mod.app) + return c + + +def test_call(client: TestClient): + response = client.post("/items/", json={"name": "Foo", "size": 3.4}) + assert response.status_code == 200, response.text + assert response.json() == { + "name": "Foo", + "description": None, + "size": 3.4, + } + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ItemV2" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": {"type": "string", "title": "Description"}, + "size": {"type": "number", "title": "Size"}, + }, + "type": "object", + "required": ["name", "size"], + "title": "Item", + }, + "ItemV2": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + "size": {"type": "number", "title": "Size"}, + }, + "type": "object", + "required": ["name", "size"], + "title": "ItemV2", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } + ) diff --git a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial004.py b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial004.py new file mode 100644 index 000000000..d2e204dda --- /dev/null +++ b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial004.py @@ -0,0 +1,153 @@ +import sys + +import pytest +from fastapi._compat import PYDANTIC_V2 +from inline_snapshot import snapshot + +from tests.utils import skip_module_if_py_gte_314 + +if sys.version_info >= (3, 14): + skip_module_if_py_gte_314() + +if not PYDANTIC_V2: + pytest.skip("This test is only for Pydantic v2", allow_module_level=True) + + +import importlib + +from fastapi.testclient import TestClient + +from ...utils import needs_py39, needs_py310 + + +@pytest.fixture( + name="client", + params=[ + "tutorial004_an", + pytest.param("tutorial004_an_py39", marks=needs_py39), + pytest.param("tutorial004_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}") + + c = TestClient(mod.app) + return c + + +def test_call(client: TestClient): + response = client.post("/items/", json={"item": {"name": "Foo", "size": 3.4}}) + assert response.status_code == 200, response.text + assert response.json() == { + "name": "Foo", + "description": None, + "size": 3.4, + } + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Body_create_item_items__post" + } + ], + "title": "Body", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "Body_create_item_items__post": { + "properties": { + "item": { + "allOf": [{"$ref": "#/components/schemas/Item"}], + "title": "Item", + } + }, + "type": "object", + "required": ["item"], + "title": "Body_create_item_items__post", + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": {"type": "string", "title": "Description"}, + "size": {"type": "number", "title": "Size"}, + }, + "type": "object", + "required": ["name", "size"], + "title": "Item", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } + ) diff --git a/tests/utils.py b/tests/utils.py index ae9543e3b..691e92bbf 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -8,10 +8,19 @@ needs_py39 = pytest.mark.skipif(sys.version_info < (3, 9), reason="requires pyth needs_py310 = pytest.mark.skipif( sys.version_info < (3, 10), reason="requires python3.10+" ) +needs_py_lt_314 = pytest.mark.skipif( + sys.version_info > (3, 13), reason="requires python3.13-" +) needs_pydanticv2 = pytest.mark.skipif(not PYDANTIC_V2, reason="requires Pydantic v2") needs_pydanticv1 = pytest.mark.skipif(PYDANTIC_V2, reason="requires Pydantic v1") +def skip_module_if_py_gte_314(): + """Skip entire module on Python 3.14+ at import time.""" + if sys.version_info >= (3, 14): + pytest.skip("requires python3.13-", allow_module_level=True) + + def pydantic_snapshot( *, v2: Snapshot, From 3a3879b2c3c33daf91121b30c78a41e1d8440a3d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 11 Oct 2025 16:46:19 +0000 Subject: [PATCH 021/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 55a941535..8957e43d4 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Features + +* ✨ Add support for `from pydantic.v1 import BaseModel`, mixed Pydantic v1 and v2 models in the same app. PR [#14168](https://github.com/fastapi/fastapi/pull/14168) by [@tiangolo](https://github.com/tiangolo). + ## 0.118.3 ### Upgrades From fc7a0686af29c7793b3cc999f1af47bf05f5fa74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 11 Oct 2025 19:07:40 +0200 Subject: [PATCH 022/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 8957e43d4..110ccd74c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,40 @@ hide: ## Latest Changes +FastAPI now (temporarily) supports both Pydantic v2 models and `pydantic.v1` models at the same time in the same app, to make it easier for any FastAPI apps still using Pydantic v1 to gradually but quickly **migrate to Pydantic v2**. + +```Python +from fastapi import FastAPI +from pydantic import BaseModel as BaseModelV2 +from pydantic.v1 import BaseModel + + +class Item(BaseModel): + name: str + description: str | None = None + + +class ItemV2(BaseModelV2): + title: str + summary: str | None = None + + +app = FastAPI() + + +@app.post("/items/", response_model=ItemV2) +def create_item(item: Item): + return {"title": item.name, "summary": item.description} +``` + +Adding this feature was a big effort with the main objective of making it easier for the few applications still stuck in Pydantic v1 to migrate to Pydantic v2. + +And with this, support for **Pydantic v1 is now deprecated** and will be **removed** from FastAPI in a future version soon. + +**Note**: have in mind that the Pydantic team already stopped supporting Pydantic v1 for recent versions of Python, starting with Python 3.14. + +You can read in the docs more about how to [Migrate from Pydantic v1 to Pydantic v2](https://fastapi.tiangolo.com/how-to/migrate-from-pydantic-v1-to-pydantic-v2/). + ### Features * ✨ Add support for `from pydantic.v1 import BaseModel`, mixed Pydantic v1 and v2 models in the same app. PR [#14168](https://github.com/fastapi/fastapi/pull/14168) by [@tiangolo](https://github.com/tiangolo). From 2e721e1b02186467dc3a7a95dfe45d8e7bd7d0d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 11 Oct 2025 19:09:01 +0200 Subject: [PATCH 023/256] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.11?= =?UTF-8?q?9.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 110ccd74c..663e99a23 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.119.0 + FastAPI now (temporarily) supports both Pydantic v2 models and `pydantic.v1` models at the same time in the same app, to make it easier for any FastAPI apps still using Pydantic v1 to gradually but quickly **migrate to Pydantic v2**. ```Python diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 297d993d6..2091f0d1f 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.118.3" +__version__ = "0.119.0" from starlette import status as status From dde7bd1ceb4b081165b75c90b2694b98c7dafce3 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 11 Oct 2025 18:48:49 +0100 Subject: [PATCH 024/256] =?UTF-8?q?=F0=9F=93=9D=20Replace=20`starlette.io`?= =?UTF-8?q?=20by=20`starlette.dev`=20and=20`uvicorn.org`=20by=20`uvicorn.d?= =?UTF-8?q?ev`=20(#14176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- README.md | 6 +++--- docs/de/docs/advanced/events.md | 2 +- docs/de/docs/advanced/middleware.md | 2 +- docs/de/docs/advanced/response-cookies.md | 2 +- docs/de/docs/advanced/response-headers.md | 2 +- docs/de/docs/advanced/templates.md | 2 +- docs/de/docs/advanced/testing-events.md | 2 +- docs/de/docs/advanced/testing-websockets.md | 2 +- docs/de/docs/advanced/using-request-directly.md | 4 ++-- docs/de/docs/advanced/websockets.md | 4 ++-- docs/de/docs/alternatives.md | 4 ++-- docs/de/docs/deployment/manually.md | 2 +- docs/de/docs/fastapi-cli.md | 2 +- docs/de/docs/features.md | 2 +- docs/de/docs/history-design-future.md | 2 +- docs/de/docs/how-to/custom-request-and-route.md | 2 +- docs/de/docs/index.md | 6 +++--- docs/de/docs/tutorial/background-tasks.md | 4 ++-- docs/de/docs/tutorial/first-steps.md | 2 +- docs/de/docs/tutorial/handling-errors.md | 2 +- docs/de/docs/tutorial/middleware.md | 2 +- docs/de/docs/tutorial/static-files.md | 2 +- docs/de/docs/tutorial/testing.md | 2 +- docs/em/docs/advanced/events.md | 2 +- docs/em/docs/advanced/middleware.md | 2 +- docs/em/docs/advanced/response-cookies.md | 2 +- docs/em/docs/advanced/response-headers.md | 2 +- docs/em/docs/advanced/templates.md | 2 +- docs/em/docs/advanced/testing-websockets.md | 2 +- docs/em/docs/advanced/using-request-directly.md | 4 ++-- docs/em/docs/advanced/websockets.md | 4 ++-- docs/em/docs/alternatives.md | 4 ++-- docs/em/docs/deployment/manually.md | 4 ++-- docs/em/docs/features.md | 2 +- docs/em/docs/history-design-future.md | 2 +- docs/em/docs/how-to/custom-request-and-route.md | 2 +- docs/em/docs/index.md | 6 +++--- docs/em/docs/tutorial/background-tasks.md | 4 ++-- docs/em/docs/tutorial/first-steps.md | 2 +- docs/em/docs/tutorial/handling-errors.md | 2 +- docs/em/docs/tutorial/middleware.md | 2 +- docs/em/docs/tutorial/static-files.md | 2 +- docs/em/docs/tutorial/testing.md | 2 +- docs/en/docs/advanced/events.md | 2 +- docs/en/docs/advanced/middleware.md | 2 +- docs/en/docs/advanced/response-cookies.md | 2 +- docs/en/docs/advanced/response-headers.md | 2 +- docs/en/docs/advanced/templates.md | 2 +- docs/en/docs/advanced/testing-events.md | 2 +- docs/en/docs/advanced/testing-websockets.md | 2 +- docs/en/docs/advanced/using-request-directly.md | 4 ++-- docs/en/docs/advanced/websockets.md | 4 ++-- docs/en/docs/alternatives.md | 4 ++-- docs/en/docs/deployment/manually.md | 2 +- docs/en/docs/fastapi-cli.md | 2 +- docs/en/docs/features.md | 2 +- docs/en/docs/history-design-future.md | 2 +- docs/en/docs/how-to/custom-request-and-route.md | 2 +- docs/en/docs/index.md | 6 +++--- docs/en/docs/release-notes.md | 2 +- docs/en/docs/tutorial/background-tasks.md | 4 ++-- docs/en/docs/tutorial/first-steps.md | 2 +- docs/en/docs/tutorial/handling-errors.md | 2 +- docs/en/docs/tutorial/middleware.md | 2 +- docs/en/docs/tutorial/static-files.md | 2 +- docs/en/docs/tutorial/testing.md | 2 +- docs/es/docs/advanced/events.md | 2 +- docs/es/docs/advanced/middleware.md | 2 +- docs/es/docs/advanced/response-cookies.md | 2 +- docs/es/docs/advanced/response-headers.md | 2 +- docs/es/docs/advanced/templates.md | 2 +- docs/es/docs/advanced/testing-websockets.md | 2 +- docs/es/docs/advanced/using-request-directly.md | 4 ++-- docs/es/docs/advanced/websockets.md | 4 ++-- docs/es/docs/alternatives.md | 4 ++-- docs/es/docs/deployment/manually.md | 2 +- docs/es/docs/fastapi-cli.md | 2 +- docs/es/docs/features.md | 2 +- docs/es/docs/history-design-future.md | 2 +- docs/es/docs/how-to/custom-request-and-route.md | 2 +- docs/es/docs/index.md | 6 +++--- docs/es/docs/tutorial/background-tasks.md | 4 ++-- docs/es/docs/tutorial/first-steps.md | 2 +- docs/es/docs/tutorial/handling-errors.md | 2 +- docs/es/docs/tutorial/middleware.md | 2 +- docs/es/docs/tutorial/static-files.md | 2 +- docs/es/docs/tutorial/testing.md | 2 +- docs/fa/docs/features.md | 2 +- docs/fa/docs/index.md | 6 +++--- docs/fa/docs/tutorial/middleware.md | 2 +- docs/fr/docs/alternatives.md | 4 ++-- docs/fr/docs/deployment/manually.md | 4 ++-- docs/fr/docs/features.md | 2 +- docs/fr/docs/history-design-future.md | 2 +- docs/fr/docs/index.md | 6 +++--- docs/fr/docs/tutorial/background-tasks.md | 4 ++-- docs/fr/docs/tutorial/first-steps.md | 2 +- docs/ja/docs/advanced/websockets.md | 4 ++-- docs/ja/docs/alternatives.md | 4 ++-- docs/ja/docs/deployment/manually.md | 4 ++-- docs/ja/docs/features.md | 2 +- docs/ja/docs/history-design-future.md | 2 +- docs/ja/docs/index.md | 6 +++--- docs/ja/docs/tutorial/background-tasks.md | 4 ++-- docs/ja/docs/tutorial/first-steps.md | 2 +- docs/ja/docs/tutorial/handling-errors.md | 2 +- docs/ja/docs/tutorial/middleware.md | 2 +- docs/ja/docs/tutorial/static-files.md | 2 +- docs/ja/docs/tutorial/testing.md | 2 +- docs/ko/docs/advanced/events.md | 2 +- docs/ko/docs/advanced/middlewares.md | 2 +- docs/ko/docs/advanced/response-cookies.md | 2 +- docs/ko/docs/advanced/response-headers.md | 2 +- docs/ko/docs/advanced/templates.md | 2 +- docs/ko/docs/advanced/testing-websockets.md | 2 +- docs/ko/docs/advanced/using-request-directly.md | 4 ++-- docs/ko/docs/advanced/websockets.md | 4 ++-- docs/ko/docs/fastapi-cli.md | 2 +- docs/ko/docs/features.md | 2 +- docs/ko/docs/history-design-future.md | 2 +- docs/ko/docs/index.md | 6 +++--- docs/ko/docs/tutorial/background-tasks.md | 4 ++-- docs/ko/docs/tutorial/first-steps.md | 2 +- docs/ko/docs/tutorial/middleware.md | 2 +- docs/ko/docs/tutorial/static-files.md | 2 +- docs/ko/docs/tutorial/testing.md | 2 +- docs/pt/docs/advanced/events.md | 2 +- docs/pt/docs/advanced/middleware.md | 2 +- docs/pt/docs/advanced/response-cookies.md | 2 +- docs/pt/docs/advanced/response-headers.md | 2 +- docs/pt/docs/advanced/templates.md | 2 +- docs/pt/docs/advanced/testing-websockets.md | 2 +- docs/pt/docs/advanced/using-request-directly.md | 4 ++-- docs/pt/docs/advanced/websockets.md | 4 ++-- docs/pt/docs/alternatives.md | 4 ++-- docs/pt/docs/deployment/manually.md | 2 +- docs/pt/docs/fastapi-cli.md | 2 +- docs/pt/docs/features.md | 2 +- docs/pt/docs/history-design-future.md | 2 +- docs/pt/docs/how-to/custom-request-and-route.md | 2 +- docs/pt/docs/index.md | 6 +++--- docs/pt/docs/tutorial/background-tasks.md | 4 ++-- docs/pt/docs/tutorial/first-steps.md | 2 +- docs/pt/docs/tutorial/handling-errors.md | 2 +- docs/pt/docs/tutorial/middleware.md | 2 +- docs/pt/docs/tutorial/static-files.md | 2 +- docs/pt/docs/tutorial/testing.md | 2 +- docs/ru/docs/advanced/events.md | 2 +- docs/ru/docs/advanced/middleware.md | 2 +- docs/ru/docs/advanced/response-cookies.md | 2 +- docs/ru/docs/advanced/response-headers.md | 2 +- docs/ru/docs/advanced/templates.md | 2 +- docs/ru/docs/advanced/testing-events.md | 2 +- docs/ru/docs/advanced/testing-websockets.md | 2 +- docs/ru/docs/advanced/using-request-directly.md | 4 ++-- docs/ru/docs/advanced/websockets.md | 4 ++-- docs/ru/docs/alternatives.md | 4 ++-- docs/ru/docs/deployment/manually.md | 2 +- docs/ru/docs/fastapi-cli.md | 2 +- docs/ru/docs/features.md | 2 +- docs/ru/docs/history-design-future.md | 2 +- docs/ru/docs/how-to/custom-request-and-route.md | 2 +- docs/ru/docs/index.md | 6 +++--- docs/ru/docs/tutorial/background-tasks.md | 4 ++-- docs/ru/docs/tutorial/first-steps.md | 2 +- docs/ru/docs/tutorial/handling-errors.md | 2 +- docs/ru/docs/tutorial/middleware.md | 2 +- docs/ru/docs/tutorial/static-files.md | 2 +- docs/ru/docs/tutorial/testing.md | 2 +- docs/tr/docs/advanced/testing-websockets.md | 2 +- docs/tr/docs/alternatives.md | 4 ++-- docs/tr/docs/features.md | 2 +- docs/tr/docs/history-design-future.md | 2 +- docs/tr/docs/index.md | 6 +++--- docs/tr/docs/tutorial/first-steps.md | 2 +- docs/tr/docs/tutorial/static-files.md | 2 +- docs/uk/docs/alternatives.md | 4 ++-- docs/uk/docs/fastapi-cli.md | 2 +- docs/uk/docs/features.md | 2 +- docs/uk/docs/index.md | 6 +++--- docs/uk/docs/tutorial/background-tasks.md | 4 ++-- docs/uk/docs/tutorial/first-steps.md | 2 +- docs/uk/docs/tutorial/handling-errors.md | 2 +- docs/uk/docs/tutorial/middleware.md | 2 +- docs/uk/docs/tutorial/static-files.md | 2 +- docs/uk/docs/tutorial/testing.md | 2 +- docs/vi/docs/fastapi-cli.md | 2 +- docs/vi/docs/index.md | 6 +++--- docs/vi/docs/tutorial/first-steps.md | 2 +- docs/vi/docs/tutorial/static-files.md | 2 +- docs/zh-hant/docs/fastapi-cli.md | 2 +- docs/zh-hant/docs/features.md | 2 +- docs/zh-hant/docs/index.md | 6 +++--- docs/zh-hant/docs/tutorial/first-steps.md | 2 +- docs/zh/docs/advanced/events.md | 2 +- docs/zh/docs/advanced/middleware.md | 2 +- docs/zh/docs/advanced/response-cookies.md | 2 +- docs/zh/docs/advanced/response-headers.md | 2 +- docs/zh/docs/advanced/templates.md | 2 +- docs/zh/docs/advanced/testing-websockets.md | 2 +- docs/zh/docs/advanced/using-request-directly.md | 4 ++-- docs/zh/docs/advanced/websockets.md | 4 ++-- docs/zh/docs/deployment/manually.md | 2 +- docs/zh/docs/fastapi-cli.md | 2 +- docs/zh/docs/features.md | 2 +- docs/zh/docs/history-design-future.md | 2 +- docs/zh/docs/index.md | 6 +++--- docs/zh/docs/tutorial/background-tasks.md | 4 ++-- docs/zh/docs/tutorial/first-steps.md | 2 +- docs/zh/docs/tutorial/handling-errors.md | 2 +- docs/zh/docs/tutorial/middleware.md | 2 +- docs/zh/docs/tutorial/static-files.md | 2 +- docs/zh/docs/tutorial/testing.md | 2 +- fastapi/applications.py | 4 ++-- 214 files changed, 288 insertions(+), 288 deletions(-) diff --git a/README.md b/README.md index a8a0e37b5..4fd87298e 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ If you are building a CLI app to be FastAPI stands on the shoulders of giants: -* Starlette for the web parts. +* Starlette for the web parts. * Pydantic for the data parts. ## Installation @@ -231,7 +231,7 @@ INFO: Application startup complete.
About the command fastapi dev main.py... -The command `fastapi dev` reads your `main.py` file, detects the **FastAPI** app in it, and starts a server using Uvicorn. +The command `fastapi dev` reads your `main.py` file, detects the **FastAPI** app in it, and starts a server using Uvicorn. By default, `fastapi dev` will start with auto-reload enabled for local development. @@ -472,7 +472,7 @@ Used by Starlette: Used by FastAPI: -* uvicorn - for the server that loads and serves your application. This includes `uvicorn[standard]`, which includes some dependencies (e.g. `uvloop`) needed for high performance serving. +* uvicorn - for the server that loads and serves your application. This includes `uvicorn[standard]`, which includes some dependencies (e.g. `uvloop`) needed for high performance serving. * `fastapi-cli[standard]` - to provide the `fastapi` command. * This includes `fastapi-cloud-cli`, which allows you to deploy your FastAPI application to FastAPI Cloud. diff --git a/docs/de/docs/advanced/events.md b/docs/de/docs/advanced/events.md index 2ceef1190..f94526b4f 100644 --- a/docs/de/docs/advanced/events.md +++ b/docs/de/docs/advanced/events.md @@ -154,7 +154,7 @@ In der technischen ASGI-Spezifikation ist dies Teil des Starlettes Lifespan-Dokumentation. +Weitere Informationen zu Starlettes `lifespan`-Handlern finden Sie in Starlettes Lifespan-Dokumentation. Einschließlich, wie man Lifespan-Zustand handhabt, der in anderen Bereichen Ihres Codes verwendet werden kann. diff --git a/docs/de/docs/advanced/middleware.md b/docs/de/docs/advanced/middleware.md index 0a2a39699..8396a626b 100644 --- a/docs/de/docs/advanced/middleware.md +++ b/docs/de/docs/advanced/middleware.md @@ -94,4 +94,4 @@ Zum Beispiel: * Uvicorns `ProxyHeadersMiddleware` * MessagePack -Um mehr über weitere verfügbare Middlewares herauszufinden, besuchen Sie Starlettes Middleware-Dokumentation und die ASGI Awesome List. +Um mehr über weitere verfügbare Middlewares herauszufinden, besuchen Sie Starlettes Middleware-Dokumentation und die ASGI Awesome List. diff --git a/docs/de/docs/advanced/response-cookies.md b/docs/de/docs/advanced/response-cookies.md index 0dd4175dd..02fe99c26 100644 --- a/docs/de/docs/advanced/response-cookies.md +++ b/docs/de/docs/advanced/response-cookies.md @@ -48,4 +48,4 @@ Und da die `Response` häufig zum Setzen von Headern und Cookies verwendet wird, /// -Um alle verfügbaren Parameter und Optionen anzuzeigen, sehen Sie sich deren Dokumentation in Starlette an. +Um alle verfügbaren Parameter und Optionen anzuzeigen, sehen Sie sich deren Dokumentation in Starlette an. diff --git a/docs/de/docs/advanced/response-headers.md b/docs/de/docs/advanced/response-headers.md index a5e310d55..1dc7c0691 100644 --- a/docs/de/docs/advanced/response-headers.md +++ b/docs/de/docs/advanced/response-headers.md @@ -38,4 +38,4 @@ Und da die `Response` häufig zum Setzen von Headern und Cookies verwendet wird, Beachten Sie, dass benutzerdefinierte proprietäre Header mittels des Präfix `X-` hinzugefügt werden können. -Wenn Sie jedoch benutzerdefinierte Header haben, die ein Client in einem Browser sehen können soll, müssen Sie diese zu Ihrer CORS-Konfiguration hinzufügen (weitere Informationen finden Sie unter [CORS (Cross-Origin Resource Sharing)](../tutorial/cors.md){.internal-link target=_blank}), unter Verwendung des Parameters `expose_headers`, dokumentiert in Starlettes CORS-Dokumentation. +Wenn Sie jedoch benutzerdefinierte Header haben, die ein Client in einem Browser sehen können soll, müssen Sie diese zu Ihrer CORS-Konfiguration hinzufügen (weitere Informationen finden Sie unter [CORS (Cross-Origin Resource Sharing)](../tutorial/cors.md){.internal-link target=_blank}), unter Verwendung des Parameters `expose_headers`, dokumentiert in Starlettes CORS-Dokumentation. diff --git a/docs/de/docs/advanced/templates.md b/docs/de/docs/advanced/templates.md index fdaeb3413..65c7998b8 100644 --- a/docs/de/docs/advanced/templates.md +++ b/docs/de/docs/advanced/templates.md @@ -123,4 +123,4 @@ Und da Sie `StaticFiles` verwenden, wird diese CSS-Datei automatisch von Ihrer * ## Mehr Details { #more-details } -Weitere Informationen, einschließlich, wie man Templates testet, finden Sie in Starlettes Dokumentation zu Templates. +Weitere Informationen, einschließlich, wie man Templates testet, finden Sie in Starlettes Dokumentation zu Templates. diff --git a/docs/de/docs/advanced/testing-events.md b/docs/de/docs/advanced/testing-events.md index 1a68b7714..569518c51 100644 --- a/docs/de/docs/advanced/testing-events.md +++ b/docs/de/docs/advanced/testing-events.md @@ -5,7 +5,7 @@ Wenn Sie `lifespan` in Ihren Tests ausführen müssen, können Sie den `TestClie {* ../../docs_src/app_testing/tutorial004.py hl[9:15,18,27:28,30:32,41:43] *} -Sie können mehr Details unter [„Lifespan in Tests ausführen in der offiziellen Starlette-Dokumentation.“](https://www.starlette.io/lifespan/#running-lifespan-in-tests) nachlesen. +Sie können mehr Details unter [„Lifespan in Tests ausführen in der offiziellen Starlette-Dokumentation.“](https://www.starlette.dev/lifespan/#running-lifespan-in-tests) nachlesen. Für die deprecateten Events `startup` und `shutdown` können Sie den `TestClient` wie folgt verwenden: diff --git a/docs/de/docs/advanced/testing-websockets.md b/docs/de/docs/advanced/testing-websockets.md index a71310cbf..f25aa4fd0 100644 --- a/docs/de/docs/advanced/testing-websockets.md +++ b/docs/de/docs/advanced/testing-websockets.md @@ -8,6 +8,6 @@ Dazu verwenden Sie den `TestClient` in einer `with`-Anweisung, eine Verbindung z /// note | Hinweis -Weitere Informationen finden Sie in Starlettes Dokumentation zum Testen von WebSockets. +Weitere Informationen finden Sie in Starlettes Dokumentation zum Testen von WebSockets. /// diff --git a/docs/de/docs/advanced/using-request-directly.md b/docs/de/docs/advanced/using-request-directly.md index 7782237ec..8ec6741d0 100644 --- a/docs/de/docs/advanced/using-request-directly.md +++ b/docs/de/docs/advanced/using-request-directly.md @@ -15,7 +15,7 @@ Es gibt jedoch Situationen, in denen Sie möglicherweise direkt auf das `Request ## Details zum `Request`-Objekt { #details-about-the-request-object } -Da **FastAPI** unter der Haube eigentlich **Starlette** ist, mit einer Ebene von mehreren Tools darüber, können Sie Starlettes `Request`-Objekt direkt verwenden, wenn Sie es benötigen. +Da **FastAPI** unter der Haube eigentlich **Starlette** ist, mit einer Ebene von mehreren Tools darüber, können Sie Starlettes `Request`-Objekt direkt verwenden, wenn Sie es benötigen. Das bedeutet allerdings auch, dass, wenn Sie Daten direkt vom `Request`-Objekt nehmen (z. B. dessen Body lesen), diese von FastAPI nicht validiert, konvertiert oder dokumentiert werden (mit OpenAPI, für die automatische API-Benutzeroberfläche). @@ -45,7 +45,7 @@ Auf die gleiche Weise können Sie wie gewohnt jeden anderen Parameter deklariere ## `Request`-Dokumentation { #request-documentation } -Weitere Details zum `Request`-Objekt finden Sie in der offiziellen Starlette-Dokumentation. +Weitere Details zum `Request`-Objekt finden Sie in der offiziellen Starlette-Dokumentation. /// note | Technische Details diff --git a/docs/de/docs/advanced/websockets.md b/docs/de/docs/advanced/websockets.md index ad1f6f5b1..5f662770f 100644 --- a/docs/de/docs/advanced/websockets.md +++ b/docs/de/docs/advanced/websockets.md @@ -182,5 +182,5 @@ Wenn Sie etwas benötigen, das sich leicht in FastAPI integrieren lässt, aber r Weitere Informationen zu Optionen finden Sie in der Dokumentation von Starlette: -* Die `WebSocket`-Klasse. -* Klassen-basierte Handhabung von WebSockets. +* Die `WebSocket`-Klasse. +* Klassen-basierte Handhabung von WebSockets. diff --git a/docs/de/docs/alternatives.md b/docs/de/docs/alternatives.md index 15c0719fb..4dd127dba 100644 --- a/docs/de/docs/alternatives.md +++ b/docs/de/docs/alternatives.md @@ -417,7 +417,7 @@ Die gesamte Datenvalidierung, Datenserialisierung und automatische Modelldokumen /// -### Starlette { #starlette } +### Starlette { #starlette } Starlette ist ein leichtgewichtiges ASGI-Framework/Toolkit, welches sich ideal für die Erstellung hochperformanter asynchroner Dienste eignet. @@ -462,7 +462,7 @@ Alles, was Sie also mit Starlette machen können, können Sie direkt mit **FastA /// -### Uvicorn { #uvicorn } +### Uvicorn { #uvicorn } Uvicorn ist ein blitzschneller ASGI-Server, der auf uvloop und httptools basiert. diff --git a/docs/de/docs/deployment/manually.md b/docs/de/docs/deployment/manually.md index 6393f8ebc..2de2913a5 100644 --- a/docs/de/docs/deployment/manually.md +++ b/docs/de/docs/deployment/manually.md @@ -52,7 +52,7 @@ Das Wichtigste, was Sie benötigen, um eine **FastAPI**-Anwendung (oder eine and Es gibt mehrere Alternativen, einschließlich: -* Uvicorn: ein hochperformanter ASGI-Server. +* Uvicorn: ein hochperformanter ASGI-Server. * Hypercorn: ein ASGI-Server, der unter anderem kompatibel mit HTTP/2 und Trio ist. * Daphne: der für Django Channels entwickelte ASGI-Server. * Granian: Ein Rust HTTP-Server für Python-Anwendungen. diff --git a/docs/de/docs/fastapi-cli.md b/docs/de/docs/fastapi-cli.md index d41ed598e..ab9c8373e 100644 --- a/docs/de/docs/fastapi-cli.md +++ b/docs/de/docs/fastapi-cli.md @@ -52,7 +52,7 @@ FastAPI CLI nimmt den Pfad zu Ihrem Python-Programm (z. B. `main.py`), erkennt a Für die Produktion würden Sie stattdessen `fastapi run` verwenden. 🚀 -Intern verwendet das **FastAPI CLI** Uvicorn, einen leistungsstarken, produktionsreifen, ASGI-Server. 😎 +Intern verwendet das **FastAPI CLI** Uvicorn, einen leistungsstarken, produktionsreifen, ASGI-Server. 😎 ## `fastapi dev` { #fastapi-dev } diff --git a/docs/de/docs/features.md b/docs/de/docs/features.md index c52f6733e..0b51e9737 100644 --- a/docs/de/docs/features.md +++ b/docs/de/docs/features.md @@ -159,7 +159,7 @@ Jede Integration wurde so entworfen, dass sie so einfach zu nutzen ist (mit Abh ## Starlette Merkmale { #starlette-features } -**FastAPI** ist vollkommen kompatibel (und basiert auf) Starlette. Das bedeutet, wenn Sie eigenen Starlette Quellcode haben, funktioniert der. +**FastAPI** ist vollkommen kompatibel (und basiert auf) Starlette. Das bedeutet, wenn Sie eigenen Starlette Quellcode haben, funktioniert der. `FastAPI` ist tatsächlich eine Unterklasse von `Starlette`. Wenn Sie also bereits Starlette kennen oder benutzen, das meiste funktioniert genau so. diff --git a/docs/de/docs/history-design-future.md b/docs/de/docs/history-design-future.md index 40a7a8286..45198ff1c 100644 --- a/docs/de/docs/history-design-future.md +++ b/docs/de/docs/history-design-future.md @@ -58,7 +58,7 @@ Nachdem ich mehrere Alternativen getestet hatte, entschied ich, dass ich **Starlette** beigetragen, der anderen Schlüsselanforderung. +Während der Entwicklung habe ich auch zu **Starlette** beigetragen, der anderen Schlüsselanforderung. ## Entwicklung { #development } diff --git a/docs/de/docs/how-to/custom-request-and-route.md b/docs/de/docs/how-to/custom-request-and-route.md index 41a85f832..246717c04 100644 --- a/docs/de/docs/how-to/custom-request-and-route.md +++ b/docs/de/docs/how-to/custom-request-and-route.md @@ -66,7 +66,7 @@ Das `scope`-`dict` und die `receive`-Funktion sind beide Teil der ASGI-Spezifika Und diese beiden Dinge, `scope` und `receive`, werden benötigt, um eine neue `Request`-Instanz zu erstellen. -Um mehr über den `Request` zu erfahren, schauen Sie sich Starlettes Dokumentation zu Requests an. +Um mehr über den `Request` zu erfahren, schauen Sie sich Starlettes Dokumentation zu Requests an. /// diff --git a/docs/de/docs/index.md b/docs/de/docs/index.md index edcb61b94..4be65071b 100644 --- a/docs/de/docs/index.md +++ b/docs/de/docs/index.md @@ -123,7 +123,7 @@ Wenn Sie eine Starlette für die Webanteile. +* Starlette für die Webanteile. * Pydantic für die Datenanteile. ## Installation { #installation } @@ -229,7 +229,7 @@ INFO: Application startup complete.
Was der Befehl fastapi dev main.py macht ... -Der Befehl `fastapi dev` liest Ihre `main.py`-Datei, erkennt die **FastAPI**-App darin und startet einen Server mit Uvicorn. +Der Befehl `fastapi dev` liest Ihre `main.py`-Datei, erkennt die **FastAPI**-App darin und startet einen Server mit Uvicorn. Standardmäßig wird `fastapi dev` mit aktiviertem Auto-Reload für die lokale Entwicklung gestartet. @@ -470,7 +470,7 @@ Verwendet von Starlette: Verwendet von FastAPI: -* uvicorn – für den Server, der Ihre Anwendung lädt und bereitstellt. Dies umfasst `uvicorn[standard]`, das einige Abhängigkeiten (z. B. `uvloop`) beinhaltet, die für eine Bereitstellung mit hoher Performanz benötigt werden. +* uvicorn – für den Server, der Ihre Anwendung lädt und bereitstellt. Dies umfasst `uvicorn[standard]`, das einige Abhängigkeiten (z. B. `uvloop`) beinhaltet, die für eine Bereitstellung mit hoher Performanz benötigt werden. * `fastapi-cli[standard]` – um den `fastapi`-Befehl bereitzustellen. * Dies beinhaltet `fastapi-cloud-cli`, das es Ihnen ermöglicht, Ihre FastAPI-Anwendung auf FastAPI Cloud bereitzustellen. diff --git a/docs/de/docs/tutorial/background-tasks.md b/docs/de/docs/tutorial/background-tasks.md index ea85207ce..2c381ccfa 100644 --- a/docs/de/docs/tutorial/background-tasks.md +++ b/docs/de/docs/tutorial/background-tasks.md @@ -63,7 +63,7 @@ Und dann schreibt ein weiterer Hintergrundtask, der in der *Pfadoperation-Funkti ## Technische Details { #technical-details } -Die Klasse `BackgroundTasks` stammt direkt von `starlette.background`. +Die Klasse `BackgroundTasks` stammt direkt von `starlette.background`. Sie wird direkt in FastAPI importiert/inkludiert, sodass Sie sie von `fastapi` importieren können und vermeiden, versehentlich das alternative `BackgroundTask` (ohne das `s` am Ende) von `starlette.background` zu importieren. @@ -71,7 +71,7 @@ Indem Sie nur `BackgroundTasks` (und nicht `BackgroundTask`) verwenden, ist es d Es ist immer noch möglich, `BackgroundTask` allein in FastAPI zu verwenden, aber Sie müssen das Objekt in Ihrem Code erstellen und eine Starlette-`Response` zurückgeben, die es enthält. -Weitere Details finden Sie in Starlettes offizieller Dokumentation für Hintergrundtasks. +Weitere Details finden Sie in Starlettes offizieller Dokumentation für Hintergrundtasks. ## Vorbehalt { #caveat } diff --git a/docs/de/docs/tutorial/first-steps.md b/docs/de/docs/tutorial/first-steps.md index 374127c17..7ec98c53b 100644 --- a/docs/de/docs/tutorial/first-steps.md +++ b/docs/de/docs/tutorial/first-steps.md @@ -155,7 +155,7 @@ Ebenfalls können Sie es verwenden, um automatisch Code für Clients zu generier `FastAPI` ist eine Klasse, die direkt von `Starlette` erbt. -Sie können alle Starlette-Funktionalitäten auch mit `FastAPI` nutzen. +Sie können alle Starlette-Funktionalitäten auch mit `FastAPI` nutzen. /// diff --git a/docs/de/docs/tutorial/handling-errors.md b/docs/de/docs/tutorial/handling-errors.md index 51294f44f..58e4607c5 100644 --- a/docs/de/docs/tutorial/handling-errors.md +++ b/docs/de/docs/tutorial/handling-errors.md @@ -81,7 +81,7 @@ Aber falls Sie es für ein fortgeschrittenes Szenario benötigen, können Sie be ## Benutzerdefinierte Exceptionhandler installieren { #install-custom-exception-handlers } -Sie können benutzerdefinierte Exceptionhandler mit den gleichen Exception-Werkzeugen von Starlette hinzufügen. +Sie können benutzerdefinierte Exceptionhandler mit den gleichen Exception-Werkzeugen von Starlette hinzufügen. Angenommen, Sie haben eine benutzerdefinierte Exception `UnicornException`, die Sie (oder eine Bibliothek, die Sie verwenden) `raise`n könnten. diff --git a/docs/de/docs/tutorial/middleware.md b/docs/de/docs/tutorial/middleware.md index a1e2ba9df..6410deba1 100644 --- a/docs/de/docs/tutorial/middleware.md +++ b/docs/de/docs/tutorial/middleware.md @@ -37,7 +37,7 @@ Die Middleware-Funktion erhält: Beachten Sie, dass benutzerdefinierte proprietäre Header hinzugefügt werden können unter Verwendung des `X-`-Präfixes. -Wenn Sie jedoch benutzerdefinierte Header haben, die ein Client in einem Browser sehen soll, müssen Sie sie zu Ihrer CORS-Konfiguration ([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank}) hinzufügen, indem Sie den Parameter `expose_headers` verwenden, der in der Starlettes CORS-Dokumentation dokumentiert ist. +Wenn Sie jedoch benutzerdefinierte Header haben, die ein Client in einem Browser sehen soll, müssen Sie sie zu Ihrer CORS-Konfiguration ([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank}) hinzufügen, indem Sie den Parameter `expose_headers` verwenden, der in der Starlettes CORS-Dokumentation dokumentiert ist. /// diff --git a/docs/de/docs/tutorial/static-files.md b/docs/de/docs/tutorial/static-files.md index 5a6cfcb2b..0c4e7c8ab 100644 --- a/docs/de/docs/tutorial/static-files.md +++ b/docs/de/docs/tutorial/static-files.md @@ -37,4 +37,4 @@ Alle diese Parameter können anders als „`static`“ lauten, passen Sie sie an ## Weitere Informationen { #more-info } -Weitere Details und Optionen finden Sie in der Dokumentation von Starlette zu statischen Dateien. +Weitere Details und Optionen finden Sie in der Dokumentation von Starlette zu statischen Dateien. diff --git a/docs/de/docs/tutorial/testing.md b/docs/de/docs/tutorial/testing.md index 75ee9fade..9c28a2a22 100644 --- a/docs/de/docs/tutorial/testing.md +++ b/docs/de/docs/tutorial/testing.md @@ -1,6 +1,6 @@ # Testen { #testing } -Dank Starlette ist das Testen von **FastAPI**-Anwendungen einfach und macht Spaß. +Dank Starlette ist das Testen von **FastAPI**-Anwendungen einfach und macht Spaß. Es basiert auf HTTPX, welches wiederum auf der Grundlage von Requests konzipiert wurde, es ist also sehr vertraut und intuitiv. diff --git a/docs/em/docs/advanced/events.md b/docs/em/docs/advanced/events.md index 68adb6d65..dcaac710e 100644 --- a/docs/em/docs/advanced/events.md +++ b/docs/em/docs/advanced/events.md @@ -140,7 +140,7 @@ async with lifespan(app): /// info -👆 💪 ✍ 🌅 🔃 👫 🎉 🐕‍🦺 💃 🎉' 🩺. +👆 💪 ✍ 🌅 🔃 👫 🎉 🐕‍🦺 💃 🎉' 🩺. /// diff --git a/docs/em/docs/advanced/middleware.md b/docs/em/docs/advanced/middleware.md index cb04fa3fb..22d707062 100644 --- a/docs/em/docs/advanced/middleware.md +++ b/docs/em/docs/advanced/middleware.md @@ -92,4 +92,4 @@ app.add_middleware(UnicornMiddleware, some_config="rainbow") * Uvicorn `ProxyHeadersMiddleware` * 🇸🇲 -👀 🎏 💪 🛠️ ✅ 💃 🛠️ 🩺 & 🔫 👌 📇. +👀 🎏 💪 🛠️ ✅ 💃 🛠️ 🩺 & 🔫 👌 📇. diff --git a/docs/em/docs/advanced/response-cookies.md b/docs/em/docs/advanced/response-cookies.md index d9fdbaa87..a6e37ad74 100644 --- a/docs/em/docs/advanced/response-cookies.md +++ b/docs/em/docs/advanced/response-cookies.md @@ -48,4 +48,4 @@ /// -👀 🌐 💪 🔢 & 🎛, ✅ 🧾 💃. +👀 🌐 💪 🔢 & 🎛, ✅ 🧾 💃. diff --git a/docs/em/docs/advanced/response-headers.md b/docs/em/docs/advanced/response-headers.md index e9e1b62d2..c255380d6 100644 --- a/docs/em/docs/advanced/response-headers.md +++ b/docs/em/docs/advanced/response-headers.md @@ -38,4 +38,4 @@ ✔️ 🤯 👈 🛃 © 🎚 💪 🚮 ⚙️ '✖-' 🔡. -✋️ 🚥 👆 ✔️ 🛃 🎚 👈 👆 💚 👩‍💻 🖥 💪 👀, 👆 💪 🚮 👫 👆 ⚜ 📳 (✍ 🌅 [⚜ (✖️-🇨🇳 ℹ 🤝)](../tutorial/cors.md){.internal-link target=_blank}), ⚙️ 🔢 `expose_headers` 📄 💃 ⚜ 🩺. +✋️ 🚥 👆 ✔️ 🛃 🎚 👈 👆 💚 👩‍💻 🖥 💪 👀, 👆 💪 🚮 👫 👆 ⚜ 📳 (✍ 🌅 [⚜ (✖️-🇨🇳 ℹ 🤝)](../tutorial/cors.md){.internal-link target=_blank}), ⚙️ 🔢 `expose_headers` 📄 💃 ⚜ 🩺. diff --git a/docs/em/docs/advanced/templates.md b/docs/em/docs/advanced/templates.md index ad4d4fc71..2e8f56228 100644 --- a/docs/em/docs/advanced/templates.md +++ b/docs/em/docs/advanced/templates.md @@ -81,4 +81,4 @@ $ pip install jinja2 ## 🌅 ℹ -🌅 ℹ, 🔌 ❔ 💯 📄, ✅ 💃 🩺 🔛 📄. +🌅 ℹ, 🔌 ❔ 💯 📄, ✅ 💃 🩺 🔛 📄. diff --git a/docs/em/docs/advanced/testing-websockets.md b/docs/em/docs/advanced/testing-websockets.md index 2a01de629..96aa8b765 100644 --- a/docs/em/docs/advanced/testing-websockets.md +++ b/docs/em/docs/advanced/testing-websockets.md @@ -8,6 +8,6 @@ /// note -🌅 ℹ, ✅ 💃 🧾 🔬 *️⃣ . +🌅 ℹ, ✅ 💃 🧾 🔬 *️⃣ . /// diff --git a/docs/em/docs/advanced/using-request-directly.md b/docs/em/docs/advanced/using-request-directly.md index 9530d49bc..238557e5e 100644 --- a/docs/em/docs/advanced/using-request-directly.md +++ b/docs/em/docs/advanced/using-request-directly.md @@ -15,7 +15,7 @@ ## ℹ 🔃 `Request` 🎚 -**FastAPI** 🤙 **💃** 🔘, ⏮️ 🧽 📚 🧰 🔛 🔝, 👆 💪 ⚙️ 💃 `Request` 🎚 🔗 🕐❔ 👆 💪. +**FastAPI** 🤙 **💃** 🔘, ⏮️ 🧽 📚 🧰 🔛 🔝, 👆 💪 ⚙️ 💃 `Request` 🎚 🔗 🕐❔ 👆 💪. ⚫️ 🔜 ⛓ 👈 🚥 👆 🤚 📊 ⚪️➡️ `Request` 🎚 🔗 (🖼, ✍ 💪) ⚫️ 🏆 🚫 ✔, 🗜 ⚖️ 📄 (⏮️ 🗄, 🏧 🛠️ 👩‍💻 🔢) FastAPI. @@ -45,7 +45,7 @@ ## `Request` 🧾 -👆 💪 ✍ 🌅 ℹ 🔃 `Request` 🎚 🛂 💃 🧾 🕸. +👆 💪 ✍ 🌅 ℹ 🔃 `Request` 🎚 🛂 💃 🧾 🕸. /// note | 📡 ℹ diff --git a/docs/em/docs/advanced/websockets.md b/docs/em/docs/advanced/websockets.md index cc6e5c5f0..a097778c7 100644 --- a/docs/em/docs/advanced/websockets.md +++ b/docs/em/docs/advanced/websockets.md @@ -182,5 +182,5 @@ Client #1596980209979 left the chat 💡 🌅 🔃 🎛, ✅ 💃 🧾: -* `WebSocket` 🎓. -* 🎓-⚓️ *️⃣ 🚚. +* `WebSocket` 🎓. +* 🎓-⚓️ *️⃣ 🚚. diff --git a/docs/em/docs/alternatives.md b/docs/em/docs/alternatives.md index 59b587285..4cbac7539 100644 --- a/docs/em/docs/alternatives.md +++ b/docs/em/docs/alternatives.md @@ -417,7 +417,7 @@ Pydantic 🗃 🔬 💽 🔬, 🛠️ & 🧾 (⚙️ 🎻 🔗) ⚓️ 🔛 /// -### 💃 +### 💃 💃 💿 🔫 🛠️/🧰, ❔ 💯 🏗 ↕-🎭 ✳ 🐕‍🦺. @@ -462,7 +462,7 @@ Pydantic 🗃 🔬 💽 🔬, 🛠️ & 🧾 (⚙️ 🎻 🔗) ⚓️ 🔛 /// -### Uvicorn +### Uvicorn Uvicorn 🌩-⏩ 🔫 💽, 🏗 🔛 uvloop & httptool. diff --git a/docs/em/docs/deployment/manually.md b/docs/em/docs/deployment/manually.md index 8ebe00c7c..4fa2d13e2 100644 --- a/docs/em/docs/deployment/manually.md +++ b/docs/em/docs/deployment/manually.md @@ -4,7 +4,7 @@ 📤 3️⃣ 👑 🎛: -* Uvicorn: ↕ 🎭 🔫 💽. +* Uvicorn: ↕ 🎭 🔫 💽. * Hypercorn: 🔫 💽 🔗 ⏮️ 🇺🇸🔍/2️⃣ & 🎻 👪 🎏 ⚒. * 👸: 🔫 💽 🏗 ✳ 📻. @@ -24,7 +24,7 @@ //// tab | Uvicorn -* Uvicorn, 🌩-⏩ 🔫 💽, 🏗 🔛 uvloop & httptool. +* Uvicorn, 🌩-⏩ 🔫 💽, 🏗 🔛 uvloop & httptool.
diff --git a/docs/em/docs/features.md b/docs/em/docs/features.md index 13cafa72f..ccbed0cae 100644 --- a/docs/em/docs/features.md +++ b/docs/em/docs/features.md @@ -159,7 +159,7 @@ FastAPI 🔌 📶 ⏩ ⚙️, ✋️ 📶 🏋️ . -✋️ 🚥 👆 ✔️ 🛃 🎚 👈 👆 💚 👩‍💻 🖥 💪 👀, 👆 💪 🚮 👫 👆 ⚜ 📳 ([⚜ (✖️-🇨🇳 ℹ 🤝)](cors.md){.internal-link target=_blank}) ⚙️ 🔢 `expose_headers` 📄 💃 ⚜ 🩺. +✋️ 🚥 👆 ✔️ 🛃 🎚 👈 👆 💚 👩‍💻 🖥 💪 👀, 👆 💪 🚮 👫 👆 ⚜ 📳 ([⚜ (✖️-🇨🇳 ℹ 🤝)](cors.md){.internal-link target=_blank}) ⚙️ 🔢 `expose_headers` 📄 💃 ⚜ 🩺. /// diff --git a/docs/em/docs/tutorial/static-files.md b/docs/em/docs/tutorial/static-files.md index 6ff6e37a9..27685c06d 100644 --- a/docs/em/docs/tutorial/static-files.md +++ b/docs/em/docs/tutorial/static-files.md @@ -37,4 +37,4 @@ ## 🌅 ℹ -🌖 ℹ & 🎛 ✅ 💃 🩺 🔃 🎻 📁. +🌖 ℹ & 🎛 ✅ 💃 🩺 🔃 🎻 📁. diff --git a/docs/em/docs/tutorial/testing.md b/docs/em/docs/tutorial/testing.md index cb4a1ca21..2e4a531f7 100644 --- a/docs/em/docs/tutorial/testing.md +++ b/docs/em/docs/tutorial/testing.md @@ -1,6 +1,6 @@ # 🔬 -👏 💃, 🔬 **FastAPI** 🈸 ⏩ & 😌. +👏 💃, 🔬 **FastAPI** 🈸 ⏩ & 😌. ⚫️ ⚓️ 🔛 🇸🇲, ❔ 🔄 🏗 ⚓️ 🔛 📨, ⚫️ 📶 😰 & 🏋️. diff --git a/docs/en/docs/advanced/events.md b/docs/en/docs/advanced/events.md index c805e81ee..d9e3cb52e 100644 --- a/docs/en/docs/advanced/events.md +++ b/docs/en/docs/advanced/events.md @@ -154,7 +154,7 @@ Underneath, in the ASGI technical specification, this is part of the Starlette's Lifespan' docs. +You can read more about the Starlette `lifespan` handlers in Starlette's Lifespan' docs. Including how to handle lifespan state that can be used in other areas of your code. diff --git a/docs/en/docs/advanced/middleware.md b/docs/en/docs/advanced/middleware.md index d1be4afff..8deb0d917 100644 --- a/docs/en/docs/advanced/middleware.md +++ b/docs/en/docs/advanced/middleware.md @@ -94,4 +94,4 @@ For example: * Uvicorn's `ProxyHeadersMiddleware` * MessagePack -To see other available middlewares check Starlette's Middleware docs and the ASGI Awesome List. +To see other available middlewares check Starlette's Middleware docs and the ASGI Awesome List. diff --git a/docs/en/docs/advanced/response-cookies.md b/docs/en/docs/advanced/response-cookies.md index d8f77b56a..1f41d84b7 100644 --- a/docs/en/docs/advanced/response-cookies.md +++ b/docs/en/docs/advanced/response-cookies.md @@ -48,4 +48,4 @@ And as the `Response` can be used frequently to set headers and cookies, **FastA /// -To see all the available parameters and options, check the documentation in Starlette. +To see all the available parameters and options, check the documentation in Starlette. diff --git a/docs/en/docs/advanced/response-headers.md b/docs/en/docs/advanced/response-headers.md index 19c9ff2ad..855ba05f8 100644 --- a/docs/en/docs/advanced/response-headers.md +++ b/docs/en/docs/advanced/response-headers.md @@ -38,4 +38,4 @@ And as the `Response` can be used frequently to set headers and cookies, **FastA Keep in mind that custom proprietary headers can be added using the `X-` prefix. -But if you have custom headers that you want a client in a browser to be able to see, you need to add them to your CORS configurations (read more in [CORS (Cross-Origin Resource Sharing)](../tutorial/cors.md){.internal-link target=_blank}), using the parameter `expose_headers` documented in Starlette's CORS docs. +But if you have custom headers that you want a client in a browser to be able to see, you need to add them to your CORS configurations (read more in [CORS (Cross-Origin Resource Sharing)](../tutorial/cors.md){.internal-link target=_blank}), using the parameter `expose_headers` documented in Starlette's CORS docs. diff --git a/docs/en/docs/advanced/templates.md b/docs/en/docs/advanced/templates.md index f41c47fe8..356f4d9ca 100644 --- a/docs/en/docs/advanced/templates.md +++ b/docs/en/docs/advanced/templates.md @@ -123,4 +123,4 @@ And because you are using `StaticFiles`, that CSS file would be served automatic ## More details { #more-details } -For more details, including how to test templates, check Starlette's docs on templates. +For more details, including how to test templates, check Starlette's docs on templates. diff --git a/docs/en/docs/advanced/testing-events.md b/docs/en/docs/advanced/testing-events.md index cb8881a09..dd93374c4 100644 --- a/docs/en/docs/advanced/testing-events.md +++ b/docs/en/docs/advanced/testing-events.md @@ -5,7 +5,7 @@ When you need `lifespan` to run in your tests, you can use the `TestClient` with {* ../../docs_src/app_testing/tutorial004.py hl[9:15,18,27:28,30:32,41:43] *} -You can read more details about the ["Running lifespan in tests in the official Starlette documentation site."](https://www.starlette.io/lifespan/#running-lifespan-in-tests) +You can read more details about the ["Running lifespan in tests in the official Starlette documentation site."](https://www.starlette.dev/lifespan/#running-lifespan-in-tests) For the deprecated `startup` and `shutdown` events, you can use the `TestClient` as follows: diff --git a/docs/en/docs/advanced/testing-websockets.md b/docs/en/docs/advanced/testing-websockets.md index 22f9bb4a0..27eb2de2f 100644 --- a/docs/en/docs/advanced/testing-websockets.md +++ b/docs/en/docs/advanced/testing-websockets.md @@ -8,6 +8,6 @@ For this, you use the `TestClient` in a `with` statement, connecting to the WebS /// note -For more details, check Starlette's documentation for testing WebSockets. +For more details, check Starlette's documentation for testing WebSockets. /// diff --git a/docs/en/docs/advanced/using-request-directly.md b/docs/en/docs/advanced/using-request-directly.md index e412ad462..c71d3b05d 100644 --- a/docs/en/docs/advanced/using-request-directly.md +++ b/docs/en/docs/advanced/using-request-directly.md @@ -15,7 +15,7 @@ But there are situations where you might need to access the `Request` object dir ## Details about the `Request` object { #details-about-the-request-object } -As **FastAPI** is actually **Starlette** underneath, with a layer of several tools on top, you can use Starlette's `Request` object directly when you need to. +As **FastAPI** is actually **Starlette** underneath, with a layer of several tools on top, you can use Starlette's `Request` object directly when you need to. It would also mean that if you get data from the `Request` object directly (for example, read the body) it won't be validated, converted or documented (with OpenAPI, for the automatic API user interface) by FastAPI. @@ -45,7 +45,7 @@ The same way, you can declare any other parameter as normally, and additionally, ## `Request` documentation { #request-documentation } -You can read more details about the `Request` object in the official Starlette documentation site. +You can read more details about the `Request` object in the official Starlette documentation site. /// note | Technical Details diff --git a/docs/en/docs/advanced/websockets.md b/docs/en/docs/advanced/websockets.md index 917dd79bd..ce11485a8 100644 --- a/docs/en/docs/advanced/websockets.md +++ b/docs/en/docs/advanced/websockets.md @@ -182,5 +182,5 @@ If you need something easy to integrate with FastAPI but that is more robust, su To learn more about the options, check Starlette's documentation for: -* The `WebSocket` class. -* Class-based WebSocket handling. +* The `WebSocket` class. +* Class-based WebSocket handling. diff --git a/docs/en/docs/alternatives.md b/docs/en/docs/alternatives.md index f0576bc47..e65681543 100644 --- a/docs/en/docs/alternatives.md +++ b/docs/en/docs/alternatives.md @@ -417,7 +417,7 @@ Handle all the data validation, data serialization and automatic model documenta /// -### Starlette { #starlette } +### Starlette { #starlette } Starlette is a lightweight ASGI framework/toolkit, which is ideal for building high-performance asyncio services. @@ -462,7 +462,7 @@ So, anything that you can do with Starlette, you can do it directly with **FastA /// -### Uvicorn { #uvicorn } +### Uvicorn { #uvicorn } Uvicorn is a lightning-fast ASGI server, built on uvloop and httptools. diff --git a/docs/en/docs/deployment/manually.md b/docs/en/docs/deployment/manually.md index 8bb3945bc..311efb99f 100644 --- a/docs/en/docs/deployment/manually.md +++ b/docs/en/docs/deployment/manually.md @@ -52,7 +52,7 @@ The main thing you need to run a **FastAPI** application (or any other ASGI appl There are several alternatives, including: -* Uvicorn: a high performance ASGI server. +* Uvicorn: a high performance ASGI server. * Hypercorn: an ASGI server compatible with HTTP/2 and Trio among other features. * Daphne: the ASGI server built for Django Channels. * Granian: A Rust HTTP server for Python applications. diff --git a/docs/en/docs/fastapi-cli.md b/docs/en/docs/fastapi-cli.md index 0fb7789db..3e5f4e350 100644 --- a/docs/en/docs/fastapi-cli.md +++ b/docs/en/docs/fastapi-cli.md @@ -52,7 +52,7 @@ FastAPI CLI takes the path to your Python program (e.g. `main.py`) and automatic For production you would use `fastapi run` instead. 🚀 -Internally, **FastAPI CLI** uses Uvicorn, a high-performance, production-ready, ASGI server. 😎 +Internally, **FastAPI CLI** uses Uvicorn, a high-performance, production-ready, ASGI server. 😎 ## `fastapi dev` { #fastapi-dev } diff --git a/docs/en/docs/features.md b/docs/en/docs/features.md index d44d7a6ac..a345e4a0e 100644 --- a/docs/en/docs/features.md +++ b/docs/en/docs/features.md @@ -159,7 +159,7 @@ Any integration is designed to be so simple to use (with dependencies) that you ## Starlette features { #starlette-features } -**FastAPI** is fully compatible with (and based on) Starlette. So, any additional Starlette code you have, will also work. +**FastAPI** is fully compatible with (and based on) Starlette. So, any additional Starlette code you have, will also work. `FastAPI` is actually a sub-class of `Starlette`. So, if you already know or use Starlette, most of the functionality will work the same way. diff --git a/docs/en/docs/history-design-future.md b/docs/en/docs/history-design-future.md index 2182c415c..6175dcbbe 100644 --- a/docs/en/docs/history-design-future.md +++ b/docs/en/docs/history-design-future.md @@ -58,7 +58,7 @@ After testing several alternatives, I decided that I was going to use **Starlette**, the other key requirement. +During the development, I also contributed to **Starlette**, the other key requirement. ## Development { #development } diff --git a/docs/en/docs/how-to/custom-request-and-route.md b/docs/en/docs/how-to/custom-request-and-route.md index 6df24a080..884c8ed04 100644 --- a/docs/en/docs/how-to/custom-request-and-route.md +++ b/docs/en/docs/how-to/custom-request-and-route.md @@ -66,7 +66,7 @@ The `scope` `dict` and `receive` function are both part of the ASGI specificatio And those two things, `scope` and `receive`, are what is needed to create a new `Request` instance. -To learn more about the `Request` check Starlette's docs about Requests. +To learn more about the `Request` check Starlette's docs about Requests. /// diff --git a/docs/en/docs/index.md b/docs/en/docs/index.md index aaadf3cc2..35c46d15f 100644 --- a/docs/en/docs/index.md +++ b/docs/en/docs/index.md @@ -123,7 +123,7 @@ If you are building a CLI app to be FastAPI stands on the shoulders of giants: -* Starlette for the web parts. +* Starlette for the web parts. * Pydantic for the data parts. ## Installation { #installation } @@ -229,7 +229,7 @@ INFO: Application startup complete.
About the command fastapi dev main.py... -The command `fastapi dev` reads your `main.py` file, detects the **FastAPI** app in it, and starts a server using Uvicorn. +The command `fastapi dev` reads your `main.py` file, detects the **FastAPI** app in it, and starts a server using Uvicorn. By default, `fastapi dev` will start with auto-reload enabled for local development. @@ -470,7 +470,7 @@ Used by Starlette: Used by FastAPI: -* uvicorn - for the server that loads and serves your application. This includes `uvicorn[standard]`, which includes some dependencies (e.g. `uvloop`) needed for high performance serving. +* uvicorn - for the server that loads and serves your application. This includes `uvicorn[standard]`, which includes some dependencies (e.g. `uvloop`) needed for high performance serving. * `fastapi-cli[standard]` - to provide the `fastapi` command. * This includes `fastapi-cloud-cli`, which allows you to deploy your FastAPI application to FastAPI Cloud. diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 663e99a23..628f26562 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -5526,7 +5526,7 @@ Note: all the previous parameters are still there, so it's still possible to dec * Upgrade the compatible version of Starlette to `0.12.0`. * This includes support for ASGI 3 (the latest version of the standard). - * It's now possible to use [Starlette's `StreamingResponse`](https://www.starlette.io/responses/#streamingresponse) with iterators, like [file-like](https://docs.python.org/3/glossary.html#term-file-like-object) objects (as those returned by `open()`). + * It's now possible to use [Starlette's `StreamingResponse`](https://www.starlette.dev/responses/#streamingresponse) with iterators, like [file-like](https://docs.python.org/3/glossary.html#term-file-like-object) objects (as those returned by `open()`). * It's now possible to use the low level utility `iterate_in_threadpool` from `starlette.concurrency` (for advanced scenarios). * PR [#243](https://github.com/tiangolo/fastapi/pull/243). diff --git a/docs/en/docs/tutorial/background-tasks.md b/docs/en/docs/tutorial/background-tasks.md index 6e16410a3..ab44f89c1 100644 --- a/docs/en/docs/tutorial/background-tasks.md +++ b/docs/en/docs/tutorial/background-tasks.md @@ -63,7 +63,7 @@ And then another background task generated at the *path operation function* will ## Technical Details { #technical-details } -The class `BackgroundTasks` comes directly from `starlette.background`. +The class `BackgroundTasks` comes directly from `starlette.background`. It is imported/included directly into FastAPI so that you can import it from `fastapi` and avoid accidentally importing the alternative `BackgroundTask` (without the `s` at the end) from `starlette.background`. @@ -71,7 +71,7 @@ By only using `BackgroundTasks` (and not `BackgroundTask`), it's then possible t It's still possible to use `BackgroundTask` alone in FastAPI, but you have to create the object in your code and return a Starlette `Response` including it. -You can see more details in Starlette's official docs for Background Tasks. +You can see more details in Starlette's official docs for Background Tasks. ## Caveat { #caveat } diff --git a/docs/en/docs/tutorial/first-steps.md b/docs/en/docs/tutorial/first-steps.md index e75e40991..7d4c12de8 100644 --- a/docs/en/docs/tutorial/first-steps.md +++ b/docs/en/docs/tutorial/first-steps.md @@ -155,7 +155,7 @@ You could also use it to generate code automatically, for clients that communica `FastAPI` is a class that inherits directly from `Starlette`. -You can use all the Starlette functionality with `FastAPI` too. +You can use all the Starlette functionality with `FastAPI` too. /// diff --git a/docs/en/docs/tutorial/handling-errors.md b/docs/en/docs/tutorial/handling-errors.md index 58bf8ffa7..53501837c 100644 --- a/docs/en/docs/tutorial/handling-errors.md +++ b/docs/en/docs/tutorial/handling-errors.md @@ -81,7 +81,7 @@ But in case you needed it for an advanced scenario, you can add custom headers: ## Install custom exception handlers { #install-custom-exception-handlers } -You can add custom exception handlers with the same exception utilities from Starlette. +You can add custom exception handlers with the same exception utilities from Starlette. Let's say you have a custom exception `UnicornException` that you (or a library you use) might `raise`. diff --git a/docs/en/docs/tutorial/middleware.md b/docs/en/docs/tutorial/middleware.md index bc0519c67..d8889fc63 100644 --- a/docs/en/docs/tutorial/middleware.md +++ b/docs/en/docs/tutorial/middleware.md @@ -37,7 +37,7 @@ The middleware function receives: Keep in mind that custom proprietary headers can be added using the `X-` prefix. -But if you have custom headers that you want a client in a browser to be able to see, you need to add them to your CORS configurations ([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank}) using the parameter `expose_headers` documented in Starlette's CORS docs. +But if you have custom headers that you want a client in a browser to be able to see, you need to add them to your CORS configurations ([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank}) using the parameter `expose_headers` documented in Starlette's CORS docs. /// diff --git a/docs/en/docs/tutorial/static-files.md b/docs/en/docs/tutorial/static-files.md index 5b75d048b..66b934d4f 100644 --- a/docs/en/docs/tutorial/static-files.md +++ b/docs/en/docs/tutorial/static-files.md @@ -37,4 +37,4 @@ All these parameters can be different than "`static`", adjust them with the need ## More info { #more-info } -For more details and options check Starlette's docs about Static Files. +For more details and options check Starlette's docs about Static Files. diff --git a/docs/en/docs/tutorial/testing.md b/docs/en/docs/tutorial/testing.md index 1e333c8f1..3dcf5dc4a 100644 --- a/docs/en/docs/tutorial/testing.md +++ b/docs/en/docs/tutorial/testing.md @@ -1,6 +1,6 @@ # Testing { #testing } -Thanks to Starlette, testing **FastAPI** applications is easy and enjoyable. +Thanks to Starlette, testing **FastAPI** applications is easy and enjoyable. It is based on HTTPX, which in turn is designed based on Requests, so it's very familiar and intuitive. diff --git a/docs/es/docs/advanced/events.md b/docs/es/docs/advanced/events.md index 022fb5a42..a33b51791 100644 --- a/docs/es/docs/advanced/events.md +++ b/docs/es/docs/advanced/events.md @@ -154,7 +154,7 @@ Por debajo, en la especificación técnica ASGI, esto es parte del la documentación de `Lifespan` de Starlette. +Puedes leer más sobre los manejadores `lifespan` de Starlette en la documentación de `Lifespan` de Starlette. Incluyendo cómo manejar el estado de lifespan que puede ser usado en otras áreas de tu código. diff --git a/docs/es/docs/advanced/middleware.md b/docs/es/docs/advanced/middleware.md index b8fd86185..0c8c44b88 100644 --- a/docs/es/docs/advanced/middleware.md +++ b/docs/es/docs/advanced/middleware.md @@ -93,4 +93,4 @@ Por ejemplo: * `ProxyHeadersMiddleware` de Uvicorn * MessagePack -Para ver otros middlewares disponibles, revisa la documentación de Middleware de Starlette y la Lista ASGI Awesome. +Para ver otros middlewares disponibles, revisa la documentación de Middleware de Starlette y la Lista ASGI Awesome. diff --git a/docs/es/docs/advanced/response-cookies.md b/docs/es/docs/advanced/response-cookies.md index c4472eaa1..05b78528e 100644 --- a/docs/es/docs/advanced/response-cookies.md +++ b/docs/es/docs/advanced/response-cookies.md @@ -48,4 +48,4 @@ Y como el `Response` se puede usar frecuentemente para establecer headers y cook /// -Para ver todos los parámetros y opciones disponibles, revisa la documentación en Starlette. +Para ver todos los parámetros y opciones disponibles, revisa la documentación en Starlette. diff --git a/docs/es/docs/advanced/response-headers.md b/docs/es/docs/advanced/response-headers.md index 49eaa53c1..31a135c40 100644 --- a/docs/es/docs/advanced/response-headers.md +++ b/docs/es/docs/advanced/response-headers.md @@ -38,4 +38,4 @@ Y como el `Response` se puede usar frecuentemente para establecer headers y cook Ten en cuenta que los headers propietarios personalizados se pueden agregar usando el prefijo 'X-'. -Pero si tienes headers personalizados que quieres que un cliente en un navegador pueda ver, necesitas agregarlos a tus configuraciones de CORS (leer más en [CORS (Cross-Origin Resource Sharing)](../tutorial/cors.md){.internal-link target=_blank}), usando el parámetro `expose_headers` documentado en la documentación CORS de Starlette. +Pero si tienes headers personalizados que quieres que un cliente en un navegador pueda ver, necesitas agregarlos a tus configuraciones de CORS (leer más en [CORS (Cross-Origin Resource Sharing)](../tutorial/cors.md){.internal-link target=_blank}), usando el parámetro `expose_headers` documentado en la documentación CORS de Starlette. diff --git a/docs/es/docs/advanced/templates.md b/docs/es/docs/advanced/templates.md index 9de866c2b..101819737 100644 --- a/docs/es/docs/advanced/templates.md +++ b/docs/es/docs/advanced/templates.md @@ -123,4 +123,4 @@ Y porque estás usando `StaticFiles`, ese archivo CSS sería servido automática ## Más detalles -Para más detalles, incluyendo cómo testear plantillas, revisa la documentación de Starlette sobre plantillas. +Para más detalles, incluyendo cómo testear plantillas, revisa la documentación de Starlette sobre plantillas. diff --git a/docs/es/docs/advanced/testing-websockets.md b/docs/es/docs/advanced/testing-websockets.md index 6d2eaf94d..190e3a224 100644 --- a/docs/es/docs/advanced/testing-websockets.md +++ b/docs/es/docs/advanced/testing-websockets.md @@ -8,6 +8,6 @@ Para esto, usas el `TestClient` en un statement `with`, conectándote al WebSock /// note | Nota -Para más detalles, revisa la documentación de Starlette sobre probando sesiones WebSocket. +Para más detalles, revisa la documentación de Starlette sobre probando sesiones WebSocket. /// diff --git a/docs/es/docs/advanced/using-request-directly.md b/docs/es/docs/advanced/using-request-directly.md index be8afffcc..f61e49849 100644 --- a/docs/es/docs/advanced/using-request-directly.md +++ b/docs/es/docs/advanced/using-request-directly.md @@ -15,7 +15,7 @@ Pero hay situaciones donde podrías necesitar acceder al objeto `Request` direct ## Detalles sobre el objeto `Request` -Como **FastAPI** es en realidad **Starlette** por debajo, con una capa de varias herramientas encima, puedes usar el objeto `Request` de Starlette directamente cuando lo necesites. +Como **FastAPI** es en realidad **Starlette** por debajo, con una capa de varias herramientas encima, puedes usar el objeto `Request` de Starlette directamente cuando lo necesites. También significa que si obtienes datos del objeto `Request` directamente (por ejemplo, leyendo el cuerpo) no serán validados, convertidos o documentados (con OpenAPI, para la interfaz automática de usuario de la API) por FastAPI. @@ -45,7 +45,7 @@ De la misma manera, puedes declarar cualquier otro parámetro como normalmente, ## Documentación de `Request` -Puedes leer más detalles sobre el objeto `Request` en el sitio de documentación oficial de Starlette. +Puedes leer más detalles sobre el objeto `Request` en el sitio de documentación oficial de Starlette. /// note | Detalles Técnicos diff --git a/docs/es/docs/advanced/websockets.md b/docs/es/docs/advanced/websockets.md index 95141c1ca..1320f8bb7 100644 --- a/docs/es/docs/advanced/websockets.md +++ b/docs/es/docs/advanced/websockets.md @@ -182,5 +182,5 @@ Si necesitas algo fácil de integrar con FastAPI pero que sea más robusto, sopo Para aprender más sobre las opciones, revisa la documentación de Starlette para: -* La clase `WebSocket`. -* Manejo de WebSocket basado en clases. +* La clase `WebSocket`. +* Manejo de WebSocket basado en clases. diff --git a/docs/es/docs/alternatives.md b/docs/es/docs/alternatives.md index 753b827c0..6605b7bb0 100644 --- a/docs/es/docs/alternatives.md +++ b/docs/es/docs/alternatives.md @@ -417,7 +417,7 @@ Manejar toda la validación de datos, serialización de datos y documentación a /// -### Starlette +### Starlette Starlette es un framework/toolkit ASGI liviano, ideal para construir servicios asyncio de alto rendimiento. @@ -462,7 +462,7 @@ Por lo tanto, cualquier cosa que puedas hacer con Starlette, puedes hacerlo dire /// -### Uvicorn +### Uvicorn Uvicorn es un servidor ASGI extremadamente rápido, construido sobre uvloop y httptools. diff --git a/docs/es/docs/deployment/manually.md b/docs/es/docs/deployment/manually.md index 509b9ebdb..b56cf9514 100644 --- a/docs/es/docs/deployment/manually.md +++ b/docs/es/docs/deployment/manually.md @@ -64,7 +64,7 @@ Lo principal que necesitas para ejecutar una aplicación **FastAPI** (o cualquie Hay varias alternativas, incluyendo: -* Uvicorn: un servidor ASGI de alto rendimiento. +* Uvicorn: un servidor ASGI de alto rendimiento. * Hypercorn: un servidor ASGI compatible con HTTP/2 y Trio entre otras funcionalidades. * Daphne: el servidor ASGI construido para Django Channels. * Granian: Un servidor HTTP Rust para aplicaciones en Python. diff --git a/docs/es/docs/fastapi-cli.md b/docs/es/docs/fastapi-cli.md index 9d7629fdb..34b18ee3d 100644 --- a/docs/es/docs/fastapi-cli.md +++ b/docs/es/docs/fastapi-cli.md @@ -52,7 +52,7 @@ FastAPI CLI toma el path de tu programa Python (por ejemplo, `main.py`), detecta Para producción usarías `fastapi run` en su lugar. 🚀 -Internamente, **FastAPI CLI** usa Uvicorn, un servidor ASGI de alto rendimiento y listo para producción. 😎 +Internamente, **FastAPI CLI** usa Uvicorn, un servidor ASGI de alto rendimiento y listo para producción. 😎 ## `fastapi dev` diff --git a/docs/es/docs/features.md b/docs/es/docs/features.md index 472fdd736..ac73bee16 100644 --- a/docs/es/docs/features.md +++ b/docs/es/docs/features.md @@ -159,7 +159,7 @@ Cualquier integración está diseñada para ser tan simple de usar (con dependen ## Funcionalidades de Starlette -**FastAPI** es totalmente compatible con (y está basado en) Starlette. Así que, cualquier código adicional de Starlette que tengas, también funcionará. +**FastAPI** es totalmente compatible con (y está basado en) Starlette. Así que, cualquier código adicional de Starlette que tengas, también funcionará. `FastAPI` es en realidad una subclase de `Starlette`. Así que, si ya conoces o usas Starlette, la mayoría de las funcionalidades funcionarán de la misma manera. diff --git a/docs/es/docs/history-design-future.md b/docs/es/docs/history-design-future.md index 8beb4f400..7ed4fdf05 100644 --- a/docs/es/docs/history-design-future.md +++ b/docs/es/docs/history-design-future.md @@ -58,7 +58,7 @@ Después de probar varias alternativas, decidí que iba a usar **Starlette**, el otro requisito clave. +Durante el desarrollo, también contribuí a **Starlette**, el otro requisito clave. ## Desarrollo diff --git a/docs/es/docs/how-to/custom-request-and-route.md b/docs/es/docs/how-to/custom-request-and-route.md index 0b479bf00..a6ea657d1 100644 --- a/docs/es/docs/how-to/custom-request-and-route.md +++ b/docs/es/docs/how-to/custom-request-and-route.md @@ -66,7 +66,7 @@ El `dict` `scope` y la función `receive` son ambos parte de la especificación Y esas dos cosas, `scope` y `receive`, son lo que se necesita para crear una nueva *Request instance*. -Para aprender más sobre el `Request`, revisa la documentación de Starlette sobre Requests. +Para aprender más sobre el `Request`, revisa la documentación de Starlette sobre Requests. /// diff --git a/docs/es/docs/index.md b/docs/es/docs/index.md index 4c8c703b3..a965fa4b5 100644 --- a/docs/es/docs/index.md +++ b/docs/es/docs/index.md @@ -123,7 +123,7 @@ Si estás construyendo una aplicación de Starlette para las partes web. +* Starlette para las partes web. * Pydantic para las partes de datos. ## Instalación @@ -229,7 +229,7 @@ INFO: Application startup complete.
Acerca del comando fastapi dev main.py... -El comando `fastapi dev` lee tu archivo `main.py`, detecta la app **FastAPI** en él y arranca un servidor usando Uvicorn. +El comando `fastapi dev` lee tu archivo `main.py`, detecta la app **FastAPI** en él y arranca un servidor usando Uvicorn. Por defecto, `fastapi dev` comenzará con auto-recarga habilitada para el desarrollo local. @@ -470,7 +470,7 @@ Usadas por Starlette: Usadas por FastAPI / Starlette: -* uvicorn - para el servidor que carga y sirve tu aplicación. Esto incluye `uvicorn[standard]`, que incluye algunas dependencias (por ejemplo, `uvloop`) necesarias para servir con alto rendimiento. +* uvicorn - para el servidor que carga y sirve tu aplicación. Esto incluye `uvicorn[standard]`, que incluye algunas dependencias (por ejemplo, `uvloop`) necesarias para servir con alto rendimiento. * `fastapi-cli` - para proporcionar el comando `fastapi`. ### Sin Dependencias `standard` diff --git a/docs/es/docs/tutorial/background-tasks.md b/docs/es/docs/tutorial/background-tasks.md index 3fe961e41..783db20a4 100644 --- a/docs/es/docs/tutorial/background-tasks.md +++ b/docs/es/docs/tutorial/background-tasks.md @@ -61,7 +61,7 @@ Y luego otra tarea en segundo plano generada en la *path operation function* esc ## Detalles Técnicos -La clase `BackgroundTasks` proviene directamente de `starlette.background`. +La clase `BackgroundTasks` proviene directamente de `starlette.background`. Se importa/incluye directamente en FastAPI para que puedas importarla desde `fastapi` y evitar importar accidentalmente la alternativa `BackgroundTask` (sin la `s` al final) de `starlette.background`. @@ -69,7 +69,7 @@ Al usar solo `BackgroundTasks` (y no `BackgroundTask`), es posible usarla como u Todavía es posible usar `BackgroundTask` solo en FastAPI, pero debes crear el objeto en tu código y devolver una `Response` de Starlette incluyéndolo. -Puedes ver más detalles en la documentación oficial de Starlette sobre Background Tasks. +Puedes ver más detalles en la documentación oficial de Starlette sobre Background Tasks. ## Advertencia diff --git a/docs/es/docs/tutorial/first-steps.md b/docs/es/docs/tutorial/first-steps.md index 5d869c22f..b451782ad 100644 --- a/docs/es/docs/tutorial/first-steps.md +++ b/docs/es/docs/tutorial/first-steps.md @@ -163,7 +163,7 @@ También podrías usarlo para generar código automáticamente, para clientes qu `FastAPI` es una clase que hereda directamente de `Starlette`. -Puedes usar toda la funcionalidad de Starlette con `FastAPI` también. +Puedes usar toda la funcionalidad de Starlette con `FastAPI` también. /// diff --git a/docs/es/docs/tutorial/handling-errors.md b/docs/es/docs/tutorial/handling-errors.md index 2e4464989..107af2a70 100644 --- a/docs/es/docs/tutorial/handling-errors.md +++ b/docs/es/docs/tutorial/handling-errors.md @@ -81,7 +81,7 @@ Pero en caso de que los necesites para un escenario avanzado, puedes agregar hea ## Instalar manejadores de excepciones personalizados -Puedes agregar manejadores de excepciones personalizados con las mismas utilidades de excepciones de Starlette. +Puedes agregar manejadores de excepciones personalizados con las mismas utilidades de excepciones de Starlette. Supongamos que tienes una excepción personalizada `UnicornException` que tú (o un paquete que usas) podría lanzar. diff --git a/docs/es/docs/tutorial/middleware.md b/docs/es/docs/tutorial/middleware.md index 296374525..c42e4eaa5 100644 --- a/docs/es/docs/tutorial/middleware.md +++ b/docs/es/docs/tutorial/middleware.md @@ -37,7 +37,7 @@ La función middleware recibe: Ten en cuenta que los custom proprietary headers se pueden añadir usando el prefijo 'X-'. -Pero si tienes custom headers que deseas que un cliente en un navegador pueda ver, necesitas añadirlos a tus configuraciones de CORS ([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank}) usando el parámetro `expose_headers` documentado en la documentación de CORS de Starlette. +Pero si tienes custom headers que deseas que un cliente en un navegador pueda ver, necesitas añadirlos a tus configuraciones de CORS ([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank}) usando el parámetro `expose_headers` documentado en la documentación de CORS de Starlette. /// diff --git a/docs/es/docs/tutorial/static-files.md b/docs/es/docs/tutorial/static-files.md index 6aefecc4b..8c5855d86 100644 --- a/docs/es/docs/tutorial/static-files.md +++ b/docs/es/docs/tutorial/static-files.md @@ -37,4 +37,4 @@ Todos estos parámetros pueden ser diferentes a "`static`", ajústalos según la ## Más info -Para más detalles y opciones revisa la documentación de Starlette sobre Archivos Estáticos. +Para más detalles y opciones revisa la documentación de Starlette sobre Archivos Estáticos. diff --git a/docs/es/docs/tutorial/testing.md b/docs/es/docs/tutorial/testing.md index 62ad89d58..c68e83ae3 100644 --- a/docs/es/docs/tutorial/testing.md +++ b/docs/es/docs/tutorial/testing.md @@ -1,6 +1,6 @@ # Testing -Gracias a Starlette, escribir pruebas para aplicaciones de **FastAPI** es fácil y agradable. +Gracias a Starlette, escribir pruebas para aplicaciones de **FastAPI** es fácil y agradable. Está basado en HTTPX, que a su vez está diseñado basado en Requests, por lo que es muy familiar e intuitivo. diff --git a/docs/fa/docs/features.md b/docs/fa/docs/features.md index a5ab1597e..c265d2970 100644 --- a/docs/fa/docs/features.md +++ b/docs/fa/docs/features.md @@ -167,7 +167,7 @@ FastAPI شامل یک سیستم Uvicorn : un serveur ASGI haute performance. +* Uvicorn : un serveur ASGI haute performance. * Hypercorn : un serveur ASGI compatible avec HTTP/2 et Trio entre autres fonctionnalités. * Daphne : le serveur ASGI @@ -27,7 +27,7 @@ Vous pouvez installer un serveur compatible ASGI avec : //// tab | Uvicorn -* Uvicorn, un serveur ASGI rapide comme l'éclair, basé sur uvloop et httptools. +* Uvicorn, un serveur ASGI rapide comme l'éclair, basé sur uvloop et httptools.
diff --git a/docs/fr/docs/features.md b/docs/fr/docs/features.md index afb1de243..bc63e11b4 100644 --- a/docs/fr/docs/features.md +++ b/docs/fr/docs/features.md @@ -158,7 +158,7 @@ Tout intégration est conçue pour être si simple à utiliser (avec des dépend ## Fonctionnalités de Starlette -**FastAPI** est complètement compatible (et basé sur) Starlette. Le code utilisant Starlette que vous ajouterez fonctionnera donc aussi. +**FastAPI** est complètement compatible (et basé sur) Starlette. Le code utilisant Starlette que vous ajouterez fonctionnera donc aussi. En fait, `FastAPI` est un sous composant de `Starlette`. Donc, si vous savez déjà comment utiliser Starlette, la plupart des fonctionnalités fonctionneront de la même manière. diff --git a/docs/fr/docs/history-design-future.md b/docs/fr/docs/history-design-future.md index 6b26dd079..15be545ee 100644 --- a/docs/fr/docs/history-design-future.md +++ b/docs/fr/docs/history-design-future.md @@ -58,7 +58,7 @@ Après avoir testé plusieurs alternatives, j'ai décidé que j'allais utiliser J'y ai ensuite contribué, pour le rendre entièrement compatible avec JSON Schema, pour supporter différentes manières de définir les déclarations de contraintes, et pour améliorer le support des éditeurs (vérifications de type, autocomplétion) sur la base des tests effectués dans plusieurs éditeurs. -Pendant le développement, j'ai également contribué à **Starlette**, l'autre exigence clé. +Pendant le développement, j'ai également contribué à **Starlette**, l'autre exigence clé. ## Développement diff --git a/docs/fr/docs/index.md b/docs/fr/docs/index.md index 015c9574a..99ea8dda1 100644 --- a/docs/fr/docs/index.md +++ b/docs/fr/docs/index.md @@ -123,7 +123,7 @@ Si vous souhaitez construire une application Starlette pour les parties web. +* Starlette pour les parties web. * Pydantic pour les parties données. ## Installation @@ -138,7 +138,7 @@ $ pip install fastapi
-Vous aurez également besoin d'un serveur ASGI pour la production tel que Uvicorn ou Hypercorn. +Vous aurez également besoin d'un serveur ASGI pour la production tel que Uvicorn ou Hypercorn.
@@ -461,7 +461,7 @@ Utilisées par Starlette : Utilisées par FastAPI / Starlette : -* uvicorn - Pour le serveur qui charge et sert votre application. +* uvicorn - Pour le serveur qui charge et sert votre application. * orjson - Obligatoire si vous voulez utiliser `ORJSONResponse`. * ujson - Obligatoire si vous souhaitez utiliser `UJSONResponse`. diff --git a/docs/fr/docs/tutorial/background-tasks.md b/docs/fr/docs/tutorial/background-tasks.md index 2065ca58e..6efd16e07 100644 --- a/docs/fr/docs/tutorial/background-tasks.md +++ b/docs/fr/docs/tutorial/background-tasks.md @@ -61,7 +61,7 @@ Et ensuite une autre tâche d'arrière-plan (générée dans les paramètres de ## Détails techniques -La classe `BackgroundTasks` provient directement de `starlette.background`. +La classe `BackgroundTasks` provient directement de `starlette.background`. Elle est importée/incluse directement dans **FastAPI** pour que vous puissiez l'importer depuis `fastapi` et éviter d'importer accidentellement `BackgroundTask` (sans `s` à la fin) depuis `starlette.background`. @@ -69,7 +69,7 @@ En utilisant seulement `BackgroundTasks` (et non `BackgroundTask`), il est possi Il est tout de même possible d'utiliser `BackgroundTask` seul dans **FastAPI**, mais dans ce cas il faut créer l'objet dans le code et renvoyer une `Response` Starlette l'incluant. -Plus de détails sont disponibles dans la documentation officielle de Starlette sur les tâches d'arrière-plan (via leurs classes `BackgroundTasks`et `BackgroundTask`). +Plus de détails sont disponibles dans la documentation officielle de Starlette sur les tâches d'arrière-plan (via leurs classes `BackgroundTasks`et `BackgroundTask`). ## Avertissement diff --git a/docs/fr/docs/tutorial/first-steps.md b/docs/fr/docs/tutorial/first-steps.md index 758145362..96ea56e62 100644 --- a/docs/fr/docs/tutorial/first-steps.md +++ b/docs/fr/docs/tutorial/first-steps.md @@ -140,7 +140,7 @@ Vous pourriez aussi l'utiliser pour générer du code automatiquement, pour les `FastAPI` est une classe héritant directement de `Starlette`. -Vous pouvez donc aussi utiliser toutes les fonctionnalités de Starlette depuis `FastAPI`. +Vous pouvez donc aussi utiliser toutes les fonctionnalités de Starlette depuis `FastAPI`. /// diff --git a/docs/ja/docs/advanced/websockets.md b/docs/ja/docs/advanced/websockets.md index 43009eba8..2517530ab 100644 --- a/docs/ja/docs/advanced/websockets.md +++ b/docs/ja/docs/advanced/websockets.md @@ -184,5 +184,5 @@ Client #1596980209979 left the chat オプションの詳細については、Starletteのドキュメントを確認してください。 -* `WebSocket` クラス -* クラスベースのWebSocket処理 +* `WebSocket` クラス +* クラスベースのWebSocket処理 diff --git a/docs/ja/docs/alternatives.md b/docs/ja/docs/alternatives.md index 8129a7002..9f5152c08 100644 --- a/docs/ja/docs/alternatives.md +++ b/docs/ja/docs/alternatives.md @@ -419,7 +419,7 @@ Marshmallowに匹敵しますが、ベンチマークではMarshmallowよりも /// -### Starlette +### Starlette Starletteは、軽量なASGIフレームワーク/ツールキットで、高性能な非同期サービスの構築に最適です。 @@ -465,7 +465,7 @@ webに関するコアな部分を全て扱います。その上に機能を追 /// -### Uvicorn +### Uvicorn Uvicornは非常に高速なASGIサーバーで、uvloopとhttptoolsにより構成されています。 diff --git a/docs/ja/docs/deployment/manually.md b/docs/ja/docs/deployment/manually.md index 4ea6bd8ff..da382a9c5 100644 --- a/docs/ja/docs/deployment/manually.md +++ b/docs/ja/docs/deployment/manually.md @@ -6,7 +6,7 @@ //// tab | Uvicorn -* Uvicorn, uvloopとhttptoolsを基にした高速なASGIサーバ。 +* Uvicorn, uvloopとhttptoolsを基にした高速なASGIサーバ。
@@ -78,7 +78,7 @@ Running on 0.0.0.0:8080 over http (CTRL + C to quit) 停止した場合に自動的に再起動させるツールを設定したいかもしれません。 -さらに、GunicornをインストールしてUvicornのマネージャーとして使用したり、複数のワーカーでHypercornを使用したいかもしれません。 +さらに、GunicornをインストールしてUvicornのマネージャーとして使用したり、複数のワーカーでHypercornを使用したいかもしれません。 ワーカー数などの微調整も行いたいかもしれません。 diff --git a/docs/ja/docs/features.md b/docs/ja/docs/features.md index 4024590cf..f78eab430 100644 --- a/docs/ja/docs/features.md +++ b/docs/ja/docs/features.md @@ -160,7 +160,7 @@ FastAPIには非常に使いやすく、非常に強力なしてカスタムの独自ヘッダーを追加できます。 -ただし、ブラウザのクライアントに表示させたいカスタムヘッダーがある場合は、StarletteのCORSドキュメントに記載されているパラメータ `expose_headers` を使用して、それらをCORS設定に追加する必要があります ([CORS (オリジン間リソース共有)](cors.md){.internal-link target=_blank}) +ただし、ブラウザのクライアントに表示させたいカスタムヘッダーがある場合は、StarletteのCORSドキュメントに記載されているパラメータ `expose_headers` を使用して、それらをCORS設定に追加する必要があります ([CORS (オリジン間リソース共有)](cors.md){.internal-link target=_blank}) /// diff --git a/docs/ja/docs/tutorial/static-files.md b/docs/ja/docs/tutorial/static-files.md index f63f3f3b1..f910d7e36 100644 --- a/docs/ja/docs/tutorial/static-files.md +++ b/docs/ja/docs/tutorial/static-files.md @@ -37,4 +37,4 @@ ## より詳しい情報 -詳細とオプションについては、Starletteの静的ファイルに関するドキュメントを確認してください。 +詳細とオプションについては、Starletteの静的ファイルに関するドキュメントを確認してください。 diff --git a/docs/ja/docs/tutorial/testing.md b/docs/ja/docs/tutorial/testing.md index fe6c8c6b4..4e8ad4f7c 100644 --- a/docs/ja/docs/tutorial/testing.md +++ b/docs/ja/docs/tutorial/testing.md @@ -1,6 +1,6 @@ # テスト -Starlette のおかげで、**FastAPI** アプリケーションのテストは簡単で楽しいものになっています。 +Starlette のおかげで、**FastAPI** アプリケーションのテストは簡単で楽しいものになっています。 HTTPX がベースなので、非常に使いやすく直感的です。 diff --git a/docs/ko/docs/advanced/events.md b/docs/ko/docs/advanced/events.md index 5f8fe0f1e..4318ada54 100644 --- a/docs/ko/docs/advanced/events.md +++ b/docs/ko/docs/advanced/events.md @@ -154,7 +154,7 @@ ASGI 기술 사양에 따르면, 이는 Starlette의 Lifespan 문서에서 확인할 수 있습니다. +Starlette의 `lifespan` 핸들러에 대해 더 읽고 싶다면 Starlette의 Lifespan 문서에서 확인할 수 있습니다. 이 문서에는 코드의 다른 영역에서 사용할 수 있는 lifespan 상태를 처리하는 방법도 포함되어 있습니다. diff --git a/docs/ko/docs/advanced/middlewares.md b/docs/ko/docs/advanced/middlewares.md index c00aedeaf..5778528a8 100644 --- a/docs/ko/docs/advanced/middlewares.md +++ b/docs/ko/docs/advanced/middlewares.md @@ -93,4 +93,4 @@ HTTP 호스트 헤더 공격을 방지하기 위해 모든 수신 요청에 올 유비콘의 `ProxyHeadersMiddleware`> MessagePack -사용 가능한 다른 미들웨어를 확인하려면 스타렛의 미들웨어 문서ASGI Awesome List를 참조하세요. +사용 가능한 다른 미들웨어를 확인하려면 스타렛의 미들웨어 문서ASGI Awesome List를 참조하세요. diff --git a/docs/ko/docs/advanced/response-cookies.md b/docs/ko/docs/advanced/response-cookies.md index 327f20afe..50da713fe 100644 --- a/docs/ko/docs/advanced/response-cookies.md +++ b/docs/ko/docs/advanced/response-cookies.md @@ -46,4 +46,4 @@ /// -사용 가능한 모든 매개변수와 옵션은 Starlette 문서에서 확인할 수 있습니다. +사용 가능한 모든 매개변수와 옵션은 Starlette 문서에서 확인할 수 있습니다. diff --git a/docs/ko/docs/advanced/response-headers.md b/docs/ko/docs/advanced/response-headers.md index e8abe0be2..e4e022c9b 100644 --- a/docs/ko/docs/advanced/response-headers.md +++ b/docs/ko/docs/advanced/response-headers.md @@ -38,4 +38,4 @@ ‘X-’ 접두어를 사용하여 커스텀 사설 헤더를 추가할 수 있습니다. -하지만, 여러분이 브라우저에서 클라이언트가 볼 수 있기를 원하는 커스텀 헤더가 있는 경우, CORS 설정에 이를 추가해야 합니다([CORS (Cross-Origin Resource Sharing)](../tutorial/cors.md){.internal-link target=_blank}에서 자세히 알아보세요). `expose_headers` 매개변수를 사용하여 Starlette의 CORS 설명서에 문서화된 대로 설정할 수 있습니다. +하지만, 여러분이 브라우저에서 클라이언트가 볼 수 있기를 원하는 커스텀 헤더가 있는 경우, CORS 설정에 이를 추가해야 합니다([CORS (Cross-Origin Resource Sharing)](../tutorial/cors.md){.internal-link target=_blank}에서 자세히 알아보세요). `expose_headers` 매개변수를 사용하여 Starlette의 CORS 설명서에 문서화된 대로 설정할 수 있습니다. diff --git a/docs/ko/docs/advanced/templates.md b/docs/ko/docs/advanced/templates.md index 4cb4cbe0d..612635713 100644 --- a/docs/ko/docs/advanced/templates.md +++ b/docs/ko/docs/advanced/templates.md @@ -124,4 +124,4 @@ Item ID: 42 ## 더 많은 세부 사항 -템플릿 테스트를 포함한 더 많은 세부 사항은 Starlette의 템플릿 문서를 확인하세요. +템플릿 테스트를 포함한 더 많은 세부 사항은 Starlette의 템플릿 문서를 확인하세요. diff --git a/docs/ko/docs/advanced/testing-websockets.md b/docs/ko/docs/advanced/testing-websockets.md index 9f3b4a451..9b6782429 100644 --- a/docs/ko/docs/advanced/testing-websockets.md +++ b/docs/ko/docs/advanced/testing-websockets.md @@ -8,6 +8,6 @@ /// note | 참고 -자세한 내용은 Starlette의 WebSocket 테스트에 관한 설명서를 참고하시길 바랍니다. +자세한 내용은 Starlette의 WebSocket 테스트에 관한 설명서를 참고하시길 바랍니다. /// diff --git a/docs/ko/docs/advanced/using-request-directly.md b/docs/ko/docs/advanced/using-request-directly.md index bfa4fa4db..b88a83bf4 100644 --- a/docs/ko/docs/advanced/using-request-directly.md +++ b/docs/ko/docs/advanced/using-request-directly.md @@ -15,7 +15,7 @@ ## `Request` 객체에 대한 세부 사항 -**FastAPI**는 실제로 내부에 **Starlette**을 사용하며, 그 위에 여러 도구를 덧붙인 구조입니다. 따라서 여러분이 필요할 때 Starlette의 `Request` 객체를 직접 사용할 수 있습니다. +**FastAPI**는 실제로 내부에 **Starlette**을 사용하며, 그 위에 여러 도구를 덧붙인 구조입니다. 따라서 여러분이 필요할 때 Starlette의 `Request` 객체를 직접 사용할 수 있습니다. `Request` 객체에서 데이터를 직접 가져오는 경우(예: 본문을 읽기)에는 FastAPI가 해당 데이터를 검증하거나 변환하지 않으며, 문서화(OpenAPI를 통한 문서 자동화(로 생성된) API 사용자 인터페이스)도 되지 않습니다. @@ -45,7 +45,7 @@ ## `Request` 설명서 -여러분은 `Request` 객체에 대한 더 자세한 내용을 공식 Starlette 설명서 사이트에서 읽어볼 수 있습니다. +여러분은 `Request` 객체에 대한 더 자세한 내용을 공식 Starlette 설명서 사이트에서 읽어볼 수 있습니다. /// note | 기술 세부사항 diff --git a/docs/ko/docs/advanced/websockets.md b/docs/ko/docs/advanced/websockets.md index fa60a428b..d9d0dd95c 100644 --- a/docs/ko/docs/advanced/websockets.md +++ b/docs/ko/docs/advanced/websockets.md @@ -182,5 +182,5 @@ FastAPI와 쉽게 통합할 수 있으면서 더 견고하고 Redis, PostgreSQL 다음 옵션에 대한 자세한 내용을 보려면 Starlette의 문서를 확인하세요: -* `WebSocket` 클래스. -* 클래스 기반 WebSocket 처리. +* `WebSocket` 클래스. +* 클래스 기반 WebSocket 처리. diff --git a/docs/ko/docs/fastapi-cli.md b/docs/ko/docs/fastapi-cli.md index 3a976af36..a1160c71f 100644 --- a/docs/ko/docs/fastapi-cli.md +++ b/docs/ko/docs/fastapi-cli.md @@ -60,7 +60,7 @@ FastAPI CLI는 Python 프로그램의 경로(예: `main.py`)를 인수로 받아 프로덕션 환경에서는 `fastapi run` 명령어를 사용합니다. 🚀 -내부적으로, **FastAPI CLI**는 고성능의, 프로덕션에 적합한, ASGI 서버인 Uvicorn을 사용합니다. 😎 +내부적으로, **FastAPI CLI**는 고성능의, 프로덕션에 적합한, ASGI 서버인 Uvicorn을 사용합니다. 😎 ## `fastapi dev` diff --git a/docs/ko/docs/features.md b/docs/ko/docs/features.md index 5e880c298..dfbf47999 100644 --- a/docs/ko/docs/features.md +++ b/docs/ko/docs/features.md @@ -159,7 +159,7 @@ FastAPI는 사용하기 매우 간편하지만, 엄청난 하여 추가할 수 있습니다. -그러나 만약 클라이언트의 브라우저에서 볼 수 있는 사용자 정의 헤더를 가지고 있다면, 그것들을 CORS 설정([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank})에 Starlette CORS 문서에 명시된 `expose_headers` 매개변수를 이용하여 헤더들을 추가하여야합니다. +그러나 만약 클라이언트의 브라우저에서 볼 수 있는 사용자 정의 헤더를 가지고 있다면, 그것들을 CORS 설정([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank})에 Starlette CORS 문서에 명시된 `expose_headers` 매개변수를 이용하여 헤더들을 추가하여야합니다. /// diff --git a/docs/ko/docs/tutorial/static-files.md b/docs/ko/docs/tutorial/static-files.md index 9db5e1c67..4f3e3ab28 100644 --- a/docs/ko/docs/tutorial/static-files.md +++ b/docs/ko/docs/tutorial/static-files.md @@ -38,4 +38,4 @@ ## 추가 정보 -자세한 내용과 선택 사항을 보려면 Starlette의 정적 파일에 관한 문서를 확인하십시오. +자세한 내용과 선택 사항을 보려면 Starlette의 정적 파일에 관한 문서를 확인하십시오. diff --git a/docs/ko/docs/tutorial/testing.md b/docs/ko/docs/tutorial/testing.md index a483cbf00..915ff6d22 100644 --- a/docs/ko/docs/tutorial/testing.md +++ b/docs/ko/docs/tutorial/testing.md @@ -1,6 +1,6 @@ # 테스팅 -Starlette 덕분에 **FastAPI** 를 테스트하는 일은 쉽고 즐거운 일이 되었습니다. +Starlette 덕분에 **FastAPI** 를 테스트하는 일은 쉽고 즐거운 일이 되었습니다. Starlette는 HTTPX를 기반으로 하며, 이는 Requests를 기반으로 설계되었기 때문에 매우 친숙하고 직관적입니다. diff --git a/docs/pt/docs/advanced/events.md b/docs/pt/docs/advanced/events.md index 504b6db57..2d38e0899 100644 --- a/docs/pt/docs/advanced/events.md +++ b/docs/pt/docs/advanced/events.md @@ -155,7 +155,7 @@ Por baixo, na especificação técnica ASGI, essa é a parte do Documentação do Lifespan Starlette. +Você pode ler mais sobre o manipulador `lifespan` do Starlette na Documentação do Lifespan Starlette. Incluindo como manipular estado do lifespan que pode ser usado em outras áreas do seu código. diff --git a/docs/pt/docs/advanced/middleware.md b/docs/pt/docs/advanced/middleware.md index 8167f7d27..7700939f0 100644 --- a/docs/pt/docs/advanced/middleware.md +++ b/docs/pt/docs/advanced/middleware.md @@ -93,4 +93,4 @@ Por exemplo: * Uvicorn's `ProxyHeadersMiddleware` * MessagePack -Para checar outros middlewares disponíveis, confira Documentação de Middlewares do Starlette e a Lista Incrível do ASGI. +Para checar outros middlewares disponíveis, confira Documentação de Middlewares do Starlette e a Lista Incrível do ASGI. diff --git a/docs/pt/docs/advanced/response-cookies.md b/docs/pt/docs/advanced/response-cookies.md index eed69f222..f005f0b9b 100644 --- a/docs/pt/docs/advanced/response-cookies.md +++ b/docs/pt/docs/advanced/response-cookies.md @@ -48,4 +48,4 @@ E como o `Response` pode ser usado frequentemente para definir cabeçalhos e coo /// -Para ver todos os parâmetros e opções disponíveis, verifique a documentação no Starlette. +Para ver todos os parâmetros e opções disponíveis, verifique a documentação no Starlette. diff --git a/docs/pt/docs/advanced/response-headers.md b/docs/pt/docs/advanced/response-headers.md index a8034a7a4..a1fc84cc0 100644 --- a/docs/pt/docs/advanced/response-headers.md +++ b/docs/pt/docs/advanced/response-headers.md @@ -38,4 +38,4 @@ E como a `Response` pode ser usada frequentemente para definir cabeçalhos e coo Tenha em mente que cabeçalhos personalizados proprietários podem ser adicionados usando o prefixo 'X-'. -Porém, se voce tiver cabeçalhos personalizados que deseja que um cliente no navegador possa ver, você precisa adicioná-los às suas configurações de CORS (saiba mais em [CORS (Cross-Origin Resource Sharing)](../tutorial/cors.md){.internal-link target=_blank}), usando o parâmetro `expose_headers` descrito na documentação de CORS do Starlette. +Porém, se voce tiver cabeçalhos personalizados que deseja que um cliente no navegador possa ver, você precisa adicioná-los às suas configurações de CORS (saiba mais em [CORS (Cross-Origin Resource Sharing)](../tutorial/cors.md){.internal-link target=_blank}), usando o parâmetro `expose_headers` descrito na documentação de CORS do Starlette. diff --git a/docs/pt/docs/advanced/templates.md b/docs/pt/docs/advanced/templates.md index 4d22bfbbf..65ff89fae 100644 --- a/docs/pt/docs/advanced/templates.md +++ b/docs/pt/docs/advanced/templates.md @@ -121,4 +121,4 @@ E como você está usando `StaticFiles`, este arquivo CSS será automaticamente ## Mais detalhes -Para obter mais detalhes, incluindo como testar templates, consulte a documentação da Starlette sobre templates. +Para obter mais detalhes, incluindo como testar templates, consulte a documentação da Starlette sobre templates. diff --git a/docs/pt/docs/advanced/testing-websockets.md b/docs/pt/docs/advanced/testing-websockets.md index 942771bc9..9b8193655 100644 --- a/docs/pt/docs/advanced/testing-websockets.md +++ b/docs/pt/docs/advanced/testing-websockets.md @@ -8,6 +8,6 @@ Para isso, você utiliza o `TestClient` dentro de uma instrução `with`, conect /// note | Nota -Para mais detalhes, confira a documentação do Starlette para testar WebSockets. +Para mais detalhes, confira a documentação do Starlette para testar WebSockets. /// diff --git a/docs/pt/docs/advanced/using-request-directly.md b/docs/pt/docs/advanced/using-request-directly.md index f31e2ed15..f4fb0ed8f 100644 --- a/docs/pt/docs/advanced/using-request-directly.md +++ b/docs/pt/docs/advanced/using-request-directly.md @@ -15,7 +15,7 @@ Porém há situações em que você possa precisar acessar o objeto `Request` di ## Detalhes sobre o objeto `Request` -Como o **FastAPI** é na verdade o **Starlette** por baixo, com camadas de diversas funcionalidades por cima, você pode utilizar o objeto `Request` do Starlette diretamente quando precisar. +Como o **FastAPI** é na verdade o **Starlette** por baixo, com camadas de diversas funcionalidades por cima, você pode utilizar o objeto `Request` do Starlette diretamente quando precisar. Isso significaria também que se você obtiver informações do objeto `Request` diretamente (ler o corpo da requisição por exemplo), as informações não serão validadas, convertidas ou documentadas (com o OpenAPI, para a interface de usuário automática da API) pelo FastAPI. @@ -45,7 +45,7 @@ Do mesmo jeito, você pode declarar qualquer outro parâmetro normalmente, e al ## Documentação do `Request` -Você pode ler mais sobre os detalhes do objeto `Request` no site da documentação oficial do Starlette.. +Você pode ler mais sobre os detalhes do objeto `Request` no site da documentação oficial do Starlette.. /// note | Detalhes Técnicos diff --git a/docs/pt/docs/advanced/websockets.md b/docs/pt/docs/advanced/websockets.md index 82e443886..721c0b403 100644 --- a/docs/pt/docs/advanced/websockets.md +++ b/docs/pt/docs/advanced/websockets.md @@ -182,5 +182,5 @@ Se você precisa de algo fácil de integrar com o FastAPI, mas que seja mais rob Para aprender mais sobre as opções, verifique a documentação do Starlette para: -* A classe `WebSocket`. -* Manipulação de WebSockets baseada em classes. +* A classe `WebSocket`. +* Manipulação de WebSockets baseada em classes. diff --git a/docs/pt/docs/alternatives.md b/docs/pt/docs/alternatives.md index 29c9693bb..66cf3fe12 100644 --- a/docs/pt/docs/alternatives.md +++ b/docs/pt/docs/alternatives.md @@ -419,7 +419,7 @@ Controlar toda a validação de dados, serialização de dados e modelo de docum /// -### Starlette +### Starlette Starlette é um framework/caixa de ferramentas ASGI peso leve, o que é ideal para construir serviços assíncronos de alta performance. @@ -465,7 +465,7 @@ Então, qualquer coisa que você faz com Starlette, você pode fazer diretamente /// -### Uvicorn +### Uvicorn Uvicorn é um servidor ASGI peso leve, construído com uvloop e httptools. diff --git a/docs/pt/docs/deployment/manually.md b/docs/pt/docs/deployment/manually.md index 46e580807..c7caabbcd 100644 --- a/docs/pt/docs/deployment/manually.md +++ b/docs/pt/docs/deployment/manually.md @@ -52,7 +52,7 @@ A principal coisa que você precisa para executar uma aplicação **FastAPI** (o Existem diversas alternativas, incluindo: -* Uvicorn: um servidor ASGI de alta performance. +* Uvicorn: um servidor ASGI de alta performance. * Hypercorn: um servidor ASGI compátivel com HTTP/2, Trio e outros recursos. * Daphne: servidor ASGI construído para Django Channels. * Granian: um servidor HTTP Rust para aplicações Python. diff --git a/docs/pt/docs/fastapi-cli.md b/docs/pt/docs/fastapi-cli.md index 829686631..f33c2ba2a 100644 --- a/docs/pt/docs/fastapi-cli.md +++ b/docs/pt/docs/fastapi-cli.md @@ -60,7 +60,7 @@ O FastAPI CLI recebe o caminho do seu programa Python, detecta automaticamente a Para produção você usaria `fastapi run` no lugar. 🚀 -Internamente, **FastAPI CLI** usa Uvicorn, um servidor ASGI de alta performance e pronto para produção. 😎 +Internamente, **FastAPI CLI** usa Uvicorn, um servidor ASGI de alta performance e pronto para produção. 😎 ## `fastapi dev` diff --git a/docs/pt/docs/features.md b/docs/pt/docs/features.md index a90a8094b..ccc3300d6 100644 --- a/docs/pt/docs/features.md +++ b/docs/pt/docs/features.md @@ -159,7 +159,7 @@ Qualquer integração é projetada para ser tão simples de usar (com dependênc ## Recursos do Starlette -**FastAPI** é totalmente compatível com (e baseado no) Starlette. Então, qualquer código adicional Starlette que você tiver, também funcionará. +**FastAPI** é totalmente compatível com (e baseado no) Starlette. Então, qualquer código adicional Starlette que você tiver, também funcionará. `FastAPI` é na verdade uma sub-classe do `Starlette`. Então, se você já conhece ou usa Starlette, a maioria das funcionalidades se comportará da mesma forma. diff --git a/docs/pt/docs/history-design-future.md b/docs/pt/docs/history-design-future.md index 4ec217405..1d0768c62 100644 --- a/docs/pt/docs/history-design-future.md +++ b/docs/pt/docs/history-design-future.md @@ -58,7 +58,7 @@ Após testar várias alternativas, eu decidi que usaria o **Starlette**, outro requisito chave. +Durante o desenvolvimento, eu também contribuí com o **Starlette**, outro requisito chave. ## Desenvolvimento diff --git a/docs/pt/docs/how-to/custom-request-and-route.md b/docs/pt/docs/how-to/custom-request-and-route.md index 8f432f6fe..151a0f5d4 100644 --- a/docs/pt/docs/how-to/custom-request-and-route.md +++ b/docs/pt/docs/how-to/custom-request-and-route.md @@ -66,7 +66,7 @@ O dicionário `scope` e a função `receive` são ambos parte da especificação E essas duas coisas, `scope` e `receive`, são o que é necessário para criar uma nova instância de `Request`. -Para aprender mais sobre o `Request` confira a documentação do Starlette sobre Requests. +Para aprender mais sobre o `Request` confira a documentação do Starlette sobre Requests. /// diff --git a/docs/pt/docs/index.md b/docs/pt/docs/index.md index ce9929bf4..a361913c3 100644 --- a/docs/pt/docs/index.md +++ b/docs/pt/docs/index.md @@ -123,7 +123,7 @@ Se você estiver construindo uma aplicação Starlette para as partes web. +* Starlette para as partes web. * Pydantic para a parte de dados. ## Instalação @@ -229,7 +229,7 @@ INFO: Application startup complete.
Sobre o comando fastapi dev main.py... -O comando `fastapi dev` lê o seu arquivo `main.py`, identifica o aplicativo **FastAPI** nele, e inicia um servidor usando o Uvicorn. +O comando `fastapi dev` lê o seu arquivo `main.py`, identifica o aplicativo **FastAPI** nele, e inicia um servidor usando o Uvicorn. Por padrão, o `fastapi dev` iniciará com *auto-reload* habilitado para desenvolvimento local. @@ -471,7 +471,7 @@ Utilizado pelo Starlette: Utilizado pelo FastAPI / Starlette: -* uvicorn - para o servidor que carrega e serve a sua aplicação. Isto inclui `uvicorn[standard]`, que inclui algumas dependências (e.g. `uvloop`) necessárias para servir em alta performance. +* uvicorn - para o servidor que carrega e serve a sua aplicação. Isto inclui `uvicorn[standard]`, que inclui algumas dependências (e.g. `uvloop`) necessárias para servir em alta performance. * `fastapi-cli` - que disponibiliza o comando `fastapi`. ### Sem as dependências `standard` diff --git a/docs/pt/docs/tutorial/background-tasks.md b/docs/pt/docs/tutorial/background-tasks.md index 0f3796371..b8ab58cda 100644 --- a/docs/pt/docs/tutorial/background-tasks.md +++ b/docs/pt/docs/tutorial/background-tasks.md @@ -61,7 +61,7 @@ E então outra tarefa em segundo plano gerada na _função de operação de cami ## Detalhes técnicos -A classe `BackgroundTasks` vem diretamente de `starlette.background`. +A classe `BackgroundTasks` vem diretamente de `starlette.background`. Ela é importada/incluída diretamente no FastAPI para que você possa importá-la do `fastapi` e evitar a importação acidental da alternativa `BackgroundTask` (sem o `s` no final) de `starlette.background`. @@ -69,7 +69,7 @@ Usando apenas `BackgroundTasks` (e não `BackgroundTask`), é então possível u Ainda é possível usar `BackgroundTask` sozinho no FastAPI, mas você deve criar o objeto em seu código e retornar uma Starlette `Response` incluindo-o. -Você pode ver mais detalhes na documentação oficiais da Starlette para tarefas em segundo plano . +Você pode ver mais detalhes na documentação oficiais da Starlette para tarefas em segundo plano . ## Ressalva diff --git a/docs/pt/docs/tutorial/first-steps.md b/docs/pt/docs/tutorial/first-steps.md index 5184d2d5f..e696bbbb7 100644 --- a/docs/pt/docs/tutorial/first-steps.md +++ b/docs/pt/docs/tutorial/first-steps.md @@ -155,7 +155,7 @@ Você também pode usá-lo para gerar código automaticamente para clientes que `FastAPI` é uma classe que herda diretamente de `Starlette`. -Você pode usar todas as funcionalidades do Starlette com `FastAPI` também. +Você pode usar todas as funcionalidades do Starlette com `FastAPI` também. /// diff --git a/docs/pt/docs/tutorial/handling-errors.md b/docs/pt/docs/tutorial/handling-errors.md index 098195db7..5cb92c744 100644 --- a/docs/pt/docs/tutorial/handling-errors.md +++ b/docs/pt/docs/tutorial/handling-errors.md @@ -83,7 +83,7 @@ Mas caso você precise, para um cenário mais complexo, você pode adicionar hea ## Instalando manipuladores de exceções customizados -Você pode adicionar manipuladores de exceção customizados com a mesma seção de utilidade de exceções presentes no Starlette +Você pode adicionar manipuladores de exceção customizados com a mesma seção de utilidade de exceções presentes no Starlette Digamos que você tenha uma exceção customizada `UnicornException` que você (ou uma biblioteca que você use) precise lançar (`raise`). diff --git a/docs/pt/docs/tutorial/middleware.md b/docs/pt/docs/tutorial/middleware.md index 32b81c646..0f5009b6d 100644 --- a/docs/pt/docs/tutorial/middleware.md +++ b/docs/pt/docs/tutorial/middleware.md @@ -37,7 +37,7 @@ A função middleware recebe: Tenha em mente que cabeçalhos proprietários personalizados podem ser adicionados usando o prefixo 'X-'. -Mas se você tiver cabeçalhos personalizados desejando que um cliente em um navegador esteja apto a ver, você precisa adicioná-los às suas configurações CORS ([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank}) usando o parâmetro `expose_headers` documentado em Documentos CORS da Starlette. +Mas se você tiver cabeçalhos personalizados desejando que um cliente em um navegador esteja apto a ver, você precisa adicioná-los às suas configurações CORS ([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank}) usando o parâmetro `expose_headers` documentado em Documentos CORS da Starlette. /// diff --git a/docs/pt/docs/tutorial/static-files.md b/docs/pt/docs/tutorial/static-files.md index 0660078f4..30e1af8e6 100644 --- a/docs/pt/docs/tutorial/static-files.md +++ b/docs/pt/docs/tutorial/static-files.md @@ -37,4 +37,4 @@ Todos esses parâmetros podem ser diferentes de "`static`", ajuste-os de acordo ## Mais informações -Para mais detalhes e opções, verifique Starlette's docs about Static Files. +Para mais detalhes e opções, verifique Starlette's docs about Static Files. diff --git a/docs/pt/docs/tutorial/testing.md b/docs/pt/docs/tutorial/testing.md index 8eb2f29b7..dc505105a 100644 --- a/docs/pt/docs/tutorial/testing.md +++ b/docs/pt/docs/tutorial/testing.md @@ -1,6 +1,6 @@ # Testando -Graças ao Starlette, testar aplicativos **FastAPI** é fácil e agradável. +Graças ao Starlette, testar aplicativos **FastAPI** é fácil e agradável. Ele é baseado no HTTPX, que por sua vez é projetado com base em Requests, por isso é muito familiar e intuitivo. diff --git a/docs/ru/docs/advanced/events.md b/docs/ru/docs/advanced/events.md index 6e1b49035..20d1df98a 100644 --- a/docs/ru/docs/advanced/events.md +++ b/docs/ru/docs/advanced/events.md @@ -154,7 +154,7 @@ async with lifespan(app): /// info | Информация -Вы можете прочитать больше про обработчики `lifespan` в Starlette в документации Starlette по Lifespan. +Вы можете прочитать больше про обработчики `lifespan` в Starlette в документации Starlette по Lifespan. Включая то, как работать с состоянием lifespan, которое можно использовать в других частях вашего кода. diff --git a/docs/ru/docs/advanced/middleware.md b/docs/ru/docs/advanced/middleware.md index 28802fd57..82c86b231 100644 --- a/docs/ru/docs/advanced/middleware.md +++ b/docs/ru/docs/advanced/middleware.md @@ -94,4 +94,4 @@ app.add_middleware(UnicornMiddleware, some_config="rainbow") - `ProxyHeadersMiddleware` от Uvicorn - MessagePack -Чтобы увидеть другие доступные middleware, посмотрите документацию по middleware в Starlette и список ASGI Awesome. +Чтобы увидеть другие доступные middleware, посмотрите документацию по middleware в Starlette и список ASGI Awesome. diff --git a/docs/ru/docs/advanced/response-cookies.md b/docs/ru/docs/advanced/response-cookies.md index 3aa32b9bb..9319aba6e 100644 --- a/docs/ru/docs/advanced/response-cookies.md +++ b/docs/ru/docs/advanced/response-cookies.md @@ -48,4 +48,4 @@ /// -Чтобы увидеть все доступные параметры и настройки, ознакомьтесь с документацией Starlette. +Чтобы увидеть все доступные параметры и настройки, ознакомьтесь с документацией Starlette. diff --git a/docs/ru/docs/advanced/response-headers.md b/docs/ru/docs/advanced/response-headers.md index 81e52cb69..1c9360b31 100644 --- a/docs/ru/docs/advanced/response-headers.md +++ b/docs/ru/docs/advanced/response-headers.md @@ -38,4 +38,4 @@ Помните, что собственные проприетарные заголовки можно добавлять, используя префикс `X-`. -Но если у вас есть пользовательские заголовки, которые вы хотите показывать клиенту в браузере, вам нужно добавить их в настройки CORS (подробнее см. в [CORS (Cross-Origin Resource Sharing)](../tutorial/cors.md){.internal-link target=_blank}), используя параметр `expose_headers`, описанный в документации Starlette по CORS. +Но если у вас есть пользовательские заголовки, которые вы хотите показывать клиенту в браузере, вам нужно добавить их в настройки CORS (подробнее см. в [CORS (Cross-Origin Resource Sharing)](../tutorial/cors.md){.internal-link target=_blank}), используя параметр `expose_headers`, описанный в документации Starlette по CORS. diff --git a/docs/ru/docs/advanced/templates.md b/docs/ru/docs/advanced/templates.md index 5675ff48a..204e88760 100644 --- a/docs/ru/docs/advanced/templates.md +++ b/docs/ru/docs/advanced/templates.md @@ -123,4 +123,4 @@ Item ID: 42 ## Подробнее { #more-details } -Больше подробностей, включая то, как тестировать шаблоны, смотрите в документации Starlette по шаблонам. +Больше подробностей, включая то, как тестировать шаблоны, смотрите в документации Starlette по шаблонам. diff --git a/docs/ru/docs/advanced/testing-events.md b/docs/ru/docs/advanced/testing-events.md index 1bf8e4723..e0ec77439 100644 --- a/docs/ru/docs/advanced/testing-events.md +++ b/docs/ru/docs/advanced/testing-events.md @@ -5,7 +5,7 @@ {* ../../docs_src/app_testing/tutorial004.py hl[9:15,18,27:28,30:32,41:43] *} -Вы можете узнать больше подробностей в статье [Запуск lifespan в тестах на официальном сайте документации Starlette.](https://www.starlette.io/lifespan/#running-lifespan-in-tests) +Вы можете узнать больше подробностей в статье [Запуск lifespan в тестах на официальном сайте документации Starlette.](https://www.starlette.dev/lifespan/#running-lifespan-in-tests) Для устаревших событий `startup` и `shutdown` вы можете использовать `TestClient` следующим образом: diff --git a/docs/ru/docs/advanced/testing-websockets.md b/docs/ru/docs/advanced/testing-websockets.md index 7c0ca2594..e840a03f2 100644 --- a/docs/ru/docs/advanced/testing-websockets.md +++ b/docs/ru/docs/advanced/testing-websockets.md @@ -8,6 +8,6 @@ /// note | Примечание -Подробности смотрите в документации Starlette по тестированию WebSocket. +Подробности смотрите в документации Starlette по тестированию WebSocket. /// diff --git a/docs/ru/docs/advanced/using-request-directly.md b/docs/ru/docs/advanced/using-request-directly.md index bff2ddcb7..b92221610 100644 --- a/docs/ru/docs/advanced/using-request-directly.md +++ b/docs/ru/docs/advanced/using-request-directly.md @@ -15,7 +15,7 @@ ## Подробности об объекте `Request` { #details-about-the-request-object } -Так как под капотом **FastAPI** — это **Starlette** с дополнительным слоем инструментов, вы можете при необходимости напрямую использовать объект `Request` из Starlette. +Так как под капотом **FastAPI** — это **Starlette** с дополнительным слоем инструментов, вы можете при необходимости напрямую использовать объект `Request` из Starlette. Это также означает, что если вы получаете данные напрямую из объекта `Request` (например, читаете тело запроса), то они не будут валидироваться, конвертироваться или документироваться (с OpenAPI, для автоматического пользовательского интерфейса API) средствами FastAPI. @@ -45,7 +45,7 @@ ## Документация по `Request` { #request-documentation } -Подробнее об объекте `Request` на официальном сайте документации Starlette. +Подробнее об объекте `Request` на официальном сайте документации Starlette. /// note | Технические детали diff --git a/docs/ru/docs/advanced/websockets.md b/docs/ru/docs/advanced/websockets.md index b73fa1ddb..f26185bea 100644 --- a/docs/ru/docs/advanced/websockets.md +++ b/docs/ru/docs/advanced/websockets.md @@ -182,5 +182,5 @@ Client #1596980209979 left the chat Для более глубокого изучения темы воспользуйтесь документацией Starlette: -* The `WebSocket` class. -* Class-based WebSocket handling. +* The `WebSocket` class. +* Class-based WebSocket handling. diff --git a/docs/ru/docs/alternatives.md b/docs/ru/docs/alternatives.md index 6380bcc45..17b54aad2 100644 --- a/docs/ru/docs/alternatives.md +++ b/docs/ru/docs/alternatives.md @@ -417,7 +417,7 @@ Pydantic — это библиотека для определения вали /// -### Starlette { #starlette } +### Starlette { #starlette } Starlette — это лёгкий ASGI фреймворк/набор инструментов, идеально подходящий для создания высокопроизводительных asyncio‑сервисов. @@ -462,7 +462,7 @@ ASGI — это новый «стандарт», разрабатываемый /// -### Uvicorn { #uvicorn } +### Uvicorn { #uvicorn } Uvicorn — молниеносный ASGI-сервер, построенный на uvloop и httptools. diff --git a/docs/ru/docs/deployment/manually.md b/docs/ru/docs/deployment/manually.md index 37fed5780..93287372a 100644 --- a/docs/ru/docs/deployment/manually.md +++ b/docs/ru/docs/deployment/manually.md @@ -52,7 +52,7 @@ FastAPI использует стандарт для построения Python Есть несколько альтернатив, например: -* Uvicorn: высокопроизводительный ASGI‑сервер. +* Uvicorn: высокопроизводительный ASGI‑сервер. * Hypercorn: ASGI‑сервер, среди прочего совместимый с HTTP/2 и Trio. * Daphne: ASGI‑сервер, созданный для Django Channels. * Granian: HTTP‑сервер на Rust для Python‑приложений. diff --git a/docs/ru/docs/fastapi-cli.md b/docs/ru/docs/fastapi-cli.md index 156e3d200..72cf55e7b 100644 --- a/docs/ru/docs/fastapi-cli.md +++ b/docs/ru/docs/fastapi-cli.md @@ -52,7 +52,7 @@ FastAPI CLI берет путь к вашей Python-программе (нап Для работы в режиме продакшн вместо `fastapi dev` нужно использовать `fastapi run`. 🚀 -Внутри **FastAPI CLI** используется Uvicorn, высокопроизводительный, готовый к работе в продакшне ASGI-сервер. 😎 +Внутри **FastAPI CLI** используется Uvicorn, высокопроизводительный, готовый к работе в продакшне ASGI-сервер. 😎 ## `fastapi dev` { #fastapi-dev } diff --git a/docs/ru/docs/features.md b/docs/ru/docs/features.md index 91ffe331b..703ff951e 100644 --- a/docs/ru/docs/features.md +++ b/docs/ru/docs/features.md @@ -159,7 +159,7 @@ FastAPI включает в себя чрезвычайно простую в и ## Возможности Starlette { #starlette-features } -**FastAPI** основан на Starlette и полностью совместим с ним. Так что любой дополнительный код Starlette, который у вас есть, также будет работать. +**FastAPI** основан на Starlette и полностью совместим с ним. Так что любой дополнительный код Starlette, который у вас есть, также будет работать. На самом деле, `FastAPI` — это подкласс `Starlette`. Таким образом, если вы уже знаете или используете Starlette, большая часть функционала будет работать так же. diff --git a/docs/ru/docs/history-design-future.md b/docs/ru/docs/history-design-future.md index d679af3e3..9cdd53376 100644 --- a/docs/ru/docs/history-design-future.md +++ b/docs/ru/docs/history-design-future.md @@ -58,7 +58,7 @@ По моим предложениям был изменён код этого фреймворка, чтобы сделать его полностью совместимым с JSON Schema, поддержать различные способы определения ограничений и улучшить поддержку в редакторах кода (проверки типов, автозавершение) на основе тестов в нескольких редакторах. -В то же время, я принимал участие в разработке **Starlette**, ещё один из основных компонентов FastAPI. +В то же время, я принимал участие в разработке **Starlette**, ещё один из основных компонентов FastAPI. ## Разработка { #development } diff --git a/docs/ru/docs/how-to/custom-request-and-route.md b/docs/ru/docs/how-to/custom-request-and-route.md index df8a5ee3c..1b8d7f7ed 100644 --- a/docs/ru/docs/how-to/custom-request-and-route.md +++ b/docs/ru/docs/how-to/custom-request-and-route.md @@ -66,7 +66,7 @@ Именно этих двух компонентов — `scope` и `receive` — достаточно, чтобы создать новый экземпляр `Request`. -Чтобы узнать больше о `Request`, см. документацию Starlette о запросах. +Чтобы узнать больше о `Request`, см. документацию Starlette о запросах. /// diff --git a/docs/ru/docs/index.md b/docs/ru/docs/index.md index 1fcc9ea9d..75cd63223 100644 --- a/docs/ru/docs/index.md +++ b/docs/ru/docs/index.md @@ -123,7 +123,7 @@ FastAPI — это современный, быстрый (высокопрои FastAPI стоит на плечах гигантов: -* Starlette для части, связанной с вебом. +* Starlette для части, связанной с вебом. * Pydantic для части, связанной с данными. ## Установка { #installation } @@ -229,7 +229,7 @@ INFO: Application startup complete.
О команде fastapi dev main.py... -Команда `fastapi dev` читает ваш файл `main.py`, находит в нём приложение **FastAPI** и запускает сервер с помощью Uvicorn. +Команда `fastapi dev` читает ваш файл `main.py`, находит в нём приложение **FastAPI** и запускает сервер с помощью Uvicorn. По умолчанию `fastapi dev` запускается с включённой авто-перезагрузкой для локальной разработки. @@ -470,7 +470,7 @@ FastAPI зависит от Pydantic и Starlette. Используется FastAPI: -* uvicorn — сервер, который загружает и обслуживает ваше приложение. Включает `uvicorn[standard]`, содержащий некоторые зависимости (например, `uvloop`), нужные для высокой производительности. +* uvicorn — сервер, который загружает и обслуживает ваше приложение. Включает `uvicorn[standard]`, содержащий некоторые зависимости (например, `uvloop`), нужные для высокой производительности. * `fastapi-cli[standard]` — чтобы предоставить команду `fastapi`. * Включает `fastapi-cloud-cli`, который позволяет развернуть ваше приложение FastAPI в FastAPI Cloud. diff --git a/docs/ru/docs/tutorial/background-tasks.md b/docs/ru/docs/tutorial/background-tasks.md index 9b6f0c8d3..1ed8522d6 100644 --- a/docs/ru/docs/tutorial/background-tasks.md +++ b/docs/ru/docs/tutorial/background-tasks.md @@ -61,7 +61,7 @@ ## Технические детали { #technical-details } -Класс `BackgroundTasks` приходит напрямую из `starlette.background`. +Класс `BackgroundTasks` приходит напрямую из `starlette.background`. Он импортируется/включается прямо в FastAPI, чтобы вы могли импортировать его из `fastapi` и избежать случайного импорта альтернативного `BackgroundTask` (без `s` на конце) из `starlette.background`. @@ -69,7 +69,7 @@ По‑прежнему можно использовать один `BackgroundTask` в FastAPI, но тогда вам нужно создать объект в своём коде и вернуть Starlette `Response`, включающий его. -Подробнее см. в официальной документации Starlette по фоновым задачам. +Подробнее см. в официальной документации Starlette по фоновым задачам. ## Предостережение { #caveat } diff --git a/docs/ru/docs/tutorial/first-steps.md b/docs/ru/docs/tutorial/first-steps.md index 9cdf76f5d..c82118cbe 100644 --- a/docs/ru/docs/tutorial/first-steps.md +++ b/docs/ru/docs/tutorial/first-steps.md @@ -155,7 +155,7 @@ OpenAPI определяет схему API для вашего API. И эта `FastAPI` — это класс, который напрямую наследуется от `Starlette`. -Вы можете использовать весь функционал Starlette и в `FastAPI`. +Вы можете использовать весь функционал Starlette и в `FastAPI`. /// diff --git a/docs/ru/docs/tutorial/handling-errors.md b/docs/ru/docs/tutorial/handling-errors.md index 33b7babf5..2378c8b04 100644 --- a/docs/ru/docs/tutorial/handling-errors.md +++ b/docs/ru/docs/tutorial/handling-errors.md @@ -81,7 +81,7 @@ ## Установка пользовательских обработчиков исключений { #install-custom-exception-handlers } -Вы можете добавить пользовательские обработчики исключений с помощью то же самое исключение - утилиты от Starlette. +Вы можете добавить пользовательские обработчики исключений с помощью то же самое исключение - утилиты от Starlette. Допустим, у вас есть пользовательское исключение `UnicornException`, которое вы (или используемая вами библиотека) можете `вызвать`. diff --git a/docs/ru/docs/tutorial/middleware.md b/docs/ru/docs/tutorial/middleware.md index ea535a151..5803b398b 100644 --- a/docs/ru/docs/tutorial/middleware.md +++ b/docs/ru/docs/tutorial/middleware.md @@ -39,7 +39,7 @@ Имейте в виду, что можно добавлять свои собственные заголовки при помощи префикса 'X-'. -Если же вы хотите добавить собственные заголовки, которые клиент сможет увидеть в браузере, то вам потребуется добавить их в настройки CORS ([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank}), используя параметр `expose_headers`, см. документацию Starlette's CORS docs. +Если же вы хотите добавить собственные заголовки, которые клиент сможет увидеть в браузере, то вам потребуется добавить их в настройки CORS ([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank}), используя параметр `expose_headers`, см. документацию Starlette's CORS docs. /// diff --git a/docs/ru/docs/tutorial/static-files.md b/docs/ru/docs/tutorial/static-files.md index 282c84db1..8455aea0a 100644 --- a/docs/ru/docs/tutorial/static-files.md +++ b/docs/ru/docs/tutorial/static-files.md @@ -38,4 +38,4 @@ OpenAPI и документация из вашего главного прил ## Больше информации { #more-info } -Для получения дополнительной информации о деталях и настройках ознакомьтесь с Документацией Starlette о статических файлах. +Для получения дополнительной информации о деталях и настройках ознакомьтесь с Документацией Starlette о статических файлах. diff --git a/docs/ru/docs/tutorial/testing.md b/docs/ru/docs/tutorial/testing.md index 94e9ae8ae..0224798b1 100644 --- a/docs/ru/docs/tutorial/testing.md +++ b/docs/ru/docs/tutorial/testing.md @@ -1,6 +1,6 @@ # Тестирование { #testing } -Благодаря Starlette, тестировать приложения **FastAPI** легко и приятно. +Благодаря Starlette, тестировать приложения **FastAPI** легко и приятно. Тестирование основано на библиотеке HTTPX, которая в свою очередь основана на библиотеке Requests, так что все действия знакомы и интуитивно понятны. diff --git a/docs/tr/docs/advanced/testing-websockets.md b/docs/tr/docs/advanced/testing-websockets.md index ddacca449..effe557d1 100644 --- a/docs/tr/docs/advanced/testing-websockets.md +++ b/docs/tr/docs/advanced/testing-websockets.md @@ -8,6 +8,6 @@ Bu işlem için, `TestClient`'ı bir `with` ifadesinde kullanarak WebSocket'e ba /// note | Not -Daha fazla detay için Starlette'in Websockets'i Test Etmek dokümantasyonunu inceleyin. +Daha fazla detay için Starlette'in Websockets'i Test Etmek dokümantasyonunu inceleyin. /// diff --git a/docs/tr/docs/alternatives.md b/docs/tr/docs/alternatives.md index c98b966b5..9b603ea81 100644 --- a/docs/tr/docs/alternatives.md +++ b/docs/tr/docs/alternatives.md @@ -415,7 +415,7 @@ Bütün veri doğrulama, veri dönüştürme ve JSON Şemasına bağlı otomatik /// -### Starlette +### Starlette Starlette hafif bir ASGI framework'ü ve yüksek performanslı asyncio servisleri oluşturmak için ideal. @@ -460,7 +460,7 @@ Yani, Starlette ile yapabileceğiniz her şeyi, Starlette'in bir nevi güçlendi /// -### Uvicorn +### Uvicorn Uvicorn, uvlook ile httptools üzerine kurulu ışık hzında bir ASGI sunucusudur. diff --git a/docs/tr/docs/features.md b/docs/tr/docs/features.md index 5d40b1086..86085c5e9 100644 --- a/docs/tr/docs/features.md +++ b/docs/tr/docs/features.md @@ -166,7 +166,7 @@ Bütün entegrasyonlar kullanımı kolay olmak üzere (zorunluluklar ile beraber ## Starlette özellikleri -**FastAPI**, Starlette ile tamamiyle uyumlu ve üzerine kurulu. Yani FastAPI üzerine ekleme yapacağınız herhangi bir Starlette kodu da çalışacaktır. +**FastAPI**, Starlette ile tamamiyle uyumlu ve üzerine kurulu. Yani FastAPI üzerine ekleme yapacağınız herhangi bir Starlette kodu da çalışacaktır. `FastAPI` aslında `Starlette`'nin bir sub-class'ı. Eğer Starlette'nin nasıl kullanılacağını biliyor isen, çoğu işlevini aynı şekilde yapıyor. diff --git a/docs/tr/docs/history-design-future.md b/docs/tr/docs/history-design-future.md index 8b2662bc3..cad290828 100644 --- a/docs/tr/docs/history-design-future.md +++ b/docs/tr/docs/history-design-future.md @@ -58,7 +58,7 @@ Hepsi, tüm geliştiriciler için en iyi geliştirme deneyimini sağlayacak şek Sonra, JSON Schema ile tamamen uyumlu olmasını sağlamak, kısıtlama bildirimlerini tanımlamanın farklı yollarını desteklemek ve birkaç editördeki testlere dayanarak editör desteğini (tip kontrolleri, otomatik tamamlama) geliştirmek için katkıda bulundum. -Geliştirme sırasında, diğer ana gereksinim olan **Starlette**'e de katkıda bulundum. +Geliştirme sırasında, diğer ana gereksinim olan **Starlette**'e de katkıda bulundum. ## Geliştirme diff --git a/docs/tr/docs/index.md b/docs/tr/docs/index.md index c7a2b2fbd..516d5959e 100644 --- a/docs/tr/docs/index.md +++ b/docs/tr/docs/index.md @@ -123,7 +123,7 @@ Eğer API yerine, terminalde kullanılmak üzere bir Starlette. +* Web tarafı için Starlette. * Data tarafı için Pydantic. ## Kurulum @@ -138,7 +138,7 @@ $ pip install fastapi
-Uygulamamızı kullanılabilir hale getirmek için Uvicorn ya da Hypercorn gibi bir ASGI sunucusuna ihtiyacımız olacak. +Uygulamamızı kullanılabilir hale getirmek için Uvicorn ya da Hypercorn gibi bir ASGI sunucusuna ihtiyacımız olacak.
@@ -463,7 +463,7 @@ Starlette tarafında kullanılan: Hem FastAPI hem de Starlette tarafından kullanılan: -* uvicorn - oluşturduğumuz uygulamayı servis edecek web sunucusu görevini üstlenir. +* uvicorn - oluşturduğumuz uygulamayı servis edecek web sunucusu görevini üstlenir. * orjson - `ORJSONResponse` kullanacaksanız gereklidir. * ujson - `UJSONResponse` kullanacaksanız gerekli. diff --git a/docs/tr/docs/tutorial/first-steps.md b/docs/tr/docs/tutorial/first-steps.md index 2d2949b50..9a8ef762d 100644 --- a/docs/tr/docs/tutorial/first-steps.md +++ b/docs/tr/docs/tutorial/first-steps.md @@ -139,7 +139,7 @@ Ayrıca, API'ınızla iletişim kuracak önyüz, mobil veya IoT uygulamaları gi `FastAPI` doğrudan `Starlette`'i miras alan bir sınıftır. -Starlette'in tüm işlevselliğini `FastAPI` ile de kullanabilirsiniz. +Starlette'in tüm işlevselliğini `FastAPI` ile de kullanabilirsiniz. /// diff --git a/docs/tr/docs/tutorial/static-files.md b/docs/tr/docs/tutorial/static-files.md index db30f13bc..4542aca77 100644 --- a/docs/tr/docs/tutorial/static-files.md +++ b/docs/tr/docs/tutorial/static-files.md @@ -37,4 +37,4 @@ Bu parametrelerin hepsi "`static`"den farklı olabilir, bunları kendi uygulaman ## Daha Fazla Bilgi -Daha fazla detay ve seçenek için Starlette'in Statik Dosyalar hakkındaki dokümantasyonunu incelleyin. +Daha fazla detay ve seçenek için Starlette'in Statik Dosyalar hakkındaki dokümantasyonunu incelleyin. diff --git a/docs/uk/docs/alternatives.md b/docs/uk/docs/alternatives.md index 1acbe237a..786df45c5 100644 --- a/docs/uk/docs/alternatives.md +++ b/docs/uk/docs/alternatives.md @@ -415,7 +415,7 @@ Pydantic — це бібліотека для визначення переві /// -### Starlette +### Starlette Starlette — це легкий фреймворк/набір інструментів ASGI, який ідеально підходить для створення високопродуктивних asyncio сервісів. @@ -460,7 +460,7 @@ ASGI — це новий «стандарт», який розробляєтьс /// -### Uvicorn +### Uvicorn Uvicorn — це блискавичний сервер ASGI, побудований на uvloop і httptools. diff --git a/docs/uk/docs/fastapi-cli.md b/docs/uk/docs/fastapi-cli.md index 6bbbbc326..f18b10471 100644 --- a/docs/uk/docs/fastapi-cli.md +++ b/docs/uk/docs/fastapi-cli.md @@ -60,7 +60,7 @@ FastAPI CLI приймає шлях до Вашої Python програми (н Натомість, для запуску у продакшн використовуйте `fastapi run`. 🚀 -Всередині **FastAPI CLI** використовує Uvicorn, високопродуктивний, production-ready, ASGI cервер. 😎 +Всередині **FastAPI CLI** використовує Uvicorn, високопродуктивний, production-ready, ASGI cервер. 😎 ## `fastapi dev` diff --git a/docs/uk/docs/features.md b/docs/uk/docs/features.md index 7d679d8ee..aa0ef7c79 100644 --- a/docs/uk/docs/features.md +++ b/docs/uk/docs/features.md @@ -147,7 +147,7 @@ FastAPI має розумні налаштування **за замовчува ## Можливості Starlette -**FastAPI** повністю сумісний із (та побудований на основі) Starlette. Тому будь-який додатковий код Starlette, який ви маєте, також працюватиме. +**FastAPI** повністю сумісний із (та побудований на основі) Starlette. Тому будь-який додатковий код Starlette, який ви маєте, також працюватиме. **FastAPI** фактично є підкласом **Starlette**. Тому, якщо ви вже знайомі зі Starlette або використовуєте його, більшість функціональності працюватиме так само. diff --git a/docs/uk/docs/index.md b/docs/uk/docs/index.md index 7e919e257..0811a4c7b 100644 --- a/docs/uk/docs/index.md +++ b/docs/uk/docs/index.md @@ -112,7 +112,7 @@ FastAPI - це сучасний, швидкий (високопродуктив FastAPI стоїть на плечах гігантів: -* Starlette для web частини. +* Starlette для web частини. * Pydantic для частини даних. ## Вставновлення @@ -127,7 +127,7 @@ $ pip install fastapi
-Вам також знадобиться сервер ASGI для продакшину, наприклад Uvicorn або Hypercorn. +Вам також знадобиться сервер ASGI для продакшину, наприклад Uvicorn або Hypercorn.
@@ -452,7 +452,7 @@ Starlette використовує: FastAPI / Starlette використовують: -* uvicorn - для сервера, який завантажує та обслуговує вашу програму. +* uvicorn - для сервера, який завантажує та обслуговує вашу програму. * orjson - Необхідно, якщо Ви хочете використовувати `ORJSONResponse`. * ujson - Необхідно, якщо Ви хочете використовувати `UJSONResponse`. diff --git a/docs/uk/docs/tutorial/background-tasks.md b/docs/uk/docs/tutorial/background-tasks.md index 912ba8c2a..0a9349650 100644 --- a/docs/uk/docs/tutorial/background-tasks.md +++ b/docs/uk/docs/tutorial/background-tasks.md @@ -62,7 +62,7 @@ ## Технічні деталі -Клас `BackgroundTasks` походить безпосередньо з `starlette.background`. +Клас `BackgroundTasks` походить безпосередньо з `starlette.background`. Він імпортується безпосередньо у FastAPI, щоб Ви могли використовувати його з `fastapi` і випадково не імпортували `BackgroundTask` (без s в кінці) з `starlette.background`. @@ -70,7 +70,7 @@ Також можна використовувати `BackgroundTask` окремо в FastAPI, але для цього Вам доведеться створити об'єкт у коді та повернути Starlette `Response`, включаючи його. -Детальніше можна почитати в офіційній документації Starlette про фонові задачі . +Детальніше можна почитати в офіційній документації Starlette про фонові задачі . ## Застереження diff --git a/docs/uk/docs/tutorial/first-steps.md b/docs/uk/docs/tutorial/first-steps.md index e910c4ccc..3f861cb48 100644 --- a/docs/uk/docs/tutorial/first-steps.md +++ b/docs/uk/docs/tutorial/first-steps.md @@ -163,7 +163,7 @@ OpenAPI описує схему для вашого API. І ця схема вк `FastAPI` це клас, який успадковується безпосередньо від `Starlette`. -Ви також можете використовувати всю функціональність Starlette у `FastAPI`. +Ви також можете використовувати всю функціональність Starlette у `FastAPI`. /// diff --git a/docs/uk/docs/tutorial/handling-errors.md b/docs/uk/docs/tutorial/handling-errors.md index 12a356cd0..32de73b2a 100644 --- a/docs/uk/docs/tutorial/handling-errors.md +++ b/docs/uk/docs/tutorial/handling-errors.md @@ -81,7 +81,7 @@ ## Встановлення власних обробників помилок -Ви можете додати власні обробники помилок за допомогою тих самих утиліт обробки помилок зі Starlette. +Ви можете додати власні обробники помилок за допомогою тих самих утиліт обробки помилок зі Starlette. Припустимо, у Вас є власний обʼєкт помилки `UnicornException`, яке Ви (або бібліотека, яку Ви використовуєте) може `згенерувати` (`raise`). diff --git a/docs/uk/docs/tutorial/middleware.md b/docs/uk/docs/tutorial/middleware.md index 807be484a..13ce8573d 100644 --- a/docs/uk/docs/tutorial/middleware.md +++ b/docs/uk/docs/tutorial/middleware.md @@ -39,7 +39,7 @@ Не забувайте, що власні заголовки можна додавати, використовуючи префікс 'X-'. -Але якщо у Вас є власні заголовки, які Ви хочете, щоб браузерний клієнт міг побачити, потрібно додати їх до Вашої конфігурації CORS (див. [CORS (Обмін ресурсами між різними джерелами)](cors.md){.internal-link target=_blank} за допомогою параметра `expose_headers`, описаного в документації Starlette по CORS. +Але якщо у Вас є власні заголовки, які Ви хочете, щоб браузерний клієнт міг побачити, потрібно додати їх до Вашої конфігурації CORS (див. [CORS (Обмін ресурсами між різними джерелами)](cors.md){.internal-link target=_blank} за допомогою параметра `expose_headers`, описаного в документації Starlette по CORS. /// diff --git a/docs/uk/docs/tutorial/static-files.md b/docs/uk/docs/tutorial/static-files.md index a84782d8f..3427f2376 100644 --- a/docs/uk/docs/tutorial/static-files.md +++ b/docs/uk/docs/tutorial/static-files.md @@ -37,4 +37,4 @@ ## Додаткова інформація -Детальніше про налаштування та можливості можна дізнатися в документації Starlette про статичні файли. +Детальніше про налаштування та можливості можна дізнатися в документації Starlette про статичні файли. diff --git a/docs/uk/docs/tutorial/testing.md b/docs/uk/docs/tutorial/testing.md index 25fc370d6..1105c6b0a 100644 --- a/docs/uk/docs/tutorial/testing.md +++ b/docs/uk/docs/tutorial/testing.md @@ -1,6 +1,6 @@ # Тестування -Тестування **FastAPI** додатків є простим та ефективним завдяки бібліотеці Starlette, яка базується на HTTPX. +Тестування **FastAPI** додатків є простим та ефективним завдяки бібліотеці Starlette, яка базується на HTTPX. Оскільки HTTPX розроблений на основі Requests, його API є інтуїтивно зрозумілим для тих, хто вже знайомий з Requests. З його допомогою Ви можете використовувати pytest безпосередньо з **FastAPI**. diff --git a/docs/vi/docs/fastapi-cli.md b/docs/vi/docs/fastapi-cli.md index d9e315ae4..e758f4d3a 100644 --- a/docs/vi/docs/fastapi-cli.md +++ b/docs/vi/docs/fastapi-cli.md @@ -52,7 +52,7 @@ FastAPI CLI nhận đường dẫn đến chương trình Python của bạn (vd Đối với vận hành thực tế (production), bạn sẽ sử dụng `fastapi run` thay thế. 🚀 -Ở bên trong, **FastAPI CLI** sử dụng Uvicorn, một server ASGI có hiệu suất cao, sẵn sàng cho vận hành thực tế (production). 😎 +Ở bên trong, **FastAPI CLI** sử dụng Uvicorn, một server ASGI có hiệu suất cao, sẵn sàng cho vận hành thực tế (production). 😎 ## `fastapi dev` diff --git a/docs/vi/docs/index.md b/docs/vi/docs/index.md index e7df2bf72..a5ac1bfb7 100644 --- a/docs/vi/docs/index.md +++ b/docs/vi/docs/index.md @@ -124,7 +124,7 @@ Nếu bạn đang xây dựng một CLIStarlette cho phần web. +* Starlette cho phần web. * Pydantic cho phần data. ## Cài đặt @@ -139,7 +139,7 @@ $ pip install fastapi
-Bạn cũng sẽ cần một ASGI server cho production như Uvicorn hoặc Hypercorn. +Bạn cũng sẽ cần một ASGI server cho production như Uvicorn hoặc Hypercorn.
@@ -464,7 +464,7 @@ Sử dụng Starlette: Sử dụng bởi FastAPI / Starlette: -* uvicorn - Server để chạy ứng dụng của bạn. +* uvicorn - Server để chạy ứng dụng của bạn. * orjson - Bắt buộc nếu bạn muốn sử dụng `ORJSONResponse`. * ujson - Bắt buộc nếu bạn muốn sử dụng `UJSONResponse`. diff --git a/docs/vi/docs/tutorial/first-steps.md b/docs/vi/docs/tutorial/first-steps.md index 901c8fd59..d1650539c 100644 --- a/docs/vi/docs/tutorial/first-steps.md +++ b/docs/vi/docs/tutorial/first-steps.md @@ -139,7 +139,7 @@ Bạn cũng có thể sử dụng nó để sinh code tự động, với các c `FastAPI` là một class kế thừa trực tiếp `Starlette`. -Bạn cũng có thể sử dụng tất cả Starlette chức năng với `FastAPI`. +Bạn cũng có thể sử dụng tất cả Starlette chức năng với `FastAPI`. /// diff --git a/docs/vi/docs/tutorial/static-files.md b/docs/vi/docs/tutorial/static-files.md index ecf8c2485..1bbec29e7 100644 --- a/docs/vi/docs/tutorial/static-files.md +++ b/docs/vi/docs/tutorial/static-files.md @@ -37,4 +37,4 @@ Tất cả các tham số này có thể khác với `static`, điều chỉnh c ## Thông tin thêm -Để biết thêm chi tiết và tùy chọn, hãy xem Starlette's docs about Static Files. +Để biết thêm chi tiết và tùy chọn, hãy xem Starlette's docs about Static Files. diff --git a/docs/zh-hant/docs/fastapi-cli.md b/docs/zh-hant/docs/fastapi-cli.md index 3c644ce46..b107e7e73 100644 --- a/docs/zh-hant/docs/fastapi-cli.md +++ b/docs/zh-hant/docs/fastapi-cli.md @@ -60,7 +60,7 @@ FastAPI CLI 接收你的 Python 程式路徑(例如 `main.py`),並自動 在生產環境,你應該使用 `fastapi run` 命令。 🚀 -**FastAPI CLI** 內部使用了 Uvicorn,這是一個高效能、適合生產環境的 ASGI 伺服器。 😎 +**FastAPI CLI** 內部使用了 Uvicorn,這是一個高效能、適合生產環境的 ASGI 伺服器。 😎 ## `fastapi dev` diff --git a/docs/zh-hant/docs/features.md b/docs/zh-hant/docs/features.md index 3a1392b51..f44d28a7f 100644 --- a/docs/zh-hant/docs/features.md +++ b/docs/zh-hant/docs/features.md @@ -167,7 +167,7 @@ FastAPI 有一個使用簡單,但是非常強大的Starlette的CORS文档中记录的`expose_headers`参数。 +但是,如果你有自定义头部,你希望浏览器中的客户端能够看到它们,你需要将它们添加到你的CORS配置中(在[CORS(跨源资源共享)](../tutorial/cors.md){.internal-link target=_blank}中阅读更多),使用在Starlette的CORS文档中记录的`expose_headers`参数。 diff --git a/docs/zh/docs/advanced/templates.md b/docs/zh/docs/advanced/templates.md index 8b7019ede..e627eed98 100644 --- a/docs/zh/docs/advanced/templates.md +++ b/docs/zh/docs/advanced/templates.md @@ -122,4 +122,4 @@ Item ID: 42 ## 更多说明 -包括测试模板等更多详情,请参阅 Starlette 官方文档 - 模板。 +包括测试模板等更多详情,请参阅 Starlette 官方文档 - 模板。 diff --git a/docs/zh/docs/advanced/testing-websockets.md b/docs/zh/docs/advanced/testing-websockets.md index 5d713d5f7..b84647a3e 100644 --- a/docs/zh/docs/advanced/testing-websockets.md +++ b/docs/zh/docs/advanced/testing-websockets.md @@ -8,6 +8,6 @@ /// note | 笔记 -更多细节详见 Starlette 官档 - 测试 WebSockets。 +更多细节详见 Starlette 官档 - 测试 WebSockets。 /// diff --git a/docs/zh/docs/advanced/using-request-directly.md b/docs/zh/docs/advanced/using-request-directly.md index db0fcafdf..a9658c034 100644 --- a/docs/zh/docs/advanced/using-request-directly.md +++ b/docs/zh/docs/advanced/using-request-directly.md @@ -15,7 +15,7 @@ ## `Request` 对象的细节 -实际上,**FastAPI** 的底层是 **Starlette**,**FastAPI** 只不过是在 **Starlette** 顶层提供了一些工具,所以能直接使用 Starlette 的 `Request` 对象。 +实际上,**FastAPI** 的底层是 **Starlette**,**FastAPI** 只不过是在 **Starlette** 顶层提供了一些工具,所以能直接使用 Starlette 的 `Request` 对象。 但直接从 `Request` 对象提取数据时(例如,读取请求体),**FastAPI** 不会验证、转换和存档数据(为 API 文档使用 OpenAPI)。 @@ -45,7 +45,7 @@ ## `Request` 文档 -更多细节详见 Starlette 官档 - `Request` 对象。 +更多细节详见 Starlette 官档 - `Request` 对象。 /// note | 技术细节 diff --git a/docs/zh/docs/advanced/websockets.md b/docs/zh/docs/advanced/websockets.md index d91aacc03..005ed9242 100644 --- a/docs/zh/docs/advanced/websockets.md +++ b/docs/zh/docs/advanced/websockets.md @@ -172,5 +172,5 @@ Client #1596980209979 left the chat 要了解更多选项,请查看 Starlette 的文档: -* [WebSocket 类](https://www.starlette.io/websockets/) -* [基于类的 WebSocket 处理](https://www.starlette.io/endpoints/#websocketendpoint)。 +* [WebSocket 类](https://www.starlette.dev/websockets/) +* [基于类的 WebSocket 处理](https://www.starlette.dev/endpoints/#websocketendpoint)。 diff --git a/docs/zh/docs/deployment/manually.md b/docs/zh/docs/deployment/manually.md index 3dc5942e3..2c2784a64 100644 --- a/docs/zh/docs/deployment/manually.md +++ b/docs/zh/docs/deployment/manually.md @@ -52,7 +52,7 @@ FastAPI 使用了一种用于构建 Python Web 框架和服务器的标准,称 除此之外,还有其他一些可选的 ASGI 服务器,例如: -* Uvicorn:高性能 ASGI 服务器。 +* Uvicorn:高性能 ASGI 服务器。 * Hypercorn:与 HTTP/2 和 Trio 等兼容的 ASGI 服务器。 * Daphne:为 Django Channels 构建的 ASGI 服务器。 * Granian:基于 Rust 的 HTTP 服务器,专为 Python 应用设计。 diff --git a/docs/zh/docs/fastapi-cli.md b/docs/zh/docs/fastapi-cli.md index 8a70e1d80..3b67eb664 100644 --- a/docs/zh/docs/fastapi-cli.md +++ b/docs/zh/docs/fastapi-cli.md @@ -52,7 +52,7 @@ FastAPI CLI 接收你的 Python 程序路径,自动检测包含 FastAPI 的变 在生产环境中,你应该使用 `fastapi run` 命令。🚀 -在内部,**FastAPI CLI** 使用了 Uvicorn,这是一个高性能、适用于生产环境的 ASGI 服务器。😎 +在内部,**FastAPI CLI** 使用了 Uvicorn,这是一个高性能、适用于生产环境的 ASGI 服务器。😎 ## `fastapi dev` diff --git a/docs/zh/docs/features.md b/docs/zh/docs/features.md index 24dc3e8ce..eaf8daff7 100644 --- a/docs/zh/docs/features.md +++ b/docs/zh/docs/features.md @@ -165,7 +165,7 @@ FastAPI 有一个使用非常简单,但是非常强大的. +更多细节查看 Starlette's official docs for Background Tasks. ## 告诫 diff --git a/docs/zh/docs/tutorial/first-steps.md b/docs/zh/docs/tutorial/first-steps.md index 80a34116a..2d7c35c8c 100644 --- a/docs/zh/docs/tutorial/first-steps.md +++ b/docs/zh/docs/tutorial/first-steps.md @@ -155,7 +155,7 @@ OpenAPI 为你的 API 定义 API 模式。该模式中包含了你的 API 发送 `FastAPI` 是直接从 `Starlette` 继承的类。 -你可以通过 `FastAPI` 使用所有的 Starlette 的功能。 +你可以通过 `FastAPI` 使用所有的 Starlette 的功能。 /// diff --git a/docs/zh/docs/tutorial/handling-errors.md b/docs/zh/docs/tutorial/handling-errors.md index 0b887c292..ae667b74a 100644 --- a/docs/zh/docs/tutorial/handling-errors.md +++ b/docs/zh/docs/tutorial/handling-errors.md @@ -83,7 +83,7 @@ ## 安装自定义异常处理器 -添加自定义处理器,要使用 [Starlette 的异常工具](https://www.starlette.io/exceptions/)。 +添加自定义处理器,要使用 [Starlette 的异常工具](https://www.starlette.dev/exceptions/)。 假设要触发的自定义异常叫作 `UnicornException`。 diff --git a/docs/zh/docs/tutorial/middleware.md b/docs/zh/docs/tutorial/middleware.md index 258ca7482..5608c4ee1 100644 --- a/docs/zh/docs/tutorial/middleware.md +++ b/docs/zh/docs/tutorial/middleware.md @@ -37,7 +37,7 @@ 请记住可以 用'X-' 前缀添加专有自定义请求头. -但是如果你想让浏览器中的客户端看到你的自定义请求头, 你需要把它们加到 CORS 配置 ([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank}) 的 `expose_headers` 参数中,在 Starlette's CORS docs文档中. +但是如果你想让浏览器中的客户端看到你的自定义请求头, 你需要把它们加到 CORS 配置 ([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank}) 的 `expose_headers` 参数中,在 Starlette's CORS docs文档中. /// diff --git a/docs/zh/docs/tutorial/static-files.md b/docs/zh/docs/tutorial/static-files.md index c19079565..1a0d4504c 100644 --- a/docs/zh/docs/tutorial/static-files.md +++ b/docs/zh/docs/tutorial/static-files.md @@ -37,4 +37,4 @@ ## 更多信息 -更多细节和选择查阅 Starlette's docs about Static Files. +更多细节和选择查阅 Starlette's docs about Static Files. diff --git a/docs/zh/docs/tutorial/testing.md b/docs/zh/docs/tutorial/testing.md index 3e0c48caf..3877adbac 100644 --- a/docs/zh/docs/tutorial/testing.md +++ b/docs/zh/docs/tutorial/testing.md @@ -1,6 +1,6 @@ # 测试 -感谢 Starlette,测试**FastAPI** 应用轻松又愉快。 +感谢 Starlette,测试**FastAPI** 应用轻松又愉快。 它基于 HTTPX, 而HTTPX又是基于Requests设计的,所以很相似且易懂。 diff --git a/fastapi/applications.py b/fastapi/applications.py index 915f5f70a..6db4b4e83 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -75,7 +75,7 @@ class FastAPI(Starlette): errors. Read more in the - [Starlette docs for Applications](https://www.starlette.io/applications/#instantiating-the-application). + [Starlette docs for Applications](https://www.starlette.dev/applications/#instantiating-the-application). """ ), ] = False, @@ -938,7 +938,7 @@ class FastAPI(Starlette): This is simply inherited from Starlette. Read more about it in the - [Starlette docs for Applications](https://www.starlette.io/applications/#storing-state-on-the-app-instance). + [Starlette docs for Applications](https://www.starlette.dev/applications/#storing-state-on-the-app-instance). """ ), ] = State() From 414f961f1f1ebcc650f9b6f3a1788688cefd1d3d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 11 Oct 2025 17:49:15 +0000 Subject: [PATCH 025/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 628f26562..f8ccedc6a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Docs + +* 📝 Replace `starlette.io` by `starlette.dev` and `uvicorn.org` by `uvicorn.dev`. PR [#14176](https://github.com/fastapi/fastapi/pull/14176) by [@Kludex](https://github.com/Kludex). + ## 0.119.0 FastAPI now (temporarily) supports both Pydantic v2 models and `pydantic.v1` models at the same time in the same app, to make it easier for any FastAPI apps still using Pydantic v1 to gradually but quickly **migrate to Pydantic v2**. From 7f810ca93b151c7e3765335118db7547b5cb9a5b Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Sat, 11 Oct 2025 21:36:25 +0200 Subject: [PATCH 026/256] =?UTF-8?q?=F0=9F=94=A7=20Configure=20reminder=20f?= =?UTF-8?q?or=20`waiting`=20label=20in=20`issue-manager`=20(#14156)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/issue-manager.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/issue-manager.yml b/.github/workflows/issue-manager.yml index b587d15e6..f40ec4dc4 100644 --- a/.github/workflows/issue-manager.yml +++ b/.github/workflows/issue-manager.yml @@ -38,7 +38,11 @@ jobs: }, "waiting": { "delay": 2628000, - "message": "As this PR has been waiting for the original user for a while but seems to be inactive, it's now going to be closed. But if there's anyone interested, feel free to create a new PR." + "message": "As this PR has been waiting for the original user for a while but seems to be inactive, it's now going to be closed. But if there's anyone interested, feel free to create a new PR.", + "reminder": { + "before": "P3D", + "message": "Heads-up: this will be closed in 3 days unless there’s new activity." + } }, "invalid": { "delay": 0, From 81f85831f5ac2aac7266eb6fb4322e7d77486587 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 11 Oct 2025 19:36:56 +0000 Subject: [PATCH 027/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f8ccedc6a..1853b54f9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -11,6 +11,10 @@ hide: * 📝 Replace `starlette.io` by `starlette.dev` and `uvicorn.org` by `uvicorn.dev`. PR [#14176](https://github.com/fastapi/fastapi/pull/14176) by [@Kludex](https://github.com/Kludex). +### Internal + +* 🔧 Configure reminder for `waiting` label in `issue-manager`. PR [#14156](https://github.com/fastapi/fastapi/pull/14156) by [@YuriiMotov](https://github.com/YuriiMotov). + ## 0.119.0 FastAPI now (temporarily) supports both Pydantic v2 models and `pydantic.v1` models at the same time in the same app, to make it easier for any FastAPI apps still using Pydantic v1 to gradually but quickly **migrate to Pydantic v2**. From 7df594d284cabf837774dffc40e542cca9333ad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 19 Oct 2025 21:12:22 +0200 Subject: [PATCH 028/256] =?UTF-8?q?=F0=9F=94=A7=20Add=20sponsor=20Requestl?= =?UTF-8?q?y=20(#14205)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/data/sponsors.yml | 3 +++ docs/en/data/sponsors_badge.yml | 1 + docs/en/docs/img/sponsors/requestly.png | Bin 0 -> 18379 bytes 3 files changed, 4 insertions(+) create mode 100644 docs/en/docs/img/sponsors/requestly.png diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index ae28410e7..7a015e404 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -58,3 +58,6 @@ bronze: - url: https://lambdatest.com/?utm_source=fastapi&utm_medium=partner&utm_campaign=sponsor&utm_term=opensource&utm_content=webpage title: LambdaTest, AI-Powered Cloud-based Test Orchestration Platform img: https://fastapi.tiangolo.com/img/sponsors/lambdatest.png + - url: https://requestly.com/fastapi + title: All-in-one platform to Test, Mock and Intercept APIs. Built for speed, privacy and offline support. + img: https://fastapi.tiangolo.com/img/sponsors/requestly.png diff --git a/docs/en/data/sponsors_badge.yml b/docs/en/data/sponsors_badge.yml index 62ba6a84c..14f55805c 100644 --- a/docs/en/data/sponsors_badge.yml +++ b/docs/en/data/sponsors_badge.yml @@ -46,3 +46,4 @@ logins: - madisonredtfeldt - railwayapp - subtotal + - requestly diff --git a/docs/en/docs/img/sponsors/requestly.png b/docs/en/docs/img/sponsors/requestly.png new file mode 100644 index 0000000000000000000000000000000000000000..a167aa017441dca85deb0079db72d0cc4c248644 GIT binary patch literal 18379 zcmaHTXEa>j7q$|FAU`#_L{D_0*AxUnh~7po(R-iKBGIE0WrUFEeRQKw^b(BGMK{`D z7|ihcziYi;-w)@WweGrS-Fx>rXYXe}cRx4wgSHw4850>E9v+4IySIAx{mFf^dq{kL zY$@u5-FKv(?@WB~@F@HK*YIH;zwPc%GWx0*`+oFr@C~r`w#N$y2oQ31ck!{c_OuuB z@OI2Sl4ruhdy1$2_LY8MUQ<}WyoEu=ZOGAO*p=Qh(q|92X1M-Se);3k1KQ7wJwHap zRq*Lt#jSD}4~mSH-_}0+ZEO8@PrCbE28UKK-W&C&5z67p)Oua#H7mHAjis=;_Tg4i z@rh&kiL8)g#hc2`l_t39@xo1B$j1bPch&@kPoSOLM_Sg|kOD8OJIP zy6H8~)uHvzFZov;bG#{Mcx*MD<|tPB%u zW1_3n?fI)RbW)mCCJZBkht4Z0*IHC3v4M29srHF~_PEgYO2kMh6Sky92( ze~=;d_WbpUQUG`XQuh`EEl5c)2Agf!^|>Kwt4-er&Rj5<+IRvI6{1V-_9DL4_+ z4zh=}>M|}%tHm~i{iCI#E`6qwb%eFWCbo}mUDNnPybDzWi(c@&sOxWiS#ka1mx%OX zAQRLQc;eV292-+3Jc@1|;~kX6wrcSL@+Sp=iqantcN}u0^!%Dw>5*Fe=WlIAw?tb7 zPkY+9^J)rT6JKzHJKy0s`%`T+cr)cu{x|e473FH!;5tIikv2w&L|-JmfFro<($XAk zM}F!SZa;<4Mb=g{TUam1RKPHZ6@+`r%0H)r-@vM|jKq0?>BKb7hWJ5YQjHBb*v-a{ z`dgI!Udzmx2iidn!Nqt}U_Rk(?5LeIg^j;kAd4W?U94W7*7j6&<-#ymDx1$a^&Ie3 z{v6lP^@qo#Ursw9R1`I-Pa+1deFkS4R~(L{YZ1?Fv1n2uvcSRLyi4PB;@MjMl}2)E zP3q%9TA@M28o=8QX*XSTQ{{{Kebf!aUwdz7G)*M|Q2p0U(enYgYbB`)wc`~=X9I5% z`?6F@Ba-%It=We@?Q@4@J%QwcUl}a!#LDzlYv#?V1Y4mbr1CgBqSH&BvleK6yBmIq z^q;agiGPI@x9X^lPv$E=+oD%$ZqRybH1<$3NCi{H;?E%`xsfbs&VfaBMb>zV0mF=m zcP_q8y}U8)t;L8JNKcOGZ;jX{OfQAc&lLYuvMSj9E=-I;*gU2 zX5Z_|=dYdL>Gz@cJz0ZLS?;+zKvKx@)!68?6myKGRc<0tl;cx?YzVd>#SK7727bo? z4V#dC#52~L@Jd`n+(*x+PqGeIui#cR8pa*r#A+ub5Wo09z!19sTxKFm`$sGQiwsS+F)-(?l-7`q4g*y4Jy-oyE(!59e2A&d0gm zl)chDe2SPP`;;oCca8A->5%EF4F17g+hkFc@TW#HDj1u`;>&T4k@#3^QMl>AT6rcG zKVQz%yyYcB8u#%!WKQ-ucm1;j3nD1wNg5$<_fJJNq|L}}QPyD1;hu-^7KEj8$pI&2 z<52C(0Gi7RlK^E#Z|*2<>`dfFtscA%v0WUsX-IXTKdzj6ZoK!^i_=1&_-h`y`KEV? zRgQ#>Y5qf3$P>bA*UAhBY*D~Ve0k?^9nZG>x`n2K3~pCEZ+UBakH?8h8W2c*ZKZ(h zC+xpUCTJClO-zb^3``QzWj7Rsa*R26}F!y9%x=*^$DX?5@#&qE3q zDx7L|J8|UwD;7#X2hke7P|SX*$V{Hl=iYCJYf{whML}Xu)XqFmKDk`vsqfC% z5jrpG8K4=(Ub_~q`69j_;Oot8FqiY+=}#6V``K^=jM1`kG9djVG)xffL<8+o*=tflW;nHZ!>oo9C_U8_n8B zh^!iPl6$bSoXsuG`>KGSziPw|o^kUhSAY*1GIDjozlSkK*>4McxnmGUri4i$Yhr(X z2##k%W3HTTxH5?b!z)vC@TQLEPhE^n(wyFy(vNK*>aeCX)ipwl3L_97+OQeK z7|EB~$UzTrZ#iu0A76COcz+1RfmJVkL1%Gzmdj>XqiA1|4fPtNH#BqqbKN%Ndb38# zs0yrdHUX)pE2NmgTK;)+;sSt!kp%fLYm5F1QJ{?LX|ctw6UCD%nYSi~@^kNc@e0=a zodx-8r_QVS8dN&)`|AbyT?T96Dm@&=oViZey5ivnn73=-ojFP#np`EMmmPJO@&HEW zA$l+tCR|y<{$RZ>^vbbr2JsRJNCs@0`MV}7-IVY$CxZ!f%H_j^^LHlx?X;ZS8dx$P zo7k}y_y2R-Tzq-^>4|LlSuxt>h&@ANLHSnltzs4loKgFJ)L2%6?9HNGuZccSIIpo> zp*i4^fjP2oQ3F{(1208A%G_L#{0u9PiDNY*iSX&>qRPD0se) za!FuTNRJ%aZ)30?5;*diR zoi4g|r4it5+BGcbS{2IfxNE>k8+TGNK_P&k0=CUs)h)5l6n_X8^55s~*rO(IFyBG?(nO7hKbSUb)wp3eN3FlNtFyV0;*C`z1oi0e z5yh@a1UhL#oqi7NMw;Q*P{Neu=xSC`EQmhMZY#Gg-FWeI@VFvo3?>X;xMhO=sbT)O z@ucJTLW6m{(&7LQKQwy7LvSEKz!khS-sL#4g%Sj|o7dh7xXB`PNe>yN9o~1w4nzz` z^v8Ld*ow!cdw#Df7R!aD#ZJ9v%&IIf;Gl=xUqsG2s+*&_&?`%{(L6pFD8T8Uz-YS! z*c1=(CbEUkMUE_Jc-)GNh*TIa8hni`{Ow44_x58*&h<*Q_D?40;N}hh;F0$IL5YLO zl+^c%%8KQ*-xYj~O8BYr`9!p;O~gr0MiC4Gh1KSD#Iq%lcCych8m+l(x5Ev~c@?OZ zO3^!&TOQSBwpYkjkA8uBTvS)JJpcS9kWp^-ef8ASSp|)~oYx1={0w!X8<(eKQeRAU zW_DSZYqZWCabsew_H2T%=AmqV;kW0spg+N8{yWhF7>TJ}Gi`bhL>$RC`+$ZJS-PXd z^Pt6tLNdG5MQipV5AY=Z-zUD|%~nt&PZ__pK>WN|UnbMs^9{Q`hMb{!Cp1Q4V-VA1 ziOQw|y6~0>h*^&BF4VQ8T6avtHg+^(XrlBDbnE}RW}rs=LgzJvG~y%!CCqMQHo|H1 zu-_-HUZ^UH(`Z@^%UFJzxq(9Xd0#!_$o(MD>WrnHWP>(YII`wqWIKObRzD}z8jio(eBhr#RZ6) z&e+>L%)BJq*8z6x;oyh<7iEFB4M2LwOar((O3i;(af5VBSRzMWrVzmcS%PEa{EEkt z7D$^2Pua{z)swm7XD%{-cNbk|U`#oCTxwkxTFNjS(svlwD zYDmkJ=J5PjpiS6_P+q%ZkzJ+84^Vhh`d_w%N ze=GKGi-h#uAMgm<7+sCk?i9bs$tDF~%ylo`xjIbz$P9KrPi^eH8S~=@c`P&6dFMp( z;=vcS&1&b`<a#fjK$RDq&iJg;OzS*MTaC<{NTIkI~vhCvUz!6n&a?=&O4V(o0FtRBA6wx z#dBI_&Dw>b=^fiQ9%N9+vvd|W`0m`70J1~nl{!v`gz}Q+xK+yv)>ITkGp?&9Gzp~! z+;vN=1%xaHvB;A4XE@M)WAX^W`h{&g$S&MbU64d^(W`}GCp+_KYAda-WUS|{+26p@ zqlT471itU87t{acz;}XZY}wt4%{#axIFw%-sTItNE*73)&Zf}K07BUqnKl2g-`}3F zL0whrwB&&)L)uyIL&&b<`aJQry=NWWh3Z02N*{cg;)x4wvj^_;V$j>)K=tvOA6O6k zVnillc3*A!a(E2xYc|^8W-cR z+gIy$d9Z1utf1y1^{hmlLg;`)atp^~vqfOkka&~!Yk|M}FbFC(CE(02qiIg3!lrXc&u5g$NHVUso~4pF3I7=WRiNw^6o@MkZTX3RDGC1og zqD!76VOgxYO2Y<{=&oyeI*GBhh{rxv^XA0`k35>xSWu^em9zYyOPFQKB+QP~A>lOL z66dRpkL&SiH|K{N*fv#ruz0?hZedNV^W|wq=3v6|bro(a^8|Gfz?>>?=luyEf~n+c zji(`W?(Z_0WW5^-hklb+j?`o^-AEE^qM%O%T<+BE=Lt?Zny@=|958fH(GUgB_0_C% zO&$`HZEPp1L3GNupUFW7J?b>?oN&wt9qeQ##EYP5giH>oofk~9kOh^rM!qda7|e&3rZiz&A<#Ock}h?xVy zAFA_nsh4CtUnQi$gi1R|oJS$Uh4cWVo>+F1P6#$~apc?31^I+_H$DxQz3IogC0XM2 zpfng^gS#Zv?zBiI&0#=4w{K;9rb|xhB9eiB@R+ckB`xjy066(E@FZ;4B#7CtitjGi za3y9VIag)FOWi_R&gQ)UC zOK$y=wLf7Mr7@ETQ3$Iz$e+ML!umC9`dsPUMG>J~4yzKGdqpy(dH#O~=s8_K&l@}L zQ^cxMwLbSBSI32h^zlHY*7CEM=ts#1= zEt>g}^@0U{cc5?btTaT(Y`7or@ULDsy{{mNy6G)7@iC_0lP!t!K`p!N$_<^>Qc^EO zFzlmv>k!pDqBvH{2W&Tjj9zCaS(tt5g0*J*^d^8i92fYnOrnmk|FPb~kBBeijfAzX zl%~wzWPaSUB&k+d?2_n)qY3(=L9zzM&MPrDyJ|WjCW9Z07<61uc4a4B`dLkvI{=xn zxwMcQ`^xD8kz$rN#(Y z@85$$#7A*^{c^8fl+O>sMG}j-h%lZq$;_=9GzKXd#ihH4c0URk1cg2n7Ve-F?hIrV z>e@Zy37Xy55w&MY0n}H|t^RA4y@4#%xG%AsEYM$t<$`C{m3?c=m2}W;jgxVabCAui z{uWE=w*^x_Svy@sd~XocEk*R-L!sFtW$E9T`X=XX%rfcs*=^w%zr81m69BWxXeL|& z`QXHvwN!p!?`NvQm3Yf8u102Ydgz}Qu>XG5fbZTVIeeQb)jEOIllfVS?C)1h@p(?i zs#zb*Pcg{(zf?_PvD_O`uPVUF11&Xu_C`XKCH=X0ig;&2nR#JMi5j7%AN1#(ROuYy zY1Jk&A4+5QFLa4}y}B<$&PwKj0!QNzS@Mb7r=JZ7{>%9c46eu-7FM^rVDF^ZTVaDr ze#f0vnkzAifc;2cFd$i^x@)qt{r+?OOMc8a;9j|_Jc|iBa{Fw(1Nz7;vak|NiRH<` zfYV3llQT$Jd?zjEgLV!hy*6>B=jr9C0d=@O@l;ngy~H7rg{4!`ev%t=1bD7HQ%nBl zih`E0U%L$eZmf1LV#;Hz4^M}Vz39uxfBX2*iH=YJgzvnhz4k?lp%GU^_yNy?lQy1I zd`?O6Q;q;w$?7h^s{aoaUbsO;u!!UMnL#os&EUrF%5!Xu)i#RZ2?A zEK<$y(^G#c8d-EQvohOiHt^RTY z7u5ORGt^EyyGlcou;-s=;#0E-lbpK9Ba-aJ0IWD!AwKBL zlK6&YktySxK<>DMl~UeQ_@Mr5Q6v;2LQWx37aH`q0CYYDfo52w8OJD6(Dgd4xjO0KUo{h5nxI zBR<|7N;`=MhY@~Z5&LVQ1nS`k#nL!hPZddd{$&T1iyYPd)iv%6X4O{0flpE%yEv|f zU3(eV`^xy1;B_2#lKkrhhw7pg_BO@6{*GPpBl^BPhC#!QqNKOdp?#7xurKr5qbA`s zp@$sun6;0CH5$V6A;)FX$4fdJg;6VPmXGnGqTaMzcJZPbz&QK>+?5wzw`Is-9J}R- z$wBifTlsMh$#G)T;}x@}{F~0z^^Riz+wl0U&*_lbdjDhAvhn=0<<)iUqiQ{1f#?DB zj;-ak%}4)EjXnQM$Pun0j5|dhmkL9F~ylqZmkSyhn>YCEeP>|tA@^*ahbaAraz@U$85}+1)O}L9Rk)Tru zrWtg@*A;NY_#zPBY9P*FHoI6Yxfcz$i1sM(3wApp=ssT#=xKsu9|ADg=XN0L^}}Xx zw3)}P3PT5;h}(Wr%T_6P{U=Q_V$)QI)j(`6BdeLjd_c;5iM|EB{Rp)s9M&iqh`+{q zoE^t}x1)xc;nfk@3huqVIgDNOXs^A$gR-L|NmAfM*S$fXu|^^0 zW$sluIM-fms>Ca6G!#X-|KQ~{oG*tFY6*|kYJ66uaE?NY>-0&D%PYt?+oWd` zWhrHhNtV~v{-YN#j=p+iX*eHW)D7>tevTc}EwL53JsZabyf2V{&LYfyZ_<7lM-Y#^ z5&O*b+`HxJqJ1rwJx!nwy~F6dRz5oeL;u;?ll9L}03Q!oSt;^-E}W~B=@@qkLfZJv za>t?z3YzZsy#8TW7$F8{Z~x7$ZotXcxACg9QqFtqNuKjM=%8-rFpR1x~g@@HBeQ~W#&`@`S z!(sA9GN@)>D&#_38ol9YGgSoFr6npo?s`<+@sHr3@$cdbv(Ozma1YoWR5-ntf1HUu5TAs1WqpZPyugnDW)j%=k_E)H)d$?v@;Uvy)~>z>@VZ;Ljx zuWe;in*U<+n|+L;!cKsWJ-E|oXqPEz&>BFX7r`Uu^5!PXYcd_LG7#UnR!M}T#(tj1 z6DLX6kLQVWyIeo;0Y*3$1g#u0V@G)JbkT=MBn+mA^6NMqM$&b)o>}(qfn}JbRxxMg ztBqxH)`JNOC;c=N1bm*OQ;LB#&|9Ir)4CcXum^PG*^(cev*j!a(Awq3zt451llU8E z0Rf%T@cy1_a)EDbfH9PrLS`mIfu}<|sL#R{Uq^tKhvlol8*Ny+MKkP?a97M!ag5v^ zv*fw;YJG-(mVAzB1P@Xx zRYIb^Mjr0F+$MO0oz2Wu{XUgGnty}Boz+C^S9I6Cx(eOO2x!_wa6e5W0!eC{e`F=T zjKE#%@H&AwWlN%uY`bEPaJQxV692v?Tc1C1hyBs9h^1oh_|HNuj3}QZFJXM|_>Jv9 z_jifaFN}k-Ox)e(E5R8;$%Bw%WiM<^1ZjKLb;~hzUuEng((SAxQpxv$f1ft19OzFd zhc0>cddfb_s)*i~cNRog!% zB9<(=8vN))`{6Q~dD-?GWPxH}eyHAIJXd$N#Mk zB9i1W;GBV$a)i>|mF|J~UylQfnLoEk)c>mm@c7weUshCYdnFch%H`|-DU8i$a&O0^MNk|fd7|(06U(Sv_gl>-@^5j% zhAF815)&Lz@B|U&(8HrAmOU7}?}PPf@I1N{BE-9QAWS?ubIhq(059GcTJ4eU+Z->8 zR#O|Q8={HVi%E=M9YzvbeFp0of|eL1nv7qyzR>`j+^7<&I1=*7P8~2%k`sz_`2HTm zJg-`-h)%!LPgu|^QfK-|n*SR_=RdI=-gv8J-;L?)Qa?WY2okV>5X939Qr?t4aAiFh z*3PLXEPao#POHba!nO$N(kd5Eb+>q#?bc;gKUMInH!-X$ocqNW$3TD8J>m+?Pi~=1$T#r7DTdA@~?tms*vMO7r&$E4afD6os8xmNeUNM0f!Rsl`1>We{0ms zhPgpQhEF>1FpqGOk5HyMStG3=czTq`_2yaV3C}V>Ec98;)oPwX&$@QyjSO?2GL5xCe)pgtM9$RsMR`-ea%k+QZ`mrA_omHHo1+< zEFg{2_4-a^gP%z(dq~2uzkX*~^f(vv|Jx>bKuk(&w$k0K20Nvm zrL3tTJ+5^O_{BUd{{vHe(JL3-YA832ue%g0rxMzQHn+|KP%Uh#`f1>;r6Qe%B|XM%kaCVdHWGSzeEKI2fZ+uZ~a$835Zmgn@wqh1=R>m;HW) zo-@d!`p;SpP8a{oQSo)cdqRZjNY6idC=dMxiX;D9@?YefcjgU~J1(y{^0s}xr!Nc7 z7@j_oG8&?e!4+m?N|^JDtc3Aghfp(FMDfXn7Zp3V; zvCaQ8z(cdqZdV~ISZG=*Dn z2grU=|%y*1)Vf}hNyB^Sviv>LaGYB<}vta1tt7GMWfG5}2#C*n&ylbWTcE-)VzM(A8 z9b%Mne{|3U*OxyVeKeVrdQu(G$vGp=ZH2o|tPUHVVHF8jdA;SdyqqtMtqmi5zEx+{ z!*2`)a;hdW={B(j?~=b{qF9(|@UPH}zLo2UeF0+4Q_NLKI-`E z|B1p?>JMIFLvfVkm&Svy5yjkZb!l79r_BuTj$3!e@2Blob?06wNuB>8v4rIXzdCtl zP)X-FPf=BHx}NcXpz{!)!D3tx!`0}HuJ6o@CEoWaEpj&}8!PmGl0rz>pcHYfyRPtD z=S+Olqma6b63a@nnRtqtbKAaumghoyYR7T-MH6imj~F%Sn9j{ zBTzw){dzh7Sl*K|P9}?wI5Ba6?$t7t=v&+mIKtaPgY@nU)~>&*o(35-S(Uc154AM> zslP&~_)mf4#jg;|x=bysd0dwD9+b}>*Y;fuP#wz@8X6(6sT&``m>Ugk|KByCR~y+P zJs$d76oa?7_)DQ9fSI!A?b?G+6hUGEzA(0sgM|C2qQ3)6VEjy@BTr`LDjF9|8ZwsC zfTyE`D@v{NvK8juieBjgx-QNCy1t*U>fNKGauKh)HeZ1-dp7pmD)1k}WkZ(hiDC7_A(p^<1UpHYizF~Vq}Zu3AE(|L$R~#2D5wh>b7WjtX&|59Xw=(6R=Z~waOv&iy z?mCnfN-XTx+itqbGE9O5`AXCx278gBHvN+GBCl3S+(GY#%wPR^wTu@RWBKT51>RE+ z<7Y!>)3aB%e%+b@MS**gR(pB%<=SGTThbZM?{n`&TLQyuvKY_|#oX_N9G?gV6FP?8 z%A1EyNzG*6XH@Yf4f~qY;U23Ow}&vqP#{l#f9&lD5?*I^#1ZTOFaPc@>8i1PEl!fJ6_liY zfOo$ld#8?-7}TJvs|9ti8lWE`ATFKSy>;tV=!N)oS##JWg7xNzGue41@Gs$?fY&w# z=i^?A)opE|V)0pzu3E5ha%L_zN_Pi3GEX;lCxYQpP>hZw?&Ll0N*q zIv7I%#F+%pev|q03fm+Oid16@YdiDwLuB$&M@TKt17L#|lUhpC@VnZaqW|mH(P_?JMwepgC`@ zW_7nh`;U!XaTam=61`K5rYW*xw;k!rN z2q@)1H*sMziSw8ZEt0<2_)@lZ+u-L9Yzc`n35Q&(paE9{>YCGb`V!ZNRRhAi@4+^qOy6VW%N*BILq|Tuxd#TJAWM zVfA`kSTx}1_50O(Z_$(Nm5TG}s{w{7-1^@1Vdl$V|K_1`Mf zx5#EtrLXCI0PaE`0@io$lioW`s~@{fkluL9+aHr3qz51jH9X#u7Hl zD1F@V7j@Kk>E&%{-okLX^ih#{*_Ed`Ef4hYmGb`Tzqa$~#Tf^BJo(c<<^qA%UIv}@ zmJt1I98uSpos_}WWQG{-vIzBEJFnVlkvJV+wicCa6?p)jN(2)t;s5(F+W(HcPS_BqzKI!W2(H zlv!i^*$7|}dC%vJvr*k19a-N4aH9lPIbshXv)IqVf`fyJVSHq3sjK%Se71+O)(VQR z(xhR0h)p4w)_>VAB!Rh|C{xHH!pPF%^?|tuKh9(W+&E@9?=Wr1AO-12#%`aobzJX@ z-4CixJfv1lU`TRn&JV8|^oZ3hjwx=gVXrW(9*BPg=$rf&*7EFh^%U3QWh>%0Y?r_y z!{Q-y+*5B~ak8DOFr#zx!|jRdw0}R<&Dvvls9NF`V#4CxEfp)dsEk&Se!Z=Gr`{2cd%0)hE_Y zbhjE|4HsQ3mT@4l!X7y#fs^ikd#XX z#N8ll-ee=mX5otaS%~k4F5Jp2cG94`;jyETiocYXuv8v{oRIrVm0}q zd~8OJt|ITQ52W)0&cwuJvaB^UW291oLxx#5Z=1H5e#$(oG)u5K z%HWAF5pz=Vl%QrZ;L?A=-L3dQ4}$-u9rR$Jo>#Zy%E;7l_rNKDIIMKMWgi8Xac2`= zRORL}sJ^*9AfGPytt2P+PAPNi`_KHr5zIiuho&JG$NWZRg`FaHUZ%tFIiOzx6R%(H z!90*EBzS3MB>`Ydm=wASoS$DzwzkNZKK+9`K_!PNqZye#{rjba^BB1HmrtTWAm%pA z!is)6Fa-L{t443DZ`MmPFA#@FkZQ3y;f1enq_n||w4P^78PXDcub?kN9nfV^W^h)T zk&i+tvo9Xg^<^sRruXDJy*BtyN~CdBDqK!6VX#{^oM1GQ-)52ln!iNtV%OewvpeU$ zK)PkFkFOF!EcuN=9!Zzwp6SMcT^Q1npuCCjdp9vEF=cns?D983z-@&+CtRRn|5H3l zOB+vfj*!uKg{*8c*2Jd%f)v#bUT*UAT3O0KS~S!iFK$;JUqw6Sulh9C=vM=K1D z@(x`q3$hUYn?kf&BEWK>fC^asqMj}IjrySW^XmQsulwcGbM?gBdArN~eIlT|GVoU6 zjEwD!ujC;-Cn@0ChIrb=(z$}xsZQ&aYFUidH+JwyH{aR3#66ItO#B7n?y2_Wr%@4~ z61JKKy-<9)_ksKn1BB57J3sD^x%BvBT1V9l*)x_-(Isx^Hc2E0Vbjb)rC&JDM$l3jXo`@9&???c* z8A8s_EBmsb*I-N*sPli{QZlN}S%UR8TN_Egx$#0KF+S~AlGF|HZ2IC=!W-E`jUWH} z%Jc1XL5%yJ(R-t9pwaAmINH8s_3R-I zM_697`gZ-NA;iq0I8}6H^3_0CRlbb1XGpBUBCVB@Emlv+qpfrSEaFqn*N$KC<10CY|uulSA z%oYt31fWY3<~E^sZt8t*XiO)$vRtKyi-(K9dA8z$$*hBoP5!i5Z_T0v(0@hxgDp1G zzQye$W}_8Ws>EVN~xMmAJ2;uZFWREYZ4KX9a$+Lde6)IU2J zgiu!kpN^_jFb6)cn*OvY0qj?&NmSJgo2dNb50O+e^Vfb%+saM%|z$h#+Hj)#b1fT> zqem*!Ia&$!^~W*XyLp$)O%4w96S1SW&7~^hKTr#-n;nujj)ZfJDQ;VRyr<+Zb1fH04?~oAy;lsmGDQ*o{w6SQ znLs3AM%q)7<^wuoY_LMXt^#u8Mve)je|UxaFDD^9g40?uS7c06xxuCtKp8#A{MbOi z#8SWB{*_OeyJ(6lGkce)|i@Ck2HCoH+bU$%cmnaEI3 zcJl`ij_f?knlg_ZQdlRLm2D+6pLp7_qVfDk_kS^Cx3Y0)k9Q~2Mvv*JBx&Lbc0^rQ z27VUnXn#)>OjC{c9yMP*@!;40InqH3*tZYZoG*6o}pVXm0SsZWnQ zWbLJ|KuF^?-3WE+t#iSBYG8#azQZSXf66Km;t-{ZLDHDho**0qbQZN24OZJT5`==A zeDrfS8_(v!jwv$r2hUsV-J7bc78W)x-465gkgkgycW1_axsO?Te$_PFx?qFM0iWmn zs-SmA6@omEdpvdocpe2w0C<18e-^Z_!~=`kdxtq}U(6`5Io~HCzne4eB%FNyEAxuX;Nr&R*t`IPwlcd+yRNb!Y^0%I$UeH+-%ja9EMW>VnKnc85RbYh6~riVp2iaD)gXEn|XwTpFS3C*XQG z&!)6k*#%}>Gy-{cIl?@?tS*+1Yx^nkVaDL3#qp-BTFS`OsKnKk-?=pDd0S{l;!4&< zxikizk$*nd-}7)BGJdaq*Hr1a$_A;77vDf&H6|^V?sC{!r|8wJqQZ$O1R4U*OOO_08 z_keRC8JYS+F>6sEsa;jYpTv8C&ZT;Z0G1IL0jqwD7X6gtjscp_gb!Gl40H2iG-EAIPoVHlS;Qu~S$x z>=F-Y6xTFX?IJBs;J#wfi6!`n8812Oj2{M9`u!55B|Un&brVBE@t3hy+a2+WAFep* zcH$Y15Op20p%5AssCO%~y$OD=*kgx1MQD4ox}=9SqaLwWMEQvGx(7Pb;`U?ncDdepT|K{r_LB4v7aw6quW`xSsX4 zwBoTp&x(r`2@T}rfR(7job%Ytrje2%4X>GQyFp(#L85t<7lfP1L^n9yA`Bq}jS=5h z9|b)G9wM)OEDF!0E92)C2Zg_A0o9KP)m&Pf#-0s-+No2eCO#qO)pjw7NGxTiw}O0? z3O9z>+mA6^wDB1~aIvFFRPxu)!2>@$P%uJTiPc>-u&MD(k?1FYqds`#1!8+m^~xL0 zw!PjSZ*G6cE)n{QM8$`WBpsq0q33oRD4#X;j9CJGr}StTce*Cqw}O+aCuc`LJB2#RjU1 zW5U(E%TDIhv1X$1hn*wg$tFTibRk{$;m~A{31*t7`CfeK9Mmu(uA%@;)ZNJHo=tL< zGGtI9iR6+WQ(PLKO=~I>0gFV>f7qyL1~)fj(%0Zae~GJwnh|pXfBzYo>Vw@Pw|>$u z4{OfwoK3u9xZ)zIfWO*+U+_=-1y$K34%?G4e%aQy$bmZKg;J|1`&O=LKE{4Lp8JgEm1#%zV|Q#<%Cruiri#CbrVhkm;#V09TX1KZE|eA z*P_I083IXvL3OIq+8&1%$XzIe2qk-<7PlhNj?RCFk~EwqxON0@ST ze@6dxWz9gy7rak!7;dpcS5nv2j@@6hYn3$oa+-ebIRn&Ba?qAh6gkAOamxWlr13Mf zL+H)t41>?Un8cV2EL6XyeW0RHjN$#hFfw!L@5eEVk>F9fyAIBs;o_4RiR+p&x3@Sf zcx$2Z14sI8b5Ed9q+G(Y!)koc3g2%lPU5!^9oJuV^->B>MsboxJ%zp4HOVR^M?sL! zGSdiPB8<}^mho+}+u@-057fwfnS*yCaoLfs#ssa6j#aFOcqF3_z= z3F(nJBzWc|)1@`o81k&>7v{~}V!a%%z+g?R>Z*4=rXk-%t&As|!B|S+PV`xKzDsS- zOAD8B$PTv6nAYfxF3$w6SVG-`)YhawlMymgtIf#EJoxARplSuIhEZy;1|0`WI5Cq` zw@*QNtHP58GRQYF6xo!F#PS$^mdB;$J5=0@bT|48y>;~(dnynM6O1H3*EZpy<3^&V zIXT7zAIDL8#VdDB7BZw{RETJsA+3$9@C!`Up)7K~AHJu8{>1ur0huGsUvpm{t179g zfAlBX`NICqNuVi-f8cn!BvM^2OUcx|{|_2d_0bfYQg9ob48A$l&8p0fKzhb44+v%E zyAe!zbCV9qBVe4Ij_1k(w+}zgy{T=`88`!@8>Aa5+x+8|>MJf7b)0}@Lz_eRW0J9; zryT4KG@6f{Ow=3dSG^!xnL+{N>0$JEzHOsjE7pcl|i7c~1pQ~F6)Fp=eS?sMCI67>Z6F+os z1+()-4#F8z2a3-6In!m(Iry#9vV7@h8G{*5V_@S|$Sda;{H%9-!!l0qtXdAY34RVh zjmtVyG7hNm`!z5>*rC6Ns*|kX*PBEB$u7iR->mcDoI52*guKe>Qn#RlPV}lXsy_UEU)#nd8n^R0OER7q zKRZ-r741ZR9l{RChSiKA`dLH88$a67Y(|Y4#0PeKEw3}R4xDiEWHWxd4)tZ91fj-g z8NE=px2$JvY-MR%qPixwhrc~#4{syEuajsFD*KRzPC_ix>c_V6iII2n`6({{?7FwR z_y4SiPLS4?u}uU!`(#0Fh5bpLb|nJ?EUJDyiFS>dWm@GWIIZ(}2jfkuY}mZs;{?IX zAaecvQp+H@pDaj6aWGj%#oCc>MAjRW^Pji0=xZIIbR`%R2r+e%0!%gpwU@U#8(9O>YVDgdo7S09#O zbDLt|c8pTDM$~P#w9ZEG$u)r|g-aU&%SNp;3hzG0w|z8A6jeX1Yc%&m4d(+g)keJ$Y!hwXA9@n;5@6 z^g6X3zf^(3#ykg%-7>DNN@BK+eAV9>iQ|&niu@o)awq+I%L~v-+DQ?|(m`l5vd9%@ z)Mq$j+BKYHwP!prP4ua3)&s9-ey<{v2TCchc7&EhN)5J~%mgLSXz>4p%T4yTxxS zAiV_2ZLE_XBdQww#+J;1`QHg6LAI>X4#a(uB!|V=$uH{K3O-Z6f}N| zUHV7s-^hM?{-s?%$iQU5`2F}Y|7?3-{@R4C6RBojy$Q%;S>G2~mVx~@zode&iNoW! zWMO{P&If{C*XRspde*Y5Uc~!nMWb@ZGD39dcFoLoOu=WriRAHV*^zzP$>Q;um=u zzm=m)>cZKH1Gx^r4OCryS)AUd>_H4{ol=<62R4@!tZ>B4(bFb*Jn6@!iDlX>gEOPj z#uzL2(;GqTn0j)BX>3qshp>U|6OPzLIY#&FYa%jw$e_S z*;?i^x*eDGq>%$=viNn?!S5|AYy7%zBu@tem&ta%IAQ8_KKnIqluF6W__>Yo^IX~a z(zaorZa@EZ_{|skVLB#tPBVr5s+nY#`s}N-j?JLn&{js_49K#)b0_u63CHTZIrw#^ zw1F9Ae!=X|+0rtXOGc`~E3X<$wyS=S!!Cm~b~|U*rv3>xi{I-X1-Hf=RVACk@BL3# zEWGUDfRZE`-SYJK#cqP1&snx+29Eq+5~0|_>&&{=6N4UK`UTHI(`6?5b%6N5%;#)I ziprcQ$gT6SlZl=pi~HJ1hysW8)(oYe`K>DpJwwF!^|;o4o5nAF?U-VJo9VTqF-}zN znmwb(QT4%$>gPfSqi4db1N%(Gw4$rN*5j|+43fEwpA@t2x#ALE^UU6Et*eu0;jR_C zc>JD~{T+ii`FdYy^OHIph%iU0{vxX9a?8rmOzU?=ZMM`-DTieR3)m(OjmzKfjr5PP zYy4ujdi(in_2Z#kLf&c&j M07*qoM6N<$g60jPApigX literal 0 HcmV?d00001 From 6e49dc029541aaa896fa63c47647ccd3f066df67 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 19 Oct 2025 19:12:47 +0000 Subject: [PATCH 029/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 1853b54f9..489bfd0fc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Internal +* 🔧 Add sponsor Requestly. PR [#14205](https://github.com/fastapi/fastapi/pull/14205) by [@tiangolo](https://github.com/tiangolo). * 🔧 Configure reminder for `waiting` label in `issue-manager`. PR [#14156](https://github.com/fastapi/fastapi/pull/14156) by [@YuriiMotov](https://github.com/YuriiMotov). ## 0.119.0 From d8c691f7f09989fa51b331b8a7a1ecfe93815a2f Mon Sep 17 00:00:00 2001 From: Sofie Van Landeghem Date: Mon, 20 Oct 2025 13:26:49 +0200 Subject: [PATCH 030/256] =?UTF-8?q?=F0=9F=90=9B=20Fix=20internal=20Pydanti?= =?UTF-8?q?c=20v1=20compatibility=20(warnings)=20for=20Python=203.14=20and?= =?UTF-8?q?=20Pydantic=202.12.1=20(#14186)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- fastapi/_compat/__init__.py | 8 +- fastapi/_compat/main.py | 127 +++++++++++++----- fastapi/_compat/may_v1.py | 123 +++++++++++++++++ fastapi/_compat/shared.py | 10 +- fastapi/_compat/v1.py | 34 +---- fastapi/_compat/v2.py | 4 +- fastapi/dependencies/utils.py | 23 ++-- fastapi/encoders.py | 18 +-- fastapi/temp_pydantic_v1_params.py | 2 +- fastapi/utils.py | 15 ++- tests/test_compat.py | 12 +- ...t_get_model_definitions_formfeed_escape.py | 3 +- ...est_response_model_as_return_annotation.py | 6 +- 13 files changed, 283 insertions(+), 102 deletions(-) create mode 100644 fastapi/_compat/may_v1.py diff --git a/fastapi/_compat/__init__.py b/fastapi/_compat/__init__.py index b2ae5adc7..0aadd68de 100644 --- a/fastapi/_compat/__init__.py +++ b/fastapi/_compat/__init__.py @@ -30,6 +30,10 @@ from .main import serialize_sequence_value as serialize_sequence_value from .main import ( with_info_plain_validator_function as with_info_plain_validator_function, ) +from .may_v1 import CoreSchema as CoreSchema +from .may_v1 import GetJsonSchemaHandler as GetJsonSchemaHandler +from .may_v1 import JsonSchemaValue as JsonSchemaValue +from .may_v1 import _normalize_errors as _normalize_errors from .model_field import ModelField as ModelField from .shared import PYDANTIC_V2 as PYDANTIC_V2 from .shared import PYDANTIC_VERSION_MINOR_TUPLE as PYDANTIC_VERSION_MINOR_TUPLE @@ -44,7 +48,3 @@ from .shared import ( from .shared import lenient_issubclass as lenient_issubclass from .shared import sequence_types as sequence_types from .shared import value_is_sequence as value_is_sequence -from .v1 import CoreSchema as CoreSchema -from .v1 import GetJsonSchemaHandler as GetJsonSchemaHandler -from .v1 import JsonSchemaValue as JsonSchemaValue -from .v1 import _normalize_errors as _normalize_errors diff --git a/fastapi/_compat/main.py b/fastapi/_compat/main.py index 3f758f072..e5275950e 100644 --- a/fastapi/_compat/main.py +++ b/fastapi/_compat/main.py @@ -1,3 +1,4 @@ +import sys from functools import lru_cache from typing import ( Any, @@ -8,7 +9,7 @@ from typing import ( Type, ) -from fastapi._compat import v1 +from fastapi._compat import may_v1 from fastapi._compat.shared import PYDANTIC_V2, lenient_issubclass from fastapi.types import ModelNameMap from pydantic import BaseModel @@ -50,7 +51,9 @@ else: @lru_cache def get_cached_model_fields(model: Type[BaseModel]) -> List[ModelField]: - if lenient_issubclass(model, v1.BaseModel): + if lenient_issubclass(model, may_v1.BaseModel): + from fastapi._compat import v1 + return v1.get_model_fields(model) else: from . import v2 @@ -59,7 +62,7 @@ def get_cached_model_fields(model: Type[BaseModel]) -> List[ModelField]: def _is_undefined(value: object) -> bool: - if isinstance(value, v1.UndefinedType): + if isinstance(value, may_v1.UndefinedType): return True elif PYDANTIC_V2: from . import v2 @@ -69,7 +72,9 @@ def _is_undefined(value: object) -> bool: def _get_model_config(model: BaseModel) -> Any: - if isinstance(model, v1.BaseModel): + if isinstance(model, may_v1.BaseModel): + from fastapi._compat import v1 + return v1._get_model_config(model) elif PYDANTIC_V2: from . import v2 @@ -80,7 +85,9 @@ def _get_model_config(model: BaseModel) -> Any: def _model_dump( model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any ) -> Any: - if isinstance(model, v1.BaseModel): + if isinstance(model, may_v1.BaseModel): + from fastapi._compat import v1 + return v1._model_dump(model, mode=mode, **kwargs) elif PYDANTIC_V2: from . import v2 @@ -89,7 +96,7 @@ def _model_dump( def _is_error_wrapper(exc: Exception) -> bool: - if isinstance(exc, v1.ErrorWrapper): + if isinstance(exc, may_v1.ErrorWrapper): return True elif PYDANTIC_V2: from . import v2 @@ -99,7 +106,9 @@ def _is_error_wrapper(exc: Exception) -> bool: def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: - if isinstance(field_info, v1.FieldInfo): + if isinstance(field_info, may_v1.FieldInfo): + from fastapi._compat import v1 + return v1.copy_field_info(field_info=field_info, annotation=annotation) else: assert PYDANTIC_V2 @@ -111,7 +120,9 @@ def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: def create_body_model( *, fields: Sequence[ModelField], model_name: str ) -> Type[BaseModel]: - if fields and isinstance(fields[0], v1.ModelField): + if fields and isinstance(fields[0], may_v1.ModelField): + from fastapi._compat import v1 + return v1.create_body_model(fields=fields, model_name=model_name) else: assert PYDANTIC_V2 @@ -123,7 +134,9 @@ def create_body_model( def get_annotation_from_field_info( annotation: Any, field_info: FieldInfo, field_name: str ) -> Any: - if isinstance(field_info, v1.FieldInfo): + if isinstance(field_info, may_v1.FieldInfo): + from fastapi._compat import v1 + return v1.get_annotation_from_field_info( annotation=annotation, field_info=field_info, field_name=field_name ) @@ -137,7 +150,9 @@ def get_annotation_from_field_info( def is_bytes_field(field: ModelField) -> bool: - if isinstance(field, v1.ModelField): + if isinstance(field, may_v1.ModelField): + from fastapi._compat import v1 + return v1.is_bytes_field(field) else: assert PYDANTIC_V2 @@ -147,7 +162,9 @@ def is_bytes_field(field: ModelField) -> bool: def is_bytes_sequence_field(field: ModelField) -> bool: - if isinstance(field, v1.ModelField): + if isinstance(field, may_v1.ModelField): + from fastapi._compat import v1 + return v1.is_bytes_sequence_field(field) else: assert PYDANTIC_V2 @@ -157,7 +174,9 @@ def is_bytes_sequence_field(field: ModelField) -> bool: def is_scalar_field(field: ModelField) -> bool: - if isinstance(field, v1.ModelField): + if isinstance(field, may_v1.ModelField): + from fastapi._compat import v1 + return v1.is_scalar_field(field) else: assert PYDANTIC_V2 @@ -167,7 +186,9 @@ def is_scalar_field(field: ModelField) -> bool: def is_scalar_sequence_field(field: ModelField) -> bool: - if isinstance(field, v1.ModelField): + if isinstance(field, may_v1.ModelField): + from fastapi._compat import v1 + return v1.is_scalar_sequence_field(field) else: assert PYDANTIC_V2 @@ -177,7 +198,9 @@ def is_scalar_sequence_field(field: ModelField) -> bool: def is_sequence_field(field: ModelField) -> bool: - if isinstance(field, v1.ModelField): + if isinstance(field, may_v1.ModelField): + from fastapi._compat import v1 + return v1.is_sequence_field(field) else: assert PYDANTIC_V2 @@ -187,7 +210,9 @@ def is_sequence_field(field: ModelField) -> bool: def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: - if isinstance(field, v1.ModelField): + if isinstance(field, may_v1.ModelField): + from fastapi._compat import v1 + return v1.serialize_sequence_value(field=field, value=value) else: assert PYDANTIC_V2 @@ -197,7 +222,9 @@ def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: def _model_rebuild(model: Type[BaseModel]) -> None: - if lenient_issubclass(model, v1.BaseModel): + if lenient_issubclass(model, may_v1.BaseModel): + from fastapi._compat import v1 + v1._model_rebuild(model) elif PYDANTIC_V2: from . import v2 @@ -206,9 +233,18 @@ def _model_rebuild(model: Type[BaseModel]) -> None: def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap: - v1_model_fields = [field for field in fields if isinstance(field, v1.ModelField)] - v1_flat_models = v1.get_flat_models_from_fields(v1_model_fields, known_models=set()) # type: ignore[attr-defined] - all_flat_models = v1_flat_models + v1_model_fields = [ + field for field in fields if isinstance(field, may_v1.ModelField) + ] + if v1_model_fields: + from fastapi._compat import v1 + + v1_flat_models = v1.get_flat_models_from_fields( + v1_model_fields, known_models=set() + ) + all_flat_models = v1_flat_models + else: + all_flat_models = set() if PYDANTIC_V2: from . import v2 @@ -222,6 +258,8 @@ def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap: model_name_map = v2.get_model_name_map(all_flat_models) return model_name_map + from fastapi._compat import v1 + model_name_map = v1.get_model_name_map(all_flat_models) return model_name_map @@ -232,17 +270,35 @@ def get_definitions( model_name_map: ModelNameMap, separate_input_output_schemas: bool = True, ) -> Tuple[ - Dict[Tuple[ModelField, Literal["validation", "serialization"]], v1.JsonSchemaValue], + Dict[ + Tuple[ModelField, Literal["validation", "serialization"]], + may_v1.JsonSchemaValue, + ], Dict[str, Dict[str, Any]], ]: - v1_fields = [field for field in fields if isinstance(field, v1.ModelField)] - v1_field_maps, v1_definitions = v1.get_definitions( - fields=v1_fields, - model_name_map=model_name_map, - separate_input_output_schemas=separate_input_output_schemas, - ) - if not PYDANTIC_V2: - return v1_field_maps, v1_definitions + if sys.version_info < (3, 14): + v1_fields = [field for field in fields if isinstance(field, may_v1.ModelField)] + v1_field_maps, v1_definitions = may_v1.get_definitions( + fields=v1_fields, + model_name_map=model_name_map, + separate_input_output_schemas=separate_input_output_schemas, + ) + if not PYDANTIC_V2: + return v1_field_maps, v1_definitions + else: + from . import v2 + + v2_fields = [field for field in fields if isinstance(field, v2.ModelField)] + v2_field_maps, v2_definitions = v2.get_definitions( + fields=v2_fields, + model_name_map=model_name_map, + separate_input_output_schemas=separate_input_output_schemas, + ) + all_definitions = {**v1_definitions, **v2_definitions} + all_field_maps = {**v1_field_maps, **v2_field_maps} + return all_field_maps, all_definitions + + # Pydantic v1 is not supported since Python 3.14 else: from . import v2 @@ -252,9 +308,7 @@ def get_definitions( model_name_map=model_name_map, separate_input_output_schemas=separate_input_output_schemas, ) - all_definitions = {**v1_definitions, **v2_definitions} - all_field_maps = {**v1_field_maps, **v2_field_maps} - return all_field_maps, all_definitions + return v2_field_maps, v2_definitions def get_schema_from_model_field( @@ -262,11 +316,14 @@ def get_schema_from_model_field( field: ModelField, model_name_map: ModelNameMap, field_mapping: Dict[ - Tuple[ModelField, Literal["validation", "serialization"]], v1.JsonSchemaValue + Tuple[ModelField, Literal["validation", "serialization"]], + may_v1.JsonSchemaValue, ], separate_input_output_schemas: bool = True, ) -> Dict[str, Any]: - if isinstance(field, v1.ModelField): + if isinstance(field, may_v1.ModelField): + from fastapi._compat import v1 + return v1.get_schema_from_model_field( field=field, model_name_map=model_name_map, @@ -286,7 +343,7 @@ def get_schema_from_model_field( def _is_model_field(value: Any) -> bool: - if isinstance(value, v1.ModelField): + if isinstance(value, may_v1.ModelField): return True elif PYDANTIC_V2: from . import v2 @@ -296,7 +353,7 @@ def _is_model_field(value: Any) -> bool: def _is_model_class(value: Any) -> bool: - if lenient_issubclass(value, v1.BaseModel): + if lenient_issubclass(value, may_v1.BaseModel): return True elif PYDANTIC_V2: from . import v2 diff --git a/fastapi/_compat/may_v1.py b/fastapi/_compat/may_v1.py new file mode 100644 index 000000000..beea4d167 --- /dev/null +++ b/fastapi/_compat/may_v1.py @@ -0,0 +1,123 @@ +import sys +from typing import Any, Dict, List, Literal, Sequence, Tuple, Type, Union + +from fastapi.types import ModelNameMap + +if sys.version_info >= (3, 14): + + class AnyUrl: + pass + + class BaseConfig: + pass + + class BaseModel: + pass + + class Color: + pass + + class CoreSchema: + pass + + class ErrorWrapper: + pass + + class FieldInfo: + pass + + class GetJsonSchemaHandler: + pass + + class JsonSchemaValue: + pass + + class ModelField: + pass + + class NameEmail: + pass + + class RequiredParam: + pass + + class SecretBytes: + pass + + class SecretStr: + pass + + class Undefined: + pass + + class UndefinedType: + pass + + class Url: + pass + + from .v2 import ValidationError, create_model + + def get_definitions( + *, + fields: List[ModelField], + model_name_map: ModelNameMap, + separate_input_output_schemas: bool = True, + ) -> Tuple[ + Dict[ + Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + ], + Dict[str, Dict[str, Any]], + ]: + return {}, {} # pragma: no cover + + +else: + from .v1 import AnyUrl as AnyUrl + from .v1 import BaseConfig as BaseConfig + from .v1 import BaseModel as BaseModel + from .v1 import Color as Color + from .v1 import CoreSchema as CoreSchema + from .v1 import ErrorWrapper as ErrorWrapper + from .v1 import FieldInfo as FieldInfo + from .v1 import GetJsonSchemaHandler as GetJsonSchemaHandler + from .v1 import JsonSchemaValue as JsonSchemaValue + from .v1 import ModelField as ModelField + from .v1 import NameEmail as NameEmail + from .v1 import RequiredParam as RequiredParam + from .v1 import SecretBytes as SecretBytes + from .v1 import SecretStr as SecretStr + from .v1 import Undefined as Undefined + from .v1 import UndefinedType as UndefinedType + from .v1 import Url as Url + from .v1 import ValidationError, create_model + from .v1 import get_definitions as get_definitions + + +RequestErrorModel: Type[BaseModel] = create_model("Request") + + +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 _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 diff --git a/fastapi/_compat/shared.py b/fastapi/_compat/shared.py index 495d5c5f7..cabf48228 100644 --- a/fastapi/_compat/shared.py +++ b/fastapi/_compat/shared.py @@ -16,7 +16,7 @@ from typing import ( Union, ) -from fastapi._compat import v1 +from fastapi._compat import may_v1 from fastapi.types import UnionType from pydantic import BaseModel from pydantic.version import VERSION as PYDANTIC_VERSION @@ -98,7 +98,9 @@ def value_is_sequence(value: Any) -> bool: def _annotation_is_complex(annotation: Union[Type[Any], None]) -> bool: return ( - lenient_issubclass(annotation, (BaseModel, v1.BaseModel, Mapping, UploadFile)) + lenient_issubclass( + annotation, (BaseModel, may_v1.BaseModel, Mapping, UploadFile) + ) or _annotation_is_sequence(annotation) or is_dataclass(annotation) ) @@ -195,12 +197,12 @@ def is_uploadfile_sequence_annotation(annotation: Any) -> bool: def annotation_is_pydantic_v1(annotation: Any) -> bool: - if lenient_issubclass(annotation, v1.BaseModel): + if lenient_issubclass(annotation, may_v1.BaseModel): return True origin = get_origin(annotation) if origin is Union or origin is UnionType: for arg in get_args(annotation): - if lenient_issubclass(arg, v1.BaseModel): + if lenient_issubclass(arg, may_v1.BaseModel): return True if field_annotation_is_sequence(annotation): for sub_annotation in get_args(annotation): diff --git a/fastapi/_compat/v1.py b/fastapi/_compat/v1.py index f0ac51634..e17ce8bea 100644 --- a/fastapi/_compat/v1.py +++ b/fastapi/_compat/v1.py @@ -54,13 +54,15 @@ if not PYDANTIC_V2: from pydantic.schema import TypeModelSet as TypeModelSet from pydantic.schema import ( field_schema, - get_flat_models_from_fields, model_process_schema, ) from pydantic.schema import ( get_annotation_from_field_info as get_annotation_from_field_info, ) from pydantic.schema import get_flat_models_from_field as get_flat_models_from_field + from pydantic.schema import ( + get_flat_models_from_fields as get_flat_models_from_fields, + ) from pydantic.schema import get_model_name_map as get_model_name_map from pydantic.types import SecretBytes as SecretBytes from pydantic.types import SecretStr as SecretStr @@ -99,7 +101,6 @@ else: from pydantic.v1.schema import TypeModelSet as TypeModelSet from pydantic.v1.schema import ( field_schema, - get_flat_models_from_fields, model_process_schema, ) from pydantic.v1.schema import ( @@ -108,6 +109,9 @@ else: from pydantic.v1.schema import ( get_flat_models_from_field as get_flat_models_from_field, ) + from pydantic.v1.schema import ( + get_flat_models_from_fields as get_flat_models_from_fields, + ) from pydantic.v1.schema import get_model_name_map as get_model_name_map from pydantic.v1.types import ( # type: ignore[assignment] SecretBytes as SecretBytes, @@ -215,32 +219,6 @@ def is_pv1_scalar_sequence_field(field: ModelField) -> bool: 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 _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 _model_rebuild(model: Type[BaseModel]) -> None: model.update_forward_refs() diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py index 29606b9f3..fb2c691d8 100644 --- a/fastapi/_compat/v2.py +++ b/fastapi/_compat/v2.py @@ -15,7 +15,7 @@ from typing import ( cast, ) -from fastapi._compat import shared, v1 +from fastapi._compat import may_v1, shared from fastapi.openapi.constants import REF_TEMPLATE from fastapi.types import IncEx, ModelNameMap from pydantic import BaseModel, TypeAdapter, create_model @@ -116,7 +116,7 @@ class ModelField: None, ) except ValidationError as exc: - return None, v1._regenerate_error_with_loc( + return None, may_v1._regenerate_error_with_loc( errors=exc.errors(include_url=False), loc_prefix=loc ) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 675ad6faf..aa06dd2a9 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -43,9 +43,9 @@ from fastapi._compat import ( is_uploadfile_or_nonable_uploadfile_annotation, is_uploadfile_sequence_annotation, lenient_issubclass, + may_v1, sequence_types, serialize_sequence_value, - v1, value_is_sequence, ) from fastapi._compat.shared import annotation_is_pydantic_v1 @@ -380,7 +380,7 @@ def analyze_param( fastapi_annotations = [ arg for arg in annotated_args[1:] - if isinstance(arg, (FieldInfo, v1.FieldInfo, params.Depends)) + if isinstance(arg, (FieldInfo, may_v1.FieldInfo, params.Depends)) ] fastapi_specific_annotations = [ arg @@ -397,21 +397,21 @@ def analyze_param( ) ] if fastapi_specific_annotations: - fastapi_annotation: Union[FieldInfo, v1.FieldInfo, params.Depends, None] = ( - fastapi_specific_annotations[-1] - ) + fastapi_annotation: Union[ + FieldInfo, may_v1.FieldInfo, params.Depends, None + ] = fastapi_specific_annotations[-1] else: fastapi_annotation = None # Set default for Annotated FieldInfo - if isinstance(fastapi_annotation, (FieldInfo, v1.FieldInfo)): + if isinstance(fastapi_annotation, (FieldInfo, may_v1.FieldInfo)): # Copy `field_info` because we mutate `field_info.default` below. field_info = copy_field_info( field_info=fastapi_annotation, annotation=use_annotation ) assert field_info.default in { Undefined, - v1.Undefined, - } or field_info.default in {RequiredParam, v1.RequiredParam}, ( + may_v1.Undefined, + } or field_info.default in {RequiredParam, may_v1.RequiredParam}, ( f"`{field_info.__class__.__name__}` default value cannot be set in" f" `Annotated` for {param_name!r}. Set the default value with `=` instead." ) @@ -435,7 +435,7 @@ def analyze_param( ) depends = value # Get FieldInfo from default value - elif isinstance(value, (FieldInfo, v1.FieldInfo)): + elif isinstance(value, (FieldInfo, may_v1.FieldInfo)): assert field_info is None, ( "Cannot specify FastAPI annotations in `Annotated` and default value" f" together for {param_name!r}" @@ -524,7 +524,8 @@ def analyze_param( type_=use_annotation_from_field_info, default=field_info.default, alias=alias, - required=field_info.default in (RequiredParam, v1.RequiredParam, Undefined), + required=field_info.default + in (RequiredParam, may_v1.RequiredParam, Undefined), field_info=field_info, ) if is_path_param: @@ -741,7 +742,7 @@ def _validate_value_with_model_field( if _is_error_wrapper(errors_): # type: ignore[arg-type] return None, [errors_] elif isinstance(errors_, list): - new_errors = v1._regenerate_error_with_loc(errors=errors_, loc_prefix=()) + new_errors = may_v1._regenerate_error_with_loc(errors=errors_, loc_prefix=()) return None, new_errors else: return v_, [] diff --git a/fastapi/encoders.py b/fastapi/encoders.py index 8ff7d58dd..bba9c970e 100644 --- a/fastapi/encoders.py +++ b/fastapi/encoders.py @@ -17,7 +17,7 @@ from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union from uuid import UUID -from fastapi._compat import v1 +from fastapi._compat import may_v1 from fastapi.types import IncEx from pydantic import BaseModel from pydantic.color import Color @@ -59,7 +59,7 @@ def decimal_encoder(dec_value: Decimal) -> Union[int, float]: ENCODERS_BY_TYPE: Dict[Type[Any], Callable[[Any], Any]] = { bytes: lambda o: o.decode(), Color: str, - v1.Color: str, + may_v1.Color: str, datetime.date: isoformat, datetime.datetime: isoformat, datetime.time: isoformat, @@ -76,19 +76,19 @@ ENCODERS_BY_TYPE: Dict[Type[Any], Callable[[Any], Any]] = { IPv6Interface: str, IPv6Network: str, NameEmail: str, - v1.NameEmail: str, + may_v1.NameEmail: str, Path: str, Pattern: lambda o: o.pattern, SecretBytes: str, - v1.SecretBytes: str, + may_v1.SecretBytes: str, SecretStr: str, - v1.SecretStr: str, + may_v1.SecretStr: str, set: list, UUID: str, Url: str, - v1.Url: str, + may_v1.Url: str, AnyUrl: str, - v1.AnyUrl: str, + may_v1.AnyUrl: str, } @@ -220,10 +220,10 @@ def jsonable_encoder( include = set(include) if exclude is not None and not isinstance(exclude, (set, dict)): exclude = set(exclude) - if isinstance(obj, (BaseModel, v1.BaseModel)): + if isinstance(obj, (BaseModel, may_v1.BaseModel)): # TODO: remove when deprecating Pydantic v1 encoders: Dict[Any, Any] = {} - if isinstance(obj, v1.BaseModel): + if isinstance(obj, may_v1.BaseModel): encoders = getattr(obj.__config__, "json_encoders", {}) # type: ignore[attr-defined] if custom_encoder: encoders = {**encoders, **custom_encoder} diff --git a/fastapi/temp_pydantic_v1_params.py b/fastapi/temp_pydantic_v1_params.py index 0535ee727..e41d71230 100644 --- a/fastapi/temp_pydantic_v1_params.py +++ b/fastapi/temp_pydantic_v1_params.py @@ -5,8 +5,8 @@ from fastapi.openapi.models import Example from fastapi.params import ParamTypes from typing_extensions import Annotated, deprecated +from ._compat.may_v1 import FieldInfo, Undefined from ._compat.shared import PYDANTIC_VERSION_MINOR_TUPLE -from ._compat.v1 import FieldInfo, Undefined _Unset: Any = Undefined diff --git a/fastapi/utils.py b/fastapi/utils.py index 3ea9271b1..2e79ee6b1 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -25,7 +25,7 @@ from fastapi._compat import ( Validator, annotation_is_pydantic_v1, lenient_issubclass, - v1, + may_v1, ) from fastapi.datastructures import DefaultPlaceholder, DefaultType from pydantic import BaseModel @@ -87,8 +87,8 @@ def create_model_field( ) -> ModelField: class_validators = class_validators or {} - v1_model_config = v1.BaseConfig - v1_field_info = field_info or v1.FieldInfo() + v1_model_config = may_v1.BaseConfig + v1_field_info = field_info or may_v1.FieldInfo() v1_kwargs = { "name": name, "field_info": v1_field_info, @@ -102,9 +102,11 @@ def create_model_field( if ( annotation_is_pydantic_v1(type_) - or isinstance(field_info, v1.FieldInfo) + or isinstance(field_info, may_v1.FieldInfo) or version == "1" ): + from fastapi._compat import v1 + try: return v1.ModelField(**v1_kwargs) # type: ignore[no-any-return] except RuntimeError: @@ -122,6 +124,8 @@ def create_model_field( raise fastapi.exceptions.FastAPIError(_invalid_args_message) from None # Pydantic v2 is not installed, but it's not a Pydantic v1 ModelField, it could be # a Pydantic v1 type, like a constrained int + from fastapi._compat import v1 + try: return v1.ModelField(**v1_kwargs) # type: ignore[no-any-return] except RuntimeError: @@ -138,6 +142,9 @@ def create_cloned_field( if isinstance(field, v2.ModelField): return field + + from fastapi._compat import v1 + # cloned_types caches already cloned types to support recursive models and improve # performance by avoiding unnecessary cloning if cloned_types is None: diff --git a/tests/test_compat.py b/tests/test_compat.py index f79dbdabc..0184c9a2e 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -7,7 +7,7 @@ from fastapi._compat import ( get_cached_model_fields, is_scalar_field, is_uploadfile_sequence_annotation, - v1, + may_v1, ) from fastapi._compat.shared import is_bytes_sequence_annotation from fastapi.testclient import TestClient @@ -27,7 +27,10 @@ def test_model_field_default_required(): assert field.default is Undefined +@needs_py_lt_314 def test_v1_plain_validator_function(): + from fastapi._compat import v1 + # For coverage def func(v): # pragma: no cover return v @@ -135,6 +138,8 @@ def test_is_uploadfile_sequence_annotation(): @needs_py_lt_314 def test_is_pv1_scalar_field(): + from fastapi._compat import v1 + # For coverage class Model(v1.BaseModel): foo: Union[str, Dict[str, Any]] @@ -143,8 +148,11 @@ def test_is_pv1_scalar_field(): assert not is_scalar_field(fields[0]) +@needs_py_lt_314 def test_get_model_fields_cached(): - class Model(v1.BaseModel): + from fastapi._compat import v1 + + class Model(may_v1.BaseModel): foo: str non_cached_fields = v1.get_model_fields(Model) diff --git a/tests/test_get_model_definitions_formfeed_escape.py b/tests/test_get_model_definitions_formfeed_escape.py index 439e6d448..6601585ef 100644 --- a/tests/test_get_model_definitions_formfeed_escape.py +++ b/tests/test_get_model_definitions_formfeed_escape.py @@ -5,7 +5,6 @@ import fastapi.openapi.utils import pydantic.schema import pytest from fastapi import FastAPI -from fastapi._compat import v1 from pydantic import BaseModel from starlette.testclient import TestClient @@ -165,6 +164,8 @@ def test_model_description_escaped_with_formfeed(sort_reversed: bool): Test `get_model_definitions` with models passed in different order. """ + from fastapi._compat import v1 + all_fields = fastapi.openapi.utils.get_fields_from_routes(app.routes) flat_models = v1.get_flat_models_from_fields(all_fields, known_models=set()) diff --git a/tests/test_response_model_as_return_annotation.py b/tests/test_response_model_as_return_annotation.py index c3c0ed6c4..1745c69b6 100644 --- a/tests/test_response_model_as_return_annotation.py +++ b/tests/test_response_model_as_return_annotation.py @@ -2,12 +2,13 @@ from typing import List, Union import pytest from fastapi import FastAPI -from fastapi._compat import v1 from fastapi.exceptions import FastAPIError, ResponseValidationError from fastapi.responses import JSONResponse, Response from fastapi.testclient import TestClient from pydantic import BaseModel +from tests.utils import needs_pydanticv1 + class BaseUser(BaseModel): name: str @@ -511,7 +512,10 @@ def test_invalid_response_model_field(): # TODO: remove when dropping Pydantic v1 support +@needs_pydanticv1 def test_invalid_response_model_field_pv1(): + from fastapi._compat import v1 + app = FastAPI() class Model(v1.BaseModel): From 43f15d3b43235323059879b4d6f47b2efba0f52f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 20 Oct 2025 11:27:39 +0000 Subject: [PATCH 031/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 489bfd0fc..a07aed05f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Fixes + +* 🐛 Fix internal Pydantic v1 compatibility (warnings) for Python 3.14 and Pydantic 2.12.1. PR [#14186](https://github.com/fastapi/fastapi/pull/14186) by [@svlandeg](https://github.com/svlandeg). + ### Docs * 📝 Replace `starlette.io` by `starlette.dev` and `uvicorn.org` by `uvicorn.dev`. PR [#14176](https://github.com/fastapi/fastapi/pull/14176) by [@Kludex](https://github.com/Kludex). From 864b569cf8453654fc3bc2c64108c0f644e2918c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 20 Oct 2025 13:28:38 +0200 Subject: [PATCH 032/256] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.11?= =?UTF-8?q?9.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index a07aed05f..25fb91eb4 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.119.1 + ### Fixes * 🐛 Fix internal Pydantic v1 compatibility (warnings) for Python 3.14 and Pydantic 2.12.1. PR [#14186](https://github.com/fastapi/fastapi/pull/14186) by [@svlandeg](https://github.com/svlandeg). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 2091f0d1f..a7164d18f 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.119.0" +__version__ = "0.119.1" from starlette import status as status From 847280450a0e2b6d82f4e912978cbc6e46535e9b Mon Sep 17 00:00:00 2001 From: Nils-Hero Lindemann Date: Mon, 20 Oct 2025 16:00:08 +0200 Subject: [PATCH 033/256] =?UTF-8?q?=F0=9F=8C=90=20Sync=20German=20docs=20(?= =?UTF-8?q?#14188)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sync German docs with #14168 Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> --- ...migrate-from-pydantic-v1-to-pydantic-v2.md | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 docs/de/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md diff --git a/docs/de/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md b/docs/de/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md new file mode 100644 index 000000000..7f60492ee --- /dev/null +++ b/docs/de/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md @@ -0,0 +1,133 @@ +# Von Pydantic v1 zu Pydantic v2 migrieren { #migrate-from-pydantic-v1-to-pydantic-v2 } + +Wenn Sie eine ältere FastAPI-App haben, nutzen Sie möglicherweise Pydantic Version 1. + +FastAPI unterstützt seit Version 0.100.0 sowohl Pydantic v1 als auch v2. + +Wenn Sie Pydantic v2 installiert hatten, wurde dieses verwendet. Wenn stattdessen Pydantic v1 installiert war, wurde jenes verwendet. + +Pydantic v1 ist jetzt deprecatet und die Unterstützung dafür wird in den nächsten Versionen von FastAPI entfernt, Sie sollten also zu **Pydantic v2 migrieren**. Auf diese Weise erhalten Sie die neuesten Features, Verbesserungen und Fixes. + +/// warning | Achtung + +Außerdem hat das Pydantic-Team die Unterstützung für Pydantic v1 in den neuesten Python-Versionen eingestellt, beginnend mit **Python 3.14**. + +Wenn Sie die neuesten Features von Python nutzen möchten, müssen Sie sicherstellen, dass Sie Pydantic v2 verwenden. + +/// + +Wenn Sie eine ältere FastAPI-App mit Pydantic v1 haben, zeige ich Ihnen hier, wie Sie sie zu Pydantic v2 migrieren, und die **neuen Features in FastAPI 0.119.0**, die Ihnen bei einer schrittweisen Migration helfen. + +## Offizieller Leitfaden { #official-guide } + +Pydantic hat einen offiziellen Migrationsleitfaden von v1 zu v2. + +Er enthält auch, was sich geändert hat, wie Validierungen nun korrekter und strikter sind, mögliche Stolpersteine, usw. + +Sie können ihn lesen, um besser zu verstehen, was sich geändert hat. + +## Tests { #tests } + +Stellen Sie sicher, dass Sie [Tests](../tutorial/testing.md){.internal-link target=_blank} für Ihre App haben und diese in Continuous Integration (CI) ausführen. + +Auf diese Weise können Sie das Update durchführen und sicherstellen, dass weiterhin alles wie erwartet funktioniert. + +## `bump-pydantic` { #bump-pydantic } + +In vielen Fällen, wenn Sie reguläre Pydantic-Modelle ohne Anpassungen verwenden, können Sie den Großteil des Prozesses der Migration von Pydantic v1 auf Pydantic v2 automatisieren. + +Sie können `bump-pydantic` vom selben Pydantic-Team verwenden. + +Dieses Tool hilft Ihnen, den Großteil des zu ändernden Codes automatisch anzupassen. + +Danach können Sie die Tests ausführen und prüfen, ob alles funktioniert. Falls ja, sind Sie fertig. 😎 + +## Pydantic v1 in v2 { #pydantic-v1-in-v2 } + +Pydantic v2 enthält alles aus Pydantic v1 als Untermodul `pydantic.v1`. + +Das bedeutet, Sie können die neueste Version von Pydantic v2 installieren und die alten Pydantic‑v1‑Komponenten aus diesem Untermodul importieren und verwenden, als hätten Sie das alte Pydantic v1 installiert. + +{* ../../docs_src/pydantic_v1_in_v2/tutorial001_an_py310.py hl[1,4] *} + +### FastAPI-Unterstützung für Pydantic v1 in v2 { #fastapi-support-for-pydantic-v1-in-v2 } + +Seit FastAPI 0.119.0 gibt es außerdem eine teilweise Unterstützung für Pydantic v1 innerhalb von Pydantic v2, um die Migration auf v2 zu erleichtern. + +Sie könnten also Pydantic auf die neueste Version 2 aktualisieren und die Importe so ändern, dass das Untermodul `pydantic.v1` verwendet wird, und in vielen Fällen würde es einfach funktionieren. + +{* ../../docs_src/pydantic_v1_in_v2/tutorial002_an_py310.py hl[2,5,15] *} + +/// warning | Achtung + +Beachten Sie, dass, da das Pydantic‑Team Pydantic v1 in neueren Python‑Versionen nicht mehr unterstützt, beginnend mit Python 3.14, auch die Verwendung von `pydantic.v1` unter Python 3.14 und höher nicht unterstützt wird. + +/// + +### Pydantic v1 und v2 in derselben App { #pydantic-v1-and-v2-on-the-same-app } + +Es wird von Pydantic **nicht unterstützt**, dass ein Pydantic‑v2‑Modell Felder hat, die als Pydantic‑v1‑Modelle definiert sind, und umgekehrt. + +```mermaid +graph TB + subgraph "❌ Nicht unterstützt" + direction TB + subgraph V2["Pydantic-v2-Modell"] + V1Field["Pydantic-v1-Modell"] + end + subgraph V1["Pydantic-v1-Modell"] + V2Field["Pydantic-v2-Modell"] + end + end + + style V2 fill:#f9fff3 + style V1 fill:#fff6f0 + style V1Field fill:#fff6f0 + style V2Field fill:#f9fff3 +``` + +... aber Sie können getrennte Modelle, die Pydantic v1 bzw. v2 nutzen, in derselben App verwenden. + +```mermaid +graph TB + subgraph "✅ Unterstützt" + direction TB + subgraph V2["Pydantic-v2-Modell"] + V2Field["Pydantic-v2-Modell"] + end + subgraph V1["Pydantic-v1-Modell"] + V1Field["Pydantic-v1-Modell"] + end + end + + style V2 fill:#f9fff3 + style V1 fill:#fff6f0 + style V1Field fill:#fff6f0 + style V2Field fill:#f9fff3 +``` + +In einigen Fällen ist es sogar möglich, sowohl Pydantic‑v1‑ als auch Pydantic‑v2‑Modelle in derselben **Pfadoperation** Ihrer FastAPI‑App zu verwenden: + +{* ../../docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py hl[2:3,6,12,21:22] *} + +Im obigen Beispiel ist das Eingabemodell ein Pydantic‑v1‑Modell, und das Ausgabemodell (definiert in `response_model=ItemV2`) ist ein Pydantic‑v2‑Modell. + +### Pydantic v1 Parameter { #pydantic-v1-parameters } + +Wenn Sie einige der FastAPI-spezifischen Tools für Parameter wie `Body`, `Query`, `Form`, usw. zusammen mit Pydantic‑v1‑Modellen verwenden müssen, können Sie die aus `fastapi.temp_pydantic_v1_params` importieren, während Sie die Migration zu Pydantic v2 abschließen: + +{* ../../docs_src/pydantic_v1_in_v2/tutorial004_an_py310.py hl[4,18] *} + +### In Schritten migrieren { #migrate-in-steps } + +/// tip | Tipp + +Probieren Sie zuerst `bump-pydantic` aus. Wenn Ihre Tests erfolgreich sind und das funktioniert, sind Sie mit einem einzigen Befehl fertig. ✨ + +/// + +Wenn `bump-pydantic` für Ihren Anwendungsfall nicht funktioniert, können Sie die Unterstützung für Pydantic‑v1‑ und Pydantic‑v2‑Modelle in derselben App nutzen, um die Migration zu Pydantic v2 schrittweise durchzuführen. + +Sie könnten zuerst Pydantic auf die neueste Version 2 aktualisieren und die Importe so ändern, dass für all Ihre Modelle `pydantic.v1` verwendet wird. + +Anschließend können Sie beginnen, Ihre Modelle gruppenweise von Pydantic v1 auf v2 zu migrieren – in kleinen, schrittweisen Etappen. 🚶 From 046d49b5a9ab1113a1dccf6767ab7def7ec35349 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 20 Oct 2025 14:00:33 +0000 Subject: [PATCH 034/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 25fb91eb4..03f8df30e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Translations + +* 🌐 Sync German docs. PR [#14188](https://github.com/fastapi/fastapi/pull/14188) by [@nilslindemann](https://github.com/nilslindemann). + ## 0.119.1 ### Fixes From da011f212a27652c31de8a7fda2ce8872f845e27 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 21 Oct 2025 09:53:27 +0200 Subject: [PATCH 035/256] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commi?= =?UTF-8?q?t=20autoupdate=20(#14181)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.13.3 → v0.14.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.13.3...v0.14.1) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9c075f68e..34f212019 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.13.3 + rev: v0.14.1 hooks: - id: ruff args: From 9c912d1dd6929795c070cd00f4ee92eefce21559 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 21 Oct 2025 07:53:59 +0000 Subject: [PATCH 036/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 03f8df30e..fa16f29a4 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -11,6 +11,10 @@ hide: * 🌐 Sync German docs. PR [#14188](https://github.com/fastapi/fastapi/pull/14188) by [@nilslindemann](https://github.com/nilslindemann). +### Internal + +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#14181](https://github.com/fastapi/fastapi/pull/14181) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). + ## 0.119.1 ### Fixes From a578ea1fd3f0b4918a4a530b85955f9b99db5a3d Mon Sep 17 00:00:00 2001 From: Nils-Hero Lindemann Date: Tue, 21 Oct 2025 22:32:28 +0200 Subject: [PATCH 037/256] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20Update=20German?= =?UTF-8?q?=20LLM=20prompt=20and=20test=20file=20(#14189)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Minor fixes in German LLM prompt and test file Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> --- docs/de/docs/_llm-test.md | 6 +++--- docs/de/llm-prompt.md | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/de/docs/_llm-test.md b/docs/de/docs/_llm-test.md index 4a5e5392c..72846ef06 100644 --- a/docs/de/docs/_llm-test.md +++ b/docs/de/docs/_llm-test.md @@ -47,7 +47,7 @@ Das LLM wird dies wahrscheinlich falsch übersetzen. Interessant ist nur, ob es //// tab | Info -Der Prompt-Designer kann entscheiden, ob neutrale Anführungszeichen in typografische Anführungszeichen umgewandelt werden sollen. Es ist in Ordnung, sie unverändert zu lassen. +Der Promptdesigner kann entscheiden, ob neutrale Anführungszeichen in typografische Anführungszeichen umgewandelt werden sollen. Es ist in Ordnung, sie unverändert zu lassen. Siehe zum Beispiel den Abschnitt `### Quotes` in `docs/de/llm-prompt.md`. @@ -459,7 +459,7 @@ Für einige sprachspezifische Anweisungen, siehe z. B. den Abschnitt `### Headin * der Commit * der Contextmanager * die Coroutine -* die Datenbank-Session +* die Datenbanksession * die Festplatte * die Domain * die Engine @@ -496,7 +496,7 @@ Für einige sprachspezifische Anweisungen, siehe z. B. den Abschnitt `### Headin //// tab | Info -Dies ist eine nicht vollständige und nicht normative Liste von (meist) technischen Begriffen, die in der Dokumentation vorkommen. Sie kann dem Prompt-Designer helfen herauszufinden, bei welchen Begriffen das LLM Unterstützung braucht. Zum Beispiel, wenn es eine gute Übersetzung immer wieder auf eine suboptimale Übersetzung zurücksetzt. Oder wenn es Probleme hat, einen Begriff in Ihrer Sprache zu konjugieren/deklinieren. +Dies ist eine nicht vollständige und nicht normative Liste von (meist) technischen Begriffen, die in der Dokumentation vorkommen. Sie kann dem Promptdesigner helfen herauszufinden, bei welchen Begriffen das LLM Unterstützung braucht. Zum Beispiel, wenn es eine gute Übersetzung immer wieder auf eine suboptimale Übersetzung zurücksetzt. Oder wenn es Probleme hat, einen Begriff in Ihrer Sprache zu konjugieren/deklinieren. Siehe z. B. den Abschnitt `### List of English terms and their preferred German translations` in `docs/de/llm-prompt.md`. diff --git a/docs/de/llm-prompt.md b/docs/de/llm-prompt.md index 23c111d2d..df202d2ff 100644 --- a/docs/de/llm-prompt.md +++ b/docs/de/llm-prompt.md @@ -185,7 +185,7 @@ Example: # FastAPI in Containern - Docker { #fastapi-in-containers-docker } »»» -3.1) Do not apply rule 3 when there is no space before or no space after the dash. +3.1) Do not apply rule 3 when there is no space before or no space after the hyphen. Example: @@ -195,13 +195,13 @@ Example: ## Type hints and annotations { #type-hints-and-annotations } »»» - Translate with (German) – use a short dash: + Translate with (German) – notice the hyphen: ««« ## Typhinweise und -annotationen { #type-hints-and-annotations } »»» - Do NOT translate with (German): + Do NOT translate with (German) – notice the dash: ««« ## Typhinweise und –annotationen { #type-hints-and-annotations } @@ -222,7 +222,7 @@ Ich versuche nicht, alles einzudeutschen. Das bezieht sich besonders auf Begriff ### List of English terms and their preferred German translations -Below is a list of English terms and their preferred German translations, separated by a colon («:»). Use these translations, do not use your own. If an existing translation does not use these terms, update it to use them. A term or a translation may be followed by an explanation in brackets, which explains when to translate the term this way. If a translation is preceded by «NOT», then that means: do NOT use this translation for this term. English nouns, starting with the word «the», have the German genus – «der», «die», «das» – prepended to their German translation, to help you to grammatically decline them in the translation. They are given in singular case, unless they have «(plural)» attached, which means they are given in plural case. Verbs are given in the full infinitive – starting with the word «to». +Below is a list of English terms and their preferred German translations, separated by a colon («:»). Use these translations, do not use your own. If an existing translation does not use these terms, update it to use them. In the below list, a term or a translation may be followed by an explanation in brackets, which explains when to translate the term this way. If a translation is preceded by «NOT», then that means: do NOT use this translation for this term. English nouns, starting with the word «the», have the German genus – «der», «die», «das» – prepended to their German translation, to help you to grammatically decline them in the translation. They are given in singular case, unless they have «(plural)» attached, which means they are given in plural case. Verbs are given in the full infinitive – starting with the word «to». * «/// check»: «/// check | Testen» * «/// danger»: «/// danger | Gefahr» From cb7018d782c88b189f11d6141c8231ffb9b403b0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 21 Oct 2025 20:32:57 +0000 Subject: [PATCH 038/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index fa16f29a4..d978536cc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Internal +* 🛠️ Update German LLM prompt and test file. PR [#14189](https://github.com/fastapi/fastapi/pull/14189) by [@nilslindemann](https://github.com/nilslindemann). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#14181](https://github.com/fastapi/fastapi/pull/14181) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). ## 0.119.1 From d390f2e41fb508de416071d481937bc7be486589 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 23 Oct 2025 22:31:35 +0200 Subject: [PATCH 039/256] =?UTF-8?q?=E2=9E=95=20Migrate=20internal=20refere?= =?UTF-8?q?nce=20documentation=20from=20`typing=5Fextensions.Doc`=20to=20`?= =?UTF-8?q?annotated=5Fdoc.Doc`=20(#14222)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/applications.py | 3 ++- fastapi/background.py | 3 ++- fastapi/datastructures.py | 3 ++- fastapi/encoders.py | 3 ++- fastapi/exceptions.py | 3 ++- fastapi/openapi/docs.py | 3 ++- fastapi/param_functions.py | 3 ++- fastapi/routing.py | 3 ++- fastapi/security/api_key.py | 3 ++- fastapi/security/http.py | 3 ++- fastapi/security/oauth2.py | 3 ++- fastapi/security/open_id_connect_url.py | 3 ++- pyproject.toml | 1 + requirements-docs.txt | 2 +- 14 files changed, 26 insertions(+), 13 deletions(-) diff --git a/fastapi/applications.py b/fastapi/applications.py index 6db4b4e83..0a47699ae 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -13,6 +13,7 @@ from typing import ( Union, ) +from annotated_doc import Doc from fastapi import routing from fastapi.datastructures import Default, DefaultPlaceholder from fastapi.exception_handlers import ( @@ -43,7 +44,7 @@ from starlette.requests import Request from starlette.responses import HTMLResponse, JSONResponse, Response from starlette.routing import BaseRoute from starlette.types import ASGIApp, ExceptionHandler, Lifespan, Receive, Scope, Send -from typing_extensions import Annotated, Doc, deprecated +from typing_extensions import Annotated, deprecated AppType = TypeVar("AppType", bound="FastAPI") diff --git a/fastapi/background.py b/fastapi/background.py index 203578a41..6d4a30d44 100644 --- a/fastapi/background.py +++ b/fastapi/background.py @@ -1,7 +1,8 @@ from typing import Any, Callable +from annotated_doc import Doc from starlette.background import BackgroundTasks as StarletteBackgroundTasks -from typing_extensions import Annotated, Doc, ParamSpec +from typing_extensions import Annotated, ParamSpec P = ParamSpec("P") diff --git a/fastapi/datastructures.py b/fastapi/datastructures.py index 34185b96a..8ad9aa11a 100644 --- a/fastapi/datastructures.py +++ b/fastapi/datastructures.py @@ -10,6 +10,7 @@ from typing import ( cast, ) +from annotated_doc import Doc from fastapi._compat import ( CoreSchema, GetJsonSchemaHandler, @@ -22,7 +23,7 @@ from starlette.datastructures import Headers as Headers # noqa: F401 from starlette.datastructures import QueryParams as QueryParams # noqa: F401 from starlette.datastructures import State as State # noqa: F401 from starlette.datastructures import UploadFile as StarletteUploadFile -from typing_extensions import Annotated, Doc +from typing_extensions import Annotated class UploadFile(StarletteUploadFile): diff --git a/fastapi/encoders.py b/fastapi/encoders.py index bba9c970e..6fc6228e1 100644 --- a/fastapi/encoders.py +++ b/fastapi/encoders.py @@ -17,13 +17,14 @@ from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union from uuid import UUID +from annotated_doc import Doc from fastapi._compat import may_v1 from fastapi.types import IncEx from pydantic import BaseModel from pydantic.color import Color from pydantic.networks import AnyUrl, NameEmail from pydantic.types import SecretBytes, SecretStr -from typing_extensions import Annotated, Doc +from typing_extensions import Annotated from ._compat import Url, _is_undefined, _model_dump diff --git a/fastapi/exceptions.py b/fastapi/exceptions.py index 44d4ada86..bb775fcbf 100644 --- a/fastapi/exceptions.py +++ b/fastapi/exceptions.py @@ -1,9 +1,10 @@ from typing import Any, Dict, Optional, Sequence, Type, Union +from annotated_doc import Doc from pydantic import BaseModel, create_model from starlette.exceptions import HTTPException as StarletteHTTPException from starlette.exceptions import WebSocketException as StarletteWebSocketException -from typing_extensions import Annotated, Doc +from typing_extensions import Annotated class HTTPException(StarletteHTTPException): diff --git a/fastapi/openapi/docs.py b/fastapi/openapi/docs.py index f181b43c1..74b23a370 100644 --- a/fastapi/openapi/docs.py +++ b/fastapi/openapi/docs.py @@ -1,9 +1,10 @@ import json from typing import Any, Dict, Optional +from annotated_doc import Doc from fastapi.encoders import jsonable_encoder from starlette.responses import HTMLResponse -from typing_extensions import Annotated, Doc +from typing_extensions import Annotated swagger_ui_default_parameters: Annotated[ Dict[str, Any], diff --git a/fastapi/param_functions.py b/fastapi/param_functions.py index b3621626c..f88937e24 100644 --- a/fastapi/param_functions.py +++ b/fastapi/param_functions.py @@ -1,9 +1,10 @@ from typing import Any, Callable, Dict, List, Optional, Sequence, Union +from annotated_doc import Doc from fastapi import params from fastapi._compat import Undefined from fastapi.openapi.models import Example -from typing_extensions import Annotated, Doc, deprecated +from typing_extensions import Annotated, deprecated _Unset: Any = Undefined diff --git a/fastapi/routing.py b/fastapi/routing.py index fe25d7dec..0b59d250a 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -24,6 +24,7 @@ from typing import ( Union, ) +from annotated_doc import Doc from fastapi import params, temp_pydantic_v1_params from fastapi._compat import ( ModelField, @@ -76,7 +77,7 @@ from starlette.routing import ( from starlette.routing import Mount as Mount # noqa from starlette.types import AppType, ASGIApp, Lifespan, Receive, Scope, Send from starlette.websockets import WebSocket -from typing_extensions import Annotated, Doc, deprecated +from typing_extensions import Annotated, deprecated if sys.version_info >= (3, 13): # pragma: no cover from inspect import iscoroutinefunction diff --git a/fastapi/security/api_key.py b/fastapi/security/api_key.py index 6d6dd01d9..496c815a7 100644 --- a/fastapi/security/api_key.py +++ b/fastapi/security/api_key.py @@ -1,11 +1,12 @@ from typing import Optional +from annotated_doc import Doc from fastapi.openapi.models import APIKey, APIKeyIn from fastapi.security.base import SecurityBase from starlette.exceptions import HTTPException from starlette.requests import Request from starlette.status import HTTP_403_FORBIDDEN -from typing_extensions import Annotated, Doc +from typing_extensions import Annotated class APIKeyBase(SecurityBase): diff --git a/fastapi/security/http.py b/fastapi/security/http.py index 9ab2df3c9..3a5985650 100644 --- a/fastapi/security/http.py +++ b/fastapi/security/http.py @@ -2,6 +2,7 @@ import binascii from base64 import b64decode from typing import Optional +from annotated_doc import Doc from fastapi.exceptions import HTTPException from fastapi.openapi.models import HTTPBase as HTTPBaseModel from fastapi.openapi.models import HTTPBearer as HTTPBearerModel @@ -10,7 +11,7 @@ from fastapi.security.utils import get_authorization_scheme_param from pydantic import BaseModel from starlette.requests import Request from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN -from typing_extensions import Annotated, Doc +from typing_extensions import Annotated class HTTPBasicCredentials(BaseModel): diff --git a/fastapi/security/oauth2.py b/fastapi/security/oauth2.py index fdedbc2da..f8d97d762 100644 --- a/fastapi/security/oauth2.py +++ b/fastapi/security/oauth2.py @@ -1,5 +1,6 @@ from typing import Any, Dict, List, Optional, Union, cast +from annotated_doc import Doc from fastapi.exceptions import HTTPException from fastapi.openapi.models import OAuth2 as OAuth2Model from fastapi.openapi.models import OAuthFlows as OAuthFlowsModel @@ -10,7 +11,7 @@ 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, Doc +from typing_extensions import Annotated class OAuth2PasswordRequestForm: diff --git a/fastapi/security/open_id_connect_url.py b/fastapi/security/open_id_connect_url.py index c8cceb911..5e99798e6 100644 --- a/fastapi/security/open_id_connect_url.py +++ b/fastapi/security/open_id_connect_url.py @@ -1,11 +1,12 @@ from typing import Optional +from annotated_doc import Doc from fastapi.openapi.models import OpenIdConnect as OpenIdConnectModel from fastapi.security.base import SecurityBase from starlette.exceptions import HTTPException from starlette.requests import Request from starlette.status import HTTP_403_FORBIDDEN -from typing_extensions import Annotated, Doc +from typing_extensions import Annotated class OpenIdConnect(SecurityBase): diff --git a/pyproject.toml b/pyproject.toml index cac8059f4..875e4bed1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,7 @@ dependencies = [ "starlette>=0.40.0,<0.49.0", "pydantic>=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0", "typing-extensions>=4.8.0", + "annotated-doc>=0.0.2", ] [project.urls] diff --git a/requirements-docs.txt b/requirements-docs.txt index 0013f9f79..6baf19b50 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -12,7 +12,7 @@ pillow==11.3.0 # For image processing by Material for MkDocs cairosvg==2.8.2 mkdocstrings[python]==0.26.1 -griffe-typingdoc==0.2.9 +griffe-typingdoc==0.3.0 # For griffe, it formats with black black==25.1.0 mkdocs-macros-plugin==1.4.0 From 09f40968cb03e878aa25bc83340f891b09d1e418 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 23 Oct 2025 20:32:06 +0000 Subject: [PATCH 040/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index d978536cc..f0daafc86 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Internal +* ➕ Migrate internal reference documentation from `typing_extensions.Doc` to `annotated_doc.Doc`. PR [#14222](https://github.com/fastapi/fastapi/pull/14222) by [@tiangolo](https://github.com/tiangolo). * 🛠️ Update German LLM prompt and test file. PR [#14189](https://github.com/fastapi/fastapi/pull/14189) by [@nilslindemann](https://github.com/nilslindemann). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#14181](https://github.com/fastapi/fastapi/pull/14181) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From 1c6ee57bbfeebfa21a393b7939c85d9d4d29f0b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 23 Oct 2025 22:54:19 +0200 Subject: [PATCH 041/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f0daafc86..14e587935 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,14 @@ hide: ## Latest Changes +There are no major nor breaking changes in this release. ☕️ + +The internal reference documentation now uses `annotated_doc.Doc` instead of `typing_extensions.Doc`, this adds a new (very small) dependency on [`annotated-doc`](https://github.com/fastapi/annotated-doc), a package made just to provide that `Doc` documentation utility class. + +I would expect `typing_extensions.Doc` to be deprecated and then removed at some point from `typing_extensions`, for that reason there's the new `annotated-doc` micro-package. If you are curious about this, you can read more in the repo for [`annotated-doc`](https://github.com/fastapi/annotated-doc). + +This new version `0.120.0` only contains that transition to the new home package for that utility class `Doc`. + ### Translations * 🌐 Sync German docs. PR [#14188](https://github.com/fastapi/fastapi/pull/14188) by [@nilslindemann](https://github.com/nilslindemann). From cd40c5b40ffd8ba0c6a6a6c96bbf34ec1cf9c525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 23 Oct 2025 22:54:45 +0200 Subject: [PATCH 042/256] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.12?= =?UTF-8?q?0.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 14e587935..4925bbfe4 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.120.0 + There are no major nor breaking changes in this release. ☕️ The internal reference documentation now uses `annotated_doc.Doc` instead of `typing_extensions.Doc`, this adds a new (very small) dependency on [`annotated-doc`](https://github.com/fastapi/annotated-doc), a package made just to provide that `Doc` documentation utility class. diff --git a/fastapi/__init__.py b/fastapi/__init__.py index a7164d18f..46198eada 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.119.1" +__version__ = "0.120.0" from starlette import status as status From 96dd32718b36155e221e96a3025f71d654d5031a Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:45:14 +0100 Subject: [PATCH 043/256] =?UTF-8?q?=F0=9F=94=A7=20Add=20`license`=20and=20?= =?UTF-8?q?`license-files`=20to=20`pyproject.toml`,=20remove=20`License`?= =?UTF-8?q?=20from=20`classifiers`=20(#14230)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 875e4bed1..2087d7d04 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,8 @@ name = "fastapi" dynamic = ["version"] description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" readme = "README.md" +license = "MIT" +license-files = ["LICENSE"] requires-python = ">=3.8" authors = [ { name = "Sebastián Ramírez", email = "tiangolo@gmail.com" }, @@ -31,7 +33,6 @@ classifiers = [ "Framework :: Pydantic :: 1", "Framework :: Pydantic :: 2", "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", From 3ea6a4a0b14583c690b31cbd6951d561f1b6da5c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 27 Oct 2025 14:47:51 +0000 Subject: [PATCH 044/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 4925bbfe4..35153706e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Internal + +* 🔧 Add `license` and `license-files` to `pyproject.toml`, remove `License` from `classifiers`. PR [#14230](https://github.com/fastapi/fastapi/pull/14230) by [@YuriiMotov](https://github.com/YuriiMotov). + ## 0.120.0 There are no major nor breaking changes in this release. ☕️ From 436932aef587e8120439f545ad25220114ea15ab Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Mon, 27 Oct 2025 18:49:54 +0100 Subject: [PATCH 045/256] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Bump=20Starlette?= =?UTF-8?q?=20to=20<`0.50.0`=20(#14234)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2087d7d04..7d2be0074 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ classifiers = [ "Topic :: Internet :: WWW/HTTP", ] dependencies = [ - "starlette>=0.40.0,<0.49.0", + "starlette>=0.40.0,<0.50.0", "pydantic>=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0", "typing-extensions>=4.8.0", "annotated-doc>=0.0.2", From 4b0301b2803ff98efa638de90ebac05a76a85e20 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 27 Oct 2025 17:50:15 +0000 Subject: [PATCH 046/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 35153706e..8f39a3c31 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Upgrades + +* ⬆️ Bump Starlette to <`0.50.0`. PR [#14234](https://github.com/fastapi/fastapi/pull/14234) by [@YuriiMotov](https://github.com/YuriiMotov). + ### Internal * 🔧 Add `license` and `license-files` to `pyproject.toml`, remove `License` from `classifiers`. PR [#14230](https://github.com/fastapi/fastapi/pull/14230) by [@YuriiMotov](https://github.com/YuriiMotov). From 78c94c3f565b9b992d1eecafaeb0991e2b177d7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 27 Oct 2025 18:51:46 +0100 Subject: [PATCH 047/256] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.12?= =?UTF-8?q?0.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 8f39a3c31..dd3f002f2 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.120.1 + ### Upgrades * ⬆️ Bump Starlette to <`0.50.0`. PR [#14234](https://github.com/fastapi/fastapi/pull/14234) by [@YuriiMotov](https://github.com/YuriiMotov). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 46198eada..787c52dda 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.120.0" +__version__ = "0.120.1" from starlette import status as status From a0ef245067b545a7e9a47064074ebddd24b24a3b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 08:48:28 +0100 Subject: [PATCH 048/256] =?UTF-8?q?=E2=AC=86=20Bump=20actions/upload-artif?= =?UTF-8?q?act=20from=204=20to=205=20(#14235)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-docs.yml | 2 +- .github/workflows/test.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index b3e53b91c..f78b6730e 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -118,7 +118,7 @@ jobs: path: docs/${{ matrix.lang }}/.cache - name: Build Docs run: python ./scripts/docs.py build-lang ${{ matrix.lang }} - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v5 with: name: docs-site-${{ matrix.lang }} path: ./site/** diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cbf1a8567..83ba1f1c4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -97,7 +97,7 @@ jobs: COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }} CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }} - name: Store coverage files - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: coverage-${{ matrix.python-version }}-${{ matrix.pydantic-version }} path: coverage @@ -136,7 +136,7 @@ jobs: - run: coverage report - run: coverage html --title "Coverage for ${{ github.sha }}" - name: Store coverage HTML - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: coverage-html path: htmlcov From ccf50ca477b2282da0dade4adce6c844b4f2afc7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 08:48:36 +0100 Subject: [PATCH 049/256] =?UTF-8?q?=E2=AC=86=20Bump=20actions/download-art?= =?UTF-8?q?ifact=20from=205=20to=206=20(#14236)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 5 to 6. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/deploy-docs.yml | 2 +- .github/workflows/smokeshow.yml | 2 +- .github/workflows/test.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 0eb26cc4d..aa4fd6b65 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -49,7 +49,7 @@ jobs: run: | rm -rf ./site mkdir ./site - - uses: actions/download-artifact@v5 + - uses: actions/download-artifact@v6 with: path: ./site/ pattern: docs-site-* diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index e42d79723..eed5fbec0 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -34,7 +34,7 @@ jobs: requirements**.txt pyproject.toml - run: uv pip install -r requirements-github-actions.txt - - uses: actions/download-artifact@v5 + - uses: actions/download-artifact@v6 with: name: coverage-html path: htmlcov diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 83ba1f1c4..9c3e2218b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -126,7 +126,7 @@ jobs: - name: Install Dependencies run: uv pip install -r requirements-tests.txt - name: Get coverage files - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v6 with: pattern: coverage-* path: coverage From b618e0f9d426f157512923a2c395772c2bffb9e3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 08:48:46 +0100 Subject: [PATCH 050/256] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commi?= =?UTF-8?q?t=20autoupdate=20(#14237)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.14.1 → v0.14.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.1...v0.14.2) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 34f212019..25dcd7b88 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.1 + rev: v0.14.2 hooks: - id: ruff args: From 448ea5ec82d71d36469b1766c3740872f3b6f8ad Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 28 Oct 2025 07:48:51 +0000 Subject: [PATCH 051/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index dd3f002f2..318f4d471 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Internal + +* ⬆ Bump actions/upload-artifact from 4 to 5. PR [#14235](https://github.com/fastapi/fastapi/pull/14235) by [@dependabot[bot]](https://github.com/apps/dependabot). + ## 0.120.1 ### Upgrades From db7feb5a3e19f6db44a81f08ed122935651fcc7f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 28 Oct 2025 07:50:26 +0000 Subject: [PATCH 052/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 318f4d471..21f7633e1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#14237](https://github.com/fastapi/fastapi/pull/14237) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump actions/upload-artifact from 4 to 5. PR [#14235](https://github.com/fastapi/fastapi/pull/14235) by [@dependabot[bot]](https://github.com/apps/dependabot). ## 0.120.1 From 7132a690461c2367f22556eb87c2b5eb7d9cfb45 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 28 Oct 2025 07:50:28 +0000 Subject: [PATCH 053/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 21f7633e1..fb4c26701 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* ⬆ Bump actions/download-artifact from 5 to 6. PR [#14236](https://github.com/fastapi/fastapi/pull/14236) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#14237](https://github.com/fastapi/fastapi/pull/14237) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump actions/upload-artifact from 4 to 5. PR [#14235](https://github.com/fastapi/fastapi/pull/14235) by [@dependabot[bot]](https://github.com/apps/dependabot). From 6a657f360db5a666c75ba143e0d923ad05789b1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 29 Oct 2025 10:09:30 -0300 Subject: [PATCH 054/256] =?UTF-8?q?=F0=9F=90=9B=20Fix=20separation=20of=20?= =?UTF-8?q?schemas=20with=20nested=20models=20introduced=20in=200.119.0=20?= =?UTF-8?q?(#14246)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/_compat/v2.py | 28 ++- tests/test_no_schema_split.py | 203 ++++++++++++++++++ .../test_multifile.py | 15 +- 3 files changed, 229 insertions(+), 17 deletions(-) create mode 100644 tests/test_no_schema_split.py diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py index fb2c691d8..6a87b9ae9 100644 --- a/fastapi/_compat/v2.py +++ b/fastapi/_compat/v2.py @@ -207,11 +207,31 @@ def get_definitions( override_mode: Union[Literal["validation"], None] = ( None if separate_input_output_schemas else "validation" ) - flat_models = get_flat_models_from_fields(fields, known_models=set()) - flat_model_fields = [ - ModelField(field_info=FieldInfo(annotation=model), name=model.__name__) - for model in flat_models + validation_fields = [field for field in fields if field.mode == "validation"] + serialization_fields = [field for field in fields if field.mode == "serialization"] + flat_validation_models = get_flat_models_from_fields( + validation_fields, known_models=set() + ) + flat_serialization_models = get_flat_models_from_fields( + serialization_fields, known_models=set() + ) + flat_validation_model_fields = [ + ModelField( + field_info=FieldInfo(annotation=model), + name=model.__name__, + mode="validation", + ) + for model in flat_validation_models ] + flat_serialization_model_fields = [ + ModelField( + field_info=FieldInfo(annotation=model), + name=model.__name__, + mode="serialization", + ) + for model in flat_serialization_models + ] + flat_model_fields = flat_validation_model_fields + flat_serialization_model_fields input_types = {f.type_ for f in fields} unique_flat_model_fields = { f for f in flat_model_fields if f.type_ not in input_types diff --git a/tests/test_no_schema_split.py b/tests/test_no_schema_split.py new file mode 100644 index 000000000..b0b5958c1 --- /dev/null +++ b/tests/test_no_schema_split.py @@ -0,0 +1,203 @@ +# Test with parts from, and to verify the report in: +# https://github.com/fastapi/fastapi/discussions/14177 +# Made an issue in: +# https://github.com/fastapi/fastapi/issues/14247 +from enum import Enum +from typing import List + +from fastapi import FastAPI +from fastapi.testclient import TestClient +from inline_snapshot import snapshot +from pydantic import BaseModel, Field + +from tests.utils import pydantic_snapshot + + +class MessageEventType(str, Enum): + alpha = "alpha" + beta = "beta" + + +class MessageEvent(BaseModel): + event_type: MessageEventType = Field(default=MessageEventType.alpha) + output: str + + +class MessageOutput(BaseModel): + body: str = "" + events: List[MessageEvent] = [] + + +class Message(BaseModel): + input: str + output: MessageOutput + + +app = FastAPI(title="Minimal FastAPI App", version="1.0.0") + + +@app.post("/messages", response_model=Message) +async def create_message(input_message: str) -> Message: + return Message( + input=input_message, + output=MessageOutput(body=f"Processed: {input_message}"), + ) + + +client = TestClient(app) + + +def test_create_message(): + response = client.post("/messages", params={"input_message": "Hello"}) + assert response.status_code == 200, response.text + assert response.json() == { + "input": "Hello", + "output": {"body": "Processed: Hello", "events": []}, + } + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "Minimal FastAPI App", "version": "1.0.0"}, + "paths": { + "/messages": { + "post": { + "summary": "Create Message", + "operationId": "create_message_messages_post", + "parameters": [ + { + "name": "input_message", + "in": "query", + "required": True, + "schema": {"type": "string", "title": "Input Message"}, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Message" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Message": { + "properties": { + "input": {"type": "string", "title": "Input"}, + "output": {"$ref": "#/components/schemas/MessageOutput"}, + }, + "type": "object", + "required": ["input", "output"], + "title": "Message", + }, + "MessageEvent": { + "properties": { + "event_type": pydantic_snapshot( + v2=snapshot( + { + "$ref": "#/components/schemas/MessageEventType", + "default": "alpha", + } + ), + v1=snapshot( + { + "allOf": [ + { + "$ref": "#/components/schemas/MessageEventType" + } + ], + "default": "alpha", + } + ), + ), + "output": {"type": "string", "title": "Output"}, + }, + "type": "object", + "required": ["output"], + "title": "MessageEvent", + }, + "MessageEventType": pydantic_snapshot( + v2=snapshot( + { + "type": "string", + "enum": ["alpha", "beta"], + "title": "MessageEventType", + } + ), + v1=snapshot( + { + "type": "string", + "enum": ["alpha", "beta"], + "title": "MessageEventType", + "description": "An enumeration.", + } + ), + ), + "MessageOutput": { + "properties": { + "body": {"type": "string", "title": "Body", "default": ""}, + "events": { + "items": {"$ref": "#/components/schemas/MessageEvent"}, + "type": "array", + "title": "Events", + "default": [], + }, + }, + "type": "object", + "title": "MessageOutput", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } + ) diff --git a/tests/test_pydantic_v1_v2_multifile/test_multifile.py b/tests/test_pydantic_v1_v2_multifile/test_multifile.py index 4472bd73e..e66d102fb 100644 --- a/tests/test_pydantic_v1_v2_multifile/test_multifile.py +++ b/tests/test_pydantic_v1_v2_multifile/test_multifile.py @@ -1028,17 +1028,6 @@ def test_openapi_schema(): "type": "object", "title": "HTTPValidationError", }, - "SubItem-Output": { - "properties": { - "new_sub_name": { - "type": "string", - "title": "New Sub Name", - } - }, - "type": "object", - "required": ["new_sub_name"], - "title": "SubItem", - }, "ValidationError": { "properties": { "loc": { @@ -1113,11 +1102,11 @@ def test_openapi_schema(): "title": "New Description", }, "new_sub": { - "$ref": "#/components/schemas/SubItem-Output" + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem" }, "new_multi": { "items": { - "$ref": "#/components/schemas/SubItem-Output" + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem" }, "type": "array", "title": "New Multi", From c01b5dd96f2b201dd3e97738d92a16f7f1278482 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 29 Oct 2025 13:09:57 +0000 Subject: [PATCH 055/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index fb4c26701..f6158beee 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Fixes + +* 🐛 Fix separation of schemas with nested models introduced in 0.119.0. PR [#14246](https://github.com/fastapi/fastapi/pull/14246) by [@tiangolo](https://github.com/tiangolo). + ### Internal * ⬆ Bump actions/download-artifact from 5 to 6. PR [#14236](https://github.com/fastapi/fastapi/pull/14236) by [@dependabot[bot]](https://github.com/apps/dependabot). From 35aa12b9bd638eb9f7b7d0be2a8439831df61899 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 29 Oct 2025 10:43:11 -0300 Subject: [PATCH 056/256] =?UTF-8?q?=F0=9F=94=A7=20Add=20sponsor:=20SerpApi?= =?UTF-8?q?=20(#14248)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + docs/en/data/sponsors.yml | 3 +++ docs/en/docs/img/sponsors/serpapi-banner.png | Bin 0 -> 6458 bytes docs/en/docs/img/sponsors/serpapi.png | Bin 0 -> 10694 bytes docs/en/overrides/main.html | 6 ++++++ 5 files changed, 10 insertions(+) create mode 100644 docs/en/docs/img/sponsors/serpapi-banner.png create mode 100644 docs/en/docs/img/sponsors/serpapi.png diff --git a/README.md b/README.md index 4fd87298e..09cd38da1 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ The key features are: + diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index 7a015e404..943b92adb 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -26,6 +26,9 @@ gold: - url: https://docs.railway.com/guides/fastapi?utm_medium=integration&utm_source=docs&utm_campaign=fastapi title: Deploy enterprise applications at startup speed img: https://fastapi.tiangolo.com/img/sponsors/railway.png + - url: https://serpapi.com/?utm_source=fastapi_website + title: "SerpApi: Web Search API" + img: https://fastapi.tiangolo.com/img/sponsors/serpapi.png silver: - url: https://databento.com/?utm_source=fastapi&utm_medium=sponsor&utm_content=display title: Pay as you go for market data diff --git a/docs/en/docs/img/sponsors/serpapi-banner.png b/docs/en/docs/img/sponsors/serpapi-banner.png new file mode 100644 index 0000000000000000000000000000000000000000..3c3fd629eb08f6e174d83e675e0fa086cb2271d6 GIT binary patch literal 6458 zcmV-A8O7#_P)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H17{^IO zK~#90?VWj;RMnmLKj+rIba&B+pdzw2HpnIrL=4j^M2%?F#L?j~iFqUA#Ej#Zm^>!Y z$vBgk(TvHsjM3M`WHkE5xWqTn;6e}=1eG-VBHCaBjo2$q*Isqc{PC;Xx2n6U7psF3 z@ArB7>8e|&&VIhX?VMBUYd2|)0|yQq90f2w>Iyh;;K0E^;SiPs2M&%JIE3ZEfrFz4 z?xD&^=Uidw&NlpMO}ZnFjt05zUn@CzriUMW{SY1Rn;04l6Zx;fFjd}bP#RP&&$!Wr zs`k@bVUQUy46Q*aRk{)m960!^#*h$})|zZGjwhNTko1yCrf?e}l1gKF^i3syfXzrC z3}rnqgtW+!RxlezHEXKdyd+x=@!p9(G9kZ1S`Hk1LQythX{{lX#^^Fh#IqRM#83)F zj-YbfqLs1<%K*&0*IL_brsR>*pcSAMiEJ7UX{N+Y_Dssq6?PApv?3L0>P|~?+=wgy zEo~X7pG?IVGIMFU0p#c6YTd}Kl)adjzckibYH~~)x&{{8C7nv*QVI=d158sm$kMR1GSP-cn|2y)%I7B7npPf}8Qr8MreG|UV(c#1 zJ~>S`6c`X`ojE_s-z=zP@BRdB?MZ4T1bMG13k(%u!5MS1%$#;CT0=)P%S&tXw8j$H zvNJ|kR|0Sd%YlP|qAbEv3bLsr$z&FnQkVu{<`%0$-sjM55*m5hRF0esgqSoMjp`;m zMZTt$M_P@FcTWxssE*-443`n%w)=f3C7O@cVt0-jk>S*7CWp5`JNGjO>~MvuzUcF4t=KYLKhg;Hr|POIRxw-U^q;bHvf7%x!?z{HbG zCXIy++fvM)Dh+<_6-7GZ;$UltOlDwhaNxj!pe(|QMcc`m9<(yMEj&>yG#HAmxJlL& z9V;5wWVFVuRH5H0&rFPUnuNnH3 zX2IoxXe`a9oi1k8YP8n8x8J~RM430M5`LQiFUc_50*WpKiA!u8`%Oko&`H#el%cQ6QOW=+lOU_1!P{&%Vh$jX9J zFn6Jkm!3<|)R09hO)3Rnydf-0yJva>7B3IdanOXU?AHRzqKxpT0aURne3;HIKPVp! zjcE)+p_C?-&Z4#E>E|>2X-x(IQ`-usDpnyN5Rfz)#YxA-@cY6f5+)1h1|-;N!u;6* z9(&dVTmTqKf!4U)ic~TQIHcvk!G}Rvgk^7Aup@loDuZz+xqFe71*KrxY!8hMS=CNGy&;N9D|8>0TGR3$t9+JrnUT=tO)}*;T!1i|_nKD^=wwswJCpdOQ zkmp}fyzokduEXlJF`{wM+RJkAXf|$3Gjpo9BniFwMoP9WXxQ7BrL84P^)arZ=`$(KEf;lQ zCc)L?SPx73j}jheso=oN0HN+@RynPQy!`Pcu=iw19ag+-zqVi~gB33+$Zd^K3f9!S zav8e1)9pRrX72%cJ#nl=HP>xO(s*E4LHR4!7!2XbCx2A;ldJ z9H#w{N&Zb+TFc+#kHy&6l-!nXv40Ax=NlOCX?luQAK-o{q6? zbDDt1C_LgR%}wo3MXQWGMT>fddC0 zbChio8o6ERK;f|(1ODfUDEB|qg-e4QMMXg2@heQzcG4T-HX@qP+;eLuwX=p3saCxB zRD$M%CWcoP9N{sP#+3=&2)L_D%mNV}Adm zeW$zhv#@@%^x072pRNR-7jC<6X!9+GNF^*i3!Z#V#(yYq+_7->>C!jb-s%1Msnei# zQsJ}CE=j-defwF>c$hb%yg5*w)4ki-uJc|C@n|JSZaQKnhhip&<7TfTIR)P}AfD3Q z=4CpqF%4ZLeUH1f`ayLBZu|$BKNA`nVdD;{48sk}Vd0!|TL1CG6&Jy^UzSO`vjN&W zVbMJJ*5AXJ<3HNEjTjE|XMHqfH#R|YoBe+J6u9wgz0=K|A@v+;G`9(19tnhk($7PA zpkm$($#d};#Xlc+qV#D#rcH)#T@4Fn+s}?4S^gX-?|PZMD`E<+jsda?uDm$J!r4B8 ze#P#+SswpGjLloq1Uz~7Wg)jT1cL_Ge!GIoDuvdt_;Uf;TcRA?o5kzO@oF)Rs~>!} z4&n*8{r^HHx0maMSKy-0z=exp?FRe#(Tqj&WWn9{2yEJ6KMw`rnk!)W!>*$@X?ND#c>5_A0oz-uXH8>O~ zi?Fij+_f;dZ3s$^toS_ez>U>ZR~p@WnWjxPn0kE`k31FSsb}H@+~Tj&8jd~I;NNeF z;C9=`c_!6*`0gDM?!C2>w>M-^O7$Sffz`1(B5G;Rg}P!jY}r}(EEE)i8#`Lw?`)8C z$&|cbybydoIB-x@dm=^#A2 zs!;lalz+}5$*U_WCf)iirQ2v2lK$m&g=7U{7X0c7xcOR`RSR!!vOk+YQ_M%751L!y z`By|6$w#CT&Z~oyPJnb8cI|;@R`u@Bg>ys_`+V^BZdmoYXnenZ8d`d^uULPZX6d{w zOd2b7x&Kjn0gN3jg6G*)@_hieTS$7vY8k^z&XxAP{DzE!bvXT>u9Uv-NhpgK+G%#} zf#0v}o9KOB>HnJC+1AD;`Hp3hS@Y%(E9L!_Ux50pFgyaMo+9&N6PvHRGYO0v>w?uDcTEOoQiN z?f-iKtJlGqr-^W|#-?19MOatVg}QGCweDg!v#?;Qw}7lbHcKjC*+n7N)u(7}78NNL z)7<`z$^x>o(gR#|Lj^Z{xeYT5y&U`lz=027?sPGU@)u(~QP_!xCC`gM0i)2nWTF^U7ON{=Dg8 zuHJgDO4odzE!W=d0lZ4OdeO-G_m@- zvrGEY>hIc#@E`Z(MV|A}^Z`sbNyfk;=4mxj&Vdi?@tibPo>`jJ=u!5x*|Sf~zELA2 zkFIE88Fllezg~Sy+JE-x;sfnVOS*a)46BrdoJ_%zg>dbc>~@SAA$eFBccP5zfe)md z=~HCPckPz`SpA0d!{rx9U95IZI0=G&8HY80mORgx1LH=+PaiDaXKpwA!!jvv)mo|F z{Fy>Ncl;bWI!m_;LtUyUUt-{>E+-b0G@t99MIps8g}m$r{T6;y&djnoh}afAKYVWR6E~= zYrZH8)S5ItFWmeSI}OO7|N3__i7bSJFy|DTus|lU$@TE)O@}+RgT{8JE{){Z3 zpFU`l)0UmKPw>^Z#38%rQ}Bz&Y%S2rt1p~&y0j%f?@im`yVr;lH(wL95RJi}y+TqJ zVbx9)jjnd0ovwD0jCHZaxaDmKg=B12z25ut)(-f^ZwsF-npc{zB9%gpcmEG;&pD*u zeNQwrkGpW5^lXbosB7!tmVcH$x1O!5hvScxw&n{~pSRGxv1vPe_v@wmY*sCdIRSof zx7{CW>*0IXK;3EZ)N{q_2SSLmro#XG|MGtQW+CvN!oL*e&5&bwLmAvEi?G)0O3BR$ zHIjszu!o^te!T_uj&&NhTwc=bT0I-qMuT z)fY$pOjl{@d0ey4s{FUbwyZA)&KYz zd~><G@xlUB?xP(2$^K6_newEw(mMxj`WQ=;n3Iw?>CF%yeDB*hDGoK zxC}U96r55c^Jl31S5U5o_45ueb-~Yc#iE+0zc-x8qdi55NNad=YYJQ%(^NF<%My)g zf`Q)G(OBtsY|j+@poNgu;Og4YD1T4cYX|{Z_24`KTTyzNZTp_x^0-_R`+R1lZ9DcmC3LFANpRBCLptlL-V-7GY_q z2*Aycbn@^ws&V(Y6H9AY^;Uw7@1zNP7242v{qUdnb#l{nRRy;;=v=t*u}7nHcA5BG zw$t4NQbd9N4?h<}V-x)H3HZS`g}in)2x;X@puynUFUdrG=trN|77c7#0 zFd&O8nX)yvo9-wsmC_{~tBp;PNA1Kyvvgff%W%6z`dK6rOTfq?BX6}omXNZtW^V~^ zwIiLj>r`39Gf-LlMxbuQ6XFzKTg30y7il!N!3VA4>l=NXy)S6X+qu0!O0>s;m@Gqu zoel7BcZr5KVT=S{Zn#Q>;Xs7oU{Gw1hJvL{&twW-Ti>_oR}9q=F?DymE3YlIc1XY1 zPAt?$o7>^P9voa59|mRZ!;1Jrkc$*{?~ekSIpmf&$!XDpshdC z6bV{tA;%Nqq(86DZdX1Q%odWdw2nP{#jlY+5kbtfbO!e4qN@82h^f)DPo_<_{VF@( z6*KnaiG}I){jFLy!dJVL=xv~+2%_;DL+cP0V&^@}ln_i7xU-{`dg)a8ZU`?GxU+BP~4 znJ5K6d!UOAn^JiEy*G`zeO?m2{;QtvBdVJ(3p5mjnYHkhF9=as4#5?x#VLN_Vo4JU zikiQ0Zr{7alPPFFB))=xUnbCk*%aLCo{z75K_-jOD`wh^lkMnVJOQu1DI__4iliA) zEn)Dh{?_^}5t)*@ZHf+-`BuU;ov zjr(O#dy>|!JrbI?d{nUnJpOFq-l->_6Lae;m)a7rxm6@xU(U;pP6<%)@Cl{U5E_(IDfq>~mr!fA7bAr|%2< z--qA)0hXR^`{UXVNz`;8`F|tu-+-&byUa(y_QWpmKY%3=OfE$-T>mF~7#kFP@9? z=!-E7t!yW~2A654KYU2@*j>YkR2kfUcPE=Rr3V&oOH_hV5FqU5~|L=cPBwnOa(v{lqXGzd_V(@+tpI0JisZ{@G z2rI*qwxzAKA0)p|ifsZdxUnOLF~^G&`j$KUKbCS#wSC5OuukuC5DH3pLlMPtyCqmL z*m*M;-2NR28m+CDK7+xPRUHwnsIPhVaYvayq^bxS0mjGEGz2y2SfX3g(%6^#fo(D> zRd|HQ%$_gT8zn@H{7sKNj6j^}`&_JR#r3h!J@(1yA^>;8enfmZireq3| zC3cthL}yq3q}ErTxZM(TSii+KOAJHIqFAE;NEDRb3ICVIP#q2i8UG`7UJyQ9pZz5? zz9WsY_n<1J2xUD?=*+M`=q9FIWRpourFtDuHjUgb-W2)uXDB?-tS!G7^3zyuSC*{b zz~yldnyeiB<&epUDR$9k#824XDM1LI7an}<=#}ISKAsqoP4kwfH8~leE!D>1fI)jA zidJ?c&&<=9fTE;TKv*a;I!ZVc1k!lI)wyjCM<-eRer5k@j#jK#Et^s&jgx3(=V2kS zq1-U&Ku#unRNCmr1!eiL95`^`;7G!`XxM=R2S*JY!gAoi!BGQ;upBsWaMa*`0WV04 UcVKxRT>t<807*qoM6N<$f)*jfDgXcg literal 0 HcmV?d00001 diff --git a/docs/en/docs/img/sponsors/serpapi.png b/docs/en/docs/img/sponsors/serpapi.png new file mode 100644 index 0000000000000000000000000000000000000000..d7258ef70a4f326d24ea4d607534feaef2039a84 GIT binary patch literal 10694 zcma)CWl$Wjy2U9@ad#+-ySpx0+}&M@yF;PH-J$5>6nA&mE$;5t#e2DL=FR*2ek7U6 zBs2MvFPW2bPPB@$EE+NqG87aPnjBC{?W4W^C>Ea(KI+zAT_Yb2k_%AR4GIc<;J*cW z#JSw!qm$5GTE|`8*~;C^^qVD=mzNidt&_c*xv7gKi}N?@>`NgcC@2ajIVo`s@0^QV zzZ{YwkE@;~PQluMDG?g2dE|>wJcU2NPb0_TW)1Dl*m4Ajlv{&vb*g4Lm`9UC z1W*dcyyW=^q$GnUq-2sxDtPh{V3$m3W(HlRjK(3;pHy%%|1zY1)K&H}$+QhiTZktYwqAh#yP#nSnRa~~vP*B^@-cTf2< zn5x)CL0X9kn-zDJcqZ`~3x(~|HzxK`+JV^c_y=9PMslnRbI)H? zFbV)UT7>dg9!|bRM5=9yoO_yrbhu%0pnO8B%1~O3r`u610B8mcjT-j_L5F)?(bJ(k zNp^c+gg*rNlLJ+_I3Gb#Pc1m9lT1zBJ9!qVBWG}NBu0Mo)tuwY%H7eL-q135_Ttb) zZWkPd*!mhvPmwS`7!|(~`7xXA4}@rs5Y`qa`))jEJSS)q$xB#wG2=jO;%+=4NSU~| z6L|h=92^p&h{%osp%`4zC3kWRu~aB1x~MV;l$pa3eSys5MCt6G8*ZT}-7J7Tr6+*( zt4}}CF&JU^|R{>XZ64z39!t_i6X_8)F%r*3_qdl0(no(X(%uZdJTD{#jA zOUhTd;ipjK0E2-+N~@+H<<6&NWBkMasI&@cB+j}O(v7hBRKozJh>a@w7W9;bnO=+J zdhU|e;Y3J-g(cPBuV;YegJqZq`BTVr^$!@n^{$8}lefIaH)C?sQTaF}8hw-7A^Q|0 zN^sOEjbJrQSMM|Lcg=K= z@6$m#Ylsa_!YLV4yJyGMSJ8iQo|-`;w##DUd5?0`!%{pawiX@}(wVqnDRwWo|wUGBsFB$+D!`0)+7j zh13*5Z{S-@22)nsm^?A_}&eU1DM&r18vX5Ecs z{45g4a+ChX-N&di@4_bOCa~zIHRI-qGwTMEy!j-7)g@t?r0cP>bN(QvU(YQCLznq? z@5+edX}uf-YFKL6Vhmx4VqIv(8vExy)Dz1+&NNu6@Cu)m74~}qi$jRP5A`PEPzT1Q z9V&6L@-?jt{k4JgNy)p(OzHItg_HG?y~H=Plv7!0L|N>Y3KDyZt*hHVg6}GCRL`AO`MR(E7I* zt0<67q)IJ%+xl+x&$nYo7k354F(QXjA6c{+?f)Bs;j=Z<{T4&td7luP(;Iccn#;3YMl)Nd_d zf|fk-`1!of=dz~4g^=G!4&l4Ng~m?~**>{4rHifm;}B^ZLrjYb+dVYc{?C1~qN-C3 z@n6cTn+O(Af*5bg@jzZ+2lWHy0r{ZUF&YNTeEUWRY2ZUbk_pMsVDvL49SYyXz;@;(z zq2IZ9DN-B9*9wnDzpas8lO6W5ML5e|T{euk9Qw}ufe}X-gT>NCzb~GbAx-v+M+f+o z-2(OYUtM9&0HJqAS-V5k`)`slgA-}x)?gg%pt*9xLzDQ9>D~n9Lc!IEvn|B}f25zb z@%~Zai!yZKzj=)Pn%XPJG&cMVo`^qpzhRg!F0PEw(a~J#ky4QWYS~T2_!u5fH-`bK zqK0YxYRBZE?_G6nwM07koyusTHm$$^I;~ZGdL4)6_Qv?z+b@Kapokk7uN%fi6f%qe zDTt5&R3-ueyZk<0kbu*FaXd@p^ns$FMkT1)?h_H*0gI!oU6(od%e|Mse(A>DQiS;N zIl)CED8>axRO56m;>Y3ZE*^k~i?9+cXgNCI=N@#4XO!^h0evp6|or1*~e;D534cLaF4LX~D1Wkk2^0ba4T4SH{8Kwnju_?CN2|0!z+j+XYiDJtb|AZ_i! z9MNylHFhLV)$+1UJ>YA}_EooG=hP)xNuYC^VHX_S>5H__9XQu7^@cuCEo{>E(^{K0 zXtq7JwDnXQ5Z$@NG0I;(F00&%Ch?r-yoSoi{0jrM?aF!$U78}`LPttYQfd30vdO^X zO=d~vUjm1dE%(NmkHeK3q+)ANRvCxI&{n8zYNIFMWkS91W1C{l2K02SyK?f@ptF^W zJ}abj2=1H}1;7Iwa{$h$03a>xKXaw;`a!i$^M7ya_abn%I)hS5WvIQ-UTOY=M%BxPN;q7C`YKw2a|Z%`S(Fq3 z(rP?^NEATC)etT5J3DRvNlBrz2%G#K32bH)*hvWqZoBP@uJyT{U_Qs+sWLsL`=3~7 z+7PszUFq63kigkomMqPH63IdMFUguUt@PTfqi0DDJOqOs()l-24oQe}W> zZlI>xU-xEq+!RqnJfF{E{dtwAK6L=vyF>Waodwu7?iID!{jRe((~5o=bvy*N!E7|L zw_*BP(n$(BTlK7%&YnNQea%tW8oZwr*W)OsMLI}?Hh1~J)fFW!`*03x=;XS!jrq(i zjP^e4tW_d-U99@3!;c~OKW4y}k0MvU@#ahu*KjKpoq(^-N2kz&w_nXS?y#~Kn z26O%>@j#R9nOKnYf8^s;Aq=S>J$#JI_KL$uVhXLOGci&qP&r6Tkp4X7vN$Fu$2>=B z!C8Ilh6jffwNc??m`ovT<;I1$W5Wop8@=)7+25lF6TNALpwEhK;B9=HUH!FO{DWC8 z7ocG>csuHoYE8m0nXm&iib|&jZhF<4KY?8hW{?&ta~hm$JS{K(v}GIx1eC4SZzs`u z7SFCBfos%5(2uZqs+eaK&fE)s@w266WiKe-73Yp>2lN?%0cvF~WOfQba1ySku)6o7 z9!eB%2H2x633ERV6sTkc5GDRI3lb`d@iBVh^Ly-Y&L;->G33F94dV23i%xw;VcbQ$ z1y+pi+P?NPc0^ItD` z=-HU7dCphVo0IOi;NDPk6u3e`hCR94QFKTEwy%9$9j81&NJ?PbGhwivpHZ8{Q?wW4 zPv4s$MKj?uU{V9V``)A%Px1P+a;kzGd@d+&8LP^B`n9}5gmMr0P#)2 z@W1xUeb4DkA!|-*N6%5)+xDk-eUf%!_BKZmVYL7%Cq3(z%~Q|2u*2N<)`_%=bO|bW zK`%duQ4m-sLG?7(G-rxJEbdN^GcVNSCEmywRbSggF93yqap*xH^ zV^>-DTx^EiKe-?*Uj=9if3^0T``(_fHsiV=plNkgdCL2^FME+rR(=g6H3O}6>=1eR zJq2!+BoS)FAZyyZbO5SJw2ZLkpBaK3@DL&u>4s@DFvT#DCCqch`7+YOr{^NysW6*O z<>|QtTt;b9JfXZ#H}vEC*#3q2|_orN705(5yof3#GFv|j}yboVsp2uNX5RUqrGlo<1~2J%sAimTjHInu&~IlN zb@_@T#35Gw(`BkluSb8cn&wksrxu_4)|j!S3ld>fp@i;&e+nu2P1@T%)0iA9Zs!IU zfpHsAi$BTlx^rd+QkfV4?w#z zFp%bQNXSfZhgdxJp~xA%#&i8RB!)vZt(@ETg*~?LiVW_9PG*1&yi(`xq-LGG4g*Vh zKhMF5!|uAmX5x~)F&n%KguPBh^zv)=T8yhXva(bY;lVv&to9+J9D+hRTG8+XPi>kE zFg(koApOdWp8`yOoF*sJkX|r+-y)&HW}^;gAH(>YKi2gL)^laPAAfwh zD*|#6&Y2)W&d92+CLjt3;pCJ79ksS9Z8Y5Z2Xer$^38lny3`OFOMzVdZY6y#E0ovn z{JBq)zS|g9uY-4ldXHh{v%4uHFkL%tFE_hRVD?~1sr)z7|JDsQgHTRskzID$aPc+` zS>D%I(W$jN>&*El?)}tc|56X>-sx7{&GA;;?W-3ylaT0lketH*pb=%^A}Q z`c(P?gOq2(PL8aUFv3sA5FR!pYy?@Q_dW6>HC#;E3#qACgqc@KPVm1uC{VE+y-xWv zGULoLez(d@7Psk4r%q&(ez|S`h=}IE6F!{Jb*)UsmY%c|24`i^5Yw79%uwi>;OYq4=~+G|5LDO066S`=VU;n{^4+MapMrN_>ejEyFfEU{*Gy)8?_@n4bOMi+sFFONEeSSr?NOL#A&tY2p`_xnaJ_EF^+6D9b2 z6xdTpWAA&@K^>BJ>v$&s{$ z>YCbzMu!fR#H=|AdNf=FQ$%HRkXk0jVAd4itF?)6L>EG?Q~ydjW9$ufd9`b!GxRBT z4CF17aHKxUEG`iY=0q|rRoL z4cCiupkp{uEx7Pt)!wDgTY2)+=vmL%mRgoEBUz9gyXOaXS!%CS`VeT~K&LQvZDEWB zaL(_+5)_96pQtS2Z{rZ42b#=k6);|D!vm`Egn%7R;2VH!J>0cIo7o4^9a%m{ziziM z2FetN=jm?rsK4M=N?cFmMgLx#_kL4JGXd`V=mW6uTm7D|L$WvpbCsqwQ?BFzXuq*I zusBK|tOLL!_V%b`L2`~7YBeT)CuIN$(-44z*Qkol^_3{;m*hmtp$mD??|}0)PGt&> zbtQEP%Zktsl9JF8Tc=o=5>)}R7?kE$^lm_k>#)H-Jex75!P^a<6N88d$g78`VqR+F zhRSyHmSQ0(7Ah?u9|rCBv731yluq8wd{9L=p~WO0B+28o>#{0ONZ85E`q67JH0fD3 z{IoODZO9A+&_6bxq*Hqqc>Wc!yavyrg!5P|v9+*oo4hC4WF|;SF^m3d3nV#rPw3^1 zhTEh0`J5D!QZAWgb6X$ER~X_TCVQ6BzjC_fAiDw=Mg&b8pqrdv5M)@ZS3{c^&esETJ3@5xEm$cpSTkqi5pAEfME18>c&MvNeI(*@@h z6qpg=2n|7@?CBAsAh4>kt6G<3uT02CEhwsXVclJbm|k)^O0rZyrjB;nFc4}Vm32*V zw9F#TRA_I`x(~KQ!EZIQ&t-`zOrX%9)wvt_(snBR+38t~`>Ji!2wnaw@cq0{UFgLb z36Bf0yjJt>Z2dMlZCwinT6&iv=x&xiUNm>xi)Z-J#5x72qxp&8x`@2Ydp}V%$(-(} z1#!%RiCFXfccKR>1zs#x+%z*pb<)ngk+J9Oq{>zqS3lnsv4_>TqN+`O?C89R>*LQH zaPdUK%UNuR7k=Q+FR3Me3G4R$$v$?)+GpMzQR4@4y{#Py9X&#Sz8u}?@M@G7`a>@B#MKhV#7LcL6x}`*+ zLu-?Z?auF|5rJw~k@~R8sOrDtzm%(A=@X|~cVJ`KWl9Vp@rZd~CaT`~zM?*6o`IJW z^04}kUvxA`vW#>eV>9Hh9-1dYhP``Z<=)e5p*w7t=Pp#8rbF$0?BwbRfg4p$u%1lmuu|*wA<)Kb5ydJs&o~p(oD@3DI@x5 zqYck0(`Z;41v8tWnYK=fzU$S{vhPca^S@A>pFVlyh|FRoyD&*G1wzMF=CJd@o;BUO z!UyP!zmqyHqhBjrI7*9EYbw)fu##^GHzRjV#PH3}@d#W8Z9%8TPn>=JqJBzS6m~DT zn!@9%+CkdqrrH6CwAm2;i~jR@m8h1a?(AH!zyM!gpWz44x1+!T2E`OADGiLsx9mw4 z&HJ@|qnKoZOBr7}librrTmcy3$Kr^uvJasuw)XpzwCtg>pPERgbYrCq_ws3BFq4k8 z74}SDzcZ{G8^Ea3JtDvKdvjdM)yC>5LBI*pJat+!A5hNa>TXm{YRC{AI5H8_ShH*g0NRJdHdJy!xl;D&$LqSan>H~!-vs#<3ri#3YNdYR zhOdXR=(^5`weEGk)x2JB2Bd4ytAoX*?Qzfz`7TPFTBRrBWJ9RbqQ@N_Bk0e8r#`qe zJ?CaRU7?|3FDJY97y#S$TaFSm8Z|q?ueIMZ4gFHaiDS#zMlZ=8%yhdoKF?)Fj*Og0 zM!@kTa7;W68yE>Lp?G-wj7P->nth2@D#P&*bN)_u%c-UJ5MwzDrSE&BohW?n3wR{hu;FTEb}m)F1?aTS(psbhkdC#8qcxMwMl>(`=J zuH#¢voe?R=x4MJ%-X=lgxf;6+mQm*5SLltam)k+~4^*BpE*XE|FOsq}0W&#(1 z)jC^FgVONTE?RF8GxbXUL{qzRiB9*FI17k?fW*a3J$246Zln^<=fB=%v^rNRfmfvU zY4ym-6%iy$O6mLofIB@`ums%>`mx5Kk^s`)U7ZW7zYcSz#onYU8gT+Fd)ni)Qak)K zx-1nmf?_BWvxG*sMi$ws=j;i@izn+;xH#{Jtzs*nIg4Wm=$mNf0+#k;S|0li?s7O^in;F$kGLG1^tg&CEkl{+e@AT$;#K(3V5?|t4g z9FV+N}T&O3#KW09-oB8owx>v*ZUkk{_5yN+nQ*rO-Jx z;rD0H2A#5Xg`loP-X+Yjg}7um%)ixq=lwXJX<7l<*1s^GZ}Be~ciuG2nG3sgW}R&z zJ0V<$Qdu|qazLa|v>!G$ZJ5H0_|~TQk>A*KXT3n;#w;hdYgs43ci(C*iu@-N;8@TW zawxLp%*RptguWaqYN^#j>x7zqUw^J2gd-gM6v_5;oYPwo_Z?wNWSVDslxCK#f=%Md zrw|urGb-TmnjJpJ=L(+uLoFuohk3bX|DoE99B{nP)r`(JGAH7d#0)pBTGig@!kV8s zs9565Y-tEVKAvIaT>w~bA=78~hGZcx9OP4vmh2}kOuGA^n-1vV=H9RWC@XtA9O+ds>6zEUP$yESwg;+!osNJm9ZK7jbdd zbIzC?q2H>n)OP{B)eb*Q1hW720%+8|rBs$L~n7&bYc(C0Q`j>1BIb#?NZCaLu^Wmte}$9!hG)?5Qt}d^Z(* z){eMJbz)>y7AH-PcnA+}&)BbkEV~`lF1oGE3y9pl69f!I%neU_o+F$uHn!D`Gz4eY zs#~nRR3H~@&7S-Xo0bKcyu^7zrf+g9&o|DC;-*H>yGwVyom)#?T6~W#Z%aJm!D~@G z!_*ZrLSf#H!6r~~ zIb6|^v~EOj0g_4gtpz13V)A^R-$G4POyt9WxLhN!Bo~@U>tB8&G^yHDMM;h*;x%a` za?^f@DJCUEZCzp5TuX>PTQQ%^qdy-GqQO8DknqSAE1?dE$tO!6!SlIOP2_kd{J=wa zsup|(#$|tu2Gz2crj&Y?-mH5H){){QF4w5Kb^13p)9>1VG#UPoQQ$STKo&!8rhTl{ z4L%zL&1@MOU6J(^RkCTg9w5_Rc)WD{T@|FlQ}@W;W$4|RFxKAcU=lqa^Doe@aS^2^ zk0{UREe#hFLo2)wAIl5Fo^-CD%1z8Uo>9|h_TE!UuJx~ogBX*^V6Z^)D-wJ**YkNaXUl__5-$7uaA1GW1Ro`YFNh8Pk6j*6 z{!eqCyRV}WPAkyT8sUMlmH5NFqWS}Vn}mBWtg*k}W9%wILlu|zvu6G-1pL`wdC9WP z#@{CXn!6hvB&Uuoe8=GK6;E|IuAuq2aZ5WC_CcDxk+vu@|Jmf<_MnggO_BES_;%%An5x%JqwU5IZJ^0O+n>^5%Tl5t| zBPwb^6*BCdK)k#JMVH7%K}p1|6C}<3(yngsn@{3wox#Zf|1dLsI9g`=m-Kjm{Y#Lt z%RsxURws3TBqNp3>i|t;@P4aWw&-g&QQ(bCR)kyScWKn-b%P1Scbi~E1XRk5(VQ+c zLZ>wc%WA%Y!$d^C^G?+#U@3Cg#S3EUV0LtY4=bz=)xk8)iIjRL+l?3~c1+u-^pZQs zdg4`QFW}{(&5PqhIBDqP^WtG=re5ALdaQItyF&yot7wyM#~i5E=#U+#laKxW=EmCc zmPQ0Ex|+Go7$iEn-a4jvRlT>NK>fO}nHr@FWhkoPdIXLF)v9i=!}*`)L6be1CSASx z2%iNtMM~dE{-l}jyqae~rR1B-FFb197i6W;y@~roZh*T}`HxG6>sRT6M6Xv6*YH)4 zWyaQ<%6C6!M=NDX+}rz(z^^eMDrCWjQcqSe9qwY}g0RLrl*+||=U~io>s16E$pHj$ zAs}eBfd~rS1@W73t^T%tYsyWM9r((vFH&kbm_^L%{eIwD^C-o7_70Z2d}&_IE9NKL zzWotqp%h}NHWtz02BY;mk{PXR7)oP%&_2;;jTl!+#Hl0Rq@jAHl+MWdDv-Pwioghm~Yx;Fnah9o%*i zPLNtGNQ7t!i6V-d_T+kl9ftR{SUe~Ycw2Sj) zc~M7R-iT|0Eq&|FMG0(Al<}*f234X@T3fG5J6!Wht=4-X#ozLb03*QY#8K9dv$J(=KNLQ+3Mu~0E!0^5^T95P1Q)n6 zN1znMqw_NP(Cl;ne)V%+$6m#_$Owj8HF53hi9^Bq_R|8XgR#@g`|b3K>=0Z{W#pIU zvd6xjlGa9QV5gQzR;acg9p9|^JP&IyV5eg3MaNAJ7i9*8EiL8swFya?7T%2*&1vz`%$m@D9d0>khZGnhoa4NCM3yv^CprXTpANKE+zfvED3JG2 za4j-?BSP@Micozw-{QN=Li2p{4}CWs6U2jmntX!|hhgKOn;FNNZ-aBZcwiS)eG;fc zy0L4D9h))Y9J7L^NlY-=@qy;h3LTF)g6DTPxc$M)0f+iYlTb<`rtS+f_0M#a{aO0g z^K@(tcsLJk1c}~g=m0Gfg61eqP0Y>4Ric@M2K5ro*#~1nJ9C7@go19FTmq8%HU5!? zvm8$n{=&3!|4C&m5=zjp$<_&xR literal 0 HcmV?d00001 diff --git a/docs/en/overrides/main.html b/docs/en/overrides/main.html index c7ffaef5d..be31bd75c 100644 --- a/docs/en/overrides/main.html +++ b/docs/en/overrides/main.html @@ -80,6 +80,12 @@
+
{% endblock %} From d2a703d5cc45df171cae50747293e1e17fbe3261 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 29 Oct 2025 13:43:35 +0000 Subject: [PATCH 057/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f6158beee..53b3393f6 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Internal +* 🔧 Add sponsor: SerpApi. PR [#14248](https://github.com/fastapi/fastapi/pull/14248) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump actions/download-artifact from 5 to 6. PR [#14236](https://github.com/fastapi/fastapi/pull/14236) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#14237](https://github.com/fastapi/fastapi/pull/14237) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump actions/upload-artifact from 4 to 5. PR [#14235](https://github.com/fastapi/fastapi/pull/14235) by [@dependabot[bot]](https://github.com/apps/dependabot). From 22ccca21fc13b7ac138e277490ec4d05b4f5094a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 29 Oct 2025 14:44:41 +0100 Subject: [PATCH 058/256] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.12?= =?UTF-8?q?0.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 53b3393f6..e2d1514c7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.120.2 + ### Fixes * 🐛 Fix separation of schemas with nested models introduced in 0.119.0. PR [#14246](https://github.com/fastapi/fastapi/pull/14246) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 787c52dda..a4c17a6bd 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.120.1" +__version__ = "0.120.2" from starlette import status as status From c144f9fbd356e7e378a6b42cff68cf4a4667111a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 30 Oct 2025 01:51:50 -0300 Subject: [PATCH 059/256] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20interna?= =?UTF-8?q?ls=20of=20dependencies,=20simplify=20using=20dataclasses=20(#14?= =?UTF-8?q?254)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/dependencies/utils.py | 4 ++-- fastapi/params.py | 25 ++++++------------------- tests/test_params_repr.py | 11 +---------- 3 files changed, 9 insertions(+), 31 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index aa06dd2a9..e13b53095 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -160,8 +160,8 @@ def get_sub_dependant( security_requirement = None security_scopes = security_scopes or [] if isinstance(depends, params.Security): - dependency_scopes = depends.scopes - security_scopes.extend(dependency_scopes) + if depends.scopes: + security_scopes.extend(depends.scopes) if isinstance(dependency, SecurityBase): use_scopes: List[str] = [] if isinstance(dependency, (OAuth2, OpenIdConnect)): diff --git a/fastapi/params.py b/fastapi/params.py index e85375018..2dc04be14 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -1,4 +1,5 @@ import warnings +from dataclasses import dataclass from enum import Enum from typing import Any, Callable, Dict, List, Optional, Sequence, Union @@ -761,26 +762,12 @@ class File(Form): # type: ignore[misc] ) +@dataclass class Depends: - def __init__( - self, dependency: Optional[Callable[..., Any]] = None, *, use_cache: bool = True - ): - self.dependency = dependency - self.use_cache = use_cache - - def __repr__(self) -> str: - attr = getattr(self.dependency, "__name__", type(self.dependency).__name__) - cache = "" if self.use_cache else ", use_cache=False" - return f"{self.__class__.__name__}({attr}{cache})" + dependency: Optional[Callable[..., Any]] = None + use_cache: bool = True +@dataclass class Security(Depends): - def __init__( - self, - dependency: Optional[Callable[..., Any]] = None, - *, - scopes: Optional[Sequence[str]] = None, - use_cache: bool = True, - ): - super().__init__(dependency=dependency, use_cache=use_cache) - self.scopes = scopes or [] + scopes: Optional[Sequence[str]] = None diff --git a/tests/test_params_repr.py b/tests/test_params_repr.py index bfc7bed09..baa172497 100644 --- a/tests/test_params_repr.py +++ b/tests/test_params_repr.py @@ -1,7 +1,7 @@ from typing import Any, List from dirty_equals import IsOneOf -from fastapi.params import Body, Cookie, Depends, Header, Param, Path, Query +from fastapi.params import Body, Cookie, Header, Param, Path, Query test_data: List[Any] = ["teststr", None, ..., 1, []] @@ -141,12 +141,3 @@ def test_body_repr_number(): def test_body_repr_list(): assert repr(Body([])) == "Body([])" - - -def test_depends_repr(): - assert repr(Depends()) == "Depends(NoneType)" - assert repr(Depends(get_user)) == "Depends(get_user)" - assert repr(Depends(use_cache=False)) == "Depends(NoneType, use_cache=False)" - assert ( - repr(Depends(get_user, use_cache=False)) == "Depends(get_user, use_cache=False)" - ) From 9d1a384f4f904aaf72be5a73a2f721f92fad3e9e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 30 Oct 2025 04:52:12 +0000 Subject: [PATCH 060/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index e2d1514c7..30c645e6c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Refactors + +* ♻️ Refactor internals of dependencies, simplify using dataclasses. PR [#14254](https://github.com/fastapi/fastapi/pull/14254) by [@tiangolo](https://github.com/tiangolo). + ## 0.120.2 ### Fixes From bb88a0f94a9633b861f90f6752e397980a7cfea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 30 Oct 2025 01:58:49 -0300 Subject: [PATCH 061/256] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20interna?= =?UTF-8?q?ls=20of=20dependencies,=20simplify=20code=20and=20remove=20`get?= =?UTF-8?q?=5Fparam=5Fsub=5Fdependant`=20(#14255)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/dependencies/utils.py | 29 +++++++---------------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index e13b53095..18f6a234e 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -125,23 +125,6 @@ def ensure_multipart_is_installed() -> None: raise RuntimeError(multipart_not_installed_error) from None -def get_param_sub_dependant( - *, - param_name: str, - depends: params.Depends, - path: str, - security_scopes: Optional[List[str]] = None, -) -> Dependant: - assert depends.dependency - return get_sub_dependant( - depends=depends, - dependency=depends.dependency, - path=path, - name=param_name, - security_scopes=security_scopes, - ) - - def get_parameterless_sub_dependant(*, depends: params.Depends, path: str) -> Dependant: assert callable(depends.dependency), ( "A parameter-less dependency must have a callable dependency" @@ -282,9 +265,6 @@ def get_dependant( security_scopes: Optional[List[str]] = None, use_cache: bool = True, ) -> Dependant: - path_param_names = get_path_param_names(path) - endpoint_signature = get_typed_signature(call) - signature_params = endpoint_signature.parameters dependant = Dependant( call=call, name=name, @@ -292,6 +272,9 @@ def get_dependant( security_scopes=security_scopes, use_cache=use_cache, ) + path_param_names = get_path_param_names(path) + endpoint_signature = get_typed_signature(call) + signature_params = endpoint_signature.parameters for param_name, param in signature_params.items(): is_path_param = param_name in path_param_names param_details = analyze_param( @@ -301,10 +284,12 @@ def get_dependant( is_path_param=is_path_param, ) if param_details.depends is not None: - sub_dependant = get_param_sub_dependant( - param_name=param_name, + assert param_details.depends.dependency + sub_dependant = get_sub_dependant( depends=param_details.depends, + dependency=param_details.depends.dependency, path=path, + name=param_name, security_scopes=security_scopes, ) dependant.dependencies.append(sub_dependant) From 1fc586c3a5ae457457c5c70adddb07c7e9185f16 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 30 Oct 2025 04:59:14 +0000 Subject: [PATCH 062/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 30c645e6c..bbcbc74b7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Refactors +* ♻️ Refactor internals of dependencies, simplify code and remove `get_param_sub_dependant`. PR [#14255](https://github.com/fastapi/fastapi/pull/14255) by [@tiangolo](https://github.com/tiangolo). * ♻️ Refactor internals of dependencies, simplify using dataclasses. PR [#14254](https://github.com/fastapi/fastapi/pull/14254) by [@tiangolo](https://github.com/tiangolo). ## 0.120.2 From dcfb8b9dda7b8117141b89e84527b48f978e0b31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 30 Oct 2025 16:35:04 -0300 Subject: [PATCH 063/256] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Reduce=20internal?= =?UTF-8?q?=20cyclic=20recursion=20in=20dependencies,=20from=202=20functio?= =?UTF-8?q?ns=20calling=20each=20other=20to=201=20calling=20itself=20(#142?= =?UTF-8?q?56)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/dependencies/utils.py | 59 +++++++++------------- tests/test_dependency_paramless.py | 78 ++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 36 deletions(-) create mode 100644 tests/test_dependency_paramless.py diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 18f6a234e..d2d4e8b4c 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -129,39 +129,12 @@ def get_parameterless_sub_dependant(*, depends: params.Depends, path: str) -> De assert callable(depends.dependency), ( "A parameter-less dependency must have a callable dependency" ) - return get_sub_dependant(depends=depends, dependency=depends.dependency, path=path) - - -def get_sub_dependant( - *, - depends: params.Depends, - dependency: Callable[..., Any], - path: str, - name: Optional[str] = None, - security_scopes: Optional[List[str]] = None, -) -> Dependant: - security_requirement = None - security_scopes = security_scopes or [] - if isinstance(depends, params.Security): - if depends.scopes: - security_scopes.extend(depends.scopes) - if isinstance(dependency, SecurityBase): - use_scopes: List[str] = [] - if isinstance(dependency, (OAuth2, OpenIdConnect)): - use_scopes = security_scopes - security_requirement = SecurityRequirement( - security_scheme=dependency, scopes=use_scopes - ) - sub_dependant = get_dependant( - path=path, - call=dependency, - name=name, - security_scopes=security_scopes, - use_cache=depends.use_cache, + use_security_scopes: List[str] = [] + if isinstance(depends, params.Security) and depends.scopes: + use_security_scopes.extend(depends.scopes) + return get_dependant( + path=path, call=depends.dependency, security_scopes=use_security_scopes ) - if security_requirement: - sub_dependant.security_requirements.append(security_requirement) - return sub_dependant CacheKey = Tuple[Optional[Callable[..., Any]], Tuple[str, ...]] @@ -285,13 +258,27 @@ def get_dependant( ) if param_details.depends is not None: assert param_details.depends.dependency - sub_dependant = get_sub_dependant( - depends=param_details.depends, - dependency=param_details.depends.dependency, + use_security_scopes = security_scopes or [] + if isinstance(param_details.depends, params.Security): + if param_details.depends.scopes: + use_security_scopes.extend(param_details.depends.scopes) + sub_dependant = get_dependant( path=path, + call=param_details.depends.dependency, name=param_name, - security_scopes=security_scopes, + security_scopes=use_security_scopes, + use_cache=param_details.depends.use_cache, ) + if isinstance(param_details.depends.dependency, SecurityBase): + use_scopes: List[str] = [] + if isinstance( + param_details.depends.dependency, (OAuth2, OpenIdConnect) + ): + use_scopes = use_security_scopes + security_requirement = SecurityRequirement( + security_scheme=param_details.depends.dependency, scopes=use_scopes + ) + sub_dependant.security_requirements.append(security_requirement) dependant.dependencies.append(sub_dependant) continue if add_non_field_param_to_dependency( diff --git a/tests/test_dependency_paramless.py b/tests/test_dependency_paramless.py new file mode 100644 index 000000000..9c3cc3878 --- /dev/null +++ b/tests/test_dependency_paramless.py @@ -0,0 +1,78 @@ +from typing import Union + +from fastapi import FastAPI, HTTPException, Security +from fastapi.security import ( + OAuth2PasswordBearer, + SecurityScopes, +) +from fastapi.testclient import TestClient +from typing_extensions import Annotated + +app = FastAPI() + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +def process_auth( + credentials: Annotated[Union[str, None], Security(oauth2_scheme)], + security_scopes: SecurityScopes, +): + # This is an incorrect way of using it, this is not checking if the scopes are + # provided by the token, only if the endpoint is requesting them, but the test + # here is just to check if FastAPI is indeed registering and passing the scopes + # correctly when using Security with parameterless dependencies. + if "a" not in security_scopes.scopes or "b" not in security_scopes.scopes: + raise HTTPException(detail="a or b not in scopes", status_code=401) + return {"token": credentials, "scopes": security_scopes.scopes} + + +@app.get("/get-credentials") +def get_credentials( + credentials: Annotated[dict, Security(process_auth, scopes=["a", "b"])], +): + return credentials + + +@app.get( + "/parameterless-with-scopes", + dependencies=[Security(process_auth, scopes=["a", "b"])], +) +def get_parameterless_with_scopes(): + return {"status": "ok"} + + +@app.get( + "/parameterless-without-scopes", + dependencies=[Security(process_auth)], +) +def get_parameterless_without_scopes(): + return {"status": "ok"} + + +client = TestClient(app) + + +def test_get_credentials(): + response = client.get("/get-credentials", headers={"authorization": "Bearer token"}) + assert response.status_code == 200, response.text + assert response.json() == {"token": "token", "scopes": ["a", "b"]} + + +def test_parameterless_with_scopes(): + response = client.get( + "/parameterless-with-scopes", headers={"authorization": "Bearer token"} + ) + assert response.status_code == 200, response.text + assert response.json() == {"status": "ok"} + + +def test_parameterless_without_scopes(): + response = client.get( + "/parameterless-without-scopes", headers={"authorization": "Bearer token"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "a or b not in scopes"} + + +def test_call_get_parameterless_without_scopes_for_coverage(): + assert get_parameterless_without_scopes() == {"status": "ok"} From 17fcbbe9102f0af63ad615ebec3ccccd0760555f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 30 Oct 2025 19:35:31 +0000 Subject: [PATCH 064/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index bbcbc74b7..fd748e6b2 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Refactors +* ♻️ Reduce internal cyclic recursion in dependencies, from 2 functions calling each other to 1 calling itself. PR [#14256](https://github.com/fastapi/fastapi/pull/14256) by [@tiangolo](https://github.com/tiangolo). * ♻️ Refactor internals of dependencies, simplify code and remove `get_param_sub_dependant`. PR [#14255](https://github.com/fastapi/fastapi/pull/14255) by [@tiangolo](https://github.com/tiangolo). * ♻️ Refactor internals of dependencies, simplify using dataclasses. PR [#14254](https://github.com/fastapi/fastapi/pull/14254) by [@tiangolo](https://github.com/tiangolo). From 8b46d8821b199f9c45e1383c296275fac21d01d8 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Thu, 30 Oct 2025 20:50:37 +0100 Subject: [PATCH 065/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20note=20for=20un?= =?UTF-8?q?translated=20pages=20(#14257)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/missing-translation.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/missing-translation.md b/docs/missing-translation.md index c2882e90e..bfff84766 100644 --- a/docs/missing-translation.md +++ b/docs/missing-translation.md @@ -1,7 +1,9 @@ /// warning -The current page still doesn't have a translation for this language. +This page hasn’t been translated into your language yet. 🌍 -But you can help translating it: [Contributing](https://fastapi.tiangolo.com/contributing/){.internal-link target=_blank}. +We’re currently switching to an automated translation system 🤖, which will help keep all translations complete and up to date. + +Learn more: [Contributing – Translations](https://fastapi.tiangolo.com/contributing/#translations){.internal-link target=_blank} /// From ec00f5a90f8f9030e8d92b7fad19ab4bde7e738f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 30 Oct 2025 19:51:03 +0000 Subject: [PATCH 066/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index fd748e6b2..64c215306 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,10 @@ hide: * ♻️ Refactor internals of dependencies, simplify code and remove `get_param_sub_dependant`. PR [#14255](https://github.com/fastapi/fastapi/pull/14255) by [@tiangolo](https://github.com/tiangolo). * ♻️ Refactor internals of dependencies, simplify using dataclasses. PR [#14254](https://github.com/fastapi/fastapi/pull/14254) by [@tiangolo](https://github.com/tiangolo). +### Docs + +* 📝 Update note for untranslated pages. PR [#14257](https://github.com/fastapi/fastapi/pull/14257) by [@YuriiMotov](https://github.com/YuriiMotov). + ## 0.120.2 ### Fixes From 2cf04ee30db9df2db04a5b1e0d2c625dfbf1a211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 30 Oct 2025 21:40:08 +0100 Subject: [PATCH 067/256] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.12?= =?UTF-8?q?0.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 64c215306..a2c99eaf9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.120.3 + ### Refactors * ♻️ Reduce internal cyclic recursion in dependencies, from 2 functions calling each other to 1 calling itself. PR [#14256](https://github.com/fastapi/fastapi/pull/14256) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index a4c17a6bd..945f800e4 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.120.2" +__version__ = "0.120.3" from starlette import status as status From 496de1816aa01a59c75a7dcd27fd3c6245fa255a Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Fri, 31 Oct 2025 19:34:30 +0100 Subject: [PATCH 068/256] =?UTF-8?q?=F0=9F=90=9B=20Fix=20security=20schemes?= =?UTF-8?q?=20in=20OpenAPI=20when=20added=20at=20the=20top=20level=20app?= =?UTF-8?q?=20(#14266)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- fastapi/dependencies/utils.py | 18 +++--- ...st_top_level_security_scheme_in_openapi.py | 60 +++++++++++++++++++ 2 files changed, 68 insertions(+), 10 deletions(-) create mode 100644 tests/test_top_level_security_scheme_in_openapi.py diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index d2d4e8b4c..6477a2cba 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -248,6 +248,14 @@ def get_dependant( path_param_names = get_path_param_names(path) endpoint_signature = get_typed_signature(call) signature_params = endpoint_signature.parameters + if isinstance(call, SecurityBase): + use_scopes: List[str] = [] + if isinstance(call, (OAuth2, OpenIdConnect)): + use_scopes = security_scopes + security_requirement = SecurityRequirement( + security_scheme=call, scopes=use_scopes + ) + dependant.security_requirements.append(security_requirement) for param_name, param in signature_params.items(): is_path_param = param_name in path_param_names param_details = analyze_param( @@ -269,16 +277,6 @@ def get_dependant( security_scopes=use_security_scopes, use_cache=param_details.depends.use_cache, ) - if isinstance(param_details.depends.dependency, SecurityBase): - use_scopes: List[str] = [] - if isinstance( - param_details.depends.dependency, (OAuth2, OpenIdConnect) - ): - use_scopes = use_security_scopes - security_requirement = SecurityRequirement( - security_scheme=param_details.depends.dependency, scopes=use_scopes - ) - sub_dependant.security_requirements.append(security_requirement) dependant.dependencies.append(sub_dependant) continue if add_non_field_param_to_dependency( diff --git a/tests/test_top_level_security_scheme_in_openapi.py b/tests/test_top_level_security_scheme_in_openapi.py new file mode 100644 index 000000000..e2de31af5 --- /dev/null +++ b/tests/test_top_level_security_scheme_in_openapi.py @@ -0,0 +1,60 @@ +# Test security scheme at the top level, including OpenAPI +# Ref: https://github.com/fastapi/fastapi/discussions/14263 +# Ref: https://github.com/fastapi/fastapi/issues/14271 +from fastapi import Depends, FastAPI +from fastapi.security import HTTPBearer +from fastapi.testclient import TestClient +from inline_snapshot import snapshot + +app = FastAPI() + +bearer_scheme = HTTPBearer() + + +@app.get("/", dependencies=[Depends(bearer_scheme)]) +async def get_root(): + return {"message": "Hello, World!"} + + +client = TestClient(app) + + +def test_get_root(): + response = client.get("/", headers={"Authorization": "Bearer token"}) + assert response.status_code == 200, response.text + assert response.json() == {"message": "Hello, World!"} + + +def test_get_root_no_token(): + response = client.get("/") + assert response.status_code == 403, response.text + assert response.json() == {"detail": "Not authenticated"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "summary": "Get Root", + "operationId": "get_root__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "security": [{"HTTPBearer": []}], + } + } + }, + "components": { + "securitySchemes": {"HTTPBearer": {"type": "http", "scheme": "bearer"}} + }, + } + ) From 4d57c13055bed83aee2cecadf7ecde2c537b3938 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 31 Oct 2025 18:35:03 +0000 Subject: [PATCH 069/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index a2c99eaf9..f4d08cd90 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Fixes + +* 🐛 Fix security schemes in OpenAPI when added at the top level app. PR [#14266](https://github.com/fastapi/fastapi/pull/14266) by [@YuriiMotov](https://github.com/YuriiMotov). + ## 0.120.3 ### Refactors From fad35ef43fdde74845b416cd2dc795ff489e9719 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 31 Oct 2025 19:35:33 +0100 Subject: [PATCH 070/256] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.12?= =?UTF-8?q?0.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f4d08cd90..33bf462bd 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.120.4 + ### Fixes * 🐛 Fix security schemes in OpenAPI when added at the top level app. PR [#14266](https://github.com/fastapi/fastapi/pull/14266) by [@YuriiMotov](https://github.com/YuriiMotov). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 945f800e4..93555bfdc 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.120.3" +__version__ = "0.120.4" from starlette import status as status From 8be5867de74e7c9cb60b884c9e16c59fc2daf256 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 2 Nov 2025 18:14:31 +0100 Subject: [PATCH 071/256] =?UTF-8?q?=E2=AC=86=20Bump=20mkdocstrings[python]?= =?UTF-8?q?=20from=200.26.1=20to=200.30.1=20(#14279)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [mkdocstrings[python]](https://github.com/mkdocstrings/mkdocstrings) from 0.26.1 to 0.30.1. - [Release notes](https://github.com/mkdocstrings/mkdocstrings/releases) - [Changelog](https://github.com/mkdocstrings/mkdocstrings/blob/main/CHANGELOG.md) - [Commits](https://github.com/mkdocstrings/mkdocstrings/compare/0.26.1...0.30.1) --- updated-dependencies: - dependency-name: mkdocstrings[python] dependency-version: 0.30.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 6baf19b50..4283076a1 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -11,7 +11,7 @@ jieba==0.42.1 pillow==11.3.0 # For image processing by Material for MkDocs cairosvg==2.8.2 -mkdocstrings[python]==0.26.1 +mkdocstrings[python]==0.30.1 griffe-typingdoc==0.3.0 # For griffe, it formats with black black==25.1.0 From 2a25f6d3a3dc6cb5311698f53f0ca739b1f8902c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 2 Nov 2025 17:15:00 +0000 Subject: [PATCH 072/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 33bf462bd..f109c9d03 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Internal + +* ⬆ Bump mkdocstrings[python] from 0.26.1 to 0.30.1. PR [#14279](https://github.com/fastapi/fastapi/pull/14279) by [@dependabot[bot]](https://github.com/apps/dependabot). + ## 0.120.4 ### Fixes From 8c42d0ce1610cc29c1bd8ad3a460085619e0dc02 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 2 Nov 2025 18:15:19 +0100 Subject: [PATCH 073/256] =?UTF-8?q?=E2=AC=86=20Bump=20mkdocs-macros-plugin?= =?UTF-8?q?=20from=201.4.0=20to=201.4.1=20(#14277)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [mkdocs-macros-plugin](https://github.com/fralau/mkdocs_macros_plugin) from 1.4.0 to 1.4.1. - [Release notes](https://github.com/fralau/mkdocs_macros_plugin/releases) - [Changelog](https://github.com/fralau/mkdocs-macros-plugin/blob/master/CHANGELOG.md) - [Commits](https://github.com/fralau/mkdocs_macros_plugin/compare/v1.4.0...v1.4.1) --- updated-dependencies: - dependency-name: mkdocs-macros-plugin dependency-version: 1.4.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 4283076a1..696eb2a33 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -15,5 +15,5 @@ mkdocstrings[python]==0.30.1 griffe-typingdoc==0.3.0 # For griffe, it formats with black black==25.1.0 -mkdocs-macros-plugin==1.4.0 +mkdocs-macros-plugin==1.4.1 markdown-include-variants==0.0.5 From 32da8ca78b13317ec252d141b6a4cc8684cbb7e3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 2 Nov 2025 17:15:57 +0000 Subject: [PATCH 074/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f109c9d03..303017d9d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* ⬆ Bump mkdocs-macros-plugin from 1.4.0 to 1.4.1. PR [#14277](https://github.com/fastapi/fastapi/pull/14277) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocstrings[python] from 0.26.1 to 0.30.1. PR [#14279](https://github.com/fastapi/fastapi/pull/14279) by [@dependabot[bot]](https://github.com/apps/dependabot). ## 0.120.4 From dbb7020a4d98ec994594096f897edaa70c01c67b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 3 Nov 2025 10:23:52 +0100 Subject: [PATCH 075/256] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20GitHu?= =?UTF-8?q?b=20topic=20repositories=20(#14280)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions --- docs/en/data/topic_repos.yml | 388 +++++++++++++++++------------------ 1 file changed, 194 insertions(+), 194 deletions(-) diff --git a/docs/en/data/topic_repos.yml b/docs/en/data/topic_repos.yml index 9d95fb8b1..1bb6fd70d 100644 --- a/docs/en/data/topic_repos.yml +++ b/docs/en/data/topic_repos.yml @@ -1,81 +1,86 @@ - name: full-stack-fastapi-template html_url: https://github.com/fastapi/full-stack-fastapi-template - stars: 38085 + stars: 38779 owner_login: fastapi owner_html_url: https://github.com/fastapi - name: Hello-Python html_url: https://github.com/mouredev/Hello-Python - stars: 32243 + stars: 32726 owner_login: mouredev owner_html_url: https://github.com/mouredev - name: serve html_url: https://github.com/jina-ai/serve - stars: 21754 + stars: 21779 owner_login: jina-ai owner_html_url: https://github.com/jina-ai - name: HivisionIDPhotos html_url: https://github.com/Zeyi-Lin/HivisionIDPhotos - stars: 19400 + stars: 20028 owner_login: Zeyi-Lin owner_html_url: https://github.com/Zeyi-Lin - name: sqlmodel html_url: https://github.com/fastapi/sqlmodel - stars: 16859 + stars: 17038 owner_login: fastapi owner_html_url: https://github.com/fastapi - name: Douyin_TikTok_Download_API html_url: https://github.com/Evil0ctal/Douyin_TikTok_Download_API - stars: 14452 + stars: 14786 owner_login: Evil0ctal owner_html_url: https://github.com/Evil0ctal - name: fastapi-best-practices html_url: https://github.com/zhanymkanov/fastapi-best-practices - stars: 13613 + stars: 13968 owner_login: zhanymkanov owner_html_url: https://github.com/zhanymkanov +- name: machine-learning-zoomcamp + html_url: https://github.com/DataTalksClub/machine-learning-zoomcamp + stars: 12171 + owner_login: DataTalksClub + owner_html_url: https://github.com/DataTalksClub - name: fastapi_mcp html_url: https://github.com/tadata-org/fastapi_mcp - stars: 10624 + stars: 10976 owner_login: tadata-org owner_html_url: https://github.com/tadata-org - name: awesome-fastapi html_url: https://github.com/mjhea0/awesome-fastapi - stars: 10415 + stars: 10618 owner_login: mjhea0 owner_html_url: https://github.com/mjhea0 -- name: FastUI - html_url: https://github.com/pydantic/FastUI - stars: 8879 - owner_login: pydantic - owner_html_url: https://github.com/pydantic -- name: XHS-Downloader - html_url: https://github.com/JoeanAmier/XHS-Downloader - stars: 8824 - owner_login: JoeanAmier - owner_html_url: https://github.com/JoeanAmier - name: SurfSense html_url: https://github.com/MODSetter/SurfSense - stars: 8257 + stars: 10243 owner_login: MODSetter owner_html_url: https://github.com/MODSetter -- name: FileCodeBox - html_url: https://github.com/vastsa/FileCodeBox - stars: 7367 - owner_login: vastsa - owner_html_url: https://github.com/vastsa +- name: XHS-Downloader + html_url: https://github.com/JoeanAmier/XHS-Downloader + stars: 9062 + owner_login: JoeanAmier + owner_html_url: https://github.com/JoeanAmier +- name: FastUI + html_url: https://github.com/pydantic/FastUI + stars: 8892 + owner_login: pydantic + owner_html_url: https://github.com/pydantic - name: polar html_url: https://github.com/polarsource/polar - stars: 7291 + stars: 8084 owner_login: polarsource owner_html_url: https://github.com/polarsource +- name: FileCodeBox + html_url: https://github.com/vastsa/FileCodeBox + stars: 7494 + owner_login: vastsa + owner_html_url: https://github.com/vastsa - name: nonebot2 html_url: https://github.com/nonebot/nonebot2 - stars: 7065 + stars: 7128 owner_login: nonebot owner_html_url: https://github.com/nonebot - name: hatchet html_url: https://github.com/hatchet-dev/hatchet - stars: 6070 + stars: 6155 owner_login: hatchet-dev owner_html_url: https://github.com/hatchet-dev - name: serge @@ -85,27 +90,27 @@ owner_html_url: https://github.com/serge-chat - name: fastapi-users html_url: https://github.com/fastapi-users/fastapi-users - stars: 5599 + stars: 5683 owner_login: fastapi-users owner_html_url: https://github.com/fastapi-users - name: strawberry html_url: https://github.com/strawberry-graphql/strawberry - stars: 4422 + stars: 4452 owner_login: strawberry-graphql owner_html_url: https://github.com/strawberry-graphql - name: chatgpt-web-share html_url: https://github.com/chatpire/chatgpt-web-share - stars: 4301 + stars: 4296 owner_login: chatpire owner_html_url: https://github.com/chatpire - name: poem html_url: https://github.com/poem-web/poem - stars: 4197 + stars: 4235 owner_login: poem-web owner_html_url: https://github.com/poem-web - name: dynaconf html_url: https://github.com/dynaconf/dynaconf - stars: 4144 + stars: 4174 owner_login: dynaconf owner_html_url: https://github.com/dynaconf - name: atrilabs-engine @@ -115,42 +120,42 @@ owner_html_url: https://github.com/Atri-Labs - name: Kokoro-FastAPI html_url: https://github.com/remsky/Kokoro-FastAPI - stars: 3739 + stars: 3875 owner_login: remsky owner_html_url: https://github.com/remsky - name: logfire html_url: https://github.com/pydantic/logfire - stars: 3614 + stars: 3717 owner_login: pydantic owner_html_url: https://github.com/pydantic - name: LitServe html_url: https://github.com/Lightning-AI/LitServe - stars: 3578 + stars: 3615 owner_login: Lightning-AI owner_html_url: https://github.com/Lightning-AI - name: datamodel-code-generator html_url: https://github.com/koxudaxi/datamodel-code-generator - stars: 3496 + stars: 3554 owner_login: koxudaxi owner_html_url: https://github.com/koxudaxi -- name: farfalle - html_url: https://github.com/rashadphz/farfalle - stars: 3459 - owner_login: rashadphz - owner_html_url: https://github.com/rashadphz -- name: fastapi-admin - html_url: https://github.com/fastapi-admin/fastapi-admin - stars: 3456 - owner_login: fastapi-admin - owner_html_url: https://github.com/fastapi-admin - name: huma html_url: https://github.com/danielgtaylor/huma - stars: 3447 + stars: 3521 owner_login: danielgtaylor owner_html_url: https://github.com/danielgtaylor +- name: fastapi-admin + html_url: https://github.com/fastapi-admin/fastapi-admin + stars: 3497 + owner_login: fastapi-admin + owner_html_url: https://github.com/fastapi-admin +- name: farfalle + html_url: https://github.com/rashadphz/farfalle + stars: 3476 + owner_login: rashadphz + owner_html_url: https://github.com/rashadphz - name: tracecat html_url: https://github.com/TracecatHQ/tracecat - stars: 3254 + stars: 3310 owner_login: TracecatHQ owner_html_url: https://github.com/TracecatHQ - name: opyrator @@ -160,336 +165,331 @@ owner_html_url: https://github.com/ml-tooling - name: docarray html_url: https://github.com/docarray/docarray - stars: 3107 + stars: 3108 owner_login: docarray owner_html_url: https://github.com/docarray - name: fastapi-realworld-example-app html_url: https://github.com/nsidnev/fastapi-realworld-example-app - stars: 2936 + stars: 2945 owner_login: nsidnev owner_html_url: https://github.com/nsidnev - name: uvicorn-gunicorn-fastapi-docker html_url: https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker - stars: 2804 + stars: 2809 owner_login: tiangolo owner_html_url: https://github.com/tiangolo -- name: best-of-web-python - html_url: https://github.com/ml-tooling/best-of-web-python - stars: 2610 - owner_login: ml-tooling - owner_html_url: https://github.com/ml-tooling +- name: devpush + html_url: https://github.com/hunvreus/devpush + stars: 2784 + owner_login: hunvreus + owner_html_url: https://github.com/hunvreus - name: mcp-context-forge html_url: https://github.com/IBM/mcp-context-forge - stars: 2572 + stars: 2763 owner_login: IBM owner_html_url: https://github.com/IBM +- name: best-of-web-python + html_url: https://github.com/ml-tooling/best-of-web-python + stars: 2630 + owner_login: ml-tooling + owner_html_url: https://github.com/ml-tooling - name: fastapi-react html_url: https://github.com/Buuntu/fastapi-react - stars: 2451 + stars: 2464 owner_login: Buuntu owner_html_url: https://github.com/Buuntu -- name: RasaGPT - html_url: https://github.com/paulpierre/RasaGPT - stars: 2441 - owner_login: paulpierre - owner_html_url: https://github.com/paulpierre - name: FastAPI-template html_url: https://github.com/s3rius/FastAPI-template - stars: 2424 + stars: 2453 owner_login: s3rius owner_html_url: https://github.com/s3rius +- name: RasaGPT + html_url: https://github.com/paulpierre/RasaGPT + stars: 2444 + owner_login: paulpierre + owner_html_url: https://github.com/paulpierre - name: sqladmin html_url: https://github.com/aminalaee/sqladmin - stars: 2357 + stars: 2423 owner_login: aminalaee owner_html_url: https://github.com/aminalaee - name: nextpy html_url: https://github.com/dot-agent/nextpy - stars: 2324 + stars: 2325 owner_login: dot-agent owner_html_url: https://github.com/dot-agent - name: supabase-py html_url: https://github.com/supabase/supabase-py - stars: 2236 + stars: 2292 owner_login: supabase owner_html_url: https://github.com/supabase - name: 30-Days-of-Python html_url: https://github.com/codingforentrepreneurs/30-Days-of-Python - stars: 2210 + stars: 2214 owner_login: codingforentrepreneurs owner_html_url: https://github.com/codingforentrepreneurs +- name: Yuxi-Know + html_url: https://github.com/xerrors/Yuxi-Know + stars: 2212 + owner_login: xerrors + owner_html_url: https://github.com/xerrors - name: langserve html_url: https://github.com/langchain-ai/langserve - stars: 2171 + stars: 2191 owner_login: langchain-ai owner_html_url: https://github.com/langchain-ai - name: fastapi-utils html_url: https://github.com/fastapiutils/fastapi-utils - stars: 2164 + stars: 2185 owner_login: fastapiutils owner_html_url: https://github.com/fastapiutils - name: solara html_url: https://github.com/widgetti/solara - stars: 2102 + stars: 2111 owner_login: widgetti owner_html_url: https://github.com/widgetti -- name: Yuxi-Know - html_url: https://github.com/xerrors/Yuxi-Know - stars: 1995 - owner_login: xerrors - owner_html_url: https://github.com/xerrors - name: mangum html_url: https://github.com/Kludex/mangum - stars: 1989 + stars: 2011 owner_login: Kludex owner_html_url: https://github.com/Kludex -- name: python-week-2022 - html_url: https://github.com/rochacbruno/python-week-2022 - stars: 1816 - owner_login: rochacbruno - owner_html_url: https://github.com/rochacbruno - name: agentkit html_url: https://github.com/BCG-X-Official/agentkit - stars: 1789 + stars: 1826 owner_login: BCG-X-Official owner_html_url: https://github.com/BCG-X-Official +- name: python-week-2022 + html_url: https://github.com/rochacbruno/python-week-2022 + stars: 1815 + owner_login: rochacbruno + owner_html_url: https://github.com/rochacbruno - name: manage-fastapi html_url: https://github.com/ycd/manage-fastapi - stars: 1780 + stars: 1787 owner_login: ycd owner_html_url: https://github.com/ycd - name: ormar html_url: https://github.com/collerek/ormar - stars: 1777 + stars: 1780 owner_login: collerek owner_html_url: https://github.com/collerek +- name: vue-fastapi-admin + html_url: https://github.com/mizhexiaoxiao/vue-fastapi-admin + stars: 1758 + owner_login: mizhexiaoxiao + owner_html_url: https://github.com/mizhexiaoxiao - name: openapi-python-client html_url: https://github.com/openapi-generators/openapi-python-client - stars: 1707 + stars: 1731 owner_login: openapi-generators owner_html_url: https://github.com/openapi-generators - name: piccolo html_url: https://github.com/piccolo-orm/piccolo - stars: 1695 + stars: 1711 owner_login: piccolo-orm owner_html_url: https://github.com/piccolo-orm -- name: vue-fastapi-admin - html_url: https://github.com/mizhexiaoxiao/vue-fastapi-admin - stars: 1695 - owner_login: mizhexiaoxiao - owner_html_url: https://github.com/mizhexiaoxiao - name: fastapi-cache html_url: https://github.com/long2ice/fastapi-cache - stars: 1653 + stars: 1677 owner_login: long2ice owner_html_url: https://github.com/long2ice +- name: slowapi + html_url: https://github.com/laurentS/slowapi + stars: 1669 + owner_login: laurentS + owner_html_url: https://github.com/laurentS - name: langchain-serve html_url: https://github.com/jina-ai/langchain-serve - stars: 1635 + stars: 1632 owner_login: jina-ai owner_html_url: https://github.com/jina-ai - name: termpair html_url: https://github.com/cs01/termpair - stars: 1624 + stars: 1621 owner_login: cs01 owner_html_url: https://github.com/cs01 -- name: slowapi - html_url: https://github.com/laurentS/slowapi - stars: 1620 - owner_login: laurentS - owner_html_url: https://github.com/laurentS +- name: FastAPI-boilerplate + html_url: https://github.com/benavlabs/FastAPI-boilerplate + stars: 1596 + owner_login: benavlabs + owner_html_url: https://github.com/benavlabs - name: coronavirus-tracker-api html_url: https://github.com/ExpDev07/coronavirus-tracker-api - stars: 1576 + stars: 1573 owner_login: ExpDev07 owner_html_url: https://github.com/ExpDev07 - name: fastapi-crudrouter html_url: https://github.com/awtkns/fastapi-crudrouter - stars: 1546 + stars: 1553 owner_login: awtkns owner_html_url: https://github.com/awtkns -- name: FastAPI-boilerplate - html_url: https://github.com/benavlabs/FastAPI-boilerplate - stars: 1516 - owner_login: benavlabs - owner_html_url: https://github.com/benavlabs - name: awesome-fastapi-projects html_url: https://github.com/Kludex/awesome-fastapi-projects - stars: 1481 + stars: 1485 owner_login: Kludex owner_html_url: https://github.com/Kludex - name: fastapi-pagination html_url: https://github.com/uriyyo/fastapi-pagination - stars: 1453 + stars: 1473 owner_login: uriyyo owner_html_url: https://github.com/uriyyo - name: bracket html_url: https://github.com/evroon/bracket - stars: 1415 + stars: 1470 owner_login: evroon owner_html_url: https://github.com/evroon -- name: awesome-python-resources - html_url: https://github.com/DjangoEx/awesome-python-resources - stars: 1413 - owner_login: DjangoEx - owner_html_url: https://github.com/DjangoEx -- name: fastapi-boilerplate - html_url: https://github.com/teamhide/fastapi-boilerplate - stars: 1406 - owner_login: teamhide - owner_html_url: https://github.com/teamhide -- name: budgetml - html_url: https://github.com/ebhy/budgetml - stars: 1346 - owner_login: ebhy - owner_html_url: https://github.com/ebhy -- name: fastapi-amis-admin - html_url: https://github.com/amisadmin/fastapi-amis-admin - stars: 1342 - owner_login: amisadmin - owner_html_url: https://github.com/amisadmin - name: fastapi-langgraph-agent-production-ready-template html_url: https://github.com/wassim249/fastapi-langgraph-agent-production-ready-template - stars: 1334 + stars: 1456 owner_login: wassim249 owner_html_url: https://github.com/wassim249 +- name: fastapi-boilerplate + html_url: https://github.com/teamhide/fastapi-boilerplate + stars: 1424 + owner_login: teamhide + owner_html_url: https://github.com/teamhide +- name: awesome-python-resources + html_url: https://github.com/DjangoEx/awesome-python-resources + stars: 1420 + owner_login: DjangoEx + owner_html_url: https://github.com/DjangoEx +- name: fastapi-amis-admin + html_url: https://github.com/amisadmin/fastapi-amis-admin + stars: 1363 + owner_login: amisadmin + owner_html_url: https://github.com/amisadmin +- name: fastcrud + html_url: https://github.com/benavlabs/fastcrud + stars: 1362 + owner_login: benavlabs + owner_html_url: https://github.com/benavlabs +- name: budgetml + html_url: https://github.com/ebhy/budgetml + stars: 1345 + owner_login: ebhy + owner_html_url: https://github.com/ebhy - name: fastapi-tutorial html_url: https://github.com/liaogx/fastapi-tutorial - stars: 1303 + stars: 1315 owner_login: liaogx owner_html_url: https://github.com/liaogx - name: fastapi_best_architecture html_url: https://github.com/fastapi-practices/fastapi_best_architecture - stars: 1276 + stars: 1311 owner_login: fastapi-practices owner_html_url: https://github.com/fastapi-practices -- name: fastcrud - html_url: https://github.com/benavlabs/fastcrud - stars: 1272 - owner_login: benavlabs - owner_html_url: https://github.com/benavlabs - name: fastapi-code-generator html_url: https://github.com/koxudaxi/fastapi-code-generator - stars: 1253 + stars: 1270 owner_login: koxudaxi owner_html_url: https://github.com/koxudaxi - name: prometheus-fastapi-instrumentator html_url: https://github.com/trallnag/prometheus-fastapi-instrumentator - stars: 1246 + stars: 1264 owner_login: trallnag owner_html_url: https://github.com/trallnag -- name: bolt-python - html_url: https://github.com/slackapi/bolt-python - stars: 1221 - owner_login: slackapi - owner_html_url: https://github.com/slackapi - name: bedrock-chat html_url: https://github.com/aws-samples/bedrock-chat - stars: 1220 + stars: 1243 owner_login: aws-samples owner_html_url: https://github.com/aws-samples +- name: bolt-python + html_url: https://github.com/slackapi/bolt-python + stars: 1238 + owner_login: slackapi + owner_html_url: https://github.com/slackapi - name: fastapi_production_template html_url: https://github.com/zhanymkanov/fastapi_production_template - stars: 1202 + stars: 1209 owner_login: zhanymkanov owner_html_url: https://github.com/zhanymkanov - name: fastapi-scaff html_url: https://github.com/atpuxiner/fastapi-scaff - stars: 1193 + stars: 1200 owner_login: atpuxiner owner_html_url: https://github.com/atpuxiner - name: langchain-extract html_url: https://github.com/langchain-ai/langchain-extract - stars: 1164 + stars: 1173 owner_login: langchain-ai owner_html_url: https://github.com/langchain-ai - name: fastapi-alembic-sqlmodel-async html_url: https://github.com/jonra1993/fastapi-alembic-sqlmodel-async - stars: 1149 + stars: 1162 owner_login: jonra1993 owner_html_url: https://github.com/jonra1993 - name: odmantic html_url: https://github.com/art049/odmantic - stars: 1133 + stars: 1137 owner_login: art049 owner_html_url: https://github.com/art049 - name: restish html_url: https://github.com/rest-sh/restish - stars: 1122 + stars: 1129 owner_login: rest-sh owner_html_url: https://github.com/rest-sh -- name: runhouse - html_url: https://github.com/run-house/runhouse - stars: 1047 +- name: kubetorch + html_url: https://github.com/run-house/kubetorch + stars: 1065 owner_login: run-house owner_html_url: https://github.com/run-house - name: flock html_url: https://github.com/Onelevenvy/flock - stars: 1027 + stars: 1039 owner_login: Onelevenvy owner_html_url: https://github.com/Onelevenvy - name: authx html_url: https://github.com/yezz123/authx - stars: 999 + stars: 1017 owner_login: yezz123 owner_html_url: https://github.com/yezz123 - name: autollm html_url: https://github.com/viddexa/autollm - stars: 999 + stars: 997 owner_login: viddexa owner_html_url: https://github.com/viddexa - name: lanarky html_url: https://github.com/ajndkr/lanarky - stars: 995 + stars: 993 owner_login: ajndkr owner_html_url: https://github.com/ajndkr -- name: titiler - html_url: https://github.com/developmentseed/titiler - stars: 952 - owner_login: developmentseed - owner_html_url: https://github.com/developmentseed -- name: energy-forecasting - html_url: https://github.com/iusztinpaul/energy-forecasting - stars: 946 - owner_login: iusztinpaul - owner_html_url: https://github.com/iusztinpaul -- name: secure - html_url: https://github.com/TypeError/secure - stars: 944 - owner_login: TypeError - owner_html_url: https://github.com/TypeError -- name: langcorn - html_url: https://github.com/msoedov/langcorn - stars: 934 - owner_login: msoedov - owner_html_url: https://github.com/msoedov - name: RuoYi-Vue3-FastAPI html_url: https://github.com/insistence/RuoYi-Vue3-FastAPI - stars: 930 + stars: 974 owner_login: insistence owner_html_url: https://github.com/insistence - name: aktools html_url: https://github.com/akfamily/aktools - stars: 916 + stars: 972 owner_login: akfamily owner_html_url: https://github.com/akfamily +- name: titiler + html_url: https://github.com/developmentseed/titiler + stars: 965 + owner_login: developmentseed + owner_html_url: https://github.com/developmentseed +- name: secure + html_url: https://github.com/TypeError/secure + stars: 953 + owner_login: TypeError + owner_html_url: https://github.com/TypeError +- name: energy-forecasting + html_url: https://github.com/iusztinpaul/energy-forecasting + stars: 949 + owner_login: iusztinpaul + owner_html_url: https://github.com/iusztinpaul - name: every-pdf html_url: https://github.com/DDULDDUCK/every-pdf - stars: 907 + stars: 942 owner_login: DDULDDUCK owner_html_url: https://github.com/DDULDDUCK -- name: marker-api - html_url: https://github.com/adithya-s-k/marker-api - stars: 903 - owner_login: adithya-s-k - owner_html_url: https://github.com/adithya-s-k +- name: langcorn + html_url: https://github.com/msoedov/langcorn + stars: 933 + owner_login: msoedov + owner_html_url: https://github.com/msoedov - name: fastapi-observability html_url: https://github.com/blueswen/fastapi-observability - stars: 902 + stars: 923 owner_login: blueswen owner_html_url: https://github.com/blueswen -- name: fastapi-do-zero - html_url: https://github.com/dunossauro/fastapi-do-zero - stars: 900 - owner_login: dunossauro - owner_html_url: https://github.com/dunossauro From f8df43d734c6982c4ad5c6094b05179d0326583b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 3 Nov 2025 10:24:09 +0100 Subject: [PATCH 076/256] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20Peopl?= =?UTF-8?q?e=20-=20Sponsors=20(#14274)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions --- docs/en/data/github_sponsors.yml | 205 ++++++++++++++++--------------- 1 file changed, 107 insertions(+), 98 deletions(-) diff --git a/docs/en/data/github_sponsors.yml b/docs/en/data/github_sponsors.yml index 7b34719b6..3d8ecdb7a 100644 --- a/docs/en/data/github_sponsors.yml +++ b/docs/en/data/github_sponsors.yml @@ -14,6 +14,9 @@ sponsors: - login: coderabbitai avatarUrl: https://avatars.githubusercontent.com/u/132028505?v=4 url: https://github.com/coderabbitai + - login: greptileai + avatarUrl: https://avatars.githubusercontent.com/u/140149887?v=4 + url: https://github.com/greptileai - login: subtotal avatarUrl: https://avatars.githubusercontent.com/u/176449348?v=4 url: https://github.com/subtotal @@ -41,9 +44,9 @@ sponsors: - login: permitio avatarUrl: https://avatars.githubusercontent.com/u/71775833?v=4 url: https://github.com/permitio -- - login: marvin-robot - avatarUrl: https://avatars.githubusercontent.com/u/41086007?u=b9fcab402d0cd0aec738b6574fe60855cb0cd36d&v=4 - url: https://github.com/marvin-robot +- - login: BoostryJP + avatarUrl: https://avatars.githubusercontent.com/u/57932412?v=4 + url: https://github.com/BoostryJP - login: mercedes-benz avatarUrl: https://avatars.githubusercontent.com/u/34240465?v=4 url: https://github.com/mercedes-benz @@ -53,9 +56,9 @@ sponsors: - login: LambdaTest-Inc avatarUrl: https://avatars.githubusercontent.com/u/171592363?u=96606606a45fa170427206199014f2a5a2a4920b&v=4 url: https://github.com/LambdaTest-Inc - - login: BoostryJP - avatarUrl: https://avatars.githubusercontent.com/u/57932412?v=4 - url: https://github.com/BoostryJP + - login: requestly + avatarUrl: https://avatars.githubusercontent.com/u/12287519?v=4 + url: https://github.com/requestly - login: acsone avatarUrl: https://avatars.githubusercontent.com/u/7601056?v=4 url: https://github.com/acsone @@ -71,27 +74,39 @@ sponsors: - - login: mainframeindustries avatarUrl: https://avatars.githubusercontent.com/u/55092103?v=4 url: https://github.com/mainframeindustries - - login: yasyf - avatarUrl: https://avatars.githubusercontent.com/u/709645?u=f36736b3c6a85f578886ecc42a740e7b436e7a01&v=4 - url: https://github.com/yasyf - - login: alixlahuec avatarUrl: https://avatars.githubusercontent.com/u/29543316?u=44357eb2a93bccf30fb9d389b8befe94a3d00985&v=4 url: https://github.com/alixlahuec + - login: Partho + avatarUrl: https://avatars.githubusercontent.com/u/2034301?u=ce195ac36835cca0cdfe6dd6e897bd38873a1524&v=4 + url: https://github.com/Partho - - login: primer-io avatarUrl: https://avatars.githubusercontent.com/u/62146168?v=4 url: https://github.com/primer-io -- - login: nilslindemann - avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 - url: https://github.com/nilslindemann - - login: upciti + - login: xsalagarcia + avatarUrl: https://avatars.githubusercontent.com/u/66035908?v=4 + url: https://github.com/xsalagarcia +- - login: upciti avatarUrl: https://avatars.githubusercontent.com/u/43346262?v=4 url: https://github.com/upciti - - login: thisisfixer - avatarUrl: https://avatars.githubusercontent.com/u/14433035?u=076d52a5b7891c764904af9f462bfb45428e25df&v=4 - url: https://github.com/thisisfixer + - login: GonnaFlyMethod + avatarUrl: https://avatars.githubusercontent.com/u/60840539?u=edf70b373fd4f1a83d3eb7c6802f4b6addb572cf&v=4 + url: https://github.com/GonnaFlyMethod + - login: ChargeStorm + avatarUrl: https://avatars.githubusercontent.com/u/26000165?v=4 + url: https://github.com/ChargeStorm + - login: DanielYang59 + avatarUrl: https://avatars.githubusercontent.com/u/80093591?u=63873f701c7c74aac83c906800a1dddc0bc8c92f&v=4 + url: https://github.com/DanielYang59 + - login: nilslindemann + avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 + url: https://github.com/nilslindemann - - login: samuelcolvin avatarUrl: https://avatars.githubusercontent.com/u/4039449?u=42eb3b833047c8c4b4f647a031eaef148c16d93f&v=4 url: https://github.com/samuelcolvin + - login: vincentkoc + avatarUrl: https://avatars.githubusercontent.com/u/25068?u=fbd5b2d51142daa4bdbc21e21953a3b8b8188a4a&v=4 + url: https://github.com/vincentkoc - login: otosky avatarUrl: https://avatars.githubusercontent.com/u/42260747?u=69d089387c743d89427aa4ad8740cfb34045a9e0&v=4 url: https://github.com/otosky @@ -101,6 +116,9 @@ sponsors: - login: roboflow avatarUrl: https://avatars.githubusercontent.com/u/53104118?v=4 url: https://github.com/roboflow + - login: dudikbender + avatarUrl: https://avatars.githubusercontent.com/u/53487583?u=3a57542938ebfd57579a0111db2b297e606d9681&v=4 + url: https://github.com/dudikbender - login: ehaca avatarUrl: https://avatars.githubusercontent.com/u/25950317?u=cec1a3e0643b785288ae8260cc295a85ab344995&v=4 url: https://github.com/ehaca @@ -113,21 +131,15 @@ sponsors: - login: Leay15 avatarUrl: https://avatars.githubusercontent.com/u/32212558?u=c4aa9c1737e515959382a5515381757b1fd86c53&v=4 url: https://github.com/Leay15 - - login: kaoru0310 - avatarUrl: https://avatars.githubusercontent.com/u/80977929?u=1b61d10142b490e56af932ddf08a390fae8ee94f&v=4 - url: https://github.com/kaoru0310 - - login: DelfinaCare - avatarUrl: https://avatars.githubusercontent.com/u/83734439?v=4 - url: https://github.com/DelfinaCare - login: Karine-Bauch avatarUrl: https://avatars.githubusercontent.com/u/90465103?u=7feb1018abb1a5631cfd9a91fea723d1ceb5f49b&v=4 url: https://github.com/Karine-Bauch - login: jugeeem avatarUrl: https://avatars.githubusercontent.com/u/116043716?u=ae590d79c38ac79c91b9c5caa6887d061e865a3d&v=4 url: https://github.com/jugeeem - - login: dudikbender - avatarUrl: https://avatars.githubusercontent.com/u/53487583?u=3a57542938ebfd57579a0111db2b297e606d9681&v=4 - url: https://github.com/dudikbender + - login: connorpark24 + avatarUrl: https://avatars.githubusercontent.com/u/142128990?u=09b84a4beb1f629b77287a837bcf3729785cdd89&v=4 + url: https://github.com/connorpark24 - login: patsatsia avatarUrl: https://avatars.githubusercontent.com/u/61111267?u=3271b85f7a37b479c8d0ae0a235182e83c166edf&v=4 url: https://github.com/patsatsia @@ -140,9 +152,12 @@ sponsors: - login: chickenandstats avatarUrl: https://avatars.githubusercontent.com/u/79477966?u=ae2b894aa954070db1d7830dab99b49eba4e4567&v=4 url: https://github.com/chickenandstats - - login: dodo5522 - avatarUrl: https://avatars.githubusercontent.com/u/1362607?u=9bf1e0e520cccc547c046610c468ce6115bbcf9f&v=4 - url: https://github.com/dodo5522 + - login: kaoru0310 + avatarUrl: https://avatars.githubusercontent.com/u/80977929?u=1b61d10142b490e56af932ddf08a390fae8ee94f&v=4 + url: https://github.com/kaoru0310 + - login: DelfinaCare + avatarUrl: https://avatars.githubusercontent.com/u/83734439?v=4 + url: https://github.com/DelfinaCare - login: knallgelb avatarUrl: https://avatars.githubusercontent.com/u/2358812?u=c48cb6362b309d74cbf144bd6ad3aed3eb443e82&v=4 url: https://github.com/knallgelb @@ -170,9 +185,12 @@ sponsors: - login: Ryandaydev avatarUrl: https://avatars.githubusercontent.com/u/4292423?u=679ff84cb7b988c5795a5fa583857f574a055763&v=4 url: https://github.com/Ryandaydev - - login: vincentkoc - avatarUrl: https://avatars.githubusercontent.com/u/25068?u=fbd5b2d51142daa4bdbc21e21953a3b8b8188a4a&v=4 - url: https://github.com/vincentkoc + - login: jaredtrog + avatarUrl: https://avatars.githubusercontent.com/u/4381365?v=4 + url: https://github.com/jaredtrog + - login: oliverxchen + avatarUrl: https://avatars.githubusercontent.com/u/4471774?u=534191f25e32eeaadda22dfab4b0a428733d5489&v=4 + url: https://github.com/oliverxchen - login: jstanden avatarUrl: https://avatars.githubusercontent.com/u/63288?u=c3658d57d2862c607a0e19c2101c3c51876e36ad&v=4 url: https://github.com/jstanden @@ -197,6 +215,9 @@ sponsors: - login: mintuhouse avatarUrl: https://avatars.githubusercontent.com/u/769950?u=ecfbd79a97d33177e0d093ddb088283cf7fe8444&v=4 url: https://github.com/mintuhouse + - login: dodo5522 + avatarUrl: https://avatars.githubusercontent.com/u/1362607?u=9bf1e0e520cccc547c046610c468ce6115bbcf9f&v=4 + url: https://github.com/dodo5522 - login: wdwinslow avatarUrl: https://avatars.githubusercontent.com/u/11562137?u=371272f2c69e680e0559a7b0a57385e83a5dc728&v=4 url: https://github.com/wdwinslow @@ -212,18 +233,15 @@ sponsors: - login: mjohnsey avatarUrl: https://avatars.githubusercontent.com/u/16784016?u=38fad2e6b411244560b3af99c5f5a4751bc81865&v=4 url: https://github.com/mjohnsey + - login: enguy-hub + avatarUrl: https://avatars.githubusercontent.com/u/16822912?u=2c45f9e7f427b2f2f3b023d7fdb0d44764c92ae8&v=4 + url: https://github.com/enguy-hub - login: ashi-agrawal avatarUrl: https://avatars.githubusercontent.com/u/17105294?u=99c7a854035e5398d8e7b674f2d42baae6c957f8&v=4 url: https://github.com/ashi-agrawal - login: RaamEEIL avatarUrl: https://avatars.githubusercontent.com/u/20320552?v=4 url: https://github.com/RaamEEIL - - login: jaredtrog - avatarUrl: https://avatars.githubusercontent.com/u/4381365?v=4 - url: https://github.com/jaredtrog - - login: oliverxchen - avatarUrl: https://avatars.githubusercontent.com/u/4471774?u=534191f25e32eeaadda22dfab4b0a428733d5489&v=4 - url: https://github.com/oliverxchen - login: ternaus avatarUrl: https://avatars.githubusercontent.com/u/5481618?u=513a26b02a39e7a28d587cd37c6cc877ea368e6e&v=4 url: https://github.com/ternaus @@ -242,7 +260,10 @@ sponsors: - - login: manoelpqueiroz avatarUrl: https://avatars.githubusercontent.com/u/23669137?u=b12e84b28a84369ab5b30bd5a79e5788df5a0756&v=4 url: https://github.com/manoelpqueiroz -- - login: pawamoy +- - login: ceb10n + avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 + url: https://github.com/ceb10n + - login: pawamoy avatarUrl: https://avatars.githubusercontent.com/u/3999221?u=b030e4c89df2f3a36bc4710b925bdeb6745c9856&v=4 url: https://github.com/pawamoy - login: siavashyj @@ -260,9 +281,9 @@ sponsors: - login: hgalytoby avatarUrl: https://avatars.githubusercontent.com/u/50397689?u=6cc9028f3db63f8f60ad21c17b1ce4b88c4e2e60&v=4 url: https://github.com/hgalytoby - - login: nisutec - avatarUrl: https://avatars.githubusercontent.com/u/25281462?u=e562484c451fdfc59053163f64405f8eb262b8b0&v=4 - url: https://github.com/nisutec + - login: johnl28 + avatarUrl: https://avatars.githubusercontent.com/u/54412955?u=47dd06082d1c39caa90c752eb55566e4f3813957&v=4 + url: https://github.com/johnl28 - login: hoenie-ams avatarUrl: https://avatars.githubusercontent.com/u/25708487?u=cda07434f0509ac728d9edf5e681117c0f6b818b&v=4 url: https://github.com/hoenie-ams @@ -278,33 +299,21 @@ sponsors: - login: petercool avatarUrl: https://avatars.githubusercontent.com/u/37613029?u=75aa8c6729e6e8f85a300561c4dbeef9d65c8797&v=4 url: https://github.com/petercool - - login: JulioPeixoto - avatarUrl: https://avatars.githubusercontent.com/u/96303574?u=27d4614350cae33653f1be35cb47c92a12627ac9&v=4 - url: https://github.com/JulioPeixoto - - login: johnl28 - avatarUrl: https://avatars.githubusercontent.com/u/54412955?u=47dd06082d1c39caa90c752eb55566e4f3813957&v=4 - url: https://github.com/johnl28 - login: PunRabbit avatarUrl: https://avatars.githubusercontent.com/u/70463212?u=1a835cfbc99295a60c8282f6aa6199d1b42241a5&v=4 url: https://github.com/PunRabbit - login: PelicanQ avatarUrl: https://avatars.githubusercontent.com/u/77930606?v=4 url: https://github.com/PelicanQ - - login: miguelgr - avatarUrl: https://avatars.githubusercontent.com/u/1484589?u=54556072b8136efa12ae3b6902032ea2a39ace4b&v=4 - url: https://github.com/miguelgr - - login: WillHogan - avatarUrl: https://avatars.githubusercontent.com/u/1661551?u=8a80356e3e7d5a417157aba7ea565dabc8678327&v=4 - url: https://github.com/WillHogan - login: my3 avatarUrl: https://avatars.githubusercontent.com/u/1825270?v=4 url: https://github.com/my3 - - login: Alisa-lisa - avatarUrl: https://avatars.githubusercontent.com/u/4137964?u=e7e393504f554f4ff15863a1e01a5746863ef9ce&v=4 - url: https://github.com/Alisa-lisa - - login: moonape1226 - avatarUrl: https://avatars.githubusercontent.com/u/8532038?u=d9f8b855a429fff9397c3833c2ff83849ebf989d&v=4 - url: https://github.com/moonape1226 + - login: danielunderwood + avatarUrl: https://avatars.githubusercontent.com/u/4472301?v=4 + url: https://github.com/danielunderwood + - login: rangulvers + avatarUrl: https://avatars.githubusercontent.com/u/5235430?u=e254d4af4ace5a05fa58372ae677c7d26f0d5a53&v=4 + url: https://github.com/rangulvers - login: ddanier avatarUrl: https://avatars.githubusercontent.com/u/113563?u=ed1dc79de72f93bd78581f88ebc6952b62f472da&v=4 url: https://github.com/ddanier @@ -314,24 +323,18 @@ sponsors: - login: slafs avatarUrl: https://avatars.githubusercontent.com/u/210173?v=4 url: https://github.com/slafs - - login: ceb10n - avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 - url: https://github.com/ceb10n - login: tochikuji avatarUrl: https://avatars.githubusercontent.com/u/851759?v=4 url: https://github.com/tochikuji - - login: xncbf - avatarUrl: https://avatars.githubusercontent.com/u/9462045?u=a80a7bb349555b277645632ed66639ff43400614&v=4 - url: https://github.com/xncbf - - login: DMantis - avatarUrl: https://avatars.githubusercontent.com/u/9536869?u=652dd0d49717803c0cbcbf44f7740e53cf2d4892&v=4 - url: https://github.com/DMantis + - login: miguelgr + avatarUrl: https://avatars.githubusercontent.com/u/1484589?u=54556072b8136efa12ae3b6902032ea2a39ace4b&v=4 + url: https://github.com/miguelgr + - login: WillHogan + avatarUrl: https://avatars.githubusercontent.com/u/1661551?u=8a80356e3e7d5a417157aba7ea565dabc8678327&v=4 + url: https://github.com/WillHogan - login: hard-coders avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 url: https://github.com/hard-coders - - login: supdann - avatarUrl: https://avatars.githubusercontent.com/u/9986994?u=9671810f4ae9504c063227fee34fd47567ff6954&v=4 - url: https://github.com/supdann - login: mntolia avatarUrl: https://avatars.githubusercontent.com/u/10390224?v=4 url: https://github.com/mntolia @@ -344,12 +347,9 @@ sponsors: - login: joshuatz avatarUrl: https://avatars.githubusercontent.com/u/17817563?u=f1bf05b690d1fc164218f0b420cdd3acb7913e21&v=4 url: https://github.com/joshuatz - - login: danielunderwood - avatarUrl: https://avatars.githubusercontent.com/u/4472301?v=4 - url: https://github.com/danielunderwood - - login: rangulvers - avatarUrl: https://avatars.githubusercontent.com/u/5235430?u=e254d4af4ace5a05fa58372ae677c7d26f0d5a53&v=4 - url: https://github.com/rangulvers + - login: nisutec + avatarUrl: https://avatars.githubusercontent.com/u/25281462?u=e562484c451fdfc59053163f64405f8eb262b8b0&v=4 + url: https://github.com/nisutec - login: sdevkota avatarUrl: https://avatars.githubusercontent.com/u/5250987?u=4ed9a120c89805a8aefda1cbdc0cf6512e64d1b4&v=4 url: https://github.com/sdevkota @@ -365,39 +365,45 @@ sponsors: - login: harsh183 avatarUrl: https://avatars.githubusercontent.com/u/7780198?v=4 url: https://github.com/harsh183 -- - login: KOZ39 - avatarUrl: https://avatars.githubusercontent.com/u/38822500?u=9dfc0a697df1c9628f08e20dc3fb17b1afc4e5a7&v=4 - url: https://github.com/KOZ39 - - login: rwxd - avatarUrl: https://avatars.githubusercontent.com/u/40308458?u=cd04a39e3655923be4f25c2ba8a5a07b3da3230a&v=4 - url: https://github.com/rwxd - - login: morzan1001 + - login: moonape1226 + avatarUrl: https://avatars.githubusercontent.com/u/8532038?u=d9f8b855a429fff9397c3833c2ff83849ebf989d&v=4 + url: https://github.com/moonape1226 + - login: xncbf + avatarUrl: https://avatars.githubusercontent.com/u/9462045?u=a80a7bb349555b277645632ed66639ff43400614&v=4 + url: https://github.com/xncbf + - login: DMantis + avatarUrl: https://avatars.githubusercontent.com/u/9536869?u=652dd0d49717803c0cbcbf44f7740e53cf2d4892&v=4 + url: https://github.com/DMantis +- - login: morzan1001 avatarUrl: https://avatars.githubusercontent.com/u/47593005?u=c30ab7230f82a12a9b938dcb54f84a996931409a&v=4 url: https://github.com/morzan1001 - - login: azharthegeek - avatarUrl: https://avatars.githubusercontent.com/u/51288109?u=0987b2a9f39c21ccb071b6bdce0fc60d8492f8e8&v=4 - url: https://github.com/azharthegeek - - login: Olegt0rr - avatarUrl: https://avatars.githubusercontent.com/u/25399456?u=3e87b5239a2f4600975ba13be73054f8567c6060&v=4 - url: https://github.com/Olegt0rr - login: larsyngvelundin avatarUrl: https://avatars.githubusercontent.com/u/34173819?u=74958599695bf83ac9f1addd935a51548a10c6b0&v=4 url: https://github.com/larsyngvelundin - login: andrecorumba avatarUrl: https://avatars.githubusercontent.com/u/37807517?u=9b9be3b41da9bda60957da9ef37b50dbf65baa61&v=4 url: https://github.com/andrecorumba - - login: ChenPu2002 - avatarUrl: https://avatars.githubusercontent.com/u/113831763?v=4 - url: https://github.com/ChenPu2002 + - login: KOZ39 + avatarUrl: https://avatars.githubusercontent.com/u/38822500?u=9dfc0a697df1c9628f08e20dc3fb17b1afc4e5a7&v=4 + url: https://github.com/KOZ39 + - login: rwxd + avatarUrl: https://avatars.githubusercontent.com/u/40308458?u=cd04a39e3655923be4f25c2ba8a5a07b3da3230a&v=4 + url: https://github.com/rwxd + - login: hippoley + avatarUrl: https://avatars.githubusercontent.com/u/135493401?u=1164ef48a645a7c12664fabc1638fbb7e1c459b0&v=4 + url: https://github.com/hippoley - login: CoderDeltaLAN avatarUrl: https://avatars.githubusercontent.com/u/152043745?u=4ff541efffb7d134e60c5fcf2dd1e343f90bb782&v=4 url: https://github.com/CoderDeltaLAN - - login: aghents - avatarUrl: https://avatars.githubusercontent.com/u/60949885?u=d8616ddf22cf998a712cdceefd6a0256a178fe9d&v=4 - url: https://github.com/aghents - - login: 0ne-stone + - login: chris1ding1 + avatarUrl: https://avatars.githubusercontent.com/u/194386334?u=5500604b50e35ed8a5aeb82ce34aa5d3ee3f88c7&v=4 + url: https://github.com/chris1ding1 + - login: onestn avatarUrl: https://avatars.githubusercontent.com/u/62360849?u=746dd21c34e7e06eefb11b03e8bb01aaae3c2a4f&v=4 - url: https://github.com/0ne-stone + url: https://github.com/onestn + - login: Rubinskiy + avatarUrl: https://avatars.githubusercontent.com/u/62457878?u=f2e35ed3d196a99cfadb5a29a91950342af07e34&v=4 + url: https://github.com/Rubinskiy - login: nayasinghania avatarUrl: https://avatars.githubusercontent.com/u/74111380?u=752e99a5e139389fdc0a0677122adc08438eb076&v=4 url: https://github.com/nayasinghania @@ -407,6 +413,9 @@ sponsors: - login: andreagrandi avatarUrl: https://avatars.githubusercontent.com/u/636391?u=13d90cb8ec313593a5b71fbd4e33b78d6da736f5&v=4 url: https://github.com/andreagrandi + - login: Olegt0rr + avatarUrl: https://avatars.githubusercontent.com/u/25399456?u=3e87b5239a2f4600975ba13be73054f8567c6060&v=4 + url: https://github.com/Olegt0rr - login: msserpa avatarUrl: https://avatars.githubusercontent.com/u/6334934?u=82c4489eb1559d88d2990d60001901b14f722bbb&v=4 url: https://github.com/msserpa From 940ee0c9c357f749ede6dd0d8d8dde361c1ef883 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 3 Nov 2025 09:24:17 +0000 Subject: [PATCH 077/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 303017d9d..e9c013a7c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* 👥 Update FastAPI GitHub topic repositories. PR [#14280](https://github.com/fastapi/fastapi/pull/14280) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump mkdocs-macros-plugin from 1.4.0 to 1.4.1. PR [#14277](https://github.com/fastapi/fastapi/pull/14277) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocstrings[python] from 0.26.1 to 0.30.1. PR [#14279](https://github.com/fastapi/fastapi/pull/14279) by [@dependabot[bot]](https://github.com/apps/dependabot). From 566e0d60b29f3fe302c0a8561d108621e496d009 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 3 Nov 2025 10:24:36 +0100 Subject: [PATCH 078/256] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20Peopl?= =?UTF-8?q?e=20-=20Contributors=20and=20Translators=20(#14273)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions --- docs/en/data/contributors.yml | 52 +++++++++++++------------- docs/en/data/translation_reviewers.yml | 8 ++-- docs/en/data/translators.yml | 12 +++--- 3 files changed, 36 insertions(+), 36 deletions(-) diff --git a/docs/en/data/contributors.yml b/docs/en/data/contributors.yml index c892d8baf..592c79af0 100644 --- a/docs/en/data/contributors.yml +++ b/docs/en/data/contributors.yml @@ -1,11 +1,11 @@ tiangolo: login: tiangolo - count: 782 + count: 794 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 url: https://github.com/tiangolo dependabot: login: dependabot - count: 117 + count: 126 avatarUrl: https://avatars.githubusercontent.com/in/29110?v=4 url: https://github.com/apps/dependabot alejsdev: @@ -15,7 +15,7 @@ alejsdev: url: https://github.com/alejsdev pre-commit-ci: login: pre-commit-ci - count: 45 + count: 49 avatarUrl: https://avatars.githubusercontent.com/in/68672?v=4 url: https://github.com/apps/pre-commit-ci github-actions: @@ -25,7 +25,7 @@ github-actions: url: https://github.com/apps/github-actions Kludex: login: Kludex - count: 24 + count: 25 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4 url: https://github.com/Kludex dmontagu: @@ -33,36 +33,36 @@ dmontagu: count: 17 avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4 url: https://github.com/dmontagu +YuriiMotov: + login: YuriiMotov + count: 15 + avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4 + url: https://github.com/YuriiMotov +nilslindemann: + login: nilslindemann + count: 14 + avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 + url: https://github.com/nilslindemann euri10: login: euri10 count: 13 avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4 url: https://github.com/euri10 -nilslindemann: - login: nilslindemann +svlandeg: + login: svlandeg count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 - url: https://github.com/nilslindemann + avatarUrl: https://avatars.githubusercontent.com/u/8796347?u=556c97650c27021911b0b9447ec55e75987b0e8a&v=4 + url: https://github.com/svlandeg kantandane: login: kantandane count: 13 avatarUrl: https://avatars.githubusercontent.com/u/3978368?u=cccc199291f991a73b1ebba5abc735a948e0bd16&v=4 url: https://github.com/kantandane -svlandeg: - login: svlandeg - count: 11 - avatarUrl: https://avatars.githubusercontent.com/u/8796347?u=556c97650c27021911b0b9447ec55e75987b0e8a&v=4 - url: https://github.com/svlandeg zhaohan-dong: login: zhaohan-dong count: 11 avatarUrl: https://avatars.githubusercontent.com/u/65422392?u=8260f8781f50248410ebfa4c9bf70e143fe5c9f2&v=4 url: https://github.com/zhaohan-dong -YuriiMotov: - login: YuriiMotov - count: 10 - avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4 - url: https://github.com/YuriiMotov mariacamilagl: login: mariacamilagl count: 9 @@ -158,6 +158,11 @@ prostomarkeloff: count: 3 avatarUrl: https://avatars.githubusercontent.com/u/28061158?u=6918e39a1224194ba636e897461a02a20126d7ad&v=4 url: https://github.com/prostomarkeloff +frankie567: + login: frankie567 + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=f3e79acfe4ed207e15c2145161a8a9759925fcd2&v=4 + url: https://github.com/frankie567 nsidnev: login: nsidnev count: 3 @@ -191,7 +196,7 @@ Serrones: uriyyo: login: uriyyo count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/32038156?u=0c68019beb28381ce5205a838937c61e0fe3fee2&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/32038156?u=c26ca9b821fcf6499b84db75f553d4980bf8d023&v=4 url: https://github.com/uriyyo andrew222651: login: andrew222651 @@ -261,7 +266,7 @@ Nimitha-jagadeesha: lucaromagnoli: login: lucaromagnoli count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/38782977?u=e66396859f493b4ddcb3a837a1b2b2039c805417&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/38782977?u=15df02e806a2293af40ac619fba11dbe3c0c4fd4&v=4 url: https://github.com/lucaromagnoli salmantec: login: salmantec @@ -328,11 +333,6 @@ svalouch: count: 2 avatarUrl: https://avatars.githubusercontent.com/u/54674660?v=4 url: https://github.com/svalouch -frankie567: - login: frankie567 - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=f3e79acfe4ed207e15c2145161a8a9759925fcd2&v=4 - url: https://github.com/frankie567 marier-nico: login: marier-nico count: 2 @@ -346,7 +346,7 @@ Dustyposa: aviramha: login: aviramha count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/41201924?u=6883cc4fc13a7b2e60d4deddd4be06f9c5287880&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/41201924?u=ce5d3ea7037c2e6b3f82eff87e2217d4fb63214b&v=4 url: https://github.com/aviramha iwpnd: login: iwpnd diff --git a/docs/en/data/translation_reviewers.yml b/docs/en/data/translation_reviewers.yml index 68ef01f6d..45aa55e5e 100644 --- a/docs/en/data/translation_reviewers.yml +++ b/docs/en/data/translation_reviewers.yml @@ -776,7 +776,7 @@ pablocm83: d2a-raudenaerde: login: d2a-raudenaerde count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/5213150?v=4 + avatarUrl: https://avatars.githubusercontent.com/u/5213150?u=e6d0ef65c571c7e544fc1c7ec151c7c0a72fb6bb&v=4 url: https://github.com/d2a-raudenaerde valentinDruzhinin: login: valentinDruzhinin @@ -1206,7 +1206,7 @@ akagaeng: phamquanganh31101998: login: phamquanganh31101998 count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/43257497?u=36fa4ee689415d869a98453083a7c4213d2136ee&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/43257497?u=6b3419ea9e318c356c42a973fb947682590bd8d3&v=4 url: https://github.com/phamquanganh31101998 peebbv6364: login: peebbv6364 @@ -1806,7 +1806,7 @@ MrL8199: ivintoiu: login: ivintoiu count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/1853336?u=5e3d0977f44661fb9712fa297cc8f7608ea6ce48&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/1853336?u=b537c905ad08b69993de8796fb235c8d4d47f039&v=4 url: https://github.com/ivintoiu TechnoService2: login: TechnoService2 @@ -1841,7 +1841,7 @@ NavesSapnis: eqsdxr: login: eqsdxr count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/157279130?u=58fddf77ed76966eaa8c73eea9bea4bb0c53b673&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/157279130?u=d7aaffb29f542b647cf0f6b0e05722490863658a&v=4 url: https://github.com/eqsdxr syedasamina56: login: syedasamina56 diff --git a/docs/en/data/translators.yml b/docs/en/data/translators.yml index cf61eee8e..a4b87e1bf 100644 --- a/docs/en/data/translators.yml +++ b/docs/en/data/translators.yml @@ -1,6 +1,6 @@ nilslindemann: login: nilslindemann - count: 122 + count: 124 avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 url: https://github.com/nilslindemann jaystone776: @@ -103,6 +103,11 @@ pablocm83: count: 8 avatarUrl: https://avatars.githubusercontent.com/u/28315068?u=3310fbb05bb8bfc50d2c48b6cb64ac9ee4a14549&v=4 url: https://github.com/pablocm83 +tiangolo: + login: tiangolo + count: 7 + avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 + url: https://github.com/tiangolo ptt3199: login: ptt3199 count: 7 @@ -118,11 +123,6 @@ batlopes: count: 6 avatarUrl: https://avatars.githubusercontent.com/u/33462923?u=0fb3d7acb316764616f11e4947faf080e49ad8d9&v=4 url: https://github.com/batlopes -tiangolo: - login: tiangolo - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 - url: https://github.com/tiangolo lucasbalieiro: login: lucasbalieiro count: 6 From 3a223b90737200628d6aaf2b6cdba0944433bae7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 3 Nov 2025 09:24:57 +0000 Subject: [PATCH 079/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index e9c013a7c..a0f870b3b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* 👥 Update FastAPI People - Sponsors. PR [#14274](https://github.com/fastapi/fastapi/pull/14274) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI GitHub topic repositories. PR [#14280](https://github.com/fastapi/fastapi/pull/14280) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump mkdocs-macros-plugin from 1.4.0 to 1.4.1. PR [#14277](https://github.com/fastapi/fastapi/pull/14277) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocstrings[python] from 0.26.1 to 0.30.1. PR [#14279](https://github.com/fastapi/fastapi/pull/14279) by [@dependabot[bot]](https://github.com/apps/dependabot). From 425a4c5bb1d1a8e00007e82e5188fc4d30585767 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 3 Nov 2025 09:25:02 +0000 Subject: [PATCH 080/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index a0f870b3b..5e6fea9d5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* 👥 Update FastAPI People - Contributors and Translators. PR [#14273](https://github.com/fastapi/fastapi/pull/14273) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People - Sponsors. PR [#14274](https://github.com/fastapi/fastapi/pull/14274) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI GitHub topic repositories. PR [#14280](https://github.com/fastapi/fastapi/pull/14280) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump mkdocs-macros-plugin from 1.4.0 to 1.4.1. PR [#14277](https://github.com/fastapi/fastapi/pull/14277) by [@dependabot[bot]](https://github.com/apps/dependabot). From ac438b99342c859ae0e10f7064021125bd247bf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 3 Nov 2025 11:12:49 +0100 Subject: [PATCH 081/256] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20depen?= =?UTF-8?q?dencies=20with=20scopes,=20support=20`scope=3D"request"`=20for?= =?UTF-8?q?=20dependencies=20with=20`yield`=20that=20exit=20before=20the?= =?UTF-8?q?=20response=20is=20sent=20(#14262)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../dependencies/dependencies-with-yield.md | 45 ++++ docs_src/dependencies/tutorial008e.py | 15 ++ docs_src/dependencies/tutorial008e_an.py | 16 ++ docs_src/dependencies/tutorial008e_an_py39.py | 17 ++ fastapi/dependencies/models.py | 54 ++++- fastapi/dependencies/utils.py | 100 +++++---- fastapi/exceptions.py | 7 + fastapi/param_functions.py | 24 ++- fastapi/params.py | 3 +- fastapi/routing.py | 27 ++- fastapi/types.py | 3 +- tests/test_dependency_yield_scope.py | 184 ++++++++++++++++ .../test_dependency_yield_scope_websockets.py | 201 ++++++++++++++++++ .../test_dependencies/test_tutorial008e.py | 27 +++ 14 files changed, 653 insertions(+), 70 deletions(-) create mode 100644 docs_src/dependencies/tutorial008e.py create mode 100644 docs_src/dependencies/tutorial008e_an.py create mode 100644 docs_src/dependencies/tutorial008e_an_py39.py create mode 100644 tests/test_dependency_yield_scope.py create mode 100644 tests/test_dependency_yield_scope_websockets.py create mode 100644 tests/test_tutorial/test_dependencies/test_tutorial008e.py diff --git a/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md b/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md index adc1afa8d..494c40efa 100644 --- a/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md +++ b/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md @@ -184,6 +184,51 @@ If you raise any exception in the code from the *path operation function*, it wi /// +## Early exit and `scope` { #early-exit-and-scope } + +Normally the exit code of dependencies with `yield` is executed **after the response** is sent to the client. + +But if you know that you won't need to use the dependency after returning from the *path operation function*, you can use `Depends(scope="function")` to tell FastAPI that it should close the dependency after the *path operation function* returns, but **before** the **response is sent**. + +{* ../../docs_src/dependencies/tutorial008e_an_py39.py hl[12,16] *} + +`Depends()` receives a `scope` parameter that can be: + +* `"function"`: start the dependency before the *path operation function* that handles the request, end the dependency after the *path operation function* ends, but **before** the response is sent back to the client. So, the dependency function will be executed **around** the *path operation **function***. +* `"request"`: start the dependency before the *path operation function* that handles the request (similar to when using `"function"`), but end **after** the response is sent back to the client. So, the dependency function will be executed **around** the **request** and response cycle. + +If not specified and the dependency has `yield`, it will have a `scope` of `"request"` by default. + +### `scope` for sub-dependencies { #scope-for-sub-dependencies } + +When you declare a dependency with a `scope="request"` (the default), any sub-dependency needs to also have a `scope` of `"request"`. + +But a dependency with `scope` of `"function"` can have dependencies with `scope` of `"function"` and `scope` of `"request"`. + +This is because any dependency needs to be able to run its exit code before the sub-dependencies, as it might need to still use them during its exit code. + +```mermaid +sequenceDiagram + +participant client as Client +participant dep_req as Dep scope="request" +participant dep_func as Dep scope="function" +participant operation as Path Operation + + client ->> dep_req: Start request + Note over dep_req: Run code up to yield + dep_req ->> dep_func: Pass dependency + Note over dep_func: Run code up to yield + dep_func ->> operation: Run path operation with dependency + operation ->> dep_func: Return from path operation + Note over dep_func: Run code after yield + Note over dep_func: ✅ Dependency closed + dep_func ->> client: Send response to client + Note over client: Response sent + Note over dep_req: Run code after yield + Note over dep_req: ✅ Dependency closed +``` + ## Dependencies with `yield`, `HTTPException`, `except` and Background Tasks { #dependencies-with-yield-httpexception-except-and-background-tasks } Dependencies with `yield` have evolved over time to cover different use cases and fix some issues. diff --git a/docs_src/dependencies/tutorial008e.py b/docs_src/dependencies/tutorial008e.py new file mode 100644 index 000000000..1ed056e91 --- /dev/null +++ b/docs_src/dependencies/tutorial008e.py @@ -0,0 +1,15 @@ +from fastapi import Depends, FastAPI + +app = FastAPI() + + +def get_username(): + try: + yield "Rick" + finally: + print("Cleanup up before response is sent") + + +@app.get("/users/me") +def get_user_me(username: str = Depends(get_username, scope="function")): + return username diff --git a/docs_src/dependencies/tutorial008e_an.py b/docs_src/dependencies/tutorial008e_an.py new file mode 100644 index 000000000..c8a0af2b3 --- /dev/null +++ b/docs_src/dependencies/tutorial008e_an.py @@ -0,0 +1,16 @@ +from fastapi import Depends, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +def get_username(): + try: + yield "Rick" + finally: + print("Cleanup up before response is sent") + + +@app.get("/users/me") +def get_user_me(username: Annotated[str, Depends(get_username, scope="function")]): + return username diff --git a/docs_src/dependencies/tutorial008e_an_py39.py b/docs_src/dependencies/tutorial008e_an_py39.py new file mode 100644 index 000000000..80a44c7e2 --- /dev/null +++ b/docs_src/dependencies/tutorial008e_an_py39.py @@ -0,0 +1,17 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +def get_username(): + try: + yield "Rick" + finally: + print("Cleanup up before response is sent") + + +@app.get("/users/me") +def get_user_me(username: Annotated[str, Depends(get_username, scope="function")]): + return username diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index 418c11725..d6359c0f5 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -1,8 +1,18 @@ +import inspect +import sys from dataclasses import dataclass, field -from typing import Any, Callable, List, Optional, Sequence, Tuple +from functools import cached_property +from typing import Any, Callable, List, Optional, Sequence, Union from fastapi._compat import ModelField from fastapi.security.base import SecurityBase +from fastapi.types import DependencyCacheKey +from typing_extensions import Literal + +if sys.version_info >= (3, 13): # pragma: no cover + from inspect import iscoroutinefunction +else: # pragma: no cover + from asyncio import iscoroutinefunction @dataclass @@ -31,7 +41,43 @@ class Dependant: security_scopes: Optional[List[str]] = None use_cache: bool = True path: Optional[str] = None - cache_key: Tuple[Optional[Callable[..., Any]], Tuple[str, ...]] = field(init=False) + scope: Union[Literal["function", "request"], None] = None - def __post_init__(self) -> None: - self.cache_key = (self.call, tuple(sorted(set(self.security_scopes or [])))) + @cached_property + def cache_key(self) -> DependencyCacheKey: + return ( + self.call, + tuple(sorted(set(self.security_scopes or []))), + self.computed_scope or "", + ) + + @cached_property + def is_gen_callable(self) -> bool: + if inspect.isgeneratorfunction(self.call): + return True + dunder_call = getattr(self.call, "__call__", None) # noqa: B004 + return inspect.isgeneratorfunction(dunder_call) + + @cached_property + def is_async_gen_callable(self) -> bool: + if inspect.isasyncgenfunction(self.call): + return True + dunder_call = getattr(self.call, "__call__", None) # noqa: B004 + return inspect.isasyncgenfunction(dunder_call) + + @cached_property + def is_coroutine_callable(self) -> bool: + if inspect.isroutine(self.call): + return iscoroutinefunction(self.call) + if inspect.isclass(self.call): + return False + dunder_call = getattr(self.call, "__call__", None) # noqa: B004 + return iscoroutinefunction(dunder_call) + + @cached_property + def computed_scope(self) -> Union[str, None]: + if self.scope: + return self.scope + if self.is_gen_callable or self.is_async_gen_callable: + return "request" + return None diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 6477a2cba..c5c6b69bb 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -1,5 +1,4 @@ import inspect -import sys from contextlib import AsyncExitStack, contextmanager from copy import copy, deepcopy from dataclasses import dataclass @@ -55,10 +54,12 @@ from fastapi.concurrency import ( contextmanager_in_threadpool, ) from fastapi.dependencies.models import Dependant, SecurityRequirement +from fastapi.exceptions import DependencyScopeError from fastapi.logger import logger from fastapi.security.base import SecurityBase from fastapi.security.oauth2 import OAuth2, SecurityScopes from fastapi.security.open_id_connect_url import OpenIdConnect +from fastapi.types import DependencyCacheKey from fastapi.utils import create_model_field, get_path_param_names from pydantic import BaseModel from pydantic.fields import FieldInfo @@ -74,15 +75,10 @@ from starlette.datastructures import ( from starlette.requests import HTTPConnection, Request from starlette.responses import Response from starlette.websockets import WebSocket -from typing_extensions import Annotated, get_args, get_origin +from typing_extensions import Annotated, Literal, get_args, get_origin from .. import temp_pydantic_v1_params -if sys.version_info >= (3, 13): # pragma: no cover - from inspect import iscoroutinefunction -else: # pragma: no cover - from asyncio import iscoroutinefunction - multipart_not_installed_error = ( 'Form data requires "python-multipart" to be installed. \n' 'You can install "python-multipart" with: \n\n' @@ -137,14 +133,11 @@ def get_parameterless_sub_dependant(*, depends: params.Depends, path: str) -> De ) -CacheKey = Tuple[Optional[Callable[..., Any]], Tuple[str, ...]] - - def get_flat_dependant( dependant: Dependant, *, skip_repeats: bool = False, - visited: Optional[List[CacheKey]] = None, + visited: Optional[List[DependencyCacheKey]] = None, ) -> Dependant: if visited is None: visited = [] @@ -237,6 +230,7 @@ def get_dependant( name: Optional[str] = None, security_scopes: Optional[List[str]] = None, use_cache: bool = True, + scope: Union[Literal["function", "request"], None] = None, ) -> Dependant: dependant = Dependant( call=call, @@ -244,6 +238,7 @@ def get_dependant( path=path, security_scopes=security_scopes, use_cache=use_cache, + scope=scope, ) path_param_names = get_path_param_names(path) endpoint_signature = get_typed_signature(call) @@ -251,7 +246,7 @@ def get_dependant( if isinstance(call, SecurityBase): use_scopes: List[str] = [] if isinstance(call, (OAuth2, OpenIdConnect)): - use_scopes = security_scopes + use_scopes = security_scopes or use_scopes security_requirement = SecurityRequirement( security_scheme=call, scopes=use_scopes ) @@ -266,6 +261,16 @@ def get_dependant( ) if param_details.depends is not None: assert param_details.depends.dependency + if ( + (dependant.is_gen_callable or dependant.is_async_gen_callable) + and dependant.computed_scope == "request" + and param_details.depends.scope == "function" + ): + assert dependant.call + raise DependencyScopeError( + f'The dependency "{dependant.call.__name__}" has a scope of ' + '"request", it cannot depend on dependencies with scope "function".' + ) use_security_scopes = security_scopes or [] if isinstance(param_details.depends, params.Security): if param_details.depends.scopes: @@ -276,6 +281,7 @@ def get_dependant( name=param_name, security_scopes=use_security_scopes, use_cache=param_details.depends.use_cache, + scope=param_details.depends.scope, ) dependant.dependencies.append(sub_dependant) continue @@ -532,36 +538,14 @@ def add_param_to_fields(*, field: ModelField, dependant: Dependant) -> None: dependant.cookie_params.append(field) -def is_coroutine_callable(call: Callable[..., Any]) -> bool: - if inspect.isroutine(call): - return iscoroutinefunction(call) - if inspect.isclass(call): - return False - dunder_call = getattr(call, "__call__", None) # noqa: B004 - return iscoroutinefunction(dunder_call) - - -def is_async_gen_callable(call: Callable[..., Any]) -> bool: - if inspect.isasyncgenfunction(call): - return True - dunder_call = getattr(call, "__call__", None) # noqa: B004 - return inspect.isasyncgenfunction(dunder_call) - - -def is_gen_callable(call: Callable[..., Any]) -> bool: - if inspect.isgeneratorfunction(call): - return True - dunder_call = getattr(call, "__call__", None) # noqa: B004 - return inspect.isgeneratorfunction(dunder_call) - - -async def solve_generator( - *, call: Callable[..., Any], stack: AsyncExitStack, sub_values: Dict[str, Any] +async def _solve_generator( + *, dependant: Dependant, stack: AsyncExitStack, sub_values: Dict[str, Any] ) -> Any: - if is_gen_callable(call): - cm = contextmanager_in_threadpool(contextmanager(call)(**sub_values)) - elif is_async_gen_callable(call): - cm = asynccontextmanager(call)(**sub_values) + assert dependant.call + if dependant.is_gen_callable: + cm = contextmanager_in_threadpool(contextmanager(dependant.call)(**sub_values)) + elif dependant.is_async_gen_callable: + cm = asynccontextmanager(dependant.call)(**sub_values) return await stack.enter_async_context(cm) @@ -571,7 +555,7 @@ class SolvedDependency: errors: List[Any] background_tasks: Optional[StarletteBackgroundTasks] response: Response - dependency_cache: Dict[Tuple[Callable[..., Any], Tuple[str]], Any] + dependency_cache: Dict[DependencyCacheKey, Any] async def solve_dependencies( @@ -582,10 +566,20 @@ async def solve_dependencies( background_tasks: Optional[StarletteBackgroundTasks] = None, response: Optional[Response] = None, dependency_overrides_provider: Optional[Any] = None, - dependency_cache: Optional[Dict[Tuple[Callable[..., Any], Tuple[str]], Any]] = None, + dependency_cache: Optional[Dict[DependencyCacheKey, Any]] = None, + # TODO: remove this parameter later, no longer used, not removing it yet as some + # people might be monkey patching this function (although that's not supported) async_exit_stack: AsyncExitStack, embed_body_fields: bool, ) -> SolvedDependency: + request_astack = request.scope.get("fastapi_inner_astack") + assert isinstance(request_astack, AsyncExitStack), ( + "fastapi_inner_astack not found in request scope" + ) + function_astack = request.scope.get("fastapi_function_astack") + assert isinstance(function_astack, AsyncExitStack), ( + "fastapi_function_astack not found in request scope" + ) values: Dict[str, Any] = {} errors: List[Any] = [] if response is None: @@ -594,12 +588,8 @@ async def solve_dependencies( response.status_code = None # type: ignore if dependency_cache is None: dependency_cache = {} - sub_dependant: Dependant for sub_dependant in dependant.dependencies: sub_dependant.call = cast(Callable[..., Any], sub_dependant.call) - sub_dependant.cache_key = cast( - Tuple[Callable[..., Any], Tuple[str]], sub_dependant.cache_key - ) call = sub_dependant.call use_sub_dependant = sub_dependant if ( @@ -616,6 +606,7 @@ async def solve_dependencies( call=call, name=sub_dependant.name, security_scopes=sub_dependant.security_scopes, + scope=sub_dependant.scope, ) solved_result = await solve_dependencies( @@ -635,11 +626,18 @@ async def solve_dependencies( continue if sub_dependant.use_cache and sub_dependant.cache_key in dependency_cache: solved = dependency_cache[sub_dependant.cache_key] - elif is_gen_callable(call) or is_async_gen_callable(call): - solved = await solve_generator( - call=call, stack=async_exit_stack, sub_values=solved_result.values + elif ( + use_sub_dependant.is_gen_callable or use_sub_dependant.is_async_gen_callable + ): + use_astack = request_astack + if sub_dependant.scope == "function": + use_astack = function_astack + solved = await _solve_generator( + dependant=use_sub_dependant, + stack=use_astack, + sub_values=solved_result.values, ) - elif is_coroutine_callable(call): + elif use_sub_dependant.is_coroutine_callable: solved = await call(**solved_result.values) else: solved = await run_in_threadpool(call, **solved_result.values) diff --git a/fastapi/exceptions.py b/fastapi/exceptions.py index bb775fcbf..0620428be 100644 --- a/fastapi/exceptions.py +++ b/fastapi/exceptions.py @@ -147,6 +147,13 @@ class FastAPIError(RuntimeError): """ +class DependencyScopeError(FastAPIError): + """ + A dependency declared that it depends on another dependency with an invalid + (narrower) scope. + """ + + class ValidationException(Exception): def __init__(self, errors: Sequence[Any]) -> None: self._errors = errors diff --git a/fastapi/param_functions.py b/fastapi/param_functions.py index f88937e24..e32f75593 100644 --- a/fastapi/param_functions.py +++ b/fastapi/param_functions.py @@ -4,7 +4,7 @@ from annotated_doc import Doc from fastapi import params from fastapi._compat import Undefined from fastapi.openapi.models import Example -from typing_extensions import Annotated, deprecated +from typing_extensions import Annotated, Literal, deprecated _Unset: Any = Undefined @@ -2245,6 +2245,26 @@ def Depends( # noqa: N802 """ ), ] = True, + scope: Annotated[ + Union[Literal["function", "request"], None], + Doc( + """ + Mainly for dependencies with `yield`, define when the dependency function + should start (the code before `yield`) and when it should end (the code + after `yield`). + + * `"function"`: start the dependency before the *path operation function* + that handles the request, end the dependency after the *path operation + function* ends, but **before** the response is sent back to the client. + So, the dependency function will be executed **around** the *path operation + **function***. + * `"request"`: start the dependency before the *path operation function* + that handles the request (similar to when using `"function"`), but end + **after** the response is sent back to the client. So, the dependency + function will be executed **around** the **request** and response cycle. + """ + ), + ] = None, ) -> Any: """ Declare a FastAPI dependency. @@ -2275,7 +2295,7 @@ def Depends( # noqa: N802 return commons ``` """ - return params.Depends(dependency=dependency, use_cache=use_cache) + return params.Depends(dependency=dependency, use_cache=use_cache, scope=scope) def Security( # noqa: N802 diff --git a/fastapi/params.py b/fastapi/params.py index 2dc04be14..6a58d5808 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -5,7 +5,7 @@ from typing import Any, Callable, Dict, List, Optional, Sequence, Union from fastapi.openapi.models import Example from pydantic.fields import FieldInfo -from typing_extensions import Annotated, deprecated +from typing_extensions import Annotated, Literal, deprecated from ._compat import ( PYDANTIC_V2, @@ -766,6 +766,7 @@ class File(Form): # type: ignore[misc] class Depends: dependency: Optional[Callable[..., Any]] = None use_cache: bool = True + scope: Union[Literal["function", "request"], None] = None @dataclass diff --git a/fastapi/routing.py b/fastapi/routing.py index 0b59d250a..a8e12eb60 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -104,10 +104,11 @@ def request_response( async def app(scope: Scope, receive: Receive, send: Send) -> None: # Starts customization response_awaited = False - async with AsyncExitStack() as stack: - scope["fastapi_inner_astack"] = stack - # Same as in Starlette - response = await f(request) + async with AsyncExitStack() as request_stack: + scope["fastapi_inner_astack"] = request_stack + async with AsyncExitStack() as function_stack: + scope["fastapi_function_astack"] = function_stack + response = await f(request) await response(scope, receive, send) # Continues customization response_awaited = True @@ -140,11 +141,11 @@ def websocket_session( session = WebSocket(scope, receive=receive, send=send) async def app(scope: Scope, receive: Receive, send: Send) -> None: - # Starts customization - async with AsyncExitStack() as stack: - scope["fastapi_inner_astack"] = stack - # Same as in Starlette - await func(session) + async with AsyncExitStack() as request_stack: + scope["fastapi_inner_astack"] = request_stack + async with AsyncExitStack() as function_stack: + scope["fastapi_function_astack"] = function_stack + await func(session) # Same as in Starlette await wrap_app_handling_exceptions(app, session)(scope, receive, send) @@ -479,7 +480,9 @@ class APIWebSocketRoute(routing.WebSocketRoute): self.name = get_name(endpoint) if name is None else name self.dependencies = list(dependencies or []) self.path_regex, self.path_format, self.param_convertors = compile_path(path) - self.dependant = get_dependant(path=self.path_format, call=self.endpoint) + self.dependant = get_dependant( + path=self.path_format, call=self.endpoint, scope="function" + ) for depends in self.dependencies[::-1]: self.dependant.dependencies.insert( 0, @@ -630,7 +633,9 @@ class APIRoute(routing.Route): self.response_fields = {} assert callable(endpoint), "An endpoint must be a callable" - self.dependant = get_dependant(path=self.path_format, call=self.endpoint) + self.dependant = get_dependant( + path=self.path_format, call=self.endpoint, scope="function" + ) for depends in self.dependencies[::-1]: self.dependant.dependencies.insert( 0, diff --git a/fastapi/types.py b/fastapi/types.py index 3205654c7..3f4e81a7c 100644 --- a/fastapi/types.py +++ b/fastapi/types.py @@ -1,6 +1,6 @@ import types from enum import Enum -from typing import Any, Callable, Dict, Set, Type, TypeVar, Union +from typing import Any, Callable, Dict, Optional, Set, Tuple, Type, TypeVar, Union from pydantic import BaseModel @@ -8,3 +8,4 @@ DecoratedCallable = TypeVar("DecoratedCallable", bound=Callable[..., Any]) UnionType = getattr(types, "UnionType", Union) ModelNameMap = Dict[Union[Type[BaseModel], Type[Enum]], str] IncEx = Union[Set[int], Set[str], Dict[int, Any], Dict[str, Any]] +DependencyCacheKey = Tuple[Optional[Callable[..., Any]], Tuple[str, ...], str] diff --git a/tests/test_dependency_yield_scope.py b/tests/test_dependency_yield_scope.py new file mode 100644 index 000000000..a5227dd7a --- /dev/null +++ b/tests/test_dependency_yield_scope.py @@ -0,0 +1,184 @@ +import json +from typing import Any, Tuple + +import pytest +from fastapi import Depends, FastAPI +from fastapi.exceptions import FastAPIError +from fastapi.responses import StreamingResponse +from fastapi.testclient import TestClient +from typing_extensions import Annotated + + +class Session: + def __init__(self) -> None: + self.open = True + + +def dep_session() -> Any: + s = Session() + yield s + s.open = False + + +SessionFuncDep = Annotated[Session, Depends(dep_session, scope="function")] +SessionRequestDep = Annotated[Session, Depends(dep_session, scope="request")] +SessionDefaultDep = Annotated[Session, Depends(dep_session)] + + +class NamedSession: + def __init__(self, name: str = "default") -> None: + self.name = name + self.open = True + + +def get_named_session(session: SessionRequestDep, session_b: SessionDefaultDep) -> Any: + assert session is session_b + named_session = NamedSession(name="named") + yield named_session, session_b + named_session.open = False + + +NamedSessionsDep = Annotated[Tuple[NamedSession, Session], Depends(get_named_session)] + + +def get_named_func_session(session: SessionFuncDep) -> Any: + named_session = NamedSession(name="named") + yield named_session, session + named_session.open = False + + +def get_named_regular_func_session(session: SessionFuncDep) -> Any: + named_session = NamedSession(name="named") + return named_session, session + + +BrokenSessionsDep = Annotated[ + Tuple[NamedSession, Session], Depends(get_named_func_session) +] +NamedSessionsFuncDep = Annotated[ + Tuple[NamedSession, Session], Depends(get_named_func_session, scope="function") +] + +RegularSessionsDep = Annotated[ + Tuple[NamedSession, Session], Depends(get_named_regular_func_session) +] + +app = FastAPI() + + +@app.get("/function-scope") +def function_scope(session: SessionFuncDep) -> Any: + def iter_data(): + yield json.dumps({"is_open": session.open}) + + return StreamingResponse(iter_data()) + + +@app.get("/request-scope") +def request_scope(session: SessionRequestDep) -> Any: + def iter_data(): + yield json.dumps({"is_open": session.open}) + + return StreamingResponse(iter_data()) + + +@app.get("/two-scopes") +def get_stream_session( + function_session: SessionFuncDep, request_session: SessionRequestDep +) -> Any: + def iter_data(): + yield json.dumps( + {"func_is_open": function_session.open, "req_is_open": request_session.open} + ) + + return StreamingResponse(iter_data()) + + +@app.get("/sub") +def get_sub(sessions: NamedSessionsDep) -> Any: + def iter_data(): + yield json.dumps( + {"named_session_open": sessions[0].open, "session_open": sessions[1].open} + ) + + return StreamingResponse(iter_data()) + + +@app.get("/named-function-scope") +def get_named_function_scope(sessions: NamedSessionsFuncDep) -> Any: + def iter_data(): + yield json.dumps( + {"named_session_open": sessions[0].open, "session_open": sessions[1].open} + ) + + return StreamingResponse(iter_data()) + + +@app.get("/regular-function-scope") +def get_regular_function_scope(sessions: RegularSessionsDep) -> Any: + def iter_data(): + yield json.dumps( + {"named_session_open": sessions[0].open, "session_open": sessions[1].open} + ) + + return StreamingResponse(iter_data()) + + +client = TestClient(app) + + +def test_function_scope() -> None: + response = client.get("/function-scope") + assert response.status_code == 200 + data = response.json() + assert data["is_open"] is False + + +def test_request_scope() -> None: + response = client.get("/request-scope") + assert response.status_code == 200 + data = response.json() + assert data["is_open"] is True + + +def test_two_scopes() -> None: + response = client.get("/two-scopes") + assert response.status_code == 200 + data = response.json() + assert data["func_is_open"] is False + assert data["req_is_open"] is True + + +def test_sub() -> None: + response = client.get("/sub") + assert response.status_code == 200 + data = response.json() + assert data["named_session_open"] is True + assert data["session_open"] is True + + +def test_broken_scope() -> None: + with pytest.raises( + FastAPIError, + match='The dependency "get_named_func_session" has a scope of "request", it cannot depend on dependencies with scope "function"', + ): + + @app.get("/broken-scope") + def get_broken(sessions: BrokenSessionsDep) -> Any: # pragma: no cover + pass + + +def test_named_function_scope() -> None: + response = client.get("/named-function-scope") + assert response.status_code == 200 + data = response.json() + assert data["named_session_open"] is False + assert data["session_open"] is False + + +def test_regular_function_scope() -> None: + response = client.get("/regular-function-scope") + assert response.status_code == 200 + data = response.json() + assert data["named_session_open"] is True + assert data["session_open"] is False diff --git a/tests/test_dependency_yield_scope_websockets.py b/tests/test_dependency_yield_scope_websockets.py new file mode 100644 index 000000000..52a30ae7a --- /dev/null +++ b/tests/test_dependency_yield_scope_websockets.py @@ -0,0 +1,201 @@ +from contextvars import ContextVar +from typing import Any, Dict, Tuple + +import pytest +from fastapi import Depends, FastAPI, WebSocket +from fastapi.exceptions import FastAPIError +from fastapi.testclient import TestClient +from typing_extensions import Annotated + +global_context: ContextVar[Dict[str, Any]] = ContextVar("global_context", default={}) # noqa: B039 + + +class Session: + def __init__(self) -> None: + self.open = True + + +async def dep_session() -> Any: + s = Session() + yield s + s.open = False + global_state = global_context.get() + global_state["session_closed"] = True + + +SessionFuncDep = Annotated[Session, Depends(dep_session, scope="function")] +SessionRequestDep = Annotated[Session, Depends(dep_session, scope="request")] +SessionDefaultDep = Annotated[Session, Depends(dep_session)] + + +class NamedSession: + def __init__(self, name: str = "default") -> None: + self.name = name + self.open = True + + +def get_named_session(session: SessionRequestDep, session_b: SessionDefaultDep) -> Any: + assert session is session_b + named_session = NamedSession(name="named") + yield named_session, session_b + named_session.open = False + global_state = global_context.get() + global_state["named_session_closed"] = True + + +NamedSessionsDep = Annotated[Tuple[NamedSession, Session], Depends(get_named_session)] + + +def get_named_func_session(session: SessionFuncDep) -> Any: + named_session = NamedSession(name="named") + yield named_session, session + named_session.open = False + global_state = global_context.get() + global_state["named_func_session_closed"] = True + + +def get_named_regular_func_session(session: SessionFuncDep) -> Any: + named_session = NamedSession(name="named") + return named_session, session + + +BrokenSessionsDep = Annotated[ + Tuple[NamedSession, Session], Depends(get_named_func_session) +] +NamedSessionsFuncDep = Annotated[ + Tuple[NamedSession, Session], Depends(get_named_func_session, scope="function") +] + +RegularSessionsDep = Annotated[ + Tuple[NamedSession, Session], Depends(get_named_regular_func_session) +] + +app = FastAPI() + + +@app.websocket("/function-scope") +async def function_scope(websocket: WebSocket, session: SessionFuncDep) -> Any: + await websocket.accept() + await websocket.send_json({"is_open": session.open}) + + +@app.websocket("/request-scope") +async def request_scope(websocket: WebSocket, session: SessionRequestDep) -> Any: + await websocket.accept() + await websocket.send_json({"is_open": session.open}) + + +@app.websocket("/two-scopes") +async def get_stream_session( + websocket: WebSocket, + function_session: SessionFuncDep, + request_session: SessionRequestDep, +) -> Any: + await websocket.accept() + await websocket.send_json( + {"func_is_open": function_session.open, "req_is_open": request_session.open} + ) + + +@app.websocket("/sub") +async def get_sub(websocket: WebSocket, sessions: NamedSessionsDep) -> Any: + await websocket.accept() + await websocket.send_json( + {"named_session_open": sessions[0].open, "session_open": sessions[1].open} + ) + + +@app.websocket("/named-function-scope") +async def get_named_function_scope( + websocket: WebSocket, sessions: NamedSessionsFuncDep +) -> Any: + await websocket.accept() + await websocket.send_json( + {"named_session_open": sessions[0].open, "session_open": sessions[1].open} + ) + + +@app.websocket("/regular-function-scope") +async def get_regular_function_scope( + websocket: WebSocket, sessions: RegularSessionsDep +) -> Any: + await websocket.accept() + await websocket.send_json( + {"named_session_open": sessions[0].open, "session_open": sessions[1].open} + ) + + +client = TestClient(app) + + +def test_function_scope() -> None: + global_context.set({}) + global_state = global_context.get() + with client.websocket_connect("/function-scope") as websocket: + data = websocket.receive_json() + assert data["is_open"] is True + assert global_state["session_closed"] is True + + +def test_request_scope() -> None: + global_context.set({}) + global_state = global_context.get() + with client.websocket_connect("/request-scope") as websocket: + data = websocket.receive_json() + assert data["is_open"] is True + assert global_state["session_closed"] is True + + +def test_two_scopes() -> None: + global_context.set({}) + global_state = global_context.get() + with client.websocket_connect("/two-scopes") as websocket: + data = websocket.receive_json() + assert data["func_is_open"] is True + assert data["req_is_open"] is True + assert global_state["session_closed"] is True + + +def test_sub() -> None: + global_context.set({}) + global_state = global_context.get() + with client.websocket_connect("/sub") as websocket: + data = websocket.receive_json() + assert data["named_session_open"] is True + assert data["session_open"] is True + assert global_state["session_closed"] is True + assert global_state["named_session_closed"] is True + + +def test_broken_scope() -> None: + with pytest.raises( + FastAPIError, + match='The dependency "get_named_func_session" has a scope of "request", it cannot depend on dependencies with scope "function"', + ): + + @app.websocket("/broken-scope") + async def get_broken( + websocket: WebSocket, sessions: BrokenSessionsDep + ) -> Any: # pragma: no cover + pass + + +def test_named_function_scope() -> None: + global_context.set({}) + global_state = global_context.get() + with client.websocket_connect("/named-function-scope") as websocket: + data = websocket.receive_json() + assert data["named_session_open"] is True + assert data["session_open"] is True + assert global_state["session_closed"] is True + assert global_state["named_func_session_closed"] is True + + +def test_regular_function_scope() -> None: + global_context.set({}) + global_state = global_context.get() + with client.websocket_connect("/regular-function-scope") as websocket: + data = websocket.receive_json() + assert data["named_session_open"] is True + assert data["session_open"] is True + assert global_state["session_closed"] is True diff --git a/tests/test_tutorial/test_dependencies/test_tutorial008e.py b/tests/test_tutorial/test_dependencies/test_tutorial008e.py new file mode 100644 index 000000000..1ae9ab2cd --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial008e.py @@ -0,0 +1,27 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + + +@pytest.fixture( + name="client", + params=[ + "tutorial008e", + "tutorial008e_an", + pytest.param("tutorial008e_an_py39", marks=needs_py39), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.dependencies.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_get_users_me(client: TestClient): + response = client.get("/users/me") + assert response.status_code == 200, response.text + assert response.json() == "Rick" From ad4d8f24ca19bdf224f216a8dca7948935ac8e05 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 3 Nov 2025 10:13:13 +0000 Subject: [PATCH 082/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 5e6fea9d5..caa7e2187 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Features + +* ✨ Add support for dependencies with scopes, support `scope="request"` for dependencies with `yield` that exit before the response is sent. PR [#14262](https://github.com/fastapi/fastapi/pull/14262) by [@tiangolo](https://github.com/tiangolo). + ### Internal * 👥 Update FastAPI People - Contributors and Translators. PR [#14273](https://github.com/fastapi/fastapi/pull/14273) by [@tiangolo](https://github.com/tiangolo). From 36901405559018bd97c1c70eb86748b8b9d67533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 3 Nov 2025 11:19:56 +0100 Subject: [PATCH 083/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index caa7e2187..93e6d3e12 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -10,6 +10,7 @@ hide: ### Features * ✨ Add support for dependencies with scopes, support `scope="request"` for dependencies with `yield` that exit before the response is sent. PR [#14262](https://github.com/fastapi/fastapi/pull/14262) by [@tiangolo](https://github.com/tiangolo). + * New docs: [Dependencies with `yield` - Early exit and `scope`](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-with-yield/#early-exit-and-scope). ### Internal From 4efae81a76b809998c12fc0f7e1c172f451cb304 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 3 Nov 2025 11:21:36 +0100 Subject: [PATCH 084/256] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.12?= =?UTF-8?q?1.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 93e6d3e12..a08aa14fc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.121.0 + ### Features * ✨ Add support for dependencies with scopes, support `scope="request"` for dependencies with `yield` that exit before the response is sent. PR [#14262](https://github.com/fastapi/fastapi/pull/14262) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 93555bfdc..f4a952bf5 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.120.4" +__version__ = "0.121.0" from starlette import status as status From 289b4aa2faff3bbfa364abbb21fe2b23aebe6270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 3 Nov 2025 14:19:58 +0100 Subject: [PATCH 085/256] =?UTF-8?q?=F0=9F=93=9D=20Upate=20docs=20for=20adv?= =?UTF-8?q?anced=20dependencies=20with=20`yield`,=20noting=20the=20changes?= =?UTF-8?q?=20in=200.121.0,=20adding=20`scope`=20(#14287)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/advanced/advanced-dependencies.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/advanced/advanced-dependencies.md b/docs/en/docs/advanced/advanced-dependencies.md index e0404b389..5d6a40f46 100644 --- a/docs/en/docs/advanced/advanced-dependencies.md +++ b/docs/en/docs/advanced/advanced-dependencies.md @@ -70,12 +70,22 @@ If you understood all this, you already know how those utility tools for securit You most probably don't need these technical details. -These details are useful mainly if you had a FastAPI application older than 0.118.0 and you are facing issues with dependencies with `yield`. +These details are useful mainly if you had a FastAPI application older than 0.121.0 and you are facing issues with dependencies with `yield`. /// Dependencies with `yield` have evolved over time to account for the different use cases and to fix some issues, here's a summary of what has changed. +### Dependencies with `yield` and `scope` { #dependencies-with-yield-and-scope } + +In version 0.121.0, FastAPI added support for `Depends(scope="function")` for dependencies with `yield`. + +Using `Depends(scope="function")`, the exit code after `yield` is executed right after the *path operation function* is finished, before the response is sent back to the client. + +And when using `Depends(scope="request")` (the default), the exit code after `yield` is executed after the response is sent. + +You can read more about it in the docs for [Dependencies with `yield` - Early exit and `scope`](../tutorial/dependencies/dependencies-with-yield.md#early-exit-and-scope). + ### Dependencies with `yield` and `StreamingResponse`, Technical Details { #dependencies-with-yield-and-streamingresponse-technical-details } Before FastAPI 0.118.0, if you used a dependency with `yield`, it would run the exit code after the *path operation function* returned but right before sending the response. From b787103226775a4ec328e48241682a34e6b2d217 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 3 Nov 2025 13:20:34 +0000 Subject: [PATCH 086/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index a08aa14fc..24b9f4ca3 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Docs + +* 📝 Upate docs for advanced dependencies with `yield`, noting the changes in 0.121.0, adding `scope`. PR [#14287](https://github.com/fastapi/fastapi/pull/14287) by [@tiangolo](https://github.com/tiangolo). + ## 0.121.0 ### Features From 34db1e2e2c153151e8fb66484cbf9f879c12bf4f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 09:38:05 +0100 Subject: [PATCH 087/256] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commi?= =?UTF-8?q?t=20autoupdate=20(#14289)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.14.2 → v0.14.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.2...v0.14.3) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 25dcd7b88..8e5eba4c4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.2 + rev: v0.14.3 hooks: - id: ruff args: From 67c8dfaf0f6baeaa351ce5f8d1bfdafd789e9059 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 09:38:16 +0100 Subject: [PATCH 088/256] =?UTF-8?q?=E2=AC=86=20Bump=20ruff=20from=200.13.2?= =?UTF-8?q?=20to=200.14.3=20(#14276)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ruff](https://github.com/astral-sh/ruff) from 0.13.2 to 0.14.3. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.13.2...0.14.3) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.14.3 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs-tests.txt b/requirements-docs-tests.txt index a909d0eef..9350f3ee4 100644 --- a/requirements-docs-tests.txt +++ b/requirements-docs-tests.txt @@ -1,4 +1,4 @@ # For mkdocstrings and tests httpx >=0.23.0,<1.0.0 # For linting and generating docs versions -ruff ==0.13.2 +ruff ==0.14.3 From 4170f621a5b7a14cf95c160d5b8a77fe41d59a62 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 4 Nov 2025 08:38:34 +0000 Subject: [PATCH 089/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 24b9f4ca3..9394a18dd 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -11,6 +11,10 @@ hide: * 📝 Upate docs for advanced dependencies with `yield`, noting the changes in 0.121.0, adding `scope`. PR [#14287](https://github.com/fastapi/fastapi/pull/14287) by [@tiangolo](https://github.com/tiangolo). +### Internal + +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#14289](https://github.com/fastapi/fastapi/pull/14289) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). + ## 0.121.0 ### Features From 972a967d5d3fa24d450637c36c6b7ad1f1d8c1b6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 4 Nov 2025 08:39:30 +0000 Subject: [PATCH 090/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 9394a18dd..9380fb7d4 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Internal +* ⬆ Bump ruff from 0.13.2 to 0.14.3. PR [#14276](https://github.com/fastapi/fastapi/pull/14276) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#14289](https://github.com/fastapi/fastapi/pull/14289) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). ## 0.121.0 From 282f372eda661c865fa1208bd0371bf47d190b2a Mon Sep 17 00:00:00 2001 From: luzzodev Date: Sat, 8 Nov 2025 22:43:30 +0100 Subject: [PATCH 091/256] =?UTF-8?q?=F0=9F=90=9B=20Fix=20`Depends(func,=20s?= =?UTF-8?q?cope=3D'function')`=20for=20top=20level=20(parameterless)=20dep?= =?UTF-8?q?endencies=20(#14301)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/dependencies/utils.py | 5 ++- tests/test_dependency_yield_scope.py | 64 +++++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index c5c6b69bb..4b69e39a1 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -129,7 +129,10 @@ def get_parameterless_sub_dependant(*, depends: params.Depends, path: str) -> De if isinstance(depends, params.Security) and depends.scopes: use_security_scopes.extend(depends.scopes) return get_dependant( - path=path, call=depends.dependency, security_scopes=use_security_scopes + path=path, + call=depends.dependency, + scope=depends.scope, + security_scopes=use_security_scopes, ) diff --git a/tests/test_dependency_yield_scope.py b/tests/test_dependency_yield_scope.py index a5227dd7a..d87164fe8 100644 --- a/tests/test_dependency_yield_scope.py +++ b/tests/test_dependency_yield_scope.py @@ -2,7 +2,7 @@ import json from typing import Any, Tuple import pytest -from fastapi import Depends, FastAPI +from fastapi import APIRouter, Depends, FastAPI, HTTPException from fastapi.exceptions import FastAPIError from fastapi.responses import StreamingResponse from fastapi.testclient import TestClient @@ -20,6 +20,11 @@ def dep_session() -> Any: s.open = False +def raise_after_yield() -> Any: + yield + raise HTTPException(status_code=503, detail="Exception after yield") + + SessionFuncDep = Annotated[Session, Depends(dep_session, scope="function")] SessionRequestDep = Annotated[Session, Depends(dep_session, scope="request")] SessionDefaultDep = Annotated[Session, Depends(dep_session)] @@ -64,6 +69,12 @@ RegularSessionsDep = Annotated[ ] app = FastAPI() +router = APIRouter() + + +@router.get("/") +def get_index(): + return {"status": "ok"} @app.get("/function-scope") @@ -124,6 +135,18 @@ def get_regular_function_scope(sessions: RegularSessionsDep) -> Any: return StreamingResponse(iter_data()) +app.include_router( + prefix="/router-scope-function", + router=router, + dependencies=[Depends(raise_after_yield, scope="function")], +) + +app.include_router( + prefix="/router-scope-request", + router=router, + dependencies=[Depends(raise_after_yield, scope="request")], +) + client = TestClient(app) @@ -182,3 +205,42 @@ def test_regular_function_scope() -> None: data = response.json() assert data["named_session_open"] is True assert data["session_open"] is False + + +def test_router_level_dep_scope_function() -> None: + response = client.get("/router-scope-function/") + assert response.status_code == 503 + assert response.json() == {"detail": "Exception after yield"} + + +def test_router_level_dep_scope_request() -> None: + with TestClient(app, raise_server_exceptions=False) as client: + response = client.get("/router-scope-request/") + assert response.status_code == 200 + assert response.json() == {"status": "ok"} + + +def test_app_level_dep_scope_function() -> None: + app = FastAPI(dependencies=[Depends(raise_after_yield, scope="function")]) + + @app.get("/app-scope-function") + def get_app_scope_function(): + return {"status": "ok"} + + with TestClient(app) as client: + response = client.get("/app-scope-function") + assert response.status_code == 503 + assert response.json() == {"detail": "Exception after yield"} + + +def test_app_level_dep_scope_request() -> None: + app = FastAPI(dependencies=[Depends(raise_after_yield, scope="request")]) + + @app.get("/app-scope-request") + def get_app_scope_request(): + return {"status": "ok"} + + with TestClient(app, raise_server_exceptions=False) as client: + response = client.get("/app-scope-request") + assert response.status_code == 200 + assert response.json() == {"status": "ok"} From 9e5439959a94a4395b4d658165886922d8ac8ad3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 8 Nov 2025 21:43:59 +0000 Subject: [PATCH 092/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 9380fb7d4..b50a2f2dc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Fixes + +* 🐛 Fix `Depends(func, scope='function')` for top level (parameterless) dependencies. PR [#14301](https://github.com/fastapi/fastapi/pull/14301) by [@luzzodev](https://github.com/luzzodev). + ### Docs * 📝 Upate docs for advanced dependencies with `yield`, noting the changes in 0.121.0, adding `scope`. PR [#14287](https://github.com/fastapi/fastapi/pull/14287) by [@tiangolo](https://github.com/tiangolo). From 1c7e2540c22ba5e81250cc715d82db31e52a63d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 8 Nov 2025 22:47:00 +0100 Subject: [PATCH 093/256] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.12?= =?UTF-8?q?1.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index b50a2f2dc..7c2ea54f9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.121.1 + ### Fixes * 🐛 Fix `Depends(func, scope='function')` for top level (parameterless) dependencies. PR [#14301](https://github.com/fastapi/fastapi/pull/14301) by [@luzzodev](https://github.com/luzzodev). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index f4a952bf5..5c4804e19 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.121.0" +__version__ = "0.121.1" from starlette import status as status From db488f3220f8dc934a91150cd7e2f1f8d2e98f51 Mon Sep 17 00:00:00 2001 From: Nils-Hero Lindemann Date: Mon, 10 Nov 2025 09:34:25 +0100 Subject: [PATCH 094/256] =?UTF-8?q?=F0=9F=8C=90=20Sync=20German=20docs=20(?= =?UTF-8?q?#14317)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Sync with #14262 * Sync with #14287 * Two improvements * "Frühzeitig beenden" -> "Frühes Beenden" * "Make herum" bold too --- .../de/docs/advanced/advanced-dependencies.md | 12 ++++- .../dependencies/dependencies-with-yield.md | 45 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/docs/de/docs/advanced/advanced-dependencies.md b/docs/de/docs/advanced/advanced-dependencies.md index da5f28c7c..2254dcf53 100644 --- a/docs/de/docs/advanced/advanced-dependencies.md +++ b/docs/de/docs/advanced/advanced-dependencies.md @@ -70,12 +70,22 @@ Wenn Sie das hier alles verstanden haben, wissen Sie bereits, wie diese Sicherhe Sie benötigen diese technischen Details höchstwahrscheinlich nicht. -Diese Details sind hauptsächlich nützlich, wenn Sie eine FastAPI-Anwendung haben, die älter als 0.118.0 ist, und Sie auf Probleme mit Abhängigkeiten mit `yield` stoßen. +Diese Details sind hauptsächlich nützlich, wenn Sie eine FastAPI-Anwendung haben, die älter als 0.121.0 ist, und Sie auf Probleme mit Abhängigkeiten mit `yield` stoßen. /// Abhängigkeiten mit `yield` haben sich im Laufe der Zeit weiterentwickelt, um verschiedene Anwendungsfälle abzudecken und einige Probleme zu beheben, hier ist eine Zusammenfassung der Änderungen. +### Abhängigkeiten mit `yield` und `scope` { #dependencies-with-yield-and-scope } + +In Version 0.121.0 hat FastAPI Unterstützung für `Depends(scope="function")` für Abhängigkeiten mit `yield` hinzugefügt. + +Mit `Depends(scope="function")` wird der Exit-Code nach `yield` direkt nach dem Ende der *Pfadoperation-Funktion* ausgeführt, bevor die Response an den Client gesendet wird. + +Und bei Verwendung von `Depends(scope="request")` (dem Default) wird der Exit-Code nach `yield` ausgeführt, nachdem die Response gesendet wurde. + +Mehr dazu finden Sie in der Dokumentation zu [Abhängigkeiten mit `yield` – Frühes Beenden und `scope`](../tutorial/dependencies/dependencies-with-yield.md#early-exit-and-scope). + ### Abhängigkeiten mit `yield` und `StreamingResponse`, Technische Details { #dependencies-with-yield-and-streamingresponse-technical-details } Vor FastAPI 0.118.0 wurde bei Verwendung einer Abhängigkeit mit `yield` der Exit-Code nach der *Pfadoperation-Funktion* ausgeführt, aber unmittelbar bevor die Response gesendet wurde. diff --git a/docs/de/docs/tutorial/dependencies/dependencies-with-yield.md b/docs/de/docs/tutorial/dependencies/dependencies-with-yield.md index e65b073a2..34db6c6be 100644 --- a/docs/de/docs/tutorial/dependencies/dependencies-with-yield.md +++ b/docs/de/docs/tutorial/dependencies/dependencies-with-yield.md @@ -184,6 +184,51 @@ Wenn Sie in dem Code der *Pfadoperation-Funktion* irgendeine Exception auslösen /// +## Frühes Beenden und `scope` { #early-exit-and-scope } + +Normalerweise wird der Exit-Code von Abhängigkeiten mit `yield` ausgeführt **nachdem die Response** an den Client gesendet wurde. + +Wenn Sie aber wissen, dass Sie die Abhängigkeit nach der Rückkehr aus der *Pfadoperation-Funktion* nicht mehr benötigen, können Sie `Depends(scope="function")` verwenden, um FastAPI mitzuteilen, dass es die Abhängigkeit nach der Rückkehr aus der *Pfadoperation-Funktion* schließen soll, jedoch **bevor** die **Response gesendet wird**. + +{* ../../docs_src/dependencies/tutorial008e_an_py39.py hl[12,16] *} + +`Depends()` erhält einen `scope`-Parameter, der sein kann: + +* `"function"`: startet die Abhängigkeit vor der *Pfadoperation-Funktion*, die den Request bearbeitet, beendet die Abhängigkeit nach dem Ende der *Pfadoperation-Funktion*, aber **bevor** die Response an den Client zurückgesendet wird. Die Abhängigkeitsfunktion wird also **um** die *Pfadoperation-**Funktion*** **herum** ausgeführt. +* `"request"`: startet die Abhängigkeit vor der *Pfadoperation-Funktion*, die den Request bearbeitet (ähnlich wie bei `"function"`), beendet sie jedoch **nachdem** die Response an den Client zurückgesendet wurde. Die Abhängigkeitsfunktion wird also **um** den **Request**- und Response-Zyklus **herum** ausgeführt. + +Wenn nicht angegeben und die Abhängigkeit `yield` hat, hat sie standardmäßig einen `scope` von `"request"`. + +### `scope` für Unterabhängigkeiten { #scope-for-sub-dependencies } + +Wenn Sie eine Abhängigkeit mit `scope="request"` (dem Default) deklarieren, muss jede Unterabhängigkeit ebenfalls einen `scope` von `"request"` haben. + +Eine Abhängigkeit mit `scope` von `"function"` kann jedoch Abhängigkeiten mit `scope` von `"function"` und `scope` von `"request"` haben. + +Das liegt daran, dass jede Abhängigkeit in der Lage sein muss, ihren Exit-Code vor den Unterabhängigkeiten auszuführen, da sie diese während ihres Exit-Codes möglicherweise noch verwenden muss. + +```mermaid +sequenceDiagram + +participant client as Client +participant dep_req as Abhängigkeit scope="request" +participant dep_func as Abhängigkeit scope="function" +participant operation as Pfadoperation + + client ->> dep_req: Startet den Request + Note over dep_req: Führt den Code bis zum yield aus + dep_req ->> dep_func: Reicht Abhängigkeit weiter + Note over dep_func: Führt den Code bis zum yield aus + dep_func ->> operation: Führt Pfadoperation mit Abhängigkeit aus + operation ->> dep_func: Kehrt aus Pfadoperation zurück + Note over dep_func: Führt Code nach yield aus + Note over dep_func: ✅ Abhängigkeit geschlossen + dep_func ->> client: Sendet Response an Client + Note over client: Response gesendet + Note over dep_req: Führt Code nach yield aus + Note over dep_req: ✅ Abhängigkeit geschlossen +``` + ## Abhängigkeiten mit `yield`, `HTTPException`, `except` und Hintergrundtasks { #dependencies-with-yield-httpexception-except-and-background-tasks } Abhängigkeiten mit `yield` haben sich im Laufe der Zeit weiterentwickelt, um verschiedene Anwendungsfälle abzudecken und einige Probleme zu beheben. From 409e7b503cbac55f0007e4f5f610baaad0da0bcb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 10 Nov 2025 08:34:48 +0000 Subject: [PATCH 095/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 7c2ea54f9..0d25ae643 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Translations + +* 🌐 Sync German docs. PR [#14317](https://github.com/fastapi/fastapi/pull/14317) by [@nilslindemann](https://github.com/nilslindemann). + ## 0.121.1 ### Fixes From c09ba719bae7b33f7d50c6c605a5274ccfbf6430 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Mon, 10 Nov 2025 21:54:56 +0100 Subject: [PATCH 096/256] =?UTF-8?q?=F0=9F=8C=90=20Sync=20Russian=20docs=20?= =?UTF-8?q?(#14331)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update outdated\missing pages in Rus translations --- .../ru/docs/advanced/advanced-dependencies.md | 12 +- ...migrate-from-pydantic-v1-to-pydantic-v2.md | 133 ++++++++++++++++++ .../dependencies/dependencies-with-yield.md | 45 ++++++ 3 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 docs/ru/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md diff --git a/docs/ru/docs/advanced/advanced-dependencies.md b/docs/ru/docs/advanced/advanced-dependencies.md index 75a6f0d1f..339c0a363 100644 --- a/docs/ru/docs/advanced/advanced-dependencies.md +++ b/docs/ru/docs/advanced/advanced-dependencies.md @@ -70,12 +70,22 @@ checker(q="somequery") Скорее всего, вам не понадобятся эти технические детали. -Они полезны главным образом, если у вас было приложение FastAPI версии ниже 0.118.0 и вы столкнулись с проблемами зависимостей с `yield`. +Они полезны главным образом, если у вас было приложение FastAPI версии ниже 0.121.0 и вы столкнулись с проблемами зависимостей с `yield`. /// Зависимости с `yield` со временем изменялись, чтобы учитывать разные случаи применения и исправлять проблемы. Ниже — краткое резюме изменений. +### Зависимости с `yield` и `scope` { #dependencies-with-yield-and-scope } + +В версии 0.121.0 FastAPI добавил поддержку `Depends(scope="function")` для зависимостей с `yield`. + +При использовании `Depends(scope="function")` код после `yield` выполняется сразу после завершения *функции-обработчика пути*, до отправки ответа клиенту. + +А при использовании `Depends(scope="request")` (значение по умолчанию) код после `yield` выполняется после отправки ответа. + +Подробнее читайте в документации: [Зависимости с `yield` — раннее завершение и `scope`](../tutorial/dependencies/dependencies-with-yield.md#early-exit-and-scope). + ### Зависимости с `yield` и `StreamingResponse`, технические детали { #dependencies-with-yield-and-streamingresponse-technical-details } До FastAPI 0.118.0, если вы использовали зависимость с `yield`, код после `yield` выполнялся после возврата из *функции-обработчика пути*, но прямо перед отправкой ответа. diff --git a/docs/ru/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md b/docs/ru/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md new file mode 100644 index 000000000..95481bc66 --- /dev/null +++ b/docs/ru/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md @@ -0,0 +1,133 @@ +# Миграция с Pydantic v1 на Pydantic v2 { #migrate-from-pydantic-v1-to-pydantic-v2 } + +Если у вас старое приложение FastAPI, возможно, вы используете Pydantic версии 1. + +FastAPI поддерживает и Pydantic v1, и v2 начиная с версии 0.100.0. + +Если у вас был установлен Pydantic v2, использовался он. Если вместо этого был установлен Pydantic v1 — использовался он. + +Сейчас Pydantic v1 объявлен устаревшим, и поддержка его будет удалена в следующих версиях FastAPI, поэтому вам следует **перейти на Pydantic v2**. Так вы получите последние возможности, улучшения и исправления. + +/// warning | Предупреждение + +Кроме того, команда Pydantic прекратила поддержку Pydantic v1 для последних версий Python, начиная с **Python 3.14**. + +Если вы хотите использовать последние возможности Python, вам нужно убедиться, что вы используете Pydantic v2. + +/// + +Если у вас старое приложение FastAPI с Pydantic v1, здесь я покажу, как мигрировать на Pydantic v2, и **новые возможности в FastAPI 0.119.0**, которые помогут выполнить постепенную миграцию. + +## Официальное руководство { #official-guide } + +У Pydantic есть официальное руководство по миграции с v1 на v2. + +Там также описано, что изменилось, как валидации стали более корректными и строгими, возможные нюансы и т.д. + +Прочитайте его, чтобы лучше понять, что изменилось. + +## Тесты { #tests } + +Убедитесь, что у вас есть [тесты](../tutorial/testing.md){.internal-link target=_blank} для вашего приложения и что вы запускаете их в системе непрерывной интеграции (CI). + +Так вы сможете выполнить обновление и убедиться, что всё работает как ожидается. + +## `bump-pydantic` { #bump-pydantic } + +Во многих случаях, когда вы используете обычные Pydantic‑модели без пользовательских настроек, вы сможете автоматизировать большую часть процесса миграции с Pydantic v1 на Pydantic v2. + +Вы можете использовать `bump-pydantic` от той же команды Pydantic. + +Этот инструмент поможет автоматически внести большую часть необходимых изменений в код. + +После этого запустите тесты и проверьте, что всё работает. Если да — на этом всё. 😎 + +## Pydantic v1 в v2 { #pydantic-v1-in-v2 } + +Pydantic v2 включает всё из Pydantic v1 как подмодуль `pydantic.v1`. + +Это означает, что вы можете установить последнюю версию Pydantic v2 и импортировать и использовать старые компоненты Pydantic v1 из этого подмодуля так, как если бы у вас был установлен старый Pydantic v1. + +{* ../../docs_src/pydantic_v1_in_v2/tutorial001_an_py310.py hl[1,4] *} + +### Поддержка FastAPI для Pydantic v1 внутри v2 { #fastapi-support-for-pydantic-v1-in-v2 } + +Начиная с FastAPI 0.119.0, есть также частичная поддержка Pydantic v1 в составе Pydantic v2, чтобы упростить миграцию на v2. + +Таким образом, вы можете обновить Pydantic до последней версии 2 и сменить импорты на подмодуль `pydantic.v1` — во многих случаях всё просто заработает. + +{* ../../docs_src/pydantic_v1_in_v2/tutorial002_an_py310.py hl[2,5,15] *} + +/// warning | Предупреждение + +Имейте в виду, что так как команда Pydantic больше не поддерживает Pydantic v1 в последних версиях Python, начиная с Python 3.14, использование `pydantic.v1` также не поддерживается в Python 3.14 и выше. + +/// + +### Pydantic v1 и v2 в одном приложении { #pydantic-v1-and-v2-on-the-same-app } + +В Pydantic **не поддерживается** ситуация, когда в одной модели Pydantic v2 используются поля, определённые как модели Pydantic v1, и наоборот. + +```mermaid +graph TB + subgraph "❌ Not Supported" + direction TB + subgraph V2["Pydantic v2 Model"] + V1Field["Pydantic v1 Model"] + end + subgraph V1["Pydantic v1 Model"] + V2Field["Pydantic v2 Model"] + end + end + + style V2 fill:#f9fff3 + style V1 fill:#fff6f0 + style V1Field fill:#fff6f0 + style V2Field fill:#f9fff3 +``` + +…но в одном и том же приложении вы можете иметь отдельные модели на Pydantic v1 и v2. + +```mermaid +graph TB + subgraph "✅ Supported" + direction TB + subgraph V2["Pydantic v2 Model"] + V2Field["Pydantic v2 Model"] + end + subgraph V1["Pydantic v1 Model"] + V1Field["Pydantic v1 Model"] + end + end + + style V2 fill:#f9fff3 + style V1 fill:#fff6f0 + style V1Field fill:#fff6f0 + style V2Field fill:#f9fff3 +``` + +В некоторых случаях можно использовать и модели Pydantic v1, и v2 в одной и той же операции пути (обработчике пути) вашего приложения FastAPI: + +{* ../../docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py hl[2:3,6,12,21:22] *} + +В примере выше модель входных данных — это модель Pydantic v1, а модель выходных данных (указанная в `response_model=ItemV2`) — это модель Pydantic v2. + +### Параметры Pydantic v1 { #pydantic-v1-parameters } + +Если вам нужно использовать некоторые специфичные для FastAPI инструменты для параметров, такие как `Body`, `Query`, `Form` и т.п., с моделями Pydantic v1, вы можете импортировать их из `fastapi.temp_pydantic_v1_params`, пока завершаете миграцию на Pydantic v2: + +{* ../../docs_src/pydantic_v1_in_v2/tutorial004_an_py310.py hl[4,18] *} + +### Мигрируйте по шагам { #migrate-in-steps } + +/// tip | Совет + +Сначала попробуйте `bump-pydantic`. Если тесты проходят и всё работает, вы справились одной командой. ✨ + +/// + +Если `bump-pydantic` не подходит для вашего случая, вы можете использовать поддержку одновременной работы моделей Pydantic v1 и v2 в одном приложении, чтобы мигрировать на Pydantic v2 постепенно. + +Сначала обновите Pydantic до последней 2-й версии и измените импорты так, чтобы все ваши модели использовали `pydantic.v1`. + +Затем начните мигрировать ваши модели с Pydantic v1 на v2 группами, поэтапно. 🚶 diff --git a/docs/ru/docs/tutorial/dependencies/dependencies-with-yield.md b/docs/ru/docs/tutorial/dependencies/dependencies-with-yield.md index 267faa406..7ff85246d 100644 --- a/docs/ru/docs/tutorial/dependencies/dependencies-with-yield.md +++ b/docs/ru/docs/tutorial/dependencies/dependencies-with-yield.md @@ -184,6 +184,51 @@ participant tasks as Background tasks /// +## Ранний выход и `scope` { #early-exit-and-scope } + +Обычно «код выхода» зависимостей с `yield` выполняется **после того, как ответ** отправлен клиенту. + +Но если вы знаете, что не будете использовать зависимость после возврата из *функции-обработчика пути*, вы можете использовать `Depends(scope="function")`, чтобы сообщить FastAPI, что он должен закрыть зависимость после возврата из *функции-обработчика пути*, но **до того**, как **ответ будет отправлен**. + +{* ../../docs_src/dependencies/tutorial008e_an_py39.py hl[12,16] *} + +`Depends()` принимает параметр `scope`, который может быть: + +* `"function"`: начать зависимость до *функции-обработчика пути*, которая обрабатывает запрос, завершить зависимость после окончания *функции-обработчика пути*, но **до того**, как ответ будет отправлен обратно клиенту. То есть функция зависимости будет выполнена **вокруг** *функции-обработчика пути*. +* `"request"`: начать зависимость до *функции-обработчика пути*, которая обрабатывает запрос (как и при использовании `"function"`), но завершить **после** того, как ответ будет отправлен обратно клиенту. То есть функция зависимости будет выполнена **вокруг** цикла запроса (**request**) и ответа. + +Если не указано и в зависимости есть `yield`, по умолчанию будет `scope` со значением `"request"`. + +### `scope` для подзависимостей { #scope-for-sub-dependencies } + +Когда вы объявляете зависимость с `scope="request"` (значение по умолчанию), любая подзависимость также должна иметь `scope` равный `"request"`. + +Но зависимость со `scope` равным `"function"` может иметь зависимости со `scope` `"function"` и со `scope` `"request"`. + +Это потому, что любая зависимость должна иметь возможность выполнить свой код выхода раньше подзависимостей, так как ей может понадобиться использовать их во время своего кода выхода. + +```mermaid +sequenceDiagram + +participant client as Client +participant dep_req as Зависимость scope="request" +participant dep_func as Зависимость scope="function" +participant operation as Функция-обработчик пути + + client ->> dep_req: Запрос + Note over dep_req: Выполнить код до yield + dep_req ->> dep_func: Передать значение + Note over dep_func: Выполнить код до yield + dep_func ->> operation: Выполнить функцию-обработчик пути + operation ->> dep_func: Выход из функции-обработчика пути + Note over dep_func: Выполнить код после yield + Note over dep_func: ✅ Зависимость закрыта + dep_func ->> client: Отправить ответ клиенту + Note over client: Ответ отправлен + Note over dep_req: Выполнить код после yield + Note over dep_req: ✅ Зависимость закрыта +``` + ## Зависимости с `yield`, `HTTPException`, `except` и фоновыми задачами { #dependencies-with-yield-httpexception-except-and-background-tasks } Зависимости с `yield` со временем эволюционировали, чтобы покрыть разные сценарии и исправить некоторые проблемы. From 6fae64ff49e8328048872714652a4dfa15406e41 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 10 Nov 2025 20:55:20 +0000 Subject: [PATCH 097/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 0d25ae643..688e65e71 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Translations +* 🌐 Sync Russian docs. PR [#14331](https://github.com/fastapi/fastapi/pull/14331) by [@YuriiMotov](https://github.com/YuriiMotov). * 🌐 Sync German docs. PR [#14317](https://github.com/fastapi/fastapi/pull/14317) by [@nilslindemann](https://github.com/nilslindemann). ## 0.121.1 From 0878361f6b8bdfabaf3d6075ad93f9d8ab6771d3 Mon Sep 17 00:00:00 2001 From: Rafael de Oliveira Marques Date: Wed, 12 Nov 2025 12:22:59 -0300 Subject: [PATCH 098/256] =?UTF-8?q?=F0=9F=94=A8=20Add=20Portuguese=20trans?= =?UTF-8?q?lations=20LLM=20prompt=20(#14208)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/pt/docs/_llm-test.md | 503 ++++++++++++++++++++++++++++++++++++++ docs/pt/llm-prompt.md | 66 +++++ 2 files changed, 569 insertions(+) create mode 100644 docs/pt/docs/_llm-test.md create mode 100644 docs/pt/llm-prompt.md diff --git a/docs/pt/docs/_llm-test.md b/docs/pt/docs/_llm-test.md new file mode 100644 index 000000000..6aed4928c --- /dev/null +++ b/docs/pt/docs/_llm-test.md @@ -0,0 +1,503 @@ +# Arquivo de teste de LLM { #llm-test-file } + +Este documento testa se o LLM, que traduz a documentação, entende o `general_prompt` em `scripts/translate.py` e o prompt específico do idioma em `docs/{language code}/llm-prompt.md`. O prompt específico do idioma é anexado ao `general_prompt`. + +Os testes adicionados aqui serão vistos por todos os autores dos prompts específicos de idioma. + +Use da seguinte forma: + +* Tenha um prompt específico do idioma – `docs/{language code}/llm-prompt.md`. +* Faça uma tradução nova deste documento para o seu idioma de destino (veja, por exemplo, o comando `translate-page` do `translate.py`). Isso criará a tradução em `docs/{language code}/docs/_llm-test.md`. +* Verifique se está tudo certo na tradução. +* Se necessário, melhore seu prompt específico do idioma, o prompt geral ou o documento em inglês. +* Em seguida, corrija manualmente os problemas restantes na tradução, para que fique uma boa tradução. +* Retraduzir, tendo a boa tradução no lugar. O resultado ideal seria que o LLM não fizesse mais mudanças na tradução. Isso significa que o prompt geral e o seu prompt específico do idioma estão tão bons quanto possível (às vezes fará algumas mudanças aparentemente aleatórias, a razão é que LLMs não são algoritmos determinísticos). + +Os testes: + +## Trechos de código { #code-snippets} + +//// tab | Teste + +Este é um trecho de código: `foo`. E este é outro trecho de código: `bar`. E mais um: `baz quux`. + +//// + +//// tab | Informações + +O conteúdo dos trechos de código deve ser deixado como está. + +Veja a seção `### Content of code snippets` no prompt geral em `scripts/translate.py`. + +//// + +## Citações { #quotes } + +//// tab | Teste + +Ontem, meu amigo escreveu: "Se você soletrar incorretamente corretamente, você a soletrou incorretamente". Ao que respondi: "Correto, mas 'incorrectly' está incorretamente não '"incorrectly"'". + +/// note | Nota + +O LLM provavelmente vai traduzir isso errado. O interessante é apenas se ele mantém a tradução corrigida ao retraduzir. + +/// + +//// + +//// tab | Informações + +O autor do prompt pode escolher se deseja converter aspas neutras em aspas tipográficas. Também é aceitável deixá-las como estão. + +Veja, por exemplo, a seção `### Quotes` em `docs/de/llm-prompt.md`. + +//// + +## Citações em trechos de código { #quotes-in-code-snippets} + +//// tab | Teste + +`pip install "foo[bar]"` + +Exemplos de literais de string em trechos de código: `"this"`, `'that'`. + +Um exemplo difícil de literais de string em trechos de código: `f"I like {'oranges' if orange else "apples"}"` + +Pesado: `Yesterday, my friend wrote: "If you spell incorrectly correctly, you have spelled it incorrectly". To which I answered: "Correct, but 'incorrectly' is incorrectly not '"incorrectly"'"` + +//// + +//// tab | Informações + +... No entanto, as aspas dentro de trechos de código devem permanecer como estão. + +//// + +## Blocos de código { #code-blocks } + +//// tab | Teste + +Um exemplo de código Bash... + +```bash +# Imprimir uma saudação ao universo +echo "Hello universe" +``` + +...e um exemplo de código de console... + +```console +$ fastapi run main.py + FastAPI Starting server + Searching for package file structure +``` + +...e outro exemplo de código de console... + +```console +// Crie um diretório "Code" +$ mkdir code +// Entre nesse diretório +$ cd code +``` + +...e um exemplo de código Python... + +```Python +wont_work() # Isto não vai funcionar 😱 +works(foo="bar") # Isto funciona 🎉 +``` + +...e é isso. + +//// + +//// tab | Informações + +O código em blocos de código não deve ser modificado, com exceção dos comentários. + +Veja a seção `### Content of code blocks` no prompt geral em `scripts/translate.py`. + +//// + +## Abas e caixas coloridas { #tabs-and-colored-boxes } + +//// tab | Teste + +/// info | Informação +Algum texto +/// + +/// note | Nota +Algum texto +/// + +/// note | Detalhes Técnicos +Algum texto +/// + +/// check | Verifique +Algum texto +/// + +/// tip | Dica +Algum texto +/// + +/// warning | Atenção +Algum texto +/// + +/// danger | Cuidado +Algum texto +/// + +//// + +//// tab | Informações + +Abas e blocos `Info`/`Note`/`Warning`/etc. devem ter a tradução do seu título adicionada após uma barra vertical (`|`). + +Veja as seções `### Special blocks` e `### Tab blocks` no prompt geral em `scripts/translate.py`. + +//// + +## Links da Web e internos { #web-and-internal-links } + +//// tab | Teste + +O texto do link deve ser traduzido, o endereço do link deve permanecer inalterado: + +* [Link para o título acima](#code-snippets) +* [Link interno](index.md#installation){.internal-link target=_blank} +* Link externo +* Link para um estilo +* Link para um script +* Link para uma imagem + +O texto do link deve ser traduzido, o endereço do link deve apontar para a tradução: + +* Link do FastAPI + +//// + +//// tab | Informações + +Os links devem ser traduzidos, mas seus endereços devem permanecer inalterados. Uma exceção são links absolutos para páginas da documentação do FastAPI. Nesse caso, devem apontar para a tradução. + +Veja a seção `### Links` no prompt geral em `scripts/translate.py`. + +//// + +## Elementos HTML "abbr" { #html-abbr-elements } + +//// tab | Teste + +Aqui estão algumas coisas envolvidas em elementos HTML "abbr" (algumas são inventadas): + +### O abbr fornece uma frase completa { #the-abbr-gives-a-full-phrase } + +* GTD +* lt +* XWT +* PSGI + +### O abbr fornece uma explicação { #the-abbr-gives-an-explanation } + +* cluster +* Aprendizado Profundo + +### O abbr fornece uma frase completa e uma explicação { #the-abbr-gives-a-full-phrase-and-an-explanation } + +* MDN +* I/O. + +//// + +//// tab | Informações + +Os atributos "title" dos elementos "abbr" são traduzidos seguindo algumas instruções específicas. + +As traduções podem adicionar seus próprios elementos "abbr" que o LLM não deve remover. Por exemplo, para explicar palavras em inglês. + +Veja a seção `### HTML abbr elements` no prompt geral em `scripts/translate.py`. + +//// + +## Títulos { #headings } + +//// tab | Teste + +### Desenvolver uma aplicação web - um tutorial { #develop-a-webapp-a-tutorial } + +Olá. + +### Anotações de tipo e -anotações { #type-hints-and-annotations } + +Olá novamente. + +### Super- e subclasses { #super-and-subclasses } + +Olá novamente. + +//// + +//// tab | Informações + +A única regra rígida para títulos é que o LLM deixe a parte do hash dentro de chaves inalterada, o que garante que os links não quebrem. + +Veja a seção `### Headings` no prompt geral em `scripts/translate.py`. + +Para algumas instruções específicas do idioma, veja, por exemplo, a seção `### Headings` em `docs/de/llm-prompt.md`. + +//// + +## Termos usados na documentação { #terms-used-in-the-docs } + +//// tab | Teste + +* você +* seu + +* por exemplo +* etc. + +* `foo` como um `int` +* `bar` como uma `str` +* `baz` como uma `list` + +* o Tutorial - Guia do Usuário +* o Guia do Usuário Avançado +* a documentação do SQLModel +* a documentação da API +* a documentação automática + +* Ciência de Dados +* Aprendizado Profundo +* Aprendizado de Máquina +* Injeção de Dependências +* autenticação HTTP Basic +* HTTP Digest +* formato ISO +* o padrão JSON Schema +* o JSON schema +* a definição do schema +* Fluxo de Senha +* Mobile + +* descontinuado +* projetado +* inválido +* dinamicamente +* padrão +* padrão predefinido +* sensível a maiúsculas e minúsculas +* não sensível a maiúsculas e minúsculas + +* servir a aplicação +* servir a página + +* o app +* a aplicação + +* a requisição +* a resposta +* a resposta de erro + +* a operação de rota +* o decorador de operação de rota +* a função de operação de rota + +* o corpo +* o corpo da requisição +* o corpo da resposta +* o corpo JSON +* o corpo do formulário +* o corpo do arquivo +* o corpo da função + +* o parâmetro +* o parâmetro de corpo +* o parâmetro de path +* o parâmetro de query +* o parâmetro de cookie +* o parâmetro de header +* o parâmetro de formulário +* o parâmetro da função + +* o evento +* o evento de inicialização +* a inicialização do servidor +* o evento de encerramento +* o evento de lifespan + +* o manipulador +* o manipulador de eventos +* o manipulador de exceções +* tratar + +* o modelo +* o modelo Pydantic +* o modelo de dados +* o modelo de banco de dados +* o modelo de formulário +* o objeto de modelo + +* a classe +* a classe base +* a classe pai +* a subclasse +* a classe filha +* a classe irmã +* o método de classe + +* o cabeçalho +* os cabeçalhos +* o cabeçalho de autorização +* o cabeçalho `Authorization` +* o cabeçalho encaminhado + +* o sistema de injeção de dependências +* a dependência +* o dependable +* o dependant + +* limitado por I/O +* limitado por CPU +* concorrência +* paralelismo +* multiprocessamento + +* a env var +* a variável de ambiente +* o `PATH` +* a variável `PATH` + +* a autenticação +* o provedor de autenticação +* a autorização +* o formulário de autorização +* o provedor de autorização +* o usuário se autentica +* o sistema autentica o usuário + +* a CLI +* a interface de linha de comando + +* o servidor +* o cliente + +* o provedor de nuvem +* o serviço de nuvem + +* o desenvolvimento +* as etapas de desenvolvimento + +* o dict +* o dicionário +* a enumeração +* o enum +* o membro do enum + +* o codificador +* o decodificador +* codificar +* decodificar + +* a exceção +* lançar + +* a expressão +* a instrução + +* o frontend +* o backend + +* a discussão do GitHub +* a issue do GitHub + +* o desempenho +* a otimização de desempenho + +* o tipo de retorno +* o valor de retorno + +* a segurança +* o esquema de segurança + +* a tarefa +* a tarefa em segundo plano +* a função da tarefa + +* o template +* o mecanismo de template + +* a anotação de tipo +* a anotação de tipo + +* o worker de servidor +* o worker do Uvicorn +* o Worker do Gunicorn +* o processo worker +* a classe de worker +* a carga de trabalho + +* a implantação +* implantar + +* o SDK +* o kit de desenvolvimento de software + +* o `APIRouter` +* o `requirements.txt` +* o Bearer Token +* a alteração com quebra de compatibilidade +* o bug +* o botão +* o chamável +* o código +* o commit +* o gerenciador de contexto +* a corrotina +* a sessão do banco de dados +* o disco +* o domínio +* o mecanismo +* o X falso +* o método HTTP GET +* o item +* a biblioteca +* o lifespan +* o bloqueio +* o middleware +* a aplicação mobile +* o módulo +* a montagem +* a rede +* a origem +* a sobrescrita +* a carga útil +* o processador +* a propriedade +* o proxy +* o pull request +* a consulta +* a RAM +* a máquina remota +* o código de status +* a string +* a tag +* o framework web +* o curinga +* retornar +* validar + +//// + +//// tab | Informações + +Esta é uma lista não completa e não normativa de termos (principalmente) técnicos vistos na documentação. Pode ser útil para o autor do prompt descobrir para quais termos o LLM precisa de uma ajudinha. Por exemplo, quando ele continua revertendo uma boa tradução para uma tradução subótima. Ou quando tem problemas para conjugar/declinar um termo no seu idioma. + +Veja, por exemplo, a seção `### List of English terms and their preferred German translations` em `docs/de/llm-prompt.md`. + +//// diff --git a/docs/pt/llm-prompt.md b/docs/pt/llm-prompt.md new file mode 100644 index 000000000..01ce4143c --- /dev/null +++ b/docs/pt/llm-prompt.md @@ -0,0 +1,66 @@ +### Target language + +Translate to Portuguese (Português). + +Language code: pt. + +For instructions or titles in imperative, keep them in imperative, for example "Import FastAPI" to "Importe o FastAPI". + +Keep existing translations as they are if the term is already translated. + +When translating documentation into Portuguese, use neutral and widely understandable language. Although Portuguese originated in Portugal and has its largest number of speakers in Brazil, it is also an official language in several countries and regions, such as Equatorial Guinea, Mozambique, Angola, Cape Verde, and São Tomé and Príncipe. Avoid words or expressions that are specific to a single country or region. + +--- + +For the next terms, use the following translations: + +* «/// check»: «/// check | Verifique» +* «/// danger»: «/// danger | Cuidado» +* «/// info»: «/// info | Informação» +* «/// note | Technical Details»: «/// note | Detalhes Técnicos» +* «/// info | Very Technical Details»: «/// note | Detalhes Técnicos Avançados» +* «/// note»: «/// note | Nota» +* «/// tip»: «/// tip | Dica» +* «/// warning»: «/// warning | Atenção» +* «(you should)»: «(você deveria)» +* async context manager: gerenciador de contexto assíncrono +* autocomplete: autocompletar +* autocompletion: preenchimento automático +* bug: bug +* context manager: gerenciador de contexto +* cross domain: cross domain (do not translate to "domínio cruzado") +* cross origin: cross origin (do not translate to "origem cruzada") +* Cross-Origin Resource Sharing: Cross-Origin Resource Sharing (do not translate to "Compartilhamento de Recursos de Origem Cruzada") +* Deep Learning: Deep Learning (do not translate to "Aprendizado Profundo") +* dependable: dependable +* dependencies: dependências +* deprecated: descontinuado +* docs: documentação +* FastAPI app: aplicação FastAPI +* framework: framework (do not translate) +* feature: funcionalidade +* guides: tutoriais +* I/O (as in "input and output"): I/O (do not translate to "E/S") +* JSON Schema: JSON Schema +* library: biblioteca +* lifespan: lifespan (do not translate to "vida útil") +* list (as in Python list): list +* Machine Learning: Aprendizado de Máquina +* media type: media type (do not translate to "tipo de mídia") +* non-Annotated: non-Annotated (do not translate non-Annotated when it comes after a Python version.e.g., “Python 3.8+ non-Annotated”) +* operation IDs: IDs de operação +* path (as in URL path): path +* path operation: operação de rota +* path operation function: função de operação de rota +* prefix: prefixo +* request (as in HTTP request): request (do not change if it's already translated to requisição) +* router (as in FastAPI's router): router (do not change if it's already translated to "roteador" or "roteadores") +* response (as in HTTP response): response (do not change if it's already translated to resposta) +* shutdown (of the app): encerramento +* shutdown event (of the app): evento de encerramento +* startup (as in the event of the app): inicialização +* startup event (as in the event of the app): evento de inicialização +* Stream: Stream +* string: string +* type hints: anotações de tipo +* wildcards: curingas From 1a2e4152ed1173dc129bf4feb45c84b704c55eb8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 12 Nov 2025 15:23:24 +0000 Subject: [PATCH 099/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 688e65e71..b16770b3c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Translations +* 🔨 Add Portuguese translations LLM prompt. PR [#14208](https://github.com/fastapi/fastapi/pull/14208) by [@ceb10n](https://github.com/ceb10n). * 🌐 Sync Russian docs. PR [#14331](https://github.com/fastapi/fastapi/pull/14331) by [@YuriiMotov](https://github.com/YuriiMotov). * 🌐 Sync German docs. PR [#14317](https://github.com/fastapi/fastapi/pull/14317) by [@nilslindemann](https://github.com/nilslindemann). From 540a83da65b2e3b219ee3d41c4be7837ab05dc84 Mon Sep 17 00:00:00 2001 From: Rafael de Oliveira Marques Date: Wed, 12 Nov 2025 13:23:57 -0300 Subject: [PATCH 100/256] =?UTF-8?q?=F0=9F=8C=90=20Update=20Portuguese=20tr?= =?UTF-8?q?anslations=20with=20LLM=20prompt=20(#14228)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * validated llm translation * validated llm translation * validated llm translation * validated llm translation * validated llm translation * validated llm translation * validated llm translation * validated llm translation * validated llm translation * validated llm translation * validated llm translation * validated llm translation * validated llm translation * validated llm translation * validated llm translation * validated llm translation * validated llm translation * fix non-Annotated in llm-prompt * rerun after a few changes in llm-prompt * fix non-Annotated * validated llm translation * fix llm translation * update outdated translations * fix translation for operation IDs * add header link * add missing link * fix line break * fix diff * fix llm translation * fix 'Atualize' to 'Atualizar' * update alternatives.md * update async.md * update fastapi-cli.md * update features.md * update help-fastapi.md * update history-design-future.md * update index.md * update advanced/events.md * update advanced/middleware.md * update advanced/response-cookies.md * update advanced/response-headers.md * update advanced/templates.md * update advanced/testing-websockets.md * update advanced/using-request-directly.md * update advanced/websockets.md * update advanced/security/oauth2-scopes.md * update deployment/cloud.md * update deployment/manually.md * update how-to/custom-request-and-route.md * update how-to/migrate-from-pydantic-v1-to-pydantic-v2.md * update tutorial/background-tasks.md * update tutorial/first-steps.md * update tutorial/handling-errors.md * update tutorial/middleware.md * update tutorial/request-files.md * update tutorial/sql-databases.md * update tutorial/static-files.md * update tutorial/testing.md * update tutorial/dependencies/dependencies-with-yield.md * update advanced/advanced-dependencies.md --------- Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> --- docs/pt/docs/about/index.md | 4 +- docs/pt/docs/advanced/additional-responses.md | 26 +- .../docs/advanced/additional-status-codes.md | 18 +- .../pt/docs/advanced/advanced-dependencies.md | 110 ++++- docs/pt/docs/advanced/async-tests.md | 22 +- docs/pt/docs/advanced/behind-a-proxy.md | 187 ++++++-- docs/pt/docs/advanced/custom-response.md | 62 ++- docs/pt/docs/advanced/dataclasses.md | 14 +- docs/pt/docs/advanced/events.md | 117 +++-- docs/pt/docs/advanced/generate-clients.md | 185 +++----- docs/pt/docs/advanced/index.md | 14 +- docs/pt/docs/advanced/middleware.md | 19 +- docs/pt/docs/advanced/openapi-callbacks.md | 58 +-- docs/pt/docs/advanced/openapi-webhooks.md | 14 +- .../path-operation-advanced-configuration.md | 44 +- .../advanced/response-change-status-code.md | 6 +- docs/pt/docs/advanced/response-cookies.md | 12 +- docs/pt/docs/advanced/response-directly.md | 15 +- docs/pt/docs/advanced/response-headers.md | 14 +- .../docs/advanced/security/http-basic-auth.md | 21 +- docs/pt/docs/advanced/security/index.md | 6 +- .../docs/advanced/security/oauth2-scopes.md | 56 +-- docs/pt/docs/advanced/settings.md | 316 ++++--------- docs/pt/docs/advanced/sub-applications.md | 34 +- docs/pt/docs/advanced/templates.md | 30 +- docs/pt/docs/advanced/testing-dependencies.md | 12 +- docs/pt/docs/advanced/testing-events.md | 10 +- docs/pt/docs/advanced/testing-websockets.md | 2 +- .../docs/advanced/using-request-directly.md | 14 +- docs/pt/docs/advanced/websockets.md | 40 +- docs/pt/docs/advanced/wsgi.md | 16 +- docs/pt/docs/alternatives.md | 335 +++++++------- docs/pt/docs/async.md | 147 +++--- docs/pt/docs/benchmarks.md | 6 +- docs/pt/docs/deployment/cloud.md | 13 +- docs/pt/docs/deployment/concepts.md | 56 +-- docs/pt/docs/deployment/docker.md | 419 ++++++----------- docs/pt/docs/deployment/https.md | 180 +++++--- docs/pt/docs/deployment/index.md | 22 +- docs/pt/docs/deployment/manually.md | 20 +- docs/pt/docs/deployment/server-workers.md | 10 +- docs/pt/docs/deployment/versions.md | 64 +-- docs/pt/docs/environment-variables.md | 14 +- docs/pt/docs/fastapi-cli.md | 100 ++-- docs/pt/docs/features.md | 63 ++- docs/pt/docs/help-fastapi.md | 319 ++++++++----- docs/pt/docs/history-design-future.md | 18 +- docs/pt/docs/how-to/conditional-openapi.md | 24 +- docs/pt/docs/how-to/configure-swagger-ui.md | 22 +- docs/pt/docs/how-to/custom-docs-ui-assets.md | 44 +- .../docs/how-to/custom-request-and-route.md | 24 +- docs/pt/docs/how-to/extending-openapi.md | 20 +- docs/pt/docs/how-to/general.md | 39 +- docs/pt/docs/how-to/graphql.md | 16 +- docs/pt/docs/how-to/index.md | 6 +- ...migrate-from-pydantic-v1-to-pydantic-v2.md | 133 ++++++ .../docs/how-to/separate-openapi-schemas.md | 24 +- docs/pt/docs/how-to/testing-database.md | 4 +- docs/pt/docs/index.md | 82 ++-- docs/pt/docs/learn/index.md | 6 +- docs/pt/docs/project-generation.md | 6 +- docs/pt/docs/python-types.md | 61 ++- docs/pt/docs/resources/index.md | 2 +- docs/pt/docs/tutorial/background-tasks.md | 77 ++-- docs/pt/docs/tutorial/bigger-applications.md | 64 +-- docs/pt/docs/tutorial/body-fields.md | 27 +- docs/pt/docs/tutorial/body-multiple-params.md | 22 +- docs/pt/docs/tutorial/body-nested-models.md | 100 ++-- docs/pt/docs/tutorial/body-updates.md | 20 +- docs/pt/docs/tutorial/body.md | 86 ++-- docs/pt/docs/tutorial/cookie-param-models.md | 20 +- docs/pt/docs/tutorial/cookie-params.md | 23 +- docs/pt/docs/tutorial/cors.md | 39 +- docs/pt/docs/tutorial/debugging.md | 14 +- .../dependencies/classes-as-dependencies.md | 16 +- ...pendencies-in-path-operation-decorators.md | 26 +- .../dependencies/dependencies-with-yield.md | 230 +++------- .../dependencies/global-dependencies.md | 13 +- docs/pt/docs/tutorial/dependencies/index.md | 36 +- .../tutorial/dependencies/sub-dependencies.md | 18 +- docs/pt/docs/tutorial/encoder.md | 6 +- docs/pt/docs/tutorial/extra-data-types.md | 14 +- docs/pt/docs/tutorial/extra-models.md | 64 +-- docs/pt/docs/tutorial/first-steps.md | 90 ++-- docs/pt/docs/tutorial/handling-errors.md | 51 +- docs/pt/docs/tutorial/header-param-models.md | 28 +- docs/pt/docs/tutorial/header-params.md | 28 +- docs/pt/docs/tutorial/index.md | 22 +- docs/pt/docs/tutorial/metadata.md | 24 +- docs/pt/docs/tutorial/middleware.md | 45 +- .../tutorial/path-operation-configuration.md | 33 +- .../path-params-numeric-validations.md | 129 ++++-- docs/pt/docs/tutorial/path-params.md | 220 ++++----- docs/pt/docs/tutorial/query-param-models.md | 10 +- .../tutorial/query-params-str-validations.md | 434 +++++++++++++----- docs/pt/docs/tutorial/query-params.md | 37 +- docs/pt/docs/tutorial/request-files.md | 32 +- docs/pt/docs/tutorial/request-form-models.md | 10 +- .../docs/tutorial/request-forms-and-files.md | 22 +- docs/pt/docs/tutorial/request-forms.md | 36 +- docs/pt/docs/tutorial/request_files.md | 172 ------- docs/pt/docs/tutorial/response-model.md | 94 ++-- docs/pt/docs/tutorial/response-status-code.md | 31 +- docs/pt/docs/tutorial/schema-extra-example.md | 206 +++++++-- docs/pt/docs/tutorial/security/first-steps.md | 187 ++++---- .../tutorial/security/get-current-user.md | 208 ++++----- docs/pt/docs/tutorial/security/oauth2-jwt.md | 51 +- .../docs/tutorial/security/simple-oauth2.md | 46 +- docs/pt/docs/tutorial/sql-databases.md | 61 ++- docs/pt/docs/tutorial/static-files.md | 30 +- docs/pt/docs/tutorial/testing.md | 22 +- docs/pt/docs/virtual-environments.md | 56 +-- 112 files changed, 3751 insertions(+), 3376 deletions(-) create mode 100644 docs/pt/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md delete mode 100644 docs/pt/docs/tutorial/request_files.md diff --git a/docs/pt/docs/about/index.md b/docs/pt/docs/about/index.md index 1f42e8831..39e160741 100644 --- a/docs/pt/docs/about/index.md +++ b/docs/pt/docs/about/index.md @@ -1,3 +1,3 @@ -# Sobre +# Sobre { #about } -Sobre o FastAPI, seus padrões, inspirações e muito mais. 🤓 +Sobre o FastAPI, seu design, inspiração e mais. 🤓 diff --git a/docs/pt/docs/advanced/additional-responses.md b/docs/pt/docs/advanced/additional-responses.md index 1060d18af..fef7f5cb1 100644 --- a/docs/pt/docs/advanced/additional-responses.md +++ b/docs/pt/docs/advanced/additional-responses.md @@ -1,6 +1,6 @@ -# Retornos Adicionais no OpenAPI +# Retornos Adicionais no OpenAPI { #additional-responses-in-openapi } -/// warning | Aviso +/// warning | Atenção Este é um tema bem avançado. @@ -14,7 +14,7 @@ Essas respostas adicionais serão incluídas no esquema do OpenAPI, e também ap Porém para as respostas adicionais, você deve garantir que está retornando um `Response` como por exemplo o `JSONResponse` diretamente, junto com o código de status e o conteúdo. -## Retorno Adicional com `model` +## Retorno Adicional com `model` { #additional-response-with-model } Você pode fornecer o parâmetro `responses` aos seus *decoradores de caminho*. @@ -49,7 +49,7 @@ O local correto é: /// -O retorno gerado no OpenAI para esta *operação de caminho* será: +O retorno gerado no OpenAPI para esta *operação de rota* será: ```JSON hl_lines="3-12" { @@ -169,11 +169,11 @@ Os esquemas são referenciados em outro local dentro do esquema OpenAPI: } ``` -## Media types adicionais para o retorno principal +## Media types adicionais para o retorno principal { #additional-media-types-for-the-main-response } Você pode utilizar o mesmo parâmetro `responses` para adicionar diferentes media types para o mesmo retorno principal. -Por exemplo, você pode adicionar um media type adicional de `image/png`, declarando que a sua *operação de caminho* pode retornar um objeto JSON (com o media type `application/json`) ou uma imagem PNG: +Por exemplo, você pode adicionar um media type adicional de `image/png`, declarando que a sua *operação de rota* pode retornar um objeto JSON (com o media type `application/json`) ou uma imagem PNG: {* ../../docs_src/additional_responses/tutorial002.py hl[19:24,28] *} @@ -191,7 +191,7 @@ Porém se você especificou uma classe de retorno com o valor `None` como media /// -## Combinando informações +## Combinando informações { #combining-information } Você também pode combinar informações de diferentes lugares, incluindo os parâmetros `response_model`, `status_code`, e `responses`. @@ -209,9 +209,9 @@ Isso será combinado e incluído em seu OpenAPI, e disponibilizado na documenta -## Combinar retornos predefinidos e personalizados +## Combinar retornos predefinidos e personalizados { #combine-predefined-responses-and-custom-ones } -Você pode querer possuir alguns retornos predefinidos que são aplicados para diversas *operações de caminho*, porém você deseja combinar com retornos personalizados que são necessários para cada *operação de caminho*. +Você pode querer possuir alguns retornos predefinidos que são aplicados para diversas *operações de rota*, porém você deseja combinar com retornos personalizados que são necessários para cada *operação de rota*. Para estes casos, você pode utilizar a técnica do Python de "desempacotamento" de um `dict` utilizando `**dict_to_unpack`: @@ -233,15 +233,15 @@ Aqui, o `new_dict` terá todos os pares de chave-valor do `old_dict` mais o novo } ``` -Você pode utilizar essa técnica para reutilizar alguns retornos predefinidos nas suas *operações de caminho* e combiná-las com personalizações adicionais. +Você pode utilizar essa técnica para reutilizar alguns retornos predefinidos nas suas *operações de rota* e combiná-las com personalizações adicionais. Por exemplo: {* ../../docs_src/additional_responses/tutorial004.py hl[13:17,26] *} -## Mais informações sobre retornos OpenAPI +## Mais informações sobre retornos OpenAPI { #more-information-about-openapi-responses } Para verificar exatamente o que você pode incluir nos retornos, você pode conferir estas seções na especificação do OpenAPI: -* Objeto de Retorno OpenAPI, inclui o `Response Object`. -* Objeto de Retorno OpenAPI, você pode incluir qualquer coisa dele diretamente em cada retorno dentro do seu parâmetro `responses`. Incluindo `description`, `headers`, `content` (dentro dele que você declara diferentes media types e esquemas JSON), e `links`. +* Objeto de Retorno OpenAPI, inclui o `Response Object`. +* Objeto de Retorno OpenAPI, você pode incluir qualquer coisa dele diretamente em cada retorno dentro do seu parâmetro `responses`. Incluindo `description`, `headers`, `content` (dentro dele que você declara diferentes media types e esquemas JSON), e `links`. diff --git a/docs/pt/docs/advanced/additional-status-codes.md b/docs/pt/docs/advanced/additional-status-codes.md index 06d619151..fd90b1795 100644 --- a/docs/pt/docs/advanced/additional-status-codes.md +++ b/docs/pt/docs/advanced/additional-status-codes.md @@ -1,22 +1,22 @@ -# Códigos de status adicionais +# Códigos de status adicionais { #additional-status-codes } -Por padrão, o **FastAPI** retornará as respostas utilizando o `JSONResponse`, adicionando o conteúdo do retorno da sua *operação de caminho* dentro do `JSONResponse`. +Por padrão, o **FastAPI** retornará as respostas utilizando o `JSONResponse`, adicionando o conteúdo do retorno da sua *operação de rota* dentro do `JSONResponse`. -Ele usará o código de status padrão ou o que você definir na sua *operação de caminho*. +Ele usará o código de status padrão ou o que você definir na sua *operação de rota*. -## Códigos de status adicionais +## Códigos de status adicionais { #additional-status-codes_1 } Caso você queira retornar códigos de status adicionais além do código principal, você pode fazer isso retornando um `Response` diretamente, como por exemplo um `JSONResponse`, e definir os códigos de status adicionais diretamente. -Por exemplo, vamos dizer que você deseja ter uma *operação de caminho* que permita atualizar itens, e retornar um código de status HTTP 200 "OK" quando for bem sucedido. +Por exemplo, vamos dizer que você deseja ter uma *operação de rota* que permita atualizar itens, e retornar um código de status HTTP 200 "OK" quando for bem sucedido. -Mas você também deseja aceitar novos itens. E quando os itens não existiam, ele os cria, e retorna o código de status HTTP 201 "Created. +Mas você também deseja aceitar novos itens. E quando os itens não existiam, ele os cria, e retorna o código de status HTTP 201 "Created". Para conseguir isso, importe `JSONResponse` e retorne o seu conteúdo diretamente, definindo o `status_code` que você deseja: {* ../../docs_src/additional_status_codes/tutorial001_an_py310.py hl[4,25] *} -/// warning | Aviso +/// warning | Atenção Quando você retorna um `Response` diretamente, como no exemplo acima, ele será retornado diretamente. @@ -26,7 +26,7 @@ Garanta que ele tenha toda informação que você deseja, e que os valores sejam /// -/// note | Detalhes técnicos +/// note | Detalhes Técnicos Você também pode utilizar `from starlette.responses import JSONResponse`. @@ -34,7 +34,7 @@ O **FastAPI** disponibiliza o `starlette.responses` como `fastapi.responses` ape /// -## OpenAPI e documentação da API +## OpenAPI e documentação da API { #openapi-and-api-docs } Se você retorna códigos de status adicionais e retornos diretamente, eles não serão incluídos no esquema do OpenAPI (a documentação da API), porque o FastAPI não tem como saber de antemão o que será retornado. diff --git a/docs/pt/docs/advanced/advanced-dependencies.md b/docs/pt/docs/advanced/advanced-dependencies.md index f57abba61..4f9353195 100644 --- a/docs/pt/docs/advanced/advanced-dependencies.md +++ b/docs/pt/docs/advanced/advanced-dependencies.md @@ -1,6 +1,6 @@ -# Dependências avançadas +# Dependências avançadas { #advanced-dependencies } -## Dependências parametrizadas +## Dependências parametrizadas { #parameterized-dependencies } Todas as dependências que vimos até agora são funções ou classes fixas. @@ -10,7 +10,7 @@ Vamos imaginar que queremos ter uma dependência que verifica se o parâmetro de Porém nós queremos poder parametrizar o conteúdo fixo. -## Uma instância "chamável" +## Uma instância "chamável" { #a-callable-instance } Em Python existe uma maneira de fazer com que uma instância de uma classe seja um "chamável". @@ -22,7 +22,7 @@ Para fazer isso, nós declaramos o método `__call__`: Neste caso, o `__call__` é o que o **FastAPI** utilizará para verificar parâmetros adicionais e sub dependências, e isso é o que será chamado para passar o valor ao parâmetro na sua *função de operação de rota* posteriormente. -## Parametrizar a instância +## Parametrizar a instância { #parameterize-the-instance } E agora, nós podemos utilizar o `__init__` para declarar os parâmetros da instância que podemos utilizar para "parametrizar" a dependência: @@ -30,7 +30,7 @@ E agora, nós podemos utilizar o `__init__` para declarar os parâmetros da inst Neste caso, o **FastAPI** nunca tocará ou se importará com o `__init__`, nós vamos utilizar diretamente em nosso código. -## Crie uma instância +## Crie uma instância { #create-an-instance } Nós poderíamos criar uma instância desta classe com: @@ -38,7 +38,7 @@ Nós poderíamos criar uma instância desta classe com: E deste modo nós podemos "parametrizar" a nossa dependência, que agora possui `"bar"` dentro dele, como o atributo `checker.fixed_content`. -## Utilize a instância como dependência +## Utilize a instância como dependência { #use-the-instance-as-a-dependency } Então, nós podemos utilizar este `checker` em um `Depends(checker)`, no lugar de `Depends(FixedContentQueryChecker)`, porque a dependência é a instância, `checker`, e não a própria classe. @@ -63,3 +63,101 @@ Nos capítulos sobre segurança, existem funções utilitárias que são impleme Se você entendeu tudo isso, você já sabe como essas funções utilitárias para segurança funcionam por debaixo dos panos. /// + +## Dependências com `yield`, `HTTPException`, `except` e Tarefas em Segundo Plano { #dependencies-with-yield-httpexception-except-and-background-tasks } + +/// warning | Atenção + +Muito provavelmente você não precisa desses detalhes técnicos. + +Esses detalhes são úteis principalmente se você tinha uma aplicação FastAPI anterior à versão 0.121.0 e está enfrentando problemas com dependências com `yield`. + +/// + +Dependências com `yield` evoluíram ao longo do tempo para contemplar diferentes casos de uso e corrigir alguns problemas, aqui está um resumo do que mudou. + +### Dependências com `yield` e `scope` { #dependencies-with-yield-and-scope } + +Na versão 0.121.0, o FastAPI adicionou suporte a `Depends(scope="function")` para dependências com `yield`. + +Usando `Depends(scope="function")`, o código de saída após o `yield` é executado logo depois que a *função de operação de rota* termina, antes de a response ser enviada de volta ao cliente. + +E ao usar `Depends(scope="request")` (o padrão), o código de saída após o `yield` é executado depois que a response é enviada. + +Você pode ler mais na documentação em [Dependências com `yield` - Saída antecipada e `scope`](../tutorial/dependencies/dependencies-with-yield.md#early-exit-and-scope). + +### Dependências com `yield` e `StreamingResponse`, Detalhes Técnicos { #dependencies-with-yield-and-streamingresponse-technical-details } + +Antes do FastAPI 0.118.0, se você usasse uma dependência com `yield`, o código de saída (após o `yield`) rodaria depois que a *função de operação de rota* retornasse, mas logo antes de enviar a resposta. + +A intenção era evitar manter recursos por mais tempo que o necessário, esperando a resposta percorrer a rede. + +Essa mudança também significava que, se você retornasse um `StreamingResponse`, o código de saída da dependência com `yield` já teria sido executado. + +Por exemplo, se você tivesse uma sessão de banco de dados em uma dependência com `yield`, o `StreamingResponse` não conseguiria usar essa sessão enquanto transmite dados, porque a sessão já teria sido fechada no código de saída após o `yield`. + +Esse comportamento foi revertido na versão 0.118.0, para que o código de saída após o `yield` seja executado depois que a resposta for enviada. + +/// info | Informação + +Como você verá abaixo, isso é muito semelhante ao comportamento antes da versão 0.106.0, mas com várias melhorias e correções de bugs para casos extremos. + +/// + +#### Casos de uso com código de saída antecipado { #use-cases-with-early-exit-code } + +Há alguns casos de uso, com condições específicas, que poderiam se beneficiar do comportamento antigo de executar o código de saída das dependências com `yield` antes de enviar a resposta. + +Por exemplo, imagine que você tem código que usa uma sessão de banco de dados em uma dependência com `yield` apenas para verificar um usuário, mas a sessão de banco de dados nunca é usada novamente na *função de operação de rota*, somente na dependência, e a resposta demora a ser enviada, como um `StreamingResponse` que envia dados lentamente, mas por algum motivo não usa o banco de dados. + +Nesse caso, a sessão de banco de dados seria mantida até que a resposta termine de ser enviada, mas se você não a usa, então não seria necessário mantê-la. + +Veja como poderia ser: + +{* ../../docs_src/dependencies/tutorial013_an_py310.py *} + +O código de saída, o fechamento automático da `Session` em: + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[19:21] *} + +...seria executado depois que a resposta terminar de enviar os dados lentos: + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[30:38] hl[31:33] *} + +Mas como `generate_stream()` não usa a sessão do banco de dados, não é realmente necessário manter a sessão aberta enquanto envia a resposta. + +Se você tiver esse caso específico usando SQLModel (ou SQLAlchemy), você poderia fechar explicitamente a sessão depois que não precisar mais dela: + +{* ../../docs_src/dependencies/tutorial014_an_py310.py ln[24:28] hl[28] *} + +Dessa forma a sessão liberaria a conexão com o banco de dados, para que outras requisições pudessem usá-la. + +Se você tiver um caso diferente que precise sair antecipadamente de uma dependência com `yield`, por favor crie uma Pergunta no GitHub Discussions com o seu caso específico e por que você se beneficiaria de ter o fechamento antecipado para dependências com `yield`. + +Se houver casos de uso convincentes para fechamento antecipado em dependências com `yield`, considerarei adicionar uma nova forma de optar por esse fechamento antecipado. + +### Dependências com `yield` e `except`, Detalhes Técnicos { #dependencies-with-yield-and-except-technical-details } + +Antes do FastAPI 0.110.0, se você usasse uma dependência com `yield`, e então capturasse uma exceção com `except` nessa dependência, e você não relançasse a exceção, a exceção seria automaticamente levantada/encaminhada para quaisquer tratadores de exceção ou para o tratador de erro interno do servidor. + +Isso foi alterado na versão 0.110.0 para corrigir consumo de memória não tratado decorrente de exceções encaminhadas sem um tratador (erros internos do servidor), e para torná-lo consistente com o comportamento do código Python regular. + +### Tarefas em Segundo Plano e Dependências com `yield`, Detalhes Técnicos { #background-tasks-and-dependencies-with-yield-technical-details } + +Antes do FastAPI 0.106.0, lançar exceções após o `yield` não era possível, o código de saída em dependências com `yield` era executado depois que a resposta era enviada, então [Tratadores de Exceções](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} já teriam sido executados. + +Isso foi projetado assim principalmente para permitir o uso dos mesmos objetos "yielded" por dependências dentro de tarefas em segundo plano, porque o código de saída seria executado depois que as tarefas em segundo plano fossem concluídas. + +Isso foi alterado no FastAPI 0.106.0 com a intenção de não manter recursos enquanto se espera a resposta percorrer a rede. + +/// tip | Dica + +Além disso, uma tarefa em segundo plano normalmente é um conjunto de lógica independente que deve ser tratado separadamente, com seus próprios recursos (por exemplo, sua própria conexão de banco de dados). + +Assim, desta forma você provavelmente terá um código mais limpo. + +/// + +Se você costumava depender desse comportamento, agora você deve criar os recursos para tarefas em segundo plano dentro da própria tarefa em segundo plano, e usar internamente apenas dados que não dependam dos recursos de dependências com `yield`. + +Por exemplo, em vez de usar a mesma sessão de banco de dados, você criaria uma nova sessão de banco de dados dentro da tarefa em segundo plano, e obteria os objetos do banco de dados usando essa nova sessão. E então, em vez de passar o objeto do banco de dados como parâmetro para a função da tarefa em segundo plano, você passaria o ID desse objeto e então obteria o objeto novamente dentro da função da tarefa em segundo plano. diff --git a/docs/pt/docs/advanced/async-tests.md b/docs/pt/docs/advanced/async-tests.md index a2b79426c..c5b2c3fbb 100644 --- a/docs/pt/docs/advanced/async-tests.md +++ b/docs/pt/docs/advanced/async-tests.md @@ -1,4 +1,4 @@ -# Testes Assíncronos +# Testes Assíncronos { #async-tests } Você já viu como testar as suas aplicações **FastAPI** utilizando o `TestClient` que é fornecido. Até agora, você viu apenas como escrever testes síncronos, sem utilizar funções `async`. @@ -6,11 +6,11 @@ Ser capaz de utilizar funções assíncronas em seus testes pode ser útil, por Vamos ver como nós podemos fazer isso funcionar. -## pytest.mark.anyio +## pytest.mark.anyio { #pytest-mark-anyio } Se quisermos chamar funções assíncronas em nossos testes, as nossas funções de teste precisam ser assíncronas. O AnyIO oferece um plugin bem legal para isso, que nos permite especificar que algumas das nossas funções de teste precisam ser chamadas de forma assíncrona. -## HTTPX +## HTTPX { #httpx } Mesmo que a sua aplicação **FastAPI** utilize funções normais com `def` no lugar de `async def`, ela ainda é uma aplicação `async` por baixo dos panos. @@ -18,7 +18,7 @@ O `TestClient` faz algumas mágicas para invocar a aplicação FastAPI assíncro O `TestClient` é baseado no HTTPX, e felizmente nós podemos utilizá-lo diretamente para testar a API. -## Exemplo +## Exemplo { #example } Para um exemplos simples, vamos considerar uma estrutura de arquivos semelhante ao descrito em [Bigger Applications](../tutorial/bigger-applications.md){.internal-link target=_blank} e [Testing](../tutorial/testing.md){.internal-link target=_blank}: @@ -38,7 +38,7 @@ O arquivo `test_main.py` teria os testes para para o arquivo `main.py`, ele pode {* ../../docs_src/async_tests/test_main.py *} -## Executá-lo +## Executá-lo { #run-it } Você pode executar os seus testes normalmente via: @@ -52,7 +52,7 @@ $ pytest -## Em Detalhes +## Em Detalhes { #in-detail } O marcador `@pytest.mark.anyio` informa ao pytest que esta função de teste deve ser invocada de maneira assíncrona: @@ -82,18 +82,18 @@ Note que nós estamos utilizando async/await com o novo `AsyncClient` - a requis /// -/// warning | Aviso +/// warning | Atenção -Se a sua aplicação depende dos eventos de vida útil (*lifespan*), o `AsyncClient` não acionará estes eventos. Para garantir que eles são acionados, utilize o `LifespanManager` do florimondmanca/asgi-lifespan. +Se a sua aplicação depende de eventos de lifespan, o `AsyncClient` não acionará estes eventos. Para garantir que eles são acionados, utilize o `LifespanManager` do florimondmanca/asgi-lifespan. /// -## Outras Chamadas de Funções Assíncronas +## Outras Chamadas de Funções Assíncronas { #other-asynchronous-function-calls } -Como a função de teste agora é assíncrona, você pode chamar (e `esperar`) outras funções `async` além de enviar requisições para a sua aplicação FastAPI em seus testes, exatamente como você as chamaria em qualquer outro lugar do seu código. +Como a função de teste agora é assíncrona, você pode chamar (e `await`) outras funções `async` além de enviar requisições para a sua aplicação FastAPI em seus testes, exatamente como você as chamaria em qualquer outro lugar do seu código. /// tip | Dica -Se você se deparar com um `RuntimeError: Task attached to a different loop` ao integrar funções assíncronas em seus testes (e.g. ao utilizar o MotorClient do MongoDB) Lembre-se de instanciar objetos que precisam de um loop de eventos (*event loop*) apenas em funções assíncronas, e.g. um *"callback"* `'@app.on_event("startup")`. +Se você se deparar com um `RuntimeError: Task attached to a different loop` ao integrar funções assíncronas em seus testes (e.g. ao utilizar o MotorClient do MongoDB) Lembre-se de instanciar objetos que precisam de um loop de eventos (*event loop*) apenas em funções assíncronas, e.g. um callback `@app.on_event("startup")`. /// diff --git a/docs/pt/docs/advanced/behind-a-proxy.md b/docs/pt/docs/advanced/behind-a-proxy.md index 6837c9542..77c413aee 100644 --- a/docs/pt/docs/advanced/behind-a-proxy.md +++ b/docs/pt/docs/advanced/behind-a-proxy.md @@ -1,34 +1,131 @@ -# Atrás de um Proxy +# Atrás de um Proxy { #behind-a-proxy } -Em algumas situações, você pode precisar usar um servidor **proxy** como Traefik ou Nginx com uma configuração que adiciona um prefixo de caminho extra que não é visto pela sua aplicação. +Em muitas situações, você usaria um **proxy** como Traefik ou Nginx na frente da sua aplicação FastAPI. + +Esses proxies podem lidar com certificados HTTPS e outras coisas. + +## Headers Encaminhados pelo Proxy { #proxy-forwarded-headers } + +Um **proxy** na frente da sua aplicação normalmente definiria alguns headers dinamicamente antes de enviar as requisições para o seu **servidor**, para informar ao servidor que a requisição foi **encaminhada** pelo proxy, informando a URL original (pública), incluindo o domínio, que está usando HTTPS, etc. + +O programa do **servidor** (por exemplo, **Uvicorn** via **CLI do FastAPI**) é capaz de interpretar esses headers e então repassar essas informações para a sua aplicação. + +Mas, por segurança, como o servidor não sabe que está atrás de um proxy confiável, ele não interpretará esses headers. + +/// note | Detalhes Técnicos + +Os headers do proxy são: + +* X-Forwarded-For +* X-Forwarded-Proto +* X-Forwarded-Host + +/// + +### Ativar headers encaminhados pelo proxy { #enable-proxy-forwarded-headers } + +Você pode iniciar a CLI do FastAPI com a opção de linha de comando `--forwarded-allow-ips` e informar os endereços IP que devem ser confiáveis para ler esses headers encaminhados. + +Se você definir como `--forwarded-allow-ips="*"`, ele confiará em todos os IPs de entrada. + +Se o seu **servidor** estiver atrás de um **proxy** confiável e somente o proxy falar com ele, isso fará com que ele aceite seja qual for o IP desse **proxy**. + +
+ +```console +$ fastapi run --forwarded-allow-ips="*" + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +### Redirecionamentos com HTTPS { #redirects-with-https } + +Por exemplo, suponha que você defina uma *operação de rota* `/items/`: + +{* ../../docs_src/behind_a_proxy/tutorial001_01.py hl[6] *} + +Se o cliente tentar ir para `/items`, por padrão, ele seria redirecionado para `/items/`. + +Mas antes de definir a opção de linha de comando `--forwarded-allow-ips`, poderia redirecionar para `http://localhost:8000/items/`. + +Mas talvez sua aplicação esteja hospedada em `https://mysuperapp.com`, e o redirecionamento deveria ser para `https://mysuperapp.com/items/`. + +Ao definir `--proxy-headers`, agora o FastAPI conseguirá redirecionar para o local correto. 😎 + +``` +https://mysuperapp.com/items/ +``` + +/// tip | Dica + +Se você quiser saber mais sobre HTTPS, confira o tutorial [Sobre HTTPS](../deployment/https.md){.internal-link target=_blank}. + +/// + +### Como funcionam os headers encaminhados pelo proxy { #how-proxy-forwarded-headers-work } + +Aqui está uma representação visual de como o **proxy** adiciona headers encaminhados entre o cliente e o **servidor da aplicação**: + +```mermaid +sequenceDiagram + participant Client + participant Proxy as Proxy/Load Balancer + participant Server as FastAPI Server + + Client->>Proxy: HTTPS Request
Host: mysuperapp.com
Path: /items + + Note over Proxy: Proxy adds forwarded headers + + Proxy->>Server: HTTP Request
X-Forwarded-For: [client IP]
X-Forwarded-Proto: https
X-Forwarded-Host: mysuperapp.com
Path: /items + + Note over Server: Server interprets headers
(if --forwarded-allow-ips is set) + + Server->>Proxy: HTTP Response
with correct HTTPS URLs + + Proxy->>Client: HTTPS Response +``` + +O **proxy** intercepta a requisição original do cliente e adiciona os headers especiais de encaminhamento (`X-Forwarded-*`) antes de repassar a requisição para o **servidor da aplicação**. + +Esses headers preservam informações sobre a requisição original que, de outra forma, seriam perdidas: + +* X-Forwarded-For: o endereço IP original do cliente +* X-Forwarded-Proto: o protocolo original (`https`) +* X-Forwarded-Host: o host original (`mysuperapp.com`) + +Quando a **CLI do FastAPI** é configurada com `--forwarded-allow-ips`, ela confia nesses headers e os utiliza, por exemplo, para gerar as URLs corretas em redirecionamentos. + +## Proxy com um prefixo de path removido { #proxy-with-a-stripped-path-prefix } + +Você pode ter um proxy que adiciona um prefixo de path à sua aplicação. Nesses casos, você pode usar `root_path` para configurar sua aplicação. -O `root_path` é um mecanismo fornecido pela especificação ASGI (que o FastAPI utiliza, através do Starlette). +O `root_path` é um mecanismo fornecido pela especificação ASGI (na qual o FastAPI é construído, através do Starlette). O `root_path` é usado para lidar com esses casos específicos. E também é usado internamente ao montar sub-aplicações. -## Proxy com um prefixo de caminho removido +Ter um proxy com um prefixo de path removido, nesse caso, significa que você poderia declarar um path em `/app` no seu código, mas então você adiciona uma camada no topo (o proxy) que colocaria sua aplicação **FastAPI** sob um path como `/api/v1`. -Ter um proxy com um prefixo de caminho removido, nesse caso, significa que você poderia declarar um caminho em `/app` no seu código, mas então, você adiciona uma camada no topo (o proxy) que colocaria sua aplicação **FastAPI** sob um caminho como `/api/v1`. - -Nesse caso, o caminho original `/app` seria servido em `/api/v1/app`. +Nesse caso, o path original `/app` seria servido em `/api/v1/app`. Embora todo o seu código esteja escrito assumindo que existe apenas `/app`. {* ../../docs_src/behind_a_proxy/tutorial001.py hl[6] *} -E o proxy estaria **"removendo"** o **prefixo do caminho** dinamicamente antes de transmitir a solicitação para o servidor da aplicação (provavelmente Uvicorn via CLI do FastAPI), mantendo sua aplicação convencida de que está sendo servida em `/app`, para que você não precise atualizar todo o seu código para incluir o prefixo `/api/v1`. +E o proxy estaria **"removendo"** o **prefixo de path** dinamicamente antes de transmitir a solicitação para o servidor da aplicação (provavelmente Uvicorn via CLI do FastAPI), mantendo sua aplicação convencida de que está sendo servida em `/app`, para que você não precise atualizar todo o seu código para incluir o prefixo `/api/v1`. Até aqui, tudo funcionaria normalmente. -Mas então, quando você abre a interface de documentação integrada (o frontend), ele esperaria obter o OpenAPI schema em `/openapi.json`, em vez de `/api/v1/openapi.json`. +Mas então, quando você abre a interface de documentação integrada (o frontend), ela esperaria obter o OpenAPI schema em `/openapi.json`, em vez de `/api/v1/openapi.json`. Então, o frontend (que roda no navegador) tentaria acessar `/openapi.json` e não conseguiria obter o OpenAPI schema. -Como temos um proxy com um prefixo de caminho de `/api/v1` para nossa aplicação, o frontend precisa buscar o OpenAPI schema em `/api/v1/openapi.json`. +Como temos um proxy com um prefixo de path de `/api/v1` para nossa aplicação, o frontend precisa buscar o OpenAPI schema em `/api/v1/openapi.json`. ```mermaid graph LR @@ -47,7 +144,7 @@ O IP `0.0.0.0` é comumente usado para significar que o programa escuta em todos /// -A interface de documentação também precisaria do OpenAPI schema para declarar que API `server` está localizado em `/api/v1` (atrás do proxy). Por exemplo: +A interface de documentação também precisaria do OpenAPI schema para declarar que este `server` da API está localizado em `/api/v1` (atrás do proxy). Por exemplo: ```JSON hl_lines="4-8" { @@ -64,16 +161,16 @@ A interface de documentação também precisaria do OpenAPI schema para declarar } ``` -Neste exemplo, o "Proxy" poderia ser algo como **Traefik**. E o servidor seria algo como CLI do FastAPI com **Uvicorn**, executando sua aplicação FastAPI. +Neste exemplo, o "Proxy" poderia ser algo como **Traefik**. E o servidor seria algo como a CLI do FastAPI com **Uvicorn**, executando sua aplicação FastAPI. -### Fornecendo o `root_path` +### Fornecendo o `root_path` { #providing-the-root-path } Para conseguir isso, você pode usar a opção de linha de comando `--root-path` assim:
```console -$ fastapi run main.py --root-path /api/v1 +$ fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1 INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ``` @@ -90,11 +187,11 @@ E a opção de linha de comando `--root-path` fornece esse `root_path`. /// -### Verificando o `root_path` atual +### Verificando o `root_path` atual { #checking-the-current-root-path } Você pode obter o `root_path` atual usado pela sua aplicação para cada solicitação, ele faz parte do dicionário `scope` (que faz parte da especificação ASGI). -Aqui estamos incluindo ele na mensagem apenas para fins de demonstração. +Aqui estamos incluindo-o na mensagem apenas para fins de demonstração. {* ../../docs_src/behind_a_proxy/tutorial001.py hl[8] *} @@ -103,7 +200,7 @@ Então, se você iniciar o Uvicorn com:
```console -$ fastapi run main.py --root-path /api/v1 +$ fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1 INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ``` @@ -119,15 +216,15 @@ A resposta seria algo como: } ``` -### Configurando o `root_path` na aplicação FastAPI +### Configurando o `root_path` na aplicação FastAPI { #setting-the-root-path-in-the-fastapi-app } -Alternativamente, se você não tiver uma maneira de fornecer uma opção de linha de comando como `--root-path` ou equivalente, você pode definir o parâmetro `--root-path` ao criar sua aplicação FastAPI: +Alternativamente, se você não tiver uma maneira de fornecer uma opção de linha de comando como `--root-path` ou equivalente, você pode definir o parâmetro `root_path` ao criar sua aplicação FastAPI: {* ../../docs_src/behind_a_proxy/tutorial002.py hl[3] *} -Passar o `root_path`h para `FastAPI` seria o equivalente a passar a opção de linha de comando `--root-path` para Uvicorn ou Hypercorn. +Passar o `root_path` para `FastAPI` seria o equivalente a passar a opção de linha de comando `--root-path` para Uvicorn ou Hypercorn. -### Sobre `root_path` +### Sobre `root_path` { #about-root-path } Tenha em mente que o servidor (Uvicorn) não usará esse `root_path` para nada além de passá-lo para a aplicação. @@ -144,19 +241,19 @@ Portanto, ele não esperará ser acessado em `http://127.0.0.1:8000/api/v1/app`. O Uvicorn esperará que o proxy acesse o Uvicorn em `http://127.0.0.1:8000/app`, e então seria responsabilidade do proxy adicionar o prefixo extra `/api/v1` no topo. -## Sobre proxies com um prefixo de caminho removido +## Sobre proxies com um prefixo de path removido { #about-proxies-with-a-stripped-path-prefix } -Tenha em mente que um proxy com prefixo de caminho removido é apenas uma das maneiras de configurá-lo. +Tenha em mente que um proxy com prefixo de path removido é apenas uma das maneiras de configurá-lo. -Provavelmente, em muitos casos, o padrão será que o proxy não tenha um prefixo de caminho removido. +Provavelmente, em muitos casos, o padrão será que o proxy não tenha um prefixo de path removido. -Em um caso como esse (sem um prefixo de caminho removido), o proxy escutaria em algo como `https://myawesomeapp.com`, e então se o navegador acessar `https://myawesomeapp.com/api/v1/app` e seu servidor (por exemplo, Uvicorn) escutar em `http://127.0.0.1:8000` o proxy (sem um prefixo de caminho removido) acessaria o Uvicorn no mesmo caminho: `http://127.0.0.1:8000/api/v1/app`. +Em um caso como esse (sem um prefixo de path removido), o proxy escutaria em algo como `https://myawesomeapp.com`, e então, se o navegador acessar `https://myawesomeapp.com/api/v1/app` e seu servidor (por exemplo, Uvicorn) escutar em `http://127.0.0.1:8000`, o proxy (sem um prefixo de path removido) acessaria o Uvicorn no mesmo path: `http://127.0.0.1:8000/api/v1/app`. -## Testando localmente com Traefik +## Testando localmente com Traefik { #testing-locally-with-traefik } -Você pode facilmente executar o experimento localmente com um prefixo de caminho removido usando Traefik. +Você pode facilmente executar o experimento localmente com um prefixo de path removido usando Traefik. -Faça o download do Traefik., Ele é um único binário e você pode extrair o arquivo compactado e executá-lo diretamente do terminal. +Faça o download do Traefik, ele é um único binário, você pode extrair o arquivo compactado e executá-lo diretamente do terminal. Então, crie um arquivo `traefik.toml` com: @@ -203,9 +300,9 @@ Agora crie esse outro arquivo `routes.toml`: url = "http://127.0.0.1:8000" ``` -Esse arquivo configura o Traefik para usar o prefixo de caminho `/api/v1`. +Esse arquivo configura o Traefik para usar o prefixo de path `/api/v1`. -E então ele redirecionará suas solicitações para seu Uvicorn rodando em `http://127.0.0.1:8000`. +E então o Traefik redirecionará suas solicitações para seu Uvicorn rodando em `http://127.0.0.1:8000`. Agora inicie o Traefik: @@ -224,14 +321,14 @@ E agora inicie sua aplicação, usando a opção `--root-path`:
```console -$ fastapi run main.py --root-path /api/v1 +$ fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1 INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ```
-### Verifique as respostas +### Verifique as respostas { #check-the-responses } Agora, se você for ao URL com a porta para o Uvicorn: http://127.0.0.1:8000/app, você verá a resposta normal: @@ -248,7 +345,7 @@ Perceba que, mesmo acessando em `http://127.0.0.1:8000/app`, ele mostra o `root_ /// -E agora abra o URL com a porta para o Traefik, incluindo o prefixo de caminho: http://127.0.0.1:9999/api/v1/app. +E agora abra o URL com a porta para o Traefik, incluindo o prefixo de path: http://127.0.0.1:9999/api/v1/app. Obtemos a mesma resposta: @@ -259,19 +356,19 @@ Obtemos a mesma resposta: } ``` -mas desta vez no URL com o prefixo de caminho fornecido pelo proxy: `/api/v1`. +mas desta vez no URL com o prefixo de path fornecido pelo proxy: `/api/v1`. -Claro, a ideia aqui é que todos acessariam a aplicação através do proxy, então a versão com o prefixo de caminho `/api/v1` é a "correta". +Claro, a ideia aqui é que todos acessariam a aplicação através do proxy, então a versão com o prefixo de path `/api/v1` é a "correta". -E a versão sem o prefixo de caminho (`http://127.0.0.1:8000/app`), fornecida diretamente pelo Uvicorn, seria exclusivamente para o _proxy_ (Traefik) acessá-la. +E a versão sem o prefixo de path (`http://127.0.0.1:8000/app`), fornecida diretamente pelo Uvicorn, seria exclusivamente para o _proxy_ (Traefik) acessá-la. -Isso demonstra como o Proxy (Traefik) usa o prefixo de caminho e como o servidor (Uvicorn) usa o `root_path` da opção `--root-path`. +Isso demonstra como o Proxy (Traefik) usa o prefixo de path e como o servidor (Uvicorn) usa o `root_path` da opção `--root-path`. -### Verifique a interface de documentação +### Verifique a interface de documentação { #check-the-docs-ui } Mas aqui está a parte divertida. ✨ -A maneira "oficial" de acessar a aplicação seria através do proxy com o prefixo de caminho que definimos. Então, como esperaríamos, se você tentar a interface de documentação servida diretamente pelo Uvicorn, sem o prefixo de caminho no URL, ela não funcionará, porque espera ser acessada através do proxy. +A maneira "oficial" de acessar a aplicação seria através do proxy com o prefixo de path que definimos. Então, como esperaríamos, se você tentar a interface de documentação servida diretamente pelo Uvicorn, sem o prefixo de path no URL, ela não funcionará, porque espera ser acessada através do proxy. Você pode verificar em http://127.0.0.1:8000/docs: @@ -287,9 +384,9 @@ Exatamente como queríamos. ✔️ Isso porque o FastAPI usa esse `root_path` para criar o `server` padrão no OpenAPI com o URL fornecido pelo `root_path`. -## Servidores adicionais +## Servidores adicionais { #additional-servers } -/// warning | Aviso +/// warning | Atenção Este é um caso de uso mais avançado. Sinta-se à vontade para pular. @@ -297,7 +394,7 @@ Este é um caso de uso mais avançado. Sinta-se à vontade para pular. Por padrão, o **FastAPI** criará um `server` no OpenAPI schema com o URL para o `root_path`. -Mas você também pode fornecer outros `servers` alternativos, por exemplo, se quiser que a *mesma* interface de documentação interaja com ambientes de staging e produção. +Mas você também pode fornecer outros `servers` alternativos, por exemplo, se quiser que a mesma interface de documentação interaja com ambientes de staging e produção. Se você passar uma lista personalizada de `servers` e houver um `root_path` (porque sua API está atrás de um proxy), o **FastAPI** inserirá um "server" com esse `root_path` no início da lista. @@ -346,7 +443,7 @@ A interface de documentação interagirá com o servidor que você selecionar. /// -### Desabilitar servidor automático de `root_path` +### Desabilitar servidor automático de `root_path` { #disable-automatic-server-from-root-path } Se você não quiser que o **FastAPI** inclua um servidor automático usando o `root_path`, você pode usar o parâmetro `root_path_in_servers=False`: @@ -354,8 +451,8 @@ Se você não quiser que o **FastAPI** inclua um servidor automático usando o ` e então ele não será incluído no OpenAPI schema. -## Montando uma sub-aplicação +## Montando uma sub-aplicação { #mounting-a-sub-application } -Se você precisar montar uma sub-aplicação (como descrito em [Sub Aplicações - Montagens](sub-applications.md){.internal-link target=_blank}) enquanto também usa um proxy com `root_path`, você pode fazer isso normalmente, como esperaria. +Se você precisar montar uma sub-aplicação (como descrito em [Sub-aplicações - Montagens](sub-applications.md){.internal-link target=_blank}) enquanto também usa um proxy com `root_path`, você pode fazer isso normalmente, como esperaria. O FastAPI usará internamente o `root_path` de forma inteligente, então tudo funcionará. ✨ diff --git a/docs/pt/docs/advanced/custom-response.md b/docs/pt/docs/advanced/custom-response.md index a0bcc2b97..036cbb635 100644 --- a/docs/pt/docs/advanced/custom-response.md +++ b/docs/pt/docs/advanced/custom-response.md @@ -1,4 +1,4 @@ -# Resposta Personalizada - HTML, Stream, File e outras +# Resposta Personalizada - HTML, Stream, File e outras { #custom-response-html-stream-file-others } Por padrão, o **FastAPI** irá retornar respostas utilizando `JSONResponse`. @@ -8,9 +8,9 @@ Mas se você retornar uma `Response` diretamente (ou qualquer subclasse, como `J Mas você também pode declarar a `Response` que você deseja utilizar (e.g. qualquer subclasse de `Response`), em um *decorador de operação de rota* utilizando o parâmetro `response_class`. -Os conteúdos que você retorna em sua *função de operador de rota* serão colocados dentro dessa `Response`. +Os conteúdos que você retorna em sua *função de operação de rota* serão colocados dentro dessa `Response`. -E se a `Response` tiver um media type JSON (`application/json`), como é o caso com `JSONResponse` e `UJSONResponse`, os dados que você retornar serão automaticamente convertidos (e filtrados) com qualquer `response_model` do Pydantic que for declarado em sua *função de operador de rota*. +E se a `Response` tiver um media type JSON (`application/json`), como é o caso com `JSONResponse` e `UJSONResponse`, os dados que você retornar serão automaticamente convertidos (e filtrados) com qualquer `response_model` do Pydantic que for declarado no decorador de operação de rota. /// note | Nota @@ -18,7 +18,7 @@ Se você utilizar uma classe de Resposta sem media type, o FastAPI esperará que /// -## Utilizando `ORJSONResponse` +## Utilizando `ORJSONResponse` { #use-orjsonresponse } Por exemplo, se você precisa bastante de performance, você pode instalar e utilizar o `orjson` e definir a resposta para ser uma `ORJSONResponse`. @@ -48,7 +48,7 @@ A `ORJSONResponse` está disponível apenas no FastAPI, e não no Starlette. /// -## Resposta HTML +## Resposta HTML { #html-response } Para retornar uma resposta com HTML diretamente do **FastAPI**, utilize `HTMLResponse`. @@ -67,7 +67,7 @@ E será documentado como tal no OpenAPI. /// -### Retornando uma `Response` +### Retornando uma `Response` { #return-a-response } Como visto em [Retornando uma Resposta Diretamente](response-directly.md){.internal-link target=_blank}, você também pode sobrescrever a resposta diretamente na sua *operação de rota*, ao retornar ela. @@ -75,7 +75,7 @@ O mesmo exemplo de antes, retornando uma `HTMLResponse`, poderia parecer com: {* ../../docs_src/custom_response/tutorial003.py hl[2,7,19] *} -/// warning | Aviso +/// warning | Atenção Uma `Response` retornada diretamente em sua *função de operação de rota* não será documentada no OpenAPI (por exemplo, o `Content-Type` não será documentado) e não será visível na documentação interativa automática. @@ -87,13 +87,13 @@ Obviamente, o cabeçalho `Content-Type`, o código de status, etc, virão do obj /// -### Documentar no OpenAPI e sobrescrever `Response` +### Documentar no OpenAPI e sobrescrever `Response` { #document-in-openapi-and-override-response } Se você deseja sobrescrever a resposta dentro de uma função, mas ao mesmo tempo documentar o "media type" no OpenAPI, você pode utilizar o parâmetro `response_class` E retornar um objeto `Response`. A `response_class` será usada apenas para documentar o OpenAPI da *operação de rota*, mas sua `Response` será usada como foi definida. -##### Retornando uma `HTMLResponse` diretamente +#### Retornando uma `HTMLResponse` diretamente { #return-an-htmlresponse-directly } Por exemplo, poderia ser algo como: @@ -107,7 +107,7 @@ Mas se você passasse uma `HTMLResponse` em `response_class` também, o **FastAP -## Respostas disponíveis +## Respostas disponíveis { #available-responses } Aqui estão algumas dos tipos de resposta disponíveis. @@ -121,7 +121,7 @@ O **FastAPI** provê a mesma `starlette.responses` como `fastapi.responses` apen /// -### `Response` +### `Response` { #response } A classe principal de respostas, todas as outras respostas herdam dela. @@ -138,23 +138,23 @@ O FastAPI (Starlette, na verdade) irá incluir o cabeçalho Content-Length autom {* ../../docs_src/response_directly/tutorial002.py hl[1,18] *} -### `HTMLResponse` +### `HTMLResponse` { #htmlresponse } Usa algum texto ou sequência de bytes e retorna uma resposta HTML. Como você leu acima. -### `PlainTextResponse` +### `PlainTextResponse` { #plaintextresponse } Usa algum texto ou sequência de bytes para retornar uma resposta de texto não formatado. {* ../../docs_src/custom_response/tutorial005.py hl[2,7,9] *} -### `JSONResponse` +### `JSONResponse` { #jsonresponse } Pega alguns dados e retorna uma resposta com codificação `application/json`. É a resposta padrão utilizada no **FastAPI**, como você leu acima. -### `ORJSONResponse` +### `ORJSONResponse` { #orjsonresponse } Uma alternativa mais rápida de resposta JSON utilizando o `orjson`, como você leu acima. @@ -164,7 +164,7 @@ Essa resposta requer a instalação do pacote `orjson`, com o comando `pip insta /// -### `UJSONResponse` +### `UJSONResponse` { #ujsonresponse } Uma alternativa de resposta JSON utilizando a biblioteca `ujson`. @@ -174,7 +174,7 @@ Essa resposta requer a instalação do pacote `ujson`, com o comando `pip instal /// -/// warning | Aviso +/// warning | Atenção `ujson` é menos cauteloso que a implementação nativa do Python na forma que os casos especiais são tratados @@ -188,7 +188,7 @@ Essa resposta requer a instalação do pacote `ujson`, com o comando `pip instal /// -### `RedirectResponse` +### `RedirectResponse` { #redirectresponse } Retorna um redirecionamento HTTP. Utiliza o código de status 307 (Redirecionamento Temporário) por padrão. @@ -212,26 +212,24 @@ Você também pode utilizar o parâmetro `status_code` combinado com o parâmetr {* ../../docs_src/custom_response/tutorial006c.py hl[2,7,9] *} -### `StreamingResponse` +### `StreamingResponse` { #streamingresponse } -Recebe uma gerador assíncrono ou um gerador/iterador comum e retorna o corpo da requisição continuamente (stream). +Recebe um gerador assíncrono ou um gerador/iterador comum e retorna o corpo da resposta de forma contínua (stream). {* ../../docs_src/custom_response/tutorial007.py hl[2,14] *} -#### Utilizando `StreamingResponse` com objetos semelhantes a arquivos +#### Utilizando `StreamingResponse` com objetos semelhantes a arquivos { #using-streamingresponse-with-file-like-objects } -Se você tiver um objeto semelhante a um arquivo (e.g. o objeto retornado por `open()`), você pode criar uma função geradora para iterar sobre esse objeto. +Se você tiver um objeto semelhante a um arquivo (e.g. o objeto retornado por `open()`), você pode criar uma função geradora para iterar sobre esse objeto. Dessa forma, você não precisa ler todo o arquivo na memória primeiro, e você pode passar essa função geradora para `StreamingResponse` e retorná-la. Isso inclui muitas bibliotecas que interagem com armazenamento em nuvem, processamento de vídeos, entre outras. -```{ .python .annotate hl_lines="2 10-12 14" } -{!../../docs_src/custom_response/tutorial008.py!} -``` +{* ../../docs_src/custom_response/tutorial008.py hl[2,10:12,14] *} 1. Essa é a função geradora. É definida como "função geradora" porque contém declarações `yield` nela. -2. Ao utilizar o bloco `with`, nós garantimos que o objeto semelhante a um arquivo é fechado após a função geradora ser finalizada. Isto é, após a resposta terminar de ser enivada. +2. Ao utilizar o bloco `with`, nós garantimos que o objeto semelhante a um arquivo é fechado após a função geradora ser finalizada. Isto é, após a resposta terminar de ser enviada. 3. Essa declaração `yield from` informa a função para iterar sobre essa coisa nomeada de `file_like`. E então, para cada parte iterada, fornece essa parte como se viesse dessa função geradora (`iterfile`). Então, é uma função geradora que transfere o trabalho de "geração" para alguma outra coisa interna. @@ -244,10 +242,10 @@ Perceba que aqui estamos utilizando o `open()` da biblioteca padrão que não su /// -### `FileResponse` +### `FileResponse` { #fileresponse } Envia um arquivo de forma assíncrona e contínua (stream). -* + Recebe um conjunto de argumentos do construtor diferente dos outros tipos de resposta: * `path` - O caminho do arquivo que será transmitido @@ -265,7 +263,7 @@ Você também pode usar o parâmetro `response_class`: Nesse caso, você pode retornar o caminho do arquivo diretamente da sua *função de operação de rota*. -## Classe de resposta personalizada +## Classe de resposta personalizada { #custom-response-class } Você pode criar sua própria classe de resposta, herdando de `Response` e usando essa nova classe. @@ -273,7 +271,7 @@ Por exemplo, vamos supor que você queira utilizar o -## Dataclasses em Estruturas de Dados Aninhadas +## Dataclasses em Estruturas de Dados Aninhadas { #dataclasses-in-nested-data-structures } Você também pode combinar `dataclasses` com outras anotações de tipo para criar estruturas de dados aninhadas. @@ -48,9 +48,7 @@ Em alguns casos, você ainda pode ter que usar a versão do Pydantic das `datacl Nesse caso, você pode simplesmente trocar as `dataclasses` padrão por `pydantic.dataclasses`, que é um substituto direto: -```{ .python .annotate hl_lines="1 5 8-11 14-17 23-25 28" } -{!../../docs_src/dataclasses/tutorial003.py!} -``` +{* ../../docs_src/dataclasses/tutorial003.py hl[1,5,8:11,14:17,23:25,28] *} 1. Ainda importamos `field` das `dataclasses` padrão. @@ -86,12 +84,12 @@ Você pode combinar `dataclasses` com outras anotações de tipo em muitas combi Confira as dicas de anotação no código acima para ver mais detalhes específicos. -## Saiba Mais +## Saiba Mais { #learn-more } Você também pode combinar `dataclasses` com outros modelos Pydantic, herdar deles, incluí-los em seus próprios modelos, etc. Para saber mais, confira a documentação do Pydantic sobre dataclasses. -## Versão +## Versão { #version } Isso está disponível desde a versão `0.67.0` do FastAPI. 🔖 diff --git a/docs/pt/docs/advanced/events.md b/docs/pt/docs/advanced/events.md index 2d38e0899..e29ece8e3 100644 --- a/docs/pt/docs/advanced/events.md +++ b/docs/pt/docs/advanced/events.md @@ -1,65 +1,64 @@ -# Eventos de vida útil +# Eventos de lifespan { #lifespan-events } -Você pode definir a lógica (código) que poderia ser executada antes da aplicação **inicializar**. Isso significa que esse código será executado **uma vez**, **antes** da aplicação **começar a receber requisições**. +Você pode definir a lógica (código) que deve ser executada antes da aplicação **inicializar**. Isso significa que esse código será executado **uma vez**, **antes** de a aplicação **começar a receber requisições**. -Do mesmo modo, você pode definir a lógica (código) que será executada quando a aplicação estiver sendo **encerrada**. Nesse caso, este código será executado **uma vez**, **depois** de ter possivelmente tratado **várias requisições**. +Da mesma forma, você pode definir a lógica (código) que deve ser executada quando a aplicação estiver **encerrando**. Nesse caso, esse código será executado **uma vez**, **depois** de possivelmente ter tratado **várias requisições**. -Por conta desse código ser executado antes da aplicação **começar** a receber requisições, e logo após **terminar** de lidar com as requisições, ele cobre toda a **vida útil** (_lifespan_) da aplicação (o termo "vida útil" será importante em um segundo 😉). +Como esse código é executado antes de a aplicação **começar** a receber requisições e logo depois que ela **termina** de lidar com as requisições, ele cobre todo o **lifespan** da aplicação (a palavra "lifespan" será importante em um segundo 😉). -Pode ser muito útil para configurar **recursos** que você precisa usar por toda aplicação, e que são **compartilhados** entre as requisições, e/ou que você precisa **limpar** depois. Por exemplo, o pool de uma conexão com o banco de dados ou carregamento de um modelo compartilhado de aprendizado de máquina (_machine learning_). +Isso pode ser muito útil para configurar **recursos** que você precisa usar por toda a aplicação, e que são **compartilhados** entre as requisições e/ou que você precisa **limpar** depois. Por exemplo, um pool de conexões com o banco de dados ou o carregamento de um modelo de machine learning compartilhado. -## Caso de uso +## Caso de uso { #use-case } -Vamos iniciar com um exemplo de **caso de uso** e então ver como resolvê-lo com isso. +Vamos começar com um exemplo de **caso de uso** e então ver como resolvê-lo com isso. -Vamos imaginar que você tem alguns **modelos de _machine learning_** que deseja usar para lidar com as requisições. 🤖 +Vamos imaginar que você tem alguns **modelos de machine learning** que deseja usar para lidar com as requisições. 🤖 -Os mesmos modelos são compartilhados entre as requisições, então não é um modelo por requisição, ou um por usuário ou algo parecido. +Os mesmos modelos são compartilhados entre as requisições, então não é um modelo por requisição, ou um por usuário, ou algo parecido. -Vamos imaginar que o carregamento do modelo pode **demorar bastante tempo**, porque ele tem que ler muitos **dados do disco**. Então você não quer fazer isso a cada requisição. +Vamos imaginar que o carregamento do modelo pode **demorar bastante tempo**, porque ele precisa ler muitos **dados do disco**. Então você não quer fazer isso a cada requisição. -Você poderia carregá-lo no nível mais alto do módulo/arquivo, mas isso também poderia significaria **carregar o modelo** mesmo se você estiver executando um simples teste automatizado, então esse teste poderia ser **lento** porque teria que esperar o carregamento do modelo antes de ser capaz de executar uma parte independente do código. +Você poderia carregá-lo no nível mais alto do módulo/arquivo, mas isso também significaria **carregar o modelo** mesmo se você estivesse executando um teste automatizado simples; então esse teste poderia ser **lento** porque teria que esperar o carregamento do modelo antes de conseguir executar uma parte independente do código. +É isso que vamos resolver: vamos carregar o modelo antes de as requisições serem tratadas, mas apenas um pouco antes de a aplicação começar a receber requisições, não enquanto o código estiver sendo carregado. -Isso é que nós iremos resolver, vamos carregar o modelo antes das requisições serem manuseadas, mas apenas um pouco antes da aplicação começar a receber requisições, não enquanto o código estiver sendo carregado. +## Lifespan { #lifespan } -## Vida útil (_Lifespan_) +Você pode definir essa lógica de *inicialização* e *encerramento* usando o parâmetro `lifespan` da aplicação `FastAPI`, e um "gerenciador de contexto" (vou mostrar o que é isso em um segundo). -Você pode definir essa lógica de *inicialização* e *encerramento* usando os parâmetros de `lifespan` da aplicação `FastAPI`, e um "gerenciador de contexto" (te mostrarei o que é isso a seguir). +Vamos começar com um exemplo e depois ver em detalhes. -Vamos iniciar com um exemplo e ver isso detalhadamente. - -Nós criamos uma função assíncrona chamada `lifespan()` com `yield` como este: +Nós criamos uma função assíncrona `lifespan()` com `yield` assim: {* ../../docs_src/events/tutorial003.py hl[16,19] *} -Aqui nós estamos simulando a *inicialização* custosa do carregamento do modelo colocando a (falsa) função de modelo no dicionário com modelos de _machine learning_ antes do `yield`. Este código será executado **antes** da aplicação **começar a receber requisições**, durante a *inicialização*. +Aqui estamos simulando a operação de *inicialização* custosa de carregar o modelo, colocando a (falsa) função do modelo no dicionário com modelos de machine learning antes do `yield`. Esse código será executado **antes** de a aplicação **começar a receber requisições**, durante a *inicialização*. -E então, logo após o `yield`, descarregaremos o modelo. Esse código será executado **após** a aplicação **terminar de lidar com as requisições**, pouco antes do *encerramento*. Isso poderia, por exemplo, liberar recursos como memória ou GPU. +E então, logo após o `yield`, descarregamos o modelo. Esse código será executado **depois** de a aplicação **terminar de lidar com as requisições**, pouco antes do *encerramento*. Isso poderia, por exemplo, liberar recursos como memória ou uma GPU. /// tip | Dica O `shutdown` aconteceria quando você estivesse **encerrando** a aplicação. -Talvez você precise inicializar uma nova versão, ou apenas cansou de executá-la. 🤷 +Talvez você precise iniciar uma nova versão, ou apenas cansou de executá-la. 🤷 /// -### Função _lifespan_ +### Função lifespan { #lifespan-function } -A primeira coisa a notar, é que estamos definindo uma função assíncrona com `yield`. Isso é muito semelhante à Dependências com `yield`. +A primeira coisa a notar é que estamos definindo uma função assíncrona com `yield`. Isso é muito semelhante a Dependências com `yield`. {* ../../docs_src/events/tutorial003.py hl[14:19] *} -A primeira parte da função, antes do `yield`, será executada **antes** da aplicação inicializar. +A primeira parte da função, antes do `yield`, será executada **antes** de a aplicação iniciar. -E a parte posterior do `yield` irá executar **após** a aplicação ser encerrada. +E a parte posterior ao `yield` será executada **depois** de a aplicação ter terminado. -### Gerenciador de Contexto Assíncrono +### Gerenciador de contexto assíncrono { #async-context-manager } Se você verificar, a função está decorada com um `@asynccontextmanager`. -Que converte a função em algo chamado de "**Gerenciador de Contexto Assíncrono**". +Isso converte a função em algo chamado "**gerenciador de contexto assíncrono**". {* ../../docs_src/events/tutorial003.py hl[1,13] *} @@ -70,97 +69,97 @@ with open("file.txt") as file: file.read() ``` -Nas versões mais recentes de Python, há também um **gerenciador de contexto assíncrono**. Você o usaria com `async with`: +Em versões mais recentes do Python, há também um **gerenciador de contexto assíncrono**. Você o usaria com `async with`: ```Python async with lifespan(app): await do_stuff() ``` -Quando você cria um gerenciador de contexto ou um gerenciador de contexto assíncrono como mencionado acima, o que ele faz é que, antes de entrar no bloco `with`, ele irá executar o código anterior ao `yield`, e depois de sair do bloco `with`, ele irá executar o código depois do `yield`. +Quando você cria um gerenciador de contexto ou um gerenciador de contexto assíncrono como acima, o que ele faz é: antes de entrar no bloco `with`, ele executa o código antes do `yield`, e após sair do bloco `with`, ele executa o código depois do `yield`. -No nosso exemplo de código acima, nós não usamos ele diretamente, mas nós passamos para o FastAPI para ele usá-lo. +No nosso exemplo de código acima, não o usamos diretamente, mas passamos para o FastAPI para que ele o use. -O parâmetro `lifespan` da aplicação `FastAPI` usa um **Gerenciador de Contexto Assíncrono**, então nós podemos passar nosso novo gerenciador de contexto assíncrono do `lifespan` para ele. +O parâmetro `lifespan` da aplicação `FastAPI` aceita um **gerenciador de contexto assíncrono**, então podemos passar para ele nosso novo gerenciador de contexto assíncrono `lifespan`. {* ../../docs_src/events/tutorial003.py hl[22] *} -## Eventos alternativos (deprecados) +## Eventos alternativos (descontinuados) { #alternative-events-deprecated } -/// warning | Aviso +/// warning | Atenção -A maneira recomendada para lidar com a *inicialização* e o *encerramento* é usando o parâmetro `lifespan` da aplicação `FastAPI` como descrito acima. +A forma recomendada de lidar com a *inicialização* e o *encerramento* é usando o parâmetro `lifespan` da aplicação `FastAPI`, como descrito acima. Se você fornecer um parâmetro `lifespan`, os manipuladores de eventos `startup` e `shutdown` não serão mais chamados. É tudo `lifespan` ou tudo por eventos, não ambos. -Você provavelmente pode pular essa parte. +Você provavelmente pode pular esta parte. /// -Existe uma forma alternativa para definir a execução dessa lógica durante *inicialização* e durante *encerramento*. +Existe uma forma alternativa de definir essa lógica para ser executada durante a *inicialização* e durante o *encerramento*. -Você pode definir manipuladores de eventos (funções) que precisam ser executadas antes da aplicação inicializar, ou quando a aplicação estiver encerrando. +Você pode definir manipuladores de eventos (funções) que precisam ser executados antes de a aplicação iniciar ou quando a aplicação estiver encerrando. Essas funções podem ser declaradas com `async def` ou `def` normal. -### Evento `startup` +### Evento `startup` { #startup-event } -Para adicionar uma função que deve rodar antes da aplicação iniciar, declare-a com o evento `"startup"`: +Para adicionar uma função que deve rodar antes de a aplicação iniciar, declare-a com o evento `"startup"`: {* ../../docs_src/events/tutorial001.py hl[8] *} -Nesse caso, a função de manipulação de evento `startup` irá inicializar os itens do "banco de dados" (só um `dict`) com alguns valores. +Nesse caso, a função de manipulador do evento `startup` inicializará os itens do "banco de dados" (apenas um `dict`) com alguns valores. -Você pode adicionar mais que uma função de manipulação de evento. +Você pode adicionar mais de uma função de manipulador de eventos. -E sua aplicação não irá começar a receber requisições até que todos os manipuladores de eventos de `startup` sejam concluídos. +E sua aplicação não começará a receber requisições até que todos os manipuladores de eventos `startup` sejam concluídos. -### Evento `shutdown` +### Evento `shutdown` { #shutdown-event } -Para adicionar uma função que deve ser executada quando a aplicação estiver encerrando, declare ela com o evento `"shutdown"`: +Para adicionar uma função que deve ser executada quando a aplicação estiver encerrando, declare-a com o evento `"shutdown"`: {* ../../docs_src/events/tutorial002.py hl[6] *} -Aqui, a função de manipulação de evento `shutdown` irá escrever uma linha de texto `"Application shutdown"` no arquivo `log.txt`. +Aqui, a função de manipulador do evento `shutdown` escreverá uma linha de texto `"Application shutdown"` no arquivo `log.txt`. /// info | Informação -Na função `open()`, o `mode="a"` significa "acrescentar", então, a linha irá ser adicionada depois de qualquer coisa que esteja naquele arquivo, sem sobrescrever o conteúdo anterior. +Na função `open()`, o `mode="a"` significa "acrescentar", então a linha será adicionada depois do que já estiver naquele arquivo, sem sobrescrever o conteúdo anterior. /// /// tip | Dica -Perceba que nesse caso nós estamos usando a função padrão do Python `open()` que interage com um arquivo. +Perceba que, nesse caso, estamos usando a função padrão do Python `open()` que interage com um arquivo. -Então, isso envolve I/O (input/output), que exige "esperar" que coisas sejam escritas em disco. +Então, isso envolve I/O (input/output), que requer "esperar" que as coisas sejam escritas em disco. Mas `open()` não usa `async` e `await`. -Então, nós declaramos uma função de manipulação de evento com o padrão `def` ao invés de `async def`. +Assim, declaramos a função de manipulador de evento com `def` padrão em vez de `async def`. /// -### `startup` e `shutdown` juntos +### `startup` e `shutdown` juntos { #startup-and-shutdown-together } -Há uma grande chance que a lógica para sua *inicialização* e *encerramento* esteja conectada, você pode querer iniciar alguma coisa e então finalizá-la, adquirir um recurso e então liberá-lo, etc. +Há uma grande chance de que a lógica para sua *inicialização* e *encerramento* esteja conectada, você pode querer iniciar alguma coisa e então finalizá-la, adquirir um recurso e então liberá-lo, etc. -Fazendo isso em funções separadas que não compartilham lógica ou variáveis entre elas é mais difícil já que você precisa armazenar os valores em variáveis globais ou truques parecidos. +Fazer isso em funções separadas que não compartilham lógica ou variáveis entre si é mais difícil, pois você precisaria armazenar valores em variáveis globais ou truques semelhantes. -Por causa disso, agora é recomendado em vez disso usar o `lifespan` como explicado acima. +Por causa disso, agora é recomendado usar o `lifespan`, como explicado acima. -## Detalhes técnicos +## Detalhes técnicos { #technical-details } -Só um detalhe técnico para nerds curiosos. 🤓 +Apenas um detalhe técnico para nerds curiosos. 🤓 -Por baixo, na especificação técnica ASGI, essa é a parte do Protocolo Lifespan, e define eventos chamados `startup` e `shutdown`. +Por baixo, na especificação técnica do ASGI, isso é parte do Protocolo Lifespan, e define eventos chamados `startup` e `shutdown`. /// info | Informação -Você pode ler mais sobre o manipulador `lifespan` do Starlette na Documentação do Lifespan Starlette. +Você pode ler mais sobre os manipuladores de `lifespan` do Starlette na Documentação do Lifespan do Starlette. -Incluindo como manipular estado do lifespan que pode ser usado em outras áreas do seu código. +Incluindo como lidar com estado do lifespan que pode ser usado em outras áreas do seu código. /// -## Sub Aplicações +## Sub Aplicações { #sub-applications } -🚨 Tenha em mente que esses eventos de lifespan (de inicialização e desligamento) irão somente ser executados para a aplicação principal, não para [Sub Aplicações - Montagem](sub-applications.md){.internal-link target=_blank}. +🚨 Tenha em mente que esses eventos de lifespan (inicialização e encerramento) serão executados apenas para a aplicação principal, não para [Sub Aplicações - Montagem](sub-applications.md){.internal-link target=_blank}. diff --git a/docs/pt/docs/advanced/generate-clients.md b/docs/pt/docs/advanced/generate-clients.md index dc6b29511..253a7e6cd 100644 --- a/docs/pt/docs/advanced/generate-clients.md +++ b/docs/pt/docs/advanced/generate-clients.md @@ -1,115 +1,76 @@ -# Generate Clients +# Gerando SDKs { #generating-sdks } -Como o **FastAPI** é baseado na especificação **OpenAPI**, você obtém compatibilidade automática com muitas ferramentas, incluindo a documentação automática da API (fornecida pelo Swagger UI). +Como o **FastAPI** é baseado na especificação **OpenAPI**, suas APIs podem ser descritas em um formato padrão que muitas ferramentas entendem. -Uma vantagem particular que nem sempre é óbvia é que você pode **gerar clientes** (às vezes chamados de **SDKs**) para a sua API, para muitas **linguagens de programação** diferentes. +Isso facilita gerar **documentação** atualizada, bibliotecas clientes (**SDKs**) em várias linguagens e **testes** ou **fluxos de trabalho de automação** que permanecem em sincronia com o seu código. -## Geradores de Clientes OpenAPI +Neste guia, você aprenderá como gerar um **SDK em TypeScript** para o seu backend FastAPI. -Existem muitas ferramentas para gerar clientes a partir do **OpenAPI**. +## Geradores de SDK de código aberto { #open-source-sdk-generators } -Uma ferramenta comum é o OpenAPI Generator. +Uma opção versátil é o OpenAPI Generator, que suporta **muitas linguagens de programação** e pode gerar SDKs a partir da sua especificação OpenAPI. -Se voce está construindo um **frontend**, uma alternativa muito interessante é o openapi-ts. +Para **clientes TypeScript**, o Hey API é uma solução feita sob medida, oferecendo uma experiência otimizada para o ecossistema TypeScript. -## Geradores de Clientes e SDKs - Patrocinadores +Você pode descobrir mais geradores de SDK em OpenAPI.Tools. -Existem também alguns geradores de clientes e SDKs baseados na OpenAPI (FastAPI) **patrocinados por empresas**, em alguns casos eles podem oferecer **recursos adicionais** além de SDKs/clientes gerados de alta qualidade. +/// tip | Dica -Alguns deles também ✨ [**patrocinam o FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨, isso garante o **desenvolvimento** contínuo e saudável do FastAPI e seu **ecossistema**. +O FastAPI gera automaticamente especificações **OpenAPI 3.1**, então qualquer ferramenta que você usar deve suportar essa versão. -E isso mostra o verdadeiro compromisso deles com o FastAPI e sua **comunidade** (você), pois eles não apenas querem fornecer um **bom serviço**, mas também querem garantir que você tenha um **framework bom e saudável**, o FastAPI. 🙇 +/// + +## Geradores de SDK dos patrocinadores do FastAPI { #sdk-generators-from-fastapi-sponsors } + +Esta seção destaca soluções **financiadas por investimento** e **com suporte de empresas** que patrocinam o FastAPI. Esses produtos fornecem **funcionalidades adicionais** e **integrações** além de SDKs gerados com alta qualidade. + +Ao ✨ [**patrocinar o FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨, essas empresas ajudam a garantir que o framework e seu **ecossistema** continuem saudáveis e **sustentáveis**. + +O patrocínio também demonstra um forte compromisso com a **comunidade** FastAPI (você), mostrando que elas se importam não apenas em oferecer um **ótimo serviço**, mas também em apoiar um **framework robusto e próspero**, o FastAPI. 🙇 Por exemplo, você pode querer experimentar: * Speakeasy -* Stainless -* liblab +* Stainless +* liblab -Existem também várias outras empresas que oferecem serviços semelhantes que você pode pesquisar e encontrar online. 🤓 +Algumas dessas soluções também podem ser open source ou oferecer planos gratuitos, para que você possa testá-las sem compromisso financeiro. Outros geradores comerciais de SDK estão disponíveis e podem ser encontrados online. 🤓 -## Gerar um Cliente Frontend TypeScript +## Crie um SDK em TypeScript { #create-a-typescript-sdk } -Vamos começar com um aplicativo **FastAPI** simples: +Vamos começar com uma aplicação FastAPI simples: {* ../../docs_src/generate_clients/tutorial001_py39.py hl[7:9,12:13,16:17,21] *} Note que as *operações de rota* definem os modelos que usam para o corpo da requisição e o corpo da resposta, usando os modelos `Item` e `ResponseMessage`. -### Documentação da API +### Documentação da API { #api-docs } -Se você acessar a documentação da API, verá que ela tem os **schemas** para os dados a serem enviados nas requisições e recebidos nas respostas: +Se você for para `/docs`, verá que ela tem os **schemas** para os dados a serem enviados nas requisições e recebidos nas respostas: Você pode ver esses schemas porque eles foram declarados com os modelos no app. -Essas informações estão disponíveis no **OpenAPI schema** do app e são mostradas na documentação da API (pelo Swagger UI). +Essas informações estão disponíveis no **schema OpenAPI** do app e são mostradas na documentação da API. E essas mesmas informações dos modelos que estão incluídas no OpenAPI são o que pode ser usado para **gerar o código do cliente**. -### Gerar um Cliente TypeScript +### Hey API { #hey-api } -Agora que temos o app com os modelos, podemos gerar o código do cliente para o frontend. +Depois que tivermos uma aplicação FastAPI com os modelos, podemos usar o Hey API para gerar um cliente TypeScript. A forma mais rápida é via npx. -#### Instalar o `openapi-ts` - -Você pode instalar o `openapi-ts` no seu código frontend com: - -
- -```console -$ npm install @hey-api/openapi-ts --save-dev - ----> 100% +```sh +npx @hey-api/openapi-ts -i http://localhost:8000/openapi.json -o src/client ``` -
+Isso gerará um SDK TypeScript em `./src/client`. -#### Gerar o Código do Cliente +Você pode aprender como instalar `@hey-api/openapi-ts` e ler sobre o resultado gerado no site deles. -Para gerar o código do cliente, você pode usar a aplicação de linha de comando `openapi-ts` que agora está instalada. +### Usando o SDK { #using-the-sdk } -Como ela está instalada no projeto local, você provavelmente não conseguiria chamar esse comando diretamente, mas você o colocaria no seu arquivo `package.json`. - -Poderia ser assim: - -```JSON hl_lines="7" -{ - "name": "frontend-app", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "generate-client": "openapi-ts --input http://localhost:8000/openapi.json --output ./src/client --client axios" - }, - "author": "", - "license": "", - "devDependencies": { - "@hey-api/openapi-ts": "^0.27.38", - "typescript": "^4.6.2" - } -} -``` - -Depois de ter esse script NPM `generate-client` lá, você pode executá-lo com: - -
- -```console -$ npm run generate-client - -frontend-app@1.0.0 generate-client /home/user/code/frontend-app -> openapi-ts --input http://localhost:8000/openapi.json --output ./src/client --client axios -``` - -
- -Esse comando gerará o código em `./src/client` e usará o `axios` (a biblioteca HTTP frontend) internamente. - -### Experimente o Código do Cliente - -Agora você pode importar e usar o código do cliente, ele poderia ser assim, observe que você obtém preenchimento automático para os métodos: +Agora você pode importar e usar o código do cliente. Poderia ser assim, observe que você obtém preenchimento automático para os métodos: @@ -119,7 +80,7 @@ Você também obterá preenchimento automático para o corpo a ser enviado: /// tip | Dica -Observe o preenchimento automático para `name` e `price`, que foi definido no aplicativo FastAPI, no modelo `Item`. +Observe o preenchimento automático para `name` e `price`, que foi definido na aplicação FastAPI, no modelo `Item`. /// @@ -131,17 +92,17 @@ O objeto de resposta também terá preenchimento automático: -## App FastAPI com Tags +## Aplicação FastAPI com Tags { #fastapi-app-with-tags } -Em muitos casos seu app FastAPI será maior, e você provavelmente usará tags para separar diferentes grupos de *operações de rota*. +Em muitos casos, sua aplicação FastAPI será maior, e você provavelmente usará tags para separar diferentes grupos de *operações de rota*. Por exemplo, você poderia ter uma seção para **items** e outra seção para **users**, e elas poderiam ser separadas por tags: {* ../../docs_src/generate_clients/tutorial002_py39.py hl[21,26,34] *} -### Gerar um Cliente TypeScript com Tags +### Gere um cliente TypeScript com Tags { #generate-a-typescript-client-with-tags } -Se você gerar um cliente para um app FastAPI usando tags, normalmente também separará o código do cliente com base nas tags. +Se você gerar um cliente para uma aplicação FastAPI usando tags, normalmente também separará o código do cliente com base nas tags. Dessa forma, você poderá ter as coisas ordenadas e agrupadas corretamente para o código do cliente: @@ -152,33 +113,33 @@ Nesse caso você tem: * `ItemsService` * `UsersService` -### Nomes dos Métodos do Cliente +### Nomes dos métodos do cliente { #client-method-names } -Agora os nomes dos métodos gerados como `createItemItemsPost` não parecem muito "limpos": +Agora os nomes dos métodos gerados como `createItemItemsPost` não parecem muito “limpos”: ```TypeScript ItemsService.createItemItemsPost({name: "Plumbus", price: 5}) ``` -...isto ocorre porque o gerador de clientes usa o **operation ID** interno do OpenAPI para cada *operação de rota*. +...isso ocorre porque o gerador de clientes usa o **ID de operação** interno do OpenAPI para cada *operação de rota*. -O OpenAPI exige que cada operation ID seja único em todas as *operações de rota*, então o FastAPI usa o **nome da função**, o **caminho** e o **método/operacao HTTP** para gerar esse operation ID, porque dessa forma ele pode garantir que os operation IDs sejam únicos. +O OpenAPI exige que cada ID de operação seja único em todas as *operações de rota*, então o FastAPI usa o **nome da função**, o **path** e o **método/operação HTTP** para gerar esse ID de operação, porque dessa forma ele pode garantir que os IDs de operação sejam únicos. Mas eu vou te mostrar como melhorar isso a seguir. 🤓 -### IDs de Operação Personalizados e Melhores Nomes de Método +## IDs de operação personalizados e nomes de métodos melhores { #custom-operation-ids-and-better-method-names } Você pode **modificar** a maneira como esses IDs de operação são **gerados** para torná-los mais simples e ter **nomes de método mais simples** nos clientes. Neste caso, você terá que garantir que cada ID de operação seja **único** de alguma outra maneira. -Por exemplo, você poderia garantir que cada *operação de rota* tenha uma tag, e então gerar o ID da operação com base na **tag** e no **nome** da *operação de rota* (o nome da função). +Por exemplo, você poderia garantir que cada *operação de rota* tenha uma tag, e então gerar o ID de operação com base na **tag** e no **nome** da *operação de rota* (o nome da função). -### Função Personalizada para Gerar IDs de Operação Únicos +### Função personalizada para gerar IDs exclusivos { #custom-generate-unique-id-function } -O FastAPI usa um **ID único** para cada *operação de rota*, ele é usado para o **ID da operação** e também para os nomes de quaisquer modelos personalizados necessários, para requisições ou respostas. +O FastAPI usa um **ID exclusivo** para cada *operação de rota*, ele é usado para o **ID de operação** e também para os nomes de quaisquer modelos personalizados necessários, para requisições ou respostas. -Você pode personalizar essa função. Ela recebe uma `APIRoute` e gera uma string. +Você pode personalizar essa função. Ela recebe uma `APIRoute` e retorna uma string. Por exemplo, aqui está usando a primeira tag (você provavelmente terá apenas uma tag) e o nome da *operação de rota* (o nome da função). @@ -186,15 +147,15 @@ Você pode então passar essa função personalizada para o **FastAPI** como o p {* ../../docs_src/generate_clients/tutorial003_py39.py hl[6:7,10] *} -### Gerar um Cliente TypeScript com IDs de Operação Personalizados +### Gere um cliente TypeScript com IDs de operação personalizados { #generate-a-typescript-client-with-custom-operation-ids } Agora, se você gerar o cliente novamente, verá que ele tem os nomes dos métodos melhorados: -Como você pode ver, os nomes dos métodos agora têm a tag e, em seguida, o nome da função. Agora eles não incluem informações do caminho da URL e da operação HTTP. +Como você pode ver, os nomes dos métodos agora têm a tag e, em seguida, o nome da função. Agora eles não incluem informações do path da URL e da operação HTTP. -### Pré-processar a Especificação OpenAPI para o Gerador de Clientes +### Pré-processar a especificação OpenAPI para o gerador de clientes { #preprocess-the-openapi-specification-for-the-client-generator } O código gerado ainda tem algumas **informações duplicadas**. @@ -202,7 +163,7 @@ Nós já sabemos que esse método está relacionado aos **items** porque essa pa Provavelmente ainda queremos mantê-lo para o OpenAPI em geral, pois isso garantirá que os IDs de operação sejam **únicos**. -Mas para o cliente gerado, poderíamos **modificar** os IDs de operação do OpenAPI logo antes de gerar os clientes, apenas para tornar esses nomes de método mais **simples**. +Mas para o cliente gerado, poderíamos **modificar** os IDs de operação do OpenAPI logo antes de gerar os clientes, apenas para tornar esses nomes de método mais agradáveis e **limpos**. Poderíamos baixar o JSON do OpenAPI para um arquivo `openapi.json` e então poderíamos **remover essa tag prefixada** com um script como este: @@ -218,44 +179,30 @@ Poderíamos baixar o JSON do OpenAPI para um arquivo `openapi.json` e então pod Com isso, os IDs de operação seriam renomeados de coisas como `items-get_items` para apenas `get_items`, dessa forma o gerador de clientes pode gerar nomes de métodos mais simples. -### Gerar um Cliente TypeScript com o OpenAPI Pré-processado +### Gere um cliente TypeScript com o OpenAPI pré-processado { #generate-a-typescript-client-with-the-preprocessed-openapi } -Agora, como o resultado final está em um arquivo `openapi.json`, você modificaria o `package.json` para usar esse arquivo local, por exemplo: +Como o resultado final está agora em um arquivo `openapi.json`, você precisa atualizar o local de entrada: -```JSON hl_lines="7" -{ - "name": "frontend-app", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "generate-client": "openapi-ts --input ./openapi.json --output ./src/client --client axios" - }, - "author": "", - "license": "", - "devDependencies": { - "@hey-api/openapi-ts": "^0.27.38", - "typescript": "^4.6.2" - } -} +```sh +npx @hey-api/openapi-ts -i ./openapi.json -o src/client ``` -Depois de gerar o novo cliente, você teria agora **nomes de métodos "limpos"**, com todo o **preenchimento automático**, **erros em linha**, etc: +Depois de gerar o novo cliente, você terá agora **nomes de métodos “limpos”**, com todo o **preenchimento automático**, **erros em linha**, etc: -## Benefícios +## Benefícios { #benefits } -Ao usar os clientes gerados automaticamente, você teria **preenchimento automático** para: +Ao usar os clientes gerados automaticamente, você terá **preenchimento automático** para: * Métodos. -* Corpo de requisições, parâmetros da query, etc. -* Corpo de respostas. +* Corpos de requisições, parâmetros de query, etc. +* Corpos de respostas. -Você também teria **erros em linha** para tudo. +Você também terá **erros em linha** para tudo. -E sempre que você atualizar o código do backend, e **regenerar** o frontend, ele teria quaisquer novas *operações de rota* disponíveis como métodos, as antigas removidas, e qualquer outra alteração seria refletida no código gerado. 🤓 +E sempre que você atualizar o código do backend e **regenerar** o frontend, ele terá quaisquer novas *operações de rota* disponíveis como métodos, as antigas removidas, e qualquer outra alteração será refletida no código gerado. 🤓 -Isso também significa que se algo mudar, será **refletido** no código do cliente automaticamente. E se você **construir** o cliente, ele dará erro se houver alguma **incompatibilidade** nos dados usados. +Isso também significa que, se algo mudou, será **refletido** no código do cliente automaticamente. E se você **construir** o cliente, ele falhará caso haja qualquer **incompatibilidade** nos dados usados. -Então, você **detectaria vários erros** muito cedo no ciclo de desenvolvimento, em vez de ter que esperar que os erros apareçam para seus usuários finais em produção e então tentar depurar onde está o problema. ✨ +Assim, você **detectará muitos erros** muito cedo no ciclo de desenvolvimento, em vez de ter que esperar que os erros apareçam para seus usuários finais em produção e então tentar depurar onde está o problema. ✨ diff --git a/docs/pt/docs/advanced/index.md b/docs/pt/docs/advanced/index.md index 22ba2bf4a..23e551074 100644 --- a/docs/pt/docs/advanced/index.md +++ b/docs/pt/docs/advanced/index.md @@ -1,10 +1,10 @@ -# Guia de Usuário Avançado +# Guia de Usuário Avançado { #advanced-user-guide } -## Recursos Adicionais +## Recursos Adicionais { #additional-features } O [Tutorial - Guia de Usuário](../tutorial/index.md){.internal-link target=_blank} deve ser o suficiente para dar a você um tour por todos os principais recursos do **FastAPI**. -Na próxima seção você verá outras opções, configurações, e recursos adicionais. +Nas próximas seções você verá outras opções, configurações, e recursos adicionais. /// tip | Dica @@ -14,14 +14,8 @@ E é possível que para seu caso de uso, a solução esteja em uma delas. /// -## Leia o Tutorial primeiro +## Leia o Tutorial primeiro { #read-the-tutorial-first } Você ainda pode usar a maior parte dos recursos no **FastAPI** com o conhecimento do [Tutorial - Guia de Usuário](../tutorial/index.md){.internal-link target=_blank}. E as próximas seções assumem que você já leu ele, e que você conhece suas ideias principais. - -## Curso TestDriven.io - -Se você gostaria de fazer um curso avançado-iniciante para complementar essa seção da documentação, você pode querer conferir: Test-Driven Development com FastAPI e Docker por **TestDriven.io**. - -Eles estão atualmente doando 10% de todos os lucros para o desenvolvimento do **FastAPI**. 🎉 😄 diff --git a/docs/pt/docs/advanced/middleware.md b/docs/pt/docs/advanced/middleware.md index 7700939f0..9186bcb49 100644 --- a/docs/pt/docs/advanced/middleware.md +++ b/docs/pt/docs/advanced/middleware.md @@ -1,4 +1,4 @@ -# Middleware Avançado +# Middleware Avançado { #advanced-middleware } No tutorial principal você leu como adicionar [Middleware Personalizado](../tutorial/middleware.md){.internal-link target=_blank} à sua aplicação. @@ -6,9 +6,9 @@ E então você também leu como lidar com [CORS com o `CORSMiddleware`](../tutor Nesta seção, veremos como usar outros middlewares. -## Adicionando middlewares ASGI +## Adicionando middlewares ASGI { #adding-asgi-middlewares } -Como o **FastAPI** é baseado no Starlette e implementa a especificação ASGI, você pode usar qualquer middleware ASGI. +Como o **FastAPI** é baseado no Starlette e implementa a especificação ASGI, você pode usar qualquer middleware ASGI. O middleware não precisa ser feito para o FastAPI ou Starlette para funcionar, desde que siga a especificação ASGI. @@ -39,19 +39,19 @@ app.add_middleware(UnicornMiddleware, some_config="rainbow") `app.add_middleware()` recebe uma classe de middleware como o primeiro argumento e quaisquer argumentos adicionais a serem passados para o middleware. -## Middlewares Integrados +## Middlewares Integrados { #integrated-middlewares } **FastAPI** inclui vários middlewares para casos de uso comuns, veremos a seguir como usá-los. /// note | Detalhes Técnicos -Para o próximo exemplo, você também poderia usar `from starlette.middleware.something import SomethingMiddleware`. +Para os próximos exemplos, você também poderia usar `from starlette.middleware.something import SomethingMiddleware`. **FastAPI** fornece vários middlewares em `fastapi.middleware` apenas como uma conveniência para você, o desenvolvedor. Mas a maioria dos middlewares disponíveis vem diretamente do Starlette. /// -## `HTTPSRedirectMiddleware` +## `HTTPSRedirectMiddleware` { #httpsredirectmiddleware } Garante que todas as requisições devem ser `https` ou `wss`. @@ -59,7 +59,7 @@ Qualquer requisição para `http` ou `ws` será redirecionada para o esquema seg {* ../../docs_src/advanced_middleware/tutorial001.py hl[2,6] *} -## `TrustedHostMiddleware` +## `TrustedHostMiddleware` { #trustedhostmiddleware } Garante que todas as requisições recebidas tenham um cabeçalho `Host` corretamente configurado, a fim de proteger contra ataques de cabeçalho de host HTTP. @@ -68,10 +68,11 @@ Garante que todas as requisições recebidas tenham um cabeçalho `Host` correta Os seguintes argumentos são suportados: * `allowed_hosts` - Uma lista de nomes de domínio que são permitidos como nomes de host. Domínios com coringa, como `*.example.com`, são suportados para corresponder a subdomínios. Para permitir qualquer nome de host, use `allowed_hosts=["*"]` ou omita o middleware. +* `www_redirect` - Se definido como True, as requisições para versões sem www dos hosts permitidos serão redirecionadas para suas versões com www. O padrão é `True`. Se uma requisição recebida não for validada corretamente, uma resposta `400` será enviada. -## `GZipMiddleware` +## `GZipMiddleware` { #gzipmiddleware } Gerencia respostas GZip para qualquer requisição que inclua `"gzip"` no cabeçalho `Accept-Encoding`. @@ -84,7 +85,7 @@ Os seguintes argumentos são suportados: * `minimum_size` - Não comprima respostas menores que este tamanho mínimo em bytes. O padrão é `500`. * `compresslevel` - Usado durante a compressão GZip. É um inteiro variando de 1 a 9. O padrão é `9`. Um valor menor resulta em uma compressão mais rápida, mas em arquivos maiores, enquanto um valor maior resulta em uma compressão mais lenta, mas em arquivos menores. -## Outros middlewares +## Outros middlewares { #other-middlewares } Há muitos outros middlewares ASGI. diff --git a/docs/pt/docs/advanced/openapi-callbacks.md b/docs/pt/docs/advanced/openapi-callbacks.md index b0659d3d6..12309df81 100644 --- a/docs/pt/docs/advanced/openapi-callbacks.md +++ b/docs/pt/docs/advanced/openapi-callbacks.md @@ -1,29 +1,29 @@ -# Callbacks na OpenAPI +# Callbacks na OpenAPI { #openapi-callbacks } Você poderia criar uma API com uma *operação de rota* que poderia acionar uma solicitação a uma *API externa* criada por outra pessoa (provavelmente o mesmo desenvolvedor que estaria *usando* sua API). -O processo que acontece quando seu aplicativo de API chama a *API externa* é chamado de "callback". Porque o software que o desenvolvedor externo escreveu envia uma solicitação para sua API e então sua API *chama de volta*, enviando uma solicitação para uma *API externa* (que provavelmente foi criada pelo mesmo desenvolvedor). +O processo que acontece quando sua aplicação de API chama a *API externa* é chamado de "callback". Porque o software que o desenvolvedor externo escreveu envia uma solicitação para sua API e então sua API *chama de volta*, enviando uma solicitação para uma *API externa* (que provavelmente foi criada pelo mesmo desenvolvedor). Nesse caso, você poderia querer documentar como essa API externa *deveria* ser. Que *operação de rota* ela deveria ter, que corpo ela deveria esperar, que resposta ela deveria retornar, etc. -## Um aplicativo com callbacks +## Um aplicativo com callbacks { #an-app-with-callbacks } Vamos ver tudo isso com um exemplo. -Imagine que você tem um aplicativo que permite criar faturas. +Imagine que você desenvolve um aplicativo que permite criar faturas. Essas faturas terão um `id`, `title` (opcional), `customer` e `total`. -O usuário da sua API (um desenvolvedor externo) criará uma fatura em sua API com uma solicitação POST. +O usuário da sua API (um desenvolvedor externo) criará uma fatura na sua API com uma solicitação POST. Então sua API irá (vamos imaginar): -* Enviar uma solicitação de pagamento para o desenvolvedor externo. +* Enviar a fatura para algum cliente do desenvolvedor externo. * Coletar o dinheiro. * Enviar a notificação de volta para o usuário da API (o desenvolvedor externo). -* Isso será feito enviando uma solicitação POST (de *sua API*) para alguma *API externa* fornecida por esse desenvolvedor externo (este é o "callback"). + * Isso será feito enviando uma solicitação POST (de *sua API*) para alguma *API externa* fornecida por esse desenvolvedor externo (este é o "callback"). -## O aplicativo **FastAPI** normal +## O aplicativo **FastAPI** normal { #the-normal-fastapi-app } Vamos primeiro ver como o aplicativo da API normal se pareceria antes de adicionar o callback. @@ -39,11 +39,11 @@ O parâmetro de consulta `callback_url` usa um tipo Pydantic HTTPX ou Requisições. +Ao implementar o callback por conta própria, você pode usar algo como HTTPX ou Requests. /// -## Escrevendo o código de documentação do callback +## Escreva o código de documentação do callback { #write-the-callback-documentation-code } Esse código não será executado em seu aplicativo, nós só precisamos dele para *documentar* como essa *API externa* deveria ser. @@ -80,37 +80,37 @@ Então vamos usar esse mesmo conhecimento para documentar como a *API externa* d /// tip | Dica -Quando escrever o código para documentar um callback, pode ser útil imaginar que você é aquele *desenvolvedor externo*. E que você está atualmente implementando a *API externa*, não *sua API*. +Ao escrever o código para documentar um callback, pode ser útil imaginar que você é aquele *desenvolvedor externo*. E que você está atualmente implementando a *API externa*, não *sua API*. -Adotar temporariamente esse ponto de vista (do *desenvolvedor externo*) pode ajudar a sentir que é mais óbvio onde colocar os parâmetros, o modelo Pydantic para o corpo, para a resposta, etc. para essa *API externa*. +Adotar temporariamente esse ponto de vista (do *desenvolvedor externo*) pode ajudar a perceber mais facilmente onde colocar os parâmetros, o modelo Pydantic para o corpo, para a resposta, etc. para essa *API externa*. /// -### Criar um `APIRouter` para o callback +### Crie um `APIRouter` de callback { #create-a-callback-apirouter } -Primeiramente crie um novo `APIRouter` que conterá um ou mais callbacks. +Primeiro crie um novo `APIRouter` que conterá um ou mais callbacks. {* ../../docs_src/openapi_callbacks/tutorial001.py hl[3,25] *} -### Crie a *operação de rota* do callback +### Crie a *operação de rota* do callback { #create-the-callback-path-operation } Para criar a *operação de rota* do callback, use o mesmo `APIRouter` que você criou acima. -Ele deve parecer exatamente como uma *operação de rota* normal do FastAPI: +Ela deve parecer exatamente como uma *operação de rota* normal do FastAPI: -* Ele provavelmente deveria ter uma declaração do corpo que deveria receber, por exemplo. `body: InvoiceEvent`. -* E também deveria ter uma declaração de um código de status de resposta, por exemplo. `response_model=InvoiceEventReceived`. +* Ela provavelmente deveria ter uma declaração do corpo que deveria receber, por exemplo, `body: InvoiceEvent`. +* E também poderia ter uma declaração da resposta que deveria retornar, por exemplo, `response_model=InvoiceEventReceived`. {* ../../docs_src/openapi_callbacks/tutorial001.py hl[16:18,21:22,28:32] *} Há 2 diferenças principais de uma *operação de rota* normal: * Ela não necessita ter nenhum código real, porque seu aplicativo nunca chamará esse código. Ele é usado apenas para documentar a *API externa*. Então, a função poderia ter apenas `pass`. -* A *rota* pode conter uma expressão OpenAPI 3 (veja mais abaixo) onde pode usar variáveis com parâmetros e partes da solicitação original enviada para *sua API*. +* O *path* pode conter uma expressão OpenAPI 3 (veja mais abaixo) em que pode usar variáveis com parâmetros e partes da solicitação original enviada para *sua API*. -### A expressão do caminho do callback +### A expressão do path do callback { #the-callback-path-expression } -A *rota* do callback pode ter uma expressão OpenAPI 3 que pode conter partes da solicitação original enviada para *sua API*. +O *path* do callback pode ter uma expressão OpenAPI 3 que pode conter partes da solicitação original enviada para *sua API*. Nesse caso, é a `str`: @@ -163,11 +163,11 @@ Perceba como a URL de callback usada contém a URL recebida como um parâmetro d /// -### Adicionar o roteador de callback +### Adicione o roteador de callback { #add-the-callback-router } -Nesse ponto você tem a(s) *operação de rota de callback* necessária(s) (a(s) que o *desenvolvedor externo* deveria implementar na *API externa*) no roteador de callback que você criou acima. +Nesse ponto você tem a(s) *operação(ões) de rota de callback* necessária(s) (a(s) que o *desenvolvedor externo* deveria implementar na *API externa*) no roteador de callback que você criou acima. -Agora use o parâmetro `callbacks` no decorador da *operação de rota de sua API* para passar o atributo `.routes` (que é na verdade apenas uma `list` de rotas/*operações de rota*) do roteador de callback que você criou acima: +Agora use o parâmetro `callbacks` no decorador da *operação de rota da sua API* para passar o atributo `.routes` (que é na verdade apenas uma `list` de rotas/*operações de path*) do roteador de callback: {* ../../docs_src/openapi_callbacks/tutorial001.py hl[35] *} @@ -177,7 +177,7 @@ Perceba que você não está passando o roteador em si (`invoices_callback_route /// -### Verifique a documentação +### Verifique a documentação { #check-the-docs } Agora você pode iniciar seu aplicativo e ir para http://127.0.0.1:8000/docs. diff --git a/docs/pt/docs/advanced/openapi-webhooks.md b/docs/pt/docs/advanced/openapi-webhooks.md index f35922234..fa3840fb3 100644 --- a/docs/pt/docs/advanced/openapi-webhooks.md +++ b/docs/pt/docs/advanced/openapi-webhooks.md @@ -1,4 +1,4 @@ -# Webhooks OpenAPI +# Webhooks OpenAPI { #openapi-webhooks } Existem situações onde você deseja informar os **usuários** da sua API que a sua aplicação pode chamar a aplicação *deles* (enviando uma requisição) com alguns dados, normalmente para **notificar** algum tipo de **evento**. @@ -6,7 +6,7 @@ Isso significa que no lugar do processo normal de seus usuários enviarem requis Isso normalmente é chamado de **webhook**. -## Etapas dos Webhooks +## Etapas dos webhooks { #webhooks-steps } Normalmente, o processo é que **você define** em seu código qual é a mensagem que você irá mandar, o **corpo da sua requisição**. @@ -16,7 +16,7 @@ E os **seus usuários** definem de alguma forma (em algum painel por exemplo) a Toda a **lógica** sobre como cadastrar as URLs para os webhooks e o código para enviar de fato as requisições cabe a você definir. Você escreve da maneira que você desejar no **seu próprio código**. -## Documentando webhooks com o FastAPI e OpenAPI +## Documentando webhooks com o FastAPI e OpenAPI { #documenting-webhooks-with-fastapi-and-openapi } Com o **FastAPI**, utilizando o OpenAPI, você pode definir os nomes destes webhooks, os tipos das operações HTTP que a sua aplicação pode enviar (e.g. `POST`, `PUT`, etc.) e os **corpos** da requisição que a sua aplicação enviaria. @@ -28,7 +28,7 @@ Webhooks estão disponíveis a partir do OpenAPI 3.1.0, e possui suporte do Fast /// -## Uma aplicação com webhooks +## Uma aplicação com webhooks { #an-app-with-webhooks } Quando você cria uma aplicação com o **FastAPI**, existe um atributo chamado `webhooks`, que você utilizar para defini-los da mesma maneira que você definiria as suas **operações de rotas**, utilizando por exemplo `@app.webhooks.post()`. @@ -42,11 +42,11 @@ O objeto `app.webhooks` é na verdade apenas um `APIRouter`, o mesmo tipo que vo /// -Note que utilizando webhooks você não está de fato declarando uma **rota** (como `/items/`), o texto que informa é apenas um **identificador** do webhook (o nome do evento), por exemplo em `@app.webhooks.post("new-subscription")`, o nome do webhook é `new-subscription`. +Note que utilizando webhooks você não está de fato declarando um *path* (como `/items/`), o texto que informa é apenas um **identificador** do webhook (o nome do evento), por exemplo em `@app.webhooks.post("new-subscription")`, o nome do webhook é `new-subscription`. -Isto porque espera-se que os **seus usuários** definam o verdadeiro **caminho da URL** onde eles desejam receber a requisição do webhook de algum outra maneira. (e.g. um painel). +Isto porque espera-se que os **seus usuários** definam o verdadeiro **URL path** onde eles desejam receber a requisição do webhook de algum outra maneira. (e.g. um painel). -### Confira a documentação +### Confira a documentação { #check-the-docs } Agora você pode iniciar a sua aplicação e ir até http://127.0.0.1:8000/docs. diff --git a/docs/pt/docs/advanced/path-operation-advanced-configuration.md b/docs/pt/docs/advanced/path-operation-advanced-configuration.md index 411d0f9a7..cd2015892 100644 --- a/docs/pt/docs/advanced/path-operation-advanced-configuration.md +++ b/docs/pt/docs/advanced/path-operation-advanced-configuration.md @@ -1,8 +1,8 @@ -# Configuração Avançada da Operação de Rota +# Configuração Avançada da Operação de Rota { #path-operation-advanced-configuration } -## operationId do OpenAPI +## operationId do OpenAPI { #openapi-operationid } -/// warning | Aviso +/// warning | Atenção Se você não é um "especialista" no OpenAPI, você provavelmente não precisa disso. @@ -14,13 +14,13 @@ Você precisa ter certeza que ele é único para cada operação. {* ../../docs_src/path_operation_advanced_configuration/tutorial001.py hl[6] *} -### Utilizando o nome da *função de operação de rota* como o operationId +### Utilizando o nome da *função de operação de rota* como o operationId { #using-the-path-operation-function-name-as-the-operationid } Se você quiser utilizar o nome das funções da sua API como `operationId`s, você pode iterar sobre todos esses nomes e sobrescrever o `operationId` em cada *operação de rota* utilizando o `APIRoute.name` dela. Você deve fazer isso depois de adicionar todas as suas *operações de rota*. -{* ../../docs_src/path_operation_advanced_configuration/tutorial002.py hl[2,12:21,24] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial002.py hl[2, 12:21, 24] *} /// tip | Dica @@ -28,7 +28,7 @@ Se você chamar `app.openapi()` manualmente, os `operationId`s devem ser atualiz /// -/// warning | Aviso +/// warning | Atenção Se você fizer isso, você tem que ter certeza de que cada uma das suas *funções de operação de rota* tem um nome único. @@ -36,13 +36,13 @@ Mesmo que elas estejam em módulos (arquivos Python) diferentes. /// -## Excluir do OpenAPI +## Excluir do OpenAPI { #exclude-from-openapi } Para excluir uma *operação de rota* do esquema OpenAPI gerado (e por consequência, dos sistemas de documentação automáticos), utilize o parâmetro `include_in_schema` e defina ele como `False`: {* ../../docs_src/path_operation_advanced_configuration/tutorial003.py hl[6] *} -## Descrição avançada a partir de docstring +## Descrição avançada a partir de docstring { #advanced-description-from-docstring } Você pode limitar as linhas utilizadas a partir de uma docstring de uma *função de operação de rota* para o OpenAPI. @@ -52,7 +52,7 @@ Ele não será mostrado na documentação, mas outras ferramentas (como o Sphinx {* ../../docs_src/path_operation_advanced_configuration/tutorial004.py hl[19:29] *} -## Respostas Adicionais +## Respostas Adicionais { #additional-responses } Você provavelmente já viu como declarar o `response_model` e `status_code` para uma *operação de rota*. @@ -62,11 +62,11 @@ Você também pode declarar respostas adicionais, com seus modelos, códigos de Existe um capítulo inteiro da nossa documentação sobre isso, você pode ler em [Retornos Adicionais no OpenAPI](additional-responses.md){.internal-link target=_blank}. -## Extras do OpenAPI +## Extras do OpenAPI { #openapi-extra } Quando você declara uma *operação de rota* na sua aplicação, o **FastAPI** irá gerar os metadados relevantes da *operação de rota* automaticamente para serem incluídos no esquema do OpenAPI. -/// note | Nota +/// note | Detalhes Técnicos Na especificação do OpenAPI, isso é chamado de um Objeto de Operação. @@ -88,7 +88,7 @@ Caso você só precise declarar respostas adicionais, uma forma conveniente de f Você pode estender o esquema do OpenAPI para uma *operação de rota* utilizando o parâmetro `openapi_extra`. -### Extensões do OpenAPI +### Extensões do OpenAPI { #openapi-extensions } Esse parâmetro `openapi_extra` pode ser útil, por exemplo, para declarar [Extensões do OpenAPI](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#specificationExtensions): @@ -129,7 +129,7 @@ E se você olhar o esquema OpenAPI resultante (na rota `/openapi.json` da sua AP } ``` -### Esquema de *operação de rota* do OpenAPI personalizado +### Esquema de *operação de rota* do OpenAPI personalizado { #custom-openapi-path-operation-schema } O dicionário em `openapi_extra` vai ter todos os seus níveis mesclados dentro do esquema OpenAPI gerado automaticamente para a *operação de rota*. @@ -139,39 +139,39 @@ Por exemplo, você poderia optar por ler e validar a requisição com seu própr Você pode fazer isso com `openapi_extra`: -{* ../../docs_src/path_operation_advanced_configuration/tutorial006.py hl[19:36,39:40] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial006.py hl[19:36, 39:40] *} Nesse exemplo, nós não declaramos nenhum modelo do Pydantic. Na verdade, o corpo da requisição não está nem mesmo analisado como JSON, ele é lido diretamente como `bytes` e a função `magic_data_reader()` seria a responsável por analisar ele de alguma forma. De toda forma, nós podemos declarar o esquema esperado para o corpo da requisição. -### Tipo de conteúdo do OpenAPI personalizado +### Tipo de conteúdo do OpenAPI personalizado { #custom-openapi-content-type } -Utilizando esse mesmo truque, você pode utilizar um modelo Pydantic para definir o esquema JSON que é então incluído na seção do esquema personalizado do OpenAPI na *operação de rota*. +Utilizando esse mesmo truque, você pode utilizar um modelo Pydantic para definir o JSON Schema que é então incluído na seção do esquema personalizado do OpenAPI na *operação de rota*. E você pode fazer isso até mesmo quando os dados da requisição não seguem o formato JSON. -Por exemplo, nesta aplicação nós não usamos a funcionalidade integrada ao FastAPI de extrair o esquema JSON dos modelos Pydantic nem a validação automática do JSON. Na verdade, estamos declarando o tipo do conteúdo da requisição como YAML, em vez de JSON: +Por exemplo, nesta aplicação nós não usamos a funcionalidade integrada ao FastAPI de extrair o JSON Schema dos modelos Pydantic nem a validação automática do JSON. Na verdade, estamos declarando o tipo do conteúdo da requisição como YAML, em vez de JSON: //// tab | Pydantic v2 -{* ../../docs_src/path_operation_advanced_configuration/tutorial007.py hl[17:22,24] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial007.py hl[17:22, 24] *} //// //// tab | Pydantic v1 -{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py hl[17:22,24] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py hl[17:22, 24] *} //// /// info | Informação -Na versão 1 do Pydantic, o método para obter o esquema JSON de um modelo é `Item.schema()`, na versão 2 do Pydantic, o método é `Item.model_json_schema()` +Na versão 1 do Pydantic, o método para obter o JSON Schema de um modelo é `Item.schema()`, na versão 2 do Pydantic, o método é `Item.model_json_schema()`. /// -Entretanto, mesmo que não utilizemos a funcionalidade integrada por padrão, ainda estamos usando um modelo Pydantic para gerar um esquema JSON manualmente para os dados que queremos receber no formato YAML. +Entretanto, mesmo que não utilizemos a funcionalidade integrada por padrão, ainda estamos usando um modelo Pydantic para gerar um JSON Schema manualmente para os dados que queremos receber no formato YAML. Então utilizamos a requisição diretamente, e extraímos o corpo como `bytes`. Isso significa que o FastAPI não vai sequer tentar analisar o corpo da requisição como JSON. @@ -195,7 +195,7 @@ Na versão 1 do Pydantic, o método para analisar e validar um objeto era `Item. /// -///tip | Dica +/// tip | Dica Aqui reutilizamos o mesmo modelo do Pydantic. diff --git a/docs/pt/docs/advanced/response-change-status-code.md b/docs/pt/docs/advanced/response-change-status-code.md index 358c12d54..0f08873f6 100644 --- a/docs/pt/docs/advanced/response-change-status-code.md +++ b/docs/pt/docs/advanced/response-change-status-code.md @@ -1,10 +1,10 @@ -# Retorno - Altere o Código de Status +# Retorno - Altere o Código de Status { #response-change-status-code } Você provavelmente leu anteriormente que você pode definir um [Código de Status do Retorno](../tutorial/response-status-code.md){.internal-link target=_blank} padrão. Porém em alguns casos você precisa retornar um código de status diferente do padrão. -## Caso de uso +## Caso de uso { #use-case } Por exemplo, imagine que você deseja retornar um código de status HTTP de "OK" `200` por padrão. @@ -14,7 +14,7 @@ Mas você ainda quer ser capaz de filtrar e converter o dado que você retornar Para estes casos, você pode utilizar um parâmetro `Response`. -## Use um parâmetro `Response` +## Use um parâmetro `Response` { #use-a-response-parameter } Você pode declarar um parâmetro do tipo `Response` em sua *função de operação de rota* (assim como você pode fazer para cookies e headers). diff --git a/docs/pt/docs/advanced/response-cookies.md b/docs/pt/docs/advanced/response-cookies.md index f005f0b9b..41fc00013 100644 --- a/docs/pt/docs/advanced/response-cookies.md +++ b/docs/pt/docs/advanced/response-cookies.md @@ -1,12 +1,12 @@ -# Cookies de Resposta +# Cookies de Resposta { #response-cookies } -## Usando um parâmetro `Response` +## Use um parâmetro `Response` { #use-a-response-parameter } Você pode declarar um parâmetro do tipo `Response` na sua *função de operação de rota*. E então você pode definir cookies nesse objeto de resposta *temporário*. -{* ../../docs_src/response_cookies/tutorial002.py hl[1,8:9] *} +{* ../../docs_src/response_cookies/tutorial002.py hl[1, 8:9] *} Em seguida, você pode retornar qualquer objeto que precise, como normalmente faria (um `dict`, um modelo de banco de dados, etc). @@ -16,11 +16,11 @@ E se você declarou um `response_model`, ele ainda será usado para filtrar e co Você também pode declarar o parâmetro `Response` em dependências e definir cookies (e cabeçalhos) nelas. -## Retornando uma `Response` diretamente +## Retorne uma `Response` diretamente { #return-a-response-directly } Você também pode criar cookies ao retornar uma `Response` diretamente no seu código. -Para fazer isso, você pode criar uma resposta como descrito em [Retornando uma Resposta Diretamente](response-directly.md){.internal-link target=_blank}. +Para fazer isso, você pode criar uma resposta como descrito em [Retorne uma Resposta Diretamente](response-directly.md){.internal-link target=_blank}. Então, defina os cookies nela e a retorne: @@ -36,7 +36,7 @@ E também que você não esteja enviando nenhum dado que deveria ter sido filtra /// -### Mais informações +### Mais informações { #more-info } /// note | Detalhes Técnicos diff --git a/docs/pt/docs/advanced/response-directly.md b/docs/pt/docs/advanced/response-directly.md index ea717be00..3bda46a04 100644 --- a/docs/pt/docs/advanced/response-directly.md +++ b/docs/pt/docs/advanced/response-directly.md @@ -1,4 +1,4 @@ -# Retornando uma Resposta Diretamente +# Retornando uma Resposta Diretamente { #return-a-response-directly } Quando você cria uma *operação de rota* no **FastAPI** você pode retornar qualquer dado nela: um dicionário (`dict`), uma lista (`list`), um modelo do Pydantic ou do seu banco de dados, etc. @@ -10,7 +10,7 @@ Mas você pode retornar a `JSONResponse` diretamente nas suas *operações de ro Pode ser útil para retornar cabeçalhos e cookies personalizados, por exemplo. -## Retornando uma `Response` +## Retornando uma `Response` { #return-a-response } Na verdade, você pode retornar qualquer `Response` ou subclasse dela. @@ -26,7 +26,7 @@ Ele não vai fazer conversões de dados com modelos do Pydantic, não irá conve Isso te dá bastante flexibilidade. Você pode retornar qualquer tipo de dado, sobrescrever qualquer declaração e validação nos dados, etc. -## Utilizando o `jsonable_encoder` em uma `Response` +## Utilizando o `jsonable_encoder` em uma `Response` { #using-the-jsonable-encoder-in-a-response } Como o **FastAPI** não realiza nenhuma mudança na `Response` que você retorna, você precisa garantir que o conteúdo dela está pronto para uso. @@ -44,23 +44,22 @@ Você também pode utilizar `from starlette.responses import JSONResponse`. /// -## Retornando uma `Response` +## Retornando uma `Response` personalizada { #returning-a-custom-response } O exemplo acima mostra todas as partes que você precisa, mas ainda não é muito útil, já que você poderia ter retornado o `item` diretamente, e o **FastAPI** colocaria em uma `JSONResponse` para você, convertendo em um `dict`, etc. Tudo isso por padrão. Agora, vamos ver como você pode usar isso para retornar uma resposta personalizada. -Vamos dizer quer retornar uma resposta XML. +Vamos dizer que você quer retornar uma resposta XML. Você pode colocar o seu conteúdo XML em uma string, colocar em uma `Response`, e retorná-lo: {* ../../docs_src/response_directly/tutorial002.py hl[1,18] *} -## Notas +## Notas { #notes } Quando você retorna uma `Response` diretamente os dados não são validados, convertidos (serializados) ou documentados automaticamente. -Mas você ainda pode documentar como descrito em [Retornos Adicionais no OpenAPI -](additional-responses.md){.internal-link target=_blank}. +Mas você ainda pode documentar como descrito em [Retornos Adicionais no OpenAPI](additional-responses.md){.internal-link target=_blank}. Você pode ver nas próximas seções como usar/declarar essas `Responses` customizadas enquanto mantém a conversão e documentação automática dos dados. diff --git a/docs/pt/docs/advanced/response-headers.md b/docs/pt/docs/advanced/response-headers.md index a1fc84cc0..1add453f4 100644 --- a/docs/pt/docs/advanced/response-headers.md +++ b/docs/pt/docs/advanced/response-headers.md @@ -1,12 +1,12 @@ -# Cabeçalhos de resposta +# Cabeçalhos de resposta { #response-headers } -## Usando um parâmetro `Response` +## Use um parâmetro `Response` { #use-a-response-parameter } Você pode declarar um parâmetro do tipo `Response` na sua *função de operação de rota* (assim como você pode fazer para cookies). Então você pode definir os cabeçalhos nesse objeto de resposta *temporário*. -{* ../../docs_src/response_headers/tutorial002.py hl[1,7:8] *} +{* ../../docs_src/response_headers/tutorial002.py hl[1, 7:8] *} Em seguida você pode retornar qualquer objeto que precisar, da maneira que faria normalmente (um `dict`, um modelo de banco de dados, etc.). @@ -16,7 +16,7 @@ Se você declarou um `response_model`, ele ainda será utilizado para filtrar e Você também pode declarar o parâmetro `Response` em dependências e definir cabeçalhos (e cookies) nelas. -## Retornar uma `Response` diretamente +## Retorne uma `Response` diretamente { #return-a-response-directly } Você também pode adicionar cabeçalhos quando retornar uma `Response` diretamente. @@ -24,7 +24,7 @@ Crie uma resposta conforme descrito em [Retornar uma resposta diretamente](respo {* ../../docs_src/response_headers/tutorial001.py hl[10:12] *} -/// note | Detalhes técnicos +/// note | Detalhes Técnicos Você também pode usar `from starlette.responses import Response` ou `from starlette.responses import JSONResponse`. @@ -34,8 +34,8 @@ E como a `Response` pode ser usada frequentemente para definir cabeçalhos e coo /// -## Cabeçalhos personalizados +## Cabeçalhos personalizados { #custom-headers } -Tenha em mente que cabeçalhos personalizados proprietários podem ser adicionados usando o prefixo 'X-'. +Tenha em mente que cabeçalhos personalizados proprietários podem ser adicionados usando o prefixo `X-`. Porém, se voce tiver cabeçalhos personalizados que deseja que um cliente no navegador possa ver, você precisa adicioná-los às suas configurações de CORS (saiba mais em [CORS (Cross-Origin Resource Sharing)](../tutorial/cors.md){.internal-link target=_blank}), usando o parâmetro `expose_headers` descrito na documentação de CORS do Starlette. diff --git a/docs/pt/docs/advanced/security/http-basic-auth.md b/docs/pt/docs/advanced/security/http-basic-auth.md index 331513927..bd572217b 100644 --- a/docs/pt/docs/advanced/security/http-basic-auth.md +++ b/docs/pt/docs/advanced/security/http-basic-auth.md @@ -1,10 +1,10 @@ -# HTTP Basic Auth +# HTTP Basic Auth { #http-basic-auth } Para os casos mais simples, você pode utilizar o HTTP Basic Auth. No HTTP Basic Auth, a aplicação espera um cabeçalho que contém um usuário e uma senha. -Caso ela não receba, ela retorna um erro HTTP 401 "Unauthorized" (*Não Autorizado*). +Caso ela não receba, ela retorna um erro HTTP 401 "Unauthorized". E retorna um cabeçalho `WWW-Authenticate` com o valor `Basic`, e um parâmetro opcional `realm`. @@ -12,7 +12,7 @@ Isso sinaliza ao navegador para mostrar o prompt integrado para um usuário e se Então, quando você digitar o usuário e senha, o navegador os envia automaticamente no cabeçalho. -## HTTP Basic Auth Simples +## HTTP Basic Auth Simples { #simple-http-basic-auth } * Importe `HTTPBasic` e `HTTPBasicCredentials`. * Crie um "esquema `security`" utilizando `HTTPBasic`. @@ -22,11 +22,11 @@ Então, quando você digitar o usuário e senha, o navegador os envia automatica {* ../../docs_src/security/tutorial006_an_py39.py hl[4,8,12] *} -Quando você tentar abrir a URL pela primeira vez (ou clicar no botão "Executar" nos documentos) o navegador vai pedir pelo seu usuário e senha: +Quando você tentar abrir a URL pela primeira vez (ou clicar no botão "Executar" na documentação) o navegador vai pedir pelo seu usuário e senha: -## Verifique o usuário +## Verifique o usuário { #check-the-username } Aqui está um exemplo mais completo. @@ -52,7 +52,7 @@ if not (credentials.username == "stanleyjobson") or not (credentials.password == Porém, ao utilizar o `secrets.compare_digest()`, isso estará seguro contra um tipo de ataque chamado "timing attacks" (ataques de temporização). -### Ataques de Temporização +### Ataques de Temporização { #timing-attacks } Mas o que é um "timing attack" (ataque de temporização)? @@ -80,19 +80,19 @@ if "stanleyjobsox" == "stanleyjobson" and "love123" == "swordfish": O Python terá que comparar todo o `stanleyjobso` tanto em `stanleyjobsox` como em `stanleyjobson` antes de perceber que as strings não são a mesma. Então isso levará alguns microssegundos a mais para retornar "Usuário ou senha incorretos". -#### O tempo para responder ajuda os invasores +#### O tempo para responder ajuda os invasores { #the-time-to-answer-helps-the-attackers } Neste ponto, ao perceber que o servidor demorou alguns microssegundos a mais para enviar o retorno "Usuário ou senha incorretos", os invasores irão saber que eles acertaram _alguma coisa_, algumas das letras iniciais estavam certas. E eles podem tentar de novo sabendo que provavelmente é algo mais parecido com `stanleyjobsox` do que com `johndoe`. -#### Um ataque "profissional" +#### Um ataque "profissional" { #a-professional-attack } Claro, os invasores não tentariam tudo isso de forma manual, eles escreveriam um programa para fazer isso, possivelmente com milhares ou milhões de testes por segundo. E obteriam apenas uma letra a mais por vez. Mas fazendo isso, em alguns minutos ou horas os invasores teriam adivinhado o usuário e senha corretos, com a "ajuda" da nossa aplicação, apenas usando o tempo levado para responder. -#### Corrija com o `secrets.compare_digest()` +#### Corrija com o `secrets.compare_digest()` { #fix-it-with-secrets-compare-digest } Mas em nosso código já estamos utilizando o `secrets.compare_digest()`. @@ -100,8 +100,7 @@ Resumindo, levará o mesmo tempo para comparar `stanleyjobsox` com `stanleyjobso Deste modo, ao utilizar `secrets.compare_digest()` no código de sua aplicação, ela estará a salvo contra toda essa gama de ataques de segurança. - -### Retorne o erro +### Retorne o erro { #return-the-error } Após detectar que as credenciais estão incorretas, retorne um `HTTPException` com o status 401 (o mesmo retornado quando nenhuma credencial foi informada) e adicione o cabeçalho `WWW-Authenticate` para fazer com que o navegador mostre o prompt de login novamente: diff --git a/docs/pt/docs/advanced/security/index.md b/docs/pt/docs/advanced/security/index.md index 6c7becb67..70fb999d0 100644 --- a/docs/pt/docs/advanced/security/index.md +++ b/docs/pt/docs/advanced/security/index.md @@ -1,6 +1,6 @@ -# Segurança Avançada +# Segurança Avançada { #advanced-security } -## Funcionalidades Adicionais +## Funcionalidades Adicionais { #additional-features } Existem algumas funcionalidades adicionais para lidar com segurança além das cobertas em [Tutorial - Guia de Usuário: Segurança](../../tutorial/security/index.md){.internal-link target=_blank}. @@ -12,7 +12,7 @@ E é possível que para o seu caso de uso, a solução está em uma delas. /// -## Leia o Tutorial primeiro +## Leia o Tutorial primeiro { #read-the-tutorial-first } As próximas seções pressupõem que você já leu o principal [Tutorial - Guia de Usuário: Segurança](../../tutorial/security/index.md){.internal-link target=_blank}. diff --git a/docs/pt/docs/advanced/security/oauth2-scopes.md b/docs/pt/docs/advanced/security/oauth2-scopes.md index 07b345945..591ac9b4a 100644 --- a/docs/pt/docs/advanced/security/oauth2-scopes.md +++ b/docs/pt/docs/advanced/security/oauth2-scopes.md @@ -1,4 +1,4 @@ -# Escopos OAuth2 +# Escopos OAuth2 { #oauth2-scopes } Você pode utilizar escopos do OAuth2 diretamente com o **FastAPI**, eles são integrados para funcionar perfeitamente. @@ -10,7 +10,7 @@ Toda vez que você "se autentica com" Facebook, Google, GitHub, Microsoft, X (Tw Nesta seção, você verá como gerenciar a autenticação e autorização com os mesmos escopos do OAuth2 em sua aplicação **FastAPI**. -/// warning | Aviso +/// warning | Atenção Isso é uma seção mais ou menos avançada. Se você está apenas começando, você pode pular. @@ -18,7 +18,7 @@ Você não necessariamente precisa de escopos do OAuth2, e você pode lidar com Mas o OAuth2 com escopos pode ser integrado de maneira fácil em sua API (com OpenAPI) e a sua documentação de API. -No entando, você ainda aplica estes escopos, ou qualquer outro requisito de segurança/autorização, conforme necessário, em seu código. +No entanto, você ainda aplica estes escopos, ou qualquer outro requisito de segurança/autorização, conforme necessário, em seu código. Em muitos casos, OAuth2 com escopos pode ser um exagero. @@ -26,7 +26,7 @@ Mas se você sabe que precisa, ou está curioso, continue lendo. /// -## Escopos OAuth2 e OpenAPI +## Escopos OAuth2 e OpenAPI { #oauth2-scopes-and-openapi } A especificação OAuth2 define "escopos" como uma lista de strings separadas por espaços. @@ -58,15 +58,15 @@ Para o OAuth2, eles são apenas strings. /// -## Visão global +## Visão global { #global-view } Primeiro, vamos olhar rapidamente as partes que mudam dos exemplos do **Tutorial - Guia de Usuário** para [OAuth2 com Senha (e hash), Bearer com tokens JWT](../../tutorial/security/oauth2-jwt.md){.internal-link target=_blank}. Agora utilizando escopos OAuth2: -{* ../../docs_src/security/tutorial005_an_py310.py hl[5,9,13,47,65,106,108:116,122:125,129:135,140,156] *} +{* ../../docs_src/security/tutorial005_an_py310.py hl[5,9,13,47,65,106,108:116,122:126,130:136,141,157] *} Agora vamos revisar essas mudanças passo a passo. -## Esquema de segurança OAuth2 +## Esquema de segurança OAuth2 { #oauth2-security-scheme } A primeira mudança é que agora nós estamos declarando o esquema de segurança OAuth2 com dois escopos disponíveis, `me` e `items`. @@ -82,9 +82,9 @@ Este é o mesmo mecanismo utilizado quando você adiciona permissões enquanto s -## Token JWT com escopos +## Token JWT com escopos { #jwt-token-with-scopes } -Agora, modifique o *caminho de rota* para retornar os escopos solicitados. +Agora, modifique a *operação de rota* do token para retornar os escopos solicitados. Nós ainda estamos utilizando o mesmo `OAuth2PasswordRequestForm`. Ele inclui a propriedade `scopes` com uma `list` de `str`, com cada escopo que ele recebeu na requisição. @@ -98,15 +98,15 @@ Porém em sua aplicação, por segurança, você deve garantir que você apenas /// -{* ../../docs_src/security/tutorial005_an_py310.py hl[156] *} +{* ../../docs_src/security/tutorial005_an_py310.py hl[157] *} -## Declare escopos em *operações de rota* e dependências +## Declare escopos em *operações de rota* e dependências { #declare-scopes-in-path-operations-and-dependencies } Agora nós declaramos que a *operação de rota* para `/users/me/items/` exige o escopo `items`. Para isso, nós importamos e utilizamos `Security` de `fastapi`. -Você pode utilizar `Security` para declarar dependências (assim como `Depends`), porém o `Security` também recebe o parâmetros `scopes` com uma lista de escopos (strings). +Você pode utilizar `Security` para declarar dependências (assim como `Depends`), porém o `Security` também recebe o parâmetro `scopes` com uma lista de escopos (strings). Neste caso, nós passamos a função `get_current_active_user` como dependência para `Security` (da mesma forma que nós faríamos com `Depends`). @@ -124,9 +124,9 @@ Nós estamos fazendo isso aqui para demonstrar como o **FastAPI** lida com escop /// -{* ../../docs_src/security/tutorial005_an_py310.py hl[5,140,171] *} +{* ../../docs_src/security/tutorial005_an_py310.py hl[5,141,172] *} -/// info | Informações Técnicas +/// info | Detalhes Técnicos `Security` é na verdade uma subclasse de `Depends`, e ele possui apenas um parâmetro extra que veremos depois. @@ -136,7 +136,7 @@ Mas quando você importa `Query`, `Path`, `Depends`, `Security` entre outros de /// -## Utilize `SecurityScopes` +## Utilize `SecurityScopes` { #use-securityscopes } Agora atualize a dependência `get_current_user`. @@ -152,7 +152,7 @@ A classe `SecurityScopes` é semelhante à classe `Request` (`Request` foi utili {* ../../docs_src/security/tutorial005_an_py310.py hl[9,106] *} -## Utilize os `scopes` +## Utilize os `scopes` { #use-the-scopes } O parâmetro `security_scopes` será do tipo `SecurityScopes`. @@ -166,7 +166,7 @@ Nesta exceção, nós incluímos os escopos necessários (se houver algum) como {* ../../docs_src/security/tutorial005_an_py310.py hl[106,108:116] *} -## Verifique o `username` e o formato dos dados +## Verifique o `username` e o formato dos dados { #verify-the-username-and-data-shape } Nós verificamos que nós obtemos um `username`, e extraímos os escopos. @@ -180,17 +180,17 @@ No lugar de, por exemplo, um `dict`, ou alguma outra coisa, que poderia quebrar Nós também verificamos que nós temos um usuário com o "*username*", e caso contrário, nós levantamos a mesma exceção que criamos anteriormente. -{* ../../docs_src/security/tutorial005_an_py310.py hl[47,117:128] *} +{* ../../docs_src/security/tutorial005_an_py310.py hl[47,117:129] *} -## Verifique os `scopes` +## Verifique os `scopes` { #verify-the-scopes } Nós verificamos agora que todos os escopos necessários, por essa dependência e todos os dependentes (incluindo as *operações de rota*) estão incluídas nos escopos fornecidos pelo token recebido, caso contrário, levantamos uma `HTTPException`. Para isso, nós utilizamos `security_scopes.scopes`, que contém uma `list` com todos esses escopos como uma `str`. -{* ../../docs_src/security/tutorial005_an_py310.py hl[129:135] *} +{* ../../docs_src/security/tutorial005_an_py310.py hl[130:136] *} -## Árvore de dependência e escopos +## Árvore de dependência e escopos { #dependency-tree-and-scopes } Vamos rever novamente essa árvore de dependência e os escopos. @@ -223,7 +223,7 @@ Tudo depende dos `scopes` declarados em cada *operação de rota* e cada depend /// -## Mais detalhes sobre `SecurityScopes` +## Mais detalhes sobre `SecurityScopes` { #more-details-about-securityscopes } Você pode utilizar `SecurityScopes` em qualquer lugar, e em diversos lugares. Ele não precisa estar na dependência "raiz". @@ -233,9 +233,9 @@ Porque o `SecurityScopes` terá todos os escopos declarados por dependentes, voc Todos eles serão validados independentemente para cada *operação de rota*. -## Verifique +## Verifique { #check-it } -Se você abrir os documentos da API, você pode antenticar e especificar quais escopos você quer autorizar. +Se você abrir os documentos da API, você pode autenticar e especificar quais escopos você quer autorizar. @@ -245,9 +245,9 @@ E se você selecionar o escopo `me`, mas não o escopo `items`, você poderá ac Isso é o que aconteceria se uma aplicação terceira que tentou acessar uma dessas *operações de rota* com um token fornecido por um usuário, dependendo de quantas permissões o usuário forneceu para a aplicação. -## Sobre integrações de terceiros +## Sobre integrações de terceiros { #about-third-party-integrations } -Neste exemplos nós estamos utilizando o fluxo de senha do OAuth2. +Neste exemplo nós estamos utilizando o fluxo de senha do OAuth2. Isso é apropriado quando nós estamos autenticando em nossa própria aplicação, provavelmente com o nosso próprio "*frontend*". @@ -269,6 +269,6 @@ Mas no final, eles estão implementando o mesmo padrão OAuth2. O **FastAPI** inclui utilitários para todos esses fluxos de autenticação OAuth2 em `fastapi.security.oauth2`. -## `Security` em docoradores de `dependências` +## `Security` em decoradores de `dependencies` { #security-in-decorator-dependencies } -Da mesma forma que você pode definir uma `list` de `Depends` no parâmetro de `dependencias` do decorador (como explicado em [Dependências em decoradores de operações de rota](../../tutorial/dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}), você também pode utilizar `Security` com escopos lá. +Da mesma forma que você pode definir uma `list` de `Depends` no parâmetro `dependencies` do decorador (como explicado em [Dependências em decoradores de operações de rota](../../tutorial/dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}), você também pode utilizar `Security` com escopos lá. diff --git a/docs/pt/docs/advanced/settings.md b/docs/pt/docs/advanced/settings.md index cdc6400ad..81fc9082c 100644 --- a/docs/pt/docs/advanced/settings.md +++ b/docs/pt/docs/advanced/settings.md @@ -1,148 +1,30 @@ -# Configurações e Variáveis de Ambiente +# Configurações e Variáveis de Ambiente { #settings-and-environment-variables } -Em muitos casos a sua aplicação pode precisar de configurações externas, como chaves secretas, credenciais de banco de dados, credenciais para serviços de email, etc. +Em muitos casos, sua aplicação pode precisar de configurações externas, por exemplo chaves secretas, credenciais de banco de dados, credenciais para serviços de e-mail, etc. -A maioria dessas configurações é variável (podem mudar), como URLs de bancos de dados. E muitas delas podem conter dados sensíveis, como tokens secretos. +A maioria dessas configurações é variável (pode mudar), como URLs de banco de dados. E muitas podem ser sensíveis, como segredos. -Por isso é comum prover essas configurações como variáveis de ambiente que são utilizidas pela aplicação. - -## Variáveis de Ambiente +Por esse motivo, é comum fornecê-las em variáveis de ambiente lidas pela aplicação. /// tip | Dica -Se você já sabe o que são variáveis de ambiente e como utilizá-las, sinta-se livre para avançar para o próximo tópico. +Para entender variáveis de ambiente, você pode ler [Variáveis de Ambiente](../environment-variables.md){.internal-link target=_blank}. /// -Uma variável de ambiente (abreviada em inglês para "env var") é uma variável definida fora do código Python, no sistema operacional, e pode ser lida pelo seu código Python (ou por outros programas). +## Tipagem e validação { #types-and-validation } -Você pode criar e utilizar variáveis de ambiente no terminal, sem precisar utilizar Python: +Essas variáveis de ambiente só conseguem lidar com strings de texto, pois são externas ao Python e precisam ser compatíveis com outros programas e com o resto do sistema (e até com diferentes sistemas operacionais, como Linux, Windows, macOS). -//// tab | Linux, macOS, Windows Bash +Isso significa que qualquer valor lido em Python a partir de uma variável de ambiente será uma `str`, e qualquer conversão para um tipo diferente ou validação precisa ser feita em código. -
+## Pydantic `Settings` { #pydantic-settings } -```console -// Você pode criar uma env var MY_NAME usando -$ export MY_NAME="Wade Wilson" +Felizmente, o Pydantic fornece uma ótima utilidade para lidar com essas configurações vindas de variáveis de ambiente com Pydantic: Settings management. -// E utilizá-la em outros programas, como -$ echo "Hello $MY_NAME" +### Instalar `pydantic-settings` { #install-pydantic-settings } -Hello Wade Wilson -``` - -
- -//// - -//// tab | Windows PowerShell - -
- -```console -// Criando env var MY_NAME -$ $Env:MY_NAME = "Wade Wilson" - -// Usando em outros programas, como -$ echo "Hello $Env:MY_NAME" - -Hello Wade Wilson -``` - -
- -//// - -### Lendo variáveis de ambiente com Python - -Você também pode criar variáveis de ambiente fora do Python, no terminal (ou com qualquer outro método), e realizar a leitura delas no Python. - -Por exemplo, você pode definir um arquivo `main.py` com o seguinte código: - -```Python hl_lines="3" -import os - -name = os.getenv("MY_NAME", "World") -print(f"Hello {name} from Python") -``` - -/// tip | Dica - -O segundo parâmetro em `os.getenv()` é o valor padrão para o retorno. - -Se nenhum valor for informado, `None` é utilizado por padrão, aqui definimos `"World"` como o valor padrão a ser utilizado. - -/// - -E depois você pode executar esse arquivo: - -
- -```console -// Aqui ainda não definimos a env var -$ python main.py - -// Por isso obtemos o valor padrão - -Hello World from Python - -// Mas se definirmos uma variável de ambiente primeiro -$ export MY_NAME="Wade Wilson" - -// E executarmos o programa novamente -$ python main.py - -// Agora ele pode ler a variável de ambiente - -Hello Wade Wilson from Python -``` - -
- -Como variáveis de ambiente podem ser definidas fora do código da aplicação, mas acessadas pela aplicação, e não precisam ser armazenadas (versionadas com `git`) junto dos outros arquivos, é comum utilizá-las para guardar configurações. - -Você também pode criar uma variável de ambiente específica para uma invocação de um programa, que é acessível somente para esse programa, e somente enquanto ele estiver executando. - -Para fazer isso, crie a variável imediatamente antes de iniciar o programa, na mesma linha: - -
- -```console -// Criando uma env var MY_NAME na mesma linha da execução do programa -$ MY_NAME="Wade Wilson" python main.py - -// Agora a aplicação consegue ler a variável de ambiente - -Hello Wade Wilson from Python - -// E a variável deixa de existir após isso -$ python main.py - -Hello World from Python -``` - -
- -/// tip | Dica - -Você pode ler mais sobre isso em: The Twelve-Factor App: Configurações. - -/// - -### Tipagem e Validação - -Essas variáveis de ambiente suportam apenas strings, por serem externas ao Python e por que precisam ser compatíveis com outros programas e o resto do sistema (e até mesmo com outros sistemas operacionais, como Linux, Windows e macOS). - -Isso significa que qualquer valor obtido de uma variável de ambiente em Python terá o tipo `str`, e qualquer conversão para um tipo diferente ou validação deve ser realizada no código. - -## Pydantic `Settings` - -Por sorte, o Pydantic possui uma funcionalidade para lidar com essas configurações vindas de variáveis de ambiente utilizando Pydantic: Settings management. - -### Instalando `pydantic-settings` - -Primeiro, instale o pacote `pydantic-settings`: +Primeiro, certifique-se de criar seu [ambiente virtual](../virtual-environments.md){.internal-link target=_blank}, ativá-lo e então instalar o pacote `pydantic-settings`:
@@ -153,7 +35,7 @@ $ pip install pydantic-settings
-Ele também está incluído no fastapi quando você instala com a opção `all`: +Ele também vem incluído quando você instala os extras `all` com:
@@ -164,19 +46,19 @@ $ pip install "fastapi[all]"
-/// info +/// info | Informação -Na v1 do Pydantic ele estava incluído no pacote principal. Agora ele está distribuido como um pacote independente para que você possa optar por instalar ou não caso você não precise dessa funcionalidade. +No Pydantic v1 ele vinha incluído no pacote principal. Agora é distribuído como um pacote independente para que você possa optar por instalá-lo ou não, caso não precise dessa funcionalidade. /// -### Criando o objeto `Settings` +### Criar o objeto `Settings` { #create-the-settings-object } -Importe a classe `BaseSettings` do Pydantic e crie uma nova subclasse, de forma parecida com um modelo do Pydantic. +Importe `BaseSettings` do Pydantic e crie uma subclasse, muito parecido com um modelo do Pydantic. -Os atributos da classe são declarados com anotações de tipo, e possíveis valores padrão, da mesma maneira que os modelos do Pydantic. +Da mesma forma que com modelos do Pydantic, você declara atributos de classe com anotações de tipo e, possivelmente, valores padrão. -Você pode utilizar todas as ferramentas e funcionalidades de validação que são utilizadas nos modelos do Pydantic, como tipos de dados diferentes e validações adicionei com `Field()`. +Você pode usar as mesmas funcionalidades e ferramentas de validação que usa em modelos do Pydantic, como diferentes tipos de dados e validações adicionais com `Field()`. //// tab | Pydantic v2 @@ -186,9 +68,9 @@ Você pode utilizar todas as ferramentas e funcionalidades de validação que s //// tab | Pydantic v1 -/// info +/// info | Informação -Na versão 1 do Pydantic você importaria `BaseSettings` diretamente do módulo `pydantic` em vez do módulo `pydantic_settings`. +No Pydantic v1 você importaria `BaseSettings` diretamente de `pydantic` em vez de `pydantic_settings`. /// @@ -198,23 +80,23 @@ Na versão 1 do Pydantic você importaria `BaseSettings` diretamente do módulo /// tip | Dica -Se você quiser algo pronto para copiar e colar na sua aplicação, não use esse exemplo, mas sim o exemplo abaixo. +Se você quer algo rápido para copiar e colar, não use este exemplo, use o último abaixo. /// -Portanto, quando você cria uma instância da classe `Settings` (nesse caso, o objeto `settings`), o Pydantic lê as variáveis de ambiente sem diferenciar maiúsculas e minúsculas, por isso, uma variável maiúscula `APP_NAME` será usada para o atributo `app_name`. +Então, quando você cria uma instância dessa classe `Settings` (neste caso, no objeto `settings`), o Pydantic vai ler as variáveis de ambiente sem diferenciar maiúsculas de minúsculas; assim, uma variável em maiúsculas `APP_NAME` ainda será lida para o atributo `app_name`. -Depois ele irá converter e validar os dados. Assim, quando você utilizar aquele objeto `settings`, os dados terão o tipo que você declarou (e.g. `items_per_user` será do tipo `int`). +Em seguida, ele converterá e validará os dados. Assim, quando você usar esse objeto `settings`, terá dados dos tipos que declarou (por exemplo, `items_per_user` será um `int`). -### Usando o objeto `settings` +### Usar o `settings` { #use-the-settings } -Depois, Você pode utilizar o novo objeto `settings` na sua aplicação: +Depois você pode usar o novo objeto `settings` na sua aplicação: {* ../../docs_src/settings/tutorial001.py hl[18:20] *} -### Executando o servidor +### Executar o servidor { #run-the-server } -No próximo passo, você pode inicializar o servidor passando as configurações em forma de variáveis de ambiente, por exemplo, você poderia definir `ADMIN_EMAIL` e `APP_NAME` da seguinte forma: +Em seguida, você executaria o servidor passando as configurações como variáveis de ambiente, por exemplo, você poderia definir `ADMIN_EMAIL` e `APP_NAME` com:
@@ -228,110 +110,110 @@ $ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" fastapi run main.p /// tip | Dica -Para definir múltiplas variáveis de ambiente para um único comando basta separá-las utilizando espaços, e incluir todas elas antes do comando. +Para definir várias variáveis de ambiente para um único comando, basta separá-las com espaço e colocá-las todas antes do comando. /// -Assim, o atributo `admin_email` seria definido como `"deadpool@example.com"`. +Então a configuração `admin_email` seria definida como `"deadpool@example.com"`. -`app_name` seria `"ChimichangApp"`. +O `app_name` seria `"ChimichangApp"`. -E `items_per_user` manteria o valor padrão de `50`. +E `items_per_user` manteria seu valor padrão de `50`. -## Configurações em um módulo separado +## Configurações em outro módulo { #settings-in-another-module } -Você também pode incluir essas configurações em um arquivo de um módulo separado como visto em [Bigger Applications - Multiple Files](../tutorial/bigger-applications.md){.internal-link target=\_blank}. +Você pode colocar essas configurações em outro arquivo de módulo como visto em [Aplicações Maiores - Múltiplos Arquivos](../tutorial/bigger-applications.md){.internal-link target=_blank}. -Por exemplo, você pode adicionar um arquivo `config.py` com: +Por exemplo, você poderia ter um arquivo `config.py` com: {* ../../docs_src/settings/app01/config.py *} -E utilizar essa configuração em `main.py`: +E então usá-lo em um arquivo `main.py`: {* ../../docs_src/settings/app01/main.py hl[3,11:13] *} /// tip | Dica -Você também precisa incluir um arquivo `__init__.py` como visto em [Bigger Applications - Multiple Files](../tutorial/bigger-applications.md){.internal-link target=\_blank}. +Você também precisaria de um arquivo `__init__.py` como visto em [Aplicações Maiores - Múltiplos Arquivos](../tutorial/bigger-applications.md){.internal-link target=_blank}. /// -## Configurações em uma dependência +## Configurações em uma dependência { #settings-in-a-dependency } -Em certas ocasiões, pode ser útil fornecer essas configurações a partir de uma dependência, em vez de definir um objeto global `settings` que é utilizado em toda a aplicação. +Em algumas ocasiões, pode ser útil fornecer as configurações a partir de uma dependência, em vez de ter um objeto global `settings` usado em todos os lugares. -Isso é especialmente útil durante os testes, já que é bastante simples sobrescrever uma dependência com suas configurações personalizadas. +Isso pode ser especialmente útil durante os testes, pois é muito fácil sobrescrever uma dependência com suas próprias configurações personalizadas. -### O arquivo de configuração +### O arquivo de configuração { #the-config-file } -Baseando-se no exemplo anterior, seu arquivo `config.py` seria parecido com isso: +Vindo do exemplo anterior, seu arquivo `config.py` poderia ser assim: {* ../../docs_src/settings/app02/config.py hl[10] *} -Perceba que dessa vez não criamos uma instância padrão `settings = Settings()`. +Perceba que agora não criamos uma instância padrão `settings = Settings()`. -### O arquivo principal da aplicação +### O arquivo principal da aplicação { #the-main-app-file } -Agora criamos a dependência que retorna um novo objeto `config.Settings()`. +Agora criamos uma dependência que retorna um novo `config.Settings()`. {* ../../docs_src/settings/app02_an_py39/main.py hl[6,12:13] *} /// tip | Dica -Vamos discutir sobre `@lru_cache` logo mais. +Vamos discutir o `@lru_cache` em breve. -Por enquanto, você pode considerar `get_settings()` como uma função normal. +Por enquanto, você pode assumir que `get_settings()` é uma função normal. /// -E então podemos declarar essas configurações como uma dependência na função de operação da rota e utilizar onde for necessário. +E então podemos exigi-la na *função de operação de rota* como dependência e usá-la onde for necessário. {* ../../docs_src/settings/app02_an_py39/main.py hl[17,19:21] *} -### Configurações e testes +### Configurações e testes { #settings-and-testing } -Então seria muito fácil fornecer uma configuração diferente durante a execução dos testes sobrescrevendo a dependência de `get_settings`: +Então seria muito fácil fornecer um objeto de configurações diferente durante os testes criando uma sobrescrita de dependência para `get_settings`: {* ../../docs_src/settings/app02/test_main.py hl[9:10,13,21] *} -Na sobrescrita da dependência, definimos um novo valor para `admin_email` quando instanciamos um novo objeto `Settings`, e então retornamos esse novo objeto. +Na sobrescrita da dependência definimos um novo valor para `admin_email` ao criar o novo objeto `Settings`, e então retornamos esse novo objeto. -Após isso, podemos testar se o valor está sendo utilizado. +Depois podemos testar que ele é usado. -## Lendo um arquivo `.env` +## Lendo um arquivo `.env` { #reading-a-env-file } -Se você tiver muitas configurações que variem bastante, talvez em ambientes distintos, pode ser útil colocá-las em um arquivo e depois lê-las como se fossem variáveis de ambiente. +Se você tiver muitas configurações que possivelmente mudam bastante, talvez em diferentes ambientes, pode ser útil colocá-las em um arquivo e então lê-las como se fossem variáveis de ambiente. -Essa prática é tão comum que possui um nome, essas variáveis de ambiente normalmente são colocadas em um arquivo `.env`, e esse arquivo é chamado de "dotenv". +Essa prática é tão comum que tem um nome: essas variáveis de ambiente são comumente colocadas em um arquivo `.env`, e o arquivo é chamado de "dotenv". /// tip | Dica -Um arquivo iniciando com um ponto final (`.`) é um arquivo oculto em sistemas baseados em Unix, como Linux e MacOS. +Um arquivo começando com um ponto (`.`) é um arquivo oculto em sistemas tipo Unix, como Linux e macOS. -Mas um arquivo dotenv não precisa ter esse nome exato. +Mas um arquivo dotenv não precisa ter exatamente esse nome de arquivo. /// -Pydantic suporta a leitura desses tipos de arquivos utilizando uma biblioteca externa. Você pode ler mais em Pydantic Settings: Dotenv (.env) support. +O Pydantic tem suporte para leitura desses tipos de arquivos usando uma biblioteca externa. Você pode ler mais em Pydantic Settings: Dotenv (.env) support. /// tip | Dica -Para que isso funcione você precisa executar `pip install python-dotenv`. +Para isso funcionar, você precisa executar `pip install python-dotenv`. /// -### O arquivo `.env` +### O arquivo `.env` { #the-env-file } -Você pode definir um arquivo `.env` com o seguinte conteúdo: +Você poderia ter um arquivo `.env` com: ```bash ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" ``` -### Obtendo configurações do `.env` +### Ler configurações do `.env` { #read-settings-from-env } -E então adicionar o seguinte código em `config.py`: +E então atualizar seu `config.py` com: //// tab | Pydantic v2 @@ -339,7 +221,7 @@ E então adicionar o seguinte código em `config.py`: /// tip | Dica -O atributo `model_config` é usado apenas para configuração do Pydantic. Você pode ler mais em Pydantic Model Config. +O atributo `model_config` é usado apenas para configuração do Pydantic. Você pode ler mais em Pydantic: Concepts: Configuration. /// @@ -357,48 +239,48 @@ A classe `Config` é usada apenas para configuração do Pydantic. Você pode le //// -/// info +/// info | Informação -Na versão 1 do Pydantic a configuração é realizada por uma classe interna `Config`, na versão 2 do Pydantic isso é feito com o atributo `model_config`. Esse atributo recebe um `dict`, para utilizar o autocomplete e checagem de erros do seu editor de texto você pode importar e utilizar `SettingsConfigDict` para definir esse `dict`. +Na versão 1 do Pydantic a configuração era feita em uma classe interna `Config`, na versão 2 do Pydantic é feita em um atributo `model_config`. Esse atributo recebe um `dict`, e para ter autocompletar e erros inline você pode importar e usar `SettingsConfigDict` para definir esse `dict`. /// -Aqui definimos a configuração `env_file` dentro da classe `Settings` do Pydantic, e definimos o valor como o nome do arquivo dotenv que queremos utilizar. +Aqui definimos a configuração `env_file` dentro da sua classe `Settings` do Pydantic e definimos o valor como o nome do arquivo dotenv que queremos usar. -### Declarando `Settings` apenas uma vez com `lru_cache` +### Criando o `Settings` apenas uma vez com `lru_cache` { #creating-the-settings-only-once-with-lru-cache } -Ler o conteúdo de um arquivo em disco normalmente é uma operação custosa (lenta), então você provavelmente quer fazer isso apenas um vez e reutilizar o mesmo objeto settings depois, em vez de ler os valores a cada requisição. +Ler um arquivo do disco normalmente é uma operação custosa (lenta), então você provavelmente vai querer fazer isso apenas uma vez e depois reutilizar o mesmo objeto de configurações, em vez de lê-lo a cada requisição. -Mas cada vez que fazemos: +Mas toda vez que fizermos: ```Python Settings() ``` -um novo objeto `Settings` é instanciado, e durante a instanciação, o arquivo `.env` é lido novamente. +um novo objeto `Settings` seria criado e, na criação, ele leria o arquivo `.env` novamente. -Se a função da dependência fosse apenas: +Se a função de dependência fosse assim: ```Python def get_settings(): return Settings() ``` -Iriamos criar um novo objeto a cada requisição, e estaríamos lendo o arquivo `.env` a cada requisição. ⚠️ +criaríamos esse objeto para cada requisição e leríamos o arquivo `.env` para cada requisição. ⚠️ -Mas como estamos utilizando o decorador `@lru_cache` acima, o objeto `Settings` é criado apenas uma vez, na primeira vez que a função é chamada. ✔️ +Mas como estamos usando o decorador `@lru_cache` por cima, o objeto `Settings` será criado apenas uma vez, na primeira vez em que for chamado. ✔️ {* ../../docs_src/settings/app03_an_py39/main.py hl[1,11] *} -Dessa forma, todas as chamadas da função `get_settings()` nas dependências das próximas requisições, em vez de executar o código interno de `get_settings()` e instanciar um novo objeto `Settings`, irão retornar o mesmo objeto que foi retornado na primeira chamada, de novo e de novo. +Em qualquer chamada subsequente de `get_settings()` nas dependências das próximas requisições, em vez de executar o código interno de `get_settings()` e criar um novo objeto `Settings`, ele retornará o mesmo objeto que foi retornado na primeira chamada, repetidamente. -#### Detalhes Técnicos de `lru_cache` +#### Detalhes Técnicos do `lru_cache` { #lru-cache-technical-details } -`@lru_cache` modifica a função decorada para retornar o mesmo valor que foi retornado na primeira vez, em vez de calculá-lo novamente, executando o código da função toda vez. +`@lru_cache` modifica a função que decora para retornar o mesmo valor que foi retornado na primeira vez, em vez de calculá-lo novamente executando o código da função todas as vezes. -Assim, a função abaixo do decorador é executada uma única vez para cada combinação dos argumentos passados. E os valores retornados para cada combinação de argumentos são sempre reutilizados para cada nova chamada da função com a mesma combinação de argumentos. +Assim, a função abaixo dele será executada uma vez para cada combinação de argumentos. E então os valores retornados para cada uma dessas combinações de argumentos serão usados repetidamente sempre que a função for chamada com exatamente a mesma combinação de argumentos. -Por exemplo, se você definir uma função: +Por exemplo, se você tiver uma função: ```Python @lru_cache @@ -406,59 +288,59 @@ def say_hi(name: str, salutation: str = "Ms."): return f"Hello {salutation} {name}" ``` -Seu programa poderia executar dessa forma: +seu programa poderia executar assim: ```mermaid sequenceDiagram -participant code as Código +participant code as Code participant function as say_hi() -participant execute as Executar Função +participant execute as Execute function rect rgba(0, 255, 0, .1) code ->> function: say_hi(name="Camila") - function ->> execute: executar código da função - execute ->> code: retornar o resultado + function ->> execute: execute function code + execute ->> code: return the result end rect rgba(0, 255, 255, .1) code ->> function: say_hi(name="Camila") - function ->> code: retornar resultado armazenado + function ->> code: return stored result end rect rgba(0, 255, 0, .1) code ->> function: say_hi(name="Rick") - function ->> execute: executar código da função - execute ->> code: retornar o resultado + function ->> execute: execute function code + execute ->> code: return the result end rect rgba(0, 255, 0, .1) code ->> function: say_hi(name="Rick", salutation="Mr.") - function ->> execute: executar código da função - execute ->> code: retornar o resultado + function ->> execute: execute function code + execute ->> code: return the result end rect rgba(0, 255, 255, .1) code ->> function: say_hi(name="Rick") - function ->> code: retornar resultado armazenado + function ->> code: return stored result end rect rgba(0, 255, 255, .1) code ->> function: say_hi(name="Camila") - function ->> code: retornar resultado armazenado + function ->> code: return stored result end ``` -No caso da nossa dependência `get_settings()`, a função não recebe nenhum argumento, então ela sempre retorna o mesmo valor. +No caso da nossa dependência `get_settings()`, a função nem recebe argumentos, então ela sempre retorna o mesmo valor. -Dessa forma, ela se comporta praticamente como uma variável global, mas ao ser utilizada como uma função de uma dependência, pode facilmente ser sobrescrita durante os testes. +Dessa forma, ela se comporta quase como se fosse apenas uma variável global. Mas como usa uma função de dependência, podemos sobrescrevê-la facilmente para testes. -`@lru_cache` é definido no módulo `functools` que faz parte da biblioteca padrão do Python, você pode ler mais sobre esse decorador no link Python Docs sobre `@lru_cache`. +`@lru_cache` faz parte de `functools`, que faz parte da biblioteca padrão do Python; você pode ler mais sobre isso na documentação do Python para `@lru_cache`. -## Recapitulando +## Recapitulando { #recap } -Você pode usar o módulo Pydantic Settings para gerenciar as configurações de sua aplicação, utilizando todo o poder dos modelos Pydantic. +Você pode usar Pydantic Settings para lidar com as configurações da sua aplicação, com todo o poder dos modelos Pydantic. -- Utilizar dependências simplifica os testes. -- Você pode utilizar arquivos .env junto das configurações do Pydantic. -- Utilizar o decorador `@lru_cache` evita que o arquivo .env seja lido de novo e de novo para cada requisição, enquanto permite que você sobrescreva durante os testes. +* Usando uma dependência você pode simplificar os testes. +* Você pode usar arquivos `.env` com ele. +* Usar `@lru_cache` permite evitar ler o arquivo dotenv repetidamente a cada requisição, enquanto permite sobrescrevê-lo durante os testes. diff --git a/docs/pt/docs/advanced/sub-applications.md b/docs/pt/docs/advanced/sub-applications.md index efc6bef64..229802905 100644 --- a/docs/pt/docs/advanced/sub-applications.md +++ b/docs/pt/docs/advanced/sub-applications.md @@ -1,41 +1,41 @@ -# Sub Aplicações - Montagens +# Sub Aplicações - Montagens { #sub-applications-mounts } Se você precisar ter duas aplicações FastAPI independentes, cada uma com seu próprio OpenAPI e suas próprias interfaces de documentação, você pode ter um aplicativo principal e "montar" uma (ou mais) sub-aplicações. -## Montando uma aplicação **FastAPI** +## Montando uma aplicação **FastAPI** { #mounting-a-fastapi-application } -"Montar" significa adicionar uma aplicação completamente "independente" em um caminho específico, que então se encarrega de lidar com tudo sob esse caminho, com as operações de rota declaradas nessa sub-aplicação. +"Montar" significa adicionar uma aplicação completamente "independente" em um path específico, que então se encarrega de lidar com tudo sob esse path, com as _operações de rota_ declaradas nessa sub-aplicação. -### Aplicação de nível superior +### Aplicação de nível superior { #top-level-application } Primeiro, crie a aplicação principal, de nível superior, **FastAPI**, e suas *operações de rota*: -{* ../../docs_src/sub_applications/tutorial001.py hl[3,6:8] *} +{* ../../docs_src/sub_applications/tutorial001.py hl[3, 6:8] *} -### Sub-aplicação +### Sub-aplicação { #sub-application } Em seguida, crie sua sub-aplicação e suas *operações de rota*. Essa sub-aplicação é apenas outra aplicação FastAPI padrão, mas esta é a que será "montada": -{* ../../docs_src/sub_applications/tutorial001.py hl[11,14:16] *} +{* ../../docs_src/sub_applications/tutorial001.py hl[11, 14:16] *} -### Monte a sub-aplicação +### Monte a sub-aplicação { #mount-the-sub-application } Na sua aplicação de nível superior, `app`, monte a sub-aplicação, `subapi`. -Neste caso, ela será montada no caminho `/subapi`: +Neste caso, ela será montada no path `/subapi`: -{* ../../docs_src/sub_applications/tutorial001.py hl[11,19] *} +{* ../../docs_src/sub_applications/tutorial001.py hl[11, 19] *} -### Verifique a documentação automática da API +### Verifique a documentação automática da API { #check-the-automatic-api-docs } -Agora, execute `uvicorn` com a aplicação principal, se o seu arquivo for `main.py`, seria: +Agora, execute o comando `fastapi` com o seu arquivo:
```console -$ uvicorn main:app --reload +$ fastapi dev main.py INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ``` @@ -50,17 +50,17 @@ Você verá a documentação automática da API para a aplicação principal, in E então, abra a documentação para a sub-aplicação, em http://127.0.0.1:8000/subapi/docs. -Você verá a documentação automática da API para a sub-aplicação, incluindo apenas suas próprias _operações de rota_, todas sob o prefixo de sub-caminho correto `/subapi`: +Você verá a documentação automática da API para a sub-aplicação, incluindo apenas suas próprias _operações de rota_, todas sob o prefixo de sub-path correto `/subapi`: Se você tentar interagir com qualquer uma das duas interfaces de usuário, elas funcionarão corretamente, porque o navegador será capaz de se comunicar com cada aplicação ou sub-aplicação específica. -### Detalhes Técnicos: `root_path` +### Detalhes Técnicos: `root_path` { #technical-details-root-path } -Quando você monta uma sub-aplicação como descrito acima, o FastAPI se encarrega de comunicar o caminho de montagem para a sub-aplicação usando um mecanismo da especificação ASGI chamado `root_path`. +Quando você monta uma sub-aplicação como descrito acima, o FastAPI se encarrega de comunicar o path de montagem para a sub-aplicação usando um mecanismo da especificação ASGI chamado `root_path`. -Dessa forma, a sub-aplicação saberá usar esse prefixo de caminho para a interface de documentação. +Dessa forma, a sub-aplicação saberá usar esse prefixo de path para a interface de documentação. E a sub-aplicação também poderia ter suas próprias sub-aplicações montadas e tudo funcionaria corretamente, porque o FastAPI lida com todos esses `root_path`s automaticamente. diff --git a/docs/pt/docs/advanced/templates.md b/docs/pt/docs/advanced/templates.md index 65ff89fae..00493d635 100644 --- a/docs/pt/docs/advanced/templates.md +++ b/docs/pt/docs/advanced/templates.md @@ -1,4 +1,4 @@ -# Templates +# Templates { #templates } Você pode usar qualquer template engine com o **FastAPI**. @@ -6,28 +6,30 @@ Uma escolha comum é o Jinja2, o mesmo usado pelo Flask e outras ferramentas. Existem utilitários para configurá-lo facilmente que você pode usar diretamente em sua aplicação **FastAPI** (fornecidos pelo Starlette). -## Instalação de dependências +## Instalar dependências { #install-dependencies } -Para instalar o `jinja2`, siga o código abaixo: +Certifique-se de criar um [ambiente virtual](../virtual-environments.md){.internal-link target=_blank}, ativá-lo e instalar `jinja2`:
```console $ pip install jinja2 + +---> 100% ```
-## Usando `Jinja2Templates` +## Usando `Jinja2Templates` { #using-jinja2templates } * Importe `Jinja2Templates`. -* Crie um `templates` que você possa reutilizar posteriormente. +* Crie um objeto `templates` que você possa reutilizar posteriormente. * Declare um parâmetro `Request` no *path operation* que retornará um template. -* Use o `template` que você criou para renderizar e retornar uma `TemplateResponse`, passe o nome do template, o request object, e um "context" dict com pares chave-valor a serem usados dentro do template do Jinja2. +* Use o `templates` que você criou para renderizar e retornar uma `TemplateResponse`, passe o nome do template, o objeto `request` e um dicionário "context" com pares chave-valor a serem usados dentro do template do Jinja2. {* ../../docs_src/templates/tutorial001.py hl[4,11,15:18] *} -/// note +/// note | Nota Antes do FastAPI 0.108.0, Starlette 0.29.0, `name` era o primeiro parâmetro. @@ -49,7 +51,7 @@ Você também poderia usar `from starlette.templating import Jinja2Templates`. /// -## Escrevendo Templates +## Escrevendo templates { #writing-templates } Então você pode escrever um template em `templates/item.html`, por exemplo: @@ -57,7 +59,7 @@ Então você pode escrever um template em `templates/item.html`, por exemplo: {!../../docs_src/templates/templates/item.html!} ``` -### Interpolação de Valores no Template +### Valores de contexto do template { #template-context-values } No código HTML que contém: @@ -81,7 +83,7 @@ Por exemplo, dado um ID de valor `42`, aparecerá: Item ID: 42 ``` -### Argumentos do `url_for` +### Argumentos do `url_for` no template { #template-url-for-arguments } Você também pode usar `url_for()` dentro do template, ele recebe como argumentos os mesmos argumentos que seriam usados pela sua *path operation function*. @@ -103,9 +105,9 @@ Por exemplo, com um ID de `42`, isso renderizará: ``` -## Templates e Arquivos Estáticos +## Templates e arquivos estáticos { #templates-and-static-files } -Você também pode usar `url_for()` dentro do template e usá-lo, por examplo, com o `StaticFiles` que você montou com o `name="static"`. +Você também pode usar `url_for()` dentro do template e usá-lo, por exemplo, com o `StaticFiles` que você montou com o `name="static"`. ```jinja hl_lines="4" {!../../docs_src/templates/templates/item.html!} @@ -117,8 +119,8 @@ Neste exemplo, ele seria vinculado a um arquivo CSS em `static/styles.css` com: {!../../docs_src/templates/static/styles.css!} ``` -E como você está usando `StaticFiles`, este arquivo CSS será automaticamente servido pela sua aplicação FastAPI na URL `/static/styles.css`. +E como você está usando `StaticFiles`, este arquivo CSS será automaticamente servido pela sua aplicação **FastAPI** na URL `/static/styles.css`. -## Mais detalhes +## Mais detalhes { #more-details } Para obter mais detalhes, incluindo como testar templates, consulte a documentação da Starlette sobre templates. diff --git a/docs/pt/docs/advanced/testing-dependencies.md b/docs/pt/docs/advanced/testing-dependencies.md index 3ede4741d..52b12dddb 100644 --- a/docs/pt/docs/advanced/testing-dependencies.md +++ b/docs/pt/docs/advanced/testing-dependencies.md @@ -1,6 +1,6 @@ -# Testando Dependências com Sobreposição (Overrides) +# Testando Dependências com Sobreposições { #testing-dependencies-with-overrides } -## Sobrepondo dependências durante os testes +## Sobrepondo dependências durante os testes { #overriding-dependencies-during-testing } Existem alguns cenários onde você deseje sobrepor uma dependência durante os testes. @@ -8,7 +8,7 @@ Você não quer que a dependência original execute (e nenhuma das subdependênc Em vez disso, você deseja fornecer uma dependência diferente que será usada somente durante os testes (possivelmente apenas para alguns testes específicos) e fornecerá um valor que pode ser usado onde o valor da dependência original foi usado. -### Casos de uso: serviço externo +### Casos de uso: serviço externo { #use-cases-external-service } Um exemplo pode ser que você possua um provedor de autenticação externo que você precisa chamar. @@ -20,7 +20,7 @@ Você provavelmente quer testar o provedor externo uma vez, mas não necessariam Neste caso, você pode sobrepor (*override*) a dependência que chama o provedor, e utilizar uma dependência customizada que retorna um *mock* do usuário, apenas para os seus testes. -### Utilize o atributo `app.dependency_overrides` +### Utilize o atributo `app.dependency_overrides` { #use-the-app-dependency-overrides-attribute } Para estes casos, a sua aplicação **FastAPI** possui o atributo `app.dependency_overrides`. Ele é um simples `dict`. @@ -34,7 +34,7 @@ E então o **FastAPI** chamará a sobreposição no lugar da dependência origin Você pode definir uma sobreposição de dependência para uma dependência que é utilizada em qualquer lugar da sua aplicação **FastAPI**. -A dependência original pode estar sendo utilizada em uma *função de operação de rota*, um *docorador de operação de rota* (quando você não utiliza o valor retornado), uma chamada ao `.include_router()`, etc. +A dependência original pode estar sendo utilizada em uma *função de operação de rota*, um *decorador de operação de rota* (quando você não utiliza o valor retornado), uma chamada ao `.include_router()`, etc. O FastAPI ainda poderá sobrescrevê-lo. @@ -48,6 +48,6 @@ app.dependency_overrides = {} /// tip | Dica -Se você quer sobrepor uma dependência apenas para alguns testes, você pode definir a sobreposição no início do testes (dentro da função de teste) e reiniciá-la ao final (no final da função de teste). +Se você quer sobrepor uma dependência apenas para alguns testes, você pode definir a sobreposição no início do teste (dentro da função de teste) e reiniciá-la ao final (no final da função de teste). /// diff --git a/docs/pt/docs/advanced/testing-events.md b/docs/pt/docs/advanced/testing-events.md index 6113c9913..fb62ad2e2 100644 --- a/docs/pt/docs/advanced/testing-events.md +++ b/docs/pt/docs/advanced/testing-events.md @@ -1,5 +1,11 @@ -# Testando Eventos: inicialização - encerramento +# Testando eventos: lifespan e inicialização - encerramento { #testing-events-lifespan-and-startup-shutdown } -Quando você precisa que os seus manipuladores de eventos (`startup` e `shutdown`) sejam executados em seus testes, você pode utilizar o `TestClient` usando a instrução `with`: +Quando você precisa que o `lifespan` seja executado em seus testes, você pode utilizar o `TestClient` com a instrução `with`: + +{* ../../docs_src/app_testing/tutorial004.py hl[9:15,18,27:28,30:32,41:43] *} + +Você pode ler mais detalhes sobre o ["Executando lifespan em testes no site oficial da documentação do Starlette."](https://www.starlette.dev/lifespan/#running-lifespan-in-tests) + +Para os eventos `startup` e `shutdown` descontinuados, você pode usar o `TestClient` da seguinte forma: {* ../../docs_src/app_testing/tutorial003.py hl[9:12,20:24] *} diff --git a/docs/pt/docs/advanced/testing-websockets.md b/docs/pt/docs/advanced/testing-websockets.md index 9b8193655..ccd7582c7 100644 --- a/docs/pt/docs/advanced/testing-websockets.md +++ b/docs/pt/docs/advanced/testing-websockets.md @@ -1,4 +1,4 @@ -# Testando WebSockets +# Testando WebSockets { #testing-websockets } Você pode usar o mesmo `TestClient` para testar WebSockets. diff --git a/docs/pt/docs/advanced/using-request-directly.md b/docs/pt/docs/advanced/using-request-directly.md index f4fb0ed8f..3653b4d37 100644 --- a/docs/pt/docs/advanced/using-request-directly.md +++ b/docs/pt/docs/advanced/using-request-directly.md @@ -1,10 +1,10 @@ -# Utilizando o Request diretamente +# Utilizando o Request diretamente { #using-the-request-directly } Até agora você declarou as partes da requisição que você precisa utilizando os seus tipos. Obtendo dados de: -* Os parâmetros das rotas. +* O path como parâmetros. * Cabeçalhos (*Headers*). * Cookies. * etc. @@ -13,7 +13,7 @@ E ao fazer isso, o **FastAPI** está validando as informações, convertendo-as Porém há situações em que você possa precisar acessar o objeto `Request` diretamente. -## Detalhes sobre o objeto `Request` +## Detalhes sobre o objeto `Request` { #details-about-the-request-object } Como o **FastAPI** é na verdade o **Starlette** por baixo, com camadas de diversas funcionalidades por cima, você pode utilizar o objeto `Request` do Starlette diretamente quando precisar. @@ -23,7 +23,7 @@ Embora qualquer outro parâmetro declarado normalmente (o corpo da requisição Mas há situações específicas onde é útil utilizar o objeto `Request`. -## Utilize o objeto `Request` diretamente +## Utilize o objeto `Request` diretamente { #use-the-request-object-directly } Vamos imaginar que você deseja obter o endereço de IP/host do cliente dentro da sua *função de operação de rota*. @@ -35,15 +35,15 @@ Ao declarar o parâmetro com o tipo sendo um `Request` em sua *função de opera /// tip | Dica -Note que neste caso, nós estamos declarando o parâmetro da rota ao lado do parâmetro da requisição. +Note que neste caso, nós estamos declarando o parâmetro de path ao lado do parâmetro da requisição. -Assim, o parâmetro da rota será extraído, validado, convertido para o tipo especificado e anotado com OpenAPI. +Assim, o parâmetro de path será extraído, validado, convertido para o tipo especificado e anotado com OpenAPI. Do mesmo jeito, você pode declarar qualquer outro parâmetro normalmente, e além disso, obter o `Request` também. /// -## Documentação do `Request` +## Documentação do `Request` { #request-documentation } Você pode ler mais sobre os detalhes do objeto `Request` no site da documentação oficial do Starlette.. diff --git a/docs/pt/docs/advanced/websockets.md b/docs/pt/docs/advanced/websockets.md index 721c0b403..c31bb94b6 100644 --- a/docs/pt/docs/advanced/websockets.md +++ b/docs/pt/docs/advanced/websockets.md @@ -1,10 +1,10 @@ -# WebSockets +# WebSockets { #websockets } Você pode usar WebSockets com **FastAPI**. -## Instalando `WebSockets` +## Instale `websockets` { #install-websockets } -Garanta que você criou um [ambiente virtual](../virtual-environments.md){.internal-link target=_blank}, o ativou e instalou o `websockets`: +Garanta que você criou um [ambiente virtual](../virtual-environments.md){.internal-link target=_blank}, o ativou e instalou o `websockets` (uma biblioteca Python que facilita o uso do protocolo "WebSocket"):
@@ -16,9 +16,9 @@ $ pip install websockets
-## Cliente WebSockets +## Cliente WebSockets { #websockets-client } -### Em produção +### Em produção { #in-production } Em seu sistema de produção, você provavelmente tem um frontend criado com um framework moderno como React, Vue.js ou Angular. @@ -40,11 +40,11 @@ Mas é a maneira mais simples de focar no lado do servidor de WebSockets e ter u {* ../../docs_src/websockets/tutorial001.py hl[2,6:38,41:43] *} -## Criando um `websocket` +## Crie um `websocket` { #create-a-websocket } Em sua aplicação **FastAPI**, crie um `websocket`: -{*../../docs_src/websockets/tutorial001.py hl[46:47]*} +{* ../../docs_src/websockets/tutorial001.py hl[1,46:47] *} /// note | Detalhes Técnicos @@ -54,15 +54,15 @@ A **FastAPI** fornece o mesmo `WebSocket` diretamente apenas como uma conveniên /// -## Aguardar por mensagens e enviar mensagens +## Aguarde mensagens e envie mensagens { #await-for-messages-and-send-messages } Em sua rota WebSocket você pode esperar (`await`) por mensagens e enviar mensagens. -{*../../docs_src/websockets/tutorial001.py hl[48:52]*} +{* ../../docs_src/websockets/tutorial001.py hl[48:52] *} Você pode receber e enviar dados binários, de texto e JSON. -## Tente você mesmo +## Tente { #try-it } Se seu arquivo for nomeado `main.py`, execute sua aplicação com: @@ -96,7 +96,7 @@ Você pode enviar (e receber) muitas mensagens: E todas elas usarão a mesma conexão WebSocket. -## Usando `Depends` e outros +## Usando `Depends` e outros { #using-depends-and-others } Nos endpoints WebSocket você pode importar do `fastapi` e usar: @@ -109,7 +109,7 @@ Nos endpoints WebSocket você pode importar do `fastapi` e usar: Eles funcionam da mesma forma que para outros endpoints FastAPI/*operações de rota*: -{*../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82]*} +{* ../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82] *} /// info | Informação @@ -119,7 +119,7 @@ Você pode usar um código de fechamento dos http://127.0.0.1:8000. +Abra seu navegador em: http://127.0.0.1:8000. Lá você pode definir: -* O "Item ID", usado na rota. +* O "Item ID", usado no path. * O "Token" usado como um parâmetro de consulta. /// tip | Dica @@ -150,15 +150,15 @@ Com isso você pode conectar o WebSocket e então enviar e receber mensagens: -## Lidando com desconexões e múltiplos clientes +## Lidando com desconexões e múltiplos clientes { #handling-disconnections-and-multiple-clients } Quando uma conexão WebSocket é fechada, o `await websocket.receive_text()` levantará uma exceção `WebSocketDisconnect`, que você pode então capturar e lidar como neste exemplo. -{*../../docs_src/websockets/tutorial003_py39.py hl[79:81]*} +{* ../../docs_src/websockets/tutorial003_py39.py hl[79:81] *} Para testar: -* Abrar o aplicativo com várias abas do navegador. +* Abra o aplicativo com várias abas do navegador. * Escreva mensagens a partir delas. * Então feche uma das abas. @@ -172,13 +172,13 @@ Client #1596980209979 left the chat O app acima é um exemplo mínimo e simples para demonstrar como lidar e transmitir mensagens para várias conexões WebSocket. -Mas tenha em mente que, como tudo é manipulado na memória, em uma única lista, ele só funcionará enquanto o processo estiver em execução e só funcionará com um único processo. +Mas tenha em mente que, como tudo é manipulado na memória, em uma única list, ele só funcionará enquanto o processo estiver em execução e só funcionará com um único processo. Se você precisa de algo fácil de integrar com o FastAPI, mas que seja mais robusto, suportado por Redis, PostgreSQL ou outros, verifique o encode/broadcaster. /// -## Mais informações +## Mais informações { #more-info } Para aprender mais sobre as opções, verifique a documentação do Starlette para: diff --git a/docs/pt/docs/advanced/wsgi.md b/docs/pt/docs/advanced/wsgi.md index a36261e5e..ee21b2501 100644 --- a/docs/pt/docs/advanced/wsgi.md +++ b/docs/pt/docs/advanced/wsgi.md @@ -1,22 +1,22 @@ -# Adicionando WSGI - Flask, Django, entre outros +# Adicionando WSGI - Flask, Django, entre outros { #including-wsgi-flask-django-others } -Como você viu em [Sub Applications - Mounts](sub-applications.md){.internal-link target=_blank} e [Behind a Proxy](behind-a-proxy.md){.internal-link target=_blank}, você pode **"montar"** aplicações WSGI. +Como você viu em [Subaplicações - Montagens](sub-applications.md){.internal-link target=_blank} e [Atrás de um Proxy](behind-a-proxy.md){.internal-link target=_blank}, você pode montar aplicações WSGI. Para isso, você pode utilizar o `WSGIMiddleware` para encapsular a sua aplicação WSGI, como por exemplo Flask, Django, etc. -## Usando o `WSGIMiddleware` +## Usando `WSGIMiddleware` { #using-wsgimiddleware } Você precisa importar o `WSGIMiddleware`. -Em seguinda, encapsular a aplicação WSGI (e.g. Flask) com o middleware. +Em seguida, encapsule a aplicação WSGI (e.g. Flask) com o middleware. -E então **"montar"** em um caminho de rota. +E então monte isso sob um path. -{* ../../docs_src/wsgi/tutorial001.py hl[2:3,23] *} +{* ../../docs_src/wsgi/tutorial001.py hl[2:3,3] *} -## Conferindo +## Confira { #check-it } -Agora todas as requisições sob o caminho `/v1/` serão manipuladas pela aplicação utilizando Flask. +Agora, todas as requisições sob o path `/v1/` serão manipuladas pela aplicação Flask. E o resto será manipulado pelo **FastAPI**. diff --git a/docs/pt/docs/alternatives.md b/docs/pt/docs/alternatives.md index 66cf3fe12..fd992ec95 100644 --- a/docs/pt/docs/alternatives.md +++ b/docs/pt/docs/alternatives.md @@ -1,34 +1,34 @@ -# Alternativas, Inspiração e Comparações +# Alternativas, Inspiração e Comparações { #alternatives-inspiration-and-comparisons } -O que inspirou o **FastAPI**, como ele se compara às alternativas e o que FastAPI aprendeu delas. +O que inspirou o **FastAPI**, como ele se compara às alternativas e o que ele aprendeu com elas. -## Introdução +## Introdução { #intro } -**FastAPI** não poderia existir se não fosse pelos trabalhos anteriores de outras pessoas. +**FastAPI** não existiria se não fosse pelo trabalho anterior de outras pessoas. -Houveram tantas ferramentas criadas que ajudaram a inspirar sua criação. +Houve muitas ferramentas criadas antes que ajudaram a inspirar sua criação. -Tenho evitado criar um novo framework por anos. Primeiramente tentei resolver todos os recursos cobertos pelo **FastAPI** utilizando muitos frameworks diferentes, plug-ins e ferramentas. +Tenho evitado criar um novo framework por vários anos. Primeiro tentei resolver todas as funcionalidades cobertas pelo **FastAPI** utilizando muitos frameworks, plug-ins e ferramentas diferentes. -Mas em algum ponto, não houve outra opção senão criar algo que fornecesse todos esses recursos, pegando as melhores idéias de ferramentas anteriores, e combinando eles da melhor forma possível, utilizando recursos da linguagem que não estavam disponíveis antes (_Type Hints_ no Python 3.6+). +Mas em algum momento, não havia outra opção senão criar algo que fornecesse todos esses recursos, pegando as melhores ideias de ferramentas anteriores e combinando-as da melhor maneira possível, usando funcionalidades da linguagem que nem sequer estavam disponíveis antes (anotações de tipo no Python 3.6+). -## Ferramentas anteriores +## Ferramentas anteriores { #previous-tools } -### Django +### Django { #django } -É o framework mais popular e largamente confiável. É utilizado para construir sistemas como o _Instagram_. +É o framework Python mais popular e amplamente confiável. É utilizado para construir sistemas como o Instagram. -É bem acoplado com banco de dados relacional (como MySQL ou PostgreSQL), então, tendo um banco de dados NoSQL (como Couchbase, MongoDB, Cassandra etc) como a principal ferramenta de armazenamento não é muito fácil. +É relativamente bem acoplado com bancos de dados relacionais (como MySQL ou PostgreSQL), então, ter um banco de dados NoSQL (como Couchbase, MongoDB, Cassandra, etc.) como mecanismo principal de armazenamento não é muito fácil. -Foi criado para gerar HTML no _backend_, não para criar APIs utilizando um _frontend_ moderno (como React, Vue.js e Angular) ou por outros sistemas (como dispositivos IoT) comunicando com ele. +Foi criado para gerar o HTML no backend, não para criar APIs usadas por um frontend moderno (como React, Vue.js e Angular) ou por outros sistemas (como dispositivos IoT) comunicando com ele. -### Django REST Framework +### Django REST Framework { #django-rest-framework } -Django REST framework foi criado para ser uma caixa de ferramentas flexível para construção de APIs web utilizando Django por baixo, para melhorar suas capacidades de API. +Django REST framework foi criado para ser uma caixa de ferramentas flexível para construção de APIs Web utilizando Django por baixo, para melhorar suas capacidades de API. -Ele é utilizado por muitas companhias incluindo Mozilla, Red Hat e Eventbrite. +Ele é utilizado por muitas empresas incluindo Mozilla, Red Hat e Eventbrite. -Ele foi um dos primeiros exemplos de **documentação automática de API**, e essa foi especificamente uma das primeiras idéias que inspirou "a busca por" **FastAPI**. +Foi um dos primeiros exemplos de **documentação automática de API**, e essa foi especificamente uma das primeiras ideias que inspirou "a busca por" **FastAPI**. /// note | Nota @@ -38,57 +38,57 @@ Django REST Framework foi criado por Tom Christie. O mesmo criador de Starlette /// check | **FastAPI** inspirado para -Ter uma documentação automática da API em interface web. +Ter uma interface web de documentação automática da API. /// -### Flask +### Flask { #flask } -Flask é um "microframework", não inclui integração com banco de dados nem muitas das coisas que vêm por padrão no Django. +Flask é um "microframework", não inclui integrações com banco de dados nem muitas das coisas que vêm por padrão no Django. -Sua simplicidade e flexibilidade permitem fazer coisas como utilizar bancos de dados NoSQL como principal sistema de armazenamento de dados. +Essa simplicidade e flexibilidade permitem fazer coisas como utilizar bancos de dados NoSQL como o principal sistema de armazenamento de dados. -Por ser tão simples, é relativamente intuitivo de aprender, embora a documentação esteja de forma mais técnica em alguns pontos. +Por ser muito simples, é relativamente intuitivo de aprender, embora a documentação se torne um pouco técnica em alguns pontos. -Ele é comumente utilizado por outras aplicações que não necessariamente precisam de banco de dados, gerenciamento de usuários, ou algum dos muitos recursos que já vem instalados no Django. Embora muitos desses recursos possam ser adicionados com plug-ins. +Ele também é comumente utilizado por outras aplicações que não necessariamente precisam de banco de dados, gerenciamento de usuários, ou qualquer uma das muitas funcionalidades que já vêm prontas no Django. Embora muitas dessas funcionalidades possam ser adicionadas com plug-ins. -Esse desacoplamento de partes, e sendo um "microframework" que pode ser extendido para cobrir exatamente o que é necessário era um recurso chave que eu queria manter. +Esse desacoplamento de partes, e ser um "microframework" que pode ser estendido para cobrir exatamente o que é necessário era uma funcionalidade chave que eu queria manter. -Dada a simplicidade do Flask, parecia uma ótima opção para construção de APIs. A próxima coisa a procurar era um "Django REST Framework" para Flask. +Dada a simplicidade do Flask, ele parecia uma boa opção para construção de APIs. A próxima coisa a encontrar era um "Django REST Framework" para Flask. /// check | **FastAPI** inspirado para -Ser um microframework. Fazer ele fácil para misturar e combinar com ferramentas e partes necessárias. +Ser um microframework. Tornar fácil misturar e combinar as ferramentas e partes necessárias. -Ser simples e com sistema de roteamento fácil de usar. +Ter um sistema de roteamento simples e fácil de usar. /// -### Requests +### Requests { #requests } -**FastAPI** não é uma alternativa para **Requests**. O escopo deles é muito diferente. +**FastAPI** na verdade não é uma alternativa ao **Requests**. O escopo deles é muito diferente. -Na verdade é comum utilizar Requests *dentro* de uma aplicação FastAPI. +Na verdade, é comum utilizar Requests dentro de uma aplicação FastAPI. -Ainda assim, FastAPI pegou alguma inspiração do Requests. +Ainda assim, o FastAPI tirou bastante inspiração do Requests. -**Requests** é uma biblioteca para interagir com APIs (como um cliente), enquanto **FastAPI** é uma biblioteca para *construir* APIs (como um servidor). +**Requests** é uma biblioteca para interagir com APIs (como um cliente), enquanto **FastAPI** é uma biblioteca para construir APIs (como um servidor). -Eles estão, mais ou menos, em pontas opostas, um complementando o outro. +Eles estão, mais ou menos, em pontas opostas, complementando-se. -Requests tem um projeto muito simples e intuitivo, fácil de usar, com padrões sensíveis. Mas ao mesmo tempo, é muito poderoso e customizável. +Requests tem um design muito simples e intuitivo, é muito fácil de usar, com padrões sensatos. Mas ao mesmo tempo, é muito poderoso e personalizável. É por isso que, como dito no site oficial: > Requests é um dos pacotes Python mais baixados de todos os tempos -O jeito de usar é muito simples. Por exemplo, para fazer uma requisição `GET`, você deveria escrever: +O jeito de usar é muito simples. Por exemplo, para fazer uma requisição `GET`, você escreveria: ```Python response = requests.get("http://example.com/some/url") ``` -A contra-parte da aplicação FastAPI, *rota de operação*, poderia parecer como: +A contra-parte na aplicação FastAPI, a operação de rota, poderia ficar assim: ```Python hl_lines="1" @app.get("/some/url") @@ -102,50 +102,50 @@ Veja as similaridades em `requests.get(...)` e `@app.get(...)`. * Ter uma API simples e intuitiva. * Utilizar nomes de métodos HTTP (operações) diretamente, de um jeito direto e intuitivo. -* Ter padrões sensíveis, mas customizações poderosas. +* Ter padrões sensatos, mas customizações poderosas. /// -### Swagger / OpenAPI +### Swagger / OpenAPI { #swagger-openapi } -O principal recurso que eu queria do Django REST Framework era a documentação automática da API. +A principal funcionalidade que eu queria do Django REST Framework era a documentação automática da API. -Então eu descobri que existia um padrão para documentar APIs, utilizando JSON (ou YAML, uma extensão do JSON) chamado Swagger. +Então descobri que existia um padrão para documentar APIs, utilizando JSON (ou YAML, uma extensão do JSON) chamado Swagger. -E tinha uma interface web para APIs Swagger já criada. Então, sendo capaz de gerar documentação Swagger para uma API poderia permitir utilizar essa interface web automaticamente. +E havia uma interface web para APIs Swagger já criada. Então, ser capaz de gerar documentação Swagger para uma API permitiria usar essa interface web automaticamente. -Em algum ponto, Swagger foi dado para a Fundação Linux, e foi renomeado OpenAPI. +Em algum ponto, Swagger foi doado para a Fundação Linux, para ser renomeado OpenAPI. -Isso acontece porquê quando alguém fala sobre a versão 2.0 é comum dizer "Swagger", e para a versão 3+, "OpenAPI". +É por isso que ao falar sobre a versão 2.0 é comum dizer "Swagger", e para a versão 3+ "OpenAPI". /// check | **FastAPI** inspirado para -Adotar e usar um padrão aberto para especificações API, ao invés de algum esquema customizado. +Adotar e usar um padrão aberto para especificações de API, em vez de um schema personalizado. -E integrar ferramentas de interface para usuários baseado nos padrões: +E integrar ferramentas de interface para usuários baseadas nos padrões: * Swagger UI * ReDoc -Esses dois foram escolhidos por serem bem populares e estáveis, mas fazendo uma pesquisa rápida, você pode encontrar dúzias de interfaces alternativas adicionais para OpenAPI (assim você poderá utilizar com **FastAPI**). +Essas duas foram escolhidas por serem bem populares e estáveis, mas fazendo uma pesquisa rápida, você pode encontrar dúzias de interfaces alternativas adicionais para OpenAPI (que você pode utilizar com **FastAPI**). /// -### Flask REST frameworks +### Flask REST frameworks { #flask-rest-frameworks } -Existem vários Flask REST frameworks, mas depois de investir tempo e trabalho investigando eles, eu descobri que muitos estão descontinuados ou abandonados, com alguns tendo questões que fizeram eles inadequados. +Existem vários Flask REST frameworks, mas depois de investir tempo e trabalho investigando-os, descobri que muitos estão descontinuados ou abandonados, com diversas questões em aberto que os tornaram inadequados. -### Marshmallow +### Marshmallow { #marshmallow } -Um dos principais recursos necessários em sistemas API é "serialização" de dados, que é pegar dados do código (Python) e converter eles em alguma coisa que possa ser enviado através da rede. Por exemplo, converter um objeto contendo dados de um banco de dados em um objeto JSON. Converter objetos `datetime` em strings etc. +Uma das principais funcionalidades necessárias em sistemas de API é a "serialização" de dados, que é pegar dados do código (Python) e convertê-los em algo que possa ser enviado pela rede. Por exemplo, converter um objeto contendo dados de um banco de dados em um objeto JSON. Converter objetos `datetime` em strings, etc. -Outro grande recurso necessário nas APIs é validação de dados, certificando que os dados são válidos, dados certos parâmetros. Por exemplo, algum campo é `int`, e não alguma string aleatória. Isso é especialmente útil para dados que estão chegando. +Outra grande funcionalidade necessária pelas APIs é a validação de dados, garantindo que os dados são válidos, dados certos parâmetros. Por exemplo, que algum campo seja `int`, e não alguma string aleatória. Isso é especialmente útil para dados de entrada. Sem um sistema de validação de dados, você teria que realizar todas as verificações manualmente, no código. -Esses recursos são o que Marshmallow foi construído para fornecer. Ele é uma ótima biblioteca, e eu já utilizei muito antes. +Essas funcionalidades são o que o Marshmallow foi construído para fornecer. É uma ótima biblioteca, e eu a utilizei bastante antes. -Mas ele foi criado antes da existência do _type hints_ do Python. Então, para definir todo o _schema_ você precisa utilizar específicas ferramentas e classes fornecidas pelo Marshmallow. +Mas ele foi criado antes de existirem as anotações de tipo do Python. Então, para definir cada schema você precisa utilizar utilitários e classes específicos fornecidos pelo Marshmallow. /// check | **FastAPI** inspirado para @@ -153,17 +153,17 @@ Usar código para definir "schemas" que forneçam, automaticamente, tipos de dad /// -### Webargs +### Webargs { #webargs } -Outro grande recurso necessário pelas APIs é a análise de dados vindos de requisições. +Outra grande funcionalidade requerida pelas APIs é o parsing de dados vindos de requisições de entrada. -Webargs é uma ferramente feita para fornecer o que está no topo de vários frameworks, inclusive Flask. +Webargs é uma ferramenta feita para fornecer isso no topo de vários frameworks, inclusive Flask. -Ele utiliza Marshmallow por baixo para validação de dados. E ele foi criado pelos mesmos desenvolvedores. +Ele utiliza Marshmallow por baixo para a validação de dados. E foi criado pelos mesmos desenvolvedores. -Ele é uma grande ferramenta e eu também a utilizei muito, antes de ter o **FastAPI**. +É uma grande ferramenta e eu também a utilizei bastante, antes de ter o **FastAPI**. -/// info +/// info | Informação Webargs foi criado pelos mesmos desenvolvedores do Marshmallow. @@ -171,29 +171,29 @@ Webargs foi criado pelos mesmos desenvolvedores do Marshmallow. /// check | **FastAPI** inspirado para -Ter validação automática de dados vindos de requisições. +Ter validação automática dos dados de requisições de entrada. /// -### APISpec +### APISpec { #apispec } -Marshmallow e Webargs fornecem validação, análise e serialização como plug-ins. +Marshmallow e Webargs fornecem validação, parsing e serialização como plug-ins. -Mas a documentação ainda está faltando. Então APISpec foi criado. +Mas a documentação ainda estava faltando. Então APISpec foi criado. -APISpec tem plug-ins para muitos frameworks (e tem um plug-in para Starlette também). +É um plug-in para muitos frameworks (e há um plug-in para Starlette também). -O jeito como ele funciona é que você escreve a definição do _schema_ usando formato YAML dentro da _docstring_ de cada função controlando uma rota. +O jeito como ele funciona é que você escreve a definição do schema usando formato YAML dentro da docstring de cada função que lida com uma rota. -E ele gera _schemas_ OpenAPI. +E ele gera schemas OpenAPI. -É assim como funciona no Flask, Starlette, Responder etc. +É assim como funciona no Flask, Starlette, Responder, etc. -Mas então, nós temos novamente o problema de ter uma micro-sintaxe, dentro de uma string Python (um grande YAML). +Mas então, temos novamente o problema de ter uma micro-sintaxe, dentro de uma string Python (um grande YAML). -O editor não poderá ajudar muito com isso. E se nós modificarmos os parâmetros dos _schemas_ do Marshmallow e esquecer de modificar também aquela _docstring_ YAML, o _schema_ gerado pode ficar obsoleto. +O editor não pode ajudar muito com isso. E se modificarmos parâmetros ou schemas do Marshmallow e esquecermos de também modificar aquela docstring em YAML, o schema gerado ficaria obsoleto. -/// info +/// info | Informação APISpec foi criado pelos mesmos desenvolvedores do Marshmallow. @@ -201,31 +201,31 @@ APISpec foi criado pelos mesmos desenvolvedores do Marshmallow. /// check | **FastAPI** inspirado para -Dar suporte a padrões abertos para APIs, OpenAPI. +Dar suporte ao padrão aberto para APIs, OpenAPI. /// -### Flask-apispec +### Flask-apispec { #flask-apispec } -É um plug-in Flask, que amarra junto Webargs, Marshmallow e APISpec. +É um plug-in Flask, que amarra juntos Webargs, Marshmallow e APISpec. -Ele utiliza a informação do Webargs e Marshmallow para gerar automaticamente _schemas_ OpenAPI, usando APISpec. +Ele utiliza a informação do Webargs e Marshmallow para gerar automaticamente schemas OpenAPI, usando APISpec. -É uma grande ferramenta, mas muito subestimada. Ela deveria ser um pouco mais popular do que muitos outros plug-ins Flask. É de ser esperado que sua documentação seja bem concisa e abstrata. +É uma grande ferramenta, muito subestimada. Deveria ser bem mais popular do que muitos plug-ins Flask por aí. Pode ser devido à sua documentação ser concisa e abstrata demais. -Isso resolveu o problema de ter que escrever YAML (outra sintaxe) dentro das _docstrings_ Python. +Isso resolveu ter que escrever YAML (outra sintaxe) dentro das docstrings do Python. -Essa combinação de Flask, Flask-apispec com Marshmallow e Webargs foi meu _backend stack_ favorito até construir o **FastAPI**. +Essa combinação de Flask, Flask-apispec com Marshmallow e Webargs foi a minha stack de backend favorita até construir o **FastAPI**. -Usando essa combinação levou a criação de vários geradores Flask _full-stack_. Há muitas _stacks_ que eu (e vários times externos) estou utilizando até agora: +Usá-la levou à criação de vários geradores Flask full-stack. Estas são as principais stacks que eu (e várias equipes externas) tenho utilizado até agora: * https://github.com/tiangolo/full-stack * https://github.com/tiangolo/full-stack-flask-couchbase * https://github.com/tiangolo/full-stack-flask-couchdb -E esses mesmos geradores _full-stack_ foram a base dos [Geradores de Projetos **FastAPI**](project-generation.md){.internal-link target=_blank}. +E esses mesmos geradores full-stack foram a base dos [Geradores de Projetos **FastAPI**](project-generation.md){.internal-link target=_blank}. -/// info +/// info | Informação Flask-apispec foi criado pelos mesmos desenvolvedores do Marshmallow. @@ -233,151 +233,149 @@ Flask-apispec foi criado pelos mesmos desenvolvedores do Marshmallow. /// check | **FastAPI** inspirado para -Gerar _schema_ OpenAPI automaticamente, a partir do mesmo código que define serialização e validação. +Gerar o schema OpenAPI automaticamente, a partir do mesmo código que define serialização e validação. /// -### NestJS (and Angular) +### NestJS (e Angular) { #nestjs-and-angular } -NestJS, que não é nem Python, é um framework NodeJS JavaScript (TypeScript) inspirado pelo Angular. +Isso nem é Python, NestJS é um framework NodeJS em JavaScript (TypeScript) inspirado pelo Angular. -Ele alcança de uma forma similar ao que pode ser feito com o Flask-apispec. +Ele alcança algo um tanto similar ao que pode ser feito com Flask-apispec. -Ele tem um sistema de injeção de dependência integrado, inspirado pelo Angular 2. É necessário fazer o pré-registro dos "injetáveis" (como todos os sistemas de injeção de dependência que conheço), então, adicionando verbosidade e repetição de código. +Ele tem um sistema de injeção de dependência integrado, inspirado pelo Angular 2. É necessário fazer o pré-registro dos "injetáveis" (como todos os sistemas de injeção de dependência que conheço), então, adiciona verbosidade e repetição de código. -Como os parâmetros são descritos com tipos TypeScript (similar aos _type hints_ do Python), o suporte ao editor é muito bom. +Como os parâmetros são descritos com tipos do TypeScript (similares às anotações de tipo do Python), o suporte do editor é muito bom. -Mas como os dados TypeScript não são preservados após a compilação para o JavaScript, ele não pode depender dos tipos para definir a validação, serialização e documentação ao mesmo tempo. Devido a isso e a algumas decisões de projeto, para pegar a validação, serialização e geração automática do _schema_, é necessário adicionar decoradores em muitos lugares. Então, ele se torna muito verboso. +Mas como os dados do TypeScript não são preservados após a compilação para JavaScript, ele não pode depender dos tipos para definir validação, serialização e documentação ao mesmo tempo. Devido a isso e a algumas decisões de projeto, para obter validação, serialização e geração automática de schema, é necessário adicionar decorators em muitos lugares. Então, ele se torna bastante verboso. -Ele também não controla modelos aninhados muito bem. Então, se o corpo JSON na requisição for um objeto JSON que contém campos internos que contém objetos JSON aninhados, ele não consegue ser validado e documentado apropriadamente. +Ele não consegue lidar muito bem com modelos aninhados. Então, se o corpo JSON na requisição for um objeto JSON que contém campos internos que por sua vez são objetos JSON aninhados, ele não consegue ser documentado e validado apropriadamente. /// check | **FastAPI** inspirado para -Usar tipos Python para ter um ótimo suporte do editor. +Usar tipos do Python para ter um ótimo suporte do editor. -Ter um sistema de injeção de dependência poderoso. Achar um jeito de minimizar repetição de código. +Ter um sistema de injeção de dependência poderoso. Encontrar um jeito de minimizar repetição de código. /// -### Sanic +### Sanic { #sanic } -Ele foi um dos primeiros frameworks Python extremamente rápido baseado em `asyncio`. Ele foi feito para ser muito similar ao Flask. +Ele foi um dos primeiros frameworks Python extremamente rápidos baseados em `asyncio`. Ele foi feito para ser muito similar ao Flask. -/// note | Detalhes técnicos +/// note | Detalhes Técnicos -Ele utiliza `uvloop` ao invés do '_loop_' `asyncio` padrão do Python. É isso que deixa ele tão rápido. +Ele utilizava `uvloop` em vez do loop `asyncio` padrão do Python. É isso que o deixava tão rápido. -Ele claramente inspirou Uvicorn e Starlette, que são atualmente mais rápidos que o Sanic em testes de performance abertos. +Ele claramente inspirou Uvicorn e Starlette, que atualmente são mais rápidos que o Sanic em benchmarks abertos. /// /// check | **FastAPI** inspirado para -Achar um jeito de ter uma performance insana. +Encontrar um jeito de ter uma performance insana. -É por isso que o **FastAPI** é baseado em Starlette, para que ele seja o framework mais rápido disponível (performance testada por terceiros). +É por isso que o **FastAPI** é baseado em Starlette, pois ela é o framework mais rápido disponível (testado por benchmarks de terceiros). /// -### Falcon +### Falcon { #falcon } -Falcon é outro framework Python de alta performance, e é projetado para ser minimalista, e funciona como fundação de outros frameworks como Hug. +Falcon é outro framework Python de alta performance, projetado para ser minimalista, e servir como base para outros frameworks como Hug. -Ele usa o padrão anterior para frameworks web Python (WSGI) que é síncrono, então ele não pode controlar _WebSockets_ e outros casos de uso. No entanto, ele também tem uma boa performance. +Ele é projetado para ter funções que recebem dois parâmetros, uma "request" e uma "response". Então você "lê" partes da requisição, e "escreve" partes para a resposta. Por causa desse design, não é possível declarar parâmetros de requisição e corpos com as anotações de tipo padrão do Python como parâmetros de função. -Ele é projetado para ter funções que recebem dois parâmetros, uma "requisição" e uma "resposta". Então você "lê" as partes da requisição, e "escreve" partes para a resposta. Devido ao seu design, não é possível declarar parâmetros de requisição e corpos com _type hints_ Python padrão como parâmetros de funções. - -Então, validação de dados, serialização e documentação tem que ser feitos no código, não automaticamente. Ou eles terão que ser implementados como um framework acima do Falcon, como o Hug. Essa mesma distinção acontece em outros frameworks que são inspirados pelo design do Falcon, tendo um objeto de requisição e um objeto de resposta como parâmetros. +Então, validação de dados, serialização e documentação têm que ser feitos no código, não automaticamente. Ou eles têm que ser implementados como um framework acima do Falcon, como o Hug. Essa mesma distinção acontece em outros frameworks inspirados pelo design do Falcon, de ter um objeto de request e um objeto de response como parâmetros. /// check | **FastAPI** inspirado para -Achar jeitos de conseguir melhor performance. +Encontrar maneiras de obter uma ótima performance. -Juntamente com Hug (como Hug é baseado no Falcon) inspirou **FastAPI** para declarar um parâmetro de `resposta` nas funções. +Juntamente com Hug (como Hug é baseado no Falcon) inspirou **FastAPI** a declarar um parâmetro de `response` nas funções. Embora no FastAPI seja opcional, é utilizado principalmente para configurar cabeçalhos, cookies e códigos de status alternativos. /// -### Molten +### Molten { #molten } -Eu descobri Molten nos primeiros estágios da construção do **FastAPI**. E ele tem umas idéias bem similares: +Eu descobri Molten nos primeiros estágios da construção do **FastAPI**. E ele tem ideias bastante similares: -* Baseado em _type hints_ Python. -* Validação e documentação desses tipos. -* Sistema de injeção de dependência. +* Baseado nas anotações de tipo do Python. +* Validação e documentação a partir desses tipos. +* Sistema de Injeção de Dependência. -Ele não utiliza validação de dados, seriallização e documentação de bibliotecas de terceiros como o Pydantic, ele tem seu prórpio. Então, essas definições de tipo de dados não podem ser reutilizados tão facilmente. +Ele não utiliza uma biblioteca de terceiros para validação de dados, serialização e documentação como o Pydantic, ele tem a sua própria. Então, essas definições de tipos de dados não seriam reutilizáveis tão facilmente. -Ele exige um pouco mais de verbosidade nas configurações. E como é baseado no WSGI (ao invés de ASGI), ele não é projetado para ter a vantagem da alta performance fornecida por ferramentas como Uvicorn, Starlette e Sanic. +Ele exige configurações um pouco mais verbosas. E como é baseado em WSGI (em vez de ASGI), ele não é projetado para tirar vantagem da alta performance fornecida por ferramentas como Uvicorn, Starlette e Sanic. -O sistema de injeção de dependência exige pré-registro das dependências e as dependências são resolvidas baseadas nos tipos declarados. Então, não é possível declarar mais do que um "componente" que fornece um certo tipo. +O sistema de injeção de dependência exige pré-registro das dependências e elas são resolvidas com base nos tipos declarados. Então, não é possível declarar mais de um "componente" que forneça um certo tipo. -Rotas são declaradas em um único lugar, usando funções declaradas em outros lugares (ao invés de usar decoradores que possam ser colocados diretamente acima da função que controla o _endpoint_). Isso é mais perto de como o Django faz isso do que como Flask (e Starlette) faz. Ele separa no código coisas que são relativamente amarradas. +As rotas são declaradas em um único lugar, usando funções declaradas em outros lugares (em vez de usar decorators que possam ser colocados diretamente acima da função que lida com o endpoint). Isso é mais próximo de como o Django faz do que de como o Flask (e o Starlette) fazem. Separa no código coisas que são relativamente bem acopladas. /// check | **FastAPI** inspirado para -Definir validações extras para tipos de dados usando valores "padrão" de atributos dos modelos. Isso melhora o suporte do editor, e não estava disponível no Pydantic antes. +Definir validações extras para tipos de dados usando o valor "padrão" de atributos dos modelos. Isso melhora o suporte do editor, e não estava disponível no Pydantic antes. Isso na verdade inspirou a atualização de partes do Pydantic, para dar suporte ao mesmo estilo de declaração da validação (toda essa funcionalidade já está disponível no Pydantic). /// -### Hug +### Hug { #hug } -Hug foi um dos primeiros frameworks a implementar a declaração de tipos de parâmetros usando Python _type hints_. Isso foi uma ótima idéia que inspirou outras ferramentas a fazer o mesmo. +Hug foi um dos primeiros frameworks a implementar a declaração de tipos de parâmetros de API usando anotações de tipo do Python. Isso foi uma ótima ideia que inspirou outras ferramentas a fazer o mesmo. -Ele usou tipos customizados em suas declarações ao invés dos tipos padrão Python, mas mesmo assim foi um grande passo. +Ele usou tipos personalizados em suas declarações em vez dos tipos padrão do Python, mas mesmo assim foi um grande passo adiante. -Ele também foi um dos primeiros frameworks a gerar um _schema_ customizado declarando a API inteira em JSON. +Ele também foi um dos primeiros frameworks a gerar um schema personalizado declarando a API inteira em JSON. -Ele não era baseado em um padrão como OpenAPI e JSON Schema. Então não poderia ter interação direta com outras ferramentas, como Swagger UI. Mas novamente, era uma idéia muito inovadora. +Ele não era baseado em um padrão como OpenAPI e JSON Schema. Então não seria simples integrá-lo com outras ferramentas, como Swagger UI. Mas novamente, era uma ideia muito inovadora. -Hug tinha um incomum, interessante recurso: usando o mesmo framework, é possível criar tanto APIs como CLIs. +Ele tem um recurso interessante e incomum: usando o mesmo framework, é possível criar APIs e também CLIs. -Como é baseado nos padrões anteriores de frameworks web síncronos (WSGI), ele não pode controlar _Websockets_ e outras coisas, embora ele ainda tenha uma alta performance também. +Como é baseado no padrão anterior para frameworks web Python síncronos (WSGI), ele não consegue lidar com Websockets e outras coisas, embora ainda tenha alta performance também. -/// info +/// info | Informação -Hug foi criado por Timothy Crosley, o mesmo criador do `isort`, uma grande ferramenta para ordenação automática de _imports_ em arquivos Python. +Hug foi criado por Timothy Crosley, o mesmo criador do `isort`, uma ótima ferramenta para ordenar automaticamente imports em arquivos Python. /// -/// check | Idéias inspiradas para o **FastAPI** +/// check | Ideias que inspiraram o **FastAPI** -Hug inspirou partes do APIStar, e foi uma das ferramentas que eu achei mais promissora, ao lado do APIStar. +Hug inspirou partes do APIStar, e foi uma das ferramentas que achei mais promissoras, ao lado do APIStar. -Hug ajudou a inspirar o **FastAPI** a usar _type hints_ do Python para declarar parâmetros, e para gerar um _schema_ definindo a API automaticamente. +Hug ajudou a inspirar o **FastAPI** a usar anotações de tipo do Python para declarar parâmetros e para gerar um schema definindo a API automaticamente. -Hug inspirou **FastAPI** a declarar um parâmetro de `resposta` em funções para definir cabeçalhos e cookies. +Hug inspirou **FastAPI** a declarar um parâmetro de `response` em funções para definir cabeçalhos e cookies. /// -### APIStar (<= 0.5) +### APIStar (<= 0.5) { #apistar-0-5 } -Antes de decidir construir **FastAPI** eu encontrei o servidor **APIStar**. Tinha quase tudo que eu estava procurando e tinha um grande projeto. +Pouco antes de decidir construir o **FastAPI** eu encontrei o servidor **APIStar**. Ele tinha quase tudo o que eu estava procurando e tinha um ótimo design. -Ele foi uma das primeiras implementações de um framework usando Python _type hints_ para declarar parâmetros e requisições que eu nunca vi (antes no NestJS e Molten). Eu encontrei ele mais ou menos na mesma época que o Hug. Mas o APIStar utilizava o padrão OpenAPI. +Foi uma das primeiras implementações de um framework usando anotações de tipo do Python para declarar parâmetros e requisições que eu já vi (antes do NestJS e Molten). Eu o encontrei mais ou menos na mesma época que o Hug. Mas o APIStar utilizava o padrão OpenAPI. -Ele tinha validação de dados automática, serialização de dados e geração de _schema_ OpenAPI baseado nos mesmos _type hints_ em vários locais. +Ele tinha validação de dados automática, serialização de dados e geração de schema OpenAPI baseadas nas mesmas anotações de tipo em vários locais. -Definições de _schema_ de corpo não utilizavam os mesmos Python _type hints_ como Pydantic, ele era um pouco mais similar ao Marshmallow, então, o suporte ao editor não seria tão bom, ainda assim, APIStar era a melhor opção disponível. +As definições de schema de corpo não utilizavam as mesmas anotações de tipo do Python como o Pydantic, eram um pouco mais similares ao Marshmallow, então o suporte do editor não seria tão bom, ainda assim, APIStar era a melhor opção disponível. -Ele obteve as melhores performances em testes na época (somente batido por Starlette). +Ele obteve os melhores benchmarks de performance na época (somente ultrapassado por Starlette). A princípio, ele não tinha uma interface web com documentação automática da API, mas eu sabia que poderia adicionar o Swagger UI a ele. -Ele tinha um sistema de injeção de dependência. Ele exigia pré-registro dos componentes, como outras ferramentas já discutidas acima. Mas ainda era um grande recurso. +Ele tinha um sistema de injeção de dependência. Exigia pré-registro dos componentes, como outras ferramentas já discutidas acima. Mas ainda assim era um grande recurso. -Eu nunca fui capaz de usar ele num projeto inteiro, por não ter integração de segurança, então, eu não pude substituir todos os recursos que eu tinha com os geradores _full-stack_ baseados no Flask-apispec. Eu tive em minha gaveta de projetos a idéia de criar um _pull request_ adicionando essa funcionalidade. +Eu nunca fui capaz de usá-lo em um projeto completo, pois ele não tinha integração de segurança, então, eu não pude substituir todos os recursos que eu tinha com os geradores full-stack baseados no Flask-apispec. Eu tinha no meu backlog de projetos criar um pull request adicionando essa funcionalidade. Mas então, o foco do projeto mudou. -Ele não era mais um framework web API, como o criador precisava focar no Starlette. +Ele não era mais um framework web de API, pois o criador precisava focar no Starlette. Agora APIStar é um conjunto de ferramentas para validar especificações OpenAPI, não um framework web. -/// info +/// info | Informação APIStar foi criado por Tom Christie. O mesmo cara que criou: @@ -391,98 +389,97 @@ APIStar foi criado por Tom Christie. O mesmo cara que criou: Existir. -A idéia de declarar múltiplas coisas (validação de dados, serialização e documentação) com os mesmos tipos Python, que ao mesmo tempo fornecesse grande suporte ao editor, era algo que eu considerava uma brilhante idéia. +A ideia de declarar múltiplas coisas (validação de dados, serialização e documentação) com os mesmos tipos do Python, que ao mesmo tempo fornecessem grande suporte ao editor, era algo que eu considerava uma ideia brilhante. -E após procurar por um logo tempo por um framework similar e testar muitas alternativas diferentes, APIStar foi a melhor opção disponível. +E após procurar por muito tempo por um framework similar e testar muitas alternativas diferentes, APIStar foi a melhor opção disponível. -Então APIStar parou de existir como um servidor e Starlette foi criado, e foi uma nova melhor fundação para tal sistema. Essa foi a inspiração final para construir **FastAPI**. +Então APIStar deixou de existir como servidor e o Starlette foi criado, sendo uma nova e melhor fundação para tal sistema. Essa foi a inspiração final para construir o **FastAPI**. -Eu considero **FastAPI** um "sucessor espiritual" para o APIStar, evoluindo e melhorando os recursos, sistema de tipagem e outras partes, baseado na aprendizagem de todas essas ferramentas acima. +Eu considero o **FastAPI** um "sucessor espiritual" do APIStar, enquanto aprimora e amplia as funcionalidades, o sistema de tipagem e outras partes, baseado nos aprendizados de todas essas ferramentas anteriores. /// -## Usados por **FastAPI** +## Usados por **FastAPI** { #used-by-fastapi } -### Pydantic +### Pydantic { #pydantic } -Pydantic é uma biblioteca para definir validação de dados, serialização e documentação (usando JSON Schema) baseado nos Python _type hints_. +Pydantic é uma biblioteca para definir validação de dados, serialização e documentação (usando JSON Schema) com base nas anotações de tipo do Python. -Isso faz dele extremamente intuitivo. +Isso o torna extremamente intuitivo. -Ele é comparável ao Marshmallow. Embora ele seja mais rápido que Marshmallow em testes de performance. E ele é baseado nos mesmos Python _type hints_, o suporte ao editor é ótimo. +Ele é comparável ao Marshmallow. Embora seja mais rápido que o Marshmallow em benchmarks. E como é baseado nas mesmas anotações de tipo do Python, o suporte do editor é ótimo. /// check | **FastAPI** usa isso para -Controlar toda a validação de dados, serialização de dados e modelo de documentação automática (baseado no JSON Schema). +Controlar toda a validação de dados, serialização de dados e documentação automática de modelos (baseada no JSON Schema). -**FastAPI** então pega dados do JSON Schema e coloca eles no OpenAPI, à parte de todas as outras coisas que ele faz. +**FastAPI** então pega esses dados do JSON Schema e os coloca no OpenAPI, além de todas as outras coisas que faz. /// -### Starlette +### Starlette { #starlette } -Starlette é um framework/caixa de ferramentas ASGI peso leve, o que é ideal para construir serviços assíncronos de alta performance. +Starlette é um framework/caixa de ferramentas ASGI leve, o que é ideal para construir serviços asyncio de alta performance. -Ele é muito simples e intuitivo. É projetado para ser extensível facilmente, e ter componentes modulares. +Ele é muito simples e intuitivo. É projetado para ser facilmente extensível, e ter componentes modulares. Ele tem: * Performance seriamente impressionante. * Suporte a WebSocket. -* Suporte a GraphQL. -* Tarefas de processamento interno por trás dos panos. +* Tarefas em segundo plano dentro do processo. * Eventos de inicialização e encerramento. * Cliente de testes construído com HTTPX. -* Respostas CORS, GZip, Arquivos Estáticos, Streaming. +* CORS, GZip, Arquivos Estáticos, respostas Streaming. * Suporte para Sessão e Cookie. * 100% coberto por testes. * Código base 100% anotado com tipagem. -* Dependências complexas Zero. +* Poucas dependências obrigatórias. -Starlette é atualmente o mais rápido framework Python testado. Somente ultrapassado pelo Uvicorn, que não é um framework, mas um servidor. +Starlette é atualmente o framework Python mais rápido testado. Somente ultrapassado pelo Uvicorn, que não é um framework, mas um servidor. Starlette fornece toda a funcionalidade básica de um microframework web. -Mas ele não fornece validação de dados automática, serialização e documentação. +Mas ele não fornece validação de dados automática, serialização ou documentação. -Essa é uma das principais coisas que **FastAPI** adiciona no topo, tudo baseado em Python _type hints_ (usando Pydantic). Isso, mais o sistema de injeção de dependência, utilidades de segurança, geração de _schema_ OpenAPI, etc. +Essa é uma das principais coisas que o **FastAPI** adiciona por cima, tudo baseado nas anotações de tipo do Python (usando Pydantic). Isso, mais o sistema de injeção de dependência, utilidades de segurança, geração de schema OpenAPI, etc. /// note | Detalhes Técnicos -ASGI é um novo "padrão" sendo desenvolvido pelos membros do time central do Django. Ele ainda não está como "Padrão Python" (PEP), embora eles estejam em processo de fazer isso. +ASGI é um novo "padrão" sendo desenvolvido por membros do time central do Django. Ele ainda não é um "padrão Python" (uma PEP), embora eles estejam no processo de fazer isso. -No entanto, ele já está sendo utilizado como "padrão" por diversas ferramentas. Isso melhora enormemente a interoperabilidade, como você poderia trocar Uvicorn por qualquer outro servidor ASGI (como Daphne ou Hypercorn), ou você poderia adicionar ferramentas compatíveis com ASGI, como `python-socketio`. +No entanto, ele já está sendo utilizado como "padrão" por diversas ferramentas. Isso melhora enormemente a interoperabilidade, pois você poderia trocar Uvicorn por qualquer outro servidor ASGI (como Daphne ou Hypercorn), ou você poderia adicionar ferramentas compatíveis com ASGI, como `python-socketio`. /// /// check | **FastAPI** usa isso para -Controlar todas as partes web centrais. Adiciona recursos no topo. +Controlar todas as partes web centrais. Adiciona funcionalidades por cima. -A classe `FastAPI` em si herda `Starlette`. +A classe `FastAPI` em si herda diretamente da classe `Starlette`. -Então, qualquer coisa que você faz com Starlette, você pode fazer diretamente com **FastAPI**, pois ele é basicamente um Starlette com esteróides. +Então, qualquer coisa que você pode fazer com Starlette, você pode fazer diretamente com o **FastAPI**, pois ele é basicamente um Starlette com esteróides. /// -### Uvicorn +### Uvicorn { #uvicorn } -Uvicorn é um servidor ASGI peso leve, construído com uvloop e httptools. +Uvicorn é um servidor ASGI extremamente rápido, construído com uvloop e httptools. -Ele não é um framework web, mas sim um servidor. Por exemplo, ele não fornece ferramentas para roteamento por rotas. Isso é algo que um framework como Starlette (ou **FastAPI**) poderia fornecer por cima. +Ele não é um framework web, mas sim um servidor. Por exemplo, ele não fornece ferramentas para roteamento por paths. Isso é algo que um framework como Starlette (ou **FastAPI**) forneceria por cima. Ele é o servidor recomendado para Starlette e **FastAPI**. -/// check | **FastAPI** recomenda isso para +/// check | **FastAPI** o recomenda como O principal servidor web para rodar aplicações **FastAPI**. -Você pode combinar ele com o Gunicorn, para ter um servidor multi-processos assíncrono. +Você também pode usar a opção de linha de comando `--workers` para ter um servidor assíncrono multi-processos. Verifique mais detalhes na seção [Deployment](deployment/index.md){.internal-link target=_blank}. /// -## Performance e velocidade +## Benchmarks e velocidade { #benchmarks-and-speed } Para entender, comparar e ver a diferença entre Uvicorn, Starlette e FastAPI, verifique a seção sobre [Benchmarks](benchmarks.md){.internal-link target=_blank}. diff --git a/docs/pt/docs/async.md b/docs/pt/docs/async.md index c70924ea5..f01ff2315 100644 --- a/docs/pt/docs/async.md +++ b/docs/pt/docs/async.md @@ -1,10 +1,10 @@ -# Concorrência e async / await +# Concorrência e async / await { #concurrency-and-async-await } Detalhes sobre a sintaxe `async def` para *funções de operação de rota* e alguns conceitos de código assíncrono, concorrência e paralelismo. -## Com pressa? +## Com pressa? { #in-a-hurry } -TL;DR: +TL;DR: Se você estiver utilizando bibliotecas de terceiros que dizem para você chamar as funções com `await`, como: @@ -12,7 +12,7 @@ Se você estiver utilizando bibliotecas de terceiros que dizem para você chamar results = await some_library() ``` -Então, declare sua *função de operação de rota* com `async def` como: +Então, declare suas *funções de operação de rota* com `async def` como: ```Python hl_lines="2" @app.get('/') @@ -21,7 +21,7 @@ async def read_results(): return results ``` -/// note +/// note | Nota Você só pode usar `await` dentro de funções criadas com `async def`. @@ -29,7 +29,7 @@ Você só pode usar `await` dentro de funções criadas com `async def`. --- -Se você está usando biblioteca de terceiros que se comunica com alguma coisa (um banco de dados, uma API, sistema de arquivos etc) e não tem suporte para utilizar `await` (esse é atualmente o caso para a maioria das bibliotecas de banco de dados), então declare suas *funções de operação de rota* normalmente, com apenas `def`, como: +Se você está usando uma biblioteca de terceiros que se comunica com alguma coisa (um banco de dados, uma API, o sistema de arquivos etc.) e não tem suporte para utilizar `await` (esse é atualmente o caso para a maioria das bibliotecas de banco de dados), então declare suas *funções de operação de rota* normalmente, com apenas `def`, como: ```Python hl_lines="2" @app.get('/') @@ -40,7 +40,7 @@ def results(): --- -Se sua aplicação (de alguma forma) não tem que se comunicar com nada mais e esperar que o respondam, use `async def`. +Se sua aplicação (de alguma forma) não tem que se comunicar com nada mais e esperar que o respondam, use `async def`, mesmo que você não precise usar `await` dentro dela. --- @@ -54,17 +54,17 @@ De qualquer forma, em ambos os casos acima, FastAPI irá trabalhar assincronamen Mas, seguindo os passos acima, ele será capaz de fazer algumas otimizações de performance. -## Detalhes Técnicos +## Detalhes Técnicos { #technical-details } -Versões modernas de Python tem suporte para **"código assíncrono"** usando algo chamado **"corrotinas"**, com sintaxe **`async` e `await`**. +Versões modernas de Python têm suporte para **"código assíncrono"** usando algo chamado **"corrotinas"**, com sintaxe **`async` e `await`**. -Vamos ver aquela frase por partes na seção abaixo: +Vamos ver aquela frase por partes nas seções abaixo: * **Código assíncrono** * **`async` e `await`** * **Corrotinas** -## Código assíncrono +## Código assíncrono { #asynchronous-code } Código assíncrono apenas significa que a linguagem 💬 tem um jeito de dizer para o computador / programa 🤖 que em certo ponto do código, ele 🤖 terá que esperar *algo* finalizar em outro lugar. Vamos dizer que esse *algo* seja chamado "arquivo lento" 📝. @@ -74,10 +74,10 @@ Então o computador / programa 🤖 irá voltar sempre que tiver uma chance, sej Depois, ele 🤖 pega a primeira tarefa para finalizar (vamos dizer, nosso "arquivo lento" 📝) e continua o que tem que fazer com ela. -Esse "esperar por algo" normalmente se refere a operações I/O que são relativamente "lentas" (comparadas à velocidade do processador e da memória RAM), como esperar por: +Esse "esperar por algo" normalmente se refere a operações I/O que são relativamente "lentas" (comparadas à velocidade do processador e da memória RAM), como esperar por: * dados do cliente para serem enviados através da rede -* dados enviados pelo seu programa serem recebidos pelo clente através da rede +* dados enviados pelo seu programa serem recebidos pelo cliente através da rede * conteúdo de um arquivo no disco ser lido pelo sistema e entregue ao seu programa * conteúdo que seu programa deu ao sistema para ser escrito no disco * uma operação em uma API remota @@ -85,7 +85,7 @@ Esse "esperar por algo" normalmente se refere a operações I/O, essas operações são chamadas operações "limitadas por I/O". +Quanto o tempo de execução é consumido majoritariamente pela espera de operações I/O, essas operações são chamadas operações "limitadas por I/O". Isso é chamado de "assíncrono" porque o computador / programa não tem que ser "sincronizado" com a tarefa lenta, esperando pelo momento exato em que a tarefa finaliza, enquanto não faz nada, para ser capaz de pegar o resultado da tarefa e dar continuidade ao trabalho. @@ -93,9 +93,9 @@ Ao invés disso, sendo um sistema "assíncrono", uma vez finalizada, a tarefa po Para "síncrono" (contrário de "assíncrono") também é utilizado o termo "sequencial", porquê o computador / programa segue todos os passos, em sequência, antes de trocar para uma tarefa diferente, mesmo se alguns passos envolvam esperar. -### Concorrência e hambúrgueres +### Concorrência e hambúrgueres { #concurrency-and-burgers } -Essa idéia de código **assíncrono** descrita acima é às vezes chamado de **"concorrência"**. Isso é diferente de **"paralelismo"**. +Essa ideia de código **assíncrono** descrita acima é às vezes chamada de **"concorrência"**. Isso é diferente de **"paralelismo"**. **Concorrência** e **paralelismo** ambos são relacionados a "diferentes coisas acontecendo mais ou menos ao mesmo tempo". @@ -103,31 +103,51 @@ Mas os detalhes entre *concorrência* e *paralelismo* são bem diferentes. Para ver essa diferença, imagine a seguinte história sobre hambúrgueres: -### Hambúrgueres concorrentes +### Hambúrgueres concorrentes { #concurrent-burgers } Você vai com seu _crush_ na lanchonete, e fica na fila enquanto o caixa pega os pedidos das pessoas na sua frente. 😍 + + Então chega a sua vez, você pede dois saborosos hambúrgueres para você e seu _crush_. 🍔🍔 -O caixa diz alguma coisa para o cozinheiro na cozinha para que eles saivam que têm que preparar seus hambúrgueres (mesmo que ele esteja atualmente preparando os lanches dos outros clientes). + + +O caixa diz alguma coisa para o cozinheiro na cozinha para que eles saibam que têm que preparar seus hambúrgueres (mesmo que ele esteja atualmente preparando os lanches dos outros clientes). + + Você paga. 💸 O caixa te entrega seu número de chamada. + + Enquanto você espera, você vai com seu _crush_ e pega uma mesa, senta e conversa com seu _crush_ por um bom tempo (já que seus hambúrgueres são muito saborosos, e leva um tempo para serem preparados). Já que você está sentado na mesa com seu _crush_, esperando os hambúrgueres, você pode passar esse tempo admirando o quão lindo, maravilhoso e esperto é seu _crush_ ✨😍✨. + + Enquanto espera e conversa com seu _crush_, de tempos em tempos, você verifica o número da chamada exibido no balcão para ver se já é sua vez. Então em algum momento, é finalmente sua vez. Você vai ao balcão, pega seus hambúrgueres e volta para a mesa. + + Você e seu _crush_ comem os hambúrgueres e aproveitam o tempo. ✨ + + +/// info | Informação + +Belas ilustrações de Ketrina Thompson. 🎨 + +/// + --- -Imagine que você seja o computador / programa nessa história. +Imagine que você seja o computador / programa 🤖 nessa história. Enquanto você está na fila, você está somente ocioso 😴, esperando por sua vez, sem fazer nada muito "produtivo". Mas a fila é rápida porque o caixa só está pegando os pedidos (não os preparando), então está tudo bem. @@ -139,13 +159,13 @@ Contudo, à medida que você se afasta do balcão e senta na mesa, com um númer Então o caixa 💁 diz que "seus hambúrgueres estão prontos" colocando seu número no balcão, mas você não corre que nem um maluco imediatamente quando o número exibido é o seu. Você sabe que ninguém irá roubar seus hambúrgueres porque você tem o seu número da chamada, e os outros têm os deles. -Então você espera seu _crush_ terminar a história que estava contando (terminar o trabalho atual ⏯ / tarefa sendo processada 🤓), sorri gentilmente e diz que você está indo buscar os hambúrgueres. +Então você espera seu _crush_ terminar a história que estava contando (terminar o trabalho atual ⏯ / tarefa sendo processada 🤓), sorri gentilmente e diz que você está indo buscar os hambúrgueres ⏸. -Então você vai ao balcão 🔀, para a tarefa inicial que agora está finalizada⏯, pega os hambúrgueres, agradece, e leva-os para a mesa. Isso finaliza esse passo / tarefa da interação com o balcão ⏹. Isso, por sua vez, cria uma nova tarefa, a de "comer hambúrgueres" 🔀 ⏯, mas a tarefa anterior de "pegar os hambúrgueres" já está finalizada ⏹. +Então você vai ao balcão 🔀, para a tarefa inicial que agora está finalizada ⏯, pega os hambúrgueres, agradece, e leva-os para a mesa. Isso finaliza esse passo / tarefa da interação com o balcão ⏹. Isso, por sua vez, cria uma nova tarefa, a de "comer hambúrgueres" 🔀 ⏯, mas a tarefa anterior de "pegar os hambúrgueres" já está finalizada ⏹. -### Hambúrgueres paralelos +### Hambúrgueres paralelos { #parallel-burgers } -Agora vamos imaginar que esses não são "Hambúrgueres Concorrentes", e sim "Hambúrgueres Paralelos" +Agora vamos imaginar que esses não são "Hambúrgueres Concorrentes", e sim "Hambúrgueres Paralelos". Você vai com seu _crush_ na lanchonete paralela. @@ -153,29 +173,47 @@ Você fica na fila enquanto vários (vamos dizer 8) caixas que também são cozi Todo mundo na sua frente está esperando seus hambúrgueres ficarem prontos antes de deixar o caixa porque cada um dos 8 caixas vai e prepara o hambúrguer logo após receber o pedido, antes de pegar o próximo pedido. + + Então é finalmente sua vez, você pede 2 hambúrgueres muito saborosos para você e seu _crush_. Você paga 💸. + + O caixa vai para a cozinha. Você espera, na frente do balcão 🕙, para que ninguém pegue seus hambúrgueres antes de você, já que não tem números de chamadas. + + Como você e seu _crush_ estão ocupados não permitindo que ninguém passe na frente e pegue seus hambúrgueres assim que estiverem prontos, você não pode dar atenção ao seu _crush_. 😞 -Isso é trabalho "síncrono", você está "sincronizado" com o caixa / cozinheiro👨‍🍳. Você tem que esperar 🕙 e estar lá no exato momento que o caixa / cozinheiro 👨‍🍳 terminar os hambúrgueres e os der a você, ou então, outro alguém pode pegá-los. +Isso é trabalho "síncrono", você está "sincronizado" com o caixa / cozinheiro 👨‍🍳. Você tem que esperar 🕙 e estar lá no exato momento que o caixa / cozinheiro 👨‍🍳 terminar os hambúrgueres e os der a você, ou então, outro alguém pode pegá-los. + + Então seu caixa / cozinheiro 👨‍🍳 finalmente volta com seus hambúrgueres, depois de um longo tempo esperando 🕙 por eles em frente ao balcão. + + Você pega seus hambúrgueres e vai para a mesa com seu _crush_. Vocês comem os hambúrgueres, e o trabalho está terminado. ⏹ + + Não houve muita conversa ou flerte já que a maior parte do tempo foi gasto esperando 🕙 na frente do balcão. 😞 +/// info | Informação + +Belas ilustrações de Ketrina Thompson. 🎨 + +/// + --- -Nesse cenário dos hambúrgueres paralelos, você é um computador / programa com dois processadores (você e seu _crush_), ambos esperando 🕙 e dedicando sua atenção ⏯ "esperando no balcão" 🕙 por um bom tempo. +Nesse cenário dos hambúrgueres paralelos, você é um computador / programa 🤖 com dois processadores (você e seu _crush_), ambos esperando 🕙 e dedicando sua atenção ⏯ "esperando no balcão" 🕙 por um bom tempo. A lanchonete paralela tem 8 processadores (caixas / cozinheiros), enquanto a lanchonete dos hambúrgueres concorrentes tinha apenas 2 (um caixa e um cozinheiro). @@ -183,7 +221,7 @@ Ainda assim, a experiência final não foi a melhor. 😞 --- -Essa seria o equivalente paralelo à histório dos hambúrgueres. 🍔 +Essa seria o equivalente paralelo à história dos hambúrgueres. 🍔 Para um exemplo "mais real", imagine um banco. @@ -195,7 +233,7 @@ E você tinha que esperar 🕙 na fila por um longo tempo ou poderia perder a ve Você provavelmente não gostaria de levar seu _crush_ 😍 com você para um rolezinho no banco 🏦. -### Conclusão dos hambúrgueres +### Conclusão dos hambúrgueres { #burger-conclusion } Nesse cenário dos "hambúrgueres com seu _crush_", como tem muita espera, faz mais sentido ter um sistema concorrente ⏸🔀⏯. @@ -215,7 +253,7 @@ E esse é o mesmo nível de performance que você tem com o **FastAPI**. E como você pode ter paralelismo e assincronicidade ao mesmo tempo, você tem uma maior performance do que a maioria dos frameworks NodeJS testados e lado a lado com Go, que é uma linguagem compilada, mais próxima ao C (tudo graças ao Starlette). -### Concorrência é melhor que paralelismo? +### Concorrência é melhor que paralelismo? { #is-concurrency-better-than-parallelism } Não! Essa não é a moral da história. @@ -239,7 +277,7 @@ Mas nesse caso, se você trouxesse os 8 ex-caixas / cozinheiros / agora-faxineir Nesse cenário, cada um dos faxineiros (incluindo você) poderia ser um processador, fazendo a sua parte do trabalho. -E a maior parte do tempo de execução é tomada por trabalho real (ao invés de ficar esperando), e o trabalho em um computador é feito pela CPU. Eles chamam esses problemas de "limitados por CPU". +E a maior parte do tempo de execução é tomada por trabalho real (ao invés de ficar esperando), e o trabalho em um computador é feito pela CPU. Eles chamam esses problemas de "limitados por CPU". --- @@ -249,22 +287,20 @@ Por exemplo: * **Processamento de áudio** ou **imagem** * **Visão Computacional**: uma imagem é composta por milhões de pixels, cada pixel tem 3 valores / cores, processar isso normalmente exige alguma computação em todos esses pixels ao mesmo tempo +* **Aprendizado de Máquina**: Normalmente exige muita multiplicação de matrizes e vetores. Pense numa grande planilha com números e em multiplicar todos eles juntos e ao mesmo tempo. +* **Deep Learning**: Esse é um subcampo do Aprendizado de Máquina, então, o mesmo se aplica. A diferença é que não há apenas uma grande planilha com números para multiplicar, mas um grande conjunto delas, e em muitos casos, você utiliza um processador especial para construir e/ou usar esses modelos. -* **Machine Learning**: Normalmente exige muita multiplicação de matrizes e vetores. Pense numa grande planilha com números e em multiplicar todos eles juntos e ao mesmo tempo. - -* **Deep Learning**: Esse é um subcampo do Machine Learning, então, o mesmo se aplica. A diferença é que não há apenas uma grande planilha com números para multiplicar, mas um grande conjunto delas, e em muitos casos, você utiliza um processador especial para construir e/ou usar esses modelos. - -### Concorrência + Paralelismo: Web + Machine learning +### Concorrência + Paralelismo: Web + Aprendizado de Máquina { #concurrency-parallelism-web-machine-learning } Com **FastAPI** você pode levar a vantagem da concorrência que é muito comum para desenvolvimento web (o mesmo atrativo de NodeJS). -Mas você também pode explorar os benefícios do paralelismo e multiprocessamento (tendo múltiplos processadores rodando em paralelo) para trabalhos **limitados por CPU** como aqueles em sistemas de Machine Learning. +Mas você também pode explorar os benefícios do paralelismo e multiprocessamento (tendo múltiplos processadores rodando em paralelo) para trabalhos **limitados por CPU** como aqueles em sistemas de Aprendizado de Máquina. -Isso, somado ao simples fato que Python é a principal linguagem para **Data Science**, Machine Learning e especialmente Deep Learning, faz do FastAPI uma ótima escolha para APIs web e aplicações com Data Science / Machine Learning (entre muitas outras). +Isso, somado ao simples fato que Python é a principal linguagem para **Data Science**, Aprendizado de Máquina e especialmente Deep Learning, faz do FastAPI uma ótima escolha para APIs web e aplicações com Data Science / Aprendizado de Máquina (entre muitas outras). -Para ver como alcançar esse paralelismo em produção veja a seção sobre [Deployment](deployment/index.md){.internal-link target=_blank}. +Para ver como alcançar esse paralelismo em produção veja a seção sobre [Implantação](deployment/index.md){.internal-link target=_blank}. -## `async` e `await` +## `async` e `await` { #async-and-await } Versões modernas do Python têm um modo muito intuitivo para definir código assíncrono. Isso faz parecer do mesmo jeito do código normal "sequencial" e fazer a "espera" para você nos momentos certos. @@ -274,7 +310,7 @@ Quando tem uma operação que exigirá espera antes de dar os resultados e tem s burgers = await get_burgers(2) ``` -A chave aqui é o `await`. Ele diz ao Python que ele tem que esperar por `get_burgers(2)` finalizar suas coisas 🕙 antes de armazenar os resultados em `burgers`. Com isso, o Python saberá que ele pode ir e fazer outras coisas 🔀 ⏯ nesse meio tempo (como receber outra requisição). +A chave aqui é o `await`. Ele diz ao Python que ele tem que esperar ⏸ por `get_burgers(2)` finalizar suas coisas 🕙 antes de armazenar os resultados em `burgers`. Com isso, o Python saberá que ele pode ir e fazer outras coisas 🔀 ⏯ nesse meio tempo (como receber outra requisição). Para o `await` funcionar, tem que estar dentro de uma função que suporte essa assincronicidade. Para fazer isso, apenas declare a função com `async def`: @@ -306,18 +342,18 @@ burgers = get_burgers(2) Então, se você está usando uma biblioteca que diz que você pode chamá-la com `await`, você precisa criar as *funções de operação de rota* com `async def`, como em: -```Python hl_lines="2 3" +```Python hl_lines="2-3" @app.get('/burgers') async def read_burgers(): burgers = await get_burgers(2) return burgers ``` -### Mais detalhes técnicos +### Mais detalhes técnicos { #more-technical-details } Você deve ter observado que `await` pode ser usado somente dentro de funções definidas com `async def`. -Mas ao mesmo tempo, funções definidas com `async def` têm que ser "aguardadas". Então, funções com `async def` pdem ser chamadas somente dentro de funções definidas com `async def` também. +Mas ao mesmo tempo, funções definidas com `async def` têm que ser "aguardadas". Então, funções com `async def` podem ser chamadas somente dentro de funções definidas com `async def` também. Então, sobre o ovo e a galinha, como você chama a primeira função async? @@ -325,7 +361,7 @@ Se você estivar trabalhando com **FastAPI** não terá que se preocupar com iss Mas se você quiser usar `async` / `await` sem FastAPI, você também pode fazê-lo. -### Escreva seu próprio código assíncrono +### Escreva seu próprio código assíncrono { #write-your-own-async-code } Starlette (e **FastAPI**) são baseados no AnyIO, o que o torna compatível com ambos o asyncio da biblioteca padrão do Python, e o Trio. @@ -333,10 +369,9 @@ Em particular, você pode usar diretamente o AnyIO por ser altamente compatível e ganhar seus benefícios (e.g. *concorrência estruturada*). -Eu criei outra biblioteca em cima do AnyIO, como uma fina camada acima, para melhorar um pouco as anotações de tipo e obter melhor **autocompletar**, **erros de linha**, etc. Ela também possui uma introdução amigável e um tutorial para ajudar você a **entender** e escrever **seu próprio código async**: Asyncer. Seria particularmente útil se você precisar **combinar código async com código regular** (bloqueador/síncrono). +Eu criei outra biblioteca em cima do AnyIO, como uma fina camada acima, para melhorar um pouco as anotações de tipo e obter melhor **preenchimento automático**, **erros inline**, etc. Ela também possui uma introdução amigável e um tutorial para ajudar você a **entender** e escrever **seu próprio código async**: Asyncer. Seria particularmente útil se você precisar **combinar código async com código regular** (bloqueador/síncrono). - -### Outras formas de código assíncrono +### Outras formas de código assíncrono { #other-forms-of-asynchronous-code } Esse estilo de usar `async` e `await` é relativamente novo na linguagem. @@ -346,17 +381,17 @@ Essa mesma sintaxe (ou quase a mesma) foi também incluída recentemente em vers Mas antes disso, controlar código assíncrono era bem mais complexo e difícil. -Nas versões anteriores do Python, você poderia utilizar threads ou Gevent. Mas o código é bem mais complexo de entender, debugar, e pensar sobre. +Nas versões anteriores do Python, você poderia utilizar threads ou Gevent. Mas o código é bem mais complexo de entender, debugar, e pensar sobre. Nas versões anteriores do NodeJS / Navegador JavaScript, você utilizaria "callbacks". O que leva ao "inferno do callback". -## Corrotinas +## Corrotinas { #coroutines } **Corrotina** é apenas um jeito bonitinho para a coisa que é retornada de uma função `async def`. O Python sabe que é algo como uma função, que pode começar e que vai terminar em algum ponto, mas que pode ser pausada ⏸ internamente também, sempre que tiver um `await` dentro dela. Mas toda essa funcionalidade de código assíncrono com `async` e `await` é muitas vezes resumida como usando "corrotinas". É comparável ao principal recurso chave do Go, a "Gorrotina". -## Conclusão +## Conclusão { #conclusion } Vamos ver a mesma frase de cima: @@ -366,9 +401,9 @@ Isso pode fazer mais sentido agora. ✨ Tudo isso é o que empodera o FastAPI (através do Starlette) e que o faz ter uma performance tão impressionante. -## Detalhes muito técnicos +## Detalhes muito técnicos { #very-technical-details } -/// warning +/// warning | Atenção Você pode provavelmente pular isso. @@ -378,23 +413,23 @@ Se você tem certo conhecimento técnico (corrotinas, threads, blocking etc) e e /// -### Funções de operação de rota +### Funções de operação de rota { #path-operation-functions } Quando você declara uma *função de operação de rota* com `def` normal ao invés de `async def`, ela é rodada em uma threadpool externa que é então aguardada, ao invés de ser chamada diretamente (já que ela bloquearia o servidor). -Se você está chegando de outro framework assíncrono que não funciona como descrito acima e você está acostumado a definir *funções de operação de rota* triviais somente de computação com simples `def` para ter um mínimo ganho de performance (cerca de 100 nanosegundos), por favor observe que no **FastAPI** o efeito pode ser bem o oposto. Nesses casos, é melhor usar `async def` a menos que suas *funções de operação de rota* utilizem código que performe bloqueamento IO. +Se você está chegando de outro framework assíncrono que não funciona como descrito acima e você está acostumado a definir *funções de operação de rota* triviais somente de computação com simples `def` para ter um mínimo ganho de performance (cerca de 100 nanosegundos), por favor observe que no **FastAPI** o efeito pode ser bem o oposto. Nesses casos, é melhor usar `async def` a menos que suas *funções de operação de rota* utilizem código que performe bloqueamento I/O. Ainda, em ambas as situações, as chances são que o **FastAPI** [ainda será mais rápido](index.md#performance){.internal-link target=_blank} do que (ou ao menos comparável a) seu framework anterior. -### Dependências +### Dependências { #dependencies } O mesmo se aplica para as [dependências](tutorial/dependencies/index.md){.internal-link target=_blank}. Se uma dependência tem as funções com padrão `def` ao invés de `async def`, ela é rodada no threadpool externo. -### Sub-dependências +### Sub-dependências { #sub-dependencies } Você pode ter múltiplas dependências e [sub-dependências](tutorial/dependencies/sub-dependencies.md){.internal-link target=_blank} requisitando uma à outra (como parâmetros de definições de funções), algumas delas podem ser criadas com `async def` e algumas com `def` normal. Isso ainda funcionaria, e aquelas criadas com `def` normal seriam chamadas em uma thread externa (do threadpool) ao invés de serem "aguardadas". -### Outras funções de utilidade +### Outras funções de utilidade { #other-utility-functions } Qualquer outra função de utilidade que você chame diretamente pode ser criada com `def` normal ou `async def` e o FastAPI não irá afetar o modo como você a chama. diff --git a/docs/pt/docs/benchmarks.md b/docs/pt/docs/benchmarks.md index 07461ce46..c0b0c4c46 100644 --- a/docs/pt/docs/benchmarks.md +++ b/docs/pt/docs/benchmarks.md @@ -1,10 +1,10 @@ -# Comparações +# Benchmarks { #benchmarks } -As comparações independentes da TechEmpower mostram as aplicações **FastAPI** rodando com Uvicorn como um dos _frameworks_ Python mais rápidos disponíveis, somente atrás dos próprios Starlette e Uvicorn (utilizados internamente pelo FastAPI). (*) +Benchmarks independentes da TechEmpower mostram as aplicações **FastAPI** rodando com Uvicorn como um dos frameworks Python mais rápidos disponíveis, somente atrás dos próprios Starlette e Uvicorn (utilizados internamente pelo FastAPI). Mas quando se checa _benchmarks_ e comparações você deveria ter o seguinte em mente. -## Comparações e velocidade +## Benchmarks e velocidade { #benchmarks-and-speed } Ao verificar os _benchmarks_, é comum observar algumas ferramentas de diferentes tipos comparadas como equivalentes. diff --git a/docs/pt/docs/deployment/cloud.md b/docs/pt/docs/deployment/cloud.md index fc490db4d..3049a6ad0 100644 --- a/docs/pt/docs/deployment/cloud.md +++ b/docs/pt/docs/deployment/cloud.md @@ -1,13 +1,16 @@ -# Implantar FastAPI em provedores de nuvem +# Implantar FastAPI em provedores de nuvem { #deploy-fastapi-on-cloud-providers } Você pode usar praticamente **qualquer provedor de nuvem** para implantar seu aplicativo FastAPI. -Na maioria dos casos, os principais provedores de nuvem têm guias para implantar o FastAPI com eles. +Na maioria dos casos, os principais provedores de nuvem têm tutoriais para implantar o FastAPI com eles. -## Provedores de Nuvem - Patrocinadores +## Provedores de Nuvem - Patrocinadores { #cloud-providers-sponsors } Alguns provedores de nuvem ✨ [**patrocinam o FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨, o que garante o **desenvolvimento** contínuo e saudável do FastAPI e seu **ecossistema**. -E isso mostra seu verdadeiro comprometimento com o FastAPI e sua **comunidade** (você), pois eles não querem apenas fornecer a você um **bom serviço**, mas também querem ter certeza de que você tenha uma **estrutura boa e saudável**, o FastAPI. 🙇 +E isso mostra seu verdadeiro comprometimento com o FastAPI e sua **comunidade** (você), pois eles não querem apenas fornecer a você um **bom serviço**, mas também querem ter certeza de que você tenha um **framework bom e saudável**, o FastAPI. 🙇 -Talvez você queira experimentar os serviços deles e seguir os guias. +Talvez você queira experimentar os serviços deles e seguir os tutoriais: + +* Render +* Railway diff --git a/docs/pt/docs/deployment/concepts.md b/docs/pt/docs/deployment/concepts.md index 014ca3797..6af4b177a 100644 --- a/docs/pt/docs/deployment/concepts.md +++ b/docs/pt/docs/deployment/concepts.md @@ -1,4 +1,4 @@ -# Conceitos de Implantações +# Conceitos de Implantações { #deployments-concepts } Ao implantar um aplicativo **FastAPI**, ou na verdade, qualquer tipo de API da web, há vários conceitos com os quais você provavelmente se importa e, usando-os, você pode encontrar a maneira **mais apropriada** de **implantar seu aplicativo**. @@ -23,7 +23,7 @@ Nos próximos capítulos, darei a você mais **receitas concretas** para implant Mas por enquanto, vamos verificar essas importantes **ideias conceituais**. Esses conceitos também se aplicam a qualquer outro tipo de API da web. 💡 -## Segurança - HTTPS +## Segurança - HTTPS { #security-https } No [capítulo anterior sobre HTTPS](https.md){.internal-link target=_blank} aprendemos como o HTTPS fornece criptografia para sua API. @@ -31,7 +31,7 @@ Também vimos que o HTTPS normalmente é fornecido por um componente **externo** E tem que haver algo responsável por **renovar os certificados HTTPS**, pode ser o mesmo componente ou pode ser algo diferente. -### Ferramentas de exemplo para HTTPS +### Ferramentas de exemplo para HTTPS { #example-tools-for-https } Algumas das ferramentas que você pode usar como um proxy de terminação TLS são: @@ -55,11 +55,11 @@ Mostrarei alguns exemplos concretos nos próximos capítulos. Os próximos conceitos a serem considerados são todos sobre o programa que executa sua API real (por exemplo, Uvicorn). -## Programa e Processo +## Programa e Processo { #program-and-process } Falaremos muito sobre o "**processo**" em execução, então é útil ter clareza sobre o que ele significa e qual é a diferença com a palavra "**programa**". -### O que é um Programa +### O que é um Programa { #what-is-a-program } A palavra **programa** é comumente usada para descrever muitas coisas: @@ -67,7 +67,7 @@ A palavra **programa** é comumente usada para descrever muitas coisas: * O **arquivo** que pode ser **executado** pelo sistema operacional, por exemplo: `python`, `python.exe` ou `uvicorn`. * Um programa específico enquanto está **em execução** no sistema operacional, usando a CPU e armazenando coisas na memória. Isso também é chamado de **processo**. -### O que é um Processo +### O que é um Processo { #what-is-a-process } A palavra **processo** normalmente é usada de forma mais específica, referindo-se apenas ao que está sendo executado no sistema operacional (como no último ponto acima): @@ -88,11 +88,11 @@ E, por exemplo, você provavelmente verá que há vários processos executando o Agora que sabemos a diferença entre os termos **processo** e **programa**, vamos continuar falando sobre implantações. -## Executando na inicialização +## Executando na inicialização { #running-on-startup } Na maioria dos casos, quando você cria uma API web, você quer que ela esteja **sempre em execução**, ininterrupta, para que seus clientes possam sempre acessá-la. Isso é claro, a menos que você tenha um motivo específico para querer que ela seja executada somente em certas situações, mas na maioria das vezes você quer que ela esteja constantemente em execução e **disponível**. -### Em um servidor remoto +### Em um servidor remoto { #in-a-remote-server } Ao configurar um servidor remoto (um servidor em nuvem, uma máquina virtual, etc.), a coisa mais simples que você pode fazer é usar `fastapi run` (que usa Uvicorn) ou algo semelhante, manualmente, da mesma forma que você faz ao desenvolver localmente. @@ -102,15 +102,15 @@ Mas se sua conexão com o servidor for perdida, o **processo em execução** pro E se o servidor for reiniciado (por exemplo, após atualizações ou migrações do provedor de nuvem), você provavelmente **não notará**. E por causa disso, você nem saberá que precisa reiniciar o processo manualmente. Então, sua API simplesmente permanecerá inativa. 😱 -### Executar automaticamente na inicialização +### Executar automaticamente na inicialização { #run-automatically-on-startup } Em geral, você provavelmente desejará que o programa do servidor (por exemplo, Uvicorn) seja iniciado automaticamente na inicialização do servidor e, sem precisar de nenhuma **intervenção humana**, tenha um processo sempre em execução com sua API (por exemplo, Uvicorn executando seu aplicativo FastAPI). -### Programa separado +### Programa separado { #separate-program } Para conseguir isso, você normalmente terá um **programa separado** que garantiria que seu aplicativo fosse executado na inicialização. E em muitos casos, ele também garantiria que outros componentes ou aplicativos também fossem executados, por exemplo, um banco de dados. -### Ferramentas de exemplo para executar na inicialização +### Ferramentas de exemplo para executar na inicialização { #example-tools-to-run-at-startup } Alguns exemplos de ferramentas que podem fazer esse trabalho são: @@ -125,29 +125,29 @@ Alguns exemplos de ferramentas que podem fazer esse trabalho são: Darei exemplos mais concretos nos próximos capítulos. -## Reinicializações +## Reinicializações { #restarts } Semelhante a garantir que seu aplicativo seja executado na inicialização, você provavelmente também deseja garantir que ele seja **reiniciado** após falhas. -### Nós cometemos erros +### Nós cometemos erros { #we-make-mistakes } Nós, como humanos, cometemos **erros** o tempo todo. O software quase *sempre* tem **bugs** escondidos em lugares diferentes. 🐛 E nós, como desenvolvedores, continuamos aprimorando o código à medida que encontramos esses bugs e implementamos novos recursos (possivelmente adicionando novos bugs também 😅). -### Pequenos erros são tratados automaticamente +### Pequenos erros são tratados automaticamente { #small-errors-automatically-handled } Ao criar APIs da web com FastAPI, se houver um erro em nosso código, o FastAPI normalmente o conterá na única solicitação que acionou o erro. 🛡 O cliente receberá um **Erro Interno do Servidor 500** para essa solicitação, mas o aplicativo continuará funcionando para as próximas solicitações em vez de travar completamente. -### Erros maiores - Travamentos +### Erros maiores - Travamentos { #bigger-errors-crashes } No entanto, pode haver casos em que escrevemos algum código que **trava todo o aplicativo**, fazendo com que o Uvicorn e o Python travem. 💥 E ainda assim, você provavelmente não gostaria que o aplicativo permanecesse inativo porque houve um erro em um lugar, você provavelmente quer que ele **continue em execução** pelo menos para as *operações de caminho* que não estão quebradas. -### Reiniciar após falha +### Reiniciar após falha { #restart-after-crash } Mas nos casos com erros realmente graves que travam o **processo** em execução, você vai querer um componente externo que seja responsável por **reiniciar** o processo, pelo menos algumas vezes... @@ -161,7 +161,7 @@ Então, vamos nos concentrar nos casos principais, onde ele pode travar completa Você provavelmente gostaria de ter a coisa responsável por reiniciar seu aplicativo como um **componente externo**, porque a essa altura, o mesmo aplicativo com Uvicorn e Python já havia travado, então não há nada no mesmo código do mesmo aplicativo que possa fazer algo a respeito. -### Ferramentas de exemplo para reiniciar automaticamente +### Ferramentas de exemplo para reiniciar automaticamente { #example-tools-to-restart-automatically } Na maioria dos casos, a mesma ferramenta usada para **executar o programa na inicialização** também é usada para lidar com **reinicializações** automáticas. @@ -176,19 +176,19 @@ Por exemplo, isso poderia ser resolvido por: * Gerenciado internamente por um provedor de nuvem como parte de seus serviços * Outros... -## Replicação - Processos e Memória +## Replicação - Processos e Memória { #replication-processes-and-memory } Com um aplicativo FastAPI, usando um programa de servidor como o comando `fastapi` que executa o Uvicorn, executá-lo uma vez em **um processo** pode atender a vários clientes simultaneamente. Mas em muitos casos, você desejará executar vários processos de trabalho ao mesmo tempo. -### Processos Múltiplos - Trabalhadores +### Processos Múltiplos - Trabalhadores { #multiple-processes-workers } Se você tiver mais clientes do que um único processo pode manipular (por exemplo, se a máquina virtual não for muito grande) e tiver **vários núcleos** na CPU do servidor, você poderá ter **vários processos** em execução com o mesmo aplicativo ao mesmo tempo e distribuir todas as solicitações entre eles. Quando você executa **vários processos** do mesmo programa de API, eles são comumente chamados de **trabalhadores**. -### Processos do Trabalhador e Portas +### Processos do Trabalhador e Portas { #worker-processes-and-ports } Lembra da documentação [Sobre HTTPS](https.md){.internal-link target=_blank} que diz que apenas um processo pode escutar em uma combinação de porta e endereço IP em um servidor? @@ -196,19 +196,19 @@ Isso ainda é verdade. Então, para poder ter **vários processos** ao mesmo tempo, tem que haver um **único processo escutando em uma porta** que então transmite a comunicação para cada processo de trabalho de alguma forma. -### Memória por Processo +### Memória por Processo { #memory-per-process } Agora, quando o programa carrega coisas na memória, por exemplo, um modelo de aprendizado de máquina em uma variável, ou o conteúdo de um arquivo grande em uma variável, tudo isso **consome um pouco da memória (RAM)** do servidor. E vários processos normalmente **não compartilham nenhuma memória**. Isso significa que cada processo em execução tem suas próprias coisas, variáveis ​​e memória. E se você estiver consumindo uma grande quantidade de memória em seu código, **cada processo** consumirá uma quantidade equivalente de memória. -### Memória do servidor +### Memória do servidor { #server-memory } Por exemplo, se seu código carrega um modelo de Machine Learning com **1 GB de tamanho**, quando você executa um processo com sua API, ele consumirá pelo menos 1 GB de RAM. E se você iniciar **4 processos** (4 trabalhadores), cada um consumirá 1 GB de RAM. Então, no total, sua API consumirá **4 GB de RAM**. E se o seu servidor remoto ou máquina virtual tiver apenas 3 GB de RAM, tentar carregar mais de 4 GB de RAM causará problemas. 🚨 -### Processos Múltiplos - Um Exemplo +### Processos Múltiplos - Um Exemplo { #multiple-processes-an-example } Neste exemplo, há um **Processo Gerenciador** que inicia e controla dois **Processos de Trabalhadores**. @@ -224,7 +224,7 @@ Um detalhe interessante é que a porcentagem da **CPU usada** por cada processo Se você tiver uma API que faz uma quantidade comparável de cálculos todas as vezes e tiver muitos clientes, então a **utilização da CPU** provavelmente *também será estável* (em vez de ficar constantemente subindo e descendo rapidamente). -### Exemplos de ferramentas e estratégias de replicação +### Exemplos de ferramentas e estratégias de replicação { #examples-of-replication-tools-and-strategies } Pode haver várias abordagens para conseguir isso, e falarei mais sobre estratégias específicas nos próximos capítulos, por exemplo, ao falar sobre Docker e contêineres. @@ -247,7 +247,7 @@ Falarei mais sobre imagens de contêiner, Docker, Kubernetes, etc. em um capítu /// -## Etapas anteriores antes de começar +## Etapas anteriores antes de começar { #previous-steps-before-starting } Há muitos casos em que você deseja executar algumas etapas **antes de iniciar** sua aplicação. @@ -269,7 +269,7 @@ Nesse caso, você não precisaria se preocupar com nada disso. 🤷 /// -### Exemplos de estratégias de etapas anteriores +### Exemplos de estratégias de etapas anteriores { #examples-of-previous-steps-strategies } Isso **dependerá muito** da maneira como você **implanta seu sistema** e provavelmente estará conectado à maneira como você inicia programas, lida com reinicializações, etc. @@ -285,7 +285,7 @@ Darei exemplos mais concretos de como fazer isso com contêineres em um capítul /// -## Utilização de recursos +## Utilização de recursos { #resource-utilization } Seu(s) servidor(es) é(são) um **recurso** que você pode consumir ou **utilizar**, com seus programas, o tempo de computação nas CPUs e a memória RAM disponível. @@ -305,7 +305,7 @@ Você poderia colocar um **número arbitrário** para atingir, por exemplo, algo Você pode usar ferramentas simples como `htop` para ver a CPU e a RAM usadas no seu servidor ou a quantidade usada por cada processo. Ou você pode usar ferramentas de monitoramento mais complexas, que podem ser distribuídas entre servidores, etc. -## Recapitular +## Recapitular { #recap } Você leu aqui alguns dos principais conceitos que provavelmente precisa ter em mente ao decidir como implantar seu aplicativo: diff --git a/docs/pt/docs/deployment/docker.md b/docs/pt/docs/deployment/docker.md index cf18bb153..b26a69b54 100644 --- a/docs/pt/docs/deployment/docker.md +++ b/docs/pt/docs/deployment/docker.md @@ -1,4 +1,4 @@ -# FastAPI em contêineres - Docker +# FastAPI em contêineres - Docker { #fastapi-in-containers-docker } Ao fazer o deploy de aplicações FastAPI uma abordagem comum é construir uma **imagem de contêiner Linux**. Isso normalmente é feito usando o **Docker**. Você pode a partir disso fazer o deploy dessa imagem de algumas maneiras. @@ -6,7 +6,7 @@ Usando contêineres Linux você tem diversas vantagens incluindo **segurança**, /// tip | Dica -Está com pressa e já sabe dessas coisas? Pode ir direto para [`Dockerfile` abaixo 👇](#construindo-uma-imagem-docker-para-fastapi). +Está com pressa e já sabe dessas coisas? Pode ir direto para o [`Dockerfile` abaixo 👇](#build-a-docker-image-for-fastapi). /// @@ -24,25 +24,25 @@ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt COPY ./app /code/app -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] +CMD ["fastapi", "run", "app/main.py", "--port", "80"] -# If running behind a proxy like Nginx or Traefik add --proxy-headers -# CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80", "--proxy-headers"] +# Se estiver executando atrás de um proxy como Nginx ou Traefik, adicione --proxy-headers +# CMD ["fastapi", "run", "app/main.py", "--port", "80", "--proxy-headers"] ```
-## O que é um Contêiner +## O que é um Contêiner { #what-is-a-container } -Contêineres (especificamente contêineres Linux) são um jeito muito **leve** de empacotar aplicações contendo todas as dependências e arquivos necessários enquanto os mantém isolados de outros contêineres (outras aplicações ou componentes) no mesmo sistema. +Contêineres (principalmente contêineres Linux) são um jeito muito **leve** de empacotar aplicações contendo todas as dependências e arquivos necessários enquanto os mantém isolados de outros contêineres (outras aplicações ou componentes) no mesmo sistema. Contêineres Linux rodam usando o mesmo kernel Linux do hospedeiro (máquina, máquina virtual, servidor na nuvem, etc). Isso simplesmente significa que eles são muito leves (comparados com máquinas virtuais emulando um sistema operacional completo). Dessa forma, contêineres consomem **poucos recursos**, uma quantidade comparável com rodar os processos diretamente (uma máquina virtual consumiria muito mais). -Contêineres também possuem seus próprios processos (comumente um único processo), sistema de arquivos e rede **isolados** simplificando deploy, segurança, desenvolvimento, etc. +Contêineres também possuem seus próprios processos em execução (comumente **um único processo**), sistema de arquivos e rede **isolados**, simplificando deploy, segurança, desenvolvimento, etc. -## O que é uma Imagem de Contêiner +## O que é uma Imagem de Contêiner { #what-is-a-container-image } Um **contêiner** roda a partir de uma **imagem de contêiner**. @@ -56,7 +56,7 @@ Uma imagem de contêiner é comparável ao arquivo de **programa** e seus conte E o **contêiner** em si (em contraste à **imagem de contêiner**) é a própria instância da imagem rodando, comparável a um **processo**. Na verdade, um contêiner está rodando somente quando há um **processo rodando** (e normalmente é somente um processo). O contêiner finaliza quando não há um processo rodando nele. -## Imagens de contêiner +## Imagens de contêiner { #container-images } Docker tem sido uma das principais ferramentas para criar e gerenciar **imagens de contêiner** e **contêineres**. @@ -71,15 +71,15 @@ E existe muitas outras imagens para diferentes coisas, como bancos de dados, por * MongoDB * Redis, etc. -Usando imagens de contêiner pré-prontas é muito fácil **combinar** e usar diferentes ferramentas. Por exemplo, para testar um novo banco de dados. Em muitos casos, você pode usar as **imagens oficiais** precisando somente de variáveis de ambiente para configurá-las. +Usando imagens de contêiner pré-prontas é muito fácil **combinar** e usar diferentes ferramentas. Por exemplo, para testar um novo banco de dados. Em muitos casos, você pode usar as **imagens oficiais**, precisando somente de variáveis de ambiente para configurá-las. -Dessa forma, em muitos casos você pode aprender sobre contêineres e Docker e re-usar essa experiência com diversos componentes e ferramentas. +Dessa forma, em muitos casos você pode aprender sobre contêineres e Docker e reusar essa experiência com diversos componentes e ferramentas. Então, você rodaria **vários contêineres** com coisas diferentes, como um banco de dados, uma aplicação Python, um servidor web com uma aplicação frontend React, e conectá-los juntos via sua rede interna. Todos os sistemas de gerenciamento de contêineres (como Docker ou Kubernetes) possuem essas funcionalidades de rede integradas a eles. -## Contêineres e Processos +## Contêineres e Processos { #containers-and-processes } Uma **imagem de contêiner** normalmente inclui em seus metadados o programa padrão ou comando que deve ser executado quando o **contêiner** é iniciado e os parâmetros a serem passados para esse programa. Muito similar ao que seria se estivesse na linha de comando. @@ -91,11 +91,11 @@ Um contêiner normalmente tem um **único processo**, mas também é possível i Mas não é possível ter um contêiner rodando sem **pelo menos um processo rodando**. Se o processo principal parar, o contêiner também para. -## Construindo uma Imagem Docker para FastAPI +## Construir uma Imagem Docker para FastAPI { #build-a-docker-image-for-fastapi } Okay, vamos construir algo agora! 🚀 -Eu vou mostrar como construir uma **imagem Docker** para FastAPI **do zero**, baseado na **imagem oficial do Python**. +Eu vou mostrar como construir uma **imagem Docker** para FastAPI **do zero**, baseada na imagem **oficial do Python**. Isso é o que você quer fazer na **maioria dos casos**, por exemplo: @@ -103,22 +103,21 @@ Isso é o que você quer fazer na **maioria dos casos**, por exemplo: * Quando rodando em uma **Raspberry Pi** * Usando um serviço em nuvem que irá rodar uma imagem de contêiner para você, etc. -### O Pacote Requirements +### Requisitos de Pacotes { #package-requirements } -Você normalmente teria os **requisitos do pacote** para sua aplicação em algum arquivo. +Você normalmente teria os **requisitos de pacotes** da sua aplicação em algum arquivo. Isso pode depender principalmente da ferramenta que você usa para **instalar** esses requisitos. -O caminho mais comum de fazer isso é ter um arquivo `requirements.txt` com os nomes dos pacotes e suas versões, um por linha. +A forma mais comum de fazer isso é ter um arquivo `requirements.txt` com os nomes dos pacotes e suas versões, um por linha. -Você, naturalmente, usaria as mesmas ideias que você leu em [Sobre Versões do FastAPI](versions.md){.internal-link target=_blank} para definir os intervalos de versões. +Você, naturalmente, usaria as mesmas ideias que você leu em [Sobre versões do FastAPI](versions.md){.internal-link target=_blank} para definir os intervalos de versões. Por exemplo, seu `requirements.txt` poderia parecer com: ``` -fastapi>=0.68.0,<0.69.0 -pydantic>=1.8.0,<2.0.0 -uvicorn>=0.15.0,<0.16.0 +fastapi[standard]>=0.113.0,<0.114.0 +pydantic>=2.7.0,<3.0.0 ``` E você normalmente instalaria essas dependências de pacote com `pip`, por exemplo: @@ -128,27 +127,25 @@ E você normalmente instalaria essas dependências de pacote com `pip`, por exem ```console $ pip install -r requirements.txt ---> 100% -Successfully installed fastapi pydantic uvicorn +Successfully installed fastapi pydantic ``` -/// info +/// info | Informação -Há outros formatos e ferramentas para definir e instalar dependências de pacote. - -Eu vou mostrar um exemplo depois usando Poetry em uma seção abaixo. 👇 +Há outros formatos e ferramentas para definir e instalar dependências de pacotes. /// -### Criando o Código do **FastAPI** +### Crie o código do **FastAPI** { #create-the-fastapi-code } * Crie um diretório `app` e entre nele. * Crie um arquivo vazio `__init__.py`. * Crie um arquivo `main.py` com: ```Python -from typing import Optional +from typing import Union from fastapi import FastAPI @@ -165,28 +162,28 @@ def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} ``` -### Dockerfile +### Dockerfile { #dockerfile } Agora, no mesmo diretório do projeto, crie um arquivo `Dockerfile` com: ```{ .dockerfile .annotate } -# (1) +# (1)! FROM python:3.9 -# (2) +# (2)! WORKDIR /code -# (3) +# (3)! COPY ./requirements.txt /code/requirements.txt -# (4) +# (4)! RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt -# (5) +# (5)! COPY ./app /code/app -# (6) -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] +# (6)! +CMD ["fastapi", "run", "app/main.py", "--port", "80"] ``` 1. Inicie a partir da imagem base oficial do Python. @@ -205,7 +202,7 @@ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] A opção `--no-cache-dir` diz ao `pip` para não salvar os pacotes baixados localmente, pois isso só aconteceria se `pip` fosse executado novamente para instalar os mesmos pacotes, mas esse não é o caso quando trabalhamos com contêineres. - /// note + /// note | Nota `--no-cache-dir` é apenas relacionado ao `pip`, não tem nada a ver com Docker ou contêineres. @@ -223,21 +220,51 @@ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] Então, é importante colocar isso **perto do final** do `Dockerfile`, para otimizar o tempo de construção da imagem do contêiner. -6. Defina o **comando** para rodar o servidor `uvicorn`. +6. Defina o **comando** para usar `fastapi run`, que utiliza o Uvicorn por baixo dos panos. `CMD` recebe uma lista de strings, cada uma dessas strings é o que você digitaria na linha de comando separado por espaços. Esse comando será executado a partir do **diretório de trabalho atual**, o mesmo diretório `/code` que você definiu acima com `WORKDIR /code`. - Porque o programa será iniciado em `/code` e dentro dele está o diretório `./app` com seu código, o **Uvicorn** será capaz de ver e **importar** `app` de `app.main`. - -/// tip +/// tip | Dica Revise o que cada linha faz clicando em cada bolha com o número no código. 👆 /// -Agora você deve ter uma estrutura de diretório como: +/// warning | Atenção + +Certifique-se de **sempre** usar a **forma exec** da instrução `CMD`, como explicado abaixo. + +/// + +#### Use `CMD` - Forma Exec { #use-cmd-exec-form } + +A instrução `CMD` no Docker pode ser escrita de duas formas: + +✅ Forma **Exec**: + +```Dockerfile +# ✅ Faça assim +CMD ["fastapi", "run", "app/main.py", "--port", "80"] +``` + +⛔️ Forma **Shell**: + +```Dockerfile +# ⛔️ Não faça assim +CMD fastapi run app/main.py --port 80 +``` + +Garanta que você sempre use a forma **exec** para assegurar que o FastAPI consiga encerrar graciosamente e que os [eventos de lifespan](../advanced/events.md){.internal-link target=_blank} sejam disparados. + +Você pode ler mais na documentação do Docker sobre as formas shell e exec. + +Isso pode ser bem perceptível ao usar `docker compose`. Veja esta seção de FAQ do Docker Compose para mais detalhes técnicos: Por que meus serviços demoram 10 segundos para recriar ou parar?. + +#### Estrutura de diretórios { #directory-structure } + +Agora você deve haver uma estrutura de diretório como: ``` . @@ -248,15 +275,15 @@ Agora você deve ter uma estrutura de diretório como: └── requirements.txt ``` -#### Por Trás de um Proxy de Terminação TLS +#### Por trás de um Proxy de Terminação TLS { #behind-a-tls-termination-proxy } -Se você está executando seu contêiner atrás de um Proxy de Terminação TLS (load balancer) como Nginx ou Traefik, adicione a opção `--proxy-headers`, isso fará com que o Uvicorn confie nos cabeçalhos enviados por esse proxy, informando que o aplicativo está sendo executado atrás do HTTPS, etc. +Se você está executando seu contêiner atrás de um Proxy de Terminação TLS (load balancer) como Nginx ou Traefik, adicione a opção `--proxy-headers`, isso fará com que o Uvicorn (pela CLI do FastAPI) confie nos cabeçalhos enviados por esse proxy, informando que o aplicativo está sendo executado atrás do HTTPS, etc. ```Dockerfile -CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"] +CMD ["fastapi", "run", "app/main.py", "--proxy-headers", "--port", "80"] ``` -#### Cache Docker +#### Cache Docker { #docker-cache } Existe um truque importante nesse `Dockerfile`, primeiro copiamos o **arquivo com as dependências sozinho**, não o resto do código. Deixe-me te contar o porquê disso. @@ -288,7 +315,7 @@ A partir daí, perto do final do `Dockerfile`, copiamos todo o código. Como iss COPY ./app /code/app ``` -### Construindo a Imagem Docker +### Construa a Imagem Docker { #build-the-docker-image } Agora que todos os arquivos estão no lugar, vamos construir a imagem do contêiner. @@ -305,7 +332,7 @@ $ docker build -t myimage . -/// tip +/// tip | Dica Note o `.` no final, é equivalente a `./`, ele diz ao Docker o diretório a ser usado para construir a imagem do contêiner. @@ -313,19 +340,19 @@ Nesse caso, é o mesmo diretório atual (`.`). /// -### Inicie o contêiner Docker +### Inicie o Contêiner Docker { #start-the-docker-container } * Execute um contêiner baseado na sua imagem:
```console -$ docker run -d --name mycontêiner -p 80:80 myimage +$ docker run -d --name mycontainer -p 80:80 myimage ```
-## Verifique +## Verifique { #check-it } Você deve ser capaz de verificar isso no URL do seu contêiner Docker, por exemplo: http://192.168.99.100/items/5?q=somequery ou http://127.0.0.1/items/5?q=somequery (ou equivalente, usando seu host Docker). @@ -335,7 +362,7 @@ Você verá algo como: {"item_id": 5, "q": "somequery"} ``` -## Documentação interativa da API +## Documentação interativa da API { #interactive-api-docs } Agora você pode ir para http://192.168.99.100/docs ou http://127.0.0.1/docs (ou equivalente, usando seu host Docker). @@ -343,7 +370,7 @@ Você verá a documentação interativa automática da API (fornecida pelo http://192.168.99.100/redoc ou http://127.0.0.1/redoc (ou equivalente, usando seu host Docker). @@ -351,7 +378,7 @@ Você verá a documentação alternativa automática (fornecida pela Traefik, lidando com **HTTPS** e aquisição **automática** de **certificados**. -/// tip +/// tip | Dica -Traefik tem integrações com Docker, Kubernetes e outros, portanto, é muito fácil configurar e configurar o HTTPS para seus contêineres com ele. +Traefik tem integrações com Docker, Kubernetes e outros, portanto, é muito fácil configurar o HTTPS para seus contêineres com ele. /// Alternativamente, o HTTPS poderia ser tratado por um provedor de nuvem como um de seus serviços (enquanto ainda executasse o aplicativo em um contêiner). -## Executando na inicialização e reinicializações +## Executando na inicialização e reinicializações { #running-on-startup-and-restarts } Normalmente, outra ferramenta é responsável por **iniciar e executar** seu contêiner. @@ -427,21 +454,21 @@ Na maioria (ou em todos) os casos, há uma opção simples para habilitar a exec Sem usar contêineres, fazer aplicativos executarem na inicialização e com reinicializações pode ser trabalhoso e difícil. Mas quando **trabalhando com contêineres** em muitos casos essa funcionalidade é incluída por padrão. ✨ -## Replicação - Número de Processos +## Replicação - Número de Processos { #replication-number-of-processes } -Se você tiver um cluster de máquinas com **Kubernetes**, Docker Swarm Mode, Nomad ou outro sistema complexo semelhante para gerenciar contêineres distribuídos em várias máquinas, então provavelmente desejará **lidar com a replicação** no **nível do cluster** em vez de usar um **gerenciador de processos** (como o Gunicorn com workers) em cada contêiner. +Se você tiver um cluster de máquinas com **Kubernetes**, Docker Swarm Mode, Nomad ou outro sistema complexo semelhante para gerenciar contêineres distribuídos em várias máquinas, então provavelmente desejará **lidar com a replicação** no **nível do cluster** em vez de usar um **gerenciador de processos** (como Uvicorn com workers) em cada contêiner. Um desses sistemas de gerenciamento de contêineres distribuídos como o Kubernetes normalmente tem alguma maneira integrada de lidar com a **replicação de contêineres** enquanto ainda oferece **balanceamento de carga** para as solicitações recebidas. Tudo no **nível do cluster**. -Nesses casos, você provavelmente desejará criar uma **imagem do contêiner do zero** como [explicado acima](#dockerfile), instalando suas dependências e executando **um único processo Uvicorn** em vez de executar algo como Gunicorn com trabalhadores Uvicorn. +Nesses casos, você provavelmente desejará criar uma **imagem Docker do zero** como [explicado acima](#dockerfile), instalando suas dependências e executando **um único processo Uvicorn** em vez de usar múltiplos workers do Uvicorn. -### Balanceamento de Carga +### Balanceador de Carga { #load-balancer } Quando usando contêineres, normalmente você terá algum componente **escutando na porta principal**. Poderia ser outro contêiner que também é um **Proxy de Terminação TLS** para lidar com **HTTPS** ou alguma ferramenta semelhante. -Como esse componente assumiria a **carga** de solicitações e distribuiria isso entre os trabalhadores de uma maneira (esperançosamente) **balanceada**, ele também é comumente chamado de **Balanceador de Carga**. +Como esse componente assumiria a **carga** de solicitações e distribuiria isso entre os workers de uma maneira (esperançosamente) **balanceada**, ele também é comumente chamado de **Balanceador de Carga**. -/// tip +/// tip | Dica O mesmo componente **Proxy de Terminação TLS** usado para HTTPS provavelmente também seria um **Balanceador de Carga**. @@ -449,9 +476,9 @@ O mesmo componente **Proxy de Terminação TLS** usado para HTTPS provavelmente E quando trabalhar com contêineres, o mesmo sistema que você usa para iniciar e gerenciá-los já terá ferramentas internas para transmitir a **comunicação de rede** (por exemplo, solicitações HTTP) do **balanceador de carga** (que também pode ser um **Proxy de Terminação TLS**) para o(s) contêiner(es) com seu aplicativo. -### Um Balanceador de Carga - Múltiplos Contêineres de Workers +### Um Balanceador de Carga - Múltiplos Contêineres de Workers { #one-load-balancer-multiple-worker-containers } -Quando trabalhando com **Kubernetes** ou sistemas similares de gerenciamento de contêiner distribuído, usando seus mecanismos de rede internos permitiria que o único **balanceador de carga** que estivesse escutando na **porta principal** transmitisse comunicação (solicitações) para possivelmente **múltiplos contêineres** executando seu aplicativo. +Quando trabalhando com **Kubernetes** ou sistemas similares de gerenciamento de contêiner distribuído, usar seus mecanismos de rede internos permite que o único **balanceador de carga** que está escutando na **porta principal** transmita a comunicação (solicitações) para possivelmente **múltiplos contêineres** executando seu aplicativo. Cada um desses contêineres executando seu aplicativo normalmente teria **apenas um processo** (ex.: um processo Uvicorn executando seu aplicativo FastAPI). Todos seriam **contêineres idênticos**, executando a mesma coisa, mas cada um com seu próprio processo, memória, etc. Dessa forma, você aproveitaria a **paralelização** em **núcleos diferentes** da CPU, ou até mesmo em **máquinas diferentes**. @@ -459,54 +486,61 @@ E o sistema de contêiner com o **balanceador de carga** iria **distribuir as so E normalmente esse **balanceador de carga** seria capaz de lidar com solicitações que vão para *outros* aplicativos em seu cluster (por exemplo, para um domínio diferente, ou sob um prefixo de URL diferente), e transmitiria essa comunicação para os contêineres certos para *esse outro* aplicativo em execução em seu cluster. -### Um Processo por Contêiner +### Um Processo por Contêiner { #one-process-per-container } Nesse tipo de cenário, provavelmente você desejará ter **um único processo (Uvicorn) por contêiner**, pois já estaria lidando com a replicação no nível do cluster. -Então, nesse caso, você **não** desejará ter um gerenciador de processos como o Gunicorn com trabalhadores Uvicorn, ou o Uvicorn usando seus próprios trabalhadores Uvicorn. Você desejará ter apenas um **único processo Uvicorn** por contêiner (mas provavelmente vários contêineres). +Então, nesse caso, você **não** desejará ter múltiplos workers no contêiner, por exemplo com a opção de linha de comando `--workers`. Você desejará ter apenas um **único processo Uvicorn** por contêiner (mas provavelmente vários contêineres). -Tendo outro gerenciador de processos dentro do contêiner (como seria com o Gunicorn ou o Uvicorn gerenciando trabalhadores Uvicorn) só adicionaria **complexidade desnecessária** que você provavelmente já está cuidando com seu sistema de cluster. +Ter outro gerenciador de processos dentro do contêiner (como seria com múltiplos workers) só adicionaria **complexidade desnecessária** que você provavelmente já está cuidando com seu sistema de cluster. -### Contêineres com Múltiplos Processos e Casos Especiais +### Contêineres com Múltiplos Processos e Casos Especiais { #containers-with-multiple-processes-and-special-cases } -Claro, existem **casos especiais** em que você pode querer ter um **contêiner** com um **gerenciador de processos Gunicorn** iniciando vários **processos trabalhadores Uvicorn** dentro. +Claro, existem **casos especiais** em que você pode querer ter **um contêiner** com vários **processos workers do Uvicorn** dentro. -Nesses casos, você pode usar a **imagem oficial do Docker** que inclui o **Gunicorn** como um gerenciador de processos executando vários **processos trabalhadores Uvicorn**, e algumas configurações padrão para ajustar o número de trabalhadores com base nos atuais núcleos da CPU automaticamente. Eu vou te contar mais sobre isso abaixo em [Imagem Oficial do Docker com Gunicorn - Uvicorn](#imagem-oficial-do-docker-com-gunicorn-uvicorn). +Nesses casos, você pode usar a opção de linha de comando `--workers` para definir o número de workers que deseja executar: + +```{ .dockerfile .annotate } +FROM python:3.9 + +WORKDIR /code + +COPY ./requirements.txt /code/requirements.txt + +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt + +COPY ./app /code/app + +# (1)! +CMD ["fastapi", "run", "app/main.py", "--port", "80", "--workers", "4"] +``` + +1. Aqui usamos a opção de linha de comando `--workers` para definir o número de workers como 4. Aqui estão alguns exemplos de quando isso pode fazer sentido: -#### Um Aplicativo Simples +#### Um Aplicativo Simples { #a-simple-app } -Você pode querer um gerenciador de processos no contêiner se seu aplicativo for **simples o suficiente** para que você não precise (pelo menos não agora) ajustar muito o número de processos, e você pode simplesmente usar um padrão automatizado (com a imagem oficial do Docker), e você está executando em um **único servidor**, não em um cluster. +Você pode querer um gerenciador de processos no contêiner se seu aplicativo for **simples o suficiente** para rodar em um **único servidor**, não em um cluster. -#### Docker Compose +#### Docker Compose { #docker-compose } Você pode estar implantando em um **único servidor** (não em um cluster) com o **Docker Compose**, então você não teria uma maneira fácil de gerenciar a replicação de contêineres (com o Docker Compose) enquanto preserva a rede compartilhada e o **balanceamento de carga**. -Então você pode querer ter **um único contêiner** com um **gerenciador de processos** iniciando **vários processos trabalhadores** dentro. - -#### Prometheus and Outros Motivos - -Você também pode ter **outros motivos** que tornariam mais fácil ter um **único contêiner** com **múltiplos processos** em vez de ter **múltiplos contêineres** com **um único processo** em cada um deles. - -Por exemplo (dependendo de sua configuração), você poderia ter alguma ferramenta como um exportador do Prometheus no mesmo contêiner que deve ter acesso a **cada uma das solicitações** que chegam. - -Nesse caso, se você tivesse **múltiplos contêineres**, por padrão, quando o Prometheus fosse **ler as métricas**, ele receberia as métricas de **um único contêiner cada vez** (para o contêiner que tratou essa solicitação específica), em vez de receber as **métricas acumuladas** de todos os contêineres replicados. - -Então, nesse caso, poderia ser mais simples ter **um único contêiner** com **múltiplos processos**, e uma ferramenta local (por exemplo, um exportador do Prometheus) no mesmo contêiner coletando métricas do Prometheus para todos os processos internos e expor essas métricas no único contêiner. +Então você pode querer ter **um único contêiner** com um **gerenciador de processos** iniciando **vários processos workers** dentro. --- -O ponto principal é que **nenhum** desses são **regras escritas em pedra** que você deve seguir cegamente. Você pode usar essas idéias para **avaliar seu próprio caso de uso** e decidir qual é a melhor abordagem para seu sistema, verificando como gerenciar os conceitos de: +O ponto principal é que **nenhum** desses são **regras escritas em pedra** que você deve seguir cegamente. Você pode usar essas ideias para **avaliar seu próprio caso de uso** e decidir qual é a melhor abordagem para seu sistema, verificando como gerenciar os conceitos de: * Segurança - HTTPS * Executando na inicialização * Reinicializações * Replicação (o número de processos em execução) * Memória -* Passos anteriores antes de inicializar +* Passos anteriores antes de iniciar -## Memória +## Memória { #memory } Se você executar **um único processo por contêiner**, terá uma quantidade mais ou menos bem definida, estável e limitada de memória consumida por cada um desses contêineres (mais de um se eles forem replicados). @@ -514,17 +548,17 @@ E então você pode definir esses mesmos limites e requisitos de memória em sua Se sua aplicação for **simples**, isso provavelmente **não será um problema**, e você pode não precisar especificar limites de memória rígidos. Mas se você estiver **usando muita memória** (por exemplo, com **modelos de aprendizado de máquina**), deve verificar quanta memória está consumindo e ajustar o **número de contêineres** que executa em **cada máquina** (e talvez adicionar mais máquinas ao seu cluster). -Se você executar **múltiplos processos por contêiner** (por exemplo, com a imagem oficial do Docker), deve garantir que o número de processos iniciados não **consuma mais memória** do que o disponível. +Se você executar **múltiplos processos por contêiner**, deve garantir que o número de processos iniciados não **consuma mais memória** do que o disponível. -## Passos anteriores antes de inicializar e contêineres +## Passos anteriores antes de iniciar e contêineres { #previous-steps-before-starting-and-containers } Se você estiver usando contêineres (por exemplo, Docker, Kubernetes), existem duas abordagens principais que você pode usar. -### Contêineres Múltiplos +### Contêineres Múltiplos { #multiple-containers } -Se você tiver **múltiplos contêineres**, provavelmente cada um executando um **único processo** (por exemplo, em um cluster do **Kubernetes**), então provavelmente você gostaria de ter um **contêiner separado** fazendo o trabalho dos **passos anteriores** em um único contêiner, executando um único processo, **antes** de executar os contêineres trabalhadores replicados. +Se você tiver **múltiplos contêineres**, provavelmente cada um executando um **único processo** (por exemplo, em um cluster do **Kubernetes**), então provavelmente você gostaria de ter um **contêiner separado** fazendo o trabalho dos **passos anteriores** em um único contêiner, executando um único processo, **antes** de executar os contêineres workers replicados. -/// info +/// info | Informação Se você estiver usando o Kubernetes, provavelmente será um Init Container. @@ -532,85 +566,29 @@ Se você estiver usando o Kubernetes, provavelmente será um tiangolo/uvicorn-gunicorn-fastapi. Mas agora ela está descontinuada. ⛔️ -Essa imagem seria útil principalmente nas situações descritas acima em: [Contêineres com Múltiplos Processos e Casos Especiais](#conteineres-com-multiplos-processos-e-casos-especiais). +Você provavelmente **não** deve usar essa imagem base do Docker (ou qualquer outra semelhante). -* tiangolo/uvicorn-gunicorn-fastapi. +Se você está usando **Kubernetes** (ou outros) e já está definindo a **replicação** no nível do cluster, com vários **contêineres**. Nesses casos, é melhor **construir uma imagem do zero** como descrito acima: [Construir uma Imagem Docker para FastAPI](#build-a-docker-image-for-fastapi). -/// warning +E se você precisar ter múltiplos workers, você pode simplesmente usar a opção de linha de comando `--workers`. -Existe uma grande chance de que você **não** precise dessa imagem base ou de qualquer outra semelhante, e seria melhor construir a imagem do zero, como [descrito acima em: Construa uma Imagem Docker para o FastAPI](#construindo-uma-imagem-docker-para-fastapi). +/// note | Detalhes Técnicos + +A imagem Docker foi criada quando o Uvicorn não suportava gerenciar e reiniciar workers mortos, então era necessário usar o Gunicorn com o Uvicorn, o que adicionava bastante complexidade, apenas para que o Gunicorn gerenciasse e reiniciasse os processos workers do Uvicorn. + +Mas agora que o Uvicorn (e o comando `fastapi`) suportam o uso de `--workers`, não há razão para usar uma imagem base do Docker em vez de construir a sua própria (é praticamente a mesma quantidade de código 😅). /// -Essa imagem tem um mecanismo de **auto-ajuste** incluído para definir o **número de processos trabalhadores** com base nos núcleos de CPU disponíveis. - -Isso tem **padrões sensíveis**, mas você ainda pode alterar e atualizar todas as configurações com **variáveis de ambiente** ou arquivos de configuração. - -Há também suporte para executar **passos anteriores antes de iniciar** com um script. - -/// tip - -Para ver todas as configurações e opções, vá para a página da imagem Docker: tiangolo/uvicorn-gunicorn-fastapi. - -/// - -### Número de Processos na Imagem Oficial do Docker - -O **número de processos** nesta imagem é **calculado automaticamente** a partir dos **núcleos de CPU** disponíveis. - -Isso significa que ele tentará **aproveitar** o máximo de **desempenho** da CPU possível. - -Você também pode ajustá-lo com as configurações usando **variáveis de ambiente**, etc. - -Mas isso também significa que, como o número de processos depende da CPU do contêiner em execução, a **quantidade de memória consumida** também dependerá disso. - -Então, se seu aplicativo consumir muito memória (por exemplo, com modelos de aprendizado de máquina), e seu servidor tiver muitos núcleos de CPU **mas pouca memória**, então seu contêiner pode acabar tentando usar mais memória do que está disponível e degradar o desempenho muito (ou até mesmo travar). 🚨 - -### Criando um `Dockerfile` - -Aqui está como você criaria um `Dockerfile` baseado nessa imagem: - -```Dockerfile -FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9 - -COPY ./requirements.txt /app/requirements.txt - -RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt - -COPY ./app /app -``` - -### Aplicações Maiores - -Se você seguiu a seção sobre a criação de [Aplicações Maiores com Múltiplos Arquivos](../tutorial/bigger-applications.md){.internal-link target=_blank}, seu `Dockerfile` pode parecer com isso: - -```Dockerfile - -```Dockerfile hl_lines="7" -FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9 - -COPY ./requirements.txt /app/requirements.txt - -RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt - -COPY ./app /app/app -``` - -### Quando Usar - -Você provavelmente **não** deve usar essa imagem base oficial (ou qualquer outra semelhante) se estiver usando **Kubernetes** (ou outros) e já estiver definindo **replicação** no nível do cluster, com vários **contêineres**. Nesses casos, é melhor **construir uma imagem do zero** conforme descrito acima: [Construindo uma Imagem Docker para FastAPI](#construindo-uma-imagem-docker-para-fastapi). - -Essa imagem seria útil principalmente nos casos especiais descritos acima em [Contêineres com Múltiplos Processos e Casos Especiais](#conteineres-com-multiplos-processos-e-casos-especiais). Por exemplo, se sua aplicação for **simples o suficiente** para que a configuração padrão de número de processos com base na CPU funcione bem, você não quer se preocupar com a configuração manual da replicação no nível do cluster e não está executando mais de um contêiner com seu aplicativo. Ou se você estiver implantando com **Docker Compose**, executando em um único servidor, etc. - -## Deploy da Imagem do Contêiner +## Deploy da Imagem do Contêiner { #deploy-the-container-image } Depois de ter uma imagem de contêiner (Docker), existem várias maneiras de implantá-la. @@ -622,100 +600,11 @@ Por exemplo: * Com outra ferramenta como o Nomad * Com um serviço de nuvem que pega sua imagem de contêiner e a implanta -## Imagem Docker com Poetry +## Imagem Docker com `uv` { #docker-image-with-uv } -Se você usa Poetry para gerenciar as dependências do seu projeto, pode usar a construção multi-estágio do Docker: +Se você está usando o uv para instalar e gerenciar seu projeto, você pode seguir o guia de Docker do uv. -```{ .dockerfile .annotate } -# (1) -FROM python:3.9 as requirements-stage - -# (2) -WORKDIR /tmp - -# (3) -RUN pip install poetry - -# (4) -COPY ./pyproject.toml ./poetry.lock* /tmp/ - -# (5) -RUN poetry export -f requirements.txt --output requirements.txt --without-hashes - -# (6) -FROM python:3.9 - -# (7) -WORKDIR /code - -# (8) -COPY --from=requirements-stage /tmp/requirements.txt /code/requirements.txt - -# (9) -RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt - -# (10) -COPY ./app /code/app - -# (11) -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] -``` - -1. Esse é o primeiro estágio, ele é chamado `requirements-stage`. - -2. Defina `/tmp` como o diretório de trabalho atual. - - Aqui é onde geraremos o arquivo `requirements.txt` - -3. Instale o Poetry nesse estágio do Docker. - -4. Copie os arquivos `pyproject.toml` e `poetry.lock` para o diretório `/tmp`. - - Porque está usando `./poetry.lock*` (terminando com um `*`), não irá falhar se esse arquivo ainda não estiver disponível. - -5. Gere o arquivo `requirements.txt`. - -6. Este é o estágio final, tudo aqui será preservado na imagem final do contêiner. - -7. Defina o diretório de trabalho atual como `/code`. - -8. Copie o arquivo `requirements.txt` para o diretório `/code`. - - Essse arquivo só existe no estágio anterior do Docker, é por isso que usamos `--from-requirements-stage` para copiá-lo. - -9. Instale as dependências de pacote do arquivo `requirements.txt` gerado. - -10. Copie o diretório `app` para o diretório `/code`. - -11. Execute o comando `uvicorn`, informando-o para usar o objeto `app` importado de `app.main`. - -/// tip - -Clique nos números das bolhas para ver o que cada linha faz. - -/// - -Um **estágio do Docker** é uma parte de um `Dockerfile` que funciona como uma **imagem temporária do contêiner** que só é usada para gerar alguns arquivos para serem usados posteriormente. - -O primeiro estágio será usado apenas para **instalar Poetry** e para **gerar o `requirements.txt`** com as dependências do seu projeto a partir do arquivo `pyproject.toml` do Poetry. - -Esse arquivo `requirements.txt` será usado com `pip` mais tarde no **próximo estágio**. - -Na imagem final do contêiner, **somente o estágio final** é preservado. Os estágios anteriores serão descartados. - -Quando usar Poetry, faz sentido usar **construções multi-estágio do Docker** porque você realmente não precisa ter o Poetry e suas dependências instaladas na imagem final do contêiner, você **apenas precisa** ter o arquivo `requirements.txt` gerado para instalar as dependências do seu projeto. - -Então, no próximo (e último) estágio, você construiria a imagem mais ou menos da mesma maneira descrita anteriormente. - -### Por trás de um proxy de terminação TLS - Poetry - -Novamente, se você estiver executando seu contêiner atrás de um proxy de terminação TLS (balanceador de carga) como Nginx ou Traefik, adicione a opção `--proxy-headers` ao comando: - -```Dockerfile -CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"] -``` - -## Recapitulando +## Recapitulando { #recap } Usando sistemas de contêiner (por exemplo, com **Docker** e **Kubernetes**), torna-se bastante simples lidar com todos os **conceitos de implantação**: @@ -724,10 +613,8 @@ Usando sistemas de contêiner (por exemplo, com **Docker** e **Kubernetes**), to * Reinícios * Replicação (o número de processos rodando) * Memória -* Passos anteriores antes de inicializar +* Passos anteriores antes de iniciar Na maioria dos casos, você provavelmente não desejará usar nenhuma imagem base e, em vez disso, **construir uma imagem de contêiner do zero** baseada na imagem oficial do Docker Python. -Tendo cuidado com a **ordem** das instruções no `Dockerfile` e o **cache do Docker**, você pode **minimizar os tempos de construção**, para maximizar sua produtividade (e evitar a tédio). 😎 - -Em alguns casos especiais, você pode querer usar a imagem oficial do Docker para o FastAPI. 🤓 +Tendo cuidado com a **ordem** das instruções no `Dockerfile` e o **cache do Docker**, você pode **minimizar os tempos de construção**, para maximizar sua produtividade (e evitar o tédio). 😎 diff --git a/docs/pt/docs/deployment/https.md b/docs/pt/docs/deployment/https.md index 904d04eaa..6195cefab 100644 --- a/docs/pt/docs/deployment/https.md +++ b/docs/pt/docs/deployment/https.md @@ -1,4 +1,4 @@ -# Sobre HTTPS +# Sobre HTTPS { #about-https } É fácil assumir que HTTPS é algo que é apenas "habilitado" ou não. @@ -10,31 +10,31 @@ Se você está com pressa ou não se importa, continue com as seções seguintes /// -Para aprender o básico de HTTPS de uma perspectiva do usuário, verifique https://howhttps.works/pt-br/. +Para aprender o básico de HTTPS do ponto de vista do consumidor, verifique https://howhttps.works/. Agora, a partir de uma perspectiva do desenvolvedor, aqui estão algumas coisas para ter em mente ao pensar em HTTPS: -* Para HTTPS, **o servidor** precisa ter certificados gerados por **um terceiro**. - * Esses certificados são na verdade **adquiridos** de um terceiro, eles não são simplesmente "gerados". -* Certificados têm um **tempo de vida**. - * Eles **expiram**. - * E então eles precisam ser **renovados**, **adquirindo-os novamente** de um terceiro. -* A criptografia da conexão acontece no **nível TCP**. - * Essa é uma camada **abaixo do HTTP**. - * Portanto, o manuseio do **certificado e da criptografia** é feito **antes do HTTP**. -* **O TCP não sabe sobre "domínios"**. Apenas sobre endereços IP. - * As informações sobre o **domínio solicitado** vão nos **dados HTTP**. -* Os **certificados HTTPS** “certificam” um **determinado domínio**, mas o protocolo e a encriptação acontecem ao nível do TCP, **antes de sabermos** de que domínio se trata. -* **Por padrão**, isso significa que você só pode ter **um certificado HTTPS por endereço IP**. +* Para HTTPS, o servidor precisa ter "certificados" gerados por um terceiro. + * Esses certificados são na verdade adquiridos de um terceiro, eles não são simplesmente "gerados". +* Certificados têm um tempo de vida. + * Eles expiram. + * E então eles precisam ser renovados, adquirindo-os novamente de um terceiro. +* A criptografia da conexão acontece no nível TCP. + * Essa é uma camada abaixo do HTTP. + * Portanto, o manuseio do certificado e da criptografia é feito antes do HTTP. +* O TCP não sabe sobre "domínios". Apenas sobre endereços IP. + * As informações sobre o domínio específico solicitado vão nos dados HTTP. +* Os certificados HTTPS “certificam” um determinado domínio, mas o protocolo e a encriptação acontecem ao nível do TCP, antes de sabermos de que domínio se trata. +* Por padrão, isso significa que você só pode ter um certificado HTTPS por endereço IP. * Não importa o tamanho do seu servidor ou quão pequeno cada aplicativo que você tem nele possa ser. - * No entanto, existe uma **solução** para isso. -* Há uma **extensão** para o protocolo **TLS** (aquele que lida com a criptografia no nível TCP, antes do HTTP) chamado **SNI**. - * Esta extensão SNI permite que um único servidor (com um **único endereço IP**) tenha **vários certificados HTTPS** e atenda a **vários domínios / aplicativos HTTPS**. - * Para que isso funcione, um **único** componente (programa) em execução no servidor, ouvindo no **endereço IP público**, deve ter **todos os certificados HTTPS** no servidor. -* **Depois** de obter uma conexão segura, o protocolo de comunicação **ainda é HTTP**. - * Os conteúdos são **criptografados**, embora sejam enviados com o **protocolo HTTP**. + * No entanto, existe uma solução para isso. +* Há uma extensão para o protocolo TLS (aquele que lida com a criptografia no nível TCP, antes do HTTP) chamada SNI. + * Esta extensão SNI permite que um único servidor (com um único endereço IP) tenha vários certificados HTTPS e atenda a vários domínios / aplicativos HTTPS. + * Para que isso funcione, um único componente (programa) em execução no servidor, ouvindo no endereço IP público, deve ter todos os certificados HTTPS no servidor. +* Depois de obter uma conexão segura, o protocolo de comunicação ainda é HTTP. + * Os conteúdos são criptografados, embora sejam enviados com o protocolo HTTP. -É uma prática comum ter um **programa/servidor HTTP** em execução no servidor (máquina, host, etc.) e **gerenciar todas as partes HTTPS**: **recebendo as requisições encriptadas**, enviando as **solicitações HTTP descriptografadas** para o aplicativo HTTP real em execução no mesmo servidor (a aplicação **FastAPI**, neste caso), pegue a **resposta HTTP** do aplicativo, **criptografe-a** usando o **certificado HTTPS** apropriado e envie-a de volta ao cliente usando **HTTPS**. Este servidor é frequentemente chamado de **Proxy de Terminação TLS**. +É uma prática comum ter um programa/servidor HTTP em execução no servidor (máquina, host, etc.) e gerenciar todas as partes HTTPS: recebendo as requisições HTTPS encriptadas, enviando as solicitações HTTP descriptografadas para o aplicativo HTTP real em execução no mesmo servidor (a aplicação FastAPI, neste caso), pegar a resposta HTTP do aplicativo, criptografá-la usando o certificado HTTPS apropriado e enviá-la de volta ao cliente usando HTTPS. Este servidor é frequentemente chamado de Proxy de Terminação TLS. Algumas das opções que você pode usar como Proxy de Terminação TLS são: @@ -43,31 +43,31 @@ Algumas das opções que você pode usar como Proxy de Terminação TLS são: * Nginx * HAProxy -## Let's Encrypt +## Let's Encrypt { #lets-encrypt } -Antes de Let's Encrypt, esses **certificados HTTPS** eram vendidos por terceiros confiáveis. +Antes de Let's Encrypt, esses certificados HTTPS eram vendidos por terceiros confiáveis. O processo de aquisição de um desses certificados costumava ser complicado, exigia bastante papelada e os certificados eram bastante caros. -Mas então o **Let's Encrypt** foi criado. +Mas então o Let's Encrypt foi criado. -Ele é um projeto da Linux Foundation que fornece **certificados HTTPS gratuitamente** . De forma automatizada. Esses certificados usam toda a segurança criptográfica padrão e têm vida curta (cerca de 3 meses), então a **segurança é, na verdade, melhor** por causa de sua vida útil reduzida. +Ele é um projeto da Linux Foundation que fornece certificados HTTPS gratuitamente. De forma automatizada. Esses certificados usam toda a segurança criptográfica padrão e têm vida curta (cerca de 3 meses), então a segurança é, na verdade, melhor por causa do seu lifespan reduzido. Os domínios são verificados com segurança e os certificados são gerados automaticamente. Isso também permite automatizar a renovação desses certificados. -A ideia é automatizar a aquisição e renovação desses certificados, para que você tenha **HTTPS seguro, de graça e para sempre**. +A ideia é automatizar a aquisição e renovação desses certificados, para que você tenha HTTPS seguro, de graça e para sempre. -## HTTPS para Desenvolvedores +## HTTPS para Desenvolvedores { #https-for-developers } Aqui está um exemplo de como uma API HTTPS poderia ser estruturada, passo a passo, com foco principal nas ideias relevantes para desenvolvedores. -### Nome do domínio +### Nome do domínio { #domain-name } -A etapa inicial provavelmente seria **adquirir** algum **nome de domínio**. Então, você iria configurá-lo em um servidor DNS (possivelmente no mesmo provedor em nuvem). +A etapa inicial provavelmente seria adquirir algum nome de domínio. Então, você iria configurá-lo em um servidor DNS (possivelmente no mesmo provedor em nuvem). -Você provavelmente usaria um servidor em nuvem (máquina virtual) ou algo parecido, e ele teria fixed **Endereço IP público**. +Você provavelmente usaria um servidor em nuvem (máquina virtual) ou algo parecido, e ele teria um fixo Endereço IP público. -No(s) servidor(es) DNS, você configuraria um registro (`registro A`) para apontar **seu domínio** para o **endereço IP público do seu servidor**. +No(s) servidor(es) DNS, você configuraria um registro (um `A record`) para apontar seu domínio para o endereço IP público do seu servidor. Você provavelmente fará isso apenas uma vez, na primeira vez em que tudo estiver sendo configurado. @@ -77,123 +77,155 @@ Essa parte do Nome do Domínio se dá muito antes do HTTPS, mas como tudo depend /// -### DNS +### DNS { #dns } Agora vamos focar em todas as partes que realmente fazem parte do HTTPS. -Primeiro, o navegador iria verificar com os **servidores DNS** qual o **IP do domínio**, nesse caso, `someapp.example.com`. +Primeiro, o navegador iria verificar com os servidores DNS qual o IP do domínio, nesse caso, `someapp.example.com`. -Os servidores DNS iriam informar o navegador para utilizar algum **endereço IP** específico. Esse seria o endereço IP público em uso no seu servidor, que você configurou nos servidores DNS. +Os servidores DNS iriam informar o navegador para utilizar algum endereço IP específico. Esse seria o endereço IP público em uso no seu servidor, que você configurou nos servidores DNS. -### Início do Handshake TLS +### Início do Handshake TLS { #tls-handshake-start } -O navegador então irá comunicar-se com esse endereço IP na **porta 443** (a porta HTTPS). +O navegador então irá comunicar-se com esse endereço IP na porta 443 (a porta HTTPS). A primeira parte dessa comunicação é apenas para estabelecer a conexão entre o cliente e o servidor e para decidir as chaves criptográficas a serem utilizadas, etc. -Esse interação entre o cliente e o servidor para estabelecer uma conexão TLS é chamada de **Handshake TLS**. +Esse interação entre o cliente e o servidor para estabelecer uma conexão TLS é chamada de Handshake TLS. -### TLS com a Extensão SNI +### TLS com a Extensão SNI { #tls-with-sni-extension } -**Apenas um processo** no servidor pode se conectar a uma **porta** em um **endereço IP**. Poderiam existir outros processos conectados em outras portas desse mesmo endereço IP, mas apenas um para cada combinação de endereço IP e porta. +Apenas um processo no servidor pode se conectar a uma porta em um endereço IP. Poderiam existir outros processos conectados em outras portas desse mesmo endereço IP, mas apenas um para cada combinação de endereço IP e porta. TLS (HTTPS) usa a porta `443` por padrão. Então essa é a porta que precisamos. -Como apenas um único processo pode se comunicar com essa porta, o processo que faria isso seria o **Proxy de Terminação TLS**. +Como apenas um único processo pode se comunicar com essa porta, o processo que faria isso seria o Proxy de Terminação TLS. -O Proxy de Terminação TLS teria acesso a um ou mais **certificados TLS** (certificados HTTPS). +O Proxy de Terminação TLS teria acesso a um ou mais certificados TLS (certificados HTTPS). -Utilizando a **extensão SNI** discutida acima, o Proxy de Terminação TLS iria checar qual dos certificados TLS (HTTPS) disponíveis deve ser usado para essa conexão, utilizando o que corresponda ao domínio esperado pelo cliente. +Utilizando a extensão SNI discutida acima, o Proxy de Terminação TLS iria checar qual dos certificados TLS (HTTPS) disponíveis deve ser usado para essa conexão, utilizando o que corresponda ao domínio esperado pelo cliente. Nesse caso, ele usaria o certificado para `someapp.example.com`. -O cliente já **confia** na entidade que gerou o certificado TLS (nesse caso, o Let's Encrypt, mas veremos sobre isso mais tarde), então ele pode **verificar** que o certificado é válido. +O cliente já confia na entidade que gerou o certificado TLS (nesse caso, o Let's Encrypt, mas veremos sobre isso mais tarde), então ele pode verificar que o certificado é válido. -Então, utilizando o certificado, o cliente e o Proxy de Terminação TLS **decidem como encriptar** o resto da **comunicação TCP**. Isso completa a parte do **Handshake TLS**. +Então, utilizando o certificado, o cliente e o Proxy de Terminação TLS decidem como encriptar o resto da comunicação TCP. Isso completa a parte do Handshake TLS. -Após isso, o cliente e o servidor possuem uma **conexão TCP encriptada**, que é provida pelo TLS. E então eles podem usar essa conexão para começar a **comunicação HTTP** propriamente dita. +Após isso, o cliente e o servidor possuem uma conexão TCP encriptada, que é provida pelo TLS. E então eles podem usar essa conexão para começar a comunicação HTTP propriamente dita. -E isso resume o que é **HTTPS**, apenas **HTTP** simples dentro de uma **conexão TLS segura** em vez de uma conexão TCP pura (não encriptada). +E isso resume o que é HTTPS, apenas HTTP simples dentro de uma conexão TLS segura em vez de uma conexão TCP pura (não encriptada). /// tip | Dica -Percebe que a encriptação da comunicação acontece no **nível do TCP**, não no nível do HTTP. +Percebe que a encriptação da comunicação acontece no nível do TCP, não no nível do HTTP. /// -### Solicitação HTTPS +### Solicitação HTTPS { #https-request } -Agora que o cliente e servidor (especialmente o navegador e o Proxy de Terminação TLS) possuem uma **conexão TCP encriptada**, eles podem iniciar a **comunicação HTTP**. +Agora que o cliente e servidor (especialmente o navegador e o Proxy de Terminação TLS) possuem uma conexão TCP encriptada, eles podem iniciar a comunicação HTTP. -Então, o cliente envia uma **solicitação HTTPS**. Que é apenas uma solicitação HTTP sobre uma conexão TLS encriptada. +Então, o cliente envia uma solicitação HTTPS. Que é apenas uma solicitação HTTP sobre uma conexão TLS encriptada. -### Desencriptando a Solicitação +### Desencriptando a Solicitação { #decrypt-the-request } -O Proxy de Terminação TLS então usaria a encriptação combinada para **desencriptar a solicitação**, e transmitiria a **solicitação básica (desencriptada)** para o processo executando a aplicação (por exemplo, um processo com Uvicorn executando a aplicação FastAPI). +O Proxy de Terminação TLS então usaria a encriptação combinada para desencriptar a solicitação, e transmitiria a solicitação básica (desencriptada) para o processo executando a aplicação (por exemplo, um processo com Uvicorn executando a aplicação FastAPI). -### Resposta HTTP +### Resposta HTTP { #http-response } -A aplicação processaria a solicitação e retornaria uma **resposta HTTP básica (não encriptada)** para o Proxy de Terminação TLS. +A aplicação processaria a solicitação e retornaria uma resposta HTTP básica (não encriptada) para o Proxy de Terminação TLS. -### Resposta HTTPS +### Resposta HTTPS { #https-response } -O Proxy de Terminação TLS iria **encriptar a resposta** utilizando a criptografia combinada anteriormente (que foi definida com o certificado para `someapp.example.com`), e devolveria para o navegador. +O Proxy de Terminação TLS iria encriptar a resposta utilizando a criptografia combinada anteriormente (que foi definida com o certificado para `someapp.example.com`), e devolveria para o navegador. -No próximo passo, o navegador verifica que a resposta é válida e encriptada com a chave criptográfica correta, etc. E depois **desencripta a resposta** e a processa. +No próximo passo, o navegador verifica que a resposta é válida e encriptada com a chave criptográfica correta, etc. E depois desencripta a resposta e a processa. -O cliente (navegador) saberá que a resposta vem do servidor correto por que ela usa a criptografia que foi combinada entre eles usando o **certificado HTTPS** anterior. +O cliente (navegador) saberá que a resposta vem do servidor correto por que ela usa a criptografia que foi combinada entre eles usando o certificado HTTPS anterior. -### Múltiplas Aplicações +### Múltiplas Aplicações { #multiple-applications } -Podem existir **múltiplas aplicações** em execução no mesmo servidor (ou servidores), por exemplo: outras APIs ou um banco de dados. +Podem existir múltiplas aplicações em execução no mesmo servidor (ou servidores), por exemplo: outras APIs ou um banco de dados. -Apenas um processo pode estar vinculado a um IP e porta (o Proxy de Terminação TLS, por exemplo), mas outras aplicações/processos também podem estar em execução no(s) servidor(es), desde que não tentem usar a mesma **combinação de IP público e porta**. +Apenas um processo pode estar vinculado a um IP e porta (o Proxy de Terminação TLS, por exemplo), mas outras aplicações/processos também podem estar em execução no(s) servidor(es), desde que não tentem usar a mesma combinação de IP público e porta. -Dessa forma, o Proxy de Terminação TLS pode gerenciar o HTTPS e os certificados de **múltiplos domínios**, para múltiplas aplicações, e então transmitir as requisições para a aplicação correta em cada caso. +Dessa forma, o Proxy de Terminação TLS pode gerenciar o HTTPS e os certificados de múltiplos domínios, para múltiplas aplicações, e então transmitir as requisições para a aplicação correta em cada caso. -### Renovação de Certificados +### Renovação de Certificados { #certificate-renewal } -Em algum momento futuro, cada certificado irá **expirar** (aproximadamente 3 meses após a aquisição). +Em algum momento futuro, cada certificado irá expirar (aproximadamente 3 meses após a aquisição). E então, haverá outro programa (em alguns casos pode ser o próprio Proxy de Terminação TLS) que irá interagir com o Let's Encrypt e renovar o(s) certificado(s). -Os **certificados TLS** são **associados com um nome de domínio**, e não a um endereço IP. +Os certificados TLS são associados com um nome de domínio, e não a um endereço IP. -Então para renovar os certificados, o programa de renovação precisa **provar** para a autoridade (Let's Encrypt) que ele realmente **possui e controla esse domínio**> +Então para renovar os certificados, o programa de renovação precisa provar para a autoridade (Let's Encrypt) que ele realmente "possui" e controla esse domínio. Para fazer isso, e acomodar as necessidades de diferentes aplicações, existem diferentes opções para esse programa. Algumas escolhas populares são: -* **Modificar alguns registros DNS** - * Para isso, o programa de renovação precisa ter suporte as APIs do provedor DNS, então, dependendo do provedor DNS que você utilize, isso pode ou não ser uma opção viável. -* **Executar como um servidor** (ao menos durante o processo de aquisição do certificado) no endereço IP público associado com o domínio. +* Modificar alguns registros DNS + * Para isso, o programa de renovação precisa ter suporte às APIs do provedor DNS, então, dependendo do provedor DNS que você utilize, isso pode ou não ser uma opção viável. +* Executar como um servidor (ao menos durante o processo de aquisição do certificado) no endereço IP público associado com o domínio. * Como dito anteriormente, apenas um processo pode estar ligado a uma porta e IP específicos. * Essa é uma dos motivos que fazem utilizar o mesmo Proxy de Terminação TLS para gerenciar a renovação de certificados ser tão útil. * Caso contrário, você pode ter que parar a execução do Proxy de Terminação TLS momentaneamente, inicializar o programa de renovação para renovar os certificados, e então reiniciar o Proxy de Terminação TLS. Isso não é o ideal, já que sua(s) aplicação(ões) não vão estar disponíveis enquanto o Proxy de Terminação TLS estiver desligado. -Todo esse processo de renovação, enquanto o aplicativo ainda funciona, é uma das principais razões para preferir um **sistema separado para gerenciar HTTPS** com um Proxy de Terminação TLS em vez de usar os certificados TLS no servidor da aplicação diretamente (e.g. com o Uvicorn). +Todo esse processo de renovação, enquanto o aplicativo ainda funciona, é uma das principais razões para preferir um sistema separado para gerenciar HTTPS com um Proxy de Terminação TLS em vez de usar os certificados TLS no servidor da aplicação diretamente (e.g. com o Uvicorn). -## Recapitulando +## Cabeçalhos encaminhados por Proxy { #proxy-forwarded-headers } -Possuir **HTTPS** habilitado na sua aplicação é bastante importante, e até **crítico** na maioria dos casos. A maior parte do esforço que você tem que colocar sobre o HTTPS como desenvolvedor está em **entender esses conceitos** e como eles funcionam. +Ao usar um proxy para lidar com HTTPS, seu servidor de aplicação (por exemplo, Uvicorn via FastAPI CLI) não sabe nada sobre o processo de HTTPS; ele se comunica com HTTP simples com o Proxy de Terminação TLS. -Mas uma vez que você saiba o básico de **HTTPS para desenvolvedores**, você pode combinar e configurar diferentes ferramentas facilmente para gerenciar tudo de uma forma simples. +Esse proxy normalmente define alguns cabeçalhos HTTP dinamicamente antes de transmitir a requisição para o servidor de aplicação, para informar ao servidor de aplicação que a requisição está sendo encaminhada pelo proxy. -Em alguns dos próximos capítulos, eu mostrarei para você vários exemplos concretos de como configurar o **HTTPS** para aplicações **FastAPI**. 🔒 +/// note | Detalhes Técnicos + +Os cabeçalhos do proxy são: + +* X-Forwarded-For +* X-Forwarded-Proto +* X-Forwarded-Host + +/// + +No entanto, como o servidor de aplicação não sabe que está atrás de um proxy confiável, por padrão ele não confiaria nesses cabeçalhos. + +Mas você pode configurar o servidor de aplicação para confiar nos cabeçalhos encaminhados enviados pelo proxy. Se você estiver usando o FastAPI CLI, pode usar a opção de CLI `--forwarded-allow-ips` para dizer de quais IPs ele deve confiar nesses cabeçalhos encaminhados. + +Por exemplo, se o servidor de aplicação só estiver recebendo comunicação do proxy confiável, você pode defini-lo como `--forwarded-allow-ips="*"` para fazê-lo confiar em todos os IPs de entrada, já que ele só receberá requisições de seja lá qual for o IP usado pelo proxy. + +Dessa forma, a aplicação seria capaz de saber qual é sua própria URL pública, se está usando HTTPS, o domínio, etc. + +Isso seria útil, por exemplo, para lidar corretamente com redirecionamentos. + +/// tip | Dica + +Você pode saber mais sobre isso na documentação em [Atrás de um Proxy - Habilitar cabeçalhos encaminhados pelo proxy](../advanced/behind-a-proxy.md#enable-proxy-forwarded-headers){.internal-link target=_blank} + +/// + +## Recapitulando { #recap } + +Possuir HTTPS habilitado na sua aplicação é bastante importante, e até crítico na maioria dos casos. A maior parte do esforço que você tem que colocar sobre o HTTPS como desenvolvedor está em entender esses conceitos e como eles funcionam. + +Mas uma vez que você saiba o básico de HTTPS para desenvolvedores, você pode combinar e configurar diferentes ferramentas facilmente para gerenciar tudo de uma forma simples. + +Em alguns dos próximos capítulos, eu mostrarei para você vários exemplos concretos de como configurar o HTTPS para aplicações FastAPI. 🔒 diff --git a/docs/pt/docs/deployment/index.md b/docs/pt/docs/deployment/index.md index 6b4290d1d..92cd4323a 100644 --- a/docs/pt/docs/deployment/index.md +++ b/docs/pt/docs/deployment/index.md @@ -1,7 +1,21 @@ -# Implantação +# Implantação { #deployment } -A implantação de uma aplicação **FastAPI** é relativamente simples. +Implantar uma aplicação **FastAPI** é relativamente fácil. -Existem várias maneiras para fazer isso, dependendo do seu caso específico e das ferramentas que você utiliza. +## O que significa Implantação { #what-does-deployment-mean } -Você verá mais detalhes para se ter em mente e algumas das técnicas para a implantação nas próximas seções. +Implantar uma aplicação significa executar as etapas necessárias para torná-la disponível para os usuários. + +Para uma **web API**, isso normalmente envolve colocá-la em uma **máquina remota**, com um **programa de servidor** que ofereça bom desempenho, estabilidade, etc., de modo que seus **usuários** possam **acessar** a aplicação com eficiência e sem interrupções ou problemas. + +Isso contrasta com as fases de **desenvolvimento**, em que você está constantemente alterando o código, quebrando e consertando, parando e reiniciando o servidor de desenvolvimento, etc. + +## Estratégias de Implantação { #deployment-strategies } + +Há várias maneiras de fazer isso, dependendo do seu caso de uso específico e das ferramentas que você utiliza. + +Você pode **implantar um servidor** por conta própria usando uma combinação de ferramentas, pode usar um **serviço em nuvem** que faça parte do trabalho por você, entre outras opções. + +Vou mostrar alguns dos principais conceitos que você provavelmente deve ter em mente ao implantar uma aplicação **FastAPI** (embora a maior parte se aplique a qualquer outro tipo de aplicação web). + +Você verá mais detalhes para ter em mente e algumas das técnicas para fazer isso nas próximas seções. ✨ diff --git a/docs/pt/docs/deployment/manually.md b/docs/pt/docs/deployment/manually.md index c7caabbcd..21d0f44cd 100644 --- a/docs/pt/docs/deployment/manually.md +++ b/docs/pt/docs/deployment/manually.md @@ -1,6 +1,6 @@ -# Execute um Servidor Manualmente +# Execute um Servidor Manualmente { #run-a-server-manually } -## Utilize o comando `fastapi run` +## Utilize o comando `fastapi run` { #use-the-fastapi-run-command } Em resumo, utilize o comando `fastapi run` para inicializar sua aplicação FastAPI: @@ -42,23 +42,23 @@ Isto deve funcionar para a maioria dos casos. 😎 Você pode utilizar esse comando, por exemplo, para iniciar sua aplicação **FastAPI** em um contêiner, em um servidor, etc. -## Servidores ASGI +## Servidores ASGI { #asgi-servers } Vamos nos aprofundar um pouco mais em detalhes. -FastAPI utiliza um padrão para construir frameworks e servidores web em Python chamado ASGI. FastAPI é um framework web ASGI. +FastAPI utiliza um padrão para construir frameworks e servidores web em Python chamado ASGI. FastAPI é um framework web ASGI. A principal coisa que você precisa para executar uma aplicação **FastAPI** (ou qualquer outra aplicação ASGI) em uma máquina de servidor remoto é um programa de servidor ASGI como o **Uvicorn**, que é o que vem por padrão no comando `fastapi`. Existem diversas alternativas, incluindo: * Uvicorn: um servidor ASGI de alta performance. -* Hypercorn: um servidor ASGI compátivel com HTTP/2, Trio e outros recursos. +* Hypercorn: um servidor ASGI compatível com HTTP/2, Trio e outros recursos. * Daphne: servidor ASGI construído para Django Channels. * Granian: um servidor HTTP Rust para aplicações Python. * NGINX Unit: NGINX Unit é um runtime de aplicação web leve e versátil. -## Máquina Servidora e Programa Servidor +## Máquina Servidora e Programa Servidor { #server-machine-and-server-program } Existe um pequeno detalhe sobre estes nomes para se manter em mente. 💡 @@ -68,7 +68,7 @@ Apenas tenha em mente que quando você ler "servidor" em geral, isso pode se ref Quando se refere à máquina remota, é comum chamá-la de **servidor**, mas também de **máquina**, **VM** (máquina virtual), **nó**. Todos esses termos se referem a algum tipo de máquina remota, normalmente executando Linux, onde você executa programas. -## Instale o Programa Servidor +## Instale o Programa Servidor { #install-the-server-program } Quando você instala o FastAPI, ele vem com um servidor de produção, o Uvicorn, e você pode iniciá-lo com o comando `fastapi run`. @@ -100,7 +100,7 @@ Quando você instala o FastAPI com algo como `pip install "fastapi[standard]"`, /// -## Execute o Programa Servidor +## Execute o Programa Servidor { #run-the-server-program } Se você instalou um servidor ASGI manualmente, normalmente precisará passar uma string de importação em um formato especial para que ele importe sua aplicação FastAPI: @@ -131,7 +131,7 @@ from main import app Cada programa de servidor ASGI alternativo teria um comando semelhante, você pode ler mais na documentação respectiva. -/// warning | Aviso +/// warning | Atenção Uvicorn e outros servidores suportam a opção `--reload` que é útil durante o desenvolvimento. @@ -141,7 +141,7 @@ Ela ajuda muito durante o **desenvolvimento**, mas você **não deve** usá-la e /// -## Conceitos de Implantação +## Conceitos de Implantação { #deployment-concepts } Esses exemplos executam o programa do servidor (por exemplo, Uvicorn), iniciando **um único processo**, ouvindo em todos os IPs (`0.0.0.0`) em uma porta predefinida (por exemplo, `80`). diff --git a/docs/pt/docs/deployment/server-workers.md b/docs/pt/docs/deployment/server-workers.md index a0db1bea4..bfb1e6687 100644 --- a/docs/pt/docs/deployment/server-workers.md +++ b/docs/pt/docs/deployment/server-workers.md @@ -1,4 +1,4 @@ -# Trabalhadores do Servidor - Uvicorn com Trabalhadores +# Trabalhadores do Servidor - Uvicorn com Trabalhadores { #server-workers-uvicorn-with-workers } Vamos rever os conceitos de implantação anteriores: @@ -25,7 +25,7 @@ Em particular, ao executar no **Kubernetes** você provavelmente **não** vai qu /// -## Vários trabalhadores +## Vários trabalhadores { #multiple-workers } Você pode iniciar vários trabalhadores com a opção de linha de comando `--workers`: @@ -111,7 +111,7 @@ A única opção nova aqui é `--workers` informando ao Uvicorn para iniciar 4 p Você também pode ver que ele mostra o **PID** de cada processo, `27365` para o processo pai (este é o **gerenciador de processos**) e um para cada processo de trabalho: `27368`, `27369`, `27370` e `27367`. -## Conceitos de Implantação +## Conceitos de Implantação { #deployment-concepts } Aqui você viu como usar vários **trabalhadores** para **paralelizar** a execução do aplicativo, aproveitar **vários núcleos** na CPU e conseguir atender **mais solicitações**. @@ -124,13 +124,13 @@ Da lista de conceitos de implantação acima, o uso de trabalhadores ajudaria pr * **Memória** * **Etapas anteriores antes de iniciar** -## Contêineres e Docker +## Contêineres e Docker { #containers-and-docker } No próximo capítulo sobre [FastAPI em contêineres - Docker](docker.md){.internal-link target=_blank}, explicarei algumas estratégias que você pode usar para lidar com os outros **conceitos de implantação**. Vou mostrar como **construir sua própria imagem do zero** para executar um único processo Uvicorn. É um processo simples e provavelmente é o que você gostaria de fazer ao usar um sistema de gerenciamento de contêineres distribuídos como o **Kubernetes**. -## Recapitular +## Recapitular { #recap } Você pode usar vários processos de trabalho com a opção CLI `--workers` com os comandos `fastapi` ou `uvicorn` para aproveitar as vantagens de **CPUs multi-core** e executar **vários processos em paralelo**. diff --git a/docs/pt/docs/deployment/versions.md b/docs/pt/docs/deployment/versions.md index 323ddbd45..a2aca5a17 100644 --- a/docs/pt/docs/deployment/versions.md +++ b/docs/pt/docs/deployment/versions.md @@ -1,46 +1,46 @@ -# Sobre as versões do FastAPI +# Sobre as versões do FastAPI { #about-fastapi-versions } -**FastAPI** já está sendo usado em produção em diversas aplicações e sistemas, a cobertura de testes é mantida em 100%, mas seu desenvolvimento está avançando rapidamente. +**FastAPI** já está sendo usado em produção em muitas aplicações e sistemas. E a cobertura de testes é mantida em 100%. Mas seu desenvolvimento ainda está avançando rapidamente. -Novos recursos são adicionados com frequência, bugs são corrigidos regularmente e o código está sempre melhorando. +Novas funcionalidades são adicionadas com frequência, bugs são corrigidos regularmente e o código continua melhorando continuamente. -Esse é o motivo das versões atuais estarem em `0.x.x`, significando que em cada versão pode haver mudanças significativas, tudo isso seguindo as convenções de controle de versão semântica. +É por isso que as versões atuais ainda são `0.x.x`, isso reflete que cada versão pode potencialmente ter mudanças significativas. Isso segue as convenções de Versionamento Semântico. -Já é possível criar aplicativos de produção com **FastAPI** (e provavelmente você já faz isso há algum tempo), apenas precisando ter certeza de usar uma versão que funcione corretamente com o resto do seu código. +Você pode criar aplicações de produção com **FastAPI** agora mesmo (e provavelmente já vem fazendo isso há algum tempo), apenas certifique-se de usar uma versão que funcione corretamente com o resto do seu código. -## Fixe a sua versão de `fastapi` +## Fixe a sua versão de `fastapi` { #pin-your-fastapi-version } -A primeira coisa que você deve fazer é "fixar" a versão do **FastAPI** que você está utilizando na mais atual, na qual você sabe que funciona corretamente para o seu aplicativo. +A primeira coisa que você deve fazer é "fixar" a versão do **FastAPI** que você está utilizando na versão mais recente específica que você sabe que funciona corretamente para a sua aplicação. -Por exemplo, supondo que você está usando a versão `0.45.0` em sua aplicação. +Por exemplo, suponha que você esteja usando a versão `0.112.0` em sua aplicação. -Caso você utilize o arquivo `requirements.txt`, você poderia especificar a versão com: +Se você usa um arquivo `requirements.txt`, você poderia especificar a versão com: ```txt -fastapi==0.45.0 +fastapi[standard]==0.112.0 ``` -Isso significa que você conseguiria utilizar a versão exata `0.45.0`. +isso significaria que você usaria exatamente a versão `0.112.0`. -Ou, você poderia fixá-la com: +Ou você também poderia fixá-la com: ```txt -fastapi>=0.45.0,<0.46.0 +fastapi[standard]>=0.112.0,<0.113.0 ``` -isso significa que você iria usar as versões `0.45.0` ou acima, mas inferiores à `0.46.0`, por exemplo, a versão `0.45.2` ainda seria aceita. +isso significaria que você usaria as versões `0.112.0` ou superiores, mas menores que `0.113.0`, por exemplo, a versão `0.112.2` ainda seria aceita. -Se você usar qualquer outra ferramenta para gerenciar suas instalações, como Poetry, Pipenv ou outras, todas elas têm uma maneira que você pode usar para definir as versões específicas dos seus pacotes. +Se você usa qualquer outra ferramenta para gerenciar suas instalações, como `uv`, Poetry, Pipenv ou outras, todas elas têm uma forma de definir versões específicas para seus pacotes. -## Versões disponíveis +## Versões disponíveis { #available-versions } -Você pode ver as versões disponíveis (por exemplo, para verificar qual é a versão atual) em [Release Notes](../release-notes.md){.internal-link target=\_blank}. +Você pode ver as versões disponíveis (por exemplo, para verificar qual é a mais recente) nas [Release Notes](../release-notes.md){.internal-link target=_blank}. -## Sobre versões +## Sobre versões { #about-versions } -Seguindo as convenções de controle de versão semântica, qualquer versão abaixo de `1.0.0` pode adicionar mudanças significativas. +Seguindo as convenções de Versionamento Semântico, qualquer versão abaixo de `1.0.0` pode potencialmente adicionar mudanças significativas. -FastAPI também segue a convenção de que qualquer alteração de versão "PATCH" é para correção de bugs e alterações não significativas. +FastAPI também segue a convenção de que qualquer alteração de versão "PATCH" é para correções de bugs e mudanças que não quebram compatibilidade. /// tip | Dica @@ -54,40 +54,40 @@ Logo, você deveria conseguir fixar a versão, como: fastapi>=0.45.0,<0.46.0 ``` -Mudanças significativas e novos recursos são adicionados em versões "MINOR". +Mudanças significativas e novas funcionalidades são adicionadas em versões "MINOR". /// tip | Dica -O "MINOR" é o número que está no meio, por exemplo, em `0.2.3`, a versão MINOR é `2`. +O "MINOR" é o número do meio, por exemplo, em `0.2.3`, a versão MINOR é `2`. /// -## Atualizando as versões do FastAPI +## Atualizando as versões do FastAPI { #upgrading-the-fastapi-versions } Você deve adicionar testes para a sua aplicação. -Com **FastAPI** isso é muito fácil (graças a Starlette), verifique a documentação: [Testing](../tutorial/testing.md){.internal-link target=\_blank} +Com **FastAPI** isso é muito fácil (graças ao Starlette), veja a documentação: [Testing](../tutorial/testing.md){.internal-link target=_blank} -Após a criação dos testes, você pode atualizar a sua versão do **FastAPI** para uma mais recente, execute os testes para se certificar de que todo o seu código está funcionando corretamente. +Depois que você tiver testes, você pode atualizar a sua versão do **FastAPI** para uma mais recente e se certificar de que todo o seu código está funcionando corretamente executando seus testes. -Se tudo estiver funcionando, ou após você realizar as alterações necessárias e todos os testes estiverem passando, então você pode fixar sua versão de `FastAPI` para essa mais nova. +Se tudo estiver funcionando, ou após você realizar as alterações necessárias e todos os testes estiverem passando, então você pode fixar sua versão de `fastapi` para essa versão mais recente. -## Sobre Starlette +## Sobre Starlette { #about-starlette } Não é recomendado fixar a versão de `starlette`. Versões diferentes de **FastAPI** utilizarão uma versão específica e mais recente de Starlette. -Então, você pode deixar **FastAPI** escolher a versão compatível e correta de Starlette. +Então, você pode deixar **FastAPI** usar a versão correta do Starlette. -## Sobre Pydantic +## Sobre Pydantic { #about-pydantic } -Pydantic incluí os testes para **FastAPI** em seus próprios testes, então as novas versões de Pydantic (acima da `1.0.0`) sempre serão compatíveis com FastAPI. +Pydantic inclui os testes para **FastAPI** em seus próprios testes, então novas versões do Pydantic (acima de `1.0.0`) são sempre compatíveis com FastAPI. -Você pode fixar qualquer versão de Pydantic que desejar, desde que seja acima da `1.0.0` e abaixo da `2.0.0`. +Você pode fixar o Pydantic em qualquer versão acima de `1.0.0` que funcione para você. Por exemplo: ```txt -pydantic>=1.2.0,<2.0.0 +pydantic>=2.7.0,<3.0.0 ``` diff --git a/docs/pt/docs/environment-variables.md b/docs/pt/docs/environment-variables.md index 432f78af0..342361b91 100644 --- a/docs/pt/docs/environment-variables.md +++ b/docs/pt/docs/environment-variables.md @@ -1,4 +1,4 @@ -# Variáveis de Ambiente +# Variáveis de Ambiente { #environment-variables } /// tip | Dica @@ -10,7 +10,7 @@ Uma variável de ambiente (também conhecida como "**env var**") é uma variáve Variáveis de ambiente podem ser úteis para lidar com **configurações** do aplicativo, como parte da **instalação** do Python, etc. -## Criar e Usar Variáveis de Ambiente +## Criar e Usar Variáveis de Ambiente { #create-and-use-env-vars } Você pode **criar** e usar variáveis de ambiente no **shell (terminal)**, sem precisar do Python: @@ -50,7 +50,7 @@ Hello Wade Wilson //// -## Ler Variáveis de Ambiente no Python +## Ler Variáveis de Ambiente no Python { #read-env-vars-in-python } Você também pode criar variáveis de ambiente **fora** do Python, no terminal (ou com qualquer outro método) e depois **lê-las no Python**. @@ -157,7 +157,7 @@ Você pode ler mais sobre isso em ```console -$ fastapi dev main.py -INFO Using path main.py -INFO Resolved absolute path /home/user/code/awesomeapp/main.py -INFO Searching for package file structure from directories with __init__.py files -INFO Importing from /home/user/code/awesomeapp +$ fastapi dev main.py - ╭─ Python module file ─╮ - │ │ - │ 🐍 main.py │ - │ │ - ╰──────────────────────╯ + FastAPI Starting development server 🚀 -INFO Importing module main -INFO Found importable FastAPI app + Searching for package file structure from directories with + __init__.py files + Importing from /home/user/code/awesomeapp - ╭─ Importable FastAPI app ─╮ - │ │ - │ from main import app │ - │ │ - ╰──────────────────────────╯ + module 🐍 main.py -INFO Using import string main:app + code Importing the FastAPI app object from the module with the + following code: - ╭────────── FastAPI CLI - Development mode ───────────╮ - │ │ - │ Serving at: http://127.0.0.1:8000 │ - │ │ - │ API docs: http://127.0.0.1:8000/docs │ - │ │ - │ Running in development mode, for production use: │ - │ │ - fastapi run - │ │ - ╰─────────────────────────────────────────────────────╯ + from main import app -INFO: Will watch for changes in these directories: ['/home/user/code/awesomeapp'] -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [2265862] using WatchFiles -INFO: Started server process [2265873] -INFO: Waiting for application startup. -INFO: Application startup complete. + app Using import string: main:app + + server Server started at http://127.0.0.1:8000 + server Documentation at http://127.0.0.1:8000/docs + + tip Running in development mode, for production use: + fastapi run + + Logs: + + INFO Will watch for changes in these directories: + ['/home/user/code/awesomeapp'] + INFO Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to + quit) + INFO Started reloader process [383138] using WatchFiles + INFO Started server process [383153] + INFO Waiting for application startup. + INFO Application startup complete. ``` -Aquele commando por linha de programa chamado `fastapi` é o **FastAPI CLI**. +O programa de linha de comando chamado `fastapi` é o **FastAPI CLI**. -O FastAPI CLI recebe o caminho do seu programa Python, detecta automaticamente a variável com o FastAPI (comumente nomeada `app`) e como importá-la, e então a serve. +O FastAPI CLI recebe o caminho para o seu programa Python (por exemplo, `main.py`), detecta automaticamente a instância de `FastAPI` (comumente nomeada `app`), determina a forma correta de importação e então a serve. -Para produção você usaria `fastapi run` no lugar. 🚀 +Para produção, você usaria `fastapi run`. 🚀 -Internamente, **FastAPI CLI** usa Uvicorn, um servidor ASGI de alta performance e pronto para produção. 😎 +Internamente, o **FastAPI CLI** usa o Uvicorn, um servidor ASGI de alta performance e pronto para produção. 😎 -## `fastapi dev` +## `fastapi dev` { #fastapi-dev } -Quando você roda `fastapi dev`, isso vai executar em modo de desenvolvimento. +Executar `fastapi dev` inicia o modo de desenvolvimento. -Por padrão, teremos o **recarregamento automático** ativo, então o programa irá recarregar o servidor automaticamente toda vez que você fizer mudanças no seu código. Isso usa muitos recursos e pode ser menos estável. Você deve apenas usá-lo em modo de desenvolvimento. +Por padrão, o recarregamento automático está ativado, recarregando o servidor automaticamente quando você faz mudanças no seu código. Isso consome muitos recursos e pode ser menos estável do que quando está desativado. Você deve usá-lo apenas no desenvolvimento. Ele também escuta no endereço IP `127.0.0.1`, que é o IP para a sua máquina se comunicar apenas consigo mesma (`localhost`). -O servidor de desenvolvimento escutará no endereço de IP `127.0.0.1` por padrão, este é o IP que sua máquina usa para se comunicar com ela mesma (`localhost`). +## `fastapi run` { #fastapi-run } -## `fastapi run` +Executar `fastapi run` inicia o FastAPI em modo de produção por padrão. -Quando você rodar `fastapi run`, isso executará em modo de produção por padrão. +Por padrão, o recarregamento automático está desativado. Ele também escuta no endereço IP `0.0.0.0`, o que significa todos os endereços IP disponíveis; dessa forma, ficará acessível publicamente para qualquer pessoa que consiga se comunicar com a máquina. É assim que você normalmente o executaria em produção, por exemplo, em um contêiner. -Este modo terá **recarregamento automático desativado** por padrão. +Na maioria dos casos, você teria (e deveria ter) um "proxy de terminação" tratando o HTTPS por cima; isso dependerá de como você faz o deploy da sua aplicação, seu provedor pode fazer isso por você ou talvez seja necessário que você configure isso por conta própria. -Isso irá escutar no endereço de IP `0.0.0.0`, o que significa todos os endereços IP disponíveis, dessa forma o programa estará acessível publicamente para qualquer um que consiga se comunicar com a máquina. Isso é como você normalmente roda em produção em um contêiner, por exemplo. +/// tip | Dica -Em muitos casos você pode ter (e deveria ter) um "proxy de saída" tratando HTTPS no topo, isso dependerá de como você fará o deploy da sua aplicação, seu provedor pode fazer isso pra você ou talvez seja necessário fazer você mesmo. - -/// tip - -Você pode aprender mais sobre em [documentação de deployment](deployment/index.md){.internal-link target=_blank}. +Você pode aprender mais sobre isso na [documentação de deployment](deployment/index.md){.internal-link target=_blank}. /// diff --git a/docs/pt/docs/features.md b/docs/pt/docs/features.md index ccc3300d6..275307775 100644 --- a/docs/pt/docs/features.md +++ b/docs/pt/docs/features.md @@ -1,19 +1,19 @@ -# Recursos +# Recursos { #features } -## Recursos do FastAPI +## Recursos do FastAPI { #fastapi-features } **FastAPI** te oferece o seguinte: -### Baseado em padrões abertos +### Baseado em padrões abertos { #based-on-open-standards } -* OpenAPI para criação de APIs, incluindo declarações de operações de caminho, parâmetros, requisições de corpo, segurança etc. +* OpenAPI para criação de APIs, incluindo declarações de caminho operações, parâmetros, requisições de corpo, segurança etc. * Modelo de documentação automática com JSON Schema (já que o OpenAPI em si é baseado no JSON Schema). * Projetado em cima desses padrões após um estudo meticuloso, em vez de uma reflexão breve. * Isso também permite o uso de **geração de código do cliente** automaticamente em muitas linguagens. -### Documentação automática +### Documentação automática { #automatic-docs } -Documentação interativa da API e navegação _web_ da interface de usuário. Como o _framework_ é baseado no OpenAPI, há várias opções, 2 incluídas por padrão. +Documentação interativa da API e navegação web da interface de usuário. Como o framework é baseado no OpenAPI, há várias opções, 2 incluídas por padrão. * Swagger UI, com navegação interativa, chame e teste sua API diretamente do navegador. @@ -23,9 +23,9 @@ Documentação interativa da API e navegação _web_ da interface de usuário. C ![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) -### Apenas Python moderno +### Apenas Python moderno { #just-modern-python } -Tudo é baseado no padrão das declarações de **tipos do Python 3.8** (graças ao Pydantic). Nenhuma sintaxe nova para aprender. Apenas o padrão moderno do Python. +Tudo é baseado no padrão das declarações de **tipos do Python** (graças ao Pydantic). Nenhuma sintaxe nova para aprender. Apenas o padrão moderno do Python. Se você precisa refrescar a memória rapidamente sobre como usar tipos do Python (mesmo que você não use o FastAPI), confira esse rápido tutorial: [Tipos do Python](python-types.md){.internal-link target=_blank}. @@ -63,7 +63,7 @@ second_user_data = { my_second_user: User = User(**second_user_data) ``` -/// info +/// info | Informação `**second_user_data` quer dizer: @@ -71,13 +71,13 @@ Passe as chaves e valores do dicionário `second_user_data` diretamente como arg /// -### Suporte de editores +### Suporte de editores { #editor-support } -Todo o _framework_ foi projetado para ser fácil e intuitivo de usar, todas as decisões foram testadas em vários editores antes do início do desenvolvimento, para garantir a melhor experiência de desenvolvimento. +Todo o framework foi projetado para ser fácil e intuitivo de usar, todas as decisões foram testadas em vários editores antes do início do desenvolvimento, para garantir a melhor experiência de desenvolvimento. -Na última pesquisa do desenvolvedor Python ficou claro que o recurso mais utilizado é o "auto completar". +Na pesquisa de desenvolvedores Python, ficou claro que um dos recursos mais utilizados é o "preenchimento automático". -Todo o _framework_ **FastAPI** é feito para satisfazer isso. Auto completação funciona em todos os lugares. +Todo o framework **FastAPI** é feito para satisfazer isso. O preenchimento automático funciona em todos os lugares. Você raramente precisará voltar à documentação. @@ -91,17 +91,17 @@ Aqui está como o editor poderá te ajudar: ![editor support](https://fastapi.tiangolo.com/img/pycharm-completion.png) -Você terá completação do seu código que você poderia considerar impossível antes. Como por exemplo, a chave `price` dentro do corpo JSON (que poderia ter sido aninhado) que vem de uma requisição. +Você terá preenchimento automático no seu código que você poderia considerar impossível antes. Como por exemplo, a chave `price` dentro do corpo JSON (que poderia ter sido aninhado) que vem de uma requisição. -Sem a necessidade de digitar nomes de chaves erroneamente, ir e voltar entre documentações, ou rolar pela página para descobrir se você utilizou `username` or `user_name`. +Sem a necessidade de digitar nomes de chaves erroneamente, ir e voltar entre documentações, ou rolar pela página para descobrir se você utilizou `username` ou `user_name`. -### Breve +### Breve { #short } Há **padrões** sensíveis para tudo, com configurações adicionais em todos os lugares. Todos os parâmetros podem ser regulados para fazer o que você precisa e para definir a API que você necessita. Por padrão, tudo **"simplesmente funciona"**. -### Validação +### Validação { #validation } * Validação para a maioria dos (ou todos?) **tipos de dados** do Python, incluindo: * objetos JSON (`dict`). @@ -117,7 +117,7 @@ Por padrão, tudo **"simplesmente funciona"**. Toda a validação é controlada pelo robusto e bem estabelecido **Pydantic**. -### Segurança e autenticação +### Segurança e autenticação { #security-and-authentication } Segurança e autenticação integradas. Sem nenhum compromisso com bancos de dados ou modelos de dados. @@ -130,34 +130,34 @@ Todos os esquemas de seguranças definidos no OpenAPI, incluindo: * parâmetros da Query. * Cookies etc. -Além disso, todos os recursos de seguranças do Starlette (incluindo **cookies de sessão**). +Além disso, todos os recursos de segurança do Starlette (incluindo **cookies de sessão**). Tudo construído como ferramentas e componentes reutilizáveis que são fáceis de integrar com seus sistemas, armazenamento de dados, banco de dados relacionais e não-relacionais etc. -### Injeção de dependência +### Injeção de dependência { #dependency-injection } FastAPI inclui um sistema de injeção de dependência extremamente fácil de usar, mas extremamente poderoso. * Mesmo dependências podem ter dependências, criando uma hierarquia ou **"grafo" de dependências**. -* Tudo **automaticamente controlado** pelo _framework_. +* Tudo **automaticamente controlado** pelo framework. * Todas as dependências podem pedir dados das requisições e **ampliar** as restrições e documentação automática da **operação de caminho**. * **Validação automática** mesmo para parâmetros da *operação de caminho* definidos em dependências. * Suporte para sistemas de autenticação complexos, **conexões com banco de dados** etc. -* **Sem comprometer** os bancos de dados, _frontends_ etc. Mas fácil integração com todos eles. +* **Sem comprometer** os bancos de dados, frontends etc. Mas fácil integração com todos eles. -### "Plug-ins" ilimitados +### "Plug-ins" ilimitados { #unlimited-plug-ins } Ou, de outra forma, sem a necessidade deles, importe e use o código que precisar. Qualquer integração é projetada para ser tão simples de usar (com dependências) que você pode criar um "plug-in" para suas aplicações com 2 linhas de código usando a mesma estrutura e sintaxe para as suas *operações de caminho*. -### Testado +### Testado { #tested } * 100% de cobertura de testes. -* 100% do código utiliza type annotations. +* 100% do código com anotações de tipo. * Usado para aplicações em produção. -## Recursos do Starlette +## Recursos do Starlette { #starlette-features } **FastAPI** é totalmente compatível com (e baseado no) Starlette. Então, qualquer código adicional Starlette que você tiver, também funcionará. @@ -165,18 +165,17 @@ Qualquer integração é projetada para ser tão simples de usar (com dependênc Com **FastAPI**, você terá todos os recursos do **Starlette** (já que FastAPI é apenas um Starlette com esteróides): -* Desempenho realmente impressionante. É um dos _frameworks_ Python disponíveis mais rápidos, a par com o **NodeJS** e **Go**. +* Desempenho realmente impressionante. É um dos frameworks Python disponíveis mais rápidos, a par com o **NodeJS** e **Go**. * Suporte a **WebSocket**. -* Suporte a **GraphQL**. -* Tarefas em processo _background_. +* Tarefas em processo background. * Eventos na inicialização e encerramento. * Cliente de testes construído sobre HTTPX. * Respostas em **CORS**, GZip, Static Files, Streaming. * Suporte a **Session e Cookie**. * 100% de cobertura de testes. -* 100% do código utilizando _type annotations_. +* 100% do código utilizando anotações de tipo. -## Recursos do Pydantic +## Recursos do Pydantic { #pydantic-features } **FastAPI** é totalmente compatível com (e baseado no) Pydantic. Então, qualquer código Pydantic adicional que você tiver, também funcionará. @@ -192,7 +191,7 @@ Com **FastAPI** você terá todos os recursos do **Pydantic** (já que FastAPI u * Sem novas definições de esquema de micro-linguagem para aprender. * Se você conhece os tipos do Python, você sabe como usar o Pydantic. * Vai bem com o/a seu/sua **IDE/linter/cérebro**: - * Como as estruturas de dados do Pydantic são apenas instâncias de classes que você define, a auto completação, _linting_, _mypy_ e a sua intuição devem funcionar corretamente com seus dados validados. + * Como as estruturas de dados do Pydantic são apenas instâncias de classes que você define, o preenchimento automático, linting, mypy e a sua intuição devem funcionar corretamente com seus dados validados. * Valida **estruturas complexas**: * Use modelos hierárquicos do Pydantic, `List` e `Dict` do `typing` do Python, etc. * Validadores permitem que esquemas de dados complexos sejam limpos e facilmente definidos, conferidos e documentados como JSON Schema. diff --git a/docs/pt/docs/help-fastapi.md b/docs/pt/docs/help-fastapi.md index 0de1ed648..4f58c091f 100644 --- a/docs/pt/docs/help-fastapi.md +++ b/docs/pt/docs/help-fastapi.md @@ -1,149 +1,256 @@ -# Ajuda FastAPI - Obter Ajuda +# Ajude o FastAPI - Obtenha ajuda { #help-fastapi-get-help } Você gosta do **FastAPI**? -Você gostaria de ajudar o FastAPI, outros usários, e o autor? +Você gostaria de ajudar o FastAPI, outras pessoas usuárias e o autor? -Ou você gostaria de obter ajuda relacionada ao **FastAPI**?? +Ou você gostaria de obter ajuda com o **FastAPI**? -Existem métodos muito simples de ajudar (A maioria das ajudas podem ser feitas com um ou dois cliques). +Há maneiras muito simples de ajudar (várias envolvem apenas um ou dois cliques). -E também existem vários modos de se conseguir ajuda. +E também há várias formas de obter ajuda. -## Inscreva-se na newsletter +## Assine a newsletter { #subscribe-to-the-newsletter } -Você pode se inscrever (pouco frequente) [**FastAPI e amigos** newsletter](newsletter.md){.internal-link target=_blank} para receber atualizações: +Você pode assinar a (infrequente) [newsletter do **FastAPI and friends**](newsletter.md){.internal-link target=_blank} para ficar por dentro de: * Notícias sobre FastAPI e amigos 🚀 * Tutoriais 📝 -* Recursos ✨ -* Mudanças de última hora 🚨 -* Truques e dicas ✅ +* Funcionalidades ✨ +* Mudanças incompatíveis 🚨 +* Dicas e truques ✅ -## Siga o FastAPI no X (Twitter) +## Siga o FastAPI no X (Twitter) { #follow-fastapi-on-x-twitter } Siga @fastapi no **X (Twitter)** para receber as últimas notícias sobre o **FastAPI**. 🐦 -## Favorite o **FastAPI** no GitHub +## Dê uma estrela ao **FastAPI** no GitHub { #star-fastapi-in-github } -Você pode "favoritar" o FastAPI no GitHub (clicando na estrela no canto superior direito): https://github.com/fastapi/fastapi. ⭐️ +Você pode “marcar com estrela” o FastAPI no GitHub (clicando no botão de estrela no canto superior direito): https://github.com/fastapi/fastapi. ⭐️ -Favoritando, outros usuários poderão encontrar mais facilmente e verão que já foi útil para muita gente. +Ao adicionar uma estrela, outras pessoas conseguirão encontrá-lo com mais facilidade e verão que já foi útil para muita gente. -## Acompanhe novos updates no repositorio do GitHub +## Acompanhe o repositório no GitHub para lançamentos { #watch-the-github-repository-for-releases } -Você pode "acompanhar" (watch) o FastAPI no GitHub (clicando no botão com um "olho" no canto superior direito): https://github.com/fastapi/fastapi. 👀 +Você pode “acompanhar” (watch) o FastAPI no GitHub (clicando no botão “watch” no canto superior direito): https://github.com/fastapi/fastapi. 👀 -Podendo selecionar apenas "Novos Updates". +Lá você pode selecionar “Apenas lançamentos” (Releases only). -Fazendo isto, serão enviadas notificações (em seu email) sempre que tiver novos updates (uma nova versão) com correções de bugs e novos recursos no **FastAPI** +Fazendo isso, você receberá notificações (no seu email) sempre que houver um novo lançamento (uma nova versão) do **FastAPI** com correções de bugs e novas funcionalidades. -## Conect-se com o autor +## Conecte-se com o autor { #connect-with-the-author } Você pode se conectar comigo (Sebastián Ramírez / `tiangolo`), o autor. Você pode: -* Me siga no **GitHub**. - * Ver também outros projetos Open Source criados por mim que podem te ajudar. - * Me seguir para saber quando um novo projeto Open Source for criado. -* Me siga no **X (Twitter)**. - * Me dizer o motivo pelo o qual você está usando o FastAPI(Adoro ouvir esse tipo de comentário). - * Saber quando eu soltar novos anúncios ou novas ferramentas. - * Também é possivel seguir o @fastapi no X (Twitter) (uma conta aparte). -* Conect-se comigo no **Linkedin**. - * Saber quando eu fizer novos anúncios ou novas ferramentas (apesar de que uso o X (Twitter) com mais frequência 🤷‍♂). -* Ler meus artigos (ou me seguir) no **Dev.to** ou no **Medium**. - * Ficar por dentro de novas ideias, artigos, e ferramentas criadas por mim. - * Me siga para saber quando eu publicar algo novo. +* Me seguir no **GitHub**. + * Ver outros projetos Open Source que criei e que podem ajudar você. + * Me seguir para saber quando eu criar um novo projeto Open Source. +* Me seguir no **X (Twitter)** ou no Mastodon. + * Me contar como você usa o FastAPI (adoro saber disso). + * Ficar sabendo quando eu fizer anúncios ou lançar novas ferramentas. + * Você também pode seguir @fastapi no X (Twitter) (uma conta separada). +* Me seguir no **LinkedIn**. + * Ver quando eu fizer anúncios ou lançar novas ferramentas (embora eu use mais o X (Twitter) 🤷‍♂). +* Ler o que escrevo (ou me seguir) no **Dev.to** ou no **Medium**. + * Ler outras ideias, artigos e conhecer ferramentas que criei. + * Me seguir para ver quando eu publicar algo novo. -## Tweete sobre **FastAPI** +## Tweet sobre o **FastAPI** { #tweet-about-fastapi } -Tweete sobre o **FastAPI** e compartilhe comigo e com os outros o porque de gostar do FastAPI. 🎉 +Tweet sobre o **FastAPI** e conte para mim e para outras pessoas por que você gosta dele. 🎉 -Adoro ouvir sobre como o **FastAPI** é usado, o que você gosta nele, em qual projeto/empresa está sendo usado, etc. +Eu adoro saber como o **FastAPI** está sendo usado, o que você tem curtido nele, em qual projeto/empresa você o utiliza, etc. -## Vote no FastAPI +## Vote no FastAPI { #vote-for-fastapi } * Vote no **FastAPI** no Slant. -* Vote no **FastAPI** no AlternativeTo. +* Vote no **FastAPI** no AlternativeTo. +* Diga que você usa o **FastAPI** no StackShare. -## Responda perguntas no GitHub +## Ajude outras pessoas com perguntas no GitHub { #help-others-with-questions-in-github } -Você pode acompanhar as perguntas existentes e tentar ajudar outros, . 🤓 +Você pode tentar ajudar outras pessoas com suas perguntas em: -Ajudando a responder as questões de varias pessoas, você pode se tornar um [Expert em FastAPI](fastapi-people.md#especialistas){.internal-link target=_blank} oficial. 🎉 +* GitHub Discussions +* GitHub Issues -## Acompanhe o repositório do GitHub +Em muitos casos você já pode saber a resposta para aquelas perguntas. 🤓 -Você pode "acompanhar" (watch) o FastAPI no GitHub (clicando no "olho" no canto superior direito): https://github.com/fastapi/fastapi. 👀 +Se você estiver ajudando muitas pessoas com suas perguntas, você se tornará um(a) [Especialista em FastAPI](fastapi-people.md#fastapi-experts){.internal-link target=_blank} oficial. 🎉 -Se você selecionar "Acompanhando" (Watching) em vez de "Apenas Lançamentos" (Releases only) você receberá notificações quando alguém tiver uma nova pergunta. +Apenas lembre-se, o ponto mais importante é: tente ser gentil. As pessoas chegam com frustrações e, em muitos casos, não perguntam da melhor forma, mas tente ao máximo ser gentil. 🤗 -Assim podendo tentar ajudar a resolver essas questões. - -## Faça perguntas - -É possível criar uma nova pergunta no repositório do GitHub, por exemplo: - -* Faça uma **pergunta** ou pergunte sobre um **problema**. -* Sugira novos **recursos**. - -**Nota**: Se você fizer uma pergunta, então eu gostaria de pedir que você também ajude os outros com suas respectivas perguntas. 😉 - -## Crie um Pull Request - -É possível [contribuir](contributing.md){.internal-link target=_blank} no código fonte fazendo Pull Requests, por exemplo: - -* Para corrigir um erro de digitação que você encontrou na documentação. -* Para compartilhar um artigo, video, ou podcast criados por você sobre o FastAPI editando este arquivo. - * Não se esqueça de adicionar o link no começo da seção correspondente. -* Para ajudar [traduzir a documentação](contributing.md#traducoes){.internal-link target=_blank} para sua lingua. - * Também é possivel revisar as traduções já existentes. -* Para propor novas seções na documentação. -* Para corrigir um bug/questão. -* Para adicionar um novo recurso. - -## Entre no chat - -Entre no 👥 server de conversa do Discord 👥 e conheça novas pessoas da comunidade -do FastAPI. - -/// tip | Dica - -Para perguntas, pergunte nas questões do GitHub, lá tem um chance maior de você ser ajudado sobre o FastAPI [FastAPI Experts](fastapi-people.md#especialistas){.internal-link target=_blank}. - -Use o chat apenas para outro tipo de assunto. - -/// - -### Não faça perguntas no chat - -Tenha em mente que os chats permitem uma "conversa mais livre", dessa forma é muito fácil fazer perguntas que são muito genéricas e dificeís de responder, assim você pode acabar não sendo respondido. - -Nas questões do GitHub o template irá te guiar para que você faça a sua pergunta de um jeito mais correto, fazendo com que você receba respostas mais completas, e até mesmo que você mesmo resolva o problema antes de perguntar. E no GitHub eu garanto que sempre irei responder todas as perguntas, mesmo que leve um tempo. Eu pessoalmente não consigo fazer isso via chat. 😅 - -Conversas no chat não são tão fáceis de serem encontrados quanto no GitHub, então questões e respostas podem se perder dentro da conversa. E apenas as que estão nas questões do GitHub contam para você se tornar um [Expert em FastAPI](fastapi-people.md#especialistas){.internal-link target=_blank}, então você receberá mais atenção nas questões do GitHub. - -Por outro lado, existem milhares de usuários no chat, então tem uma grande chance de você encontrar alguém para trocar uma idéia por lá em qualquer horário. 😄 - -## Patrocine o autor - -Você também pode ajudar o autor financeiramente (eu) através do GitHub sponsors. - -Lá você pode me pagar um cafézinho ☕️ como agradecimento. 😄 - -E você também pode se tornar um patrocinador Prata ou Ouro do FastAPI. 🏅🎉 - -## Patrocine as ferramente que potencializam o FastAPI - -Como você viu na documentação, o FastAPI se apoia em nos gigantes, Starlette e Pydantic. - -Patrocine também: - -* Samuel Colvin (Pydantic) -* Encode (Starlette, Uvicorn) +A ideia é que a comunidade do **FastAPI** seja gentil e acolhedora. Ao mesmo tempo, não aceite bullying ou comportamentos desrespeitosos com outras pessoas. Temos que cuidar uns dos outros. --- -Muito Obrigado! 🚀 +Veja como ajudar outras pessoas com perguntas (em discussions ou issues): + +### Entenda a pergunta { #understand-the-question } + +* Verifique se você consegue entender qual é o **objetivo** e o caso de uso de quem está perguntando. + +* Depois verifique se a pergunta (a grande maioria são perguntas) está **clara**. + +* Em muitos casos a pergunta feita é sobre uma solução imaginada pela pessoa usuária, mas pode haver uma solução **melhor**. Se você entender melhor o problema e o caso de uso, pode sugerir uma **solução alternativa** melhor. + +* Se você não entender a pergunta, peça mais **detalhes**. + +### Reproduza o problema { #reproduce-the-problem } + +Na maioria dos casos e na maioria das perguntas há algo relacionado ao **código original** da pessoa. + +Em muitos casos ela só copia um fragmento do código, mas isso não é suficiente para **reproduzir o problema**. + +* Você pode pedir que forneçam um exemplo mínimo, reproduzível, que você possa **copiar e colar** e executar localmente para ver o mesmo erro ou comportamento que elas estão vendo, ou para entender melhor o caso de uso. + +* Se você estiver muito generoso(a), pode tentar **criar um exemplo** assim você mesmo(a), apenas com base na descrição do problema. Só tenha em mente que isso pode levar bastante tempo e pode ser melhor pedir primeiro que esclareçam o problema. + +### Sugira soluções { #suggest-solutions } + +* Depois de conseguir entender a pergunta, você pode dar uma possível **resposta**. + +* Em muitos casos, é melhor entender o **problema subjacente ou caso de uso**, pois pode haver uma forma melhor de resolver do que aquilo que estão tentando fazer. + +### Peça para encerrar { #ask-to-close } + +Se a pessoa responder, há uma grande chance de você ter resolvido o problema, parabéns, **você é um(a) herói(na)**! 🦸 + +* Agora, se isso resolveu o problema, você pode pedir para: + + * No GitHub Discussions: marcar o comentário como a **resposta**. + * No GitHub Issues: **encerrar** a issue. + +## Acompanhe o repositório do GitHub { #watch-the-github-repository } + +Você pode “acompanhar” (watch) o FastAPI no GitHub (clicando no botão “watch” no canto superior direito): https://github.com/fastapi/fastapi. 👀 + +Se você selecionar “Acompanhando” (Watching) em vez de “Apenas lançamentos” (Releases only), receberá notificações quando alguém criar uma nova issue ou pergunta. Você também pode especificar que quer ser notificado(a) apenas sobre novas issues, ou discussions, ou PRs, etc. + +Assim você pode tentar ajudar a resolver essas questões. + +## Faça perguntas { #ask-questions } + +Você pode criar uma nova pergunta no repositório do GitHub, por exemplo para: + +* Fazer uma **pergunta** ou perguntar sobre um **problema**. +* Sugerir uma nova **funcionalidade**. + +**Nota**: se você fizer isso, então vou pedir que você também ajude outras pessoas. 😉 + +## Revise Pull Requests { #review-pull-requests } + +Você pode me ajudar revisando pull requests de outras pessoas. + +Novamente, por favor tente ao máximo ser gentil. 🤗 + +--- + +Veja o que ter em mente e como revisar um pull request: + +### Entenda o problema { #understand-the-problem } + +* Primeiro, garanta que você **entendeu o problema** que o pull request tenta resolver. Pode haver uma discussão mais longa em uma Discussion ou issue do GitHub. + +* Também há uma boa chance de o pull request não ser realmente necessário porque o problema pode ser resolvido de uma **forma diferente**. Aí você pode sugerir ou perguntar sobre isso. + +### Não se preocupe com estilo { #dont-worry-about-style } + +* Não se preocupe muito com coisas como estilos de mensagens de commit, eu vou fazer squash e merge personalizando o commit manualmente. + +* Também não se preocupe com regras de estilo, já há ferramentas automatizadas verificando isso. + +E se houver qualquer outra necessidade de estilo ou consistência, vou pedir diretamente, ou vou adicionar commits por cima com as mudanças necessárias. + +### Verifique o código { #check-the-code } + +* Verifique e leia o código, veja se faz sentido, **execute localmente** e veja se realmente resolve o problema. + +* Depois **comente** dizendo que você fez isso, é assim que saberei que você realmente verificou. + +/// info | Informação + +Infelizmente, eu não posso simplesmente confiar em PRs que têm várias aprovações. + +Já aconteceu várias vezes de haver PRs com 3, 5 ou mais aprovações, provavelmente porque a descrição é atraente, mas quando eu verifico os PRs, eles estão quebrados, têm um bug, ou não resolvem o problema que afirmam resolver. 😅 + +Por isso, é realmente importante que você leia e execute o código, e me avise nos comentários que você fez isso. 🤓 + +/// + +* Se o PR puder ser simplificado de alguma forma, você pode pedir isso, mas não há necessidade de ser exigente demais, pode haver muitos pontos de vista subjetivos (e eu terei o meu também 🙈), então é melhor focar nas coisas fundamentais. + +### Testes { #tests } + +* Me ajude a verificar se o PR tem **testes**. + +* Verifique se os testes **falham** antes do PR. 🚨 + +* Depois verifique se os testes **passam** após o PR. ✅ + +* Muitos PRs não têm testes, você pode **lembrar** a pessoa de adicionar testes, ou até **sugerir** alguns testes você mesmo(a). Essa é uma das coisas que consomem mais tempo e você pode ajudar muito com isso. + +* Depois também comente o que você testou, assim vou saber que você verificou. 🤓 + +## Crie um Pull Request { #create-a-pull-request } + +Você pode [contribuir](contributing.md){.internal-link target=_blank} com o código-fonte fazendo Pull Requests, por exemplo: + +* Para corrigir um erro de digitação que você encontrou na documentação. +* Para compartilhar um artigo, vídeo ou podcast que você criou ou encontrou sobre o FastAPI, editando este arquivo. + * Garanta que você adicione seu link no início da seção correspondente. +* Para ajudar a [traduzir a documentação](contributing.md#translations){.internal-link target=_blank} para seu idioma. + * Você também pode ajudar a revisar as traduções criadas por outras pessoas. +* Para propor novas seções de documentação. +* Para corrigir uma issue/bug existente. + * Garanta que você adicione testes. +* Para adicionar uma nova funcionalidade. + * Garanta que você adicione testes. + * Garanta que você adicione documentação se for relevante. + +## Ajude a manter o FastAPI { #help-maintain-fastapi } + +Ajude-me a manter o **FastAPI**! 🤓 + +Há muito trabalho a fazer e, para a maior parte dele, **VOCÊ** pode ajudar. + +As principais tarefas que você pode fazer agora são: + +* [Ajudar outras pessoas com perguntas no GitHub](#help-others-with-questions-in-github){.internal-link target=_blank} (veja a seção acima). +* [Revisar Pull Requests](#review-pull-requests){.internal-link target=_blank} (veja a seção acima). + +Essas duas tarefas são as que **mais consomem tempo**. Esse é o principal trabalho de manter o FastAPI. + +Se você puder me ajudar com isso, **você está me ajudando a manter o FastAPI** e garantindo que ele continue **avançando mais rápido e melhor**. 🚀 + +## Entre no chat { #join-the-chat } + +Entre no 👥 servidor de chat do Discord 👥 e converse com outras pessoas da comunidade FastAPI. + +/// tip | Dica + +Para perguntas, faça-as no GitHub Discussions, há uma chance muito maior de você receber ajuda pelos [Especialistas em FastAPI](fastapi-people.md#fastapi-experts){.internal-link target=_blank}. + +Use o chat apenas para outras conversas gerais. + +/// + +### Não use o chat para perguntas { #dont-use-the-chat-for-questions } + +Tenha em mente que, como os chats permitem uma “conversa mais livre”, é fácil fazer perguntas muito gerais e mais difíceis de responder, então você pode acabar não recebendo respostas. + +No GitHub, o template vai orientar você a escrever a pergunta certa para que você consiga obter uma boa resposta com mais facilidade, ou até resolver o problema sozinho(a) antes de perguntar. E no GitHub eu consigo garantir que sempre vou responder tudo, mesmo que leve algum tempo. Eu pessoalmente não consigo fazer isso com os sistemas de chat. 😅 + +As conversas nos sistemas de chat também não são tão fáceis de pesquisar quanto no GitHub, então perguntas e respostas podem se perder na conversa. E somente as que estão no GitHub contam para você se tornar um(a) [Especialista em FastAPI](fastapi-people.md#fastapi-experts){.internal-link target=_blank}, então é bem provável que você receba mais atenção no GitHub. + +Por outro lado, há milhares de usuários nos sistemas de chat, então há uma grande chance de você encontrar alguém para conversar por lá quase o tempo todo. 😄 + +## Patrocine o autor { #sponsor-the-author } + +Se o seu **produto/empresa** depende de ou está relacionado ao **FastAPI** e você quer alcançar suas pessoas usuárias, você pode patrocinar o autor (eu) através do GitHub sponsors. Dependendo do nível, você pode obter benefícios extras, como um selo na documentação. 🎁 + +--- + +Obrigado! 🚀 diff --git a/docs/pt/docs/history-design-future.md b/docs/pt/docs/history-design-future.md index 1d0768c62..699528739 100644 --- a/docs/pt/docs/history-design-future.md +++ b/docs/pt/docs/history-design-future.md @@ -1,4 +1,4 @@ -# História, Design e Futuro +# História, Design e Futuro { #history-design-and-future } Há algum tempo, um usuário **FastAPI** perguntou: @@ -6,7 +6,7 @@ Há algum tempo, **Pydantic** por suas vantagens. -Então eu contribuí com ele, para deixá-lo completamente de acordo com o JSON Schema, para dar suporte a diferentes maneiras de definir declarações de restrições, e melhorar o suporte a editores (conferências de tipos, auto completações) baseado nos testes em vários editores. +Então eu contribuí com ele, para deixá-lo completamente de acordo com o JSON Schema, para dar suporte a diferentes maneiras de definir declarações de restrições, e melhorar o suporte a editores (conferências de tipos, preenchimento automático) baseado nos testes em vários editores. Durante o desenvolvimento, eu também contribuí com o **Starlette**, outro requisito chave. -## Desenvolvimento +## Desenvolvimento { #development } Quando comecei a criar o **FastAPI** de fato, a maior parte das peças já estavam encaixadas, o design estava definido, os requisitos e ferramentas já estavam prontos, e o conhecimento sobre os padrões e especificações estavam claros e frescos. -## Futuro +## Futuro { #future } Nesse ponto, já está claro que o **FastAPI** com suas ideias está sendo útil para muitas pessoas. diff --git a/docs/pt/docs/how-to/conditional-openapi.md b/docs/pt/docs/how-to/conditional-openapi.md index 6b44e9c81..da4f5f764 100644 --- a/docs/pt/docs/how-to/conditional-openapi.md +++ b/docs/pt/docs/how-to/conditional-openapi.md @@ -1,12 +1,12 @@ -# OpenAPI condicional +# OpenAPI condicional { #conditional-openapi } -Se necessário, você pode usar configurações e variáveis ​​de ambiente para configurar o OpenAPI condicionalmente, dependendo do ambiente, e até mesmo desativá-lo completamente. +Se necessário, você pode usar configurações e variáveis de ambiente para configurar o OpenAPI condicionalmente dependendo do ambiente e até mesmo desativá-lo completamente. -## Sobre segurança, APIs e documentos +## Sobre segurança, APIs e documentação { #about-security-apis-and-docs } -Ocultar suas interfaces de usuário de documentação na produção *não deveria* ser a maneira de proteger sua API. +Ocultar suas interfaces de usuário de documentação na produção não *deveria* ser a maneira de proteger sua API. -Isso não adiciona nenhuma segurança extra à sua API; as *operações de rotas* ainda estarão disponíveis onde estão. +Isso não adiciona nenhuma segurança extra à sua API; as *operações de rota* ainda estarão disponíveis onde estão. Se houver uma falha de segurança no seu código, ela ainda existirá. @@ -17,15 +17,15 @@ Se você quiser proteger sua API, há várias coisas melhores que você pode faz * Certifique-se de ter modelos Pydantic bem definidos para seus corpos de solicitação e respostas. * Configure quaisquer permissões e funções necessárias usando dependências. * Nunca armazene senhas em texto simples, apenas hashes de senha. -* Implemente e use ferramentas criptográficas bem conhecidas, como tokens JWT e Passlib, etc. +* Implemente e use ferramentas criptográficas bem conhecidas, como pwdlib e tokens JWT, etc. * Adicione controles de permissão mais granulares com escopos OAuth2 quando necessário. * ...etc. -No entanto, você pode ter um caso de uso muito específico em que realmente precisa desabilitar a documentação da API para algum ambiente (por exemplo, para produção) ou dependendo de configurações de variáveis ​​de ambiente. +No entanto, você pode ter um caso de uso muito específico em que realmente precisa desabilitar a documentação da API para algum ambiente (por exemplo, para produção) ou dependendo de configurações de variáveis de ambiente. -## OpenAPI condicional com configurações e variáveis ​​de ambiente +## OpenAPI condicional com configurações e variáveis de ambiente { #conditional-openapi-from-settings-and-env-vars } -Você pode usar facilmente as mesmas configurações do Pydantic para configurar sua OpenAPI gerada e as interfaces de usuário de documentos. +Você pode usar facilmente as mesmas configurações do Pydantic para configurar sua OpenAPI gerada e as interfaces de usuário da documentação. Por exemplo: @@ -33,9 +33,9 @@ Por exemplo: Aqui declaramos a configuração `openapi_url` com o mesmo padrão de `"/openapi.json"`. -E então o usamos ao criar o aplicativo `FastAPI`. +E então a usamos ao criar a aplicação `FastAPI`. -Então você pode desabilitar o OpenAPI (incluindo os documentos da interface do usuário) definindo a variável de ambiente `OPENAPI_URL` como uma string vazia, como: +Então você pode desabilitar o OpenAPI (incluindo a documentação da interface do usuário) definindo a variável de ambiente `OPENAPI_URL` como uma string vazia, como:
@@ -47,7 +47,7 @@ $ OPENAPI_URL= uvicorn main:app
-Então, se você acessar as URLs em `/openapi.json`, `/docs` ou `/redoc`, você receberá apenas um erro `404 Não Encontrado` como: +Então, se você acessar as URLs em `/openapi.json`, `/docs` ou `/redoc`, você receberá apenas um erro `404 Not Found` como: ```JSON { diff --git a/docs/pt/docs/how-to/configure-swagger-ui.md b/docs/pt/docs/how-to/configure-swagger-ui.md index 915b2b5c5..ecf85a6ee 100644 --- a/docs/pt/docs/how-to/configure-swagger-ui.md +++ b/docs/pt/docs/how-to/configure-swagger-ui.md @@ -1,14 +1,14 @@ -# Configurar Swagger UI +# Configure a UI do Swagger { #configure-swagger-ui } Você pode configurar alguns parâmetros extras da UI do Swagger. -Para configurá-los, passe o argumento `swagger_ui_parameters` ao criar o objeto de aplicativo `FastAPI()` ou para a função `get_swagger_ui_html()`. +Para configurá-los, passe o argumento `swagger_ui_parameters` ao criar o objeto da aplicação `FastAPI()` ou para a função `get_swagger_ui_html()`. `swagger_ui_parameters` recebe um dicionário com as configurações passadas diretamente para o Swagger UI. O FastAPI converte as configurações para **JSON** para torná-las compatíveis com JavaScript, pois é disso que o Swagger UI precisa. -## Desabilitar realce de sintaxe +## Desabilitar destaque de sintaxe { #disable-syntax-highlighting } Por exemplo, você pode desabilitar o destaque de sintaxe na UI do Swagger. @@ -24,7 +24,7 @@ Mas você pode desabilitá-lo definindo `syntaxHighlight` como `False`: -## Alterar o tema +## Alterar o tema { #change-the-theme } Da mesma forma que você pode definir o tema de destaque de sintaxe com a chave `"syntaxHighlight.theme"` (observe que há um ponto no meio): @@ -34,13 +34,13 @@ Essa configuração alteraria o tema de cores de destaque de sintaxe: -## Alterar parâmetros de UI padrão do Swagger +## Alterar parâmetros de UI padrão do Swagger { #change-default-swagger-ui-parameters } O FastAPI inclui alguns parâmetros de configuração padrão apropriados para a maioria dos casos de uso. Inclui estas configurações padrão: -{* ../../fastapi/openapi/docs.py ln[7:23] *} +{* ../../fastapi/openapi/docs.py ln[8:23] hl[17:23] *} Você pode substituir qualquer um deles definindo um valor diferente no argumento `swagger_ui_parameters`. @@ -48,15 +48,15 @@ Por exemplo, para desabilitar `deepLinking` você pode passar essas configuraç {* ../../docs_src/configure_swagger_ui/tutorial003.py hl[3] *} -## Outros parâmetros da UI do Swagger +## Outros parâmetros da UI do Swagger { #other-swagger-ui-parameters } Para ver todas as outras configurações possíveis que você pode usar, leia a documentação oficial dos parâmetros da UI do Swagger. -## Configurações somente JavaScript +## Configurações somente JavaScript { #javascript-only-settings } -A interface do usuário do Swagger também permite que outras configurações sejam objetos **somente JavaScript** (por exemplo, funções JavaScript). +A UI do Swagger também permite que outras configurações sejam objetos **somente JavaScript** (por exemplo, funções JavaScript). -O FastAPI também inclui estas configurações de `predefinições` somente para JavaScript: +O FastAPI também inclui estas configurações `presets` somente para JavaScript: ```JavaScript presets: [ @@ -67,4 +67,4 @@ presets: [ Esses são objetos **JavaScript**, não strings, então você não pode passá-los diretamente do código Python. -Se você precisar usar configurações somente JavaScript como essas, você pode usar um dos métodos acima. Sobrescreva todas as *operações de rotas* do Swagger UI e escreva manualmente qualquer JavaScript que você precisar. +Se você precisar usar configurações somente JavaScript como essas, você pode usar um dos métodos acima. Substitua toda a *operação de rota* do Swagger UI e escreva manualmente qualquer JavaScript que você precisar. diff --git a/docs/pt/docs/how-to/custom-docs-ui-assets.md b/docs/pt/docs/how-to/custom-docs-ui-assets.md index b7de6c8bd..30224c72b 100644 --- a/docs/pt/docs/how-to/custom-docs-ui-assets.md +++ b/docs/pt/docs/how-to/custom-docs-ui-assets.md @@ -1,26 +1,26 @@ -# Recursos Estáticos Personalizados para a UI de Documentação (Hospedagem Própria) +# Recursos Estáticos Personalizados para a UI de Documentação (Hospedagem Própria) { #custom-docs-ui-static-assets-self-hosting } A documentação da API usa **Swagger UI** e **ReDoc**, e cada um deles precisa de alguns arquivos JavaScript e CSS. -Por padrão, esses arquivos são fornecidos por um CDN. +Por padrão, esses arquivos são fornecidos por um CDN. Mas é possível personalizá-los, você pode definir um CDN específico ou providenciar os arquivos você mesmo. -## CDN Personalizado para JavaScript e CSS +## CDN Personalizado para JavaScript e CSS { #custom-cdn-for-javascript-and-css } -Vamos supor que você deseja usar um CDN diferente, por exemplo, você deseja usar `https://unpkg.com/`. +Vamos supor que você deseja usar um CDN diferente, por exemplo, você deseja usar `https://unpkg.com/`. Isso pode ser útil se, por exemplo, você mora em um país que restringe algumas URLs. -### Desativar a documentação automática +### Desativar a documentação automática { #disable-the-automatic-docs } O primeiro passo é desativar a documentação automática, pois por padrão, ela usa o CDN padrão. -Para desativá-los, defina suas URLs como `None` ao criar seu aplicativo `FastAPI`: +Para desativá-los, defina suas URLs como `None` ao criar sua aplicação FastAPI: {* ../../docs_src/custom_docs_ui/tutorial001.py hl[8] *} -### Incluir a documentação personalizada +### Incluir a documentação personalizada { #include-the-custom-docs } Agora você pode criar as *operações de rota* para a documentação personalizada. @@ -46,23 +46,23 @@ Swagger UI lidará com isso nos bastidores para você, mas ele precisa desse aux /// -### Criar uma *operação de rota* para testar +### Criar uma *operação de rota* para testar { #create-a-path-operation-to-test-it } Agora, para poder testar se tudo funciona, crie uma *operação de rota*: {* ../../docs_src/custom_docs_ui/tutorial001.py hl[36:38] *} -### Teste +### Teste { #test-it } Agora, você deve ser capaz de ir para a documentação em http://127.0.0.1:8000/docs, e recarregar a página, ela carregará esses recursos do novo CDN. -## Hospedagem Própria de JavaScript e CSS para a documentação +## Hospedagem Própria de JavaScript e CSS para a documentação { #self-hosting-javascript-and-css-for-docs } Hospedar o JavaScript e o CSS pode ser útil se, por exemplo, você precisa que seu aplicativo continue funcionando mesmo offline, sem acesso aberto à Internet, ou em uma rede local. -Aqui você verá como providenciar esses arquivos você mesmo, no mesmo aplicativo FastAPI, e configurar a documentação para usá-los. +Aqui você verá como providenciar esses arquivos você mesmo, na mesma aplicação FastAPI, e configurar a documentação para usá-los. -### Estrutura de Arquivos do Projeto +### Estrutura de Arquivos do Projeto { #project-file-structure } Vamos supor que a estrutura de arquivos do seu projeto se pareça com isso: @@ -85,7 +85,7 @@ Sua nova estrutura de arquivos poderia se parecer com isso: └── static/ ``` -### Baixe os arquivos +### Baixe os arquivos { #download-the-files } Baixe os arquivos estáticos necessários para a documentação e coloque-os no diretório `static/`. @@ -96,7 +96,7 @@ Você provavelmente pode clicar com o botão direito em cada link e selecionar u * `swagger-ui-bundle.js` * `swagger-ui.css` -E o **ReDoc** usa os arquivos: +E o **ReDoc** usa o arquivo: * `redoc.standalone.js` @@ -113,14 +113,14 @@ Depois disso, sua estrutura de arquivos deve se parecer com: └── swagger-ui.css ``` -### Prover os arquivos estáticos +### Prover os arquivos estáticos { #serve-the-static-files } * Importe `StaticFiles`. * "Monte" a instância `StaticFiles()` em um caminho específico. {* ../../docs_src/custom_docs_ui/tutorial002.py hl[7,11] *} -### Teste os arquivos estáticos +### Teste os arquivos estáticos { #test-the-static-files } Inicialize seu aplicativo e vá para http://127.0.0.1:8000/static/redoc.standalone.js. @@ -138,15 +138,15 @@ Isso confirma que você está conseguindo fornecer arquivos estáticos do seu ap Agora, podemos configurar o aplicativo para usar esses arquivos estáticos para a documentação. -### Desativar a documentação automática para arquivos estáticos +### Desativar a documentação automática para arquivos estáticos { #disable-the-automatic-docs-for-static-files } Da mesma forma que ao usar um CDN personalizado, o primeiro passo é desativar a documentação automática, pois ela usa o CDN padrão. -Para desativá-los, defina suas URLs como `None` ao criar seu aplicativo `FastAPI`: +Para desativá-los, defina suas URLs como `None` ao criar sua aplicação FastAPI: {* ../../docs_src/custom_docs_ui/tutorial002.py hl[9] *} -### Incluir a documentação personalizada para arquivos estáticos +### Incluir a documentação personalizada para arquivos estáticos { #include-the-custom-docs-for-static-files } E da mesma forma que com um CDN personalizado, agora você pode criar as *operações de rota* para a documentação personalizada. @@ -155,7 +155,7 @@ Novamente, você pode reutilizar as funções internas do FastAPI para criar as * `openapi_url`: a URL onde a página HTML para a documentação pode obter o esquema OpenAPI para a sua API. Você pode usar aqui o atributo `app.openapi_url`. * `title`: o título da sua API. * `oauth2_redirect_url`: Você pode usar `app.swagger_ui_oauth2_redirect_url` aqui para usar o padrão. -* `swagger_js_url`: a URL onde a página HTML para a sua documentação do Swagger UI pode obter o arquivo **JavaScript**. Este é o URL do CDN personalizado. **Este é o URL que seu aplicativo está fornecendo**. +* `swagger_js_url`: a URL onde a página HTML para a sua documentação do Swagger UI pode obter o arquivo **JavaScript**. **Este é o URL que seu aplicativo está fornecendo**. * `swagger_css_url`: a URL onde a página HTML para a sua documentação do Swagger UI pode obter o arquivo **CSS**. **Esse é o que seu aplicativo está fornecendo**. E de forma semelhante para o ReDoc... @@ -172,13 +172,13 @@ Swagger UI lidará com isso nos bastidores para você, mas ele precisa desse aux /// -### Criar uma *operação de rota* para testar arquivos estáticos +### Criar uma *operação de rota* para testar arquivos estáticos { #create-a-path-operation-to-test-static-files } Agora, para poder testar se tudo funciona, crie uma *operação de rota*: {* ../../docs_src/custom_docs_ui/tutorial002.py hl[39:41] *} -### Teste a UI de Arquivos Estáticos +### Teste a UI de Arquivos Estáticos { #test-static-files-ui } Agora, você deve ser capaz de desconectar o WiFi, ir para a documentação em http://127.0.0.1:8000/docs, e recarregar a página. diff --git a/docs/pt/docs/how-to/custom-request-and-route.md b/docs/pt/docs/how-to/custom-request-and-route.md index 151a0f5d4..c623dd8a0 100644 --- a/docs/pt/docs/how-to/custom-request-and-route.md +++ b/docs/pt/docs/how-to/custom-request-and-route.md @@ -1,12 +1,12 @@ -# Requisições Personalizadas e Classes da APIRoute +# Request e classe APIRoute personalizadas { #custom-request-and-apiroute-class } -Em algum casos, você pode querer sobreescrever a lógica usada pelas classes `Request`e `APIRoute`. +Em alguns casos, você pode querer sobrescrever a lógica usada pelas classes `Request` e `APIRoute`. -Em particular, isso pode ser uma boa alternativa para uma lógica em um middleware +Em particular, isso pode ser uma boa alternativa para uma lógica em um middleware. Por exemplo, se você quiser ler ou manipular o corpo da requisição antes que ele seja processado pela sua aplicação. -/// danger | Perigo +/// danger | Cuidado Isso é um recurso "avançado". @@ -14,7 +14,7 @@ Se você for um iniciante em **FastAPI** você deve considerar pular essa seçã /// -## Casos de Uso +## Casos de Uso { #use-cases } Alguns casos de uso incluem: @@ -22,13 +22,13 @@ Alguns casos de uso incluem: * Descomprimir corpos de requisição comprimidos com gzip. * Registrar automaticamente todos os corpos de requisição. -## Manipulando codificações de corpo de requisição personalizadas +## Manipulando codificações de corpo de requisição personalizadas { #handling-custom-request-body-encodings } Vamos ver como usar uma subclasse personalizada de `Request` para descomprimir requisições gzip. E uma subclasse de `APIRoute` para usar essa classe de requisição personalizada. -### Criar uma classe `GzipRequest` personalizada +### Criar uma classe `GzipRequest` personalizada { #create-a-custom-gziprequest-class } /// tip | Dica @@ -44,7 +44,7 @@ Dessa forma, a mesma classe de rota pode lidar com requisições comprimidas ou {* ../../docs_src/custom_request_and_route/tutorial001.py hl[8:15] *} -### Criar uma classe `GzipRoute` personalizada +### Criar uma classe `GzipRoute` personalizada { #create-a-custom-gziproute-class } Em seguida, criamos uma subclasse personalizada de `fastapi.routing.APIRoute` que fará uso do `GzipRequest`. @@ -58,7 +58,7 @@ Aqui nós usamos para criar um `GzipRequest` a partir da requisição original. /// note | Detalhes Técnicos -Um `Request` também tem um `request.receive`, que é uma função para "receber" o corpo da requisição. +Um `Request` tem um atributo `request.scope`, que é apenas um `dict` do Python contendo os metadados relacionados à requisição. Um `Request` também tem um `request.receive`, que é uma função para "receber" o corpo da requisição. @@ -78,7 +78,7 @@ Depois disso, toda a lógica de processamento é a mesma. Mas por causa das nossas mudanças em `GzipRequest.body`, o corpo da requisição será automaticamente descomprimido quando for carregado pelo **FastAPI** quando necessário. -## Acessando o corpo da requisição em um manipulador de exceção +## Acessando o corpo da requisição em um manipulador de exceção { #accessing-the-request-body-in-an-exception-handler } /// tip | Dica @@ -98,9 +98,9 @@ Se uma exceção ocorrer, a instância `Request` ainda estará em escopo, então {* ../../docs_src/custom_request_and_route/tutorial002.py hl[16:18] *} -## Classe `APIRoute` personalizada em um router +## Classe `APIRoute` personalizada em um router { #custom-apiroute-class-in-a-router } -você também pode definir o parametro `route_class` de uma `APIRouter`; +Você também pode definir o parâmetro `route_class` de uma `APIRouter`: {* ../../docs_src/custom_request_and_route/tutorial003.py hl[26] *} diff --git a/docs/pt/docs/how-to/extending-openapi.md b/docs/pt/docs/how-to/extending-openapi.md index b4785edc1..54d56b95a 100644 --- a/docs/pt/docs/how-to/extending-openapi.md +++ b/docs/pt/docs/how-to/extending-openapi.md @@ -1,10 +1,10 @@ -# Extendendo o OpenAPI +# Extendendo o OpenAPI { #extending-openapi } Existem alguns casos em que pode ser necessário modificar o esquema OpenAPI gerado. Nesta seção, você verá como fazer isso. -## O processo normal +## O processo normal { #the-normal-process } O processo normal (padrão) é o seguinte: @@ -33,31 +33,31 @@ O parâmetro `summary` está disponível no OpenAPI 3.1.0 e superior, suportado /// -## Sobrescrevendo os padrões +## Sobrescrevendo os padrões { #overriding-the-defaults } Com as informações acima, você pode usar a mesma função utilitária para gerar o esquema OpenAPI e sobrescrever cada parte que precisar. Por exemplo, vamos adicionar Extensão OpenAPI do ReDoc para incluir um logo personalizado. -### **FastAPI** Normal +### **FastAPI** Normal { #normal-fastapi } Primeiro, escreva toda a sua aplicação **FastAPI** normalmente: {* ../../docs_src/extending_openapi/tutorial001.py hl[1,4,7:9] *} -### Gerar o esquema OpenAPI +### Gerar o esquema OpenAPI { #generate-the-openapi-schema } Em seguida, use a mesma função utilitária para gerar o esquema OpenAPI, dentro de uma função `custom_openapi()`: {* ../../docs_src/extending_openapi/tutorial001.py hl[2,15:21] *} -### Modificar o esquema OpenAPI +### Modificar o esquema OpenAPI { #modify-the-openapi-schema } Agora, você pode adicionar a extensão do ReDoc, incluindo um `x-logo` personalizado ao "objeto" `info` no esquema OpenAPI: {* ../../docs_src/extending_openapi/tutorial001.py hl[22:24] *} -### Armazenar em cache o esquema OpenAPI +### Armazenar em cache o esquema OpenAPI { #cache-the-openapi-schema } Você pode usar a propriedade `.openapi_schema` como um "cache" para armazenar o esquema gerado. @@ -67,14 +67,14 @@ Ele será gerado apenas uma vez, e o mesmo esquema armazenado em cache será uti {* ../../docs_src/extending_openapi/tutorial001.py hl[13:14,25:26] *} -### Sobrescrever o método +### Sobrescrever o método { #override-the-method } Agora, você pode substituir o método `.openapi()` pela sua nova função. {* ../../docs_src/extending_openapi/tutorial001.py hl[29] *} -### Verificar +### Verificar { #check-it } Uma vez que você acessar http://127.0.0.1:8000/redoc, verá que está usando seu logo personalizado (neste exemplo, o logo do **FastAPI**): - + diff --git a/docs/pt/docs/how-to/general.md b/docs/pt/docs/how-to/general.md index 4f21463b2..7f9146862 100644 --- a/docs/pt/docs/how-to/general.md +++ b/docs/pt/docs/how-to/general.md @@ -1,39 +1,38 @@ -# Geral - Como Fazer - Receitas +# Geral - Como Fazer - Receitas { #general-how-to-recipes } Aqui estão vários links para outros locais na documentação, para perguntas gerais ou frequentes -## Filtro de dados- Segurança +## Filtro de dados- Segurança { #filter-data-security } -Para assegurar que você não vai retornar mais dados do que deveria, leia a seção [Tutorial - Response Model - Return Type](../tutorial/response-model.md){.internal-link target=_blank}. +Para assegurar que você não vai retornar mais dados do que deveria, leia a seção [Tutorial - Modelo de Resposta - Tipo de Retorno](../tutorial/response-model.md){.internal-link target=_blank}. -## Tags de Documentação - OpenAPI -Para adicionar tags às suas *rotas* e agrupá-las na UI da documentação, leia a seção [Tutorial - Path Operation Configurations - Tags](../tutorial/path-operation-configuration.md#tags){.internal-link target=_blank}. +## Tags de Documentação - OpenAPI { #documentation-tags-openapi } +Para adicionar tags às suas *operações de rota* e agrupá-las na UI da documentação, leia a seção [Tutorial - Configurações da Operação de Rota - Tags](../tutorial/path-operation-configuration.md#tags){.internal-link target=_blank}. -## Resumo e Descrição da documentação - OpenAPI +## Resumo e Descrição da documentação - OpenAPI { #documentation-summary-and-description-openapi } -Para adicionar um resumo e uma descrição às suas *rotas* e exibi-los na UI da documentação, leia a seção [Tutorial - Path Operation Configurations - Summary and Description](../tutorial/path-operation-configuration.md#summary-and-description){.internal-link target=_blank}. +Para adicionar um resumo e uma descrição às suas *operações de rota* e exibi-los na UI da documentação, leia a seção [Tutorial - Configurações da Operação de Rota - Resumo e Descrição](../tutorial/path-operation-configuration.md#summary-and-description){.internal-link target=_blank}. -## Documentação das Descrições de Resposta - OpenAPI +## Documentação das Descrições de Resposta - OpenAPI { #documentation-response-description-openapi } -Para definir a descrição de uma resposta exibida na interface da documentação, leia a seção [Tutorial - Path Operation Configurations - Response description](../tutorial/path-operation-configuration.md#response-description){.internal-link target=_blank}. +Para definir a descrição de uma resposta exibida na interface da documentação, leia a seção [Tutorial - Configurações da Operação de Rota - Descrição da Resposta](../tutorial/path-operation-configuration.md#response-description){.internal-link target=_blank}. -## Documentação para Depreciar uma *Operação de Rota* - OpenAPI +## Documentação para Depreciar uma *Operação de Rota* - OpenAPI { #documentation-deprecate-a-path-operation-openapi } -Para depreciar uma *operação de rota* e exibi-la na interface da documentação, leia a seção [Tutorial - Path Operation Configurations - Deprecation](../tutorial/path-operation-configuration.md#deprecate-a-path-operation){.internal-link target=_blank}. +Para depreciar uma *operação de rota* e exibi-la na interface da documentação, leia a seção [Tutorial - Configurações da Operação de Rota - Depreciação](../tutorial/path-operation-configuration.md#deprecate-a-path-operation){.internal-link target=_blank}. -## Converter qualquer dado para JSON +## Converter qualquer dado para compatível com JSON { #convert-any-data-to-json-compatible } +Para converter qualquer dado para um formato compatível com JSON, leia a seção [Tutorial - Codificador Compatível com JSON](../tutorial/encoder.md){.internal-link target=_blank}. -Para converter qualquer dado para um formato compatível com JSON, leia a seção [Tutorial - JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank}. +## OpenAPI Metadata - Docs { #openapi-metadata-docs } -## OpenAPI Metadata - Docs +Para adicionar metadados ao seu esquema OpenAPI, incluindo licensa, versão, contato, etc, leia a seção [Tutorial - Metadados e URLs da Documentação](../tutorial/metadata.md){.internal-link target=_blank}. -Para adicionar metadados ao seu esquema OpenAPI, incluindo licensa, versão, contato, etc, leia a seção [Tutorial - Metadata and Docs URLs](../tutorial/metadata.md){.internal-link target=_blank}. +## OpenAPI com URL customizada { #openapi-custom-url } -## OpenAPI com URL customizada +Para customizar a URL do OpenAPI (ou removê-la), leia a seção [Tutorial - Metadados e URLs da Documentação](../tutorial/metadata.md#openapi-url){.internal-link target=_blank}. -Para customizar a URL do OpenAPI (ou removê-la), leia a seção [Tutorial - Metadata and Docs URLs](../tutorial/metadata.md#openapi-url){.internal-link target=_blank}. +## URLs de documentação do OpenAPI { #openapi-docs-urls } -## URLs de documentação do OpenAPI - -Para alterar as URLs usadas ​​para as interfaces de usuário da documentação gerada automaticamente, leia a seção [Tutorial - Metadata and Docs URLs](../tutorial/metadata.md#docs-urls){.internal-link target=_blank}. +Para alterar as URLs usadas ​​para as interfaces de usuário da documentação gerada automaticamente, leia a seção [Tutorial - Metadados e URLs da Documentação](../tutorial/metadata.md#docs-urls){.internal-link target=_blank}. diff --git a/docs/pt/docs/how-to/graphql.md b/docs/pt/docs/how-to/graphql.md index ef0bad7f6..b1e782c4f 100644 --- a/docs/pt/docs/how-to/graphql.md +++ b/docs/pt/docs/how-to/graphql.md @@ -1,4 +1,4 @@ -# GraphQL +# GraphQL { #graphql } Como o **FastAPI** é baseado no padrão **ASGI**, é muito fácil integrar qualquer biblioteca **GraphQL** também compatível com ASGI. @@ -14,7 +14,7 @@ Certifique-se de avaliar se os **benefícios** para o seu caso de uso compensam /// -## Bibliotecas GraphQL +## Bibliotecas GraphQL { #graphql-libraries } Aqui estão algumas das bibliotecas **GraphQL** que têm suporte **ASGI**. Você pode usá-las com **FastAPI**: @@ -27,21 +27,21 @@ Aqui estão algumas das bibliotecas **GraphQL** que têm suporte **ASGI**. Você * Graphene * Com starlette-graphene3 -## GraphQL com Strawberry +## GraphQL com Strawberry { #graphql-with-strawberry } -Se você precisar ou quiser trabalhar com **GraphQL**, **Strawberry** é a biblioteca **recomendada** pois tem o design mais próximo ao design do **FastAPI**, ela é toda baseada em **type annotations**. +Se você precisar ou quiser trabalhar com **GraphQL**, **Strawberry** é a biblioteca **recomendada** pois tem o design mais próximo ao design do **FastAPI**, ela é toda baseada em **anotações de tipo**. Dependendo do seu caso de uso, você pode preferir usar uma biblioteca diferente, mas se você me perguntasse, eu provavelmente sugeriria que você experimentasse o **Strawberry**. Aqui está uma pequena prévia de como você poderia integrar Strawberry com FastAPI: -{* ../../docs_src/graphql/tutorial001.py hl[3,22,25:26] *} +{* ../../docs_src/graphql/tutorial001.py hl[3,22,25] *} Você pode aprender mais sobre Strawberry na documentação do Strawberry. E também na documentação sobre Strawberry com FastAPI. -## Antigo `GraphQLApp` do Starlette +## Antigo `GraphQLApp` do Starlette { #older-graphqlapp-from-starlette } Versões anteriores do Starlette incluiam uma classe `GraphQLApp` para integrar com Graphene. @@ -49,11 +49,11 @@ Ela foi descontinuada do Starlette, mas se você tem código que a utilizava, vo /// tip | Dica -Se você precisa de GraphQL, eu ainda recomendaria que você desse uma olhada no Strawberry, pois ele é baseado em type annotations em vez de classes e tipos personalizados. +Se você precisa de GraphQL, eu ainda recomendaria que você desse uma olhada no Strawberry, pois ele é baseado em anotações de tipo em vez de classes e tipos personalizados. /// -## Saiba Mais +## Saiba Mais { #learn-more } Você pode aprender mais sobre **GraphQL** na documentação oficial do GraphQL. diff --git a/docs/pt/docs/how-to/index.md b/docs/pt/docs/how-to/index.md index 6747b01c7..a3d16380f 100644 --- a/docs/pt/docs/how-to/index.md +++ b/docs/pt/docs/how-to/index.md @@ -1,12 +1,12 @@ -# Como Fazer - Exemplos Práticos +# Como Fazer - Receitas { #how-to-recipes } -Aqui você encontrará diferentes exemplos práticos ou tutoriais de "como fazer" para vários tópicos. +Aqui você encontrará diferentes exemplos práticos ou tutoriais de "como fazer" para **vários tópicos**. A maioria dessas ideias será mais ou menos **independente**, e na maioria dos casos você só precisará estudá-las se elas se aplicarem diretamente ao **seu projeto**. Se algo parecer interessante e útil para o seu projeto, vá em frente e dê uma olhada. Caso contrário, você pode simplesmente ignorá-lo. -/// tip +/// tip | Dica Se você deseja **aprender FastAPI** de forma estruturada (recomendado), leia capítulo por capítulo [Tutorial - Guia de Usuário](../tutorial/index.md){.internal-link target=_blank} em vez disso. diff --git a/docs/pt/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md b/docs/pt/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md new file mode 100644 index 000000000..2a2659a03 --- /dev/null +++ b/docs/pt/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md @@ -0,0 +1,133 @@ +# Migrar do Pydantic v1 para o Pydantic v2 { #migrate-from-pydantic-v1-to-pydantic-v2 } + +Se você tem uma aplicação FastAPI antiga, pode estar usando o Pydantic versão 1. + +O FastAPI tem suporte ao Pydantic v1 ou v2 desde a versão 0.100.0. + +Se você tiver o Pydantic v2 instalado, ele será utilizado. Se, em vez disso, tiver o Pydantic v1, será ele que será utilizado. + +O Pydantic v1 está agora descontinuado e o suporte a ele será removido nas próximas versões do FastAPI, você deveria migrar para o Pydantic v2. Assim, você terá as funcionalidades, melhorias e correções mais recentes. + +/// warning | Atenção + +Além disso, a equipe do Pydantic interrompeu o suporte ao Pydantic v1 para as versões mais recentes do Python, a partir do **Python 3.14**. + +Se quiser usar as funcionalidades mais recentes do Python, você precisará garantir que usa o Pydantic v2. + +/// + +Se você tem uma aplicação FastAPI antiga com Pydantic v1, aqui vou mostrar como migrá-la para o Pydantic v2 e as **novas funcionalidades no FastAPI 0.119.0** para ajudar em uma migração gradual. + +## Guia oficial { #official-guide } + +O Pydantic tem um Guia de Migração oficial do v1 para o v2. + +Ele também inclui o que mudou, como as validações agora são mais corretas e rigorosas, possíveis ressalvas, etc. + +Você pode lê-lo para entender melhor o que mudou. + +## Testes { #tests } + +Garanta que você tenha [testes](../tutorial/testing.md){.internal-link target=_blank} para sua aplicação e que os execute na integração contínua (CI). + +Assim, você pode fazer a atualização e garantir que tudo continua funcionando como esperado. + +## `bump-pydantic` { #bump-pydantic } + +Em muitos casos, quando você usa modelos Pydantic regulares sem personalizações, será possível automatizar a maior parte do processo de migração do Pydantic v1 para o Pydantic v2. + +Você pode usar o `bump-pydantic` da própria equipe do Pydantic. + +Essa ferramenta ajuda a alterar automaticamente a maior parte do código que precisa ser modificado. + +Depois disso, você pode rodar os testes e verificar se tudo funciona. Se funcionar, está concluído. 😎 + +## Pydantic v1 no v2 { #pydantic-v1-in-v2 } + +O Pydantic v2 inclui tudo do Pydantic v1 como um submódulo `pydantic.v1`. + +Isso significa que você pode instalar a versão mais recente do Pydantic v2 e importar e usar os componentes antigos do Pydantic v1 a partir desse submódulo, como se tivesse o Pydantic v1 antigo instalado. + +{* ../../docs_src/pydantic_v1_in_v2/tutorial001_an_py310.py hl[1,4] *} + +### Suporte do FastAPI ao Pydantic v1 no v2 { #fastapi-support-for-pydantic-v1-in-v2 } + +Desde o FastAPI 0.119.0, há também suporte parcial ao Pydantic v1 a partir de dentro do Pydantic v2, para facilitar a migração para o v2. + +Assim, você pode atualizar o Pydantic para a versão 2 mais recente e alterar os imports para usar o submódulo `pydantic.v1`, e em muitos casos tudo simplesmente funcionará. + +{* ../../docs_src/pydantic_v1_in_v2/tutorial002_an_py310.py hl[2,5,15] *} + +/// warning | Atenção + +Tenha em mente que, como a equipe do Pydantic não oferece mais suporte ao Pydantic v1 nas versões recentes do Python, a partir do Python 3.14, o uso de `pydantic.v1` também não é suportado no Python 3.14 e superiores. + +/// + +### Pydantic v1 e v2 na mesma aplicação { #pydantic-v1-and-v2-on-the-same-app } + +Não é suportado pelo Pydantic ter um modelo do Pydantic v2 com campos próprios definidos como modelos do Pydantic v1, ou vice-versa. + +```mermaid +graph TB + subgraph "❌ Not Supported" + direction TB + subgraph V2["Pydantic v2 Model"] + V1Field["Pydantic v1 Model"] + end + subgraph V1["Pydantic v1 Model"] + V2Field["Pydantic v2 Model"] + end + end + + style V2 fill:#f9fff3 + style V1 fill:#fff6f0 + style V1Field fill:#fff6f0 + style V2Field fill:#f9fff3 +``` + +...but, you can have separated models using Pydantic v1 and v2 in the same app. + +```mermaid +graph TB + subgraph "✅ Supported" + direction TB + subgraph V2["Pydantic v2 Model"] + V2Field["Pydantic v2 Model"] + end + subgraph V1["Pydantic v1 Model"] + V1Field["Pydantic v1 Model"] + end + end + + style V2 fill:#f9fff3 + style V1 fill:#fff6f0 + style V1Field fill:#fff6f0 + style V2Field fill:#f9fff3 +``` + +Em alguns casos, é até possível ter modelos Pydantic v1 e v2 na mesma operação de rota na sua aplicação FastAPI: + +{* ../../docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py hl[2:3,6,12,21:22] *} + +No exemplo acima, o modelo de entrada é um modelo Pydantic v1, e o modelo de saída (definido em `response_model=ItemV2`) é um modelo Pydantic v2. + +### Parâmetros do Pydantic v1 { #pydantic-v1-parameters } + +Se você precisar usar algumas das ferramentas específicas do FastAPI para parâmetros como `Body`, `Query`, `Form` etc. com modelos do Pydantic v1, pode importá-las de `fastapi.temp_pydantic_v1_params` enquanto conclui a migração para o Pydantic v2: + +{* ../../docs_src/pydantic_v1_in_v2/tutorial004_an_py310.py hl[4,18] *} + +### Migre em etapas { #migrate-in-steps } + +/// tip | Dica + +Primeiro tente com o `bump-pydantic`; se seus testes passarem e isso funcionar, então você concluiu tudo com um único comando. ✨ + +/// + +Se o `bump-pydantic` não funcionar para o seu caso, você pode usar o suporte a modelos Pydantic v1 e v2 na mesma aplicação para fazer a migração para o Pydantic v2 gradualmente. + +Você poderia primeiro atualizar o Pydantic para usar a versão 2 mais recente e alterar os imports para usar `pydantic.v1` para todos os seus modelos. + +Depois, você pode começar a migrar seus modelos do Pydantic v1 para o v2 em grupos, em etapas graduais. 🚶 diff --git a/docs/pt/docs/how-to/separate-openapi-schemas.md b/docs/pt/docs/how-to/separate-openapi-schemas.md index 291b0e163..8855934fd 100644 --- a/docs/pt/docs/how-to/separate-openapi-schemas.md +++ b/docs/pt/docs/how-to/separate-openapi-schemas.md @@ -1,4 +1,4 @@ -# Esquemas OpenAPI Separados para Entrada e Saída ou Não +# Esquemas OpenAPI Separados para Entrada e Saída ou Não { #separate-openapi-schemas-for-input-and-output-or-not } Ao usar **Pydantic v2**, o OpenAPI gerado é um pouco mais exato e **correto** do que antes. 😎 @@ -6,21 +6,21 @@ Inclusive, em alguns casos, ele terá até **dois JSON Schemas** no OpenAPI para Vamos ver como isso funciona e como alterar se for necessário. -## Modelos Pydantic para Entrada e Saída +## Modelos Pydantic para Entrada e Saída { #pydantic-models-for-input-and-output } Digamos que você tenha um modelo Pydantic com valores padrão, como este: {* ../../docs_src/separate_openapi_schemas/tutorial001_py310.py ln[1:7] hl[7] *} -### Modelo para Entrada +### Modelo para Entrada { #model-for-input } Se você usar esse modelo como entrada, como aqui: {* ../../docs_src/separate_openapi_schemas/tutorial001_py310.py ln[1:15] hl[14] *} -... então o campo `description` não será obrigatório. Porque ele tem um valor padrão de `None`. +... então o campo `description` **não será obrigatório**. Porque ele tem um valor padrão de `None`. -### Modelo de Entrada na Documentação +### Modelo de Entrada na Documentação { #input-model-in-docs } Você pode confirmar que na documentação, o campo `description` não tem um **asterisco vermelho**, não é marcado como obrigatório: @@ -28,7 +28,7 @@ Você pode confirmar que na documentação, o campo `description` não tem um ** -### Modelo para Saída +### Modelo para Saída { #model-for-output } Mas se você usar o mesmo modelo como saída, como aqui: @@ -36,7 +36,7 @@ Mas se você usar o mesmo modelo como saída, como aqui: ... então, como `description` tem um valor padrão, se você **não retornar nada** para esse campo, ele ainda terá o **valor padrão**. -### Modelo para Dados de Resposta de Saída +### Modelo para Dados de Resposta de Saída { #model-for-output-response-data } Se você interagir com a documentação e verificar a resposta, mesmo que o código não tenha adicionado nada em um dos campos `description`, a resposta JSON contém o valor padrão (`null`): @@ -55,15 +55,15 @@ Por causa disso, o JSON Schema para um modelo pode ser diferente dependendo se e * para **entrada**, o `description` **não será obrigatório** * para **saída**, ele será **obrigatório** (e possivelmente `None`, ou em termos de JSON, `null`) -### Modelo para Saída na Documentação +### Modelo para Saída na Documentação { #model-for-output-in-docs } -Você pode verificar o modelo de saída na documentação também, ambos `name` e `description` são marcados como **obrigatórios** com um **asterisco vermelho**: +Você pode verificar o modelo de saída na documentação também, **ambos** `name` e `description` são marcados como **obrigatórios** com um **asterisco vermelho**:
-### Modelo para Entrada e Saída na Documentação +### Modelo para Entrada e Saída na Documentação { #model-for-input-and-output-in-docs } E se você verificar todos os Schemas disponíveis (JSON Schemas) no OpenAPI, verá que há dois, um `Item-Input` e um `Item-Output`. @@ -77,7 +77,7 @@ Mas para `Item-Output`, `description` **é obrigatório**, tem um asterisco verm Com esse recurso do **Pydantic v2**, sua documentação da API fica mais **precisa**, e se você tiver clientes e SDKs gerados automaticamente, eles serão mais precisos também, proporcionando uma melhor **experiência para desenvolvedores** e consistência. 🎉 -## Não Separe Schemas +## Não Separe Schemas { #do-not-separate-schemas } Agora, há alguns casos em que você pode querer ter o **mesmo esquema para entrada e saída**. @@ -93,7 +93,7 @@ O suporte para `separate_input_output_schemas` foi adicionado no FastAPI `0.102. {* ../../docs_src/separate_openapi_schemas/tutorial002_py310.py hl[10] *} -### Mesmo Esquema para Modelos de Entrada e Saída na Documentação +### Mesmo Esquema para Modelos de Entrada e Saída na Documentação { #same-schema-for-input-and-output-models-in-docs } E agora haverá um único esquema para entrada e saída para o modelo, apenas `Item`, e `description` **não será obrigatório**: diff --git a/docs/pt/docs/how-to/testing-database.md b/docs/pt/docs/how-to/testing-database.md index 02f909f24..4258d1e24 100644 --- a/docs/pt/docs/how-to/testing-database.md +++ b/docs/pt/docs/how-to/testing-database.md @@ -1,7 +1,7 @@ -# Testando a Base de Dados +# Testando a Base de Dados { #testing-a-database } Você pode estudar sobre bases de dados, SQL e SQLModel na documentação de SQLModel. 🤓 Aqui tem um mini tutorial de como usar SQLModel com FastAPI. ✨ -Esse tutorial inclui uma sessão sobre testar bases de dados SQL. 😎 +Esse tutorial inclui uma seção sobre testar bases de dados SQL. 😎 diff --git a/docs/pt/docs/index.md b/docs/pt/docs/index.md index a361913c3..43bae7a10 100644 --- a/docs/pt/docs/index.md +++ b/docs/pt/docs/index.md @@ -1,11 +1,11 @@ -# FastAPI +# FastAPI { #fastapi }

- FastAPI + FastAPI

Framework FastAPI, alta performance, fácil de aprender, fácil de codar, pronto para produção @@ -27,7 +27,7 @@ --- -**Documentação**: https://fastapi.tiangolo.com +**Documentação**: https://fastapi.tiangolo.com **Código fonte**: https://github.com/fastapi/fastapi @@ -40,15 +40,15 @@ Os recursos chave são: * **Rápido**: alta performance, equivalente a **NodeJS** e **Go** (graças ao Starlette e Pydantic). [Um dos frameworks mais rápidos disponíveis](#performance). * **Rápido para codar**: Aumenta a velocidade para desenvolver recursos entre 200% a 300%. * * **Poucos bugs**: Reduz cerca de 40% de erros induzidos por humanos (desenvolvedores). * -* **Intuitivo**: Grande suporte a _IDEs_. _Auto-Complete_ em todos os lugares. Menos tempo debugando. +* **Intuitivo**: Grande suporte a _IDEs_. Preenchimento automático em todos os lugares. Menos tempo debugando. * **Fácil**: Projetado para ser fácil de aprender e usar. Menos tempo lendo documentação. -* **Enxuto**: Minimize duplicação de código. Múltiplos recursos para cada declaração de parâmetro. Menos bugs. +* **Enxuto**: Minimize duplicação de código. Múltiplas funcionalidades para cada declaração de parâmetro. Menos bugs. * **Robusto**: Tenha código pronto para produção. E com documentação interativa automática. -* **Baseado em padrões**: Baseado em (e totalmente compatível com) os padrões abertos para APIs: OpenAPI (anteriormente conhecido como Swagger) e _JSON Schema_. +* **Baseado em padrões**: Baseado em (e totalmente compatível com) os padrões abertos para APIs: OpenAPI (anteriormente conhecido como Swagger) e JSON Schema. * estimativas baseadas em testes realizados com equipe interna de desenvolvimento, construindo aplicações em produção. -## Patrocinadores Ouro +## Patrocinadores { #sponsors } @@ -63,9 +63,9 @@ Os recursos chave são: -Outros patrocinadores +Outros patrocinadores -## Opiniões +## Opiniões { #opinions } "*[...] Estou usando **FastAPI** muito esses dias. [...] Estou na verdade planejando utilizar ele em todos os times de **serviços _Machine Learning_ na Microsoft**. Alguns deles estão sendo integrados no _core_ do produto **Windows** e alguns produtos **Office**.*" @@ -111,24 +111,24 @@ Os recursos chave são: --- -## **Typer**, o FastAPI das interfaces de linhas de comando +## **Typer**, o FastAPI das interfaces de linhas de comando { #typer-the-fastapi-of-clis } -Se você estiver construindo uma aplicação _CLI_ para ser utilizada em um terminal ao invés de uma aplicação web, dê uma olhada no **Typer**. +Se você estiver construindo uma aplicação CLI para ser utilizada em um terminal ao invés de uma aplicação web, dê uma olhada no **Typer**. **Typer** é o irmão menor do FastAPI. E seu propósito é ser o **FastAPI das _CLIs_**. ⌨️ 🚀 -## Requisitos +## Requisitos { #requirements } FastAPI está nos ombros de gigantes: * Starlette para as partes web. * Pydantic para a parte de dados. -## Instalação +## Instalação { #installation } -Crie e ative um ambiente virtual, e então instale o FastAPI: +Crie e ative um ambiente virtual e então instale o FastAPI:

@@ -142,11 +142,11 @@ $ pip install "fastapi[standard]" **Nota**: Certifique-se de que você colocou `"fastapi[standard]"` com aspas, para garantir que funcione em todos os terminais. -## Exemplo +## Exemplo { #example } -### Crie +### Crie { #create-it } -* Crie um arquivo `main.py` com: +Crie um arquivo `main.py` com: ```Python from typing import Union @@ -191,11 +191,11 @@ async def read_item(item_id: int, q: Union[str, None] = None): **Nota**: -Se você não sabe, verifique a seção _"Com pressa?"_ sobre `async` e `await` nas docs. +Se você não sabe, verifique a seção _"Com pressa?"_ sobre `async` e `await` nas docs.
-### Rode +### Rode { #run-it } Rode o servidor com: @@ -237,7 +237,7 @@ Você pode ler mais sobre isso na http://127.0.0.1:8000/items/5?q=somequery. @@ -254,7 +254,7 @@ Você acabou de criar uma API que: * A _rota_ `/items/{item_id}` tem um _parâmetro de rota_ `item_id` que deve ser um `int`. * A _rota_ `/items/{item_id}` tem um _parâmetro query_ `q` `str` opcional. -### Documentação Interativa da API +### Documentação Interativa da API { #interactive-api-docs } Agora vá para http://127.0.0.1:8000/docs. @@ -262,7 +262,7 @@ Você verá a documentação automática interativa da API (fornecida por http://127.0.0.1:8000/redoc. @@ -270,7 +270,7 @@ Você verá a documentação automática alternativa (fornecida por http://127.0.0.1:8000/docs. @@ -324,7 +324,7 @@ Agora vá para http://127.0.0.1:8000/redoc. @@ -332,7 +332,7 @@ E agora, vá para Conversão de dados de entrada: vindo da rede para dados e tipos Python. Consegue ler: +* Conversão de dados de entrada: vindo da rede para dados e tipos Python. Consegue ler: * JSON. * Parâmetros de rota. * Parâmetros de _query_ . @@ -370,7 +370,7 @@ item: Item * Cabeçalhos. * Formulários. * Arquivos. -* Conversão de dados de saída de tipos e dados Python para dados de rede (como JSON): +* Conversão de dados de saída de tipos e dados Python para dados de rede (como JSON): * Converte tipos Python (`str`, `int`, `float`, `bool`, `list` etc). * Objetos `datetime`. * Objetos `UUID`. @@ -390,7 +390,7 @@ Voltando ao código do exemplo anterior, **FastAPI** irá: * Verificar se existe um parâmetro de _query_ opcional nomeado como `q` (como em `http://127.0.0.1:8000/items/foo?q=somequery`) para requisições `GET`. * Como o parâmetro `q` é declarado com `= None`, ele é opcional. * Sem o `None` ele poderia ser obrigatório (como o corpo no caso de `PUT`). -* Para requisições `PUT` para `/items/{item_id}`, lerá o corpo como JSON e: +* Para requisições `PUT` para `/items/{item_id}`, lerá o corpo como JSON: * Verifica que tem um atributo obrigatório `name` que deve ser `str`. * Verifica que tem um atributo obrigatório `price` que deve ser `float`. * Verifica que tem an atributo opcional `is_offer`, que deve ser `bool`, se presente. @@ -444,18 +444,17 @@ Para um exemplo mais completo incluindo mais recursos, veja um dos _frameworks_ Python mais rápidos disponíveis, somente atrás de Starlette e Uvicorn (utilizados internamente pelo FastAPI). (*) Para entender mais sobre performance, veja a seção Comparações. -## Dependências +## Dependências { #dependencies } O FastAPI depende do Pydantic e do Starlette. - -### Dependências `standard` +### Dependências `standard` { #standard-dependencies } Quando você instala o FastAPI com `pip install "fastapi[standard]"`, ele vêm com o grupo `standard` (padrão) de dependências opcionais: @@ -467,18 +466,23 @@ Utilizado pelo Starlette: * httpx - Obrigatório caso você queira utilizar o `TestClient`. * jinja2 - Obrigatório se você quer utilizar a configuração padrão de templates. -* python-multipart - Obrigatório se você deseja suporte a "parsing" de formulário, com `request.form()`. +* python-multipart - Obrigatório se você deseja suporte a "parsing" de formulário, com `request.form()`. -Utilizado pelo FastAPI / Starlette: +Utilizado pelo FastAPI: * uvicorn - para o servidor que carrega e serve a sua aplicação. Isto inclui `uvicorn[standard]`, que inclui algumas dependências (e.g. `uvloop`) necessárias para servir em alta performance. -* `fastapi-cli` - que disponibiliza o comando `fastapi`. +* `fastapi-cli[standard]` - que disponibiliza o comando `fastapi`. + * Isso inclui `fastapi-cloud-cli`, que permite implantar sua aplicação FastAPI na FastAPI Cloud. -### Sem as dependências `standard` +### Sem as dependências `standard` { #without-standard-dependencies } Se você não deseja incluir as dependências opcionais `standard`, você pode instalar utilizando `pip install fastapi` ao invés de `pip install "fastapi[standard]"`. -### Dpendências opcionais adicionais +### Sem o `fastapi-cloud-cli` { #without-fastapi-cloud-cli } + +Se você quiser instalar o FastAPI com as dependências padrão, mas sem o `fastapi-cloud-cli`, você pode instalar com `pip install "fastapi[standard-no-fastapi-cloud-cli]"`. + +### Dependências opcionais adicionais { #additional-optional-dependencies } Existem algumas dependências adicionais que você pode querer instalar. @@ -492,6 +496,6 @@ Dependências opcionais adicionais do FastAPI: * orjson - Obrigatório se você deseja utilizar o `ORJSONResponse`. * ujson - Obrigatório se você deseja utilizar o `UJSONResponse`. -## Licença +## Licença { #license } Esse projeto é licenciado sob os termos da licença MIT. diff --git a/docs/pt/docs/learn/index.md b/docs/pt/docs/learn/index.md index b9a7f5972..1f04929f7 100644 --- a/docs/pt/docs/learn/index.md +++ b/docs/pt/docs/learn/index.md @@ -1,5 +1,5 @@ -# Aprender +# Aprender { #learn } -Nesta parte da documentação encontramos as seções introdutórias e os tutoriais para aprendermos como usar o **FastAPI**. +Aqui estão as seções introdutórias e os tutoriais para aprender o **FastAPI**. -Nós poderíamos considerar isto um **livro**, **curso**, a maneira **oficial** e recomendada de aprender o FastAPI. 😎 +Pode considerar isto um **livro**, um **curso**, a forma **oficial** e recomendada de aprender o FastAPI. 😎 diff --git a/docs/pt/docs/project-generation.md b/docs/pt/docs/project-generation.md index e337ad762..c2015dd2c 100644 --- a/docs/pt/docs/project-generation.md +++ b/docs/pt/docs/project-generation.md @@ -1,4 +1,4 @@ -# Full Stack FastAPI Template +# Full Stack FastAPI Template { #full-stack-fastapi-template } _Templates_, embora tipicamente venham com alguma configuração específica, são desenhados para serem flexíveis e customizáveis. Isso permite que você os modifique e adapte para as especificações do seu projeto, fazendo-os um excelente ponto de partida. 🏁 @@ -6,9 +6,9 @@ Você pode usar esse _template_ para começar, já que ele inclui várias config Repositório GitHub: Full Stack FastAPI Template -## Full Stack FastAPI Template - Pilha de Tecnologias e Recursos +## Full Stack FastAPI Template - Pilha de Tecnologias e Recursos { #full-stack-fastapi-template-technology-stack-and-features } -- ⚡ [**FastAPI**](https://fastapi.tiangolo.com) para a API do backend em Python. +- ⚡ [**FastAPI**](https://fastapi.tiangolo.com/pt) para a API do backend em Python. - 🧰 [SQLModel](https://sqlmodel.tiangolo.com) para as interações do Python com bancos de dados SQL (ORM). - 🔍 [Pydantic](https://docs.pydantic.dev), usado pelo FastAPI, para validação de dados e gerenciamento de configurações. - 💾 [PostgreSQL](https://www.postgresql.org) como banco de dados SQL. diff --git a/docs/pt/docs/python-types.md b/docs/pt/docs/python-types.md index 90a361f40..3e2d1ccb3 100644 --- a/docs/pt/docs/python-types.md +++ b/docs/pt/docs/python-types.md @@ -1,4 +1,4 @@ -# Introdução aos tipos Python +# Introdução aos tipos Python { #python-types-intro } O Python possui suporte para "dicas de tipo" ou "type hints" (também chamado de "anotações de tipo" ou "type annotations") @@ -18,7 +18,7 @@ Se você é um especialista em Python e já sabe tudo sobre type hints, pule par /// -## Motivação +## Motivação { #motivation } Vamos começar com um exemplo simples: @@ -38,7 +38,7 @@ A função faz o seguinte: {* ../../docs_src/python_types/tutorial001.py hl[2] *} -### Edite-o +### Edite-o { #edit-it } É um programa muito simples. @@ -58,7 +58,7 @@ Mas, infelizmente, você não obtém nada útil: -### Adicionar tipos +### Adicionar tipos { #add-types } Vamos modificar uma única linha da versão anterior. @@ -102,7 +102,7 @@ Com isso, você pode rolar, vendo as opções, até encontrar o que "soa familia -## Mais motivação +## Mais motivação { #more-motivation } Verifique esta função, ela já possui type hints: @@ -116,13 +116,13 @@ Agora você sabe que precisa corrigí-lo, converta `age` em uma string com `str( {* ../../docs_src/python_types/tutorial004.py hl[2] *} -## Declarando Tipos +## Declarando Tipos { #declaring-types } Você acabou de ver o local principal para declarar type hints. Como parâmetros de função. Este também é o principal local em que você os usaria com o **FastAPI**. -### Tipos simples +### Tipos simples { #simple-types } Você pode declarar todos os tipos padrão de Python, não apenas `str`. @@ -135,7 +135,7 @@ Você pode usar, por exemplo: {* ../../docs_src/python_types/tutorial005.py hl[1] *} -### Tipos genéricos com parâmetros de tipo +### Tipos genéricos com parâmetros de tipo { #generic-types-with-type-parameters } Existem algumas estruturas de dados que podem conter outros valores, como `dict`, `list`, `set` e `tuple`. E os valores internos também podem ter seu próprio tipo. @@ -143,7 +143,7 @@ Estes tipos que possuem tipos internos são chamados de tipos "**genéricos**". Para declarar esses tipos e os tipos internos, você pode usar o módulo Python padrão `typing`. Ele existe especificamente para suportar esses type hints. -#### Versões mais recentes do Python +#### Versões mais recentes do Python { #newer-versions-of-python } A sintaxe utilizando `typing` é **compatível** com todas as versões, desde o Python 3.6 até as últimas, incluindo o Python 3.9, 3.10, etc. @@ -157,7 +157,7 @@ Por exemplo, "**Python 3.6+**" significa que é compatível com o Python 3.6 ou Se você pode utilizar a **versão mais recente do Python**, utilize os exemplos para as últimas versões. Eles terão as **melhores e mais simples sintaxes**, como por exemplo, "**Python 3.10+**". -#### List +#### List { #list } Por exemplo, vamos definir uma variável para ser uma `list` de `str`. @@ -221,7 +221,7 @@ Observe que a variável `item` é um dos elementos da lista `items`. E, ainda assim, o editor sabe que é um `str` e fornece suporte para isso. -#### Tuple e Set +#### Tuple e Set { #tuple-and-set } Você faria o mesmo para declarar `tuple`s e `set`s: @@ -246,7 +246,7 @@ Isso significa que: * A variável `items_t` é uma `tuple` com 3 itens, um `int`, outro `int` e uma `str`. * A variável `items_s` é um `set`, e cada um de seus itens é do tipo `bytes`. -#### Dict +#### Dict { #dict } Para definir um `dict`, você passa 2 parâmetros de tipo, separados por vírgulas. @@ -272,17 +272,17 @@ O segundo parâmetro de tipo é para os valores do `dict`: Isso significa que: -* A variável `prices` é um dict`: +* A variável `prices` é um `dict`: * As chaves deste `dict` são do tipo `str` (digamos, o nome de cada item). * Os valores deste `dict` são do tipo `float` (digamos, o preço de cada item). -#### Union +#### Union { #union } Você pode declarar que uma variável pode ser de qualquer um dentre **diversos tipos**. Por exemplo, um `int` ou um `str`. No Python 3.6 e superior (incluindo o Python 3.10), você pode utilizar o tipo `Union` de `typing`, e colocar dentro dos colchetes os possíveis tipos aceitáveis. -No Python 3.10 também existe uma **nova sintaxe** onde você pode colocar os possívels tipos separados por uma barra vertical (`|`). +No Python 3.10 também existe uma **nova sintaxe** onde você pode colocar os possíveis tipos separados por uma barra vertical (`|`). //// tab | Python 3.10+ @@ -302,8 +302,7 @@ No Python 3.10 também existe uma **nova sintaxe** onde você pode colocar os po Em ambos os casos, isso significa que `item` poderia ser um `int` ou um `str`. - -#### Possívelmente `None` +#### Possivelmente `None` { #possibly-none } Você pode declarar que um valor pode ter um tipo, como `str`, mas que ele também pode ser `None`. @@ -335,7 +334,7 @@ Isso também significa que no Python 3.10, você pode utilizar `Something | None //// -//// tab | Python 3.8+ alternative +//// tab | Python 3.8+ alternativa ```Python hl_lines="1 4" {!> ../../docs_src/python_types/tutorial009b.py!} @@ -343,7 +342,7 @@ Isso também significa que no Python 3.10, você pode utilizar `Something | None //// -#### Utilizando `Union` ou `Optional` +#### Utilizando `Union` ou `Optional` { #using-union-or-optional } Se você está utilizando uma versão do Python abaixo da 3.10, aqui vai uma dica do meu ponto de vista bem **subjetivo**: @@ -360,13 +359,13 @@ Por exemplo, vamos pegar esta função: {* ../../docs_src/python_types/tutorial009c.py hl[1,4] *} -O paâmetro `name` é definido como `Optional[str]`, mas ele **não é opcional**, você não pode chamar a função sem o parâmetro: +O parâmetro `name` é definido como `Optional[str]`, mas ele **não é opcional**, você não pode chamar a função sem o parâmetro: ```Python say_hi() # Oh, no, this throws an error! 😱 ``` -O parâmetro `name` **ainda é obrigatório** (não *opicional*) porque ele não possui um valor padrão. Mesmo assim, `name` aceita `None` como valor: +O parâmetro `name` **ainda é obrigatório** (não *opcional*) porque ele não possui um valor padrão. Mesmo assim, `name` aceita `None` como valor: ```Python say_hi(name=None) # This works, None is valid 🎉 @@ -378,7 +377,7 @@ A boa notícia é, quando você estiver no Python 3.10 você não precisará se E então você não precisará mais se preocupar com nomes como `Optional` e `Union`. 😎 -#### Tipos genéricos +#### Tipos genéricos { #generic-types } Esses tipos que usam parâmetros de tipo entre colchetes são chamados **tipos genéricos** ou **genéricos**. Por exemplo: @@ -395,7 +394,7 @@ E o mesmo como no Python 3.8, do módulo `typing`: * `Union` * `Optional` (o mesmo que com o 3.8) -* ...entro outros. +* ...entre outros. No Python 3.10, como uma alternativa para a utilização dos genéricos `Union` e `Optional`, você pode usar a barra vertical (`|`) para declarar uniões de tipos. Isso é muito melhor e mais simples. @@ -414,7 +413,7 @@ E o mesmo como no Python 3.8, do módulo `typing`: * `Union` * `Optional` -* ...entro outros. +* ...entre outros. //// @@ -426,11 +425,11 @@ E o mesmo como no Python 3.8, do módulo `typing`: * `Dict` * `Union` * `Optional` -* ...entro outros. +* ...entre outros. //// -### Classes como tipos +### Classes como tipos { #classes-as-types } Você também pode declarar uma classe como o tipo de uma variável. @@ -450,7 +449,7 @@ Perceba que isso significa que "`one_person` é uma **instância** da classe `Pe Isso não significa que "`one_person` é a **classe** chamada `Person`". -## Modelos Pydantic +## Modelos Pydantic { #pydantic-models } O Pydantic é uma biblioteca Python para executar a validação de dados. @@ -504,8 +503,7 @@ O Pydantic tem um comportamento especial quando você usa `Optional` ou `Union[S /// - -## Type Hints com Metadados de Anotações +## Type Hints com Metadados de Anotações { #type-hints-with-metadata-annotations } O Python possui uma funcionalidade que nos permite incluir **metadados adicionais** nos type hints utilizando `Annotated`. @@ -549,8 +547,7 @@ E também que o seu código será muito compatível com diversas outras ferramen /// - -## Type hints no **FastAPI** +## Type hints no **FastAPI** { #type-hints-in-fastapi } O **FastAPI** aproveita esses type hints para fazer várias coisas. @@ -574,6 +571,6 @@ O importante é que, usando tipos padrão de Python, em um único local (em vez /// info | Informação -Se você já passou por todo o tutorial e voltou para ver mais sobre os tipos, um bom recurso é a "cheat sheet" do `mypy` . +Se você já passou por todo o tutorial e voltou para ver mais sobre os tipos, um bom recurso é a "cheat sheet" do `mypy` . /// diff --git a/docs/pt/docs/resources/index.md b/docs/pt/docs/resources/index.md index 6eff8f9e7..0ac95342b 100644 --- a/docs/pt/docs/resources/index.md +++ b/docs/pt/docs/resources/index.md @@ -1,3 +1,3 @@ -# Recursos +# Recursos { #resources } Material complementar, links externos, artigos e muito mais. ✈️ diff --git a/docs/pt/docs/tutorial/background-tasks.md b/docs/pt/docs/tutorial/background-tasks.md index b8ab58cda..af0c8b2ac 100644 --- a/docs/pt/docs/tutorial/background-tasks.md +++ b/docs/pt/docs/tutorial/background-tasks.md @@ -1,84 +1,85 @@ -# Tarefas em segundo plano +# Tarefas em segundo plano { #background-tasks } -Você pode definir tarefas em segundo plano a serem executadas _ após _ retornar uma resposta. +Você pode definir tarefas em segundo plano para serem executadas *após* retornar uma resposta. -Isso é útil para operações que precisam acontecer após uma solicitação, mas que o cliente realmente não precisa esperar a operação ser concluída para receber a resposta. +Isso é útil para operações que precisam acontecer após uma request, mas que o cliente não precisa realmente esperar a operação terminar antes de receber a resposta. Isso inclui, por exemplo: -- Envio de notificações por email após a realização de uma ação: - - Como conectar-se a um servidor de e-mail e enviar um e-mail tende a ser "lento" (vários segundos), você pode retornar a resposta imediatamente e enviar a notificação por e-mail em segundo plano. -- Processando dados: - - Por exemplo, digamos que você receba um arquivo que deve passar por um processo lento, você pode retornar uma resposta de "Aceito" (HTTP 202) e processá-lo em segundo plano. +* Notificações por e-mail enviadas após realizar uma ação: + * Como conectar-se a um servidor de e-mail e enviar um e-mail tende a ser “lento” (vários segundos), você pode retornar a resposta imediatamente e enviar a notificação por e-mail em segundo plano. +* Processamento de dados: + * Por exemplo, digamos que você receba um arquivo que precisa passar por um processo lento; você pode retornar uma resposta “Accepted” (HTTP 202) e processar o arquivo em segundo plano. -## Usando `BackgroundTasks` +## Usando `BackgroundTasks` { #using-backgroundtasks } -Primeiro, importe `BackgroundTasks` e defina um parâmetro em sua _função de operação de caminho_ com uma declaração de tipo de `BackgroundTasks`: +Primeiro, importe `BackgroundTasks` e defina um parâmetro na sua *função de operação de rota* com uma declaração de tipo `BackgroundTasks`: {* ../../docs_src/background_tasks/tutorial001.py hl[1,13] *} O **FastAPI** criará o objeto do tipo `BackgroundTasks` para você e o passará como esse parâmetro. -## Criar uma função de tarefa +## Crie uma função de tarefa { #create-a-task-function } -Crie uma função a ser executada como tarefa em segundo plano. +Crie uma função para ser executada como a tarefa em segundo plano. É apenas uma função padrão que pode receber parâmetros. -Pode ser uma função `async def` ou `def` normal, o **FastAPI** saberá como lidar com isso corretamente. +Pode ser uma função `async def` ou um `def` normal, o **FastAPI** saberá como lidar com isso corretamente. -Nesse caso, a função de tarefa gravará em um arquivo (simulando o envio de um e-mail). +Neste caso, a função da tarefa escreverá em um arquivo (simulando o envio de um e-mail). -E como a operação de gravação não usa `async` e `await`, definimos a função com `def` normal: +E como a operação de escrita não usa `async` e `await`, definimos a função com um `def` normal: {* ../../docs_src/background_tasks/tutorial001.py hl[6:9] *} -## Adicionar a tarefa em segundo plano +## Adicione a tarefa em segundo plano { #add-the-background-task } -Dentro de sua _função de operação de caminho_, passe sua função de tarefa para o objeto _tarefas em segundo plano_ com o método `.add_task()`: +Dentro da sua *função de operação de rota*, passe sua função de tarefa para o objeto de *tarefas em segundo plano* com o método `.add_task()`: {* ../../docs_src/background_tasks/tutorial001.py hl[14] *} -`.add_task()` recebe como argumentos: +O `.add_task()` recebe como argumentos: -- Uma função de tarefa a ser executada em segundo plano (`write_notification`). -- Qualquer sequência de argumentos que deve ser passada para a função de tarefa na ordem (`email`). -- Quaisquer argumentos nomeados que devem ser passados ​​para a função de tarefa (`mensagem = "alguma notificação"`). +* Uma função de tarefa a ser executada em segundo plano (`write_notification`). +* Qualquer sequência de argumentos que deve ser passada para a função de tarefa na ordem (`email`). +* Quaisquer argumentos nomeados que devem ser passados para a função de tarefa (`message="some notification"`). -## Injeção de dependência +## Injeção de dependências { #dependency-injection } -Usar `BackgroundTasks` também funciona com o sistema de injeção de dependência, você pode declarar um parâmetro do tipo `BackgroundTasks` em vários níveis: em uma _função de operação de caminho_, em uma dependência (confiável), em uma subdependência, etc. +Usar `BackgroundTasks` também funciona com o sistema de injeção de dependências; você pode declarar um parâmetro do tipo `BackgroundTasks` em vários níveis: em uma *função de operação de rota*, em uma dependência (dependable), em uma subdependência, etc. -O **FastAPI** sabe o que fazer em cada caso e como reutilizar o mesmo objeto, de forma que todas as tarefas em segundo plano sejam mescladas e executadas em segundo plano posteriormente: +O **FastAPI** sabe o que fazer em cada caso e como reutilizar o mesmo objeto, de forma que todas as tarefas em segundo plano sejam combinadas e executadas em segundo plano depois: -{* ../../docs_src/background_tasks/tutorial002.py hl[13,15,22,25] *} -Neste exemplo, as mensagens serão gravadas no arquivo `log.txt` _após_ o envio da resposta. +{* ../../docs_src/background_tasks/tutorial002_an_py310.py hl[13,15,22,25] *} -Se houver uma consulta na solicitação, ela será gravada no log em uma tarefa em segundo plano. +Neste exemplo, as mensagens serão escritas no arquivo `log.txt` *após* o envio da resposta. -E então outra tarefa em segundo plano gerada na _função de operação de caminho_ escreverá uma mensagem usando o parâmetro de caminho `email`. +Se houver uma query na request, ela será registrada em uma tarefa em segundo plano. -## Detalhes técnicos +E então outra tarefa em segundo plano gerada na *função de operação de rota* escreverá uma mensagem usando o parâmetro de path `email`. + +## Detalhes técnicos { #technical-details } A classe `BackgroundTasks` vem diretamente de `starlette.background`. -Ela é importada/incluída diretamente no FastAPI para que você possa importá-la do `fastapi` e evitar a importação acidental da alternativa `BackgroundTask` (sem o `s` no final) de `starlette.background`. +Ela é importada/incluída diretamente no FastAPI para que você possa importá-la de `fastapi` e evitar importar acidentalmente a alternativa `BackgroundTask` (sem o `s` no final) de `starlette.background`. -Usando apenas `BackgroundTasks` (e não `BackgroundTask`), é então possível usá-la como um parâmetro de _função de operação de caminho_ e deixar o **FastAPI** cuidar do resto para você, assim como ao usar o objeto `Request` diretamente. +Usando apenas `BackgroundTasks` (e não `BackgroundTask`), é possível usá-la como um parâmetro de *função de operação de rota* e deixar o **FastAPI** cuidar do resto para você, assim como ao usar o objeto `Request` diretamente. -Ainda é possível usar `BackgroundTask` sozinho no FastAPI, mas você deve criar o objeto em seu código e retornar uma Starlette `Response` incluindo-o. +Ainda é possível usar `BackgroundTask` sozinho no FastAPI, mas você precisa criar o objeto no seu código e retornar uma `Response` da Starlette incluindo-o. -Você pode ver mais detalhes na documentação oficiais da Starlette para tarefas em segundo plano . +Você pode ver mais detalhes na documentação oficial da Starlette para tarefas em segundo plano. -## Ressalva +## Ressalva { #caveat } -Se você precisa realizar cálculos pesados ​​em segundo plano e não necessariamente precisa que seja executado pelo mesmo processo (por exemplo, você não precisa compartilhar memória, variáveis, etc), você pode se beneficiar do uso de outras ferramentas maiores, como Celery . +Se você precisar realizar computação pesada em segundo plano e não necessariamente precisar que seja executada pelo mesmo processo (por exemplo, você não precisa compartilhar memória, variáveis, etc.), pode se beneficiar do uso de outras ferramentas maiores, como o Celery. -Eles tendem a exigir configurações mais complexas, um gerenciador de fila de mensagens/tarefas, como RabbitMQ ou Redis, mas permitem que você execute tarefas em segundo plano em vários processos e, especialmente, em vários servidores. +Elas tendem a exigir configurações mais complexas, um gerenciador de fila de mensagens/tarefas, como RabbitMQ ou Redis, mas permitem executar tarefas em segundo plano em vários processos e, especialmente, em vários servidores. -Mas se você precisa acessar variáveis ​​e objetos do mesmo aplicativo **FastAPI**, ou precisa realizar pequenas tarefas em segundo plano (como enviar uma notificação por e-mail), você pode simplesmente usar `BackgroundTasks`. +Mas se você precisa acessar variáveis e objetos da mesma aplicação **FastAPI**, ou precisa realizar pequenas tarefas em segundo plano (como enviar uma notificação por e-mail), você pode simplesmente usar `BackgroundTasks`. -## Recapitulando +## Recapitulando { #recap } -Importe e use `BackgroundTasks` com parâmetros em _funções de operação de caminho_ e dependências para adicionar tarefas em segundo plano. +Importe e use `BackgroundTasks` com parâmetros em *funções de operação de rota* e dependências para adicionar tarefas em segundo plano. diff --git a/docs/pt/docs/tutorial/bigger-applications.md b/docs/pt/docs/tutorial/bigger-applications.md index b621f3c72..c479eb5d9 100644 --- a/docs/pt/docs/tutorial/bigger-applications.md +++ b/docs/pt/docs/tutorial/bigger-applications.md @@ -1,4 +1,4 @@ -# Aplicações Maiores - Múltiplos Arquivos +# Aplicações Maiores - Múltiplos Arquivos { #bigger-applications-multiple-files } Se você está construindo uma aplicação ou uma API web, é raro que você possa colocar tudo em um único arquivo. @@ -10,7 +10,7 @@ Se você vem do Flask, isso seria o equivalente aos Blueprints do Flask. /// -## Um exemplo de estrutura de arquivos +## Um exemplo de estrutura de arquivos { #an-example-file-structure } Digamos que você tenha uma estrutura de arquivos como esta: @@ -71,7 +71,7 @@ A mesma estrutura de arquivos com comentários: │   └── admin.py # "admin" submódulo, e.g. import app.internal.admin ``` -## `APIRouter` +## `APIRouter` { #apirouter } Vamos supor que o arquivo dedicado a lidar apenas com usuários seja o submódulo em `/app/routers/users.py`. @@ -81,7 +81,7 @@ Mas ele ainda faz parte da mesma aplicação/web API **FastAPI** (faz parte do m Você pode criar as *operações de rotas* para esse módulo usando o `APIRouter`. -### Importar `APIRouter` +### Importe `APIRouter` { #import-apirouter } você o importa e cria uma "instância" da mesma maneira que faria com a classe `FastAPI`: @@ -89,7 +89,7 @@ você o importa e cria uma "instância" da mesma maneira que faria com a classe {!../../docs_src/bigger_applications/app/routers/users.py!} ``` -### *Operações de Rota* com `APIRouter` +### *Operações de Rota* com `APIRouter` { #path-operations-with-apirouter } E então você o utiliza para declarar suas *operações de rota*. @@ -113,7 +113,7 @@ Neste exemplo, a variável é chamada de `router`, mas você pode nomeá-la como Vamos incluir este `APIRouter` na aplicação principal `FastAPI`, mas primeiro, vamos verificar as dependências e outro `APIRouter`. -## Dependências +## Dependências { #dependencies } Vemos que precisaremos de algumas dependências usadas em vários lugares da aplicação. @@ -159,7 +159,7 @@ Mas em casos reais, você obterá melhores resultados usando os [Utilitários de /// -## Outro módulo com `APIRouter` +## Outro módulo com `APIRouter` { #another-module-with-apirouter } Digamos que você também tenha os endpoints dedicados a manipular "itens" do seu aplicativo no módulo em `app/routers/items.py`. @@ -177,7 +177,7 @@ Sabemos que todas as *operações de rota* neste módulo têm o mesmo: * Path `prefix`: `/items`. * `tags`: (apenas uma tag: `items`). * Extra `responses`. -* `dependências`: todas elas precisam da dependência `X-Token` que criamos. +* `dependencies`: todas elas precisam da dependência `X-Token` que criamos. Então, em vez de adicionar tudo isso a cada *operação de rota*, podemos adicioná-lo ao `APIRouter`. @@ -224,17 +224,17 @@ O resultado final é que os caminhos dos itens agora são: /// tip | Dica -Ter `dependências` no `APIRouter` pode ser usado, por exemplo, para exigir autenticação para um grupo inteiro de *operações de rota*. Mesmo que as dependências não sejam adicionadas individualmente a cada uma delas. +Ter `dependencies` no `APIRouter` pode ser usado, por exemplo, para exigir autenticação para um grupo inteiro de *operações de rota*. Mesmo que as dependências não sejam adicionadas individualmente a cada uma delas. /// -/// check +/// check | Verifique Os parâmetros `prefix`, `tags`, `responses` e `dependencies` são (como em muitos outros casos) apenas um recurso do **FastAPI** para ajudar a evitar duplicação de código. /// -### Importar as dependências +### Importe as dependências { #import-the-dependencies } Este código reside no módulo `app.routers.items`, o arquivo `app/routers/items.py`. @@ -246,7 +246,7 @@ Então usamos uma importação relativa com `..` para as dependências: {!../../docs_src/bigger_applications/app/routers/items.py!} ``` -#### Como funcionam as importações relativas +#### Como funcionam as importações relativas { #how-relative-imports-work } /// tip | Dica @@ -309,11 +309,11 @@ Isso se referiria a algum pacote acima de `app/`, com seu próprio arquivo `__in Mas agora você sabe como funciona, então você pode usar importações relativas em seus próprios aplicativos, não importa o quão complexos eles sejam. 🤓 -### Adicione algumas `tags`, `respostas` e `dependências` personalizadas +### Adicione algumas `tags`, `responses` e `dependencies` personalizadas { #add-some-custom-tags-responses-and-dependencies } Não estamos adicionando o prefixo `/items` nem `tags=["items"]` a cada *operação de rota* porque os adicionamos ao `APIRouter`. -Mas ainda podemos adicionar _mais_ `tags` que serão aplicadas a uma *operação de rota* específica, e também algumas `respostas` extras específicas para essa *operação de rota*: +Mas ainda podemos adicionar _mais_ `tags` que serão aplicadas a uma *operação de rota* específica, e também algumas `responses` extras específicas para essa *operação de rota*: ```Python hl_lines="30-31" title="app/routers/items.py" {!../../docs_src/bigger_applications/app/routers/items.py!} @@ -327,7 +327,7 @@ E também terá ambas as respostas na documentação, uma para `404` e uma para /// -## O principal `FastAPI` +## O principal `FastAPI` { #the-main-fastapi } Agora, vamos ver o módulo em `app/main.py`. @@ -337,7 +337,7 @@ Este será o arquivo principal em seu aplicativo que une tudo. E como a maior parte de sua lógica agora viverá em seu próprio módulo específico, o arquivo principal será bem simples. -### Importar `FastAPI` +### Importe o `FastAPI` { #import-fastapi } Você importa e cria uma classe `FastAPI` normalmente. @@ -347,7 +347,7 @@ E podemos até declarar [dependências globais](dependencies/global-dependencies {!../../docs_src/bigger_applications/app/main.py!} ``` -### Importe o `APIRouter` +### Importe o `APIRouter` { #import-the-apirouter } Agora importamos os outros submódulos que possuem `APIRouter`s: @@ -357,7 +357,7 @@ Agora importamos os outros submódulos que possuem `APIRouter`s: Como os arquivos `app/routers/users.py` e `app/routers/items.py` são submódulos que fazem parte do mesmo pacote Python `app`, podemos usar um único ponto `.` para importá-los usando "importações relativas". -### Como funciona a importação +### Como funciona a importação { #how-the-importing-works } A seção: @@ -399,7 +399,7 @@ Para saber mais sobre pacotes e módulos Python, leia ```console -$ uvicorn app.main:app --reload +$ fastapi dev app/main.py INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ``` @@ -537,7 +537,7 @@ Você verá a documentação automática da API, incluindo os caminhos de todos -## Incluir o mesmo roteador várias vezes com `prefixos` diferentes +## Inclua o mesmo roteador várias vezes com `prefix` diferentes { #include-the-same-router-multiple-times-with-different-prefix } Você também pode usar `.include_router()` várias vezes com o *mesmo* roteador usando prefixos diferentes. @@ -545,7 +545,7 @@ Isso pode ser útil, por exemplo, para expor a mesma API sob prefixos diferentes Esse é um uso avançado que você pode não precisar, mas está lá caso precise. -## Incluir um `APIRouter` em outro +## Inclua um `APIRouter` em outro { #include-an-apirouter-in-another } Da mesma forma que você pode incluir um `APIRouter` em um aplicativo `FastAPI`, você pode incluir um `APIRouter` em outro `APIRouter` usando: diff --git a/docs/pt/docs/tutorial/body-fields.md b/docs/pt/docs/tutorial/body-fields.md index e7dfb07f2..25e11189e 100644 --- a/docs/pt/docs/tutorial/body-fields.md +++ b/docs/pt/docs/tutorial/body-fields.md @@ -1,28 +1,28 @@ -# Corpo - Campos +# Corpo - Campos { #body-fields } -Da mesma forma que você pode declarar validações adicionais e metadados nos parâmetros de *funções de operações de rota* com `Query`, `Path` e `Body`, você pode declarar validações e metadados dentro de modelos do Pydantic usando `Field` do Pydantic. +Da mesma forma que você pode declarar validações adicionais e metadados nos parâmetros de uma *função de operação de rota* com `Query`, `Path` e `Body`, você pode declarar validações e metadados dentro de modelos do Pydantic usando `Field` do Pydantic. -## Importe `Field` +## Importe `Field` { #import-field } Primeiro, você tem que importá-lo: -{* ../../docs_src/body_fields/tutorial001.py hl[4] *} +{* ../../docs_src/body_fields/tutorial001_an_py310.py hl[4] *} -/// warning | Aviso +/// warning | Atenção Note que `Field` é importado diretamente do `pydantic`, não do `fastapi` como todo o resto (`Query`, `Path`, `Body`, etc). /// -## Declare atributos do modelo +## Declare atributos do modelo { #declare-model-attributes } Você pode então utilizar `Field` com atributos do modelo: -{* ../../docs_src/body_fields/tutorial001.py hl[11:14] *} +{* ../../docs_src/body_fields/tutorial001_an_py310.py hl[11:14] *} `Field` funciona da mesma forma que `Query`, `Path` e `Body`, ele possui todos os mesmos parâmetros, etc. -/// note | Detalhes técnicos +/// note | Detalhes Técnicos Na realidade, `Query`, `Path` e outros que você verá em seguida, criam objetos de subclasses de uma classe `Param` comum, que é ela mesma uma subclasse da classe `FieldInfo` do Pydantic. @@ -40,13 +40,20 @@ Note como cada atributo do modelo com um tipo, valor padrão e `Field` possuem a /// -## Adicione informações extras +## Adicione informações extras { #add-extra-information } Você pode declarar informação extra em `Field`, `Query`, `Body`, etc. E isso será incluído no JSON Schema gerado. Você irá aprender mais sobre adicionar informações extras posteriormente nessa documentação, quando estiver aprendendo a declarar exemplos. -## Recapitulando +/// warning | Atenção + +Chaves extras passadas para `Field` também estarão presentes no schema OpenAPI resultante da sua aplicação. +Como essas chaves podem não fazer necessariamente parte da especificação OpenAPI, algumas ferramentas de OpenAPI, por exemplo [o validador do OpenAPI](https://validator.swagger.io/), podem não funcionar com o schema gerado. + +/// + +## Recapitulando { #recap } Você pode usar `Field` do Pydantic para declarar validações extras e metadados para atributos do modelo. diff --git a/docs/pt/docs/tutorial/body-multiple-params.md b/docs/pt/docs/tutorial/body-multiple-params.md index eda9b4dff..3cba04912 100644 --- a/docs/pt/docs/tutorial/body-multiple-params.md +++ b/docs/pt/docs/tutorial/body-multiple-params.md @@ -1,14 +1,14 @@ -# Corpo - Múltiplos parâmetros +# Corpo - Múltiplos parâmetros { #body-multiple-parameters } Agora que nós vimos como usar `Path` e `Query`, veremos usos mais avançados de declarações no corpo da requisição. -## Misture `Path`, `Query` e parâmetros de corpo +## Misture `Path`, `Query` e parâmetros de corpo { #mix-path-query-and-body-parameters } Primeiro, é claro, você pode misturar `Path`, `Query` e declarações de parâmetro no corpo da requisição livremente e o **FastAPI** saberá o que fazer. E você também pode declarar parâmetros de corpo como opcionais, definindo o valor padrão com `None`: -{* ../../docs_src/body_multiple_params/tutorial001_py310.py hl[17:19] *} +{* ../../docs_src/body_multiple_params/tutorial001_an_py310.py hl[18:20] *} /// note | Nota @@ -16,7 +16,7 @@ Repare que, neste caso, o `item` que seria capturado a partir do corpo é opcion /// -## Múltiplos parâmetros de corpo +## Múltiplos parâmetros de corpo { #multiple-body-parameters } No exemplo anterior, as *operações de rota* esperariam um JSON no corpo contendo os atributos de um `Item`, exemplo: @@ -62,7 +62,7 @@ O **FastAPI** fará a conversão automática a partir da requisição, assim ess Ele executará a validação dos dados compostos e irá documentá-los de maneira compatível com o esquema OpenAPI e documentação automática. -## Valores singulares no corpo +## Valores singulares no corpo { #singular-values-in-body } Assim como existem uma `Query` e uma `Path` para definir dados adicionais para parâmetros de consulta e de rota, o **FastAPI** provê o equivalente para `Body`. @@ -72,7 +72,7 @@ Se você declará-lo como é, porque é um valor singular, o **FastAPI** assumir Mas você pode instruir o **FastAPI** para tratá-lo como outra chave do corpo usando `Body`: -{* ../../docs_src/body_multiple_params/tutorial003.py hl[22] *} +{* ../../docs_src/body_multiple_params/tutorial003_an_py310.py hl[23] *} Neste caso, o **FastAPI** esperará um corpo como: @@ -94,7 +94,7 @@ Neste caso, o **FastAPI** esperará um corpo como: Mais uma vez, ele converterá os tipos de dados, validar, documentar, etc. -## Múltiplos parâmetros de corpo e consulta +## Múltiplos parâmetros de corpo e consulta { #multiple-body-params-and-query } Obviamente, você também pode declarar parâmetros de consulta assim que você precisar, de modo adicional a quaisquer parâmetros de corpo. @@ -112,7 +112,7 @@ q: str | None = None Por exemplo: -{* ../../docs_src/body_multiple_params/tutorial004_py310.py hl[26] *} +{* ../../docs_src/body_multiple_params/tutorial004_an_py310.py hl[28] *} /// info | Informação @@ -120,7 +120,7 @@ Por exemplo: /// -## Declare um único parâmetro de corpo indicando sua chave +## Declare um único parâmetro de corpo indicando sua chave { #embed-a-single-body-parameter } Suponha que você tem um único parâmetro de corpo `item`, a partir de um modelo Pydantic `Item`. @@ -134,7 +134,7 @@ item: Item = Body(embed=True) como em: -{* ../../docs_src/body_multiple_params/tutorial005_py310.py hl[15] *} +{* ../../docs_src/body_multiple_params/tutorial005_an_py310.py hl[17] *} Neste caso o **FastAPI** esperará um corpo como: @@ -160,7 +160,7 @@ ao invés de: } ``` -## Recapitulando +## Recapitulando { #recap } Você pode adicionar múltiplos parâmetros de corpo para sua *função de operação de rota*, mesmo que a requisição possa ter somente um único corpo. diff --git a/docs/pt/docs/tutorial/body-nested-models.md b/docs/pt/docs/tutorial/body-nested-models.md index 2954ae3db..4f3ca661f 100644 --- a/docs/pt/docs/tutorial/body-nested-models.md +++ b/docs/pt/docs/tutorial/body-nested-models.md @@ -1,32 +1,42 @@ -# Corpo - Modelos aninhados +# Corpo - Modelos aninhados { #body-nested-models } -Com o **FastAPI**, você pode definir, validar, documentar e usar modelos profundamente aninhados de forma arbitrária (graças ao Pydantic). +Com o **FastAPI**, você pode definir, validar, documentar e usar modelos arbitrariamente e profundamente aninhados (graças ao Pydantic). -## Campos do tipo Lista +## Campos do tipo Lista { #list-fields } Você pode definir um atributo como um subtipo. Por exemplo, uma `list` do Python: -{* ../../docs_src/body_nested_models/tutorial001.py hl[14] *} +{* ../../docs_src/body_nested_models/tutorial001_py310.py hl[12] *} Isso fará com que tags seja uma lista de itens mesmo sem declarar o tipo dos elementos desta lista. -## Campos do tipo Lista com um parâmetro de tipo +## Campos do tipo Lista com um parâmetro de tipo { #list-fields-with-type-parameter } Mas o Python tem uma maneira específica de declarar listas com tipos internos ou "parâmetros de tipo": -### Importe `List` do typing +### Importe `List` do typing { #import-typings-list } -Primeiramente, importe `List` do módulo `typing` que já vem por padrão no Python: +No Python 3.9 e superior você pode usar a `list` padrão para declarar essas anotações de tipo, como veremos abaixo. 💡 + +Mas nas versões do Python anteriores à 3.9 (3.6 e superiores), primeiro é necessário importar `List` do módulo padrão `typing` do Python: {* ../../docs_src/body_nested_models/tutorial002.py hl[1] *} -### Declare a `List` com um parâmetro de tipo +### Declare uma `list` com um parâmetro de tipo { #declare-a-list-with-a-type-parameter } -Para declarar tipos que têm parâmetros de tipo(tipos internos), como `list`, `dict`, `tuple`: +Para declarar tipos que têm parâmetros de tipo (tipos internos), como `list`, `dict`, `tuple`: -* Importe os do modulo `typing` +* Se você estiver em uma versão do Python inferior a 3.9, importe a versão equivalente do módulo `typing` * Passe o(s) tipo(s) interno(s) como "parâmetros de tipo" usando colchetes: `[` e `]` +No Python 3.9, seria: + +```Python +my_list: list[str] +``` + +Em versões do Python anteriores à 3.9, seria: + ```Python from typing import List @@ -39,20 +49,17 @@ Use a mesma sintaxe padrão para atributos de modelo com tipos internos. Portanto, em nosso exemplo, podemos fazer com que `tags` sejam especificamente uma "lista de strings": +{* ../../docs_src/body_nested_models/tutorial002_py310.py hl[12] *} -{* ../../docs_src/body_nested_models/tutorial002.py hl[14] *} - -## Tipo "set" - +## Tipos "set" { #set-types } Mas então, quando nós pensamos mais, percebemos que as tags não devem se repetir, elas provavelmente devem ser strings únicas. E que o Python tem um tipo de dados especial para conjuntos de itens únicos, o `set`. -Então podemos importar `Set` e declarar `tags` como um `set` de `str`s: +Então podemos declarar `tags` como um conjunto de strings: - -{* ../../docs_src/body_nested_models/tutorial003.py hl[1,14] *} +{* ../../docs_src/body_nested_models/tutorial003_py310.py hl[12] *} Com isso, mesmo que você receba uma requisição contendo dados duplicados, ela será convertida em um conjunto de itens exclusivos. @@ -60,7 +67,7 @@ E sempre que você enviar esses dados como resposta, mesmo se a fonte tiver dupl E também teremos anotações/documentação em conformidade. -## Modelos aninhados +## Modelos aninhados { #nested-models } Cada atributo de um modelo Pydantic tem um tipo. @@ -70,17 +77,17 @@ Portanto, você pode declarar "objects" JSON profundamente aninhados com nomes, Tudo isso, aninhado arbitrariamente. -### Defina um sub-modelo +### Defina um sub-modelo { #define-a-submodel } Por exemplo, nós podemos definir um modelo `Image`: -{* ../../docs_src/body_nested_models/tutorial004.py hl[9:11] *} +{* ../../docs_src/body_nested_models/tutorial004_py310.py hl[7:9] *} -### Use o sub-modelo como um tipo +### Use o sub-modelo como um tipo { #use-the-submodel-as-a-type } E então podemos usa-lo como o tipo de um atributo: -{* ../../docs_src/body_nested_models/tutorial004.py hl[20] *} +{* ../../docs_src/body_nested_models/tutorial004_py310.py hl[18] *} Isso significa que o **FastAPI** vai esperar um corpo similar à: @@ -100,28 +107,28 @@ Isso significa que o **FastAPI** vai esperar um corpo similar à: Novamente, apenas fazendo essa declaração, com o **FastAPI**, você ganha: -* Suporte do editor de texto (compleção, etc), inclusive para modelos aninhados +* Suporte do editor (preenchimento automático, etc.), inclusive para modelos aninhados * Conversão de dados * Validação de dados * Documentação automatica -## Tipos especiais e validação +## Tipos especiais e validação { #special-types-and-validation } Além dos tipos singulares normais como `str`, `int`, `float`, etc. Você também pode usar tipos singulares mais complexos que herdam de `str`. -Para ver todas as opções possíveis, cheque a documentação para ostipos exoticos do Pydantic. Você verá alguns exemplos no próximo capitulo. +Para ver todas as opções possíveis, consulte a Visão geral dos tipos do Pydantic. Você verá alguns exemplos no próximo capítulo. Por exemplo, no modelo `Image` nós temos um campo `url`, nós podemos declara-lo como um `HttpUrl` do Pydantic invés de como uma `str`: -{* ../../docs_src/body_nested_models/tutorial005.py hl[4,10] *} +{* ../../docs_src/body_nested_models/tutorial005_py310.py hl[2,8] *} -A string será verificada para se tornar uma URL válida e documentada no esquema JSON/1OpenAPI como tal. +A string será verificada para se tornar uma URL válida e documentada no JSON Schema / OpenAPI como tal. -## Atributos como listas de submodelos +## Atributos como listas de submodelos { #attributes-with-lists-of-submodels } Você também pode usar modelos Pydantic como subtipos de `list`, `set`, etc: -{* ../../docs_src/body_nested_models/tutorial006.py hl[20] *} +{* ../../docs_src/body_nested_models/tutorial006_py310.py hl[18] *} Isso vai esperar(converter, validar, documentar, etc) um corpo JSON tal qual: @@ -149,38 +156,43 @@ Isso vai esperar(converter, validar, documentar, etc) um corpo JSON tal qual: } ``` -/// info | informação +/// info | Informação -Note como o campo `images` agora tem uma lista de objetos de image. +Observe como a chave `images` agora tem uma lista de objetos de imagem. /// -## Modelos profundamente aninhados +## Modelos profundamente aninhados { #deeply-nested-models } Você pode definir modelos profundamente aninhados de forma arbitrária: -{* ../../docs_src/body_nested_models/tutorial007.py hl[9,14,20,23,27] *} +{* ../../docs_src/body_nested_models/tutorial007_py310.py hl[7,12,18,21,25] *} -/// info | informação +/// info | Informação -Note como `Offer` tem uma lista de `Item`s, que por sua vez possui opcionalmente uma lista `Image`s +Observe como `Offer` tem uma lista de `Item`s, que por sua vez têm uma lista opcional de `Image`s /// -## Corpos de listas puras +## Corpos de listas puras { #bodies-of-pure-lists } Se o valor de primeiro nível do corpo JSON que você espera for um `array` do JSON (uma` lista` do Python), você pode declarar o tipo no parâmetro da função, da mesma forma que nos modelos do Pydantic: - ```Python images: List[Image] ``` +ou no Python 3.9 e superior: + +```Python +images: list[Image] +``` + como em: -{* ../../docs_src/body_nested_models/tutorial008.py hl[15] *} +{* ../../docs_src/body_nested_models/tutorial008_py39.py hl[13] *} -## Suporte de editor em todo canto +## Suporte de editor em todo canto { #editor-support-everywhere } E você obtém suporte do editor em todos os lugares. @@ -192,7 +204,7 @@ Você não conseguiria este tipo de suporte de editor se estivesse trabalhando d Mas você também não precisa se preocupar com eles, os dicts de entrada são convertidos automaticamente e sua saída é convertida automaticamente para JSON também. -## Corpos de `dict`s arbitrários +## Corpos de `dict`s arbitrários { #bodies-of-arbitrary-dicts } Você também pode declarar um corpo como um `dict` com chaves de algum tipo e valores de outro tipo. @@ -208,7 +220,7 @@ Outro caso útil é quando você deseja ter chaves de outro tipo, por exemplo, ` Neste caso, você aceitaria qualquer `dict`, desde que tenha chaves` int` com valores `float`: -{* ../../docs_src/body_nested_models/tutorial009.py hl[9] *} +{* ../../docs_src/body_nested_models/tutorial009_py39.py hl[7] *} /// tip | Dica @@ -222,14 +234,14 @@ E o `dict` que você recebe como `weights` terá, na verdade, chaves `int` e val /// -## Recapitulação +## Recapitulação { #recap } Com **FastAPI** você tem a flexibilidade máxima fornecida pelos modelos Pydantic, enquanto seu código é mantido simples, curto e elegante. Mas com todos os benefícios: -* Suporte do editor (compleção em todo canto!) -* Conversão de dados (leia-se parsing/serialização) +* Suporte do editor (preenchimento automático em todo canto!) +* Conversão de dados (parsing/serialização) * Validação de dados * Documentação dos esquemas * Documentação automática diff --git a/docs/pt/docs/tutorial/body-updates.md b/docs/pt/docs/tutorial/body-updates.md index bf38aeb9e..67bf68492 100644 --- a/docs/pt/docs/tutorial/body-updates.md +++ b/docs/pt/docs/tutorial/body-updates.md @@ -1,6 +1,6 @@ -# Corpo - Atualizações +# Corpo - Atualizações { #body-updates } -## Atualização de dados existentes com `PUT` +## Atualização de dados existentes com `PUT` { #update-replacing-with-put } Para atualizar um item, você pode usar a operação HTTP `PUT`. @@ -10,7 +10,7 @@ Você pode usar `jsonable_encoder` para converter os dados de entrada em dados q `PUT` é usado para receber dados que devem substituir os dados existentes. -### Aviso sobre a substituição +### Aviso sobre a substituição { #warning-about-replacing } Isso significa que, se você quiser atualizar o item `bar` usando `PUT` com um corpo contendo: @@ -26,9 +26,9 @@ Como ele não inclui o atributo já armazenado `"tax": 20.2`, o modelo de entrad E os dados seriam salvos com esse "novo" `tax` de `10.5`. -## Atualizações parciais com `PATCH` +## Atualizações parciais com `PATCH` { #partial-updates-with-patch } -Você também pode usar a operação HTTP `PATCH` para *atualizar* parcialmente os dados. +Você também pode usar a operação HTTP `PATCH` para atualizar parcialmente os dados. Isso significa que você pode enviar apenas os dados que deseja atualizar, deixando o restante intacto. @@ -44,7 +44,7 @@ Mas este guia te dá uma ideia de como eles são destinados a serem usados. /// -### Usando o parâmetro `exclude_unset` do Pydantic +### Usando o parâmetro `exclude_unset` do Pydantic { #using-pydantics-exclude-unset-parameter } Se você quiser receber atualizações parciais, é muito útil usar o parâmetro `exclude_unset` no método `.model_dump()` do modelo do Pydantic. @@ -52,7 +52,7 @@ Como `item.model_dump(exclude_unset=True)`. /// info | Informação -No Pydantic v1, o método que era chamado `.dict()` e foi depreciado (mas ainda suportado) no Pydantic v2. Agora, deve-se usar o método `.model_dump()`. +No Pydantic v1, o método que era chamado `.dict()` e foi descontinuado (mas ainda suportado) no Pydantic v2. Agora, deve-se usar o método `.model_dump()`. Os exemplos aqui usam `.dict()` para compatibilidade com o Pydantic v1, mas você deve usar `.model_dump()` a partir do Pydantic v2. @@ -64,13 +64,13 @@ Então, você pode usar isso para gerar um `dict` com apenas os dados definidos {* ../../docs_src/body_updates/tutorial002_py310.py hl[32] *} -### Usando o parâmetro `update` do Pydantic +### Usando o parâmetro `update` do Pydantic { #using-pydantics-update-parameter } Agora, você pode criar uma cópia do modelo existente usando `.model_copy()`, e passar o parâmetro `update` com um `dict` contendo os dados para atualizar. /// info | Informação -No Pydantic v1, o método era chamado `.copy()`, ele foi depreciado (mas ainda suportado) no Pydantic v2, e renomeado para `.model_copy()`. +No Pydantic v1, o método era chamado `.copy()`, ele foi descontinuado (mas ainda suportado) no Pydantic v2, e renomeado para `.model_copy()`. Os exemplos aqui usam `.copy()` para compatibilidade com o Pydantic v1, mas você deve usar `.model_copy()` com o Pydantic v2. @@ -80,7 +80,7 @@ Como `stored_item_model.model_copy(update=update_data)`: {* ../../docs_src/body_updates/tutorial002_py310.py hl[33] *} -### Recapitulando as atualizações parciais +### Recapitulando as atualizações parciais { #partial-updates-recap } Resumindo, para aplicar atualizações parciais você pode: diff --git a/docs/pt/docs/tutorial/body.md b/docs/pt/docs/tutorial/body.md index 2508d7981..ef00b9a7a 100644 --- a/docs/pt/docs/tutorial/body.md +++ b/docs/pt/docs/tutorial/body.md @@ -1,16 +1,16 @@ -# Corpo da Requisição +# Corpo da requisição { #request-body } -Quando você precisa enviar dados de um cliente (como de um navegador web) para sua API, você o envia como um **corpo da requisição**. +Quando você precisa enviar dados de um cliente (como de um navegador) para sua API, você os envia como um **corpo da requisição**. O corpo da **requisição** é a informação enviada pelo cliente para sua API. O corpo da **resposta** é a informação que sua API envia para o cliente. -Sua API quase sempre irá enviar um corpo na **resposta**. Mas os clientes não necessariamente precisam enviar um corpo em toda **requisição**. +Sua API quase sempre precisa enviar um corpo na **resposta**. Mas os clientes não necessariamente precisam enviar **corpos de requisição** o tempo todo, às vezes eles apenas requisitam um path, talvez com alguns parâmetros de consulta, mas não enviam um corpo. Para declarar um corpo da **requisição**, você utiliza os modelos do Pydantic com todos os seus poderes e benefícios. /// info | Informação -Para enviar dados, você deve usar utilizar um dos métodos: `POST` (Mais comum), `PUT`, `DELETE` ou `PATCH`. +Para enviar dados, você deve usar um dos: `POST` (o mais comum), `PUT`, `DELETE` ou `PATCH`. Enviar um corpo em uma requisição `GET` não tem um comportamento definido nas especificações, porém é suportado pelo FastAPI, apenas para casos de uso bem complexos/extremos. @@ -18,19 +18,19 @@ Como é desencorajado, a documentação interativa com Swagger UI não irá most /// -## Importe o `BaseModel` do Pydantic +## Importe o `BaseModel` do Pydantic { #import-pydantics-basemodel } Primeiro, você precisa importar `BaseModel` do `pydantic`: -{* ../../docs_src/body/tutorial001.py hl[4] *} +{* ../../docs_src/body/tutorial001_py310.py hl[2] *} -## Crie seu modelo de dados +## Crie seu modelo de dados { #create-your-data-model } Então você declara seu modelo de dados como uma classe que herda `BaseModel`. Utilize os tipos Python padrão para todos os atributos: -{* ../../docs_src/body/tutorial001.py hl[7:11] *} +{* ../../docs_src/body/tutorial001_py310.py hl[5:9] *} Assim como quando declaramos parâmetros de consulta, quando um atributo do modelo possui um valor padrão, ele se torna opcional. Caso contrário, se torna obrigatório. Use `None` para torná-lo opcional. @@ -39,13 +39,13 @@ Por exemplo, o modelo acima declara um JSON "`object`" (ou `dict` no Python) com ```JSON { "name": "Foo", - "description": "Uma descrição opcional", + "description": "An optional description", "price": 45.2, "tax": 3.5 } ``` -...como `description` e `tax` são opcionais (Com um valor padrão de `None`), esse JSON "`object`" também é válido: +...como `description` e `tax` são opcionais (com um valor padrão de `None`), esse JSON "`object`" também é válido: ```JSON { @@ -54,40 +54,40 @@ Por exemplo, o modelo acima declara um JSON "`object`" (ou `dict` no Python) com } ``` -## Declare como um parâmetro +## Declare como um parâmetro { #declare-it-as-a-parameter } -Para adicionar o corpo na *função de operação de rota*, declare-o da mesma maneira que você declarou parâmetros de rota e consulta: +Para adicioná-lo à sua *operação de rota*, declare-o da mesma maneira que você declarou parâmetros de rota e de consulta: -{* ../../docs_src/body/tutorial001.py hl[18] *} +{* ../../docs_src/body/tutorial001_py310.py hl[16] *} -...E declare o tipo como o modelo que você criou, `Item`. +...e declare o seu tipo como o modelo que você criou, `Item`. -## Resultados +## Resultados { #results } -Apenas com esse declaração de tipos do Python, o **FastAPI** irá: +Apenas com essa declaração de tipos do Python, o **FastAPI** irá: * Ler o corpo da requisição como um JSON. * Converter os tipos correspondentes (se necessário). * Validar os dados. - * Se algum dados for inválido, irá retornar um erro bem claro, indicando exatamente onde e o que está incorreto. + * Se algum dado for inválido, irá retornar um erro bem claro, indicando exatamente onde e o que estava incorreto. * Entregar a você a informação recebida no parâmetro `item`. * Como você o declarou na função como do tipo `Item`, você também terá o suporte do editor (completação, etc) para todos os atributos e seus tipos. -* Gerar um Esquema JSON com as definições do seu modelo, você também pode utilizá-lo em qualquer lugar que quiser, se fizer sentido para seu projeto. -* Esses esquemas farão parte do esquema OpenAPI, e utilizados nas UIs de documentação automática. +* Gerar definições de JSON Schema para o seu modelo; você também pode usá-las em qualquer outro lugar se fizer sentido para o seu projeto. +* Esses schemas farão parte do esquema OpenAPI gerado, e serão usados pelas UIs de documentação automática. -## Documentação automática +## Documentação automática { #automatic-docs } -Os esquemas JSON dos seus modelos farão parte do esquema OpenAPI gerado para sua aplicação, e aparecerão na documentação interativa da API: +Os JSON Schemas dos seus modelos farão parte do esquema OpenAPI gerado para sua aplicação, e aparecerão na documentação interativa da API: -E também serão utilizados em cada *função de operação de rota* que utilizá-los: +E também serão utilizados na documentação da API dentro de cada *operação de rota* que precisar deles: -## Suporte do editor de texto: +## Suporte do editor { #editor-support } -No seu editor de texto, dentro da função você receberá dicas de tipos e completação em todo lugar (isso não aconteceria se você recebesse um `dict` em vez de um modelo Pydantic): +No seu editor, dentro da função você receberá dicas de tipos e completação em todo lugar (isso não aconteceria se você recebesse um `dict` em vez de um modelo Pydantic): @@ -111,9 +111,9 @@ Mas você terá o mesmo suporte do editor no PyCharm como editor, você pode utilizar o Plugin do Pydantic para o PyCharm . -Melhora o suporte do editor para seus modelos Pydantic com:: +Melhora o suporte do editor para seus modelos Pydantic com: -* completação automática +* preenchimento automático * verificação de tipos * refatoração * buscas @@ -121,42 +121,52 @@ Melhora o suporte do editor para seus modelos Pydantic com:: /// -## Use o modelo +## Use o modelo { #use-the-model } Dentro da função, você pode acessar todos os atributos do objeto do modelo diretamente: -{* ../../docs_src/body/tutorial002.py hl[21] *} +{* ../../docs_src/body/tutorial002_py310.py *} -## Corpo da requisição + parâmetros de rota +/// info | Informação + +No Pydantic v1 o método se chamava `.dict()`, ele foi descontinuado (mas ainda é suportado) no Pydantic v2, e renomeado para `.model_dump()`. + +Os exemplos aqui usam `.dict()` para compatibilidade com o Pydantic v1, mas você deve usar `.model_dump()` se puder usar o Pydantic v2. + +/// + +## Corpo da requisição + parâmetros de rota { #request-body-path-parameters } Você pode declarar parâmetros de rota e corpo da requisição ao mesmo tempo. -O **FastAPI** irá reconhecer que os parâmetros da função que combinam com parâmetros de rota devem ser **retirados da rota**, e parâmetros da função que são declarados como modelos Pydantic sejam **retirados do corpo da requisição**. +O **FastAPI** irá reconhecer que os parâmetros da função que combinam com parâmetros de rota devem ser **retirados da rota**, e que parâmetros da função que são declarados como modelos Pydantic sejam **retirados do corpo da requisição**. -{* ../../docs_src/body/tutorial003.py hl[17:18] *} +{* ../../docs_src/body/tutorial003_py310.py hl[15:16] *} -## Corpo da requisição + parâmetros de rota + parâmetros de consulta +## Corpo da requisição + parâmetros de rota + parâmetros de consulta { #request-body-path-query-parameters } Você também pode declarar parâmetros de **corpo**, **rota** e **consulta**, ao mesmo tempo. O **FastAPI** irá reconhecer cada um deles e retirar a informação do local correto. -{* ../../docs_src/body/tutorial004.py hl[18] *} +{* ../../docs_src/body/tutorial004_py310.py hl[16] *} Os parâmetros da função serão reconhecidos conforme abaixo: -* Se o parâmetro também é declarado na **rota**, será utilizado como um parâmetro de rota. +* Se o parâmetro também é declarado no **path**, será utilizado como um parâmetro de rota. * Se o parâmetro é de um **tipo único** (como `int`, `float`, `str`, `bool`, etc) será interpretado como um parâmetro de **consulta**. * Se o parâmetro é declarado como um **modelo Pydantic**, será interpretado como o **corpo** da requisição. -/// note | Observação +/// note | Nota O FastAPI saberá que o valor de `q` não é obrigatório por causa do valor padrão `= None`. -O `Union` em `Union[str, None]` não é utilizado pelo FastAPI, mas permite ao seu editor de texto lhe dar um suporte melhor e detectar erros. +O `str | None` (Python 3.10+) ou o `Union` em `Union[str, None]` (Python 3.8+) não é utilizado pelo FastAPI para determinar que o valor não é obrigatório, ele saberá que não é obrigatório porque tem um valor padrão `= None`. + +Mas adicionar as anotações de tipo permitirá ao seu editor oferecer um suporte melhor e detectar erros. /// -## Sem o Pydantic +## Sem o Pydantic { #without-pydantic } -Se você não quer utilizar os modelos Pydantic, você também pode utilizar o parâmetro **Body**. Veja a documentação para [Body - Parâmetros múltiplos: Valores singulares no body](body-multiple-params.md#valores-singulares-no-corpo){.internal-link target=_blank}. +Se você não quer utilizar os modelos Pydantic, você também pode utilizar o parâmetro **Body**. Veja a documentação para [Body - Parâmetros múltiplos: Valores singulares no body](body-multiple-params.md#singular-values-in-body){.internal-link target=_blank}. diff --git a/docs/pt/docs/tutorial/cookie-param-models.md b/docs/pt/docs/tutorial/cookie-param-models.md index 3d46ba44c..470544f99 100644 --- a/docs/pt/docs/tutorial/cookie-param-models.md +++ b/docs/pt/docs/tutorial/cookie-param-models.md @@ -1,4 +1,4 @@ -# Modelos de Parâmetros de Cookie +# Modelos de Parâmetros de Cookie { #cookie-parameter-models } Se você possui um grupo de **cookies** que estão relacionados, você pode criar um **modelo Pydantic** para declará-los. 🍪 @@ -16,7 +16,7 @@ Essa mesma técnica se aplica para `Query`, `Cookie`, e `Header`. 😎 /// -## Cookies com Modelos Pydantic +## Cookies com Modelos Pydantic { #cookies-with-a-pydantic-model } Declare o parâmetro de **cookie** que você precisa em um **modelo Pydantic**, e depois declare o parâmetro como um `Cookie`: @@ -24,9 +24,9 @@ Declare o parâmetro de **cookie** que você precisa em um **modelo Pydantic**, O **FastAPI** irá **extrair** os dados para **cada campo** dos **cookies** recebidos na requisição e lhe fornecer o modelo Pydantic que você definiu. -## Verifique os Documentos +## Verifique a Documentação { #check-the-docs } -Você pode ver os cookies definidos na IU dos documentos em `/docs`: +Você pode ver os cookies definidos na IU da documentação em `/docs`:
@@ -36,17 +36,17 @@ Você pode ver os cookies definidos na IU dos documentos em `/docs`: Tenha em mente que, como os **navegadores lidam com cookies** de maneira especial e por baixo dos panos, eles **não** permitem facilmente que o **JavaScript** lidem com eles. -Se você for na **IU de documentos da API** em `/docs` você poderá ver a **documentação** para cookies das suas *operações de rotas*. +Se você for na **IU da documentação da API** em `/docs` você poderá ver a **documentação** para cookies das suas *operações de rotas*. -Mas mesmo que você **adicionar os dados** e clicar em "Executar", pelo motivo da IU dos documentos trabalharem com **JavaScript**, os cookies não serão enviados, e você verá uma mensagem de **erro** como se você não tivesse escrito nenhum dado. +Mas mesmo que você **adicionar os dados** e clicar em "Executar", pelo motivo da IU da documentação trabalhar com **JavaScript**, os cookies não serão enviados, e você verá uma mensagem de **erro** como se você não tivesse escrito nenhum dado. /// -## Proibir Cookies Adicionais +## Proibir Cookies Adicionais { #forbid-extra-cookies } Em alguns casos especiais (provavelmente não muito comuns), você pode querer **restringir** os cookies que você deseja receber. -Agora a sua API possui o poder de contrar o seu próprio consentimento de cookie. 🤪🍪 +Agora a sua API possui o poder de controlar o seu próprio consentimento de cookie. 🤪🍪 Você pode utilizar a configuração do modelo Pydantic para `proibir` qualquer campo `extra`. @@ -58,7 +58,7 @@ Se o cliente tentar enviar alguns **cookies extras**, eles receberão um retorno Coitados dos banners de cookies com todo o seu esforço para obter o seu consentimento para a API rejeitá-lo. 🍪 -Por exemplo, se o cliente tentar enviar um cookie `santa_tracker` com o valor de `good-list-please`, o cliente receberá uma resposta de **erro** informando que o cookie `santa_tracker` is not allowed: +Por exemplo, se o cliente tentar enviar um cookie `santa_tracker` com o valor de `good-list-please`, o cliente receberá uma resposta de **erro** informando que o `santa_tracker` cookie não é permitido: ```json { @@ -73,6 +73,6 @@ Por exemplo, se o cliente tentar enviar um cookie `santa_tracker` com o valor de } ``` -## Resumo +## Resumo { #summary } Você consegue utilizar **modelos Pydantic** para declarar **cookies** no **FastAPI**. 😎 diff --git a/docs/pt/docs/tutorial/cookie-params.md b/docs/pt/docs/tutorial/cookie-params.md index da85d796e..5540a67d2 100644 --- a/docs/pt/docs/tutorial/cookie-params.md +++ b/docs/pt/docs/tutorial/cookie-params.md @@ -1,20 +1,19 @@ -# Parâmetros de Cookie +# Parâmetros de Cookie { #cookie-parameters } -Você pode definir parâmetros de Cookie da mesma maneira que define paramêtros com `Query` e `Path`. +Você pode definir parâmetros de Cookie da mesma maneira que define parâmetros com `Query` e `Path`. -## Importe `Cookie` +## Importe `Cookie` { #import-cookie } Primeiro importe `Cookie`: {* ../../docs_src/cookie_params/tutorial001_an_py310.py hl[3] *} -## Declare parâmetros de `Cookie` +## Declare parâmetros de `Cookie` { #declare-cookie-parameters } -Então declare os paramêtros de cookie usando a mesma estrutura que em `Path` e `Query`. +Então declare os parâmetros de cookie usando a mesma estrutura que em `Path` e `Query`. Você pode definir o valor padrão, assim como todas as validações extras ou parâmetros de anotação: - {* ../../docs_src/cookie_params/tutorial001_an_py310.py hl[9] *} /// note | Detalhes Técnicos @@ -31,6 +30,16 @@ Para declarar cookies, você precisa usar `Cookie`, pois caso contrário, os par /// -## Recapitulando +/// info | Informação + +Tenha em mente que, como os **navegadores lidam com cookies** de maneiras especiais e nos bastidores, eles **não** permitem facilmente que o **JavaScript** os acesse. + +Se você for à **interface de documentação da API** em `/docs`, poderá ver a **documentação** de cookies para suas *operações de rota*. + +Mas mesmo que você **preencha os dados** e clique em "Execute", como a interface de documentação funciona com **JavaScript**, os cookies não serão enviados e você verá uma mensagem de **erro** como se você não tivesse escrito nenhum valor. + +/// + +## Recapitulando { #recap } Declare cookies com `Cookie`, usando o mesmo padrão comum que utiliza-se em `Query` e `Path`. diff --git a/docs/pt/docs/tutorial/cors.md b/docs/pt/docs/tutorial/cors.md index 0ab07a3c2..c08191db1 100644 --- a/docs/pt/docs/tutorial/cors.md +++ b/docs/pt/docs/tutorial/cors.md @@ -1,8 +1,8 @@ -# CORS (Cross-Origin Resource Sharing) +# CORS (Cross-Origin Resource Sharing) { #cors-cross-origin-resource-sharing } CORS ou "Cross-Origin Resource Sharing" refere-se às situações em que um frontend rodando em um navegador possui um código JavaScript que se comunica com um backend, e o backend está em uma "origem" diferente do frontend. -## Origem +## Origem { #origin } Uma origem é a combinação de protocolo (`http`, `https`), domínio (`myapp.com`, `localhost`, `localhost.tiangolo.com`), e porta (`80`, `443`, `8080`). @@ -12,27 +12,27 @@ Então, todos estes são origens diferentes: * `https://localhost` * `http://localhost:8080` -Mesmo se todos estiverem em `localhost`, eles usam diferentes protocolos e portas, portanto, são "origens" diferentes. +Mesmo se todos estiverem em `localhost`, eles usam diferentes protocolos ou portas, portanto, são "origens" diferentes. -## Passos +## Passos { #steps } -Então, digamos que você tenha um frontend rodando no seu navegador em `http://localhost:8080`, e seu JavaScript esteja tentando se comunicar com um backend rodando em http://localhost (como não especificamos uma porta, o navegador assumirá a porta padrão `80`). +Então, digamos que você tenha um frontend rodando no seu navegador em `http://localhost:8080`, e seu JavaScript esteja tentando se comunicar com um backend rodando em `http://localhost` (como não especificamos uma porta, o navegador assumirá a porta padrão `80`). -Portanto, o navegador irá enviar uma requisição HTTP `OPTIONS` ao backend, e se o backend enviar os cabeçalhos apropriados autorizando a comunicação a partir de uma origem diferente (`http://localhost:8080`) então o navegador deixará o JavaScript no frontend enviar sua requisição para o backend. +Portanto, o navegador enviará uma requisição HTTP `OPTIONS` ao backend `:80`, e se o backend enviar os cabeçalhos apropriados autorizando a comunicação a partir dessa origem diferente (`http://localhost:8080`), então o navegador `:8080` permitirá que o JavaScript no frontend envie sua requisição para o backend `:80`. -Para conseguir isso, o backend deve ter uma lista de "origens permitidas". +Para conseguir isso, o backend `:80` deve ter uma lista de "origens permitidas". -Neste caso, ele terá que incluir `http://localhost:8080` para o frontend funcionar corretamente. +Neste caso, a lista terá que incluir `http://localhost:8080` para o frontend `:8080` funcionar corretamente. -## Curingas +## Curingas { #wildcards } -É possível declarar uma lista com `"*"` (um "curinga") para dizer que tudo está permitido. +É possível declarar a lista como `"*"` (um "curinga") para dizer que tudo está permitido. Mas isso só permitirá certos tipos de comunicação, excluindo tudo que envolva credenciais: cookies, cabeçalhos de autorização como aqueles usados ​​com Bearer Tokens, etc. Então, para que tudo funcione corretamente, é melhor especificar explicitamente as origens permitidas. -## Usar `CORSMiddleware` +## Usar `CORSMiddleware` { #use-corsmiddleware } Você pode configurá-lo em sua aplicação **FastAPI** usando o `CORSMiddleware`. @@ -48,7 +48,7 @@ Você também pode especificar se o seu backend permite: {* ../../docs_src/cors/tutorial001.py hl[2,6:11,13:19] *} -Os parâmetros padrão usados ​​pela implementação `CORSMiddleware` são restritivos por padrão, então você precisará habilitar explicitamente as origens, métodos ou cabeçalhos específicos para que os navegadores tenham permissão para usá-los em um contexto de domínios diferentes. +Os parâmetros padrão usados ​​pela implementação `CORSMiddleware` são restritivos por padrão, então você precisará habilitar explicitamente as origens, métodos ou cabeçalhos específicos para que os navegadores tenham permissão para usá-los em um contexto cross domain. Os seguintes argumentos são suportados: @@ -56,27 +56,30 @@ Os seguintes argumentos são suportados: * `allow_origin_regex` - Uma string regex para corresponder às origens que devem ter permissão para fazer requisições de origem cruzada. Por exemplo, `'https://.*\.example\.org'`. * `allow_methods` - Uma lista de métodos HTTP que devem ser permitidos para requisições de origem cruzada. O padrão é `['GET']`. Você pode usar `['*']` para permitir todos os métodos padrão. * `allow_headers` - Uma lista de cabeçalhos de solicitação HTTP que devem ter suporte para requisições de origem cruzada. O padrão é `[]`. Você pode usar `['*']` para permitir todos os cabeçalhos. Os cabeçalhos `Accept`, `Accept-Language`, `Content-Language` e `Content-Type` são sempre permitidos para requisições CORS simples. -* `allow_credentials` - Indica que os cookies devem ser suportados para requisições de origem cruzada. O padrão é `False`. Além disso, `allow_origins` não pode ser definido como `['*']` para que as credenciais sejam permitidas, as origens devem ser especificadas. +* `allow_credentials` - Indica que os cookies devem ser suportados para requisições de origem cruzada. O padrão é `False`. + + Nenhum de `allow_origins`, `allow_methods` e `allow_headers` pode ser definido como `['*']` se `allow_credentials` estiver definido como `True`. Todos eles devem ser especificados explicitamente. + * `expose_headers` - Indica quaisquer cabeçalhos de resposta que devem ser disponibilizados ao navegador. O padrão é `[]`. * `max_age` - Define um tempo máximo em segundos para os navegadores armazenarem em cache as respostas CORS. O padrão é `600`. O middleware responde a dois tipos específicos de solicitação HTTP... -### Requisições CORS pré-voo (preflight) +### Requisições CORS pré-voo (preflight) { #cors-preflight-requests } Estas são quaisquer solicitações `OPTIONS` com cabeçalhos `Origin` e `Access-Control-Request-Method`. Nesse caso, o middleware interceptará a solicitação recebida e responderá com cabeçalhos CORS apropriados e uma resposta `200` ou `400` para fins informativos. -### Requisições Simples +### Requisições Simples { #simple-requests } Qualquer solicitação com um cabeçalho `Origin`. Neste caso, o middleware passará a solicitação normalmente, mas incluirá cabeçalhos CORS apropriados na resposta. -## Mais informações +## Mais informações { #more-info } -Para mais informações CORS, acesse Mozilla CORS documentation. +Para mais informações sobre CORS, consulte a documentação do CORS da Mozilla. -/// note | Detalhes técnicos +/// note | Detalhes Técnicos Você também pode usar `from starlette.middleware.cors import CORSMiddleware`. diff --git a/docs/pt/docs/tutorial/debugging.md b/docs/pt/docs/tutorial/debugging.md index 67b764457..21d1d527b 100644 --- a/docs/pt/docs/tutorial/debugging.md +++ b/docs/pt/docs/tutorial/debugging.md @@ -1,14 +1,14 @@ -# Depuração +# Depuração { #debugging } Você pode conectar o depurador no seu editor, por exemplo, com o Visual Studio Code ou PyCharm. -## Chamar `uvicorn` +## Chamar `uvicorn` { #call-uvicorn } -Em seu aplicativo FastAPI, importe e execute `uvicorn` diretamente: +Em sua aplicação FastAPI, importe e execute `uvicorn` diretamente: {* ../../docs_src/debugging/tutorial001.py hl[1,15] *} -### Sobre `__name__ == "__main__"` +### Sobre `__name__ == "__main__"` { #about-name-main } O objetivo principal de `__name__ == "__main__"` é ter algum código que seja executado quando seu arquivo for chamado com: @@ -26,7 +26,7 @@ mas não é chamado quando outro arquivo o importa, como em: from myapp import app ``` -#### Mais detalhes +#### Mais detalhes { #more-details } Digamos que seu arquivo se chama `myapp.py`. @@ -78,9 +78,9 @@ Para mais informações, consulte -## Pegando um Atalho +## Pegando um Atalho { #shortcut } Mas você pode ver que temos uma repetição do código neste exemplo, escrevendo `CommonQueryParams` duas vezes: diff --git a/docs/pt/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md b/docs/pt/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md index d7d31bb45..ee8a58dc2 100644 --- a/docs/pt/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md +++ b/docs/pt/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md @@ -1,4 +1,4 @@ -# Dependências em decoradores de operações de rota +# Dependências em decoradores de operações de rota { #dependencies-in-path-operation-decorators } Em alguns casos você não precisa necessariamente retornar o valor de uma dependência dentro de uma *função de operação de rota*. @@ -8,7 +8,7 @@ Mas você ainda precisa que ela seja executada/resolvida. Para esses casos, em vez de declarar um parâmetro em uma *função de operação de rota* com `Depends`, você pode adicionar um argumento `dependencies` do tipo `list` ao decorador da operação de rota. -## Adicionando `dependencies` ao decorador da operação de rota +## Adicione `dependencies` ao decorador da operação de rota { #add-dependencies-to-the-path-operation-decorator } O *decorador da operação de rota* recebe um argumento opcional `dependencies`. @@ -22,7 +22,7 @@ Essas dependências serão executadas/resolvidas da mesma forma que dependência Alguns editores de texto checam parâmetros de funções não utilizados, e os mostram como erros. -Utilizando `dependencies` no *decorador da operação de rota* você pode garantir que elas serão executadas enquanto evita errors de editores/ferramentas. +Utilizando `dependencies` no *decorador da operação de rota* você pode garantir que elas serão executadas enquanto evita erros de editores/ferramentas. Isso também pode ser útil para evitar confundir novos desenvolvedores que ao ver um parâmetro não usado no seu código podem pensar que ele é desnecessário. @@ -30,29 +30,29 @@ Isso também pode ser útil para evitar confundir novos desenvolvedores que ao v /// info | Informação -Neste exemplo utilizamos cabeçalhos personalizados inventados `X-Keys` e `X-Token`. +Neste exemplo utilizamos cabeçalhos personalizados inventados `X-Key` e `X-Token`. Mas em situações reais, como implementações de segurança, você pode obter mais vantagens em usar as [Ferramentas de segurança integradas (o próximo capítulo)](../security/index.md){.internal-link target=_blank}. /// -## Erros das dependências e valores de retorno +## Erros das dependências e valores de retorno { #dependencies-errors-and-return-values } Você pode utilizar as mesmas *funções* de dependências que você usaria normalmente. -### Requisitos de Dependências +### Requisitos de Dependências { #dependency-requirements } Dependências podem declarar requisitos de requisições (como cabeçalhos) ou outras subdependências: {* ../../docs_src/dependencies/tutorial006_an_py39.py hl[8,13] *} -### Levantando exceções +### Levantar exceções { #raise-exceptions } -Essas dependências podem levantar exceções, da mesma forma que dependências comuns: +Essas dependências podem `raise` exceções, da mesma forma que dependências comuns: {* ../../docs_src/dependencies/tutorial006_an_py39.py hl[10,15] *} -### Valores de retorno +### Valores de retorno { #return-values } E elas também podem ou não retornar valores, eles não serão utilizados. @@ -60,10 +60,10 @@ Então, você pode reutilizar uma dependência comum (que retorna um valor) que {* ../../docs_src/dependencies/tutorial006_an_py39.py hl[11,16] *} -## Dependências para um grupo de *operações de rota* +## Dependências para um grupo de *operações de rota* { #dependencies-for-a-group-of-path-operations } -Mais a frente, quando você ler sobre como estruturar aplicações maiores ([Bigger Applications - Multiple Files](../../tutorial/bigger-applications.md){.internal-link target=_blank}), possivelmente com múltiplos arquivos, você aprenderá a declarar um único parâmetro `dependencies` para um grupo de *operações de rota*. +Mais a frente, quando você ler sobre como estruturar aplicações maiores ([Aplicações maiores - Múltiplos arquivos](../../tutorial/bigger-applications.md){.internal-link target=_blank}), possivelmente com múltiplos arquivos, você aprenderá a declarar um único parâmetro `dependencies` para um grupo de *operações de rota*. -## Dependências globais +## Dependências globais { #global-dependencies } -No próximo passo veremos como adicionar dependências para uma aplicação `FastAPI` inteira, para que ela seja aplicada em toda *operação de rota*. +No próximo passo veremos como adicionar dependências para uma aplicação `FastAPI` inteira, para que elas sejam aplicadas em toda *operação de rota*. diff --git a/docs/pt/docs/tutorial/dependencies/dependencies-with-yield.md b/docs/pt/docs/tutorial/dependencies/dependencies-with-yield.md index eaf711197..0aedcfb31 100644 --- a/docs/pt/docs/tutorial/dependencies/dependencies-with-yield.md +++ b/docs/pt/docs/tutorial/dependencies/dependencies-with-yield.md @@ -1,12 +1,12 @@ -# Dependências com yield +# Dependências com yield { #dependencies-with-yield } -O FastAPI possui suporte para dependências que realizam alguns passos extras ao finalizar. +O **FastAPI** possui suporte para dependências que realizam alguns passos extras ao finalizar. Para fazer isso, utilize `yield` em vez de `return`, e escreva os passos extras (código) depois. /// tip | Dica -Garanta que `yield` é utilizado apenas uma vez. +Garanta utilizar `yield` apenas uma vez por dependência. /// @@ -23,19 +23,19 @@ Na realidade, o FastAPI utiliza esses dois decoradores internamente. /// -## Uma dependência de banco de dados com `yield` +## Uma dependência de banco de dados com `yield` { #a-database-dependency-with-yield } -Por exemplo, você poderia utilizar isso para criar uma sessão do banco de dados, e fechá-la após terminar sua operação. +Por exemplo, você poderia utilizar isso para criar uma sessão do banco de dados, e fechá-la após terminar. -Apenas o código anterior a declaração com `yield` e o código contendo essa declaração são executados antes de criar uma resposta. +Apenas o código anterior à declaração com `yield` e o código contendo essa declaração são executados antes de criar uma resposta: {* ../../docs_src/dependencies/tutorial007.py hl[2:4] *} -O valor gerado (yielded) é o que é injetado nas *operações de rota* e outras dependências. +O valor gerado (yielded) é o que é injetado nas *operações de rota* e outras dependências: {* ../../docs_src/dependencies/tutorial007.py hl[4] *} -O código após o `yield` é executado após a resposta ser entregue: +O código após o `yield` é executado após a resposta: {* ../../docs_src/dependencies/tutorial007.py hl[5:6] *} @@ -47,21 +47,19 @@ O **FastAPI** saberá o que fazer com cada uma, da mesma forma que as dependênc /// -## Uma dependência com `yield` e `try` +## Uma dependência com `yield` e `try` { #a-dependency-with-yield-and-try } Se você utilizar um bloco `try` em uma dependência com `yield`, você irá capturar qualquer exceção que for lançada enquanto a dependência é utilizada. -Por exemplo, se algum código em um certo momento no meio da operação, em outra dependência ou em uma *operação de rota*, fizer um "rollback" de uma transação de banco de dados ou causar qualquer outro erro, você irá capturar a exceção em sua dependência. +Por exemplo, se algum código em um certo momento no meio, em outra dependência ou em uma *operação de rota*, fizer um "rollback" de uma transação de banco de dados ou causar qualquer outra exceção, você irá capturar a exceção em sua dependência. Então, você pode procurar por essa exceção específica dentro da dependência com `except AlgumaExcecao`. Da mesma forma, você pode utilizar `finally` para garantir que os passos de saída são executados, com ou sem exceções. -```python hl_lines="3 5" -{!../../docs_src/dependencies/tutorial007.py!} -``` +{* ../../docs_src/dependencies/tutorial007.py hl[3,5] *} -## Subdependências com `yield` +## Subdependências com `yield` { #sub-dependencies-with-yield } Você pode ter subdependências e "árvores" de subdependências de qualquer tamanho e forma, e qualquer uma ou todas elas podem utilizar `yield`. @@ -69,73 +67,17 @@ O **FastAPI** garantirá que o "código de saída" em cada dependência com `yie Por exemplo, `dependency_c` pode depender de `dependency_b`, e `dependency_b` depender de `dependency_a`: -//// tab | python 3.9+ - -```python hl_lines="6 14 22" -{!> ../../docs_src/dependencies/tutorial008_an_py39.py!} -``` - -//// - -//// tab | python 3.8+ - -```python hl_lines="5 13 21" -{!> ../../docs_src/dependencies/tutorial008_an.py!} -``` - -//// - -//// tab | python 3.8+ non-annotated - -/// tip | Dica - -Utilize a versão com `Annotated` se possível. - -/// - -```python hl_lines="4 12 20" -{!> ../../docs_src/dependencies/tutorial008.py!} -``` - -//// +{* ../../docs_src/dependencies/tutorial008_an_py39.py hl[6,14,22] *} E todas elas podem utilizar `yield`. -Neste caso, `dependency_c` precisa que o valor de `dependency_b` (nomeada de `dep_b` aqui) continue disponível para executar seu código de saída. +Neste caso, `dependency_c`, para executar seu código de saída, precisa que o valor de `dependency_b` (nomeado de `dep_b` aqui) continue disponível. -E, por outro lado, `dependency_b` precisa que o valor de `dependency_a` (nomeada de `dep_a`) continue disponível para executar seu código de saída. +E, por outro lado, `dependency_b` precisa que o valor de `dependency_a` (nomeado de `dep_a`) esteja disponível para executar seu código de saída. -//// tab | python 3.9+ +{* ../../docs_src/dependencies/tutorial008_an_py39.py hl[18:19,26:27] *} -```python hl_lines="18-19 26-27" -{!> ../../docs_src/dependencies/tutorial008_an_py39.py!} -``` - -//// - -//// tab | python 3.8+ - -```python hl_lines="17-18 25-26" -{!> ../../docs_src/dependencies/tutorial008_an.py!} -``` - -//// - -//// tab | python 3.8+ non-annotated - -/// tip | Dica - -Utilize a versão com `Annotated` se possível. - -/// - -```python hl_lines="16-17 24-25" -{!> ../../docs_src/dependencies/tutorial008.py!} -``` - -//// - -Da mesma forma, você pode ter algumas dependências com `yield` e outras com `return` e ter uma relação de dependência entre algumas dos dois tipos. +Da mesma forma, você pode ter algumas dependências com `yield` e outras com `return` e ter uma relação de dependência entre algumas das duas. E você poderia ter uma única dependência que precisa de diversas outras dependências com `yield`, etc. @@ -151,83 +93,45 @@ O **FastAPI** utiliza eles internamente para alcançar isso. /// -## Dependências com `yield` e `httpexception` +## Dependências com `yield` e `HTTPException` { #dependencies-with-yield-and-httpexception } -Você viu que dependências podem ser utilizadas com `yield` e podem incluir blocos `try` para capturar exceções. +Você viu que pode usar dependências com `yield` e ter blocos `try` que tentam executar algum código e depois executar algum código de saída com `finally`. -Da mesma forma, você pode lançar uma `httpexception` ou algo parecido no código de saída, após o `yield` +Você também pode usar `except` para capturar a exceção que foi levantada e fazer algo com ela. + +Por exemplo, você pode levantar uma exceção diferente, como `HTTPException`. /// tip | Dica -Essa é uma técnica relativamente avançada, e na maioria dos casos você não precisa dela totalmente, já que você pode lançar exceções (incluindo `httpexception`) dentro do resto do código da sua aplicação, por exemplo, em uma *função de operação de rota*. +Essa é uma técnica relativamente avançada, e na maioria dos casos você não vai precisar dela, já que você pode levantar exceções (incluindo `HTTPException`) dentro do resto do código da sua aplicação, por exemplo, na *função de operação de rota*. Mas ela existe para ser utilizada caso você precise. 🤓 /// -//// tab | python 3.9+ +{* ../../docs_src/dependencies/tutorial008b_an_py39.py hl[18:22,31] *} -```python hl_lines="18-22 31" -{!> ../../docs_src/dependencies/tutorial008b_an_py39.py!} -``` +Se você quiser capturar exceções e criar uma resposta personalizada com base nisso, crie um [Manipulador de Exceções Customizado](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}. -//// +## Dependências com `yield` e `except` { #dependencies-with-yield-and-except } -//// tab | python 3.8+ - -```python hl_lines="17-21 30" -{!> ../../docs_src/dependencies/tutorial008b_an.py!} -``` - -//// - -//// tab | python 3.8+ non-annotated - -/// tip | Dica - -Utilize a versão com `Annotated` se possível. - -/// - -```python hl_lines="16-20 29" -{!> ../../docs_src/dependencies/tutorial008b.py!} -``` - -//// - -Uma alternativa que você pode utilizar para capturar exceções (e possivelmente lançar outra HTTPException) é criar um [Manipulador de Exceções Customizado](../handling-errors.md#instalando-manipuladores-de-excecoes-customizados){.internal-link target=_blank}. - -## Dependências com `yield` e `except` - -Se você capturar uma exceção com `except` em uma dependência que utilize `yield` e ela não for levantada novamente (ou uma nova exceção for levantada), o FastAPI não será capaz de identifcar que houve uma exceção, da mesma forma que aconteceria com Python puro: +Se você capturar uma exceção com `except` em uma dependência que utilize `yield` e ela não for levantada novamente (ou uma nova exceção for levantada), o FastAPI não será capaz de identificar que houve uma exceção, da mesma forma que aconteceria com Python puro: {* ../../docs_src/dependencies/tutorial008c_an_py39.py hl[15:16] *} Neste caso, o cliente irá ver uma resposta *HTTP 500 Internal Server Error* como deveria acontecer, já que não estamos levantando nenhuma `HTTPException` ou coisa parecida, mas o servidor **não terá nenhum log** ou qualquer outra indicação de qual foi o erro. 😱 -### Sempre levante (`raise`) exceções em Dependências com `yield` e `except` +### Sempre levante (`raise`) em Dependências com `yield` e `except` { #always-raise-in-dependencies-with-yield-and-except } -Se você capturar uma exceção em uma dependência com `yield`, a menos que você esteja levantando outra `HTTPException` ou coisa parecida, você deveria relançar a exceção original. +Se você capturar uma exceção em uma dependência com `yield`, a menos que você esteja levantando outra `HTTPException` ou coisa parecida, **você deve relançar a exceção original**. Você pode relançar a mesma exceção utilizando `raise`: {* ../../docs_src/dependencies/tutorial008d_an_py39.py hl[17] *} -//// tab | python 3.8+ non-annotated - -/// tip | Dica - -Utilize a versão com `Annotated` se possível. - -/// - -{* ../../docs_src/dependencies/tutorial008d.py hl[15] *} - -//// - Agora o cliente irá receber a mesma resposta *HTTP 500 Internal Server Error*, mas o servidor terá nosso `InternalError` personalizado nos logs. 😎 -## Execução de dependências com `yield` +## Execução de dependências com `yield` { #execution-of-dependencies-with-yield } A sequência de execução é mais ou menos como esse diagrama. O tempo passa do topo para baixo. E cada coluna é uma das partes interagindo ou executando código. @@ -270,57 +174,69 @@ participant tasks as Tarefas de Background Apenas **uma resposta** será enviada para o cliente. Ela pode ser uma das respostas de erro, ou então a resposta da *operação de rota*. -Após uma dessas respostas ser enviada, nenhuma outra resposta pode ser enviada +Após uma dessas respostas ser enviada, nenhuma outra resposta pode ser enviada. /// /// tip | Dica -Esse diagrama mostra `HttpException`, mas você pode levantar qualquer outra exceção que você capture em uma dependência com `yield` ou um [Manipulador de exceções personalizado](../handling-errors.md#instalando-manipuladores-de-excecoes-customizados){.internal-link target=_blank}. - -Se você lançar qualquer exceção, ela será passada para as dependências com yield, inlcuindo a `HTTPException`. Na maioria dos casos você vai querer relançar essa mesma exceção ou uma nova a partir da dependência com `yield` para garantir que ela seja tratada adequadamente. +Se você levantar qualquer exceção no código da *função de operação de rota*, ela será passada para as dependências com `yield`, incluindo `HTTPException`. Na maioria dos casos, você vai querer relançar essa mesma exceção ou uma nova a partir da dependência com `yield` para garantir que ela seja tratada adequadamente. /// -## Dependências com `yield`, `HTTPException`, `except` e Tarefas de Background +## Saída antecipada e `scope` { #early-exit-and-scope } -/// warning | Aviso +Normalmente, o código de saída das dependências com `yield` é executado **após a resposta** ser enviada ao cliente. -Você provavelmente não precisa desses detalhes técnicos, você pode pular essa seção e continuar na próxima seção abaixo. +Mas se você sabe que não precisará usar a dependência depois de retornar da *função de operação de rota*, você pode usar `Depends(scope="function")` para dizer ao FastAPI que deve fechar a dependência depois que a *função de operação de rota* retornar, mas **antes** de a **resposta ser enviada**. -Esses detalhes são úteis principalmente se você estiver usando uma versão do FastAPI anterior à 0.106.0 e utilizando recursos de dependências com `yield` em tarefas de background. +{* ../../docs_src/dependencies/tutorial008e_an_py39.py hl[12,16] *} -/// +`Depends()` recebe um parâmetro `scope` que pode ser: -### Dependências com `yield` e `except`, Detalhes Técnicos +* `"function"`: iniciar a dependência antes da *função de operação de rota* que trata a requisição, encerrar a dependência depois que a *função de operação de rota* termina, mas **antes** de a resposta ser enviada de volta ao cliente. Assim, a função da dependência será executada **em torno** da *função de operação de rota*. +* `"request"`: iniciar a dependência antes da *função de operação de rota* que trata a requisição (semelhante a quando se usa `"function"`), mas encerrar **depois** que a resposta é enviada de volta ao cliente. Assim, a função da dependência será executada **em torno** do ciclo de **requisição** e resposta. -Antes do FastAPI 0.110.0, se você utilizasse uma dependência com `yield`, e então capturasse uma dependência com `except` nessa dependência, caso a exceção não fosse relançada, ela era automaticamente lançada para qualquer manipulador de exceções ou o manipulador de erros interno do servidor. +Se não for especificado e a dependência tiver `yield`, ela terá `scope` igual a `"request"` por padrão. -Isso foi modificado na versão 0.110.0 para consertar o consumo de memória não controlado das exceções relançadas automaticamente sem um manipulador (erros internos do servidor), e para manter o comportamento consistente com o código Python tradicional. +### `scope` para subdependências { #scope-for-sub-dependencies } -### Tarefas de Background e Dependências com `yield`, Detalhes Técnicos +Quando você declara uma dependência com `scope="request"` (o padrão), qualquer subdependência também precisa ter `scope` igual a `"request"`. -Antes do FastAPI 0.106.0, levantar exceções após um `yield` não era possível, o código de saída nas dependências com `yield` era executado *após* a resposta ser enviada, então os [Manipuladores de Exceções](../handling-errors.md#instalando-manipuladores-de-excecoes-customizados){.internal-link target=_blank} já teriam executado. +Mas uma dependência com `scope` igual a `"function"` pode ter dependências com `scope` igual a `"function"` e com `scope` igual a `"request"`. -Isso foi implementado dessa forma principalmente para permitir que os mesmos objetos fornecidos ("yielded") pelas dependências dentro de tarefas de background fossem reutilizados, por que o código de saída era executado antes das tarefas de background serem finalizadas. +Isso porque qualquer dependência precisa conseguir executar seu código de saída antes das subdependências, pois pode ainda precisar usá-las durante seu código de saída. -Ainda assim, como isso exigiria esperar que a resposta navegasse pela rede enquanto mantia ativo um recurso desnecessário na dependência com yield (por exemplo, uma conexão com banco de dados), isso mudou na versão 0.106.0 do FastAPI. +```mermaid +sequenceDiagram -/// tip | Dica +participant client as Cliente +participant dep_req as Dep scope="request" +participant dep_func as Dep scope="function" +participant operation as Operação de Rota -Adicionalmente, uma tarefa de background é, normalmente, um conjunto de lógicas independentes que devem ser manipuladas separadamente, com seus próprios recursos (e.g. sua própria conexão com banco de dados). + client ->> dep_req: Iniciar requisição + Note over dep_req: Executar código até o yield + dep_req ->> dep_func: Passar dependência + Note over dep_func: Executar código até o yield + dep_func ->> operation: Executar operação de rota com dependência + operation ->> dep_func: Retornar da operação de rota + Note over dep_func: Executar código após o yield + Note over dep_func: ✅ Dependência fechada + dep_func ->> client: Enviar resposta ao cliente + Note over client: Resposta enviada + Note over dep_req: Executar código após o yield + Note over dep_req: ✅ Dependência fechada +``` -Então, dessa forma você provavelmente terá um código mais limpo. +## Dependências com `yield`, `HTTPException`, `except` e Tarefas de Background { #dependencies-with-yield-httpexception-except-and-background-tasks } -/// +Dependências com `yield` evoluíram ao longo do tempo para cobrir diferentes casos de uso e corrigir alguns problemas. -Se você costumava depender desse comportamento, agora você precisa criar os recursos para uma tarefa de background dentro dela mesma, e usar internamente apenas dados que não dependam de recursos de dependências com `yield`. +Se você quiser ver o que mudou em diferentes versões do FastAPI, você pode ler mais sobre isso no guia avançado, em [Dependências Avançadas - Dependências com `yield`, `HTTPException`, `except` e Tarefas de Background](../../advanced/advanced-dependencies.md#dependencies-with-yield-httpexception-except-and-background-tasks){.internal-link target=_blank}. +## Gerenciadores de contexto { #context-managers } -Por exemplo, em vez de utilizar a mesma sessão do banco de dados, você criaria uma nova sessão dentro da tarefa de background, e você obteria os objetos do banco de dados utilizando essa nova sessão. E então, em vez de passar o objeto obtido do banco de dados como um parâmetro para a função da tarefa de background, você passaria o ID desse objeto e buscaria ele novamente dentro da função da tarefa de background. - -## Gerenciadores de contexto - -### O que são gerenciadores de contexto +### O que são "Gerenciadores de Contexto" { #what-are-context-managers } "Gerenciadores de Contexto" são qualquer um dos objetos Python que podem ser utilizados com a declaração `with`. @@ -338,9 +254,9 @@ Quando o bloco `with` finaliza, ele se certifica de fechar o arquivo, mesmo que Quando você cria uma dependência com `yield`, o **FastAPI** irá criar um gerenciador de contexto internamente para ela, e combiná-lo com algumas outras ferramentas relacionadas. -### Utilizando gerenciadores de contexto em dependências com `yield` +### Utilizando gerenciadores de contexto em dependências com `yield` { #using-context-managers-in-dependencies-with-yield } -/// warning | Aviso +/// warning | Atenção Isso é uma ideia mais ou menos "avançada". @@ -348,9 +264,10 @@ Se você está apenas iniciando com o **FastAPI** você pode querer pular isso p /// -Em python, você pode criar Gerenciadores de Contexto ao criar uma classe com dois métodos: `__enter__()` e `__exit__()`. +Em Python, você pode criar Gerenciadores de Contexto ao criar uma classe com dois métodos: `__enter__()` e `__exit__()`. -Você também pode usá-los dentro de dependências com `yield` do **FastAPI** ao utilizar `with` ou `async with` dentro da função da dependência: +Você também pode usá-los dentro de dependências com `yield` do **FastAPI** ao utilizar +`with` ou `async with` dentro da função da dependência: {* ../../docs_src/dependencies/tutorial010.py hl[1:9,13] *} @@ -359,7 +276,6 @@ Você também pode usá-los dentro de dependências com `yield` do **FastAPI** a Outra forma de criar um gerenciador de contexto é utilizando: * `@contextlib.contextmanager` ou - * `@contextlib.asynccontextmanager` Para decorar uma função com um único `yield`. diff --git a/docs/pt/docs/tutorial/dependencies/global-dependencies.md b/docs/pt/docs/tutorial/dependencies/global-dependencies.md index a9a7e3b89..cf0f90802 100644 --- a/docs/pt/docs/tutorial/dependencies/global-dependencies.md +++ b/docs/pt/docs/tutorial/dependencies/global-dependencies.md @@ -1,15 +1,16 @@ -# Dependências Globais +# Dependências Globais { #global-dependencies } -Para alguns tipos de aplicação específicos você pode querer adicionar dependências para toda a aplicação. +Para alguns tipos de aplicação você pode querer adicionar dependências para toda a aplicação. -De forma semelhante a [adicionar dependências (`dependencies`) em *decoradores de operação de rota*](dependencies-in-path-operation-decorators.md){.internal-link target=_blank}, você pode adicioná-las à aplicação `FastAPI`. +De forma semelhante a [adicionar `dependencies` aos *decoradores de operação de rota*](dependencies-in-path-operation-decorators.md){.internal-link target=_blank}, você pode adicioná-las à aplicação `FastAPI`. Nesse caso, elas serão aplicadas a todas as *operações de rota* da aplicação: {* ../../docs_src/dependencies/tutorial012_an_py39.py hl[16] *} -E todos os conceitos apresentados na sessão sobre [adicionar dependências em *decoradores de operação de rota*](dependencies-in-path-operation-decorators.md){.internal-link target=_blank} ainda se aplicam, mas nesse caso, para todas as *operações de rota* da aplicação. -## Dependências para conjuntos de *operações de rota* +E todos os conceitos apresentados na seção sobre [adicionar `dependencies` aos *decoradores de operação de rota*](dependencies-in-path-operation-decorators.md){.internal-link target=_blank} ainda se aplicam, mas nesse caso, para todas as *operações de rota* da aplicação. -Mais para a frente, quando você ler sobre como estruturar aplicações maiores ([Bigger Applications - Multiple Files](../../tutorial/bigger-applications.md){.internal-link target=_blank}), possivelmente com múltiplos arquivos, você irá aprender a declarar um único parâmetro `dependencies` para um conjunto de *operações de rota*. +## Dependências para conjuntos de *operações de rota* { #dependencies-for-groups-of-path-operations } + +Mais para a frente, quando você ler sobre como estruturar aplicações maiores ([Aplicações Maiores - Múltiplos Arquivos](../../tutorial/bigger-applications.md){.internal-link target=_blank}), possivelmente com múltiplos arquivos, você irá aprender a declarar um único parâmetro `dependencies` para um conjunto de *operações de rota*. diff --git a/docs/pt/docs/tutorial/dependencies/index.md b/docs/pt/docs/tutorial/dependencies/index.md index 1500b715a..bdfe1ac39 100644 --- a/docs/pt/docs/tutorial/dependencies/index.md +++ b/docs/pt/docs/tutorial/dependencies/index.md @@ -1,10 +1,10 @@ -# Dependências +# Dependências { #dependencies } -O **FastAPI** possui um poderoso, mas intuitivo sistema de **Injeção de Dependência**. +O **FastAPI** possui um poderoso, mas intuitivo sistema de **Injeção de Dependência**. Esse sistema foi pensado para ser fácil de usar, e permitir que qualquer desenvolvedor possa integrar facilmente outros componentes ao **FastAPI**. -## O que é "Injeção de Dependência" +## O que é "Injeção de Dependência" { #what-is-dependency-injection } **"Injeção de Dependência"** no mundo da programação significa, que existe uma maneira de declarar no seu código (nesse caso, suas *funções de operação de rota*) para declarar as coisas que ele precisa para funcionar e que serão utilizadas: "dependências". @@ -19,13 +19,13 @@ Isso é bastante útil quando você precisa: Tudo isso, enquanto minimizamos a repetição de código. -## Primeiros passos +## Primeiros passos { #first-steps } Vamos ver um exemplo simples. Tão simples que não será muito útil, por enquanto. Mas dessa forma podemos focar em como o sistema de **Injeção de Dependência** funciona. -### Criando uma dependência, ou "injetável" +### Criando uma dependência, ou "injetável" { #create-a-dependency-or-dependable } Primeiro vamos focar na dependência. @@ -57,15 +57,15 @@ FastAPI passou a suportar a notação `Annotated` (e começou a recomendá-la) n Se você utiliza uma versão anterior, ocorrerão erros ao tentar utilizar `Annotated`. -Certifique-se de [Atualizar a versão do FastAPI](../../deployment/versions.md#atualizando-as-versoes-do-fastapi){.internal-link target=_blank} para pelo menos 0.95.1 antes de usar `Annotated`. +Certifique-se de [Atualizar a versão do FastAPI](../../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank} para pelo menos 0.95.1 antes de usar `Annotated`. /// -### Importando `Depends` +### Importando `Depends` { #import-depends } {* ../../docs_src/dependencies/tutorial001_an_py310.py hl[3] *} -### Declarando a dependência, no "dependente" +### Declarando a dependência, no "dependente" { #declare-the-dependency-in-the-dependant } Da mesma forma que você utiliza `Body`, `Query`, etc. Como parâmetros de sua *função de operação de rota*, utilize `Depends` com um novo parâmetro: @@ -106,7 +106,7 @@ common_parameters --> read_users Assim, você escreve um código compartilhado apenas uma vez e o **FastAPI** se encarrega de chamá-lo em suas *operações de rota*. -/// check | Checando +/// check | Verifique Perceba que você não precisa criar uma classe especial e enviar a dependência para algum outro lugar em que o **FastAPI** a "registre" ou realize qualquer operação similar. @@ -114,7 +114,7 @@ Você apenas envia para `Depends` e o **FastAPI** sabe como fazer o resto. /// -## Compartilhando dependências `Annotated` +## Compartilhando dependências `Annotated` { #share-annotated-dependencies } Nos exemplos acima, você pode ver que existe uma pequena **duplicação de código**. @@ -140,7 +140,7 @@ As dependências continuarão funcionando como esperado, e a **melhor parte** é Isso é especialmente útil para uma **base de código grande** onde **as mesmas dependências** são utilizadas repetidamente em **muitas *operações de rota***. -## `Async` ou não, eis a questão +## `Async` ou não, eis a questão { #to-async-or-not-to-async } Como as dependências também serão chamadas pelo **FastAPI** (da mesma forma que *funções de operação de rota*), as mesmas regras se aplicam ao definir suas funções. @@ -152,11 +152,11 @@ Não faz diferença. O **FastAPI** sabe o que fazer. /// note | Nota -Caso você não conheça, veja em [Async: *"Com Pressa?"*](../../async.md#com-pressa){.internal-link target=_blank} a sessão acerca de `async` e `await` na documentação. +Caso você não conheça, veja em [Async: *"Com Pressa?"*](../../async.md#in-a-hurry){.internal-link target=_blank} a sessão acerca de `async` e `await` na documentação. /// -## Integrando com OpenAPI +## Integrando com OpenAPI { #integrated-with-openapi } Todas as declarações de requisições, validações e requisitos para suas dependências (e sub-dependências) serão integradas em um mesmo esquema OpenAPI. @@ -164,7 +164,7 @@ Então, a documentação interativa também terá toda a informação sobre essa -## Caso de Uso Simples +## Caso de Uso Simples { #simple-usage } Se você parar para ver, *funções de operação de rota* são declaradas para serem usadas sempre que uma *rota* e uma *operação* se encaixam, e então o **FastAPI** se encarrega de chamar a função correspondente com os argumentos corretos, extraindo os dados da requisição. @@ -182,7 +182,7 @@ Outros termos comuns para essa mesma ideia de "injeção de dependência" são: * injetáveis * componentes -## Plug-ins em **FastAPI** +## Plug-ins em **FastAPI** { #fastapi-plug-ins } Integrações e "plug-ins" podem ser construídos com o sistema de **Injeção de Dependência**. Mas na verdade, **não há necessidade de criar "plug-ins"**, já que utilizando dependências é possível declarar um número infinito de integrações e interações que se tornam disponíveis para as suas *funções de operação de rota*. @@ -190,7 +190,7 @@ E as dependências pode ser criadas de uma forma bastante simples e intuitiva qu Você verá exemplos disso nos próximos capítulos, acerca de bancos de dados relacionais e NoSQL, segurança, etc. -## Compatibilidade do **FastAPI** +## Compatibilidade do **FastAPI** { #fastapi-compatibility } A simplicidade do sistema de injeção de dependência do **FastAPI** faz ele compatível com: @@ -203,7 +203,7 @@ A simplicidade do sistema de injeção de dependência do **FastAPI** faz ele co * sistemas de injeção de dados de resposta * etc. -## Simples e Poderoso +## Simples e Poderoso { #simple-and-powerful } Mesmo que o sistema hierárquico de injeção de dependência seja simples de definir e utilizar, ele ainda é bastante poderoso. @@ -243,7 +243,7 @@ admin_user --> activate_user paying_user --> pro_items ``` -## Integração com **OpenAPI** +## Integração com **OpenAPI** { #integrated-with-openapi_1 } Todas essas dependências, ao declarar os requisitos para suas *operações de rota*, também adicionam parâmetros, validações, etc. diff --git a/docs/pt/docs/tutorial/dependencies/sub-dependencies.md b/docs/pt/docs/tutorial/dependencies/sub-dependencies.md index 3975ce182..fa746d5a1 100644 --- a/docs/pt/docs/tutorial/dependencies/sub-dependencies.md +++ b/docs/pt/docs/tutorial/dependencies/sub-dependencies.md @@ -1,4 +1,4 @@ -# Subdependências +# Subdependências { #sub-dependencies } Você pode criar dependências que possuem **subdependências**. @@ -6,9 +6,9 @@ Elas podem ter o nível de **profundidade** que você achar necessário. O **FastAPI** se encarrega de resolver essas dependências. -## Primeira dependência "injetável" +## Primeira dependência "dependable" { #first-dependency-dependable } -Você pode criar uma primeira dependência (injetável) dessa forma: +Você pode criar uma primeira dependência ("dependable") dessa forma: {* ../../docs_src/dependencies/tutorial005_an_py310.py hl[8:9] *} @@ -16,20 +16,20 @@ Esse código declara um parâmetro de consulta opcional, `q`, com o tipo `str`, Isso é bastante simples (e não muito útil), mas irá nos ajudar a focar em como as subdependências funcionam. -## Segunda dependência, "injetável" e "dependente" +## Segunda dependência, "dependable" e "dependente" { #second-dependency-dependable-and-dependant } -Então, você pode criar uma outra função para uma dependência (um "injetável") que ao mesmo tempo declara sua própria dependência (o que faz dela um "dependente" também): +Então, você pode criar uma outra função para uma dependência (um "dependable") que ao mesmo tempo declara sua própria dependência (o que faz dela um "dependente" também): {* ../../docs_src/dependencies/tutorial005_an_py310.py hl[13] *} Vamos focar nos parâmetros declarados: -* Mesmo que essa função seja uma dependência ("injetável") por si mesma, ela também declara uma outra dependência (ela "depende" de outra coisa). +* Mesmo que essa função seja uma dependência ("dependable") por si mesma, ela também declara uma outra dependência (ela "depende" de outra coisa). * Ela depende do `query_extractor`, e atribui o valor retornado pela função ao parâmetro `q`. * Ela também declara um cookie opcional `last_query`, do tipo `str`. * Se o usuário não passou nenhuma consulta `q`, a última consulta é utilizada, que foi salva em um cookie anteriormente. -## Utilizando a dependência +## Utilizando a dependência { #use-the-dependency } Então podemos utilizar a dependência com: @@ -54,7 +54,7 @@ read_query["/items/"] query_extractor --> query_or_cookie_extractor --> read_query ``` -## Utilizando a mesma dependência múltiplas vezes +## Utilizando a mesma dependência múltiplas vezes { #using-the-same-dependency-multiple-times } Se uma de suas dependências é declarada várias vezes para a mesma *operação de rota*, por exemplo, múltiplas dependências com uma mesma subdependência, o **FastAPI** irá chamar essa subdependência uma única vez para cada requisição. @@ -86,7 +86,7 @@ async def needy_dependency(fresh_value: str = Depends(get_value, use_cache=False //// -## Recapitulando +## Recapitulando { #recap } Com exceção de todas as palavras complicadas usadas aqui, o sistema de **Injeção de Dependência** é bastante simples. diff --git a/docs/pt/docs/tutorial/encoder.md b/docs/pt/docs/tutorial/encoder.md index 87c6322e1..b3b1b69bc 100644 --- a/docs/pt/docs/tutorial/encoder.md +++ b/docs/pt/docs/tutorial/encoder.md @@ -1,4 +1,4 @@ -# Codificador Compatível com JSON +# Codificador Compatível com JSON { #json-compatible-encoder } Existem alguns casos em que você pode precisar converter um tipo de dados (como um modelo Pydantic) para algo compatível com JSON (como um `dict`, `list`, etc). @@ -6,13 +6,13 @@ Por exemplo, se você precisar armazená-lo em um banco de dados. Para isso, **FastAPI** fornece uma função `jsonable_encoder()`. -## Usando a função `jsonable_encoder` +## Usando a função `jsonable_encoder` { #using-the-jsonable-encoder } Vamos imaginar que você tenha um banco de dados `fake_db` que recebe apenas dados compatíveis com JSON. Por exemplo, ele não recebe objetos `datetime`, pois estes objetos não são compatíveis com JSON. -Então, um objeto `datetime` teria que ser convertido em um `str` contendo os dados no formato ISO. +Então, um objeto `datetime` teria que ser convertido em um `str` contendo os dados no formato ISO. Da mesma forma, este banco de dados não receberia um modelo Pydantic (um objeto com atributos), apenas um `dict`. diff --git a/docs/pt/docs/tutorial/extra-data-types.md b/docs/pt/docs/tutorial/extra-data-types.md index 09c838be0..97e4cc475 100644 --- a/docs/pt/docs/tutorial/extra-data-types.md +++ b/docs/pt/docs/tutorial/extra-data-types.md @@ -1,4 +1,4 @@ -# Tipos de dados extras +# Tipos de dados extras { #extra-data-types } Até agora, você tem usado tipos de dados comuns, tais como: @@ -17,7 +17,7 @@ E você ainda terá os mesmos recursos que viu até agora: * Validação de dados. * Anotação e documentação automáticas. -## Outros tipos de dados +## Outros tipos de dados { #other-data-types } Aqui estão alguns dos tipos de dados adicionais que você pode usar: @@ -36,7 +36,7 @@ Aqui estão alguns dos tipos de dados adicionais que você pode usar: * `datetime.timedelta`: * O `datetime.timedelta` do Python. * Em requisições e respostas será representado como um `float` de segundos totais. - * O Pydantic também permite representá-lo como uma "codificação ISO 8601 diferença de tempo", cheque a documentação para mais informações. + * O Pydantic também permite representá-lo como uma "codificação ISO 8601 diferença de tempo", cheque a documentação para mais informações. * `frozenset`: * Em requisições e respostas, será tratado da mesma forma que um `set`: * Nas requisições, uma lista será lida, eliminando duplicadas e convertendo-a em um `set`. @@ -49,14 +49,14 @@ Aqui estão alguns dos tipos de dados adicionais que você pode usar: * `Decimal`: * O `Decimal` padrão do Python. * Em requisições e respostas será representado como um `float`. -* Você pode checar todos os tipos de dados válidos do Pydantic aqui: Tipos de dados do Pydantic. +* Você pode checar todos os tipos de dados válidos do Pydantic aqui: Tipos de dados do Pydantic. -## Exemplo +## Exemplo { #example } Aqui está um exemplo de *operação de rota* com parâmetros utilizando-se de alguns dos tipos acima. -{* ../../docs_src/extra_data_types/tutorial001.py hl[1,3,12:16] *} +{* ../../docs_src/extra_data_types/tutorial001_an_py310.py hl[1,3,12:16] *} Note que os parâmetros dentro da função tem seu tipo de dados natural, e você pode, por exemplo, realizar manipulações normais de data, como: -{* ../../docs_src/extra_data_types/tutorial001.py hl[18:19] *} +{* ../../docs_src/extra_data_types/tutorial001_an_py310.py hl[18:19] *} diff --git a/docs/pt/docs/tutorial/extra-models.md b/docs/pt/docs/tutorial/extra-models.md index cccef16e3..c0d22df57 100644 --- a/docs/pt/docs/tutorial/extra-models.md +++ b/docs/pt/docs/tutorial/extra-models.md @@ -1,4 +1,4 @@ -# Modelos Adicionais +# Modelos Adicionais { #extra-models } Continuando com o exemplo anterior, será comum ter mais de um modelo relacionado. @@ -6,9 +6,9 @@ Isso é especialmente o caso para modelos de usuários, porque: * O **modelo de entrada** precisa ser capaz de ter uma senha. * O **modelo de saída** não deve ter uma senha. -* O **modelo de banco de dados** provavelmente precisaria ter uma senha criptografada. +* O **modelo de banco de dados** provavelmente precisaria ter uma senha com hash. -/// danger +/// danger | Cuidado Nunca armazene senhas em texto simples dos usuários. Sempre armazene uma "hash segura" que você pode verificar depois. @@ -16,15 +16,23 @@ Se não souber, você aprenderá o que é uma "senha hash" nos [capítulos de se /// -## Múltiplos modelos +## Múltiplos modelos { #multiple-models } Aqui está uma ideia geral de como os modelos poderiam parecer com seus campos de senha e os lugares onde são usados: -{* ../../docs_src/extra_models/tutorial001.py hl[9,11,16,22,24,29:30,33:35,40:41] *} +{* ../../docs_src/extra_models/tutorial001_py310.py hl[7,9,14,20,22,27:28,31:33,38:39] *} -### Sobre `**user_in.dict()` +/// info | Informação -#### O `.dict()` do Pydantic +No Pydantic v1 o método se chamava `.dict()`, ele foi descontinuado (mas ainda é suportado) no Pydantic v2 e renomeado para `.model_dump()`. + +Os exemplos aqui usam `.dict()` por compatibilidade com o Pydantic v1, mas você deve usar `.model_dump()` se puder usar o Pydantic v2. + +/// + +### Sobre `**user_in.dict()` { #about-user-in-dict } + +#### O `.dict()` do Pydantic { #pydantics-dict } `user_in` é um modelo Pydantic da classe `UserIn`. @@ -61,7 +69,7 @@ teríamos um `dict` Python com: } ``` -#### Desembrulhando um `dict` +#### Desembrulhando um `dict` { #unpacking-a-dict } Se tomarmos um `dict` como `user_dict` e passarmos para uma função (ou classe) com `**user_dict`, o Python irá "desembrulhá-lo". Ele passará as chaves e valores do `user_dict` diretamente como argumentos chave-valor. @@ -93,7 +101,7 @@ UserInDB( ) ``` -#### Um modelo Pydantic a partir do conteúdo de outro +#### Um modelo Pydantic a partir do conteúdo de outro { #a-pydantic-model-from-the-contents-of-another } Como no exemplo acima, obtivemos o `user_dict` a partir do `user_in.dict()`, este código: @@ -108,11 +116,11 @@ seria equivalente a: UserInDB(**user_in.dict()) ``` -...porque `user_in.dict()` é um `dict`, e depois fazemos o Python "desembrulhá-lo" passando-o para UserInDB precedido por `**`. +...porque `user_in.dict()` é um `dict`, e depois fazemos o Python "desembrulhá-lo" passando-o para `UserInDB` precedido por `**`. Então, obtemos um modelo Pydantic a partir dos dados em outro modelo Pydantic. -#### Desembrulhando um `dict` e palavras-chave extras +#### Desembrulhando um `dict` e palavras-chave extras { #unpacking-a-dict-and-extra-keywords } E, então, adicionando o argumento de palavra-chave extra `hashed_password=hashed_password`, como em: @@ -132,13 +140,13 @@ UserInDB( ) ``` -/// warning +/// warning | Atenção -As funções adicionais de suporte são apenas para demonstração de um fluxo possível dos dados, mas é claro que elas não fornecem segurança real. +As funções adicionais de suporte `fake_password_hasher` e `fake_save_user` servem apenas para demonstrar um fluxo possível dos dados, mas é claro que elas não fornecem segurança real. /// -## Reduzir duplicação +## Reduzir duplicação { #reduce-duplication } Reduzir a duplicação de código é uma das ideias principais no **FastAPI**. @@ -154,25 +162,25 @@ Toda conversão de dados, validação, documentação, etc. ainda funcionará no Dessa forma, podemos declarar apenas as diferenças entre os modelos (com `password` em texto claro, com `hashed_password` e sem senha): -{* ../../docs_src/extra_models/tutorial002.py hl[9,15:16,19:20,23:24] *} +{* ../../docs_src/extra_models/tutorial002_py310.py hl[7,13:14,17:18,21:22] *} -## `Union` ou `anyOf` +## `Union` ou `anyOf` { #union-or-anyof } -Você pode declarar uma resposta como o `Union` de dois tipos, o que significa que a resposta seria qualquer um dos dois. +Você pode declarar uma resposta como o `Union` de dois ou mais tipos, o que significa que a resposta seria qualquer um deles. Isso será definido no OpenAPI com `anyOf`. -Para fazer isso, use a dica de tipo padrão do Python `typing.Union`: +Para fazer isso, use a anotação de tipo padrão do Python `typing.Union`: -/// note +/// note | Nota Ao definir um `Union`, inclua o tipo mais específico primeiro, seguido pelo tipo menos específico. No exemplo abaixo, o tipo mais específico `PlaneItem` vem antes de `CarItem` em `Union[PlaneItem, CarItem]`. /// -{* ../../docs_src/extra_models/tutorial003.py hl[1,14:15,18:20,33] *} +{* ../../docs_src/extra_models/tutorial003_py310.py hl[1,14:15,18:20,33] *} -### `Union` no Python 3.10 +### `Union` no Python 3.10 { #union-in-python-3-10 } Neste exemplo, passamos `Union[PlaneItem, CarItem]` como o valor do argumento `response_model`. @@ -184,27 +192,27 @@ Se estivesse em uma anotação de tipo, poderíamos ter usado a barra vertical, some_variable: PlaneItem | CarItem ``` -Mas se colocarmos isso em `response_model=PlaneItem | CarItem` teríamos um erro, pois o Python tentaria executar uma **operação inválida** entre `PlaneItem` e `CarItem` em vez de interpretar isso como uma anotação de tipo. +Mas se colocarmos isso na atribuição `response_model=PlaneItem | CarItem`, teríamos um erro, pois o Python tentaria executar uma **operação inválida** entre `PlaneItem` e `CarItem` em vez de interpretar isso como uma anotação de tipo. -## Lista de modelos +## Lista de modelos { #list-of-models } Da mesma forma, você pode declarar respostas de listas de objetos. Para isso, use o padrão Python `typing.List` (ou simplesmente `list` no Python 3.9 e superior): -{* ../../docs_src/extra_models/tutorial004.py hl[1,20] *} +{* ../../docs_src/extra_models/tutorial004_py39.py hl[18] *} -## Resposta com `dict` arbitrário +## Resposta com `dict` arbitrário { #response-with-arbitrary-dict } Você também pode declarar uma resposta usando um simples `dict` arbitrário, declarando apenas o tipo das chaves e valores, sem usar um modelo Pydantic. Isso é útil se você não souber os nomes de campo / atributo válidos (que seriam necessários para um modelo Pydantic) antecipadamente. -Neste caso, você pode usar `typing.Dict` (ou simplesmente dict no Python 3.9 e superior): +Neste caso, você pode usar `typing.Dict` (ou simplesmente `dict` no Python 3.9 e superior): -{* ../../docs_src/extra_models/tutorial005.py hl[1,8] *} +{* ../../docs_src/extra_models/tutorial005_py39.py hl[6] *} -## Em resumo +## Recapitulação { #recap } Use vários modelos Pydantic e herde livremente para cada caso. diff --git a/docs/pt/docs/tutorial/first-steps.md b/docs/pt/docs/tutorial/first-steps.md index e696bbbb7..32d286fb2 100644 --- a/docs/pt/docs/tutorial/first-steps.md +++ b/docs/pt/docs/tutorial/first-steps.md @@ -1,4 +1,4 @@ -# Primeiros Passos +# Primeiros Passos { #first-steps } O arquivo FastAPI mais simples pode se parecer com: @@ -48,15 +48,15 @@ $ fastapi dev http://127.0.0.1:8000. @@ -66,7 +66,7 @@ Você verá essa resposta em JSON: {"message": "Hello World"} ``` -### Documentação Interativa de APIs +### Documentação Interativa de APIs { #interactive-api-docs } Agora vá para http://127.0.0.1:8000/docs. @@ -74,7 +74,7 @@ Você verá a documentação interativa automática da API (fornecida por http://127.0.0.1:8000/redoc. @@ -82,31 +82,31 @@ Você verá a documentação alternativa automática (fornecida por OpenAPI é uma especificação que determina como definir um *schema* da sua API. -Esta definição de *schema* inclui as rotas da sua API, os parâmetros possíveis que elas usam, etc. +Esta definição de *schema* inclui os paths da sua API, os parâmetros possíveis que eles usam, etc. -#### "*Schema*" de dados +#### "*Schema*" de dados { #data-schema } O termo "*schema*" também pode se referir à forma de alguns dados, como um conteúdo JSON. Nesse caso, significaria os atributos JSON e os tipos de dados que eles possuem, etc. -#### OpenAPI e JSON *Schema* +#### OpenAPI e JSON Schema { #openapi-and-json-schema } -OpenAPI define um *schema* de API para sua API. E esse *schema* inclui definições (ou "*schemas*") dos dados enviados e recebidos por sua API usando **JSON *Schema***, o padrão para *schemas* de dados JSON. +OpenAPI define um *schema* de API para sua API. E esse *schema* inclui definições (ou "*schemas*") dos dados enviados e recebidos por sua API usando **JSON Schema**, o padrão para *schemas* de dados JSON. -#### Verifique o `openapi.json` +#### Verifique o `openapi.json` { #check-the-openapi-json } Se você está curioso(a) sobre a aparência do *schema* bruto OpenAPI, o FastAPI gera automaticamente um JSON (*schema*) com as descrições de toda a sua API. @@ -116,7 +116,7 @@ Ele mostrará um JSON começando com algo como: ```JSON { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": { "title": "FastAPI", "version": "0.1.0" @@ -135,7 +135,7 @@ Ele mostrará um JSON começando com algo como: ... ``` -#### Para que serve o OpenAPI +#### Para que serve o OpenAPI { #what-is-openapi-for } O *schema* OpenAPI é o que possibilita os dois sistemas de documentação interativos mostrados. @@ -143,15 +143,15 @@ E existem dezenas de alternativas, todas baseadas em OpenAPI. Você pode facilme Você também pode usá-lo para gerar código automaticamente para clientes que se comunicam com sua API. Por exemplo, aplicativos front-end, móveis ou IoT. -## Recapitulando, passo a passo +## Recapitulando, passo a passo { #recap-step-by-step } -### Passo 1: importe `FastAPI` +### Passo 1: importe `FastAPI` { #step-1-import-fastapi } {* ../../docs_src/first_steps/tutorial001.py hl[1] *} `FastAPI` é uma classe Python que fornece todas as funcionalidades para sua API. -/// note | Detalhes técnicos +/// note | Detalhes Técnicos `FastAPI` é uma classe que herda diretamente de `Starlette`. @@ -159,7 +159,7 @@ Você pode usar todas as funcionalidades do operador get +* o path `/` +* usando uma operação get -/// info | `@decorador` +/// info | Informações sobre `@decorator` Essa sintaxe `@alguma_coisa` em Python é chamada de "decorador". @@ -245,9 +245,9 @@ Você o coloca em cima de uma função. Como um chapéu decorativo (acho que é Um "decorador" pega a função abaixo e faz algo com ela. -Em nosso caso, este decorador informa ao **FastAPI** que a função abaixo corresponde a **rota** `/` com uma **operação** `get`. +Em nosso caso, este decorador informa ao **FastAPI** que a função abaixo corresponde ao **path** `/` com uma **operação** `get`. -É o "**decorador de rota**". +É o "**decorador de operação de rota**". /// @@ -276,11 +276,11 @@ Por exemplo, ao usar GraphQL, você normalmente executa todas as ações usando /// -### Passo 4: defina uma **função de rota** +### Passo 4: defina a função de operação de rota { #step-4-define-the-path-operation-function } -Esta é a nossa "**função de rota**": +Esta é a nossa "**função de operação de rota**": -* **rota**: é `/`. +* **path**: é `/`. * **operação**: é `get`. * **função**: é a função abaixo do "decorador" (abaixo do `@app.get("/")`). @@ -288,9 +288,9 @@ Esta é a nossa "**função de rota**": Esta é uma função Python. -Ela será chamada pelo **FastAPI** sempre que receber uma requisição para a URL "`/ `" usando uma operação `GET`. +Ela será chamada pelo **FastAPI** sempre que receber uma requisição para a URL "`/`" usando uma operação `GET`. -Neste caso, é uma função `assíncrona`. +Neste caso, é uma função `async`. --- @@ -300,11 +300,11 @@ Você também pode defini-la como uma função normal em vez de `async def`: /// note | Nota -Se você não sabe a diferença, verifique o [Async: *"Com pressa?"*](../async.md#com-pressa){.internal-link target=_blank}. +Se você não sabe a diferença, verifique o [Async: *"Com pressa?"*](../async.md#in-a-hurry){.internal-link target=_blank}. /// -### Passo 5: retorne o conteúdo +### Passo 5: retorne o conteúdo { #step-5-return-the-content } {* ../../docs_src/first_steps/tutorial001.py hl[8] *} @@ -314,10 +314,10 @@ Você também pode devolver modelos Pydantic (você verá mais sobre isso mais t Existem muitos outros objetos e modelos que serão convertidos automaticamente para JSON (incluindo ORMs, etc). Tente usar seus favoritos, é altamente provável que já sejam compatíveis. -## Recapitulando +## Recapitulando { #recap } * Importe `FastAPI`. * Crie uma instância do `app`. -* Coloque o **decorador que define a operação** (como `@app.get("/")`). -* Escreva uma **função para a operação da rota** (como `def root(): ...`) abaixo. -* Execute o servidor de desenvolvimento (como `uvicorn main:app --reload`). +* Escreva um **decorador de operação de rota** usando decoradores como `@app.get("/")`. +* Defina uma **função de operação de rota**; por exemplo, `def root(): ...`. +* Execute o servidor de desenvolvimento usando o comando `fastapi dev`. diff --git a/docs/pt/docs/tutorial/handling-errors.md b/docs/pt/docs/tutorial/handling-errors.md index 5cb92c744..a2cfcf963 100644 --- a/docs/pt/docs/tutorial/handling-errors.md +++ b/docs/pt/docs/tutorial/handling-errors.md @@ -1,4 +1,4 @@ -# Manipulação de erros +# Manipulação de erros { #handling-errors } Há diversas situações em que você precisa notificar um erro a um cliente que está utilizando a sua API. @@ -20,15 +20,15 @@ Os status codes na faixa dos 400 significam que houve um erro por parte do clien Você se lembra de todos aqueles erros (e piadas) a respeito do "**404 Not Found**"? -## Use o `HTTPException` +## Use o `HTTPException` { #use-httpexception } Para retornar ao cliente *responses* HTTP com erros, use o `HTTPException`. -### Import `HTTPException` +### Import `HTTPException` { #import-httpexception } {* ../../docs_src/handling_errors/tutorial001.py hl[1] *} -### Lance o `HTTPException` no seu código. +### Lance o `HTTPException` no seu código. { #raise-an-httpexception-in-your-code } `HTTPException`, ao fundo, nada mais é do que a conjunção entre uma exceção comum do Python e informações adicionais relevantes para APIs. @@ -42,13 +42,12 @@ Neste exemplo, quando o cliente pede, na requisição, por um item cujo ID não {* ../../docs_src/handling_errors/tutorial001.py hl[11] *} -### A response resultante - +### A response resultante { #the-resulting-response } Se o cliente faz uma requisição para `http://example.com/items/foo` (um `item_id` `"foo"`), esse cliente receberá um HTTP status code 200, e uma resposta JSON: -``` +```JSON { "item": "The Foo Wrestlers" } @@ -71,7 +70,7 @@ Esses tipos de dados são manipulados automaticamente pelo **FastAPI** e convert /// -## Adicione headers customizados +## Adicione headers customizados { #add-custom-headers } Há certas situações em que é bastante útil poder adicionar headers customizados no HTTP error. Exemplo disso seria adicionar headers customizados para tipos de segurança. @@ -81,7 +80,7 @@ Mas caso você precise, para um cenário mais complexo, você pode adicionar hea {* ../../docs_src/handling_errors/tutorial002.py hl[14] *} -## Instalando manipuladores de exceções customizados +## Instale manipuladores de exceções customizados { #install-custom-exception-handlers } Você pode adicionar manipuladores de exceção customizados com a mesma seção de utilidade de exceções presentes no Starlette @@ -109,7 +108,7 @@ Você também pode usar `from starlette.requests import Request` and `from starl /// -## Sobrescreva o manipulador padrão de exceções +## Sobrescreva os manipuladores de exceções padrão { #override-the-default-exception-handlers } **FastAPI** tem alguns manipuladores padrão de exceções. @@ -117,12 +116,16 @@ Esses manipuladores são os responsáveis por retornar o JSON padrão de respost Você pode sobrescrever esses manipuladores de exceção com os seus próprios manipuladores. -## Sobrescreva exceções de validação da requisição +### Sobrescreva exceções de validação da requisição { #override-request-validation-exceptions } Quando a requisição contém dados inválidos, **FastAPI** internamente lança para o `RequestValidationError`. +E também inclui um manipulador de exceções padrão para ele. + Para sobrescrevê-lo, importe o `RequestValidationError` e use-o com o `@app.exception_handler(RequestValidationError)` para decorar o manipulador de exceções. +O manipulador de exceções receberá um `Request` e a exceção. + {* ../../docs_src/handling_errors/tutorial004.py hl[2,14:16] *} Se você for ao `/items/foo`, em vez de receber o JSON padrão com o erro: @@ -150,15 +153,15 @@ path -> item_id value is not a valid integer (type=type_error.integer) ``` -### `RequestValidationError` vs `ValidationError` +#### `RequestValidationError` vs `ValidationError` { #requestvalidationerror-vs-validationerror } -/// warning | Aviso +/// warning | Atenção Você pode pular estes detalhes técnicos caso eles não sejam importantes para você neste momento. /// -`RequestValidationError` é uma subclasse do `ValidationError` existente no Pydantic. +`RequestValidationError` é uma subclasse do `ValidationError` existente no Pydantic. **FastAPI** faz uso dele para que você veja o erro no seu log, caso você utilize um modelo de Pydantic em `response_model`, e seus dados tenham erro. @@ -168,6 +171,8 @@ E assim deve ser porque seria um bug no seu código ter o `ValidationError` do P E enquanto você conserta o bug, os clientes / usuários não deveriam ter acesso às informações internas do erro, porque, desse modo, haveria exposição de uma vulnerabilidade de segurança. +### Sobrescreva o manipulador de erro `HTTPException` { #override-the-httpexception-error-handler } + Do mesmo modo, você pode sobreescrever o `HTTPException`. Por exemplo, você pode querer retornar uma *response* em *plain text* ao invés de um JSON para os seguintes erros: @@ -182,12 +187,14 @@ Você pode usar `from starlette.responses import PlainTextResponse`. /// -### Use o body do `RequestValidationError`. +### Use o body do `RequestValidationError`. { #use-the-requestvalidationerror-body } O `RequestValidationError` contém o `body` que ele recebeu de dados inválidos. Você pode utilizá-lo enquanto desenvolve seu app para conectar o *body* e debugá-lo, e assim retorná-lo ao usuário, etc. +{* ../../docs_src/handling_errors/tutorial005.py hl[14] *} + Tente enviar um item inválido como este: ```JSON @@ -197,7 +204,7 @@ Tente enviar um item inválido como este: } ``` -Você receberá uma *response* informando-o de que a data é inválida, e contendo o *body* recebido: +Você receberá uma *response* informando-o de que os dados são inválidos, e contendo o *body* recebido: ```JSON hl_lines="12-15" { @@ -218,27 +225,27 @@ Você receberá uma *response* informando-o de que a data é inválida, e conten } ``` -#### O `HTTPException` do FastAPI vs o `HTTPException` do Starlette. +#### O `HTTPException` do FastAPI vs o `HTTPException` do Starlette { #fastapis-httpexception-vs-starlettes-httpexception } O **FastAPI** tem o seu próprio `HTTPException`. E a classe de erro `HTTPException` do **FastAPI** herda da classe de erro do `HTTPException` do Starlette. -A diferença entre os dois é a de que o `HTTPException` do **FastAPI** permite que você adicione *headers* que serão incluídos nas *responses*. - -Esses *headers* são necessários/utilizados internamente pelo OAuth 2.0 e também por outras utilidades de segurança. +A única diferença é que o `HTTPException` do **FastAPI** aceita qualquer dado que possa ser convertido em JSON para o campo `detail`, enquanto o `HTTPException` do Starlette aceita apenas strings para esse campo. Portanto, você pode continuar lançando o `HTTPException` do **FastAPI** normalmente no seu código. Porém, quando você registrar um manipulador de exceção, você deve registrá-lo através do `HTTPException` do Starlette. -Dessa forma, se qualquer parte do código interno, extensão ou plug-in do Starlette lançar o `HTTPException`, o seu manipulador de exceção poderá capturar esse lançamento e tratá-lo. +Dessa forma, se qualquer parte do código interno, extensão ou plug-in do Starlette lançar um `HTTPException` do Starlette, o seu manipulador poderá capturar e tratá-lo. + +Neste exemplo, para poder ter ambos os `HTTPException` no mesmo código, a exceção do Starlette é renomeada para `StarletteHTTPException`: ```Python from starlette.exceptions import HTTPException as StarletteHTTPException ``` -### Re-use os manipulares de exceção do **FastAPI** +### Reutilize os manipuladores de exceção do **FastAPI** { #reuse-fastapis-exception-handlers } Se você quer usar a exceção em conjunto com o mesmo manipulador de exceção *default* do **FastAPI**, você pode importar e re-usar esses manipuladores de exceção do `fastapi.exception_handlers`: diff --git a/docs/pt/docs/tutorial/header-param-models.md b/docs/pt/docs/tutorial/header-param-models.md index 9a88dbfec..046c99c29 100644 --- a/docs/pt/docs/tutorial/header-param-models.md +++ b/docs/pt/docs/tutorial/header-param-models.md @@ -1,8 +1,8 @@ -# Modelos de Parâmetros do Cabeçalho +# Modelos de Parâmetros do Cabeçalho { #header-parameter-models } Se você possui um grupo de **parâmetros de cabeçalho** relacionados, você pode criar um **modelo do Pydantic** para declará-los. -Isso vai lhe permitir **reusar o modelo** em **múltiplos lugares** e também declarar validações e metadadados para todos os parâmetros de uma vez. 😎 +Isso vai lhe permitir **reusar o modelo** em **múltiplos lugares** e também declarar validações e metadados para todos os parâmetros de uma vez. 😎 /// note | Nota @@ -10,7 +10,7 @@ Isso é possível desde a versão `0.115.0` do FastAPI. 🤓 /// -## Parâmetros do Cabeçalho com um Modelo Pydantic +## Parâmetros do Cabeçalho com um Modelo Pydantic { #header-parameters-with-a-pydantic-model } Declare os **parâmetros de cabeçalho** que você precisa em um **modelo do Pydantic**, e então declare o parâmetro como `Header`: @@ -18,7 +18,7 @@ Declare os **parâmetros de cabeçalho** que você precisa em um **modelo do Pyd O **FastAPI** irá **extrair** os dados de **cada campo** a partir dos **cabeçalhos** da requisição e te retornará o modelo do Pydantic que você definiu. -### Checando a documentação +## Checando a documentação { #check-the-docs } Você pode ver os headers necessários na interface gráfica da documentação em `/docs`: @@ -26,7 +26,7 @@ Você pode ver os headers necessários na interface gráfica da documentação e
-### Proibindo Cabeçalhos adicionais +## Proibindo Cabeçalhos adicionais { #forbid-extra-headers } Em alguns casos de uso especiais (provavelmente não muito comuns), você pode querer **restringir** os cabeçalhos que você quer receber. @@ -51,6 +51,22 @@ Por exemplo, se o cliente tentar enviar um cabeçalho `tool` com o valor `plumbu } ``` -## Resumo +## Desativar conversão de underscores { #disable-convert-underscores } + +Da mesma forma que com parâmetros de cabeçalho normais, quando você tem caracteres de sublinhado nos nomes dos parâmetros, eles são **automaticamente convertidos em hifens**. + +Por exemplo, se você tem um parâmetro de cabeçalho `save_data` no código, o cabeçalho HTTP esperado será `save-data`, e ele aparecerá assim na documentação. + +Se por algum motivo você precisar desativar essa conversão automática, também poderá fazê-lo para modelos do Pydantic para parâmetros de cabeçalho. + +{* ../../docs_src/header_param_models/tutorial003_an_py310.py hl[19] *} + +/// warning | Atenção + +Antes de definir `convert_underscores` como `False`, tenha em mente que alguns proxies e servidores HTTP não permitem o uso de cabeçalhos com sublinhados. + +/// + +## Resumo { #summary } Você pode utilizar **modelos do Pydantic** para declarar **cabeçalhos** no **FastAPI**. 😎 diff --git a/docs/pt/docs/tutorial/header-params.md b/docs/pt/docs/tutorial/header-params.md index d071bcc35..a0deb98be 100644 --- a/docs/pt/docs/tutorial/header-params.md +++ b/docs/pt/docs/tutorial/header-params.md @@ -1,20 +1,20 @@ -# Parâmetros de Cabeçalho +# Parâmetros de Cabeçalho { #header-parameters } Você pode definir parâmetros de Cabeçalho da mesma maneira que define paramêtros com `Query`, `Path` e `Cookie`. -## importe `Header` +## Importe `Header` { #import-header } Primeiro importe `Header`: -{* ../../docs_src/header_params/tutorial001_py310.py hl[1] *} +{* ../../docs_src/header_params/tutorial001_an_py310.py hl[3] *} -## Declare parâmetros de `Header` +## Declare parâmetros de `Header` { #declare-header-parameters } Então declare os paramêtros de cabeçalho usando a mesma estrutura que em `Path`, `Query` e `Cookie`. O primeiro valor é o valor padrão, você pode passar todas as validações adicionais ou parâmetros de anotação: -{* ../../docs_src/header_params/tutorial001_py310.py hl[7] *} +{* ../../docs_src/header_params/tutorial001_an_py310.py hl[9] *} /// note | Detalhes Técnicos @@ -24,13 +24,13 @@ Mas lembre-se que quando você importa `Query`, `Path`, `Header`, e outras de `f /// -/// info +/// info | Informação Para declarar headers, você precisa usar `Header`, caso contrário, os parâmetros seriam interpretados como parâmetros de consulta. /// -## Conversão automática +## Conversão automática { #automatic-conversion } `Header` tem algumas funcionalidades a mais em relação a `Path`, `Query` e `Cookie`. @@ -46,15 +46,15 @@ Portanto, você pode usar `user_agent` como faria normalmente no código Python, Se por algum motivo você precisar desabilitar a conversão automática de sublinhados para hífens, defina o parâmetro `convert_underscores` de `Header` para `False`: -{* ../../docs_src/header_params/tutorial002_py310.py hl[8] *} +{* ../../docs_src/header_params/tutorial002_an_py310.py hl[10] *} -/// warning | Aviso +/// warning | Atenção Antes de definir `convert_underscores` como `False`, lembre-se de que alguns proxies e servidores HTTP não permitem o uso de cabeçalhos com sublinhados. /// -## Cabeçalhos duplicados +## Cabeçalhos duplicados { #duplicate-headers } É possível receber cabeçalhos duplicados. Isso significa, o mesmo cabeçalho com vários valores. @@ -64,9 +64,9 @@ Você receberá todos os valores do cabeçalho duplicado como uma `list` Python. Por exemplo, para declarar um cabeçalho de `X-Token` que pode aparecer mais de uma vez, você pode escrever: -{* ../../docs_src/header_params/tutorial003_py310.py hl[7] *} +{* ../../docs_src/header_params/tutorial003_an_py310.py hl[9] *} -Se você se comunicar com essa *operação de caminho* enviando dois cabeçalhos HTTP como: +Se você se comunicar com essa *operação de rota* enviando dois cabeçalhos HTTP como: ``` X-Token: foo @@ -84,8 +84,8 @@ A resposta seria como: } ``` -## Recapitulando +## Recapitulando { #recap } Declare cabeçalhos com `Header`, usando o mesmo padrão comum que utiliza-se em `Query`, `Path` e `Cookie`. -E não se preocupe com sublinhados em suas variáveis, FastAPI cuidará da conversão deles. +E não se preocupe com sublinhados em suas variáveis, **FastAPI** cuidará da conversão deles. diff --git a/docs/pt/docs/tutorial/index.md b/docs/pt/docs/tutorial/index.md index 7c04b17f2..cd7dd88fe 100644 --- a/docs/pt/docs/tutorial/index.md +++ b/docs/pt/docs/tutorial/index.md @@ -1,4 +1,4 @@ -# Tutorial - Guia de Usuário +# Tutorial - Guia de Usuário { #tutorial-user-guide } Esse tutorial mostra como usar o **FastAPI** com a maior parte de seus recursos, passo a passo. @@ -6,11 +6,11 @@ Cada seção constrói, gradualmente, sobre as anteriores, mas sua estrutura sã Ele também foi construído para servir como uma referência futura, então você pode voltar e ver exatamente o que você precisa. -## Rode o código +## Rode o código { #run-the-code } Todos os blocos de código podem ser copiados e utilizados diretamente (eles são, na verdade, arquivos Python testados). -Para rodar qualquer um dos exemplos, copie o codigo para um arquivo `main.py`, e inicie o `uvivorn` com: +Para rodar qualquer um dos exemplos, copie o código para um arquivo `main.py`, e inicie o `fastapi dev` com:
@@ -54,15 +54,15 @@ $ fastapi dev @@ -76,17 +76,19 @@ $ pip install "fastapi[standard]" /// note | Nota -Quando você instala com pip install "fastapi[standard]", ele vem com algumas dependências opcionais padrão. +Quando você instala com `pip install "fastapi[standard]"`, ele vem com algumas dependências opcionais padrão, incluindo `fastapi-cloud-cli`, que permite fazer deploy na FastAPI Cloud. -Se você não quiser ter essas dependências opcionais, pode instalar pip install fastapi em vez disso. +Se você não quiser ter essas dependências opcionais, pode instalar `pip install fastapi` em vez disso. + +Se você quiser instalar as dependências padrão, mas sem o `fastapi-cloud-cli`, você pode instalar com `pip install "fastapi[standard-no-fastapi-cloud-cli]"`. /// -## Guia Avançado de Usuário +## Guia Avançado de Usuário { #advanced-user-guide } Há também um **Guia Avançado de Usuário** que você pode ler após esse **Tutorial - Guia de Usuário**. -O **Guia Avançado de Usuário** constrói sobre esse, usa os mesmos conceitos e te ensina alguns recursos extras. +O **Guia Avançado de Usuário** constrói sobre esse, usa os mesmos conceitos e te ensina algumas funcionalidades extras. Mas você deveria ler primeiro o **Tutorial - Guia de Usuário** (que você está lendo agora). diff --git a/docs/pt/docs/tutorial/metadata.md b/docs/pt/docs/tutorial/metadata.md index 57effb3ff..06b2e6b9e 100644 --- a/docs/pt/docs/tutorial/metadata.md +++ b/docs/pt/docs/tutorial/metadata.md @@ -1,8 +1,8 @@ -# Metadados e Urls de Documentos +# Metadados e Urls de Documentos { #metadata-and-docs-urls } Você pode personalizar várias configurações de metadados na sua aplicação **FastAPI**. -## Metadados para API +## Metadados para API { #metadata-for-api } Você pode definir os seguintes campos que são usados na especificação OpenAPI e nas interfaces automáticas de documentação da API: @@ -30,7 +30,7 @@ Com essa configuração, a documentação automática da API se pareceria com: -## Identificador de Licença +## Identificador de Licença { #license-identifier } Desde o OpenAPI 3.1.0 e FastAPI 0.99.0, você também pode definir o license_info com um identifier em vez de uma url. @@ -38,7 +38,7 @@ Por exemplo: {* ../../docs_src/metadata/tutorial001_1.py hl[31] *} -## Metadados para tags +## Metadados para tags { #metadata-for-tags } Você também pode adicionar metadados adicionais para as diferentes tags usadas para agrupar suas operações de rota com o parâmetro `openapi_tags`. @@ -52,7 +52,7 @@ Cada dicionário pode conter: * `description`: uma `str` com uma breve descrição da documentação externa. * `url` (**obrigatório**): uma `str` com a URL da documentação externa. -### Criar Metadados para tags +### Criar Metadados para tags { #create-metadata-for-tags } Vamos tentar isso em um exemplo com tags para `users` e `items`. @@ -68,31 +68,31 @@ Você não precisa adicionar metadados para todas as tags que você usa. /// -### Use suas tags +### Use suas tags { #use-your-tags } Use o parâmetro `tags` com suas *operações de rota* (e `APIRouter`s) para atribuí-los a diferentes tags: {* ../../docs_src/metadata/tutorial004.py hl[21,26] *} -/// info | Informação +/// info | Informação -Leia mais sobre tags em [Configuração de Operação de Caminho](path-operation-configuration.md#tags){.internal-link target=_blank}. +Leia mais sobre tags em [Configuração de operação de rota](path-operation-configuration.md#tags){.internal-link target=_blank}. /// -### Cheque os documentos +### Cheque os documentos { #check-the-docs } Agora, se você verificar a documentação, ela exibirá todos os metadados adicionais: -### Ordem das tags +### Ordem das tags { #order-of-tags } A ordem de cada dicionário de metadados de tag também define a ordem exibida na interface de documentação. Por exemplo, embora `users` apareça após `items` em ordem alfabética, ele é exibido antes deles, porque adicionamos seus metadados como o primeiro dicionário na lista. -## URL da OpenAPI +## URL da OpenAPI { #openapi-url } Por padrão, o esquema OpenAPI é servido em `/openapi.json`. @@ -104,7 +104,7 @@ Por exemplo, para defini-lo para ser servido em `/api/v1/openapi.json`: Se você quiser desativar completamente o esquema OpenAPI, pode definir `openapi_url=None`, o que também desativará as interfaces de documentação que o utilizam. -## URLs da Documentação +## URLs da Documentação { #docs-urls } Você pode configurar as duas interfaces de documentação incluídas: diff --git a/docs/pt/docs/tutorial/middleware.md b/docs/pt/docs/tutorial/middleware.md index 0f5009b6d..e1eb2f528 100644 --- a/docs/pt/docs/tutorial/middleware.md +++ b/docs/pt/docs/tutorial/middleware.md @@ -1,4 +1,4 @@ -# Middleware +# Middleware { #middleware } Você pode adicionar middleware à suas aplicações **FastAPI**. @@ -11,15 +11,15 @@ Um "middleware" é uma função que manipula cada **requisição** antes de ser * Ele pode fazer algo com essa **resposta** ou executar qualquer código necessário. * Então ele retorna a **resposta**. -/// note | Detalhes técnicos +/// note | Detalhes Técnicos Se você tiver dependências com `yield`, o código de saída será executado *depois* do middleware. -Se houver alguma tarefa em segundo plano (documentada posteriormente), ela será executada *depois* de todo o middleware. +Se houver alguma tarefa em segundo plano (abordada na seção [Tarefas em segundo plano](background-tasks.md){.internal-link target=_blank}, que você verá mais adiante), ela será executada *depois* de todo o middleware. /// -## Criar um middleware +## Criar um middleware { #create-a-middleware } Para criar um middleware, use o decorador `@app.middleware("http")` logo acima de uma função. @@ -35,13 +35,13 @@ A função middleware recebe: /// tip | Dica -Tenha em mente que cabeçalhos proprietários personalizados podem ser adicionados usando o prefixo 'X-'. +Tenha em mente que cabeçalhos proprietários personalizados podem ser adicionados usando o prefixo `X-`. Mas se você tiver cabeçalhos personalizados desejando que um cliente em um navegador esteja apto a ver, você precisa adicioná-los às suas configurações CORS ([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank}) usando o parâmetro `expose_headers` documentado em Documentos CORS da Starlette. /// -/// note | Detalhes técnicos +/// note | Detalhes Técnicos Você também pode usar `from starlette.requests import Request`. @@ -49,7 +49,7 @@ Você também pode usar `from starlette.requests import Request`. /// -### Antes e depois da `response` +### Antes e depois da `response` { #before-and-after-the-response } Você pode adicionar código para ser executado com a `request`, antes que qualquer *operação de rota* o receba. @@ -59,7 +59,36 @@ Por exemplo, você pode adicionar um cabeçalho personalizado `X-Process-Time` c {* ../../docs_src/middleware/tutorial001.py hl[10,12:13] *} -## Outros middlewares +/// tip | Dica + +Aqui usamos `time.perf_counter()` em vez de `time.time()` porque ele pode ser mais preciso para esses casos de uso. 🤓 + +/// + +## Ordem de execução de múltiplos middlewares { #multiple-middleware-execution-order } + +Quando você adiciona múltiplos middlewares usando o decorador `@app.middleware()` ou o método `app.add_middleware()`, cada novo middleware envolve a aplicação, formando uma pilha. O último middleware adicionado é o mais externo, e o primeiro é o mais interno. + +No caminho da requisição, o middleware mais externo roda primeiro. + +No caminho da resposta, ele roda por último. + +Por exemplo: + +```Python +app.add_middleware(MiddlewareA) +app.add_middleware(MiddlewareB) +``` + +Isso resulta na seguinte ordem de execução: + +* **Requisição**: MiddlewareB → MiddlewareA → rota + +* **Resposta**: rota → MiddlewareA → MiddlewareB + +Esse comportamento de empilhamento garante que os middlewares sejam executados em uma ordem previsível e controlável. + +## Outros middlewares { #other-middlewares } Mais tarde, você pode ler mais sobre outros middlewares no [Guia do usuário avançado: Middleware avançado](../advanced/middleware.md){.internal-link target=_blank}. diff --git a/docs/pt/docs/tutorial/path-operation-configuration.md b/docs/pt/docs/tutorial/path-operation-configuration.md index f183c9d23..5b4c82aa8 100644 --- a/docs/pt/docs/tutorial/path-operation-configuration.md +++ b/docs/pt/docs/tutorial/path-operation-configuration.md @@ -1,14 +1,14 @@ -# Configuração da Operação de Rota +# Configuração da Operação de Rota { #path-operation-configuration } Existem vários parâmetros que você pode passar para o seu *decorador de operação de rota* para configurá-lo. -/// warning | Aviso +/// warning | Atenção Observe que esses parâmetros são passados diretamente para o *decorador de operação de rota*, não para a sua *função de operação de rota*. /// -## Código de Status da Resposta +## Código de Status da Resposta { #response-status-code } Você pode definir o `status_code` (HTTP) para ser usado na resposta da sua *operação de rota*. @@ -16,7 +16,7 @@ Você pode passar diretamente o código `int`, como `404`. Mas se você não se lembrar o que cada código numérico significa, pode usar as constantes de atalho em `status`: -{* ../../docs_src/path_operation_configuration/tutorial001.py hl[3,17] *} +{* ../../docs_src/path_operation_configuration/tutorial001_py310.py hl[1,15] *} Esse código de status será usado na resposta e será adicionado ao esquema OpenAPI. @@ -28,17 +28,17 @@ Você também poderia usar `from starlette import status`. /// -## Tags +## Tags { #tags } Você pode adicionar tags para sua *operação de rota*, passe o parâmetro `tags` com uma `list` de `str` (comumente apenas um `str`): -{* ../../docs_src/path_operation_configuration/tutorial002.py hl[17,22,27] *} +{* ../../docs_src/path_operation_configuration/tutorial002_py310.py hl[15,20,25] *} Eles serão adicionados ao esquema OpenAPI e usados pelas interfaces de documentação automática: -### Tags com Enums +### Tags com Enums { #tags-with-enums } Se você tem uma grande aplicação, você pode acabar acumulando **várias tags**, e você gostaria de ter certeza de que você sempre usa a **mesma tag** para *operações de rota* relacionadas. @@ -48,30 +48,29 @@ Nestes casos, pode fazer sentido armazenar as tags em um `Enum`. {* ../../docs_src/path_operation_configuration/tutorial002b.py hl[1,8:10,13,18] *} -## Resumo e descrição +## Resumo e descrição { #summary-and-description } Você pode adicionar um `summary` e uma `description`: -{* ../../docs_src/path_operation_configuration/tutorial003.py hl[20:21] *} +{* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[18:19] *} -## Descrição do docstring +## Descrição do docstring { #description-from-docstring } Como as descrições tendem a ser longas e cobrir várias linhas, você pode declarar a descrição da *operação de rota* na docstring da função e o **FastAPI** irá lê-la de lá. Você pode escrever Markdown na docstring, ele será interpretado e exibido corretamente (levando em conta a indentação da docstring). -{* ../../docs_src/path_operation_configuration/tutorial004.py hl[19:27] *} +{* ../../docs_src/path_operation_configuration/tutorial004_py310.py hl[17:25] *} Ela será usada nas documentações interativas: - -## Descrição da resposta +## Descrição da resposta { #response-description } Você pode especificar a descrição da resposta com o parâmetro `response_description`: -{* ../../docs_src/path_operation_configuration/tutorial005.py hl[21] *} +{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[19] *} /// info | Informação @@ -79,7 +78,7 @@ Note que `response_description` se refere especificamente à resposta, a `descri /// -/// check +/// check | Verifique OpenAPI especifica que cada *operação de rota* requer uma descrição de resposta. @@ -89,7 +88,7 @@ Então, se você não fornecer uma, o **FastAPI** irá gerar automaticamente uma -## Depreciar uma *operação de rota* +## Descontinuar uma *operação de rota* { #deprecate-a-path-operation } Se você precisar marcar uma *operação de rota* como descontinuada, mas sem removê-la, passe o parâmetro `deprecated`: @@ -103,6 +102,6 @@ Verifique como *operações de rota* descontinuadas e não descontinuadas se par -## Resumindo +## Resumindo { #recap } Você pode configurar e adicionar metadados para suas *operações de rota* facilmente passando parâmetros para os *decoradores de operação de rota*. diff --git a/docs/pt/docs/tutorial/path-params-numeric-validations.md b/docs/pt/docs/tutorial/path-params-numeric-validations.md index 3aea1188d..cec744fd5 100644 --- a/docs/pt/docs/tutorial/path-params-numeric-validations.md +++ b/docs/pt/docs/tutorial/path-params-numeric-validations.md @@ -1,91 +1,128 @@ -# Parâmetros da Rota e Validações Numéricas +# Parâmetros de path e validações numéricas { #path-parameters-and-numeric-validations } -Do mesmo modo que você pode declarar mais validações e metadados para parâmetros de consulta com `Query`, você pode declarar os mesmos tipos de validações e metadados para parâmetros de rota com `Path`. +Da mesma forma que você pode declarar mais validações e metadados para parâmetros de consulta com `Query`, você pode declarar o mesmo tipo de validações e metadados para parâmetros de path com `Path`. -## Importe `Path` +## Importe `Path` { #import-path } -Primeiro, importe `Path` de `fastapi`: +Primeiro, importe `Path` de `fastapi`, e importe `Annotated`: -{* ../../docs_src/path_params_numeric_validations/tutorial001_py310.py hl[1] *} +{* ../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py hl[1,3] *} -## Declare metadados +/// info | Informação -Você pode declarar todos os parâmetros da mesma maneira que na `Query`. +O FastAPI adicionou suporte a `Annotated` (e passou a recomendá-lo) na versão 0.95.0. -Por exemplo para declarar um valor de metadado `title` para o parâmetro de rota `item_id` você pode digitar: +Se você tiver uma versão mais antiga, verá erros ao tentar usar `Annotated`. -{* ../../docs_src/path_params_numeric_validations/tutorial001_py310.py hl[8] *} - -/// note | Nota - -Um parâmetro de rota é sempre obrigatório, como se fizesse parte da rota. - -Então, você deve declará-lo com `...` para marcá-lo como obrigatório. - -Mesmo que você declare-o como `None` ou defina um valor padrão, isso não teria efeito algum, o parâmetro ainda seria obrigatório. +Certifique-se de [Atualizar a versão do FastAPI](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank} para pelo menos 0.95.1 antes de usar `Annotated`. /// -## Ordene os parâmetros de acordo com sua necessidade +## Declare metadados { #declare-metadata } -Suponha que você queira declarar o parâmetro de consulta `q` como uma `str` obrigatória. +Você pode declarar todos os mesmos parâmetros que em `Query`. -E você não precisa declarar mais nada em relação a este parâmetro, então você não precisa necessariamente usar `Query`. +Por exemplo, para declarar um valor de metadado `title` para o parâmetro de path `item_id` você pode digitar: -Mas você ainda precisa usar `Path` para o parâmetro de rota `item_id`. +{* ../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py hl[10] *} -O Python irá acusar se você colocar um elemento com um valor padrão definido antes de outro que não tenha um valor padrão. +/// note | Nota -Mas você pode reordená-los, colocando primeiro o elemento sem o valor padrão (o parâmetro de consulta `q`). +Um parâmetro de path é sempre obrigatório, pois precisa fazer parte do path. Mesmo que você o declare como `None` ou defina um valor padrão, isso não afetaria nada, ele ainda seria sempre obrigatório. -Isso não faz diferença para o **FastAPI**. Ele vai detectar os parâmetros pelos seus nomes, tipos e definições padrão (`Query`, `Path`, etc), sem se importar com a ordem. +/// + +## Ordene os parâmetros de acordo com sua necessidade { #order-the-parameters-as-you-need } + +/// tip | Dica + +Isso provavelmente não é tão importante ou necessário se você usar `Annotated`. + +/// + +Vamos supor que você queira declarar o parâmetro de consulta `q` como uma `str` obrigatória. + +E você não precisa declarar mais nada para esse parâmetro, então você realmente não precisa usar `Query`. + +Mas você ainda precisa usar `Path` para o parâmetro de path `item_id`. E você não quer usar `Annotated` por algum motivo. + +O Python vai reclamar se você colocar um valor com “padrão” antes de um valor que não tem “padrão”. + +Mas você pode reordená-los e colocar primeiro o valor sem padrão (o parâmetro de consulta `q`). + +Isso não faz diferença para o **FastAPI**. Ele vai detectar os parâmetros pelos seus nomes, tipos e declarações de padrão (`Query`, `Path`, etc.), sem se importar com a ordem. Então, você pode declarar sua função assim: {* ../../docs_src/path_params_numeric_validations/tutorial002.py hl[7] *} -## Ordene os parâmetros de a acordo com sua necessidade, truques +Mas tenha em mente que, se você usar `Annotated`, você não terá esse problema, não fará diferença, pois você não está usando valores padrão de parâmetros de função para `Query()` ou `Path()`. -Se você quiser declarar o parâmetro de consulta `q` sem um `Query` nem um valor padrão, e o parâmetro de rota `item_id` usando `Path`, e definí-los em uma ordem diferente, Python tem um pequeno truque na sintaxe para isso. +{* ../../docs_src/path_params_numeric_validations/tutorial002_an_py39.py *} + +## Ordene os parâmetros de acordo com sua necessidade, truques { #order-the-parameters-as-you-need-tricks } + +/// tip | Dica + +Isso provavelmente não é tão importante ou necessário se você usar `Annotated`. + +/// + +Aqui vai um pequeno truque que pode ser útil, mas você não vai precisar dele com frequência. + +Se você quiser: + +* declarar o parâmetro de consulta `q` sem um `Query` nem qualquer valor padrão +* declarar o parâmetro de path `item_id` usando `Path` +* tê-los em uma ordem diferente +* não usar `Annotated` + +...o Python tem uma pequena sintaxe especial para isso. Passe `*`, como o primeiro parâmetro da função. -O Python não vai fazer nada com esse `*`, mas ele vai saber que a partir dali os parâmetros seguintes deverão ser chamados argumentos nomeados (pares chave-valor), também conhecidos como kwargs. Mesmo que eles não possuam um valor padrão. +O Python não fará nada com esse `*`, mas saberá que todos os parâmetros seguintes devem ser chamados como argumentos nomeados (pares chave-valor), também conhecidos como kwargs. Mesmo que eles não tenham um valor padrão. {* ../../docs_src/path_params_numeric_validations/tutorial003.py hl[7] *} -## Validações numéricas: maior que ou igual +### Melhor com `Annotated` { #better-with-annotated } -Com `Query` e `Path` (e outras que você verá mais tarde) você pode declarar restrições numéricas. +Tenha em mente que, se você usar `Annotated`, como você não está usando valores padrão de parâmetros de função, você não terá esse problema e provavelmente não precisará usar `*`. -Aqui, com `ge=1`, `item_id` precisará ser um número inteiro maior que ("`g`reater than") ou igual ("`e`qual") a 1. +{* ../../docs_src/path_params_numeric_validations/tutorial003_an_py39.py hl[10] *} -{* ../../docs_src/path_params_numeric_validations/tutorial004.py hl[8] *} +## Validações numéricas: maior que ou igual { #number-validations-greater-than-or-equal } -## Validações numéricas: maior que e menor que ou igual +Com `Query` e `Path` (e outras que você verá depois) você pode declarar restrições numéricas. -O mesmo se aplica para: +Aqui, com `ge=1`, `item_id` precisará ser um número inteiro “`g`reater than or `e`qual” a `1`. + +{* ../../docs_src/path_params_numeric_validations/tutorial004_an_py39.py hl[10] *} + +## Validações numéricas: maior que e menor que ou igual { #number-validations-greater-than-and-less-than-or-equal } + +O mesmo se aplica a: * `gt`: maior que (`g`reater `t`han) * `le`: menor que ou igual (`l`ess than or `e`qual) -{* ../../docs_src/path_params_numeric_validations/tutorial005.py hl[9] *} +{* ../../docs_src/path_params_numeric_validations/tutorial005_an_py39.py hl[10] *} -## Validações numéricas: valores do tipo float, maior que e menor que +## Validações numéricas: floats, maior que e menor que { #number-validations-floats-greater-than-and-less-than } -Validações numéricas também funcionam para valores do tipo `float`. +Validações numéricas também funcionam para valores `float`. -Aqui é onde se torna importante a possibilidade de declarar gt e não apenas ge. Com isso você pode especificar, por exemplo, que um valor deve ser maior que `0`, ainda que seja menor que `1`. +Aqui é onde se torna importante poder declarar gt e não apenas ge. Com isso você pode exigir, por exemplo, que um valor seja maior que `0`, mesmo que seja menor que `1`. -Assim, `0.5` seria um valor válido. Mas `0.0` ou `0` não seria. +Assim, `0.5` seria um valor válido. Mas `0.0` ou `0` não seriam. -E o mesmo para lt. +E o mesmo para lt. -{* ../../docs_src/path_params_numeric_validations/tutorial006.py hl[11] *} +{* ../../docs_src/path_params_numeric_validations/tutorial006_an_py39.py hl[13] *} -## Recapitulando +## Recapitulando { #recap } -Com `Query`, `Path` (e outras que você ainda não viu) você pode declarar metadados e validações de texto do mesmo modo que com [Parâmetros de consulta e validações de texto](query-params-str-validations.md){.internal-link target=_blank}. +Com `Query`, `Path` (e outras que você ainda não viu) você pode declarar metadados e validações de string do mesmo modo que em [Parâmetros de consulta e validações de string](query-params-str-validations.md){.internal-link target=_blank}. E você também pode declarar validações numéricas: @@ -96,7 +133,7 @@ E você também pode declarar validações numéricas: /// info | Informação -`Query`, `Path` e outras classes que você verá a frente são subclasses de uma classe comum `Param`. +`Query`, `Path` e outras classes que você verá depois são subclasses de uma classe comum `Param`. Todas elas compartilham os mesmos parâmetros para validação adicional e metadados que você viu. @@ -106,12 +143,12 @@ Todas elas compartilham os mesmos parâmetros para validação adicional e metad Quando você importa `Query`, `Path` e outras de `fastapi`, elas são na verdade funções. -Que quando chamadas, retornam instâncias de classes de mesmo nome. +Que, quando chamadas, retornam instâncias de classes de mesmo nome. Então, você importa `Query`, que é uma função. E quando você a chama, ela retorna uma instância de uma classe também chamada `Query`. -Estas funções são assim (ao invés de apenas usar as classes diretamente) para que seu editor não acuse erros sobre seus tipos. +Essas funções existem (em vez de usar diretamente as classes) para que seu editor não marque erros sobre seus tipos. -Dessa maneira você pode user seu editor e ferramentas de desenvolvimento sem precisar adicionar configurações customizadas para ignorar estes erros. +Dessa forma, você pode usar seu editor e ferramentas de codificação normais sem precisar adicionar configurações personalizadas para desconsiderar esses erros. /// diff --git a/docs/pt/docs/tutorial/path-params.md b/docs/pt/docs/tutorial/path-params.md index ecf77d676..d795d5b2a 100644 --- a/docs/pt/docs/tutorial/path-params.md +++ b/docs/pt/docs/tutorial/path-params.md @@ -1,207 +1,188 @@ -# Parâmetros da rota da URL +# Parâmetros de path { #path-parameters } -Você pode declarar os "parâmetros" ou "variáveis" com a mesma sintaxe utilizada pelo formato de strings do Python: +Você pode declarar "parâmetros" ou "variáveis" de path com a mesma sintaxe usada por strings de formatação do Python: {* ../../docs_src/path_params/tutorial001.py hl[6:7] *} -O valor do parâmetro que foi passado à `item_id` será passado para a sua função como o argumento `item_id`. +O valor do parâmetro de path `item_id` será passado para a sua função como o argumento `item_id`. -Então, se você rodar este exemplo e for até http://127.0.0.1:8000/items/foo, você verá a seguinte resposta: +Então, se você executar este exemplo e acessar http://127.0.0.1:8000/items/foo, você verá uma resposta: ```JSON {"item_id":"foo"} ``` -## Parâmetros da rota com tipos +## Parâmetros de path com tipos { #path-parameters-with-types } -Você pode declarar o tipo de um parâmetro na função usando as anotações padrões do Python: +Você pode declarar o tipo de um parâmetro de path na função, usando as anotações de tipo padrão do Python: {* ../../docs_src/path_params/tutorial002.py hl[7] *} -Nesse caso, `item_id` está sendo declarado como um `int`. +Neste caso, `item_id` é declarado como um `int`. /// check | Verifique - - - +Isso fornecerá suporte do editor dentro da sua função, com verificações de erros, preenchimento automático, etc. /// - Isso vai dar à você suporte do seu editor dentro das funções, com verificações de erros, autocompletar, etc. +## Dados conversão { #data-conversion } -## Conversão de dados - -Se você rodar esse exemplo e abrir o seu navegador em http://127.0.0.1:8000/items/3, você verá a seguinte resposta: +Se você executar este exemplo e abrir seu navegador em http://127.0.0.1:8000/items/3, você verá uma resposta: ```JSON {"item_id":3} ``` /// check | Verifique +Perceba que o valor que sua função recebeu (e retornou) é `3`, como um `int` do Python, não uma string `"3"`. - - +Então, com essa declaração de tipo, o **FastAPI** fornece "parsing" automático do request. /// - Observe que o valor recebido pela função (e também retornado por ela) é `3`, como um Python `int`, não como uma string `"3"`. +## Validação de dados { #data-validation } - Então, com essa declaração de tipo, o **FastAPI** dá pra você um "parsing" automático no request . - -## Validação de dados - -Mas se você abrir o seu navegador em http://127.0.0.1:8000/items/foo, você verá um belo erro HTTP: +Mas se você for no navegador para http://127.0.0.1:8000/items/foo, verá um bom erro HTTP: ```JSON { - "detail": [ - { - "loc": [ - "path", - "item_id" - ], - "msg": "value is not a valid integer", - "type": "type_error.integer" - } - ] + "detail": [ + { + "type": "int_parsing", + "loc": [ + "path", + "item_id" + ], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo" + } + ] } ``` -devido ao parâmetro da rota `item_id` ter um valor `"foo"`, que não é um `int`. +porque o parâmetro de path `item_id` tinha o valor `"foo"`, que não é um `int`. -O mesmo erro apareceria se você tivesse fornecido um `float` ao invés de um `int`, como em: http://127.0.0.1:8000/items/4.2 +O mesmo erro apareceria se você fornecesse um `float` em vez de um `int`, como em: http://127.0.0.1:8000/items/4.2 /// check | Verifique +Então, com a mesma declaração de tipo do Python, o **FastAPI** fornece validação de dados. +Observe que o erro também declara claramente exatamente o ponto onde a validação não passou. - +Isso é incrivelmente útil ao desenvolver e depurar código que interage com sua API. /// - Então, com a mesma declaração de tipo do Python, o **FastAPI** dá pra você validação de dados. +## Documentação { #documentation } - Observe que o erro também mostra claramente o ponto exato onde a validação não passou. - - Isso é incrivelmente útil enquanto se desenvolve e debuga o código que interage com a sua API. - -## Documentação - -Quando você abrir o seu navegador em http://127.0.0.1:8000/docs, você verá de forma automática e interativa a documentação da API como: +E quando você abrir seu navegador em http://127.0.0.1:8000/docs, você verá documentação automática, interativa, da API como: /// check | Verifique +Novamente, apenas com a mesma declaração de tipo do Python, o **FastAPI** fornece documentação automática e interativa (integrando o Swagger UI). - - +Observe que o parâmetro de path está declarado como um inteiro. /// - Novamente, apenas com a mesma declaração de tipo do Python, o **FastAPI** te dá de forma automática e interativa a documentação (integrada com o Swagger UI). +## Benefícios baseados em padrões, documentação alternativa { #standards-based-benefits-alternative-documentation } - Veja que o parâmetro de rota está declarado como sendo um inteiro (int). +E como o schema gerado é do padrão OpenAPI, existem muitas ferramentas compatíveis. -## Beneficios baseados em padrões, documentação alternativa - -Devido ao schema gerado ser o padrão do OpenAPI, existem muitas ferramentas compatíveis. - -Por esse motivo, o próprio **FastAPI** fornece uma API alternativa para documentação (utilizando ReDoc), que você pode acessar em http://127.0.0.1:8000/redoc: +Por causa disso, o próprio **FastAPI** fornece uma documentação alternativa da API (usando ReDoc), que você pode acessar em http://127.0.0.1:8000/redoc: Da mesma forma, existem muitas ferramentas compatíveis. Incluindo ferramentas de geração de código para muitas linguagens. -## Pydantic +## Pydantic { #pydantic } -Toda a validação de dados é feita por baixo dos panos pelo Pydantic, então você tem todos os benefícios disso. E assim você sabe que está em boas mãos. +Toda a validação de dados é realizada nos bastidores pelo Pydantic, então você recebe todos os benefícios disso. E você sabe que está em boas mãos. -Você pode usar as mesmas declarações de tipo com `str`, `float`, `bool` e muitos outros tipos complexos de dados. +Você pode usar as mesmas declarações de tipo com `str`, `float`, `bool` e muitos outros tipos de dados complexos. -Vamos explorar muitos destes tipos nos próximos capítulos do tutorial. +Vários deles são explorados nos próximos capítulos do tutorial. -## A ordem importa +## A ordem importa { #order-matters } -Quando você cria operações de rota, você pode se deparar com situações onde você pode ter uma rota fixa. +Ao criar *operações de rota*, você pode encontrar situações em que tem um path fixo. -Algo como `/users/me` por exemplo, digamos que essa rota seja utilizada para pegar dados sobre o usuário atual. +Como `/users/me`, digamos que seja para obter dados sobre o usuário atual. -E então você pode ter também uma rota `/users/{user_id}` para pegar dados sobre um usuário específico associado a um ID de usuário. +E então você também pode ter um path `/users/{user_id}` para obter dados sobre um usuário específico por algum ID de usuário. -Porque as operações de rota são avaliadas em ordem, você precisa ter certeza que a rota para `/users/me` está sendo declarado antes da rota `/users/{user_id}`: +Como as *operações de rota* são avaliadas em ordem, você precisa garantir que o path para `/users/me` seja declarado antes do de `/users/{user_id}`: {* ../../docs_src/path_params/tutorial003.py hl[6,11] *} -Caso contrário, a rota para `/users/{user_id}` coincidiria também para `/users/me`, "pensando" que estaria recebendo o parâmetro `user_id` com o valor de `"me"`. +Caso contrário, o path para `/users/{user_id}` também corresponderia a `/users/me`, "achando" que está recebendo um parâmetro `user_id` com o valor `"me"`. -## Valores predefinidos +Da mesma forma, você não pode redefinir uma operação de rota: -Se você tem uma operação de rota que recebe um parâmetro da rota, mas que você queira que esses valores possíveis do parâmetro da rota sejam predefinidos, você pode usar `Enum` padrão do Python. +{* ../../docs_src/path_params/tutorial003b.py hl[6,11] *} -### Criando uma classe `Enum` +A primeira sempre será usada, já que o path corresponde primeiro. -Importe `Enum` e crie uma sub-classe que herde de `str` e de `Enum`. +## Valores predefinidos { #predefined-values } -Por herdar de `str` a documentação da API vai ser capaz de saber que os valores devem ser do tipo `string` e assim ser capaz de mostrar eles corretamente. +Se você tem uma *operação de rota* que recebe um *parâmetro de path*, mas quer que os valores válidos possíveis do *parâmetro de path* sejam predefinidos, você pode usar um `Enum` padrão do Python. -Assim, crie atributos de classe com valores fixos, que serão os valores válidos disponíveis. +### Crie uma classe `Enum` { #create-an-enum-class } + +Importe `Enum` e crie uma subclasse que herde de `str` e de `Enum`. + +Ao herdar de `str`, a documentação da API saberá que os valores devem ser do tipo `string` e poderá renderizá-los corretamente. + +Em seguida, crie atributos de classe com valores fixos, que serão os valores válidos disponíveis: {* ../../docs_src/path_params/tutorial005.py hl[1,6:9] *} -/// info | informação - +/// info | Informação Enumerations (ou enums) estão disponíveis no Python desde a versão 3.4. - /// /// tip | Dica - - - +Se você está se perguntando, "AlexNet", "ResNet" e "LeNet" são apenas nomes de modelos de Aprendizado de Máquina. /// - Se você está se perguntando, "AlexNet", "ResNet", e "LeNet" são apenas nomes de modelos de Machine Learning (aprendizado de máquina). +### Declare um parâmetro de path { #declare-a-path-parameter } -### Declare um *parâmetro de rota* - -Logo, crie um *parâmetro de rota* com anotações de tipo usando a classe enum que você criou (`ModelName`): +Em seguida, crie um *parâmetro de path* com anotação de tipo usando a classe enum que você criou (`ModelName`): {* ../../docs_src/path_params/tutorial005.py hl[16] *} -### Revise a documentação +### Verifique a documentação { #check-the-docs } -Visto que os valores disponíveis para o parâmetro da rota estão predefinidos, a documentação interativa pode mostrar esses valores de uma forma bem legal: +Como os valores disponíveis para o *parâmetro de path* são predefinidos, a documentação interativa pode mostrá-los de forma agradável: -### Trabalhando com os *enumeration* do Python +### Trabalhando com *enumerações* do Python { #working-with-python-enumerations } -O valor do *parâmetro da rota* será um *membro de enumeration*. +O valor do *parâmetro de path* será um *membro de enumeração*. -#### Compare *membros de enumeration* +#### Compare membros de enumeração { #compare-enumeration-members } -Você pode comparar eles com o *membro de enumeration* no enum `ModelName` que você criou: +Você pode compará-lo com o *membro de enumeração* no seu enum `ModelName` criado: {* ../../docs_src/path_params/tutorial005.py hl[17] *} -#### Obtenha o *valor de enumerate* +#### Obtenha o valor da enumeração { #get-the-enumeration-value } -Você pode ter o valor exato de enumerate (um `str` nesse caso) usando `model_name.value`, ou em geral, `your_enum_member.value`: +Você pode obter o valor real (um `str` neste caso) usando `model_name.value`, ou, em geral, `your_enum_member.value`: {* ../../docs_src/path_params/tutorial005.py hl[20] *} /// tip | Dica - - - +Você também pode acessar o valor `"lenet"` com `ModelName.lenet.value`. /// - Você também poderia acessar o valor `"lenet"` com `ModelName.lenet.value` +#### Retorne membros de enumeração { #return-enumeration-members } -#### Retorne *membros de enumeration* +Você pode retornar *membros de enum* da sua *operação de rota*, até mesmo aninhados em um corpo JSON (por exemplo, um `dict`). -Você pode retornar *membros de enum* da sua *rota de operação*, em um corpo JSON aninhado (por exemplo um `dict`). - -Eles serão convertidos para o seus valores correspondentes (strings nesse caso) antes de serem retornados ao cliente: +Eles serão convertidos para seus valores correspondentes (strings neste caso) antes de serem retornados ao cliente: {* ../../docs_src/path_params/tutorial005.py hl[18,21,23] *} -No seu cliente você vai obter uma resposta JSON como: +No seu cliente, você receberá uma resposta JSON como: ```JSON { @@ -210,56 +191,51 @@ No seu cliente você vai obter uma resposta JSON como: } ``` -## Parâmetros de rota que contém caminhos +## Parâmetros de path que contêm paths { #path-parameters-containing-paths } -Digamos que você tenha uma *operação de rota* com uma rota `/files/{file_path}`. +Digamos que você tenha uma *operação de rota* com um path `/files/{file_path}`. -Mas você precisa que o próprio `file_path` contenha uma *rota*, como `home/johndoe/myfile.txt`. +Mas você precisa que o próprio `file_path` contenha um *path*, como `home/johndoe/myfile.txt`. -Então, a URL para este arquivo deveria ser algo como: `/files/home/johndoe/myfile.txt`. +Então, a URL para esse arquivo seria algo como: `/files/home/johndoe/myfile.txt`. -### Suporte do OpenAPI +### Suporte do OpenAPI { #openapi-support } -O OpenAPI não suporta uma maneira de declarar um *parâmetro de rota* que contenha uma *rota* dentro, dado que isso poderia levar a cenários que são difíceis de testar e definir. +O OpenAPI não oferece suporte a uma maneira de declarar um *parâmetro de path* que contenha um *path* dentro, pois isso poderia levar a cenários difíceis de testar e definir. -No entanto, você pode fazer isso no **FastAPI**, usando uma das ferramentas internas do Starlette. +Ainda assim, você pode fazer isso no **FastAPI**, usando uma das ferramentas internas do Starlette. -A documentação continuaria funcionando, ainda que não adicionaria nenhuma informação dizendo que o parâmetro deveria conter uma rota. +E a documentação continuará funcionando, embora não adicione nenhuma informação dizendo que o parâmetro deve conter um path. -### Conversor de rota +### Conversor de path { #path-convertor } -Usando uma opção direta do Starlette você pode declarar um *parâmetro de rota* contendo uma *rota* usando uma URL como: +Usando uma opção diretamente do Starlette você pode declarar um *parâmetro de path* contendo um *path* usando uma URL como: ``` /files/{file_path:path} ``` -Nesse caso, o nome do parâmetro é `file_path`, e a última parte, `:path`, diz que o parâmetro deveria coincidir com qualquer *rota*. +Nesse caso, o nome do parâmetro é `file_path`, e a última parte, `:path`, diz que o parâmetro deve corresponder a qualquer *path*. -Então, você poderia usar ele com: +Então, você pode usá-lo com: {* ../../docs_src/path_params/tutorial004.py hl[6] *} /// tip | Dica +Você pode precisar que o parâmetro contenha `/home/johndoe/myfile.txt`, com uma barra inicial (`/`). - - +Nesse caso, a URL seria: `/files//home/johndoe/myfile.txt`, com uma barra dupla (`//`) entre `files` e `home`. /// - Você poderia precisar que o parâmetro contivesse `/home/johndoe/myfile.txt`, com uma barra no inicio (`/`). +## Recapitulação { #recap } - Neste caso, a URL deveria ser: `/files//home/johndoe/myfile.txt`, com barra dupla (`//`) entre `files` e `home`. +Com o **FastAPI**, ao usar declarações de tipo do Python curtas, intuitivas e padrão, você obtém: +- Suporte no editor: verificações de erro, autocompletar, etc. +- "Parsing" de dados +- Validação de dados +- Anotação da API e documentação automática -## Recapitulando +E você só precisa declará-los uma vez. -Com o **FastAPI**, usando as declarações de tipo do Python, você obtém: - -* Suporte no editor: verificação de erros, e opção de autocompletar, etc. -* "Parsing" de dados -* Validação de dados -* Anotação da API e documentação automática - -Você apenas tem que declará-los uma vez. - -Essa é provavelmente a vantagem mais visível do **FastAPI** se comparado com frameworks alternativos (além do desempenho puro). +Essa é provavelmente a principal vantagem visível do **FastAPI** em comparação com frameworks alternativos (além do desempenho bruto). diff --git a/docs/pt/docs/tutorial/query-param-models.md b/docs/pt/docs/tutorial/query-param-models.md index 01a6e462f..42d2604cd 100644 --- a/docs/pt/docs/tutorial/query-param-models.md +++ b/docs/pt/docs/tutorial/query-param-models.md @@ -1,4 +1,4 @@ -# Modelos de Parâmetros de Consulta +# Modelos de Parâmetros de Consulta { #query-parameter-models } Se você possui um grupo de **parâmetros de consultas** que são relacionados, você pode criar um **modelo Pydantic** para declará-los. @@ -10,7 +10,7 @@ Isso é suportado desde o FastAPI versão `0.115.0`. 🤓 /// -## Parâmetros de Consulta com um Modelo Pydantic +## Parâmetros de Consulta com um Modelo Pydantic { #query-parameters-with-a-pydantic-model } Declare os **parâmetros de consulta** que você precisa em um **modelo Pydantic**, e então declare o parâmetro como `Query`: @@ -19,7 +19,7 @@ Declare os **parâmetros de consulta** que você precisa em um **modelo Pydantic O **FastAPI** **extrairá** os dados para **cada campo** dos **parâmetros de consulta** presentes na requisição, e fornecerá o modelo Pydantic que você definiu. -## Verifique os Documentos +## Verifique os Documentos { #check-the-docs } Você pode ver os parâmetros de consulta nos documentos de IU em `/docs`: @@ -27,7 +27,7 @@ Você pode ver os parâmetros de consulta nos documentos de IU em `/docs`:
-## Restrinja Parâmetros de Consulta Extras +## Restrinja Parâmetros de Consulta Extras { #forbid-extra-query-parameters } Em alguns casos especiais (provavelmente não muito comuns), você queira **restrinjir** os parâmetros de consulta que deseja receber. @@ -58,7 +58,7 @@ Eles receberão um retorno de **erro** informando-os que o parâmentro de consul } ``` -## Resumo +## Resumo { #summary } Você pode utilizar **modelos Pydantic** para declarar **parâmetros de consulta** no **FastAPI**. 😎 diff --git a/docs/pt/docs/tutorial/query-params-str-validations.md b/docs/pt/docs/tutorial/query-params-str-validations.md index 8c4f2e655..948f8ca8f 100644 --- a/docs/pt/docs/tutorial/query-params-str-validations.md +++ b/docs/pt/docs/tutorial/query-params-str-validations.md @@ -1,164 +1,286 @@ -# Parâmetros de consulta e validações de texto +# Parâmetros de consulta e validações de string { #query-parameters-and-string-validations } -O **FastAPI** permite que você declare informações adicionais e validações aos seus parâmetros. +O **FastAPI** permite declarar informações adicionais e validações para os seus parâmetros. -Vamos utilizar essa aplicação como exemplo: +Vamos usar esta aplicação como exemplo: -{* ../../docs_src/query_params_str_validations/tutorial001.py hl[9] *} +{* ../../docs_src/query_params_str_validations/tutorial001_py310.py hl[7] *} -O parâmetro de consulta `q` é do tipo `Union[str, None]`, o que significa que é do tipo `str` mas que também pode ser `None`, e de fato, o valor padrão é `None`, então o FastAPI saberá que não é obrigatório. +O parâmetro de consulta `q` é do tipo `str | None`, isso significa que é do tipo `str`, mas também pode ser `None`, e de fato, o valor padrão é `None`, então o FastAPI saberá que não é obrigatório. -/// note | Observação +/// note | Nota O FastAPI saberá que o valor de `q` não é obrigatório por causa do valor padrão `= None`. -O `Union` em `Union[str, None]` não é usado pelo FastAPI, mas permitirá que seu editor lhe dê um melhor suporte e detecte erros. +Ter `str | None` permitirá que seu editor lhe ofereça melhor suporte e detecte erros. /// -## Validação adicional +## Validação adicional { #additional-validation } -Nós iremos forçar que mesmo o parâmetro `q` seja opcional, sempre que informado, **seu tamanho não exceda 50 caracteres**. +Vamos impor que, embora `q` seja opcional, sempre que for fornecido, **seu comprimento não exceda 50 caracteres**. -### Importe `Query` +### Importe `Query` e `Annotated` { #import-query-and-annotated } -Para isso, primeiro importe `Query` de `fastapi`: +Para isso, primeiro importe: -{* ../../docs_src/query_params_str_validations/tutorial002.py hl[3] *} +* `Query` de `fastapi` +* `Annotated` de `typing` -## Use `Query` como o valor padrão +{* ../../docs_src/query_params_str_validations/tutorial002_an_py310.py hl[1,3] *} -Agora utilize-o como valor padrão do seu parâmetro, definindo o parâmetro `max_length` para 50: +/// info | Informação -{* ../../docs_src/query_params_str_validations/tutorial002.py hl[9] *} +O FastAPI adicionou suporte a `Annotated` (e passou a recomendá-lo) na versão 0.95.0. -Note que substituímos o valor padrão de `None` para `Query(default=None)`, o primeiro parâmetro de `Query` serve para o mesmo propósito: definir o valor padrão do parâmetro. +Se você tiver uma versão mais antiga, terá erros ao tentar usar `Annotated`. -Então: +Certifique-se de [Atualizar a versão do FastAPI](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank} para pelo menos 0.95.1 antes de usar `Annotated`. + +/// + +## Use `Annotated` no tipo do parâmetro `q` { #use-annotated-in-the-type-for-the-q-parameter } + +Lembra que eu disse antes que `Annotated` pode ser usado para adicionar metadados aos seus parâmetros na [Introdução aos tipos do Python](../python-types.md#type-hints-with-metadata-annotations){.internal-link target=_blank}? + +Agora é a hora de usá-lo com FastAPI. 🚀 + +Tínhamos esta anotação de tipo: + +//// tab | Python 3.10+ ```Python -q: Union[str, None] = Query(default=None) +q: str | None = None ``` -...Torna o parâmetro opcional, da mesma maneira que: +//// + +//// tab | Python 3.8+ ```Python q: Union[str, None] = None ``` -Mas o declara explicitamente como um parâmetro de consulta. +//// -/// info | Informação +O que faremos é envolver isso com `Annotated`, para que fique assim: -Tenha em mente que o FastAPI se preocupa com a parte: +//// tab | Python 3.10+ ```Python -= None +q: Annotated[str | None] = None ``` -Ou com: +//// + +//// tab | Python 3.8+ ```Python -= Query(default=None) +q: Annotated[Union[str, None]] = None ``` -E irá utilizar o `None` para detectar que o parâmetro de consulta não é obrigatório. +//// -O `Union` é apenas para permitir que seu editor de texto lhe dê um melhor suporte. +Ambas as versões significam a mesma coisa, `q` é um parâmetro que pode ser `str` ou `None`, e por padrão é `None`. + +Agora vamos pular para a parte divertida. 🎉 + +## Adicione `Query` ao `Annotated` no parâmetro `q` { #add-query-to-annotated-in-the-q-parameter } + +Agora que temos esse `Annotated` onde podemos colocar mais informações (neste caso, uma validação adicional), adicione `Query` dentro de `Annotated` e defina o parâmetro `max_length` como `50`: + +{* ../../docs_src/query_params_str_validations/tutorial002_an_py310.py hl[9] *} + +Perceba que o valor padrão continua sendo `None`, então o parâmetro ainda é opcional. + +Mas agora, com `Query(max_length=50)` dentro de `Annotated`, estamos dizendo ao FastAPI que queremos **validação adicional** para este valor, queremos que tenha no máximo 50 caracteres. 😎 + +/// tip | Dica + +Aqui estamos usando `Query()` porque este é um **parâmetro de consulta**. Mais adiante veremos outros como `Path()`, `Body()`, `Header()` e `Cookie()`, que também aceitam os mesmos argumentos que `Query()`. /// -Então, podemos passar mais parâmetros para `Query`. Neste caso, o parâmetro `max_length` que se aplica a textos: +Agora o FastAPI vai: -```Python -q: str = Query(default=None, max_length=50) -``` +* **Validar** os dados garantindo que o comprimento máximo seja de 50 caracteres +* Mostrar um **erro claro** para o cliente quando os dados não forem válidos +* **Documentar** o parâmetro na *operação de rota* do esquema OpenAPI (então ele aparecerá na **UI de docs automática**) -Isso irá validar os dados, mostrar um erro claro quando os dados forem inválidos, e documentar o parâmetro na *operação de rota* do esquema OpenAPI.. +## Alternativa (antiga): `Query` como valor padrão { #alternative-old-query-as-the-default-value } -## Adicionando mais validações +Versões anteriores do FastAPI (antes de 0.95.0) exigiam que você usasse `Query` como valor padrão do seu parâmetro, em vez de colocá-lo em `Annotated`. É muito provável que você veja código assim por aí, então vou te explicar. -Você também pode incluir um parâmetro `min_length`: +/// tip | Dica -{* ../../docs_src/query_params_str_validations/tutorial003.py hl[10] *} - -## Adicionando expressões regulares - -Você pode definir uma expressão regular que combine com um padrão esperado pelo parâmetro: - -{* ../../docs_src/query_params_str_validations/tutorial004.py hl[11] *} - -Essa expressão regular específica verifica se o valor recebido no parâmetro: - -* `^`: Inicia com os seguintes caracteres, ou seja, não contém caracteres anteriores. -* `fixedquery`: contém o valor exato `fixedquery`. -* `$`: termina aqui, não contém nenhum caractere após `fixedquery`. - -Se você se sente perdido com todo esse assunto de **"expressão regular"**, não se preocupe. Esse é um assunto complicado para a maioria das pessoas. Você ainda pode fazer muitas coisas sem utilizar expressões regulares. - -Mas assim que você precisar e já tiver aprendido sobre, saiba que você poderá usá-las diretamente no **FastAPI**. - -## Valores padrão - -Da mesma maneira que você utiliza `None` como o primeiro argumento para ser utilizado como um valor padrão, você pode usar outros valores. - -Vamos dizer que você queira que o parâmetro de consulta `q` tenha um `min_length` de `3`, e um valor padrão de `"fixedquery"`, então declararíamos assim: - -{* ../../docs_src/query_params_str_validations/tutorial005.py hl[7] *} - -/// note | Observação - -O parâmetro torna-se opcional quando possui um valor padrão. +Para código novo e sempre que possível, use `Annotated` como explicado acima. Há múltiplas vantagens (explicadas abaixo) e nenhuma desvantagem. 🍰 /// -## Torne-o obrigatório +É assim que você usaria `Query()` como valor padrão do parâmetro da sua função, definindo o parâmetro `max_length` como 50: -Quando você não necessita de validações ou de metadados adicionais, podemos fazer com que o parâmetro de consulta `q` seja obrigatório por não declarar um valor padrão, dessa forma: +{* ../../docs_src/query_params_str_validations/tutorial002_py310.py hl[7] *} + +Como neste caso (sem usar `Annotated`) temos que substituir o valor padrão `None` na função por `Query()`, agora precisamos definir o valor padrão com o parâmetro `Query(default=None)`, ele serve ao mesmo propósito de definir esse valor padrão (pelo menos para o FastAPI). + +Então: + +```Python +q: str | None = Query(default=None) +``` + +...torna o parâmetro opcional, com um valor padrão de `None`, o mesmo que: + + +```Python +q: str | None = None +``` + +Mas a versão com `Query` o declara explicitamente como sendo um parâmetro de consulta. + +Então, podemos passar mais parâmetros para `Query`. Neste caso, o parâmetro `max_length` que se aplica a strings: + +```Python +q: str | None = Query(default=None, max_length=50) +``` + +Isso validará os dados, mostrará um erro claro quando os dados não forem válidos e documentará o parâmetro na *operação de rota* do esquema OpenAPI. + +### `Query` como valor padrão ou em `Annotated` { #query-as-the-default-value-or-in-annotated } + +Tenha em mente que, ao usar `Query` dentro de `Annotated`, você não pode usar o parâmetro `default` de `Query`. + +Em vez disso, use o valor padrão real do parâmetro da função. Caso contrário, haveria inconsistência. + +Por exemplo, isto não é permitido: + +```Python +q: Annotated[str, Query(default="rick")] = "morty" +``` + +...porque não está claro se o valor padrão deveria ser `"rick"` ou `"morty"`. + +Então, você usaria (preferencialmente): + +```Python +q: Annotated[str, Query()] = "rick" +``` + +...ou em bases de código mais antigas você encontrará: + +```Python +q: str = Query(default="rick") +``` + +### Vantagens de `Annotated` { #advantages-of-annotated } + +**Usar `Annotated` é recomendado** em vez do valor padrão nos parâmetros da função, é **melhor** por vários motivos. 🤓 + +O valor **padrão** do **parâmetro da função** é o **valor padrão real**, isso é mais intuitivo com Python em geral. 😌 + +Você poderia **chamar** essa mesma função em **outros lugares** sem FastAPI, e ela **funcionaria como esperado**. Se houver um parâmetro **obrigatório** (sem valor padrão), seu **editor** vai avisar com um erro, e o **Python** também reclamará se você executá-la sem passar o parâmetro obrigatório. + +Quando você não usa `Annotated` e em vez disso usa o estilo de **valor padrão (antigo)**, se você chamar essa função sem FastAPI em **outros lugares**, terá que **lembrar** de passar os argumentos para a função para que funcione corretamente, caso contrário os valores serão diferentes do esperado (por exemplo, `QueryInfo` ou algo parecido em vez de `str`). E seu editor não vai avisar, e o Python também não vai reclamar ao executar a função, apenas quando as operações internas falharem. + +Como `Annotated` pode ter mais de uma anotação de metadados, você agora pode até usar a mesma função com outras ferramentas, como o Typer. 🚀 + +## Adicione mais validações { #add-more-validations } + +Você também pode adicionar um parâmetro `min_length`: + +{* ../../docs_src/query_params_str_validations/tutorial003_an_py310.py hl[10] *} + +## Adicione expressões regulares { #add-regular-expressions } + +Você pode definir um `pattern` de expressão regular que o parâmetro deve corresponder: + +{* ../../docs_src/query_params_str_validations/tutorial004_an_py310.py hl[11] *} + +Esse padrão específico de expressão regular verifica se o valor recebido no parâmetro: + +* `^`: começa com os caracteres seguintes, não tem caracteres antes. +* `fixedquery`: tem exatamente o valor `fixedquery`. +* `$`: termina ali, não tem mais caracteres depois de `fixedquery`. + +Se você se sentir perdido com essas ideias de **"expressão regular"**, não se preocupe. Esse é um assunto difícil para muitas pessoas. Você ainda pode fazer muitas coisas sem precisar de expressões regulares por enquanto. + +Agora você sabe que, sempre que precisar delas, pode usá-las no **FastAPI**. + +### Pydantic v1 `regex` em vez de `pattern` { #pydantic-v1-regex-instead-of-pattern } + +Antes da versão 2 do Pydantic e antes do FastAPI 0.100.0, o parâmetro se chamava `regex` em vez de `pattern`, mas agora está descontinuado. + +Você ainda pode ver algum código usando isso: + +//// tab | Pydantic v1 + +{* ../../docs_src/query_params_str_validations/tutorial004_regex_an_py310.py hl[11] *} + +//// + +Mas saiba que isso está descontinuado e deve ser atualizado para usar o novo parâmetro `pattern`. 🤓 + +## Valores padrão { #default-values } + +Você pode, claro, usar valores padrão diferentes de `None`. + +Digamos que você queira declarar o parâmetro de consulta `q` com `min_length` de `3` e ter um valor padrão de `"fixedquery"`: + +{* ../../docs_src/query_params_str_validations/tutorial005_an_py39.py hl[9] *} + +/// note | Nota + +Ter um valor padrão de qualquer tipo, incluindo `None`, torna o parâmetro opcional (não obrigatório). + +/// + +## Parâmetros obrigatórios { #required-parameters } + +Quando não precisamos declarar mais validações ou metadados, podemos tornar o parâmetro de consulta `q` obrigatório simplesmente não declarando um valor padrão, assim: ```Python q: str ``` -em vez desta: +em vez de: ```Python -q: Union[str, None] = None +q: str | None = None ``` -Mas agora nós o estamos declarando como `Query`, conforme abaixo: +Mas agora estamos declarando com `Query`, por exemplo assim: ```Python -q: Union[str, None] = Query(default=None, min_length=3) +q: Annotated[str | None, Query(min_length=3)] = None ``` -Então, quando você precisa declarar um parâmetro obrigatório utilizando o `Query`, você pode utilizar `...` como o primeiro argumento: +Então, quando você precisa declarar um valor como obrigatório usando `Query`, você pode simplesmente não declarar um valor padrão: -{* ../../docs_src/query_params_str_validations/tutorial006.py hl[7] *} +{* ../../docs_src/query_params_str_validations/tutorial006_an_py39.py hl[9] *} -/// info | Informação +### Obrigatório, pode ser `None` { #required-can-be-none } -Se você nunca viu os `...` antes: é um valor único especial, faz parte do Python e é chamado "Ellipsis". +Você pode declarar que um parâmetro pode aceitar `None`, mas que ainda assim é obrigatório. Isso forçaria os clientes a enviarem um valor, mesmo que o valor seja `None`. -/// +Para isso, você pode declarar que `None` é um tipo válido, mas simplesmente não declarar um valor padrão: -Dessa forma o **FastAPI** saberá que o parâmetro é obrigatório. +{* ../../docs_src/query_params_str_validations/tutorial006c_an_py310.py hl[9] *} -## Lista de parâmetros de consulta / múltiplos valores +## Lista de parâmetros de consulta / múltiplos valores { #query-parameter-list-multiple-values } -Quando você declara explicitamente um parâmetro com `Query` você pode declará-lo para receber uma lista de valores, ou podemos dizer, que irá receber mais de um valor. +Quando você define explicitamente um parâmetro de consulta com `Query`, você também pode declará-lo para receber uma lista de valores, ou seja, receber múltiplos valores. -Por exemplo, para declarar que o parâmetro `q` pode aparecer diversas vezes na URL, você escreveria: +Por exemplo, para declarar um parâmetro de consulta `q` que pode aparecer várias vezes na URL, você pode escrever: -{* ../../docs_src/query_params_str_validations/tutorial011.py hl[9] *} +{* ../../docs_src/query_params_str_validations/tutorial011_an_py310.py hl[9] *} -Então, com uma URL assim: +Então, com uma URL como: ``` http://localhost:8000/items/?q=foo&q=bar ``` -você receberá os múltiplos *parâmetros de consulta* `q` com os valores (`foo` e `bar`) em uma lista (`list`) Python dentro da *função de operação de rota*, no *parâmetro da função* `q`. +você receberá os múltiplos valores do *parâmetro de consulta* `q` (`foo` e `bar`) em uma `list` Python dentro da sua *função de operação de rota*, no *parâmetro da função* `q`. Assim, a resposta para essa URL seria: @@ -173,19 +295,19 @@ Assim, a resposta para essa URL seria: /// tip | Dica -Para declarar um parâmetro de consulta com o tipo `list`, como no exemplo acima, você precisa usar explicitamente o `Query`, caso contrário será interpretado como um corpo da requisição. +Para declarar um parâmetro de consulta com tipo `list`, como no exemplo acima, você precisa usar explicitamente `Query`, caso contrário seria interpretado como um corpo da requisição. /// -A documentação interativa da API irá atualizar de acordo, permitindo múltiplos valores: +A documentação interativa da API será atualizada de acordo, permitindo múltiplos valores: -### Lista de parâmetros de consulta / múltiplos valores por padrão +### Lista de parâmetros de consulta / múltiplos valores com valores padrão { #query-parameter-list-multiple-values-with-defaults } -E você também pode definir uma lista (`list`) de valores padrão caso nenhum seja informado: +Você também pode definir uma `list` de valores padrão caso nenhum seja fornecido: -{* ../../docs_src/query_params_str_validations/tutorial012.py hl[9] *} +{* ../../docs_src/query_params_str_validations/tutorial012_an_py39.py hl[9] *} Se você for até: @@ -193,7 +315,7 @@ Se você for até: http://localhost:8000/items/ ``` -O valor padrão de `q` será: `["foo", "bar"]` e sua resposta será: +o valor padrão de `q` será: `["foo", "bar"]` e sua resposta será: ```JSON { @@ -204,93 +326,163 @@ O valor padrão de `q` será: `["foo", "bar"]` e sua resposta será: } ``` -#### Usando `list` +#### Usando apenas `list` { #using-just-list } -Você também pode utilizar o tipo `list` diretamente em vez de `List[str]`: +Você também pode usar `list` diretamente em vez de `list[str]`: -{* ../../docs_src/query_params_str_validations/tutorial013.py hl[7] *} +{* ../../docs_src/query_params_str_validations/tutorial013_an_py39.py hl[9] *} -/// note | Observação +/// note | Nota -Tenha em mente que neste caso, o FastAPI não irá validar os conteúdos da lista. +Tenha em mente que, neste caso, o FastAPI não verificará o conteúdo da lista. -Por exemplo, um `List[int]` iria validar (e documentar) que os contéudos da lista são números inteiros. Mas apenas `list` não. +Por exemplo, `list[int]` verificaria (e documentaria) que os conteúdos da lista são inteiros. Mas `list` sozinho não. /// -## Declarando mais metadados +## Declare mais metadados { #declare-more-metadata } Você pode adicionar mais informações sobre o parâmetro. -Essa informações serão inclusas no esquema do OpenAPI e utilizado pela documentação interativa e ferramentas externas. +Essas informações serão incluídas no OpenAPI gerado e usadas pelas interfaces de documentação e por ferramentas externas. -/// note | Observação +/// note | Nota -Tenha em mente que cada ferramenta oferece diferentes níveis de suporte ao OpenAPI. +Tenha em mente que ferramentas diferentes podem ter níveis diferentes de suporte ao OpenAPI. -Algumas delas não exibem todas as informações extras que declaramos, ainda que na maioria dos casos, esses recursos estão planejados para desenvolvimento. +Algumas delas podem ainda não mostrar todas as informações extras declaradas, embora na maioria dos casos o recurso ausente já esteja planejado para desenvolvimento. /// Você pode adicionar um `title`: -{* ../../docs_src/query_params_str_validations/tutorial007.py hl[10] *} +{* ../../docs_src/query_params_str_validations/tutorial007_an_py310.py hl[10] *} E uma `description`: -{* ../../docs_src/query_params_str_validations/tutorial008.py hl[13] *} +{* ../../docs_src/query_params_str_validations/tutorial008_an_py310.py hl[14] *} -## Apelidos (alias) de parâmetros +## Parâmetros com alias { #alias-parameters } -Imagine que você queira que um parâmetro tenha o nome `item-query`. +Imagine que você queira que o parâmetro seja `item-query`. -Desta maneira: +Assim: ``` http://127.0.0.1:8000/items/?item-query=foobaritems ``` -Mas o nome `item-query` não é um nome de váriavel válido no Python. +Mas `item-query` não é um nome de variável Python válido. -O que mais se aproxima é `item_query`. +O mais próximo seria `item_query`. -Mas ainda você precisa que o nome seja exatamente `item-query`... +Mas você ainda precisa que seja exatamente `item-query`... -Então você pode declarar um `alias`, e esse apelido (alias) que será utilizado para encontrar o valor do parâmetro: +Então você pode declarar um `alias`, e esse alias será usado para encontrar o valor do parâmetro: -{* ../../docs_src/query_params_str_validations/tutorial009.py hl[9] *} +{* ../../docs_src/query_params_str_validations/tutorial009_an_py310.py hl[9] *} -## Parâmetros descontinuados +## Descontinuando parâmetros { #deprecating-parameters } -Agora vamos dizer que você não queria mais utilizar um parâmetro. +Agora digamos que você não gosta mais desse parâmetro. -Você tem que deixá-lo ativo por um tempo, já que existem clientes o utilizando. Mas você quer que a documentação deixe claro que este parâmetro será descontinuado. +Você tem que deixá-lo por um tempo, pois há clientes usando-o, mas quer que a documentação mostre claramente que ele está descontinuado. -Então você passa o parâmetro `deprecated=True` para `Query`: +Então passe o parâmetro `deprecated=True` para `Query`: -{* ../../docs_src/query_params_str_validations/tutorial010.py hl[18] *} +{* ../../docs_src/query_params_str_validations/tutorial010_an_py310.py hl[19] *} -Na documentação aparecerá assim: +A documentação vai mostrar assim: -## Recapitulando +## Excluir parâmetros do OpenAPI { #exclude-parameters-from-openapi } -Você pode adicionar validações e metadados adicionais aos seus parâmetros. +Para excluir um parâmetro de consulta do OpenAPI gerado (e portanto, dos sistemas de documentação automáticos), defina o parâmetro `include_in_schema` de `Query` como `False`: -Validações genéricas e metadados: +{* ../../docs_src/query_params_str_validations/tutorial014_an_py310.py hl[10] *} + +## Validação personalizada { #custom-validation } + +Podem existir casos em que você precise fazer alguma **validação personalizada** que não pode ser feita com os parâmetros mostrados acima. + +Nesses casos, você pode usar uma **função validadora personalizada** que é aplicada após a validação normal (por exemplo, depois de validar que o valor é uma `str`). + +Você pode fazer isso usando o `AfterValidator` do Pydantic dentro de `Annotated`. + +/// tip | Dica + +O Pydantic também tem `BeforeValidator` e outros. 🤓 + +/// + +Por exemplo, este validador personalizado verifica se o ID do item começa com `isbn-` para um número de livro ISBN ou com `imdb-` para um ID de URL de filme IMDB: + +{* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py hl[5,16:19,24] *} + +/// info | Informação + +Isso está disponível com a versão 2 do Pydantic ou superior. 😎 + +/// + +/// tip | Dica + +Se você precisar fazer qualquer tipo de validação que exija comunicação com algum **componente externo**, como um banco de dados ou outra API, você deve usar **Dependências do FastAPI** em vez disso; você aprenderá sobre elas mais adiante. + +Esses validadores personalizados são para coisas que podem ser verificadas **apenas** com os **mesmos dados** fornecidos na requisição. + +/// + +### Entenda esse código { #understand-that-code } + +O ponto importante é apenas usar **`AfterValidator` com uma função dentro de `Annotated`**. Sinta-se à vontade para pular esta parte. 🤸 + +--- + +Mas se você está curioso sobre este exemplo específico e ainda entretido, aqui vão alguns detalhes extras. + +#### String com `value.startswith()` { #string-with-value-startswith } + +Percebeu? Uma string usando `value.startswith()` pode receber uma tupla, e verificará cada valor na tupla: + +{* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py ln[16:19] hl[17] *} + +#### Um item aleatório { #a-random-item } + +Com `data.items()` obtemos um objeto iterável com tuplas contendo a chave e o valor de cada item do dicionário. + +Convertimos esse objeto iterável em uma `list` adequada com `list(data.items())`. + +Em seguida, com `random.choice()` podemos obter um **valor aleatório** da lista, então obtemos uma tupla com `(id, name)`. Será algo como `("imdb-tt0371724", "The Hitchhiker's Guide to the Galaxy")`. + +Depois **atribuímos esses dois valores** da tupla às variáveis `id` e `name`. + +Assim, se o usuário não fornecer um ID de item, ele ainda receberá uma sugestão aleatória. + +...fazemos tudo isso em **uma única linha simples**. 🤯 Você não ama Python? 🐍 + +{* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py ln[22:30] hl[29] *} + +## Recapitulando { #recap } + +Você pode declarar validações adicionais e metadados para seus parâmetros. + +Validações e metadados genéricos: * `alias` * `title` * `description` * `deprecated` -Validações específicas para textos: +Validações específicas para strings: * `min_length` * `max_length` -* `regex` +* `pattern` -Nesses exemplos você viu como declarar validações em valores do tipo `str`. +Validações personalizadas usando `AfterValidator`. -Leia os próximos capítulos para ver como declarar validação de outros tipos, como números. +Nestes exemplos você viu como declarar validações para valores `str`. + +Veja os próximos capítulos para aprender a declarar validações para outros tipos, como números. diff --git a/docs/pt/docs/tutorial/query-params.md b/docs/pt/docs/tutorial/query-params.md index 8199de5af..5a3fed035 100644 --- a/docs/pt/docs/tutorial/query-params.md +++ b/docs/pt/docs/tutorial/query-params.md @@ -1,4 +1,4 @@ -# Parâmetros de Consulta +# Parâmetros de Consulta { #query-parameters } Quando você declara outros parâmetros na função que não fazem parte dos parâmetros da rota, esses parâmetros são automaticamente interpretados como parâmetros de "consulta". @@ -28,7 +28,7 @@ Todo o processo que era aplicado para parâmetros de rota também é aplicado pa * Validação de dados * Documentação automática -## Valores padrão +## Valores padrão { #defaults } Como os parâmetros de consulta não são uma parte fixa da rota, eles podem ser opcionais e podem ter valores padrão. @@ -57,7 +57,7 @@ Os valores dos parâmetros na sua função serão: * `skip=20`: Por que você definiu isso na URL * `limit=10`: Por que esse era o valor padrão -## Parâmetros opcionais +## Parâmetros opcionais { #optional-parameters } Da mesma forma, você pode declarar parâmetros de consulta opcionais, definindo o valor padrão para `None`: @@ -65,13 +65,13 @@ Da mesma forma, você pode declarar parâmetros de consulta opcionais, definindo Nesse caso, o parâmetro da função `q` será opcional, e `None` será o padrão. -/// check | Verificar +/// check | Verifique Você também pode notar que o **FastAPI** é esperto o suficiente para perceber que o parâmetro da rota `item_id` é um parâmetro da rota, e `q` não é, portanto, `q` é o parâmetro de consulta. /// -## Conversão dos tipos de parâmetros de consulta +## Conversão dos tipos de parâmetros de consulta { #query-parameter-type-conversion } Você também pode declarar tipos `bool`, e eles serão convertidos: @@ -109,7 +109,7 @@ http://127.0.0.1:8000/items/foo?short=yes ou qualquer outra variação (tudo em maiúscula, primeira letra em maiúscula, etc), a sua função vai ver o parâmetro `short` com um valor `bool` de `True`. Caso contrário `False`. -## Múltiplos parâmetros de rota e consulta +## Múltiplos parâmetros de rota e consulta { #multiple-path-and-query-parameters } Você pode declarar múltiplos parâmetros de rota e parâmetros de consulta ao mesmo tempo, o **FastAPI** vai saber o quê é o quê. @@ -119,7 +119,7 @@ Eles serão detectados pelo nome: {* ../../docs_src/query_params/tutorial004_py310.py hl[6,8] *} -## Parâmetros de consulta obrigatórios +## Parâmetros de consulta obrigatórios { #required-query-parameters } Quando você declara um valor padrão para parâmetros que não são de rota (até agora, nós vimos apenas parâmetros de consulta), então eles não são obrigatórios. @@ -141,16 +141,17 @@ http://127.0.0.1:8000/items/foo-item ```JSON { - "detail": [ - { - "loc": [ - "query", - "needy" - ], - "msg": "field required", - "type": "value_error.missing" - } - ] + "detail": [ + { + "type": "missing", + "loc": [ + "query", + "needy" + ], + "msg": "Field required", + "input": null + } + ] } ``` @@ -181,6 +182,6 @@ Nesse caso, existem 3 parâmetros de consulta: /// tip | Dica -Você também poderia usar `Enum` da mesma forma que com [Path Parameters](path-params.md#valores-predefinidos){.internal-link target=_blank}. +Você também poderia usar `Enum` da mesma forma que com [Path Parameters](path-params.md#predefined-values){.internal-link target=_blank}. /// diff --git a/docs/pt/docs/tutorial/request-files.md b/docs/pt/docs/tutorial/request-files.md index c22c1c513..5d0891163 100644 --- a/docs/pt/docs/tutorial/request-files.md +++ b/docs/pt/docs/tutorial/request-files.md @@ -1,4 +1,4 @@ -# Arquivos de Requisição +# Arquivos de Requisição { #request-files } Você pode definir arquivos para serem enviados pelo cliente usando `File`. @@ -16,13 +16,13 @@ Isso é necessário, visto que os arquivos enviados são enviados como "dados de /// -## Importe `File` +## Importe `File` { #import-file } Importe `File` e `UploadFile` de `fastapi`: {* ../../docs_src/request_files/tutorial001_an_py39.py hl[3] *} -## Definir Parâmetros `File` +## Definir Parâmetros `File` { #define-file-parameters } Crie parâmetros de arquivo da mesma forma que você faria para `Body` ou `Form`: @@ -50,7 +50,7 @@ Mantenha em mente que isso significa que todo o conteúdo será armazenado na me Mas há muitos casos em que você pode se beneficiar do uso de `UploadFile`. -## Parâmetros de Arquivo com `UploadFile` +## Parâmetros de Arquivo com `UploadFile` { #file-parameters-with-uploadfile } Defina um parâmetro de arquivo com um tipo de `UploadFile`: @@ -66,12 +66,12 @@ Utilizar `UploadFile` tem várias vantagens sobre `bytes`: * Ele tem uma file-like interface `assíncrona`. * Ele expõe um objeto python `SpooledTemporaryFile` que você pode passar diretamente para outras bibliotecas que esperam um objeto semelhante a um arquivo("file-like"). -### `UploadFile` +### `UploadFile` { #uploadfile } `UploadFile` tem os seguintes atributos: * `filename`: Uma `str` com o nome do arquivo original que foi enviado (por exemplo, `myimage.jpg`). -* `content_type`: Uma `str` com o tipo de conteúdo (tipo MIME / tipo de mídia) (por exemplo, `image/jpeg`). +* `content_type`: Uma `str` com o tipo de conteúdo (MIME type / media type) (por exemplo, `image/jpeg`). * `file`: Um `SpooledTemporaryFile` (um file-like objeto). Este é o objeto de arquivo Python que você pode passar diretamente para outras funções ou bibliotecas que esperam um objeto semelhante a um arquivo("file-like"). `UploadFile` tem os seguintes métodos `assíncronos`. Todos eles chamam os métodos de arquivo correspondentes por baixo dos panos (usando o `SpooledTemporaryFile` interno). @@ -105,11 +105,11 @@ Quando você usa os métodos `async`, o **FastAPI** executa os métodos de arqui /// note | Detalhes Técnicos do Starlette -O `UploadFile` do ***FastAPI** herda diretamente do `UploadFile` do **Starlette** , mas adiciona algumas partes necessárias para torná-lo compatível com o **Pydantic** e as outras partes do FastAPI. +O `UploadFile` do **FastAPI** herda diretamente do `UploadFile` do **Starlette**, mas adiciona algumas partes necessárias para torná-lo compatível com o **Pydantic** e as outras partes do FastAPI. /// -## O que é "Form Data" +## O que é "Form Data" { #what-is-form-data } O jeito que os formulários HTML (`
`) enviam os dados para o servidor normalmente usa uma codificação "especial" para esses dados, a qual é diferente do JSON. @@ -117,15 +117,15 @@ O jeito que os formulários HTML (`
`) enviam os dados para o servid /// note | Detalhes Técnicos -Dados de formulários normalmente são codificados usando o "media type" (tipo de mídia) `application/x-www-form-urlencoded` quando não incluem arquivos. +Dados de formulários normalmente são codificados usando o "media type" `application/x-www-form-urlencoded` quando não incluem arquivos. Mas quando o formulário inclui arquivos, ele é codificado como `multipart/form-data`. Se você usar `File`, o **FastAPI** saberá que tem que pegar os arquivos da parte correta do corpo da requisição. -Se você quiser ler mais sobre essas codificações e campos de formulário, vá para a MDN web docs para POST. +Se você quiser ler mais sobre essas codificações e campos de formulário, vá para a MDN web docs para POST. /// -/// warning | Aviso +/// warning | Atenção Você pode declarar múltiplos parâmetros `File` e `Form` em uma *operação de rota*, mas você não pode declarar campos `Body` que você espera receber como JSON, pois a requisição terá o corpo codificado usando `multipart/form-data` ao invés de `application/json`. @@ -133,19 +133,19 @@ Isso não é uma limitação do **FastAPI**, é parte do protocolo HTTP. /// -## Upload de Arquivo Opcional +## Upload de Arquivo Opcional { #optional-file-upload } Você pode tornar um arquivo opcional usando anotações de tipo padrão e definindo um valor padrão de `None`: {* ../../docs_src/request_files/tutorial001_02_an_py310.py hl[9,17] *} -## `UploadFile` com Metadados Adicionais +## `UploadFile` com Metadados Adicionais { #uploadfile-with-additional-metadata } Você também pode usar `File()` com `UploadFile`, por exemplo, para definir metadados adicionais: {* ../../docs_src/request_files/tutorial001_03_an_py39.py hl[9,15] *} -## Uploads de Múltiplos Arquivos +## Uploads de Múltiplos Arquivos { #multiple-file-uploads } É possível realizar o upload de vários arquivos ao mesmo tempo. @@ -165,12 +165,12 @@ Você pode também pode usar `from starlette.responses import HTMLResponse`. /// -### Uploads de Múltiplos Arquivos com Metadados Adicionais +### Uploads de Múltiplos Arquivos com Metadados Adicionais { #multiple-file-uploads-with-additional-metadata } Da mesma forma de antes, você pode usar `File()` para definir parâmetros adicionais, mesmo para `UploadFile`: {* ../../docs_src/request_files/tutorial003_an_py39.py hl[11,18:20] *} -## Recapitulando +## Recapitulando { #recap } Utilize `File`, `bytes` e `UploadFile` para declarar arquivos a serem enviados na requisição, enviados como dados de formulário. diff --git a/docs/pt/docs/tutorial/request-form-models.md b/docs/pt/docs/tutorial/request-form-models.md index ea0e63d38..8eeffac2a 100644 --- a/docs/pt/docs/tutorial/request-form-models.md +++ b/docs/pt/docs/tutorial/request-form-models.md @@ -1,4 +1,4 @@ -# Modelos de Formulários +# Modelos de Formulários { #form-models } Você pode utilizar **Modelos Pydantic** para declarar **campos de formulários** no FastAPI. @@ -20,7 +20,7 @@ Isto é suportado desde a versão `0.113.0` do FastAPI. 🤓 /// -## Modelos Pydantic para Formulários +## Modelos Pydantic para Formulários { #pydantic-models-for-forms } Você precisa apenas declarar um **modelo Pydantic** com os campos que deseja receber como **campos de formulários**, e então declarar o parâmetro como um `Form`: @@ -28,7 +28,7 @@ Você precisa apenas declarar um **modelo Pydantic** com os campos que deseja re O **FastAPI** irá **extrair** as informações para **cada campo** dos **dados do formulário** na requisição e dar para você o modelo Pydantic que você definiu. -## Confira os Documentos +## Confira os Documentos { #check-the-docs } Você pode verificar na UI de documentação em `/docs`: @@ -36,7 +36,7 @@ Você pode verificar na UI de documentação em `/docs`: -## Proibir Campos Extras de Formulários +## Proibir Campos Extras de Formulários { #forbid-extra-form-fields } Em alguns casos de uso especiais (provavelmente não muito comum), você pode desejar **restringir** os campos do formulário para aceitar apenas os declarados no modelo Pydantic. E **proibir** qualquer campo **extra**. @@ -73,6 +73,6 @@ Ele receberá um retorno de erro informando-o que o campo `extra` não é permit } ``` -## Resumo +## Resumo { #summary } Você pode utilizar modelos Pydantic para declarar campos de formulários no FastAPI. 😎 diff --git a/docs/pt/docs/tutorial/request-forms-and-files.md b/docs/pt/docs/tutorial/request-forms-and-files.md index b08d87013..277fc2f60 100644 --- a/docs/pt/docs/tutorial/request-forms-and-files.md +++ b/docs/pt/docs/tutorial/request-forms-and-files.md @@ -1,4 +1,4 @@ -# Formulários e Arquivos da Requisição +# Formulários e Arquivos da Requisição { #request-forms-and-files } Você pode definir arquivos e campos de formulário ao mesmo tempo usando `File` e `Form`. @@ -6,32 +6,36 @@ Você pode definir arquivos e campos de formulário ao mesmo tempo usando `File` Para receber arquivos carregados e/ou dados de formulário, primeiro instale `python-multipart`. -Por exemplo: `pip install python-multipart`. +Certifique-se de criar um [ambiente virtual](../virtual-environments.md){.internal-link target=_blank}, ativá-lo e então instalar, por exemplo: + +```console +$ pip install python-multipart +``` /// -## Importe `File` e `Form` +## Importe `File` e `Form` { #import-file-and-form } -{* ../../docs_src/request_forms_and_files/tutorial001.py hl[1] *} +{* ../../docs_src/request_forms_and_files/tutorial001_an_py39.py hl[3] *} -## Defina parâmetros de `File` e `Form` +## Defina parâmetros de `File` e `Form` { #define-file-and-form-parameters } Crie parâmetros de arquivo e formulário da mesma forma que você faria para `Body` ou `Query`: -{* ../../docs_src/request_forms_and_files/tutorial001.py hl[8] *} +{* ../../docs_src/request_forms_and_files/tutorial001_an_py39.py hl[10:12] *} Os arquivos e campos de formulário serão carregados como dados de formulário e você receberá os arquivos e campos de formulário. E você pode declarar alguns dos arquivos como `bytes` e alguns como `UploadFile`. -/// warning | Aviso +/// warning | Atenção Você pode declarar vários parâmetros `File` e `Form` em uma *operação de caminho*, mas não é possível declarar campos `Body` para receber como JSON, pois a requisição terá o corpo codificado usando `multipart/form-data` ao invés de `application/json`. -Isso não é uma limitação do **FastAPI** , é parte do protocolo HTTP. +Isso não é uma limitação do **FastAPI**, é parte do protocolo HTTP. /// -## Recapitulando +## Recapitulando { #recap } Usar `File` e `Form` juntos quando precisar receber dados e arquivos na mesma requisição. diff --git a/docs/pt/docs/tutorial/request-forms.md b/docs/pt/docs/tutorial/request-forms.md index 572ddf003..faa50bcbf 100644 --- a/docs/pt/docs/tutorial/request-forms.md +++ b/docs/pt/docs/tutorial/request-forms.md @@ -1,12 +1,12 @@ -# Dados do formulário +# Dados do formulário { #form-data } -Quando você precisar receber campos de formulário ao invés de JSON, você pode usar `Form`. +Quando você precisar receber campos de formulário em vez de JSON, você pode usar `Form`. /// info | Informação Para usar formulários, primeiro instale `python-multipart`. -Lembre-se de criar um [ambiente virtual](../virtual-environments.md){.internal-link target=_blank}, ativá-lo e então instalar a dependência, por exemplo: +Certifique-se de criar um [ambiente virtual](../virtual-environments.md){.internal-link target=_blank}, ativá-lo e então instalá-lo, por exemplo: ```console $ pip install python-multipart @@ -14,23 +14,23 @@ $ pip install python-multipart /// -## Importe `Form` +## Importe `Form` { #import-form } Importe `Form` de `fastapi`: -{* ../../docs_src/request_forms/tutorial001.py hl[1] *} +{* ../../docs_src/request_forms/tutorial001_an_py39.py hl[3] *} -## Declare parâmetros de `Form` +## Defina parâmetros de `Form` { #define-form-parameters } Crie parâmetros de formulário da mesma forma que você faria para `Body` ou `Query`: -{* ../../docs_src/request_forms/tutorial001.py hl[7] *} +{* ../../docs_src/request_forms/tutorial001_an_py39.py hl[9] *} Por exemplo, em uma das maneiras que a especificação OAuth2 pode ser usada (chamada "fluxo de senha"), é necessário enviar um `username` e uma `password` como campos do formulário. -A spec exige que os campos sejam exatamente nomeados como `username` e `password` e sejam enviados como campos de formulário, não JSON. +A spec exige que os campos sejam exatamente nomeados como `username` e `password` e sejam enviados como campos de formulário, não JSON. -Com `Form` você pode declarar os mesmos metadados e validação que com `Body` (e `Query`, `Path`, `Cookie`). +Com `Form` você pode declarar as mesmas configurações que com `Body` (e `Query`, `Path`, `Cookie`), incluindo validação, exemplos, um alias (por exemplo, `user-name` em vez de `username`), etc. /// info | Informação @@ -44,30 +44,30 @@ Para declarar corpos de formulário, você precisa usar `Form` explicitamente, p /// -## Sobre "Campos de formulário" +## Sobre "Campos de formulário" { #about-form-fields } A forma como os formulários HTML (`
`) enviam os dados para o servidor normalmente usa uma codificação "especial" para esses dados, é diferente do JSON. O **FastAPI** fará a leitura desses dados no lugar certo em vez de JSON. -/// note | Detalhes técnicos +/// note | Detalhes Técnicos -Os dados dos formulários são normalmente codificados usando o "tipo de mídia" `application/x-www-form-urlencoded`. +Os dados dos formulários são normalmente codificados usando o "media type" `application/x-www-form-urlencoded`. - Mas quando o formulário inclui arquivos, ele é codificado como `multipart/form-data`. Você lerá sobre como lidar com arquivos no próximo capítulo. +Mas quando o formulário inclui arquivos, ele é codificado como `multipart/form-data`. Você lerá sobre como lidar com arquivos no próximo capítulo. -Se você quiser ler mais sobre essas codificações e campos de formulário, vá para o MDN web docs para POST. +Se você quiser ler mais sobre essas codificações e campos de formulário, vá para o MDN web docs para POST. /// -/// warning | Aviso +/// warning | Atenção -Você pode declarar vários parâmetros `Form` em uma *operação de caminho*, mas não pode declarar campos `Body` que espera receber como JSON, pois a solicitação terá o corpo codificado usando `application/x-www- form-urlencoded` em vez de `application/json`. +Você pode declarar vários parâmetros `Form` em uma *operação de rota*, mas não pode declarar campos `Body` que espera receber como JSON, pois a requisição terá o corpo codificado usando `application/x-www-form-urlencoded` em vez de `application/json`. -Esta não é uma limitação do **FastAPI**, é parte do protocolo HTTP. +Isso não é uma limitação do **FastAPI**, é parte do protocolo HTTP. /// -## Recapitulando +## Recapitulando { #recap } Use `Form` para declarar os parâmetros de entrada de dados de formulário. diff --git a/docs/pt/docs/tutorial/request_files.md b/docs/pt/docs/tutorial/request_files.md deleted file mode 100644 index 15c1ad825..000000000 --- a/docs/pt/docs/tutorial/request_files.md +++ /dev/null @@ -1,172 +0,0 @@ -# Arquivos de Requisição - -Você pode definir arquivos para serem enviados para o cliente utilizando `File`. - -/// info - -Para receber arquivos compartilhados, primeiro instale `python-multipart`. - -E.g. `pip install python-multipart`. - -Isso se deve por que arquivos enviados são enviados como "dados de formulário". - -/// - -## Importe `File` - -Importe `File` e `UploadFile` do `fastapi`: - -{* ../../docs_src/request_files/tutorial001_an_py39.py hl[3] *} - -## Defina os parâmetros de `File` - -Cria os parâmetros do arquivo da mesma forma que você faria para `Body` ou `Form`: - -{* ../../docs_src/request_files/tutorial001_an_py39.py hl[9] *} - -/// info | Informação - -`File` é uma classe que herda diretamente de `Form`. - -Mas lembre-se que quando você importa `Query`,`Path`, `File`, entre outros, do `fastapi`, essas são na verdade funções que retornam classes especiais. - -/// - -/// tip | Dica - -Para declarar o corpo de arquivos, você precisa utilizar `File`, do contrário os parâmetros seriam interpretados como parâmetros de consulta ou corpo (JSON) da requisição. - -/// - -Os arquivos serão enviados como "form data". - -Se você declarar o tipo do seu parâmetro na sua *função de operação de rota* como `bytes`, o **FastAPI** irá ler o arquivo para você e você receberá o conteúdo como `bytes`. - -Lembre-se que isso significa que o conteúdo inteiro será armazenado em memória. Isso funciona bem para arquivos pequenos. - -Mas existem vários casos em que você pode se beneficiar ao usar `UploadFile`. - -## Parâmetros de arquivo com `UploadFile` - -Defina um parâmetro de arquivo com o tipo `UploadFile` - -{* ../../docs_src/request_files/tutorial001_an_py39.py hl[14] *} - -Utilizando `UploadFile` tem várias vantagens sobre `bytes`: - -* Você não precisa utilizar `File()` como o valor padrão do parâmetro. -* A classe utiliza um arquivo em "spool": - * Um arquivo guardado em memória até um tamanho máximo, depois desse limite ele é guardado em disco. -* Isso significa que a classe funciona bem com arquivos grandes como imagens, vídeos, binários extensos, etc. Sem consumir toda a memória. -* Você pode obter metadados do arquivo enviado. -* Ela possui uma interface semelhante a arquivos `async`. -* Ela expõe um objeto python `SpooledTemporaryFile` que você pode repassar para bibliotecas que esperam um objeto com comportamento de arquivo. - -### `UploadFile` - -`UploadFile` tem os seguintes atributos: - -* `filename`: Uma string (`str`) com o nome original do arquivo enviado (e.g. `myimage.jpg`). -* `content-type`: Uma `str` com o tipo do conteúdo (tipo MIME / media) (e.g. `image/jpeg`). -* `file`: Um objeto do tipo `SpooledTemporaryFile` (um objeto file-like). O arquivo propriamente dito que você pode passar diretamente para outras funções ou bibliotecas que esperam um objeto "file-like". - -`UploadFile` tem os seguintes métodos `async`. Todos eles chamam os métodos de arquivos por baixo dos panos (usando o objeto `SpooledTemporaryFile` interno). - -* `write(data)`: escreve dados (`data`) em `str` ou `bytes` no arquivo. -* `read(size)`: Lê um número de bytes/caracteres de acordo com a quantidade `size` (`int`). -* `seek(offset)`: Navega para o byte na posição `offset` (`int`) do arquivo. - * E.g., `await myfile.seek(0)` navegaria para o ínicio do arquivo. - * Isso é especialmente útil se você executar `await myfile.read()` uma vez e depois precisar ler os conteúdos do arquivo de novo. -* `close()`: Fecha o arquivo. - -Como todos esses métodos são assíncronos (`async`) você precisa esperar ("await") por eles. - -Por exemplo, dentro de uma *função de operação de rota* assíncrona você pode obter os conteúdos com: - -```Python -contents = await myfile.read() -``` - -Se você estiver dentro de uma *função de operação de rota* definida normalmente com `def`, você pode acessar `UploadFile.file` diretamente, por exemplo: - -```Python -contents = myfile.file.read() -``` - -/// note | Detalhes técnicos do `async` - -Quando você utiliza métodos assíncronos, o **FastAPI** executa os métodos do arquivo em uma threadpool e espera por eles. - -/// - -/// note | Detalhes técnicos do Starlette - -O `UploadFile` do **FastAPI** herda diretamente do `UploadFile` do **Starlette**, mas adiciona algumas funcionalidades necessárias para ser compatível com o **Pydantic** - -/// - -## O que é "Form Data" - -A forma como formulários HTML(`
`) enviam dados para o servidor normalmente utilizam uma codificação "especial" para esses dados, que é diferente do JSON. - -O **FastAPI** garante que os dados serão lidos da forma correta, em vez do JSON. - -/// note | Detalhes Técnicos - -Dados vindos de formulários geralmente tem a codificação com o "media type" `application/x-www-form-urlencoded` quando estes não incluem arquivos. - -Mas quando os dados incluem arquivos, eles são codificados como `multipart/form-data`. Se você utilizar `File`, **FastAPI** saberá que deve receber os arquivos da parte correta do corpo da requisição. - -Se você quer ler mais sobre essas codificações e campos de formulário, veja a documentação online da MDN sobre POST . - -/// - -/// warning | Aviso - -Você pode declarar múltiplos parâmetros `File` e `Form` em uma *operação de rota*, mas você não pode declarar campos `Body`que seriam recebidos como JSON junto desses parâmetros, por que a codificação do corpo da requisição será `multipart/form-data` em vez de `application/json`. - -Isso não é uma limitação do **FastAPI**, é uma parte do protocolo HTTP. - -/// - -## Arquivo de upload opcional - -Você pode definir um arquivo como opcional utilizando as anotações de tipo padrão e definindo o valor padrão como `None`: - -{* ../../docs_src/request_files/tutorial001_02_an_py310.py hl[9,17] *} - -## `UploadFile` com Metadados Adicionais - -Você também pode utilizar `File()` com `UploadFile`, por exemplo, para definir metadados adicionais: - -{* ../../docs_src/request_files/tutorial001_03_an_py39.py hl[9,15] *} - -## Envio de Múltiplos Arquivos - -É possível enviar múltiplos arquivos ao mesmo tmepo. - -Ele ficam associados ao mesmo "campo do formulário" enviado com "form data". - -Para usar isso, declare uma lista de `bytes` ou `UploadFile`: - -{* ../../docs_src/request_files/tutorial002_an_py39.py hl[10,15] *} - -Você irá receber, como delcarado uma lista (`list`) de `bytes` ou `UploadFile`s, - -/// note | Detalhes Técnicos - -Você também poderia utilizar `from starlette.responses import HTMLResponse`. - -O **FastAPI** fornece as mesmas `starlette.responses` como `fastapi.responses` apenas como um facilitador para você, desenvolvedor. Mas a maior parte das respostas vem diretamente do Starlette. - -/// - -### Enviando Múltiplos Arquivos com Metadados Adicionais - -E da mesma forma que antes, você pode utilizar `File()` para definir parâmetros adicionais, até mesmo para `UploadFile`: - -{* ../../docs_src/request_files/tutorial003_an_py39.py hl[11,18:20] *} - -## Recapitulando - -Use `File`, `bytes` e `UploadFile` para declarar arquivos que serão enviados na requisição, enviados como dados do formulário. diff --git a/docs/pt/docs/tutorial/response-model.md b/docs/pt/docs/tutorial/response-model.md index 6726a20a7..5958240e4 100644 --- a/docs/pt/docs/tutorial/response-model.md +++ b/docs/pt/docs/tutorial/response-model.md @@ -1,6 +1,6 @@ -# Modelo de resposta - Tipo de retorno +# Modelo de resposta - Tipo de retorno { #response-model-return-type } -Você pode declarar o tipo usado para a resposta anotando o **tipo de retorno** *da função de operação de rota*. +Você pode declarar o tipo usado para a resposta anotando o **tipo de retorno** da *função de operação de rota*. Você pode usar **anotações de tipo** da mesma forma que usaria para dados de entrada em **parâmetros** de função, você pode usar modelos Pydantic, listas, dicionários, valores escalares como inteiros, booleanos, etc. @@ -10,7 +10,7 @@ O FastAPI usará este tipo de retorno para: * **Validar** os dados retornados. * Se os dados forem inválidos (por exemplo, se estiver faltando um campo), significa que o código do *seu* aplicativo está quebrado, não retornando o que deveria, e retornará um erro de servidor em vez de retornar dados incorretos. Dessa forma, você e seus clientes podem ter certeza de que receberão os dados e o formato de dados esperados. -* Adicionar um **Esquema JSON** para a resposta, na *operação de rota* do OpenAPI. +* Adicionar um **JSON Schema** para a resposta, na *operação de rota* do OpenAPI. * Isso será usado pela **documentação automática**. * Também será usado por ferramentas de geração automática de código do cliente. @@ -19,7 +19,7 @@ Mas o mais importante: * Ele **limitará e filtrará** os dados de saída para o que está definido no tipo de retorno. * Isso é particularmente importante para a **segurança**, veremos mais sobre isso abaixo. -## Parâmetro `response_model` +## Parâmetro `response_model` { #response-model-parameter } Existem alguns casos em que você precisa ou deseja retornar alguns dados que não são exatamente o que o tipo declara. @@ -27,7 +27,7 @@ Por exemplo, você pode querer **retornar um dicionário** ou um objeto de banco Se você adicionasse a anotação do tipo de retorno, ferramentas e editores reclamariam com um erro (correto) informando que sua função está retornando um tipo (por exemplo, um dict) diferente do que você declarou (por exemplo, um modelo Pydantic). -Nesses casos, você pode usar o parâmetro `response_model` do *decorador de operação de rota* em vez do tipo de retorno. +Nesses casos, você pode usar o parâmetro `response_model` do *decorador de operação de rota* em vez do tipo de retorno. Você pode usar o parâmetro `response_model` em qualquer uma das *operações de rota*: @@ -45,7 +45,7 @@ Observe que `response_model` é um parâmetro do método "decorator" (`get`, `po /// -`response_model` recebe o mesmo tipo que você declararia para um campo de modelo Pydantic, então, pode ser um modelo Pydantic, mas também pode ser, por exemplo, uma `lista` de modelos Pydantic, como `List[Item]`. +`response_model` recebe o mesmo tipo que você declararia para um campo de modelo Pydantic, então, pode ser um modelo Pydantic, mas também pode ser, por exemplo, uma `list` de modelos Pydantic, como `List[Item]`. O FastAPI usará este `response_model` para fazer toda a documentação de dados, validação, etc. e também para **converter e filtrar os dados de saída** para sua declaração de tipo. @@ -57,7 +57,7 @@ Dessa forma, você diz ao editor que está retornando qualquer coisa intencional /// -### Prioridade `response_model` +### Prioridade `response_model` { #response-model-priority } Se você declarar tanto um tipo de retorno quanto um `response_model`, o `response_model` terá prioridade e será usado pelo FastAPI. @@ -65,7 +65,7 @@ Dessa forma, você pode adicionar anotações de tipo corretas às suas funçõe Você também pode usar `response_model=None` para desabilitar a criação de um modelo de resposta para essa *operação de rota*, você pode precisar fazer isso se estiver adicionando anotações de tipo para coisas que não são campos Pydantic válidos, você verá um exemplo disso em uma das seções abaixo. -## Retorna os mesmos dados de entrada +## Retorne os mesmos dados de entrada { #return-the-same-input-data } Aqui estamos declarando um modelo `UserIn`, ele conterá uma senha em texto simples: @@ -99,13 +99,13 @@ Neste caso, pode não ser um problema, porque é o mesmo usuário enviando a sen Mas se usarmos o mesmo modelo para outra *operação de rota*, poderíamos estar enviando as senhas dos nossos usuários para todos os clientes. -/// danger | Perigo +/// danger | Cuidado Nunca armazene a senha simples de um usuário ou envie-a em uma resposta como esta, a menos que você saiba todas as ressalvas e saiba o que está fazendo. /// -## Adicionar um modelo de saída +## Adicione um modelo de saída { #add-an-output-model } Podemos, em vez disso, criar um modelo de entrada com a senha em texto simples e um modelo de saída sem ela: @@ -121,7 +121,7 @@ Aqui, embora nossa *função de operação de rota* esteja retornando o mesmo us Então, **FastAPI** cuidará de filtrar todos os dados que não são declarados no modelo de saída (usando Pydantic). -### `response_model` ou Tipo de Retorno +### `response_model` ou Tipo de Retorno { #response-model-or-return-type } Neste caso, como os dois modelos são diferentes, se anotássemos o tipo de retorno da função como `UserOut`, o editor e as ferramentas reclamariam que estamos retornando um tipo inválido, pois são classes diferentes. @@ -129,7 +129,7 @@ Neste caso, como os dois modelos são diferentes, se anotássemos o tipo de reto ...mas continue lendo abaixo para ver como superar isso. -## Tipo de Retorno e Filtragem de Dados +## Tipo de Retorno e Filtragem de Dados { #return-type-and-data-filtering } Vamos continuar do exemplo anterior. Queríamos **anotar a função com um tipo**, mas queríamos poder retornar da função algo que realmente incluísse **mais dados**. @@ -147,7 +147,7 @@ Com isso, temos suporte de ferramentas, de editores e mypy, pois este código es Como isso funciona? Vamos verificar. 🤓 -### Anotações de tipo e ferramentas +### Anotações de tipo e ferramentas { #type-annotations-and-tooling } Primeiro, vamos ver como editores, mypy e outras ferramentas veriam isso. @@ -157,7 +157,7 @@ Anotamos o tipo de retorno da função como `BaseUser`, mas na verdade estamos r O editor, mypy e outras ferramentas não reclamarão disso porque, em termos de digitação, `UserIn` é uma subclasse de `BaseUser`, o que significa que é um tipo *válido* quando o que é esperado é qualquer coisa que seja um `BaseUser`. -### Filtragem de dados FastAPI +### Filtragem de dados FastAPI { #fastapi-data-filtering } Agora, para FastAPI, ele verá o tipo de retorno e garantirá que o que você retornar inclua **apenas** os campos que são declarados no tipo. @@ -165,7 +165,7 @@ O FastAPI faz várias coisas internamente com o Pydantic para garantir que essas Dessa forma, você pode obter o melhor dos dois mundos: anotações de tipo com **suporte a ferramentas** e **filtragem de dados**. -## Veja na documentação +## Veja na documentação { #see-it-in-the-docs } Quando você vê a documentação automática, pode verificar se o modelo de entrada e o modelo de saída terão seus próprios esquemas JSON: @@ -175,13 +175,13 @@ E ambos os modelos serão usados ​​para a documentação interativa da API: -## Outras anotações de tipo de retorno +## Outras anotações de tipo de retorno { #other-return-type-annotations } Pode haver casos em que você retorna algo que não é um campo Pydantic válido e anota na função, apenas para obter o suporte fornecido pelas ferramentas (o editor, mypy, etc). -### Retornar uma resposta diretamente +### Retorne uma Response diretamente { #return-a-response-directly } -O caso mais comum seria [retornar uma resposta diretamente, conforme explicado posteriormente na documentação avançada](../advanced/response-directly.md){.internal-link target=_blank}. +O caso mais comum seria [retornar uma Response diretamente, conforme explicado posteriormente na documentação avançada](../advanced/response-directly.md){.internal-link target=_blank}. {* ../../docs_src/response_model/tutorial003_02.py hl[8,10:11] *} @@ -189,7 +189,7 @@ Este caso simples é tratado automaticamente pelo FastAPI porque a anotação do E as ferramentas também ficarão felizes porque `RedirectResponse` e ​​`JSONResponse` são subclasses de `Response`, então a anotação de tipo está correta. -### Anotar uma subclasse de resposta +### Anote uma subclasse de Response { #annotate-a-response-subclass } Você também pode usar uma subclasse de `Response` na anotação de tipo: @@ -197,7 +197,7 @@ Você também pode usar uma subclasse de `Response` na anotação de tipo: Isso também funcionará porque `RedirectResponse` é uma subclasse de `Response`, e o FastAPI tratará automaticamente este caso simples. -### Anotações de Tipo de Retorno Inválido +### Anotações de Tipo de Retorno Inválido { #invalid-return-type-annotations } Mas quando você retorna algum outro objeto arbitrário que não é um tipo Pydantic válido (por exemplo, um objeto de banco de dados) e você o anota dessa forma na função, o FastAPI tentará criar um modelo de resposta Pydantic a partir dessa anotação de tipo e falhará. @@ -205,9 +205,9 @@ O mesmo aconteceria se você tivesse algo como uma /// note | Nota - +Não importa o que você digite no formulário, ainda não vai funcionar. Mas nós vamos chegar lá. /// - Não importa o que você digita no formulário, não vai funcionar ainda. Mas nós vamos chegar lá. +Claro que este não é o frontend para os usuários finais, mas é uma ótima ferramenta automática para documentar interativamente toda a sua API. -Claro que este não é o frontend para os usuários finais, mas é uma ótima ferramenta automática para documentar interativamente toda sua API. +Pode ser usada pelo time de frontend (que pode ser você mesmo). -Pode ser usado pelo time de frontend (que pode ser você no caso). +Pode ser usada por aplicações e sistemas de terceiros. -Pode ser usado por aplicações e sistemas third party (de terceiros). +E também pode ser usada por você mesmo, para depurar, verificar e testar a mesma aplicação. -E também pode ser usada por você mesmo, para debugar, checar e testar a mesma aplicação. - -## O Fluxo da `senha` +## O fluxo de `password` { #the-password-flow } Agora vamos voltar um pouco e entender o que é isso tudo. -O "fluxo" da `senha` é um dos caminhos ("fluxos") definidos no OAuth2, para lidar com a segurança e autenticação. +O "fluxo" `password` é uma das formas ("fluxos") definidas no OAuth2 para lidar com segurança e autenticação. -OAuth2 foi projetado para que o backend ou a API pudesse ser independente do servidor que autentica o usuário. +O OAuth2 foi projetado para que o backend ou a API pudesse ser independente do servidor que autentica o usuário. -Mas nesse caso, a mesma aplicação **FastAPI** irá lidar com a API e a autenticação. +Mas, neste caso, a mesma aplicação **FastAPI** irá lidar com a API e com a autenticação. Então, vamos rever de um ponto de vista simplificado: -* O usuário digita o `username` e a `senha` no frontend e aperta `Enter`. -* O frontend (rodando no browser do usuário) manda o `username` e a `senha` para uma URL específica na sua API (declarada com `tokenUrl="token"`). -* A API checa aquele `username` e `senha`, e responde com um "token" (nós não implementamos nada disso ainda). - * Um "token" é apenas uma string com algum conteúdo que nós podemos utilizar mais tarde para verificar o usuário. - * Normalmente, um token é definido para expirar depois de um tempo. - * Então, o usuário terá que se logar de novo depois de um tempo. - * E se o token for roubado, o risco é menor. Não é como se fosse uma chave permanente que vai funcionar para sempre (na maioria dos casos). - * O frontend armazena aquele token temporariamente em algum lugar. - * O usuário clica no frontend para ir à outra seção daquele frontend do aplicativo web. - * O frontend precisa buscar mais dados daquela API. - * Mas precisa de autenticação para aquele endpoint em específico. - * Então, para autenticar com nossa API, ele manda um header de `Autorização` com o valor `Bearer` mais o token. - * Se o token contém `foobar`, o conteúdo do header de `Autorização` será: `Bearer foobar`. +* O usuário digita o `username` e o `password` no frontend e pressiona `Enter`. +* O frontend (rodando no navegador do usuário) envia esse `username` e `password` para uma URL específica na nossa API (declarada com `tokenUrl="token"`). +* A API verifica esse `username` e `password`, e responde com um "token" (ainda não implementamos nada disso). + * Um "token" é apenas uma string com algum conteúdo que podemos usar depois para verificar esse usuário. + * Normalmente, um token é definido para expirar depois de algum tempo. + * Então, o usuário terá que fazer login novamente em algum momento. + * E se o token for roubado, o risco é menor. Não é como uma chave permanente que funcionará para sempre (na maioria dos casos). +* O frontend armazena esse token temporariamente em algum lugar. +* O usuário clica no frontend para ir para outra seção do aplicativo web. +* O frontend precisa buscar mais dados da API. + * Mas precisa de autenticação para aquele endpoint específico. + * Então, para autenticar com nossa API, ele envia um header `Authorization` com o valor `Bearer ` mais o token. + * Se o token contém `foobar`, o conteúdo do header `Authorization` seria: `Bearer foobar`. -## **FastAPI**'s `OAuth2PasswordBearer` +## O `OAuth2PasswordBearer` do **FastAPI** { #fastapis-oauth2passwordbearer } -**FastAPI** fornece várias ferramentas, em diferentes níveis de abstração, para implementar esses recursos de segurança. +O **FastAPI** fornece várias ferramentas, em diferentes níveis de abstração, para implementar essas funcionalidades de segurança. -Neste exemplo, nós vamos usar o **OAuth2** com o fluxo de **Senha**, usando um token **Bearer**. Fazemos isso usando a classe `OAuth2PasswordBearer`. +Neste exemplo, vamos usar **OAuth2**, com o fluxo **Password**, usando um token **Bearer**. Fazemos isso usando a classe `OAuth2PasswordBearer`. -/// info | informação +/// info | Informação +Um token "bearer" não é a única opção. +Mas é a melhor para o nosso caso de uso. + +E pode ser a melhor para a maioria dos casos de uso, a menos que você seja um especialista em OAuth2 e saiba exatamente por que existe outra opção que se adapta melhor às suas necessidades. + +Nesse caso, o **FastAPI** também fornece as ferramentas para construí-la. /// - Um token "bearer" não é a única opção. +Quando criamos uma instância da classe `OAuth2PasswordBearer`, passamos o parâmetro `tokenUrl`. Esse parâmetro contém a URL que o client (o frontend rodando no navegador do usuário) usará para enviar o `username` e o `password` para obter um token. - Mas é a melhor no nosso caso. - - E talvez seja a melhor para a maioria dos casos, a não ser que você seja um especialista em OAuth2 e saiba exatamente o porquê de existir outras opções que se adequam melhor às suas necessidades. - - Nesse caso, **FastAPI** também fornece as ferramentas para construir. - -Quando nós criamos uma instância da classe `OAuth2PasswordBearer`, nós passamos pelo parâmetro `tokenUrl` Esse parâmetro contém a URL que o client (o frontend rodando no browser do usuário) vai usar para mandar o `username` e `senha` para obter um token. - -{* ../../docs_src/security/tutorial001.py hl[6] *} +{* ../../docs_src/security/tutorial001_an_py39.py hl[8] *} /// tip | Dica +Aqui `tokenUrl="token"` refere-se a uma URL relativa `token` que ainda não criamos. Como é uma URL relativa, é equivalente a `./token`. +Como estamos usando uma URL relativa, se sua API estivesse localizada em `https://example.com/`, então se referiria a `https://example.com/token`. Mas se sua API estivesse localizada em `https://example.com/api/v1/`, então se referiria a `https://example.com/api/v1/token`. + +Usar uma URL relativa é importante para garantir que sua aplicação continue funcionando mesmo em um caso de uso avançado como [Atrás de um Proxy](../../advanced/behind-a-proxy.md){.internal-link target=_blank}. /// - Esse `tokenUrl="token"` se refere a uma URL relativa que nós não criamos ainda. Como é uma URL relativa, é equivalente a `./token`. +Esse parâmetro não cria aquele endpoint/operação de rota, mas declara que a URL `/token` será aquela que o client deve usar para obter o token. Essa informação é usada no OpenAPI e depois nos sistemas de documentação interativa da API. - Porque estamos usando uma URL relativa, se sua API estava localizada em `https://example.com/`, então irá referir-se à `https://example.com/token`. Mas se sua API estava localizada em `https://example.com/api/v1/`, então irá referir-se à `https://example.com/api/v1/token`. +Em breve também criaremos a operação de rota real. - Usar uma URL relativa é importante para garantir que sua aplicação continue funcionando, mesmo em um uso avançado tipo [Atrás de um Proxy](../../advanced/behind-a-proxy.md){.internal-link target=_blank}. - -Esse parâmetro não cria um endpoint / *path operation*, mas declara que a URL `/token` vai ser aquela que o client deve usar para obter o token. Essa informação é usada no OpenAPI, e depois na API Interativa de documentação de sistemas. - -Em breve também criaremos o atual path operation. - -/// info | informação +/// info | Informação +Se você é um "Pythonista" muito rigoroso, pode não gostar do estilo do nome do parâmetro `tokenUrl` em vez de `token_url`. +Isso ocorre porque ele usa o mesmo nome da especificação do OpenAPI. Assim, se você precisar investigar mais sobre qualquer um desses esquemas de segurança, pode simplesmente copiar e colar para encontrar mais informações sobre isso. /// - Se você é um "Pythonista" muito rigoroso, você pode não gostar do estilo do nome do parâmetro `tokenUrl` em vez de `token_url`. +A variável `oauth2_scheme` é uma instância de `OAuth2PasswordBearer`, mas também é "chamável" (callable). - Isso ocorre porque está utilizando o mesmo nome que está nas especificações do OpenAPI. Então, se você precisa investigar mais sobre qualquer um desses esquemas de segurança, você pode simplesmente copiar e colar para encontrar mais informações sobre isso. - -A variável `oauth2_scheme` é um instância de `OAuth2PasswordBearer`, mas também é um "callable". - -Pode ser chamada de: +Ela pode ser chamada como: ```Python oauth2_scheme(some, parameters) ``` -Então, pode ser usado com `Depends`. +Então, pode ser usada com `Depends`. -## Use-o +### Use-o { #use-it } -Agora você pode passar aquele `oauth2_scheme` em uma dependência com `Depends`. +Agora você pode passar esse `oauth2_scheme` em uma dependência com `Depends`. -{* ../../docs_src/security/tutorial001.py hl[10] *} +{* ../../docs_src/security/tutorial001_an_py39.py hl[12] *} -Esse dependência vai fornecer uma `str` que é atribuído ao parâmetro `token da *função do path operation* +Essa dependência fornecerá uma `str` que é atribuída ao parâmetro `token` da função de operação de rota. -A **FastAPI** saberá que pode usar essa dependência para definir um "esquema de segurança" no esquema da OpenAPI (e na documentação da API automática). +O **FastAPI** saberá que pode usar essa dependência para definir um "esquema de segurança" no esquema OpenAPI (e na documentação automática da API). -/// info | Detalhes técnicos +/// info | Detalhes Técnicos +O **FastAPI** saberá que pode usar a classe `OAuth2PasswordBearer` (declarada em uma dependência) para definir o esquema de segurança no OpenAPI porque ela herda de `fastapi.security.oauth2.OAuth2`, que por sua vez herda de `fastapi.security.base.SecurityBase`. +Todos os utilitários de segurança que se integram com o OpenAPI (e com a documentação automática da API) herdam de `SecurityBase`, é assim que o **FastAPI** sabe como integrá-los ao OpenAPI. /// - **FastAPI** saberá que pode usar a classe `OAuth2PasswordBearer` (declarada na dependência) para definir o esquema de segurança na OpenAPI porque herda de `fastapi.security.oauth2.OAuth2`, que por sua vez herda de `fastapi.security.base.Securitybase`. +## O que ele faz { #what-it-does } - Todos os utilitários de segurança que se integram com OpenAPI (e na documentação da API automática) herdam de `SecurityBase`, é assim que **FastAPI** pode saber como integrá-los no OpenAPI. +Ele irá procurar na requisição pelo header `Authorization`, verificar se o valor é `Bearer ` mais algum token e retornará o token como uma `str`. -## O que ele faz +Se não houver um header `Authorization`, ou se o valor não tiver um token `Bearer `, ele responderá diretamente com um erro de status 401 (`UNAUTHORIZED`). -Ele irá e olhará na requisição para aquele header de `Autorização`, verificará se o valor é `Bearer` mais algum token, e vai retornar o token como uma `str` - -Se ele não ver o header de `Autorização` ou o valor não tem um token `Bearer`, vai responder com um código de erro 401 (`UNAUTHORIZED`) diretamente. - -Você nem precisa verificar se o token existe para retornar um erro. Você pode ter certeza de que se a sua função for executada, ela terá um `str` nesse token. +Você nem precisa verificar se o token existe para retornar um erro. Você pode ter certeza de que, se sua função for executada, ela terá uma `str` nesse token. Você já pode experimentar na documentação interativa: -Não estamos verificando a validade do token ainda, mas isso já é um começo +Ainda não estamos verificando a validade do token, mas isso já é um começo. -## Recapitulando +## Recapitulando { #recap } -Então, em apenas 3 ou 4 linhas extras, você já tem alguma forma primitiva de segurança. +Então, com apenas 3 ou 4 linhas extras, você já tem alguma forma primitiva de segurança. diff --git a/docs/pt/docs/tutorial/security/get-current-user.md b/docs/pt/docs/tutorial/security/get-current-user.md index 1a2badb83..2135ae236 100644 --- a/docs/pt/docs/tutorial/security/get-current-user.md +++ b/docs/pt/docs/tutorial/security/get-current-user.md @@ -1,105 +1,105 @@ -# Obter Usuário Atual - -No capítulo anterior, o sistema de segurança (que é baseado no sistema de injeção de dependências) estava fornecendo à *função de operação de rota* um `token` como uma `str`: - -{* ../../docs_src/security/tutorial001_an_py39.py hl[12] *} - -Mas isso ainda não é tão útil. - -Vamos fazer com que ele nos forneça o usuário atual. - -## Criar um modelo de usuário - -Primeiro, vamos criar um modelo de usuário com Pydantic. - -Da mesma forma que usamos o Pydantic para declarar corpos, podemos usá-lo em qualquer outro lugar: - -{* ../../docs_src/security/tutorial002_an_py310.py hl[5,12:6] *} - -## Criar uma dependência `get_current_user` - -Vamos criar uma dependência chamada `get_current_user`. - -Lembra que as dependências podem ter subdependências? - -`get_current_user` terá uma dependência com o mesmo `oauth2_scheme` que criamos antes. - -Da mesma forma que estávamos fazendo antes diretamente na *operação de rota*, a nossa nova dependência `get_current_user` receberá um `token` como uma `str` da subdependência `oauth2_scheme`: - -{* ../../docs_src/security/tutorial002_an_py310.py hl[25] *} - -## Obter o usuário - -`get_current_user` usará uma função utilitária (falsa) que criamos, que recebe um token como uma `str` e retorna nosso modelo Pydantic `User`: - -{* ../../docs_src/security/tutorial002_an_py310.py hl[19:22,26:27] *} - -## Injetar o usuário atual - -Então agora nós podemos usar o mesmo `Depends` com nosso `get_current_user` na *operação de rota*: - -{* ../../docs_src/security/tutorial002_an_py310.py hl[31] *} - -Observe que nós declaramos o tipo de `current_user` como o modelo Pydantic `User`. - -Isso nos ajudará dentro da função com todo o preenchimento automático e verificações de tipo. - -/// tip | Dica - -Você pode se lembrar que corpos de requisição também são declarados com modelos Pydantic. - -Aqui, o **FastAPI** não ficará confuso porque você está usando `Depends`. - -/// - -/// check | Verifique - -A forma como esse sistema de dependências foi projetado nos permite ter diferentes dependências (diferentes "dependables") que retornam um modelo `User`. - -Não estamos restritos a ter apenas uma dependência que possa retornar esse tipo de dado. - -/// - -## Outros modelos - -Agora você pode obter o usuário atual diretamente nas *funções de operação de rota* e lidar com os mecanismos de segurança no nível da **Injeção de Dependências**, usando `Depends`. - -E você pode usar qualquer modelo ou dado para os requisitos de segurança (neste caso, um modelo Pydantic `User`). - -Mas você não está restrito a usar um modelo de dados, classe ou tipo específico. - -Você quer ter apenas um `id` e `email`, sem incluir nenhum `username` no modelo? Claro. Você pode usar essas mesmas ferramentas. - -Você quer ter apenas uma `str`? Ou apenas um `dict`? Ou uma instância de modelo de classe de banco de dados diretamente? Tudo funciona da mesma forma. - -Na verdade, você não tem usuários que fazem login no seu aplicativo, mas sim robôs, bots ou outros sistemas, que possuem apenas um token de acesso? Novamente, tudo funciona da mesma forma. - -Apenas use qualquer tipo de modelo, qualquer tipo de classe, qualquer tipo de banco de dados que você precise para a sua aplicação. O **FastAPI** cobre tudo com o sistema de injeção de dependências. - -## Tamanho do código - -Este exemplo pode parecer verboso. Lembre-se de que estamos misturando segurança, modelos de dados, funções utilitárias e *operações de rota* no mesmo arquivo. - -Mas aqui está o ponto principal. - -O código relacionado à segurança e à injeção de dependências é escrito apenas uma vez. - -E você pode torná-lo tão complexo quanto quiser. E ainda assim, tê-lo escrito apenas uma vez, em um único lugar. Com toda a flexibilidade. - -Mas você pode ter milhares de endpoints (*operações de rota*) usando o mesmo sistema de segurança. - -E todos eles (ou qualquer parte deles que você desejar) podem aproveitar o reuso dessas dependências ou de quaisquer outras dependências que você criar. - -E todos esses milhares de *operações de rota* podem ter apenas 3 linhas: - -{* ../../docs_src/security/tutorial002_an_py310.py hl[30:32] *} - -## Recapitulação - -Agora você pode obter o usuário atual diretamente na sua *função de operação de rota*. - -Já estamos na metade do caminho. - -Só precisamos adicionar uma *operação de rota* para que o usuário/cliente realmente envie o `username` e `password`. - +# Obter Usuário Atual { #get-current-user } + +No capítulo anterior, o sistema de segurança (que é baseado no sistema de injeção de dependências) estava fornecendo à *função de operação de rota* um `token` como uma `str`: + +{* ../../docs_src/security/tutorial001_an_py39.py hl[12] *} + +Mas isso ainda não é tão útil. + +Vamos fazer com que ele nos forneça o usuário atual. + +## Criar um modelo de usuário { #create-a-user-model } + +Primeiro, vamos criar um modelo de usuário com Pydantic. + +Da mesma forma que usamos o Pydantic para declarar corpos, podemos usá-lo em qualquer outro lugar: + +{* ../../docs_src/security/tutorial002_an_py310.py hl[5,12:6] *} + +## Criar uma dependência `get_current_user` { #create-a-get-current-user-dependency } + +Vamos criar uma dependência chamada `get_current_user`. + +Lembra que as dependências podem ter subdependências? + +`get_current_user` terá uma dependência com o mesmo `oauth2_scheme` que criamos antes. + +Da mesma forma que estávamos fazendo antes diretamente na *operação de rota*, a nossa nova dependência `get_current_user` receberá um `token` como uma `str` da subdependência `oauth2_scheme`: + +{* ../../docs_src/security/tutorial002_an_py310.py hl[25] *} + +## Obter o usuário { #get-the-user } + +`get_current_user` usará uma função utilitária (falsa) que criamos, que recebe um token como uma `str` e retorna nosso modelo Pydantic `User`: + +{* ../../docs_src/security/tutorial002_an_py310.py hl[19:22,26:27] *} + +## Injetar o usuário atual { #inject-the-current-user } + +Então agora nós podemos usar o mesmo `Depends` com nosso `get_current_user` na *operação de rota*: + +{* ../../docs_src/security/tutorial002_an_py310.py hl[31] *} + +Observe que nós declaramos o tipo de `current_user` como o modelo Pydantic `User`. + +Isso nos ajudará dentro da função com todo o preenchimento automático e verificações de tipo. + +/// tip | Dica + +Você pode se lembrar que corpos de requisição também são declarados com modelos Pydantic. + +Aqui, o **FastAPI** não ficará confuso porque você está usando `Depends`. + +/// + +/// check | Verifique + +A forma como esse sistema de dependências foi projetado nos permite ter diferentes dependências (diferentes "dependables") que retornam um modelo `User`. + +Não estamos restritos a ter apenas uma dependência que possa retornar esse tipo de dado. + +/// + +## Outros modelos { #other-models } + +Agora você pode obter o usuário atual diretamente nas *funções de operação de rota* e lidar com os mecanismos de segurança no nível da **Injeção de Dependências**, usando `Depends`. + +E você pode usar qualquer modelo ou dado para os requisitos de segurança (neste caso, um modelo Pydantic `User`). + +Mas você não está restrito a usar um modelo de dados, classe ou tipo específico. + +Você quer ter apenas um `id` e `email`, sem incluir nenhum `username` no modelo? Claro. Você pode usar essas mesmas ferramentas. + +Você quer ter apenas uma `str`? Ou apenas um `dict`? Ou uma instância de modelo de classe de banco de dados diretamente? Tudo funciona da mesma forma. + +Na verdade, você não tem usuários que fazem login no seu aplicativo, mas sim robôs, bots ou outros sistemas, que possuem apenas um token de acesso? Novamente, tudo funciona da mesma forma. + +Apenas use qualquer tipo de modelo, qualquer tipo de classe, qualquer tipo de banco de dados que você precise para a sua aplicação. O **FastAPI** cobre tudo com o sistema de injeção de dependências. + +## Tamanho do código { #code-size } + +Este exemplo pode parecer verboso. Lembre-se de que estamos misturando segurança, modelos de dados, funções utilitárias e *operações de rota* no mesmo arquivo. + +Mas aqui está o ponto principal. + +O código relacionado à segurança e à injeção de dependências é escrito apenas uma vez. + +E você pode torná-lo tão complexo quanto quiser. E ainda assim, tê-lo escrito apenas uma vez, em um único lugar. Com toda a flexibilidade. + +Mas você pode ter milhares de endpoints (*operações de rota*) usando o mesmo sistema de segurança. + +E todos eles (ou qualquer parte deles que você desejar) podem aproveitar o reuso dessas dependências ou de quaisquer outras dependências que você criar. + +E todos esses milhares de *operações de rota* podem ter apenas 3 linhas: + +{* ../../docs_src/security/tutorial002_an_py310.py hl[30:32] *} + +## Recapitulação { #recap } + +Agora você pode obter o usuário atual diretamente na sua *função de operação de rota*. + +Já estamos na metade do caminho. + +Só precisamos adicionar uma *operação de rota* para que o usuário/cliente realmente envie o `username` e `password`. + Isso vem a seguir. diff --git a/docs/pt/docs/tutorial/security/oauth2-jwt.md b/docs/pt/docs/tutorial/security/oauth2-jwt.md index 7d80d12fa..f68b8c39e 100644 --- a/docs/pt/docs/tutorial/security/oauth2-jwt.md +++ b/docs/pt/docs/tutorial/security/oauth2-jwt.md @@ -1,4 +1,4 @@ -# OAuth2 com Senha (e hashing), Bearer com tokens JWT +# OAuth2 com Senha (e hashing), Bearer com tokens JWT { #oauth2-with-password-and-hashing-bearer-with-jwt-tokens } Agora que temos todo o fluxo de segurança, vamos tornar a aplicação realmente segura, usando tokens JWT e hashing de senhas seguras. @@ -6,7 +6,7 @@ Este código é algo que você pode realmente usar na sua aplicação, salvar os Vamos começar de onde paramos no capítulo anterior e incrementá-lo. -## Sobre o JWT +## Sobre o JWT { #about-jwt } JWT significa "JSON Web Tokens". @@ -26,7 +26,7 @@ Depois de uma semana, o token expirará e o usuário não estará autorizado, pr Se você quiser brincar com tokens JWT e ver como eles funcionam, visite https://jwt.io. -## Instalar `PyJWT` +## Instalar `PyJWT` { #install-pyjwt } Nós precisamos instalar o `PyJWT` para criar e verificar os tokens JWT em Python. @@ -50,7 +50,7 @@ Você pode ler mais sobre isso na ```console -$ pip install "passlib[bcrypt]" +$ pip install "pwdlib[argon2]" ---> 100% ``` @@ -86,7 +86,7 @@ $ pip install "passlib[bcrypt]" /// tip | Dica -Com o `passlib`, você poderia até configurá-lo para ser capaz de ler senhas criadas pelo **Django**, um plug-in de segurança do **Flask** ou muitos outros. +Com o `pwdlib`, você poderia até configurá-lo para ser capaz de ler senhas criadas pelo **Django**, um plug-in de segurança do **Flask** ou muitos outros. Assim, você poderia, por exemplo, compartilhar os mesmos dados de um aplicativo Django em um banco de dados com um aplicativo FastAPI. Ou migrar gradualmente uma aplicação Django usando o mesmo banco de dados. @@ -94,17 +94,17 @@ E seus usuários poderiam fazer login tanto pela sua aplicação Django quanto p /// -## Criar o hash e verificar as senhas +## Criar o hash e verificar as senhas { #hash-and-verify-the-passwords } -Importe as ferramentas que nós precisamos de `passlib`. +Importe as ferramentas que nós precisamos de `pwdlib`. -Crie um "contexto" do PassLib. Este será usado para criar o hash e verificar as senhas. +Crie uma instância de PasswordHash com as configurações recomendadas – ela será usada para criar o hash e verificar as senhas. /// tip | Dica -O contexto do PassLib também possui funcionalidades para usar diferentes algoritmos de hashing, incluindo algoritmos antigos que estão obsoletos, apenas para permitir verificá-los, etc. +pwdlib também oferece suporte ao algoritmo de hashing bcrypt, mas não inclui algoritmos legados – para trabalhar com hashes antigos, é recomendado usar a biblioteca passlib. -Por exemplo, você poderia usá-lo para ler e verificar senhas geradas por outro sistema (como Django), mas criar o hash de novas senhas com um algoritmo diferente, como o Bcrypt. +Por exemplo, você poderia usá-lo para ler e verificar senhas geradas por outro sistema (como Django), mas criar o hash de novas senhas com um algoritmo diferente, como o Argon2 ou o Bcrypt. E ser compatível com todos eles ao mesmo tempo. @@ -120,11 +120,11 @@ E outra para autenticar e retornar um usuário. /// note | Nota -Se você verificar o novo banco de dados (falso) `fake_users_db`, você verá como o hash da senha se parece agora: `"$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW"`. +Se você verificar o novo banco de dados (falso) `fake_users_db`, você verá como o hash da senha se parece agora: `"$argon2id$v=19$m=65536,t=3,p=4$wagCPXjifgvUFBzq4hqe3w$CYaIb8sB+wtD+Vu/P4uod1+Qof8h+1g7bbDlBID48Rc"`. /// -## Manipular tokens JWT +## Manipular tokens JWT { #handle-jwt-tokens } Importe os módulos instalados. @@ -154,7 +154,7 @@ Crie uma função utilitária para gerar um novo token de acesso. {* ../../docs_src/security/tutorial004_an_py310.py hl[4,7,13:15,29:31,79:87] *} -## Atualize as dependências +## Atualize as dependências { #update-the-dependencies } Atualize `get_current_user` para receber o mesmo token de antes, mas desta vez, usando tokens JWT. @@ -164,7 +164,7 @@ Se o token for inválido, retorne um erro HTTP imediatamente. {* ../../docs_src/security/tutorial004_an_py310.py hl[90:107] *} -## Atualize a *operação de rota* `/token` +## Atualize a *operação de rota* `/token` { #update-the-token-path-operation } Crie um `timedelta` com o tempo de expiração do token. @@ -172,7 +172,7 @@ Crie um token de acesso JWT real e o retorne. {* ../../docs_src/security/tutorial004_an_py310.py hl[118:133] *} -### Detalhes técnicos sobre o "sujeito" `sub` do JWT +### Detalhes técnicos sobre o "sujeito" `sub` do JWT { #technical-details-about-the-jwt-subject-sub } A especificação JWT diz que existe uma chave `sub`, com o sujeito do token. @@ -194,7 +194,7 @@ Então, para evitar colisões de ID, ao criar o token JWT para o usuário, você O importante a se lembrar é que a chave `sub` deve ter um identificador único em toda a aplicação e deve ser uma string. -## Testando +## Verifique { #check-it } Execute o servidor e vá para a documentação: http://127.0.0.1:8000/docs. @@ -240,7 +240,7 @@ Perceba que o cabeçalho `Authorization`, com o valor que começa com `Bearer `. /// -## Uso avançado com `scopes` +## Uso avançado com `scopes` { #advanced-usage-with-scopes } O OAuth2 tem a noção de "scopes" (escopos). @@ -250,8 +250,7 @@ Então, você pode dar este token diretamente a um usuário ou a uma terceira pa Você pode aprender como usá-los e como eles são integrados ao **FastAPI** mais adiante no **Guia Avançado do Usuário**. - -## Recapitulação +## Recapitulação { #recap } Com o que você viu até agora, você pode configurar uma aplicação **FastAPI** segura usando padrões como OAuth2 e JWT. @@ -265,7 +264,7 @@ O **FastAPI** não faz nenhuma concessão com nenhum banco de dados, modelo de d Ele oferece toda a flexibilidade para você escolher as opções que melhor se ajustam ao seu projeto. -E você pode usar diretamente muitos pacotes bem mantidos e amplamente utilizados, como `passlib` e `PyJWT`, porque o **FastAPI** não exige mecanismos complexos para integrar pacotes externos. +E você pode usar diretamente muitos pacotes bem mantidos e amplamente utilizados, como `pwdlib` e `PyJWT`, porque o **FastAPI** não exige mecanismos complexos para integrar pacotes externos. Mas ele fornece as ferramentas para simplificar o processo o máximo possível, sem comprometer a flexibilidade, robustez ou segurança. diff --git a/docs/pt/docs/tutorial/security/simple-oauth2.md b/docs/pt/docs/tutorial/security/simple-oauth2.md index 1cf05785e..902ae2d22 100644 --- a/docs/pt/docs/tutorial/security/simple-oauth2.md +++ b/docs/pt/docs/tutorial/security/simple-oauth2.md @@ -1,8 +1,8 @@ -# Simples OAuth2 com senha e Bearer +# Simples OAuth2 com senha e Bearer { #simple-oauth2-with-password-and-bearer } Agora vamos construir a partir do capítulo anterior e adicionar as partes que faltam para ter um fluxo de segurança completo. -## Pegue o `username` (nome de usuário) e `password` (senha) +## Obtenha o `username` e a `password` { #get-the-username-and-password } É utilizado o utils de segurança da **FastAPI** para obter o `username` e a `password`. @@ -18,9 +18,9 @@ Mas para a *operação de rota* de login, precisamos usar esses nomes para serem A especificação também afirma que o `username` e a `password` devem ser enviados como dados de formulário (portanto, não há JSON aqui). -### `scope` +### `scope` { #scope } -A especificação também diz que o cliente pode enviar outro campo de formulário "`scope`" (Escopo). +A especificação também diz que o cliente pode enviar outro campo de formulário "`scope`". O nome do campo do formulário é `scope` (no singular), mas na verdade é uma longa string com "escopos" separados por espaços. @@ -44,11 +44,11 @@ Para OAuth2 são apenas strings. /// -## Código para conseguir o `username` e a `password` +## Código para conseguir o `username` e a `password` { #code-to-get-the-username-and-password } Agora vamos usar os utilitários fornecidos pelo **FastAPI** para lidar com isso. -### `OAuth2PasswordRequestForm` +### `OAuth2PasswordRequestForm` { #oauth2passwordrequestform } Primeiro, importe `OAuth2PasswordRequestForm` e use-o como uma dependência com `Depends` na *operação de rota* para `/token`: @@ -59,7 +59,7 @@ Primeiro, importe `OAuth2PasswordRequestForm` e use-o como uma dependência com * O `username`. * A `password`. * Um campo `scope` opcional como uma string grande, composta de strings separadas por espaços. -* Um `grant_type` (tipo de concessão) opcional. +* Um `grant_type` opcional. /// tip | Dica @@ -84,7 +84,7 @@ Mas como é um caso de uso comum, ele é fornecido diretamente pelo **FastAPI**, /// -### Use os dados do formulário +### Use os dados do formulário { #use-the-form-data } /// tip | Dica @@ -96,13 +96,13 @@ Não estamos usando `scopes` neste exemplo, mas a funcionalidade está disponív Agora, obtenha os dados do usuário do banco de dados (falso), usando o `username` do campo do formulário. -Se não existir tal usuário, retornaremos um erro dizendo "Incorrect username or password" (Nome de usuário ou senha incorretos). +Se não existir tal usuário, retornaremos um erro dizendo "Incorrect username or password". Para o erro, usamos a exceção `HTTPException`: {* ../../docs_src/security/tutorial003_an_py310.py hl[3,79:81] *} -### Confira a password (senha) +### Confira a senha { #check-the-password } Neste ponto temos os dados do usuário do nosso banco de dados, mas não verificamos a senha. @@ -112,7 +112,7 @@ Você nunca deve salvar senhas em texto simples, portanto, usaremos o sistema de Se as senhas não corresponderem, retornaremos o mesmo erro. -#### Hashing de senha +#### Hashing de senha { #password-hashing } "Hashing" significa: converter algum conteúdo (uma senha neste caso) em uma sequência de bytes (apenas uma string) que parece algo sem sentido. @@ -120,7 +120,7 @@ Sempre que você passa exatamente o mesmo conteúdo (exatamente a mesma senha), Mas você não pode converter a sequência aleatória de caracteres de volta para a senha. -##### Porque usar hashing de senha +##### Porque usar hashing de senha { #why-use-password-hashing } Se o seu banco de dados for roubado, o ladrão não terá as senhas em texto simples dos seus usuários, apenas os hashes. @@ -128,11 +128,11 @@ Assim, o ladrão não poderá tentar usar essas mesmas senhas em outro sistema ( {* ../../docs_src/security/tutorial003_an_py310.py hl[82:85] *} -#### Sobre `**user_dict` +#### Sobre `**user_dict` { #about-user-dict } `UserInDB(**user_dict)` significa: -*Passe as keys (chaves) e values (valores) de `user_dict` diretamente como argumentos de valor-chave, equivalente a:* +*Passe as chaves e valores de `user_dict` diretamente como argumentos de valor-chave, equivalente a:* ```Python UserInDB( @@ -146,11 +146,11 @@ UserInDB( /// info | Informação -Para uma explicação mais completa de `**user_dict`, verifique [a documentação para **Extra Models**](../extra-models.md#about-user_indict){.internal-link target=_blank}. +Para uma explicação mais completa de `**user_dict`, verifique [a documentação para **Extra Models**](../extra-models.md#about-user-in-dict){.internal-link target=_blank}. /// -## Retorne o token +## Retorne o token { #return-the-token } A resposta do endpoint `token` deve ser um objeto JSON. @@ -182,11 +182,11 @@ De resto, **FastAPI** cuida disso para você. /// -## Atualize as dependências +## Atualize as dependências { #update-the-dependencies } Agora vamos atualizar nossas dependências. -Queremos obter o `user_user` *somente* se este usuário estiver ativo. +Queremos obter o `current_user` *somente* se este usuário estiver ativo. Portanto, criamos uma dependência adicional `get_current_active_user` que por sua vez usa `get_current_user` como dependência. @@ -214,11 +214,11 @@ Esse é o benefício dos padrões... /// -## Veja em ação +## Veja em ação { #see-it-in-action } Abra o docs interativo: http://127.0.0.1:8000/docs. -### Autenticação +### Autentique-se { #authenticate } Clique no botão "Authorize". @@ -234,7 +234,7 @@ Após autenticar no sistema, você verá assim: -### Obtenha seus próprios dados de usuário +### Obtenha seus próprios dados de usuário { #get-your-own-user-data } Agora use a operação `GET` com o caminho `/users/me`. @@ -260,7 +260,7 @@ Se você clicar no ícone de cadeado, sair e tentar a mesma operação novamente } ``` -### Usuário inativo +### Usuário inativo { #inactive-user } Agora tente com um usuário inativo, autentique-se com: @@ -278,7 +278,7 @@ Você receberá um erro "Usuário inativo", como: } ``` -## Recaptulando +## Recapitulando { #recap } Agora você tem as ferramentas para implementar um sistema de segurança completo baseado em `username` e `password` para sua API. diff --git a/docs/pt/docs/tutorial/sql-databases.md b/docs/pt/docs/tutorial/sql-databases.md index 3d76a532c..b1c7a3fa7 100644 --- a/docs/pt/docs/tutorial/sql-databases.md +++ b/docs/pt/docs/tutorial/sql-databases.md @@ -1,4 +1,4 @@ -# Bancos de Dados SQL (Relacionais) +# Bancos de Dados SQL (Relacionais) { #sql-relational-databases } **FastAPI** não exige que você use um banco de dados SQL (relacional). Mas você pode usar **qualquer banco de dados** que quiser. @@ -8,7 +8,7 @@ Aqui veremos um exemplo usando "ORMs"), o FastAPI não obriga você a usar nada. 😎 +Você pode usar qualquer outra biblioteca de banco de dados SQL ou NoSQL que quiser (em alguns casos chamadas de "ORMs"), o FastAPI não obriga você a usar nada. 😎 /// @@ -32,7 +32,7 @@ Existe um gerador de projetos oficial com **FastAPI** e **PostgreSQL** incluindo Este é um tutorial muito simples e curto, se você quiser aprender sobre bancos de dados em geral, sobre SQL ou recursos mais avançados, acesse a documentação do SQLModel. -## Instalar o `SQLModel` +## Instalar o `SQLModel` { #install-sqlmodel } Primeiro, certifique-se de criar seu [ambiente virtual](../virtual-environments.md){.internal-link target=_blank}, ativá-lo e, em seguida, instalar o `sqlmodel`: @@ -45,13 +45,13 @@ $ pip install sqlmodel -## Criar o App com um Único Modelo +## Criar o App com um Único Modelo { #create-the-app-with-a-single-model } Vamos criar a primeira versão mais simples do app com um único modelo **SQLModel**. Depois, vamos melhorá-lo aumentando a segurança e versatilidade com **múltiplos modelos** abaixo. 🤓 -### Criar Modelos +### Criar Modelos { #create-models } Importe o `SQLModel` e crie um modelo de banco de dados: @@ -71,7 +71,7 @@ Existem algumas diferenças: O SQLModel saberá que algo declarado como `str` será uma coluna SQL do tipo `TEXT` (ou `VARCHAR`, dependendo do banco de dados). -### Criar um Engine +### Criar um Engine { #create-an-engine } Um `engine` SQLModel (por baixo dos panos, ele é na verdade um `engine` do SQLAlchemy) é o que **mantém as conexões** com o banco de dados. Você teria **um único objeto `engine`** para todo o seu código se conectar ao mesmo banco de dados. @@ -82,13 +82,13 @@ Usar `check_same_thread=False` permite que o FastAPI use o mesmo banco de dados Não se preocupe, com a forma como o código está estruturado, garantiremos que usamos **uma única *sessão* SQLModel por requisição** mais tarde, isso é realmente o que o `check_same_thread` está tentando conseguir. -### Criar as Tabelas +### Criar as Tabelas { #create-the-tables } Em seguida, adicionamos uma função que usa `SQLModel.metadata.create_all(engine)` para **criar as tabelas** para todos os *modelos de tabela*. {* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[21:22] hl[21:22] *} -### Criar uma Dependência de Sessão +### Criar uma Dependência de Sessão { #create-a-session-dependency } Uma **`Session`** é o que armazena os **objetos na memória** e acompanha as alterações necessárias nos dados, para então **usar o `engine`** para se comunicar com o banco de dados. @@ -98,7 +98,7 @@ Então, criamos uma dependência `Annotated` chamada `SessionDep` para simplific {* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[25:30] hl[25:27,30] *} -### Criar Tabelas de Banco de Dados na Inicialização +### Criar Tabelas de Banco de Dados na Inicialização { #create-database-tables-on-startup } Vamos criar as tabelas do banco de dados quando o aplicativo for iniciado. @@ -114,7 +114,7 @@ O SQLModel terá utilitários de migração envolvendo o Alembic, mas por enquan /// -### Criar um Hero +### Criar um Hero { #create-a-hero } Como cada modelo SQLModel também é um modelo Pydantic, você pode usá-lo nas mesmas **anotações de tipo** que usaria para modelos Pydantic. @@ -124,29 +124,27 @@ Da mesma forma, você pode declará-lo como o **tipo de retorno** da função, e {* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[40:45] hl[40:45] *} - - Aqui, usamos a dependência `SessionDep` (uma `Session`) para adicionar o novo `Hero` à instância `Session`, fazer commit das alterações no banco de dados, atualizar os dados no `hero` e então retorná-lo. -### Ler Heroes +### Ler Heroes { #read-heroes } Podemos **ler** `Hero`s do banco de dados usando um `select()`. Podemos incluir um `limit` e `offset` para paginar os resultados. {* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[48:55] hl[51:52,54] *} -### Ler um Único Hero +### Ler um Único Hero { #read-one-hero } Podemos **ler** um único `Hero`. {* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[58:63] hl[60] *} -### Deletar um Hero +### Deletar um Hero { #delete-a-hero } Também podemos **deletar** um `Hero`. {* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[66:73] hl[71] *} -### Executar o App +### Executar o App { #run-the-app } Você pode executar o app: @@ -166,7 +164,7 @@ Então, vá para a interface `/docs`, você verá que o **FastAPI** está usando -## Atualizar o App com Múltiplos Modelos +## Atualizar o App com Múltiplos Modelos { #update-the-app-with-multiple-models } Agora vamos **refatorar** este app um pouco para aumentar a **segurança** e **versatilidade**. @@ -178,7 +176,7 @@ Além disso, criamos um `secret_name` para o hero, mas até agora estamos retorn Vamos corrigir essas coisas adicionando alguns **modelos extras**. Aqui é onde o SQLModel vai brilhar. ✨ -### Criar Múltiplos Modelos +### Criar Múltiplos Modelos { #create-multiple-models } No **SQLModel**, qualquer classe de modelo que tenha `table=True` é um **modelo de tabela**. @@ -186,7 +184,7 @@ E qualquer classe de modelo que não tenha `table=True` é um **modelo de dados* Com o SQLModel, podemos usar a **herança** para **evitar duplicação** de todos os campos em todos os casos. -#### `HeroBase` - a classe base +#### `HeroBase` - a classe base { #herobase-the-base-class } Vamos começar com um modelo `HeroBase` que tem todos os **campos compartilhados** por todos os modelos: @@ -195,7 +193,7 @@ Vamos começar com um modelo `HeroBase` que tem todos os **campos compartilhados {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:9] hl[7:9] *} -#### `Hero` - o *modelo de tabela* +#### `Hero` - o *modelo de tabela* { #hero-the-table-model } Em seguida, vamos criar `Hero`, o verdadeiro *modelo de tabela*, com os **campos extras** que nem sempre estão nos outros modelos: @@ -211,7 +209,7 @@ Como `Hero` herda de `HeroBase`, ele **também** tem os **campos** declarados em {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:14] hl[12:14] *} -#### `HeroPublic` - o *modelo de dados* público +#### `HeroPublic` - o *modelo de dados* público { #heropublic-the-public-data-model } Em seguida, criamos um modelo `HeroPublic`, que será **retornado** para os clientes da API. @@ -234,11 +232,10 @@ Todos os campos em `HeroPublic` são os mesmos que em `HeroBase`, com `id` decla * `id` * `name` * `age` -* `secret_name` {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:18] hl[17:18] *} -#### `HeroCreate` - o *modelo de dados* para criar um hero +#### `HeroCreate` - o *modelo de dados* para criar um hero { #herocreate-the-data-model-to-create-a-hero } Agora criamos um modelo `HeroCreate`, este é o que **validará** os dados dos clientes. @@ -262,7 +259,7 @@ Os campos de `HeroCreate` são: {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:22] hl[21:22] *} -#### `HeroUpdate` - o *modelo de dados* para atualizar um hero +#### `HeroUpdate` - o *modelo de dados* para atualizar um hero { #heroupdate-the-data-model-to-update-a-hero } Não tínhamos uma maneira de **atualizar um hero** na versão anterior do app, mas agora com **múltiplos modelos**, podemos fazer isso. 🎉 @@ -280,7 +277,7 @@ Os campos de `HeroUpdate` são: {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:28] hl[25:28] *} -### Criar com `HeroCreate` e retornar um `HeroPublic` +### Criar com `HeroCreate` e retornar um `HeroPublic` { #create-with-herocreate-and-return-a-heropublic } Agora que temos **múltiplos modelos**, podemos atualizar as partes do app que os utilizam. @@ -302,19 +299,19 @@ Ao declará-lo no `response_model`, estamos dizendo ao **FastAPI** para fazer o /// -### Ler Heroes com `HeroPublic` +### Ler Heroes com `HeroPublic` { #read-heroes-with-heropublic } Podemos fazer o mesmo que antes para **ler** `Hero`s, novamente, usamos `response_model=list[HeroPublic]` para garantir que os dados sejam validados e serializados corretamente. {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[65:72] hl[65] *} -### Ler Um Hero com `HeroPublic` +### Ler Um Hero com `HeroPublic` { #read-one-hero-with-heropublic } Podemos **ler** um único herói: {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[75:80] hl[77] *} -### Atualizar um Hero com `HeroUpdate` +### Atualizar um Hero com `HeroUpdate` { #update-a-hero-with-heroupdate } Podemos **atualizar um hero**. Para isso, usamos uma operação HTTP `PATCH`. @@ -324,7 +321,7 @@ Em seguida, usamos `hero_db.sqlmodel_update(hero_data)` para atualizar o `hero_d {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[83:93] hl[83:84,88:89] *} -### Deletar um Hero Novamente +### Deletar um Hero Novamente { #delete-a-hero-again } **Deletar** um hero permanece praticamente o mesmo. @@ -332,7 +329,7 @@ Não vamos satisfazer o desejo de refatorar tudo neste aqui. 😅 {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[96:103] hl[101] *} -### Executar o App Novamente +### Executar o App Novamente { #run-the-app-again } Você pode executar o app novamente: @@ -346,13 +343,13 @@ $ fastapi dev main.py -If you go to the `/docs` API UI, you will see that it is now updated, and it won't expect to receive the `id` from the client when creating a hero, etc. +Se você for para a interface `/docs` da API, verá que agora ela está atualizada e não esperará receber o `id` do cliente ao criar um hero, etc.
-## Recapitulando +## Recapitulando { #recap } Você pode usar **SQLModel** para interagir com um banco de dados SQL e simplificar o código com *modelos de dados* e *modelos de tabela*. diff --git a/docs/pt/docs/tutorial/static-files.md b/docs/pt/docs/tutorial/static-files.md index 30e1af8e6..13313a909 100644 --- a/docs/pt/docs/tutorial/static-files.md +++ b/docs/pt/docs/tutorial/static-files.md @@ -1,15 +1,15 @@ -# Arquivos Estáticos +# Arquivos Estáticos { #static-files } -Você pode servir arquivos estáticos automaticamente de um diretório usando `StaticFiles`. +Você pode servir arquivos estáticos automaticamente a partir de um diretório usando `StaticFiles`. -## Use `StaticFiles` +## Use `StaticFiles` { #use-staticfiles } * Importe `StaticFiles`. -* "Monte" uma instância de `StaticFiles()` em um caminho específico. +* "Monte" uma instância de `StaticFiles()` em um path específico. {* ../../docs_src/static_files/tutorial001.py hl[2,6] *} -/// note | Detalhes técnicos +/// note | Detalhes Técnicos Você também pode usar `from starlette.staticfiles import StaticFiles`. @@ -17,24 +17,24 @@ O **FastAPI** fornece o mesmo que `starlette.staticfiles` como `fastapi.staticfi /// -### O que é "Montagem" +### O que é "Montagem" { #what-is-mounting } -"Montagem" significa adicionar um aplicativo completamente "independente" em uma rota específica, que então cuida de todas as subrotas. +"Montagem" significa adicionar uma aplicação completamente "independente" em um path específico, que então cuida de lidar com todos os sub-paths. -Isso é diferente de usar um `APIRouter`, pois um aplicativo montado é completamente independente. A OpenAPI e a documentação do seu aplicativo principal não incluirão nada do aplicativo montado, etc. +Isso é diferente de usar um `APIRouter`, pois uma aplicação montada é completamente independente. A OpenAPI e a documentação da sua aplicação principal não incluirão nada da aplicação montada, etc. -Você pode ler mais sobre isso no **Guia Avançado do Usuário**. +Você pode ler mais sobre isso no [Guia Avançado do Usuário](../advanced/index.md){.internal-link target=_blank}. -## Detalhes +## Detalhes { #details } -O primeiro `"/static"` refere-se à subrota em que este "subaplicativo" será "montado". Portanto, qualquer caminho que comece com `"/static"` será tratado por ele. +O primeiro `"/static"` refere-se ao sub-path no qual este "subaplicativo" será "montado". Assim, qualquer path que comece com `"/static"` será tratado por ele. O `directory="static"` refere-se ao nome do diretório que contém seus arquivos estáticos. -O `name="static"` dá a ela um nome que pode ser usado internamente pelo FastAPI. +O `name="static"` dá a ele um nome que pode ser usado internamente pelo **FastAPI**. -Todos esses parâmetros podem ser diferentes de "`static`", ajuste-os de acordo com as necessidades e detalhes específicos de sua própria aplicação. +Todos esses parâmetros podem ser diferentes de "`static`", ajuste-os de acordo com as necessidades e detalhes específicos da sua própria aplicação. -## Mais informações +## Mais informações { #more-info } -Para mais detalhes e opções, verifique Starlette's docs about Static Files. +Para mais detalhes e opções, consulte a documentação da Starlette sobre Arquivos Estáticos. diff --git a/docs/pt/docs/tutorial/testing.md b/docs/pt/docs/tutorial/testing.md index dc505105a..a1821e660 100644 --- a/docs/pt/docs/tutorial/testing.md +++ b/docs/pt/docs/tutorial/testing.md @@ -1,4 +1,4 @@ -# Testando +# Testando { #testing } Graças ao Starlette, testar aplicativos **FastAPI** é fácil e agradável. @@ -6,7 +6,7 @@ Ele é baseado no pytest diretamente com **FastAPI**. -## Usando `TestClient` +## Usando `TestClient` { #using-testclient } /// info | Informação @@ -42,7 +42,7 @@ Isso permite que você use `pytest` diretamente sem complicações. /// -/// note | Detalhes técnicos +/// note | Detalhes Técnicos Você também pode usar `from starlette.testclient import TestClient`. @@ -56,15 +56,15 @@ Se você quiser chamar funções `async` em seus testes além de enviar solicita /// -## Separando testes +## Separando testes { #separating-tests } Em uma aplicação real, você provavelmente teria seus testes em um arquivo diferente. E seu aplicativo **FastAPI** também pode ser composto de vários arquivos/módulos, etc. -### Arquivo do aplicativo **FastAPI** +### Arquivo do aplicativo **FastAPI** { #fastapi-app-file } -Digamos que você tenha uma estrutura de arquivo conforme descrito em [Aplicativos maiores](bigger-applications.md){.internal-link target=_blank}: +Digamos que você tenha uma estrutura de arquivo conforme descrito em [Aplicações maiores](bigger-applications.md){.internal-link target=_blank}: ``` . @@ -78,7 +78,7 @@ No arquivo `main.py` você tem seu aplicativo **FastAPI**: {* ../../docs_src/app_testing/main.py *} -### Arquivo de teste +### Arquivo de teste { #testing-file } Então você poderia ter um arquivo `test_main.py` com seus testes. Ele poderia estar no mesmo pacote Python (o mesmo diretório com um arquivo `__init__.py`): @@ -96,11 +96,11 @@ Como esse arquivo está no mesmo pacote, você pode usar importações relativas ...e ter o código para os testes como antes. -## Testando: exemplo estendido +## Testando: exemplo estendido { #testing-extended-example } Agora vamos estender este exemplo e adicionar mais detalhes para ver como testar diferentes partes. -### Arquivo de aplicativo **FastAPI** estendido +### Arquivo de aplicativo **FastAPI** estendido { #extended-fastapi-app-file } Vamos continuar com a mesma estrutura de arquivo de antes: @@ -172,7 +172,7 @@ Prefira usar a versão `Annotated` se possível. //// -### Arquivo de teste estendido +### Arquivo de teste estendido { #extended-testing-file } Você pode então atualizar `test_main.py` com os testes estendidos: @@ -200,7 +200,7 @@ Se você tiver um modelo Pydantic em seu teste e quiser enviar seus dados para o /// -## Execute-o +## Execute-o { #run-it } Depois disso, você só precisa instalar o `pytest`. diff --git a/docs/pt/docs/virtual-environments.md b/docs/pt/docs/virtual-environments.md index 5fc1a8866..244f532b5 100644 --- a/docs/pt/docs/virtual-environments.md +++ b/docs/pt/docs/virtual-environments.md @@ -1,4 +1,4 @@ -# Ambientes Virtuais +# Ambientes Virtuais { #virtual-environments } Ao trabalhar em projetos Python, você provavelmente deve usar um **ambiente virtual** (ou um mecanismo similar) para isolar os pacotes que você instala para cada projeto. @@ -26,7 +26,7 @@ Se você estiver pronto para adotar uma **ferramenta que gerencia tudo** para vo /// -## Criar um Projeto +## Criar um Projeto { #create-a-project } Primeiro, crie um diretório para seu projeto. @@ -51,7 +51,7 @@ $ cd awesome-project -## Crie um ambiente virtual +## Crie um ambiente virtual { #create-a-virtual-environment } Ao começar a trabalhar em um projeto Python **pela primeira vez**, crie um ambiente virtual **dentro do seu projeto**. @@ -114,7 +114,7 @@ Você pode criar o ambiente virtual em um diretório diferente, mas há uma conv /// -## Ative o ambiente virtual +## Ative o ambiente virtual { #activate-the-virtual-environment } Ative o novo ambiente virtual para que qualquer comando Python que você executar ou pacote que você instalar o utilize. @@ -166,17 +166,17 @@ $ source .venv/Scripts/activate Toda vez que você instalar um **novo pacote** naquele ambiente, **ative** o ambiente novamente. -Isso garante que, se você usar um **programa de terminal (CLI)** instalado por esse pacote, você usará aquele do seu ambiente virtual e não qualquer outro que possa ser instalado globalmente, provavelmente com uma versão diferente do que você precisa. +Isso garante que, se você usar um **programa de terminal (CLI)** instalado por esse pacote, você usará aquele do seu ambiente virtual e não qualquer outro que possa ser instalado globalmente, provavelmente com uma versão diferente do que você precisa. /// -## Verifique se o ambiente virtual está ativo +## Verifique se o ambiente virtual está ativo { #check-the-virtual-environment-is-active } Verifique se o ambiente virtual está ativo (o comando anterior funcionou). /// tip | Dica -Isso é **opcional**, mas é uma boa maneira de **verificar** se tudo está funcionando conforme o esperado e se você está usando o ambiente virtual pretendido. +Isso é **opcional**, mas é uma boa maneira de **verificar** se tudo está funcionando conforme o esperado e se você está usando o ambiente virtual intendido. /// @@ -212,7 +212,7 @@ Se ele mostrar o binário `python` em `.venv\Scripts\python`, dentro do seu proj //// -## Atualizar `pip` +## Atualizar `pip` { #upgrade-pip } /// tip | Dica @@ -242,7 +242,7 @@ $ python -m pip install --upgrade pip -## Adicionar `.gitignore` +## Adicionar `.gitignore` { #add-gitignore } Se você estiver usando **Git** (você deveria), adicione um arquivo `.gitignore` para excluir tudo em seu `.venv` do Git. @@ -282,7 +282,7 @@ Esse comando criará um arquivo `.gitignore` com o conteúdo: /// -## Instalar Pacotes +## Instalar Pacotes { #install-packages } Após ativar o ambiente, você pode instalar pacotes nele. @@ -294,7 +294,7 @@ Se precisar atualizar uma versão ou adicionar um novo pacote, você **fará iss /// -### Instalar pacotes diretamente +### Instalar pacotes diretamente { #install-packages-directly } Se estiver com pressa e não quiser usar um arquivo para declarar os requisitos de pacote do seu projeto, você pode instalá-los diretamente. @@ -333,7 +333,7 @@ $ uv pip install "fastapi[standard]" //// -### Instalar a partir de `requirements.txt` +### Instalar a partir de `requirements.txt` { #install-from-requirements-txt } Se você tiver um `requirements.txt`, agora poderá usá-lo para instalar seus pacotes. @@ -376,7 +376,7 @@ pydantic==2.8.0 /// -## Execute seu programa +## Execute seu programa { #run-your-program } Depois de ativar o ambiente virtual, você pode executar seu programa, e ele usará o Python dentro do seu ambiente virtual com os pacotes que você instalou lá. @@ -390,9 +390,9 @@ Hello World -## Configure seu editor +## Configure seu editor { #configure-your-editor } -Você provavelmente usaria um editor. Certifique-se de configurá-lo para usar o mesmo ambiente virtual que você criou (ele provavelmente o detectará automaticamente) para que você possa obter erros de preenchimento automático e em linha. +Você provavelmente usaria um editor. Certifique-se de configurá-lo para usar o mesmo ambiente virtual que você criou (ele provavelmente o detectará automaticamente) para que você possa obter preenchimento automático e erros em linha. Por exemplo: @@ -405,7 +405,7 @@ Normalmente, você só precisa fazer isso **uma vez**, ao criar o ambiente virtu /// -## Desativar o ambiente virtual +## Desativar o ambiente virtual { #deactivate-the-virtual-environment } Quando terminar de trabalhar no seu projeto, você pode **desativar** o ambiente virtual. @@ -419,7 +419,7 @@ $ deactivate Dessa forma, quando você executar `python`, ele não tentará executá-lo naquele ambiente virtual com os pacotes instalados nele. -## Pronto para trabalhar +## Pronto para trabalhar { #ready-to-work } Agora você está pronto para começar a trabalhar no seu projeto. @@ -433,7 +433,7 @@ Continue lendo. 👇🤓 /// -## Por que ambientes virtuais +## Por que ambientes virtuais { #why-virtual-environments } Para trabalhar com o FastAPI, você precisa instalar o Python. @@ -443,7 +443,7 @@ Para instalar pacotes, você normalmente usaria o comando `pip` que vem com o Py No entanto, se você usar `pip` diretamente, os pacotes serão instalados no seu **ambiente Python global** (a instalação global do Python). -### O Problema +### O Problema { #the-problem } Então, qual é o problema em instalar pacotes no ambiente global do Python? @@ -526,7 +526,7 @@ Agora, imagine isso com **muitos** outros **pacotes** dos quais todos os seus ** Além disso, dependendo do seu sistema operacional (por exemplo, Linux, Windows, macOS), ele pode ter vindo com o Python já instalado. E, nesse caso, provavelmente tinha alguns pacotes pré-instalados com algumas versões específicas **necessárias para o seu sistema**. Se você instalar pacotes no ambiente global do Python, poderá acabar **quebrando** alguns dos programas que vieram com seu sistema operacional. -## Onde os pacotes são instalados +## Onde os pacotes são instalados { #where-are-packages-installed } Quando você instala o Python, ele cria alguns diretórios com alguns arquivos no seu computador. @@ -552,7 +552,7 @@ Em seguida, ele **extrairá** todos esses arquivos e os colocará em um diretór Por padrão, ele colocará os arquivos baixados e extraídos no diretório que vem com a instalação do Python, que é o **ambiente global**. -## O que são ambientes virtuais +## O que são ambientes virtuais { #what-are-virtual-environments } A solução para os problemas de ter todos os pacotes no ambiente global é usar um **ambiente virtual para cada projeto** em que você trabalha. @@ -577,7 +577,7 @@ flowchart TB stone-project ~~~ azkaban-project ``` -## O que significa ativar um ambiente virtual +## O que significa ativar um ambiente virtual { #what-does-activating-a-virtual-environment-mean } Quando você ativa um ambiente virtual, por exemplo com: @@ -714,7 +714,7 @@ Um detalhe importante é que ele colocará o caminho do ambiente virtual no **in Ativar um ambiente virtual também muda algumas outras coisas, mas esta é uma das mais importantes. -## Verificando um ambiente virtual +## Verificando um ambiente virtual { #checking-a-virtual-environment } Ao verificar se um ambiente virtual está ativo, por exemplo com: @@ -752,7 +752,7 @@ você usa `which` no Linux e macOS e `Get-Command` no Windows PowerShell. A maneira como esse comando funciona é que ele vai e verifica na variável de ambiente `PATH`, passando por **cada caminho em ordem**, procurando pelo programa chamado `python`. Uma vez que ele o encontre, ele **mostrará o caminho** para esse programa. -A parte mais importante é que quando você chama ``python`, esse é exatamente o "`python`" que será executado. +A parte mais importante é que quando você chama `python`, esse é exatamente o "`python`" que será executado. Assim, você pode confirmar se está no ambiente virtual correto. @@ -766,7 +766,7 @@ E o segundo projeto **não funcionaria** porque você está usando o **Python in /// -## Por que desativar um ambiente virtual +## Por que desativar um ambiente virtual { #why-deactivate-a-virtual-environment } Por exemplo, você pode estar trabalhando em um projeto `philosophers-stone`, **ativar esse ambiente virtual**, instalar pacotes e trabalhar com esse ambiente. @@ -820,7 +820,7 @@ Eu juro solenemente 🐺 -## Alternativas +## Alternativas { #alternatives } Este é um guia simples para você começar e lhe ensinar como tudo funciona **por baixo**. @@ -834,10 +834,10 @@ Quando estiver pronto e quiser usar uma ferramenta para **gerenciar todo o proje * Gerenciar o **ambiente virtual** para seus projetos * Instalar **pacotes** * Gerenciar **dependências e versões** de pacotes para seu projeto -* Certifique-se de ter um conjunto **exato** de pacotes e versões para instalar, incluindo suas dependências, para que você possa ter certeza de que pode executar seu projeto em produção exatamente da mesma forma que em seu computador durante o desenvolvimento, isso é chamado de **bloqueio** +* Certificar-se de que você tenha um conjunto **exato** de pacotes e versões para instalar, incluindo suas dependências, para que você possa ter certeza de que pode executar seu projeto em produção exatamente da mesma forma que em seu computador durante o desenvolvimento, isso é chamado de **bloqueio** * E muitas outras coisas -## Conclusão +## Conclusão { #conclusion } Se você leu e entendeu tudo isso, agora **você sabe muito mais** sobre ambientes virtuais do que muitos desenvolvedores por aí. 🤓 From 9e362d9f6e66078c67295e5ed5e5918739dfe5f0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 12 Nov 2025 16:24:24 +0000 Subject: [PATCH 101/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index b16770b3c..b9f2a35d0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Translations +* 🌐 Update Portuguese translations with LLM prompt. PR [#14228](https://github.com/fastapi/fastapi/pull/14228) by [@ceb10n](https://github.com/ceb10n). * 🔨 Add Portuguese translations LLM prompt. PR [#14208](https://github.com/fastapi/fastapi/pull/14208) by [@ceb10n](https://github.com/ceb10n). * 🌐 Sync Russian docs. PR [#14331](https://github.com/fastapi/fastapi/pull/14331) by [@YuriiMotov](https://github.com/YuriiMotov). * 🌐 Sync German docs. PR [#14317](https://github.com/fastapi/fastapi/pull/14317) by [@nilslindemann](https://github.com/nilslindemann). From 42930fe60065420034b7d6ff4932b25993a68481 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Thu, 13 Nov 2025 08:37:15 +0100 Subject: [PATCH 102/256] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20links=20and?= =?UTF-8?q?=20add=20missing=20permalink=20in=20docs=20(#14217)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix link url to match link text * Add missing permalink in `behind-a-proxy.md` * Fix link in `advanced-dependencies.md` --- docs/en/docs/advanced/advanced-dependencies.md | 2 +- docs/en/docs/advanced/behind-a-proxy.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/en/docs/advanced/advanced-dependencies.md b/docs/en/docs/advanced/advanced-dependencies.md index 5d6a40f46..37f5c78f2 100644 --- a/docs/en/docs/advanced/advanced-dependencies.md +++ b/docs/en/docs/advanced/advanced-dependencies.md @@ -144,7 +144,7 @@ This was changed in version 0.110.0 to fix unhandled memory consumption from for ### Background Tasks and Dependencies with `yield`, Technical Details { #background-tasks-and-dependencies-with-yield-technical-details } -Before FastAPI 0.106.0, raising exceptions after `yield` was not possible, the exit code in dependencies with `yield` was executed *after* the response was sent, so [Exception Handlers](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} would have already run. +Before FastAPI 0.106.0, raising exceptions after `yield` was not possible, the exit code in dependencies with `yield` was executed *after* the response was sent, so [Exception Handlers](../tutorial/handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} would have already run. This was designed this way mainly to allow using the same objects "yielded" by dependencies inside of background tasks, because the exit code would be executed after the background tasks were finished. diff --git a/docs/en/docs/advanced/behind-a-proxy.md b/docs/en/docs/advanced/behind-a-proxy.md index 4d19d29e0..f692a28e8 100644 --- a/docs/en/docs/advanced/behind-a-proxy.md +++ b/docs/en/docs/advanced/behind-a-proxy.md @@ -64,7 +64,7 @@ If you want to learn more about HTTPS, check the guide [About HTTPS](../deployme /// -### How Proxy Forwarded Headers Work +### How Proxy Forwarded Headers Work { #how-proxy-forwarded-headers-work } Here's a visual representation of how the **proxy** adds forwarded headers between the client and the **application server**: @@ -228,7 +228,7 @@ Passing the `root_path` to `FastAPI` would be the equivalent of passing the `--r Keep in mind that the server (Uvicorn) won't use that `root_path` for anything else than passing it to the app. -But if you go with your browser to http://127.0.0.1:8000/app you will see the normal response: +But if you go with your browser to http://127.0.0.1:8000/app you will see the normal response: ```JSON { From d1be85c728c64aa3d99f8bc7e9865b342f209117 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 13 Nov 2025 07:37:43 +0000 Subject: [PATCH 103/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index b9f2a35d0..042fa4bdc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Docs + +* ✏️ Fix links and add missing permalink in docs. PR [#14217](https://github.com/fastapi/fastapi/pull/14217) by [@YuriiMotov](https://github.com/YuriiMotov). + ### Translations * 🌐 Update Portuguese translations with LLM prompt. PR [#14228](https://github.com/fastapi/fastapi/pull/14228) by [@ceb10n](https://github.com/ceb10n). From 004ab1a9d1c746bf885d4d6428a721074b2c2d78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mia=20Baji=C4=87?= <38294198+clytaemnestra@users.noreply.github.com> Date: Thu, 13 Nov 2025 10:21:43 +0100 Subject: [PATCH 104/256] =?UTF-8?q?=F0=9F=93=9D=20Add=20EuroPython=20talk?= =?UTF-8?q?=20&=20podcast=20episode=20with=20Sebasti=C3=A1n=20Ram=C3=ADrez?= =?UTF-8?q?=20(#14260)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add EuroPython & podcast episode with Sebastián Ramírez --- docs/en/data/external_links.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/en/data/external_links.yml b/docs/en/data/external_links.yml index b8a5fdb3a..6e71ab9eb 100644 --- a/docs/en/data/external_links.yml +++ b/docs/en/data/external_links.yml @@ -381,6 +381,10 @@ Articles: title: 'Tutorial de FastAPI, ¿el mejor framework de Python?' Podcasts: English: + - author: Behind the Commit + author_link: https://www.youtube.com/@BehindtheCommit + link: https://youtu.be/iaDRYUQ0OMM + title: Why FastAPI Became Python’s Fastest‑Growing Framework – Chat with Sebastián Ramírez - author: Real Python author_link: https://realpython.com/ link: https://realpython.com/podcasts/rpp/72/ @@ -399,6 +403,10 @@ Podcasts: title: FastAPI on PythonBytes Talks: English: + - author: Sebastián Ramírez (tiangolo) + author_link: https://x.com/tiangolo + link: https://www.youtube.com/watch?v=mwvmfl8nN_U + title: 'Keynote: Behind the scenes of FastAPI and friends for developers and builders — Sebastián Ramírez' - author: Jeny Sadadia author_link: https://github.com/JenySadadia link: https://www.youtube.com/watch?v=uZdTe8_Z6BQ From eaf611f9ee56de4f16c837d4f487a4b9fca507c4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 13 Nov 2025 09:22:06 +0000 Subject: [PATCH 105/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 042fa4bdc..f689a40bc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* 📝 Add EuroPython talk & podcast episode with Sebastián Ramírez. PR [#14260](https://github.com/fastapi/fastapi/pull/14260) by [@clytaemnestra](https://github.com/clytaemnestra). * ✏️ Fix links and add missing permalink in docs. PR [#14217](https://github.com/fastapi/fastapi/pull/14217) by [@YuriiMotov](https://github.com/YuriiMotov). ### Translations From 5d40dfbc9bc1df1c7801acc53857ec7a072b7697 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 13 Nov 2025 14:59:07 +0100 Subject: [PATCH 106/256] =?UTF-8?q?=F0=9F=90=9B=20Fix=20handling=20of=20JS?= =?UTF-8?q?ON=20Schema=20attributes=20named=20"$ref"=20(#14349)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/_compat/v2.py | 12 ++--- tests/test_schema_ref_pydantic_v2.py | 72 ++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 tests/test_schema_ref_pydantic_v2.py diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py index 6a87b9ae9..5cd49343b 100644 --- a/fastapi/_compat/v2.py +++ b/fastapi/_compat/v2.py @@ -262,12 +262,12 @@ def _replace_refs( new_schema = deepcopy(schema) for key, value in new_schema.items(): if key == "$ref": - ref_name = schema["$ref"].split("/")[-1] - if ref_name in old_name_to_new_name_map: - new_name = old_name_to_new_name_map[ref_name] - new_schema["$ref"] = REF_TEMPLATE.format(model=new_name) - else: - new_schema["$ref"] = schema["$ref"] + value = schema["$ref"] + if isinstance(value, str): + ref_name = schema["$ref"].split("/")[-1] + if ref_name in old_name_to_new_name_map: + new_name = old_name_to_new_name_map[ref_name] + new_schema["$ref"] = REF_TEMPLATE.format(model=new_name) continue if isinstance(value, dict): new_schema[key] = _replace_refs( diff --git a/tests/test_schema_ref_pydantic_v2.py b/tests/test_schema_ref_pydantic_v2.py new file mode 100644 index 000000000..119b76a52 --- /dev/null +++ b/tests/test_schema_ref_pydantic_v2.py @@ -0,0 +1,72 @@ +from typing import Any + +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient +from inline_snapshot import snapshot +from pydantic import BaseModel, ConfigDict, Field + +from tests.utils import needs_pydanticv2 + + +@pytest.fixture(name="client") +def get_client(): + app = FastAPI() + + class ModelWithRef(BaseModel): + ref: str = Field(validation_alias="$ref", serialization_alias="$ref") + model_config = ConfigDict(validate_by_alias=True, serialize_by_alias=True) + + @app.get("/", response_model=ModelWithRef) + async def read_root() -> Any: + return {"$ref": "some-ref"} + + client = TestClient(app) + return client + + +@needs_pydanticv2 +def test_get(client: TestClient): + response = client.get("/") + assert response.json() == {"$ref": "some-ref"} + + +@needs_pydanticv2 +def test_openapi_schema(client: TestClient): + response = client.get("openapi.json") + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "summary": "Read Root", + "operationId": "read_root__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModelWithRef" + } + } + }, + } + }, + } + } + }, + "components": { + "schemas": { + "ModelWithRef": { + "properties": {"$ref": {"type": "string", "title": "$Ref"}}, + "type": "object", + "required": ["$ref"], + "title": "ModelWithRef", + } + } + }, + } + ) From d3b75974f48cd180f6f87de39adc475c7bf8900a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 13 Nov 2025 13:59:34 +0000 Subject: [PATCH 107/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f689a40bc..535dadb0b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Fixes + +* 🐛 Fix handling of JSON Schema attributes named "$ref". PR [#14349](https://github.com/fastapi/fastapi/pull/14349) by [@tiangolo](https://github.com/tiangolo). + ### Docs * 📝 Add EuroPython talk & podcast episode with Sebastián Ramírez. PR [#14260](https://github.com/fastapi/fastapi/pull/14260) by [@clytaemnestra](https://github.com/clytaemnestra). From 02e108d1663149faac4156dd23595cbdf74287c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 13 Nov 2025 18:03:55 +0100 Subject: [PATCH 108/256] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.12?= =?UTF-8?q?1.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 535dadb0b..a3e051a30 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.121.2 + ### Fixes * 🐛 Fix handling of JSON Schema attributes named "$ref". PR [#14349](https://github.com/fastapi/fastapi/pull/14349) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 5c4804e19..0672423cf 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.121.1" +__version__ = "0.121.2" from starlette import status as status From 994d6cc912ecb160e1495132bcd347f4511708bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 17 Nov 2025 20:33:53 +0100 Subject: [PATCH 109/256] =?UTF-8?q?=F0=9F=93=9D=20Add=20docs=20for=20using?= =?UTF-8?q?=20FastAPI=20Cloud=20(#14359)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 57 ++++++++++++++++++ docs/en/data/sponsors.yml | 4 ++ docs/en/docs/deployment/cloud.md | 16 +++-- docs/en/docs/deployment/fastapicloud.md | 65 +++++++++++++++++++++ docs/en/docs/deployment/index.md | 2 + docs/en/docs/img/sponsors/fastapicloud.png | Bin 0 -> 16777 bytes docs/en/docs/index.md | 62 +++++++++++++++++++- docs/en/docs/tutorial/first-steps.md | 57 ++++++++++++++++++ docs/en/mkdocs.yml | 1 + docs/en/overrides/main.html | 7 +++ scripts/docs.py | 10 +++- 11 files changed, 273 insertions(+), 8 deletions(-) create mode 100644 docs/en/docs/deployment/fastapicloud.md create mode 100644 docs/en/docs/img/sponsors/fastapicloud.png diff --git a/README.md b/README.md index 09cd38da1..9864fa1ef 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,11 @@ The key features are: ## Sponsors +### Keystone Sponsor + + + +### Gold and Silver Sponsors @@ -447,6 +452,58 @@ For a more complete example including more features, see the FastAPI Cloud, go and join the waiting list if you haven't. 🚀 + +If you already have a **FastAPI Cloud** account (we invited you from the waiting list 😉), you can deploy your application with one command. + +Before deploying, make sure you are logged in: + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +
+ +Then deploy your app: + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +That's it! Now you can access your app at that URL. ✨ + +#### About FastAPI Cloud + +**FastAPI Cloud** is built by the same author and team behind **FastAPI**. + +It streamlines the process of **building**, **deploying**, and **accessing** an API with minimal effort. + +It brings the same **developer experience** of building apps with FastAPI to **deploying** them to the cloud. 🎉 + +FastAPI Cloud is the primary sponsor and funding provider for the *FastAPI and friends* open source projects. ✨ + +#### Deploy to other cloud providers + +FastAPI is open source and based on standards. You can deploy FastAPI apps to any cloud provider you choose. + +Follow your cloud provider's guides to deploy FastAPI apps with them. 🤓 + ## Performance Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index 943b92adb..b8cc31dbe 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -1,3 +1,7 @@ +keystone: + - url: https://fastapicloud.com + title: FastAPI Cloud. By the same team behind FastAPI. You code. We Cloud. + img: https://fastapi.tiangolo.com/img/sponsors/fastapicloud.png gold: - url: https://blockbee.io?ref=fastapi title: BlockBee Cryptocurrency Payment Gateway diff --git a/docs/en/docs/deployment/cloud.md b/docs/en/docs/deployment/cloud.md index c88c4b51a..bdba87bce 100644 --- a/docs/en/docs/deployment/cloud.md +++ b/docs/en/docs/deployment/cloud.md @@ -4,13 +4,21 @@ You can use virtually **any cloud provider** to deploy your FastAPI application. In most of the cases, the main cloud providers have guides to deploy FastAPI with them. +## FastAPI Cloud + +**FastAPI Cloud** is built by the same author and team behind **FastAPI**. + +It streamlines the process of **building**, **deploying**, and **accessing** an API with minimal effort. + +It brings the same **developer experience** of building apps with FastAPI to **deploying** them to the cloud. 🎉 + +FastAPI Cloud is the primary sponsor and funding provider for the *FastAPI and friends* open source projects. ✨ + ## Cloud Providers - Sponsors { #cloud-providers-sponsors } -Some cloud providers ✨ [**sponsor FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨, this ensures the continued and healthy **development** of FastAPI and its **ecosystem**. +Some other cloud providers ✨ [**sponsor FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨ too. 🙇 -And it shows their true commitment to FastAPI and its **community** (you), as they not only want to provide you a **good service** but also want to make sure you have a **good and healthy framework**, FastAPI. 🙇 - -You might want to try their services and follow their guides: +You might also want to consider them to follow their guides and try their services: * Render * Railway diff --git a/docs/en/docs/deployment/fastapicloud.md b/docs/en/docs/deployment/fastapicloud.md new file mode 100644 index 000000000..b0889974f --- /dev/null +++ b/docs/en/docs/deployment/fastapicloud.md @@ -0,0 +1,65 @@ +# FastAPI Cloud { #fastapi-cloud } + +You can deploy your FastAPI app to FastAPI Cloud with **one command**, go and join the waiting list if you haven't. 🚀 + +## Login { #login } + +Make sure you already have a **FastAPI Cloud** account (we invited you from the waiting list 😉). + +Then log in: + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +
+ +## Deploy { #deploy } + +Now deploy your app, with **one command**: + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +That's it! Now you can access your app at that URL. ✨ + +## About FastAPI Cloud { #about-fastapi-cloud } + +**FastAPI Cloud** is built by the same author and team behind **FastAPI**. + +It streamlines the process of **building**, **deploying**, and **accessing** an API with minimal effort. + +It brings the same **developer experience** of building apps with FastAPI to **deploying** them to the cloud. 🎉 + +It will also take care of most of the things you would need when deploying an app, like: + +* HTTPS +* Replication, with autoscaling based on requests +* etc. + +FastAPI Cloud is the primary sponsor and funding provider for the *FastAPI and friends* open source projects. ✨ + +## Deploy to other cloud providers { #deploy-to-other-cloud-providers } + +FastAPI is open source and based on standards. You can deploy FastAPI apps to any cloud provider you choose. + +Follow your cloud provider's guides to deploy FastAPI apps with them. 🤓 + +## Deploy your own server { #deploy-your-own-server } + +I will also teach you later in this **Deployment** guide all the details, so you can understand what is going on, what needs to happen, or how to deploy FastAPI apps on your own, also with your own servers. 🤓 diff --git a/docs/en/docs/deployment/index.md b/docs/en/docs/deployment/index.md index 2364791a7..8d7521e73 100644 --- a/docs/en/docs/deployment/index.md +++ b/docs/en/docs/deployment/index.md @@ -16,6 +16,8 @@ There are several ways to do it depending on your specific use case and the tool You could **deploy a server** yourself using a combination of tools, you could use a **cloud service** that does part of the work for you, or other possible options. +For example, we, the team behind FastAPI, built **FastAPI Cloud**, to make deploying FastAPI apps to the cloud as streamlined as possible, with the same developer experience of working with FastAPI. + I will show you some of the main concepts you should probably keep in mind when deploying a **FastAPI** application (although most of it applies to any other type of web application). You will see more details to keep in mind and some of the techniques to do it in the next sections. ✨ diff --git a/docs/en/docs/img/sponsors/fastapicloud.png b/docs/en/docs/img/sponsors/fastapicloud.png new file mode 100644 index 0000000000000000000000000000000000000000..c23dec220951ca1e5321411ea7200aee48cbe89d GIT binary patch literal 16777 zcmeIaRaBL2^e?&q1w=|fN?HXK0qF)sN+boO8|m(DMWp1TyQEvXK}w{%yF)^{_vC-Z zxj5ro@2kBX=t5j;z3+Nr&R;zS$V!W0VUS`V5D2Vy;=&&g2&8%V{x>=*{P{|q#RVSj zS&6IIA`sXux4)4(EVK3Dha`3)%69UW26m1*HlGlVj*g7R7N)j(I#!<;Eo}_rclbyV z2r9%o;Wr9S34doC)fHDL(f0Z@w(I4r($T+VbIGb#v(v@JWv>42mSA}QX!4xBTk?JU z$8**mHU^@M6=7VoloX$@FT>tR?;zNlEjii#wgn=g8zvdYW(2{0%=7O36}5Z0}5ySkjzZTV7e z&?S?s*C9PWUNWb4NZMNbT%J~Uv9%;&NUO@~f#CJ2ZFnKO%b{Kjx2;Kd?_ET!WXR8- zHhfO9e#66T>NR$E+1bkzdLIV(oo<&(;bPns9WtLf##?#L5Tln~<_c!zM7S`9B&JzCo$)(k@gxfT@5*Qg9TdJH3(YZLbl-1DC zV>1t>X#V;Yoik39F?wmXUcEH3C?`TfB;AvqLHR$it^{t@n8N0S1lO;@^=1orJ_oN};S`{4P)xpz+I_p=o z45s^wSyZ^$s^Zd#JY%^idyly>c-YG5Oibxh75=2kvfYT$7{AugV3_s1c|1H!T!a)O z@r;1xWqzp2!En@fD{?HX;l?&mD%_^Av8jA#Y_f66rS@Q!Gd{Yk%)9dx{63P{$Lh{b zReD`fp|70(-SIU>#UgErlTn|3XiJ#aNr3V6;O)bC&ydx%*&4x9_}Kn_cK!-4ywwYh zjzbk*yfrp{o|eu;MZ20d(KT6WXcai~YGiRybr;{j_V!)7W5Q17eJic5FYigl>2p=Z zNIbz?EFaUf7_62mK&rGe#Ypb!P_wKLDb%Q4C$qJ)do3$VWlC`qvYU>F@i}ckW()hy z4Q;UC$tZ;vWi_plm>8OqQ`PGnh1ZPHoV<C?gLJKl@Dm<)^4DP7mS=p^`ze<5{BUkr&g9&48QIDo z+gux7w%su2dcq>7q7p*s$v3R)nV=rG9TymvtayHc<14OzS;{dspE8h6+0vit8=n}O zbJe6Cmz?q8d2Vj5y~lVaQHe(1+#HUFhq+2$wbTB8onyw&gV9i8jVovt=H*B)kHy*F zxVqH%bcDCGw^yk2^o@)}{_bt?y-$R=LXkncF7^U(ap9U?QGtq$-794wq412L%7FnJhk}5bb~P^c z_VQ9&-LXbLeoR{o*zjSXVzbTi#f97AI6I&9H1vLZ*aw@h!-({=5RyQ0z4f1gFjs`d zR0Y;#`GKL7q1ks8cX$4@G-)cjHHzcyvyk#2C=K0>)qL}I%Mmig$o^CZ2j43|#2HMT%dmaAVF9JT zqJXKczFynV&>u>0M0E7h`H_ACk3Dv!MFxu}7hGv?&x$k7Qbgo#RtBasABD@&W`H-! zMXKwG#d?1#e?kHoHC_N!n7FKhLJ(AMVX+PxT3YN6ISQ5n4Gpun=oB7D{Cv{`4eq?@ zW76;54P9Jh=jSg^^G#*EH{?58#?RltCg<^qWz8zdw6(TgwbSr&#bSH>IFNm&C;qv( zq@<9tvSs?l@Enr&>n3Byi;VZHTi7KfC0|}s`6<7&_$O6ojWw?B9)|3VfP2l(!>>HQ zmSR^^Q`utQ>Lu$2k9Yhg}=w3URU(`h*ip~u6C@d^|{bBIwkE}InrlOU&ByUfp za1E#i#by&Un8dj4`LaVXrKR{#2SNqhg-VPD+|(P$LM!L45insf{H~~)E*sA2XG|LB ze2iKloKDldz1X&E0^TI7nIszMEiEnNZ{B!+4NCC&^^3;s`b>$-;}R#B4IBFyzQHy1 zEUMUF64Sdp9pEVvyt(Z98WQq}Ke=OR;b?5ESd;VV3Wvw}1|kb#CV@3ReP4UJHoA+{ ztF+V>k-hqpmz#tsPbl@(Ir*#hwlLyi8|(2Ro8|(8fdCWapWYKyqkP6XI}V=zeM{|H95ryBjvfXFNqk#b=C+&E=+@;^N{$qM}jn z#`cennwz|Jx^I}YLc}~gQae`^4)*s)iJv*HNFe|Fhavf=Wnciw{o+fS<(%xDJ9a)W zOsvInsj_L=+32yc6XN2sSB!@Yh+!^7ylKw7XPy`>qPCVdrLYjy!Xim3xB5loYrDhU zXG~0Qg?`NtK1-!#z(oCVGoEN-eYF4$Wv-k-fb=tO6D% zx5ZX#<SQs4JCrG6r$h1-!>%E*QVu}*jCg?Atv&9KUg`A)C;x&DmG@z_C%P`kZ5Ub zs&$gIbcmIWjqqA8IkR3n0iYCf-5 zmSU>~GJ_XHV~@&TQ#Dmtr{g|&5VG4aWo}34Wso%e&(YSl`8`vC_D5var(SlO!$@0W zjXuz|_S=Z&-n=Z-Z!ku3Vooj*HkJms&#PB*20*>;Bwwmu- zAlY2rUS%?ZF)G&rfONd!>JehzZs~5*Il0^t9%5*4kbCU4SO{6~Bn>rTp!NA7H*`*F zt{C@yoWkU%fLrREWwd|YQFMI0{Mh9EuTF2Kfsw6l9*IjOZlayu85Pw;perQEr!OS{k0NHTm}6zw@iB zU*_j6c=Vo}A7i!E?ly!-#aApeqqLmhXMD&}d~e8*CO3Izea}NaGpGaB#QcG?YH~EP zJv~}vy(a`7RbGPZ2DTI?hf?rVVIv5&`bo9Q6DK7L#Pgue9a^ZV?) zp_@G#ynt8QueBtT1?~w5Xm)Q6WyuJ^Zb?hK9_D$=TSbC%N2A8>*CWe1OnUkUwn-my zgzfCeLMX1t@7~41Oqxpd70i@!CA&K3H#;(cNw93JC)^tI^!o3=5A|!$BPZQ|^x%BV ztXup2`yEtv%n`#333{0H%hO~o`^|)A??|CkTx#lOr^SGr+i~>DZ8MvxQFJ=!xsk=U zGhJFWzzO?}F*|D>&xYulnZe)LNpcAt8U6V9`3A<%pWg=hwUeG6R^6GOpWhLewwNpn zek_(&>~ZBvS~M{}KEG8|nI@CmPrEWfAR^NCyL*_-ekxB%M*8t*DF9UgKYlzS<{NkG z=>q_nKh)?aM9pF56l`B3EhI|scr7a|qP+>j zc;zg)cNr|((>aq`aqss+LmLgOT3z&j1+*6H__F!5X1w1&-Km|)F5+y6k5+ggBH8^^ zy&=I1TJ|e;BgE||7JpwGSnJ5j-n&2=G(D|eVlq++tgV~-)29_6X!QD@R*D;nwzt*I zCjLDHbiDMLDC$>pGlkcSX9X&y$h&o=olU+XU87#XD@25&kL0tYhs8a&KE9~1yrCEw z84)dd9NzgoCdSWn>_29mUq;5_ksdV50P?5CTB5aBq^QZJkE4^l}Z1{9cyY6vy z{&V#8*4CEw-?#;DE5vQZOZJ@o+~7XZ`1s<4QkT$aVQ{%G<9G&^*J^j{^$D5WNZ&xZ z=X@YjgC15x@Vkr;mxn`dE>D;GBn8|AaBSCl`^W82yxPk#M;M{Ye9TwIG&B^WHJ9$? zHyhuh&YGVZFHp;I+A0VIyw$oq0_cs9F`fH-_OYSR9spra&i3kl|i?- z-OlvDkDo?@=yrc6@AA8ndY)E!3gUD{6$o4GxFd2C9iy1EzCC?U_{#Wa>8{RNFC}ap zN=a>e^g9MKUz5xc30YZaQ4DI`aebZPSyn3&KD}G&$giSFUqz2boS(botDXGkZOWBq z*rz?JTBIr`$J}skf`&+udZaaaHiD?kyoBY5?4(Q|99$oSf`)DTB3yt@Ch2psmKr8G zX-G|tOKXP~N@&NB6V>p}&~TQe*2pm3(f*%5FU)2ypKhU>zNO%M_G`6A3)BntZXsF} z)Wfy_{H(CZ4E~k=G6NfqNvX*%|KfSNF0neXvBUi*mzu5Lb; z-zr3^JV%%r8r}sOT22%S>)= zlHLNu)1$F;m-zdQR}OT93@#Woh8$}eukUFx>#^C3C_ntn%o1e3)98;HD<)%udAqIs-MIBkm7GthK zU9Vpw$7pqC0o(_0CBsVKi_A zFNe4PQE*uif5;&a3%T@$(MntjMnh}_qQL%x0y`*vax_*OSI?@dsfl!bF?Xe0bGTrk z5*8M|-$@rY_fWBk&3*VMvzc#FT|GauQ9puKLrRLF;dJrBfJwsc`cee;R0^wk7^==y zvaNNAe_|qeLV|jH-Fc({JA35so}M=@E)6ON<{MY4@Xb(<=li%mYmN?Qg7BsUQuaG`()<|HMDdDb*l2jzp^q* zRH|QnO0m67qiublCH9Skw>OfNwe@JW3g;&nk{b7>nzPTJk5EHTtXdDCB3MzB_pSD) z>a9;zMz<&IjB27-9Jx0Y7Gm%1?fC%L5u)atlCiL$#kk)kE34fQ_S<~&G|oPvP#_p> z%J0jU;!(u?`+<9hLw%%BR{qY`hsyJ~*Unn0xRh$}dT!5NF@%s!ghtXQzZDn90csR( zNA-(;%Oa>kv@=_@I%9|D z^#QZT)zN|-g)J zh0;aV&!5OAyK5klxRr8*mDwGX3eyY_o0Ynq;iYdxN<1AcnDQF@u!GXg!oqUI$dn~D zm#r4yccC2@LO!Lpzj!uYy2cAU7ISN?h&2btWl#HfemKIuBKoCDQ_kqweuo;?s5+7b zyLOY4U@`X^-y@pHKgAX^YFt+HO2K2kKFBKZzu_wW4R(%IH9V{+HvffNUg>eJw4-<* zVxewMmgC#zH#ATf8?S0m@3sdMJR`7KC7r3UXI5=r9rFraqr(dTF+_y>fPl;qjtGgTKR(0`fCzzw<#He;Ro`OVAHsoy z-i<5((q!x}b6%Z~r6=#~|+RIGWLfZn@N;3lkYkdyZ^nx;t zcXY|z1RC2=tW;HbMvFXgDlO**xZCabaVnzj)6vld8_3Cv5ptrb+2)rXwETo6GDgxl zs}54tGX@6V_;`9mQ=7l~i0BA~wA??Z z!NZ-MNU5prRCpy!waW7H!PC=x`UZqCM_tY3orM)2r^f@Xssl3tvx z`HF^aEp2bNcI&Y{pyiA@f>4hG73#HEEjz<9Her;gD z_@}GqVr6MkpVL}bzf?>GxhY$&C77ZpKi}+wmReuZHcAdA`(MraxUU+u$Jz_OoN2Wx znW3wgO;y|rr{z!O{OYhf8*FbZF7JSf>{X&CW}SPq@5BcThr*Pn5Hq;UB48)z+c%8u zDMwtvtHb**=&>rVZ|TXCR=z;bufjCJq+!+Ze8B_x0UI2jIW(QpGyW)QY4xeUbqz@srG)C-7=pBn4DQ#ar9|^6mAbC7YJz zLH&-;uc@>(sA$;P3*H@M(n%7x#I-A1O@TsB@Gx>7Fkka%RQ>xVTo9iC!lBH$9r*S# zGkOn3Aj~W+{gpITj6a-pxAnh`=Z?Z>pr;qpSwK;*#tR)E?-G|BZnXZw!5HWXd&wwB zpYumW#mC2|*5)Ke0vYZ0MCd>L#G9CL6X}p-mPZ}%?xNPTVrmnN$?UQ z;A`Dl;>){@Tj@ zdAk(}zO{LL-2S_z1|l|$;nf=#{6|}3{^h7I!o;mMhqskME$qk7+{t%aG zV^z$0o@8obBBw_GrJ!JQfuAZxzO*n)W~?w#IsRHL{3AbB0DY_^_GckuyG=P`V`huF zUSV_d`r8jm%gZ?K^914Ncc@+Pl(Ry&jg^?k?v*Kz-bE1yhtuBcJtsc#j1RZw67=xq z^?`6u`vSRaSNXE|4v)HyHiiO^u58J#KqoBGX_+L{;E4!s0Q5;}B!pZ^nw zWUWWRmu1f*C4OYi>V&wux^9|fjH~B9>@%X+dn{6)E4`%Uwv>GC!aO*nc;B+@js<7; zmO-tK&Bl~Hh&O{7_f>`hglY0MYDxR}4%kdpN9tr+kYYNPbbuL;S6b2m4$kWpBKeSm zX>F}kY%-#{J#oOgb#%yXZ#yBLv{koeJVa~#r|9X%7Ru^J6m}WYZybyVdG948tWOFT zEYQt!ayDe2DX7ifyMv5LDUi)AGOWF=Bk3;nSty4Sce)`FyIQ^Z2Z;cmkG%ns5~vi! zY}u6Ijv5*$JF_F|KjmK*4n0dxeXy^Ne#Z?OOUHSV<&4CTIIGEUKdqL$TBU`?{O;Vx z$2X(1cchSrU%&MYGWHq03QPU*gE%KAHoM3&HY{Y-?B|lt77fn(sFzK-ihUd8m`@2X zm@THCk#QA20!p+Pq2=Q8W!63t7!gQxAt-1LOQn4RqE^KwZIpWPBvmy(IYqXnmc6>x zr`rOFQb3zbckk=%?NzR};WpJ|km5zSIy^^HHiuiSPgY=RNr0fUzdEy}UETbcliI`Ez607m@ky zqOfHH341!gVSGU8i3I{QSU7t}N1{eX=v!`D=97;D&)fuVO~O9DQd}gYn!L*(NxTs7^r-Xf&zxa)#~g8eS0 z#)bM54i@8Zp_z#PHw&QYady|hBlD>-=NnrRDJ@miFT9T3wI=vY<{s?Y=Z@7sf0CAz zL^InNaUn(ylkeU)SO{ctNNZ>?)(`^o%yw1Mzn}o?3l629fV<1$l;w#LAC(Qs{9y;B zAucg7!uz#iGzq&2I*7|w7Y;7rf9@i5bW+#n8aZvR&d*U31l-@yC8{%uNlP>G*ncP| zuwS3Y0g#ifX1Dnm7Kv`Fc48E>`>x81$c5pYgBXs#3p>WPYofXr$Gh=-L~$KU)GRhM zz&^MlQ`u7ym?K^UpOJauUjVtfZA!g~lHyOchnV7MXlN4HgL9-D%wD{B@jc!(`JiS9 zH~=xrkFy?+U@_}ZVrBxShN=Ynv)|3WXf`IJ!w$>h5wr27IgfKk=v~^{1zx(|`iYgWjuP6gcI!)J@uLU6$BNi`%_VOgt5u-z!g@ubZh-APf&Wq9_wQ7z zS2eY@x&vtw09n6x-WO65-Wa?GKT%xLtqR*7%xlS6!I->u z*Fh833gJv@me?SY7}(GI=Ub-AYB&TKcR`%e(sQJe=i=e9!e|N!!R{8Q;G+l~I_XR( zcphw><33kyO9VPE?V5&?QYfY8<=yvD-Fm3?<;F|ChRGUU5Ggyw7HQ>XB z<*>TS7$&~g#Uy5Lajc$on!sgEsHsV@J}7eJPKa?AY;;ma^{SxtbuQ`q9{~mF9ImH= zwm(wk;O2(MsKLVb>=iZjy~m=M2AOZ|H-{;PGJ`w99@D9eu|tn)FEhr+CA+km4|gf6 zX11v&b3G<&{kL*I22GWdlZfwP+l@@n1LNJh>Cvs@{rz9doqE?ht{>~?1h2V#_m?R3 z{tQf-HVva;j__$$tE2XihwA_CqHmh1J)sJZb)?h*0NZDL))`f$bA34EO-ORrR9rW0 zmFlP9&9FHI2O3x!M_V^7+wbyvB7&a|Tg*s&`WFu>^%f1SS1#%-1P8K_v5ACIzgM53 zqB?v^%*=r0b!O9r*|ey7VUb%`Cv1`V%Y1_O}#N?Eff7nutNg=_Ed0d~p!oa{_KWFsE+z1`X zOK$!h_5jxBh_lhs!5QESO1%#3j?IxiD80DBTQk++-g+I^Z0E5cbPJ0l4ssPa?ov2i zpJfeX$;i|PesXek)ouOy#%Uwt6FLTlwt&Cwny$)4{RsHI`$IB~X@!L$?<~#DKmY!H zPq{>w8XS5QPR^N8TvJnekSE&kWh8&u_Az)hdOljcG##^;AI!MZ`^g9^8x>P=2x#Jn z)4BYMNO0(b<46yfYElUa2*!0`Yhz$g%uiW5E`BCTVt1WAHxd*EaUdurhD%y%PrXy0 z1)A*aIwHNQiav(V*{6BwJo_a7==k{8zm+d&UvfrUf zn3J&(E&Zm}!F-@d>)AmZ5VA~3610pDo!xT~ zqOr2F>L<3!r1#_=rnnqiK~NBP7|iv*Q$gd>~o2xeH(VEO&Qi zx;pAzof#(4nm--o9UcbJvQJp=x8MlcB*~kKzj;HQMH<dnP@BK|KE5kIKyY?L*cb7~Iy~U^VO>&pl7skV* z-|P!J!3Ksn1rrkv7uUFV$57(AvYcFiFIwnQYXB?lj4Rme3*E8AzgKJ$Z9pt=LQ+Jk&PzIZ4uQS-T`Cdxc*h38Xx23)W1-=9ZL zS4T>?ptSz5eTZ@Q`gBRg?R3Gw?Vx8(bSNq^a*NzvOuS#%JTAJ^_u3d4$8(3T)y$v$tZ?LlbEcR^Ns1@IM zOgLsuYW#P5`wkF_!=0HxcJue{$yLJ*J=LdTzzM*DRs!Sljc_Z%ZhO6t2Th4lX?|zs zWouj8W6rVD;~$e#Km#5KE3Gr^WyvW?|!GJKWOcp0re&b zQu^0AmcDX3r83YjqlWScfRoFXwqjsTYAC8AKQHgL(gG(8O?``&_C7i!KX3?e`V+b= z8b(Go%l@Vg6Tk65mZ#yp5cqcxsCu~Z{s~@lOG~2t++@2Ndc_^6la8}z zxLGpEEs&>pWow1?Ah3{ozYB_7x|)oJf|)vX5#6DhaW00)vfBK zhq_;Q=y3#|93A;2CNhwbZ8xtS$Z9dY0ltTEKk$2EVwdw64SX7@_y+xJ%T2!qt!sg~3P_1AG zr&d*c`dEKnxiy%<3hwyZwcf<}#YM~j;cq{GvP48hQ9s{6E!2|XxM*Jrw%?h4keW&j z`XdVXMcR6LD4&JAfz=WaMZ+qSgrJP5CWRVSyB+w|4i33@H?Xi4Y5Z_)ZxNhP>pgtY zrlFySjyw9)j614%$)2K+3H@n=OQ_&Z{vqcMUI4+ncR2)Z5F9Pz0{n7UJ67^bM8qLs zqJuDmNVDY-*J{9DEM`p3&kJrdr_Y}uZvL+J@G?})vO?H2Ef{%kE;rX`n4a42yC?Sl zjhz1fVii1fGl8N_@r=d6gRXl8R6bhz<~zxaR!=RvGDo32_(enE@0cX`ltYo>uj zUw1fYiaJzoxf=>@8!{p-T~AIyA)k{R{B;~bw|n3^Umi~}Z_jyVi~N!LogQS;)S7}% zC*2Gg22lDS;mY8|m(*N#OP*0N(ceP2)#`L_-?KR44&vXzeOv;9uOJ_`w6(Qq*JLHd z`C>g9)oJ-+?{vApk2++(Md$?ypa<|IhyK5{Y!)%(#pe{Mzw=UfVIC`l|LfsdWnM4 zSQA8BAl#8BN1EX_dV*3Z!vn_;v$|KwCG!1A}mZEhSd7j6NC!ubhbi-K-|`0K=Ew72Zx8R z^_K~5@6yoF@OFQLIL2l31Y=}`oz%G2ZX>9Jk}?qdIk$sg3kDuGwmUFFDL;N-x*lJ4 z=S)Ma>%oHsVvd){kcug;th^NkZ(CY8oeW3_5%0Rz4j#Zdxt+qXu}3ViWKh??e&Y$) z8VkqLY=MtjYS{O>(gNxBr3MCsw|@W1%0tnv)xUoMT|Qr*EWZzml-!38J}_TE8!$jk zPnb=vfWN&|=fVNC-3yG#Z-MXGLU0~sn%D?<3ZOPL2moqr-CKBT1sqVcCMD%HfC&_Y zt{#&ynXQAvuQKEC2V`XEz*7}Ieq7uf5hmk3<^^mk^}-TZdwY;GrukHbcX&8Hc!Aby zy&4I8&KTU>)d~M@<4pTY9ckujT6<2fJv^wdFAp#oHENI{&=KY^S@zCoZkAH1@X7pQ zTNjUWPdw*+3W^;r*JI@W{vo_BmNre@-mg-K_lcPo5L3Dz=7^qyR$)J-vB-EUaC!RX3 zL~=eLI@zNgPkqb#Qk9Jo2G9k?3UauF3~`}lWp>g?Rp2q1&741|vD?VEHG>2-w5PTZ z@>)c;&-!S=qX(pyP27oWbAflu^{g? zFi-*w4*>gnT#8xBj~_om#)Whj31MSnV}0X6ac~Zv%>IDbE=#k@-;$0mIs<80#V$uY zka5yy$bu{zhlQKWZdoGOfoZmD(#o|C^#gpjDTxLDUG397YMu_7$650bB!zqx7R;^j zGCzEd-zymPNKTu>?`~HU*vO;kO-M)@dm~C}^D8Tzb%g&DX%Gm3TNOp4!_&pQtNkkg z>@?S1^VZq=xpW9pDmQvCWJ<=OBTg3sEg`b{Al%__y<@MJ*Aju~O$u%gdn^Y=%iF1n z21r|MtPjx0i0 z=rVjI2%GBZA14fvf56S>6AdE%=5fV_vRAD0V1C~TT1UYD#7&jvn|*134yJnEG>}U5 zY$C|;ZNr=yp8P-)xY*W1zk~ebiF$`OiodL~@*OQL%kJcxClEs`vELGvjOP%0ylxBu z0vI%z2Y7=huyp~tajG}CJycX2fwlzEQOfCR+p+bF<$b5|y0f>Kq-+RCa-HsXxQmNd zp#tX6tas5}_*G5(Duohc@H;Ys_0!9JCCpH&w#Q4ri!6Nh;zfI*hHF@y%Oe4SS>&q+pMr7$nxV!k{ohoHwC;@1 zfL`gg%F=$#%`yG9*4KeRfXfISm)UleEr^&o!dZ(Z?5puTqv^UDk1JJ}oz^yuX4noX zvd^v=IjwfoF+(V3fLzH)Ch;K&UK>??*4-TLxSbnF5WJ6WNExd0Tx@+ldcp;VKL8sv z%{lXhs#Si7)q?)M{ZbURHs zTT3?FTZvY~#CCb?hlTZvhV~4$P3&Z$P-+uws^M%MVPU`9+7JEpHnf*`uwuhqVK#w{ zSf5}ehLZlu^bD4IdnhFe0@4G~qI)BTe!-|RdQSmXT@tP;l`M`jhpWoN`kK%po9c%Fd6Fa_rm!A1nwzqbVea97JhO9!)+ zwjgMm)J#m25JG%qe48=W&>)A4;=(cqu(3X;^)MSBuaMQZdAOMfalFGW(9=CK;#6Jh zsN{~Se*O(h?uMeQ(KUFcCW8k=9dwG&u^&_ukbx@A?gUzQl-b$a;14XUR8agsF_gR7 zx-5T5W%V|j2C8I~lhJU#4V37)R@HJc^bTGk{)`ipX>V-}S*V*@zAs&Q0;@Ya z$0zIxp9-_H{Um9JTtnali-^cmI41N+qwj3j=ArRn(I!5TW81p59<=*ZuoOV5W&&-b zS5k1$VlYblNtNBki`#0q(Bf}&XTIJ2*QU+b@bF@5Kx2^R)Huz{(b3UvFZREaPn1CO zd>(rIY_}Z)L!r@?o}M01glSOC8jsPRo}S<{M`PpIX1k|PGYGuig#$b^$e{2_$Nu;w zJ2Y=7&$p(&UL)M}C4w#U7aVwbRBsKMD>--q4Y>to}iT#hm~O%&(LfFV}# z;1fXdpk4oJ)D0h%LAM3HuW|(atEq6PmDmXDDGng5Cc_>O%p0G z76Jm7MIxq>_wJ3!X=&kDS~5w-u~9(n1~2}NkWiZRBc}AuoxbdX^m@U?`Yho|%=$ahLg+SDrK8(7=G%Tj|Pb zb!n+*;~S(>Sm!eK_O4xqxVX8m^BhfRZY+Qns3h__B0!P9CuLW(vD1&8a7Q#yjyd+IKC$4v>2W< zzR~|vK=HENX2q80z!^NAkNHlbbVcLiTyS*A2NDi5HPLpxtVAG0Vy3uUpM63og9gIzcjWa<<+FARgKcyJ_8aR{Kpdgf^tiT;@(n`xu!|Q{JIJ z^7i#@nTle(b9Tc5F`>iF5qt;*o~}gCv4udg@oSqs9GU9?mI4Z-cAO@FeLv8SJ0hOr zr$QdfVg}V9)juvy^>+CIx?dm3qXVEVCohjeNlDoa;5Lw`4=uTG%vH)94x>OPdAIlP zA7oN@$MjeN4g zkd=n!myTtT3*zkhVnz~6GrqR=5!6AwL63{S(urHQ90tHPC|Klr9U($*-=b`8ZGjTB zur@}xuy9ojXHLK!GB3NJVrR!n7ZXHNe_vTIDxaQ}i@2O? zfPnZ?O{QuM57@K_Z;H3N$F^fLt(BY)=&9!@p@z8~++GASDufxjx z?FgkkwoFCakdpyWn5F)g*)=S&b|F+-G z!{q_fmL!8(twZ^2RBT$B?(oP+y1T3AtpL0|b#mV}A53kaSS7QB|CJi14?z06s=9h{ z!cd8Y-s~xD`)fGU$-_f^X03_MwOI=W(MZ{m*L?lRIx2hNYcP0W;#nVMnC4| zMY|>F=SRC6{6*Q>!~pev&CU|igqb+E#&cTg3}yCNK|I&W*0$~MlyQP6IuW5j(?W~f zGkSVo*=Igr$GW?t3=Zus0;gP{A|ybLoF6Sv0Qnl{ zbUD255WDG}OkdN+kRco-!~!<*1@#`tf$b1aZ2^EikQvMb7DC&`&;aQcZ&sG!{QAJ0 zoSNEwZ!d7Lu}i8bR8%HlUkU!n#eg%tXz}aV$0+qkscSbS`M~ZS9)3>J1qg8FU^Xcg(kpZmXy<7)l?^@&e#z zQt#T76kK3Zh&?zii3N^5oE32cR0lJ@GFQNt@iibouW%wqUJPPEkS -{% if sponsors %} +### Keystone Sponsor + +{% for sponsor in sponsors.keystone -%} + +{% endfor -%} + +### Gold and Silver Sponsors + {% for sponsor in sponsors.gold -%} {% endfor -%} {%- for sponsor in sponsors.silver -%} {% endfor %} -{% endif %} @@ -444,6 +450,58 @@ For a more complete example including more features, see the FastAPI Cloud, go and join the waiting list if you haven't. 🚀 + +If you already have a **FastAPI Cloud** account (we invited you from the waiting list 😉), you can deploy your application with one command. + +Before deploying, make sure you are logged in: + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +
+ +Then deploy your app: + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +That's it! Now you can access your app at that URL. ✨ + +#### About FastAPI Cloud { #about-fastapi-cloud } + +**FastAPI Cloud** is built by the same author and team behind **FastAPI**. + +It streamlines the process of **building**, **deploying**, and **accessing** an API with minimal effort. + +It brings the same **developer experience** of building apps with FastAPI to **deploying** them to the cloud. 🎉 + +FastAPI Cloud is the primary sponsor and funding provider for the *FastAPI and friends* open source projects. ✨ + +#### Deploy to other cloud providers { #deploy-to-other-cloud-providers } + +FastAPI is open source and based on standards. You can deploy FastAPI apps to any cloud provider you choose. + +Follow your cloud provider's guides to deploy FastAPI apps with them. 🤓 + ## Performance { #performance } Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) diff --git a/docs/en/docs/tutorial/first-steps.md b/docs/en/docs/tutorial/first-steps.md index 7d4c12de8..b88ff6a18 100644 --- a/docs/en/docs/tutorial/first-steps.md +++ b/docs/en/docs/tutorial/first-steps.md @@ -143,6 +143,42 @@ And there are dozens of alternatives, all based on OpenAPI. You could easily add You could also use it to generate code automatically, for clients that communicate with your API. For example, frontend, mobile or IoT applications. +### Deploy your app (optional) { #deploy-your-app-optional } + +You can optionally deploy your FastAPI app to FastAPI Cloud, go and join the waiting list if you haven't. 🚀 + +If you already have a **FastAPI Cloud** account (we invited you from the waiting list 😉), you can deploy your application with one command. + +Before deploying, make sure you are logged in: + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +
+ +Then deploy your app: + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +That's it! Now you can access your app at that URL. ✨ + ## Recap, step by step { #recap-step-by-step } ### Step 1: import `FastAPI` { #step-1-import-fastapi } @@ -314,6 +350,26 @@ You can also return Pydantic models (you'll see more about that later). There are many other objects and models that will be automatically converted to JSON (including ORMs, etc). Try using your favorite ones, it's highly probable that they are already supported. +### Step 6: Deploy it { #step-6-deploy-it } + +Deploy your app to **FastAPI Cloud** with one command: `fastapi deploy`. 🎉 + +#### About FastAPI Cloud { #about-fastapi-cloud } + +**FastAPI Cloud** is built by the same author and team behind **FastAPI**. + +It streamlines the process of **building**, **deploying**, and **accessing** an API with minimal effort. + +It brings the same **developer experience** of building apps with FastAPI to **deploying** them to the cloud. 🎉 + +FastAPI Cloud is the primary sponsor and funding provider for the *FastAPI and friends* open source projects. ✨ + +#### Deploy to other cloud providers { #deploy-to-other-cloud-providers } + +FastAPI is open source and based on standards. You can deploy FastAPI apps to any cloud provider you choose. + +Follow your cloud provider's guides to deploy FastAPI apps with them. 🤓 + ## Recap { #recap } * Import `FastAPI`. @@ -321,3 +377,4 @@ There are many other objects and models that will be automatically converted to * Write a **path operation decorator** using decorators like `@app.get("/")`. * Define a **path operation function**; for example, `def root(): ...`. * Run the development server using the command `fastapi dev`. +* Optionally deploy your app with `fastapi deploy`. diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 323035240..df47c6f9c 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -192,6 +192,7 @@ nav: - Deployment: - deployment/index.md - deployment/versions.md + - deployment/fastapicloud.md - deployment/https.md - deployment/manually.md - deployment/concepts.md diff --git a/docs/en/overrides/main.html b/docs/en/overrides/main.html index be31bd75c..01d39817b 100644 --- a/docs/en/overrides/main.html +++ b/docs/en/overrides/main.html @@ -3,6 +3,13 @@ {% block announce %}
+
diff --git a/scripts/docs.py b/scripts/docs.py index 56ffb9d36..1a336a036 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -145,14 +145,20 @@ def build_lang( index_sponsors_template = """ -{% if sponsors %} +### Keystone Sponsor + +{% for sponsor in sponsors.keystone -%} + +{% endfor %} +### Gold and Silver Sponsors + {% for sponsor in sponsors.gold -%} {% endfor -%} {%- for sponsor in sponsors.silver -%} {% endfor %} -{% endif %} + """ From 4e84f3169436dfe06c963928a6af297d06cc560e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 17 Nov 2025 19:34:16 +0000 Subject: [PATCH 110/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index a3e051a30..a3a83473d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Docs + +* 📝 Add docs for using FastAPI Cloud. PR [#14359](https://github.com/fastapi/fastapi/pull/14359) by [@tiangolo](https://github.com/tiangolo). + ## 0.121.2 ### Fixes From df83eb7278edf036612dc1b4c33c02415123d6a7 Mon Sep 17 00:00:00 2001 From: Edge-Seven <143301646+Edge-Seven@users.noreply.github.com> Date: Tue, 18 Nov 2025 15:30:20 +0700 Subject: [PATCH 111/256] =?UTF-8?q?=F0=9F=93=9D=20Fix=20typos=20in=20code?= =?UTF-8?q?=20comments=20(#14364)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix typos in some files Co-authored-by: khanhkhanhlele --- tests/test_tutorial/test_sql_databases/test_tutorial001.py | 2 +- tests/test_tutorial/test_sql_databases/test_tutorial002.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_tutorial/test_sql_databases/test_tutorial001.py b/tests/test_tutorial/test_sql_databases/test_tutorial001.py index 6604a2fd3..b45be4884 100644 --- a/tests/test_tutorial/test_sql_databases/test_tutorial001.py +++ b/tests/test_tutorial/test_sql_databases/test_tutorial001.py @@ -45,7 +45,7 @@ def get_client(request: pytest.FixtureRequest): with TestClient(mod.app) as c: yield c - # Clean up connection explicitely to avoid resource warning + # Clean up connection explicitly to avoid resource warning mod.engine.dispose() diff --git a/tests/test_tutorial/test_sql_databases/test_tutorial002.py b/tests/test_tutorial/test_sql_databases/test_tutorial002.py index 2c4e0988c..da0b8b7ce 100644 --- a/tests/test_tutorial/test_sql_databases/test_tutorial002.py +++ b/tests/test_tutorial/test_sql_databases/test_tutorial002.py @@ -45,7 +45,7 @@ def get_client(request: pytest.FixtureRequest): with TestClient(mod.app) as c: yield c - # Clean up connection explicitely to avoid resource warning + # Clean up connection explicitly to avoid resource warning mod.engine.dispose() From 827ed1e6a24300af9b46adb8a3d208b8685eef85 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 18 Nov 2025 08:30:46 +0000 Subject: [PATCH 112/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index a3a83473d..c3ee858ae 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* 📝 Fix typos in code comments. PR [#14364](https://github.com/fastapi/fastapi/pull/14364) by [@Edge-Seven](https://github.com/Edge-Seven). * 📝 Add docs for using FastAPI Cloud. PR [#14359](https://github.com/fastapi/fastapi/pull/14359) by [@tiangolo](https://github.com/tiangolo). ## 0.121.2 From 89baa704a9f88ab09b0b01cd2fa7dcf758d6d417 Mon Sep 17 00:00:00 2001 From: Nils-Hero Lindemann Date: Wed, 19 Nov 2025 11:12:00 +0100 Subject: [PATCH 113/256] =?UTF-8?q?=F0=9F=93=9D=20Add=20missing=20hash=20p?= =?UTF-8?q?art=20(#14369)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add missing hash part Was forgotten in #14359. I already added it in #14367 --- docs/en/docs/deployment/cloud.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/deployment/cloud.md b/docs/en/docs/deployment/cloud.md index bdba87bce..4f5c23e4b 100644 --- a/docs/en/docs/deployment/cloud.md +++ b/docs/en/docs/deployment/cloud.md @@ -4,7 +4,7 @@ You can use virtually **any cloud provider** to deploy your FastAPI application. In most of the cases, the main cloud providers have guides to deploy FastAPI with them. -## FastAPI Cloud +## FastAPI Cloud { #fastapi-cloud } **FastAPI Cloud** is built by the same author and team behind **FastAPI**. From 33a75f481738c3d20ebc9fb17773d2da08b0e869 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 19 Nov 2025 10:12:24 +0000 Subject: [PATCH 114/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index c3ee858ae..6001079cd 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* 📝 Add missing hash part. PR [#14369](https://github.com/fastapi/fastapi/pull/14369) by [@nilslindemann](https://github.com/nilslindemann). * 📝 Fix typos in code comments. PR [#14364](https://github.com/fastapi/fastapi/pull/14364) by [@Edge-Seven](https://github.com/Edge-Seven). * 📝 Add docs for using FastAPI Cloud. PR [#14359](https://github.com/fastapi/fastapi/pull/14359) by [@tiangolo](https://github.com/tiangolo). From 569226e753f601954a129331f61147f89abdb52a Mon Sep 17 00:00:00 2001 From: Ben Beasley Date: Wed, 19 Nov 2025 11:55:05 +0000 Subject: [PATCH 115/256] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Bump=20Starlette?= =?UTF-8?q?=20to=20<`0.51.0`=20(#14282)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7d2be0074..cafcf65c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ classifiers = [ "Topic :: Internet :: WWW/HTTP", ] dependencies = [ - "starlette>=0.40.0,<0.50.0", + "starlette>=0.40.0,<0.51.0", "pydantic>=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0", "typing-extensions>=4.8.0", "annotated-doc>=0.0.2", From 566e3157a5517784c741dbf36e39687af326fe90 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 19 Nov 2025 11:55:32 +0000 Subject: [PATCH 116/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 6001079cd..69b15e645 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Upgrades + +* ⬆️ Bump Starlette to <`0.51.0`. PR [#14282](https://github.com/fastapi/fastapi/pull/14282) by [@musicinmybrain](https://github.com/musicinmybrain). + ### Docs * 📝 Add missing hash part. PR [#14369](https://github.com/fastapi/fastapi/pull/14369) by [@nilslindemann](https://github.com/nilslindemann). From 85701631a0241c5f02b4940734a5428f66abe167 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 19 Nov 2025 17:50:18 +0100 Subject: [PATCH 117/256] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Make=20the=20resul?= =?UTF-8?q?t=20of=20`Depends()`=20and=20`Security()`=20hashable,=20as=20a?= =?UTF-8?q?=20workaround=20for=20other=20tools=20interacting=20with=20thes?= =?UTF-8?q?e=20internal=20parts=20(#14372)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/dependencies/utils.py | 3 ++- fastapi/params.py | 4 ++-- tests/test_depends_hashable.py | 25 +++++++++++++++++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 tests/test_depends_hashable.py diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 4b69e39a1..1e92c1ba2 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -1,3 +1,4 @@ +import dataclasses import inspect from contextlib import AsyncExitStack, contextmanager from copy import copy, deepcopy @@ -428,7 +429,7 @@ def analyze_param( if depends is not None and depends.dependency is None: # Copy `depends` before mutating it depends = copy(depends) - depends.dependency = type_annotation + depends = dataclasses.replace(depends, dependency=type_annotation) # Handle non-param type annotations like Request if lenient_issubclass( diff --git a/fastapi/params.py b/fastapi/params.py index 6a58d5808..6d07df35e 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -762,13 +762,13 @@ class File(Form): # type: ignore[misc] ) -@dataclass +@dataclass(frozen=True) class Depends: dependency: Optional[Callable[..., Any]] = None use_cache: bool = True scope: Union[Literal["function", "request"], None] = None -@dataclass +@dataclass(frozen=True) class Security(Depends): scopes: Optional[Sequence[str]] = None diff --git a/tests/test_depends_hashable.py b/tests/test_depends_hashable.py new file mode 100644 index 000000000..d57f2726e --- /dev/null +++ b/tests/test_depends_hashable.py @@ -0,0 +1,25 @@ +# This is more or less a workaround to make Depends and Security hashable +# as other tools that use them depend on that +# Ref: https://github.com/fastapi/fastapi/pull/14320 + +from fastapi import Depends, Security + + +def dep(): + pass + + +def test_depends_hashable(): + dep() # just for coverage + d1 = Depends(dep) + d2 = Depends(dep) + d3 = Depends(dep, scope="function") + d4 = Depends(dep, scope="function") + + s1 = Security(dep) + s2 = Security(dep) + + assert hash(d1) == hash(d2) + assert hash(s1) == hash(s2) + assert hash(d1) != hash(d3) + assert hash(d3) == hash(d4) From 7659b70da0aea0678a0842a5d7a8aaaef2527bab Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 19 Nov 2025 16:50:42 +0000 Subject: [PATCH 118/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 69b15e645..c466152f2 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Refactors + +* ♻️ Make the result of `Depends()` and `Security()` hashable, as a workaround for other tools interacting with these internal parts. PR [#14372](https://github.com/fastapi/fastapi/pull/14372) by [@tiangolo](https://github.com/tiangolo). + ### Upgrades * ⬆️ Bump Starlette to <`0.51.0`. PR [#14282](https://github.com/fastapi/fastapi/pull/14282) by [@musicinmybrain](https://github.com/musicinmybrain). From 325fd16d32fd815633d6caf3d0d75aa7f30c51c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 19 Nov 2025 17:51:59 +0100 Subject: [PATCH 119/256] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.12?= =?UTF-8?q?1.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index c466152f2..c02fe075d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.121.3 + ### Refactors * ♻️ Make the result of `Depends()` and `Security()` hashable, as a workaround for other tools interacting with these internal parts. PR [#14372](https://github.com/fastapi/fastapi/pull/14372) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 0672423cf..85a7ea7b5 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.121.2" +__version__ = "0.121.3" from starlette import status as status From be5a6311f5ad542f253f48ca95cd863026b67c05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 20 Nov 2025 11:45:16 +0100 Subject: [PATCH 120/256] =?UTF-8?q?=F0=9F=94=A7=20Upgrade=20Material=20for?= =?UTF-8?q?=20MkDocs=20and=20remove=20insiders=20(#14375)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-docs.yml | 16 +--------------- ...{mkdocs.maybe-insiders.yml => mkdocs.env.yml} | 1 - docs/en/mkdocs.insiders.yml | 10 ---------- docs/en/mkdocs.no-insiders.yml | 0 docs/en/mkdocs.yml | 10 +++++++++- requirements-docs-insiders.txt | 3 --- requirements-docs.txt | 3 ++- scripts/docs.py | 16 +--------------- 8 files changed, 13 insertions(+), 46 deletions(-) rename docs/en/{mkdocs.maybe-insiders.yml => mkdocs.env.yml} (78%) delete mode 100644 docs/en/mkdocs.insiders.yml delete mode 100644 docs/en/mkdocs.no-insiders.yml delete mode 100644 requirements-docs-insiders.txt diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index f78b6730e..995a0eb41 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -32,12 +32,9 @@ jobs: - docs/** - docs_src/** - requirements-docs.txt - - requirements-docs-insiders.txt - pyproject.toml - mkdocs.yml - - mkdocs.insiders.yml - - mkdocs.maybe-insiders.yml - - mkdocs.no-insiders.yml + - mkdocs.env.yml - .github/workflows/build-docs.yml - .github/workflows/deploy-docs.yml - scripts/mkdocs_hooks.py @@ -63,12 +60,6 @@ jobs: pyproject.toml - name: Install docs extras run: uv pip install -r requirements-docs.txt - # Install MkDocs Material Insiders here just to put it in the cache for the rest of the steps - - name: Install Material for MkDocs Insiders - if: ( github.event_name != 'pull_request' || github.secret_source == 'Actions' ) - run: uv pip install -r requirements-docs-insiders.txt - env: - TOKEN: ${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }} - name: Verify Docs run: python ./scripts/docs.py verify-docs - name: Export Language Codes @@ -105,11 +96,6 @@ jobs: pyproject.toml - name: Install docs extras run: uv pip install -r requirements-docs.txt - - name: Install Material for MkDocs Insiders - if: ( github.event_name != 'pull_request' || github.secret_source == 'Actions' ) - run: uv pip install -r requirements-docs-insiders.txt - env: - TOKEN: ${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }} - name: Update Languages run: python ./scripts/docs.py update-languages - uses: actions/cache@v4 diff --git a/docs/en/mkdocs.maybe-insiders.yml b/docs/en/mkdocs.env.yml similarity index 78% rename from docs/en/mkdocs.maybe-insiders.yml rename to docs/en/mkdocs.env.yml index 37fd9338e..c5f6e07d7 100644 --- a/docs/en/mkdocs.maybe-insiders.yml +++ b/docs/en/mkdocs.env.yml @@ -1,6 +1,5 @@ # Define this here and not in the main mkdocs.yml file because that one is auto # updated and written, and the script would remove the env var -INHERIT: !ENV [INSIDERS_FILE, '../en/mkdocs.no-insiders.yml'] markdown_extensions: pymdownx.highlight: linenums: !ENV [LINENUMS, false] diff --git a/docs/en/mkdocs.insiders.yml b/docs/en/mkdocs.insiders.yml deleted file mode 100644 index 8d6d26e17..000000000 --- a/docs/en/mkdocs.insiders.yml +++ /dev/null @@ -1,10 +0,0 @@ -plugins: - social: - cards_layout_options: - logo: ../en/docs/img/icon-white.svg - typeset: -markdown_extensions: - material.extensions.preview: - targets: - include: - - "*" diff --git a/docs/en/mkdocs.no-insiders.yml b/docs/en/mkdocs.no-insiders.yml deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index df47c6f9c..8be832f11 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -1,4 +1,4 @@ -INHERIT: ../en/mkdocs.maybe-insiders.yml +INHERIT: ../en/mkdocs.env.yml site_name: FastAPI site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production site_url: https://fastapi.tiangolo.com/ @@ -52,6 +52,10 @@ theme: repo_name: fastapi/fastapi repo_url: https://github.com/fastapi/fastapi plugins: + social: + cards_layout_options: + logo: ../en/docs/img/icon-white.svg + typeset: search: null macros: include_yaml: @@ -253,6 +257,10 @@ nav: - management.md - release-notes.md markdown_extensions: + material.extensions.preview: + targets: + include: + - "*" abbr: null attr_list: null footnotes: null diff --git a/requirements-docs-insiders.txt b/requirements-docs-insiders.txt deleted file mode 100644 index d8d3c37a9..000000000 --- a/requirements-docs-insiders.txt +++ /dev/null @@ -1,3 +0,0 @@ -git+https://${TOKEN}@github.com/squidfunk/mkdocs-material-insiders.git@9.5.30-insiders-4.53.11 -git+https://${TOKEN}@github.com/pawamoy-insiders/griffe-typing-deprecated.git -git+https://${TOKEN}@github.com/pawamoy-insiders/mkdocstrings-python.git diff --git a/requirements-docs.txt b/requirements-docs.txt index 696eb2a33..d60125bbe 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,6 +1,6 @@ -e . -r requirements-docs-tests.txt -mkdocs-material==9.6.16 +mkdocs-material==9.7.0 mdx-include >=1.4.1,<2.0.0 mkdocs-redirects>=1.2.1,<1.3.0 typer == 0.16.0 @@ -13,6 +13,7 @@ pillow==11.3.0 cairosvg==2.8.2 mkdocstrings[python]==0.30.1 griffe-typingdoc==0.3.0 +griffe-warnings-deprecated==1.1.0 # For griffe, it formats with black black==25.1.0 mkdocs-macros-plugin==1.4.1 diff --git a/scripts/docs.py b/scripts/docs.py index 1a336a036..d08a218f8 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -4,9 +4,7 @@ import os import re import shutil import subprocess -from functools import lru_cache from http.server import HTTPServer, SimpleHTTPRequestHandler -from importlib import metadata from multiprocessing import Pool from pathlib import Path from typing import Any, Dict, List, Optional, Union @@ -47,12 +45,6 @@ build_site_path = Path("site_build").absolute() header_with_permalink_pattern = re.compile(r"^(#{1,6}) (.+?)(\s*\{\s*#.*\s*\})\s*$") -@lru_cache -def is_mkdocs_insiders() -> bool: - version = metadata.version("mkdocs-material") - return "insiders" in version - - def get_en_config() -> Dict[str, Any]: return mkdocs.utils.yaml_load(en_config_path.read_text(encoding="utf-8")) @@ -77,9 +69,7 @@ def complete_existing_lang(incomplete: str): @app.callback() def callback() -> None: - if is_mkdocs_insiders(): - os.environ["INSIDERS_FILE"] = "../en/mkdocs.insiders.yml" - # For MacOS with insiders and Cairo + # For MacOS with Cairo os.environ["DYLD_FALLBACK_LIBRARY_PATH"] = "/opt/homebrew/lib" @@ -115,10 +105,6 @@ def build_lang( """ Build the docs for a language. """ - insiders_env_file = os.environ.get("INSIDERS_FILE") - print(f"Insiders file {insiders_env_file}") - if is_mkdocs_insiders(): - print("Using insiders") lang_path: Path = Path("docs") / lang if not lang_path.is_dir(): typer.echo(f"The language translation doesn't seem to exist yet: {lang}") From 456008a52b01b570a0f03f8c9d53722eda137728 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 20 Nov 2025 10:45:39 +0000 Subject: [PATCH 121/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index c02fe075d..bf4120f36 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Internal + +* 🔧 Upgrade Material for MkDocs and remove insiders. PR [#14375](https://github.com/fastapi/fastapi/pull/14375) by [@tiangolo](https://github.com/tiangolo). + ## 0.121.3 ### Refactors From 32b375c5e445efb1efd129d825016bec8dbfb08a Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Fri, 21 Nov 2025 13:49:11 +0100 Subject: [PATCH 122/256] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20Add=20`add-perm?= =?UTF-8?q?alinks`=20and=20`add-permalinks-page`=20to=20`scripts/docs.py`?= =?UTF-8?q?=20(#14033)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- requirements-docs.txt | 1 + scripts/docs.py | 109 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 107 insertions(+), 3 deletions(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index d60125bbe..05b47fe92 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -18,3 +18,4 @@ griffe-warnings-deprecated==1.1.0 black==25.1.0 mkdocs-macros-plugin==1.4.1 markdown-include-variants==0.0.5 +python-slugify==8.0.4 diff --git a/scripts/docs.py b/scripts/docs.py index d08a218f8..d67ab50f7 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -4,6 +4,7 @@ import os import re import shutil import subprocess +from html.parser import HTMLParser from http.server import HTTPServer, SimpleHTTPRequestHandler from multiprocessing import Pool from pathlib import Path @@ -14,6 +15,7 @@ import typer import yaml from jinja2 import Template from ruff.__main__ import find_ruff_bin +from slugify import slugify as py_slugify logging.basicConfig(level=logging.INFO) @@ -25,8 +27,8 @@ missing_translation_snippet = """ {!../../docs/missing-translation.md!} """ -non_translated_sections = [ - "reference/", +non_translated_sections = ( + f"reference{os.sep}", "release-notes.md", "fastapi-people.md", "external-links.md", @@ -34,7 +36,7 @@ non_translated_sections = [ "management-tasks.md", "management.md", "contributing.md", -] +) docs_path = Path("docs") en_docs_path = Path("docs/en") @@ -42,7 +44,39 @@ en_config_path: Path = en_docs_path / mkdocs_name site_path = Path("site").absolute() build_site_path = Path("site_build").absolute() +header_pattern = re.compile(r"^(#{1,6}) (.+?)(?:\s*\{\s*(#.*)\s*\})?\s*$") header_with_permalink_pattern = re.compile(r"^(#{1,6}) (.+?)(\s*\{\s*#.*\s*\})\s*$") +code_block3_pattern = re.compile(r"^\s*```") +code_block4_pattern = re.compile(r"^\s*````") + + +class VisibleTextExtractor(HTMLParser): + """Extract visible text from a string with HTML tags.""" + + def __init__(self): + super().__init__() + self.text_parts = [] + + def handle_data(self, data): + self.text_parts.append(data) + + def extract_visible_text(self, html: str) -> str: + self.reset() + self.text_parts = [] + self.feed(html) + return "".join(self.text_parts).strip() + + +def slugify(text: str) -> str: + return py_slugify( + text, + replacements=[ + ("`", ""), # `dict`s -> dicts + ("'s", "s"), # it's -> its + ("'t", "t"), # don't -> dont + ("**", ""), # **FastAPI**s -> FastAPIs + ], + ) def get_en_config() -> Dict[str, Any]: @@ -426,5 +460,74 @@ def generate_docs_src_versions_for_file(file_path: Path) -> None: version_file.write_text(content_format, encoding="utf-8") +@app.command() +def add_permalinks_page(path: Path, update_existing: bool = False): + """ + Add or update header permalinks in specific page of En docs. + """ + + if not path.is_relative_to(en_docs_path / "docs"): + raise RuntimeError(f"Path must be inside {en_docs_path}") + rel_path = path.relative_to(en_docs_path / "docs") + + # Skip excluded sections + if str(rel_path).startswith(non_translated_sections): + return + + visible_text_extractor = VisibleTextExtractor() + updated_lines = [] + in_code_block3 = False + in_code_block4 = False + permalinks = set() + + with path.open("r", encoding="utf-8") as f: + lines = f.readlines() + + for line in lines: + # Handle codeblocks start and end + if not (in_code_block3 or in_code_block4): + if code_block4_pattern.match(line): + in_code_block4 = True + elif code_block3_pattern.match(line): + in_code_block3 = True + else: + if in_code_block4 and code_block4_pattern.match(line): + in_code_block4 = False + elif in_code_block3 and code_block3_pattern.match(line): + in_code_block3 = False + + # Process Headers only outside codeblocks + if not (in_code_block3 or in_code_block4): + match = header_pattern.match(line) + if match: + hashes, title, _permalink = match.groups() + if (not _permalink) or update_existing: + slug = slugify(visible_text_extractor.extract_visible_text(title)) + if slug in permalinks: + # If the slug is already used, append a number to make it unique + count = 1 + original_slug = slug + while slug in permalinks: + slug = f"{original_slug}_{count}" + count += 1 + permalinks.add(slug) + + line = f"{hashes} {title} {{ #{slug} }}\n" + + updated_lines.append(line) + + with path.open("w", encoding="utf-8") as f: + f.writelines(updated_lines) + + +@app.command() +def add_permalinks(update_existing: bool = False) -> None: + """ + Add or update header permalinks in all pages of En docs. + """ + for md_file in en_docs_path.rglob("*.md"): + add_permalinks_page(md_file, update_existing=update_existing) + + if __name__ == "__main__": app() From 2909f8a6281611bcf9b0f127a147893bcd5a0716 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 21 Nov 2025 12:49:34 +0000 Subject: [PATCH 123/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index bf4120f36..4a01df4b8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* 🛠️ Add `add-permalinks` and `add-permalinks-page` to `scripts/docs.py`. PR [#14033](https://github.com/fastapi/fastapi/pull/14033) by [@YuriiMotov](https://github.com/YuriiMotov). * 🔧 Upgrade Material for MkDocs and remove insiders. PR [#14375](https://github.com/fastapi/fastapi/pull/14375) by [@tiangolo](https://github.com/tiangolo). ## 0.121.3 From cbe5bdb85f393712e5176cfa27275781ffcc07c0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Nov 2025 14:03:21 +0100 Subject: [PATCH 124/256] =?UTF-8?q?=E2=AC=86=20Bump=20actions/checkout=20f?= =?UTF-8?q?rom=205=20to=206=20(#14381)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-docs.yml | 6 +++--- .github/workflows/contributors.yml | 2 +- .github/workflows/deploy-docs.yml | 2 +- .github/workflows/label-approved.yml | 2 +- .github/workflows/latest-changes.yml | 2 +- .github/workflows/notify-translations.yml | 2 +- .github/workflows/people.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/smokeshow.yml | 2 +- .github/workflows/sponsors.yml | 2 +- .github/workflows/test-redistribute.yml | 2 +- .github/workflows/test.yml | 6 +++--- .github/workflows/topic-repos.yml | 2 +- .github/workflows/translate.yml | 2 +- 14 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 995a0eb41..73e1c6b67 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -21,7 +21,7 @@ jobs: outputs: docs: ${{ steps.filter.outputs.docs }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 # For pull requests it's not necessary to checkout the code but for the main branch it is - uses: dorny/paths-filter@v3 id: filter @@ -45,7 +45,7 @@ jobs: outputs: langs: ${{ steps.show-langs.outputs.langs }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: @@ -81,7 +81,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: diff --git a/.github/workflows/contributors.yml b/.github/workflows/contributors.yml index 7d5449c6a..2abd2fdcf 100644 --- a/.github/workflows/contributors.yml +++ b/.github/workflows/contributors.yml @@ -24,7 +24,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index aa4fd6b65..50662a190 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -23,7 +23,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: diff --git a/.github/workflows/label-approved.yml b/.github/workflows/label-approved.yml index e6ae3d963..7f16254db 100644 --- a/.github/workflows/label-approved.yml +++ b/.github/workflows/label-approved.yml @@ -20,7 +20,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: diff --git a/.github/workflows/latest-changes.yml b/.github/workflows/latest-changes.yml index 2fa832fab..3bff707c0 100644 --- a/.github/workflows/latest-changes.yml +++ b/.github/workflows/latest-changes.yml @@ -24,7 +24,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: # To allow latest-changes to commit to the main branch token: ${{ secrets.FASTAPI_LATEST_CHANGES }} diff --git a/.github/workflows/notify-translations.yml b/.github/workflows/notify-translations.yml index 04beeb64e..971e6bbd8 100644 --- a/.github/workflows/notify-translations.yml +++ b/.github/workflows/notify-translations.yml @@ -28,7 +28,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: diff --git a/.github/workflows/people.yml b/.github/workflows/people.yml index f15b92137..9b35a3d7e 100644 --- a/.github/workflows/people.yml +++ b/.github/workflows/people.yml @@ -24,7 +24,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 441eb4560..6d9a00b49 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -20,7 +20,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index eed5fbec0..84c743019 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -21,7 +21,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: python-version: '3.9' diff --git a/.github/workflows/sponsors.yml b/.github/workflows/sponsors.yml index 7d29469a5..8b0249001 100644 --- a/.github/workflows/sponsors.yml +++ b/.github/workflows/sponsors.yml @@ -24,7 +24,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: diff --git a/.github/workflows/test-redistribute.yml b/.github/workflows/test-redistribute.yml index a44f0b681..653ab2a74 100644 --- a/.github/workflows/test-redistribute.yml +++ b/.github/workflows/test-redistribute.yml @@ -22,7 +22,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9c3e2218b..8157e364b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,7 +23,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: @@ -65,7 +65,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: @@ -111,7 +111,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: python-version: '3.8' diff --git a/.github/workflows/topic-repos.yml b/.github/workflows/topic-repos.yml index 22b37d59d..41dabee1e 100644 --- a/.github/workflows/topic-repos.yml +++ b/.github/workflows/topic-repos.yml @@ -19,7 +19,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: diff --git a/.github/workflows/translate.yml b/.github/workflows/translate.yml index a7fcf84df..6506b8e28 100644 --- a/.github/workflows/translate.yml +++ b/.github/workflows/translate.yml @@ -42,7 +42,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: From ae951f698177b7fc00a61768376c78a92d8b3995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 23 Nov 2025 01:27:40 -0800 Subject: [PATCH 125/256] =?UTF-8?q?=F0=9F=92=84=20Use=20font=20Fira=20Code?= =?UTF-8?q?=20to=20fix=20display=20of=20Rich=20panels=20in=20docs=20in=20W?= =?UTF-8?q?indows=20(#14387)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/css/custom.css | 8 ++++++++ docs/en/docs/css/termynal.css | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/css/custom.css b/docs/en/docs/css/custom.css index a38df772f..8849d8741 100644 --- a/docs/en/docs/css/custom.css +++ b/docs/en/docs/css/custom.css @@ -1,3 +1,11 @@ +/* Fira Code, including characters used by Rich output, like the "heavy right-pointing angle bracket ornament", not included in Google Fonts */ +@import url(https://cdn.jsdelivr.net/npm/firacode@6.2.0/distr/fira_code.css); + +/* Override default code font in Material for MkDocs to Fira Code */ +:root { + --md-code-font: "Fira Code", monospace; +} + .termynal-comment { color: #4a968f; font-style: italic; diff --git a/docs/en/docs/css/termynal.css b/docs/en/docs/css/termynal.css index 8534f9102..a2564e286 100644 --- a/docs/en/docs/css/termynal.css +++ b/docs/en/docs/css/termynal.css @@ -20,7 +20,7 @@ /* font-size: 18px; */ font-size: 15px; /* font-family: 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; */ - font-family: 'Roboto Mono', 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; + font-family: var(--md-code-font-family), 'Roboto Mono', 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; border-radius: 4px; padding: 75px 45px 35px; position: relative; From 79bc4b9ca0d039921310e13f726f8a17c0469d8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 23 Nov 2025 08:36:34 -0800 Subject: [PATCH 126/256] =?UTF-8?q?=F0=9F=91=B7=20Add=20custom=20pre-commi?= =?UTF-8?q?t=20CI=20(#14397)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/pre-commit.yml | 85 ++++++++++++++++++++++++++++++++ .pre-commit-config.yaml | 5 -- 2 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/pre-commit.yml diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 000000000..838764057 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,85 @@ +name: pre-commit + +on: + pull_request: + types: + - opened + - synchronize + +env: + UV_SYSTEM_PYTHON: 1 + IS_FORK: ${{ github.event.pull_request.head.repo.full_name != github.repository }} + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: actions/checkout@v5 + name: Checkout PR for own repo + if: env.IS_FORK == 'false' + with: + # To be able to commit it needs more than the last commit + ref: ${{ github.head_ref }} + # A token other than the default GITHUB_TOKEN is needed to be able to trigger CI + token: ${{ secrets.PRE_COMMIT }} + # pre-commit lite ci needs the default checkout configs to work + - uses: actions/checkout@v5 + name: Checkout PR for fork + if: env.IS_FORK == 'true' + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.14" + - name: Setup uv + uses: astral-sh/setup-uv@v7 + with: + cache-dependency-glob: | + requirements**.txt + pyproject.toml + uv.lock + - name: Run pre-commit + id: precommit + run: | + # Fetch the base branch for comparison + git fetch origin ${{ github.base_ref }} + uvx pre-commit run --from-ref origin/${{ github.base_ref }} --to-ref HEAD --show-diff-on-failure + continue-on-error: true + - name: Commit and push changes + if: env.IS_FORK == 'false' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add -A + if git diff --staged --quiet; then + echo "No changes to commit" + else + git commit -m "🎨 Auto format" + git push + fi + - uses: pre-commit-ci/lite-action@v1.1.0 + if: env.IS_FORK == 'true' + with: + msg: 🎨 Auto format + - name: Error out on pre-commit errors + if: steps.precommit.outcome == 'failure' + run: exit 1 + + # https://github.com/marketplace/actions/alls-green#why + alls-green: # This job does nothing and is only used for the branch protection + if: always() + needs: + - pre-commit + runs-on: ubuntu-latest + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8e5eba4c4..cc3d93779 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,5 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks -default_language_version: - python: python3.10 repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 @@ -20,6 +18,3 @@ repos: args: - --fix - id: ruff-format -ci: - autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks - autoupdate_commit_msg: ⬆ [pre-commit.ci] pre-commit autoupdate From 4f3ff797361a7a1d953353d90fd809d54469267f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 23 Nov 2025 09:41:43 -0800 Subject: [PATCH 127/256] =?UTF-8?q?=F0=9F=91=B7=20Add=20pre-commit=20confi?= =?UTF-8?q?g=20with=20local=20script=20for=20permalinks=20(#14398)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] --- .github/workflows/pre-commit.yml | 7 +++++-- .gitignore | 3 +++ .pre-commit-config.yaml | 29 +++++++++++++++++++---------- requirements.txt | 2 +- scripts/docs.py | 9 +++++++++ 5 files changed, 37 insertions(+), 13 deletions(-) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 838764057..fa0574d7d 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -7,7 +7,6 @@ on: - synchronize env: - UV_SYSTEM_PYTHON: 1 IS_FORK: ${{ github.event.pull_request.head.repo.full_name != github.repository }} jobs: @@ -41,6 +40,10 @@ jobs: requirements**.txt pyproject.toml uv.lock + - name: Install Dependencies + run: | + uv venv + uv pip install -r requirements.txt - name: Run pre-commit id: precommit run: | @@ -69,7 +72,7 @@ jobs: run: exit 1 # https://github.com/marketplace/actions/alls-green#why - alls-green: # This job does nothing and is only used for the branch protection + pre-commit-alls-green: # This job does nothing and is only used for the branch protection if: always() needs: - pre-commit diff --git a/.gitignore b/.gitignore index ef6364a9a..6016ffa59 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,6 @@ archive.zip # macOS .DS_Store + +# Ignore while the setup still depends on requirements.txt files +uv.lock diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cc3d93779..8e6d93fb7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,20 +1,29 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks repos: -- repo: https://github.com/pre-commit/pre-commit-hooks + - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - - id: check-added-large-files - - id: check-toml - - id: check-yaml + - id: check-added-large-files + - id: check-toml + - id: check-yaml args: - - --unsafe - - id: end-of-file-fixer - - id: trailing-whitespace -- repo: https://github.com/astral-sh/ruff-pre-commit + - --unsafe + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.14.3 hooks: - - id: ruff + - id: ruff args: - --fix - - id: ruff-format + - id: ruff-format + - repo: local + hooks: + - id: local-script + language: unsupported + name: local script + entry: uv run ./scripts/docs.py add-permalinks-pages + args: + - --update-existing + files: ^docs/en/docs/.*\.md$ diff --git a/requirements.txt b/requirements.txt index 9180bf1be..5d9f97b75 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ -e .[all] -r requirements-tests.txt -r requirements-docs.txt -pre-commit >=2.17.0,<5.0.0 +pre-commit >=4.5.0,<5.0.0 # For generating screenshots playwright diff --git a/scripts/docs.py b/scripts/docs.py index d67ab50f7..73f60e68c 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -520,6 +520,15 @@ def add_permalinks_page(path: Path, update_existing: bool = False): f.writelines(updated_lines) +@app.command() +def add_permalinks_pages(pages: List[Path], update_existing: bool = False) -> None: + """ + Add or update header permalinks in specific pages of En docs. + """ + for md_file in pages: + add_permalinks_page(md_file, update_existing=update_existing) + + @app.command() def add_permalinks(update_existing: bool = False) -> None: """ From 5265c4f5cb4d8fb4fb5015bccf1a6de8f3fc6044 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Sun, 23 Nov 2025 21:10:04 +0100 Subject: [PATCH 128/256] =?UTF-8?q?=F0=9F=94=A7=20Configure=20labeler=20to?= =?UTF-8?q?=20exclude=20files=20that=20start=20from=20underscore=20for=20`?= =?UTF-8?q?lang-all`=20label=20(#14213)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/labeler.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/labeler.yml b/.github/labeler.yml index c5b1f84f3..cdaefbf2d 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -17,6 +17,7 @@ lang-all: - docs/*/docs/** - all-globs-to-all-files: - '!docs/en/docs/**' + - '!docs/*/**/_*.md' - '!fastapi/**' - '!pyproject.toml' From ab33b457182976c244d43fd0af838e72e6feee72 Mon Sep 17 00:00:00 2001 From: Sofie Van Landeghem Date: Mon, 24 Nov 2025 15:58:32 +0100 Subject: [PATCH 129/256] =?UTF-8?q?=F0=9F=91=B7=20Upgrade=20`latest-change?= =?UTF-8?q?s`=20GitHub=20Action=20and=20pin=20`actions/checkout@v5`=20(#14?= =?UTF-8?q?403)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 👷 Upgrade latest-changes and pin actions/checkout@v5 --- .github/workflows/latest-changes.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/latest-changes.yml b/.github/workflows/latest-changes.yml index 3bff707c0..b9e45ea62 100644 --- a/.github/workflows/latest-changes.yml +++ b/.github/workflows/latest-changes.yml @@ -24,7 +24,9 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v6 + # pin to actions/checkout@v5 for compatibility with latest-changes + # Ref: https://github.com/actions/checkout/issues/2313 + - uses: actions/checkout@v5 with: # To allow latest-changes to commit to the main branch token: ${{ secrets.FASTAPI_LATEST_CHANGES }} @@ -34,7 +36,7 @@ jobs: if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} with: limit-access-to-actor: true - - uses: tiangolo/latest-changes@0.4.0 + - uses: tiangolo/latest-changes@0.4.1 with: token: ${{ secrets.GITHUB_TOKEN }} latest_changes_file: docs/en/docs/release-notes.md From c7d05a903ce34e8578237cf2aab7242cefe51af2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 24 Nov 2025 14:58:56 +0000 Subject: [PATCH 130/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 4a01df4b8..c38766dc1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* 👷 Upgrade `latest-changes` GitHub Action and pin `actions/checkout@v5`. PR [#14403](https://github.com/fastapi/fastapi/pull/14403) by [@svlandeg](https://github.com/svlandeg). * 🛠️ Add `add-permalinks` and `add-permalinks-page` to `scripts/docs.py`. PR [#14033](https://github.com/fastapi/fastapi/pull/14033) by [@YuriiMotov](https://github.com/YuriiMotov). * 🔧 Upgrade Material for MkDocs and remove insiders. PR [#14375](https://github.com/fastapi/fastapi/pull/14375) by [@tiangolo](https://github.com/tiangolo). From a2395e02436a3788400d864696120fcd91af38cc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 24 Nov 2025 14:59:55 +0000 Subject: [PATCH 131/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index c38766dc1..9fd9817ed 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* ⬆ Bump actions/checkout from 5 to 6. PR [#14381](https://github.com/fastapi/fastapi/pull/14381) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👷 Upgrade `latest-changes` GitHub Action and pin `actions/checkout@v5`. PR [#14403](https://github.com/fastapi/fastapi/pull/14403) by [@svlandeg](https://github.com/svlandeg). * 🛠️ Add `add-permalinks` and `add-permalinks-page` to `scripts/docs.py`. PR [#14033](https://github.com/fastapi/fastapi/pull/14033) by [@YuriiMotov](https://github.com/YuriiMotov). * 🔧 Upgrade Material for MkDocs and remove insiders. PR [#14375](https://github.com/fastapi/fastapi/pull/14375) by [@tiangolo](https://github.com/tiangolo). From 8b18522205b9ac738b241c4143c983e968fe6e15 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 24 Nov 2025 15:00:12 +0000 Subject: [PATCH 132/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 9fd9817ed..892eeb8f4 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* 👷 Add custom pre-commit CI. PR [#14397](https://github.com/fastapi/fastapi/pull/14397) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump actions/checkout from 5 to 6. PR [#14381](https://github.com/fastapi/fastapi/pull/14381) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👷 Upgrade `latest-changes` GitHub Action and pin `actions/checkout@v5`. PR [#14403](https://github.com/fastapi/fastapi/pull/14403) by [@svlandeg](https://github.com/svlandeg). * 🛠️ Add `add-permalinks` and `add-permalinks-page` to `scripts/docs.py`. PR [#14033](https://github.com/fastapi/fastapi/pull/14033) by [@YuriiMotov](https://github.com/YuriiMotov). From ecfb752487bc3abef35b2786297bc575005c9e36 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 24 Nov 2025 15:00:13 +0000 Subject: [PATCH 133/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 892eeb8f4..fbb108994 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* 💄 Use font Fira Code to fix display of Rich panels in docs in Windows. PR [#14387](https://github.com/fastapi/fastapi/pull/14387) by [@tiangolo](https://github.com/tiangolo). * 👷 Add custom pre-commit CI. PR [#14397](https://github.com/fastapi/fastapi/pull/14397) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump actions/checkout from 5 to 6. PR [#14381](https://github.com/fastapi/fastapi/pull/14381) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👷 Upgrade `latest-changes` GitHub Action and pin `actions/checkout@v5`. PR [#14403](https://github.com/fastapi/fastapi/pull/14403) by [@svlandeg](https://github.com/svlandeg). From cc66dee55c9a0f34c2e277c0509c45c74abcefd1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 24 Nov 2025 15:00:29 +0000 Subject: [PATCH 134/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index fbb108994..d8495d571 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* 👷 Add pre-commit config with local script for permalinks. PR [#14398](https://github.com/fastapi/fastapi/pull/14398) by [@tiangolo](https://github.com/tiangolo). * 💄 Use font Fira Code to fix display of Rich panels in docs in Windows. PR [#14387](https://github.com/fastapi/fastapi/pull/14387) by [@tiangolo](https://github.com/tiangolo). * 👷 Add custom pre-commit CI. PR [#14397](https://github.com/fastapi/fastapi/pull/14397) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump actions/checkout from 5 to 6. PR [#14381](https://github.com/fastapi/fastapi/pull/14381) by [@dependabot[bot]](https://github.com/apps/dependabot). From e2354a0a063f2fcb890ec568f1a98e136a39fd25 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 24 Nov 2025 15:00:36 +0000 Subject: [PATCH 135/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index d8495d571..ef1b98b67 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* 🔧 Configure labeler to exclude files that start from underscore for `lang-all` label. PR [#14213](https://github.com/fastapi/fastapi/pull/14213) by [@YuriiMotov](https://github.com/YuriiMotov). * 👷 Add pre-commit config with local script for permalinks. PR [#14398](https://github.com/fastapi/fastapi/pull/14398) by [@tiangolo](https://github.com/tiangolo). * 💄 Use font Fira Code to fix display of Rich panels in docs in Windows. PR [#14387](https://github.com/fastapi/fastapi/pull/14387) by [@tiangolo](https://github.com/tiangolo). * 👷 Add custom pre-commit CI. PR [#14397](https://github.com/fastapi/fastapi/pull/14397) by [@tiangolo](https://github.com/tiangolo). From 51ad909ffe9f5b2d5c9315554e75e39a8a2d725c Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Mon, 24 Nov 2025 20:03:06 +0100 Subject: [PATCH 136/256] =?UTF-8?q?=F0=9F=90=9B=20Use=20`401`=20status=20c?= =?UTF-8?q?ode=20in=20security=20classes=20when=20credentials=20are=20miss?= =?UTF-8?q?ing=20(#13786)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- .../authentication-error-status-code.md | 17 +++++ docs/en/mkdocs.yml | 1 + .../tutorial001_an.py | 20 +++++ .../tutorial001_an_py39.py | 21 +++++ docs_src/security/tutorial003.py | 2 +- docs_src/security/tutorial003_an.py | 2 +- docs_src/security/tutorial003_an_py310.py | 2 +- docs_src/security/tutorial003_an_py39.py | 2 +- docs_src/security/tutorial003_py310.py | 2 +- fastapi/security/api_key.py | 76 +++++++++++++------ fastapi/security/http.py | 74 +++++++++--------- fastapi/security/oauth2.py | 40 ++++++---- fastapi/security/open_id_connect_url.py | 18 ++++- tests/test_security_api_key_cookie.py | 3 +- ...est_security_api_key_cookie_description.py | 3 +- tests/test_security_api_key_header.py | 3 +- ...est_security_api_key_header_description.py | 3 +- tests/test_security_api_key_query.py | 3 +- ...test_security_api_key_query_description.py | 3 +- tests/test_security_http_base.py | 3 +- tests/test_security_http_base_description.py | 3 +- tests/test_security_http_basic_optional.py | 4 +- tests/test_security_http_basic_realm.py | 4 +- ...t_security_http_basic_realm_description.py | 4 +- tests/test_security_http_bearer.py | 8 +- .../test_security_http_bearer_description.py | 8 +- tests/test_security_http_digest.py | 8 +- .../test_security_http_digest_description.py | 8 +- tests/test_security_oauth2.py | 3 +- tests/test_security_openid_connect.py | 3 +- ...est_security_openid_connect_description.py | 3 +- ...st_top_level_security_scheme_in_openapi.py | 2 +- .../__init__.py | 0 .../test_tutorial001.py | 69 +++++++++++++++++ .../test_security/test_tutorial003.py | 2 +- .../test_security/test_tutorial006.py | 4 +- 36 files changed, 315 insertions(+), 116 deletions(-) create mode 100644 docs/en/docs/how-to/authentication-error-status-code.md create mode 100644 docs_src/authentication_error_status_code/tutorial001_an.py create mode 100644 docs_src/authentication_error_status_code/tutorial001_an_py39.py create mode 100644 tests/test_tutorial/test_authentication_error_status_code/__init__.py create mode 100644 tests/test_tutorial/test_authentication_error_status_code/test_tutorial001.py diff --git a/docs/en/docs/how-to/authentication-error-status-code.md b/docs/en/docs/how-to/authentication-error-status-code.md new file mode 100644 index 000000000..f9433e5dd --- /dev/null +++ b/docs/en/docs/how-to/authentication-error-status-code.md @@ -0,0 +1,17 @@ +# Use Old 403 Authentication Error Status Codes { #use-old-403-authentication-error-status-codes } + +Before FastAPI version `0.122.0`, when the integrated security utilities returned an error to the client after a failed authentication, they used the HTTP status code `403 Forbidden`. + +Starting with FastAPI version `0.122.0`, they use the more appropriate HTTP status code `401 Unauthorized`, and return a sensible `WWW-Authenticate` header in the response, following the HTTP specifications, RFC 7235, RFC 9110. + +But if for some reason your clients depend on the old behavior, you can revert to it by overriding the method `make_not_authenticated_error` in your security classes. + +For example, you can create a subclass of `HTTPBearer` that returns a `403 Forbidden` error instead of the default `401 Unauthorized` error: + +{* ../../docs_src/authentication_error_status_code/tutorial001_an_py39.py hl[9:13] *} + +/// tip + +Notice that the function returns the exception instance, it doesn't raise it. The raising is done in the rest of the internal code. + +/// diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 8be832f11..fd346a3d3 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -215,6 +215,7 @@ nav: - how-to/custom-docs-ui-assets.md - how-to/configure-swagger-ui.md - how-to/testing-database.md + - how-to/authentication-error-status-code.md - Reference (Code API): - reference/index.md - reference/fastapi.md diff --git a/docs_src/authentication_error_status_code/tutorial001_an.py b/docs_src/authentication_error_status_code/tutorial001_an.py new file mode 100644 index 000000000..40678e858 --- /dev/null +++ b/docs_src/authentication_error_status_code/tutorial001_an.py @@ -0,0 +1,20 @@ +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer +from typing_extensions import Annotated + +app = FastAPI() + + +class HTTPBearer403(HTTPBearer): + def make_not_authenticated_error(self) -> HTTPException: + return HTTPException( + status_code=status.HTTP_403_FORBIDDEN, detail="Not authenticated" + ) + + +CredentialsDep = Annotated[HTTPAuthorizationCredentials, Depends(HTTPBearer403())] + + +@app.get("/me") +def read_me(credentials: CredentialsDep): + return {"message": "You are authenticated", "token": credentials.credentials} diff --git a/docs_src/authentication_error_status_code/tutorial001_an_py39.py b/docs_src/authentication_error_status_code/tutorial001_an_py39.py new file mode 100644 index 000000000..7bbc2f717 --- /dev/null +++ b/docs_src/authentication_error_status_code/tutorial001_an_py39.py @@ -0,0 +1,21 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer + +app = FastAPI() + + +class HTTPBearer403(HTTPBearer): + def make_not_authenticated_error(self) -> HTTPException: + return HTTPException( + status_code=status.HTTP_403_FORBIDDEN, detail="Not authenticated" + ) + + +CredentialsDep = Annotated[HTTPAuthorizationCredentials, Depends(HTTPBearer403())] + + +@app.get("/me") +def read_me(credentials: CredentialsDep): + return {"message": "You are authenticated", "token": credentials.credentials} diff --git a/docs_src/security/tutorial003.py b/docs_src/security/tutorial003.py index 4b324866f..ce7a71b68 100644 --- a/docs_src/security/tutorial003.py +++ b/docs_src/security/tutorial003.py @@ -60,7 +60,7 @@ async def get_current_user(token: str = Depends(oauth2_scheme)): if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid authentication credentials", + detail="Not authenticated", headers={"WWW-Authenticate": "Bearer"}, ) return user diff --git a/docs_src/security/tutorial003_an.py b/docs_src/security/tutorial003_an.py index 8fb40dd4a..1b7056a20 100644 --- a/docs_src/security/tutorial003_an.py +++ b/docs_src/security/tutorial003_an.py @@ -61,7 +61,7 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid authentication credentials", + detail="Not authenticated", headers={"WWW-Authenticate": "Bearer"}, ) return user diff --git a/docs_src/security/tutorial003_an_py310.py b/docs_src/security/tutorial003_an_py310.py index ced4a2fbc..4a2743f6f 100644 --- a/docs_src/security/tutorial003_an_py310.py +++ b/docs_src/security/tutorial003_an_py310.py @@ -60,7 +60,7 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid authentication credentials", + detail="Not authenticated", headers={"WWW-Authenticate": "Bearer"}, ) return user diff --git a/docs_src/security/tutorial003_an_py39.py b/docs_src/security/tutorial003_an_py39.py index 068a3933e..b396210c8 100644 --- a/docs_src/security/tutorial003_an_py39.py +++ b/docs_src/security/tutorial003_an_py39.py @@ -60,7 +60,7 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid authentication credentials", + detail="Not authenticated", headers={"WWW-Authenticate": "Bearer"}, ) return user diff --git a/docs_src/security/tutorial003_py310.py b/docs_src/security/tutorial003_py310.py index af935e997..081259b31 100644 --- a/docs_src/security/tutorial003_py310.py +++ b/docs_src/security/tutorial003_py310.py @@ -58,7 +58,7 @@ async def get_current_user(token: str = Depends(oauth2_scheme)): if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid authentication credentials", + detail="Not authenticated", headers={"WWW-Authenticate": "Bearer"}, ) return user diff --git a/fastapi/security/api_key.py b/fastapi/security/api_key.py index 496c815a7..81c7be10d 100644 --- a/fastapi/security/api_key.py +++ b/fastapi/security/api_key.py @@ -1,22 +1,52 @@ -from typing import Optional +from typing import Optional, Union from annotated_doc import Doc from fastapi.openapi.models import APIKey, APIKeyIn from fastapi.security.base import SecurityBase from starlette.exceptions import HTTPException from starlette.requests import Request -from starlette.status import HTTP_403_FORBIDDEN +from starlette.status import HTTP_401_UNAUTHORIZED from typing_extensions import Annotated class APIKeyBase(SecurityBase): - @staticmethod - def check_api_key(api_key: Optional[str], auto_error: bool) -> Optional[str]: + def __init__( + self, + location: APIKeyIn, + name: str, + description: Union[str, None], + scheme_name: Union[str, None], + auto_error: bool, + ): + self.auto_error = auto_error + + self.model: APIKey = APIKey( + **{"in": location}, + name=name, + description=description, + ) + self.scheme_name = scheme_name or self.__class__.__name__ + + def make_not_authenticated_error(self) -> HTTPException: + """ + The WWW-Authenticate header is not standardized for API Key authentication but + the HTTP specification requires that an error of 401 "Unauthorized" must + include a WWW-Authenticate header. + + Ref: https://datatracker.ietf.org/doc/html/rfc9110#name-401-unauthorized + + For this, this method sends a custom challenge `APIKey`. + """ + return HTTPException( + status_code=HTTP_401_UNAUTHORIZED, + detail="Not authenticated", + headers={"WWW-Authenticate": "APIKey"}, + ) + + def check_api_key(self, api_key: Optional[str]) -> Optional[str]: if not api_key: - if auto_error: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" - ) + if self.auto_error: + raise self.make_not_authenticated_error() return None return api_key @@ -100,17 +130,17 @@ class APIKeyQuery(APIKeyBase): ), ] = True, ): - self.model: APIKey = APIKey( - **{"in": APIKeyIn.query}, + super().__init__( + location=APIKeyIn.query, name=name, + scheme_name=scheme_name, description=description, + auto_error=auto_error, ) - self.scheme_name = scheme_name or self.__class__.__name__ - self.auto_error = auto_error async def __call__(self, request: Request) -> Optional[str]: api_key = request.query_params.get(self.model.name) - return self.check_api_key(api_key, self.auto_error) + return self.check_api_key(api_key) class APIKeyHeader(APIKeyBase): @@ -188,17 +218,17 @@ class APIKeyHeader(APIKeyBase): ), ] = True, ): - self.model: APIKey = APIKey( - **{"in": APIKeyIn.header}, + super().__init__( + location=APIKeyIn.header, name=name, + scheme_name=scheme_name, description=description, + auto_error=auto_error, ) - self.scheme_name = scheme_name or self.__class__.__name__ - self.auto_error = auto_error async def __call__(self, request: Request) -> Optional[str]: api_key = request.headers.get(self.model.name) - return self.check_api_key(api_key, self.auto_error) + return self.check_api_key(api_key) class APIKeyCookie(APIKeyBase): @@ -276,14 +306,14 @@ class APIKeyCookie(APIKeyBase): ), ] = True, ): - self.model: APIKey = APIKey( - **{"in": APIKeyIn.cookie}, + super().__init__( + location=APIKeyIn.cookie, name=name, + scheme_name=scheme_name, description=description, + auto_error=auto_error, ) - self.scheme_name = scheme_name or self.__class__.__name__ - self.auto_error = auto_error async def __call__(self, request: Request) -> Optional[str]: api_key = request.cookies.get(self.model.name) - return self.check_api_key(api_key, self.auto_error) + return self.check_api_key(api_key) diff --git a/fastapi/security/http.py b/fastapi/security/http.py index 3a5985650..0d1bbba3a 100644 --- a/fastapi/security/http.py +++ b/fastapi/security/http.py @@ -1,6 +1,6 @@ import binascii from base64 import b64decode -from typing import Optional +from typing import Dict, Optional from annotated_doc import Doc from fastapi.exceptions import HTTPException @@ -10,7 +10,7 @@ from fastapi.security.base import SecurityBase from fastapi.security.utils import get_authorization_scheme_param from pydantic import BaseModel from starlette.requests import Request -from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN +from starlette.status import HTTP_401_UNAUTHORIZED from typing_extensions import Annotated @@ -76,10 +76,22 @@ class HTTPBase(SecurityBase): description: Optional[str] = None, auto_error: bool = True, ): - self.model = HTTPBaseModel(scheme=scheme, description=description) + self.model: HTTPBaseModel = HTTPBaseModel( + scheme=scheme, description=description + ) self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error + def make_authenticate_headers(self) -> Dict[str, str]: + return {"WWW-Authenticate": f"{self.model.scheme.title()}"} + + def make_not_authenticated_error(self) -> HTTPException: + return HTTPException( + status_code=HTTP_401_UNAUTHORIZED, + detail="Not authenticated", + headers=self.make_authenticate_headers(), + ) + async def __call__( self, request: Request ) -> Optional[HTTPAuthorizationCredentials]: @@ -87,9 +99,7 @@ class HTTPBase(SecurityBase): scheme, credentials = get_authorization_scheme_param(authorization) if not (authorization and scheme and credentials): if self.auto_error: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" - ) + raise self.make_not_authenticated_error() else: return None return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials) @@ -99,6 +109,8 @@ class HTTPBasic(HTTPBase): """ HTTP Basic authentication. + Ref: https://datatracker.ietf.org/doc/html/rfc7617 + ## Usage Create an instance object and use that object as the dependency in `Depends()`. @@ -185,36 +197,28 @@ class HTTPBasic(HTTPBase): self.realm = realm self.auto_error = auto_error + def make_authenticate_headers(self) -> Dict[str, str]: + if self.realm: + return {"WWW-Authenticate": f'Basic realm="{self.realm}"'} + return {"WWW-Authenticate": "Basic"} + async def __call__( # type: ignore self, request: Request ) -> Optional[HTTPBasicCredentials]: authorization = request.headers.get("Authorization") scheme, param = get_authorization_scheme_param(authorization) - if self.realm: - unauthorized_headers = {"WWW-Authenticate": f'Basic realm="{self.realm}"'} - else: - unauthorized_headers = {"WWW-Authenticate": "Basic"} if not authorization or scheme.lower() != "basic": if self.auto_error: - raise HTTPException( - status_code=HTTP_401_UNAUTHORIZED, - detail="Not authenticated", - headers=unauthorized_headers, - ) + raise self.make_not_authenticated_error() else: return None - invalid_user_credentials_exc = HTTPException( - status_code=HTTP_401_UNAUTHORIZED, - detail="Invalid authentication credentials", - headers=unauthorized_headers, - ) try: data = b64decode(param).decode("ascii") - except (ValueError, UnicodeDecodeError, binascii.Error): - raise invalid_user_credentials_exc # noqa: B904 + except (ValueError, UnicodeDecodeError, binascii.Error) as e: + raise self.make_not_authenticated_error() from e username, separator, password = data.partition(":") if not separator: - raise invalid_user_credentials_exc + raise self.make_not_authenticated_error() return HTTPBasicCredentials(username=username, password=password) @@ -306,17 +310,12 @@ class HTTPBearer(HTTPBase): scheme, credentials = get_authorization_scheme_param(authorization) if not (authorization and scheme and credentials): if self.auto_error: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" - ) + raise self.make_not_authenticated_error() else: return None if scheme.lower() != "bearer": if self.auto_error: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, - detail="Invalid authentication credentials", - ) + raise self.make_not_authenticated_error() else: return None return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials) @@ -326,6 +325,12 @@ class HTTPDigest(HTTPBase): """ HTTP Digest authentication. + **Warning**: this is only a stub to connect the components with OpenAPI in FastAPI, + but it doesn't implement the full Digest scheme, you would need to to subclass it + and implement it in your code. + + Ref: https://datatracker.ietf.org/doc/html/rfc7616 + ## Usage Create an instance object and use that object as the dependency in `Depends()`. @@ -408,17 +413,12 @@ class HTTPDigest(HTTPBase): scheme, credentials = get_authorization_scheme_param(authorization) if not (authorization and scheme and credentials): if self.auto_error: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" - ) + raise self.make_not_authenticated_error() else: return None if scheme.lower() != "digest": if self.auto_error: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, - detail="Invalid authentication credentials", - ) + raise self.make_not_authenticated_error() else: return None return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials) diff --git a/fastapi/security/oauth2.py b/fastapi/security/oauth2.py index f8d97d762..b41b0f877 100644 --- a/fastapi/security/oauth2.py +++ b/fastapi/security/oauth2.py @@ -8,7 +8,7 @@ from fastapi.param_functions import Form from fastapi.security.base import SecurityBase from fastapi.security.utils import get_authorization_scheme_param from starlette.requests import Request -from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN +from starlette.status import HTTP_401_UNAUTHORIZED # TODO: import from typing when deprecating Python 3.9 from typing_extensions import Annotated @@ -377,13 +377,33 @@ class OAuth2(SecurityBase): self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error + def make_not_authenticated_error(self) -> HTTPException: + """ + The OAuth 2 specification doesn't define the challenge that should be used, + because a `Bearer` token is not really the only option to authenticate. + + But declaring any other authentication challenge would be application-specific + as it's not defined in the specification. + + For practical reasons, this method uses the `Bearer` challenge by default, as + it's probably the most common one. + + If you are implementing an OAuth2 authentication scheme other than the provided + ones in FastAPI (based on bearer tokens), you might want to override this. + + Ref: https://datatracker.ietf.org/doc/html/rfc6749 + """ + return HTTPException( + status_code=HTTP_401_UNAUTHORIZED, + detail="Not authenticated", + headers={"WWW-Authenticate": "Bearer"}, + ) + async def __call__(self, request: Request) -> Optional[str]: authorization = request.headers.get("Authorization") if not authorization: if self.auto_error: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" - ) + raise self.make_not_authenticated_error() else: return None return authorization @@ -491,11 +511,7 @@ class OAuth2PasswordBearer(OAuth2): scheme, param = get_authorization_scheme_param(authorization) if not authorization or scheme.lower() != "bearer": if self.auto_error: - raise HTTPException( - status_code=HTTP_401_UNAUTHORIZED, - detail="Not authenticated", - headers={"WWW-Authenticate": "Bearer"}, - ) + raise self.make_not_authenticated_error() else: return None return param @@ -601,11 +617,7 @@ class OAuth2AuthorizationCodeBearer(OAuth2): scheme, param = get_authorization_scheme_param(authorization) if not authorization or scheme.lower() != "bearer": if self.auto_error: - raise HTTPException( - status_code=HTTP_401_UNAUTHORIZED, - detail="Not authenticated", - headers={"WWW-Authenticate": "Bearer"}, - ) + raise self.make_not_authenticated_error() else: return None # pragma: nocover return param diff --git a/fastapi/security/open_id_connect_url.py b/fastapi/security/open_id_connect_url.py index 5e99798e6..e574a56a8 100644 --- a/fastapi/security/open_id_connect_url.py +++ b/fastapi/security/open_id_connect_url.py @@ -5,7 +5,7 @@ from fastapi.openapi.models import OpenIdConnect as OpenIdConnectModel from fastapi.security.base import SecurityBase from starlette.exceptions import HTTPException from starlette.requests import Request -from starlette.status import HTTP_403_FORBIDDEN +from starlette.status import HTTP_401_UNAUTHORIZED from typing_extensions import Annotated @@ -13,6 +13,11 @@ class OpenIdConnect(SecurityBase): """ OpenID Connect authentication class. An instance of it would be used as a dependency. + + **Warning**: this is only a stub to connect the components with OpenAPI in FastAPI, + but it doesn't implement the full OpenIdConnect scheme, for example, it doesn't use + the OpenIDConnect URL. You would need to to subclass it and implement it in your + code. """ def __init__( @@ -73,13 +78,18 @@ class OpenIdConnect(SecurityBase): self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error + def make_not_authenticated_error(self) -> HTTPException: + return HTTPException( + status_code=HTTP_401_UNAUTHORIZED, + detail="Not authenticated", + headers={"WWW-Authenticate": "Bearer"}, + ) + async def __call__(self, request: Request) -> Optional[str]: authorization = request.headers.get("Authorization") if not authorization: if self.auto_error: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" - ) + raise self.make_not_authenticated_error() else: return None return authorization diff --git a/tests/test_security_api_key_cookie.py b/tests/test_security_api_key_cookie.py index 4ddb8e2ee..9bacfc56e 100644 --- a/tests/test_security_api_key_cookie.py +++ b/tests/test_security_api_key_cookie.py @@ -32,8 +32,9 @@ def test_security_api_key(): def test_security_api_key_no_key(): client = TestClient(app) response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "APIKey" def test_openapi_schema(): diff --git a/tests/test_security_api_key_cookie_description.py b/tests/test_security_api_key_cookie_description.py index d99d616e0..d0cab324e 100644 --- a/tests/test_security_api_key_cookie_description.py +++ b/tests/test_security_api_key_cookie_description.py @@ -32,8 +32,9 @@ def test_security_api_key(): def test_security_api_key_no_key(): client = TestClient(app) response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "APIKey" def test_openapi_schema(): diff --git a/tests/test_security_api_key_header.py b/tests/test_security_api_key_header.py index 1ff883703..3e761b150 100644 --- a/tests/test_security_api_key_header.py +++ b/tests/test_security_api_key_header.py @@ -33,8 +33,9 @@ def test_security_api_key(): def test_security_api_key_no_key(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "APIKey" def test_openapi_schema(): diff --git a/tests/test_security_api_key_header_description.py b/tests/test_security_api_key_header_description.py index 27f9d0f29..38a1a8881 100644 --- a/tests/test_security_api_key_header_description.py +++ b/tests/test_security_api_key_header_description.py @@ -33,8 +33,9 @@ def test_security_api_key(): def test_security_api_key_no_key(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "APIKey" def test_openapi_schema(): diff --git a/tests/test_security_api_key_query.py b/tests/test_security_api_key_query.py index dc7a0a621..11ed19468 100644 --- a/tests/test_security_api_key_query.py +++ b/tests/test_security_api_key_query.py @@ -33,8 +33,9 @@ def test_security_api_key(): def test_security_api_key_no_key(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "APIKey" def test_openapi_schema(): diff --git a/tests/test_security_api_key_query_description.py b/tests/test_security_api_key_query_description.py index 35dc7743a..658798326 100644 --- a/tests/test_security_api_key_query_description.py +++ b/tests/test_security_api_key_query_description.py @@ -33,8 +33,9 @@ def test_security_api_key(): def test_security_api_key_no_key(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "APIKey" def test_openapi_schema(): diff --git a/tests/test_security_http_base.py b/tests/test_security_http_base.py index 51928bafd..8cf259a75 100644 --- a/tests/test_security_http_base.py +++ b/tests/test_security_http_base.py @@ -23,8 +23,9 @@ def test_security_http_base(): def test_security_http_base_no_credentials(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Other" def test_openapi_schema(): diff --git a/tests/test_security_http_base_description.py b/tests/test_security_http_base_description.py index bc79f3242..791ea59f4 100644 --- a/tests/test_security_http_base_description.py +++ b/tests/test_security_http_base_description.py @@ -23,8 +23,9 @@ def test_security_http_base(): def test_security_http_base_no_credentials(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Other" def test_openapi_schema(): diff --git a/tests/test_security_http_basic_optional.py b/tests/test_security_http_basic_optional.py index 9b6cb6c45..7071f381a 100644 --- a/tests/test_security_http_basic_optional.py +++ b/tests/test_security_http_basic_optional.py @@ -38,7 +38,7 @@ def test_security_http_basic_invalid_credentials(): ) assert response.status_code == 401, response.text assert response.headers["WWW-Authenticate"] == "Basic" - assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.json() == {"detail": "Not authenticated"} def test_security_http_basic_non_basic_credentials(): @@ -47,7 +47,7 @@ def test_security_http_basic_non_basic_credentials(): response = client.get("/users/me", headers={"Authorization": auth_header}) assert response.status_code == 401, response.text assert response.headers["WWW-Authenticate"] == "Basic" - assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.json() == {"detail": "Not authenticated"} def test_openapi_schema(): diff --git a/tests/test_security_http_basic_realm.py b/tests/test_security_http_basic_realm.py index 9fc33971a..ec7371f90 100644 --- a/tests/test_security_http_basic_realm.py +++ b/tests/test_security_http_basic_realm.py @@ -36,7 +36,7 @@ def test_security_http_basic_invalid_credentials(): ) assert response.status_code == 401, response.text assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"' - assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.json() == {"detail": "Not authenticated"} def test_security_http_basic_non_basic_credentials(): @@ -45,7 +45,7 @@ def test_security_http_basic_non_basic_credentials(): response = client.get("/users/me", headers={"Authorization": auth_header}) assert response.status_code == 401, response.text assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"' - assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.json() == {"detail": "Not authenticated"} def test_openapi_schema(): diff --git a/tests/test_security_http_basic_realm_description.py b/tests/test_security_http_basic_realm_description.py index 02122442e..a93d5fc86 100644 --- a/tests/test_security_http_basic_realm_description.py +++ b/tests/test_security_http_basic_realm_description.py @@ -36,7 +36,7 @@ def test_security_http_basic_invalid_credentials(): ) assert response.status_code == 401, response.text assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"' - assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.json() == {"detail": "Not authenticated"} def test_security_http_basic_non_basic_credentials(): @@ -45,7 +45,7 @@ def test_security_http_basic_non_basic_credentials(): response = client.get("/users/me", headers={"Authorization": auth_header}) assert response.status_code == 401, response.text assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"' - assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.json() == {"detail": "Not authenticated"} def test_openapi_schema(): diff --git a/tests/test_security_http_bearer.py b/tests/test_security_http_bearer.py index 5b9e2d691..961b42f4d 100644 --- a/tests/test_security_http_bearer.py +++ b/tests/test_security_http_bearer.py @@ -23,14 +23,16 @@ def test_security_http_bearer(): def test_security_http_bearer_no_credentials(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" def test_security_http_bearer_incorrect_scheme_credentials(): response = client.get("/users/me", headers={"Authorization": "Basic notreally"}) - assert response.status_code == 403, response.text - assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" def test_openapi_schema(): diff --git a/tests/test_security_http_bearer_description.py b/tests/test_security_http_bearer_description.py index 2f11c3a14..e16994abc 100644 --- a/tests/test_security_http_bearer_description.py +++ b/tests/test_security_http_bearer_description.py @@ -23,14 +23,16 @@ def test_security_http_bearer(): def test_security_http_bearer_no_credentials(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" def test_security_http_bearer_incorrect_scheme_credentials(): response = client.get("/users/me", headers={"Authorization": "Basic notreally"}) - assert response.status_code == 403, response.text - assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" def test_openapi_schema(): diff --git a/tests/test_security_http_digest.py b/tests/test_security_http_digest.py index 133d35763..3fad4c7a5 100644 --- a/tests/test_security_http_digest.py +++ b/tests/test_security_http_digest.py @@ -23,16 +23,18 @@ def test_security_http_digest(): def test_security_http_digest_no_credentials(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Digest" def test_security_http_digest_incorrect_scheme_credentials(): response = client.get( "/users/me", headers={"Authorization": "Other invalidauthorization"} ) - assert response.status_code == 403, response.text - assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Digest" def test_openapi_schema(): diff --git a/tests/test_security_http_digest_description.py b/tests/test_security_http_digest_description.py index 4e31a0c00..319416a07 100644 --- a/tests/test_security_http_digest_description.py +++ b/tests/test_security_http_digest_description.py @@ -23,16 +23,18 @@ def test_security_http_digest(): def test_security_http_digest_no_credentials(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Digest" def test_security_http_digest_incorrect_scheme_credentials(): response = client.get( "/users/me", headers={"Authorization": "Other invalidauthorization"} ) - assert response.status_code == 403, response.text - assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Digest" def test_openapi_schema(): diff --git a/tests/test_security_oauth2.py b/tests/test_security_oauth2.py index 2b7e3457a..804e4152d 100644 --- a/tests/test_security_oauth2.py +++ b/tests/test_security_oauth2.py @@ -56,8 +56,9 @@ def test_security_oauth2_password_other_header(): def test_security_oauth2_password_bearer_no_header(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" def test_strict_login_no_data(): diff --git a/tests/test_security_openid_connect.py b/tests/test_security_openid_connect.py index 1e322e640..c9a0a8db7 100644 --- a/tests/test_security_openid_connect.py +++ b/tests/test_security_openid_connect.py @@ -39,8 +39,9 @@ def test_security_oauth2_password_other_header(): def test_security_oauth2_password_bearer_no_header(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" def test_openapi_schema(): diff --git a/tests/test_security_openid_connect_description.py b/tests/test_security_openid_connect_description.py index 44cf57f86..d008cbc63 100644 --- a/tests/test_security_openid_connect_description.py +++ b/tests/test_security_openid_connect_description.py @@ -41,8 +41,9 @@ def test_security_oauth2_password_other_header(): def test_security_oauth2_password_bearer_no_header(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" def test_openapi_schema(): diff --git a/tests/test_top_level_security_scheme_in_openapi.py b/tests/test_top_level_security_scheme_in_openapi.py index e2de31af5..a36c66d1a 100644 --- a/tests/test_top_level_security_scheme_in_openapi.py +++ b/tests/test_top_level_security_scheme_in_openapi.py @@ -27,7 +27,7 @@ def test_get_root(): def test_get_root_no_token(): response = client.get("/") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} diff --git a/tests/test_tutorial/test_authentication_error_status_code/__init__.py b/tests/test_tutorial/test_authentication_error_status_code/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tutorial/test_authentication_error_status_code/test_tutorial001.py b/tests/test_tutorial/test_authentication_error_status_code/test_tutorial001.py new file mode 100644 index 000000000..bbd7bff30 --- /dev/null +++ b/tests/test_tutorial/test_authentication_error_status_code/test_tutorial001.py @@ -0,0 +1,69 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient +from inline_snapshot import snapshot + +from ...utils import needs_py39 + + +@pytest.fixture( + name="client", + params=[ + "tutorial001_an", + pytest.param("tutorial001_an_py39", marks=needs_py39), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module( + f"docs_src.authentication_error_status_code.{request.param}" + ) + + client = TestClient(mod.app) + return client + + +def test_get_me(client: TestClient): + response = client.get("/me", headers={"Authorization": "Bearer secrettoken"}) + assert response.status_code == 200 + assert response.json() == { + "message": "You are authenticated", + "token": "secrettoken", + } + + +def test_get_me_no_credentials(client: TestClient): + response = client.get("/me") + assert response.status_code == 403 + assert response.json() == {"detail": "Not authenticated"} + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/me": { + "get": { + "summary": "Read Me", + "operationId": "read_me_me_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "security": [{"HTTPBearer403": []}], + } + } + }, + "components": { + "securitySchemes": { + "HTTPBearer403": {"type": "http", "scheme": "bearer"} + } + }, + } + ) diff --git a/tests/test_tutorial/test_security/test_tutorial003.py b/tests/test_tutorial/test_security/test_tutorial003.py index 2bbb2e851..6b8735113 100644 --- a/tests/test_tutorial/test_security/test_tutorial003.py +++ b/tests/test_tutorial/test_security/test_tutorial003.py @@ -66,7 +66,7 @@ def test_token(client: TestClient): def test_incorrect_token(client: TestClient): response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) assert response.status_code == 401, response.text - assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.json() == {"detail": "Not authenticated"} assert response.headers["WWW-Authenticate"] == "Bearer" diff --git a/tests/test_tutorial/test_security/test_tutorial006.py b/tests/test_tutorial/test_security/test_tutorial006.py index 40b413806..9587159dc 100644 --- a/tests/test_tutorial/test_security/test_tutorial006.py +++ b/tests/test_tutorial/test_security/test_tutorial006.py @@ -41,7 +41,7 @@ def test_security_http_basic_invalid_credentials(client: TestClient): ) assert response.status_code == 401, response.text assert response.headers["WWW-Authenticate"] == "Basic" - assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.json() == {"detail": "Not authenticated"} def test_security_http_basic_non_basic_credentials(client: TestClient): @@ -50,7 +50,7 @@ def test_security_http_basic_non_basic_credentials(client: TestClient): response = client.get("/users/me", headers={"Authorization": auth_header}) assert response.status_code == 401, response.text assert response.headers["WWW-Authenticate"] == "Basic" - assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.json() == {"detail": "Not authenticated"} def test_openapi_schema(client: TestClient): From a4ef97afd937a8fd180a78e11c3648509e5bc14d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 24 Nov 2025 19:03:33 +0000 Subject: [PATCH 137/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index ef1b98b67..c2190dafe 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Fixes + +* 🐛 Use `401` status code in security classes when credentials are missing. PR [#13786](https://github.com/fastapi/fastapi/pull/13786) by [@YuriiMotov](https://github.com/YuriiMotov). + ### Internal * 🔧 Configure labeler to exclude files that start from underscore for `lang-all` label. PR [#14213](https://github.com/fastapi/fastapi/pull/14213) by [@YuriiMotov](https://github.com/YuriiMotov). From 8732c53478513ddd35ae152ff9bf5e6217ed3d99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 24 Nov 2025 20:12:28 +0100 Subject: [PATCH 138/256] =?UTF-8?q?=F0=9F=93=9D=20Updates=20release=20note?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index c2190dafe..0ccb6b04e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -10,6 +10,7 @@ hide: ### Fixes * 🐛 Use `401` status code in security classes when credentials are missing. PR [#13786](https://github.com/fastapi/fastapi/pull/13786) by [@YuriiMotov](https://github.com/YuriiMotov). + * If your code depended on these classes raising the old (less correct) `403` status code, check the new docs about how to override the classes, to use the same old behavior: [Use Old 403 Authentication Error Status Codes](https://fastapi.tiangolo.com/how-to/authentication-error-status-code/). ### Internal From 5b0625df96e4ea11b54fcb2a76f21f7ad94764fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 24 Nov 2025 20:14:34 +0100 Subject: [PATCH 139/256] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.12?= =?UTF-8?q?2.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 0ccb6b04e..2c50bc9f2 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.122.0 + ### Fixes * 🐛 Use `401` status code in security classes when credentials are missing. PR [#13786](https://github.com/fastapi/fastapi/pull/13786) by [@YuriiMotov](https://github.com/YuriiMotov). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 85a7ea7b5..3fbd7fc28 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.121.3" +__version__ = "0.122.0" from starlette import status as status From 8ab7167eaf046fb1c7a700dd72e773bb16e7d88f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 28 Nov 2025 07:55:15 -0800 Subject: [PATCH 140/256] =?UTF-8?q?=F0=9F=92=85=20Update=20CSS=20to=20expl?= =?UTF-8?q?icitly=20use=20emoji=20font=20(#14415)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/css/custom.css | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/css/custom.css b/docs/en/docs/css/custom.css index 8849d8741..87111ff64 100644 --- a/docs/en/docs/css/custom.css +++ b/docs/en/docs/css/custom.css @@ -1,9 +1,16 @@ /* Fira Code, including characters used by Rich output, like the "heavy right-pointing angle bracket ornament", not included in Google Fonts */ @import url(https://cdn.jsdelivr.net/npm/firacode@6.2.0/distr/fira_code.css); +/* Noto Color Emoji for emoji support with the same font everywhere */ +@import url(https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&display=swap); /* Override default code font in Material for MkDocs to Fira Code */ :root { - --md-code-font: "Fira Code", monospace; + --md-code-font: "Fira Code", monospace, "Noto Color Emoji"; +} + +/* Override default regular font in Material for MkDocs to include Noto Color Emoji */ +:root { + --md-text-font: "Roboto", "Noto Color Emoji"; } .termynal-comment { From 998288261af114efd39fb2061ed7ceba32f8699f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 28 Nov 2025 15:55:40 +0000 Subject: [PATCH 141/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 2c50bc9f2..4b2cb937d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Docs + +* 💅 Update CSS to explicitly use emoji font. PR [#14415](https://github.com/fastapi/fastapi/pull/14415) by [@tiangolo](https://github.com/tiangolo). + ## 0.122.0 ### Fixes From 62a69740041726b8c27815f6246272db5ebf7ee5 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Sat, 29 Nov 2025 13:08:57 +0100 Subject: [PATCH 142/256] =?UTF-8?q?=E2=AC=86=20Bump=20markdown-include-var?= =?UTF-8?q?iants=20from=200.0.5=20to=200.0.6=20(#14418)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 05b47fe92..ae1ddbc3d 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -17,5 +17,5 @@ griffe-warnings-deprecated==1.1.0 # For griffe, it formats with black black==25.1.0 mkdocs-macros-plugin==1.4.1 -markdown-include-variants==0.0.5 +markdown-include-variants==0.0.6 python-slugify==8.0.4 From c6487ed632056e450d844846a1b63be551a3cbc6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 29 Nov 2025 12:09:26 +0000 Subject: [PATCH 143/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 4b2cb937d..fa8ddcdad 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -11,6 +11,10 @@ hide: * 💅 Update CSS to explicitly use emoji font. PR [#14415](https://github.com/fastapi/fastapi/pull/14415) by [@tiangolo](https://github.com/tiangolo). +### Internal + +* ⬆ Bump markdown-include-variants from 0.0.5 to 0.0.6. PR [#14418](https://github.com/fastapi/fastapi/pull/14418) by [@YuriiMotov](https://github.com/YuriiMotov). + ## 0.122.0 ### Fixes From 378ad688b7e57efb190506b3b36be65eb8ad5e6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Sun, 30 Nov 2025 11:57:01 +0000 Subject: [PATCH 144/256] =?UTF-8?q?=F0=9F=90=9B=20Fix=20hierarchical=20sec?= =?UTF-8?q?urity=20scope=20propagation=20(#5624)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Co-authored-by: svlandeg Co-authored-by: Sofie Van Landeghem --- fastapi/dependencies/utils.py | 4 +- tests/test_security_scopes_dont_propagate.py | 45 ++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 tests/test_security_scopes_dont_propagate.py diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 1e92c1ba2..45353835b 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -278,7 +278,9 @@ def get_dependant( use_security_scopes = security_scopes or [] if isinstance(param_details.depends, params.Security): if param_details.depends.scopes: - use_security_scopes.extend(param_details.depends.scopes) + use_security_scopes = use_security_scopes + list( + param_details.depends.scopes + ) sub_dependant = get_dependant( path=path, call=param_details.depends.dependency, diff --git a/tests/test_security_scopes_dont_propagate.py b/tests/test_security_scopes_dont_propagate.py new file mode 100644 index 000000000..2bbcc749d --- /dev/null +++ b/tests/test_security_scopes_dont_propagate.py @@ -0,0 +1,45 @@ +# Ref: https://github.com/tiangolo/fastapi/issues/5623 + +from typing import Any, Dict, List + +from fastapi import FastAPI, Security +from fastapi.security import SecurityScopes +from fastapi.testclient import TestClient +from typing_extensions import Annotated + + +async def security1(scopes: SecurityScopes): + return scopes.scopes + + +async def security2(scopes: SecurityScopes): + return scopes.scopes + + +async def dep3( + dep1: Annotated[List[str], Security(security1, scopes=["scope1"])], + dep2: Annotated[List[str], Security(security2, scopes=["scope2"])], +): + return {"dep1": dep1, "dep2": dep2} + + +app = FastAPI() + + +@app.get("/scopes") +def get_scopes( + dep3: Annotated[Dict[str, Any], Security(dep3, scopes=["scope3"])], +): + return dep3 + + +client = TestClient(app) + + +def test_security_scopes_dont_propagate(): + response = client.get("/scopes") + assert response.status_code == 200 + assert response.json() == { + "dep1": ["scope3", "scope1"], + "dep2": ["scope3", "scope2"], + } From 7681f2904d2f902057e357c107adf39ecfb14ea9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 30 Nov 2025 11:57:24 +0000 Subject: [PATCH 145/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index fa8ddcdad..65306828d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Fixes + +* 🐛 Fix hierarchical security scope propagation. PR [#5624](https://github.com/fastapi/fastapi/pull/5624) by [@kristjanvalur](https://github.com/kristjanvalur). + ### Docs * 💅 Update CSS to explicitly use emoji font. PR [#14415](https://github.com/fastapi/fastapi/pull/14415) by [@tiangolo](https://github.com/tiangolo). From 63d7a2b9978258d13dfc22664e60fc2110d30e20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 30 Nov 2025 13:00:20 +0100 Subject: [PATCH 146/256] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.12?= =?UTF-8?q?2.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 65306828d..975166a63 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.122.1 + ### Fixes * 🐛 Fix hierarchical security scope propagation. PR [#5624](https://github.com/fastapi/fastapi/pull/5624) by [@kristjanvalur](https://github.com/kristjanvalur). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 3fbd7fc28..92c067e50 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.122.0" +__version__ = "0.122.1" from starlette import status as status From 7fbd30460f480e90faf321b9158bffb5116000d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 30 Nov 2025 06:45:49 -0800 Subject: [PATCH 147/256] =?UTF-8?q?=F0=9F=90=9B=20Cache=20dependencies=20t?= =?UTF-8?q?hat=20don't=20use=20scopes=20and=20don't=20have=20sub-dependenc?= =?UTF-8?q?ies=20with=20scopes=20(#14419)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] --- fastapi/dependencies/models.py | 28 ++++- fastapi/dependencies/utils.py | 34 +++--- tests/test_security_scopes.py | 46 ++++++++ tests/test_security_scopes_sub_dependency.py | 107 +++++++++++++++++++ 4 files changed, 195 insertions(+), 20 deletions(-) create mode 100644 tests/test_security_scopes.py create mode 100644 tests/test_security_scopes_sub_dependency.py diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index d6359c0f5..fbb666a7d 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -38,19 +38,43 @@ class Dependant: response_param_name: Optional[str] = None background_tasks_param_name: Optional[str] = None security_scopes_param_name: Optional[str] = None - security_scopes: Optional[List[str]] = None + own_oauth_scopes: Optional[List[str]] = None + parent_oauth_scopes: Optional[List[str]] = None use_cache: bool = True path: Optional[str] = None scope: Union[Literal["function", "request"], None] = None + @cached_property + def oauth_scopes(self) -> List[str]: + scopes = self.parent_oauth_scopes.copy() if self.parent_oauth_scopes else [] + # This doesn't use a set to preserve order, just in case + for scope in self.own_oauth_scopes or []: + if scope not in scopes: + scopes.append(scope) + return scopes + @cached_property def cache_key(self) -> DependencyCacheKey: + scopes_for_cache = ( + tuple(sorted(set(self.oauth_scopes or []))) if self._uses_scopes else () + ) return ( self.call, - tuple(sorted(set(self.security_scopes or []))), + scopes_for_cache, self.computed_scope or "", ) + @cached_property + def _uses_scopes(self) -> bool: + if self.own_oauth_scopes: + return True + if self.security_scopes_param_name is not None: + return True + for sub_dep in self.dependencies: + if sub_dep._uses_scopes: + return True + return False + @cached_property def is_gen_callable(self) -> bool: if inspect.isgeneratorfunction(self.call): diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 45353835b..d43fa8a51 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -58,8 +58,7 @@ from fastapi.dependencies.models import Dependant, SecurityRequirement from fastapi.exceptions import DependencyScopeError from fastapi.logger import logger from fastapi.security.base import SecurityBase -from fastapi.security.oauth2 import OAuth2, SecurityScopes -from fastapi.security.open_id_connect_url import OpenIdConnect +from fastapi.security.oauth2 import SecurityScopes from fastapi.types import DependencyCacheKey from fastapi.utils import create_model_field, get_path_param_names from pydantic import BaseModel @@ -126,14 +125,14 @@ def get_parameterless_sub_dependant(*, depends: params.Depends, path: str) -> De assert callable(depends.dependency), ( "A parameter-less dependency must have a callable dependency" ) - use_security_scopes: List[str] = [] + own_oauth_scopes: List[str] = [] if isinstance(depends, params.Security) and depends.scopes: - use_security_scopes.extend(depends.scopes) + own_oauth_scopes.extend(depends.scopes) return get_dependant( path=path, call=depends.dependency, scope=depends.scope, - security_scopes=use_security_scopes, + own_oauth_scopes=own_oauth_scopes, ) @@ -232,7 +231,8 @@ def get_dependant( path: str, call: Callable[..., Any], name: Optional[str] = None, - security_scopes: Optional[List[str]] = None, + own_oauth_scopes: Optional[List[str]] = None, + parent_oauth_scopes: Optional[List[str]] = None, use_cache: bool = True, scope: Union[Literal["function", "request"], None] = None, ) -> Dependant: @@ -240,19 +240,18 @@ def get_dependant( call=call, name=name, path=path, - security_scopes=security_scopes, use_cache=use_cache, scope=scope, + own_oauth_scopes=own_oauth_scopes, + parent_oauth_scopes=parent_oauth_scopes, ) + current_scopes = (parent_oauth_scopes or []) + (own_oauth_scopes or []) path_param_names = get_path_param_names(path) endpoint_signature = get_typed_signature(call) signature_params = endpoint_signature.parameters if isinstance(call, SecurityBase): - use_scopes: List[str] = [] - if isinstance(call, (OAuth2, OpenIdConnect)): - use_scopes = security_scopes or use_scopes security_requirement = SecurityRequirement( - security_scheme=call, scopes=use_scopes + security_scheme=call, scopes=current_scopes ) dependant.security_requirements.append(security_requirement) for param_name, param in signature_params.items(): @@ -275,17 +274,16 @@ def get_dependant( f'The dependency "{dependant.call.__name__}" has a scope of ' '"request", it cannot depend on dependencies with scope "function".' ) - use_security_scopes = security_scopes or [] + sub_own_oauth_scopes: List[str] = [] if isinstance(param_details.depends, params.Security): if param_details.depends.scopes: - use_security_scopes = use_security_scopes + list( - param_details.depends.scopes - ) + sub_own_oauth_scopes = list(param_details.depends.scopes) sub_dependant = get_dependant( path=path, call=param_details.depends.dependency, name=param_name, - security_scopes=use_security_scopes, + own_oauth_scopes=sub_own_oauth_scopes, + parent_oauth_scopes=current_scopes, use_cache=param_details.depends.use_cache, scope=param_details.depends.scope, ) @@ -611,7 +609,7 @@ async def solve_dependencies( path=use_path, call=call, name=sub_dependant.name, - security_scopes=sub_dependant.security_scopes, + parent_oauth_scopes=sub_dependant.oauth_scopes, scope=sub_dependant.scope, ) @@ -693,7 +691,7 @@ async def solve_dependencies( values[dependant.response_param_name] = response if dependant.security_scopes_param_name: values[dependant.security_scopes_param_name] = SecurityScopes( - scopes=dependant.security_scopes + scopes=dependant.oauth_scopes ) return SolvedDependency( values=values, diff --git a/tests/test_security_scopes.py b/tests/test_security_scopes.py new file mode 100644 index 000000000..248fd2bcc --- /dev/null +++ b/tests/test_security_scopes.py @@ -0,0 +1,46 @@ +from typing import Dict + +import pytest +from fastapi import Depends, FastAPI, Security +from fastapi.testclient import TestClient +from typing_extensions import Annotated + + +@pytest.fixture(name="call_counter") +def call_counter_fixture(): + return {"count": 0} + + +@pytest.fixture(name="app") +def app_fixture(call_counter: Dict[str, int]): + def get_db(): + call_counter["count"] += 1 + return f"db_{call_counter['count']}" + + def get_user(db: Annotated[str, Depends(get_db)]): + return "user" + + app = FastAPI() + + @app.get("/") + def endpoint( + db: Annotated[str, Depends(get_db)], + user: Annotated[str, Security(get_user, scopes=["read"])], + ): + return {"db": db} + + return app + + +@pytest.fixture(name="client") +def client_fixture(app: FastAPI): + return TestClient(app) + + +def test_security_scopes_dependency_called_once( + client: TestClient, call_counter: Dict[str, int] +): + response = client.get("/") + + assert response.status_code == 200 + assert call_counter["count"] == 1 diff --git a/tests/test_security_scopes_sub_dependency.py b/tests/test_security_scopes_sub_dependency.py new file mode 100644 index 000000000..9cc668d8e --- /dev/null +++ b/tests/test_security_scopes_sub_dependency.py @@ -0,0 +1,107 @@ +# Ref: https://github.com/fastapi/fastapi/discussions/6024#discussioncomment-8541913 + +from typing import Dict + +import pytest +from fastapi import Depends, FastAPI, Security +from fastapi.security import SecurityScopes +from fastapi.testclient import TestClient +from typing_extensions import Annotated + + +@pytest.fixture(name="call_counts") +def call_counts_fixture(): + return { + "get_db_session": 0, + "get_current_user": 0, + "get_user_me": 0, + "get_user_items": 0, + } + + +@pytest.fixture(name="app") +def app_fixture(call_counts: Dict[str, int]): + def get_db_session(): + call_counts["get_db_session"] += 1 + return f"db_session_{call_counts['get_db_session']}" + + def get_current_user( + security_scopes: SecurityScopes, + db_session: Annotated[str, Depends(get_db_session)], + ): + call_counts["get_current_user"] += 1 + return { + "user": f"user_{call_counts['get_current_user']}", + "scopes": security_scopes.scopes, + "db_session": db_session, + } + + def get_user_me( + current_user: Annotated[dict, Security(get_current_user, scopes=["me"])], + ): + call_counts["get_user_me"] += 1 + return { + "user_me": f"user_me_{call_counts['get_user_me']}", + "current_user": current_user, + } + + def get_user_items( + user_me: Annotated[dict, Depends(get_user_me)], + ): + call_counts["get_user_items"] += 1 + return { + "user_items": f"user_items_{call_counts['get_user_items']}", + "user_me": user_me, + } + + app = FastAPI() + + @app.get("/") + def path_operation( + user_me: Annotated[dict, Depends(get_user_me)], + user_items: Annotated[dict, Security(get_user_items, scopes=["items"])], + ): + return { + "user_me": user_me, + "user_items": user_items, + } + + return app + + +@pytest.fixture(name="client") +def client_fixture(app: FastAPI): + return TestClient(app) + + +def test_security_scopes_sub_dependency_caching( + client: TestClient, call_counts: Dict[str, int] +): + response = client.get("/") + + assert response.status_code == 200 + assert call_counts["get_db_session"] == 1 + assert call_counts["get_current_user"] == 2 + assert call_counts["get_user_me"] == 2 + assert call_counts["get_user_items"] == 1 + assert response.json() == { + "user_me": { + "user_me": "user_me_1", + "current_user": { + "user": "user_1", + "scopes": ["me"], + "db_session": "db_session_1", + }, + }, + "user_items": { + "user_items": "user_items_1", + "user_me": { + "user_me": "user_me_2", + "current_user": { + "user": "user_2", + "scopes": ["items", "me"], + "db_session": "db_session_1", + }, + }, + }, + } From c38e3e0108852f0dfec0e9bb5fec7b3ccf7ddad3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 30 Nov 2025 14:46:13 +0000 Subject: [PATCH 148/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 975166a63..505ae48a5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Fixes + +* 🐛 Cache dependencies that don't use scopes and don't have sub-dependencies with scopes. PR [#14419](https://github.com/fastapi/fastapi/pull/14419) by [@tiangolo](https://github.com/tiangolo). + ## 0.122.1 ### Fixes From f2bab952678f301349c9805dd576af9425a95953 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 30 Nov 2025 15:47:35 +0100 Subject: [PATCH 149/256] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.12?= =?UTF-8?q?3.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 505ae48a5..8848784b1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.123.0 + ### Fixes * 🐛 Cache dependencies that don't use scopes and don't have sub-dependencies with scopes. PR [#14419](https://github.com/fastapi/fastapi/pull/14419) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 92c067e50..25ed2bbeb 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.122.1" +__version__ = "0.123.0" from starlette import status as status From 32aba57b499c9ec01a87f4e3a9aca980941e8959 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 30 Nov 2025 22:27:43 -0800 Subject: [PATCH 150/256] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20Peopl?= =?UTF-8?q?e=20-=20Contributors=20and=20Translators=20(#14420)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions --- docs/en/data/contributors.yml | 45 ++++++++++++++------------ docs/en/data/translation_reviewers.yml | 16 ++++----- docs/en/data/translators.yml | 24 +++++++------- 3 files changed, 45 insertions(+), 40 deletions(-) diff --git a/docs/en/data/contributors.yml b/docs/en/data/contributors.yml index 592c79af0..163dc68e3 100644 --- a/docs/en/data/contributors.yml +++ b/docs/en/data/contributors.yml @@ -1,21 +1,21 @@ tiangolo: login: tiangolo - count: 794 + count: 808 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 url: https://github.com/tiangolo dependabot: login: dependabot - count: 126 + count: 130 avatarUrl: https://avatars.githubusercontent.com/in/29110?v=4 url: https://github.com/apps/dependabot alejsdev: login: alejsdev count: 52 - avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=447d12a1b347f466b35378bee4c7104cc9b2c571&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=85ceac49fb87138aebe8d663912e359447329090&v=4 url: https://github.com/alejsdev pre-commit-ci: login: pre-commit-ci - count: 49 + count: 50 avatarUrl: https://avatars.githubusercontent.com/in/68672?v=4 url: https://github.com/apps/pre-commit-ci github-actions: @@ -28,31 +28,31 @@ Kludex: count: 25 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4 url: https://github.com/Kludex +YuriiMotov: + login: YuriiMotov + count: 20 + avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4 + url: https://github.com/YuriiMotov dmontagu: login: dmontagu count: 17 avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4 url: https://github.com/dmontagu -YuriiMotov: - login: YuriiMotov - count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4 - url: https://github.com/YuriiMotov nilslindemann: login: nilslindemann - count: 14 + count: 15 avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 url: https://github.com/nilslindemann +svlandeg: + login: svlandeg + count: 14 + avatarUrl: https://avatars.githubusercontent.com/u/8796347?u=556c97650c27021911b0b9447ec55e75987b0e8a&v=4 + url: https://github.com/svlandeg euri10: login: euri10 count: 13 avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4 url: https://github.com/euri10 -svlandeg: - login: svlandeg - count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/8796347?u=556c97650c27021911b0b9447ec55e75987b0e8a&v=4 - url: https://github.com/svlandeg kantandane: login: kantandane count: 13 @@ -103,6 +103,11 @@ waynerv: count: 5 avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4 url: https://github.com/waynerv +musicinmybrain: + login: musicinmybrain + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/6898909?u=9010312053e7141383b9bdf538036c7f37fbaba0&v=4 + url: https://github.com/musicinmybrain krishnamadhavan: login: krishnamadhavan count: 5 @@ -133,11 +138,6 @@ iudeen: count: 4 avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=f09cdd745e5bf16138f29b42732dd57c7f02bee1&v=4 url: https://github.com/iudeen -musicinmybrain: - login: musicinmybrain - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/6898909?u=9010312053e7141383b9bdf538036c7f37fbaba0&v=4 - url: https://github.com/musicinmybrain philipokiokio: login: philipokiokio count: 4 @@ -483,6 +483,11 @@ nzig: count: 2 avatarUrl: https://avatars.githubusercontent.com/u/7372858?u=e769add36ed73c778cdb136eb10bf96b1e119671&v=4 url: https://github.com/nzig +kristjanvalur: + login: kristjanvalur + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/6009543?u=1419f20bbfff8f031be8cb470962e7e62de2595e&v=4 + url: https://github.com/kristjanvalur yezz123: login: yezz123 count: 2 diff --git a/docs/en/data/translation_reviewers.yml b/docs/en/data/translation_reviewers.yml index 45aa55e5e..c3d3d0388 100644 --- a/docs/en/data/translation_reviewers.yml +++ b/docs/en/data/translation_reviewers.yml @@ -75,7 +75,7 @@ mattwang44: url: https://github.com/mattwang44 tiangolo: login: tiangolo - count: 55 + count: 56 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 url: https://github.com/tiangolo Laineyzhang55: @@ -136,7 +136,7 @@ JavierSanchezCastro: alejsdev: login: alejsdev count: 37 - avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=447d12a1b347f466b35378bee4c7104cc9b2c571&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=85ceac49fb87138aebe8d663912e359447329090&v=4 url: https://github.com/alejsdev stlucasgarcia: login: stlucasgarcia @@ -436,7 +436,7 @@ jburckel: peidrao: login: peidrao count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/32584628?u=64c634bb10381905038ff7faf3c8c3df47fb799a&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/32584628?u=979c62398e16ff000cc0faa028e028efd679887c&v=4 url: https://github.com/peidrao impocode: login: impocode @@ -1006,7 +1006,7 @@ takacs: anton2yakovlev: login: anton2yakovlev count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/44229180?u=bdd445ba99074b378e7298d23c4bf6d707d2c282&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/44229180?u=ac245e57bc834ff80f08ca8128000bb650a77a3d&v=4 url: https://github.com/anton2yakovlev ILoveSorasakiHina: login: ILoveSorasakiHina @@ -1161,7 +1161,7 @@ cookie-byte217: AbolfazlKameli: login: AbolfazlKameli count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/120686133?u=e41743da3c1820efafc59c5870cacd4f4425334c&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/120686133?u=af8f025278cce0d489007071254e4055df60b78c&v=4 url: https://github.com/AbolfazlKameli tyronedamasceno: login: tyronedamasceno @@ -1196,7 +1196,7 @@ Xaraxx: Suyoung789: login: Suyoung789 count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/31277231?u=744bd3e641413e19bfad6b06a90bb0887c3f9332&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/31277231?u=1591aaf651eb860017231a36590050e154c026b6&v=4 url: https://github.com/Suyoung789 akagaeng: login: akagaeng @@ -1806,7 +1806,7 @@ MrL8199: ivintoiu: login: ivintoiu count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/1853336?u=b537c905ad08b69993de8796fb235c8d4d47f039&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/1853336?u=e3de5fd0ab17efc12256b4295285b504ca281440&v=4 url: https://github.com/ivintoiu TechnoService2: login: TechnoService2 @@ -1841,7 +1841,7 @@ NavesSapnis: eqsdxr: login: eqsdxr count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/157279130?u=d7aaffb29f542b647cf0f6b0e05722490863658a&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/157279130?u=7927dc0366995334f9a18c3204a41d3a34d6d96f&v=4 url: https://github.com/eqsdxr syedasamina56: login: syedasamina56 diff --git a/docs/en/data/translators.yml b/docs/en/data/translators.yml index a4b87e1bf..c66eff4d4 100644 --- a/docs/en/data/translators.yml +++ b/docs/en/data/translators.yml @@ -1,6 +1,6 @@ nilslindemann: login: nilslindemann - count: 124 + count: 125 avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 url: https://github.com/nilslindemann jaystone776: @@ -8,16 +8,16 @@ jaystone776: count: 46 avatarUrl: https://avatars.githubusercontent.com/u/11191137?u=299205a95e9b6817a43144a48b643346a5aac5cc&v=4 url: https://github.com/jaystone776 +ceb10n: + login: ceb10n + count: 29 + avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 + url: https://github.com/ceb10n valentinDruzhinin: login: valentinDruzhinin count: 29 avatarUrl: https://avatars.githubusercontent.com/u/12831905?u=aae1ebc675c91e8fa582df4fcc4fc4128106344d&v=4 url: https://github.com/valentinDruzhinin -ceb10n: - login: ceb10n - count: 27 - avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 - url: https://github.com/ceb10n tokusumi: login: tokusumi count: 23 @@ -286,7 +286,7 @@ hsuanchi: alejsdev: login: alejsdev count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=447d12a1b347f466b35378bee4c7104cc9b2c571&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=85ceac49fb87138aebe8d663912e359447329090&v=4 url: https://github.com/alejsdev riroan: login: riroan @@ -358,6 +358,11 @@ ruzia: count: 3 avatarUrl: https://avatars.githubusercontent.com/u/24503?v=4 url: https://github.com/ruzia +YuriiMotov: + login: YuriiMotov + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4 + url: https://github.com/YuriiMotov izaguerreiro: login: izaguerreiro count: 2 @@ -543,8 +548,3 @@ EdmilsonRodrigues: count: 2 avatarUrl: https://avatars.githubusercontent.com/u/62777025?u=217d6f3cd6cc750bb8818a3af7726c8d74eb7c2d&v=4 url: https://github.com/EdmilsonRodrigues -YuriiMotov: - login: YuriiMotov - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4 - url: https://github.com/YuriiMotov From d661bb1324b84d53512874c34429488db600cbef Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 1 Dec 2025 06:28:06 +0000 Subject: [PATCH 151/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 8848784b1..431e6149e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Internal + +* 👥 Update FastAPI People - Contributors and Translators. PR [#14420](https://github.com/fastapi/fastapi/pull/14420) by [@tiangolo](https://github.com/tiangolo). + ## 0.123.0 ### Fixes From 8a7ad3d255f7318b67bf5dd03f8f231d2ade4b42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 30 Nov 2025 22:30:56 -0800 Subject: [PATCH 152/256] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20Peopl?= =?UTF-8?q?e=20-=20Sponsors=20(#14422)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions --- docs/en/data/github_sponsors.yml | 128 +++++++++++++------------------ 1 file changed, 52 insertions(+), 76 deletions(-) diff --git a/docs/en/data/github_sponsors.yml b/docs/en/data/github_sponsors.yml index 3d8ecdb7a..24780603d 100644 --- a/docs/en/data/github_sponsors.yml +++ b/docs/en/data/github_sponsors.yml @@ -23,9 +23,6 @@ sponsors: - login: railwayapp avatarUrl: https://avatars.githubusercontent.com/u/66716858?v=4 url: https://github.com/railwayapp - - login: scalar - avatarUrl: https://avatars.githubusercontent.com/u/301879?v=4 - url: https://github.com/scalar - - login: dribia avatarUrl: https://avatars.githubusercontent.com/u/41189616?v=4 url: https://github.com/dribia @@ -44,25 +41,25 @@ sponsors: - login: permitio avatarUrl: https://avatars.githubusercontent.com/u/71775833?v=4 url: https://github.com/permitio -- - login: BoostryJP - avatarUrl: https://avatars.githubusercontent.com/u/57932412?v=4 - url: https://github.com/BoostryJP - - login: mercedes-benz - avatarUrl: https://avatars.githubusercontent.com/u/34240465?v=4 - url: https://github.com/mercedes-benz - - login: Ponte-Energy-Partners +- - login: Ponte-Energy-Partners avatarUrl: https://avatars.githubusercontent.com/u/114745848?v=4 url: https://github.com/Ponte-Energy-Partners - login: LambdaTest-Inc avatarUrl: https://avatars.githubusercontent.com/u/171592363?u=96606606a45fa170427206199014f2a5a2a4920b&v=4 url: https://github.com/LambdaTest-Inc + - login: BoostryJP + avatarUrl: https://avatars.githubusercontent.com/u/57932412?v=4 + url: https://github.com/BoostryJP - login: requestly avatarUrl: https://avatars.githubusercontent.com/u/12287519?v=4 url: https://github.com/requestly - login: acsone avatarUrl: https://avatars.githubusercontent.com/u/7601056?v=4 url: https://github.com/acsone -- - login: Trivie +- - login: scalar + avatarUrl: https://avatars.githubusercontent.com/u/301879?v=4 + url: https://github.com/scalar + - login: Trivie avatarUrl: https://avatars.githubusercontent.com/u/8161763?v=4 url: https://github.com/Trivie - - login: takashi-yoneya @@ -71,42 +68,30 @@ sponsors: - login: Doist avatarUrl: https://avatars.githubusercontent.com/u/2565372?v=4 url: https://github.com/Doist + - login: bholagabbar + avatarUrl: https://avatars.githubusercontent.com/u/11693595?v=4 + url: https://github.com/bholagabbar - - login: mainframeindustries avatarUrl: https://avatars.githubusercontent.com/u/55092103?v=4 url: https://github.com/mainframeindustries - - login: alixlahuec avatarUrl: https://avatars.githubusercontent.com/u/29543316?u=44357eb2a93bccf30fb9d389b8befe94a3d00985&v=4 url: https://github.com/alixlahuec - - login: Partho - avatarUrl: https://avatars.githubusercontent.com/u/2034301?u=ce195ac36835cca0cdfe6dd6e897bd38873a1524&v=4 - url: https://github.com/Partho - - login: primer-io avatarUrl: https://avatars.githubusercontent.com/u/62146168?v=4 url: https://github.com/primer-io - - login: xsalagarcia - avatarUrl: https://avatars.githubusercontent.com/u/66035908?v=4 - url: https://github.com/xsalagarcia - - login: upciti avatarUrl: https://avatars.githubusercontent.com/u/43346262?v=4 url: https://github.com/upciti - - login: GonnaFlyMethod - avatarUrl: https://avatars.githubusercontent.com/u/60840539?u=edf70b373fd4f1a83d3eb7c6802f4b6addb572cf&v=4 - url: https://github.com/GonnaFlyMethod - login: ChargeStorm avatarUrl: https://avatars.githubusercontent.com/u/26000165?v=4 url: https://github.com/ChargeStorm - - login: DanielYang59 - avatarUrl: https://avatars.githubusercontent.com/u/80093591?u=63873f701c7c74aac83c906800a1dddc0bc8c92f&v=4 - url: https://github.com/DanielYang59 - login: nilslindemann avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 url: https://github.com/nilslindemann - - login: samuelcolvin avatarUrl: https://avatars.githubusercontent.com/u/4039449?u=42eb3b833047c8c4b4f647a031eaef148c16d93f&v=4 url: https://github.com/samuelcolvin - - login: vincentkoc - avatarUrl: https://avatars.githubusercontent.com/u/25068?u=fbd5b2d51142daa4bdbc21e21953a3b8b8188a4a&v=4 - url: https://github.com/vincentkoc - login: otosky avatarUrl: https://avatars.githubusercontent.com/u/42260747?u=69d089387c743d89427aa4ad8740cfb34045a9e0&v=4 url: https://github.com/otosky @@ -137,9 +122,6 @@ sponsors: - login: jugeeem avatarUrl: https://avatars.githubusercontent.com/u/116043716?u=ae590d79c38ac79c91b9c5caa6887d061e865a3d&v=4 url: https://github.com/jugeeem - - login: connorpark24 - avatarUrl: https://avatars.githubusercontent.com/u/142128990?u=09b84a4beb1f629b77287a837bcf3729785cdd89&v=4 - url: https://github.com/connorpark24 - login: patsatsia avatarUrl: https://avatars.githubusercontent.com/u/61111267?u=3271b85f7a37b479c8d0ae0a235182e83c166edf&v=4 url: https://github.com/patsatsia @@ -155,9 +137,9 @@ sponsors: - login: kaoru0310 avatarUrl: https://avatars.githubusercontent.com/u/80977929?u=1b61d10142b490e56af932ddf08a390fae8ee94f&v=4 url: https://github.com/kaoru0310 - - login: DelfinaCare - avatarUrl: https://avatars.githubusercontent.com/u/83734439?v=4 - url: https://github.com/DelfinaCare + - login: jstanden + avatarUrl: https://avatars.githubusercontent.com/u/63288?u=c3658d57d2862c607a0e19c2101c3c51876e36ad&v=4 + url: https://github.com/jstanden - login: knallgelb avatarUrl: https://avatars.githubusercontent.com/u/2358812?u=c48cb6362b309d74cbf144bd6ad3aed3eb443e82&v=4 url: https://github.com/knallgelb @@ -191,9 +173,6 @@ sponsors: - login: oliverxchen avatarUrl: https://avatars.githubusercontent.com/u/4471774?u=534191f25e32eeaadda22dfab4b0a428733d5489&v=4 url: https://github.com/oliverxchen - - login: jstanden - avatarUrl: https://avatars.githubusercontent.com/u/63288?u=c3658d57d2862c607a0e19c2101c3c51876e36ad&v=4 - url: https://github.com/jstanden - login: paulcwatts avatarUrl: https://avatars.githubusercontent.com/u/150269?u=1819e145d573b44f0ad74b87206d21cd60331d4e&v=4 url: https://github.com/paulcwatts @@ -233,9 +212,6 @@ sponsors: - login: mjohnsey avatarUrl: https://avatars.githubusercontent.com/u/16784016?u=38fad2e6b411244560b3af99c5f5a4751bc81865&v=4 url: https://github.com/mjohnsey - - login: enguy-hub - avatarUrl: https://avatars.githubusercontent.com/u/16822912?u=2c45f9e7f427b2f2f3b023d7fdb0d44764c92ae8&v=4 - url: https://github.com/enguy-hub - login: ashi-agrawal avatarUrl: https://avatars.githubusercontent.com/u/17105294?u=99c7a854035e5398d8e7b674f2d42baae6c957f8&v=4 url: https://github.com/ashi-agrawal @@ -260,10 +236,7 @@ sponsors: - - login: manoelpqueiroz avatarUrl: https://avatars.githubusercontent.com/u/23669137?u=b12e84b28a84369ab5b30bd5a79e5788df5a0756&v=4 url: https://github.com/manoelpqueiroz -- - login: ceb10n - avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 - url: https://github.com/ceb10n - - login: pawamoy +- - login: pawamoy avatarUrl: https://avatars.githubusercontent.com/u/3999221?u=b030e4c89df2f3a36bc4710b925bdeb6745c9856&v=4 url: https://github.com/pawamoy - login: siavashyj @@ -281,9 +254,9 @@ sponsors: - login: hgalytoby avatarUrl: https://avatars.githubusercontent.com/u/50397689?u=6cc9028f3db63f8f60ad21c17b1ce4b88c4e2e60&v=4 url: https://github.com/hgalytoby - - login: johnl28 - avatarUrl: https://avatars.githubusercontent.com/u/54412955?u=47dd06082d1c39caa90c752eb55566e4f3813957&v=4 - url: https://github.com/johnl28 + - login: nisutec + avatarUrl: https://avatars.githubusercontent.com/u/25281462?u=e562484c451fdfc59053163f64405f8eb262b8b0&v=4 + url: https://github.com/nisutec - login: hoenie-ams avatarUrl: https://avatars.githubusercontent.com/u/25708487?u=cda07434f0509ac728d9edf5e681117c0f6b818b&v=4 url: https://github.com/hoenie-ams @@ -299,21 +272,24 @@ sponsors: - login: petercool avatarUrl: https://avatars.githubusercontent.com/u/37613029?u=75aa8c6729e6e8f85a300561c4dbeef9d65c8797&v=4 url: https://github.com/petercool + - login: johnl28 + avatarUrl: https://avatars.githubusercontent.com/u/54412955?u=47dd06082d1c39caa90c752eb55566e4f3813957&v=4 + url: https://github.com/johnl28 - login: PunRabbit avatarUrl: https://avatars.githubusercontent.com/u/70463212?u=1a835cfbc99295a60c8282f6aa6199d1b42241a5&v=4 url: https://github.com/PunRabbit - login: PelicanQ avatarUrl: https://avatars.githubusercontent.com/u/77930606?v=4 url: https://github.com/PelicanQ + - login: WillHogan + avatarUrl: https://avatars.githubusercontent.com/u/1661551?u=8a80356e3e7d5a417157aba7ea565dabc8678327&v=4 + url: https://github.com/WillHogan - login: my3 avatarUrl: https://avatars.githubusercontent.com/u/1825270?v=4 url: https://github.com/my3 - login: danielunderwood avatarUrl: https://avatars.githubusercontent.com/u/4472301?v=4 url: https://github.com/danielunderwood - - login: rangulvers - avatarUrl: https://avatars.githubusercontent.com/u/5235430?u=e254d4af4ace5a05fa58372ae677c7d26f0d5a53&v=4 - url: https://github.com/rangulvers - login: ddanier avatarUrl: https://avatars.githubusercontent.com/u/113563?u=ed1dc79de72f93bd78581f88ebc6952b62f472da&v=4 url: https://github.com/ddanier @@ -323,15 +299,21 @@ sponsors: - login: slafs avatarUrl: https://avatars.githubusercontent.com/u/210173?v=4 url: https://github.com/slafs + - login: ceb10n + avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 + url: https://github.com/ceb10n - login: tochikuji avatarUrl: https://avatars.githubusercontent.com/u/851759?v=4 url: https://github.com/tochikuji - login: miguelgr avatarUrl: https://avatars.githubusercontent.com/u/1484589?u=54556072b8136efa12ae3b6902032ea2a39ace4b&v=4 url: https://github.com/miguelgr - - login: WillHogan - avatarUrl: https://avatars.githubusercontent.com/u/1661551?u=8a80356e3e7d5a417157aba7ea565dabc8678327&v=4 - url: https://github.com/WillHogan + - login: xncbf + avatarUrl: https://avatars.githubusercontent.com/u/9462045?u=a80a7bb349555b277645632ed66639ff43400614&v=4 + url: https://github.com/xncbf + - login: DMantis + avatarUrl: https://avatars.githubusercontent.com/u/9536869?u=652dd0d49717803c0cbcbf44f7740e53cf2d4892&v=4 + url: https://github.com/DMantis - login: hard-coders avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 url: https://github.com/hard-coders @@ -347,9 +329,9 @@ sponsors: - login: joshuatz avatarUrl: https://avatars.githubusercontent.com/u/17817563?u=f1bf05b690d1fc164218f0b420cdd3acb7913e21&v=4 url: https://github.com/joshuatz - - login: nisutec - avatarUrl: https://avatars.githubusercontent.com/u/25281462?u=e562484c451fdfc59053163f64405f8eb262b8b0&v=4 - url: https://github.com/nisutec + - login: rangulvers + avatarUrl: https://avatars.githubusercontent.com/u/5235430?u=e254d4af4ace5a05fa58372ae677c7d26f0d5a53&v=4 + url: https://github.com/rangulvers - login: sdevkota avatarUrl: https://avatars.githubusercontent.com/u/5250987?u=4ed9a120c89805a8aefda1cbdc0cf6512e64d1b4&v=4 url: https://github.com/sdevkota @@ -368,19 +350,7 @@ sponsors: - login: moonape1226 avatarUrl: https://avatars.githubusercontent.com/u/8532038?u=d9f8b855a429fff9397c3833c2ff83849ebf989d&v=4 url: https://github.com/moonape1226 - - login: xncbf - avatarUrl: https://avatars.githubusercontent.com/u/9462045?u=a80a7bb349555b277645632ed66639ff43400614&v=4 - url: https://github.com/xncbf - - login: DMantis - avatarUrl: https://avatars.githubusercontent.com/u/9536869?u=652dd0d49717803c0cbcbf44f7740e53cf2d4892&v=4 - url: https://github.com/DMantis -- - login: morzan1001 - avatarUrl: https://avatars.githubusercontent.com/u/47593005?u=c30ab7230f82a12a9b938dcb54f84a996931409a&v=4 - url: https://github.com/morzan1001 - - login: larsyngvelundin - avatarUrl: https://avatars.githubusercontent.com/u/34173819?u=74958599695bf83ac9f1addd935a51548a10c6b0&v=4 - url: https://github.com/larsyngvelundin - - login: andrecorumba +- - login: andrecorumba avatarUrl: https://avatars.githubusercontent.com/u/37807517?u=9b9be3b41da9bda60957da9ef37b50dbf65baa61&v=4 url: https://github.com/andrecorumba - login: KOZ39 @@ -389,21 +359,30 @@ sponsors: - login: rwxd avatarUrl: https://avatars.githubusercontent.com/u/40308458?u=cd04a39e3655923be4f25c2ba8a5a07b3da3230a&v=4 url: https://github.com/rwxd + - login: morzan1001 + avatarUrl: https://avatars.githubusercontent.com/u/47593005?u=c30ab7230f82a12a9b938dcb54f84a996931409a&v=4 + url: https://github.com/morzan1001 + - login: Olegt0rr + avatarUrl: https://avatars.githubusercontent.com/u/25399456?u=3e87b5239a2f4600975ba13be73054f8567c6060&v=4 + url: https://github.com/Olegt0rr + - login: dinoz0rg + avatarUrl: https://avatars.githubusercontent.com/u/32940067?u=739cda1eb123a2dd5e1db45c361396f239e23f8b&v=4 + url: https://github.com/dinoz0rg + - login: larsyngvelundin + avatarUrl: https://avatars.githubusercontent.com/u/34173819?u=74958599695bf83ac9f1addd935a51548a10c6b0&v=4 + url: https://github.com/larsyngvelundin - login: hippoley avatarUrl: https://avatars.githubusercontent.com/u/135493401?u=1164ef48a645a7c12664fabc1638fbb7e1c459b0&v=4 url: https://github.com/hippoley + - login: 4anklee + avatarUrl: https://avatars.githubusercontent.com/u/144109238?u=a79c0d581b2a3d8f3897e7ef4c012640a6c1eb3a&v=4 + url: https://github.com/4anklee - login: CoderDeltaLAN avatarUrl: https://avatars.githubusercontent.com/u/152043745?u=4ff541efffb7d134e60c5fcf2dd1e343f90bb782&v=4 url: https://github.com/CoderDeltaLAN - - login: chris1ding1 - avatarUrl: https://avatars.githubusercontent.com/u/194386334?u=5500604b50e35ed8a5aeb82ce34aa5d3ee3f88c7&v=4 - url: https://github.com/chris1ding1 - login: onestn avatarUrl: https://avatars.githubusercontent.com/u/62360849?u=746dd21c34e7e06eefb11b03e8bb01aaae3c2a4f&v=4 url: https://github.com/onestn - - login: Rubinskiy - avatarUrl: https://avatars.githubusercontent.com/u/62457878?u=f2e35ed3d196a99cfadb5a29a91950342af07e34&v=4 - url: https://github.com/Rubinskiy - login: nayasinghania avatarUrl: https://avatars.githubusercontent.com/u/74111380?u=752e99a5e139389fdc0a0677122adc08438eb076&v=4 url: https://github.com/nayasinghania @@ -413,9 +392,6 @@ sponsors: - login: andreagrandi avatarUrl: https://avatars.githubusercontent.com/u/636391?u=13d90cb8ec313593a5b71fbd4e33b78d6da736f5&v=4 url: https://github.com/andreagrandi - - login: Olegt0rr - avatarUrl: https://avatars.githubusercontent.com/u/25399456?u=3e87b5239a2f4600975ba13be73054f8567c6060&v=4 - url: https://github.com/Olegt0rr - login: msserpa avatarUrl: https://avatars.githubusercontent.com/u/6334934?u=82c4489eb1559d88d2990d60001901b14f722bbb&v=4 url: https://github.com/msserpa From f8e46d98a07fd8102598659f0b7151384b4c69cf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 1 Dec 2025 06:31:20 +0000 Subject: [PATCH 153/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 431e6149e..1d348cb65 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* 👥 Update FastAPI People - Sponsors. PR [#14422](https://github.com/fastapi/fastapi/pull/14422) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People - Contributors and Translators. PR [#14420](https://github.com/fastapi/fastapi/pull/14420) by [@tiangolo](https://github.com/tiangolo). ## 0.123.0 From 6400d8a6239dd8fbb9e0b27a068281d26ce0e929 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Mon, 1 Dec 2025 07:32:32 +0100 Subject: [PATCH 154/256] =?UTF-8?q?=E2=AC=86=20Bump=20markdown-include-var?= =?UTF-8?q?iants=20from=200.0.6=20to=200.0.7=20(#14423)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index ae1ddbc3d..4f1863a4a 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -17,5 +17,5 @@ griffe-warnings-deprecated==1.1.0 # For griffe, it formats with black black==25.1.0 mkdocs-macros-plugin==1.4.1 -markdown-include-variants==0.0.6 +markdown-include-variants==0.0.7 python-slugify==8.0.4 From 938f4710793e89f4ba31b8867d35185dd1771a20 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 1 Dec 2025 06:33:00 +0000 Subject: [PATCH 155/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 1d348cb65..93a4c5c97 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* ⬆ Bump markdown-include-variants from 0.0.6 to 0.0.7. PR [#14423](https://github.com/fastapi/fastapi/pull/14423) by [@YuriiMotov](https://github.com/YuriiMotov). * 👥 Update FastAPI People - Sponsors. PR [#14422](https://github.com/fastapi/fastapi/pull/14422) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People - Contributors and Translators. PR [#14420](https://github.com/fastapi/fastapi/pull/14420) by [@tiangolo](https://github.com/tiangolo). From 0dee714026a01df50f24b7f69b8978bdb91c8ff0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 1 Dec 2025 05:17:29 -0800 Subject: [PATCH 156/256] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20GitHu?= =?UTF-8?q?b=20topic=20repositories=20(#14426)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions --- docs/en/data/topic_repos.yml | 492 +++++++++++++++++------------------ 1 file changed, 246 insertions(+), 246 deletions(-) diff --git a/docs/en/data/topic_repos.yml b/docs/en/data/topic_repos.yml index 1bb6fd70d..cb7e3c033 100644 --- a/docs/en/data/topic_repos.yml +++ b/docs/en/data/topic_repos.yml @@ -1,368 +1,388 @@ - name: full-stack-fastapi-template html_url: https://github.com/fastapi/full-stack-fastapi-template - stars: 38779 + stars: 39475 owner_login: fastapi owner_html_url: https://github.com/fastapi - name: Hello-Python html_url: https://github.com/mouredev/Hello-Python - stars: 32726 + stars: 33090 owner_login: mouredev owner_html_url: https://github.com/mouredev - name: serve html_url: https://github.com/jina-ai/serve - stars: 21779 + stars: 21798 owner_login: jina-ai owner_html_url: https://github.com/jina-ai - name: HivisionIDPhotos html_url: https://github.com/Zeyi-Lin/HivisionIDPhotos - stars: 20028 + stars: 20258 owner_login: Zeyi-Lin owner_html_url: https://github.com/Zeyi-Lin - name: sqlmodel html_url: https://github.com/fastapi/sqlmodel - stars: 17038 + stars: 17212 owner_login: fastapi owner_html_url: https://github.com/fastapi - name: Douyin_TikTok_Download_API html_url: https://github.com/Evil0ctal/Douyin_TikTok_Download_API - stars: 14786 + stars: 15145 owner_login: Evil0ctal owner_html_url: https://github.com/Evil0ctal - name: fastapi-best-practices html_url: https://github.com/zhanymkanov/fastapi-best-practices - stars: 13968 + stars: 14644 owner_login: zhanymkanov owner_html_url: https://github.com/zhanymkanov - name: machine-learning-zoomcamp html_url: https://github.com/DataTalksClub/machine-learning-zoomcamp - stars: 12171 + stars: 12320 owner_login: DataTalksClub owner_html_url: https://github.com/DataTalksClub - name: fastapi_mcp html_url: https://github.com/tadata-org/fastapi_mcp - stars: 10976 + stars: 11174 owner_login: tadata-org owner_html_url: https://github.com/tadata-org -- name: awesome-fastapi - html_url: https://github.com/mjhea0/awesome-fastapi - stars: 10618 - owner_login: mjhea0 - owner_html_url: https://github.com/mjhea0 - name: SurfSense html_url: https://github.com/MODSetter/SurfSense - stars: 10243 + stars: 10858 owner_login: MODSetter owner_html_url: https://github.com/MODSetter +- name: awesome-fastapi + html_url: https://github.com/mjhea0/awesome-fastapi + stars: 10758 + owner_login: mjhea0 + owner_html_url: https://github.com/mjhea0 - name: XHS-Downloader html_url: https://github.com/JoeanAmier/XHS-Downloader - stars: 9062 + stars: 9313 owner_login: JoeanAmier owner_html_url: https://github.com/JoeanAmier - name: FastUI html_url: https://github.com/pydantic/FastUI - stars: 8892 + stars: 8915 owner_login: pydantic owner_html_url: https://github.com/pydantic - name: polar html_url: https://github.com/polarsource/polar - stars: 8084 + stars: 8339 owner_login: polarsource owner_html_url: https://github.com/polarsource - name: FileCodeBox html_url: https://github.com/vastsa/FileCodeBox - stars: 7494 + stars: 7721 owner_login: vastsa owner_html_url: https://github.com/vastsa - name: nonebot2 html_url: https://github.com/nonebot/nonebot2 - stars: 7128 + stars: 7170 owner_login: nonebot owner_html_url: https://github.com/nonebot - name: hatchet html_url: https://github.com/hatchet-dev/hatchet - stars: 6155 + stars: 6253 owner_login: hatchet-dev owner_html_url: https://github.com/hatchet-dev -- name: serge - html_url: https://github.com/serge-chat/serge - stars: 5754 - owner_login: serge-chat - owner_html_url: https://github.com/serge-chat - name: fastapi-users html_url: https://github.com/fastapi-users/fastapi-users - stars: 5683 + stars: 5849 owner_login: fastapi-users owner_html_url: https://github.com/fastapi-users +- name: serge + html_url: https://github.com/serge-chat/serge + stars: 5756 + owner_login: serge-chat + owner_html_url: https://github.com/serge-chat - name: strawberry html_url: https://github.com/strawberry-graphql/strawberry - stars: 4452 + stars: 4569 owner_login: strawberry-graphql owner_html_url: https://github.com/strawberry-graphql - name: chatgpt-web-share html_url: https://github.com/chatpire/chatgpt-web-share - stars: 4296 + stars: 4294 owner_login: chatpire owner_html_url: https://github.com/chatpire - name: poem html_url: https://github.com/poem-web/poem - stars: 4235 + stars: 4276 owner_login: poem-web owner_html_url: https://github.com/poem-web - name: dynaconf html_url: https://github.com/dynaconf/dynaconf - stars: 4174 + stars: 4202 owner_login: dynaconf owner_html_url: https://github.com/dynaconf - name: atrilabs-engine html_url: https://github.com/Atri-Labs/atrilabs-engine - stars: 4094 + stars: 4093 owner_login: Atri-Labs owner_html_url: https://github.com/Atri-Labs - name: Kokoro-FastAPI html_url: https://github.com/remsky/Kokoro-FastAPI - stars: 3875 + stars: 4019 owner_login: remsky owner_html_url: https://github.com/remsky - name: logfire html_url: https://github.com/pydantic/logfire - stars: 3717 + stars: 3805 owner_login: pydantic owner_html_url: https://github.com/pydantic - name: LitServe html_url: https://github.com/Lightning-AI/LitServe - stars: 3615 + stars: 3719 owner_login: Lightning-AI owner_html_url: https://github.com/Lightning-AI +- name: fastapi-admin + html_url: https://github.com/fastapi-admin/fastapi-admin + stars: 3632 + owner_login: fastapi-admin + owner_html_url: https://github.com/fastapi-admin - name: datamodel-code-generator html_url: https://github.com/koxudaxi/datamodel-code-generator - stars: 3554 + stars: 3609 owner_login: koxudaxi owner_html_url: https://github.com/koxudaxi - name: huma html_url: https://github.com/danielgtaylor/huma - stars: 3521 + stars: 3603 owner_login: danielgtaylor owner_html_url: https://github.com/danielgtaylor -- name: fastapi-admin - html_url: https://github.com/fastapi-admin/fastapi-admin - stars: 3497 - owner_login: fastapi-admin - owner_html_url: https://github.com/fastapi-admin - name: farfalle html_url: https://github.com/rashadphz/farfalle - stars: 3476 + stars: 3490 owner_login: rashadphz owner_html_url: https://github.com/rashadphz - name: tracecat html_url: https://github.com/TracecatHQ/tracecat - stars: 3310 + stars: 3379 owner_login: TracecatHQ owner_html_url: https://github.com/TracecatHQ - name: opyrator html_url: https://github.com/ml-tooling/opyrator - stars: 3134 + stars: 3135 owner_login: ml-tooling owner_html_url: https://github.com/ml-tooling - name: docarray html_url: https://github.com/docarray/docarray - stars: 3108 + stars: 3114 owner_login: docarray owner_html_url: https://github.com/docarray +- name: devpush + html_url: https://github.com/hunvreus/devpush + stars: 3097 + owner_login: hunvreus + owner_html_url: https://github.com/hunvreus - name: fastapi-realworld-example-app html_url: https://github.com/nsidnev/fastapi-realworld-example-app - stars: 2945 + stars: 3050 owner_login: nsidnev owner_html_url: https://github.com/nsidnev - name: uvicorn-gunicorn-fastapi-docker html_url: https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker - stars: 2809 + stars: 2911 owner_login: tiangolo owner_html_url: https://github.com/tiangolo -- name: devpush - html_url: https://github.com/hunvreus/devpush - stars: 2784 - owner_login: hunvreus - owner_html_url: https://github.com/hunvreus - name: mcp-context-forge html_url: https://github.com/IBM/mcp-context-forge - stars: 2763 + stars: 2899 owner_login: IBM owner_html_url: https://github.com/IBM - name: best-of-web-python html_url: https://github.com/ml-tooling/best-of-web-python - stars: 2630 + stars: 2648 owner_login: ml-tooling owner_html_url: https://github.com/ml-tooling -- name: fastapi-react - html_url: https://github.com/Buuntu/fastapi-react - stars: 2464 - owner_login: Buuntu - owner_html_url: https://github.com/Buuntu - name: FastAPI-template html_url: https://github.com/s3rius/FastAPI-template - stars: 2453 + stars: 2637 owner_login: s3rius owner_html_url: https://github.com/s3rius -- name: RasaGPT - html_url: https://github.com/paulpierre/RasaGPT - stars: 2444 - owner_login: paulpierre - owner_html_url: https://github.com/paulpierre -- name: sqladmin - html_url: https://github.com/aminalaee/sqladmin - stars: 2423 - owner_login: aminalaee - owner_html_url: https://github.com/aminalaee -- name: nextpy - html_url: https://github.com/dot-agent/nextpy - stars: 2325 - owner_login: dot-agent - owner_html_url: https://github.com/dot-agent -- name: supabase-py - html_url: https://github.com/supabase/supabase-py - stars: 2292 - owner_login: supabase - owner_html_url: https://github.com/supabase -- name: 30-Days-of-Python - html_url: https://github.com/codingforentrepreneurs/30-Days-of-Python - stars: 2214 - owner_login: codingforentrepreneurs - owner_html_url: https://github.com/codingforentrepreneurs +- name: YC-Killer + html_url: https://github.com/sahibzada-allahyar/YC-Killer + stars: 2599 + owner_login: sahibzada-allahyar + owner_html_url: https://github.com/sahibzada-allahyar +- name: fastapi-react + html_url: https://github.com/Buuntu/fastapi-react + stars: 2569 + owner_login: Buuntu + owner_html_url: https://github.com/Buuntu - name: Yuxi-Know html_url: https://github.com/xerrors/Yuxi-Know - stars: 2212 + stars: 2563 owner_login: xerrors owner_html_url: https://github.com/xerrors -- name: langserve - html_url: https://github.com/langchain-ai/langserve - stars: 2191 - owner_login: langchain-ai - owner_html_url: https://github.com/langchain-ai +- name: sqladmin + html_url: https://github.com/aminalaee/sqladmin + stars: 2558 + owner_login: aminalaee + owner_html_url: https://github.com/aminalaee +- name: RasaGPT + html_url: https://github.com/paulpierre/RasaGPT + stars: 2451 + owner_login: paulpierre + owner_html_url: https://github.com/paulpierre +- name: supabase-py + html_url: https://github.com/supabase/supabase-py + stars: 2344 + owner_login: supabase + owner_html_url: https://github.com/supabase +- name: nextpy + html_url: https://github.com/dot-agent/nextpy + stars: 2335 + owner_login: dot-agent + owner_html_url: https://github.com/dot-agent - name: fastapi-utils html_url: https://github.com/fastapiutils/fastapi-utils - stars: 2185 + stars: 2291 owner_login: fastapiutils owner_html_url: https://github.com/fastapiutils +- name: 30-Days-of-Python + html_url: https://github.com/codingforentrepreneurs/30-Days-of-Python + stars: 2220 + owner_login: codingforentrepreneurs + owner_html_url: https://github.com/codingforentrepreneurs +- name: langserve + html_url: https://github.com/langchain-ai/langserve + stars: 2215 + owner_login: langchain-ai + owner_html_url: https://github.com/langchain-ai - name: solara html_url: https://github.com/widgetti/solara - stars: 2111 + stars: 2122 owner_login: widgetti owner_html_url: https://github.com/widgetti - name: mangum html_url: https://github.com/Kludex/mangum - stars: 2011 + stars: 2029 owner_login: Kludex owner_html_url: https://github.com/Kludex - name: agentkit html_url: https://github.com/BCG-X-Official/agentkit - stars: 1826 + stars: 1912 owner_login: BCG-X-Official owner_html_url: https://github.com/BCG-X-Official -- name: python-week-2022 - html_url: https://github.com/rochacbruno/python-week-2022 - stars: 1815 - owner_login: rochacbruno - owner_html_url: https://github.com/rochacbruno - name: manage-fastapi html_url: https://github.com/ycd/manage-fastapi - stars: 1787 + stars: 1885 owner_login: ycd owner_html_url: https://github.com/ycd -- name: ormar - html_url: https://github.com/collerek/ormar - stars: 1780 - owner_login: collerek - owner_html_url: https://github.com/collerek -- name: vue-fastapi-admin - html_url: https://github.com/mizhexiaoxiao/vue-fastapi-admin - stars: 1758 - owner_login: mizhexiaoxiao - owner_html_url: https://github.com/mizhexiaoxiao - name: openapi-python-client html_url: https://github.com/openapi-generators/openapi-python-client - stars: 1731 + stars: 1862 owner_login: openapi-generators owner_html_url: https://github.com/openapi-generators - name: piccolo html_url: https://github.com/piccolo-orm/piccolo - stars: 1711 + stars: 1836 owner_login: piccolo-orm owner_html_url: https://github.com/piccolo-orm -- name: fastapi-cache - html_url: https://github.com/long2ice/fastapi-cache - stars: 1677 - owner_login: long2ice - owner_html_url: https://github.com/long2ice +- name: vue-fastapi-admin + html_url: https://github.com/mizhexiaoxiao/vue-fastapi-admin + stars: 1831 + owner_login: mizhexiaoxiao + owner_html_url: https://github.com/mizhexiaoxiao +- name: python-week-2022 + html_url: https://github.com/rochacbruno/python-week-2022 + stars: 1817 + owner_login: rochacbruno + owner_html_url: https://github.com/rochacbruno - name: slowapi html_url: https://github.com/laurentS/slowapi - stars: 1669 + stars: 1798 owner_login: laurentS owner_html_url: https://github.com/laurentS -- name: langchain-serve - html_url: https://github.com/jina-ai/langchain-serve - stars: 1632 - owner_login: jina-ai - owner_html_url: https://github.com/jina-ai +- name: fastapi-cache + html_url: https://github.com/long2ice/fastapi-cache + stars: 1789 + owner_login: long2ice + owner_html_url: https://github.com/long2ice +- name: ormar + html_url: https://github.com/collerek/ormar + stars: 1783 + owner_login: collerek + owner_html_url: https://github.com/collerek - name: termpair html_url: https://github.com/cs01/termpair - stars: 1621 + stars: 1716 owner_login: cs01 owner_html_url: https://github.com/cs01 - name: FastAPI-boilerplate html_url: https://github.com/benavlabs/FastAPI-boilerplate - stars: 1596 + stars: 1660 owner_login: benavlabs owner_html_url: https://github.com/benavlabs -- name: coronavirus-tracker-api - html_url: https://github.com/ExpDev07/coronavirus-tracker-api - stars: 1573 - owner_login: ExpDev07 - owner_html_url: https://github.com/ExpDev07 -- name: fastapi-crudrouter - html_url: https://github.com/awtkns/fastapi-crudrouter - stars: 1553 - owner_login: awtkns - owner_html_url: https://github.com/awtkns +- name: fastapi-langgraph-agent-production-ready-template + html_url: https://github.com/wassim249/fastapi-langgraph-agent-production-ready-template + stars: 1638 + owner_login: wassim249 + owner_html_url: https://github.com/wassim249 +- name: langchain-serve + html_url: https://github.com/jina-ai/langchain-serve + stars: 1635 + owner_login: jina-ai + owner_html_url: https://github.com/jina-ai - name: awesome-fastapi-projects html_url: https://github.com/Kludex/awesome-fastapi-projects - stars: 1485 + stars: 1589 owner_login: Kludex owner_html_url: https://github.com/Kludex - name: fastapi-pagination html_url: https://github.com/uriyyo/fastapi-pagination - stars: 1473 + stars: 1585 owner_login: uriyyo owner_html_url: https://github.com/uriyyo +- name: coronavirus-tracker-api + html_url: https://github.com/ExpDev07/coronavirus-tracker-api + stars: 1574 + owner_login: ExpDev07 + owner_html_url: https://github.com/ExpDev07 +- name: fastapi-crudrouter + html_url: https://github.com/awtkns/fastapi-crudrouter + stars: 1559 + owner_login: awtkns + owner_html_url: https://github.com/awtkns - name: bracket html_url: https://github.com/evroon/bracket - stars: 1470 + stars: 1489 owner_login: evroon owner_html_url: https://github.com/evroon -- name: fastapi-langgraph-agent-production-ready-template - html_url: https://github.com/wassim249/fastapi-langgraph-agent-production-ready-template - stars: 1456 - owner_login: wassim249 - owner_html_url: https://github.com/wassim249 +- name: fastapi-amis-admin + html_url: https://github.com/amisadmin/fastapi-amis-admin + stars: 1475 + owner_login: amisadmin + owner_html_url: https://github.com/amisadmin - name: fastapi-boilerplate html_url: https://github.com/teamhide/fastapi-boilerplate - stars: 1424 + stars: 1436 owner_login: teamhide owner_html_url: https://github.com/teamhide - name: awesome-python-resources html_url: https://github.com/DjangoEx/awesome-python-resources - stars: 1420 + stars: 1426 owner_login: DjangoEx owner_html_url: https://github.com/DjangoEx -- name: fastapi-amis-admin - html_url: https://github.com/amisadmin/fastapi-amis-admin - stars: 1363 - owner_login: amisadmin - owner_html_url: https://github.com/amisadmin - name: fastcrud html_url: https://github.com/benavlabs/fastcrud - stars: 1362 + stars: 1414 owner_login: benavlabs owner_html_url: https://github.com/benavlabs +- name: prometheus-fastapi-instrumentator + html_url: https://github.com/trallnag/prometheus-fastapi-instrumentator + stars: 1388 + owner_login: trallnag + owner_html_url: https://github.com/trallnag +- name: fastapi_best_architecture + html_url: https://github.com/fastapi-practices/fastapi_best_architecture + stars: 1378 + owner_login: fastapi-practices + owner_html_url: https://github.com/fastapi-practices +- name: fastapi-code-generator + html_url: https://github.com/koxudaxi/fastapi-code-generator + stars: 1375 + owner_login: koxudaxi + owner_html_url: https://github.com/koxudaxi - name: budgetml html_url: https://github.com/ebhy/budgetml stars: 1345 @@ -370,126 +390,106 @@ owner_html_url: https://github.com/ebhy - name: fastapi-tutorial html_url: https://github.com/liaogx/fastapi-tutorial - stars: 1315 + stars: 1327 owner_login: liaogx owner_html_url: https://github.com/liaogx -- name: fastapi_best_architecture - html_url: https://github.com/fastapi-practices/fastapi_best_architecture - stars: 1311 - owner_login: fastapi-practices - owner_html_url: https://github.com/fastapi-practices -- name: fastapi-code-generator - html_url: https://github.com/koxudaxi/fastapi-code-generator - stars: 1270 - owner_login: koxudaxi - owner_html_url: https://github.com/koxudaxi -- name: prometheus-fastapi-instrumentator - html_url: https://github.com/trallnag/prometheus-fastapi-instrumentator - stars: 1264 - owner_login: trallnag - owner_html_url: https://github.com/trallnag +- name: fastapi-alembic-sqlmodel-async + html_url: https://github.com/jonra1993/fastapi-alembic-sqlmodel-async + stars: 1259 + owner_login: jonra1993 + owner_html_url: https://github.com/jonra1993 +- name: fastapi-scaff + html_url: https://github.com/atpuxiner/fastapi-scaff + stars: 1255 + owner_login: atpuxiner + owner_html_url: https://github.com/atpuxiner - name: bedrock-chat html_url: https://github.com/aws-samples/bedrock-chat - stars: 1243 + stars: 1254 owner_login: aws-samples owner_html_url: https://github.com/aws-samples - name: bolt-python html_url: https://github.com/slackapi/bolt-python - stars: 1238 + stars: 1253 owner_login: slackapi owner_html_url: https://github.com/slackapi - name: fastapi_production_template html_url: https://github.com/zhanymkanov/fastapi_production_template - stars: 1209 + stars: 1217 owner_login: zhanymkanov owner_html_url: https://github.com/zhanymkanov -- name: fastapi-scaff - html_url: https://github.com/atpuxiner/fastapi-scaff - stars: 1200 - owner_login: atpuxiner - owner_html_url: https://github.com/atpuxiner - name: langchain-extract html_url: https://github.com/langchain-ai/langchain-extract - stars: 1173 + stars: 1176 owner_login: langchain-ai owner_html_url: https://github.com/langchain-ai -- name: fastapi-alembic-sqlmodel-async - html_url: https://github.com/jonra1993/fastapi-alembic-sqlmodel-async - stars: 1162 - owner_login: jonra1993 - owner_html_url: https://github.com/jonra1993 -- name: odmantic - html_url: https://github.com/art049/odmantic - stars: 1137 - owner_login: art049 - owner_html_url: https://github.com/art049 - name: restish html_url: https://github.com/rest-sh/restish - stars: 1129 + stars: 1140 owner_login: rest-sh owner_html_url: https://github.com/rest-sh -- name: kubetorch - html_url: https://github.com/run-house/kubetorch - stars: 1065 - owner_login: run-house - owner_html_url: https://github.com/run-house -- name: flock - html_url: https://github.com/Onelevenvy/flock - stars: 1039 - owner_login: Onelevenvy - owner_html_url: https://github.com/Onelevenvy +- name: odmantic + html_url: https://github.com/art049/odmantic + stars: 1138 + owner_login: art049 + owner_html_url: https://github.com/art049 - name: authx html_url: https://github.com/yezz123/authx - stars: 1017 + stars: 1119 owner_login: yezz123 owner_html_url: https://github.com/yezz123 -- name: autollm - html_url: https://github.com/viddexa/autollm - stars: 997 - owner_login: viddexa - owner_html_url: https://github.com/viddexa -- name: lanarky - html_url: https://github.com/ajndkr/lanarky - stars: 993 - owner_login: ajndkr - owner_html_url: https://github.com/ajndkr -- name: RuoYi-Vue3-FastAPI - html_url: https://github.com/insistence/RuoYi-Vue3-FastAPI - stars: 974 - owner_login: insistence - owner_html_url: https://github.com/insistence -- name: aktools - html_url: https://github.com/akfamily/aktools - stars: 972 - owner_login: akfamily - owner_html_url: https://github.com/akfamily -- name: titiler - html_url: https://github.com/developmentseed/titiler - stars: 965 - owner_login: developmentseed - owner_html_url: https://github.com/developmentseed -- name: secure - html_url: https://github.com/TypeError/secure - stars: 953 - owner_login: TypeError - owner_html_url: https://github.com/TypeError -- name: energy-forecasting - html_url: https://github.com/iusztinpaul/energy-forecasting - stars: 949 - owner_login: iusztinpaul - owner_html_url: https://github.com/iusztinpaul -- name: every-pdf - html_url: https://github.com/DDULDDUCK/every-pdf - stars: 942 - owner_login: DDULDDUCK - owner_html_url: https://github.com/DDULDDUCK -- name: langcorn - html_url: https://github.com/msoedov/langcorn - stars: 933 - owner_login: msoedov - owner_html_url: https://github.com/msoedov +- name: NoteDiscovery + html_url: https://github.com/gamosoft/NoteDiscovery + stars: 1107 + owner_login: gamosoft + owner_html_url: https://github.com/gamosoft +- name: flock + html_url: https://github.com/Onelevenvy/flock + stars: 1055 + owner_login: Onelevenvy + owner_html_url: https://github.com/Onelevenvy - name: fastapi-observability html_url: https://github.com/blueswen/fastapi-observability - stars: 923 + stars: 1038 owner_login: blueswen owner_html_url: https://github.com/blueswen +- name: aktools + html_url: https://github.com/akfamily/aktools + stars: 1027 + owner_login: akfamily + owner_html_url: https://github.com/akfamily +- name: RuoYi-Vue3-FastAPI + html_url: https://github.com/insistence/RuoYi-Vue3-FastAPI + stars: 1016 + owner_login: insistence + owner_html_url: https://github.com/insistence +- name: autollm + html_url: https://github.com/viddexa/autollm + stars: 1002 + owner_login: viddexa + owner_html_url: https://github.com/viddexa +- name: titiler + html_url: https://github.com/developmentseed/titiler + stars: 999 + owner_login: developmentseed + owner_html_url: https://github.com/developmentseed +- name: lanarky + html_url: https://github.com/ajndkr/lanarky + stars: 994 + owner_login: ajndkr + owner_html_url: https://github.com/ajndkr +- name: every-pdf + html_url: https://github.com/DDULDDUCK/every-pdf + stars: 985 + owner_login: DDULDDUCK + owner_html_url: https://github.com/DDULDDUCK +- name: enterprise-deep-research + html_url: https://github.com/SalesforceAIResearch/enterprise-deep-research + stars: 973 + owner_login: SalesforceAIResearch + owner_html_url: https://github.com/SalesforceAIResearch +- name: fastapi-mail + html_url: https://github.com/sabuhish/fastapi-mail + stars: 964 + owner_login: sabuhish + owner_html_url: https://github.com/sabuhish From e752224bceff321002214be31a9559526b90e1ee Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 1 Dec 2025 13:17:51 +0000 Subject: [PATCH 157/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 93a4c5c97..a54d6e500 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* 👥 Update FastAPI GitHub topic repositories. PR [#14426](https://github.com/fastapi/fastapi/pull/14426) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump markdown-include-variants from 0.0.6 to 0.0.7. PR [#14423](https://github.com/fastapi/fastapi/pull/14423) by [@YuriiMotov](https://github.com/YuriiMotov). * 👥 Update FastAPI People - Sponsors. PR [#14422](https://github.com/fastapi/fastapi/pull/14422) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People - Contributors and Translators. PR [#14420](https://github.com/fastapi/fastapi/pull/14420) by [@tiangolo](https://github.com/tiangolo). From 6e82df816dcc3eeaae821e67cac598b0fb90179d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 1 Dec 2025 12:06:57 -0800 Subject: [PATCH 158/256] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors:=20add?= =?UTF-8?q?=20Greptile=20(#14429)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + docs/en/data/sponsors.yml | 3 +++ docs/en/data/sponsors_badge.yml | 1 + docs/en/docs/img/sponsors/greptile-banner.png | Bin 0 -> 7222 bytes docs/en/docs/img/sponsors/greptile.png | Bin 0 -> 7381 bytes docs/en/overrides/main.html | 6 ++++++ 6 files changed, 11 insertions(+) create mode 100644 docs/en/docs/img/sponsors/greptile-banner.png create mode 100644 docs/en/docs/img/sponsors/greptile.png diff --git a/README.md b/README.md index 9864fa1ef..26a6c32ae 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ The key features are: + diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index b8cc31dbe..50b114530 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -33,6 +33,9 @@ gold: - url: https://serpapi.com/?utm_source=fastapi_website title: "SerpApi: Web Search API" img: https://fastapi.tiangolo.com/img/sponsors/serpapi.png + - url: https://www.greptile.com/?utm_source=fastapi&utm_medium=sponsorship&utm_campaign=fastapi_sponsor_page + title: "Greptile: The AI Code Reviewer" + img: https://fastapi.tiangolo.com/img/sponsors/greptile.png silver: - url: https://databento.com/?utm_source=fastapi&utm_medium=sponsor&utm_content=display title: Pay as you go for market data diff --git a/docs/en/data/sponsors_badge.yml b/docs/en/data/sponsors_badge.yml index 14f55805c..d648be5fc 100644 --- a/docs/en/data/sponsors_badge.yml +++ b/docs/en/data/sponsors_badge.yml @@ -47,3 +47,4 @@ logins: - railwayapp - subtotal - requestly + - greptileai diff --git a/docs/en/docs/img/sponsors/greptile-banner.png b/docs/en/docs/img/sponsors/greptile-banner.png new file mode 100644 index 0000000000000000000000000000000000000000..e0909b39d1d42469436ba4c1a406ef0a27f02d34 GIT binary patch literal 7222 zcmaJ`1ydXC7RHLSxH}Yg_aem|io3hJ6)Ock+}#4jDNb;0ad&sO;O^Y*AGkY{>`o?| z>?`N76Y)(+8s#J5M<^&L6j>PwRVXNECg7S55gvG#$QoDxPDsu&I&M%#p?vmQ>YEG8!UM8*fYI>*o#B}{Cv@sDDmwml@?_qRs=Pb0)N+w#VS&ji5K2Y(iP9G$I zV>!yY1~`%8O9WF>P+a{XCuG9FNI-y}GL8rj7i0^EmzR*(4HD(jrl6oeLq6ru=9c>~ zau?X=I}lJ@*t%UkFALmVC>hem9Xro}(~e#P>ZmP&s$u6ayS%UwbwDo*Y3VVoa! zdk#HFJY(ILuSdU~Eq?gT)#81KZjPZoL?G-|PU&ORy*-ri!HWi8hOQem?S~&9edhdU zR3vsqOEiKt3e>qcHDk(3%Pgg5kvt`Gz7|s!H5u@JUuoI2Jo=HPfv3-WQp?RDT}=5p zlH9dXI%^AhxrNQIbF7*4!mN(Y^ytz!Mt4~n!ap27v^5Fr(s-0k)Jc|?wcsbcTSJH2 z3Mi)(BZ6mS;H=d!^Prt;;MP}&J23s;zAq$-4m>OV{&t_!^}5uR$OBoGW;uVZXH567 zYBEF0{_E#)PzaMD+E?CBU+N-1z47E=y3Gu+vf8>umw`T$-c<>9KBB-2_}1(l%>}j? z@>72(#5UhDwv9;&H2_2QuH;NPv1&PMd;99-+stY9tX%my6Moj2B3pdt%UIbyJ&91) zI{+QFzdi88_JSzxI@SW^EL{U=c;!EFS9?$`rxV~$&2^KKRPqF8DUPeWBK_)GYW$@8dt3{Vr=ZhuhEL|Fqn;p4Pw{da^%g z;K^c18Br%g3q+Mv(cQj-{B870xow0#l8~i7f|0MKU{_^2!Hxgt4@F97&gTb?@EBW_a^` zD2-u_gf3}z1pUs+PM&4U;pHh1+jrincDf8it;h~9`WIV}ot+Ex4MTrb8MYnFF{IeX z@QvS<8arZ}7qHItO*lYR$7P5s*?cC;S%tQJ*s2Y56A}rIQ#yfy4#))mEV#sb;O0R5 z@j6ifP2N`cL(vKiBOP59_&x^-{cBtE<;^D2!##ef%e@HlTg+fI1li+mw+Lfq$#siv zg(7&6Zn#Hb15u<2x?R8)_l_p@BwRrBs{+2ol^hhX^EUN$i5vU(+XY=|oJb#0XD>D) zJ}tjgAo&p33;TxG6lz~FMzT-O4V2ii8sc+=0@F{jeyy|q_TUbw^m(XJgJFIXc=L#^qqDF*>M;xYOENq4+vOgmvW_Tyq} zq$Z|k{XMTOVM`@gmA^eFV2#aCR9>l~(_pBJyjt|Q5mHi67m#ATU%omPzXAF;Oif$|#w6QXLrUxBZ zQ!yW#jWqRzd2&Rt9LKrPlNh1Czl>H7#?xS?o1EES{$$(BVV|q~xvB$&{n1Fe~De-V*1WITv!apJlp&Sp9}ge)fM ztnZu^9p7W0SCJySK}vjt-15lKs*Og+qXxVkUS520hhc6!PGpS+K=%skBB4AespN=E z7deac!Q4wTc>ZP4%1UR2dHEstaqrzn$&B{d4aj!lGnxku$Q#w{n#!m*R!4&w+C-U& zy6#Eb15I^zy?2@i0WP+zSgQy-*0!M$qd&4Nn$nw2VdZ`^;-Bn}XNKkeDrunX)7X3Q z-l8)CEl=!&!!A5e{*DS$a=VHpngTHO;IS_EUz_)@Z)FdZZ!p021g+NTwd2=-Zf9_J zOdUredp1;As<$yFeknMUMgOZV%z6w0B&Kz65Mj|zS<=o5{4ZfumOQIWjN3CwZHpvW z-C1E>1zo{J>!?BkHP<91^}naN>;<_=i6D&jw$>eF4<)!6(>a6l# z!8s#~M3SUH^y7k9SUT#D|2mvhBBa-!m#m?c zS5OpVSfKPHJsQqc?j!!@>?gY&fw+$5y6E_asNEDC6BFxh{QJL(S5{jQD%$v&(<9@d zoU-hNIJ+0k6CtRlO9oy(eiNeU#{{bS=fCRloWcYkr0%bcc$nGV5eY`jW;>&41d#cz zC1PN+0$2sh_Sc_{sF-!?T02Yn;x*bf5~6gyu$A#vmjEH4Zo@4 z^zIphrWn4v(I#9_uL1_AN_2N8aQgVH>W_IbHjrn9igZc$SL%=PF;a3eoj2D4Ql&z; zCPL_zJ`BE9@5|+yTDbtu{(c$`tg$*-fAL1^P<|zuGiCLW*Nci%;CgOjc-^sFs>6H8 z5p4g})@YGpe|zV%TV2#OQsA|;619|#t2Pg*2fSV2tgI&4qJ&4KKIgzUtz2cxE?}KS zLujRH>CJh|Nqs;q!f3GQDWnstP8+>F>nR;k3pV>59p`qAyB5w@8ym3BYyu$~&s{bY zD_^zZ;>ZmCGpcUQ8d=_Gn(QfNjKyuVM@1@Sd~u&NUTc^!0I;x&6jp!=`_J?r{1jb9 zjY?wJGUSX{?6OpzP#o&frPlvqO3=8n0IEv@sv1i0=yYRLZE9|QtG2U1&%jNrpeJt&$jjwZZ4jI`ZI~BG}$qb)aJ$et0(}Tds`J za+HWo$*h7f9ua1y{Xb}r&kixFQ%4PFQl2itaVVfaEh&2Z?{_>PQsI=4tTOwLNR!bz zZijln+L|9Vw`2$U5>1tRU5zIngv-SpB#8vrs=XzQYyqT1b6Usbi#(@71f<>1(9$brz6 zF<3=ElS5Ic?QmTyZG^*<3)5zFM`GrUC&>+ugZx`F7^DHBcfLK4q8N z-PHsR9V`;+U7Q<0=&P7!20ulA830IK`|@Ju#kadlpx$Z{744I8h`iHXZ%Bh)X;cgI zIIgD*Tu`_cuS$V>-ZzHQvUiozWEnl@U!0NGnS<65EXA6YRg5#8Yu6e&dWuT^^p6Lh z1{MSP+n2ms@^MSud}ksZHwaO0zi#=6Awe%&e=WMr{Geq1{3ycbxrcnNtT+4nC3xzyOoK=ICO7tvt+i(}N8{0iMK(hskuGtZT~{9* zRfil;joAQUxA@~_LgAcO27lsJDCgRfZYwuKvllDYD}zN+(%U=Qo9}SESJOIzckGs^ z6(fQC=}R>MrUE_=wF2(f1S ze_Z_BZ=}VxDe;ZEs!>w52w-1y^`FBoUIAM%KNg@dqxlcxAH_>9*8eJIw+2K+_)8g^ zST7eo5rXhTo4^POHVb??_DC#7kHz&-sB#H%yjRd8mHG7{%S*2vt&^Vk<=@uFmhq-o3%2RD~hl*2XSC z7mfJ%8~1N1IbGTDrhAA%xqALLdgypYJNoh}33Rww+lKv-V$Jee4mMGAxa3+3{efu$B|`zQ7fD zK7Q{syz(VRxZV#9f~Ryau^uiZT@Rs=P}MXTx?SaRjl9;`<}I1~8Q0zj@2keRZr4ZT z+29IgT<@FnH&P)+ytp7ruF_H>?#&!~jcU`B9H@!GSw8z{X0A}EmV9_UtPxEMFR;6{ zn~{K68**wwm2jife&KrS(s4RHskrvxmx$gM1=6J}9$tBftI7sQXtd6gROhnhC^7;B zO|5}*(nh?_#9pmhzPKP}y@1)eEO6s>sHCmRWHMCCeC*838v__%fPj=2XbGUJRuz6V z|88H}I)glBn5XW-PBzIO5Q&p0R34g|%4*6&Zw;k!KmP8|mXD(3`9ffpA?)_l^n!5Z zHQ6BSN0+9>KHK2bc2G8F3m9aR((;KwOD0Fp*PQ|pmzQIL^^7b1wm2t6GPcyb=4*RY z6yxkL(d{vgT*VYBZ z;G|FzAQkamZhNCYFB7VN_R(I`<6|*l;`}o+PvA5Fs<_Dw6RX@lA|bPj#`%-B^-TAh z!H%bi`lg736v7MtV}s?xv(G`>d8R&b7`!wG> z+opWv2|KeXUT7)A@4q1q0jc+USN{TV=3#%giUy;%AshQAWW_FzC7WougVid@6fyG4 zL~h@BZz6y2fSRts-~A_;0q=>=gYNE2*mYT}Qqn%Ursi=$fEjQl7YG4VZGVa0IZu{E z$sJj+zDf^m;L9r2tt%}s;@~zPj0D$|i(WdZdA!x9FRf-&GY2Dir*s-}ihPJ{mGAhr z*bRPuHzhn48=scf1h;KXl+hD_nE}k2Amj@YS&88xkFDpYaHPHCv00jHM~&1=z?ZLI z2G$usDu{XGILowJ5|g@@H%}|jQ2u<=ikm)`tEa-meNfnLa|>Icwo_#_+DEl&78jT#>GsA&_q}+o(lQ?w1?f~w=+Ydtc zUyJwDNHJq*N)a3YA-V=Z01Y`kj5%n`Zv>GeR^;4rOxXLR`D|30XNRFV@+hefmdgyv zTFBgYGT*6lhXkuV#oBvTZoQ6u2H+3XLhKW%rCqC^pdG=8r{8cbqZOYUWoCj9ys4SJ zsH_J6&|`*#6_xYWsZj>MGqJR(^5xi&L2n`L4nIOpf3x-U2V|(&eIBM; z-|X9IR_NTir{fROErUO=;g#Q3_BS5)JuZA3-q2t8>ZC1J{viwW@Nm#&YDqYLjML5uiI|?hGfr1zb(|h}`z(Gz!Ub zTBCK--(!>09~qq(Mibh@Jsp1E3ie%3Wuuh^b(K2B=# z>zvAd#f7R@yLe*DO2pXV!qZbKx3KVc_V%zVJYVxA5Pr!p4m&jc#e@q&nMvC;bsWGe zxpSrGOpwor$b_8DD^&n~3)+jy@-&+H!Ty*6wDyRk2N~Ib0%RGG;`Jm-br_z<=1%;~ z$xYPKln|9flB#rX;B^My;O<>9m@lh(jvM{Ur?=9=ce3R=EBKq}5n;r$g_hVH*bM}H zLW-b?u;ktKkXndGD{38+dF}s9Phum0S2T11_m8A~L%$JnvVRj}4O0#3n;wid^klj5 zw0^T^P0<~jJE0P6Yu=ubi~Zf!fCZ!w3A*u$7K+@e4kW~54s3J0w%6DA5Q3{$we!07 zX(SkmV4&{f02!Go@iQj^=h}~U?qH6}@44D*kr?yOuk>%@CHnp96X9XLXl7*_t2FC_ z*9BH47UunEC3lzpi-sqPmD&Q1EDdrW+E_?uR!50P?H9q;Bw1oGd>Da27roXBw=su~ zr-S5Hahe`HoNWD5tMD21HrBQ##@%iOmUXlo;Rbkd;L&%a;#)FtJWjg+#Fc>vJ3P9| zsy_f0y(yh7uBm*^WuJ`u(+mbw{&t`=>Fif6G-)@Y8_2?JqRb-SIdfyqbB=%7&!vDj zI!sY9txsS2ThXM7H0|<3*@ZeDr~E_!&Vc7zJCnS*zxXUnOOWf>G3B71kcJ3jG~pX% zG(lprQG`Xfq=eh};Pa5DUN)I1Q^Q`H_i6!7ZGphP5Xvx&N>Ru-vp4N29BZr#TnVu+ z$wK*Y+UX<-%6nf~2n${kS*bdvN#~Mrgf=AU-3t<0m!8PNAu%*Wc1Nn-xVy7gDMva+ zA#WFT7?G35Kb|Qi+u|HadYqyQRgg^CP*3f92JL;<6f_X z2e2qE&X2~b9?JnIhp5~s_T7cW@~I!$>$1Bp^zNr_QQ~ZV)-0WE!aoOOD(h;3`fULO zq((9fgY(hnrlw0&BA(@S(bqN$gY?Y3U$vx+e6_yo@<+yT+0Yiv;o!^MP9}yczq}ch zh<`SL1vtn$8t3aR3H967+IP3XJy;kl3T>kHoWm2M7xzniR^*(YZj7%Q->fRyMSO!+ z>nL#1a{E`_u4`I)VEGI-oR3URqCo0+(9jc}T$qH%#~#hlnp%3xRr~_Fy3K0|J8fzu zjqY3vyxUtmu|LD>$3$*xz=ItnYE6X}lvW)1Q&`ZtwqAg1-&sSzc-W@(dtY?T9<9*9 zt1DM*S;l03`Hq~YtI`9cgnZt&3I$i+ne`}Pm((`Md7bhCqsDT89?*N6`|**9fbOz= zz>S7fKtuG;$V8f$_%YmgYj*I2OtCjAbv_ABXUfjL3lDd!U5m{Nq^TkOh{&YaN=8&N zqS%zL*jEG>C2N^rP7-K484HlytPtp3+>WMACCX%mhA{sNoJzq51_K!oBA1uPIG-LT z7Dr7~^|@hL`gYk$RavrUM?bDde$P>(xk3S-Y9QG*wlU5hN(1^;FJNo0=??C7{?-r7 z5g~IsxvkO+Yf>?pfPDmlJPrq&FjAOB0(HRb9FD&?l@vyX3nISQ1XF9xCTggDZNdf3 zFatw$Wqq9a*Y=XwJr}dr|JN-uMn_2v4(==|*W7K)O4mW(esR8Uz%iksca{A*Dg? z`QAU`-XG4Kcg~u1)_Z2}{p@FdPK?eQH4+dV2m=FyQ9M%MQz-Mc5*D!D* zbXPa>z`!8w|L?#YcB`-kF4B6cfIZ*2*?Rg|e6+#v@$unzaCP#qvT(QIcl&6Ub0|xP zfx%$;O8JGJZ!S8|&qPoEVOYV(`yvS6M$L!s|L)w{c)}_x zaeK$dGO z0I_>3mr?AHjrU1WP*XB!1rzJjFOTR@LL8Auf=oq0tN(}PkTn+EzNUY*`s?IVCn5b5 zoIY%_35xiw*;H1)CGO=a?oTaUM5_gLkI3=maUN@%$Lbp3ltb`aY2tTH`5uLC=rSC75dm^hUjM;{kU5v)~MBsfASx_Rwd!gpm8cm?G4ad~jYKTE~?l5v=Err?&a5k`p3B&_F!XK@!=ZT_)t}^uR5$rg;hY;eu!Y?edNLPwjHO!op?- zN-Zy)%31fu#us&QEt9*O!ev<8#<-8Q3Eoiq}2FH;(EmE@V>bEL-}e=!Q2viFq2K$*o{ zy!2v33U*8{gEUiEs}HGahZSO3O?&NbSb?ty#V-w-XPVRh_GU=3VX41^h&go%{8 zzWPEVD{svkc|(kmQL?1NQ&Zg|^1MJ1uGg3r(ROP&%|3=<&yeU4F@K`J7<>mo6_>-B zPcWRr(`o2san6dFhiD#pq28q+sq)}6Z7w~HU!w7yi^2xCV;3&gMuAJa59Fj&bH1lv z9}11xY#Ek1>?s*|YrOsm+_33?hp-)iyiKKJH+|sth|Rf^j>|D)`xxDSkJXuyqf6Im zj^kSo9FDW)ob!&sdTa>E9LK$?^S-qa>sS9KHTg$WkAaxLJtj^^>Ri_pWwxQUt zwe@)?JCo_;{~d_lLC&!9qhIxPywdLZETHVFPY6$4wdgck+BBZi8Iv}h zpTa3Kj?hl{y57xsp;&J1Z|@+;ZmsVq7Tl2^Ijh#pT3ZhF>OAMr!cL3#TJT1am72&W zj1<*_r7SQz(F>Sj!etDGW2ZDhl*=6@qp6@yA738CnB+{aul<~N$cS_VQ16*i_7n!% zCOCFHG8m!R2JF>;Nw>H;P~Tw7a9bHIF{)^~-`lucZZ$4EU%5cBZ~jb0L5u0ZkYjq2 z_8g)#7~D#4I$u3ybIl5gZF4>{IewM!~~MGe{;!zb^8>O$LSPct%2KI%G{H@Qc!Hf zkJil*)w0^$4+019MsTcN0BTqxdn%W&d5z=asPTIPU0Tp>o!6IIyY|AsD1S95r8?DD z?%X3VtJ$pflFX)aGfA_+cSj!TD%B9UAihU; z2y?foj8!UvR;35?O};uYoh7tNW^i%}FHv=!lFmRI+fMGvZnQ^!hy4~&ixiqSH zGU9b*&pQQJZ^XuX>BKWB6C6oDwnZ75;Dkfyk7{hG{i}&dMma}`j$#a_#Czg0pU76nA1FEl{P?@7{wAKV2&XM|W&EcHJ~1X9TWJHVW*2 zrqr(!0oorm`7ccH`*in9g`R653=Ox_`~vFbJG(!uvd>i2_&Y+!UKW0%whRFNnztpK-Elm+d7U;p!%kG+u1%T7~R$jnX+TnpV#^I(Ho7 z^QUiFZq}4VpMnm&a!}fbs{TkXgSPGz{Pe`Euu6-53!a9V1p^A5wW)A9!#5j1Viq3+p)$mntbq-w}C+g{3!E$`;P&bS)#w3=bV}5flS=*ea zK^Xi1eYW14k4XSIe{s6l+|2?v!GNYRT0qg}9i#k}hs>r2Lu_BMMM|CHhLY{gvoc8I`8S`z0CFl8haMf- z+40Aoq7vj^Pa6cA$x&6!ZI3Y?ZBvFb@7`nQdym%$)jf$8ln}-|!A$aX91d}Z3w}lO ziP}5MHc^l0#`y9G(S{C5`NEH-HGBSR-Qxn#*(g;bkEnYTDJTu(cJz?l6!%)m{3#;$ zjLxJ64SvXpYhuh#%!!4tPO7gq)3q5}3<70WfBl+!l_|&h%Rfxj<+sD11qM=oA&08+ zaifYKA(8VJ?G2UrLcZuqH-|n{rP+F!nLMg~I8b};dHUCBprp)x1ec9*)C6C^7bKmY zGc@u>9`+P*)#M;>+@AhWQNTA`h^;Z_YqY;bLRuGsPXtnD429$+9L#wEb@9w-n};AQ zz+w>A{-=6X`d8BTiWwgE?~^FS$NjUOn=-ckmR$dqCD1!Enyza)2aW^EFfVwH;w~-V zafYUw$f%M@(D7idVb;d%i_jEKMD`iCQx-3vJfi9(9-lCwot$%x$C6eC6FmmjxQ0%* z&GRZ=lod`62Y*Ezvxneg!%2U)b6Ua00i;X6?Jhsa{p2=dkfD{ofcww>SIRrYOw%y$ z_c5oSzO7#iq={qt8h5@YWU5&}`^<^)IB%A;RfQ_m#gUcp!q%l@QB~4c;gMSv%Hi#| ze`ZyI>-kXH39b?cy2|*0uEL^ooX>;U|~V7Y(ov zhl?)*e|c2muMQH7Oc7sE9``L13(~F8RleK5cv){hS+xj=j56@Lv@`O{azGs-rV1gv z65r&+qfHXpiz`F*)0;+R ze!R73=82)CVZzctA5PKtZu;;mM*MWWZPGM<4EVACDx zfC``T9@FDE*AGfRs&=~jI7%dRjS2xaN)-VWqvV3ue$FoMAoiNYFCmfy`VAJpb}jCG zK2SriiQ@X>*zI~$?MI}?%Ouym-|5}xto2q(S2*?!f>_)#upV*l?=3R8uwu1P&_tZioQv>z9s6{mcL3EmYt z!EN0vR#4`_isVPG_MU#ZFn{w&8qf!q_aJ5W>1pg0TDUkVRP1)u{0U}M>&?3PxK!AT ziY^5eNzQkWvQCwLtEp}@$=8dGwilUY}H_kfT)DAN&PDA^5)hktY59gvKIK0Y!xD9hLBC#KV`nJ-UpISi2lXz4g z6)W+all@u_x|O-e3w>!41Qr?t1f*gP$#9_g`r~s*3nV;Eai;o_FlI(n8ab8V=huWqWad`8ijiLHTjEj-JL!*1P0*?-S4xbugVMZB; zeLV%({l^$FgEy~%A_ovPm@g+v}Is;z0e&f=O z70t#02ORnX<4rmQQ}C%QDfLJaAygRH1lhwf+@~(Y+YeJbZ#61RYIiYsiU^L%HFNrQ z`NzQou>@&M7dNu;TS)Or)>YM|D32WUYJ(*rbUoxg6?6SaWEuwbUEMiXt*&K(xqGuq zWN0>h_EhmqYW$A~oCcp_QJvuU594EJd+Fj6(c`rFviJ3qg~?&p1^#f3eARjZ27w&M zWMFDlUp7muWCE6t;4}xcp`~AFRB5%QulP3~6#6m9$+?17td&U$nAhJ*1ZKS3wjOQH!HR2gS;_kZ8@>P z;WfpEx6_|P@r2AO%FpUAF7wAFxQUExBCS1p%m;2Fg-1u}9Z&v_C%UVt6-J4_Eo0EU zbTu#2RQ1DUox*Uafn_?2GXWES-OjtvmvtX>hDkjby&V?_AW=nrtn>Y9gwNJA}8;S{+uOm15Z~QmZI9j*0 zc|3C&&WL7&F7DuM`9OsJ5b_&%gXJKPsUAAhlQOy<{H5V()aLsYMl|%IIP#>~S_k&o zstzwnf+wV+yg0o@K2~DJ+uhDyN#!*^-y&pLKXvbDr`YTcB#N`*w74h~I`%lVC)yjS zecsZJ25u;#iFbW_EHxfKFv`#>3CAD6Q$&$;^da!RIcef^OWArr#B=bT@Pzhh_l-{&4 zkgodq=D(0Q70C+q@>6*p+xMuJg_r(}U;`Tu1udN#+!5*ab~23j8uqbhb)&gb^(Czb zY&jyciy_WC_=m6-Fc%7IeoQhdmbo1mUZtdGHj3qJd01M%ceK&ibMMR-q;^U;GzVkj zHf{UzoqGUk1a#WZuIx#i}@bSrOk6k4;6I6P*^-trnz7 zKF6fl8~HB4g0C@@!;B!t1QcU3hlJCUG zQrU_p!5yo^4Ubgjw}~+vpkD7Ei4(GO56nR8QbbU1lv}u3E9KyFyyMnkcTj``Q35bY zhT=%Q+dWYH%qDM4bh>Y2eO4v7UZ{`@^bIe*ul)CzCtOpL@*^;4Zra)ifl%B4?6Zx@ z7)Dwm>j=R@u87`#TP;p=>Lc;Vi1y22gi7_vMl>z28!WpVQeB19QJdRylhRR-8YMLN z@SZ7%#ObSCVle5=&uSpHa||mEGaRV{JzNaIhwQw}BKYD;W28Srcs>yTDZAM+$ZQ_n z;?RuP{wV1i;4B=R-HnB03ic1!ejcmT8pZtIcVJE`y&U0OKRsTf)jU4^p5+hAI+Zk$ zrA-M(pBZ}Yv3N8#_ELMKU&cgBQA7a#umqeYhG6|#~$ ze4z{U9|CkZhMAhgT*GVa{G41GiU^@kVUVpI+yg`))^42J2D~*EG7N+Q(5vVZ!l9C+ z6B_1Ioka2H)F(Lq5fMO(k=2d&l35vpHwgd$eZ(wCqfN?~SR~K-VNPmF0Be6t90^#6 z|3KAS2+0<=Z88+dbijlz{41((_$RafxtMDx6R*wJr*(#MuGba^c^JHKJ`>QYmmHVM zGYr`nr=*xkt)CKS}KprpjBGH`K`kP7Y zPrG*~gTM9C*$IC6`X}-m&F{~4SG|I|jWZ5hQj?Z+7Fn@I1zaH~cIQ_OwXp%__o(a8 zFTI~DWKi41X;sv|@gcn;AOL($kv(_tZiU53RmF;tuJk7`0Iu6^VgT_0ALD))LakPD z4`phN(F&~t#Pxe1n#cBi;UsTHQ9~sYWqi~6M@jb<$mf(Y65gvmrrK!~UB=v5X%}sE zP)*5%_utioFV89egTBsHVXZ1ym>Kw-JTk4|rQT!#VfEv;hM5PXKQ`Tk#JOVi5q}b0 zu7qKmffki-i=^g8LfgShNOb)O`1qG^VDK|a0CKB_CDInvJUBe-|U}VM7*~si7cN8lV*;8-E+B{=qah-k~G#nZrUBy5zlV$ z1{}(1nOt}MSN@Umz54Y#%0DvS;~4Z@zkH+ zAx+QQ(u6P%nzn&h9*9c6&dJ`vbCy2iDS;Mqy=IX7u}((KE}QwcEfmun`I20a-LiDt zs_S>z8UTxOnEh}=5Oq}3{c4K70uiXVZ9! z;$}_&M*YLp6{L2x7IC{8aMU$FpSP~VK#Y>WP;)g^))JXcQh{&*PnEeuMdRk~;abG*=F4^me%DKMIv}47Q6qGyrPm)n` zoZI+)LI}fE_PMeWK-3e<dC2Y(Jd zM%nT9*1#SteZ|B))>m^H1&AH4e-jAnB%A_;+Nu*s^j?HiXbKCk#Lll2 zLIINUw<-z%)HKpeA~JQ#1UMg-sh@fB)Zf268Sg&>kAZ}-UG@9RM3uy6`n&xY_~r92 z7}68+XE&AIfEGqcHZhE!l5cAx@t7bFVn+BM>W;}7A3aW@b5PTK*;^**E3yBI!FOj( z9TYPe;A8}d=q5h#;L!xE_w?$wYS1H5WbYP2#;TQpR6gqh%To3#-M>P39`RBa zke6QVGeefzX5z1h*2s_hF3WI!l@FTi+1qb{w(8#85C(MS;;xs1Qwj{h2~4WL4dy5$ z!HXMh$al1pItf&A0dwRy+Y3@+drK{{tx_wEz2jR)PDLzaFcLu}_(^du_jWG)*&~7vFL7S8 lWC|(w;C!b2zg?a^*jw6?
+
{% endblock %} From ee490906d88b323731a76df41fce7b27d4029e92 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 1 Dec 2025 20:07:18 +0000 Subject: [PATCH 159/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index a54d6e500..ea509a760 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* 🔧 Update sponsors: add Greptile. PR [#14429](https://github.com/fastapi/fastapi/pull/14429) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI GitHub topic repositories. PR [#14426](https://github.com/fastapi/fastapi/pull/14426) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump markdown-include-variants from 0.0.6 to 0.0.7. PR [#14423](https://github.com/fastapi/fastapi/pull/14423) by [@YuriiMotov](https://github.com/YuriiMotov). * 👥 Update FastAPI People - Sponsors. PR [#14422](https://github.com/fastapi/fastapi/pull/14422) by [@tiangolo](https://github.com/tiangolo). From 20f40b29c0241fc73d82857ab456ef6fda15659f Mon Sep 17 00:00:00 2001 From: Kent Huang Date: Tue, 2 Dec 2025 11:31:59 +0800 Subject: [PATCH 160/256] =?UTF-8?q?=F0=9F=90=9B=20Fix=20`TypeError`=20when?= =?UTF-8?q?=20encoding=20a=20decimal=20with=20a=20`NaN`=20or=20`Infinity`?= =?UTF-8?q?=20value=20(#12935)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kent Huang --- fastapi/encoders.py | 12 ++++++++---- tests/test_jsonable_encoder.py | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/fastapi/encoders.py b/fastapi/encoders.py index 6fc6228e1..793951089 100644 --- a/fastapi/encoders.py +++ b/fastapi/encoders.py @@ -34,14 +34,14 @@ def isoformat(o: Union[datetime.date, datetime.time]) -> str: return o.isoformat() -# Taken from Pydantic v1 as is +# Adapted from Pydantic v1 # 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 + Encodes a Decimal as int if 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 + where an 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. @@ -50,8 +50,12 @@ def decimal_encoder(dec_value: Decimal) -> Union[int, float]: >>> decimal_encoder(Decimal("1")) 1 + + >>> decimal_encoder(Decimal("NaN")) + nan """ - if dec_value.as_tuple().exponent >= 0: # type: ignore[operator] + exponent = dec_value.as_tuple().exponent + if isinstance(exponent, int) and exponent >= 0: return int(dec_value) else: return float(dec_value) diff --git a/tests/test_jsonable_encoder.py b/tests/test_jsonable_encoder.py index 447c5b4d6..3b6513e27 100644 --- a/tests/test_jsonable_encoder.py +++ b/tests/test_jsonable_encoder.py @@ -3,6 +3,7 @@ from dataclasses import dataclass from datetime import datetime, timezone from decimal import Decimal from enum import Enum +from math import isinf, isnan from pathlib import PurePath, PurePosixPath, PureWindowsPath from typing import Optional @@ -306,6 +307,20 @@ def test_decimal_encoder_int(): assert jsonable_encoder(data) == {"value": 2} +@needs_pydanticv2 +def test_decimal_encoder_nan(): + data = {"value": Decimal("NaN")} + assert isnan(jsonable_encoder(data)["value"]) + + +@needs_pydanticv2 +def test_decimal_encoder_infinity(): + data = {"value": Decimal("Infinity")} + assert isinf(jsonable_encoder(data)["value"]) + data = {"value": Decimal("-Infinity")} + assert isinf(jsonable_encoder(data)["value"]) + + def test_encode_deque_encodes_child_models(): class Model(BaseModel): test: str From 327bad18aa326b02ba5e680648d26a92052aef1d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 2 Dec 2025 03:32:40 +0000 Subject: [PATCH 161/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index ea509a760..d1602598a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Fixes + +* 🐛 Fix `TypeError` when encoding a decimal with a `NaN` or `Infinity` value. PR [#12935](https://github.com/fastapi/fastapi/pull/12935) by [@kentwelcome](https://github.com/kentwelcome). + ### Internal * 🔧 Update sponsors: add Greptile. PR [#14429](https://github.com/fastapi/fastapi/pull/14429) by [@tiangolo](https://github.com/tiangolo). From bf322d0e94abf67fc407abb26c9718c3ac73d2d4 Mon Sep 17 00:00:00 2001 From: Hemanth U <77799372+hemanth-thirthahalli@users.noreply.github.com> Date: Tue, 2 Dec 2025 09:32:38 +0530 Subject: [PATCH 162/256] =?UTF-8?q?=F0=9F=90=9B=20Fix=20Windows=20UnicodeE?= =?UTF-8?q?ncodeError=20in=20CLI=20test=20(#14295)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sofie Van Landeghem Co-authored-by: Sebastián Ramírez --- tests/test_fastapi_cli.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_fastapi_cli.py b/tests/test_fastapi_cli.py index a5c10778a..78a49a1fb 100644 --- a/tests/test_fastapi_cli.py +++ b/tests/test_fastapi_cli.py @@ -1,3 +1,4 @@ +import os import subprocess import sys from unittest.mock import patch @@ -20,6 +21,7 @@ def test_fastapi_cli(): ], capture_output=True, encoding="utf-8", + env={**os.environ, "PYTHONIOENCODING": "utf-8"}, ) assert result.returncode == 1, result.stdout assert "Path does not exist non_existent_file.py" in result.stdout From 74b4c3c9a1237326716eb2a7743cd3ad824dadb6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 2 Dec 2025 04:03:00 +0000 Subject: [PATCH 163/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index d1602598a..7997e69b5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Internal +* 🐛 Fix Windows UnicodeEncodeError in CLI test. PR [#14295](https://github.com/fastapi/fastapi/pull/14295) by [@hemanth-thirthahalli](https://github.com/hemanth-thirthahalli). * 🔧 Update sponsors: add Greptile. PR [#14429](https://github.com/fastapi/fastapi/pull/14429) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI GitHub topic repositories. PR [#14426](https://github.com/fastapi/fastapi/pull/14426) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump markdown-include-variants from 0.0.6 to 0.0.7. PR [#14423](https://github.com/fastapi/fastapi/pull/14423) by [@YuriiMotov](https://github.com/YuriiMotov). From 8f99a2b7347aeaff09ca3f06eaecf52a786ef2dc Mon Sep 17 00:00:00 2001 From: Sofie Van Landeghem Date: Tue, 2 Dec 2025 09:34:13 +0530 Subject: [PATCH 164/256] =?UTF-8?q?=F0=9F=90=9B=20Avoid=20accessing=20non-?= =?UTF-8?q?existing=20"$ref"=20key=20for=20Pydantic=20v2=20compat=20remapp?= =?UTF-8?q?ing=20(#14361)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- fastapi/_compat/v2.py | 2 +- tests/test_schema_compat_pydantic_v2.py | 92 +++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 tests/test_schema_compat_pydantic_v2.py diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py index 5cd49343b..7196a6190 100644 --- a/fastapi/_compat/v2.py +++ b/fastapi/_compat/v2.py @@ -304,7 +304,7 @@ def _remap_definitions_and_field_mappings( old_name_to_new_name_map = {} for field_key, schema in field_mapping.items(): model = field_key[0].type_ - if model not in model_name_map: + if model not in model_name_map or "$ref" not in schema: continue new_name = model_name_map[model] old_name = schema["$ref"].split("/")[-1] diff --git a/tests/test_schema_compat_pydantic_v2.py b/tests/test_schema_compat_pydantic_v2.py new file mode 100644 index 000000000..39626c0ec --- /dev/null +++ b/tests/test_schema_compat_pydantic_v2.py @@ -0,0 +1,92 @@ +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient +from inline_snapshot import snapshot +from pydantic import BaseModel + +from tests.utils import needs_py310, needs_pydanticv2 + + +@pytest.fixture(name="client") +def get_client(): + from enum import Enum + + app = FastAPI() + + class PlatformRole(str, Enum): + admin = "admin" + user = "user" + + class OtherRole(str, Enum): ... + + class User(BaseModel): + username: str + role: PlatformRole | OtherRole + + @app.get("/users") + async def get_user() -> User: + return {"username": "alice", "role": "admin"} + + client = TestClient(app) + return client + + +@needs_py310 +@needs_pydanticv2 +def test_get(client: TestClient): + response = client.get("/users") + assert response.json() == {"username": "alice", "role": "admin"} + + +@needs_py310 +@needs_pydanticv2 +def test_openapi_schema(client: TestClient): + response = client.get("openapi.json") + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users": { + "get": { + "summary": "Get User", + "operationId": "get_user_users_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + } + }, + "components": { + "schemas": { + "PlatformRole": { + "type": "string", + "enum": ["admin", "user"], + "title": "PlatformRole", + }, + "User": { + "properties": { + "username": {"type": "string", "title": "Username"}, + "role": { + "anyOf": [ + {"$ref": "#/components/schemas/PlatformRole"}, + {"enum": [], "title": "OtherRole"}, + ], + "title": "Role", + }, + }, + "type": "object", + "required": ["username", "role"], + "title": "User", + }, + } + }, + } + ) From 3b4b5ab8c819dce27fbaf0aa0a9c74b7f3255ec0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 2 Dec 2025 04:04:37 +0000 Subject: [PATCH 165/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 7997e69b5..f3f3a0e85 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Fixes +* 🐛 Avoid accessing non-existing "$ref" key for Pydantic v2 compat remapping. PR [#14361](https://github.com/fastapi/fastapi/pull/14361) by [@svlandeg](https://github.com/svlandeg). * 🐛 Fix `TypeError` when encoding a decimal with a `NaN` or `Infinity` value. PR [#12935](https://github.com/fastapi/fastapi/pull/12935) by [@kentwelcome](https://github.com/kentwelcome). ### Internal From c3373205d03af626631b12ad91f26e77e6ddfd49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 2 Dec 2025 05:30:18 +0100 Subject: [PATCH 166/256] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.12?= =?UTF-8?q?3.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f3f3a0e85..b5b3e5b0f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.123.1 + ### Fixes * 🐛 Avoid accessing non-existing "$ref" key for Pydantic v2 compat remapping. PR [#14361](https://github.com/fastapi/fastapi/pull/14361) by [@svlandeg](https://github.com/svlandeg). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 25ed2bbeb..4f6982035 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.123.0" +__version__ = "0.123.1" from starlette import status as status From d68c066246c9a70f179ca165614dac19b8c8beeb Mon Sep 17 00:00:00 2001 From: ad hoc Date: Tue, 2 Dec 2025 05:39:55 +0100 Subject: [PATCH 167/256] =?UTF-8?q?=F0=9F=90=9B=20Fix=20support=20for=20fo?= =?UTF-8?q?rm=20values=20with=20empty=20strings=20interpreted=20as=20missi?= =?UTF-8?q?ng=20(`None`=20if=20that's=20the=20default),=20for=20compatibil?= =?UTF-8?q?ity=20with=20HTML=20forms=20(#13537)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Co-authored-by: Yurii Motov Co-authored-by: Sebastián Ramírez --- fastapi/dependencies/utils.py | 3 ++- tests/test_form_default.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 tests/test_form_default.py diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index d43fa8a51..0f25a3c36 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -902,8 +902,9 @@ async def _extract_form_body( value = serialize_sequence_value(field=field, value=results) if value is not None: values[field.alias] = value + field_aliases = {field.alias for field in body_fields} for key, value in received_body.items(): - if key not in values: + if key not in field_aliases: values[key] = value return values diff --git a/tests/test_form_default.py b/tests/test_form_default.py new file mode 100644 index 000000000..2a12049d1 --- /dev/null +++ b/tests/test_form_default.py @@ -0,0 +1,35 @@ +from typing import Optional + +from fastapi import FastAPI, File, Form +from starlette.testclient import TestClient +from typing_extensions import Annotated + +app = FastAPI() + + +@app.post("/urlencoded") +async def post_url_encoded(age: Annotated[Optional[int], Form()] = None): + return age + + +@app.post("/multipart") +async def post_multi_part( + age: Annotated[Optional[int], Form()] = None, + file: Annotated[Optional[bytes], File()] = None, +): + return {"file": file, "age": age} + + +client = TestClient(app) + + +def test_form_default_url_encoded(): + response = client.post("/urlencoded", data={"age": ""}) + assert response.status_code == 200 + assert response.text == "null" + + +def test_form_default_multi_part(): + response = client.post("/multipart", data={"age": ""}) + assert response.status_code == 200 + assert response.json() == {"file": None, "age": None} From 740ec2787b25e59f7aa9e3df1db09d4489c22718 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 2 Dec 2025 04:40:16 +0000 Subject: [PATCH 168/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index b5b3e5b0f..cdd807be6 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Fixes + +* 🐛 Fix support for form values with empty strings interpreted as missing (`None` if that's the default), for compatibility with HTML forms. PR [#13537](https://github.com/fastapi/fastapi/pull/13537) by [@MarinPostma](https://github.com/MarinPostma). + ## 0.123.1 ### Fixes From 6cf40df24d1d199fd25d034fc87fcae284fc23a2 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Tue, 2 Dec 2025 05:49:32 +0100 Subject: [PATCH 169/256] =?UTF-8?q?=F0=9F=90=9B=20Fix=20parsing=20extra=20?= =?UTF-8?q?`Form`=20parameter=20list=20(#14303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- fastapi/dependencies/utils.py | 8 ++++-- tests/test_forms_single_model.py | 47 ++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 0f25a3c36..2b2e6c5af 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -903,9 +903,13 @@ async def _extract_form_body( if value is not None: values[field.alias] = value field_aliases = {field.alias for field in body_fields} - for key, value in received_body.items(): + for key in received_body.keys(): if key not in field_aliases: - values[key] = value + param_values = received_body.getlist(key) + if len(param_values) == 1: + values[key] = param_values[0] + else: + values[key] = param_values return values diff --git a/tests/test_forms_single_model.py b/tests/test_forms_single_model.py index 880ab3820..1db63f021 100644 --- a/tests/test_forms_single_model.py +++ b/tests/test_forms_single_model.py @@ -2,6 +2,7 @@ from typing import List, Optional from dirty_equals import IsDict from fastapi import FastAPI, Form +from fastapi._compat import PYDANTIC_V2 from fastapi.testclient import TestClient from pydantic import BaseModel, Field from typing_extensions import Annotated @@ -17,11 +18,27 @@ class FormModel(BaseModel): alias_with: str = Field(alias="with", default="nothing") +class FormModelExtraAllow(BaseModel): + param: str + + if PYDANTIC_V2: + model_config = {"extra": "allow"} + else: + + class Config: + extra = "allow" + + @app.post("/form/") def post_form(user: Annotated[FormModel, Form()]): return user +@app.post("/form-extra-allow/") +def post_form_extra_allow(params: Annotated[FormModelExtraAllow, Form()]): + return params + + client = TestClient(app) @@ -131,3 +148,33 @@ def test_no_data(): ] } ) + + +def test_extra_param_single(): + response = client.post( + "/form-extra-allow/", + data={ + "param": "123", + "extra_param": "456", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "param": "123", + "extra_param": "456", + } + + +def test_extra_param_list(): + response = client.post( + "/form-extra-allow/", + data={ + "param": "123", + "extra_params": ["456", "789"], + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "param": "123", + "extra_params": ["456", "789"], + } From 2330e2de752c686d162c02dff6c831ec8c0b754a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 2 Dec 2025 04:49:52 +0000 Subject: [PATCH 170/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index cdd807be6..4432328f6 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Fixes +* 🐛 Fix parsing extra `Form` parameter list. PR [#14303](https://github.com/fastapi/fastapi/pull/14303) by [@YuriiMotov](https://github.com/YuriiMotov). * 🐛 Fix support for form values with empty strings interpreted as missing (`None` if that's the default), for compatibility with HTML forms. PR [#13537](https://github.com/fastapi/fastapi/pull/13537) by [@MarinPostma](https://github.com/MarinPostma). ## 0.123.1 From de5bec637c6af5126382c1e604514964ba9e5093 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Tue, 2 Dec 2025 05:57:19 +0100 Subject: [PATCH 171/256] =?UTF-8?q?=F0=9F=90=9B=20Fix=20parsing=20extra=20?= =?UTF-8?q?non-body=20parameter=20list=20(#14356)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- fastapi/dependencies/utils.py | 11 +- ..._query_cookie_header_model_extra_params.py | 111 ++++++++++++++++++ .../test_tutorial003.py | 2 +- 3 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 tests/test_query_cookie_header_model_extra_params.py diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 2b2e6c5af..20cb2c88c 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -791,9 +791,16 @@ def request_params_to_args( processed_keys.add(alias or field.alias) processed_keys.add(field.name) - for key, value in received_params.items(): + for key in received_params.keys(): if key not in processed_keys: - params_to_process[key] = value + if hasattr(received_params, "getlist"): + value = received_params.getlist(key) + if isinstance(value, list) and (len(value) == 1): + params_to_process[key] = value[0] + else: + params_to_process[key] = value + else: + params_to_process[key] = received_params.get(key) if single_not_embedded_field: field_info = first_field.field_info diff --git a/tests/test_query_cookie_header_model_extra_params.py b/tests/test_query_cookie_header_model_extra_params.py new file mode 100644 index 000000000..f4ebefb3f --- /dev/null +++ b/tests/test_query_cookie_header_model_extra_params.py @@ -0,0 +1,111 @@ +from fastapi import Cookie, FastAPI, Header, Query +from fastapi._compat import PYDANTIC_V2 +from fastapi.testclient import TestClient +from pydantic import BaseModel + +app = FastAPI() + + +class Model(BaseModel): + param: str + + if PYDANTIC_V2: + model_config = {"extra": "allow"} + else: + + class Config: + extra = "allow" + + +@app.get("/query") +async def query_model_with_extra(data: Model = Query()): + return data + + +@app.get("/header") +async def header_model_with_extra(data: Model = Header()): + return data + + +@app.get("/cookie") +async def cookies_model_with_extra(data: Model = Cookie()): + return data + + +def test_query_pass_extra_list(): + client = TestClient(app) + resp = client.get( + "/query", + params={ + "param": "123", + "param2": ["456", "789"], # Pass a list of values as extra parameter + }, + ) + assert resp.status_code == 200 + assert resp.json() == { + "param": "123", + "param2": ["456", "789"], + } + + +def test_query_pass_extra_single(): + client = TestClient(app) + resp = client.get( + "/query", + params={ + "param": "123", + "param2": "456", + }, + ) + assert resp.status_code == 200 + assert resp.json() == { + "param": "123", + "param2": "456", + } + + +def test_header_pass_extra_list(): + client = TestClient(app) + + resp = client.get( + "/header", + headers=[ + ("param", "123"), + ("param2", "456"), # Pass a list of values as extra parameter + ("param2", "789"), + ], + ) + assert resp.status_code == 200 + resp_json = resp.json() + assert "param2" in resp_json + assert resp_json["param2"] == ["456", "789"] + + +def test_header_pass_extra_single(): + client = TestClient(app) + + resp = client.get( + "/header", + headers=[ + ("param", "123"), + ("param2", "456"), + ], + ) + assert resp.status_code == 200 + resp_json = resp.json() + assert "param2" in resp_json + assert resp_json["param2"] == "456" + + +def test_cookie_pass_extra_list(): + client = TestClient(app) + client.cookies = [ + ("param", "123"), + ("param2", "456"), # Pass a list of values as extra parameter + ("param2", "789"), + ] + resp = client.get("/cookie") + assert resp.status_code == 200 + resp_json = resp.json() + assert "param2" in resp_json + assert resp_json["param2"] == "789" # Cookies only keep the last value diff --git a/tests/test_tutorial/test_header_param_models/test_tutorial003.py b/tests/test_tutorial/test_header_param_models/test_tutorial003.py index 60940e1da..554a48d2e 100644 --- a/tests/test_tutorial/test_header_param_models/test_tutorial003.py +++ b/tests/test_tutorial/test_header_param_models/test_tutorial003.py @@ -77,7 +77,7 @@ def test_header_param_model_no_underscore(client: TestClient): "user-agent": "testclient", "save-data": "true", "if-modified-since": "yesterday", - "x-tag": "two", + "x-tag": ["one", "two"], }, } ) From 10eed3806a2a22a39ccec780451288b5ec955db9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 2 Dec 2025 04:57:45 +0000 Subject: [PATCH 172/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 4432328f6..1fbb810d2 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Fixes +* 🐛 Fix parsing extra non-body parameter list. PR [#14356](https://github.com/fastapi/fastapi/pull/14356) by [@YuriiMotov](https://github.com/YuriiMotov). * 🐛 Fix parsing extra `Form` parameter list. PR [#14303](https://github.com/fastapi/fastapi/pull/14303) by [@YuriiMotov](https://github.com/YuriiMotov). * 🐛 Fix support for form values with empty strings interpreted as missing (`None` if that's the default), for compatibility with HTML forms. PR [#13537](https://github.com/fastapi/fastapi/pull/13537) by [@MarinPostma](https://github.com/MarinPostma). From cb3792d39e1004947419a2a06b5764894730892d Mon Sep 17 00:00:00 2001 From: Alex Colby Date: Tue, 2 Dec 2025 05:01:11 +0000 Subject: [PATCH 173/256] =?UTF-8?q?=F0=9F=90=9B=20Fix=20unformatted=20`{ty?= =?UTF-8?q?pe=5F}`=20in=20FastAPIError=20(#14416)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alex Colby --- fastapi/utils.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/fastapi/utils.py b/fastapi/utils.py index 2e79ee6b1..b3b89ed2b 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -110,7 +110,9 @@ def create_model_field( try: return v1.ModelField(**v1_kwargs) # type: ignore[no-any-return] except RuntimeError: - raise fastapi.exceptions.FastAPIError(_invalid_args_message) from None + raise fastapi.exceptions.FastAPIError( + _invalid_args_message.format(type_=type_) + ) from None elif PYDANTIC_V2: from ._compat import v2 @@ -121,7 +123,9 @@ def create_model_field( try: return v2.ModelField(**kwargs) # type: ignore[return-value,arg-type] except PydanticSchemaGenerationError: - raise fastapi.exceptions.FastAPIError(_invalid_args_message) from None + raise fastapi.exceptions.FastAPIError( + _invalid_args_message.format(type_=type_) + ) from None # Pydantic v2 is not installed, but it's not a Pydantic v1 ModelField, it could be # a Pydantic v1 type, like a constrained int from fastapi._compat import v1 @@ -129,7 +133,9 @@ def create_model_field( try: return v1.ModelField(**v1_kwargs) # type: ignore[no-any-return] except RuntimeError: - raise fastapi.exceptions.FastAPIError(_invalid_args_message) from None + raise fastapi.exceptions.FastAPIError( + _invalid_args_message.format(type_=type_) + ) from None def create_cloned_field( From c6c7b720969252d994fe380e885ab49a246f068f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 2 Dec 2025 05:01:37 +0000 Subject: [PATCH 174/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 1fbb810d2..5cb4ce279 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Fixes +* 🐛 Fix unformatted `{type_}` in FastAPIError. PR [#14416](https://github.com/fastapi/fastapi/pull/14416) by [@Just-Helpful](https://github.com/Just-Helpful). * 🐛 Fix parsing extra non-body parameter list. PR [#14356](https://github.com/fastapi/fastapi/pull/14356) by [@YuriiMotov](https://github.com/YuriiMotov). * 🐛 Fix parsing extra `Form` parameter list. PR [#14303](https://github.com/fastapi/fastapi/pull/14303) by [@YuriiMotov](https://github.com/YuriiMotov). * 🐛 Fix support for form values with empty strings interpreted as missing (`None` if that's the default), for compatibility with HTML forms. PR [#13537](https://github.com/fastapi/fastapi/pull/13537) by [@MarinPostma](https://github.com/MarinPostma). From cdafd64c15558ba5de69e7297e526640ff9f422f Mon Sep 17 00:00:00 2001 From: SaisakthiM Date: Tue, 2 Dec 2025 10:33:46 +0530 Subject: [PATCH 175/256] =?UTF-8?q?=F0=9F=93=9D=20Clarify=20estimation=20n?= =?UTF-8?q?ote=20in=20documentation=20(#14070)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- docs/en/docs/index.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 26a6c32ae..a42cedae6 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ The key features are: * **Robust**: Get production-ready code. With automatic interactive documentation. * **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. -* estimation based on tests on an internal development team, building production applications. +* estimation based on tests conducted by an internal development team, building production applications. ## Sponsors diff --git a/docs/en/docs/index.md b/docs/en/docs/index.md index 8a79b26a6..df03b7675 100644 --- a/docs/en/docs/index.md +++ b/docs/en/docs/index.md @@ -46,7 +46,7 @@ The key features are: * **Robust**: Get production-ready code. With automatic interactive documentation. * **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. -* estimation based on tests on an internal development team, building production applications. +* estimation based on tests conducted by an internal development team, building production applications. ## Sponsors { #sponsors } From 0f7ce0b78ad212e54a132e0a57996e3b09bdb87c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 2 Dec 2025 05:04:09 +0000 Subject: [PATCH 176/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 5cb4ce279..6a8f009f8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -14,6 +14,10 @@ hide: * 🐛 Fix parsing extra `Form` parameter list. PR [#14303](https://github.com/fastapi/fastapi/pull/14303) by [@YuriiMotov](https://github.com/YuriiMotov). * 🐛 Fix support for form values with empty strings interpreted as missing (`None` if that's the default), for compatibility with HTML forms. PR [#13537](https://github.com/fastapi/fastapi/pull/13537) by [@MarinPostma](https://github.com/MarinPostma). +### Docs + +* 📝 Clarify estimation note in documentation. PR [#14070](https://github.com/fastapi/fastapi/pull/14070) by [@SaisakthiM](https://github.com/SaisakthiM). + ## 0.123.1 ### Fixes From bb05007f55ef3057d604a9f0861a80a54e642151 Mon Sep 17 00:00:00 2001 From: Flavius <55413297+FlaviusRaducu@users.noreply.github.com> Date: Tue, 2 Dec 2025 05:06:56 +0000 Subject: [PATCH 177/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20Primary=20Key?= =?UTF-8?q?=20notes=20for=20the=20SQL=20databases=20tutorial=20to=20avoid?= =?UTF-8?q?=20confusion=20(#14120)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/tutorial/sql-databases.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/tutorial/sql-databases.md b/docs/en/docs/tutorial/sql-databases.md index cfa1c9073..b42e9ba2f 100644 --- a/docs/en/docs/tutorial/sql-databases.md +++ b/docs/en/docs/tutorial/sql-databases.md @@ -65,7 +65,7 @@ There are a few differences: * `Field(primary_key=True)` tells SQLModel that the `id` is the **primary key** in the SQL database (you can learn more about SQL primary keys in the SQLModel docs). - By having the type as `int | None`, SQLModel will know that this column should be an `INTEGER` in the SQL database and that it should be `NULLABLE`. + **Note:** We use `int | None` for the primary key field so that in Python code we can *create an object without an `id`* (`id=None`), assuming the database will *generate it when saving*. SQLModel understands that the database will provide the `id` and *defines the column as a non-null `INTEGER`* in the database schema. See SQLModel docs on primary keys for details. * `Field(index=True)` tells SQLModel that it should create a **SQL index** for this column, that would allow faster lookups in the database when reading data filtered by this column. From 2ca7709c24358d5ebe61c5fc69235bc030b10128 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 2 Dec 2025 05:07:29 +0000 Subject: [PATCH 178/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 6a8f009f8..be1f56a64 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -16,6 +16,7 @@ hide: ### Docs +* 📝 Update Primary Key notes for the SQL databases tutorial to avoid confusion. PR [#14120](https://github.com/fastapi/fastapi/pull/14120) by [@FlaviusRaducu](https://github.com/FlaviusRaducu). * 📝 Clarify estimation note in documentation. PR [#14070](https://github.com/fastapi/fastapi/pull/14070) by [@SaisakthiM](https://github.com/SaisakthiM). ## 0.123.1 From 00b97526e753614e762cad9128df5df48cb0fd54 Mon Sep 17 00:00:00 2001 From: zadevhub <138465437+zadevhub@users.noreply.github.com> Date: Tue, 2 Dec 2025 05:09:25 +0000 Subject: [PATCH 179/256] =?UTF-8?q?=F0=9F=93=9D=20Add=20tip=20on=20how=20t?= =?UTF-8?q?o=20install=20`pip`=20in=20case=20of=20`No=20module=20named=20p?= =?UTF-8?q?ip`=20error=20in=20`virtual-environments.md`=20(#14211)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/virtual-environments.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/en/docs/virtual-environments.md b/docs/en/docs/virtual-environments.md index e9b0a9fc4..c02e43ab9 100644 --- a/docs/en/docs/virtual-environments.md +++ b/docs/en/docs/virtual-environments.md @@ -242,6 +242,26 @@ $ python -m pip install --upgrade pip +/// tip + +Sometimes, you might get a **`No module named pip`** error when trying to upgrade pip. + +If this happens, install and upgrade pip using the command below: + +
+ +```console +$ python -m ensurepip --upgrade + +---> 100% +``` + +
+ +This command will install pip if it is not already installed and also ensures that the installed version of pip is at least as recent as the one available in `ensurepip`. + +/// + ## Add `.gitignore` { #add-gitignore } If you are using **Git** (you should), add a `.gitignore` file to exclude everything in your `.venv` from Git. From e1a2933739097f2e01d93acd752ba537ca7786cd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 2 Dec 2025 05:09:48 +0000 Subject: [PATCH 180/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index be1f56a64..200e8cc89 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -16,6 +16,7 @@ hide: ### Docs +* 📝 Add tip on how to install `pip` in case of `No module named pip` error in `virtual-environments.md`. PR [#14211](https://github.com/fastapi/fastapi/pull/14211) by [@zadevhub](https://github.com/zadevhub). * 📝 Update Primary Key notes for the SQL databases tutorial to avoid confusion. PR [#14120](https://github.com/fastapi/fastapi/pull/14120) by [@FlaviusRaducu](https://github.com/FlaviusRaducu). * 📝 Clarify estimation note in documentation. PR [#14070](https://github.com/fastapi/fastapi/pull/14070) by [@SaisakthiM](https://github.com/SaisakthiM). From 3c54a8f07b70cdd40e3d81ea319e9fcccd2481d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 2 Dec 2025 06:31:27 +0100 Subject: [PATCH 181/256] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.12?= =?UTF-8?q?3.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 200e8cc89..2e64f0c0a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.123.2 + ### Fixes * 🐛 Fix unformatted `{type_}` in FastAPIError. PR [#14416](https://github.com/fastapi/fastapi/pull/14416) by [@Just-Helpful](https://github.com/Just-Helpful). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 4f6982035..7a6ee14f2 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.123.1" +__version__ = "0.123.2" from starlette import status as status From 0f613d9051ce8a84b19c3786647a6d0cfc7973f6 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Tue, 2 Dec 2025 08:10:27 +0100 Subject: [PATCH 182/256] =?UTF-8?q?=F0=9F=90=9B=20Fix=20optional=20sequenc?= =?UTF-8?q?e=20handling=20in=20`serialize=20sequence=20value`=20with=20Pyd?= =?UTF-8?q?antic=20V2=20(#14297)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- fastapi/_compat/v2.py | 7 +++++++ tests/test_compat.py | 24 ++++++++++++++++++++++++ tests/test_optional_file_list.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 tests/test_optional_file_list.py diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py index 7196a6190..3d91814c0 100644 --- a/fastapi/_compat/v2.py +++ b/fastapi/_compat/v2.py @@ -371,6 +371,13 @@ def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: origin_type = get_origin(field.field_info.annotation) or field.field_info.annotation + if origin_type is Union: # Handle optional sequences + union_args = get_args(field.field_info.annotation) + for union_arg in union_args: + if union_arg is type(None): + continue + origin_type = get_origin(union_arg) or union_arg + break assert issubclass(origin_type, shared.sequence_types) # type: ignore[arg-type] return shared.sequence_annotation_to_type[origin_type](value) # type: ignore[no-any-return] diff --git a/tests/test_compat.py b/tests/test_compat.py index 0184c9a2e..c3a97209a 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -136,6 +136,30 @@ def test_is_uploadfile_sequence_annotation(): assert is_uploadfile_sequence_annotation(Union[List[str], List[UploadFile]]) +@needs_pydanticv2 +def test_serialize_sequence_value_with_optional_list(): + """Test that serialize_sequence_value handles optional lists correctly.""" + from fastapi._compat import v2 + + field_info = FieldInfo(annotation=Union[List[str], None]) + field = v2.ModelField(name="items", field_info=field_info) + result = v2.serialize_sequence_value(field=field, value=["a", "b", "c"]) + assert result == ["a", "b", "c"] + assert isinstance(result, list) + + +@needs_pydanticv2 +def test_serialize_sequence_value_with_none_first_in_union(): + """Test that serialize_sequence_value handles Union[None, List[...]] correctly.""" + from fastapi._compat import v2 + + field_info = FieldInfo(annotation=Union[None, List[str]]) + field = v2.ModelField(name="items", field_info=field_info) + result = v2.serialize_sequence_value(field=field, value=["x", "y"]) + assert result == ["x", "y"] + assert isinstance(result, list) + + @needs_py_lt_314 def test_is_pv1_scalar_field(): from fastapi._compat import v1 diff --git a/tests/test_optional_file_list.py b/tests/test_optional_file_list.py new file mode 100644 index 000000000..0228900cf --- /dev/null +++ b/tests/test_optional_file_list.py @@ -0,0 +1,30 @@ +from typing import List, Optional + +from fastapi import FastAPI, File +from fastapi.testclient import TestClient + +app = FastAPI() + + +@app.post("/files") +async def upload_files(files: Optional[List[bytes]] = File(None)): + if files is None: + return {"files_count": 0} + return {"files_count": len(files), "sizes": [len(f) for f in files]} + + +def test_optional_bytes_list(): + client = TestClient(app) + response = client.post( + "/files", + files=[("files", b"content1"), ("files", b"content2")], + ) + assert response.status_code == 200 + assert response.json() == {"files_count": 2, "sizes": [8, 8]} + + +def test_optional_bytes_list_no_files(): + client = TestClient(app) + response = client.post("/files") + assert response.status_code == 200 + assert response.json() == {"files_count": 0} From eead41bf4cf8587b4d86091dd3eabbb85796e717 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 2 Dec 2025 07:10:50 +0000 Subject: [PATCH 183/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 2e64f0c0a..c0090b49a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Fixes + +* 🐛 Fix optional sequence handling in `serialize sequence value` with Pydantic V2. PR [#14297](https://github.com/fastapi/fastapi/pull/14297) by [@YuriiMotov](https://github.com/YuriiMotov). + ## 0.123.2 ### Fixes From 015b4fae9ce949ce46b285dabce7aa050b370eba Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Tue, 2 Dec 2025 08:24:09 +0100 Subject: [PATCH 184/256] =?UTF-8?q?=F0=9F=90=9B=20Fix=20Query\Header\Cooki?= =?UTF-8?q?e=20parameter=20model=20alias=20(#14360)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- fastapi/dependencies/utils.py | 3 +- tests/test_request_param_model_by_alias.py | 76 ++++++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 tests/test_request_param_model_by_alias.py diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 20cb2c88c..ef3f56417 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -787,9 +787,8 @@ def request_params_to_args( ) value = _get_multidict_value(field, received_params, alias=alias) if value is not None: - params_to_process[field.name] = value + params_to_process[field.alias] = value processed_keys.add(alias or field.alias) - processed_keys.add(field.name) for key in received_params.keys(): if key not in processed_keys: diff --git a/tests/test_request_param_model_by_alias.py b/tests/test_request_param_model_by_alias.py new file mode 100644 index 000000000..a6f759f23 --- /dev/null +++ b/tests/test_request_param_model_by_alias.py @@ -0,0 +1,76 @@ +from dirty_equals import IsPartialDict +from fastapi import Cookie, FastAPI, Header, Query +from fastapi._compat import PYDANTIC_V2 +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field + +app = FastAPI() + + +class Model(BaseModel): + param: str = Field(alias="param_alias") + + +@app.get("/query") +async def query_model(data: Model = Query()): + return {"param": data.param} + + +@app.get("/header") +async def header_model(data: Model = Header()): + return {"param": data.param} + + +@app.get("/cookie") +async def cookie_model(data: Model = Cookie()): + return {"param": data.param} + + +def test_query_model_with_alias(): + client = TestClient(app) + response = client.get("/query", params={"param_alias": "value"}) + assert response.status_code == 200, response.text + assert response.json() == {"param": "value"} + + +def test_header_model_with_alias(): + client = TestClient(app) + response = client.get("/header", headers={"param_alias": "value"}) + assert response.status_code == 200, response.text + assert response.json() == {"param": "value"} + + +def test_cookie_model_with_alias(): + client = TestClient(app) + client.cookies.set("param_alias", "value") + response = client.get("/cookie") + assert response.status_code == 200, response.text + assert response.json() == {"param": "value"} + + +def test_query_model_with_alias_by_name(): + client = TestClient(app) + response = client.get("/query", params={"param": "value"}) + assert response.status_code == 422, response.text + details = response.json() + if PYDANTIC_V2: + assert details["detail"][0]["input"] == {"param": "value"} + + +def test_header_model_with_alias_by_name(): + client = TestClient(app) + response = client.get("/header", headers={"param": "value"}) + assert response.status_code == 422, response.text + details = response.json() + if PYDANTIC_V2: + assert details["detail"][0]["input"] == IsPartialDict({"param": "value"}) + + +def test_cookie_model_with_alias_by_name(): + client = TestClient(app) + client.cookies.set("param", "value") + response = client.get("/cookie") + assert response.status_code == 422, response.text + details = response.json() + if PYDANTIC_V2: + assert details["detail"][0]["input"] == {"param": "value"} From b49c05ec22d949dac549314183994f71c0c8c21f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 2 Dec 2025 07:24:31 +0000 Subject: [PATCH 185/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index c0090b49a..3a82da71d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Fixes +* 🐛 Fix Query\Header\Cookie parameter model alias. PR [#14360](https://github.com/fastapi/fastapi/pull/14360) by [@YuriiMotov](https://github.com/YuriiMotov). * 🐛 Fix optional sequence handling in `serialize sequence value` with Pydantic V2. PR [#14297](https://github.com/fastapi/fastapi/pull/14297) by [@YuriiMotov](https://github.com/YuriiMotov). ## 0.123.2 From c516c9904bfd07a5b48aaac205740bc20e8ec21e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 2 Dec 2025 08:42:22 +0100 Subject: [PATCH 186/256] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.12?= =?UTF-8?q?3.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 3a82da71d..1d42e180a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.123.3 + ### Fixes * 🐛 Fix Query\Header\Cookie parameter model alias. PR [#14360](https://github.com/fastapi/fastapi/pull/14360) by [@YuriiMotov](https://github.com/YuriiMotov). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 7a6ee14f2..a15326cc2 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.123.2" +__version__ = "0.123.3" from starlette import status as status From dcf0299195c7182b0b0f9d07118a157a05490b8d Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Tue, 2 Dec 2025 10:11:29 +0100 Subject: [PATCH 187/256] =?UTF-8?q?=F0=9F=93=9D=20Fix=20docstring=20of=20`?= =?UTF-8?q?servers`=20parameter=20(#14405)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/en/docs/advanced/behind-a-proxy.md | 8 ++++++++ fastapi/applications.py | 7 ++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/advanced/behind-a-proxy.md b/docs/en/docs/advanced/behind-a-proxy.md index f692a28e8..f4dbd4560 100644 --- a/docs/en/docs/advanced/behind-a-proxy.md +++ b/docs/en/docs/advanced/behind-a-proxy.md @@ -443,6 +443,14 @@ The docs UI will interact with the server that you select. /// +/// note | Technical Details + +The `servers` property in the OpenAPI specification is optional. + +If you don't specify the `servers` parameter and `root_path` is equal to `/`, the `servers` property in the generated OpenAPI schema will be omitted entirely by default, which is the equivalent of a single server with a `url` value of `/`. + +/// + ### Disable automatic server from `root_path` { #disable-automatic-server-from-root-path } If you don't want **FastAPI** to include an automatic server using the `root_path`, you can use the parameter `root_path_in_servers=False`: diff --git a/fastapi/applications.py b/fastapi/applications.py index 0a47699ae..02193312b 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -301,7 +301,12 @@ class FastAPI(Starlette): browser tabs open). Or if you want to leave fixed the possible URLs. If the servers `list` is not provided, or is an empty `list`, the - default value would be a `dict` with a `url` value of `/`. + `servers` property in the generated OpenAPI will be: + + * a `dict` with a `url` value of the application's mounting point + (`root_path`) if it's different from `/`. + * otherwise, the `servers` property will be omitted from the OpenAPI + schema. Each item in the `list` is a `dict` containing: From 5126e099bdda51f261fd5a101b5f914991d31897 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 2 Dec 2025 09:11:52 +0000 Subject: [PATCH 188/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 1d42e180a..4eb02ea42 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Docs + +* 📝 Fix docstring of `servers` parameter. PR [#14405](https://github.com/fastapi/fastapi/pull/14405) by [@YuriiMotov](https://github.com/YuriiMotov). + ## 0.123.3 ### Fixes From f95a174288c07182905b6742b973e6e174dac598 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20Graf=C3=A9?= Date: Tue, 2 Dec 2025 01:22:08 -0800 Subject: [PATCH 189/256] =?UTF-8?q?=F0=9F=90=9B=20Fix=20OpenAPI=20schema?= =?UTF-8?q?=20support=20for=20computed=20fields=20when=20using=20`separate?= =?UTF-8?q?=5Finput=5Foutput=5Fschemas=3DFalse`=20(#13207)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sofie Van Landeghem Co-authored-by: svlandeg Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: svlandeg Co-authored-by: Sebastián Ramírez --- fastapi/_compat/v2.py | 14 ++++++++++++-- tests/test_computed_fields.py | 7 +++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py index 3d91814c0..543a42dda 100644 --- a/fastapi/_compat/v2.py +++ b/fastapi/_compat/v2.py @@ -180,8 +180,13 @@ def get_schema_from_model_field( ], separate_input_output_schemas: bool = True, ) -> Dict[str, Any]: + computed_fields = field._type_adapter.core_schema.get("schema", {}).get( + "computed_fields", [] + ) override_mode: Union[Literal["validation"], None] = ( - None if separate_input_output_schemas else "validation" + None + if (separate_input_output_schemas or len(computed_fields) > 0) + else "validation" ) # This expects that GenerateJsonSchema was already used to generate the definitions json_schema = field_mapping[(field, override_mode or field.mode)] @@ -203,9 +208,14 @@ def get_definitions( Dict[Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue], Dict[str, Dict[str, Any]], ]: + has_computed_fields: bool = any( + field._type_adapter.core_schema.get("schema", {}).get("computed_fields", []) + for field in fields + ) + schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE) override_mode: Union[Literal["validation"], None] = ( - None if separate_input_output_schemas else "validation" + None if (separate_input_output_schemas or has_computed_fields) else "validation" ) validation_fields = [field for field in fields if field.mode == "validation"] serialization_fields = [field for field in fields if field.mode == "serialization"] diff --git a/tests/test_computed_fields.py b/tests/test_computed_fields.py index a1b412168..f2e42999b 100644 --- a/tests/test_computed_fields.py +++ b/tests/test_computed_fields.py @@ -6,8 +6,9 @@ from .utils import needs_pydanticv2 @pytest.fixture(name="client") -def get_client(): - app = FastAPI() +def get_client(request): + separate_input_output_schemas = request.param + app = FastAPI(separate_input_output_schemas=separate_input_output_schemas) from pydantic import BaseModel, computed_field @@ -32,6 +33,7 @@ def get_client(): return client +@pytest.mark.parametrize("client", [True, False], indirect=True) @pytest.mark.parametrize("path", ["/", "/responses"]) @needs_pydanticv2 def test_get(client: TestClient, path: str): @@ -40,6 +42,7 @@ def test_get(client: TestClient, path: str): assert response.json() == {"width": 3, "length": 4, "area": 12} +@pytest.mark.parametrize("client", [True, False], indirect=True) @needs_pydanticv2 def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") From fb30cc2f50a896175f705e365430004049af0fe9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 2 Dec 2025 09:22:35 +0000 Subject: [PATCH 190/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 4eb02ea42..6040c4930 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Fixes + +* 🐛 Fix OpenAPI schema support for computed fields when using `separate_input_output_schemas=False`. PR [#13207](https://github.com/fastapi/fastapi/pull/13207) by [@vgrafe](https://github.com/vgrafe). + ### Docs * 📝 Fix docstring of `servers` parameter. PR [#14405](https://github.com/fastapi/fastapi/pull/14405) by [@YuriiMotov](https://github.com/YuriiMotov). From 4976568fc7cb7501729d8f84d946cde1e91299cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 2 Dec 2025 11:47:05 +0100 Subject: [PATCH 191/256] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.12?= =?UTF-8?q?3.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 6040c4930..74c7eb9ea 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.123.4 + ### Fixes * 🐛 Fix OpenAPI schema support for computed fields when using `separate_input_output_schemas=False`. PR [#13207](https://github.com/fastapi/fastapi/pull/13207) by [@vgrafe](https://github.com/vgrafe). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index a15326cc2..b1d2dcecc 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.123.3" +__version__ = "0.123.4" from starlette import status as status From 73c411e1b92a1659fef76655562e6d2f28be064e Mon Sep 17 00:00:00 2001 From: Matthew Martin Date: Tue, 2 Dec 2025 07:34:19 -0600 Subject: [PATCH 192/256] =?UTF-8?q?=E2=9C=A8=20Handle=20wrapped=20dependen?= =?UTF-8?q?cies=20(#9555)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Co-authored-by: Yurii Motov Co-authored-by: Sebastián Ramírez --- fastapi/dependencies/models.py | 22 +++++---- tests/test_dependency_wrapped.py | 77 ++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 8 deletions(-) create mode 100644 tests/test_dependency_wrapped.py diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index fbb666a7d..13486dd18 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -75,27 +75,33 @@ class Dependant: return True return False + @cached_property + def _unwrapped_call(self) -> Any: + if self.call is None: + return self.call # pragma: no cover + return inspect.unwrap(self.call) + @cached_property def is_gen_callable(self) -> bool: - if inspect.isgeneratorfunction(self.call): + if inspect.isgeneratorfunction(self._unwrapped_call): return True - dunder_call = getattr(self.call, "__call__", None) # noqa: B004 + dunder_call = getattr(self._unwrapped_call, "__call__", None) # noqa: B004 return inspect.isgeneratorfunction(dunder_call) @cached_property def is_async_gen_callable(self) -> bool: - if inspect.isasyncgenfunction(self.call): + if inspect.isasyncgenfunction(self._unwrapped_call): return True - dunder_call = getattr(self.call, "__call__", None) # noqa: B004 + dunder_call = getattr(self._unwrapped_call, "__call__", None) # noqa: B004 return inspect.isasyncgenfunction(dunder_call) @cached_property def is_coroutine_callable(self) -> bool: - if inspect.isroutine(self.call): - return iscoroutinefunction(self.call) - if inspect.isclass(self.call): + if inspect.isroutine(self._unwrapped_call): + return iscoroutinefunction(self._unwrapped_call) + if inspect.isclass(self._unwrapped_call): return False - dunder_call = getattr(self.call, "__call__", None) # noqa: B004 + dunder_call = getattr(self._unwrapped_call, "__call__", None) # noqa: B004 return iscoroutinefunction(dunder_call) @cached_property diff --git a/tests/test_dependency_wrapped.py b/tests/test_dependency_wrapped.py new file mode 100644 index 000000000..f581ccba4 --- /dev/null +++ b/tests/test_dependency_wrapped.py @@ -0,0 +1,77 @@ +from functools import wraps +from typing import AsyncGenerator, Generator + +import pytest +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient + + +def noop_wrap(func): + @wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + return wrapper + + +app = FastAPI() + + +@noop_wrap +def wrapped_dependency() -> bool: + return True + + +@noop_wrap +def wrapped_gen_dependency() -> Generator[bool, None, None]: + yield True + + +@noop_wrap +async def async_wrapped_dependency() -> bool: + return True + + +@noop_wrap +async def async_wrapped_gen_dependency() -> AsyncGenerator[bool, None]: + yield True + + +@app.get("/wrapped-dependency/") +async def get_wrapped_dependency(value: bool = Depends(wrapped_dependency)): + return value + + +@app.get("/wrapped-gen-dependency/") +async def get_wrapped_gen_dependency(value: bool = Depends(wrapped_gen_dependency)): + return value + + +@app.get("/async-wrapped-dependency/") +async def get_async_wrapped_dependency(value: bool = Depends(async_wrapped_dependency)): + return value + + +@app.get("/async-wrapped-gen-dependency/") +async def get_async_wrapped_gen_dependency( + value: bool = Depends(async_wrapped_gen_dependency), +): + return value + + +client = TestClient(app) + + +@pytest.mark.parametrize( + "route", + [ + "/wrapped-dependency", + "/wrapped-gen-dependency", + "/async-wrapped-dependency", + "/async-wrapped-gen-dependency", + ], +) +def test_class_dependency(route): + response = client.get(route) + assert response.status_code == 200, response.text + assert response.json() is True From 13a98c99889cdeec97632bd287127f3bbcd94a9f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 2 Dec 2025 13:34:45 +0000 Subject: [PATCH 193/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 74c7eb9ea..5319af0eb 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Features + +* ✨ Handle wrapped dependencies. PR [#9555](https://github.com/fastapi/fastapi/pull/9555) by [@phy1729](https://github.com/phy1729). + ## 0.123.4 ### Fixes From 247ef32e790ef296d8febc3fbc639849ff24b1a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 2 Dec 2025 05:43:31 -0800 Subject: [PATCH 194/256] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20interna?= =?UTF-8?q?ls,=20update=20`is=5Fcoroutine`=20check=20to=20reuse=20internal?= =?UTF-8?q?=20supported=20variants=20(unwrap,=20check=20class)=20(#14434)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/routing.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fastapi/routing.py b/fastapi/routing.py index a8e12eb60..94e8b0722 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -80,9 +80,9 @@ from starlette.websockets import WebSocket from typing_extensions import Annotated, deprecated if sys.version_info >= (3, 13): # pragma: no cover - from inspect import iscoroutinefunction + pass else: # pragma: no cover - from asyncio import iscoroutinefunction + pass # Copy of starlette.routing.request_response modified to include the @@ -308,7 +308,7 @@ def get_request_handler( embed_body_fields: bool = False, ) -> Callable[[Request], Coroutine[Any, Any, Response]]: assert dependant.call is not None, "dependant.call must be a function" - is_coroutine = iscoroutinefunction(dependant.call) + is_coroutine = dependant.is_coroutine_callable is_body_form = body_field and isinstance( body_field.field_info, (params.Form, temp_pydantic_v1_params.Form) ) From f636513390096512dbfc49ce8edf93e9d17eac78 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 2 Dec 2025 13:43:52 +0000 Subject: [PATCH 195/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 5319af0eb..f540ac78e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -11,6 +11,10 @@ hide: * ✨ Handle wrapped dependencies. PR [#9555](https://github.com/fastapi/fastapi/pull/9555) by [@phy1729](https://github.com/phy1729). +### Refactors + +* ♻️ Refactor internals, update `is_coroutine` check to reuse internal supported variants (unwrap, check class). PR [#14434](https://github.com/fastapi/fastapi/pull/14434) by [@tiangolo](https://github.com/tiangolo). + ## 0.123.4 ### Fixes From a79ae3d66fb093adfe9f8d15a53af74b49947562 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 2 Dec 2025 08:48:46 -0800 Subject: [PATCH 196/256] =?UTF-8?q?=F0=9F=94=A5=20Remove=20dangling=20extr?= =?UTF-8?q?a=20condiitonal=20no=20longer=20needed=20(#14435)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/routing.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/fastapi/routing.py b/fastapi/routing.py index 94e8b0722..c10175b16 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -3,7 +3,6 @@ import email.message import functools import inspect import json -import sys from contextlib import AsyncExitStack, asynccontextmanager from enum import Enum, IntEnum from typing import ( @@ -79,11 +78,6 @@ from starlette.types import AppType, ASGIApp, Lifespan, Receive, Scope, Send from starlette.websockets import WebSocket from typing_extensions import Annotated, deprecated -if sys.version_info >= (3, 13): # pragma: no cover - pass -else: # pragma: no cover - pass - # Copy of starlette.routing.request_response modified to include the # dependencies' AsyncExitStack From cff2236dacb7aedcaf2d2de0873cfbc9bb2b897a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 2 Dec 2025 16:49:12 +0000 Subject: [PATCH 197/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f540ac78e..e80df1709 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Refactors +* 🔥 Remove dangling extra condiitonal no longer needed. PR [#14435](https://github.com/fastapi/fastapi/pull/14435) by [@tiangolo](https://github.com/tiangolo). * ♻️ Refactor internals, update `is_coroutine` check to reuse internal supported variants (unwrap, check class). PR [#14434](https://github.com/fastapi/fastapi/pull/14434) by [@tiangolo](https://github.com/tiangolo). ## 0.123.4 From 80d69ae0bb393c728c61c41086b770237b7b676c Mon Sep 17 00:00:00 2001 From: Victorien <65306057+Viicos@users.noreply.github.com> Date: Tue, 2 Dec 2025 17:59:38 +0100 Subject: [PATCH 198/256] =?UTF-8?q?=F0=9F=90=9B=20Fix=20optional=20sequenc?= =?UTF-8?q?e=20handling=20with=20new=20union=20syntax=20from=20Python=203.?= =?UTF-8?q?10=20(#14430)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- fastapi/_compat/v2.py | 4 ++-- tests/test_compat.py | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py index 543a42dda..0faa7d5a8 100644 --- a/fastapi/_compat/v2.py +++ b/fastapi/_compat/v2.py @@ -17,7 +17,7 @@ from typing import ( from fastapi._compat import may_v1, shared from fastapi.openapi.constants import REF_TEMPLATE -from fastapi.types import IncEx, ModelNameMap +from fastapi.types import IncEx, ModelNameMap, UnionType from pydantic import BaseModel, TypeAdapter, create_model from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError from pydantic import PydanticUndefinedAnnotation as PydanticUndefinedAnnotation @@ -381,7 +381,7 @@ def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: origin_type = get_origin(field.field_info.annotation) or field.field_info.annotation - if origin_type is Union: # Handle optional sequences + if origin_type is Union or origin_type is UnionType: # Handle optional sequences union_args = get_args(field.field_info.annotation) for union_arg in union_args: if union_arg is type(None): diff --git a/tests/test_compat.py b/tests/test_compat.py index c3a97209a..26537c5ab 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -14,7 +14,7 @@ from fastapi.testclient import TestClient from pydantic import BaseModel, ConfigDict from pydantic.fields import FieldInfo -from .utils import needs_py_lt_314, needs_pydanticv2 +from .utils import needs_py310, needs_py_lt_314, needs_pydanticv2 @needs_pydanticv2 @@ -148,6 +148,19 @@ def test_serialize_sequence_value_with_optional_list(): assert isinstance(result, list) +@needs_pydanticv2 +@needs_py310 +def test_serialize_sequence_value_with_optional_list_pipe_union(): + """Test that serialize_sequence_value handles optional lists correctly (with new syntax).""" + from fastapi._compat import v2 + + field_info = FieldInfo(annotation=list[str] | None) + field = v2.ModelField(name="items", field_info=field_info) + result = v2.serialize_sequence_value(field=field, value=["a", "b", "c"]) + assert result == ["a", "b", "c"] + assert isinstance(result, list) + + @needs_pydanticv2 def test_serialize_sequence_value_with_none_first_in_union(): """Test that serialize_sequence_value handles Union[None, List[...]] correctly.""" From 930b27e5fa32ce150ddcea61f0c913435ae71c83 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 2 Dec 2025 17:00:02 +0000 Subject: [PATCH 199/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index e80df1709..6fffdc58c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -11,6 +11,10 @@ hide: * ✨ Handle wrapped dependencies. PR [#9555](https://github.com/fastapi/fastapi/pull/9555) by [@phy1729](https://github.com/phy1729). +### Fixes + +* 🐛 Fix optional sequence handling with new union syntax from Python 3.10. PR [#14430](https://github.com/fastapi/fastapi/pull/14430) by [@Viicos](https://github.com/Viicos). + ### Refactors * 🔥 Remove dangling extra condiitonal no longer needed. PR [#14435](https://github.com/fastapi/fastapi/pull/14435) by [@tiangolo](https://github.com/tiangolo). From 1c1e584abd5cb98920bf22966f7a8b79d3ec64be Mon Sep 17 00:00:00 2001 From: "[object Object]" Date: Tue, 2 Dec 2025 09:23:14 -0800 Subject: [PATCH 200/256] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20wrapp?= =?UTF-8?q?ed=20functions=20(e.g.=20`@functools.wraps()`)=20used=20with=20?= =?UTF-8?q?forward=20references=20(#5077)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yurii Karabas <1998uriyyo@gmail.com> Co-authored-by: Sebastián Ramírez --- fastapi/dependencies/utils.py | 6 ++-- tests/forward_reference_type.py | 9 ++++++ .../test_wrapped_method_forward_reference.py | 31 +++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 tests/forward_reference_type.py create mode 100644 tests/test_wrapped_method_forward_reference.py diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index ef3f56417..1a493a9fd 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -192,7 +192,8 @@ def get_flat_params(dependant: Dependant) -> List[ModelField]: def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature: signature = inspect.signature(call) - globalns = getattr(call, "__globals__", {}) + unwrapped = inspect.unwrap(call) + globalns = getattr(unwrapped, "__globals__", {}) typed_params = [ inspect.Parameter( name=param.name, @@ -217,12 +218,13 @@ def get_typed_annotation(annotation: Any, globalns: Dict[str, Any]) -> Any: def get_typed_return_annotation(call: Callable[..., Any]) -> Any: signature = inspect.signature(call) + unwrapped = inspect.unwrap(call) annotation = signature.return_annotation if annotation is inspect.Signature.empty: return None - globalns = getattr(call, "__globals__", {}) + globalns = getattr(unwrapped, "__globals__", {}) return get_typed_annotation(annotation, globalns) diff --git a/tests/forward_reference_type.py b/tests/forward_reference_type.py new file mode 100644 index 000000000..52a0d4a70 --- /dev/null +++ b/tests/forward_reference_type.py @@ -0,0 +1,9 @@ +from pydantic import BaseModel + + +def forwardref_method(input: "ForwardRefModel") -> "ForwardRefModel": + return ForwardRefModel(x=input.x + 1) + + +class ForwardRefModel(BaseModel): + x: int = 0 diff --git a/tests/test_wrapped_method_forward_reference.py b/tests/test_wrapped_method_forward_reference.py new file mode 100644 index 000000000..7403f6002 --- /dev/null +++ b/tests/test_wrapped_method_forward_reference.py @@ -0,0 +1,31 @@ +import functools + +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from .forward_reference_type import forwardref_method + + +def passthrough(f): + @functools.wraps(f) + def method(*args, **kwargs): + return f(*args, **kwargs) + + return method + + +def test_wrapped_method_type_inference(): + """ + Regression test ensuring that when a method imported from another module + is decorated with something that sets the __wrapped__ attribute (functools.wraps), + then the types are still processed correctly, including dereferencing of forward + references. + """ + app = FastAPI() + client = TestClient(app) + app.post("/endpoint")(passthrough(forwardref_method)) + app.post("/endpoint2")(passthrough(passthrough(forwardref_method))) + with client: + response = client.post("/endpoint", json={"input": {"x": 0}}) + response2 = client.post("/endpoint2", json={"input": {"x": 0}}) + assert response.json() == response2.json() == {"x": 1} From 4ade6d62e2274e596e396dfde368d32879d935b6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 2 Dec 2025 17:23:36 +0000 Subject: [PATCH 201/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 6fffdc58c..d46f1d4f4 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Features +* ✨ Add support for wrapped functions (e.g. `@functools.wraps()`) used with forward references. PR [#5077](https://github.com/fastapi/fastapi/pull/5077) by [@lucaswiman](https://github.com/lucaswiman). * ✨ Handle wrapped dependencies. PR [#9555](https://github.com/fastapi/fastapi/pull/9555) by [@phy1729](https://github.com/phy1729). ### Fixes From f4a17b7568514092f8a65e1863a32d5c67a4d842 Mon Sep 17 00:00:00 2001 From: Nils-Hero Lindemann Date: Tue, 2 Dec 2025 18:32:56 +0100 Subject: [PATCH 202/256] =?UTF-8?q?=F0=9F=8C=90=20Sync=20German=20docs=20(?= =?UTF-8?q?#14367)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Sync with #14217 * Sync with #14359 * Sync with #13786 * Sync with #14070 * Sync with #14120 * Sync with #14211 * Sync with #14405 * "to deploy" -> "deployen" The LLM used that translation a lot ithis convinced me that "deployen" it is the better word. "bereitstellen" (or "ausliefern") is still used for "to serve". --------- Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Co-authored-by: Yurii Motov --- docs/de/docs/_llm-test.md | 2 +- .../de/docs/advanced/advanced-dependencies.md | 2 +- docs/de/docs/advanced/behind-a-proxy.md | 12 +++- docs/de/docs/deployment/cloud.md | 20 ++++-- docs/de/docs/deployment/concepts.md | 12 ++-- docs/de/docs/deployment/docker.md | 6 +- docs/de/docs/deployment/fastapicloud.md | 65 +++++++++++++++++++ docs/de/docs/deployment/index.md | 4 +- docs/de/docs/deployment/server-workers.md | 2 +- docs/de/docs/fastapi-cli.md | 2 +- .../authentication-error-status-code.md | 17 +++++ docs/de/docs/index.md | 64 +++++++++++++++++- docs/de/docs/project-generation.md | 2 +- docs/de/docs/tutorial/first-steps.md | 57 ++++++++++++++++ docs/de/docs/tutorial/sql-databases.md | 2 +- docs/de/docs/virtual-environments.md | 20 ++++++ docs/de/llm-prompt.md | 2 + 17 files changed, 264 insertions(+), 27 deletions(-) create mode 100644 docs/de/docs/deployment/fastapicloud.md create mode 100644 docs/de/docs/how-to/authentication-error-status-code.md diff --git a/docs/de/docs/_llm-test.md b/docs/de/docs/_llm-test.md index 72846ef06..3a95f42e8 100644 --- a/docs/de/docs/_llm-test.md +++ b/docs/de/docs/_llm-test.md @@ -443,7 +443,7 @@ Für einige sprachspezifische Anweisungen, siehe z. B. den Abschnitt `### Headin * die Workload * das Deployment -* bereitstellen +* deployen * das SDK * das Software Development Kit diff --git a/docs/de/docs/advanced/advanced-dependencies.md b/docs/de/docs/advanced/advanced-dependencies.md index 2254dcf53..e60df2883 100644 --- a/docs/de/docs/advanced/advanced-dependencies.md +++ b/docs/de/docs/advanced/advanced-dependencies.md @@ -144,7 +144,7 @@ Dies wurde in Version 0.110.0 geändert, um unbehandelten Speicherverbrauch durc ### Hintergrundtasks und Abhängigkeiten mit `yield`, Technische Details { #background-tasks-and-dependencies-with-yield-technical-details } -Vor FastAPI 0.106.0 war das Werfen von Exceptions nach `yield` nicht möglich, der Exit-Code in Abhängigkeiten mit `yield` wurde ausgeführt, nachdem die Response gesendet wurde, sodass [Exceptionhandler](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} bereits ausgeführt worden wären. +Vor FastAPI 0.106.0 war das Werfen von Exceptions nach `yield` nicht möglich, der Exit-Code in Abhängigkeiten mit `yield` wurde ausgeführt, nachdem die Response gesendet wurde, sodass [Exceptionhandler](../tutorial/handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} bereits ausgeführt worden wären. Dies war so designt, hauptsächlich um die Verwendung derselben von Abhängigkeiten „geyieldeten“ Objekte in Hintergrundtasks zu ermöglichen, da der Exit-Code erst ausgeführt wurde, nachdem die Hintergrundtasks abgeschlossen waren. diff --git a/docs/de/docs/advanced/behind-a-proxy.md b/docs/de/docs/advanced/behind-a-proxy.md index 036916cbe..183d0beee 100644 --- a/docs/de/docs/advanced/behind-a-proxy.md +++ b/docs/de/docs/advanced/behind-a-proxy.md @@ -64,7 +64,7 @@ Wenn Sie mehr über HTTPS erfahren möchten, lesen Sie den Leitfaden [Über HTTP /// -### Wie Proxy-Forwarded-Header funktionieren +### Wie Proxy-Forwarded-Header funktionieren { #how-proxy-forwarded-headers-work } Hier ist eine visuelle Darstellung, wie der **Proxy** weitergeleitete Header zwischen dem Client und dem **Anwendungsserver** hinzufügt: @@ -228,7 +228,7 @@ Die Übergabe des `root_path` an `FastAPI` wäre das Äquivalent zur Übergabe d Beachten Sie, dass der Server (Uvicorn) diesen `root_path` für nichts anderes verwendet als für die Weitergabe an die Anwendung. -Aber wenn Sie mit Ihrem Browser auf http://127.0.0.1:8000/app gehen, sehen Sie die normale Response: +Aber wenn Sie mit Ihrem Browser auf http://127.0.0.1:8000/app gehen, sehen Sie die normale Response: ```JSON { @@ -443,6 +443,14 @@ Die Dokumentationsoberfläche interagiert mit dem von Ihnen ausgewählten Server /// +/// note | Technische Details + +Die Eigenschaft `servers` in der OpenAPI-Spezifikation ist optional. + +Wenn Sie den Parameter `servers` nicht angeben und `root_path` den Wert `/` hat, wird die Eigenschaft `servers` im generierten OpenAPI-Schema standardmäßig vollständig weggelassen, was dem Äquivalent eines einzelnen Servers mit einem `url`-Wert von `/` entspricht. + +/// + ### Den automatischen Server von `root_path` deaktivieren { #disable-automatic-server-from-root-path } Wenn Sie nicht möchten, dass **FastAPI** einen automatischen Server inkludiert, welcher `root_path` verwendet, können Sie den Parameter `root_path_in_servers=False` verwenden: diff --git a/docs/de/docs/deployment/cloud.md b/docs/de/docs/deployment/cloud.md index ca1ba3b3b..ad3ff76db 100644 --- a/docs/de/docs/deployment/cloud.md +++ b/docs/de/docs/deployment/cloud.md @@ -1,16 +1,24 @@ -# FastAPI bei Cloudanbietern bereitstellen { #deploy-fastapi-on-cloud-providers } +# FastAPI bei Cloudanbietern deployen { #deploy-fastapi-on-cloud-providers } Sie können praktisch **jeden Cloudanbieter** verwenden, um Ihre FastAPI-Anwendung bereitzustellen. -In den meisten Fällen bieten die großen Cloudanbieter Anleitungen zum Bereitstellen von FastAPI an. +In den meisten Fällen bieten die großen Cloudanbieter Anleitungen zum Deployment von FastAPI an. + +## FastAPI Cloud { #fastapi-cloud } + +**FastAPI Cloud** wurde vom selben Autor und Team hinter **FastAPI** entwickelt. + +Es vereinfacht den Prozess des **Erstellens**, **Deployens** und **Zugreifens** auf eine API mit minimalem Aufwand. + +Es bringt die gleiche **Developer-Experience** beim Erstellen von Apps mit FastAPI auch zum **Deployment** in der Cloud. 🎉 + +FastAPI Cloud ist der Hauptsponsor und Finanzierungsgeber für die *FastAPI and friends* Open-Source-Projekte. ✨ ## Cloudanbieter – Sponsoren { #cloud-providers-sponsors } -Einige Cloudanbieter ✨ [**sponsern FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨, dies stellt die kontinuierliche und gesunde **Entwicklung** von FastAPI und seinem **Ökosystem** sicher. +Einige andere Cloudanbieter ✨ [**sponsern FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨ ebenfalls. 🙇 -Und es zeigt ihr wahres Engagement für FastAPI und seine **Community** (Sie), da sie Ihnen nicht nur einen **guten Service** bieten möchten, sondern auch sicherstellen möchten, dass Sie ein **gutes und gesundes Framework**, FastAPI, haben. 🙇 - -Vielleicht möchten Sie deren Dienste ausprobieren und deren Anleitungen folgen: +Sie könnten diese ebenfalls in Betracht ziehen, deren Anleitungen folgen und ihre Dienste ausprobieren: * Render * Railway diff --git a/docs/de/docs/deployment/concepts.md b/docs/de/docs/deployment/concepts.md index ef0f458a7..dde922805 100644 --- a/docs/de/docs/deployment/concepts.md +++ b/docs/de/docs/deployment/concepts.md @@ -1,6 +1,6 @@ # Deployment-Konzepte { #deployments-concepts } -Bei dem Deployment – der Bereitstellung – einer **FastAPI**-Anwendung, oder eigentlich jeder Art von Web-API, gibt es mehrere Konzepte, die Sie wahrscheinlich interessieren, und mithilfe der Sie die **am besten geeignete** Methode zur **Bereitstellung Ihrer Anwendung** finden können. +Bei dem Deployment – der Bereitstellung – einer **FastAPI**-Anwendung, oder eigentlich jeder Art von Web-API, gibt es mehrere Konzepte, die Sie wahrscheinlich interessieren, und mithilfe der Sie die **am besten geeignete** Methode zum **Deployment Ihrer Anwendung** finden können. Einige wichtige Konzepte sind: @@ -15,11 +15,11 @@ Wir werden sehen, wie diese sich auf das **Deployment** auswirken. Letztendlich besteht das ultimative Ziel darin, **Ihre API-Clients** auf **sichere** Weise zu versorgen, um **Unterbrechungen** zu vermeiden und die **Rechenressourcen** (z. B. entfernte Server/virtuelle Maschinen) so effizient wie möglich zu nutzen. 🚀 -Ich erzähle Ihnen hier etwas mehr über diese **Konzepte**, was Ihnen hoffentlich die **Intuition** gibt, die Sie benötigen, um zu entscheiden, wie Sie Ihre API in sehr unterschiedlichen Umgebungen bereitstellen, möglicherweise sogar in **zukünftigen**, die jetzt noch nicht existieren. +Ich erzähle Ihnen hier etwas mehr über diese **Konzepte**, was Ihnen hoffentlich die **Intuition** gibt, die Sie benötigen, um zu entscheiden, wie Sie Ihre API in sehr unterschiedlichen Umgebungen deployen, möglicherweise sogar in **zukünftigen**, die jetzt noch nicht existieren. -Durch die Berücksichtigung dieser Konzepte können Sie die beste Variante der Bereitstellung **Ihrer eigenen APIs** **evaluieren und konzipieren**. +Durch die Berücksichtigung dieser Konzepte können Sie die beste Variante des Deployments **Ihrer eigenen APIs** **evaluieren und konzipieren**. -In den nächsten Kapiteln werde ich Ihnen mehr **konkrete Rezepte** für die Bereitstellung von FastAPI-Anwendungen geben. +In den nächsten Kapiteln werde ich Ihnen mehr **konkrete Rezepte** für das Deployment von FastAPI-Anwendungen geben. Aber schauen wir uns zunächst einmal diese grundlegenden **konzeptionellen Ideen** an. Diese Konzepte gelten auch für jede andere Art von Web-API. 💡 @@ -271,7 +271,7 @@ In diesem Fall müssen Sie sich darüber keine Sorgen machen. 🤷 ### Beispiele für Strategien für Vorab-Schritte { #examples-of-previous-steps-strategies } -Es hängt **stark** davon ab, wie Sie **Ihr System bereitstellen**, und hängt wahrscheinlich mit der Art und Weise zusammen, wie Sie Programme starten, Neustarts durchführen, usw. +Es hängt **stark** davon ab, wie Sie **Ihr System deployen**, und hängt wahrscheinlich mit der Art und Weise zusammen, wie Sie Programme starten, Neustarts durchführen, usw. Hier sind einige mögliche Ideen: @@ -307,7 +307,7 @@ Sie können einfache Tools wie `htop` verwenden, um die in Ihrem Server verwende ## Zusammenfassung { #recap } -Sie haben hier einige der wichtigsten Konzepte gelesen, die Sie wahrscheinlich berücksichtigen müssen, wenn Sie entscheiden, wie Sie Ihre Anwendung bereitstellen: +Sie haben hier einige der wichtigsten Konzepte gelesen, die Sie wahrscheinlich berücksichtigen müssen, wenn Sie entscheiden, wie Sie Ihre Anwendung deployen: * Sicherheit – HTTPS * Beim Hochfahren ausführen diff --git a/docs/de/docs/deployment/docker.md b/docs/de/docs/deployment/docker.md index 52ac99913..d4b74635d 100644 --- a/docs/de/docs/deployment/docker.md +++ b/docs/de/docs/deployment/docker.md @@ -1,6 +1,6 @@ # FastAPI in Containern – Docker { #fastapi-in-containers-docker } -Beim Deployment von FastAPI-Anwendungen besteht ein gängiger Ansatz darin, ein **Linux-Containerimage** zu erstellen. Normalerweise erfolgt dies mit **Docker**. Sie können dieses Containerimage dann auf eine von mehreren möglichen Arten bereitstellen. +Beim Deployment von FastAPI-Anwendungen besteht ein gängiger Ansatz darin, ein **Linux-Containerimage** zu erstellen. Normalerweise erfolgt dies mit **Docker**. Sie können dieses Containerimage dann auf eine von mehreren möglichen Arten deployen. Die Verwendung von Linux-Containern bietet mehrere Vorteile, darunter **Sicherheit**, **Replizierbarkeit**, **Einfachheit** und andere. @@ -40,7 +40,7 @@ Linux-Container werden mit demselben Linux-Kernel des Hosts (Maschine, virtuelle Auf diese Weise verbrauchen Container **wenig Ressourcen**, eine Menge vergleichbar mit der direkten Ausführung der Prozesse (eine virtuelle Maschine würde viel mehr verbrauchen). -Container verfügen außerdem über ihre eigenen **isoliert** laufenden Prozesse (üblicherweise nur einen Prozess), über ihr eigenes Dateisystem und ihr eigenes Netzwerk, was die Bereitstellung, Sicherheit, Entwicklung usw. vereinfacht. +Container verfügen außerdem über ihre eigenen **isoliert** laufenden Prozesse (üblicherweise nur einen Prozess), über ihr eigenes Dateisystem und ihr eigenes Netzwerk, was Deployment, Sicherheit, Entwicklung usw. vereinfacht. ## Was ist ein Containerimage { #what-is-a-container-image } @@ -598,7 +598,7 @@ Zum Beispiel: * Mit einem **Kubernetes**-Cluster * Mit einem Docker Swarm Mode-Cluster * Mit einem anderen Tool wie Nomad -* Mit einem Cloud-Dienst, der Ihr Containerimage nimmt und es bereitstellt +* Mit einem Cloud-Dienst, der Ihr Containerimage nimmt und es deployt ## Docker-Image mit `uv` { #docker-image-with-uv } diff --git a/docs/de/docs/deployment/fastapicloud.md b/docs/de/docs/deployment/fastapicloud.md new file mode 100644 index 000000000..18c3bb8a4 --- /dev/null +++ b/docs/de/docs/deployment/fastapicloud.md @@ -0,0 +1,65 @@ +# FastAPI Cloud { #fastapi-cloud } + +Sie können Ihre FastAPI-App in der FastAPI Cloud mit **einem einzigen Befehl** deployen – tragen Sie sich in die Warteliste ein, falls noch nicht geschehen. 🚀 + +## Anmelden { #login } + +Stellen Sie sicher, dass Sie bereits ein **FastAPI-Cloud-Konto** haben (wir haben Sie von der Warteliste eingeladen 😉). + +Melden Sie sich dann an: + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +
+ +## Deployen { #deploy } + +Stellen Sie Ihre App jetzt mit **einem einzigen Befehl** bereit: + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +Das war’s! Jetzt können Sie Ihre App unter dieser URL aufrufen. ✨ + +## Über FastAPI Cloud { #about-fastapi-cloud } + +**FastAPI Cloud** wird vom gleichen Autor und Team hinter **FastAPI** entwickelt. + +Es vereinfacht den Prozess des **Erstellens**, **Deployens** und **Nutzens** einer API mit minimalem Aufwand. + +Es bringt die gleiche **Developer-Experience** beim Erstellen von Apps mit FastAPI auch zum **Deployment** in der Cloud. 🎉 + +Es kümmert sich außerdem um das meiste, was beim Deployen einer App nötig ist, zum Beispiel: + +* HTTPS +* Replikation, mit Autoscaling basierend auf Requests +* usw. + +FastAPI Cloud ist Hauptsponsor und Finanzierer der Open-Source-Projekte *FastAPI and friends*. ✨ + +## Bei anderen Cloudanbietern deployen { #deploy-to-other-cloud-providers } + +FastAPI ist Open Source und basiert auf Standards. Sie können FastAPI-Apps bei jedem Cloudanbieter Ihrer Wahl deployen. + +Folgen Sie den Anleitungen Ihres Cloudanbieters, um dort FastAPI-Apps zu deployen. 🤓 + +## Auf den eigenen Server deployen { #deploy-your-own-server } + +Ich werde Ihnen später in diesem **Deployment-Leitfaden** auch alle Details zeigen, sodass Sie verstehen, was passiert, was geschehen muss und wie Sie FastAPI-Apps selbst deployen können, auch auf Ihre eigenen Server. 🤓 diff --git a/docs/de/docs/deployment/index.md b/docs/de/docs/deployment/index.md index 65c76edce..cb3e53746 100644 --- a/docs/de/docs/deployment/index.md +++ b/docs/de/docs/deployment/index.md @@ -14,7 +14,9 @@ Das steht im Gegensatz zu den **Entwicklungsphasen**, in denen Sie ständig den Es gibt mehrere Möglichkeiten, dies zu tun, abhängig von Ihrem spezifischen Anwendungsfall und den von Ihnen verwendeten Tools. -Sie könnten mithilfe einer Kombination von Tools selbst **einen Server bereitstellen**, Sie könnten einen **Cloud-Dienst** nutzen, der einen Teil der Arbeit für Sie erledigt, oder andere mögliche Optionen. +Sie könnten mithilfe einer Kombination von Tools selbst **einen Server deployen**, Sie könnten einen **Cloud-Dienst** nutzen, der einen Teil der Arbeit für Sie erledigt, oder andere mögliche Optionen. + +Zum Beispiel haben wir, das Team hinter FastAPI, **FastAPI Cloud** entwickelt, um das Deployment von FastAPI-Apps in der Cloud so reibungslos wie möglich zu gestalten, mit derselben Developer-Experience wie beim Arbeiten mit FastAPI. Ich zeige Ihnen einige der wichtigsten Konzepte, die Sie beim Deployment einer **FastAPI**-Anwendung wahrscheinlich berücksichtigen sollten (obwohl das meiste davon auch für jede andere Art von Webanwendung gilt). diff --git a/docs/de/docs/deployment/server-workers.md b/docs/de/docs/deployment/server-workers.md index 169ed822b..7b68f1b1a 100644 --- a/docs/de/docs/deployment/server-workers.md +++ b/docs/de/docs/deployment/server-workers.md @@ -11,7 +11,7 @@ Schauen wir uns die Deployment-Konzepte von früher noch einmal an: Bis zu diesem Punkt, in allen Tutorials in der Dokumentation, haben Sie wahrscheinlich ein **Serverprogramm** ausgeführt, zum Beispiel mit dem `fastapi`-Befehl, der Uvicorn startet, und einen **einzelnen Prozess** ausführt. -Wenn Sie Anwendungen bereitstellen, möchten Sie wahrscheinlich eine gewisse **Replikation von Prozessen**, um **mehrere Kerne** zu nutzen und mehr Requests bearbeiten zu können. +Wenn Sie Anwendungen deployen, möchten Sie wahrscheinlich eine gewisse **Replikation von Prozessen**, um **mehrere Kerne** zu nutzen und mehr Requests bearbeiten zu können. Wie Sie im vorherigen Kapitel über [Deployment-Konzepte](concepts.md){.internal-link target=_blank} gesehen haben, gibt es mehrere Strategien, die Sie anwenden können. diff --git a/docs/de/docs/fastapi-cli.md b/docs/de/docs/fastapi-cli.md index ab9c8373e..86a797a9e 100644 --- a/docs/de/docs/fastapi-cli.md +++ b/docs/de/docs/fastapi-cli.md @@ -66,7 +66,7 @@ Das Ausführen von `fastapi run` startet FastAPI standardmäßig im Produktionsm Standardmäßig ist **Autoreload** deaktiviert. Es horcht auch auf der IP-Adresse `0.0.0.0`, was alle verfügbaren IP-Adressen bedeutet, so wird es öffentlich zugänglich für jeden, der mit der Maschine kommunizieren kann. So würden Sie es normalerweise in der Produktion ausführen, beispielsweise in einem Container. -In den meisten Fällen würden (und sollten) Sie einen „Terminierungsproxy“ haben, der HTTPS für Sie verwaltet. Dies hängt davon ab, wie Sie Ihre Anwendung bereitstellen. Ihr Anbieter könnte dies für Sie erledigen, oder Sie müssen es selbst einrichten. +In den meisten Fällen würden (und sollten) Sie einen „Terminierungsproxy“ haben, der HTTPS für Sie verwaltet. Dies hängt davon ab, wie Sie Ihre Anwendung deployen. Ihr Anbieter könnte dies für Sie erledigen, oder Sie müssen es selbst einrichten. /// tip | Tipp diff --git a/docs/de/docs/how-to/authentication-error-status-code.md b/docs/de/docs/how-to/authentication-error-status-code.md new file mode 100644 index 000000000..c743b54d9 --- /dev/null +++ b/docs/de/docs/how-to/authentication-error-status-code.md @@ -0,0 +1,17 @@ +# Alte 403-Authentifizierungsfehler-Statuscodes verwenden { #use-old-403-authentication-error-status-codes } + +Vor FastAPI-Version `0.122.0` verwendeten die integrierten Sicherheits-Utilities den HTTP-Statuscode `403 Forbidden`, wenn sie dem Client nach einer fehlgeschlagenen Authentifizierung einen Fehler zurückgaben. + +Ab FastAPI-Version `0.122.0` verwenden sie den passenderen HTTP-Statuscode `401 Unauthorized` und geben in der Response einen sinnvollen `WWW-Authenticate`-Header zurück, gemäß den HTTP-Spezifikationen, RFC 7235, RFC 9110. + +Aber falls Ihre Clients aus irgendeinem Grund vom alten Verhalten abhängen, können Sie darauf zurückgreifen, indem Sie in Ihren Sicherheitsklassen die Methode `make_not_authenticated_error` überschreiben. + +Sie können beispielsweise eine Unterklasse von `HTTPBearer` erstellen, die einen Fehler `403 Forbidden` zurückgibt, statt des Default-`401 Unauthorized`-Fehlers: + +{* ../../docs_src/authentication_error_status_code/tutorial001_an_py39.py hl[9:13] *} + +/// tip | Tipp + +Beachten Sie, dass die Funktion die Exception-Instanz zurückgibt; sie wirft sie nicht. Das Werfen erfolgt im restlichen internen Code. + +/// diff --git a/docs/de/docs/index.md b/docs/de/docs/index.md index 4be65071b..efa34652c 100644 --- a/docs/de/docs/index.md +++ b/docs/de/docs/index.md @@ -46,20 +46,26 @@ Seine Schlüssel-Merkmale sind: * **Robust**: Erhalten Sie produktionsreifen Code. Mit automatischer, interaktiver Dokumentation. * **Standards-basiert**: Basierend auf (und vollständig kompatibel mit) den offenen Standards für APIs: OpenAPI (früher bekannt als Swagger) und JSON Schema. -* Schätzung basierend auf Tests in einem internen Entwicklungsteam, das Produktionsanwendungen erstellt. +* Schätzung basierend auf Tests, die von einem internen Entwicklungsteam durchgeführt wurden, das Produktionsanwendungen erstellt. ## Sponsoren { #sponsors } -{% if sponsors %} +### Keystone-Sponsor + +{% for sponsor in sponsors.keystone -%} + +{% endfor -%} + +### Gold- und Silber-Sponsoren + {% for sponsor in sponsors.gold -%} {% endfor -%} {%- for sponsor in sponsors.silver -%} {% endfor %} -{% endif %} @@ -444,6 +450,58 @@ Für ein vollständigeres Beispiel, mit weiteren Funktionen, siehe das FastAPI Cloud deployen, treten Sie der Warteliste bei, falls noch nicht geschehen. 🚀 + +Wenn Sie bereits ein **FastAPI Cloud**-Konto haben (wir haben Sie von der Warteliste eingeladen 😉), können Sie Ihre Anwendung mit einem einzigen Befehl deployen. + +Stellen Sie vor dem Deployen sicher, dass Sie eingeloggt sind: + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +
+ +Stellen Sie dann Ihre App bereit: + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +Das war’s! Jetzt können Sie unter dieser URL auf Ihre App zugreifen. ✨ + +#### Über FastAPI Cloud { #about-fastapi-cloud } + +**FastAPI Cloud** wird vom selben Autor und Team hinter **FastAPI** entwickelt. + +Es vereinfacht den Prozess des **Erstellens**, **Deployens** und **Zugreifens** auf eine API mit minimalem Aufwand. + +Es bringt die gleiche **Developer-Experience** beim Erstellen von Apps mit FastAPI auch zum **Deployment** in der Cloud. 🎉 + +FastAPI Cloud ist der Hauptsponsor und Finanzierer der „FastAPI and friends“ Open-Source-Projekte. ✨ + +#### Bei anderen Cloudanbietern deployen { #deploy-to-other-cloud-providers } + +FastAPI ist Open Source und basiert auf Standards. Sie können FastAPI-Apps bei jedem Cloudanbieter Ihrer Wahl deployen. + +Folgen Sie den Anleitungen Ihres Cloudanbieters, um FastAPI-Apps dort bereitzustellen. 🤓 + ## Performanz { #performance } Unabhängige TechEmpower-Benchmarks zeigen **FastAPI**-Anwendungen, die unter Uvicorn laufen, als eines der schnellsten verfügbaren Python-Frameworks, nur hinter Starlette und Uvicorn selbst (intern von FastAPI verwendet). (*) diff --git a/docs/de/docs/project-generation.md b/docs/de/docs/project-generation.md index e6da4949c..f830f0f4d 100644 --- a/docs/de/docs/project-generation.md +++ b/docs/de/docs/project-generation.md @@ -25,4 +25,4 @@ GitHub-Repository: FastAPI Cloud deployen, treten Sie der Warteliste bei, falls Sie es noch nicht getan haben. 🚀 + +Wenn Sie bereits ein **FastAPI Cloud**-Konto haben (wir haben Sie von der Warteliste eingeladen 😉), können Sie Ihre Anwendung mit einem Befehl deployen. + +Vor dem Deployen, stellen Sie sicher, dass Sie eingeloggt sind: + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +
+ +Dann stellen Sie Ihre App bereit: + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +Das war's! Jetzt können Sie Ihre App unter dieser URL aufrufen. ✨ + ## Zusammenfassung, Schritt für Schritt { #recap-step-by-step } ### Schritt 1: `FastAPI` importieren { #step-1-import-fastapi } @@ -314,6 +350,26 @@ Sie können auch Pydantic-Modelle zurückgeben (dazu später mehr). Es gibt viele andere Objekte und Modelle, die automatisch zu JSON konvertiert werden (einschließlich ORMs, usw.). Versuchen Sie, Ihre Lieblingsobjekte zu verwenden. Es ist sehr wahrscheinlich, dass sie bereits unterstützt werden. +### Schritt 6: Deployen { #step-6-deploy-it } + +Stellen Sie Ihre App in der **FastAPI Cloud** mit einem Befehl bereit: `fastapi deploy`. 🎉 + +#### Über FastAPI Cloud { #about-fastapi-cloud } + +**FastAPI Cloud** wird vom selben Autor und Team hinter **FastAPI** entwickelt. + +Es vereinfacht den Prozess des Erstellens, Deployens und des Zugriffs auf eine API mit minimalem Aufwand. + +Es bringt die gleiche **Developer-Experience** beim Erstellen von Apps mit FastAPI auch zum **Deployment** in der Cloud. 🎉 + +FastAPI Cloud ist der Hauptsponsor und Finanzierer der „FastAPI and friends“ Open-Source-Projekte. ✨ + +#### Zu anderen Cloudanbietern deployen { #deploy-to-other-cloud-providers } + +FastAPI ist Open Source und basiert auf Standards. Sie können FastAPI-Apps bei jedem Cloudanbieter Ihrer Wahl deployen. + +Folgen Sie den Anleitungen Ihres Cloudanbieters, um dort FastAPI-Apps bereitzustellen. 🤓 + ## Zusammenfassung { #recap } * Importieren Sie `FastAPI`. @@ -321,3 +377,4 @@ Es gibt viele andere Objekte und Modelle, die automatisch zu JSON konvertiert we * Schreiben Sie einen **Pfadoperation-Dekorator** unter Verwendung von Dekoratoren wie `@app.get("/")`. * Definieren Sie eine **Pfadoperation-Funktion**, zum Beispiel `def root(): ...`. * Starten Sie den Entwicklungsserver mit dem Befehl `fastapi dev`. +* Optional: Ihre App mit `fastapi deploy` deployen. diff --git a/docs/de/docs/tutorial/sql-databases.md b/docs/de/docs/tutorial/sql-databases.md index cf9731aee..3af4ecdfc 100644 --- a/docs/de/docs/tutorial/sql-databases.md +++ b/docs/de/docs/tutorial/sql-databases.md @@ -65,7 +65,7 @@ Es gibt ein paar Unterschiede: * `Field(primary_key=True)` sagt SQLModel, dass die `id` der **Primärschlüssel** in der SQL-Datenbank ist (Sie können mehr über SQL-Primärschlüssel in der SQLModel-Dokumentation erfahren). - Durch das Festlegen des Typs als `int | None` wird SQLModel wissen, dass diese Spalte ein `INTEGER` in der SQL-Datenbank sein sollte und dass sie `NULLABLE` sein sollte. + **Hinweis:** Wir verwenden für das Primärschlüsselfeld `int | None`, damit wir im Python-Code *ein Objekt ohne `id` erstellen* können (`id=None`), in der Annahme, dass die Datenbank sie *beim Speichern generiert*. SQLModel versteht, dass die Datenbank die `id` bereitstellt, und *definiert die Spalte im Datenbankschema als ein Nicht-Null-`INTEGER`*. Siehe die SQLModel-Dokumentation zu Primärschlüsseln für Details. * `Field(index=True)` sagt SQLModel, dass es einen **SQL-Index** für diese Spalte erstellen soll, was schnelleres Suchen in der Datenbank ermöglicht, wenn Daten mittels dieser Spalte gefiltert werden. diff --git a/docs/de/docs/virtual-environments.md b/docs/de/docs/virtual-environments.md index 497f1b44d..11da496c5 100644 --- a/docs/de/docs/virtual-environments.md +++ b/docs/de/docs/virtual-environments.md @@ -242,6 +242,26 @@ $ python -m pip install --upgrade pip +/// tip | Tipp + +Manchmal kann beim Versuch, `pip` zu aktualisieren, der Fehler **`No module named pip`** auftreten. + +Wenn das passiert, installieren und aktualisieren Sie `pip` mit dem folgenden Befehl: + +
+ +```console +$ python -m ensurepip --upgrade + +---> 100% +``` + +
+ +Dieser Befehl installiert `pip`, falls es noch nicht installiert ist, und stellt außerdem sicher, dass die installierte Version von `pip` mindestens so aktuell ist wie die in `ensurepip` verfügbare. + +/// + ## `.gitignore` hinzufügen { #add-gitignore } Wenn Sie **Git** verwenden (was Sie sollten), fügen Sie eine `.gitignore`-Datei hinzu, um alles in Ihrem `.venv` von Git auszuschließen. diff --git a/docs/de/llm-prompt.md b/docs/de/llm-prompt.md index df202d2ff..5df904ac7 100644 --- a/docs/de/llm-prompt.md +++ b/docs/de/llm-prompt.md @@ -255,6 +255,7 @@ Below is a list of English terms and their preferred German translations, separa * «the default value»: «der Defaultwert» * «the default value»: NOT «der Standardwert» * «the default declaration»: «die Default-Deklaration» +* «the deployment»: «das Deployment» * «the dict»: «das Dict» * «the dictionary»: «das Dictionary» * «the enumeration»: «die Enumeration» @@ -316,6 +317,7 @@ Below is a list of English terms and their preferred German translations, separa * «the worker process»: «der Workerprozess» * «the worker process»: NOT «der Arbeiterprozess» * «to commit»: «committen» +* «to deploy» (in the cloud): «deployen» * «to modify»: «ändern» * «to serve» (an application): «bereitstellen» * «to serve» (a response): «ausliefern» From aee8e78078e8a7f2736d3cab7a1cb7197356951c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 2 Dec 2025 17:33:22 +0000 Subject: [PATCH 203/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index d46f1d4f4..98ebbcf9b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -21,6 +21,10 @@ hide: * 🔥 Remove dangling extra condiitonal no longer needed. PR [#14435](https://github.com/fastapi/fastapi/pull/14435) by [@tiangolo](https://github.com/tiangolo). * ♻️ Refactor internals, update `is_coroutine` check to reuse internal supported variants (unwrap, check class). PR [#14434](https://github.com/fastapi/fastapi/pull/14434) by [@tiangolo](https://github.com/tiangolo). +### Translations + +* 🌐 Sync German docs. PR [#14367](https://github.com/fastapi/fastapi/pull/14367) by [@nilslindemann](https://github.com/nilslindemann). + ## 0.123.4 ### Fixes From 982448661630e2f412f18a192eed29d0da4a8fb1 Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Wed, 3 Dec 2025 07:58:30 +1100 Subject: [PATCH 204/256] =?UTF-8?q?=E2=9C=A8=20Allow=20using=20dependables?= =?UTF-8?q?=20with=20`functools.partial()`=20(#9753)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Co-authored-by: Yurii Motov Co-authored-by: Sebastián Ramírez --- fastapi/dependencies/models.py | 7 +- tests/test_dependency_partial.py | 251 +++++++++++++++++++++++++++++++ 2 files changed, 256 insertions(+), 2 deletions(-) create mode 100644 tests/test_dependency_partial.py diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index 13486dd18..2a4d9a010 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -1,7 +1,7 @@ import inspect import sys from dataclasses import dataclass, field -from functools import cached_property +from functools import cached_property, partial from typing import Any, Callable, List, Optional, Sequence, Union from fastapi._compat import ModelField @@ -79,7 +79,10 @@ class Dependant: def _unwrapped_call(self) -> Any: if self.call is None: return self.call # pragma: no cover - return inspect.unwrap(self.call) + unwrapped = inspect.unwrap(self.call) + if isinstance(unwrapped, partial): + unwrapped = unwrapped.func + return unwrapped @cached_property def is_gen_callable(self) -> bool: diff --git a/tests/test_dependency_partial.py b/tests/test_dependency_partial.py new file mode 100644 index 000000000..61a76236f --- /dev/null +++ b/tests/test_dependency_partial.py @@ -0,0 +1,251 @@ +from functools import partial +from typing import AsyncGenerator, Generator + +import pytest +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient +from typing_extensions import Annotated + +app = FastAPI() + + +def function_dependency(value: str) -> str: + return value + + +async def async_function_dependency(value: str) -> str: + return value + + +def gen_dependency(value: str) -> Generator[str, None, None]: + yield value + + +async def async_gen_dependency(value: str) -> AsyncGenerator[str, None]: + yield value + + +class CallableDependency: + def __call__(self, value: str) -> str: + return value + + +class CallableGenDependency: + def __call__(self, value: str) -> Generator[str, None, None]: + yield value + + +class AsyncCallableDependency: + async def __call__(self, value: str) -> str: + return value + + +class AsyncCallableGenDependency: + async def __call__(self, value: str) -> AsyncGenerator[str, None]: + yield value + + +class MethodsDependency: + def synchronous(self, value: str) -> str: + return value + + async def asynchronous(self, value: str) -> str: + return value + + def synchronous_gen(self, value: str) -> Generator[str, None, None]: + yield value + + async def asynchronous_gen(self, value: str) -> AsyncGenerator[str, None]: + yield value + + +callable_dependency = CallableDependency() +callable_gen_dependency = CallableGenDependency() +async_callable_dependency = AsyncCallableDependency() +async_callable_gen_dependency = AsyncCallableGenDependency() +methods_dependency = MethodsDependency() + + +@app.get("/partial-function-dependency") +async def get_partial_function_dependency( + value: Annotated[ + str, Depends(partial(function_dependency, "partial-function-dependency")) + ], +) -> str: + return value + + +@app.get("/partial-async-function-dependency") +async def get_partial_async_function_dependency( + value: Annotated[ + str, + Depends( + partial(async_function_dependency, "partial-async-function-dependency") + ), + ], +) -> str: + return value + + +@app.get("/partial-gen-dependency") +async def get_partial_gen_dependency( + value: Annotated[str, Depends(partial(gen_dependency, "partial-gen-dependency"))], +) -> str: + return value + + +@app.get("/partial-async-gen-dependency") +async def get_partial_async_gen_dependency( + value: Annotated[ + str, Depends(partial(async_gen_dependency, "partial-async-gen-dependency")) + ], +) -> str: + return value + + +@app.get("/partial-callable-dependency") +async def get_partial_callable_dependency( + value: Annotated[ + str, Depends(partial(callable_dependency, "partial-callable-dependency")) + ], +) -> str: + return value + + +@app.get("/partial-callable-gen-dependency") +async def get_partial_callable_gen_dependency( + value: Annotated[ + str, + Depends(partial(callable_gen_dependency, "partial-callable-gen-dependency")), + ], +) -> str: + return value + + +@app.get("/partial-async-callable-dependency") +async def get_partial_async_callable_dependency( + value: Annotated[ + str, + Depends( + partial(async_callable_dependency, "partial-async-callable-dependency") + ), + ], +) -> str: + return value + + +@app.get("/partial-async-callable-gen-dependency") +async def get_partial_async_callable_gen_dependency( + value: Annotated[ + str, + Depends( + partial( + async_callable_gen_dependency, "partial-async-callable-gen-dependency" + ) + ), + ], +) -> str: + return value + + +@app.get("/partial-synchronous-method-dependency") +async def get_partial_synchronous_method_dependency( + value: Annotated[ + str, + Depends( + partial( + methods_dependency.synchronous, "partial-synchronous-method-dependency" + ) + ), + ], +) -> str: + return value + + +@app.get("/partial-synchronous-method-gen-dependency") +async def get_partial_synchronous_method_gen_dependency( + value: Annotated[ + str, + Depends( + partial( + methods_dependency.synchronous_gen, + "partial-synchronous-method-gen-dependency", + ) + ), + ], +) -> str: + return value + + +@app.get("/partial-asynchronous-method-dependency") +async def get_partial_asynchronous_method_dependency( + value: Annotated[ + str, + Depends( + partial( + methods_dependency.asynchronous, + "partial-asynchronous-method-dependency", + ) + ), + ], +) -> str: + return value + + +@app.get("/partial-asynchronous-method-gen-dependency") +async def get_partial_asynchronous_method_gen_dependency( + value: Annotated[ + str, + Depends( + partial( + methods_dependency.asynchronous_gen, + "partial-asynchronous-method-gen-dependency", + ) + ), + ], +) -> str: + return value + + +client = TestClient(app) + + +@pytest.mark.parametrize( + "route,value", + [ + ("/partial-function-dependency", "partial-function-dependency"), + ( + "/partial-async-function-dependency", + "partial-async-function-dependency", + ), + ("/partial-gen-dependency", "partial-gen-dependency"), + ("/partial-async-gen-dependency", "partial-async-gen-dependency"), + ("/partial-callable-dependency", "partial-callable-dependency"), + ("/partial-callable-gen-dependency", "partial-callable-gen-dependency"), + ("/partial-async-callable-dependency", "partial-async-callable-dependency"), + ( + "/partial-async-callable-gen-dependency", + "partial-async-callable-gen-dependency", + ), + ( + "/partial-synchronous-method-dependency", + "partial-synchronous-method-dependency", + ), + ( + "/partial-synchronous-method-gen-dependency", + "partial-synchronous-method-gen-dependency", + ), + ( + "/partial-asynchronous-method-dependency", + "partial-asynchronous-method-dependency", + ), + ( + "/partial-asynchronous-method-gen-dependency", + "partial-asynchronous-method-gen-dependency", + ), + ], +) +def test_dependency_types_with_partial(route: str, value: str) -> None: + response = client.get(route) + assert response.status_code == 200, response.text + assert response.json() == value From 3c440c762a8b49d13f54fb586467e7870f9604b3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 2 Dec 2025 20:58:53 +0000 Subject: [PATCH 205/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 98ebbcf9b..529c80160 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Features +* ✨ Allow using dependables with `functools.partial()`. PR [#9753](https://github.com/fastapi/fastapi/pull/9753) by [@lieryan](https://github.com/lieryan). * ✨ Add support for wrapped functions (e.g. `@functools.wraps()`) used with forward references. PR [#5077](https://github.com/fastapi/fastapi/pull/5077) by [@lucaswiman](https://github.com/lucaswiman). * ✨ Handle wrapped dependencies. PR [#9555](https://github.com/fastapi/fastapi/pull/9555) by [@phy1729](https://github.com/phy1729). From c57ac7bdf3613798c94caceff562a41fa16d4a2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 2 Dec 2025 22:06:25 +0100 Subject: [PATCH 206/256] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.12?= =?UTF-8?q?3.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 529c80160..c397528aa 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.123.5 + ### Features * ✨ Allow using dependables with `functools.partial()`. PR [#9753](https://github.com/fastapi/fastapi/pull/9753) by [@lieryan](https://github.com/lieryan). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index b1d2dcecc..80b2a99c1 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.123.4" +__version__ = "0.123.5" from starlette import status as status From bba4d4c95e0de8fc9c99f88c269909d37d64d494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 3 Dec 2025 23:29:28 -0800 Subject: [PATCH 207/256] =?UTF-8?q?=F0=9F=90=9B=20Fix=20support=20for=20fu?= =?UTF-8?q?nctools=20wraps=20and=20partial=20combined,=20for=20async=20and?= =?UTF-8?q?=20regular=20functions=20and=20classes=20in=20path=20operations?= =?UTF-8?q?=20and=20dependencies=20(#14448)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yurii Motov --- fastapi/dependencies/models.py | 100 ++++++-- fastapi/dependencies/utils.py | 6 +- tests/test_dependency_wrapped.py | 380 ++++++++++++++++++++++++++++++- 3 files changed, 458 insertions(+), 28 deletions(-) diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index 2a4d9a010..9b545e4e5 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -15,6 +15,19 @@ else: # pragma: no cover from asyncio import iscoroutinefunction +def _unwrapped_call(call: Optional[Callable[..., Any]]) -> Any: + if call is None: + return call # pragma: no cover + unwrapped = inspect.unwrap(_impartial(call)) + return unwrapped + + +def _impartial(func: Callable[..., Any]) -> Callable[..., Any]: + while isinstance(func, partial): + func = func.func + return func + + @dataclass class SecurityRequirement: security_scheme: SecurityBase @@ -75,37 +88,82 @@ class Dependant: return True return False - @cached_property - def _unwrapped_call(self) -> Any: - if self.call is None: - return self.call # pragma: no cover - unwrapped = inspect.unwrap(self.call) - if isinstance(unwrapped, partial): - unwrapped = unwrapped.func - return unwrapped - @cached_property def is_gen_callable(self) -> bool: - if inspect.isgeneratorfunction(self._unwrapped_call): + if self.call is None: + return False # pragma: no cover + if inspect.isgeneratorfunction( + _impartial(self.call) + ) or inspect.isgeneratorfunction(_unwrapped_call(self.call)): return True - dunder_call = getattr(self._unwrapped_call, "__call__", None) # noqa: B004 - return inspect.isgeneratorfunction(dunder_call) + dunder_call = getattr(_impartial(self.call), "__call__", None) # noqa: B004 + if dunder_call is None: + return False # pragma: no cover + if inspect.isgeneratorfunction( + _impartial(dunder_call) + ) or inspect.isgeneratorfunction(_unwrapped_call(dunder_call)): + return True + dunder_unwrapped_call = getattr(_unwrapped_call(self.call), "__call__", None) # noqa: B004 + if dunder_unwrapped_call is None: + return False # pragma: no cover + if inspect.isgeneratorfunction( + _impartial(dunder_unwrapped_call) + ) or inspect.isgeneratorfunction(_unwrapped_call(dunder_unwrapped_call)): + return True + return False @cached_property def is_async_gen_callable(self) -> bool: - if inspect.isasyncgenfunction(self._unwrapped_call): + if self.call is None: + return False # pragma: no cover + if inspect.isasyncgenfunction( + _impartial(self.call) + ) or inspect.isasyncgenfunction(_unwrapped_call(self.call)): return True - dunder_call = getattr(self._unwrapped_call, "__call__", None) # noqa: B004 - return inspect.isasyncgenfunction(dunder_call) + dunder_call = getattr(_impartial(self.call), "__call__", None) # noqa: B004 + if dunder_call is None: + return False # pragma: no cover + if inspect.isasyncgenfunction( + _impartial(dunder_call) + ) or inspect.isasyncgenfunction(_unwrapped_call(dunder_call)): + return True + dunder_unwrapped_call = getattr(_unwrapped_call(self.call), "__call__", None) # noqa: B004 + if dunder_unwrapped_call is None: + return False # pragma: no cover + if inspect.isasyncgenfunction( + _impartial(dunder_unwrapped_call) + ) or inspect.isasyncgenfunction(_unwrapped_call(dunder_unwrapped_call)): + return True + return False @cached_property def is_coroutine_callable(self) -> bool: - if inspect.isroutine(self._unwrapped_call): - return iscoroutinefunction(self._unwrapped_call) - if inspect.isclass(self._unwrapped_call): - return False - dunder_call = getattr(self._unwrapped_call, "__call__", None) # noqa: B004 - return iscoroutinefunction(dunder_call) + if self.call is None: + return False # pragma: no cover + if inspect.isroutine(_impartial(self.call)) and iscoroutinefunction( + _impartial(self.call) + ): + return True + if inspect.isroutine(_unwrapped_call(self.call)) and iscoroutinefunction( + _unwrapped_call(self.call) + ): + return True + dunder_call = getattr(_impartial(self.call), "__call__", None) # noqa: B004 + if dunder_call is None: + return False # pragma: no cover + if iscoroutinefunction(_impartial(dunder_call)) or iscoroutinefunction( + _unwrapped_call(dunder_call) + ): + return True + dunder_unwrapped_call = getattr(_unwrapped_call(self.call), "__call__", None) # noqa: B004 + if dunder_unwrapped_call is None: + return False # pragma: no cover + if iscoroutinefunction( + _impartial(dunder_unwrapped_call) + ) or iscoroutinefunction(_unwrapped_call(dunder_unwrapped_call)): + return True + # if inspect.isclass(self.call): False, covered by default return + return False @cached_property def computed_scope(self) -> Union[str, None]: diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 1a493a9fd..91348c8ea 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -548,10 +548,10 @@ async def _solve_generator( *, dependant: Dependant, stack: AsyncExitStack, sub_values: Dict[str, Any] ) -> Any: assert dependant.call - if dependant.is_gen_callable: - cm = contextmanager_in_threadpool(contextmanager(dependant.call)(**sub_values)) - elif dependant.is_async_gen_callable: + if dependant.is_async_gen_callable: cm = asynccontextmanager(dependant.call)(**sub_values) + elif dependant.is_gen_callable: + cm = contextmanager_in_threadpool(contextmanager(dependant.call)(**sub_values)) return await stack.enter_async_context(cm) diff --git a/tests/test_dependency_wrapped.py b/tests/test_dependency_wrapped.py index f581ccba4..08356712d 100644 --- a/tests/test_dependency_wrapped.py +++ b/tests/test_dependency_wrapped.py @@ -1,10 +1,18 @@ +import inspect +import sys from functools import wraps from typing import AsyncGenerator, Generator import pytest from fastapi import Depends, FastAPI +from fastapi.concurrency import iterate_in_threadpool, run_in_threadpool from fastapi.testclient import TestClient +if sys.version_info >= (3, 13): # pragma: no cover + from inspect import iscoroutinefunction +else: # pragma: no cover + from asyncio import iscoroutinefunction + def noop_wrap(func): @wraps(func) @@ -14,8 +22,163 @@ def noop_wrap(func): return wrapper +def noop_wrap_async(func): + if inspect.isgeneratorfunction(func): + + @wraps(func) + async def gen_wrapper(*args, **kwargs): + async for item in iterate_in_threadpool(func(*args, **kwargs)): + yield item + + return gen_wrapper + + elif inspect.isasyncgenfunction(func): + + @wraps(func) + async def async_gen_wrapper(*args, **kwargs): + async for item in func(*args, **kwargs): + yield item + + return async_gen_wrapper + + @wraps(func) + async def wrapper(*args, **kwargs): + if inspect.isroutine(func) and iscoroutinefunction(func): + return await func(*args, **kwargs) + if inspect.isclass(func): + return await run_in_threadpool(func, *args, **kwargs) + dunder_call = getattr(func, "__call__", None) # noqa: B004 + if iscoroutinefunction(dunder_call): + return await dunder_call(*args, **kwargs) + return await run_in_threadpool(func, *args, **kwargs) + + return wrapper + + +class ClassInstanceDep: + def __call__(self): + return True + + +class_instance_dep = ClassInstanceDep() +wrapped_class_instance_dep = noop_wrap(class_instance_dep) +wrapped_class_instance_dep_async_wrapper = noop_wrap_async(class_instance_dep) + + +class ClassInstanceGenDep: + def __call__(self): + yield True + + +class_instance_gen_dep = ClassInstanceGenDep() +wrapped_class_instance_gen_dep = noop_wrap(class_instance_gen_dep) + + +class ClassInstanceWrappedDep: + @noop_wrap + def __call__(self): + return True + + +class_instance_wrapped_dep = ClassInstanceWrappedDep() + + +class ClassInstanceWrappedAsyncDep: + @noop_wrap_async + def __call__(self): + return True + + +class_instance_wrapped_async_dep = ClassInstanceWrappedAsyncDep() + + +class ClassInstanceWrappedGenDep: + @noop_wrap + def __call__(self): + yield True + + +class_instance_wrapped_gen_dep = ClassInstanceWrappedGenDep() + + +class ClassInstanceWrappedAsyncGenDep: + @noop_wrap_async + def __call__(self): + yield True + + +class_instance_wrapped_async_gen_dep = ClassInstanceWrappedAsyncGenDep() + + +class ClassDep: + def __init__(self): + self.value = True + + +wrapped_class_dep = noop_wrap(ClassDep) +wrapped_class_dep_async_wrapper = noop_wrap_async(ClassDep) + + +class ClassInstanceAsyncDep: + async def __call__(self): + return True + + +class_instance_async_dep = ClassInstanceAsyncDep() +wrapped_class_instance_async_dep = noop_wrap(class_instance_async_dep) +wrapped_class_instance_async_dep_async_wrapper = noop_wrap_async( + class_instance_async_dep +) + + +class ClassInstanceAsyncGenDep: + async def __call__(self): + yield True + + +class_instance_async_gen_dep = ClassInstanceAsyncGenDep() +wrapped_class_instance_async_gen_dep = noop_wrap(class_instance_async_gen_dep) + + +class ClassInstanceAsyncWrappedDep: + @noop_wrap + async def __call__(self): + return True + + +class_instance_async_wrapped_dep = ClassInstanceAsyncWrappedDep() + + +class ClassInstanceAsyncWrappedAsyncDep: + @noop_wrap_async + async def __call__(self): + return True + + +class_instance_async_wrapped_async_dep = ClassInstanceAsyncWrappedAsyncDep() + + +class ClassInstanceAsyncWrappedGenDep: + @noop_wrap + async def __call__(self): + yield True + + +class_instance_async_wrapped_gen_dep = ClassInstanceAsyncWrappedGenDep() + + +class ClassInstanceAsyncWrappedGenAsyncDep: + @noop_wrap_async + async def __call__(self): + yield True + + +class_instance_async_wrapped_gen_async_dep = ClassInstanceAsyncWrappedGenAsyncDep() + app = FastAPI() +# Sync wrapper + @noop_wrap def wrapped_dependency() -> bool: @@ -59,16 +222,225 @@ async def get_async_wrapped_gen_dependency( return value +@app.get("/wrapped-class-instance-dependency/") +async def get_wrapped_class_instance_dependency( + value: bool = Depends(wrapped_class_instance_dep), +): + return value + + +@app.get("/wrapped-class-instance-async-dependency/") +async def get_wrapped_class_instance_async_dependency( + value: bool = Depends(wrapped_class_instance_async_dep), +): + return value + + +@app.get("/wrapped-class-instance-gen-dependency/") +async def get_wrapped_class_instance_gen_dependency( + value: bool = Depends(wrapped_class_instance_gen_dep), +): + return value + + +@app.get("/wrapped-class-instance-async-gen-dependency/") +async def get_wrapped_class_instance_async_gen_dependency( + value: bool = Depends(wrapped_class_instance_async_gen_dep), +): + return value + + +@app.get("/class-instance-wrapped-dependency/") +async def get_class_instance_wrapped_dependency( + value: bool = Depends(class_instance_wrapped_dep), +): + return value + + +@app.get("/class-instance-wrapped-async-dependency/") +async def get_class_instance_wrapped_async_dependency( + value: bool = Depends(class_instance_wrapped_async_dep), +): + return value + + +@app.get("/class-instance-async-wrapped-dependency/") +async def get_class_instance_async_wrapped_dependency( + value: bool = Depends(class_instance_async_wrapped_dep), +): + return value + + +@app.get("/class-instance-async-wrapped-async-dependency/") +async def get_class_instance_async_wrapped_async_dependency( + value: bool = Depends(class_instance_async_wrapped_async_dep), +): + return value + + +@app.get("/class-instance-wrapped-gen-dependency/") +async def get_class_instance_wrapped_gen_dependency( + value: bool = Depends(class_instance_wrapped_gen_dep), +): + return value + + +@app.get("/class-instance-wrapped-async-gen-dependency/") +async def get_class_instance_wrapped_async_gen_dependency( + value: bool = Depends(class_instance_wrapped_async_gen_dep), +): + return value + + +@app.get("/class-instance-async-wrapped-gen-dependency/") +async def get_class_instance_async_wrapped_gen_dependency( + value: bool = Depends(class_instance_async_wrapped_gen_dep), +): + return value + + +@app.get("/class-instance-async-wrapped-gen-async-dependency/") +async def get_class_instance_async_wrapped_gen_async_dependency( + value: bool = Depends(class_instance_async_wrapped_gen_async_dep), +): + return value + + +@app.get("/wrapped-class-dependency/") +async def get_wrapped_class_dependency(value: ClassDep = Depends(wrapped_class_dep)): + return value.value + + +@app.get("/wrapped-endpoint/") +@noop_wrap +def get_wrapped_endpoint(): + return True + + +@app.get("/async-wrapped-endpoint/") +@noop_wrap +async def get_async_wrapped_endpoint(): + return True + + +# Async wrapper + + +@noop_wrap_async +def wrapped_dependency_async_wrapper() -> bool: + return True + + +@noop_wrap_async +def wrapped_gen_dependency_async_wrapper() -> Generator[bool, None, None]: + yield True + + +@noop_wrap_async +async def async_wrapped_dependency_async_wrapper() -> bool: + return True + + +@noop_wrap_async +async def async_wrapped_gen_dependency_async_wrapper() -> AsyncGenerator[bool, None]: + yield True + + +@app.get("/wrapped-dependency-async-wrapper/") +async def get_wrapped_dependency_async_wrapper( + value: bool = Depends(wrapped_dependency_async_wrapper), +): + return value + + +@app.get("/wrapped-gen-dependency-async-wrapper/") +async def get_wrapped_gen_dependency_async_wrapper( + value: bool = Depends(wrapped_gen_dependency_async_wrapper), +): + return value + + +@app.get("/async-wrapped-dependency-async-wrapper/") +async def get_async_wrapped_dependency_async_wrapper( + value: bool = Depends(async_wrapped_dependency_async_wrapper), +): + return value + + +@app.get("/async-wrapped-gen-dependency-async-wrapper/") +async def get_async_wrapped_gen_dependency_async_wrapper( + value: bool = Depends(async_wrapped_gen_dependency_async_wrapper), +): + return value + + +@app.get("/wrapped-class-instance-dependency-async-wrapper/") +async def get_wrapped_class_instance_dependency_async_wrapper( + value: bool = Depends(wrapped_class_instance_dep_async_wrapper), +): + return value + + +@app.get("/wrapped-class-instance-async-dependency-async-wrapper/") +async def get_wrapped_class_instance_async_dependency_async_wrapper( + value: bool = Depends(wrapped_class_instance_async_dep_async_wrapper), +): + return value + + +@app.get("/wrapped-class-dependency-async-wrapper/") +async def get_wrapped_class_dependency_async_wrapper( + value: ClassDep = Depends(wrapped_class_dep_async_wrapper), +): + return value.value + + +@app.get("/wrapped-endpoint-async-wrapper/") +@noop_wrap_async +def get_wrapped_endpoint_async_wrapper(): + return True + + +@app.get("/async-wrapped-endpoint-async-wrapper/") +@noop_wrap_async +async def get_async_wrapped_endpoint_async_wrapper(): + return True + + client = TestClient(app) @pytest.mark.parametrize( "route", [ - "/wrapped-dependency", - "/wrapped-gen-dependency", - "/async-wrapped-dependency", - "/async-wrapped-gen-dependency", + "/wrapped-dependency/", + "/wrapped-gen-dependency/", + "/async-wrapped-dependency/", + "/async-wrapped-gen-dependency/", + "/wrapped-class-instance-dependency/", + "/wrapped-class-instance-async-dependency/", + "/wrapped-class-instance-gen-dependency/", + "/wrapped-class-instance-async-gen-dependency/", + "/class-instance-wrapped-dependency/", + "/class-instance-wrapped-async-dependency/", + "/class-instance-async-wrapped-dependency/", + "/class-instance-async-wrapped-async-dependency/", + "/class-instance-wrapped-gen-dependency/", + "/class-instance-wrapped-async-gen-dependency/", + "/class-instance-async-wrapped-gen-dependency/", + "/class-instance-async-wrapped-gen-async-dependency/", + "/wrapped-class-dependency/", + "/wrapped-endpoint/", + "/async-wrapped-endpoint/", + "/wrapped-dependency-async-wrapper/", + "/wrapped-gen-dependency-async-wrapper/", + "/async-wrapped-dependency-async-wrapper/", + "/async-wrapped-gen-dependency-async-wrapper/", + "/wrapped-class-instance-dependency-async-wrapper/", + "/wrapped-class-instance-async-dependency-async-wrapper/", + "/wrapped-class-dependency-async-wrapper/", + "/wrapped-endpoint-async-wrapper/", + "/async-wrapped-endpoint-async-wrapper/", ], ) def test_class_dependency(route): From 6c6b9d7a2b1df17fc24c8d2e4c5303fc334e75b9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 4 Dec 2025 07:29:53 +0000 Subject: [PATCH 208/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index c397528aa..a687e89ab 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Fixes + +* 🐛 Fix support for functools wraps and partial combined, for async and regular functions and classes in path operations and dependencies. PR [#14448](https://github.com/fastapi/fastapi/pull/14448) by [@tiangolo](https://github.com/tiangolo). + ## 0.123.5 ### Features From 811fa898752e7f5697733a2958ff79ad8dfb5276 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 4 Dec 2025 08:33:11 +0100 Subject: [PATCH 209/256] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.12?= =?UTF-8?q?3.6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index a687e89ab..8c80851b4 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.123.6 + ### Fixes * 🐛 Fix support for functools wraps and partial combined, for async and regular functions and classes in path operations and dependencies. PR [#14448](https://github.com/fastapi/fastapi/pull/14448) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 80b2a99c1..32223231e 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.123.5" +__version__ = "0.123.6" from starlette import status as status From 861598b4e30a7a7297aee803097f77431f799ea5 Mon Sep 17 00:00:00 2001 From: chaen Date: Thu, 4 Dec 2025 09:18:32 +0100 Subject: [PATCH 210/256] =?UTF-8?q?=F0=9F=90=9B=20Fix=20evaluating=20strin?= =?UTF-8?q?gified=20annotations=20in=20Python=203.10=20(#11355)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sofie Van Landeghem Co-authored-by: svlandeg Co-authored-by: Sebastián Ramírez --- fastapi/dependencies/utils.py | 11 +++++++-- tests/test_stringified_annotations_simple.py | 26 ++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 tests/test_stringified_annotations_simple.py diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 91348c8ea..1ff35f648 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -1,5 +1,6 @@ import dataclasses import inspect +import sys from contextlib import AsyncExitStack, contextmanager from copy import copy, deepcopy from dataclasses import dataclass @@ -191,7 +192,10 @@ def get_flat_params(dependant: Dependant) -> List[ModelField]: def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature: - signature = inspect.signature(call) + if sys.version_info >= (3, 10): + signature = inspect.signature(call, eval_str=True) + else: + signature = inspect.signature(call) unwrapped = inspect.unwrap(call) globalns = getattr(unwrapped, "__globals__", {}) typed_params = [ @@ -217,7 +221,10 @@ def get_typed_annotation(annotation: Any, globalns: Dict[str, Any]) -> Any: def get_typed_return_annotation(call: Callable[..., Any]) -> Any: - signature = inspect.signature(call) + if sys.version_info >= (3, 10): + signature = inspect.signature(call, eval_str=True) + else: + signature = inspect.signature(call) unwrapped = inspect.unwrap(call) annotation = signature.return_annotation diff --git a/tests/test_stringified_annotations_simple.py b/tests/test_stringified_annotations_simple.py new file mode 100644 index 000000000..9bd6d09d6 --- /dev/null +++ b/tests/test_stringified_annotations_simple.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +from fastapi import Depends, FastAPI, Request +from fastapi.testclient import TestClient +from typing_extensions import Annotated + +from .utils import needs_py310 + + +class Dep: + def __call__(self, request: Request): + return "test" + + +@needs_py310 +def test_stringified_annotations(): + app = FastAPI() + + client = TestClient(app) + + @app.get("/test/") + def call(test: Annotated[str, Depends(Dep())]): + return {"test": test} + + response = client.get("/test") + assert response.status_code == 200 From 6c565482cfc729e2f93d6917731156c244b484a7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 4 Dec 2025 08:18:55 +0000 Subject: [PATCH 211/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 8c80851b4..592fa5089 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Fixes + +* 🐛 Fix evaluating stringified annotations in Python 3.10. PR [#11355](https://github.com/fastapi/fastapi/pull/11355) by [@chaen](https://github.com/chaen). + ## 0.123.6 ### Fixes From 603df6e36f59cc4a3e189f506b8f36d1e816c3ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 4 Dec 2025 09:27:38 +0100 Subject: [PATCH 212/256] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.12?= =?UTF-8?q?3.7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 592fa5089..4747d5729 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.123.7 + ### Fixes * 🐛 Fix evaluating stringified annotations in Python 3.10. PR [#11355](https://github.com/fastapi/fastapi/pull/11355) by [@chaen](https://github.com/chaen). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 32223231e..61d751e58 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.123.6" +__version__ = "0.123.7" from starlette import status as status From 0ec4bafca204c92dca903437e78514246fe14eac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 4 Dec 2025 04:59:24 -0800 Subject: [PATCH 213/256] =?UTF-8?q?=F0=9F=90=9B=20Fix=20OpenAPI=20security?= =?UTF-8?q?=20scheme=20OAuth2=20scopes=20declaration,=20deduplicate=20secu?= =?UTF-8?q?rity=20schemes=20with=20different=20scopes=20(#14455)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/openapi/utils.py | 13 +- ...uthorization_code_bearer_scopes_openapi.py | 131 ++++++++++++++++++ 2 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 tests/test_security_oauth2_authorization_code_bearer_scopes_openapi.py diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index dbc93d289..e7e6da2f7 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -79,7 +79,8 @@ def get_openapi_security_definitions( flat_dependant: Dependant, ) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]: security_definitions = {} - operation_security = [] + # Use a dict to merge scopes for same security scheme + operation_security_dict: Dict[str, List[str]] = {} for security_requirement in flat_dependant.security_requirements: security_definition = jsonable_encoder( security_requirement.security_scheme.model, @@ -88,7 +89,15 @@ def get_openapi_security_definitions( ) security_name = security_requirement.security_scheme.scheme_name security_definitions[security_name] = security_definition - operation_security.append({security_name: security_requirement.scopes}) + # Merge scopes for the same security scheme + if security_name not in operation_security_dict: + operation_security_dict[security_name] = [] + for scope in security_requirement.scopes or []: + if scope not in operation_security_dict[security_name]: + operation_security_dict[security_name].append(scope) + operation_security = [ + {name: scopes} for name, scopes in operation_security_dict.items() + ] return security_definitions, operation_security diff --git a/tests/test_security_oauth2_authorization_code_bearer_scopes_openapi.py b/tests/test_security_oauth2_authorization_code_bearer_scopes_openapi.py new file mode 100644 index 000000000..644df8de6 --- /dev/null +++ b/tests/test_security_oauth2_authorization_code_bearer_scopes_openapi.py @@ -0,0 +1,131 @@ +# Ref: https://github.com/fastapi/fastapi/issues/14454 + +from typing import Optional + +from fastapi import APIRouter, FastAPI, Security +from fastapi.security import OAuth2AuthorizationCodeBearer +from fastapi.testclient import TestClient +from inline_snapshot import snapshot + +oauth2_scheme = OAuth2AuthorizationCodeBearer( + authorizationUrl="authorize", + tokenUrl="token", + auto_error=True, + scopes={"read": "Read access", "write": "Write access"}, +) + +app = FastAPI(dependencies=[Security(oauth2_scheme)]) + + +@app.get("/") +async def root(): + return {"message": "Hello World"} + + +router = APIRouter(dependencies=[Security(oauth2_scheme, scopes=["read"])]) + + +@router.get("/items/") +async def read_items(token: Optional[str] = Security(oauth2_scheme)): + return {"token": token} + + +@router.post("/items/") +async def create_item( + token: Optional[str] = Security(oauth2_scheme, scopes=["read", "write"]), +): + return {"token": token} + + +app.include_router(router) + +client = TestClient(app) + + +def test_root(): + response = client.get("/", headers={"Authorization": "Bearer testtoken"}) + assert response.status_code == 200, response.text + assert response.json() == {"message": "Hello World"} + + +def test_read_token(): + response = client.get("/items/", headers={"Authorization": "Bearer testtoken"}) + assert response.status_code == 200, response.text + assert response.json() == {"token": "testtoken"} + + +def test_create_token(): + response = client.post("/items/", headers={"Authorization": "Bearer testtoken"}) + assert response.status_code == 200, response.text + assert response.json() == {"token": "testtoken"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "summary": "Root", + "operationId": "root__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "security": [{"OAuth2AuthorizationCodeBearer": []}], + } + }, + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "security": [ + {"OAuth2AuthorizationCodeBearer": ["read"]}, + ], + }, + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "security": [ + {"OAuth2AuthorizationCodeBearer": ["read", "write"]}, + ], + }, + }, + }, + "components": { + "securitySchemes": { + "OAuth2AuthorizationCodeBearer": { + "type": "oauth2", + "flows": { + "authorizationCode": { + "scopes": { + "read": "Read access", + "write": "Write access", + }, + "authorizationUrl": "authorize", + "tokenUrl": "token", + } + }, + } + } + }, + } + ) From e248a4d22b6de0630771895cee309e32e64bfbd4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 4 Dec 2025 12:59:45 +0000 Subject: [PATCH 214/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 4747d5729..86b21e8f1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Fixes + +* 🐛 Fix OpenAPI security scheme OAuth2 scopes declaration, deduplicate security schemes with different scopes. PR [#14455](https://github.com/fastapi/fastapi/pull/14455) by [@tiangolo](https://github.com/tiangolo). + ## 0.123.7 ### Fixes From eb1d50479ba0ac873e4ffa08a824b1d822ca3a17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 4 Dec 2025 14:01:00 +0100 Subject: [PATCH 215/256] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.12?= =?UTF-8?q?3.8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 86b21e8f1..50eaef514 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.123.8 + ### Fixes * 🐛 Fix OpenAPI security scheme OAuth2 scopes declaration, deduplicate security schemes with different scopes. PR [#14455](https://github.com/fastapi/fastapi/pull/14455) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 61d751e58..b5f5300f0 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.123.7" +__version__ = "0.123.8" from starlette import status as status From 0b5fa563cdfb887e4145d8419ae91b6a40905349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 4 Dec 2025 14:22:01 -0800 Subject: [PATCH 216/256] =?UTF-8?q?=F0=9F=90=9B=20Fix=20OAuth2=20scopes=20?= =?UTF-8?q?in=20OpenAPI=20in=20extra=20corner=20cases,=20parent=20dependen?= =?UTF-8?q?cy=20with=20scopes,=20sub-dependency=20security=20scheme=20with?= =?UTF-8?q?out=20scopes=20(#14459)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/dependencies/models.py | 30 +++++-- fastapi/dependencies/utils.py | 33 +++++--- fastapi/openapi/utils.py | 8 +- ...uthorization_code_bearer_scopes_openapi.py | 73 ++++++++++++++++- ...ation_code_bearer_scopes_openapi_simple.py | 79 +++++++++++++++++++ 5 files changed, 198 insertions(+), 25 deletions(-) create mode 100644 tests/test_security_oauth2_authorization_code_bearer_scopes_openapi_simple.py diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index 9b545e4e5..af168a177 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -2,7 +2,7 @@ import inspect import sys from dataclasses import dataclass, field from functools import cached_property, partial -from typing import Any, Callable, List, Optional, Sequence, Union +from typing import Any, Callable, List, Optional, Union from fastapi._compat import ModelField from fastapi.security.base import SecurityBase @@ -28,12 +28,6 @@ def _impartial(func: Callable[..., Any]) -> Callable[..., Any]: return func -@dataclass -class SecurityRequirement: - security_scheme: SecurityBase - scopes: Optional[Sequence[str]] = None - - @dataclass class Dependant: path_params: List[ModelField] = field(default_factory=list) @@ -42,7 +36,6 @@ class Dependant: cookie_params: List[ModelField] = field(default_factory=list) body_params: List[ModelField] = field(default_factory=list) dependencies: List["Dependant"] = field(default_factory=list) - security_requirements: List[SecurityRequirement] = field(default_factory=list) name: Optional[str] = None call: Optional[Callable[..., Any]] = None request_param_name: Optional[str] = None @@ -83,11 +76,32 @@ class Dependant: return True if self.security_scopes_param_name is not None: return True + if self._is_security_scheme: + return True for sub_dep in self.dependencies: if sub_dep._uses_scopes: return True return False + @cached_property + def _is_security_scheme(self) -> bool: + if self.call is None: + return False # pragma: no cover + unwrapped = _unwrapped_call(self.call) + return isinstance(unwrapped, SecurityBase) + + # Mainly to get the type of SecurityBase, but it's the same self.call + @cached_property + def _security_scheme(self) -> SecurityBase: + unwrapped = _unwrapped_call(self.call) + assert isinstance(unwrapped, SecurityBase) + return unwrapped + + @cached_property + def _security_dependencies(self) -> List["Dependant"]: + security_deps = [dep for dep in self.dependencies if dep._is_security_scheme] + return security_deps + @cached_property def is_gen_callable(self) -> bool: if self.call is None: diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 1ff35f648..23bca6f2a 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -55,10 +55,9 @@ from fastapi.concurrency import ( asynccontextmanager, contextmanager_in_threadpool, ) -from fastapi.dependencies.models import Dependant, SecurityRequirement +from fastapi.dependencies.models import Dependant from fastapi.exceptions import DependencyScopeError from fastapi.logger import logger -from fastapi.security.base import SecurityBase from fastapi.security.oauth2 import SecurityScopes from fastapi.types import DependencyCacheKey from fastapi.utils import create_model_field, get_path_param_names @@ -142,10 +141,14 @@ def get_flat_dependant( *, skip_repeats: bool = False, visited: Optional[List[DependencyCacheKey]] = None, + parent_oauth_scopes: Optional[List[str]] = None, ) -> Dependant: if visited is None: visited = [] visited.append(dependant.cache_key) + use_parent_oauth_scopes = (parent_oauth_scopes or []) + ( + dependant.oauth_scopes or [] + ) flat_dependant = Dependant( path_params=dependant.path_params.copy(), @@ -153,22 +156,37 @@ def get_flat_dependant( header_params=dependant.header_params.copy(), cookie_params=dependant.cookie_params.copy(), body_params=dependant.body_params.copy(), - security_requirements=dependant.security_requirements.copy(), + name=dependant.name, + call=dependant.call, + request_param_name=dependant.request_param_name, + websocket_param_name=dependant.websocket_param_name, + http_connection_param_name=dependant.http_connection_param_name, + response_param_name=dependant.response_param_name, + background_tasks_param_name=dependant.background_tasks_param_name, + security_scopes_param_name=dependant.security_scopes_param_name, + own_oauth_scopes=dependant.own_oauth_scopes, + parent_oauth_scopes=use_parent_oauth_scopes, use_cache=dependant.use_cache, path=dependant.path, + scope=dependant.scope, ) for sub_dependant in dependant.dependencies: if skip_repeats and sub_dependant.cache_key in visited: continue flat_sub = get_flat_dependant( - sub_dependant, skip_repeats=skip_repeats, visited=visited + sub_dependant, + skip_repeats=skip_repeats, + visited=visited, + parent_oauth_scopes=flat_dependant.oauth_scopes, ) + flat_dependant.dependencies.append(flat_sub) flat_dependant.path_params.extend(flat_sub.path_params) flat_dependant.query_params.extend(flat_sub.query_params) flat_dependant.header_params.extend(flat_sub.header_params) flat_dependant.cookie_params.extend(flat_sub.cookie_params) flat_dependant.body_params.extend(flat_sub.body_params) - flat_dependant.security_requirements.extend(flat_sub.security_requirements) + flat_dependant.dependencies.extend(flat_sub.dependencies) + return flat_dependant @@ -258,11 +276,6 @@ def get_dependant( path_param_names = get_path_param_names(path) endpoint_signature = get_typed_signature(call) signature_params = endpoint_signature.parameters - if isinstance(call, SecurityBase): - security_requirement = SecurityRequirement( - security_scheme=call, scopes=current_scopes - ) - dependant.security_requirements.append(security_requirement) for param_name, param in signature_params.items(): is_path_param = param_name in path_param_names param_details = analyze_param( diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index e7e6da2f7..06c14861a 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -81,18 +81,18 @@ def get_openapi_security_definitions( security_definitions = {} # Use a dict to merge scopes for same security scheme operation_security_dict: Dict[str, List[str]] = {} - for security_requirement in flat_dependant.security_requirements: + for security_dependency in flat_dependant._security_dependencies: security_definition = jsonable_encoder( - security_requirement.security_scheme.model, + security_dependency._security_scheme.model, by_alias=True, exclude_none=True, ) - security_name = security_requirement.security_scheme.scheme_name + security_name = security_dependency._security_scheme.scheme_name security_definitions[security_name] = security_definition # Merge scopes for the same security scheme if security_name not in operation_security_dict: operation_security_dict[security_name] = [] - for scope in security_requirement.scopes or []: + for scope in security_dependency.oauth_scopes or []: if scope not in operation_security_dict[security_name]: operation_security_dict[security_name].append(scope) operation_security = [ diff --git a/tests/test_security_oauth2_authorization_code_bearer_scopes_openapi.py b/tests/test_security_oauth2_authorization_code_bearer_scopes_openapi.py index 644df8de6..d41f1dc1f 100644 --- a/tests/test_security_oauth2_authorization_code_bearer_scopes_openapi.py +++ b/tests/test_security_oauth2_authorization_code_bearer_scopes_openapi.py @@ -2,10 +2,11 @@ from typing import Optional -from fastapi import APIRouter, FastAPI, Security +from fastapi import APIRouter, Depends, FastAPI, Security from fastapi.security import OAuth2AuthorizationCodeBearer from fastapi.testclient import TestClient from inline_snapshot import snapshot +from typing_extensions import Annotated oauth2_scheme = OAuth2AuthorizationCodeBearer( authorizationUrl="authorize", @@ -14,7 +15,12 @@ oauth2_scheme = OAuth2AuthorizationCodeBearer( scopes={"read": "Read access", "write": "Write access"}, ) -app = FastAPI(dependencies=[Security(oauth2_scheme)]) + +async def get_token(token: Annotated[str, Depends(oauth2_scheme)]) -> str: + return token + + +app = FastAPI(dependencies=[Depends(get_token)]) @app.get("/") @@ -22,11 +28,26 @@ async def root(): return {"message": "Hello World"} +@app.get( + "/with-oauth2-scheme", + dependencies=[Security(oauth2_scheme, scopes=["read", "write"])], +) +async def read_with_oauth2_scheme(): + return {"message": "Admin Access"} + + +@app.get( + "/with-get-token", dependencies=[Security(get_token, scopes=["read", "write"])] +) +async def read_with_get_token(): + return {"message": "Admin Access"} + + router = APIRouter(dependencies=[Security(oauth2_scheme, scopes=["read"])]) @router.get("/items/") -async def read_items(token: Optional[str] = Security(oauth2_scheme)): +async def read_items(token: Optional[str] = Depends(oauth2_scheme)): return {"token": token} @@ -48,6 +69,22 @@ def test_root(): assert response.json() == {"message": "Hello World"} +def test_read_with_oauth2_scheme(): + response = client.get( + "/with-oauth2-scheme", headers={"Authorization": "Bearer testtoken"} + ) + assert response.status_code == 200, response.text + assert response.json() == {"message": "Admin Access"} + + +def test_read_with_get_token(): + response = client.get( + "/with-get-token", headers={"Authorization": "Bearer testtoken"} + ) + assert response.status_code == 200, response.text + assert response.json() == {"message": "Admin Access"} + + def test_read_token(): response = client.get("/items/", headers={"Authorization": "Bearer testtoken"}) assert response.status_code == 200, response.text @@ -81,6 +118,36 @@ def test_openapi_schema(): "security": [{"OAuth2AuthorizationCodeBearer": []}], } }, + "/with-oauth2-scheme": { + "get": { + "summary": "Read With Oauth2 Scheme", + "operationId": "read_with_oauth2_scheme_with_oauth2_scheme_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "security": [ + {"OAuth2AuthorizationCodeBearer": ["read", "write"]} + ], + } + }, + "/with-get-token": { + "get": { + "summary": "Read With Get Token", + "operationId": "read_with_get_token_with_get_token_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "security": [ + {"OAuth2AuthorizationCodeBearer": ["read", "write"]} + ], + } + }, "/items/": { "get": { "summary": "Read Items", diff --git a/tests/test_security_oauth2_authorization_code_bearer_scopes_openapi_simple.py b/tests/test_security_oauth2_authorization_code_bearer_scopes_openapi_simple.py new file mode 100644 index 000000000..ff866d4fc --- /dev/null +++ b/tests/test_security_oauth2_authorization_code_bearer_scopes_openapi_simple.py @@ -0,0 +1,79 @@ +# Ref: https://github.com/fastapi/fastapi/issues/14454 + +from fastapi import Depends, FastAPI, Security +from fastapi.security import OAuth2AuthorizationCodeBearer +from fastapi.testclient import TestClient +from inline_snapshot import snapshot +from typing_extensions import Annotated + +oauth2_scheme = OAuth2AuthorizationCodeBearer( + authorizationUrl="api/oauth/authorize", + tokenUrl="/api/oauth/token", + scopes={"read": "Read access", "write": "Write access"}, +) + + +async def get_token(token: Annotated[str, Depends(oauth2_scheme)]) -> str: + return token + + +app = FastAPI(dependencies=[Depends(get_token)]) + + +@app.get("/admin", dependencies=[Security(get_token, scopes=["read", "write"])]) +async def read_admin(): + return {"message": "Admin Access"} + + +client = TestClient(app) + + +def test_read_admin(): + response = client.get("/admin", headers={"Authorization": "Bearer faketoken"}) + assert response.status_code == 200, response.text + assert response.json() == {"message": "Admin Access"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/admin": { + "get": { + "summary": "Read Admin", + "operationId": "read_admin_admin_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "security": [ + {"OAuth2AuthorizationCodeBearer": ["read", "write"]} + ], + } + } + }, + "components": { + "securitySchemes": { + "OAuth2AuthorizationCodeBearer": { + "type": "oauth2", + "flows": { + "authorizationCode": { + "scopes": { + "read": "Read access", + "write": "Write access", + }, + "authorizationUrl": "api/oauth/authorize", + "tokenUrl": "/api/oauth/token", + } + }, + } + } + }, + } + ) From 188d63101115ca40f274ed1e0b7093edf4ce696d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 4 Dec 2025 22:22:25 +0000 Subject: [PATCH 217/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 50eaef514..9323eb758 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Fixes + +* 🐛 Fix OAuth2 scopes in OpenAPI in extra corner cases, parent dependency with scopes, sub-dependency security scheme without scopes. PR [#14459](https://github.com/fastapi/fastapi/pull/14459) by [@tiangolo](https://github.com/tiangolo). + ## 0.123.8 ### Fixes From f0dd1046a688935ffd23666b3d4164b838a4d8fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 4 Dec 2025 23:23:21 +0100 Subject: [PATCH 218/256] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.12?= =?UTF-8?q?3.9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 9323eb758..ed39da111 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.123.9 + ### Fixes * 🐛 Fix OAuth2 scopes in OpenAPI in extra corner cases, parent dependency with scopes, sub-dependency security scheme without scopes. PR [#14459](https://github.com/fastapi/fastapi/pull/14459) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index b5f5300f0..dc5467b0f 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.123.8" +__version__ = "0.123.9" from starlette import status as status From 812a1926f06391b22b081fdb11fe7528e3b91293 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Fri, 5 Dec 2025 21:19:30 +0100 Subject: [PATCH 219/256] =?UTF-8?q?=F0=9F=90=9B=20Fix=20`separate=5Finput?= =?UTF-8?q?=5Foutput=5Fschemas=3DFalse`=20with=20`computed=5Ffield`=20(#14?= =?UTF-8?q?453)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/_compat/v2.py | 31 ++-- ...t_openapi_separate_input_output_schemas.py | 151 ++++++++++++++++++ 2 files changed, 168 insertions(+), 14 deletions(-) diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py index 0faa7d5a8..acd23d846 100644 --- a/fastapi/_compat/v2.py +++ b/fastapi/_compat/v2.py @@ -171,6 +171,13 @@ def _get_model_config(model: BaseModel) -> Any: return model.model_config +def _has_computed_fields(field: ModelField) -> bool: + computed_fields = field._type_adapter.core_schema.get("schema", {}).get( + "computed_fields", [] + ) + return len(computed_fields) > 0 + + def get_schema_from_model_field( *, field: ModelField, @@ -180,12 +187,9 @@ def get_schema_from_model_field( ], separate_input_output_schemas: bool = True, ) -> Dict[str, Any]: - computed_fields = field._type_adapter.core_schema.get("schema", {}).get( - "computed_fields", [] - ) override_mode: Union[Literal["validation"], None] = ( None - if (separate_input_output_schemas or len(computed_fields) > 0) + if (separate_input_output_schemas or _has_computed_fields(field)) else "validation" ) # This expects that GenerateJsonSchema was already used to generate the definitions @@ -208,15 +212,7 @@ def get_definitions( Dict[Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue], Dict[str, Dict[str, Any]], ]: - has_computed_fields: bool = any( - field._type_adapter.core_schema.get("schema", {}).get("computed_fields", []) - for field in fields - ) - schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE) - override_mode: Union[Literal["validation"], None] = ( - None if (separate_input_output_schemas or has_computed_fields) else "validation" - ) validation_fields = [field for field in fields if field.mode == "validation"] serialization_fields = [field for field in fields if field.mode == "serialization"] flat_validation_models = get_flat_models_from_fields( @@ -246,9 +242,16 @@ def get_definitions( unique_flat_model_fields = { f for f in flat_model_fields if f.type_ not in input_types } - inputs = [ - (field, override_mode or field.mode, field._type_adapter.core_schema) + ( + field, + ( + field.mode + if (separate_input_output_schemas or _has_computed_fields(field)) + else "validation" + ), + field._type_adapter.core_schema, + ) for field in list(fields) + list(unique_flat_model_fields) ] field_mapping, definitions = schema_generator.generate_definitions(inputs=inputs) diff --git a/tests/test_openapi_separate_input_output_schemas.py b/tests/test_openapi_separate_input_output_schemas.py index fa73620ea..c9a05418b 100644 --- a/tests/test_openapi_separate_input_output_schemas.py +++ b/tests/test_openapi_separate_input_output_schemas.py @@ -24,6 +24,18 @@ class Item(BaseModel): model_config = {"json_schema_serialization_defaults_required": True} +if PYDANTIC_V2: + from pydantic import computed_field + + class WithComputedField(BaseModel): + name: str + + @computed_field + @property + def computed_field(self) -> str: + return f"computed {self.name}" + + def get_app_client(separate_input_output_schemas: bool = True) -> TestClient: app = FastAPI(separate_input_output_schemas=separate_input_output_schemas) @@ -46,6 +58,14 @@ def get_app_client(separate_input_output_schemas: bool = True) -> TestClient: Item(name="Plumbus"), ] + if PYDANTIC_V2: + + @app.post("/with-computed-field/") + def create_with_computed_field( + with_computed_field: WithComputedField, + ) -> WithComputedField: + return with_computed_field + client = TestClient(app) return client @@ -131,6 +151,23 @@ def test_read_items(): ) +@needs_pydanticv2 +def test_with_computed_field(): + client = get_app_client() + client_no = get_app_client(separate_input_output_schemas=False) + response = client.post("/with-computed-field/", json={"name": "example"}) + response2 = client_no.post("/with-computed-field/", json={"name": "example"}) + assert response.status_code == response2.status_code == 200, response.text + assert ( + response.json() + == response2.json() + == { + "name": "example", + "computed_field": "computed example", + } + ) + + @needs_pydanticv2 def test_openapi_schema(): client = get_app_client() @@ -245,6 +282,44 @@ def test_openapi_schema(): }, } }, + "/with-computed-field/": { + "post": { + "summary": "Create With Computed Field", + "operationId": "create_with_computed_field_with_computed_field__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WithComputedField-Input" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WithComputedField-Output" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, }, "components": { "schemas": { @@ -333,6 +408,25 @@ def test_openapi_schema(): "required": ["subname", "sub_description", "tags"], "title": "SubItem", }, + "WithComputedField-Input": { + "properties": {"name": {"type": "string", "title": "Name"}}, + "type": "object", + "required": ["name"], + "title": "WithComputedField", + }, + "WithComputedField-Output": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "computed_field": { + "type": "string", + "title": "Computed Field", + "readOnly": True, + }, + }, + "type": "object", + "required": ["name", "computed_field"], + "title": "WithComputedField", + }, "ValidationError": { "properties": { "loc": { @@ -458,6 +552,44 @@ def test_openapi_schema_no_separate(): }, } }, + "/with-computed-field/": { + "post": { + "summary": "Create With Computed Field", + "operationId": "create_with_computed_field_with_computed_field__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WithComputedField-Input" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WithComputedField-Output" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, }, "components": { "schemas": { @@ -508,6 +640,25 @@ def test_openapi_schema_no_separate(): "required": ["subname"], "title": "SubItem", }, + "WithComputedField-Input": { + "properties": {"name": {"type": "string", "title": "Name"}}, + "type": "object", + "required": ["name"], + "title": "WithComputedField", + }, + "WithComputedField-Output": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "computed_field": { + "type": "string", + "title": "Computed Field", + "readOnly": True, + }, + }, + "type": "object", + "required": ["name", "computed_field"], + "title": "WithComputedField", + }, "ValidationError": { "properties": { "loc": { From 516169428d2fa189d34318ebc469a082c49c1189 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 5 Dec 2025 20:19:54 +0000 Subject: [PATCH 220/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index ed39da111..aa8a85843 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Fixes + +* 🐛 Fix `separate_input_output_schemas=False` with `computed_field`. PR [#14453](https://github.com/fastapi/fastapi/pull/14453) by [@YuriiMotov](https://github.com/YuriiMotov). + ## 0.123.9 ### Fixes From da0ffab0b260475499294d3dc767409d7bca5c34 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Fri, 5 Dec 2025 22:21:05 +0100 Subject: [PATCH 221/256] =?UTF-8?q?=F0=9F=90=9B=20Fix=20using=20class=20(n?= =?UTF-8?q?ot=20instance)=20dependency=20that=20has=20`=5F=5Fcall=5F=5F`?= =?UTF-8?q?=20method=20(#14458)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- fastapi/dependencies/models.py | 7 ++++++- tests/test_dependency_class.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index af168a177..6c4bf18b3 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -110,6 +110,8 @@ class Dependant: _impartial(self.call) ) or inspect.isgeneratorfunction(_unwrapped_call(self.call)): return True + if inspect.isclass(_unwrapped_call(self.call)): + return False dunder_call = getattr(_impartial(self.call), "__call__", None) # noqa: B004 if dunder_call is None: return False # pragma: no cover @@ -134,6 +136,8 @@ class Dependant: _impartial(self.call) ) or inspect.isasyncgenfunction(_unwrapped_call(self.call)): return True + if inspect.isclass(_unwrapped_call(self.call)): + return False dunder_call = getattr(_impartial(self.call), "__call__", None) # noqa: B004 if dunder_call is None: return False # pragma: no cover @@ -162,6 +166,8 @@ class Dependant: _unwrapped_call(self.call) ): return True + if inspect.isclass(_unwrapped_call(self.call)): + return False dunder_call = getattr(_impartial(self.call), "__call__", None) # noqa: B004 if dunder_call is None: return False # pragma: no cover @@ -176,7 +182,6 @@ class Dependant: _impartial(dunder_unwrapped_call) ) or iscoroutinefunction(_unwrapped_call(dunder_unwrapped_call)): return True - # if inspect.isclass(self.call): False, covered by default return return False @cached_property diff --git a/tests/test_dependency_class.py b/tests/test_dependency_class.py index 0233492e6..75241b467 100644 --- a/tests/test_dependency_class.py +++ b/tests/test_dependency_class.py @@ -48,6 +48,34 @@ async_callable_gen_dependency = AsyncCallableGenDependency() methods_dependency = MethodsDependency() +@app.get("/callable-dependency-class") +async def get_callable_dependency_class( + value: str, instance: CallableDependency = Depends() +): + return instance(value) + + +@app.get("/callable-gen-dependency-class") +async def get_callable_gen_dependency_class( + value: str, instance: CallableGenDependency = Depends() +): + return next(instance(value)) + + +@app.get("/async-callable-dependency-class") +async def get_async_callable_dependency_class( + value: str, instance: AsyncCallableDependency = Depends() +): + return await instance(value) + + +@app.get("/async-callable-gen-dependency-class") +async def get_async_callable_gen_dependency_class( + value: str, instance: AsyncCallableGenDependency = Depends() +): + return await instance(value).__anext__() + + @app.get("/callable-dependency") async def get_callable_dependency(value: str = Depends(callable_dependency)): return value @@ -114,6 +142,10 @@ client = TestClient(app) ("/synchronous-method-gen-dependency", "synchronous-method-gen-dependency"), ("/asynchronous-method-dependency", "asynchronous-method-dependency"), ("/asynchronous-method-gen-dependency", "asynchronous-method-gen-dependency"), + ("/callable-dependency-class", "callable-dependency-class"), + ("/callable-gen-dependency-class", "callable-gen-dependency-class"), + ("/async-callable-dependency-class", "async-callable-dependency-class"), + ("/async-callable-gen-dependency-class", "async-callable-gen-dependency-class"), ], ) def test_class_dependency(route, value): From e7d7038dfa35fc923f20fd11a969d2e65e1b9df1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 5 Dec 2025 21:21:29 +0000 Subject: [PATCH 222/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index aa8a85843..ce620b132 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Fixes +* 🐛 Fix using class (not instance) dependency that has `__call__` method. PR [#14458](https://github.com/fastapi/fastapi/pull/14458) by [@YuriiMotov](https://github.com/YuriiMotov). * 🐛 Fix `separate_input_output_schemas=False` with `computed_field`. PR [#14453](https://github.com/fastapi/fastapi/pull/14453) by [@YuriiMotov](https://github.com/YuriiMotov). ## 0.123.9 From 08b09e5236e315b6f10265ed229f130d4befb4ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 5 Dec 2025 22:26:36 +0100 Subject: [PATCH 223/256] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.12?= =?UTF-8?q?3.10?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index ce620b132..d27c47383 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.123.10 + ### Fixes * 🐛 Fix using class (not instance) dependency that has `__call__` method. PR [#14458](https://github.com/fastapi/fastapi/pull/14458) by [@YuriiMotov](https://github.com/YuriiMotov). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index dc5467b0f..2396c501d 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.123.9" +__version__ = "0.123.10" from starlette import status as status From e1117f75505bbdb2d42321a009dbf26c9c2b8b6d Mon Sep 17 00:00:00 2001 From: Savannah Ostrowski Date: Sat, 6 Dec 2025 04:21:57 -0800 Subject: [PATCH 224/256] =?UTF-8?q?=F0=9F=9A=B8=20=20Improve=20tracebacks?= =?UTF-8?q?=20by=20adding=20endpoint=20metadata=20(#14306)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- fastapi/exceptions.py | 75 +++++++++-- fastapi/routing.py | 62 ++++++++- tests/test_validation_error_context.py | 168 +++++++++++++++++++++++++ 3 files changed, 289 insertions(+), 16 deletions(-) create mode 100644 tests/test_validation_error_context.py diff --git a/fastapi/exceptions.py b/fastapi/exceptions.py index 0620428be..a46e82350 100644 --- a/fastapi/exceptions.py +++ b/fastapi/exceptions.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, Optional, Sequence, Type, Union +from typing import Any, Dict, Optional, Sequence, Type, TypedDict, Union from annotated_doc import Doc from pydantic import BaseModel, create_model @@ -7,6 +7,13 @@ from starlette.exceptions import WebSocketException as StarletteWebSocketExcepti from typing_extensions import Annotated +class EndpointContext(TypedDict, total=False): + function: str + path: str + file: str + line: int + + class HTTPException(StarletteHTTPException): """ An HTTP exception you can raise in your own code to show errors to the client. @@ -155,30 +162,72 @@ class DependencyScopeError(FastAPIError): class ValidationException(Exception): - def __init__(self, errors: Sequence[Any]) -> None: + def __init__( + self, + errors: Sequence[Any], + *, + endpoint_ctx: Optional[EndpointContext] = None, + ) -> None: self._errors = errors + self.endpoint_ctx = endpoint_ctx + + ctx = endpoint_ctx or {} + self.endpoint_function = ctx.get("function") + self.endpoint_path = ctx.get("path") + self.endpoint_file = ctx.get("file") + self.endpoint_line = ctx.get("line") def errors(self) -> Sequence[Any]: return self._errors + def _format_endpoint_context(self) -> str: + if not (self.endpoint_file and self.endpoint_line and self.endpoint_function): + if self.endpoint_path: + return f"\n Endpoint: {self.endpoint_path}" + return "" + + context = f'\n File "{self.endpoint_file}", line {self.endpoint_line}, in {self.endpoint_function}' + if self.endpoint_path: + context += f"\n {self.endpoint_path}" + return context + + def __str__(self) -> str: + message = f"{len(self._errors)} validation error{'s' if len(self._errors) != 1 else ''}:\n" + for err in self._errors: + message += f" {err}\n" + message += self._format_endpoint_context() + return message.rstrip() + class RequestValidationError(ValidationException): - def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None: - super().__init__(errors) + def __init__( + self, + errors: Sequence[Any], + *, + body: Any = None, + endpoint_ctx: Optional[EndpointContext] = None, + ) -> None: + super().__init__(errors, endpoint_ctx=endpoint_ctx) self.body = body class WebSocketRequestValidationError(ValidationException): - pass + def __init__( + self, + errors: Sequence[Any], + *, + endpoint_ctx: Optional[EndpointContext] = None, + ) -> None: + super().__init__(errors, endpoint_ctx=endpoint_ctx) class ResponseValidationError(ValidationException): - def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None: - super().__init__(errors) + def __init__( + self, + errors: Sequence[Any], + *, + body: Any = None, + endpoint_ctx: Optional[EndpointContext] = None, + ) -> None: + super().__init__(errors, endpoint_ctx=endpoint_ctx) self.body = body - - def __str__(self) -> str: - message = f"{len(self._errors)} validation errors:\n" - for err in self._errors: - message += f" {err}\n" - return message diff --git a/fastapi/routing.py b/fastapi/routing.py index c10175b16..9be2b44bc 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -46,6 +46,7 @@ from fastapi.dependencies.utils import ( ) from fastapi.encoders import jsonable_encoder from fastapi.exceptions import ( + EndpointContext, FastAPIError, RequestValidationError, ResponseValidationError, @@ -212,6 +213,33 @@ def _merge_lifespan_context( return merged_lifespan # type: ignore[return-value] +# Cache for endpoint context to avoid re-extracting on every request +_endpoint_context_cache: Dict[int, EndpointContext] = {} + + +def _extract_endpoint_context(func: Any) -> EndpointContext: + """Extract endpoint context with caching to avoid repeated file I/O.""" + func_id = id(func) + + if func_id in _endpoint_context_cache: + return _endpoint_context_cache[func_id] + + try: + ctx: EndpointContext = {} + + if (source_file := inspect.getsourcefile(func)) is not None: + ctx["file"] = source_file + if (line_number := inspect.getsourcelines(func)[1]) is not None: + ctx["line"] = line_number + if (func_name := getattr(func, "__name__", None)) is not None: + ctx["function"] = func_name + except Exception: + ctx = EndpointContext() + + _endpoint_context_cache[func_id] = ctx + return ctx + + async def serialize_response( *, field: Optional[ModelField] = None, @@ -223,6 +251,7 @@ async def serialize_response( exclude_defaults: bool = False, exclude_none: bool = False, is_coroutine: bool = True, + endpoint_ctx: Optional[EndpointContext] = None, ) -> Any: if field: errors = [] @@ -245,8 +274,11 @@ async def serialize_response( elif errors_: errors.append(errors_) if errors: + ctx = endpoint_ctx or EndpointContext() raise ResponseValidationError( - errors=_normalize_errors(errors), body=response_content + errors=_normalize_errors(errors), + body=response_content, + endpoint_ctx=ctx, ) if hasattr(field, "serialize"): @@ -318,6 +350,18 @@ def get_request_handler( "fastapi_middleware_astack not found in request scope" ) + # Extract endpoint context for error messages + endpoint_ctx = ( + _extract_endpoint_context(dependant.call) + if dependant.call + else EndpointContext() + ) + + if dependant.path: + # For mounted sub-apps, include the mount path prefix + mount_path = request.scope.get("root_path", "").rstrip("/") + endpoint_ctx["path"] = f"{request.method} {mount_path}{dependant.path}" + # Read body and auto-close files try: body: Any = None @@ -355,6 +399,7 @@ def get_request_handler( } ], body=e.doc, + endpoint_ctx=endpoint_ctx, ) raise validation_error from e except HTTPException: @@ -414,6 +459,7 @@ def get_request_handler( exclude_defaults=response_model_exclude_defaults, exclude_none=response_model_exclude_none, is_coroutine=is_coroutine, + endpoint_ctx=endpoint_ctx, ) response = actual_response_class(content, **response_args) if not is_body_allowed_for_status_code(response.status_code): @@ -421,7 +467,7 @@ def get_request_handler( response.headers.raw.extend(solved_result.response.headers.raw) if errors: validation_error = RequestValidationError( - _normalize_errors(errors), body=body + _normalize_errors(errors), body=body, endpoint_ctx=endpoint_ctx ) raise validation_error @@ -438,6 +484,15 @@ def get_websocket_app( embed_body_fields: bool = False, ) -> Callable[[WebSocket], Coroutine[Any, Any, Any]]: async def app(websocket: WebSocket) -> None: + endpoint_ctx = ( + _extract_endpoint_context(dependant.call) + if dependant.call + else EndpointContext() + ) + if dependant.path: + # For mounted sub-apps, include the mount path prefix + mount_path = websocket.scope.get("root_path", "").rstrip("/") + endpoint_ctx["path"] = f"WS {mount_path}{dependant.path}" async_exit_stack = websocket.scope.get("fastapi_inner_astack") assert isinstance(async_exit_stack, AsyncExitStack), ( "fastapi_inner_astack not found in request scope" @@ -451,7 +506,8 @@ def get_websocket_app( ) if solved_result.errors: raise WebSocketRequestValidationError( - _normalize_errors(solved_result.errors) + _normalize_errors(solved_result.errors), + endpoint_ctx=endpoint_ctx, ) assert dependant.call is not None, "dependant.call must be a function" await dependant.call(**solved_result.values) diff --git a/tests/test_validation_error_context.py b/tests/test_validation_error_context.py new file mode 100644 index 000000000..844b8a64f --- /dev/null +++ b/tests/test_validation_error_context.py @@ -0,0 +1,168 @@ +from fastapi import FastAPI, Request, WebSocket +from fastapi.exceptions import ( + RequestValidationError, + ResponseValidationError, + WebSocketRequestValidationError, +) +from fastapi.testclient import TestClient +from pydantic import BaseModel + + +class Item(BaseModel): + id: int + name: str + + +class ExceptionCapture: + def __init__(self): + self.exception = None + + def capture(self, exc): + self.exception = exc + return exc + + +app = FastAPI() +sub_app = FastAPI() +captured_exception = ExceptionCapture() + +app.mount(path="/sub", app=sub_app) + + +@app.exception_handler(RequestValidationError) +@sub_app.exception_handler(RequestValidationError) +async def request_validation_handler(request: Request, exc: RequestValidationError): + captured_exception.capture(exc) + raise exc + + +@app.exception_handler(ResponseValidationError) +@sub_app.exception_handler(ResponseValidationError) +async def response_validation_handler(_: Request, exc: ResponseValidationError): + captured_exception.capture(exc) + raise exc + + +@app.exception_handler(WebSocketRequestValidationError) +@sub_app.exception_handler(WebSocketRequestValidationError) +async def websocket_validation_handler( + websocket: WebSocket, exc: WebSocketRequestValidationError +): + captured_exception.capture(exc) + raise exc + + +@app.get("/users/{user_id}") +def get_user(user_id: int): + return {"user_id": user_id} # pragma: no cover + + +@app.get("/items/", response_model=Item) +def get_item(): + return {"name": "Widget"} + + +@sub_app.get("/items/", response_model=Item) +def get_sub_item(): + return {"name": "Widget"} # pragma: no cover + + +@app.websocket("/ws/{item_id}") +async def websocket_endpoint(websocket: WebSocket, item_id: int): + await websocket.accept() # pragma: no cover + await websocket.send_text(f"Item: {item_id}") # pragma: no cover + await websocket.close() # pragma: no cover + + +@sub_app.websocket("/ws/{item_id}") +async def subapp_websocket_endpoint(websocket: WebSocket, item_id: int): + await websocket.accept() # pragma: no cover + await websocket.send_text(f"Item: {item_id}") # pragma: no cover + await websocket.close() # pragma: no cover + + +client = TestClient(app) + + +def test_request_validation_error_includes_endpoint_context(): + captured_exception.exception = None + try: + client.get("/users/invalid") + except Exception: + pass + + assert captured_exception.exception is not None + error_str = str(captured_exception.exception) + assert "get_user" in error_str + assert "/users/" in error_str + + +def test_response_validation_error_includes_endpoint_context(): + captured_exception.exception = None + try: + client.get("/items/") + except Exception: + pass + + assert captured_exception.exception is not None + error_str = str(captured_exception.exception) + assert "get_item" in error_str + assert "/items/" in error_str + + +def test_websocket_validation_error_includes_endpoint_context(): + captured_exception.exception = None + try: + with client.websocket_connect("/ws/invalid"): + pass # pragma: no cover + except Exception: + pass + + assert captured_exception.exception is not None + error_str = str(captured_exception.exception) + assert "websocket_endpoint" in error_str + assert "/ws/" in error_str + + +def test_subapp_request_validation_error_includes_endpoint_context(): + captured_exception.exception = None + try: + client.get("/sub/items/") + except Exception: + pass + + assert captured_exception.exception is not None + error_str = str(captured_exception.exception) + assert "get_sub_item" in error_str + assert "/sub/items/" in error_str + + +def test_subapp_websocket_validation_error_includes_endpoint_context(): + captured_exception.exception = None + try: + with client.websocket_connect("/sub/ws/invalid"): + pass # pragma: no cover + except Exception: + pass + + assert captured_exception.exception is not None + error_str = str(captured_exception.exception) + assert "subapp_websocket_endpoint" in error_str + assert "/sub/ws/" in error_str + + +def test_validation_error_with_only_path(): + errors = [{"type": "missing", "loc": ("body", "name"), "msg": "Field required"}] + exc = RequestValidationError(errors, endpoint_ctx={"path": "GET /api/test"}) + error_str = str(exc) + assert "Endpoint: GET /api/test" in error_str + assert 'File "' not in error_str + + +def test_validation_error_with_no_context(): + errors = [{"type": "missing", "loc": ("body", "name"), "msg": "Field required"}] + exc = RequestValidationError(errors, endpoint_ctx={}) + error_str = str(exc) + assert "1 validation error:" in error_str + assert "Endpoint" not in error_str + assert 'File "' not in error_str From dbd34f15789f4afa851e339cea4fcd49f421039d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 6 Dec 2025 12:22:24 +0000 Subject: [PATCH 225/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index d27c47383..b294de906 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Features + +* 🚸 Improve tracebacks by adding endpoint metadata. PR [#14306](https://github.com/fastapi/fastapi/pull/14306) by [@savannahostrowski](https://github.com/savannahostrowski). + ## 0.123.10 ### Fixes From 5b6245666b8a499d2551caff7567056ef7f881b2 Mon Sep 17 00:00:00 2001 From: Yuji Teshima <36704166+yujiteshima@users.noreply.github.com> Date: Sat, 6 Dec 2025 21:23:01 +0900 Subject: [PATCH 226/256] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typo=20in=20?= =?UTF-8?q?`scripts/mkdocs=5Fhooks.py`=20(#14457)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/mkdocs_hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/mkdocs_hooks.py b/scripts/mkdocs_hooks.py index b9e4ff59e..09cfa99e3 100644 --- a/scripts/mkdocs_hooks.py +++ b/scripts/mkdocs_hooks.py @@ -132,7 +132,7 @@ def on_pre_page(page: Page, *, config: MkDocsConfig, files: Files) -> Page: def on_page_markdown( markdown: str, *, page: Page, config: MkDocsConfig, files: Files ) -> str: - # Set matadata["social"]["cards_layout_options"]["title"] to clean title (without + # Set metadata["social"]["cards_layout_options"]["title"] to clean title (without # permalink) title = page.title clean_title = title.split("{ #")[0] From a2cef707e30fb6eb14812e4e273e34079d30ae6b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 6 Dec 2025 12:23:23 +0000 Subject: [PATCH 227/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index b294de906..f49d46a8f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -11,6 +11,10 @@ hide: * 🚸 Improve tracebacks by adding endpoint metadata. PR [#14306](https://github.com/fastapi/fastapi/pull/14306) by [@savannahostrowski](https://github.com/savannahostrowski). +### Internal + +* ✏️ Fix typo in `scripts/mkdocs_hooks.py`. PR [#14457](https://github.com/fastapi/fastapi/pull/14457) by [@yujiteshima](https://github.com/yujiteshima). + ## 0.123.10 ### Fixes From b5ca13249e3f2002c70c3f2de528a128af2008f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 6 Dec 2025 14:09:51 +0100 Subject: [PATCH 228/256] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.12?= =?UTF-8?q?4.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f49d46a8f..f40223338 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.124.0 + ### Features * 🚸 Improve tracebacks by adding endpoint metadata. PR [#14306](https://github.com/fastapi/fastapi/pull/14306) by [@savannahostrowski](https://github.com/savannahostrowski). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 2396c501d..7009a7777 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.123.10" +__version__ = "0.124.0" from starlette import status as status From 81517f66ccb906afef898570c823053afba994d8 Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Mon, 8 Dec 2025 14:04:54 +0100 Subject: [PATCH 229/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20tech=20stack=20?= =?UTF-8?q?in=20project=20generation=20docs=20(#14472)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/de/docs/project-generation.md | 2 +- docs/en/docs/project-generation.md | 18 +++++++++--------- docs/pt/docs/project-generation.md | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/de/docs/project-generation.md b/docs/de/docs/project-generation.md index f830f0f4d..dd3c3b427 100644 --- a/docs/de/docs/project-generation.md +++ b/docs/de/docs/project-generation.md @@ -14,7 +14,7 @@ GitHub-Repository: Date: Mon, 8 Dec 2025 13:05:20 +0000 Subject: [PATCH 230/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f40223338..0af51573f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Docs + +* 📝 Update tech stack in project generation docs. PR [#14472](https://github.com/fastapi/fastapi/pull/14472) by [@alejsdev](https://github.com/alejsdev). + ## 0.124.0 ### Features From 8cedb742cb59f72a752bb0a6b4f73c02aeb15bf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 9 Dec 2025 03:12:24 -0800 Subject: [PATCH 231/256] =?UTF-8?q?=E2=9C=85=20Add=20test=20for=20Pydantic?= =?UTF-8?q?=20v2,=20dataclasses,=20UUID,=20and=20`=5F=5Fannotations=5F=5F`?= =?UTF-8?q?=20(#14477)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ataclasses_uuid_stringified_annotations.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 tests/test_pydanticv2_dataclasses_uuid_stringified_annotations.py diff --git a/tests/test_pydanticv2_dataclasses_uuid_stringified_annotations.py b/tests/test_pydanticv2_dataclasses_uuid_stringified_annotations.py new file mode 100644 index 000000000..c9f94563b --- /dev/null +++ b/tests/test_pydanticv2_dataclasses_uuid_stringified_annotations.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +import uuid +from dataclasses import dataclass, field +from typing import List, Union + +from dirty_equals import IsUUID +from fastapi import FastAPI +from fastapi.testclient import TestClient +from inline_snapshot import snapshot + + +@dataclass +class Item: + id: uuid.UUID + name: str + price: float + tags: List[str] = field(default_factory=list) + description: Union[str, None] = None + tax: Union[float, None] = None + + +app = FastAPI() + + +@app.get("/item", response_model=Item) +async def read_item(): + return { + "id": uuid.uuid4(), + "name": "Island In The Moon", + "price": 12.99, + "description": "A place to be be playin' and havin' fun", + "tags": ["breater"], + } + + +client = TestClient(app) + + +def test_annotations(): + response = client.get("/item") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "id": IsUUID(), + "name": "Island In The Moon", + "price": 12.99, + "tags": ["breater"], + "description": "A place to be be playin' and havin' fun", + "tax": None, + } + ) From 5b28a04d550d95ef0061f41f5a5dcfe36045e8c8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 9 Dec 2025 11:12:49 +0000 Subject: [PATCH 232/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 0af51573f..60bc1cc6f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -11,6 +11,10 @@ hide: * 📝 Update tech stack in project generation docs. PR [#14472](https://github.com/fastapi/fastapi/pull/14472) by [@alejsdev](https://github.com/alejsdev). +### Internal + +* ✅ Add test for Pydantic v2, dataclasses, UUID, and `__annotations__`. PR [#14477](https://github.com/fastapi/fastapi/pull/14477) by [@tiangolo](https://github.com/tiangolo). + ## 0.124.0 ### Features From 9475024640f2e204944c5aa2cd9c67a8826189d9 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Wed, 10 Dec 2025 09:55:32 +0100 Subject: [PATCH 233/256] =?UTF-8?q?=F0=9F=93=9D=20Add=20variants=20for=20c?= =?UTF-8?q?ode=20examples=20in=20"Advanced=20User=20Guide"=20(#14413)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/advanced/additional-responses.md | 4 +- docs/en/docs/advanced/dataclasses.md | 6 +- docs/en/docs/advanced/openapi-callbacks.md | 8 +- .../path-operation-advanced-configuration.md | 10 +- docs/en/docs/advanced/response-directly.md | 2 +- docs/en/docs/advanced/settings.md | 8 +- docs/en/docs/how-to/configure-swagger-ui.md | 2 +- .../docs/how-to/custom-request-and-route.md | 12 +- docs/en/docs/tutorial/bigger-applications.md | 78 +---- docs/en/docs/tutorial/cookie-param-models.md | 2 +- docs/en/docs/tutorial/testing.md | 54 +--- .../additional_responses/tutorial002_py310.py | 28 ++ .../additional_responses/tutorial004_py310.py | 30 ++ .../tutorial001_an.py | 36 +++ .../tutorial001_an_py310.py | 36 +++ .../tutorial001_an_py39.py | 35 +++ .../tutorial001_py310.py | 35 +++ .../tutorial001_py39.py | 35 +++ .../tutorial002_an.py | 30 ++ .../tutorial002_an_py310.py | 30 ++ .../tutorial002_an_py39.py | 29 ++ .../tutorial002_py310.py | 29 ++ .../tutorial002_py39.py | 29 ++ .../tutorial003_py310.py | 39 +++ docs_src/dataclasses/tutorial001_py310.py | 19 ++ docs_src/dataclasses/tutorial002_py310.py | 25 ++ docs_src/dataclasses/tutorial002_py39.py | 26 ++ docs_src/dataclasses/tutorial003_py310.py | 54 ++++ docs_src/dataclasses/tutorial003_py39.py | 55 ++++ .../openapi_callbacks/tutorial001_py310.py | 51 ++++ .../tutorial004_py310.py | 28 ++ .../tutorial004_py39.py | 30 ++ .../tutorial007_pv1_py39.py | 32 ++ .../tutorial007_py39.py | 32 ++ .../response_directly/tutorial001_py310.py | 21 ++ docs_src/settings/app03/config.py | 5 +- docs_src/settings/app03/config_pv1.py | 10 + docs_src/settings/app03_an/main.py | 2 +- docs_src/settings/app03_an_py39/config.py | 5 +- docs_src/settings/app03_an_py39/config_pv1.py | 10 + docs_src/settings/app03_an_py39/main.py | 2 +- pyproject.toml | 7 + .../test_tutorial002.py | 27 +- .../test_tutorial004.py | 27 +- .../test_tutorial001.py | 29 +- .../test_tutorial002.py | 29 +- .../test_tutorial003.py | 25 +- .../test_dataclasses/test_tutorial001.py | 28 +- .../test_dataclasses/test_tutorial002.py | 27 +- .../test_dataclasses/test_tutorial003.py | 33 +- .../test_tutorial001.py | 35 ++- .../test_tutorial004.py | 33 +- .../test_tutorial007.py | 20 +- .../test_tutorial007_pv1.py | 20 +- .../test_response_directly/__init__.py | 0 .../test_tutorial001.py | 288 ++++++++++++++++++ .../test_tutorial/test_settings/test_app02.py | 43 ++- .../test_tutorial/test_settings/test_app03.py | 59 ++++ .../test_using_request_directly/__init__.py | 0 .../test_tutorial001.py | 112 +++++++ .../test_websockets/test_tutorial003.py | 39 ++- .../test_websockets/test_tutorial003_py39.py | 50 --- 62 files changed, 1655 insertions(+), 290 deletions(-) create mode 100644 docs_src/additional_responses/tutorial002_py310.py create mode 100644 docs_src/additional_responses/tutorial004_py310.py create mode 100644 docs_src/custom_request_and_route/tutorial001_an.py create mode 100644 docs_src/custom_request_and_route/tutorial001_an_py310.py create mode 100644 docs_src/custom_request_and_route/tutorial001_an_py39.py create mode 100644 docs_src/custom_request_and_route/tutorial001_py310.py create mode 100644 docs_src/custom_request_and_route/tutorial001_py39.py create mode 100644 docs_src/custom_request_and_route/tutorial002_an.py create mode 100644 docs_src/custom_request_and_route/tutorial002_an_py310.py create mode 100644 docs_src/custom_request_and_route/tutorial002_an_py39.py create mode 100644 docs_src/custom_request_and_route/tutorial002_py310.py create mode 100644 docs_src/custom_request_and_route/tutorial002_py39.py create mode 100644 docs_src/custom_request_and_route/tutorial003_py310.py create mode 100644 docs_src/dataclasses/tutorial001_py310.py create mode 100644 docs_src/dataclasses/tutorial002_py310.py create mode 100644 docs_src/dataclasses/tutorial002_py39.py create mode 100644 docs_src/dataclasses/tutorial003_py310.py create mode 100644 docs_src/dataclasses/tutorial003_py39.py create mode 100644 docs_src/openapi_callbacks/tutorial001_py310.py create mode 100644 docs_src/path_operation_advanced_configuration/tutorial004_py310.py create mode 100644 docs_src/path_operation_advanced_configuration/tutorial004_py39.py create mode 100644 docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py create mode 100644 docs_src/path_operation_advanced_configuration/tutorial007_py39.py create mode 100644 docs_src/response_directly/tutorial001_py310.py create mode 100644 docs_src/settings/app03/config_pv1.py create mode 100644 docs_src/settings/app03_an_py39/config_pv1.py create mode 100644 tests/test_tutorial/test_response_directly/__init__.py create mode 100644 tests/test_tutorial/test_response_directly/test_tutorial001.py create mode 100644 tests/test_tutorial/test_settings/test_app03.py create mode 100644 tests/test_tutorial/test_using_request_directly/__init__.py create mode 100644 tests/test_tutorial/test_using_request_directly/test_tutorial001.py delete mode 100644 tests/test_tutorial/test_websockets/test_tutorial003_py39.py diff --git a/docs/en/docs/advanced/additional-responses.md b/docs/en/docs/advanced/additional-responses.md index 799532c5b..cb3a40d13 100644 --- a/docs/en/docs/advanced/additional-responses.md +++ b/docs/en/docs/advanced/additional-responses.md @@ -175,7 +175,7 @@ You can use this same `responses` parameter to add different media types for the For example, you can add an additional media type of `image/png`, declaring that your *path operation* can return a JSON object (with media type `application/json`) or a PNG image: -{* ../../docs_src/additional_responses/tutorial002.py hl[19:24,28] *} +{* ../../docs_src/additional_responses/tutorial002_py310.py hl[17:22,26] *} /// note @@ -237,7 +237,7 @@ You can use that technique to reuse some predefined responses in your *path oper For example: -{* ../../docs_src/additional_responses/tutorial004.py hl[13:17,26] *} +{* ../../docs_src/additional_responses/tutorial004_py310.py hl[11:15,24] *} ## More information about OpenAPI responses { #more-information-about-openapi-responses } diff --git a/docs/en/docs/advanced/dataclasses.md b/docs/en/docs/advanced/dataclasses.md index b7b9b65c5..574beb65f 100644 --- a/docs/en/docs/advanced/dataclasses.md +++ b/docs/en/docs/advanced/dataclasses.md @@ -4,7 +4,7 @@ FastAPI is built on top of **Pydantic**, and I have been showing you how to use But FastAPI also supports using `dataclasses` the same way: -{* ../../docs_src/dataclasses/tutorial001.py hl[1,7:12,19:20] *} +{* ../../docs_src/dataclasses/tutorial001_py310.py hl[1,6:11,18:19] *} This is still supported thanks to **Pydantic**, as it has internal support for `dataclasses`. @@ -32,7 +32,7 @@ But if you have a bunch of dataclasses laying around, this is a nice trick to us You can also use `dataclasses` in the `response_model` parameter: -{* ../../docs_src/dataclasses/tutorial002.py hl[1,7:13,19] *} +{* ../../docs_src/dataclasses/tutorial002_py310.py hl[1,6:12,18] *} The dataclass will be automatically converted to a Pydantic dataclass. @@ -48,7 +48,7 @@ In some cases, you might still have to use Pydantic's version of `dataclasses`. In that case, you can simply swap the standard `dataclasses` with `pydantic.dataclasses`, which is a drop-in replacement: -{* ../../docs_src/dataclasses/tutorial003.py hl[1,5,8:11,14:17,23:25,28] *} +{* ../../docs_src/dataclasses/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *} 1. We still import `field` from standard `dataclasses`. diff --git a/docs/en/docs/advanced/openapi-callbacks.md b/docs/en/docs/advanced/openapi-callbacks.md index 059d893c2..5bd7c2cfd 100644 --- a/docs/en/docs/advanced/openapi-callbacks.md +++ b/docs/en/docs/advanced/openapi-callbacks.md @@ -31,7 +31,7 @@ It will have a *path operation* that will receive an `Invoice` body, and a query This part is pretty normal, most of the code is probably already familiar to you: -{* ../../docs_src/openapi_callbacks/tutorial001.py hl[9:13,36:53] *} +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[7:11,34:51] *} /// tip @@ -90,7 +90,7 @@ Temporarily adopting this point of view (of the *external developer*) can help y First create a new `APIRouter` that will contain one or more callbacks. -{* ../../docs_src/openapi_callbacks/tutorial001.py hl[3,25] *} +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[1,23] *} ### Create the callback *path operation* { #create-the-callback-path-operation } @@ -101,7 +101,7 @@ It should look just like a normal FastAPI *path operation*: * It should probably have a declaration of the body it should receive, e.g. `body: InvoiceEvent`. * And it could also have a declaration of the response it should return, e.g. `response_model=InvoiceEventReceived`. -{* ../../docs_src/openapi_callbacks/tutorial001.py hl[16:18,21:22,28:32] *} +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[14:16,19:20,26:30] *} There are 2 main differences from a normal *path operation*: @@ -169,7 +169,7 @@ At this point you have the *callback path operation(s)* needed (the one(s) that Now use the parameter `callbacks` in *your API's path operation decorator* to pass the attribute `.routes` (that's actually just a `list` of routes/*path operations*) from that callback router: -{* ../../docs_src/openapi_callbacks/tutorial001.py hl[35] *} +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[33] *} /// tip diff --git a/docs/en/docs/advanced/path-operation-advanced-configuration.md b/docs/en/docs/advanced/path-operation-advanced-configuration.md index b9961f9f3..5879bc5c7 100644 --- a/docs/en/docs/advanced/path-operation-advanced-configuration.md +++ b/docs/en/docs/advanced/path-operation-advanced-configuration.md @@ -50,7 +50,7 @@ Adding an `\f` (an escaped "form feed" character) causes **FastAPI** to truncate It won't show up in the documentation, but other tools (such as Sphinx) will be able to use the rest. -{* ../../docs_src/path_operation_advanced_configuration/tutorial004.py hl[19:29] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial004_py310.py hl[17:27] *} ## Additional Responses { #additional-responses } @@ -155,13 +155,13 @@ For example, in this application we don't use FastAPI's integrated functionality //// tab | Pydantic v2 -{* ../../docs_src/path_operation_advanced_configuration/tutorial007.py hl[17:22, 24] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[15:20, 22] *} //// //// tab | Pydantic v1 -{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py hl[17:22, 24] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py hl[15:20, 22] *} //// @@ -179,13 +179,13 @@ And then in our code, we parse that YAML content directly, and then we are again //// tab | Pydantic v2 -{* ../../docs_src/path_operation_advanced_configuration/tutorial007.py hl[26:33] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[24:31] *} //// //// tab | Pydantic v1 -{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py hl[26:33] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py hl[24:31] *} //// diff --git a/docs/en/docs/advanced/response-directly.md b/docs/en/docs/advanced/response-directly.md index 3197e1bd4..156b4dac7 100644 --- a/docs/en/docs/advanced/response-directly.md +++ b/docs/en/docs/advanced/response-directly.md @@ -34,7 +34,7 @@ For example, you cannot put a Pydantic model in a `JSONResponse` without first c For those cases, you can use the `jsonable_encoder` to convert your data before passing it to a response: -{* ../../docs_src/response_directly/tutorial001.py hl[6:7,21:22] *} +{* ../../docs_src/response_directly/tutorial001_py310.py hl[5:6,20:21] *} /// note | Technical Details diff --git a/docs/en/docs/advanced/settings.md b/docs/en/docs/advanced/settings.md index a218c3d01..0220c52ce 100644 --- a/docs/en/docs/advanced/settings.md +++ b/docs/en/docs/advanced/settings.md @@ -148,7 +148,7 @@ This could be especially useful during testing, as it's very easy to override a Coming from the previous example, your `config.py` file could look like: -{* ../../docs_src/settings/app02/config.py hl[10] *} +{* ../../docs_src/settings/app02_an_py39/config.py hl[10] *} Notice that now we don't create a default instance `settings = Settings()`. @@ -174,7 +174,7 @@ And then we can require it from the *path operation function* as a dependency an Then it would be very easy to provide a different settings object during testing by creating a dependency override for `get_settings`: -{* ../../docs_src/settings/app02/test_main.py hl[9:10,13,21] *} +{* ../../docs_src/settings/app02_an_py39/test_main.py hl[9:10,13,21] *} In the dependency override we set a new value for the `admin_email` when creating the new `Settings` object, and then we return that new object. @@ -217,7 +217,7 @@ And then update your `config.py` with: //// tab | Pydantic v2 -{* ../../docs_src/settings/app03_an/config.py hl[9] *} +{* ../../docs_src/settings/app03_an_py39/config.py hl[9] *} /// tip @@ -229,7 +229,7 @@ The `model_config` attribute is used just for Pydantic configuration. You can re //// tab | Pydantic v1 -{* ../../docs_src/settings/app03_an/config_pv1.py hl[9:10] *} +{* ../../docs_src/settings/app03_an_py39/config_pv1.py hl[9:10] *} /// tip diff --git a/docs/en/docs/how-to/configure-swagger-ui.md b/docs/en/docs/how-to/configure-swagger-ui.md index 2d7b99f8f..3dbfcffec 100644 --- a/docs/en/docs/how-to/configure-swagger-ui.md +++ b/docs/en/docs/how-to/configure-swagger-ui.md @@ -40,7 +40,7 @@ FastAPI includes some default configuration parameters appropriate for most of t It includes these default configurations: -{* ../../fastapi/openapi/docs.py ln[8:23] hl[17:23] *} +{* ../../fastapi/openapi/docs.py ln[9:24] hl[18:24] *} You can override any of them by setting a different value in the argument `swagger_ui_parameters`. diff --git a/docs/en/docs/how-to/custom-request-and-route.md b/docs/en/docs/how-to/custom-request-and-route.md index 884c8ed04..bfc60729f 100644 --- a/docs/en/docs/how-to/custom-request-and-route.md +++ b/docs/en/docs/how-to/custom-request-and-route.md @@ -42,7 +42,7 @@ If there's no `gzip` in the header, it will not try to decompress the body. That way, the same route class can handle gzip compressed or uncompressed requests. -{* ../../docs_src/custom_request_and_route/tutorial001.py hl[8:15] *} +{* ../../docs_src/custom_request_and_route/tutorial001_an_py310.py hl[9:16] *} ### Create a custom `GzipRoute` class { #create-a-custom-gziproute-class } @@ -54,7 +54,7 @@ This method returns a function. And that function is what will receive a request Here we use it to create a `GzipRequest` from the original request. -{* ../../docs_src/custom_request_and_route/tutorial001.py hl[18:26] *} +{* ../../docs_src/custom_request_and_route/tutorial001_an_py310.py hl[19:27] *} /// note | Technical Details @@ -92,18 +92,18 @@ We can also use this same approach to access the request body in an exception ha All we need to do is handle the request inside a `try`/`except` block: -{* ../../docs_src/custom_request_and_route/tutorial002.py hl[13,15] *} +{* ../../docs_src/custom_request_and_route/tutorial002_an_py310.py hl[14,16] *} If an exception occurs, the`Request` instance will still be in scope, so we can read and make use of the request body when handling the error: -{* ../../docs_src/custom_request_and_route/tutorial002.py hl[16:18] *} +{* ../../docs_src/custom_request_and_route/tutorial002_an_py310.py hl[17:19] *} ## Custom `APIRoute` class in a router { #custom-apiroute-class-in-a-router } You can also set the `route_class` parameter of an `APIRouter`: -{* ../../docs_src/custom_request_and_route/tutorial003.py hl[26] *} +{* ../../docs_src/custom_request_and_route/tutorial003_py310.py hl[26] *} In this example, the *path operations* under the `router` will use the custom `TimedRoute` class, and will have an extra `X-Response-Time` header in the response with the time it took to generate the response: -{* ../../docs_src/custom_request_and_route/tutorial003.py hl[13:20] *} +{* ../../docs_src/custom_request_and_route/tutorial003_py310.py hl[13:20] *} diff --git a/docs/en/docs/tutorial/bigger-applications.md b/docs/en/docs/tutorial/bigger-applications.md index 74daa5483..3cc9d7ecf 100644 --- a/docs/en/docs/tutorial/bigger-applications.md +++ b/docs/en/docs/tutorial/bigger-applications.md @@ -85,9 +85,7 @@ You can create the *path operations* for that module using `APIRouter`. You import it and create an "instance" the same way you would with the class `FastAPI`: -```Python hl_lines="1 3" title="app/routers/users.py" -{!../../docs_src/bigger_applications/app/routers/users.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[1,3] title["app/routers/users.py"] *} ### *Path operations* with `APIRouter` { #path-operations-with-apirouter } @@ -95,9 +93,7 @@ And then you use it to declare your *path operations*. Use it the same way you would use the `FastAPI` class: -```Python hl_lines="6 11 16" title="app/routers/users.py" -{!../../docs_src/bigger_applications/app/routers/users.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[6,11,16] title["app/routers/users.py"] *} You can think of `APIRouter` as a "mini `FastAPI`" class. @@ -121,35 +117,7 @@ So we put them in their own `dependencies` module (`app/dependencies.py`). We will now use a simple dependency to read a custom `X-Token` header: -//// tab | Python 3.9+ - -```Python hl_lines="3 6-8" title="app/dependencies.py" -{!> ../../docs_src/bigger_applications/app_an_py39/dependencies.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python hl_lines="1 5-7" title="app/dependencies.py" -{!> ../../docs_src/bigger_applications/app_an/dependencies.py!} -``` - -//// - -//// tab | Python 3.8+ non-Annotated - -/// tip - -Prefer to use the `Annotated` version if possible. - -/// - -```Python hl_lines="1 4-6" title="app/dependencies.py" -{!> ../../docs_src/bigger_applications/app/dependencies.py!} -``` - -//// +{* ../../docs_src/bigger_applications/app_an_py39/dependencies.py hl[3,6:8] title["app/dependencies.py"] *} /// tip @@ -181,9 +149,7 @@ We know all the *path operations* in this module have the same: So, instead of adding all that to each *path operation*, we can add it to the `APIRouter`. -```Python hl_lines="5-10 16 21" title="app/routers/items.py" -{!../../docs_src/bigger_applications/app/routers/items.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[5:10,16,21] title["app/routers/items.py"] *} As the path of each *path operation* has to start with `/`, like in: @@ -242,9 +208,7 @@ And we need to get the dependency function from the module `app.dependencies`, t So we use a relative import with `..` for the dependencies: -```Python hl_lines="3" title="app/routers/items.py" -{!../../docs_src/bigger_applications/app/routers/items.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[3] title["app/routers/items.py"] *} #### How relative imports work { #how-relative-imports-work } @@ -315,9 +279,7 @@ We are not adding the prefix `/items` nor the `tags=["items"]` to each *path ope But we can still add _more_ `tags` that will be applied to a specific *path operation*, and also some extra `responses` specific to that *path operation*: -```Python hl_lines="30-31" title="app/routers/items.py" -{!../../docs_src/bigger_applications/app/routers/items.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[30:31] title["app/routers/items.py"] *} /// tip @@ -343,17 +305,13 @@ You import and create a `FastAPI` class as normally. And we can even declare [global dependencies](dependencies/global-dependencies.md){.internal-link target=_blank} that will be combined with the dependencies for each `APIRouter`: -```Python hl_lines="1 3 7" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[1,3,7] title["app/main.py"] *} ### Import the `APIRouter` { #import-the-apirouter } Now we import the other submodules that have `APIRouter`s: -```Python hl_lines="4-5" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[4:5] title["app/main.py"] *} As the files `app/routers/users.py` and `app/routers/items.py` are submodules that are part of the same Python package `app`, we can use a single dot `.` to import them using "relative imports". @@ -416,17 +374,13 @@ the `router` from `users` would overwrite the one from `items` and we wouldn't b So, to be able to use both of them in the same file, we import the submodules directly: -```Python hl_lines="5" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[5] title["app/main.py"] *} ### Include the `APIRouter`s for `users` and `items` { #include-the-apirouters-for-users-and-items } Now, let's include the `router`s from the submodules `users` and `items`: -```Python hl_lines="10-11" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[10:11] title["app/main.py"] *} /// info @@ -466,17 +420,13 @@ It contains an `APIRouter` with some admin *path operations* that your organizat For this example it will be super simple. But let's say that because it is shared with other projects in the organization, we cannot modify it and add a `prefix`, `dependencies`, `tags`, etc. directly to the `APIRouter`: -```Python hl_lines="3" title="app/internal/admin.py" -{!../../docs_src/bigger_applications/app/internal/admin.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *} But we still want to set a custom `prefix` when including the `APIRouter` so that all its *path operations* start with `/admin`, we want to secure it with the `dependencies` we already have for this project, and we want to include `tags` and `responses`. We can declare all that without having to modify the original `APIRouter` by passing those parameters to `app.include_router()`: -```Python hl_lines="14-17" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[14:17] title["app/main.py"] *} That way, the original `APIRouter` will stay unmodified, so we can still share that same `app/internal/admin.py` file with other projects in the organization. @@ -497,9 +447,7 @@ We can also add *path operations* directly to the `FastAPI` app. Here we do it... just to show that we can 🤷: -```Python hl_lines="21-23" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[21:23] title["app/main.py"] *} and it will work correctly, together with all the other *path operations* added with `app.include_router()`. diff --git a/docs/en/docs/tutorial/cookie-param-models.md b/docs/en/docs/tutorial/cookie-param-models.md index 96dc5cf3d..016a65d7f 100644 --- a/docs/en/docs/tutorial/cookie-param-models.md +++ b/docs/en/docs/tutorial/cookie-param-models.md @@ -50,7 +50,7 @@ Your API now has the power to control its own bytes: + if not hasattr(self, "_body"): + body = await super().body() + if "gzip" in self.headers.getlist("Content-Encoding"): + body = gzip.decompress(body) + self._body = body + return self._body + + +class GzipRoute(APIRoute): + def get_route_handler(self) -> Callable: + original_route_handler = super().get_route_handler() + + async def custom_route_handler(request: Request) -> Response: + request = GzipRequest(request.scope, request.receive) + return await original_route_handler(request) + + return custom_route_handler + + +app = FastAPI() +app.router.route_class = GzipRoute + + +@app.post("/sum") +async def sum_numbers(numbers: Annotated[List[int], Body()]): + return {"sum": sum(numbers)} diff --git a/docs_src/custom_request_and_route/tutorial001_an_py310.py b/docs_src/custom_request_and_route/tutorial001_an_py310.py new file mode 100644 index 000000000..381bab6d8 --- /dev/null +++ b/docs_src/custom_request_and_route/tutorial001_an_py310.py @@ -0,0 +1,36 @@ +import gzip +from collections.abc import Callable +from typing import Annotated + +from fastapi import Body, FastAPI, Request, Response +from fastapi.routing import APIRoute + + +class GzipRequest(Request): + async def body(self) -> bytes: + if not hasattr(self, "_body"): + body = await super().body() + if "gzip" in self.headers.getlist("Content-Encoding"): + body = gzip.decompress(body) + self._body = body + return self._body + + +class GzipRoute(APIRoute): + def get_route_handler(self) -> Callable: + original_route_handler = super().get_route_handler() + + async def custom_route_handler(request: Request) -> Response: + request = GzipRequest(request.scope, request.receive) + return await original_route_handler(request) + + return custom_route_handler + + +app = FastAPI() +app.router.route_class = GzipRoute + + +@app.post("/sum") +async def sum_numbers(numbers: Annotated[list[int], Body()]): + return {"sum": sum(numbers)} diff --git a/docs_src/custom_request_and_route/tutorial001_an_py39.py b/docs_src/custom_request_and_route/tutorial001_an_py39.py new file mode 100644 index 000000000..076727e64 --- /dev/null +++ b/docs_src/custom_request_and_route/tutorial001_an_py39.py @@ -0,0 +1,35 @@ +import gzip +from typing import Annotated, Callable + +from fastapi import Body, FastAPI, Request, Response +from fastapi.routing import APIRoute + + +class GzipRequest(Request): + async def body(self) -> bytes: + if not hasattr(self, "_body"): + body = await super().body() + if "gzip" in self.headers.getlist("Content-Encoding"): + body = gzip.decompress(body) + self._body = body + return self._body + + +class GzipRoute(APIRoute): + def get_route_handler(self) -> Callable: + original_route_handler = super().get_route_handler() + + async def custom_route_handler(request: Request) -> Response: + request = GzipRequest(request.scope, request.receive) + return await original_route_handler(request) + + return custom_route_handler + + +app = FastAPI() +app.router.route_class = GzipRoute + + +@app.post("/sum") +async def sum_numbers(numbers: Annotated[list[int], Body()]): + return {"sum": sum(numbers)} diff --git a/docs_src/custom_request_and_route/tutorial001_py310.py b/docs_src/custom_request_and_route/tutorial001_py310.py new file mode 100644 index 000000000..c678088ce --- /dev/null +++ b/docs_src/custom_request_and_route/tutorial001_py310.py @@ -0,0 +1,35 @@ +import gzip +from collections.abc import Callable + +from fastapi import Body, FastAPI, Request, Response +from fastapi.routing import APIRoute + + +class GzipRequest(Request): + async def body(self) -> bytes: + if not hasattr(self, "_body"): + body = await super().body() + if "gzip" in self.headers.getlist("Content-Encoding"): + body = gzip.decompress(body) + self._body = body + return self._body + + +class GzipRoute(APIRoute): + def get_route_handler(self) -> Callable: + original_route_handler = super().get_route_handler() + + async def custom_route_handler(request: Request) -> Response: + request = GzipRequest(request.scope, request.receive) + return await original_route_handler(request) + + return custom_route_handler + + +app = FastAPI() +app.router.route_class = GzipRoute + + +@app.post("/sum") +async def sum_numbers(numbers: list[int] = Body()): + return {"sum": sum(numbers)} diff --git a/docs_src/custom_request_and_route/tutorial001_py39.py b/docs_src/custom_request_and_route/tutorial001_py39.py new file mode 100644 index 000000000..54b20b942 --- /dev/null +++ b/docs_src/custom_request_and_route/tutorial001_py39.py @@ -0,0 +1,35 @@ +import gzip +from typing import Callable + +from fastapi import Body, FastAPI, Request, Response +from fastapi.routing import APIRoute + + +class GzipRequest(Request): + async def body(self) -> bytes: + if not hasattr(self, "_body"): + body = await super().body() + if "gzip" in self.headers.getlist("Content-Encoding"): + body = gzip.decompress(body) + self._body = body + return self._body + + +class GzipRoute(APIRoute): + def get_route_handler(self) -> Callable: + original_route_handler = super().get_route_handler() + + async def custom_route_handler(request: Request) -> Response: + request = GzipRequest(request.scope, request.receive) + return await original_route_handler(request) + + return custom_route_handler + + +app = FastAPI() +app.router.route_class = GzipRoute + + +@app.post("/sum") +async def sum_numbers(numbers: list[int] = Body()): + return {"sum": sum(numbers)} diff --git a/docs_src/custom_request_and_route/tutorial002_an.py b/docs_src/custom_request_and_route/tutorial002_an.py new file mode 100644 index 000000000..127f7a9ce --- /dev/null +++ b/docs_src/custom_request_and_route/tutorial002_an.py @@ -0,0 +1,30 @@ +from typing import Callable, List + +from fastapi import Body, FastAPI, HTTPException, Request, Response +from fastapi.exceptions import RequestValidationError +from fastapi.routing import APIRoute +from typing_extensions import Annotated + + +class ValidationErrorLoggingRoute(APIRoute): + def get_route_handler(self) -> Callable: + original_route_handler = super().get_route_handler() + + async def custom_route_handler(request: Request) -> Response: + try: + return await original_route_handler(request) + except RequestValidationError as exc: + body = await request.body() + detail = {"errors": exc.errors(), "body": body.decode()} + raise HTTPException(status_code=422, detail=detail) + + return custom_route_handler + + +app = FastAPI() +app.router.route_class = ValidationErrorLoggingRoute + + +@app.post("/") +async def sum_numbers(numbers: Annotated[List[int], Body()]): + return sum(numbers) diff --git a/docs_src/custom_request_and_route/tutorial002_an_py310.py b/docs_src/custom_request_and_route/tutorial002_an_py310.py new file mode 100644 index 000000000..69b7de485 --- /dev/null +++ b/docs_src/custom_request_and_route/tutorial002_an_py310.py @@ -0,0 +1,30 @@ +from collections.abc import Callable +from typing import Annotated + +from fastapi import Body, FastAPI, HTTPException, Request, Response +from fastapi.exceptions import RequestValidationError +from fastapi.routing import APIRoute + + +class ValidationErrorLoggingRoute(APIRoute): + def get_route_handler(self) -> Callable: + original_route_handler = super().get_route_handler() + + async def custom_route_handler(request: Request) -> Response: + try: + return await original_route_handler(request) + except RequestValidationError as exc: + body = await request.body() + detail = {"errors": exc.errors(), "body": body.decode()} + raise HTTPException(status_code=422, detail=detail) + + return custom_route_handler + + +app = FastAPI() +app.router.route_class = ValidationErrorLoggingRoute + + +@app.post("/") +async def sum_numbers(numbers: Annotated[list[int], Body()]): + return sum(numbers) diff --git a/docs_src/custom_request_and_route/tutorial002_an_py39.py b/docs_src/custom_request_and_route/tutorial002_an_py39.py new file mode 100644 index 000000000..e7de09de4 --- /dev/null +++ b/docs_src/custom_request_and_route/tutorial002_an_py39.py @@ -0,0 +1,29 @@ +from typing import Annotated, Callable + +from fastapi import Body, FastAPI, HTTPException, Request, Response +from fastapi.exceptions import RequestValidationError +from fastapi.routing import APIRoute + + +class ValidationErrorLoggingRoute(APIRoute): + def get_route_handler(self) -> Callable: + original_route_handler = super().get_route_handler() + + async def custom_route_handler(request: Request) -> Response: + try: + return await original_route_handler(request) + except RequestValidationError as exc: + body = await request.body() + detail = {"errors": exc.errors(), "body": body.decode()} + raise HTTPException(status_code=422, detail=detail) + + return custom_route_handler + + +app = FastAPI() +app.router.route_class = ValidationErrorLoggingRoute + + +@app.post("/") +async def sum_numbers(numbers: Annotated[list[int], Body()]): + return sum(numbers) diff --git a/docs_src/custom_request_and_route/tutorial002_py310.py b/docs_src/custom_request_and_route/tutorial002_py310.py new file mode 100644 index 000000000..13a5ca542 --- /dev/null +++ b/docs_src/custom_request_and_route/tutorial002_py310.py @@ -0,0 +1,29 @@ +from collections.abc import Callable + +from fastapi import Body, FastAPI, HTTPException, Request, Response +from fastapi.exceptions import RequestValidationError +from fastapi.routing import APIRoute + + +class ValidationErrorLoggingRoute(APIRoute): + def get_route_handler(self) -> Callable: + original_route_handler = super().get_route_handler() + + async def custom_route_handler(request: Request) -> Response: + try: + return await original_route_handler(request) + except RequestValidationError as exc: + body = await request.body() + detail = {"errors": exc.errors(), "body": body.decode()} + raise HTTPException(status_code=422, detail=detail) + + return custom_route_handler + + +app = FastAPI() +app.router.route_class = ValidationErrorLoggingRoute + + +@app.post("/") +async def sum_numbers(numbers: list[int] = Body()): + return sum(numbers) diff --git a/docs_src/custom_request_and_route/tutorial002_py39.py b/docs_src/custom_request_and_route/tutorial002_py39.py new file mode 100644 index 000000000..c4e474828 --- /dev/null +++ b/docs_src/custom_request_and_route/tutorial002_py39.py @@ -0,0 +1,29 @@ +from typing import Callable + +from fastapi import Body, FastAPI, HTTPException, Request, Response +from fastapi.exceptions import RequestValidationError +from fastapi.routing import APIRoute + + +class ValidationErrorLoggingRoute(APIRoute): + def get_route_handler(self) -> Callable: + original_route_handler = super().get_route_handler() + + async def custom_route_handler(request: Request) -> Response: + try: + return await original_route_handler(request) + except RequestValidationError as exc: + body = await request.body() + detail = {"errors": exc.errors(), "body": body.decode()} + raise HTTPException(status_code=422, detail=detail) + + return custom_route_handler + + +app = FastAPI() +app.router.route_class = ValidationErrorLoggingRoute + + +@app.post("/") +async def sum_numbers(numbers: list[int] = Body()): + return sum(numbers) diff --git a/docs_src/custom_request_and_route/tutorial003_py310.py b/docs_src/custom_request_and_route/tutorial003_py310.py new file mode 100644 index 000000000..f4e60be61 --- /dev/null +++ b/docs_src/custom_request_and_route/tutorial003_py310.py @@ -0,0 +1,39 @@ +import time +from collections.abc import Callable + +from fastapi import APIRouter, FastAPI, Request, Response +from fastapi.routing import APIRoute + + +class TimedRoute(APIRoute): + def get_route_handler(self) -> Callable: + original_route_handler = super().get_route_handler() + + async def custom_route_handler(request: Request) -> Response: + before = time.time() + response: Response = await original_route_handler(request) + duration = time.time() - before + response.headers["X-Response-Time"] = str(duration) + print(f"route duration: {duration}") + print(f"route response: {response}") + print(f"route response headers: {response.headers}") + return response + + return custom_route_handler + + +app = FastAPI() +router = APIRouter(route_class=TimedRoute) + + +@app.get("/") +async def not_timed(): + return {"message": "Not timed"} + + +@router.get("/timed") +async def timed(): + return {"message": "It's the time of my life"} + + +app.include_router(router) diff --git a/docs_src/dataclasses/tutorial001_py310.py b/docs_src/dataclasses/tutorial001_py310.py new file mode 100644 index 000000000..ab709a7c8 --- /dev/null +++ b/docs_src/dataclasses/tutorial001_py310.py @@ -0,0 +1,19 @@ +from dataclasses import dataclass + +from fastapi import FastAPI + + +@dataclass +class Item: + name: str + price: float + description: str | None = None + tax: float | None = None + + +app = FastAPI() + + +@app.post("/items/") +async def create_item(item: Item): + return item diff --git a/docs_src/dataclasses/tutorial002_py310.py b/docs_src/dataclasses/tutorial002_py310.py new file mode 100644 index 000000000..e16249f1e --- /dev/null +++ b/docs_src/dataclasses/tutorial002_py310.py @@ -0,0 +1,25 @@ +from dataclasses import dataclass, field + +from fastapi import FastAPI + + +@dataclass +class Item: + name: str + price: float + tags: list[str] = field(default_factory=list) + description: str | None = None + tax: float | None = None + + +app = FastAPI() + + +@app.get("/items/next", response_model=Item) +async def read_next_item(): + return { + "name": "Island In The Moon", + "price": 12.99, + "description": "A place to be playin' and havin' fun", + "tags": ["breater"], + } diff --git a/docs_src/dataclasses/tutorial002_py39.py b/docs_src/dataclasses/tutorial002_py39.py new file mode 100644 index 000000000..0c23765d8 --- /dev/null +++ b/docs_src/dataclasses/tutorial002_py39.py @@ -0,0 +1,26 @@ +from dataclasses import dataclass, field +from typing import Union + +from fastapi import FastAPI + + +@dataclass +class Item: + name: str + price: float + tags: list[str] = field(default_factory=list) + description: Union[str, None] = None + tax: Union[float, None] = None + + +app = FastAPI() + + +@app.get("/items/next", response_model=Item) +async def read_next_item(): + return { + "name": "Island In The Moon", + "price": 12.99, + "description": "A place to be playin' and havin' fun", + "tags": ["breater"], + } diff --git a/docs_src/dataclasses/tutorial003_py310.py b/docs_src/dataclasses/tutorial003_py310.py new file mode 100644 index 000000000..9b9a3fd63 --- /dev/null +++ b/docs_src/dataclasses/tutorial003_py310.py @@ -0,0 +1,54 @@ +from dataclasses import field # (1) + +from fastapi import FastAPI +from pydantic.dataclasses import dataclass # (2) + + +@dataclass +class Item: + name: str + description: str | None = None + + +@dataclass +class Author: + name: str + items: list[Item] = field(default_factory=list) # (3) + + +app = FastAPI() + + +@app.post("/authors/{author_id}/items/", response_model=Author) # (4) +async def create_author_items(author_id: str, items: list[Item]): # (5) + return {"name": author_id, "items": items} # (6) + + +@app.get("/authors/", response_model=list[Author]) # (7) +def get_authors(): # (8) + return [ # (9) + { + "name": "Breaters", + "items": [ + { + "name": "Island In The Moon", + "description": "A place to be playin' and havin' fun", + }, + {"name": "Holy Buddies"}, + ], + }, + { + "name": "System of an Up", + "items": [ + { + "name": "Salt", + "description": "The kombucha mushroom people's favorite", + }, + {"name": "Pad Thai"}, + { + "name": "Lonely Night", + "description": "The mostests lonliest nightiest of allest", + }, + ], + }, + ] diff --git a/docs_src/dataclasses/tutorial003_py39.py b/docs_src/dataclasses/tutorial003_py39.py new file mode 100644 index 000000000..991708c00 --- /dev/null +++ b/docs_src/dataclasses/tutorial003_py39.py @@ -0,0 +1,55 @@ +from dataclasses import field # (1) +from typing import Union + +from fastapi import FastAPI +from pydantic.dataclasses import dataclass # (2) + + +@dataclass +class Item: + name: str + description: Union[str, None] = None + + +@dataclass +class Author: + name: str + items: list[Item] = field(default_factory=list) # (3) + + +app = FastAPI() + + +@app.post("/authors/{author_id}/items/", response_model=Author) # (4) +async def create_author_items(author_id: str, items: list[Item]): # (5) + return {"name": author_id, "items": items} # (6) + + +@app.get("/authors/", response_model=list[Author]) # (7) +def get_authors(): # (8) + return [ # (9) + { + "name": "Breaters", + "items": [ + { + "name": "Island In The Moon", + "description": "A place to be playin' and havin' fun", + }, + {"name": "Holy Buddies"}, + ], + }, + { + "name": "System of an Up", + "items": [ + { + "name": "Salt", + "description": "The kombucha mushroom people's favorite", + }, + {"name": "Pad Thai"}, + { + "name": "Lonely Night", + "description": "The mostests lonliest nightiest of allest", + }, + ], + }, + ] diff --git a/docs_src/openapi_callbacks/tutorial001_py310.py b/docs_src/openapi_callbacks/tutorial001_py310.py new file mode 100644 index 000000000..3efe0ee25 --- /dev/null +++ b/docs_src/openapi_callbacks/tutorial001_py310.py @@ -0,0 +1,51 @@ +from fastapi import APIRouter, FastAPI +from pydantic import BaseModel, HttpUrl + +app = FastAPI() + + +class Invoice(BaseModel): + id: str + title: str | None = None + customer: str + total: float + + +class InvoiceEvent(BaseModel): + description: str + paid: bool + + +class InvoiceEventReceived(BaseModel): + ok: bool + + +invoices_callback_router = APIRouter() + + +@invoices_callback_router.post( + "{$callback_url}/invoices/{$request.body.id}", response_model=InvoiceEventReceived +) +def invoice_notification(body: InvoiceEvent): + pass + + +@app.post("/invoices/", callbacks=invoices_callback_router.routes) +def create_invoice(invoice: Invoice, callback_url: HttpUrl | None = None): + """ + Create an invoice. + + This will (let's imagine) let the API user (some external developer) create an + invoice. + + And this path operation will: + + * Send the invoice to the client. + * Collect the money from the client. + * Send a notification back to the API user (the external developer), as a callback. + * At this point is that the API will somehow send a POST request to the + external API with the notification of the invoice event + (e.g. "payment successful"). + """ + # Send the invoice, collect the money, send the notification (the callback) + return {"msg": "Invoice received"} diff --git a/docs_src/path_operation_advanced_configuration/tutorial004_py310.py b/docs_src/path_operation_advanced_configuration/tutorial004_py310.py new file mode 100644 index 000000000..a815a564b --- /dev/null +++ b/docs_src/path_operation_advanced_configuration/tutorial004_py310.py @@ -0,0 +1,28 @@ +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + tags: set[str] = set() + + +@app.post("/items/", response_model=Item, summary="Create an item") +async def create_item(item: Item): + """ + Create an item with all the information: + + - **name**: each item must have a name + - **description**: a long description + - **price**: required + - **tax**: if the item doesn't have tax, you can omit this + - **tags**: a set of unique tag strings for this item + \f + :param item: User input. + """ + return item diff --git a/docs_src/path_operation_advanced_configuration/tutorial004_py39.py b/docs_src/path_operation_advanced_configuration/tutorial004_py39.py new file mode 100644 index 000000000..d5fe6705c --- /dev/null +++ b/docs_src/path_operation_advanced_configuration/tutorial004_py39.py @@ -0,0 +1,30 @@ +from typing import Union + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + tags: set[str] = set() + + +@app.post("/items/", response_model=Item, summary="Create an item") +async def create_item(item: Item): + """ + Create an item with all the information: + + - **name**: each item must have a name + - **description**: a long description + - **price**: required + - **tax**: if the item doesn't have tax, you can omit this + - **tags**: a set of unique tag strings for this item + \f + :param item: User input. + """ + return item diff --git a/docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py b/docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py new file mode 100644 index 000000000..831966553 --- /dev/null +++ b/docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py @@ -0,0 +1,32 @@ +import yaml +from fastapi import FastAPI, HTTPException, Request +from pydantic import BaseModel, ValidationError + +app = FastAPI() + + +class Item(BaseModel): + name: str + tags: list[str] + + +@app.post( + "/items/", + openapi_extra={ + "requestBody": { + "content": {"application/x-yaml": {"schema": Item.schema()}}, + "required": True, + }, + }, +) +async def create_item(request: Request): + raw_body = await request.body() + try: + data = yaml.safe_load(raw_body) + except yaml.YAMLError: + raise HTTPException(status_code=422, detail="Invalid YAML") + try: + item = Item.parse_obj(data) + except ValidationError as e: + raise HTTPException(status_code=422, detail=e.errors()) + return item diff --git a/docs_src/path_operation_advanced_configuration/tutorial007_py39.py b/docs_src/path_operation_advanced_configuration/tutorial007_py39.py new file mode 100644 index 000000000..ff64ef792 --- /dev/null +++ b/docs_src/path_operation_advanced_configuration/tutorial007_py39.py @@ -0,0 +1,32 @@ +import yaml +from fastapi import FastAPI, HTTPException, Request +from pydantic import BaseModel, ValidationError + +app = FastAPI() + + +class Item(BaseModel): + name: str + tags: list[str] + + +@app.post( + "/items/", + openapi_extra={ + "requestBody": { + "content": {"application/x-yaml": {"schema": Item.model_json_schema()}}, + "required": True, + }, + }, +) +async def create_item(request: Request): + raw_body = await request.body() + try: + data = yaml.safe_load(raw_body) + except yaml.YAMLError: + raise HTTPException(status_code=422, detail="Invalid YAML") + try: + item = Item.model_validate(data) + except ValidationError as e: + raise HTTPException(status_code=422, detail=e.errors(include_url=False)) + return item diff --git a/docs_src/response_directly/tutorial001_py310.py b/docs_src/response_directly/tutorial001_py310.py new file mode 100644 index 000000000..81e094dc6 --- /dev/null +++ b/docs_src/response_directly/tutorial001_py310.py @@ -0,0 +1,21 @@ +from datetime import datetime + +from fastapi import FastAPI +from fastapi.encoders import jsonable_encoder +from fastapi.responses import JSONResponse +from pydantic import BaseModel + + +class Item(BaseModel): + title: str + timestamp: datetime + description: str | None = None + + +app = FastAPI() + + +@app.put("/items/{id}") +def update_item(id: str, item: Item): + json_compatible_item_data = jsonable_encoder(item) + return JSONResponse(content=json_compatible_item_data) diff --git a/docs_src/settings/app03/config.py b/docs_src/settings/app03/config.py index 942aea3e5..08f8f88c2 100644 --- a/docs_src/settings/app03/config.py +++ b/docs_src/settings/app03/config.py @@ -1,4 +1,4 @@ -from pydantic_settings import BaseSettings +from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): @@ -6,5 +6,4 @@ class Settings(BaseSettings): admin_email: str items_per_user: int = 50 - class Config: - env_file = ".env" + model_config = SettingsConfigDict(env_file=".env") diff --git a/docs_src/settings/app03/config_pv1.py b/docs_src/settings/app03/config_pv1.py new file mode 100644 index 000000000..e1c3ee300 --- /dev/null +++ b/docs_src/settings/app03/config_pv1.py @@ -0,0 +1,10 @@ +from pydantic import BaseSettings + + +class Settings(BaseSettings): + app_name: str = "Awesome API" + admin_email: str + items_per_user: int = 50 + + class Config: + env_file = ".env" diff --git a/docs_src/settings/app03_an/main.py b/docs_src/settings/app03_an/main.py index 2f64b9cd1..62f347639 100644 --- a/docs_src/settings/app03_an/main.py +++ b/docs_src/settings/app03_an/main.py @@ -1,7 +1,7 @@ from functools import lru_cache -from typing import Annotated from fastapi import Depends, FastAPI +from typing_extensions import Annotated from . import config diff --git a/docs_src/settings/app03_an_py39/config.py b/docs_src/settings/app03_an_py39/config.py index 942aea3e5..08f8f88c2 100644 --- a/docs_src/settings/app03_an_py39/config.py +++ b/docs_src/settings/app03_an_py39/config.py @@ -1,4 +1,4 @@ -from pydantic_settings import BaseSettings +from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): @@ -6,5 +6,4 @@ class Settings(BaseSettings): admin_email: str items_per_user: int = 50 - class Config: - env_file = ".env" + model_config = SettingsConfigDict(env_file=".env") diff --git a/docs_src/settings/app03_an_py39/config_pv1.py b/docs_src/settings/app03_an_py39/config_pv1.py new file mode 100644 index 000000000..e1c3ee300 --- /dev/null +++ b/docs_src/settings/app03_an_py39/config_pv1.py @@ -0,0 +1,10 @@ +from pydantic import BaseSettings + + +class Settings(BaseSettings): + app_name: str = "Awesome API" + admin_email: str + items_per_user: int = 50 + + class Config: + env_file = ".env" diff --git a/docs_src/settings/app03_an_py39/main.py b/docs_src/settings/app03_an_py39/main.py index 62f347639..2f64b9cd1 100644 --- a/docs_src/settings/app03_an_py39/main.py +++ b/docs_src/settings/app03_an_py39/main.py @@ -1,7 +1,7 @@ from functools import lru_cache +from typing import Annotated from fastapi import Depends, FastAPI -from typing_extensions import Annotated from . import config diff --git a/pyproject.toml b/pyproject.toml index cafcf65c6..f8d5fa7c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -236,8 +236,15 @@ ignore = [ "docs_src/custom_response/tutorial007.py" = ["B007"] "docs_src/dataclasses/tutorial003.py" = ["I001"] "docs_src/path_operation_advanced_configuration/tutorial007.py" = ["B904"] +"docs_src/path_operation_advanced_configuration/tutorial007_py39.py" = ["B904"] "docs_src/path_operation_advanced_configuration/tutorial007_pv1.py" = ["B904"] +"docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py" = ["B904"] "docs_src/custom_request_and_route/tutorial002.py" = ["B904"] +"docs_src/custom_request_and_route/tutorial002_py39.py" = ["B904"] +"docs_src/custom_request_and_route/tutorial002_py310.py" = ["B904"] +"docs_src/custom_request_and_route/tutorial002_an.py" = ["B904"] +"docs_src/custom_request_and_route/tutorial002_an_py39.py" = ["B904"] +"docs_src/custom_request_and_route/tutorial002_an_py310.py" = ["B904"] "docs_src/dependencies/tutorial008_an.py" = ["F821"] "docs_src/dependencies/tutorial008_an_py39.py" = ["F821"] "docs_src/query_params_str_validations/tutorial012_an.py" = ["B006"] diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial002.py b/tests/test_tutorial/test_additional_responses/test_tutorial002.py index 588a3160a..91d6ff101 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial002.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial002.py @@ -1,21 +1,36 @@ +import importlib import os import shutil +import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient -from docs_src.additional_responses.tutorial002 import app - -client = TestClient(app) +from tests.utils import needs_py310 -def test_path_operation(): +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002"), + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.additional_responses.{request.param}") + + client = TestClient(mod.app) + client.headers.clear() + return client + + +def test_path_operation(client: TestClient): response = client.get("/items/foo") assert response.status_code == 200, response.text assert response.json() == {"id": "foo", "value": "there goes my hero"} -def test_path_operation_img(): +def test_path_operation_img(client: TestClient): shutil.copy("./docs/en/docs/img/favicon.png", "./image.png") response = client.get("/items/foo?img=1") assert response.status_code == 200, response.text @@ -24,7 +39,7 @@ def test_path_operation_img(): os.remove("./image.png") -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial004.py b/tests/test_tutorial/test_additional_responses/test_tutorial004.py index 55b556d8e..2d9491467 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial004.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial004.py @@ -1,21 +1,36 @@ +import importlib import os import shutil +import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient -from docs_src.additional_responses.tutorial004 import app - -client = TestClient(app) +from tests.utils import needs_py310 -def test_path_operation(): +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial004"), + pytest.param("tutorial004_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.additional_responses.{request.param}") + + client = TestClient(mod.app) + client.headers.clear() + return client + + +def test_path_operation(client: TestClient): response = client.get("/items/foo") assert response.status_code == 200, response.text assert response.json() == {"id": "foo", "value": "there goes my hero"} -def test_path_operation_img(): +def test_path_operation_img(client: TestClient): shutil.copy("./docs/en/docs/img/favicon.png", "./image.png") response = client.get("/items/foo?img=1") assert response.status_code == 200, response.text @@ -24,7 +39,7 @@ def test_path_operation_img(): os.remove("./image.png") -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_custom_request_and_route/test_tutorial001.py b/tests/test_tutorial/test_custom_request_and_route/test_tutorial001.py index e6da630e8..f9fd0d1af 100644 --- a/tests/test_tutorial/test_custom_request_and_route/test_tutorial001.py +++ b/tests/test_tutorial/test_custom_request_and_route/test_tutorial001.py @@ -1,23 +1,38 @@ import gzip +import importlib import json import pytest from fastapi import Request from fastapi.testclient import TestClient -from docs_src.custom_request_and_route.tutorial001 import app +from tests.utils import needs_py39, needs_py310 -@app.get("/check-class") -async def check_gzip_request(request: Request): - return {"request_class": type(request).__name__} +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial001"), + pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py310", marks=needs_py310), + pytest.param("tutorial001_an"), + pytest.param("tutorial001_an_py39", marks=needs_py39), + pytest.param("tutorial001_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.custom_request_and_route.{request.param}") + @mod.app.get("/check-class") + async def check_gzip_request(request: Request): + return {"request_class": type(request).__name__} -client = TestClient(app) + client = TestClient(mod.app) + return client @pytest.mark.parametrize("compress", [True, False]) -def test_gzip_request(compress): +def test_gzip_request(client: TestClient, compress): n = 1000 headers = {} body = [1] * n @@ -30,6 +45,6 @@ def test_gzip_request(compress): assert response.json() == {"sum": n} -def test_request_class(): +def test_request_class(client: TestClient): response = client.get("/check-class") assert response.json() == {"request_class": "GzipRequest"} diff --git a/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py b/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py index 647f1c5dd..c35752ed1 100644 --- a/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py +++ b/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py @@ -1,17 +1,36 @@ +import importlib + +import pytest from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient -from docs_src.custom_request_and_route.tutorial002 import app - -client = TestClient(app) +from tests.utils import needs_py39, needs_py310 -def test_endpoint_works(): +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002"), + pytest.param("tutorial002_py39", marks=needs_py39), + pytest.param("tutorial002_py310", marks=needs_py310), + pytest.param("tutorial002_an"), + pytest.param("tutorial002_an_py39", marks=needs_py39), + pytest.param("tutorial002_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.custom_request_and_route.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_endpoint_works(client: TestClient): response = client.post("/", json=[1, 2, 3]) assert response.json() == 6 -def test_exception_handler_body_access(): +def test_exception_handler_body_access(client: TestClient): response = client.post("/", json={"numbers": [1, 2, 3]}) assert response.json() == IsDict( { diff --git a/tests/test_tutorial/test_custom_request_and_route/test_tutorial003.py b/tests/test_tutorial/test_custom_request_and_route/test_tutorial003.py index db5dad7cf..9e895b2da 100644 --- a/tests/test_tutorial/test_custom_request_and_route/test_tutorial003.py +++ b/tests/test_tutorial/test_custom_request_and_route/test_tutorial003.py @@ -1,17 +1,32 @@ +import importlib + +import pytest from fastapi.testclient import TestClient -from docs_src.custom_request_and_route.tutorial003 import app - -client = TestClient(app) +from tests.utils import needs_py310 -def test_get(): +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial003"), + pytest.param("tutorial003_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.custom_request_and_route.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_get(client: TestClient): response = client.get("/") assert response.json() == {"message": "Not timed"} assert "X-Response-Time" not in response.headers -def test_get_timed(): +def test_get_timed(client: TestClient): response = client.get("/timed") assert response.json() == {"message": "It's the time of my life"} assert "X-Response-Time" in response.headers diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial001.py b/tests/test_tutorial/test_dataclasses/test_tutorial001.py index 762654d29..b36dee768 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial001.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial001.py @@ -1,12 +1,28 @@ +import importlib + +import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient -from docs_src.dataclasses.tutorial001 import app - -client = TestClient(app) +from tests.utils import needs_py310 -def test_post_item(): +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial001"), + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.dataclasses.{request.param}") + + client = TestClient(mod.app) + client.headers.clear() + return client + + +def test_post_item(client: TestClient): response = client.post("/items/", json={"name": "Foo", "price": 3}) assert response.status_code == 200 assert response.json() == { @@ -17,7 +33,7 @@ def test_post_item(): } -def test_post_invalid_item(): +def test_post_invalid_item(client: TestClient): response = client.post("/items/", json={"name": "Foo", "price": "invalid price"}) assert response.status_code == 422 assert response.json() == IsDict( @@ -45,7 +61,7 @@ def test_post_invalid_item(): ) -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial002.py b/tests/test_tutorial/test_dataclasses/test_tutorial002.py index e6d303cfc..baaea45d8 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial002.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial002.py @@ -1,12 +1,29 @@ +import importlib + +import pytest from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient -from docs_src.dataclasses.tutorial002 import app - -client = TestClient(app) +from tests.utils import needs_py39, needs_py310 -def test_get_item(): +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002"), + pytest.param("tutorial002_py39", marks=needs_py39), + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.dataclasses.{request.param}") + + client = TestClient(mod.app) + client.headers.clear() + return client + + +def test_get_item(client: TestClient): response = client.get("/items/next") assert response.status_code == 200 assert response.json() == { @@ -18,7 +35,7 @@ def test_get_item(): } -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial003.py b/tests/test_tutorial/test_dataclasses/test_tutorial003.py index e1fa45201..5728d2b6b 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial003.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial003.py @@ -1,13 +1,28 @@ +import importlib + +import pytest from fastapi.testclient import TestClient -from docs_src.dataclasses.tutorial003 import app - -from ...utils import needs_pydanticv1, needs_pydanticv2 - -client = TestClient(app) +from ...utils import needs_py39, needs_py310, needs_pydanticv1, needs_pydanticv2 -def test_post_authors_item(): +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial003"), + pytest.param("tutorial003_py39", marks=needs_py39), + pytest.param("tutorial003_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.dataclasses.{request.param}") + + client = TestClient(mod.app) + client.headers.clear() + return client + + +def test_post_authors_item(client: TestClient): response = client.post( "/authors/foo/items/", json=[{"name": "Bar"}, {"name": "Baz", "description": "Drop the Baz"}], @@ -22,7 +37,7 @@ def test_post_authors_item(): } -def test_get_authors(): +def test_get_authors(client: TestClient): response = client.get("/authors/") assert response.status_code == 200 assert response.json() == [ @@ -54,7 +69,7 @@ def test_get_authors(): @needs_pydanticv2 -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { @@ -191,7 +206,7 @@ def test_openapi_schema(): # TODO: remove when deprecating Pydantic v1 @needs_pydanticv1 -def test_openapi_schema_pv1(): +def test_openapi_schema_pv1(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { diff --git a/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py b/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py index 73af420ae..2df2b9889 100644 --- a/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py +++ b/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py @@ -1,12 +1,33 @@ +import importlib +from types import ModuleType + +import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient -from docs_src.openapi_callbacks.tutorial001 import app, invoice_notification - -client = TestClient(app) +from tests.utils import needs_py310 -def test_get(): +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial001"), + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_mod(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.openapi_callbacks.{request.param}") + return mod + + +@pytest.fixture(name="client") +def get_client(mod: ModuleType): + client = TestClient(mod.app) + client.headers.clear() + return client + + +def test_get(client: TestClient): response = client.post( "/invoices/", json={"id": "fooinvoice", "customer": "John", "total": 5.3} ) @@ -14,12 +35,12 @@ def test_get(): assert response.json() == {"msg": "Invoice received"} -def test_dummy_callback(): +def test_dummy_callback(mod: ModuleType): # Just for coverage - invoice_notification({}) + mod.invoice_notification({}) -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py index 4f69e4646..da5782d18 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py @@ -1,13 +1,30 @@ +import importlib + +import pytest from fastapi.testclient import TestClient -from docs_src.path_operation_advanced_configuration.tutorial004 import app - -from ...utils import needs_pydanticv1, needs_pydanticv2 - -client = TestClient(app) +from ...utils import needs_py39, needs_py310, needs_pydanticv1, needs_pydanticv2 -def test_query_params_str_validations(): +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial004"), + pytest.param("tutorial004_py39", marks=needs_py39), + pytest.param("tutorial004_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module( + f"docs_src.path_operation_advanced_configuration.{request.param}" + ) + + client = TestClient(mod.app) + client.headers.clear() + return client + + +def test_query_params_str_validations(client: TestClient): response = client.post("/items/", json={"name": "Foo", "price": 42}) assert response.status_code == 200, response.text assert response.json() == { @@ -20,7 +37,7 @@ def test_query_params_str_validations(): @needs_pydanticv2 -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -123,7 +140,7 @@ def test_openapi_schema(): # TODO: remove when deprecating Pydantic v1 @needs_pydanticv1 -def test_openapi_schema_pv1(): +def test_openapi_schema_pv1(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py index 8240b60a6..a90337a63 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py @@ -1,14 +1,24 @@ +import importlib + import pytest from fastapi.testclient import TestClient -from ...utils import needs_pydanticv2 +from ...utils import needs_py39, needs_pydanticv2 -@pytest.fixture(name="client") -def get_client(): - from docs_src.path_operation_advanced_configuration.tutorial007 import app +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial007"), + pytest.param("tutorial007_py39", marks=needs_py39), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module( + f"docs_src.path_operation_advanced_configuration.{request.param}" + ) - client = TestClient(app) + client = TestClient(mod.app) return client diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007_pv1.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007_pv1.py index ef012f8a6..b38e4947c 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007_pv1.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007_pv1.py @@ -1,14 +1,24 @@ +import importlib + import pytest from fastapi.testclient import TestClient -from ...utils import needs_pydanticv1 +from ...utils import needs_py39, needs_pydanticv1 -@pytest.fixture(name="client") -def get_client(): - from docs_src.path_operation_advanced_configuration.tutorial007_pv1 import app +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial007_pv1"), + pytest.param("tutorial007_pv1_py39", marks=needs_py39), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module( + f"docs_src.path_operation_advanced_configuration.{request.param}" + ) - client = TestClient(app) + client = TestClient(mod.app) return client diff --git a/tests/test_tutorial/test_response_directly/__init__.py b/tests/test_tutorial/test_response_directly/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tutorial/test_response_directly/test_tutorial001.py b/tests/test_tutorial/test_response_directly/test_tutorial001.py new file mode 100644 index 000000000..2cc4f3b0c --- /dev/null +++ b/tests/test_tutorial/test_response_directly/test_tutorial001.py @@ -0,0 +1,288 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310, needs_pydanticv1, needs_pydanticv2 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial001"), + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.response_directly.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_path_operation(client: TestClient): + response = client.put( + "/items/1", + json={ + "title": "Foo", + "timestamp": "2023-01-01T12:00:00", + "description": "A test item", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "description": "A test item", + "timestamp": "2023-01-01T12:00:00", + "title": "Foo", + } + + +@needs_pydanticv2 +def test_openapi_schema_pv2(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "info": { + "title": "FastAPI", + "version": "0.1.0", + }, + "openapi": "3.1.0", + "paths": { + "/items/{id}": { + "put": { + "operationId": "update_item_items__id__put", + "parameters": [ + { + "in": "path", + "name": "id", + "required": True, + "schema": {"title": "Id", "type": "string"}, + }, + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item", + }, + }, + }, + "required": True, + }, + "responses": { + "200": { + "content": { + "application/json": {"schema": {}}, + }, + "description": "Successful Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + "summary": "Update Item", + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "Item": { + "properties": { + "description": { + "anyOf": [ + {"type": "string"}, + {"type": "null"}, + ], + "title": "Description", + }, + "timestamp": { + "format": "date-time", + "title": "Timestamp", + "type": "string", + }, + "title": {"title": "Title", "type": "string"}, + }, + "required": [ + "title", + "timestamp", + ], + "title": "Item", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + {"type": "string"}, + {"type": "integer"}, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + "required": ["loc", "msg", "type"], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } + + +@needs_pydanticv1 +def test_openapi_schema_pv1(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "info": { + "title": "FastAPI", + "version": "0.1.0", + }, + "openapi": "3.1.0", + "paths": { + "/items/{id}": { + "put": { + "operationId": "update_item_items__id__put", + "parameters": [ + { + "in": "path", + "name": "id", + "required": True, + "schema": { + "title": "Id", + "type": "string", + }, + }, + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item", + }, + }, + }, + "required": True, + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, + }, + }, + "description": "Successful Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + "summary": "Update Item", + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "Item": { + "properties": { + "description": { + "title": "Description", + "type": "string", + }, + "timestamp": { + "format": "date-time", + "title": "Timestamp", + "type": "string", + }, + "title": { + "title": "Title", + "type": "string", + }, + }, + "required": [ + "title", + "timestamp", + ], + "title": "Item", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_settings/test_app02.py b/tests/test_tutorial/test_settings/test_app02.py index eced88c04..5e1232ea0 100644 --- a/tests/test_tutorial/test_settings/test_app02.py +++ b/tests/test_tutorial/test_settings/test_app02.py @@ -1,20 +1,45 @@ +import importlib +from types import ModuleType + +import pytest from pytest import MonkeyPatch -from ...utils import needs_pydanticv2 +from ...utils import needs_py39, needs_pydanticv2 + + +@pytest.fixture( + name="mod_path", + params=[ + pytest.param("app02"), + pytest.param("app02_an"), + pytest.param("app02_an_py39", marks=needs_py39), + ], +) +def get_mod_path(request: pytest.FixtureRequest): + mod_path = f"docs_src.settings.{request.param}" + return mod_path + + +@pytest.fixture(name="main_mod") +def get_main_mod(mod_path: str) -> ModuleType: + main_mod = importlib.import_module(f"{mod_path}.main") + return main_mod + + +@pytest.fixture(name="test_main_mod") +def get_test_main_mod(mod_path: str) -> ModuleType: + test_main_mod = importlib.import_module(f"{mod_path}.test_main") + return test_main_mod @needs_pydanticv2 -def test_settings(monkeypatch: MonkeyPatch): - from docs_src.settings.app02 import main - +def test_settings(main_mod: ModuleType, monkeypatch: MonkeyPatch): monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com") - settings = main.get_settings() + settings = main_mod.get_settings() assert settings.app_name == "Awesome API" assert settings.items_per_user == 50 @needs_pydanticv2 -def test_override_settings(): - from docs_src.settings.app02 import test_main - - test_main.test_app() +def test_override_settings(test_main_mod: ModuleType): + test_main_mod.test_app() diff --git a/tests/test_tutorial/test_settings/test_app03.py b/tests/test_tutorial/test_settings/test_app03.py new file mode 100644 index 000000000..d9872c15f --- /dev/null +++ b/tests/test_tutorial/test_settings/test_app03.py @@ -0,0 +1,59 @@ +import importlib +from types import ModuleType + +import pytest +from fastapi.testclient import TestClient +from pytest import MonkeyPatch + +from ...utils import needs_py39, needs_pydanticv1, needs_pydanticv2 + + +@pytest.fixture( + name="mod_path", + params=[ + pytest.param("app03"), + pytest.param("app03_an"), + pytest.param("app03_an_py39", marks=needs_py39), + ], +) +def get_mod_path(request: pytest.FixtureRequest): + mod_path = f"docs_src.settings.{request.param}" + return mod_path + + +@pytest.fixture(name="main_mod") +def get_main_mod(mod_path: str) -> ModuleType: + main_mod = importlib.import_module(f"{mod_path}.main") + return main_mod + + +@needs_pydanticv2 +def test_settings(main_mod: ModuleType, monkeypatch: MonkeyPatch): + monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com") + settings = main_mod.get_settings() + assert settings.app_name == "Awesome API" + assert settings.admin_email == "admin@example.com" + assert settings.items_per_user == 50 + + +@needs_pydanticv1 +def test_settings_pv1(mod_path: str, monkeypatch: MonkeyPatch): + monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com") + config_mod = importlib.import_module(f"{mod_path}.config_pv1") + settings = config_mod.Settings() + assert settings.app_name == "Awesome API" + assert settings.admin_email == "admin@example.com" + assert settings.items_per_user == 50 + + +@needs_pydanticv2 +def test_endpoint(main_mod: ModuleType, monkeypatch: MonkeyPatch): + monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com") + client = TestClient(main_mod.app) + response = client.get("/info") + assert response.status_code == 200 + assert response.json() == { + "app_name": "Awesome API", + "admin_email": "admin@example.com", + "items_per_user": 50, + } diff --git a/tests/test_tutorial/test_using_request_directly/__init__.py b/tests/test_tutorial/test_using_request_directly/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tutorial/test_using_request_directly/test_tutorial001.py b/tests/test_tutorial/test_using_request_directly/test_tutorial001.py new file mode 100644 index 000000000..54c53ae1e --- /dev/null +++ b/tests/test_tutorial/test_using_request_directly/test_tutorial001.py @@ -0,0 +1,112 @@ +from fastapi.testclient import TestClient + +from docs_src.using_request_directly.tutorial001 import app + +client = TestClient(app) + + +def test_path_operation(): + response = client.get("/items/foo") + assert response.status_code == 200 + assert response.json() == {"client_host": "testclient", "item_id": "foo"} + + +def test_openapi(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "info": { + "title": "FastAPI", + "version": "0.1.0", + }, + "openapi": "3.1.0", + "paths": { + "/items/{item_id}": { + "get": { + "operationId": "read_root_items__item_id__get", + "parameters": [ + { + "in": "path", + "name": "item_id", + "required": True, + "schema": { + "title": "Item Id", + "type": "string", + }, + }, + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, + }, + }, + "description": "Successful Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + "summary": "Read Root", + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_websockets/test_tutorial003.py b/tests/test_tutorial/test_websockets/test_tutorial003.py index dbcad3b02..85efc1859 100644 --- a/tests/test_tutorial/test_websockets/test_tutorial003.py +++ b/tests/test_tutorial/test_websockets/test_tutorial003.py @@ -1,16 +1,45 @@ +import importlib +from types import ModuleType + +import pytest from fastapi.testclient import TestClient -from docs_src.websockets.tutorial003 import app, html - -client = TestClient(app) +from ...utils import needs_py39 -def test_get(): +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial003"), + pytest.param("tutorial003_py39", marks=needs_py39), + ], +) +def get_mod(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.websockets.{request.param}") + + return mod + + +@pytest.fixture(name="html") +def get_html(mod: ModuleType): + return mod.html + + +@pytest.fixture(name="client") +def get_client(mod: ModuleType): + client = TestClient(mod.app) + + return client + + +@needs_py39 +def test_get(client: TestClient, html: str): response = client.get("/") assert response.text == html -def test_websocket_handle_disconnection(): +@needs_py39 +def test_websocket_handle_disconnection(client: TestClient): with client.websocket_connect("/ws/1234") as connection, client.websocket_connect( "/ws/5678" ) as connection_two: diff --git a/tests/test_tutorial/test_websockets/test_tutorial003_py39.py b/tests/test_tutorial/test_websockets/test_tutorial003_py39.py deleted file mode 100644 index 06c4a9279..000000000 --- a/tests/test_tutorial/test_websockets/test_tutorial003_py39.py +++ /dev/null @@ -1,50 +0,0 @@ -import pytest -from fastapi import FastAPI -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="app") -def get_app(): - from docs_src.websockets.tutorial003_py39 import app - - return app - - -@pytest.fixture(name="html") -def get_html(): - from docs_src.websockets.tutorial003_py39 import html - - return html - - -@pytest.fixture(name="client") -def get_client(app: FastAPI): - client = TestClient(app) - - return client - - -@needs_py39 -def test_get(client: TestClient, html: str): - response = client.get("/") - assert response.text == html - - -@needs_py39 -def test_websocket_handle_disconnection(client: TestClient): - with client.websocket_connect("/ws/1234") as connection, client.websocket_connect( - "/ws/5678" - ) as connection_two: - connection.send_text("Hello from 1234") - data1 = connection.receive_text() - assert data1 == "You wrote: Hello from 1234" - data2 = connection_two.receive_text() - client1_says = "Client #1234 says: Hello from 1234" - assert data2 == client1_says - data1 = connection.receive_text() - assert data1 == client1_says - connection_two.close() - data1 = connection.receive_text() - assert data1 == "Client #5678 left the chat" From 71a17b5932e38bcf798fcd05c5bb384e6d4db5bf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 10 Dec 2025 08:55:57 +0000 Subject: [PATCH 234/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 60bc1cc6f..2b8c72767 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* 📝 Add variants for code examples in "Advanced User Guide". PR [#14413](https://github.com/fastapi/fastapi/pull/14413) by [@YuriiMotov](https://github.com/YuriiMotov). * 📝 Update tech stack in project generation docs. PR [#14472](https://github.com/fastapi/fastapi/pull/14472) by [@alejsdev](https://github.com/alejsdev). ### Internal From 42b250d14dd42d3c0c24dd085fa53878172a985f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 10 Dec 2025 02:36:29 -0800 Subject: [PATCH 235/256] =?UTF-8?q?=F0=9F=90=9B=20Fix=20handling=20arbitra?= =?UTF-8?q?ry=20types=20when=20using=20`arbitrary=5Ftypes=5Fallowed=3DTrue?= =?UTF-8?q?`=20(#14482)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/_compat/v2.py | 32 ++++++-- tests/test_arbitrary_types.py | 141 ++++++++++++++++++++++++++++++++++ 2 files changed, 166 insertions(+), 7 deletions(-) create mode 100644 tests/test_arbitrary_types.py diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py index acd23d846..46a30b3ee 100644 --- a/fastapi/_compat/v2.py +++ b/fastapi/_compat/v2.py @@ -1,7 +1,7 @@ import re import warnings from copy import copy, deepcopy -from dataclasses import dataclass +from dataclasses import dataclass, is_dataclass from enum import Enum from typing import ( Any, @@ -18,7 +18,7 @@ from typing import ( from fastapi._compat import may_v1, shared from fastapi.openapi.constants import REF_TEMPLATE from fastapi.types import IncEx, ModelNameMap, UnionType -from pydantic import BaseModel, TypeAdapter, create_model +from pydantic import BaseModel, ConfigDict, TypeAdapter, create_model from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError from pydantic import PydanticUndefinedAnnotation as PydanticUndefinedAnnotation from pydantic import ValidationError as ValidationError @@ -64,6 +64,7 @@ class ModelField: field_info: FieldInfo name: str mode: Literal["validation", "serialization"] = "validation" + config: Union[ConfigDict, None] = None @property def alias(self) -> str: @@ -94,8 +95,14 @@ class ModelField: warnings.simplefilter( "ignore", category=UnsupportedFieldAttributeWarning ) + annotated_args = ( + self.field_info.annotation, + *self.field_info.metadata, + self.field_info, + ) self._type_adapter: TypeAdapter[Any] = TypeAdapter( - Annotated[self.field_info.annotation, self.field_info] + Annotated[annotated_args], + config=self.config, ) def get_default(self) -> Any: @@ -412,10 +419,21 @@ def create_body_model( def get_model_fields(model: Type[BaseModel]) -> List[ModelField]: - return [ - ModelField(field_info=field_info, name=name) - for name, field_info in model.model_fields.items() - ] + model_fields: List[ModelField] = [] + for name, field_info in model.model_fields.items(): + type_ = field_info.annotation + if lenient_issubclass(type_, (BaseModel, dict)) or is_dataclass(type_): + model_config = None + else: + model_config = model.model_config + model_fields.append( + ModelField( + field_info=field_info, + name=name, + config=model_config, + ) + ) + return model_fields # Duplicate of several schema functions from Pydantic v1 to make them compatible with diff --git a/tests/test_arbitrary_types.py b/tests/test_arbitrary_types.py new file mode 100644 index 000000000..e5fa95ef2 --- /dev/null +++ b/tests/test_arbitrary_types.py @@ -0,0 +1,141 @@ +from typing import List + +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient +from inline_snapshot import snapshot +from typing_extensions import Annotated + +from .utils import needs_pydanticv2 + + +@pytest.fixture(name="client") +def get_client(): + from pydantic import ( + BaseModel, + ConfigDict, + PlainSerializer, + TypeAdapter, + WithJsonSchema, + ) + + class FakeNumpyArray: + def __init__(self): + self.data = [1.0, 2.0, 3.0] + + FakeNumpyArrayPydantic = Annotated[ + FakeNumpyArray, + WithJsonSchema(TypeAdapter(List[float]).json_schema()), + PlainSerializer(lambda v: v.data), + ] + + class MyModel(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True) + custom_field: FakeNumpyArrayPydantic + + app = FastAPI() + + @app.get("/") + def test() -> MyModel: + return MyModel(custom_field=FakeNumpyArray()) + + client = TestClient(app) + return client + + +@needs_pydanticv2 +def test_get(client: TestClient): + response = client.get("/") + assert response.json() == {"custom_field": [1.0, 2.0, 3.0]} + + +@needs_pydanticv2 +def test_typeadapter(): + # This test is only to confirm that Pydantic alone is working as expected + from pydantic import ( + BaseModel, + ConfigDict, + PlainSerializer, + TypeAdapter, + WithJsonSchema, + ) + + class FakeNumpyArray: + def __init__(self): + self.data = [1.0, 2.0, 3.0] + + FakeNumpyArrayPydantic = Annotated[ + FakeNumpyArray, + WithJsonSchema(TypeAdapter(List[float]).json_schema()), + PlainSerializer(lambda v: v.data), + ] + + class MyModel(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True) + custom_field: FakeNumpyArrayPydantic + + ta = TypeAdapter(MyModel) + assert ta.dump_python(MyModel(custom_field=FakeNumpyArray())) == { + "custom_field": [1.0, 2.0, 3.0] + } + assert ta.json_schema() == snapshot( + { + "properties": { + "custom_field": { + "items": {"type": "number"}, + "title": "Custom Field", + "type": "array", + } + }, + "required": ["custom_field"], + "title": "MyModel", + "type": "object", + } + ) + + +@needs_pydanticv2 +def test_openapi_schema(client: TestClient): + response = client.get("openapi.json") + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "summary": "Test", + "operationId": "test__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MyModel" + } + } + }, + } + }, + } + } + }, + "components": { + "schemas": { + "MyModel": { + "properties": { + "custom_field": { + "items": {"type": "number"}, + "type": "array", + "title": "Custom Field", + } + }, + "type": "object", + "required": ["custom_field"], + "title": "MyModel", + } + } + }, + } + ) From ae7af59c6d5921a3c700a9595788a444975a9d8c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 10 Dec 2025 10:36:56 +0000 Subject: [PATCH 236/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 2b8c72767..e7dbadff7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Fixes + +* 🐛 Fix handling arbitrary types when using `arbitrary_types_allowed=True`. PR [#14482](https://github.com/fastapi/fastapi/pull/14482) by [@tiangolo](https://github.com/tiangolo). + ### Docs * 📝 Add variants for code examples in "Advanced User Guide". PR [#14413](https://github.com/fastapi/fastapi/pull/14413) by [@YuriiMotov](https://github.com/YuriiMotov). From 60699f306b67dc1f4918ebea17f04d7a48cda645 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 10 Dec 2025 11:38:41 +0100 Subject: [PATCH 237/256] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.12?= =?UTF-8?q?4.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index e7dbadff7..08db56c8e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.124.1 + ### Fixes * 🐛 Fix handling arbitrary types when using `arbitrary_types_allowed=True`. PR [#14482](https://github.com/fastapi/fastapi/pull/14482) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 7009a7777..2e7dba8a1 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.124.0" +__version__ = "0.124.1" from starlette import status as status From 7ba042e069ad424a584a37f1db03887798d9af80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 10 Dec 2025 04:06:05 -0800 Subject: [PATCH 238/256] =?UTF-8?q?=F0=9F=90=9B=20Fix=20support=20for=20`i?= =?UTF-8?q?f=20TYPE=5FCHECKING`,=20=20non-evaluated=20stringified=20annota?= =?UTF-8?q?tions=20(#14485)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/dependencies/utils.py | 19 +++-- .../test_stringified_annotation_dependency.py | 80 +++++++++++++++++++ 2 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 tests/test_stringified_annotation_dependency.py diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 23bca6f2a..262dba6fd 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -209,11 +209,21 @@ def get_flat_params(dependant: Dependant) -> List[ModelField]: return path_params + query_params + header_params + cookie_params -def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature: +def _get_signature(call: Callable[..., Any]) -> inspect.Signature: if sys.version_info >= (3, 10): - signature = inspect.signature(call, eval_str=True) + try: + signature = inspect.signature(call, eval_str=True) + except NameError: + # Handle type annotations with if TYPE_CHECKING, not used by FastAPI + # e.g. dependency return types + signature = inspect.signature(call) else: signature = inspect.signature(call) + return signature + + +def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature: + signature = _get_signature(call) unwrapped = inspect.unwrap(call) globalns = getattr(unwrapped, "__globals__", {}) typed_params = [ @@ -239,10 +249,7 @@ def get_typed_annotation(annotation: Any, globalns: Dict[str, Any]) -> Any: def get_typed_return_annotation(call: Callable[..., Any]) -> Any: - if sys.version_info >= (3, 10): - signature = inspect.signature(call, eval_str=True) - else: - signature = inspect.signature(call) + signature = _get_signature(call) unwrapped = inspect.unwrap(call) annotation = signature.return_annotation diff --git a/tests/test_stringified_annotation_dependency.py b/tests/test_stringified_annotation_dependency.py new file mode 100644 index 000000000..89bb884b5 --- /dev/null +++ b/tests/test_stringified_annotation_dependency.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient +from inline_snapshot import snapshot +from typing_extensions import Annotated + +if TYPE_CHECKING: # pragma: no cover + from collections.abc import AsyncGenerator + + +class DummyClient: + async def get_people(self) -> list: + return ["John Doe", "Jane Doe"] + + async def close(self) -> None: + pass + + +async def get_client() -> AsyncGenerator[DummyClient, None]: + client = DummyClient() + yield client + await client.close() + + +Client = Annotated[DummyClient, Depends(get_client)] + + +@pytest.fixture(name="client") +def client_fixture() -> TestClient: + app = FastAPI() + + @app.get("/") + async def get_people(client: Client) -> list: + return await client.get_people() + + client = TestClient(app) + return client + + +def test_get(client: TestClient): + response = client.get("/") + assert response.status_code == 200, response.text + assert response.json() == ["John Doe", "Jane Doe"] + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "summary": "Get People", + "operationId": "get_people__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": {}, + "type": "array", + "title": "Response Get People Get", + } + } + }, + } + }, + } + } + }, + } + ) From 96bdde376f900d3c32c9e57f7cc16930fb29aa5e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 10 Dec 2025 12:06:32 +0000 Subject: [PATCH 239/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 08db56c8e..8e69b52f7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Fixes + +* 🐛 Fix support for `if TYPE_CHECKING`, non-evaluated stringified annotations. PR [#14485](https://github.com/fastapi/fastapi/pull/14485) by [@tiangolo](https://github.com/tiangolo). + ## 0.124.1 ### Fixes From 7b0b915749582206025f306924e6a7bf86041a13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 10 Dec 2025 13:07:53 +0100 Subject: [PATCH 240/256] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.12?= =?UTF-8?q?4.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 8e69b52f7..a0d0a4037 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.124.2 + ### Fixes * 🐛 Fix support for `if TYPE_CHECKING`, non-evaluated stringified annotations. PR [#14485](https://github.com/fastapi/fastapi/pull/14485) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 2e7dba8a1..8de426ad4 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.124.1" +__version__ = "0.124.2" from starlette import status as status From 442cb306f6bcffa3e3d7446a67800ad637829d68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 10 Dec 2025 04:28:40 -0800 Subject: [PATCH 241/256] =?UTF-8?q?=F0=9F=94=A5=20Remove=20external=20link?= =?UTF-8?q?s=20section=20(#14486)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/data/external_links.yml | 430 -------------------------------- docs/en/docs/external-links.md | 28 +-- docs/en/docs/resources/index.md | 2 +- docs/en/mkdocs.yml | 1 - 4 files changed, 8 insertions(+), 453 deletions(-) delete mode 100644 docs/en/data/external_links.yml diff --git a/docs/en/data/external_links.yml b/docs/en/data/external_links.yml deleted file mode 100644 index 6e71ab9eb..000000000 --- a/docs/en/data/external_links.yml +++ /dev/null @@ -1,430 +0,0 @@ -Articles: - English: - - author: Apitally - author_link: https://apitally.io - link: https://apitally.io/blog/getting-started-with-logging-in-fastapi - title: Getting started with logging in FastAPI - - author: Balthazar Rouberol - author_link: https://balthazar-rouberol.com - link: https://blog.balthazar-rouberol.com/how-to-profile-a-fastapi-asynchronous-request - title: How to profile a FastAPI asynchronous request - - author: Stephen Siegert - Neon - link: https://neon.tech/blog/deploy-a-serverless-fastapi-app-with-neon-postgres-and-aws-app-runner-at-any-scale - title: Deploy a Serverless FastAPI App with Neon Postgres and AWS App Runner at any scale - - author: Kurtis Pykes - NVIDIA - link: https://developer.nvidia.com/blog/building-a-machine-learning-microservice-with-fastapi/ - title: Building a Machine Learning Microservice with FastAPI - - author: Ravgeet Dhillon - Twilio - link: https://www.twilio.com/en-us/blog/booking-appointments-twilio-notion-fastapi - title: Booking Appointments with Twilio, Notion, and FastAPI - - author: Abhinav Tripathi - Microsoft Blogs - link: https://devblogs.microsoft.com/cosmosdb/azure-cosmos-db-python-and-fastapi/ - title: Write a Python data layer with Azure Cosmos DB and FastAPI - - author: Donny Peeters - author_link: https://github.com/Donnype - link: https://bitestreams.com/blog/fastapi-sqlalchemy/ - title: 10 Tips for adding SQLAlchemy to FastAPI - - author: Jessica Temporal - author_link: https://jtemporal.com/socials - link: https://jtemporal.com/tips-on-migrating-from-flask-to-fastapi-and-vice-versa/ - title: Tips on migrating from Flask to FastAPI and vice-versa - - author: Ankit Anchlia - author_link: https://linkedin.com/in/aanchlia21 - link: https://hackernoon.com/explore-how-to-effectively-use-jwt-with-fastapi - title: Explore How to Effectively Use JWT With FastAPI - - author: Nicoló Lino - author_link: https://www.nlino.com - link: https://github.com/softwarebloat/python-tracing-demo - title: Instrument FastAPI with OpenTelemetry tracing and visualize traces in Grafana Tempo. - - author: Mikhail Rozhkov, Elena Samuylova - author_link: https://www.linkedin.com/in/mnrozhkov/ - link: https://www.evidentlyai.com/blog/fastapi-tutorial - title: ML serving and monitoring with FastAPI and Evidently - - author: Visual Studio Code Team - author_link: https://code.visualstudio.com/ - link: https://code.visualstudio.com/docs/python/tutorial-fastapi - title: FastAPI Tutorial in Visual Studio Code - - author: Apitally - author_link: https://apitally.io - link: https://blog.apitally.io/fastapi-application-monitoring-made-easy - title: FastAPI application monitoring made easy - - author: John Philip - author_link: https://medium.com/@amjohnphilip - link: https://python.plainenglish.io/building-a-restful-api-with-fastapi-secure-signup-and-login-functionality-included-45cdbcb36106 - title: "Building a RESTful API with FastAPI: Secure Signup and Login Functionality Included" - - author: Keshav Malik - author_link: https://theinfosecguy.xyz/ - link: https://blog.theinfosecguy.xyz/building-a-crud-api-with-fastapi-and-supabase-a-step-by-step-guide - title: Building a CRUD API with FastAPI and Supabase - - author: Adejumo Ridwan Suleiman - author_link: https://www.linkedin.com/in/adejumoridwan/ - link: https://medium.com/python-in-plain-english/build-an-sms-spam-classifier-serverless-database-with-faunadb-and-fastapi-23dbb275bc5b - title: Build an SMS Spam Classifier Serverless Database with FaunaDB and FastAPI - - author: Raf Rasenberg - author_link: https://rafrasenberg.com/about/ - link: https://rafrasenberg.com/fastapi-lambda/ - title: 'FastAPI lambda container: serverless simplified' - - author: Teresa N. Fontanella De Santis - author_link: https://dev.to/ - link: https://dev.to/teresafds/authorization-on-fastapi-with-casbin-41og - title: Authorization on FastAPI with Casbin - - author: New Relic - author_link: https://newrelic.com - link: https://newrelic.com/instant-observability/fastapi/e559ec64-f765-4470-a15f-1901fcebb468 - title: How to monitor FastAPI application performance using Python agent - - author: Jean-Baptiste Rocher - author_link: https://hashnode.com/@jibrocher - link: https://dev.indooroutdoor.io/series/fastapi-react-poll-app - title: Building the Poll App From Django Tutorial With FastAPI And React - - author: Silvan Melchior - author_link: https://github.com/silvanmelchior - link: https://blog.devgenius.io/seamless-fastapi-configuration-with-confz-90949c14ea12 - title: Seamless FastAPI Configuration with ConfZ - - author: Kaustubh Gupta - author_link: https://medium.com/@kaustubhgupta1828/ - link: https://levelup.gitconnected.com/5-advance-features-of-fastapi-you-should-try-7c0ac7eebb3e - title: 5 Advanced Features of FastAPI You Should Try - - author: Kaustubh Gupta - author_link: https://medium.com/@kaustubhgupta1828/ - link: https://www.analyticsvidhya.com/blog/2021/06/deploying-ml-models-as-api-using-fastapi-and-heroku/ - title: Deploying ML Models as API Using FastAPI and Heroku - - link: https://jarmos.netlify.app/posts/using-github-actions-to-deploy-a-fastapi-project-to-heroku/ - title: Using GitHub Actions to Deploy a FastAPI Project to Heroku - author_link: https://jarmos.netlify.app/ - author: Somraj Saha - - author: "@pystar" - author_link: https://pystar.substack.com/ - link: https://pystar.substack.com/p/how-to-create-a-fake-certificate - title: How to Create A Fake Certificate Authority And Generate TLS Certs for FastAPI - - author: Ben Gamble - author_link: https://uk.linkedin.com/in/bengamble7 - link: https://ably.com/blog/realtime-ticket-booking-solution-kafka-fastapi-ably - title: Building a realtime ticket booking solution with Kafka, FastAPI, and Ably - - author: Shahriyar(Shako) Rzayev - author_link: https://www.linkedin.com/in/shahriyar-rzayev/ - link: https://www.azepug.az/posts/fastapi/#building-simple-e-commerce-with-nuxtjs-and-fastapi-series - title: Building simple E-Commerce with NuxtJS and FastAPI - - author: Rodrigo Arenas - author_link: https://rodrigo-arenas.medium.com/ - link: https://medium.com/analytics-vidhya/serve-a-machine-learning-model-using-sklearn-fastapi-and-docker-85aabf96729b - title: "Serve a machine learning model using Sklearn, FastAPI and Docker" - - author: Yashasvi Singh - author_link: https://hashnode.com/@aUnicornDev - link: https://aunicorndev.hashnode.dev/series/supafast-api - title: "Building an API with FastAPI and Supabase and Deploying on Deta" - - author: Navule Pavan Kumar Rao - author_link: https://www.linkedin.com/in/navule/ - link: https://www.tutlinks.com/deploy-fastapi-on-ubuntu-gunicorn-caddy-2/ - title: Deploy FastAPI on Ubuntu and Serve using Caddy 2 Web Server - - author: Patrick Ladon - author_link: https://dev.to/factorlive - link: https://dev.to/factorlive/python-facebook-messenger-webhook-with-fastapi-on-glitch-4n90 - title: Python Facebook messenger webhook with FastAPI on Glitch - - author: Valon Januzaj - author_link: https://www.linkedin.com/in/valon-januzaj-b02692187/ - link: https://valonjanuzaj.medium.com/deploy-a-dockerized-fastapi-application-to-aws-cc757830ba1b - title: Deploy a dockerized FastAPI application to AWS - - author: Amit Chaudhary - author_link: https://x.com/amitness - link: https://amitness.com/2020/06/fastapi-vs-flask/ - title: FastAPI for Flask Users - - author: Louis Guitton - author_link: https://x.com/louis_guitton - link: https://guitton.co/posts/fastapi-monitoring/ - title: How to monitor your FastAPI service - - author: Precious Ndubueze - author_link: https://medium.com/@gabbyprecious2000 - link: https://medium.com/@gabbyprecious2000/creating-a-crud-app-with-fastapi-part-one-7c049292ad37 - title: Creating a CRUD App with FastAPI (Part one) - - author: Farhad Malik - author_link: https://medium.com/@farhadmalik - link: https://towardsdatascience.com/build-and-host-fast-data-science-applications-using-fastapi-823be8a1d6a0 - title: Build And Host Fast Data Science Applications Using FastAPI - - author: Navule Pavan Kumar Rao - author_link: https://www.linkedin.com/in/navule/ - link: https://www.tutlinks.com/deploy-fastapi-on-azure/ - title: Deploy FastAPI on Azure App Service - - author: Davide Fiocco - author_link: https://github.com/davidefiocco - link: https://davidefiocco.github.io/streamlit-fastapi-ml-serving/ - title: Machine learning model serving in Python using FastAPI and streamlit - - author: Netflix - author_link: https://netflixtechblog.com/ - link: https://netflixtechblog.com/introducing-dispatch-da4b8a2a8072 - title: Introducing Dispatch - - author: Stavros Korokithakis - author_link: https://x.com/Stavros - link: https://www.stavros.io/posts/fastapi-with-django/ - title: Using FastAPI with Django - - author: Twilio - author_link: https://www.twilio.com - link: https://www.twilio.com/blog/build-secure-twilio-webhook-python-fastapi - title: Build a Secure Twilio Webhook with Python and FastAPI - - author: Sebastián Ramírez (tiangolo) - author_link: https://x.com/tiangolo - link: https://dev.to/tiangolo/build-a-web-api-from-scratch-with-fastapi-the-workshop-2ehe - title: Build a web API from scratch with FastAPI - the workshop - - author: Paul Sec - author_link: https://x.com/PaulWebSec - link: https://paulsec.github.io/posts/fastapi_plus_zeit_serverless_fu/ - title: FastAPI + Zeit.co = 🚀 - - author: cuongld2 - author_link: https://dev.to/cuongld2 - link: https://dev.to/cuongld2/build-simple-api-service-with-python-fastapi-part-1-581o - title: Build simple API service with Python FastAPI — Part 1 - - author: Paurakh Sharma Humagain - author_link: https://x.com/PaurakhSharma - link: https://dev.to/paurakhsharma/microservice-in-python-using-fastapi-24cc - title: Microservice in Python using FastAPI - - author: Guillermo Cruz - author_link: https://wuilly.com/ - link: https://wuilly.com/2019/10/real-time-notifications-with-python-and-postgres/ - title: Real-time Notifications with Python and Postgres - - author: Navule Pavan Kumar Rao - author_link: https://www.linkedin.com/in/navule/ - link: https://www.tutlinks.com/create-and-deploy-fastapi-app-to-heroku/ - title: Create and Deploy FastAPI app to Heroku without using Docker - - author: Arthur Henrique - author_link: https://x.com/arthurheinrique - link: https://medium.com/@arthur393/another-boilerplate-to-fastapi-azure-pipeline-ci-pytest-3c8d9a4be0bb - title: 'Another Boilerplate to FastAPI: Azure Pipeline CI + Pytest' - - author: Shane Soh - author_link: https://medium.com/@shane.soh - link: https://medium.com/analytics-vidhya/deploy-machine-learning-models-with-keras-fastapi-redis-and-docker-4940df614ece - title: Deploy Machine Learning Models with Keras, FastAPI, Redis and Docker - - author: Mandy Gu - author_link: https://towardsdatascience.com/@mandygu - link: https://towardsdatascience.com/deploying-iris-classifications-with-fastapi-and-docker-7c9b83fdec3a - title: 'Towards Data Science: Deploying Iris Classifications with FastAPI and Docker' - - author: Michael Herman - author_link: https://testdriven.io/authors/herman - link: https://testdriven.io/blog/fastapi-crud/ - title: 'TestDriven.io: Developing and Testing an Asynchronous API with FastAPI and Pytest' - - author: Bernard Brenyah - author_link: https://medium.com/@bbrenyah - link: https://medium.com/python-data/how-to-deploy-tensorflow-2-0-models-as-an-api-service-with-fastapi-docker-128b177e81f3 - title: How To Deploy Tensorflow 2.0 Models As An API Service With FastAPI & Docker - - author: Dylan Anthony - author_link: https://dev.to/dbanty - link: https://dev.to/dbanty/why-i-m-leaving-flask-3ki6 - title: Why I'm Leaving Flask - - author: Mike Moritz - author_link: https://medium.com/@mike.p.moritz - link: https://medium.com/@mike.p.moritz/using-docker-compose-to-deploy-a-lightweight-python-rest-api-with-a-job-queue-37e6072a209b - title: Using Docker Compose to deploy a lightweight Python REST API with a job queue - - author: '@euri10' - author_link: https://gitlab.com/euri10 - link: https://gitlab.com/euri10/fastapi_cheatsheet - title: A FastAPI and Swagger UI visual cheatsheet - - author: Uber Engineering - author_link: https://eng.uber.com - link: https://eng.uber.com/ludwig-v0-2/ - title: 'Uber: Ludwig v0.2 Adds New Features and Other Improvements to its Deep Learning Toolbox [including a FastAPI server]' - - author: Maarten Grootendorst - author_link: https://www.linkedin.com/in/mgrootendorst/ - link: https://towardsdatascience.com/how-to-deploy-a-machine-learning-model-dc51200fe8cf - title: How to Deploy a Machine Learning Model - - author: Johannes Gontrum - author_link: https://x.com/gntrm - link: https://medium.com/@gntrm/jwt-authentication-with-fastapi-and-aws-cognito-1333f7f2729e - title: JWT Authentication with FastAPI and AWS Cognito - - author: Ankush Thakur - author_link: https://geekflare.com/author/ankush/ - link: https://geekflare.com/python-asynchronous-web-frameworks/ - title: Top 5 Asynchronous Web Frameworks for Python - - author: Nico Axtmann - author_link: https://www.linkedin.com/in/nico-axtmann - link: https://medium.com/@nico.axtmann95/deploying-a-scikit-learn-model-with-onnx-und-fastapi-1af398268915 - title: Deploying a scikit-learn model with ONNX and FastAPI - - author: Nils de Bruin - author_link: https://medium.com/@nilsdebruin - link: https://medium.com/data-rebels/fastapi-authentication-revisited-enabling-api-key-authentication-122dc5975680 - title: 'FastAPI authentication revisited: Enabling API key authentication' - - author: Nick Cortale - author_link: https://nickc1.github.io/ - link: https://nickc1.github.io/api,/scikit-learn/2019/01/10/scikit-fastapi.html - title: 'FastAPI and Scikit-Learn: Easily Deploy Models' - - author: Errieta Kostala - author_link: https://dev.to/errietta - link: https://dev.to/errietta/introduction-to-the-fastapi-python-framework-2n10 - title: Introduction to the fastapi python framework - - author: Nils de Bruin - author_link: https://medium.com/@nilsdebruin - link: https://medium.com/data-rebels/fastapi-how-to-add-basic-and-cookie-authentication-a45c85ef47d3 - title: FastAPI — How to add basic and cookie authentication - - author: Nils de Bruin - author_link: https://medium.com/@nilsdebruin - link: https://medium.com/data-rebels/fastapi-google-as-an-external-authentication-provider-3a527672cf33 - title: FastAPI — Google as an external authentication provider - - author: William Hayes - author_link: https://medium.com/@williamhayes - link: https://medium.com/@williamhayes/fastapi-starlette-debug-vs-prod-5f7561db3a59 - title: FastAPI/Starlette debug vs prod - - author: Mukul Mantosh - author_link: https://x.com/MantoshMukul - link: https://www.jetbrains.com/pycharm/guide/tutorials/fastapi-aws-kubernetes/ - title: Developing FastAPI Application using K8s & AWS - - author: KrishNa - author_link: https://medium.com/@krishnardt365 - link: https://medium.com/@krishnardt365/fastapi-docker-and-postgres-91943e71be92 - title: Fastapi, Docker(Docker compose) and Postgres - - author: Devon Ray - author_link: https://devonray.com - link: https://devonray.com/blog/deploying-a-fastapi-project-using-aws-lambda-aurora-cdk - title: Deployment using Docker, Lambda, Aurora, CDK & GH Actions - - author: Shubhendra Kushwaha - author_link: https://www.linkedin.com/in/theshubhendra/ - link: https://theshubhendra.medium.com/mastering-soft-delete-advanced-sqlalchemy-techniques-4678f4738947 - title: 'Mastering Soft Delete: Advanced SQLAlchemy Techniques' - - author: Shubhendra Kushwaha - author_link: https://www.linkedin.com/in/theshubhendra/ - link: https://theshubhendra.medium.com/role-based-row-filtering-advanced-sqlalchemy-techniques-733e6b1328f6 - title: 'Role based row filtering: Advanced SQLAlchemy Techniques' - 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://x.com/_nicoax - link: https://blog.codecentric.de/2019/08/inbetriebnahme-eines-scikit-learn-modells-mit-onnx-und-fastapi/ - title: Inbetriebnahme eines scikit-learn-Modells mit ONNX und FastAPI - - author: Felix Schürmeyer - author_link: https://hellocoding.de/autor/felix-schuermeyer/ - link: https://hellocoding.de/blog/coding-language/python/fastapi - title: REST-API Programmieren mittels Python und dem FastAPI Modul - Japanese: - - author: '@bee2' - author_link: https://qiita.com/bee2 - link: https://qiita.com/bee2/items/75d9c0d7ba20e7a4a0e9 - title: '[FastAPI] Python製のASGI Web フレームワーク FastAPIに入門する' - - author: '@bee2' - author_link: https://qiita.com/bee2 - link: https://qiita.com/bee2/items/0ad260ab9835a2087dae - title: PythonのWeb frameworkのパフォーマンス比較 (Django, Flask, responder, FastAPI, japronto) - - author: ライトコードメディア編集部 - author_link: https://rightcode.co.jp/author/jun - link: https://rightcode.co.jp/blog/information-technology/fastapi-tutorial-todo-apps-admin-page-improvement - title: '【第4回】FastAPIチュートリアル: toDoアプリを作ってみよう【管理者ページ改良編】' - - author: ライトコードメディア編集部 - author_link: https://rightcode.co.jp/author/jun - link: https://rightcode.co.jp/blog/information-technology/fastapi-tutorial-todo-apps-authentication-user-registration - title: '【第3回】FastAPIチュートリアル: toDoアプリを作ってみよう【認証・ユーザ登録編】' - - author: ライトコードメディア編集部 - author_link: https://rightcode.co.jp/author/jun - link: https://rightcode.co.jp/blog/information-technology/fastapi-tutorial-todo-apps-model-building - title: '【第2回】FastAPIチュートリアル: ToDoアプリを作ってみよう【モデル構築編】' - - author: ライトコードメディア編集部 - author_link: https://rightcode.co.jp/author/jun - link: https://rightcode.co.jp/blog/information-technology/fastapi-tutorial-todo-apps-environment - title: '【第1回】FastAPIチュートリアル: ToDoアプリを作ってみよう【環境構築編】' - - author: Hikaru Takahashi - author_link: https://qiita.com/hikarut - link: https://qiita.com/hikarut/items/b178af2e2440c67c6ac4 - title: フロントエンド開発者向けのDockerによるPython開発環境構築 - - author: '@angel_katayoku' - author_link: https://qiita.com/angel_katayoku - link: https://qiita.com/angel_katayoku/items/8a458a8952f50b73f420 - title: FastAPIでPOSTされたJSONのレスポンスbodyを受け取る - - author: '@angel_katayoku' - author_link: https://qiita.com/angel_katayoku - link: https://qiita.com/angel_katayoku/items/4fbc1a4e2b33fa2237d2 - title: FastAPIをMySQLと接続してDockerで管理してみる - - author: '@angel_katayoku' - author_link: https://qiita.com/angel_katayoku - link: https://qiita.com/angel_katayoku/items/0e1f5dbbe62efc612a78 - title: FastAPIでCORSを回避 - - author: '@ryoryomaru' - author_link: https://qiita.com/ryoryomaru - link: https://qiita.com/ryoryomaru/items/59958ed385b3571d50de - title: python製の最新APIフレームワーク FastAPI を触ってみた - - author: '@mtitg' - author_link: https://qiita.com/mtitg - link: https://qiita.com/mtitg/items/47770e9a562dd150631d - title: FastAPI|DB接続してCRUDするPython製APIサーバーを構築 - Portuguese: - - author: Eduardo Mendes - author_link: https://bolha.us/@dunossauro - link: https://fastapidozero.dunossauro.com/ - title: FastAPI do ZERO - - author: Jessica Temporal - author_link: https://jtemporal.com/socials - link: https://jtemporal.com/dicas-para-migrar-de-flask-para-fastapi-e-vice-versa/ - title: Dicas para migrar uma aplicação de Flask para FastAPI e vice-versa - Russian: - - author: Troy Köhler - author_link: https://www.linkedin.com/in/trkohler/ - link: https://trkohler.com/fast-api-introduction-to-framework - title: 'FastAPI: знакомимся с фреймворком' - - author: prostomarkeloff - author_link: https://github.com/prostomarkeloff - link: https://habr.com/ru/post/478620/ - title: Почему Вы должны попробовать FastAPI? - - author: Andrey Korchak - author_link: https://habr.com/ru/users/57uff3r/ - link: https://habr.com/ru/post/454440/ - title: 'Мелкая питонячая радость #2: Starlette - Солидная примочка – FastAPI' - Vietnamese: - - author: Nguyễn Nhân - author_link: https://fullstackstation.com/author/figonking/ - link: https://fullstackstation.com/fastapi-trien-khai-bang-docker/ - title: 'FASTAPI: TRIỂN KHAI BẰNG DOCKER' - Taiwanese: - - author: Leon - author_link: http://editor.leonh.space/ - link: https://editor.leonh.space/2022/tortoise/ - title: 'Tortoise ORM / FastAPI 整合快速筆記' - Spanish: - - author: Eduardo Zepeda - author_link: https://coffeebytes.dev/en/authors/eduardo-zepeda/ - link: https://coffeebytes.dev/es/python-fastapi-el-mejor-framework-de-python/ - title: 'Tutorial de FastAPI, ¿el mejor framework de Python?' -Podcasts: - English: - - author: Behind the Commit - author_link: https://www.youtube.com/@BehindtheCommit - link: https://youtu.be/iaDRYUQ0OMM - title: Why FastAPI Became Python’s Fastest‑Growing Framework – Chat with Sebastián Ramírez - - author: Real Python - author_link: https://realpython.com/ - link: https://realpython.com/podcasts/rpp/72/ - title: Starting With FastAPI and Examining Python's Import System - Episode 72 - - author: Python Bytes FM - author_link: https://pythonbytes.fm/ - link: https://www.pythonpodcast.com/fastapi-web-application-framework-episode-259/ - title: 'Do you dare to press "."? - Episode 247 - Dan #6: SQLModel - use the same models for SQL and FastAPI' - - author: Podcast.`__init__` - author_link: https://www.pythonpodcast.com/ - link: https://www.pythonpodcast.com/fastapi-web-application-framework-episode-259/ - title: Build The Next Generation Of Python Web Applications With FastAPI - Episode 259 - interview to Sebastían Ramírez (tiangolo) - - author: Python Bytes FM - author_link: https://pythonbytes.fm/ - link: https://pythonbytes.fm/episodes/show/123/time-to-right-the-py-wrongs?time_in_sec=855 - title: FastAPI on PythonBytes -Talks: - English: - - author: Sebastián Ramírez (tiangolo) - author_link: https://x.com/tiangolo - link: https://www.youtube.com/watch?v=mwvmfl8nN_U - title: 'Keynote: Behind the scenes of FastAPI and friends for developers and builders — Sebastián Ramírez' - - author: Jeny Sadadia - author_link: https://github.com/JenySadadia - link: https://www.youtube.com/watch?v=uZdTe8_Z6BQ - title: 'PyCon AU 2023: Testing asynchronous applications with FastAPI and pytest' - - author: Sebastián Ramírez (tiangolo) - author_link: https://x.com/tiangolo - link: https://www.youtube.com/watch?v=PnpTY1f4k2U - title: '[VIRTUAL] Py.Amsterdam''s flying Software Circus: Intro to FastAPI' - - author: Sebastián Ramírez (tiangolo) - author_link: https://x.com/tiangolo - link: https://www.youtube.com/watch?v=z9K5pwb0rt8 - title: 'PyConBY 2020: Serve ML models easily with FastAPI' - - author: Chris Withers - author_link: https://x.com/chriswithers13 - link: https://www.youtube.com/watch?v=3DLwPcrE5mA - title: 'PyCon UK 2019: FastAPI from the ground up' - Taiwanese: - - author: Blueswen - author_link: https://github.com/blueswen - link: https://www.youtube.com/watch?v=y3sumuoDq4w - title: 'PyCon TW 2024: 全方位強化 Python 服務可觀測性:以 FastAPI 和 Grafana Stack 為例' diff --git a/docs/en/docs/external-links.md b/docs/en/docs/external-links.md index 3ed04e5c5..481cf1d7f 100644 --- a/docs/en/docs/external-links.md +++ b/docs/en/docs/external-links.md @@ -1,36 +1,22 @@ -# External Links and Articles +# External Links **FastAPI** has a great community constantly growing. There are many posts, articles, tools, and projects, related to **FastAPI**. -Here's an incomplete list of some of them. +You could easily use a search engine or video platform to find many resources related to FastAPI. -/// tip +/// info -If you have an article, project, tool, or anything related to **FastAPI** that is not yet listed here, create a Pull Request adding it. +Before, this page used to list links to external articles. + +But now that FastAPI is the backend framework with the most GitHub stars across languages, and the most starred and used framework in Python, it no longer makes sense to attempt to list all articles written about it. /// -{% for section_name, section_content in external_links.items() %} - -## {{ section_name }} - -{% for lang_name, lang_content in section_content.items() %} - -### {{ lang_name }} - -{% for item in lang_content %} - -* {{ item.title }} by {{ item.author }}. - -{% endfor %} -{% endfor %} -{% endfor %} - ## GitHub Repositories -Most starred GitHub repositories with the topic `fastapi`: +Most starred GitHub repositories with the topic `fastapi`: {% for repo in topic_repos %} diff --git a/docs/en/docs/resources/index.md b/docs/en/docs/resources/index.md index caefdf125..f7d48576f 100644 --- a/docs/en/docs/resources/index.md +++ b/docs/en/docs/resources/index.md @@ -1,3 +1,3 @@ # Resources { #resources } -Additional resources, external links, articles and more. ✈️ +Additional resources, external links, and more. ✈️ diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index fd346a3d3..0e0adab9b 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -59,7 +59,6 @@ plugins: search: null macros: include_yaml: - - external_links: ../en/data/external_links.yml - github_sponsors: ../en/data/github_sponsors.yml - people: ../en/data/people.yml - contributors: ../en/data/contributors.yml From 4a98a66778987648d2195155c9cbe0824d67b02a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 10 Dec 2025 12:29:04 +0000 Subject: [PATCH 242/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index a0d0a4037..5af3540e9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Docs + +* 🔥 Remove external links section. PR [#14486](https://github.com/fastapi/fastapi/pull/14486) by [@tiangolo](https://github.com/tiangolo). + ## 0.124.2 ### Fixes From cd9d093f603b81f34eb2751e9ca4522089fa9696 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 10 Dec 2025 04:56:50 -0800 Subject: [PATCH 243/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20docs=20about=20?= =?UTF-8?q?re-raising=20validation=20errors,=20do=20not=20include=20string?= =?UTF-8?q?=20as=20is=20to=20not=20leak=20information=20(#14487)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/tutorial/handling-errors.md | 35 +++++++------------ docs_src/handling_errors/tutorial004.py | 7 ++-- .../test_handling_errors/test_tutorial004.py | 14 ++------ 3 files changed, 19 insertions(+), 37 deletions(-) diff --git a/docs/en/docs/tutorial/handling-errors.md b/docs/en/docs/tutorial/handling-errors.md index 53501837c..4092039b1 100644 --- a/docs/en/docs/tutorial/handling-errors.md +++ b/docs/en/docs/tutorial/handling-errors.md @@ -127,7 +127,7 @@ To override it, import the `RequestValidationError` and use it with `@app.except The exception handler will receive a `Request` and the exception. -{* ../../docs_src/handling_errors/tutorial004.py hl[2,14:16] *} +{* ../../docs_src/handling_errors/tutorial004.py hl[2,14:19] *} Now, if you go to `/items/foo`, instead of getting the default JSON error with: @@ -149,36 +149,17 @@ Now, if you go to `/items/foo`, instead of getting the default JSON error with: you will get a text version, with: ``` -1 validation error -path -> item_id - value is not a valid integer (type=type_error.integer) +Validation errors: +Field: ('path', 'item_id'), Error: Input should be a valid integer, unable to parse string as an integer ``` -#### `RequestValidationError` vs `ValidationError` { #requestvalidationerror-vs-validationerror } - -/// warning - -These are technical details that you might skip if it's not important for you now. - -/// - -`RequestValidationError` is a sub-class of Pydantic's `ValidationError`. - -**FastAPI** uses it so that, if you use a Pydantic model in `response_model`, and your data has an error, you will see the error in your log. - -But the client/user will not see it. Instead, the client will receive an "Internal Server Error" with an HTTP status code `500`. - -It should be this way because if you have a Pydantic `ValidationError` in your *response* or anywhere in your code (not in the client's *request*), it's actually a bug in your code. - -And while you fix it, your clients/users shouldn't have access to internal information about the error, as that could expose a security vulnerability. - ### Override the `HTTPException` error handler { #override-the-httpexception-error-handler } The same way, you can override the `HTTPException` handler. For example, you could want to return a plain text response instead of JSON for these errors: -{* ../../docs_src/handling_errors/tutorial004.py hl[3:4,9:11,22] *} +{* ../../docs_src/handling_errors/tutorial004.py hl[3:4,9:11,25] *} /// note | Technical Details @@ -188,6 +169,14 @@ You could also use `from starlette.responses import PlainTextResponse`. /// +/// warning + +Have in mind that the `RequestValidationError` contains the information of the file name and line where the validation error happens so that you can show it in your logs with the relevant information if you want to. + +But that means that if you just convert it to a string and return that information directly, you could be leaking a bit of information about your system, that's why here the code extracts and shows each error independently. + +/// + ### Use the `RequestValidationError` body { #use-the-requestvalidationerror-body } The `RequestValidationError` contains the `body` it received with invalid data. diff --git a/docs_src/handling_errors/tutorial004.py b/docs_src/handling_errors/tutorial004.py index 300a3834f..ae50807e9 100644 --- a/docs_src/handling_errors/tutorial004.py +++ b/docs_src/handling_errors/tutorial004.py @@ -12,8 +12,11 @@ async def http_exception_handler(request, exc): @app.exception_handler(RequestValidationError) -async def validation_exception_handler(request, exc): - return PlainTextResponse(str(exc), status_code=400) +async def validation_exception_handler(request, exc: RequestValidationError): + message = "Validation errors:" + for error in exc.errors(): + message += f"\nField: {error['loc']}, Error: {error['msg']}" + return PlainTextResponse(message, status_code=400) @app.get("/items/{item_id}") diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial004.py b/tests/test_tutorial/test_handling_errors/test_tutorial004.py index 217159a59..c04bf3724 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial004.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial004.py @@ -8,18 +8,8 @@ client = TestClient(app) def test_get_validation_error(): response = client.get("/items/foo") assert response.status_code == 400, response.text - # TODO: remove when deprecating Pydantic v1 - assert ( - # TODO: remove when deprecating Pydantic v1 - "path -> item_id" in response.text - or "'loc': ('path', 'item_id')" in response.text - ) - assert ( - # TODO: remove when deprecating Pydantic v1 - "value is not a valid integer" in response.text - or "Input should be a valid integer, unable to parse string as an integer" - in response.text - ) + assert "Validation errors:" in response.text + assert "Field: ('path', 'item_id')" in response.text def test_get_http_error(): From 30747a69c842ffa29084b811b69e1629f2bbb4bd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 10 Dec 2025 12:57:18 +0000 Subject: [PATCH 244/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 5af3540e9..81eeb9f99 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* 📝 Update docs about re-raising validation errors, do not include string as is to not leak information. PR [#14487](https://github.com/fastapi/fastapi/pull/14487) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove external links section. PR [#14486](https://github.com/fastapi/fastapi/pull/14486) by [@tiangolo](https://github.com/tiangolo). ## 0.124.2 From 4a9f13763dee116092b610ef5b3a86c71b2d55b8 Mon Sep 17 00:00:00 2001 From: Nils-Hero Lindemann Date: Wed, 10 Dec 2025 14:54:34 +0100 Subject: [PATCH 245/256] =?UTF-8?q?=F0=9F=8C=90=20Sync=20German=20docs=20(?= =?UTF-8?q?#14488)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Sync with #14472 * Sync with #14413 * Sync with #14486 * Sync with #14487 --- docs/de/docs/advanced/additional-responses.md | 4 +- docs/de/docs/advanced/dataclasses.md | 6 +- docs/de/docs/advanced/openapi-callbacks.md | 8 +- .../path-operation-advanced-configuration.md | 10 +-- docs/de/docs/advanced/response-directly.md | 2 +- docs/de/docs/advanced/settings.md | 8 +- docs/de/docs/how-to/configure-swagger-ui.md | 2 +- .../docs/how-to/custom-request-and-route.md | 12 +-- docs/de/docs/project-generation.md | 18 ++--- docs/de/docs/resources/index.md | 2 +- docs/de/docs/tutorial/bigger-applications.md | 78 ++++--------------- docs/de/docs/tutorial/cookie-param-models.md | 2 +- docs/de/docs/tutorial/handling-errors.md | 35 +++------ docs/de/docs/tutorial/testing.md | 54 +------------ 14 files changed, 64 insertions(+), 177 deletions(-) diff --git a/docs/de/docs/advanced/additional-responses.md b/docs/de/docs/advanced/additional-responses.md index 218dd6c4f..29a0a1477 100644 --- a/docs/de/docs/advanced/additional-responses.md +++ b/docs/de/docs/advanced/additional-responses.md @@ -175,7 +175,7 @@ Sie können denselben `responses`-Parameter verwenden, um verschiedene Medientyp Sie können beispielsweise einen zusätzlichen Medientyp `image/png` hinzufügen und damit deklarieren, dass Ihre *Pfadoperation* ein JSON-Objekt (mit dem Medientyp `application/json`) oder ein PNG-Bild zurückgeben kann: -{* ../../docs_src/additional_responses/tutorial002.py hl[19:24,28] *} +{* ../../docs_src/additional_responses/tutorial002_py310.py hl[17:22,26] *} /// note | Hinweis @@ -237,7 +237,7 @@ Mit dieser Technik können Sie einige vordefinierte Responses in Ihren *Pfadoper Zum Beispiel: -{* ../../docs_src/additional_responses/tutorial004.py hl[13:17,26] *} +{* ../../docs_src/additional_responses/tutorial004_py310.py hl[11:15,24] *} ## Weitere Informationen zu OpenAPI-Responses { #more-information-about-openapi-responses } diff --git a/docs/de/docs/advanced/dataclasses.md b/docs/de/docs/advanced/dataclasses.md index 12ea8e9ec..e2d59c776 100644 --- a/docs/de/docs/advanced/dataclasses.md +++ b/docs/de/docs/advanced/dataclasses.md @@ -4,7 +4,7 @@ FastAPI basiert auf **Pydantic**, und ich habe Ihnen gezeigt, wie Sie Pydantic-M Aber FastAPI unterstützt auf die gleiche Weise auch die Verwendung von `dataclasses`: -{* ../../docs_src/dataclasses/tutorial001.py hl[1,7:12,19:20] *} +{* ../../docs_src/dataclasses/tutorial001_py310.py hl[1,6:11,18:19] *} Das ist dank **Pydantic** ebenfalls möglich, da es `dataclasses` intern unterstützt. @@ -32,7 +32,7 @@ Wenn Sie jedoch eine Menge Datenklassen herumliegen haben, ist dies ein guter Tr Sie können `dataclasses` auch im Parameter `response_model` verwenden: -{* ../../docs_src/dataclasses/tutorial002.py hl[1,7:13,19] *} +{* ../../docs_src/dataclasses/tutorial002_py310.py hl[1,6:12,18] *} Die Datenklasse wird automatisch in eine Pydantic-Datenklasse konvertiert. @@ -48,7 +48,7 @@ In einigen Fällen müssen Sie möglicherweise immer noch Pydantics Version von In diesem Fall können Sie einfach die Standard-`dataclasses` durch `pydantic.dataclasses` ersetzen, was einen direkten Ersatz darstellt: -{* ../../docs_src/dataclasses/tutorial003.py hl[1,5,8:11,14:17,23:25,28] *} +{* ../../docs_src/dataclasses/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *} 1. Wir importieren `field` weiterhin von Standard-`dataclasses`. diff --git a/docs/de/docs/advanced/openapi-callbacks.md b/docs/de/docs/advanced/openapi-callbacks.md index afc48bbb8..fd68ab8dc 100644 --- a/docs/de/docs/advanced/openapi-callbacks.md +++ b/docs/de/docs/advanced/openapi-callbacks.md @@ -31,7 +31,7 @@ Sie verfügt über eine *Pfadoperation*, die einen `Invoice`-Body empfängt, und Dieser Teil ist ziemlich normal, der größte Teil des Codes ist Ihnen wahrscheinlich bereits bekannt: -{* ../../docs_src/openapi_callbacks/tutorial001.py hl[9:13,36:53] *} +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[7:11,34:51] *} /// tip | Tipp @@ -90,7 +90,7 @@ Wenn Sie diese Sichtweise (des *externen Entwicklers*) vorübergehend übernehme Erstellen Sie zunächst einen neuen `APIRouter`, der einen oder mehrere Callbacks enthält. -{* ../../docs_src/openapi_callbacks/tutorial001.py hl[3,25] *} +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[1,23] *} ### Die Callback-*Pfadoperation* erstellen { #create-the-callback-path-operation } @@ -101,7 +101,7 @@ Sie sollte wie eine normale FastAPI-*Pfadoperation* aussehen: * Sie sollte wahrscheinlich eine Deklaration des Bodys enthalten, die sie erhalten soll, z. B. `body: InvoiceEvent`. * Und sie könnte auch eine Deklaration der Response enthalten, die zurückgegeben werden soll, z. B. `response_model=InvoiceEventReceived`. -{* ../../docs_src/openapi_callbacks/tutorial001.py hl[16:18,21:22,28:32] *} +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[14:16,19:20,26:30] *} Es gibt zwei Hauptunterschiede zu einer normalen *Pfadoperation*: @@ -169,7 +169,7 @@ An diesem Punkt haben Sie die benötigte(n) *Callback-Pfadoperation(en)* (diejen Verwenden Sie nun den Parameter `callbacks` im *Pfadoperation-Dekorator Ihrer API*, um das Attribut `.routes` (das ist eigentlich nur eine `list`e von Routen/*Pfadoperationen*) dieses Callback-Routers zu übergeben: -{* ../../docs_src/openapi_callbacks/tutorial001.py hl[35] *} +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[33] *} /// tip | Tipp diff --git a/docs/de/docs/advanced/path-operation-advanced-configuration.md b/docs/de/docs/advanced/path-operation-advanced-configuration.md index f5ec7c49e..bad768feb 100644 --- a/docs/de/docs/advanced/path-operation-advanced-configuration.md +++ b/docs/de/docs/advanced/path-operation-advanced-configuration.md @@ -50,7 +50,7 @@ Das Hinzufügen eines `\f` (ein maskiertes „Form Feed“-Zeichen) führt dazu, Sie wird nicht in der Dokumentation angezeigt, aber andere Tools (z. B. Sphinx) können den Rest verwenden. -{* ../../docs_src/path_operation_advanced_configuration/tutorial004.py hl[19:29] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial004_py310.py hl[17:27] *} ## Zusätzliche Responses { #additional-responses } @@ -155,13 +155,13 @@ In der folgenden Anwendung verwenden wir beispielsweise weder die integrierte Fu //// tab | Pydantic v2 -{* ../../docs_src/path_operation_advanced_configuration/tutorial007.py hl[17:22, 24] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[15:20, 22] *} //// //// tab | Pydantic v1 -{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py hl[17:22, 24] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py hl[15:20, 22] *} //// @@ -179,13 +179,13 @@ Und dann parsen wir in unserem Code diesen YAML-Inhalt direkt und verwenden dann //// tab | Pydantic v2 -{* ../../docs_src/path_operation_advanced_configuration/tutorial007.py hl[26:33] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[24:31] *} //// //// tab | Pydantic v1 -{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py hl[26:33] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py hl[24:31] *} //// diff --git a/docs/de/docs/advanced/response-directly.md b/docs/de/docs/advanced/response-directly.md index d99517373..06ec2c32e 100644 --- a/docs/de/docs/advanced/response-directly.md +++ b/docs/de/docs/advanced/response-directly.md @@ -34,7 +34,7 @@ Sie können beispielsweise kein Pydantic-Modell in eine `JSONResponse` einfügen In diesen Fällen können Sie den `jsonable_encoder` verwenden, um Ihre Daten zu konvertieren, bevor Sie sie an eine Response übergeben: -{* ../../docs_src/response_directly/tutorial001.py hl[6:7,21:22] *} +{* ../../docs_src/response_directly/tutorial001_py310.py hl[5:6,20:21] *} /// note | Technische Details diff --git a/docs/de/docs/advanced/settings.md b/docs/de/docs/advanced/settings.md index ccd7f373d..03263a28b 100644 --- a/docs/de/docs/advanced/settings.md +++ b/docs/de/docs/advanced/settings.md @@ -148,7 +148,7 @@ Dies könnte besonders beim Testen nützlich sein, da es sehr einfach ist, eine Ausgehend vom vorherigen Beispiel könnte Ihre Datei `config.py` so aussehen: -{* ../../docs_src/settings/app02/config.py hl[10] *} +{* ../../docs_src/settings/app02_an_py39/config.py hl[10] *} Beachten Sie, dass wir jetzt keine Standardinstanz `settings = Settings()` erstellen. @@ -174,7 +174,7 @@ Und dann können wir das von der *Pfadoperation-Funktion* als Abhängigkeit einf Dann wäre es sehr einfach, beim Testen ein anderes Einstellungsobjekt bereitzustellen, indem man eine Abhängigkeitsüberschreibung für `get_settings` erstellt: -{* ../../docs_src/settings/app02/test_main.py hl[9:10,13,21] *} +{* ../../docs_src/settings/app02_an_py39/test_main.py hl[9:10,13,21] *} Bei der Abhängigkeitsüberschreibung legen wir einen neuen Wert für `admin_email` fest, wenn wir das neue `Settings`-Objekt erstellen, und geben dann dieses neue Objekt zurück. @@ -217,7 +217,7 @@ Und dann aktualisieren Sie Ihre `config.py` mit: //// tab | Pydantic v2 -{* ../../docs_src/settings/app03_an/config.py hl[9] *} +{* ../../docs_src/settings/app03_an_py39/config.py hl[9] *} /// tip | Tipp @@ -229,7 +229,7 @@ Das Attribut `model_config` wird nur für die Pydantic-Konfiguration verwendet. //// tab | Pydantic v1 -{* ../../docs_src/settings/app03_an/config_pv1.py hl[9:10] *} +{* ../../docs_src/settings/app03_an_py39/config_pv1.py hl[9:10] *} /// tip | Tipp diff --git a/docs/de/docs/how-to/configure-swagger-ui.md b/docs/de/docs/how-to/configure-swagger-ui.md index 351cb996c..3616f03ac 100644 --- a/docs/de/docs/how-to/configure-swagger-ui.md +++ b/docs/de/docs/how-to/configure-swagger-ui.md @@ -40,7 +40,7 @@ FastAPI enthält einige Defaultkonfigurationsparameter, die für die meisten Anw Es umfasst die folgenden Defaultkonfigurationen: -{* ../../fastapi/openapi/docs.py ln[8:23] hl[17:23] *} +{* ../../fastapi/openapi/docs.py ln[9:24] hl[18:24] *} Sie können jede davon überschreiben, indem Sie im Argument `swagger_ui_parameters` einen anderen Wert festlegen. diff --git a/docs/de/docs/how-to/custom-request-and-route.md b/docs/de/docs/how-to/custom-request-and-route.md index 246717c04..017de2096 100644 --- a/docs/de/docs/how-to/custom-request-and-route.md +++ b/docs/de/docs/how-to/custom-request-and-route.md @@ -42,7 +42,7 @@ Wenn der Header kein `gzip` enthält, wird nicht versucht, den Body zu dekomprim Auf diese Weise kann dieselbe Routenklasse gzip-komprimierte oder unkomprimierte Requests verarbeiten. -{* ../../docs_src/custom_request_and_route/tutorial001.py hl[8:15] *} +{* ../../docs_src/custom_request_and_route/tutorial001_an_py310.py hl[9:16] *} ### Eine benutzerdefinierte `GzipRoute`-Klasse erstellen { #create-a-custom-gziproute-class } @@ -54,7 +54,7 @@ Diese Methode gibt eine Funktion zurück. Und diese Funktion empfängt einen ../../docs_src/bigger_applications/app_an_py39/dependencies.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python hl_lines="1 5-7" title="app/dependencies.py" -{!> ../../docs_src/bigger_applications/app_an/dependencies.py!} -``` - -//// - -//// tab | Python 3.8+ nicht annotiert - -/// tip | Tipp - -Bevorzugen Sie die `Annotated`-Version, falls möglich. - -/// - -```Python hl_lines="1 4-6" title="app/dependencies.py" -{!> ../../docs_src/bigger_applications/app/dependencies.py!} -``` - -//// +{* ../../docs_src/bigger_applications/app_an_py39/dependencies.py hl[3,6:8] title["app/dependencies.py"] *} /// tip | Tipp @@ -181,9 +149,7 @@ Wir wissen, dass alle *Pfadoperationen* in diesem Modul folgendes haben: Anstatt also alles zu jeder *Pfadoperation* hinzuzufügen, können wir es dem `APIRouter` hinzufügen. -```Python hl_lines="5-10 16 21" title="app/routers/items.py" -{!../../docs_src/bigger_applications/app/routers/items.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[5:10,16,21] title["app/routers/items.py"] *} Da der Pfad jeder *Pfadoperation* mit `/` beginnen muss, wie in: @@ -242,9 +208,7 @@ Und wir müssen die Abhängigkeitsfunktion aus dem Modul `app.dependencies` impo Daher verwenden wir einen relativen Import mit `..` für die Abhängigkeiten: -```Python hl_lines="3" title="app/routers/items.py" -{!../../docs_src/bigger_applications/app/routers/items.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[3] title["app/routers/items.py"] *} #### Wie relative Importe funktionieren { #how-relative-imports-work } @@ -315,9 +279,7 @@ Wir fügen weder das Präfix `/items` noch `tags=["items"]` zu jeder *Pfadoperat Aber wir können immer noch _mehr_ `tags` hinzufügen, die auf eine bestimmte *Pfadoperation* angewendet werden, sowie einige zusätzliche `responses`, die speziell für diese *Pfadoperation* gelten: -```Python hl_lines="30-31" title="app/routers/items.py" -{!../../docs_src/bigger_applications/app/routers/items.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[30:31] title["app/routers/items.py"] *} /// tip | Tipp @@ -343,17 +305,13 @@ Sie importieren und erstellen wie gewohnt eine `FastAPI`-Klasse. Und wir können sogar [globale Abhängigkeiten](dependencies/global-dependencies.md){.internal-link target=_blank} deklarieren, die mit den Abhängigkeiten für jeden `APIRouter` kombiniert werden: -```Python hl_lines="1 3 7" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[1,3,7] title["app/main.py"] *} ### Den `APIRouter` importieren { #import-the-apirouter } Jetzt importieren wir die anderen Submodule, die `APIRouter` haben: -```Python hl_lines="4-5" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[4:5] title["app/main.py"] *} Da es sich bei den Dateien `app/routers/users.py` und `app/routers/items.py` um Submodule handelt, die Teil desselben Python-Packages `app` sind, können wir einen einzelnen Punkt `.` verwenden, um sie mit „relativen Imports“ zu importieren. @@ -416,17 +374,13 @@ würde der `router` von `users` den von `items` überschreiben und wir könnten Um also beide in derselben Datei verwenden zu können, importieren wir die Submodule direkt: -```Python hl_lines="5" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[5] title["app/main.py"] *} ### Die `APIRouter` für `users` und `items` inkludieren { #include-the-apirouters-for-users-and-items } Inkludieren wir nun die `router` aus diesen Submodulen `users` und `items`: -```Python hl_lines="10-11" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[10:11] title["app/main.py"] *} /// info | Info @@ -466,17 +420,13 @@ Sie enthält einen `APIRouter` mit einigen administrativen *Pfadoperationen*, di In diesem Beispiel wird es ganz einfach sein. Nehmen wir jedoch an, dass wir, da sie mit anderen Projekten in der Organisation geteilt wird, sie nicht ändern und kein `prefix`, `dependencies`, `tags`, usw. direkt zum `APIRouter` hinzufügen können: -```Python hl_lines="3" title="app/internal/admin.py" -{!../../docs_src/bigger_applications/app/internal/admin.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *} Aber wir möchten immer noch ein benutzerdefiniertes `prefix` festlegen, wenn wir den `APIRouter` einbinden, sodass alle seine *Pfadoperationen* mit `/admin` beginnen, wir möchten es mit den `dependencies` sichern, die wir bereits für dieses Projekt haben, und wir möchten `tags` und `responses` hinzufügen. Wir können das alles deklarieren, ohne den ursprünglichen `APIRouter` ändern zu müssen, indem wir diese Parameter an `app.include_router()` übergeben: -```Python hl_lines="14-17" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[14:17] title["app/main.py"] *} Auf diese Weise bleibt der ursprüngliche `APIRouter` unverändert, sodass wir dieselbe `app/internal/admin.py`-Datei weiterhin mit anderen Projekten in der Organisation teilen können. @@ -497,9 +447,7 @@ Wir können *Pfadoperationen* auch direkt zur `FastAPI`-App hinzufügen. Hier machen wir es ... nur um zu zeigen, dass wir es können 🤷: -```Python hl_lines="21-23" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[21:23] title["app/main.py"] *} und es wird korrekt funktionieren, zusammen mit allen anderen *Pfadoperationen*, die mit `app.include_router()` hinzugefügt wurden. diff --git a/docs/de/docs/tutorial/cookie-param-models.md b/docs/de/docs/tutorial/cookie-param-models.md index 2baf3d70d..25718bd33 100644 --- a/docs/de/docs/tutorial/cookie-param-models.md +++ b/docs/de/docs/tutorial/cookie-param-models.md @@ -50,7 +50,7 @@ Ihre API hat jetzt die Macht, ihre eigene Response**. diff --git a/docs/de/docs/tutorial/handling-errors.md b/docs/de/docs/tutorial/handling-errors.md index 58e4607c5..a39c3db37 100644 --- a/docs/de/docs/tutorial/handling-errors.md +++ b/docs/de/docs/tutorial/handling-errors.md @@ -127,7 +127,7 @@ Um diesen zu überschreiben, importieren Sie den `RequestValidationError` und ve Der Exceptionhandler erhält einen `Request` und die Exception. -{* ../../docs_src/handling_errors/tutorial004.py hl[2,14:16] *} +{* ../../docs_src/handling_errors/tutorial004.py hl[2,14:19] *} Wenn Sie nun zu `/items/foo` gehen, erhalten Sie anstelle des standardmäßigen JSON-Fehlers mit: @@ -149,36 +149,17 @@ Wenn Sie nun zu `/items/foo` gehen, erhalten Sie anstelle des standardmäßigen eine Textversion mit: ``` -1 validation error -path -> item_id - value is not a valid integer (type=type_error.integer) +Validation errors: +Field: ('path', 'item_id'), Error: Input should be a valid integer, unable to parse string as an integer ``` -#### `RequestValidationError` vs. `ValidationError` { #requestvalidationerror-vs-validationerror } - -/// warning | Achtung - -Dies sind technische Details, die Sie überspringen können, wenn sie für Sie jetzt nicht wichtig sind. - -/// - -`RequestValidationError` ist eine Unterklasse von Pydantics `ValidationError`. - -**FastAPI** verwendet diesen so, dass, wenn Sie ein Pydantic-Modell in `response_model` verwenden und Ihre Daten einen Fehler haben, Sie den Fehler in Ihrem Log sehen. - -Aber der Client/Benutzer wird ihn nicht sehen. Stattdessen erhält der Client einen „Internal Server Error“ mit einem HTTP-Statuscode `500`. - -Es sollte so sein, denn wenn Sie einen Pydantic `ValidationError` in Ihrer *Response* oder irgendwo anders in Ihrem Code haben (nicht im *Request* des Clients), ist es tatsächlich ein Fehler in Ihrem Code. - -Und während Sie den Fehler beheben, sollten Ihre Clients/Benutzer keinen Zugriff auf interne Informationen über den Fehler haben, da das eine Sicherheitslücke aufdecken könnte. - ### Überschreiben des `HTTPException`-Fehlerhandlers { #override-the-httpexception-error-handler } Auf die gleiche Weise können Sie den `HTTPException`-Handler überschreiben. Zum Beispiel könnten Sie eine Klartext-Response statt JSON für diese Fehler zurückgeben wollen: -{* ../../docs_src/handling_errors/tutorial004.py hl[3:4,9:11,22] *} +{* ../../docs_src/handling_errors/tutorial004.py hl[3:4,9:11,25] *} /// note | Technische Details @@ -188,6 +169,14 @@ Sie könnten auch `from starlette.responses import PlainTextResponse` verwenden. /// +/// warning | Achtung + +Beachten Sie, dass der `RequestValidationError` Informationen über den Dateinamen und die Zeile enthält, in der der Validierungsfehler auftritt, sodass Sie ihn bei Bedarf mit den relevanten Informationen in Ihren Logs anzeigen können. + +Das bedeutet aber auch, dass, wenn Sie ihn einfach in einen String umwandeln und diese Informationen direkt zurückgeben, Sie möglicherweise ein paar Informationen über Ihr System preisgeben. Daher extrahiert und zeigt der Code hier jeden Fehler getrennt. + +/// + ### Verwenden des `RequestValidationError`-Bodys { #use-the-requestvalidationerror-body } Der `RequestValidationError` enthält den empfangenen `body` mit den ungültigen Daten. diff --git a/docs/de/docs/tutorial/testing.md b/docs/de/docs/tutorial/testing.md index 9c28a2a22..b18469998 100644 --- a/docs/de/docs/tutorial/testing.md +++ b/docs/de/docs/tutorial/testing.md @@ -122,63 +122,13 @@ Sie verfügt über eine `POST`-Operation, die mehrere Fehler zurückgeben könnt Beide *Pfadoperationen* erfordern einen `X-Token`-Header. -//// tab | Python 3.10+ - -```Python -{!> ../../docs_src/app_testing/app_b_an_py310/main.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!> ../../docs_src/app_testing/app_b_an_py39/main.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python -{!> ../../docs_src/app_testing/app_b_an/main.py!} -``` - -//// - -//// tab | Python 3.10+ nicht annotiert - -/// tip | Tipp - -Bevorzugen Sie die `Annotated`-Version, falls möglich. - -/// - -```Python -{!> ../../docs_src/app_testing/app_b_py310/main.py!} -``` - -//// - -//// tab | Python 3.8+ nicht annotiert - -/// tip | Tipp - -Bevorzugen Sie die `Annotated`-Version, falls möglich. - -/// - -```Python -{!> ../../docs_src/app_testing/app_b/main.py!} -``` - -//// +{* ../../docs_src/app_testing/app_b_an_py310/main.py *} ### Erweiterte Testdatei { #extended-testing-file } Anschließend könnten Sie `test_main.py` mit den erweiterten Tests aktualisieren: -{* ../../docs_src/app_testing/app_b/test_main.py *} +{* ../../docs_src/app_testing/app_b_an_py310/test_main.py *} Wenn Sie möchten, dass der Client Informationen im Request übergibt und Sie nicht wissen, wie das geht, können Sie suchen (googeln), wie es mit `httpx` gemacht wird, oder sogar, wie es mit `requests` gemacht wird, da das Design von HTTPX auf dem Design von Requests basiert. From 1cf7cd8af0bd71a9d02d45744cced86f660003ea Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 10 Dec 2025 13:54:57 +0000 Subject: [PATCH 246/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 81eeb9f99..bb1e95aa9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -12,6 +12,10 @@ hide: * 📝 Update docs about re-raising validation errors, do not include string as is to not leak information. PR [#14487](https://github.com/fastapi/fastapi/pull/14487) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove external links section. PR [#14486](https://github.com/fastapi/fastapi/pull/14486) by [@tiangolo](https://github.com/tiangolo). +### Translations + +* 🌐 Sync German docs. PR [#14488](https://github.com/fastapi/fastapi/pull/14488) by [@nilslindemann](https://github.com/nilslindemann). + ## 0.124.2 ### Fixes From 4c4d5201986dce315107ba70ae930d9db4575904 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 11 Dec 2025 06:48:47 -0800 Subject: [PATCH 247/256] =?UTF-8?q?=F0=9F=93=9D=20Tweak=20links=20format?= =?UTF-8?q?=20(#14505)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/_llm-test.md | 4 ++-- docs/en/docs/index.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/en/docs/_llm-test.md b/docs/en/docs/_llm-test.md index e72450b91..9f216f9d7 100644 --- a/docs/en/docs/_llm-test.md +++ b/docs/en/docs/_llm-test.md @@ -15,7 +15,7 @@ Use as follows: The tests: -## Code snippets { #code-snippets} +## Code snippets { #code-snippets } //// tab | Test @@ -53,7 +53,7 @@ See for example section `### Quotes` in `docs/de/llm-prompt.md`. //// -## Quotes in code snippets { #quotes-in-code-snippets} +## Quotes in code snippets { #quotes-in-code-snippets } //// tab | Test diff --git a/docs/en/docs/index.md b/docs/en/docs/index.md index df03b7675..a0a5de3b7 100644 --- a/docs/en/docs/index.md +++ b/docs/en/docs/index.md @@ -52,13 +52,13 @@ The key features are: -### Keystone Sponsor +### Keystone Sponsor { #keystone-sponsor } {% for sponsor in sponsors.keystone -%} {% endfor -%} -### Gold and Silver Sponsors +### Gold and Silver Sponsors { #gold-and-silver-sponsors } {% for sponsor in sponsors.gold -%} From 009c8af7fec44206c97cb08fba20effe7895fd77 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 11 Dec 2025 14:49:09 +0000 Subject: [PATCH 248/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index bb1e95aa9..dc97a1a15 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* 📝 Tweak links format. PR [#14505](https://github.com/fastapi/fastapi/pull/14505) by [@tiangolo](https://github.com/tiangolo). * 📝 Update docs about re-raising validation errors, do not include string as is to not leak information. PR [#14487](https://github.com/fastapi/fastapi/pull/14487) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove external links section. PR [#14486](https://github.com/fastapi/fastapi/pull/14486) by [@tiangolo](https://github.com/tiangolo). From a7ba9932ba5fe87adb3ea3884dc6cd6294d117dd Mon Sep 17 00:00:00 2001 From: Sofie Van Landeghem Date: Thu, 11 Dec 2025 20:58:21 +0530 Subject: [PATCH 249/256] =?UTF-8?q?=E2=9C=85=20Expand=20test=20matrix=20to?= =?UTF-8?q?=20include=20Windows=20and=20MacOS=20(#14171)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez Co-authored-by: github-actions[bot] --- .github/workflows/test.yml | 50 ++++++++++++++++++++++++++++---------- pyproject.toml | 1 + 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8157e364b..94b3fbd9c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -44,22 +44,44 @@ jobs: run: bash scripts/lint.sh test: - runs-on: ubuntu-latest strategy: matrix: - python-version: - - "3.14" - - "3.13" - - "3.12" - - "3.11" - - "3.10" - - "3.9" - - "3.8" - pydantic-version: ["pydantic-v1", "pydantic-v2"] - exclude: - - python-version: "3.14" + os: [ ubuntu-latest, windows-latest, macos-latest ] + python-version: [ "3.14" ] + pydantic-version: [ "pydantic-v2" ] + coverage: ["coverage"] + include: + - os: macos-latest + python-version: "3.8" pydantic-version: "pydantic-v1" + coverage: coverage + - os: windows-latest + python-version: "3.8" + pydantic-version: "pydantic-v2" + coverage: coverage + - os: ubuntu-latest + python-version: "3.9" + pydantic-version: "pydantic-v1" + coverage: coverage + - os: macos-latest + python-version: "3.10" + pydantic-version: "pydantic-v2" + - os: windows-latest + python-version: "3.11" + pydantic-version: "pydantic-v1" + - os: ubuntu-latest + python-version: "3.12" + pydantic-version: "pydantic-v2" + - os: macos-latest + python-version: "3.13" + pydantic-version: "pydantic-v1" + coverage: coverage + - os: windows-latest + python-version: "3.13" + pydantic-version: "pydantic-v2" + coverage: coverage fail-fast: false + runs-on: ${{ matrix.os }} steps: - name: Dump GitHub context env: @@ -96,10 +118,12 @@ jobs: env: COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }} CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }} + # Do not store coverage for all possible combinations to avoid file size max errors in Smokeshow - name: Store coverage files + if: matrix.coverage == 'coverage' uses: actions/upload-artifact@v5 with: - name: coverage-${{ matrix.python-version }}-${{ matrix.pydantic-version }} + name: coverage-${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.pydantic-version }} path: coverage include-hidden-files: true diff --git a/pyproject.toml b/pyproject.toml index f8d5fa7c7..ef4440b1b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -196,6 +196,7 @@ source = [ "tests", "fastapi" ] +relative_files = true context = '${CONTEXT}' dynamic_context = "test_function" omit = [ From 931e80f20c54b6587a76f8b6249c904cce3c5af3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 11 Dec 2025 15:28:47 +0000 Subject: [PATCH 250/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index dc97a1a15..17d179614 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -17,6 +17,10 @@ hide: * 🌐 Sync German docs. PR [#14488](https://github.com/fastapi/fastapi/pull/14488) by [@nilslindemann](https://github.com/nilslindemann). +### Internal + +* ✅ Expand test matrix to include Windows and MacOS. PR [#14171](https://github.com/fastapi/fastapi/pull/14171) by [@svlandeg](https://github.com/svlandeg). + ## 0.124.2 ### Fixes From 564a4ac1b8c7c75c7229f18f06c5800944b38508 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 11 Dec 2025 08:02:26 -0800 Subject: [PATCH 251/256] =?UTF-8?q?=F0=9F=91=B7=20Tweak=20coverage=20to=20?= =?UTF-8?q?not=20pass=20Smokeshow=20max=20file=20size=20limit=20(#14507)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 94b3fbd9c..85f9c4afd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -46,15 +46,13 @@ jobs: test: strategy: matrix: - os: [ ubuntu-latest, windows-latest, macos-latest ] + os: [ windows-latest, macos-latest ] python-version: [ "3.14" ] pydantic-version: [ "pydantic-v2" ] - coverage: ["coverage"] include: - os: macos-latest python-version: "3.8" pydantic-version: "pydantic-v1" - coverage: coverage - os: windows-latest python-version: "3.8" pydantic-version: "pydantic-v2" @@ -75,11 +73,14 @@ jobs: - os: macos-latest python-version: "3.13" pydantic-version: "pydantic-v1" - coverage: coverage - os: windows-latest python-version: "3.13" pydantic-version: "pydantic-v2" coverage: coverage + - os: ubuntu-latest + python-version: "3.14" + pydantic-version: "pydantic-v2" + coverage: coverage fail-fast: false runs-on: ${{ matrix.os }} steps: From 475ce41268f600d88e094443ecf2e2781763e47e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 11 Dec 2025 16:02:50 +0000 Subject: [PATCH 252/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 17d179614..a6fc2f923 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -19,6 +19,7 @@ hide: ### Internal +* 👷 Tweak coverage to not pass Smokeshow max file size limit. PR [#14507](https://github.com/fastapi/fastapi/pull/14507) by [@tiangolo](https://github.com/tiangolo). * ✅ Expand test matrix to include Windows and MacOS. PR [#14171](https://github.com/fastapi/fastapi/pull/14171) by [@svlandeg](https://github.com/svlandeg). ## 0.124.2 From 6c54bcefd3c37c8656e909980af00f86acea99c3 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Thu, 11 Dec 2025 17:15:36 +0100 Subject: [PATCH 253/256] =?UTF-8?q?=E2=9C=85=20Add=20set=20of=20tests=20fo?= =?UTF-8?q?r=20request=20parameters=20and=20alias=20(#14358)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- tests/test_request_params/__init__.py | 0 .../test_request_params/test_body/__init__.py | 0 .../test_body/test_list.py | 523 +++++++++++++++ .../test_body/test_optional_list.py | 600 ++++++++++++++++++ .../test_body/test_optional_str.py | 569 +++++++++++++++++ .../test_body/test_required_str.py | 514 +++++++++++++++ tests/test_request_params/test_body/utils.py | 7 + .../test_cookie/__init__.py | 0 .../test_cookie/test_list.py | 3 + .../test_cookie/test_optional_list.py | 3 + .../test_cookie/test_optional_str.py | 383 +++++++++++ .../test_cookie/test_required_str.py | 503 +++++++++++++++ .../test_request_params/test_file/__init__.py | 0 .../test_file/test_list.py | 597 +++++++++++++++++ .../test_file/test_optional.py | 443 +++++++++++++ .../test_file/test_optional_list.py | 487 ++++++++++++++ .../test_file/test_required.py | 536 ++++++++++++++++ tests/test_request_params/test_file/utils.py | 7 + .../test_request_params/test_form/__init__.py | 0 .../test_form/test_list.py | 527 +++++++++++++++ .../test_form/test_optional_list.py | 454 +++++++++++++ .../test_form/test_optional_str.py | 419 ++++++++++++ .../test_form/test_required_str.py | 502 +++++++++++++++ tests/test_request_params/test_form/utils.py | 7 + .../test_header/__init__.py | 0 .../test_header/test_list.py | 505 +++++++++++++++ .../test_header/test_optional_list.py | 407 ++++++++++++ .../test_header/test_optional_str.py | 375 +++++++++++ .../test_header/test_required_str.py | 492 ++++++++++++++ .../test_request_params/test_path/__init__.py | 0 .../test_path/test_list.py | 1 + .../test_path/test_optional_list.py | 1 + .../test_path/test_optional_str.py | 1 + .../test_path/test_required_str.py | 102 +++ .../test_query/__init__.py | 0 .../test_query/test_list.py | 506 +++++++++++++++ .../test_query/test_optional_list.py | 403 ++++++++++++ .../test_query/test_optional_str.py | 375 +++++++++++ .../test_query/test_required_str.py | 495 +++++++++++++++ 39 files changed, 10747 insertions(+) create mode 100644 tests/test_request_params/__init__.py create mode 100644 tests/test_request_params/test_body/__init__.py create mode 100644 tests/test_request_params/test_body/test_list.py create mode 100644 tests/test_request_params/test_body/test_optional_list.py create mode 100644 tests/test_request_params/test_body/test_optional_str.py create mode 100644 tests/test_request_params/test_body/test_required_str.py create mode 100644 tests/test_request_params/test_body/utils.py create mode 100644 tests/test_request_params/test_cookie/__init__.py create mode 100644 tests/test_request_params/test_cookie/test_list.py create mode 100644 tests/test_request_params/test_cookie/test_optional_list.py create mode 100644 tests/test_request_params/test_cookie/test_optional_str.py create mode 100644 tests/test_request_params/test_cookie/test_required_str.py create mode 100644 tests/test_request_params/test_file/__init__.py create mode 100644 tests/test_request_params/test_file/test_list.py create mode 100644 tests/test_request_params/test_file/test_optional.py create mode 100644 tests/test_request_params/test_file/test_optional_list.py create mode 100644 tests/test_request_params/test_file/test_required.py create mode 100644 tests/test_request_params/test_file/utils.py create mode 100644 tests/test_request_params/test_form/__init__.py create mode 100644 tests/test_request_params/test_form/test_list.py create mode 100644 tests/test_request_params/test_form/test_optional_list.py create mode 100644 tests/test_request_params/test_form/test_optional_str.py create mode 100644 tests/test_request_params/test_form/test_required_str.py create mode 100644 tests/test_request_params/test_form/utils.py create mode 100644 tests/test_request_params/test_header/__init__.py create mode 100644 tests/test_request_params/test_header/test_list.py create mode 100644 tests/test_request_params/test_header/test_optional_list.py create mode 100644 tests/test_request_params/test_header/test_optional_str.py create mode 100644 tests/test_request_params/test_header/test_required_str.py create mode 100644 tests/test_request_params/test_path/__init__.py create mode 100644 tests/test_request_params/test_path/test_list.py create mode 100644 tests/test_request_params/test_path/test_optional_list.py create mode 100644 tests/test_request_params/test_path/test_optional_str.py create mode 100644 tests/test_request_params/test_path/test_required_str.py create mode 100644 tests/test_request_params/test_query/__init__.py create mode 100644 tests/test_request_params/test_query/test_list.py create mode 100644 tests/test_request_params/test_query/test_optional_list.py create mode 100644 tests/test_request_params/test_query/test_optional_str.py create mode 100644 tests/test_request_params/test_query/test_required_str.py diff --git a/tests/test_request_params/__init__.py b/tests/test_request_params/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_request_params/test_body/__init__.py b/tests/test_request_params/test_body/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_request_params/test_body/test_list.py b/tests/test_request_params/test_body/test_list.py new file mode 100644 index 000000000..884e1d08a --- /dev/null +++ b/tests/test_request_params/test_body/test_list.py @@ -0,0 +1,523 @@ +from typing import List, Union + +import pytest +from dirty_equals import IsDict, IsOneOf, IsPartialDict +from fastapi import Body, FastAPI +from fastapi._compat import PYDANTIC_V2 +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field +from typing_extensions import Annotated + +from tests.utils import needs_pydanticv2 + +from .utils import get_body_model_name + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.post("/required-list-str", operation_id="required_list_str") +async def read_required_list_str(p: Annotated[List[str], Body(embed=True)]): + return {"p": p} + + +class BodyModelRequiredListStr(BaseModel): + p: List[str] + + +@app.post("/model-required-list-str", operation_id="model_required_list_str") +def read_model_required_list_str(p: BodyModelRequiredListStr): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/required-list-str", "/model-required-list-str"], +) +def test_required_list_str_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p": { + "items": {"type": "string"}, + "title": "P", + "type": "array", + }, + }, + "required": ["p"], + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize("json", [None, {}]) +@pytest.mark.parametrize( + "path", + ["/required-list-str", "/model-required-list-str"], +) +def test_required_list_str_missing(path: str, json: Union[dict, None]): + client = TestClient(app) + response = client.post(path, json=json) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": IsOneOf(["body", "p"], ["body"]), + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + ) | IsDict( + { + "detail": [ + { + "loc": IsOneOf(["body", "p"], ["body"]), + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@pytest.mark.parametrize( + "path", + ["/required-list-str", "/model-required-list-str"], +) +def test_required_list_str(path: str): + client = TestClient(app) + response = client.post(path, json={"p": ["hello", "world"]}) + assert response.status_code == 200 + assert response.json() == {"p": ["hello", "world"]} + + +# ===================================================================================== +# Alias + + +@app.post("/required-list-alias", operation_id="required_list_alias") +async def read_required_list_alias( + p: Annotated[List[str], Body(embed=True, alias="p_alias")], +): + return {"p": p} + + +class BodyModelRequiredListAlias(BaseModel): + p: List[str] = Field(alias="p_alias") + + +@app.post("/model-required-list-alias", operation_id="model_required_list_alias") +async def read_model_required_list_alias(p: BodyModelRequiredListAlias): + return {"p": p.p} # pragma: no cover + + +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-list-alias", + marks=pytest.mark.xfail( + raises=AssertionError, + condition=PYDANTIC_V2, + reason="Fails only with PDv2 models", + strict=False, + ), + ), + "/model-required-list-alias", + ], +) +def test_required_list_str_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_alias": { + "items": {"type": "string"}, + "title": "P Alias", + "type": "array", + }, + }, + "required": ["p_alias"], + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize("json", [None, {}]) +@pytest.mark.parametrize( + "path", + ["/required-list-alias", "/model-required-list-alias"], +) +def test_required_list_alias_missing(path: str, json: Union[dict, None]): + client = TestClient(app) + response = client.post(path, json=json) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": IsOneOf(["body", "p_alias"], ["body"]), + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": IsOneOf(["body", "p_alias"], ["body"]), + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@pytest.mark.parametrize( + "path", + ["/required-list-alias", "/model-required-list-alias"], +) +def test_required_list_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, json={"p": ["hello", "world"]}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, {"p": ["hello", "world"]}), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "p_alias"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@pytest.mark.parametrize( + "path", + ["/required-list-alias", "/model-required-list-alias"], +) +def test_required_list_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, json={"p_alias": ["hello", "world"]}) + assert response.status_code == 200, response.text + assert response.json() == {"p": ["hello", "world"]} + + +# ===================================================================================== +# Validation alias + + +@app.post( + "/required-list-validation-alias", operation_id="required_list_validation_alias" +) +def read_required_list_validation_alias( + p: Annotated[List[str], Body(embed=True, validation_alias="p_val_alias")], +): + return {"p": p} + + +class BodyModelRequiredListValidationAlias(BaseModel): + p: List[str] = Field(validation_alias="p_val_alias") + + +@app.post( + "/model-required-list-validation-alias", + operation_id="model_required_list_validation_alias", +) +async def read_model_required_list_validation_alias( + p: BodyModelRequiredListValidationAlias, +): + return {"p": p.p} # pragma: no cover + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + ["/required-list-validation-alias", "/model-required-list-validation-alias"], +) +def test_required_list_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "items": {"type": "string"}, + "title": "P Val Alias", + "type": "array", + }, + }, + "required": ["p_val_alias"], + "title": body_model_name, + "type": "object", + } + + +@needs_pydanticv2 +@pytest.mark.parametrize("json", [None, {}]) +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-list-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-list-validation-alias", + ], +) +def test_required_list_validation_alias_missing(path: str, json: Union[dict, None]): + client = TestClient(app) + response = client.post(path, json=json) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": IsOneOf( # /required-validation-alias fails here + ["body"], ["body", "p_val_alias"] + ), + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-list-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-list-validation-alias", + ], +) +def test_required_list_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, json={"p": ["hello", "world"]}) + assert response.status_code == 422, ( + response.text # /required-list-validation-alias fails here + ) + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_val_alias"], + "msg": "Field required", + "input": IsOneOf(None, IsPartialDict({"p": ["hello", "world"]})), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-list-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-list-validation-alias", + ], +) +def test_required_list_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, json={"p_val_alias": ["hello", "world"]}) + assert response.status_code == 200, ( + response.text # /required-list-validation-alias fails here + ) + assert response.json() == {"p": ["hello", "world"]} + + +# ===================================================================================== +# Alias and validation alias + + +@app.post( + "/required-list-alias-and-validation-alias", + operation_id="required_list_alias_and_validation_alias", +) +def read_required_list_alias_and_validation_alias( + p: Annotated[ + List[str], Body(embed=True, alias="p_alias", validation_alias="p_val_alias") + ], +): + return {"p": p} + + +class BodyModelRequiredListAliasAndValidationAlias(BaseModel): + p: List[str] = Field(alias="p_alias", validation_alias="p_val_alias") + + +@app.post( + "/model-required-list-alias-and-validation-alias", + operation_id="model_required_list_alias_and_validation_alias", +) +def read_model_required_list_alias_and_validation_alias( + p: BodyModelRequiredListAliasAndValidationAlias, +): + return {"p": p.p} # pragma: no cover + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias-and-validation-alias", + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "items": {"type": "string"}, + "title": "P Val Alias", + "type": "array", + }, + }, + "required": ["p_val_alias"], + "title": body_model_name, + "type": "object", + } + + +@needs_pydanticv2 +@pytest.mark.parametrize("json", [None, {}]) +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-list-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_missing(path: str, json): + client = TestClient(app) + response = client.post(path, json=json) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": IsOneOf( # /required-list-alias-and-validation-alias fails here + ["body"], ["body", "p_val_alias"] + ), + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-list-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, json={"p": ["hello", "world"]}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ # /required-list-alias-and-validation-alias fails here + "body", + "p_val_alias", + ], + "msg": "Field required", + "input": IsOneOf(None, {"p": ["hello", "world"]}), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-list-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, json={"p_alias": ["hello", "world"]}) + assert response.status_code == 422, response.text + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_val_alias"], + "msg": "Field required", + "input": IsOneOf(None, {"p_alias": ["hello", "world"]}), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-list-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, json={"p_val_alias": ["hello", "world"]}) + assert response.status_code == 200, ( + response.text # /required-list-alias-and-validation-alias fails here + ) + assert response.json() == {"p": ["hello", "world"]} diff --git a/tests/test_request_params/test_body/test_optional_list.py b/tests/test_request_params/test_body/test_optional_list.py new file mode 100644 index 000000000..c86398ce9 --- /dev/null +++ b/tests/test_request_params/test_body/test_optional_list.py @@ -0,0 +1,600 @@ +from typing import List, Optional + +import pytest +from dirty_equals import IsDict +from fastapi import Body, FastAPI +from fastapi._compat import PYDANTIC_V2 +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field +from typing_extensions import Annotated + +from tests.utils import needs_pydanticv2 + +from .utils import get_body_model_name + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.post("/optional-list-str", operation_id="optional_list_str") +async def read_optional_list_str( + p: Annotated[Optional[List[str]], Body(embed=True)] = None, +): + return {"p": p} + + +class BodyModelOptionalListStr(BaseModel): + p: Optional[List[str]] = None + + +@app.post("/model-optional-list-str", operation_id="model_optional_list_str") +async def read_model_optional_list_str(p: BodyModelOptionalListStr): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/optional-list-str", "/model-optional-list-str"], +) +def test_optional_list_str_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( + { + "properties": { + "p": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P", + }, + }, + "title": body_model_name, + "type": "object", + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "properties": { + "p": {"items": {"type": "string"}, "type": "array", "title": "P"}, + }, + "title": body_model_name, + "type": "object", + } + ) + + +def test_optional_list_str_missing(): + client = TestClient(app) + response = client.post("/optional-list-str") + assert response.status_code == 200, response.text + assert response.json() == {"p": None} + + +def test_model_optional_list_str_missing(): + client = TestClient(app) + response = client.post("/model-optional-list-str") + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "input": None, + "loc": ["body"], + "msg": "Field required", + "type": "missing", + }, + ], + } + ) | IsDict( + { + # TODO: remove when deprecating Pydantic v1 + "detail": [ + { + "loc": ["body"], + "msg": "field required", + "type": "value_error.missing", + }, + ], + } + ) + + +@pytest.mark.parametrize( + "path", + ["/optional-list-str", "/model-optional-list-str"], +) +def test_optional_list_str_missing_empty_dict(path: str): + client = TestClient(app) + response = client.post(path, json={}) + assert response.status_code == 200, response.text + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-list-str", "/model-optional-list-str"], +) +def test_optional_list_str(path: str): + client = TestClient(app) + response = client.post(path, json={"p": ["hello", "world"]}) + assert response.status_code == 200 + assert response.json() == {"p": ["hello", "world"]} + + +# ===================================================================================== +# Alias + + +@app.post("/optional-list-alias", operation_id="optional_list_alias") +async def read_optional_list_alias( + p: Annotated[Optional[List[str]], Body(embed=True, alias="p_alias")] = None, +): + return {"p": p} + + +class BodyModelOptionalListAlias(BaseModel): + p: Optional[List[str]] = Field(None, alias="p_alias") + + +@app.post("/model-optional-list-alias", operation_id="model_optional_list_alias") +async def read_model_optional_list_alias(p: BodyModelOptionalListAlias): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-list-alias", + marks=pytest.mark.xfail( + raises=AssertionError, + strict=False, + condition=PYDANTIC_V2, + reason="Fails only with PDv2", + ), + ), + "/model-optional-list-alias", + ], +) +def test_optional_list_str_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( + { + "properties": { + "p_alias": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Alias", + }, + }, + "title": body_model_name, + "type": "object", + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "properties": { + "p_alias": { + "items": {"type": "string"}, + "type": "array", + "title": "P Alias", + }, + }, + "title": body_model_name, + "type": "object", + } + ) + + +def test_optional_list_alias_missing(): + client = TestClient(app) + response = client.post("/optional-list-alias") + assert response.status_code == 200, response.text + assert response.json() == {"p": None} + + +def test_model_optional_list_alias_missing(): + client = TestClient(app) + response = client.post("/model-optional-list-alias") + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "input": None, + "loc": ["body"], + "msg": "Field required", + "type": "missing", + }, + ], + } + ) | IsDict( + { + # TODO: remove when deprecating Pydantic v1 + "detail": [ + { + "loc": ["body"], + "msg": "field required", + "type": "value_error.missing", + }, + ], + } + ) + + +@pytest.mark.parametrize( + "path", + ["/optional-list-alias", "/model-optional-list-alias"], +) +def test_optional_list_alias_missing_empty_dict(path: str): + client = TestClient(app) + response = client.post(path, json={}) + assert response.status_code == 200, response.text + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-list-alias", "/model-optional-list-alias"], +) +def test_optional_list_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, json={"p": ["hello", "world"]}) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-list-alias", "/model-optional-list-alias"], +) +def test_optional_list_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, json={"p_alias": ["hello", "world"]}) + assert response.status_code == 200 + assert response.json() == {"p": ["hello", "world"]} + + +# ===================================================================================== +# Validation alias + + +@app.post( + "/optional-list-validation-alias", operation_id="optional_list_validation_alias" +) +def read_optional_list_validation_alias( + p: Annotated[ + Optional[List[str]], Body(embed=True, validation_alias="p_val_alias") + ] = None, +): + return {"p": p} + + +class BodyModelOptionalListValidationAlias(BaseModel): + p: Optional[List[str]] = Field(None, validation_alias="p_val_alias") + + +@app.post( + "/model-optional-list-validation-alias", + operation_id="model_optional_list_validation_alias", +) +def read_model_optional_list_validation_alias( + p: BodyModelOptionalListValidationAlias, +): + return {"p": p.p} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + ["/optional-list-validation-alias", "/model-optional-list-validation-alias"], +) +def test_optional_list_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( + { + "properties": { + "p_val_alias": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Val Alias", + }, + }, + "title": body_model_name, + "type": "object", + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "properties": { + "p_val_alias": { + "items": {"type": "string"}, + "type": "array", + "title": "P Val Alias", + }, + }, + "title": body_model_name, + "type": "object", + } + ) + + +def test_optional_list_validation_alias_missing(): + client = TestClient(app) + response = client.post("/optional-list-validation-alias") + assert response.status_code == 200, response.text + assert response.json() == {"p": None} + + +def test_model_optional_list_validation_alias_missing(): + client = TestClient(app) + response = client.post("/model-optional-list-validation-alias") + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "input": None, + "loc": ["body"], + "msg": "Field required", + "type": "missing", + }, + ], + } + ) | IsDict( + { + # TODO: remove when deprecating Pydantic v1 + "detail": [ + { + "loc": ["body"], + "msg": "field required", + "type": "value_error.missing", + }, + ], + } + ) + + +@pytest.mark.parametrize( + "path", + ["/optional-list-validation-alias", "/model-optional-list-validation-alias"], +) +def test_optional_list_validation_alias_missing_empty_dict(path: str): + client = TestClient(app) + response = client.post(path, json={}) + assert response.status_code == 200, response.text + assert response.json() == {"p": None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-list-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-optional-list-validation-alias", + ], +) +def test_optional_list_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, json={"p": ["hello", "world"]}) + assert response.status_code == 200 + assert response.json() == {"p": None} # /optional-list-validation-alias fails here + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-list-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-optional-list-validation-alias", + ], +) +def test_optional_list_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, json={"p_val_alias": ["hello", "world"]}) + assert response.status_code == 200, response.text + assert response.json() == { # /optional-list-validation-alias fails here + "p": ["hello", "world"] + } + + +# ===================================================================================== +# Alias and validation alias + + +@app.post( + "/optional-list-alias-and-validation-alias", + operation_id="optional_list_alias_and_validation_alias", +) +def read_optional_list_alias_and_validation_alias( + p: Annotated[ + Optional[List[str]], + Body(embed=True, alias="p_alias", validation_alias="p_val_alias"), + ] = None, +): + return {"p": p} + + +class BodyModelOptionalListAliasAndValidationAlias(BaseModel): + p: Optional[List[str]] = Field( + None, alias="p_alias", validation_alias="p_val_alias" + ) + + +@app.post( + "/model-optional-list-alias-and-validation-alias", + operation_id="model_optional_list_alias_and_validation_alias", +) +def read_model_optional_list_alias_and_validation_alias( + p: BodyModelOptionalListAliasAndValidationAlias, +): + return {"p": p.p} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias-and-validation-alias", + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( + { + "properties": { + "p_val_alias": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Val Alias", + }, + }, + "title": body_model_name, + "type": "object", + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "properties": { + "p_val_alias": { + "items": {"type": "string"}, + "type": "array", + "title": "P Val Alias", + }, + }, + "title": body_model_name, + "type": "object", + } + ) + + +def test_optional_list_alias_and_validation_alias_missing(): + client = TestClient(app) + response = client.post("/optional-list-alias-and-validation-alias") + assert response.status_code == 200, response.text + assert response.json() == {"p": None} + + +def test_model_optional_list_alias_and_validation_alias_missing(): + client = TestClient(app) + response = client.post("/model-optional-list-alias-and-validation-alias") + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "input": None, + "loc": ["body"], + "msg": "Field required", + "type": "missing", + }, + ], + } + ) | IsDict( + { + # TODO: remove when deprecating Pydantic v1 + "detail": [ + { + "loc": ["body"], + "msg": "field required", + "type": "value_error.missing", + }, + ], + } + ) + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias-and-validation-alias", + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_missing_empty_dict(path: str): + client = TestClient(app) + response = client.post(path, json={}) + assert response.status_code == 200, response.text + assert response.json() == {"p": None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias-and-validation-alias", + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, json={"p": ["hello", "world"]}) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-list-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, json={"p_alias": ["hello", "world"]}) + assert response.status_code == 200 + assert response.json() == { + "p": None # /optional-list-alias-and-validation-alias fails here + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-list-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, json={"p_val_alias": ["hello", "world"]}) + assert response.status_code == 200, response.text + assert response.json() == { + "p": [ # /optional-list-alias-and-validation-alias fails here + "hello", + "world", + ] + } diff --git a/tests/test_request_params/test_body/test_optional_str.py b/tests/test_request_params/test_body/test_optional_str.py new file mode 100644 index 000000000..43ed367dd --- /dev/null +++ b/tests/test_request_params/test_body/test_optional_str.py @@ -0,0 +1,569 @@ +from typing import Optional + +import pytest +from dirty_equals import IsDict +from fastapi import Body, FastAPI +from fastapi._compat import PYDANTIC_V2 +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field +from typing_extensions import Annotated + +from tests.utils import needs_pydanticv2 + +from .utils import get_body_model_name + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.post("/optional-str", operation_id="optional_str") +async def read_optional_str(p: Annotated[Optional[str], Body(embed=True)] = None): + return {"p": p} + + +class BodyModelOptionalStr(BaseModel): + p: Optional[str] = None + + +@app.post("/model-optional-str", operation_id="model_optional_str") +async def read_model_optional_str(p: BodyModelOptionalStr): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/optional-str", "/model-optional-str"], +) +def test_optional_str_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( + { + "properties": { + "p": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P", + }, + }, + "title": body_model_name, + "type": "object", + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "properties": { + "p": {"type": "string", "title": "P"}, + }, + "title": body_model_name, + "type": "object", + } + ) + + +def test_optional_str_missing(): + client = TestClient(app) + response = client.post("/optional-str") + assert response.status_code == 200, response.text + assert response.json() == {"p": None} + + +def test_model_optional_str_missing(): + client = TestClient(app) + response = client.post("/model-optional-str") + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "input": None, + "loc": ["body"], + "msg": "Field required", + "type": "missing", + }, + ], + } + ) | IsDict( + { + # TODO: remove when deprecating Pydantic v1 + "detail": [ + { + "loc": ["body"], + "msg": "field required", + "type": "value_error.missing", + }, + ], + } + ) + + +@pytest.mark.parametrize( + "path", + ["/optional-str", "/model-optional-str"], +) +def test_optional_str_missing_empty_dict(path: str): + client = TestClient(app) + response = client.post(path, json={}) + assert response.status_code == 200, response.text + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-str", "/model-optional-str"], +) +def test_optional_str(path: str): + client = TestClient(app) + response = client.post(path, json={"p": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Alias + + +@app.post("/optional-alias", operation_id="optional_alias") +async def read_optional_alias( + p: Annotated[Optional[str], Body(embed=True, alias="p_alias")] = None, +): + return {"p": p} + + +class BodyModelOptionalAlias(BaseModel): + p: Optional[str] = Field(None, alias="p_alias") + + +@app.post("/model-optional-alias", operation_id="model_optional_alias") +async def read_model_optional_alias(p: BodyModelOptionalAlias): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-alias", + marks=pytest.mark.xfail( + raises=AssertionError, + strict=False, + condition=PYDANTIC_V2, + reason="Fails only with PDv2", + ), + ), + "/model-optional-alias", + ], +) +def test_optional_str_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( + { + "properties": { + "p_alias": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Alias", + }, + }, + "title": body_model_name, + "type": "object", + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "properties": { + "p_alias": {"type": "string", "title": "P Alias"}, + }, + "title": body_model_name, + "type": "object", + } + ) + + +def test_optional_alias_missing(): + client = TestClient(app) + response = client.post("/optional-alias") + assert response.status_code == 200 + assert response.json() == {"p": None} + + +def test_model_optional_alias_missing(): + client = TestClient(app) + response = client.post("/model-optional-alias") + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "input": None, + "loc": ["body"], + "msg": "Field required", + "type": "missing", + }, + ], + } + ) | IsDict( + { + # TODO: remove when deprecating Pydantic v1 + "detail": [ + { + "loc": ["body"], + "msg": "field required", + "type": "value_error.missing", + }, + ], + } + ) + + +@pytest.mark.parametrize( + "path", + ["/optional-alias", "/model-optional-alias"], +) +def test_model_optional_alias_missing_empty_dict(path: str): + client = TestClient(app) + response = client.post(path, json={}) + assert response.status_code == 200, response.text + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-alias", "/model-optional-alias"], +) +def test_optional_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, json={"p": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-alias", "/model-optional-alias"], +) +def test_optional_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, json={"p_alias": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Validation alias + + +@app.post("/optional-validation-alias", operation_id="optional_validation_alias") +def read_optional_validation_alias( + p: Annotated[ + Optional[str], Body(embed=True, validation_alias="p_val_alias") + ] = None, +): + return {"p": p} + + +class BodyModelOptionalValidationAlias(BaseModel): + p: Optional[str] = Field(None, validation_alias="p_val_alias") + + +@app.post( + "/model-optional-validation-alias", operation_id="model_optional_validation_alias" +) +def read_model_optional_validation_alias( + p: BodyModelOptionalValidationAlias, +): + return {"p": p.p} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + ["/optional-validation-alias", "/model-optional-validation-alias"], +) +def test_optional_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( + { + "properties": { + "p_val_alias": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Val Alias", + }, + }, + "title": body_model_name, + "type": "object", + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "properties": { + "p_val_alias": {"type": "string", "title": "P Val Alias"}, + }, + "title": body_model_name, + "type": "object", + } + ) + + +@needs_pydanticv2 +def test_optional_validation_alias_missing(): + client = TestClient(app) + response = client.post("/optional-validation-alias") + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@needs_pydanticv2 +def test_model_optional_validation_alias_missing(): + client = TestClient(app) + response = client.post("/model-optional-validation-alias") + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "input": None, + "loc": ["body"], + "msg": "Field required", + "type": "missing", + }, + ], + } + ) | IsDict( + { + # TODO: remove when deprecating Pydantic v1 + "detail": [ + { + "loc": ["body"], + "msg": "field required", + "type": "value_error.missing", + }, + ], + } + ) + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + ["/optional-validation-alias", "/model-optional-validation-alias"], +) +def test_model_optional_validation_alias_missing_empty_dict(path: str): + client = TestClient(app) + response = client.post(path, json={}) + assert response.status_code == 200, response.text + assert response.json() == {"p": None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-optional-validation-alias", + ], +) +def test_optional_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, json={"p": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": None} # /optional-validation-alias fails here + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-optional-validation-alias", + ], +) +def test_optional_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, json={"p_val_alias": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": "hello"} # /optional-validation-alias fails here + + +# ===================================================================================== +# Alias and validation alias + + +@app.post( + "/optional-alias-and-validation-alias", + operation_id="optional_alias_and_validation_alias", +) +def read_optional_alias_and_validation_alias( + p: Annotated[ + Optional[str], Body(embed=True, alias="p_alias", validation_alias="p_val_alias") + ] = None, +): + return {"p": p} + + +class BodyModelOptionalAliasAndValidationAlias(BaseModel): + p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias") + + +@app.post( + "/model-optional-alias-and-validation-alias", + operation_id="model_optional_alias_and_validation_alias", +) +def read_model_optional_alias_and_validation_alias( + p: BodyModelOptionalAliasAndValidationAlias, +): + return {"p": p.p} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( + { + "properties": { + "p_val_alias": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Val Alias", + }, + }, + "title": body_model_name, + "type": "object", + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "properties": { + "p_val_alias": {"type": "string", "title": "P Val Alias"}, + }, + "title": body_model_name, + "type": "object", + } + ) + + +@needs_pydanticv2 +def test_optional_alias_and_validation_alias_missing(): + client = TestClient(app) + response = client.post("/optional-alias-and-validation-alias") + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@needs_pydanticv2 +def test_model_optional_alias_and_validation_alias_missing(): + client = TestClient(app) + response = client.post("/model-optional-alias-and-validation-alias") + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "input": None, + "loc": ["body"], + "msg": "Field required", + "type": "missing", + }, + ], + } + ) | IsDict( + { + # TODO: remove when deprecating Pydantic v1 + "detail": [ + { + "loc": ["body"], + "msg": "field required", + "type": "value_error.missing", + }, + ], + } + ) + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_model_optional_alias_and_validation_alias_missing_empty_dict(path: str): + client = TestClient(app) + response = client.post(path, json={}) + assert response.status_code == 200, response.text + assert response.json() == {"p": None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, json={"p": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, json={"p_alias": "hello"}) + assert response.status_code == 200 + assert response.json() == { + "p": None # /optional-alias-and-validation-alias fails here + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, json={"p_val_alias": "hello"}) + assert response.status_code == 200 + assert response.json() == { + "p": "hello" # /optional-alias-and-validation-alias fails here + } diff --git a/tests/test_request_params/test_body/test_required_str.py b/tests/test_request_params/test_body/test_required_str.py new file mode 100644 index 000000000..fba3fe1f6 --- /dev/null +++ b/tests/test_request_params/test_body/test_required_str.py @@ -0,0 +1,514 @@ +from typing import Any, Dict, Union + +import pytest +from dirty_equals import IsDict, IsOneOf +from fastapi import Body, FastAPI +from fastapi._compat import PYDANTIC_V2 +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field +from typing_extensions import Annotated + +from tests.utils import needs_pydanticv2 + +from .utils import get_body_model_name + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.post("/required-str", operation_id="required_str") +async def read_required_str(p: Annotated[str, Body(embed=True)]): + return {"p": p} + + +class BodyModelRequiredStr(BaseModel): + p: str + + +@app.post("/model-required-str", operation_id="model_required_str") +async def read_model_required_str(p: BodyModelRequiredStr): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/required-str", "/model-required-str"], +) +def test_required_str_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p": {"title": "P", "type": "string"}, + }, + "required": ["p"], + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize("json", [None, {}]) +@pytest.mark.parametrize( + "path", + ["/required-str", "/model-required-str"], +) +def test_required_str_missing(path: str, json: Union[Dict[str, Any], None]): + client = TestClient(app) + response = client.post(path, json=json) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": IsOneOf(["body"], ["body", "p"]), + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": IsOneOf(["body"], ["body", "p"]), + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@pytest.mark.parametrize( + "path", + ["/required-str", "/model-required-str"], +) +def test_required_str(path: str): + client = TestClient(app) + response = client.post(path, json={"p": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Alias + + +@app.post("/required-alias", operation_id="required_alias") +async def read_required_alias( + p: Annotated[str, Body(embed=True, alias="p_alias")], +): + return {"p": p} + + +class BodyModelRequiredAlias(BaseModel): + p: str = Field(alias="p_alias") + + +@app.post("/model-required-alias", operation_id="model_required_alias") +async def read_model_required_alias(p: BodyModelRequiredAlias): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-alias", + marks=pytest.mark.xfail( + raises=AssertionError, + condition=PYDANTIC_V2, + reason="Fails only with PDv2", + strict=False, + ), + ), + "/model-required-alias", + ], +) +def test_required_str_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_alias": {"title": "P Alias", "type": "string"}, + }, + "required": ["p_alias"], + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize("json", [None, {}]) +@pytest.mark.parametrize( + "path", + ["/required-alias", "/model-required-alias"], +) +def test_required_alias_missing(path: str, json: Union[Dict[str, Any], None]): + client = TestClient(app) + response = client.post(path, json=json) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": IsOneOf(["body", "p_alias"], ["body"]), + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": IsOneOf(["body", "p_alias"], ["body"]), + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@pytest.mark.parametrize( + "path", + ["/required-alias", "/model-required-alias"], +) +def test_required_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, json={"p": "hello"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, {"p": "hello"}), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": IsOneOf(["body", "p_alias"], ["body"]), + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@pytest.mark.parametrize( + "path", + ["/required-alias", "/model-required-alias"], +) +def test_required_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, json={"p_alias": "hello"}) + assert response.status_code == 200, response.text + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Validation alias + + +@app.post("/required-validation-alias", operation_id="required_validation_alias") +def read_required_validation_alias( + p: Annotated[str, Body(embed=True, validation_alias="p_val_alias")], +): + return {"p": p} + + +class BodyModelRequiredValidationAlias(BaseModel): + p: str = Field(validation_alias="p_val_alias") + + +@app.post( + "/model-required-validation-alias", operation_id="model_required_validation_alias" +) +def read_model_required_validation_alias( + p: BodyModelRequiredValidationAlias, +): + return {"p": p.p} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + ["/required-validation-alias", "/model-required-validation-alias"], +) +def test_required_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": {"title": "P Val Alias", "type": "string"}, + }, + "required": ["p_val_alias"], + "title": body_model_name, + "type": "object", + } + + +@needs_pydanticv2 +@pytest.mark.parametrize("json", [None, {}]) +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-validation-alias", + ], +) +def test_required_validation_alias_missing( + path: str, json: Union[Dict[str, Any], None] +): + client = TestClient(app) + response = client.post(path, json=json) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": IsOneOf( # /required-validation-alias fails here + ["body", "p_val_alias"], ["body"] + ), + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-validation-alias", + ], +) +def test_required_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, json={"p": "hello"}) + assert response.status_code == 422, ( # /required-validation-alias fails here + response.text + ) + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_val_alias"], + "msg": "Field required", + "input": IsOneOf(None, {"p": "hello"}), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-validation-alias", + ], +) +def test_required_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, json={"p_val_alias": "hello"}) + assert response.status_code == 200, ( # /required-validation-alias fails here + response.text + ) + + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Alias and validation alias + + +@app.post( + "/required-alias-and-validation-alias", + operation_id="required_alias_and_validation_alias", +) +def read_required_alias_and_validation_alias( + p: Annotated[ + str, Body(embed=True, alias="p_alias", validation_alias="p_val_alias") + ], +): + return {"p": p} + + +class BodyModelRequiredAliasAndValidationAlias(BaseModel): + p: str = Field(alias="p_alias", validation_alias="p_val_alias") + + +@app.post( + "/model-required-alias-and-validation-alias", + operation_id="model_required_alias_and_validation_alias", +) +def read_model_required_alias_and_validation_alias( + p: BodyModelRequiredAliasAndValidationAlias, +): + return {"p": p.p} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/required-alias-and-validation-alias", + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": {"title": "P Val Alias", "type": "string"}, + }, + "required": ["p_val_alias"], + "title": body_model_name, + "type": "object", + } + + +@needs_pydanticv2 +@pytest.mark.parametrize("json", [None, {}]) +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_missing( + path: str, json: Union[Dict[str, Any], None] +): + client = TestClient(app) + response = client.post(path, json=json) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": IsOneOf( # /required-alias-and-validation-alias fails here + ["body"], ["body", "p_val_alias"] + ), + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, json={"p": "hello"}) + assert response.status_code == 422 + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "body", + "p_val_alias", # /required-alias-and-validation-alias fails here + ], + "msg": "Field required", + "input": IsOneOf(None, {"p": "hello"}), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, json={"p_alias": "hello"}) + assert response.status_code == 422, ( + response.text # /required-alias-and-validation-alias fails here + ) + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_val_alias"], + "msg": "Field required", + "input": IsOneOf(None, {"p_alias": "hello"}), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, json={"p_val_alias": "hello"}) + assert response.status_code == 200, ( + response.text # /required-alias-and-validation-alias fails here + ) + + assert response.json() == {"p": "hello"} diff --git a/tests/test_request_params/test_body/utils.py b/tests/test_request_params/test_body/utils.py new file mode 100644 index 000000000..5151a82d3 --- /dev/null +++ b/tests/test_request_params/test_body/utils.py @@ -0,0 +1,7 @@ +from typing import Any, Dict + + +def get_body_model_name(openapi: Dict[str, Any], path: str) -> str: + body = openapi["paths"][path]["post"]["requestBody"] + body_schema = body["content"]["application/json"]["schema"] + return body_schema.get("$ref", "").split("/")[-1] diff --git a/tests/test_request_params/test_cookie/__init__.py b/tests/test_request_params/test_cookie/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_request_params/test_cookie/test_list.py b/tests/test_request_params/test_cookie/test_list.py new file mode 100644 index 000000000..4ae80e001 --- /dev/null +++ b/tests/test_request_params/test_cookie/test_list.py @@ -0,0 +1,3 @@ +# Currently, there is no way to pass multiple cookies with the same name. +# The only way to pass multiple values for cookie params is to serialize them using +# a comma as a delimiter, but this is not currently supported by Starlette. diff --git a/tests/test_request_params/test_cookie/test_optional_list.py b/tests/test_request_params/test_cookie/test_optional_list.py new file mode 100644 index 000000000..4ae80e001 --- /dev/null +++ b/tests/test_request_params/test_cookie/test_optional_list.py @@ -0,0 +1,3 @@ +# Currently, there is no way to pass multiple cookies with the same name. +# The only way to pass multiple values for cookie params is to serialize them using +# a comma as a delimiter, but this is not currently supported by Starlette. diff --git a/tests/test_request_params/test_cookie/test_optional_str.py b/tests/test_request_params/test_cookie/test_optional_str.py new file mode 100644 index 000000000..7298baacd --- /dev/null +++ b/tests/test_request_params/test_cookie/test_optional_str.py @@ -0,0 +1,383 @@ +from typing import Optional + +import pytest +from dirty_equals import IsDict +from fastapi import Cookie, FastAPI +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field +from typing_extensions import Annotated + +from tests.utils import needs_pydanticv2 + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.get("/optional-str") +async def read_optional_str(p: Annotated[Optional[str], Cookie()] = None): + return {"p": p} + + +class CookieModelOptionalStr(BaseModel): + p: Optional[str] = None + + +@app.get("/model-optional-str") +async def read_model_optional_str(p: Annotated[CookieModelOptionalStr, Cookie()]): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/optional-str", "/model-optional-str"], +) +def test_optional_str_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + IsDict( + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P", + }, + "name": "p", + "in": "cookie", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "required": False, + "schema": {"title": "P", "type": "string"}, + "name": "p", + "in": "cookie", + } + ) + ] + + +@pytest.mark.parametrize( + "path", + ["/optional-str", "/model-optional-str"], +) +def test_optional_str_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-str", "/model-optional-str"], +) +def test_optional_str(path: str): + client = TestClient(app) + client.cookies.set("p", "hello") + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Alias + + +@app.get("/optional-alias") +async def read_optional_alias( + p: Annotated[Optional[str], Cookie(alias="p_alias")] = None, +): + return {"p": p} + + +class CookieModelOptionalAlias(BaseModel): + p: Optional[str] = Field(None, alias="p_alias") + + +@app.get("/model-optional-alias") +async def read_model_optional_alias(p: Annotated[CookieModelOptionalAlias, Cookie()]): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/optional-alias", "/model-optional-alias"], +) +def test_optional_str_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + IsDict( + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Alias", + }, + "name": "p_alias", + "in": "cookie", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "required": False, + "schema": {"title": "P Alias", "type": "string"}, + "name": "p_alias", + "in": "cookie", + } + ) + ] + + +@pytest.mark.parametrize( + "path", + ["/optional-alias", "/model-optional-alias"], +) +def test_optional_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-alias", "/model-optional-alias"], +) +def test_optional_alias_by_name(path: str): + client = TestClient(app) + client.cookies.set("p", "hello") + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-alias", + pytest.param( + "/model-optional-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + ], +) +def test_optional_alias_by_alias(path: str): + client = TestClient(app) + client.cookies.set("p_alias", "hello") + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": "hello"} # /model-optional-alias fails here + + +# ===================================================================================== +# Validation alias + + +@app.get("/optional-validation-alias") +def read_optional_validation_alias( + p: Annotated[Optional[str], Cookie(validation_alias="p_val_alias")] = None, +): + return {"p": p} + + +class CookieModelOptionalValidationAlias(BaseModel): + p: Optional[str] = Field(None, validation_alias="p_val_alias") + + +@app.get("/model-optional-validation-alias") +def read_model_optional_validation_alias( + p: Annotated[CookieModelOptionalValidationAlias, Cookie()], +): + return {"p": p.p} + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + ["/optional-validation-alias", "/model-optional-validation-alias"], +) +def test_optional_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Val Alias", + }, + "name": "p_val_alias", + "in": "cookie", + } + ] + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + ["/optional-validation-alias", "/model-optional-validation-alias"], +) +def test_optional_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-optional-validation-alias", + ], +) +def test_optional_validation_alias_by_name(path: str): + client = TestClient(app) + client.cookies.set("p", "hello") + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-optional-validation-alias", + ], +) +def test_optional_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + client.cookies.set("p_val_alias", "hello") + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": "hello"} # /optional-validation-alias fails here + + +# ===================================================================================== +# Alias and validation alias + + +@app.get("/optional-alias-and-validation-alias") +def read_optional_alias_and_validation_alias( + p: Annotated[ + Optional[str], Cookie(alias="p_alias", validation_alias="p_val_alias") + ] = None, +): + return {"p": p} + + +class CookieModelOptionalAliasAndValidationAlias(BaseModel): + p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias") + + +@app.get("/model-optional-alias-and-validation-alias") +def read_model_optional_alias_and_validation_alias( + p: Annotated[CookieModelOptionalAliasAndValidationAlias, Cookie()], +): + return {"p": p.p} + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Val Alias", + }, + "name": "p_val_alias", + "in": "cookie", + } + ] + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + client.cookies.set("p", "hello") + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + client.cookies.set("p_alias", "hello") + response = client.get(path) + assert response.status_code == 200 + assert response.json() == { + "p": None # /optional-alias-and-validation-alias fails here + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + client.cookies.set("p_val_alias", "hello") + response = client.get(path) + assert response.status_code == 200 + assert response.json() == { + "p": "hello" # /optional-alias-and-validation-alias fails here + } diff --git a/tests/test_request_params/test_cookie/test_required_str.py b/tests/test_request_params/test_cookie/test_required_str.py new file mode 100644 index 000000000..9c1442ccb --- /dev/null +++ b/tests/test_request_params/test_cookie/test_required_str.py @@ -0,0 +1,503 @@ +import pytest +from dirty_equals import IsDict, IsOneOf +from fastapi import Cookie, FastAPI +from fastapi._compat import PYDANTIC_V2 +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field +from typing_extensions import Annotated + +from tests.utils import needs_pydanticv2 + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.get("/required-str") +async def read_required_str(p: Annotated[str, Cookie()]): + return {"p": p} + + +class CookieModelRequiredStr(BaseModel): + p: str + + +@app.get("/model-required-str") +async def read_model_required_str(p: Annotated[CookieModelRequiredStr, Cookie()]): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/required-str", "/model-required-str"], +) +def test_required_str_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": {"title": "P", "type": "string"}, + "name": "p", + "in": "cookie", + } + ] + + +@pytest.mark.parametrize( + "path", + ["/required-str", "/model-required-str"], +) +def test_required_str_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["cookie", "p"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["cookie", "p"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@pytest.mark.parametrize( + "path", + ["/required-str", "/model-required-str"], +) +def test_required_str(path: str): + client = TestClient(app) + client.cookies.set("p", "hello") + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Alias + + +@app.get("/required-alias") +async def read_required_alias(p: Annotated[str, Cookie(alias="p_alias")]): + return {"p": p} + + +class CookieModelRequiredAlias(BaseModel): + p: str = Field(alias="p_alias") + + +@app.get("/model-required-alias") +async def read_model_required_alias(p: Annotated[CookieModelRequiredAlias, Cookie()]): + return {"p": p.p} # pragma: no cover + + +@pytest.mark.parametrize( + "path", + ["/required-alias", "/model-required-alias"], +) +def test_required_str_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": {"title": "P Alias", "type": "string"}, + "name": "p_alias", + "in": "cookie", + } + ] + + +@pytest.mark.parametrize( + "path", + ["/required-alias", "/model-required-alias"], +) +def test_required_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["cookie", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["cookie", "p_alias"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@pytest.mark.parametrize( + "path", + [ + "/required-alias", + pytest.param( + "/model-required-alias", + marks=pytest.mark.xfail( + raises=AssertionError, + condition=PYDANTIC_V2, + reason="Fails only with PDv2 models", + strict=False, + ), + ), + ], +) +def test_required_alias_by_name(path: str): + client = TestClient(app) + client.cookies.set("p", "hello") + response = client.get(path) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["cookie", "p_alias"], + "msg": "Field required", + "input": IsOneOf( + None, + {"p": "hello"}, # /model-required-alias PDv2 fails here + ), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["cookie", "p_alias"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@pytest.mark.parametrize( + "path", + [ + "/required-alias", + pytest.param( + "/model-required-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + ], +) +def test_required_alias_by_alias(path: str): + client = TestClient(app) + client.cookies.set("p_alias", "hello") + response = client.get(path) + assert response.status_code == 200, ( # /model-required-alias fails here + response.text + ) + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Validation alias + + +@app.get("/required-validation-alias") +def read_required_validation_alias( + p: Annotated[str, Cookie(validation_alias="p_val_alias")], +): + return {"p": p} + + +class CookieModelRequiredValidationAlias(BaseModel): + p: str = Field(validation_alias="p_val_alias") + + +@app.get("/model-required-validation-alias") +def read_model_required_validation_alias( + p: Annotated[CookieModelRequiredValidationAlias, Cookie()], +): + return {"p": p.p} + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + ["/required-validation-alias", "/model-required-validation-alias"], +) +def test_required_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": {"title": "P Val Alias", "type": "string"}, + "name": "p_val_alias", + "in": "cookie", + } + ] + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-validation-alias", + ], +) +def test_required_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "cookie", + "p_val_alias", # /required-validation-alias fails here + ], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-validation-alias", + ], +) +def test_required_validation_alias_by_name(path: str): + client = TestClient(app) + client.cookies.set("p", "hello") + response = client.get(path) + assert response.status_code == 422, ( # /required-validation-alias fails here + response.text + ) + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["cookie", "p_val_alias"], + "msg": "Field required", + "input": IsOneOf(None, {"p": "hello"}), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-validation-alias", + ], +) +def test_required_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + client.cookies.set("p_val_alias", "hello") + response = client.get(path) + assert response.status_code == 200, ( # /required-validation-alias fails here + response.text + ) + + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Alias and validation alias + + +@app.get("/required-alias-and-validation-alias") +def read_required_alias_and_validation_alias( + p: Annotated[str, Cookie(alias="p_alias", validation_alias="p_val_alias")], +): + return {"p": p} + + +class CookieModelRequiredAliasAndValidationAlias(BaseModel): + p: str = Field(alias="p_alias", validation_alias="p_val_alias") + + +@app.get("/model-required-alias-and-validation-alias") +def read_model_required_alias_and_validation_alias( + p: Annotated[CookieModelRequiredAliasAndValidationAlias, Cookie()], +): + return {"p": p.p} + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + [ + "/required-alias-and-validation-alias", + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": {"title": "P Val Alias", "type": "string"}, + "name": "p_val_alias", + "in": "cookie", + } + ] + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "cookie", + "p_val_alias", # /required-alias-and-validation-alias fails here + ], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + [ + "/required-alias-and-validation-alias", + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + client.cookies.set("p", "hello") + response = client.get(path) + assert response.status_code == 422 + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "cookie", + "p_val_alias", # /required-alias-and-validation-alias fails here + ], + "msg": "Field required", + "input": IsOneOf( # /model-alias-and-validation-alias fails here + None, + {"p": "hello"}, + ), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + [ + "/required-alias-and-validation-alias", + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + client.cookies.set("p_alias", "hello") + response = client.get(path) + assert ( + response.status_code == 422 # /required-alias-and-validation-alias fails here + ) + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["cookie", "p_val_alias"], + "msg": "Field required", + "input": IsOneOf( # /model-alias-and-validation-alias fails here + None, + {"p_alias": "hello"}, + ), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + client.cookies.set("p_val_alias", "hello") + response = client.get(path) + assert response.status_code == 200, ( + response.text # /required-alias-and-validation-alias fails here + ) + + assert response.json() == {"p": "hello"} diff --git a/tests/test_request_params/test_file/__init__.py b/tests/test_request_params/test_file/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_request_params/test_file/test_list.py b/tests/test_request_params/test_file/test_list.py new file mode 100644 index 000000000..8722ce5ab --- /dev/null +++ b/tests/test_request_params/test_file/test_list.py @@ -0,0 +1,597 @@ +from typing import List + +import pytest +from dirty_equals import IsDict +from fastapi import FastAPI, File, UploadFile +from fastapi._compat import PYDANTIC_V2 +from fastapi.testclient import TestClient +from typing_extensions import Annotated + +from tests.utils import needs_pydanticv2 + +from .utils import get_body_model_name + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.post("/list-bytes", operation_id="list_bytes") +async def read_list_bytes(p: Annotated[List[bytes], File()]): + return {"file_size": [len(file) for file in p]} + + +@app.post("/list-uploadfile", operation_id="list_uploadfile") +async def read_list_uploadfile(p: Annotated[List[UploadFile], File()]): + return {"file_size": [file.size for file in p]} + + +@pytest.mark.parametrize( + "path", + [ + "/list-bytes", + "/list-uploadfile", + ], +) +def test_list_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p": ( + IsDict( + { + "anyOf": [ + { + "type": "array", + "items": {"type": "string", "format": "binary"}, + }, + {"type": "null"}, + ], + "title": "P", + }, + ) + | IsDict( + { + "type": "array", + "items": {"type": "string", "format": "binary"}, + "title": "P", + }, + ) + ) + }, + "required": ["p"], + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + [ + "/list-bytes", + "/list-uploadfile", + ], +) +def test_list_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "p"], + "msg": "Field required", + "input": None, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "p"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@pytest.mark.parametrize( + "path", + [ + "/list-bytes", + "/list-uploadfile", + ], +) +def test_list(path: str): + client = TestClient(app) + response = client.post(path, files=[("p", b"hello"), ("p", b"world")]) + assert response.status_code == 200 + assert response.json() == {"file_size": [5, 5]} + + +# ===================================================================================== +# Alias + + +@app.post("/list-bytes-alias", operation_id="list_bytes_alias") +async def read_list_bytes_alias(p: Annotated[List[bytes], File(alias="p_alias")]): + return {"file_size": [len(file) for file in p]} + + +@app.post("/list-uploadfile-alias", operation_id="list_uploadfile_alias") +async def read_list_uploadfile_alias( + p: Annotated[List[UploadFile], File(alias="p_alias")], +): + return {"file_size": [file.size for file in p]} + + +@pytest.mark.xfail( + raises=AssertionError, + condition=PYDANTIC_V2, + reason="Fails only with PDv2", + strict=False, +) +@pytest.mark.parametrize( + "path", + [ + "/list-bytes-alias", + "/list-uploadfile-alias", + ], +) +def test_list_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_alias": ( + IsDict( + { + "anyOf": [ + { + "type": "array", + "items": {"type": "string", "format": "binary"}, + }, + {"type": "null"}, + ], + "title": "P Alias", + }, + ) + | IsDict( + { + "type": "array", + "items": {"type": "string", "format": "binary"}, + "title": "P Alias", + }, + ) + ) + }, + "required": ["p_alias"], + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + [ + "/list-bytes-alias", + "/list-uploadfile-alias", + ], +) +def test_list_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": None, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "p_alias"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@pytest.mark.parametrize( + "path", + [ + "/list-bytes-alias", + "/list-uploadfile-alias", + ], +) +def test_list_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, files=[("p", b"hello"), ("p", b"world")]) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": None, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "p_alias"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@pytest.mark.parametrize( + "path", + [ + "/list-bytes-alias", + "/list-uploadfile-alias", + ], +) +def test_list_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, files=[("p_alias", b"hello"), ("p_alias", b"world")]) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": [5, 5]} + + +# ===================================================================================== +# Validation alias + + +@app.post("/list-bytes-validation-alias", operation_id="list_bytes_validation_alias") +def read_list_bytes_validation_alias( + p: Annotated[List[bytes], File(validation_alias="p_val_alias")], +): + return {"file_size": [len(file) for file in p]} + + +@app.post( + "/list-uploadfile-validation-alias", + operation_id="list_uploadfile_validation_alias", +) +def read_list_uploadfile_validation_alias( + p: Annotated[List[UploadFile], File(validation_alias="p_val_alias")], +): + return {"file_size": [file.size for file in p]} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/list-bytes-validation-alias", + "/list-uploadfile-validation-alias", + ], +) +def test_list_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": ( + IsDict( + { + "anyOf": [ + { + "type": "array", + "items": {"type": "string", "format": "binary"}, + }, + {"type": "null"}, + ], + "title": "P Val Alias", + }, + ) + | IsDict( + { + "type": "array", + "items": {"type": "string", "format": "binary"}, + "title": "P Val Alias", + }, + ) + ) + }, + "required": ["p_val_alias"], + "title": body_model_name, + "type": "object", + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/list-bytes-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + pytest.param( + "/list-uploadfile-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + ], +) +def test_list_validation_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ # /list-*-validation-alias fail here + "body", + "p_val_alias", + ], + "msg": "Field required", + "input": None, + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/list-bytes-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + pytest.param( + "/list-uploadfile-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + ], +) +def test_list_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, files=[("p", b"hello"), ("p", b"world")]) + assert response.status_code == 422, ( # /list-*-validation-alias fail here + response.text + ) + + assert response.json() == { # pragma: no cover + "detail": [ + { + "type": "missing", + "loc": ["body", "p_val_alias"], + "msg": "Field required", + "input": None, + } + ] + } + + +@pytest.mark.xfail(raises=AssertionError, strict=False) +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/list-bytes-validation-alias", + "/list-uploadfile-validation-alias", + ], +) +def test_list_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post( + path, files=[("p_val_alias", b"hello"), ("p_val_alias", b"world")] + ) + assert response.status_code == 200, response.text # all 2 fail here + assert response.json() == {"file_size": [5, 5]} # pragma: no cover + + +# ===================================================================================== +# Alias and validation alias + + +@app.post( + "/list-bytes-alias-and-validation-alias", + operation_id="list_bytes_alias_and_validation_alias", +) +def read_list_bytes_alias_and_validation_alias( + p: Annotated[List[bytes], File(alias="p_alias", validation_alias="p_val_alias")], +): + return {"file_size": [len(file) for file in p]} + + +@app.post( + "/list-uploadfile-alias-and-validation-alias", + operation_id="list_uploadfile_alias_and_validation_alias", +) +def read_list_uploadfile_alias_and_validation_alias( + p: Annotated[ + List[UploadFile], File(alias="p_alias", validation_alias="p_val_alias") + ], +): + return {"file_size": [file.size for file in p]} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/list-bytes-alias-and-validation-alias", + "/list-uploadfile-alias-and-validation-alias", + ], +) +def test_list_alias_and_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": ( + IsDict( + { + "anyOf": [ + { + "type": "array", + "items": {"type": "string", "format": "binary"}, + }, + {"type": "null"}, + ], + "title": "P Val Alias", + }, + ) + | IsDict( + { + "type": "array", + "items": {"type": "string", "format": "binary"}, + "title": "P Val Alias", + }, + ) + ) + }, + "required": ["p_val_alias"], + "title": body_model_name, + "type": "object", + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/list-bytes-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + pytest.param( + "/list-uploadfile-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + ], +) +def test_list_alias_and_validation_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "body", + "p_val_alias", # /list-*-alias-and-validation-alias fail here + ], + "msg": "Field required", + "input": None, + } + ] + } + + +@pytest.mark.xfail(raises=AssertionError, strict=False) +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/list-bytes-alias-and-validation-alias", + "/list-uploadfile-alias-and-validation-alias", + ], +) +def test_list_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, files=[("p", "hello"), ("p", "world")]) + assert response.status_code == 422 + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "body", + "p_val_alias", # /list-*-alias-and-validation-alias fail here + ], + "msg": "Field required", + "input": None, + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/list-bytes-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + pytest.param( + "/list-uploadfile-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + ], +) +def test_list_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, files=[("p_alias", b"hello"), ("p_alias", b"world")]) + assert response.status_code == 422, ( + response.text # /list-*-alias-and-validation-alias fails here + ) + + assert response.json() == { # pragma: no cover + "detail": [ + { + "type": "missing", + "loc": ["body", "p_val_alias"], + "msg": "Field required", + "input": None, + } + ] + } + + +@pytest.mark.xfail(raises=AssertionError, strict=False) +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/list-bytes-alias-and-validation-alias", + "/list-uploadfile-alias-and-validation-alias", + ], +) +def test_list_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post( + path, files=[("p_val_alias", b"hello"), ("p_val_alias", b"world")] + ) + assert response.status_code == 200, ( # all 2 fail here + response.text + ) + assert response.json() == {"file_size": [5, 5]} # pragma: no cover diff --git a/tests/test_request_params/test_file/test_optional.py b/tests/test_request_params/test_file/test_optional.py new file mode 100644 index 000000000..14fc0a220 --- /dev/null +++ b/tests/test_request_params/test_file/test_optional.py @@ -0,0 +1,443 @@ +from typing import Optional + +import pytest +from dirty_equals import IsDict +from fastapi import FastAPI, File, UploadFile +from fastapi._compat import PYDANTIC_V2 +from fastapi.testclient import TestClient +from typing_extensions import Annotated + +from tests.utils import needs_pydanticv2 + +from .utils import get_body_model_name + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.post("/optional-bytes", operation_id="optional_bytes") +async def read_optional_bytes(p: Annotated[Optional[bytes], File()] = None): + return {"file_size": len(p) if p else None} + + +@app.post("/optional-uploadfile", operation_id="optional_uploadfile") +async def read_optional_uploadfile(p: Annotated[Optional[UploadFile], File()] = None): + return {"file_size": p.size if p else None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-bytes", + "/optional-uploadfile", + ], +) +def test_optional_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p": ( + IsDict( + { + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + "title": "P", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "P", "type": "string", "format": "binary"} + ) + ), + }, + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + [ + "/optional-bytes", + "/optional-uploadfile", + ], +) +def test_optional_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-bytes", + "/optional-uploadfile", + ], +) +def test_optional(path: str): + client = TestClient(app) + response = client.post(path, files=[("p", b"hello")]) + assert response.status_code == 200 + assert response.json() == {"file_size": 5} + + +# ===================================================================================== +# Alias + + +@app.post("/optional-bytes-alias", operation_id="optional_bytes_alias") +async def read_optional_bytes_alias( + p: Annotated[Optional[bytes], File(alias="p_alias")] = None, +): + return {"file_size": len(p) if p else None} + + +@app.post("/optional-uploadfile-alias", operation_id="optional_uploadfile_alias") +async def read_optional_uploadfile_alias( + p: Annotated[Optional[UploadFile], File(alias="p_alias")] = None, +): + return {"file_size": p.size if p else None} + + +@pytest.mark.xfail( + raises=AssertionError, + condition=PYDANTIC_V2, + reason="Fails only with PDv2", + strict=False, +) +@pytest.mark.parametrize( + "path", + [ + "/optional-bytes-alias", + "/optional-uploadfile-alias", + ], +) +def test_optional_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_alias": ( + IsDict( + { + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + "title": "P Alias", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "P Alias", "type": "string", "format": "binary"} + ) + ), + }, + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + [ + "/optional-bytes-alias", + "/optional-uploadfile-alias", + ], +) +def test_optional_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 200 + assert response.json() == {"file_size": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-bytes-alias", + "/optional-uploadfile-alias", + ], +) +def test_optional_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, files=[("p", b"hello")]) + assert response.status_code == 200 + assert response.json() == {"file_size": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-bytes-alias", + "/optional-uploadfile-alias", + ], +) +def test_optional_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, files=[("p_alias", b"hello")]) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 5} + + +# ===================================================================================== +# Validation alias + + +@app.post( + "/optional-bytes-validation-alias", operation_id="optional_bytes_validation_alias" +) +def read_optional_bytes_validation_alias( + p: Annotated[Optional[bytes], File(validation_alias="p_val_alias")] = None, +): + return {"file_size": len(p) if p else None} + + +@app.post( + "/optional-uploadfile-validation-alias", + operation_id="optional_uploadfile_validation_alias", +) +def read_optional_uploadfile_validation_alias( + p: Annotated[Optional[UploadFile], File(validation_alias="p_val_alias")] = None, +): + return {"file_size": p.size if p else None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/optional-bytes-validation-alias", + "/optional-uploadfile-validation-alias", + ], +) +def test_optional_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": ( + IsDict( + { + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + "title": "P Val Alias", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "P Val Alias", "type": "string", "format": "binary"} + ) + ), + }, + "title": body_model_name, + "type": "object", + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/optional-bytes-validation-alias", + "/optional-uploadfile-validation-alias", + ], +) +def test_optional_validation_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 200 + assert response.json() == {"file_size": None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-bytes-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + pytest.param( + "/optional-uploadfile-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + ], +) +def test_optional_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, files=[("p", b"hello")]) + assert response.status_code == 200, response.text + assert response.json() == { # /optional-*-validation-alias fail here + "file_size": None + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-bytes-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + pytest.param( + "/optional-uploadfile-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + ], +) +def test_optional_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, files=[("p_val_alias", b"hello")]) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 5} # /optional-*-validation-alias fail here + + +# ===================================================================================== +# Alias and validation alias + + +@app.post( + "/optional-bytes-alias-and-validation-alias", + operation_id="optional_bytes_alias_and_validation_alias", +) +def read_optional_bytes_alias_and_validation_alias( + p: Annotated[ + Optional[bytes], File(alias="p_alias", validation_alias="p_val_alias") + ] = None, +): + return {"file_size": len(p) if p else None} + + +@app.post( + "/optional-uploadfile-alias-and-validation-alias", + operation_id="optional_uploadfile_alias_and_validation_alias", +) +def read_optional_uploadfile_alias_and_validation_alias( + p: Annotated[ + Optional[UploadFile], File(alias="p_alias", validation_alias="p_val_alias") + ] = None, +): + return {"file_size": p.size if p else None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/optional-bytes-alias-and-validation-alias", + "/optional-uploadfile-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": ( + IsDict( + { + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + "title": "P Val Alias", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "P Val Alias", "type": "string", "format": "binary"} + ) + ), + }, + "title": body_model_name, + "type": "object", + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/optional-bytes-alias-and-validation-alias", + "/optional-uploadfile-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 200 + assert response.json() == {"file_size": None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/optional-bytes-alias-and-validation-alias", + "/optional-uploadfile-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, files={"p": "hello"}) + assert response.status_code == 200 + assert response.json() == {"file_size": None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-bytes-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + pytest.param( + "/optional-uploadfile-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + ], +) +def test_optional_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, files=[("p_alias", b"hello")]) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-bytes-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + pytest.param( + "/optional-uploadfile-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + ], +) +def test_optional_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, files=[("p_val_alias", b"hello")]) + assert response.status_code == 200, response.text + assert response.json() == { + "file_size": 5 + } # /optional-*-alias-and-validation-alias fail here diff --git a/tests/test_request_params/test_file/test_optional_list.py b/tests/test_request_params/test_file/test_optional_list.py new file mode 100644 index 000000000..f266642a6 --- /dev/null +++ b/tests/test_request_params/test_file/test_optional_list.py @@ -0,0 +1,487 @@ +from typing import List, Optional + +import pytest +from dirty_equals import IsDict +from fastapi import FastAPI, File, UploadFile +from fastapi._compat import PYDANTIC_V2 +from fastapi.testclient import TestClient +from typing_extensions import Annotated + +from tests.utils import needs_pydanticv2 + +from .utils import get_body_model_name + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.post("/optional-list-bytes") +async def read_optional_list_bytes(p: Annotated[Optional[List[bytes]], File()] = None): + return {"file_size": [len(file) for file in p] if p else None} + + +@app.post("/optional-list-uploadfile") +async def read_optional_list_uploadfile( + p: Annotated[Optional[List[UploadFile]], File()] = None, +): + return {"file_size": [file.size for file in p] if p else None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-bytes", + "/optional-list-uploadfile", + ], +) +def test_optional_list_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p": ( + IsDict( + { + "anyOf": [ + { + "type": "array", + "items": {"type": "string", "format": "binary"}, + }, + {"type": "null"}, + ], + "title": "P", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "P", + "type": "array", + "items": {"type": "string", "format": "binary"}, + }, + ) + ), + }, + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-bytes", + "/optional-list-uploadfile", + ], +) +def test_optional_list_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": None} + + +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-list-bytes", + marks=pytest.mark.xfail( + raises=(TypeError, AssertionError), + condition=PYDANTIC_V2, + reason="Fails only with PDv2 due to #14297", + strict=False, + ), + ), + "/optional-list-uploadfile", + ], +) +def test_optional_list(path: str): + client = TestClient(app) + response = client.post(path, files=[("p", b"hello"), ("p", b"world")]) + assert response.status_code == 200 + assert response.json() == {"file_size": [5, 5]} + + +# ===================================================================================== +# Alias + + +@app.post("/optional-list-bytes-alias") +async def read_optional_list_bytes_alias( + p: Annotated[Optional[List[bytes]], File(alias="p_alias")] = None, +): + return {"file_size": [len(file) for file in p] if p else None} + + +@app.post("/optional-list-uploadfile-alias") +async def read_optional_list_uploadfile_alias( + p: Annotated[Optional[List[UploadFile]], File(alias="p_alias")] = None, +): + return {"file_size": [file.size for file in p] if p else None} + + +@pytest.mark.xfail( + raises=AssertionError, + condition=PYDANTIC_V2, + reason="Fails only with PDv2", + strict=False, +) +@pytest.mark.parametrize( + "path", + [ + "/optional-list-bytes-alias", + "/optional-list-uploadfile-alias", + ], +) +def test_optional_list_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_alias": ( + IsDict( + { + "anyOf": [ + { + "type": "array", + "items": {"type": "string", "format": "binary"}, + }, + {"type": "null"}, + ], + "title": "P Alias", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "P Alias", + "type": "array", + "items": {"type": "string", "format": "binary"}, + } + ) + ), + }, + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-bytes-alias", + "/optional-list-uploadfile-alias", + ], +) +def test_optional_list_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 200 + assert response.json() == {"file_size": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-bytes-alias", + "/optional-list-uploadfile-alias", + ], +) +def test_optional_list_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, files=[("p", b"hello"), ("p", b"world")]) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": None} + + +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-list-bytes-alias", + marks=pytest.mark.xfail( + raises=(TypeError, AssertionError), + strict=False, + condition=PYDANTIC_V2, + reason="Fails only with PDv2 model due to #14297", + ), + ), + "/optional-list-uploadfile-alias", + ], +) +def test_optional_list_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, files=[("p_alias", b"hello"), ("p_alias", b"world")]) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": [5, 5]} + + +# ===================================================================================== +# Validation alias + + +@app.post("/optional-list-bytes-validation-alias") +def read_optional_list_bytes_validation_alias( + p: Annotated[Optional[List[bytes]], File(validation_alias="p_val_alias")] = None, +): + return {"file_size": [len(file) for file in p] if p else None} + + +@app.post("/optional-list-uploadfile-validation-alias") +def read_optional_list_uploadfile_validation_alias( + p: Annotated[ + Optional[List[UploadFile]], File(validation_alias="p_val_alias") + ] = None, +): + return {"file_size": [file.size for file in p] if p else None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/optional-list-bytes-validation-alias", + "/optional-list-uploadfile-validation-alias", + ], +) +def test_optional_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": ( + IsDict( + { + "anyOf": [ + { + "type": "array", + "items": {"type": "string", "format": "binary"}, + }, + {"type": "null"}, + ], + "title": "P Val Alias", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "P Val Alias", + "type": "array", + "items": {"type": "string", "format": "binary"}, + } + ) + ), + }, + "title": body_model_name, + "type": "object", + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/optional-list-bytes-validation-alias", + "/optional-list-uploadfile-validation-alias", + ], +) +def test_optional_validation_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 200 + assert response.json() == {"file_size": None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-list-bytes-validation-alias", + marks=pytest.mark.xfail( + raises=(TypeError, AssertionError), + strict=False, + reason="Fails due to #14297", + ), + ), + pytest.param( + "/optional-list-uploadfile-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + ], +) +def test_optional_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, files=[("p", b"hello"), ("p", b"world")]) + assert response.status_code == 200, response.text + assert response.json() == { # /optional-list-uploadfile-validation-alias fails here + "file_size": None + } + + +@pytest.mark.xfail(raises=AssertionError, strict=False) +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/optional-list-bytes-validation-alias", + "/optional-list-uploadfile-validation-alias", + ], +) +def test_optional_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post( + path, files=[("p_val_alias", b"hello"), ("p_val_alias", b"world")] + ) + assert response.status_code == 200, response.text + assert response.json() == { + "file_size": [5, 5] # /optional-list-*-validation-alias fail here + } + + +# ===================================================================================== +# Alias and validation alias + + +@app.post("/optional-list-bytes-alias-and-validation-alias") +def read_optional_list_bytes_alias_and_validation_alias( + p: Annotated[ + Optional[List[bytes]], File(alias="p_alias", validation_alias="p_val_alias") + ] = None, +): + return {"file_size": [len(file) for file in p] if p else None} + + +@app.post("/optional-list-uploadfile-alias-and-validation-alias") +def read_optional_list_uploadfile_alias_and_validation_alias( + p: Annotated[ + Optional[List[UploadFile]], + File(alias="p_alias", validation_alias="p_val_alias"), + ] = None, +): + return {"file_size": [file.size for file in p] if p else None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/optional-list-bytes-alias-and-validation-alias", + "/optional-list-uploadfile-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": ( + IsDict( + { + "anyOf": [ + { + "type": "array", + "items": {"type": "string", "format": "binary"}, + }, + {"type": "null"}, + ], + "title": "P Val Alias", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "P Val Alias", + "type": "array", + "items": {"type": "string", "format": "binary"}, + } + ) + ), + }, + "title": body_model_name, + "type": "object", + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/optional-list-bytes-alias-and-validation-alias", + "/optional-list-uploadfile-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 200 + assert response.json() == {"file_size": None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/optional-list-bytes-alias-and-validation-alias", + "/optional-list-uploadfile-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, files={"p": "hello"}) + assert response.status_code == 200 + assert response.json() == {"file_size": None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-list-bytes-alias-and-validation-alias", + marks=pytest.mark.xfail( + raises=(TypeError, AssertionError), + strict=False, + reason="Fails due to #14297", + ), + ), + pytest.param( + "/optional-list-uploadfile-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + ], +) +def test_optional_list_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, files=[("p_alias", b"hello"), ("p_alias", b"world")]) + assert response.status_code == 200, response.text + assert ( # /optional-list-uploadfile-alias-and-validation-alias fails here + response.json() == {"file_size": None} + ) + + +@pytest.mark.xfail(raises=AssertionError, strict=False) +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/optional-list-bytes-alias-and-validation-alias", + "/optional-list-uploadfile-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post( + path, files=[("p_val_alias", b"hello"), ("p_val_alias", b"world")] + ) + assert response.status_code == 200, response.text + assert response.json() == { + "file_size": [5, 5] # /optional-list-*-alias-and-validation-alias fail here + } diff --git a/tests/test_request_params/test_file/test_required.py b/tests/test_request_params/test_file/test_required.py new file mode 100644 index 000000000..e50597370 --- /dev/null +++ b/tests/test_request_params/test_file/test_required.py @@ -0,0 +1,536 @@ +import pytest +from dirty_equals import IsDict +from fastapi import FastAPI, File, UploadFile +from fastapi._compat import PYDANTIC_V2 +from fastapi.testclient import TestClient +from typing_extensions import Annotated + +from tests.utils import needs_pydanticv2 + +from .utils import get_body_model_name + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.post("/required-bytes", operation_id="required_bytes") +async def read_required_bytes(p: Annotated[bytes, File()]): + return {"file_size": len(p)} + + +@app.post("/required-uploadfile", operation_id="required_uploadfile") +async def read_required_uploadfile(p: Annotated[UploadFile, File()]): + return {"file_size": p.size} + + +@pytest.mark.parametrize( + "path", + [ + "/required-bytes", + "/required-uploadfile", + ], +) +def test_required_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p": {"title": "P", "type": "string", "format": "binary"}, + }, + "required": ["p"], + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-bytes", + "/required-uploadfile", + ], +) +def test_required_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "p"], + "msg": "Field required", + "input": None, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "p"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@pytest.mark.parametrize( + "path", + [ + "/required-bytes", + "/required-uploadfile", + ], +) +def test_required(path: str): + client = TestClient(app) + response = client.post(path, files=[("p", b"hello")]) + assert response.status_code == 200 + assert response.json() == {"file_size": 5} + + +# ===================================================================================== +# Alias + + +@app.post("/required-bytes-alias", operation_id="required_bytes_alias") +async def read_required_bytes_alias(p: Annotated[bytes, File(alias="p_alias")]): + return {"file_size": len(p)} + + +@app.post("/required-uploadfile-alias", operation_id="required_uploadfile_alias") +async def read_required_uploadfile_alias( + p: Annotated[UploadFile, File(alias="p_alias")], +): + return {"file_size": p.size} + + +@pytest.mark.xfail( + raises=AssertionError, + condition=PYDANTIC_V2, + reason="Fails only with PDv2", + strict=False, +) +@pytest.mark.parametrize( + "path", + [ + "/required-bytes-alias", + "/required-uploadfile-alias", + ], +) +def test_required_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_alias": {"title": "P Alias", "type": "string", "format": "binary"}, + }, + "required": ["p_alias"], + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-bytes-alias", + "/required-uploadfile-alias", + ], +) +def test_required_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": None, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "p_alias"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@pytest.mark.parametrize( + "path", + [ + "/required-bytes-alias", + "/required-uploadfile-alias", + ], +) +def test_required_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, files=[("p", b"hello")]) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": None, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "p_alias"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@pytest.mark.parametrize( + "path", + [ + "/required-bytes-alias", + "/required-uploadfile-alias", + ], +) +def test_required_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, files=[("p_alias", b"hello")]) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 5} + + +# ===================================================================================== +# Validation alias + + +@app.post( + "/required-bytes-validation-alias", operation_id="required_bytes_validation_alias" +) +def read_required_bytes_validation_alias( + p: Annotated[bytes, File(validation_alias="p_val_alias")], +): + return {"file_size": len(p)} + + +@app.post( + "/required-uploadfile-validation-alias", + operation_id="required_uploadfile_validation_alias", +) +def read_required_uploadfile_validation_alias( + p: Annotated[UploadFile, File(validation_alias="p_val_alias")], +): + return {"file_size": p.size} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/required-bytes-validation-alias", + "/required-uploadfile-validation-alias", + ], +) +def test_required_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "title": "P Val Alias", + "type": "string", + "format": "binary", + }, + }, + "required": ["p_val_alias"], + "title": body_model_name, + "type": "object", + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-bytes-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + pytest.param( + "/required-uploadfile-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + ], +) +def test_required_validation_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ # /required-*-validation-alias fail here + "body", + "p_val_alias", + ], + "msg": "Field required", + "input": None, + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-bytes-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + pytest.param( + "/required-uploadfile-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + ], +) +def test_required_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, files=[("p", b"hello")]) + assert response.status_code == 422, ( # /required-*-validation-alias fail here + response.text + ) + + assert response.json() == { # pragma: no cover + "detail": [ + { + "type": "missing", + "loc": ["body", "p_val_alias"], + "msg": "Field required", + "input": None, + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-bytes-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + pytest.param( + "/required-uploadfile-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + ], +) +def test_required_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, files=[("p_val_alias", b"hello")]) + assert response.status_code == 200, ( # all 2 fail here + response.text + ) + assert response.json() == {"file_size": 5} # pragma: no cover + + +# ===================================================================================== +# Alias and validation alias + + +@app.post( + "/required-bytes-alias-and-validation-alias", + operation_id="required_bytes_alias_and_validation_alias", +) +def read_required_bytes_alias_and_validation_alias( + p: Annotated[bytes, File(alias="p_alias", validation_alias="p_val_alias")], +): + return {"file_size": len(p)} + + +@app.post( + "/required-uploadfile-alias-and-validation-alias", + operation_id="required_uploadfile_alias_and_validation_alias", +) +def read_required_uploadfile_alias_and_validation_alias( + p: Annotated[UploadFile, File(alias="p_alias", validation_alias="p_val_alias")], +): + return {"file_size": p.size} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/required-bytes-alias-and-validation-alias", + "/required-uploadfile-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "title": "P Val Alias", + "type": "string", + "format": "binary", + }, + }, + "required": ["p_val_alias"], + "title": body_model_name, + "type": "object", + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-bytes-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + pytest.param( + "/required-uploadfile-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + ], +) +def test_required_alias_and_validation_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "body", + "p_val_alias", # /required-*-alias-and-validation-alias fail here + ], + "msg": "Field required", + "input": None, + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-bytes-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + pytest.param( + "/required-uploadfile-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + ], +) +def test_required_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, files={"p": "hello"}) + assert response.status_code == 422 + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "body", + "p_val_alias", # /required-*-alias-and-validation-alias fail here + ], + "msg": "Field required", + "input": None, + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-bytes-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + pytest.param( + "/required-uploadfile-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + ], +) +def test_required_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, files=[("p_alias", b"hello")]) + assert response.status_code == 422, ( + response.text # /required-*-alias-and-validation-alias fails here + ) + + assert response.json() == { # pragma: no cover + "detail": [ + { + "type": "missing", + "loc": ["body", "p_val_alias"], + "msg": "Field required", + "input": None, + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-bytes-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + pytest.param( + "/required-uploadfile-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + ], +) +def test_required_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, files=[("p_val_alias", b"hello")]) + assert response.status_code == 200, ( # all 2 fail here + response.text + ) + assert response.json() == {"file_size": 5} # pragma: no cover diff --git a/tests/test_request_params/test_file/utils.py b/tests/test_request_params/test_file/utils.py new file mode 100644 index 000000000..e33f64385 --- /dev/null +++ b/tests/test_request_params/test_file/utils.py @@ -0,0 +1,7 @@ +from typing import Any, Dict + + +def get_body_model_name(openapi: Dict[str, Any], path: str) -> str: + body = openapi["paths"][path]["post"]["requestBody"] + body_schema = body["content"]["multipart/form-data"]["schema"] + return body_schema.get("$ref", "").split("/")[-1] diff --git a/tests/test_request_params/test_form/__init__.py b/tests/test_request_params/test_form/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_request_params/test_form/test_list.py b/tests/test_request_params/test_form/test_list.py new file mode 100644 index 000000000..c57180f6a --- /dev/null +++ b/tests/test_request_params/test_form/test_list.py @@ -0,0 +1,527 @@ +from typing import List + +import pytest +from dirty_equals import IsDict, IsOneOf, IsPartialDict +from fastapi import FastAPI, Form +from fastapi._compat import PYDANTIC_V2 +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field +from typing_extensions import Annotated + +from tests.utils import needs_pydanticv2 + +from .utils import get_body_model_name + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.post("/required-list-str", operation_id="required_list_str") +async def read_required_list_str(p: Annotated[List[str], Form()]): + return {"p": p} + + +class FormModelRequiredListStr(BaseModel): + p: List[str] + + +@app.post("/model-required-list-str", operation_id="model_required_list_str") +def read_model_required_list_str(p: Annotated[FormModelRequiredListStr, Form()]): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/required-list-str", "/model-required-list-str"], +) +def test_required_list_str_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p": { + "items": {"type": "string"}, + "title": "P", + "type": "array", + }, + }, + "required": ["p"], + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + ["/required-list-str", "/model-required-list-str"], +) +def test_required_list_str_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "p"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + ) | IsDict( + { + "detail": [ + { + "loc": ["body", "p"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@pytest.mark.parametrize( + "path", + ["/required-list-str", "/model-required-list-str"], +) +def test_required_list_str(path: str): + client = TestClient(app) + response = client.post(path, data={"p": ["hello", "world"]}) + assert response.status_code == 200 + assert response.json() == {"p": ["hello", "world"]} + + +# ===================================================================================== +# Alias + + +@app.post("/required-list-alias", operation_id="required_list_alias") +async def read_required_list_alias(p: Annotated[List[str], Form(alias="p_alias")]): + return {"p": p} + + +class FormModelRequiredListAlias(BaseModel): + p: List[str] = Field(alias="p_alias") + + +@app.post("/model-required-list-alias", operation_id="model_required_list_alias") +async def read_model_required_list_alias( + p: Annotated[FormModelRequiredListAlias, Form()], +): + return {"p": p.p} # pragma: no cover + + +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-list-alias", + marks=pytest.mark.xfail( + raises=AssertionError, + condition=PYDANTIC_V2, + reason="Fails only with PDv2 models", + strict=False, + ), + ), + "/model-required-list-alias", + ], +) +def test_required_list_str_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_alias": { + "items": {"type": "string"}, + "title": "P Alias", + "type": "array", + }, + }, + "required": ["p_alias"], + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + ["/required-list-alias", "/model-required-list-alias"], +) +def test_required_list_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "p_alias"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias", + pytest.param( + "/model-required-list-alias", + marks=pytest.mark.xfail( + raises=AssertionError, + condition=PYDANTIC_V2, + reason="Fails only with PDv2 models", + strict=False, + ), + ), + ], +) +def test_required_list_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, data={"p": ["hello", "world"]}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": IsOneOf( # /model-required-list-alias with PDv2 fails here + None, {"p": ["hello", "world"]} + ), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "p_alias"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@pytest.mark.parametrize( + "path", + ["/required-list-alias", "/model-required-list-alias"], +) +def test_required_list_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, data={"p_alias": ["hello", "world"]}) + assert response.status_code == 200, response.text + assert response.json() == {"p": ["hello", "world"]} + + +# ===================================================================================== +# Validation alias + + +@app.post( + "/required-list-validation-alias", operation_id="required_list_validation_alias" +) +def read_required_list_validation_alias( + p: Annotated[List[str], Form(validation_alias="p_val_alias")], +): + return {"p": p} + + +class FormModelRequiredListValidationAlias(BaseModel): + p: List[str] = Field(validation_alias="p_val_alias") + + +@app.post( + "/model-required-list-validation-alias", + operation_id="model_required_list_validation_alias", +) +async def read_model_required_list_validation_alias( + p: Annotated[FormModelRequiredListValidationAlias, Form()], +): + return {"p": p.p} # pragma: no cover + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + ["/required-list-validation-alias", "/model-required-list-validation-alias"], +) +def test_required_list_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "items": {"type": "string"}, + "title": "P Val Alias", + "type": "array", + }, + }, + "required": ["p_val_alias"], + "title": body_model_name, + "type": "object", + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-list-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-list-validation-alias", + ], +) +def test_required_list_validation_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "body", + "p_val_alias", # /required-list-validation-alias fails here + ], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-list-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-list-validation-alias", + ], +) +def test_required_list_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, data={"p": ["hello", "world"]}) + assert response.status_code == 422, ( + response.text # /required-list-validation-alias fails here + ) + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_val_alias"], + "msg": "Field required", + "input": IsOneOf(None, IsPartialDict({"p": ["hello", "world"]})), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + ["/required-list-validation-alias", "/model-required-list-validation-alias"], +) +def test_required_list_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, data={"p_val_alias": ["hello", "world"]}) + assert response.status_code == 200, response.text # both fail here + + assert response.json() == {"p": ["hello", "world"]} # pragma: no cover + + +# ===================================================================================== +# Alias and validation alias + + +@app.post( + "/required-list-alias-and-validation-alias", + operation_id="required_list_alias_and_validation_alias", +) +def read_required_list_alias_and_validation_alias( + p: Annotated[List[str], Form(alias="p_alias", validation_alias="p_val_alias")], +): + return {"p": p} + + +class FormModelRequiredListAliasAndValidationAlias(BaseModel): + p: List[str] = Field(alias="p_alias", validation_alias="p_val_alias") + + +@app.post( + "/model-required-list-alias-and-validation-alias", + operation_id="model_required_list_alias_and_validation_alias", +) +def read_model_required_list_alias_and_validation_alias( + p: Annotated[FormModelRequiredListAliasAndValidationAlias, Form()], +): + return {"p": p.p} # pragma: no cover + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias-and-validation-alias", + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "items": {"type": "string"}, + "title": "P Val Alias", + "type": "array", + }, + }, + "required": ["p_val_alias"], + "title": body_model_name, + "type": "object", + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-list-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "body", + # /required-list-alias-and-validation-alias fails here + "p_val_alias", + ], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias-and-validation-alias", + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, data={"p": ["hello", "world"]}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "body", + # /required-list-alias-and-validation-alias fails here + "p_val_alias", + ], + "msg": "Field required", + "input": IsOneOf( + None, + # /model-required-list-alias-and-validation-alias fails here + {"p": ["hello", "world"]}, + ), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-list-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, data={"p_alias": ["hello", "world"]}) + assert ( # /required-list-alias-and-validation-alias fails here + response.status_code == 422 + ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_val_alias"], + "msg": "Field required", + "input": IsOneOf(None, {"p_alias": ["hello", "world"]}), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias-and-validation-alias", + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, data={"p_val_alias": ["hello", "world"]}) + assert response.status_code == 200, response.text # both fail here + assert response.json() == {"p": ["hello", "world"]} # pragma: no cover diff --git a/tests/test_request_params/test_form/test_optional_list.py b/tests/test_request_params/test_form/test_optional_list.py new file mode 100644 index 000000000..288a0cfe4 --- /dev/null +++ b/tests/test_request_params/test_form/test_optional_list.py @@ -0,0 +1,454 @@ +from typing import List, Optional + +import pytest +from dirty_equals import IsDict +from fastapi import FastAPI, Form +from fastapi._compat import PYDANTIC_V2 +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field +from typing_extensions import Annotated + +from tests.utils import needs_pydanticv2 + +from .utils import get_body_model_name + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.post("/optional-list-str", operation_id="optional_list_str") +async def read_optional_list_str( + p: Annotated[Optional[List[str]], Form()] = None, +): + return {"p": p} + + +class FormModelOptionalListStr(BaseModel): + p: Optional[List[str]] = None + + +@app.post("/model-optional-list-str", operation_id="model_optional_list_str") +async def read_model_optional_list_str(p: Annotated[FormModelOptionalListStr, Form()]): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/optional-list-str", "/model-optional-list-str"], +) +def test_optional_list_str_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( + { + "properties": { + "p": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P", + }, + }, + "title": body_model_name, + "type": "object", + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "properties": { + "p": {"items": {"type": "string"}, "type": "array", "title": "P"}, + }, + "title": body_model_name, + "type": "object", + } + ) + + +@pytest.mark.parametrize( + "path", + ["/optional-list-str", "/model-optional-list-str"], +) +def test_optional_list_str_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 200, response.text + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-list-str", "/model-optional-list-str"], +) +def test_optional_list_str(path: str): + client = TestClient(app) + response = client.post(path, data={"p": ["hello", "world"]}) + assert response.status_code == 200 + assert response.json() == {"p": ["hello", "world"]} + + +# ===================================================================================== +# Alias + + +@app.post("/optional-list-alias", operation_id="optional_list_alias") +async def read_optional_list_alias( + p: Annotated[Optional[List[str]], Form(alias="p_alias")] = None, +): + return {"p": p} + + +class FormModelOptionalListAlias(BaseModel): + p: Optional[List[str]] = Field(None, alias="p_alias") + + +@app.post("/model-optional-list-alias", operation_id="model_optional_list_alias") +async def read_model_optional_list_alias( + p: Annotated[FormModelOptionalListAlias, Form()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-list-alias", + marks=pytest.mark.xfail( + raises=AssertionError, + strict=False, + condition=PYDANTIC_V2, + reason="Fails only with PDv2", + ), + ), + "/model-optional-list-alias", + ], +) +def test_optional_list_str_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( + { + "properties": { + "p_alias": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Alias", + }, + }, + "title": body_model_name, + "type": "object", + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "properties": { + "p_alias": { + "items": {"type": "string"}, + "type": "array", + "title": "P Alias", + }, + }, + "title": body_model_name, + "type": "object", + } + ) + + +@pytest.mark.parametrize( + "path", + ["/optional-list-alias", "/model-optional-list-alias"], +) +def test_optional_list_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-list-alias", "/model-optional-list-alias"], +) +def test_optional_list_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, data={"p": ["hello", "world"]}) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-list-alias", "/model-optional-list-alias"], +) +def test_optional_list_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, data={"p_alias": ["hello", "world"]}) + assert response.status_code == 200 + assert response.json() == {"p": ["hello", "world"]} + + +# ===================================================================================== +# Validation alias + + +@app.post( + "/optional-list-validation-alias", operation_id="optional_list_validation_alias" +) +def read_optional_list_validation_alias( + p: Annotated[Optional[List[str]], Form(validation_alias="p_val_alias")] = None, +): + return {"p": p} + + +class FormModelOptionalListValidationAlias(BaseModel): + p: Optional[List[str]] = Field(None, validation_alias="p_val_alias") + + +@app.post( + "/model-optional-list-validation-alias", + operation_id="model_optional_list_validation_alias", +) +def read_model_optional_list_validation_alias( + p: Annotated[FormModelOptionalListValidationAlias, Form()], +): + return {"p": p.p} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + ["/optional-list-validation-alias", "/model-optional-list-validation-alias"], +) +def test_optional_list_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( + { + "properties": { + "p_val_alias": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Val Alias", + }, + }, + "title": body_model_name, + "type": "object", + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "properties": { + "p_val_alias": { + "items": {"type": "string"}, + "type": "array", + "title": "P Val Alias", + }, + }, + "title": body_model_name, + "type": "object", + } + ) + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + ["/optional-list-validation-alias", "/model-optional-list-validation-alias"], +) +def test_optional_list_validation_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-list-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-optional-list-validation-alias", + ], +) +def test_optional_list_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, data={"p": ["hello", "world"]}) + assert response.status_code == 200 + assert response.json() == {"p": None} # /optional-list-validation-alias fails here + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + ["/optional-list-validation-alias", "/model-optional-list-validation-alias"], +) +def test_optional_list_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, data={"p_val_alias": ["hello", "world"]}) + assert response.status_code == 200, ( + response.text # /model-optional-list-validation-alias fails here + ) + assert response.json() == { # /optional-list-validation-alias fails here + "p": ["hello", "world"] + } + + +# ===================================================================================== +# Alias and validation alias + + +@app.post( + "/optional-list-alias-and-validation-alias", + operation_id="optional_list_alias_and_validation_alias", +) +def read_optional_list_alias_and_validation_alias( + p: Annotated[ + Optional[List[str]], Form(alias="p_alias", validation_alias="p_val_alias") + ] = None, +): + return {"p": p} + + +class FormModelOptionalListAliasAndValidationAlias(BaseModel): + p: Optional[List[str]] = Field( + None, alias="p_alias", validation_alias="p_val_alias" + ) + + +@app.post( + "/model-optional-list-alias-and-validation-alias", + operation_id="model_optional_list_alias_and_validation_alias", +) +def read_model_optional_list_alias_and_validation_alias( + p: Annotated[FormModelOptionalListAliasAndValidationAlias, Form()], +): + return {"p": p.p} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias-and-validation-alias", + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( + { + "properties": { + "p_val_alias": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Val Alias", + }, + }, + "title": body_model_name, + "type": "object", + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "properties": { + "p_val_alias": { + "items": {"type": "string"}, + "type": "array", + "title": "P Val Alias", + }, + }, + "title": body_model_name, + "type": "object", + } + ) + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias-and-validation-alias", + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias-and-validation-alias", + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, data={"p": ["hello", "world"]}) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-list-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, data={"p_alias": ["hello", "world"]}) + assert response.status_code == 200 + assert response.json() == { + "p": None # /optional-list-alias-and-validation-alias fails here + } + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias-and-validation-alias", + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, data={"p_val_alias": ["hello", "world"]}) + assert response.status_code == 200, ( + response.text # /model-optional-list-alias-and-validation-alias fails here + ) + assert response.json() == { + "p": [ # /optional-list-alias-and-validation-alias fails here + "hello", + "world", + ] + } diff --git a/tests/test_request_params/test_form/test_optional_str.py b/tests/test_request_params/test_form/test_optional_str.py new file mode 100644 index 000000000..66c003a95 --- /dev/null +++ b/tests/test_request_params/test_form/test_optional_str.py @@ -0,0 +1,419 @@ +from typing import Optional + +import pytest +from dirty_equals import IsDict +from fastapi import FastAPI, Form +from fastapi._compat import PYDANTIC_V2 +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field +from typing_extensions import Annotated + +from tests.utils import needs_pydanticv2 + +from .utils import get_body_model_name + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.post("/optional-str", operation_id="optional_str") +async def read_optional_str(p: Annotated[Optional[str], Form()] = None): + return {"p": p} + + +class FormModelOptionalStr(BaseModel): + p: Optional[str] = None + + +@app.post("/model-optional-str", operation_id="model_optional_str") +async def read_model_optional_str(p: Annotated[FormModelOptionalStr, Form()]): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/optional-str", "/model-optional-str"], +) +def test_optional_str_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( + { + "properties": { + "p": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P", + }, + }, + "title": body_model_name, + "type": "object", + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "properties": { + "p": {"type": "string", "title": "P"}, + }, + "title": body_model_name, + "type": "object", + } + ) + + +@pytest.mark.parametrize( + "path", + ["/optional-str", "/model-optional-str"], +) +def test_optional_str_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-str", "/model-optional-str"], +) +def test_optional_str(path: str): + client = TestClient(app) + response = client.post(path, data={"p": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Alias + + +@app.post("/optional-alias", operation_id="optional_alias") +async def read_optional_alias( + p: Annotated[Optional[str], Form(alias="p_alias")] = None, +): + return {"p": p} + + +class FormModelOptionalAlias(BaseModel): + p: Optional[str] = Field(None, alias="p_alias") + + +@app.post("/model-optional-alias", operation_id="model_optional_alias") +async def read_model_optional_alias(p: Annotated[FormModelOptionalAlias, Form()]): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-alias", + marks=pytest.mark.xfail( + raises=AssertionError, + strict=False, + condition=PYDANTIC_V2, + reason="Fails only with PDv2", + ), + ), + "/model-optional-alias", + ], +) +def test_optional_str_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( + { + "properties": { + "p_alias": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Alias", + }, + }, + "title": body_model_name, + "type": "object", + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "properties": { + "p_alias": {"type": "string", "title": "P Alias"}, + }, + "title": body_model_name, + "type": "object", + } + ) + + +@pytest.mark.parametrize( + "path", + ["/optional-alias", "/model-optional-alias"], +) +def test_optional_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-alias", "/model-optional-alias"], +) +def test_optional_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, data={"p": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-alias", "/model-optional-alias"], +) +def test_optional_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, data={"p_alias": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Validation alias + + +@app.post("/optional-validation-alias", operation_id="optional_validation_alias") +def read_optional_validation_alias( + p: Annotated[Optional[str], Form(validation_alias="p_val_alias")] = None, +): + return {"p": p} + + +class FormModelOptionalValidationAlias(BaseModel): + p: Optional[str] = Field(None, validation_alias="p_val_alias") + + +@app.post( + "/model-optional-validation-alias", operation_id="model_optional_validation_alias" +) +def read_model_optional_validation_alias( + p: Annotated[FormModelOptionalValidationAlias, Form()], +): + return {"p": p.p} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + ["/optional-validation-alias", "/model-optional-validation-alias"], +) +def test_optional_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( + { + "properties": { + "p_val_alias": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Val Alias", + }, + }, + "title": body_model_name, + "type": "object", + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "properties": { + "p_val_alias": {"type": "string", "title": "P Val Alias"}, + }, + "title": body_model_name, + "type": "object", + } + ) + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + ["/optional-validation-alias", "/model-optional-validation-alias"], +) +def test_optional_validation_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-optional-validation-alias", + ], +) +def test_optional_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, data={"p": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": None} # /optional-validation-alias fails here + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-optional-validation-alias", + ], +) +def test_optional_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, data={"p_val_alias": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": "hello"} # /optional-validation-alias fails here + + +# ===================================================================================== +# Alias and validation alias + + +@app.post( + "/optional-alias-and-validation-alias", + operation_id="optional_alias_and_validation_alias", +) +def read_optional_alias_and_validation_alias( + p: Annotated[ + Optional[str], Form(alias="p_alias", validation_alias="p_val_alias") + ] = None, +): + return {"p": p} + + +class FormModelOptionalAliasAndValidationAlias(BaseModel): + p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias") + + +@app.post( + "/model-optional-alias-and-validation-alias", + operation_id="model_optional_alias_and_validation_alias", +) +def read_model_optional_alias_and_validation_alias( + p: Annotated[FormModelOptionalAliasAndValidationAlias, Form()], +): + return {"p": p.p} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( + { + "properties": { + "p_val_alias": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Val Alias", + }, + }, + "title": body_model_name, + "type": "object", + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "properties": { + "p_val_alias": {"type": "string", "title": "P Val Alias"}, + }, + "title": body_model_name, + "type": "object", + } + ) + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, data={"p": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, data={"p_alias": "hello"}) + assert response.status_code == 200 + assert response.json() == { + "p": None # /optional-alias-and-validation-alias fails here + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, data={"p_val_alias": "hello"}) + assert response.status_code == 200 + assert response.json() == { + "p": "hello" # /optional-alias-and-validation-alias fails here + } diff --git a/tests/test_request_params/test_form/test_required_str.py b/tests/test_request_params/test_form/test_required_str.py new file mode 100644 index 000000000..fcbce015d --- /dev/null +++ b/tests/test_request_params/test_form/test_required_str.py @@ -0,0 +1,502 @@ +import pytest +from dirty_equals import IsDict, IsOneOf +from fastapi import FastAPI, Form +from fastapi._compat import PYDANTIC_V2 +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field +from typing_extensions import Annotated + +from tests.utils import needs_pydanticv2 + +from .utils import get_body_model_name + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.post("/required-str", operation_id="required_str") +async def read_required_str(p: Annotated[str, Form()]): + return {"p": p} + + +class FormModelRequiredStr(BaseModel): + p: str + + +@app.post("/model-required-str", operation_id="model_required_str") +async def read_model_required_str(p: Annotated[FormModelRequiredStr, Form()]): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/required-str", "/model-required-str"], +) +def test_required_str_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p": {"title": "P", "type": "string"}, + }, + "required": ["p"], + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + ["/required-str", "/model-required-str"], +) +def test_required_str_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "p"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "p"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@pytest.mark.parametrize( + "path", + ["/required-str", "/model-required-str"], +) +def test_required_str(path: str): + client = TestClient(app) + response = client.post(path, data={"p": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Alias + + +@app.post("/required-alias", operation_id="required_alias") +async def read_required_alias(p: Annotated[str, Form(alias="p_alias")]): + return {"p": p} + + +class FormModelRequiredAlias(BaseModel): + p: str = Field(alias="p_alias") + + +@app.post("/model-required-alias", operation_id="model_required_alias") +async def read_model_required_alias(p: Annotated[FormModelRequiredAlias, Form()]): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-alias", + marks=pytest.mark.xfail( + raises=AssertionError, + condition=PYDANTIC_V2, + reason="Fails only with PDv2", + strict=False, + ), + ), + "/model-required-alias", + ], +) +def test_required_str_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_alias": {"title": "P Alias", "type": "string"}, + }, + "required": ["p_alias"], + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + ["/required-alias", "/model-required-alias"], +) +def test_required_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "p_alias"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@pytest.mark.parametrize( + "path", + ["/required-alias", "/model-required-alias"], +) +def test_required_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, data={"p": "hello"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, {"p": "hello"}), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "p_alias"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@pytest.mark.parametrize( + "path", + ["/required-alias", "/model-required-alias"], +) +def test_required_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, data={"p_alias": "hello"}) + assert response.status_code == 200, response.text + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Validation alias + + +@app.post("/required-validation-alias", operation_id="required_validation_alias") +def read_required_validation_alias( + p: Annotated[str, Form(validation_alias="p_val_alias")], +): + return {"p": p} + + +class FormModelRequiredValidationAlias(BaseModel): + p: str = Field(validation_alias="p_val_alias") + + +@app.post( + "/model-required-validation-alias", operation_id="model_required_validation_alias" +) +def read_model_required_validation_alias( + p: Annotated[FormModelRequiredValidationAlias, Form()], +): + return {"p": p.p} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + ["/required-validation-alias", "/model-required-validation-alias"], +) +def test_required_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": {"title": "P Val Alias", "type": "string"}, + }, + "required": ["p_val_alias"], + "title": body_model_name, + "type": "object", + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-validation-alias", + ], +) +def test_required_validation_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "body", + "p_val_alias", # /required-validation-alias fails here + ], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-validation-alias", + ], +) +def test_required_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, data={"p": "hello"}) + assert response.status_code == 422, ( # /required-validation-alias fails here + response.text + ) + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_val_alias"], + "msg": "Field required", + "input": IsOneOf(None, {"p": "hello"}), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-validation-alias", + ], +) +def test_required_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, data={"p_val_alias": "hello"}) + assert response.status_code == 200, ( # /required-validation-alias fails here + response.text + ) + + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Alias and validation alias + + +@app.post( + "/required-alias-and-validation-alias", + operation_id="required_alias_and_validation_alias", +) +def read_required_alias_and_validation_alias( + p: Annotated[str, Form(alias="p_alias", validation_alias="p_val_alias")], +): + return {"p": p} + + +class FormModelRequiredAliasAndValidationAlias(BaseModel): + p: str = Field(alias="p_alias", validation_alias="p_val_alias") + + +@app.post( + "/model-required-alias-and-validation-alias", + operation_id="model_required_alias_and_validation_alias", +) +def read_model_required_alias_and_validation_alias( + p: Annotated[FormModelRequiredAliasAndValidationAlias, Form()], +): + return {"p": p.p} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/required-alias-and-validation-alias", + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": {"title": "P Val Alias", "type": "string"}, + }, + "required": ["p_val_alias"], + "title": body_model_name, + "type": "object", + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "body", + "p_val_alias", # /required-alias-and-validation-alias fails here + ], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, data={"p": "hello"}) + assert response.status_code == 422 + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "body", + "p_val_alias", # /required-alias-and-validation-alias fails here + ], + "msg": "Field required", + "input": IsOneOf(None, {"p": "hello"}), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, data={"p_alias": "hello"}) + assert response.status_code == 422, ( + response.text # /required-alias-and-validation-alias fails here + ) + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_val_alias"], + "msg": "Field required", + "input": IsOneOf(None, {"p_alias": "hello"}), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, data={"p_val_alias": "hello"}) + assert response.status_code == 200, ( + response.text # /required-alias-and-validation-alias fails here + ) + + assert response.json() == {"p": "hello"} diff --git a/tests/test_request_params/test_form/utils.py b/tests/test_request_params/test_form/utils.py new file mode 100644 index 000000000..d200650df --- /dev/null +++ b/tests/test_request_params/test_form/utils.py @@ -0,0 +1,7 @@ +from typing import Any, Dict + + +def get_body_model_name(openapi: Dict[str, Any], path: str) -> str: + body = openapi["paths"][path]["post"]["requestBody"] + body_schema = body["content"]["application/x-www-form-urlencoded"]["schema"] + return body_schema.get("$ref", "").split("/")[-1] diff --git a/tests/test_request_params/test_header/__init__.py b/tests/test_request_params/test_header/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_request_params/test_header/test_list.py b/tests/test_request_params/test_header/test_list.py new file mode 100644 index 000000000..1bd3628b8 --- /dev/null +++ b/tests/test_request_params/test_header/test_list.py @@ -0,0 +1,505 @@ +from typing import List + +import pytest +from dirty_equals import AnyThing, IsDict, IsOneOf, IsPartialDict +from fastapi import FastAPI, Header +from fastapi._compat import PYDANTIC_V2 +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field +from typing_extensions import Annotated + +from tests.utils import needs_pydanticv2 + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.get("/required-list-str") +async def read_required_list_str(p: Annotated[List[str], Header()]): + return {"p": p} + + +class HeaderModelRequiredListStr(BaseModel): + p: List[str] + + +@app.get("/model-required-list-str") +def read_model_required_list_str(p: Annotated[HeaderModelRequiredListStr, Header()]): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/required-list-str", "/model-required-list-str"], +) +def test_required_list_str_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": { + "title": "P", + "type": "array", + "items": {"type": "string"}, + }, + "name": "p", + "in": "header", + } + ] + + +@pytest.mark.parametrize( + "path", + ["/required-list-str", "/model-required-list-str"], +) +def test_required_list_str_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "p"], + "msg": "Field required", + "input": AnyThing, + } + ] + } + ) | IsDict( + { + "detail": [ + { + "loc": ["header", "p"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@pytest.mark.parametrize( + "path", + ["/required-list-str", "/model-required-list-str"], +) +def test_required_list_str(path: str): + client = TestClient(app) + response = client.get(path, headers=[("p", "hello"), ("p", "world")]) + assert response.status_code == 200 + assert response.json() == {"p": ["hello", "world"]} + + +# ===================================================================================== +# Alias + + +@app.get("/required-list-alias") +async def read_required_list_alias(p: Annotated[List[str], Header(alias="p_alias")]): + return {"p": p} + + +class HeaderModelRequiredListAlias(BaseModel): + p: List[str] = Field(alias="p_alias") + + +@app.get("/model-required-list-alias") +async def read_model_required_list_alias( + p: Annotated[HeaderModelRequiredListAlias, Header()], +): + return {"p": p.p} # pragma: no cover + + +@pytest.mark.parametrize( + "path", + ["/required-list-alias", "/model-required-list-alias"], +) +def test_required_list_str_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": { + "title": "P Alias", + "type": "array", + "items": {"type": "string"}, + }, + "name": "p_alias", + "in": "header", + } + ] + + +@pytest.mark.parametrize( + "path", + ["/required-list-alias", "/model-required-list-alias"], +) +def test_required_list_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "p_alias"], + "msg": "Field required", + "input": AnyThing, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "p_alias"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias", + pytest.param( + "/model-required-list-alias", + marks=pytest.mark.xfail( + raises=AssertionError, + condition=PYDANTIC_V2, + reason="Fails only with PDv2 models", + strict=False, + ), + ), + ], +) +def test_required_list_alias_by_name(path: str): + client = TestClient(app) + response = client.get(path, headers=[("p", "hello"), ("p", "world")]) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "p_alias"], + "msg": "Field required", + "input": IsOneOf( # /model-required-list-alias with PDv2 fails here + None, IsPartialDict({"p": ["hello", "world"]}) + ), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "p_alias"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias", + pytest.param( + "/model-required-list-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + ], +) +def test_required_list_alias_by_alias(path: str): + client = TestClient(app) + response = client.get(path, headers=[("p_alias", "hello"), ("p_alias", "world")]) + assert response.status_code == 200, ( # /model-required-list-alias fails here + response.text + ) + assert response.json() == {"p": ["hello", "world"]} + + +# ===================================================================================== +# Validation alias + + +@app.get("/required-list-validation-alias") +def read_required_list_validation_alias( + p: Annotated[List[str], Header(validation_alias="p_val_alias")], +): + return {"p": p} + + +class HeaderModelRequiredListValidationAlias(BaseModel): + p: List[str] = Field(validation_alias="p_val_alias") + + +@app.get("/model-required-list-validation-alias") +async def read_model_required_list_validation_alias( + p: Annotated[HeaderModelRequiredListValidationAlias, Header()], +): + return {"p": p.p} # pragma: no cover + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + ["/required-list-validation-alias", "/model-required-list-validation-alias"], +) +def test_required_list_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": { + "title": "P Val Alias", + "type": "array", + "items": {"type": "string"}, + }, + "name": "p_val_alias", + "in": "header", + } + ] + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-list-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-list-validation-alias", + ], +) +def test_required_list_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "header", + "p_val_alias", # /required-list-validation-alias fails here + ], + "msg": "Field required", + "input": AnyThing, + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-list-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-list-validation-alias", + ], +) +def test_required_list_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.get(path, headers=[("p", "hello"), ("p", "world")]) + assert response.status_code == 422 # /required-list-validation-alias fails here + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "p_val_alias"], + "msg": "Field required", + "input": IsOneOf(None, IsPartialDict({"p": ["hello", "world"]})), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + ["/required-list-validation-alias", "/model-required-list-validation-alias"], +) +def test_required_list_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.get( + path, headers=[("p_val_alias", "hello"), ("p_val_alias", "world")] + ) + assert response.status_code == 200, response.text # both fail here + + assert response.json() == {"p": ["hello", "world"]} # pragma: no cover + + +# ===================================================================================== +# Alias and validation alias + + +@app.get("/required-list-alias-and-validation-alias") +def read_required_list_alias_and_validation_alias( + p: Annotated[List[str], Header(alias="p_alias", validation_alias="p_val_alias")], +): + return {"p": p} + + +class HeaderModelRequiredListAliasAndValidationAlias(BaseModel): + p: List[str] = Field(alias="p_alias", validation_alias="p_val_alias") + + +@app.get("/model-required-list-alias-and-validation-alias") +def read_model_required_list_alias_and_validation_alias( + p: Annotated[HeaderModelRequiredListAliasAndValidationAlias, Header()], +): + return {"p": p.p} # pragma: no cover + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias-and-validation-alias", + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": { + "title": "P Val Alias", + "type": "array", + "items": {"type": "string"}, + }, + "name": "p_val_alias", + "in": "header", + } + ] + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-list-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "header", + # /required-list-alias-and-validation-alias fails here + "p_val_alias", + ], + "msg": "Field required", + "input": AnyThing, + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias-and-validation-alias", + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.get(path, headers=[("p", "hello"), ("p", "world")]) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "header", + # /required-list-alias-and-validation-alias fails here + "p_val_alias", + ], + "msg": "Field required", + "input": IsOneOf( + None, + # /model-required-list-alias-and-validation-alias fails here + IsPartialDict({"p": ["hello", "world"]}), + ), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias-and-validation-alias", + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.get(path, headers=[("p_alias", "hello"), ("p_alias", "world")]) + assert ( # /required-list-alias-and-validation-alias fails here + response.status_code == 422 + ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "p_val_alias"], + "msg": "Field required", + "input": IsOneOf( + None, + # /model-required-list-alias-and-validation-alias fails here + IsPartialDict({"p_alias": ["hello", "world"]}), + ), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias-and-validation-alias", + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.get( + path, headers=[("p_val_alias", "hello"), ("p_val_alias", "world")] + ) + assert response.status_code == 200, response.text # both fail here + assert response.json() == {"p": ["hello", "world"]} # pragma: no cover diff --git a/tests/test_request_params/test_header/test_optional_list.py b/tests/test_request_params/test_header/test_optional_list.py new file mode 100644 index 000000000..328f039ba --- /dev/null +++ b/tests/test_request_params/test_header/test_optional_list.py @@ -0,0 +1,407 @@ +from typing import List, Optional + +import pytest +from dirty_equals import IsDict +from fastapi import FastAPI, Header +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field +from typing_extensions import Annotated + +from tests.utils import needs_pydanticv2 + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.get("/optional-list-str") +async def read_optional_list_str( + p: Annotated[Optional[List[str]], Header()] = None, +): + return {"p": p} + + +class HeaderModelOptionalListStr(BaseModel): + p: Optional[List[str]] = None + + +@app.get("/model-optional-list-str") +async def read_model_optional_list_str( + p: Annotated[HeaderModelOptionalListStr, Header()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/optional-list-str", "/model-optional-list-str"], +) +def test_optional_list_str_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + IsDict( + { + "required": False, + "schema": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P", + }, + "name": "p", + "in": "header", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "required": False, + "schema": {"items": {"type": "string"}, "type": "array", "title": "P"}, + "name": "p", + "in": "header", + } + ) + ] + + +@pytest.mark.parametrize( + "path", + ["/optional-list-str", "/model-optional-list-str"], +) +def test_optional_list_str_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200, response.text + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-list-str", "/model-optional-list-str"], +) +def test_optional_list_str(path: str): + client = TestClient(app) + response = client.get(path, headers=[("p", "hello"), ("p", "world")]) + assert response.status_code == 200 + assert response.json() == {"p": ["hello", "world"]} + + +# ===================================================================================== +# Alias + + +@app.get("/optional-list-alias") +async def read_optional_list_alias( + p: Annotated[Optional[List[str]], Header(alias="p_alias")] = None, +): + return {"p": p} + + +class HeaderModelOptionalListAlias(BaseModel): + p: Optional[List[str]] = Field(None, alias="p_alias") + + +@app.get("/model-optional-list-alias") +async def read_model_optional_list_alias( + p: Annotated[HeaderModelOptionalListAlias, Header()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/optional-list-alias", "/model-optional-list-alias"], +) +def test_optional_list_str_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + IsDict( + { + "required": False, + "schema": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Alias", + }, + "name": "p_alias", + "in": "header", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "required": False, + "schema": { + "items": {"type": "string"}, + "type": "array", + "title": "P Alias", + }, + "name": "p_alias", + "in": "header", + } + ) + ] + + +@pytest.mark.parametrize( + "path", + ["/optional-list-alias", "/model-optional-list-alias"], +) +def test_optional_list_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-list-alias", "/model-optional-list-alias"], +) +def test_optional_list_alias_by_name(path: str): + client = TestClient(app) + response = client.get(path, headers=[("p", "hello"), ("p", "world")]) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias", + pytest.param( + "/model-optional-list-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + ], +) +def test_optional_list_alias_by_alias(path: str): + client = TestClient(app) + response = client.get(path, headers=[("p_alias", "hello"), ("p_alias", "world")]) + assert response.status_code == 200 + assert response.json() == { + "p": ["hello", "world"] # /model-optional-list-alias fails here + } + + +# ===================================================================================== +# Validation alias + + +@app.get("/optional-list-validation-alias") +def read_optional_list_validation_alias( + p: Annotated[Optional[List[str]], Header(validation_alias="p_val_alias")] = None, +): + return {"p": p} + + +class HeaderModelOptionalListValidationAlias(BaseModel): + p: Optional[List[str]] = Field(None, validation_alias="p_val_alias") + + +@app.get("/model-optional-list-validation-alias") +def read_model_optional_list_validation_alias( + p: Annotated[HeaderModelOptionalListValidationAlias, Header()], +): + return {"p": p.p} + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + ["/optional-list-validation-alias", "/model-optional-list-validation-alias"], +) +def test_optional_list_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": False, + "schema": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Val Alias", + }, + "name": "p_val_alias", + "in": "header", + } + ] + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + ["/optional-list-validation-alias", "/model-optional-list-validation-alias"], +) +def test_optional_list_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-list-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-optional-list-validation-alias", + ], +) +def test_optional_list_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.get(path, headers=[("p", "hello"), ("p", "world")]) + assert response.status_code == 200 + assert response.json() == {"p": None} # /optional-list-validation-alias fails here + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + ["/optional-list-validation-alias", "/model-optional-list-validation-alias"], +) +def test_optional_list_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.get( + path, headers=[("p_val_alias", "hello"), ("p_val_alias", "world")] + ) + assert response.status_code == 200, ( + response.text # /model-optional-list-validation-alias fails here + ) + assert response.json() == { # /optional-list-validation-alias fails here + "p": ["hello", "world"] + } + + +# ===================================================================================== +# Alias and validation alias + + +@app.get("/optional-list-alias-and-validation-alias") +def read_optional_list_alias_and_validation_alias( + p: Annotated[ + Optional[List[str]], Header(alias="p_alias", validation_alias="p_val_alias") + ] = None, +): + return {"p": p} + + +class HeaderModelOptionalListAliasAndValidationAlias(BaseModel): + p: Optional[List[str]] = Field( + None, alias="p_alias", validation_alias="p_val_alias" + ) + + +@app.get("/model-optional-list-alias-and-validation-alias") +def read_model_optional_list_alias_and_validation_alias( + p: Annotated[HeaderModelOptionalListAliasAndValidationAlias, Header()], +): + return {"p": p.p} + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias-and-validation-alias", + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": False, + "schema": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Val Alias", + }, + "name": "p_val_alias", + "in": "header", + } + ] + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias-and-validation-alias", + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias-and-validation-alias", + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.get(path, headers=[("p", "hello"), ("p", "world")]) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-list-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.get(path, headers=[("p_alias", "hello"), ("p_alias", "world")]) + assert response.status_code == 200 + assert response.json() == { + "p": None # /optional-list-alias-and-validation-alias fails here + } + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias-and-validation-alias", + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.get( + path, headers=[("p_val_alias", "hello"), ("p_val_alias", "world")] + ) + assert response.status_code == 200, ( + response.text # /model-optional-list-alias-and-validation-alias fails here + ) + assert response.json() == { + "p": [ # /optional-list-alias-and-validation-alias fails here + "hello", + "world", + ] + } diff --git a/tests/test_request_params/test_header/test_optional_str.py b/tests/test_request_params/test_header/test_optional_str.py new file mode 100644 index 000000000..d63e0a2b8 --- /dev/null +++ b/tests/test_request_params/test_header/test_optional_str.py @@ -0,0 +1,375 @@ +from typing import Optional + +import pytest +from dirty_equals import IsDict +from fastapi import FastAPI, Header +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field +from typing_extensions import Annotated + +from tests.utils import needs_pydanticv2 + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.get("/optional-str") +async def read_optional_str(p: Annotated[Optional[str], Header()] = None): + return {"p": p} + + +class HeaderModelOptionalStr(BaseModel): + p: Optional[str] = None + + +@app.get("/model-optional-str") +async def read_model_optional_str(p: Annotated[HeaderModelOptionalStr, Header()]): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/optional-str", "/model-optional-str"], +) +def test_optional_str_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + IsDict( + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P", + }, + "name": "p", + "in": "header", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "required": False, + "schema": {"title": "P", "type": "string"}, + "name": "p", + "in": "header", + } + ) + ] + + +@pytest.mark.parametrize( + "path", + ["/optional-str", "/model-optional-str"], +) +def test_optional_str_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-str", "/model-optional-str"], +) +def test_optional_str(path: str): + client = TestClient(app) + response = client.get(path, headers={"p": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Alias + + +@app.get("/optional-alias") +async def read_optional_alias( + p: Annotated[Optional[str], Header(alias="p_alias")] = None, +): + return {"p": p} + + +class HeaderModelOptionalAlias(BaseModel): + p: Optional[str] = Field(None, alias="p_alias") + + +@app.get("/model-optional-alias") +async def read_model_optional_alias(p: Annotated[HeaderModelOptionalAlias, Header()]): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/optional-alias", "/model-optional-alias"], +) +def test_optional_str_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + IsDict( + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Alias", + }, + "name": "p_alias", + "in": "header", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "required": False, + "schema": {"title": "P Alias", "type": "string"}, + "name": "p_alias", + "in": "header", + } + ) + ] + + +@pytest.mark.parametrize( + "path", + ["/optional-alias", "/model-optional-alias"], +) +def test_optional_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-alias", "/model-optional-alias"], +) +def test_optional_alias_by_name(path: str): + client = TestClient(app) + response = client.get(path, headers={"p": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-alias", + pytest.param( + "/model-optional-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + ], +) +def test_optional_alias_by_alias(path: str): + client = TestClient(app) + response = client.get(path, headers={"p_alias": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": "hello"} # /model-optional-alias fails here + + +# ===================================================================================== +# Validation alias + + +@app.get("/optional-validation-alias") +def read_optional_validation_alias( + p: Annotated[Optional[str], Header(validation_alias="p_val_alias")] = None, +): + return {"p": p} + + +class HeaderModelOptionalValidationAlias(BaseModel): + p: Optional[str] = Field(None, validation_alias="p_val_alias") + + +@app.get("/model-optional-validation-alias") +def read_model_optional_validation_alias( + p: Annotated[HeaderModelOptionalValidationAlias, Header()], +): + return {"p": p.p} + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + ["/optional-validation-alias", "/model-optional-validation-alias"], +) +def test_optional_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Val Alias", + }, + "name": "p_val_alias", + "in": "header", + } + ] + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + ["/optional-validation-alias", "/model-optional-validation-alias"], +) +def test_optional_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-optional-validation-alias", + ], +) +def test_optional_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.get(path, headers={"p": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": None} # /optional-validation-alias fails here + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-optional-validation-alias", + ], +) +def test_optional_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.get(path, headers={"p_val_alias": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": "hello"} # /optional-validation-alias fails here + + +# ===================================================================================== +# Alias and validation alias + + +@app.get("/optional-alias-and-validation-alias") +def read_optional_alias_and_validation_alias( + p: Annotated[ + Optional[str], Header(alias="p_alias", validation_alias="p_val_alias") + ] = None, +): + return {"p": p} + + +class HeaderModelOptionalAliasAndValidationAlias(BaseModel): + p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias") + + +@app.get("/model-optional-alias-and-validation-alias") +def read_model_optional_alias_and_validation_alias( + p: Annotated[HeaderModelOptionalAliasAndValidationAlias, Header()], +): + return {"p": p.p} + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Val Alias", + }, + "name": "p_val_alias", + "in": "header", + } + ] + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.get(path, headers={"p": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.get(path, headers={"p_alias": "hello"}) + assert response.status_code == 200 + assert response.json() == { + "p": None # /optional-alias-and-validation-alias fails here + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.get(path, headers={"p_val_alias": "hello"}) + assert response.status_code == 200 + assert response.json() == { + "p": "hello" # /optional-alias-and-validation-alias fails here + } diff --git a/tests/test_request_params/test_header/test_required_str.py b/tests/test_request_params/test_header/test_required_str.py new file mode 100644 index 000000000..6eb4fd6f6 --- /dev/null +++ b/tests/test_request_params/test_header/test_required_str.py @@ -0,0 +1,492 @@ +import pytest +from dirty_equals import AnyThing, IsDict, IsOneOf, IsPartialDict +from fastapi import FastAPI, Header +from fastapi._compat import PYDANTIC_V2 +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field +from typing_extensions import Annotated + +from tests.utils import needs_pydanticv2 + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.get("/required-str") +async def read_required_str(p: Annotated[str, Header()]): + return {"p": p} + + +class HeaderModelRequiredStr(BaseModel): + p: str + + +@app.get("/model-required-str") +async def read_model_required_str(p: Annotated[HeaderModelRequiredStr, Header()]): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/required-str", "/model-required-str"], +) +def test_required_str_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": {"title": "P", "type": "string"}, + "name": "p", + "in": "header", + } + ] + + +@pytest.mark.parametrize( + "path", + ["/required-str", "/model-required-str"], +) +def test_required_str_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "p"], + "msg": "Field required", + "input": AnyThing, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "p"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@pytest.mark.parametrize( + "path", + ["/required-str", "/model-required-str"], +) +def test_required_str(path: str): + client = TestClient(app) + response = client.get(path, headers={"p": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Alias + + +@app.get("/required-alias") +async def read_required_alias(p: Annotated[str, Header(alias="p_alias")]): + return {"p": p} + + +class HeaderModelRequiredAlias(BaseModel): + p: str = Field(alias="p_alias") + + +@app.get("/model-required-alias") +async def read_model_required_alias(p: Annotated[HeaderModelRequiredAlias, Header()]): + return {"p": p.p} # pragma: no cover + + +@pytest.mark.parametrize( + "path", + ["/required-alias", "/model-required-alias"], +) +def test_required_str_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": {"title": "P Alias", "type": "string"}, + "name": "p_alias", + "in": "header", + } + ] + + +@pytest.mark.parametrize( + "path", + ["/required-alias", "/model-required-alias"], +) +def test_required_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "p_alias"], + "msg": "Field required", + "input": AnyThing, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "p_alias"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@pytest.mark.parametrize( + "path", + [ + "/required-alias", + pytest.param( + "/model-required-alias", + marks=pytest.mark.xfail( + raises=AssertionError, + condition=PYDANTIC_V2, + reason="Fails only with PDv2 models", + strict=False, + ), + ), + ], +) +def test_required_alias_by_name(path: str): + client = TestClient(app) + response = client.get(path, headers={"p": "hello"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, IsPartialDict({"p": "hello"})), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "p_alias"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@pytest.mark.parametrize( + "path", + [ + "/required-alias", + pytest.param( + "/model-required-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + ], +) +def test_required_alias_by_alias(path: str): + client = TestClient(app) + response = client.get(path, headers={"p_alias": "hello"}) + assert response.status_code == 200, ( # /model-required-alias fails here + response.text + ) + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Validation alias + + +@app.get("/required-validation-alias") +def read_required_validation_alias( + p: Annotated[str, Header(validation_alias="p_val_alias")], +): + return {"p": p} + + +class HeaderModelRequiredValidationAlias(BaseModel): + p: str = Field(validation_alias="p_val_alias") + + +@app.get("/model-required-validation-alias") +def read_model_required_validation_alias( + p: Annotated[HeaderModelRequiredValidationAlias, Header()], +): + return {"p": p.p} + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + ["/required-validation-alias", "/model-required-validation-alias"], +) +def test_required_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": {"title": "P Val Alias", "type": "string"}, + "name": "p_val_alias", + "in": "header", + } + ] + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-validation-alias", + ], +) +def test_required_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "header", + "p_val_alias", # /required-validation-alias fails here + ], + "msg": "Field required", + "input": AnyThing, + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-validation-alias", + ], +) +def test_required_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.get(path, headers={"p": "hello"}) + assert response.status_code == 422, ( # /required-validation-alias fails here + response.text + ) + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "p_val_alias"], + "msg": "Field required", + "input": IsOneOf(None, IsPartialDict({"p": "hello"})), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-validation-alias", + ], +) +def test_required_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.get(path, headers={"p_val_alias": "hello"}) + assert response.status_code == 200, ( # /required-validation-alias fails here + response.text + ) + + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Alias and validation alias + + +@app.get("/required-alias-and-validation-alias") +def read_required_alias_and_validation_alias( + p: Annotated[str, Header(alias="p_alias", validation_alias="p_val_alias")], +): + return {"p": p} + + +class HeaderModelRequiredAliasAndValidationAlias(BaseModel): + p: str = Field(alias="p_alias", validation_alias="p_val_alias") + + +@app.get("/model-required-alias-and-validation-alias") +def read_model_required_alias_and_validation_alias( + p: Annotated[HeaderModelRequiredAliasAndValidationAlias, Header()], +): + return {"p": p.p} + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + [ + "/required-alias-and-validation-alias", + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": {"title": "P Val Alias", "type": "string"}, + "name": "p_val_alias", + "in": "header", + } + ] + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "header", + "p_val_alias", # /required-alias-and-validation-alias fails here + ], + "msg": "Field required", + "input": AnyThing, + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + [ + "/required-alias-and-validation-alias", + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.get(path, headers={"p": "hello"}) + assert response.status_code == 422 + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "header", + "p_val_alias", # /required-alias-and-validation-alias fails here + ], + "msg": "Field required", + "input": IsOneOf( # /model-alias-and-validation-alias fails here + None, + IsPartialDict({"p": "hello"}), + ), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + [ + "/required-alias-and-validation-alias", + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.get(path, headers={"p_alias": "hello"}) + assert ( + response.status_code == 422 # /required-alias-and-validation-alias fails here + ) + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "p_val_alias"], + "msg": "Field required", + "input": IsOneOf( # /model-alias-and-validation-alias fails here + None, + IsPartialDict({"p_alias": "hello"}), + ), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.get(path, headers={"p_val_alias": "hello"}) + assert response.status_code == 200, ( + response.text # /required-alias-and-validation-alias fails here + ) + + assert response.json() == {"p": "hello"} diff --git a/tests/test_request_params/test_path/__init__.py b/tests/test_request_params/test_path/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_request_params/test_path/test_list.py b/tests/test_request_params/test_path/test_list.py new file mode 100644 index 000000000..bba055d9a --- /dev/null +++ b/tests/test_request_params/test_path/test_list.py @@ -0,0 +1 @@ +# FastAPI doesn't currently support non-scalar Path parameters diff --git a/tests/test_request_params/test_path/test_optional_list.py b/tests/test_request_params/test_path/test_optional_list.py new file mode 100644 index 000000000..0719430ac --- /dev/null +++ b/tests/test_request_params/test_path/test_optional_list.py @@ -0,0 +1 @@ +# Optional Path parameters are not supported diff --git a/tests/test_request_params/test_path/test_optional_str.py b/tests/test_request_params/test_path/test_optional_str.py new file mode 100644 index 000000000..0719430ac --- /dev/null +++ b/tests/test_request_params/test_path/test_optional_str.py @@ -0,0 +1 @@ +# Optional Path parameters are not supported diff --git a/tests/test_request_params/test_path/test_required_str.py b/tests/test_request_params/test_path/test_required_str.py new file mode 100644 index 000000000..8e2e60004 --- /dev/null +++ b/tests/test_request_params/test_path/test_required_str.py @@ -0,0 +1,102 @@ +import pytest +from fastapi import FastAPI, Path +from fastapi.testclient import TestClient +from typing_extensions import Annotated + +from tests.utils import needs_pydanticv2 + +app = FastAPI() + + +@app.get("/required-str/{p}") +async def read_required_str(p: Annotated[str, Path()]): + return {"p": p} + + +@app.get("/required-alias/{p_alias}") +async def read_required_alias(p: Annotated[str, Path(alias="p_alias")]): + return {"p": p} + + +@app.get("/required-validation-alias/{p_val_alias}") +def read_required_validation_alias( + p: Annotated[str, Path(validation_alias="p_val_alias")], +): + return {"p": p} # pragma: no cover + + +@app.get("/required-alias-and-validation-alias/{p_val_alias}") +def read_required_alias_and_validation_alias( + p: Annotated[str, Path(alias="p_alias", validation_alias="p_val_alias")], +): + return {"p": p} # pragma: no cover + + +@pytest.mark.parametrize( + ("path", "expected_name", "expected_title"), + [ + pytest.param("/required-str/{p}", "p", "P", id="required-str"), + pytest.param( + "/required-alias/{p_alias}", "p_alias", "P Alias", id="required-alias" + ), + pytest.param( + "/required-validation-alias/{p_val_alias}", + "p_val_alias", + "P Val Alias", + id="required-validation-alias", + marks=( + needs_pydanticv2, + pytest.mark.xfail(raises=AssertionError, strict=False), + ), + ), + pytest.param( + "/required-alias-and-validation-alias/{p_val_alias}", + "p_val_alias", + "P Val Alias", + id="required-alias-and-validation-alias", + marks=( + needs_pydanticv2, + pytest.mark.xfail(raises=AssertionError, strict=False), + ), + ), + ], +) +def test_schema(path: str, expected_name: str, expected_title: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": {"title": expected_title, "type": "string"}, + "name": expected_name, + "in": "path", + } + ] + + +@pytest.mark.parametrize( + "path", + [ + pytest.param("/required-str", id="required-str"), + pytest.param("/required-alias", id="required-alias"), + pytest.param( + "/required-validation-alias", + id="required-validation-alias", + marks=( + needs_pydanticv2, + pytest.mark.xfail(raises=AssertionError, strict=False), + ), + ), + pytest.param( + "/required-alias-and-validation-alias", + id="required-alias-and-validation-alias", + marks=( + needs_pydanticv2, + pytest.mark.xfail(raises=AssertionError, strict=False), + ), + ), + ], +) +def test_success(path: str): + client = TestClient(app) + response = client.get(f"{path}/hello") + assert response.status_code == 200, response.text + assert response.json() == {"p": "hello"} diff --git a/tests/test_request_params/test_query/__init__.py b/tests/test_request_params/test_query/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_request_params/test_query/test_list.py b/tests/test_request_params/test_query/test_list.py new file mode 100644 index 000000000..4edd192e0 --- /dev/null +++ b/tests/test_request_params/test_query/test_list.py @@ -0,0 +1,506 @@ +from typing import List + +import pytest +from dirty_equals import IsDict, IsOneOf +from fastapi import FastAPI, Query +from fastapi._compat import PYDANTIC_V2 +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field +from typing_extensions import Annotated + +from tests.utils import needs_pydanticv2 + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.get("/required-list-str") +async def read_required_list_str(p: Annotated[List[str], Query()]): + return {"p": p} + + +class QueryModelRequiredListStr(BaseModel): + p: List[str] + + +@app.get("/model-required-list-str") +def read_model_required_list_str(p: Annotated[QueryModelRequiredListStr, Query()]): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/required-list-str", "/model-required-list-str"], +) +def test_required_list_str_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": { + "title": "P", + "type": "array", + "items": {"type": "string"}, + }, + "name": "p", + "in": "query", + } + ] + + +@pytest.mark.parametrize( + "path", + ["/required-list-str", "/model-required-list-str"], +) +def test_required_list_str_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "p"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + ) | IsDict( + { + "detail": [ + { + "loc": ["query", "p"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@pytest.mark.parametrize( + "path", + ["/required-list-str", "/model-required-list-str"], +) +def test_required_list_str(path: str): + client = TestClient(app) + response = client.get(f"{path}?p=hello&p=world") + assert response.status_code == 200 + assert response.json() == {"p": ["hello", "world"]} + + +# ===================================================================================== +# Alias + + +@app.get("/required-list-alias") +async def read_required_list_alias(p: Annotated[List[str], Query(alias="p_alias")]): + return {"p": p} + + +class QueryModelRequiredListAlias(BaseModel): + p: List[str] = Field(alias="p_alias") + + +@app.get("/model-required-list-alias") +async def read_model_required_list_alias( + p: Annotated[QueryModelRequiredListAlias, Query()], +): + return {"p": p.p} # pragma: no cover + + +@pytest.mark.parametrize( + "path", + ["/required-list-alias", "/model-required-list-alias"], +) +def test_required_list_str_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": { + "title": "P Alias", + "type": "array", + "items": {"type": "string"}, + }, + "name": "p_alias", + "in": "query", + } + ] + + +@pytest.mark.parametrize( + "path", + ["/required-list-alias", "/model-required-list-alias"], +) +def test_required_list_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "p_alias"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias", + pytest.param( + "/model-required-list-alias", + marks=pytest.mark.xfail( + raises=AssertionError, + condition=PYDANTIC_V2, + reason="Fails only with PDv2 models", + strict=False, + ), + ), + ], +) +def test_required_list_alias_by_name(path: str): + client = TestClient(app) + response = client.get(f"{path}?p=hello&p=world") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "p_alias"], + "msg": "Field required", + "input": IsOneOf( # /model-required-list-alias with PDv2 fails here + None, {"p": ["hello", "world"]} + ), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "p_alias"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias", + pytest.param( + "/model-required-list-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + ], +) +def test_required_list_alias_by_alias(path: str): + client = TestClient(app) + response = client.get(f"{path}?p_alias=hello&p_alias=world") + assert response.status_code == 200, ( # /model-required-list-alias fails here + response.text + ) + assert response.json() == {"p": ["hello", "world"]} + + +# ===================================================================================== +# Validation alias + + +@app.get("/required-list-validation-alias") +def read_required_list_validation_alias( + p: Annotated[List[str], Query(validation_alias="p_val_alias")], +): + return {"p": p} + + +class QueryModelRequiredListValidationAlias(BaseModel): + p: List[str] = Field(validation_alias="p_val_alias") + + +@app.get("/model-required-list-validation-alias") +async def read_model_required_list_validation_alias( + p: Annotated[QueryModelRequiredListValidationAlias, Query()], +): + return {"p": p.p} # pragma: no cover + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + ["/required-list-validation-alias", "/model-required-list-validation-alias"], +) +def test_required_list_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": { + "title": "P Val Alias", + "type": "array", + "items": {"type": "string"}, + }, + "name": "p_val_alias", + "in": "query", + } + ] + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-list-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-list-validation-alias", + ], +) +def test_required_list_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "query", + "p_val_alias", # /required-list-validation-alias fails here + ], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-list-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-list-validation-alias", + ], +) +def test_required_list_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.get(f"{path}?p=hello&p=world") + assert response.status_code == 422 # /required-list-validation-alias fails here + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "p_val_alias"], + "msg": "Field required", + "input": IsOneOf(None, {"p": ["hello", "world"]}), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + ["/required-list-validation-alias", "/model-required-list-validation-alias"], +) +def test_required_list_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.get(f"{path}?p_val_alias=hello&p_val_alias=world") + assert response.status_code == 200, response.text # both fail here + + assert response.json() == {"p": ["hello", "world"]} # pragma: no cover + + +# ===================================================================================== +# Alias and validation alias + + +@app.get("/required-list-alias-and-validation-alias") +def read_required_list_alias_and_validation_alias( + p: Annotated[List[str], Query(alias="p_alias", validation_alias="p_val_alias")], +): + return {"p": p} + + +class QueryModelRequiredListAliasAndValidationAlias(BaseModel): + p: List[str] = Field(alias="p_alias", validation_alias="p_val_alias") + + +@app.get("/model-required-list-alias-and-validation-alias") +def read_model_required_list_alias_and_validation_alias( + p: Annotated[QueryModelRequiredListAliasAndValidationAlias, Query()], +): + return {"p": p.p} # pragma: no cover + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias-and-validation-alias", + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": { + "title": "P Val Alias", + "type": "array", + "items": {"type": "string"}, + }, + "name": "p_val_alias", + "in": "query", + } + ] + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-list-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "query", + # /required-list-alias-and-validation-alias fails here + "p_val_alias", + ], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias-and-validation-alias", + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.get(f"{path}?p=hello&p=world") + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "query", + # /required-list-alias-and-validation-alias fails here + "p_val_alias", + ], + "msg": "Field required", + "input": IsOneOf( + None, + # /model-required-list-alias-and-validation-alias fails here + { + "p": [ + "hello", + "world", + ] + }, + ), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias-and-validation-alias", + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.get(f"{path}?p_alias=hello&p_alias=world") + assert ( # /required-list-alias-and-validation-alias fails here + response.status_code == 422 + ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "p_val_alias"], + "msg": "Field required", + "input": IsOneOf( + None, + # /model-required-list-alias-and-validation-alias fails here + {"p_alias": ["hello", "world"]}, + ), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias-and-validation-alias", + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.get(f"{path}?p_val_alias=hello&p_val_alias=world") + assert response.status_code == 200, response.text # both fail here + assert response.json() == {"p": ["hello", "world"]} # pragma: no cover diff --git a/tests/test_request_params/test_query/test_optional_list.py b/tests/test_request_params/test_query/test_optional_list.py new file mode 100644 index 000000000..76f960554 --- /dev/null +++ b/tests/test_request_params/test_query/test_optional_list.py @@ -0,0 +1,403 @@ +from typing import List, Optional + +import pytest +from dirty_equals import IsDict +from fastapi import FastAPI, Query +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field +from typing_extensions import Annotated + +from tests.utils import needs_pydanticv2 + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.get("/optional-list-str") +async def read_optional_list_str( + p: Annotated[Optional[List[str]], Query()] = None, +): + return {"p": p} + + +class QueryModelOptionalListStr(BaseModel): + p: Optional[List[str]] = None + + +@app.get("/model-optional-list-str") +async def read_model_optional_list_str( + p: Annotated[QueryModelOptionalListStr, Query()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/optional-list-str", "/model-optional-list-str"], +) +def test_optional_list_str_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + IsDict( + { + "required": False, + "schema": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P", + }, + "name": "p", + "in": "query", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "required": False, + "schema": {"items": {"type": "string"}, "type": "array", "title": "P"}, + "name": "p", + "in": "query", + } + ) + ] + + +@pytest.mark.parametrize( + "path", + ["/optional-list-str", "/model-optional-list-str"], +) +def test_optional_list_str_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200, response.text + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-list-str", "/model-optional-list-str"], +) +def test_optional_list_str(path: str): + client = TestClient(app) + response = client.get(f"{path}?p=hello&p=world") + assert response.status_code == 200 + assert response.json() == {"p": ["hello", "world"]} + + +# ===================================================================================== +# Alias + + +@app.get("/optional-list-alias") +async def read_optional_list_alias( + p: Annotated[Optional[List[str]], Query(alias="p_alias")] = None, +): + return {"p": p} + + +class QueryModelOptionalListAlias(BaseModel): + p: Optional[List[str]] = Field(None, alias="p_alias") + + +@app.get("/model-optional-list-alias") +async def read_model_optional_list_alias( + p: Annotated[QueryModelOptionalListAlias, Query()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/optional-list-alias", "/model-optional-list-alias"], +) +def test_optional_list_str_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + IsDict( + { + "required": False, + "schema": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Alias", + }, + "name": "p_alias", + "in": "query", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "required": False, + "schema": { + "items": {"type": "string"}, + "type": "array", + "title": "P Alias", + }, + "name": "p_alias", + "in": "query", + } + ) + ] + + +@pytest.mark.parametrize( + "path", + ["/optional-list-alias", "/model-optional-list-alias"], +) +def test_optional_list_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-list-alias", "/model-optional-list-alias"], +) +def test_optional_list_alias_by_name(path: str): + client = TestClient(app) + response = client.get(f"{path}?p=hello&p=world") + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias", + pytest.param( + "/model-optional-list-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + ], +) +def test_optional_list_alias_by_alias(path: str): + client = TestClient(app) + response = client.get(f"{path}?p_alias=hello&p_alias=world") + assert response.status_code == 200 + assert response.json() == { + "p": ["hello", "world"] # /model-optional-list-alias fails here + } + + +# ===================================================================================== +# Validation alias + + +@app.get("/optional-list-validation-alias") +def read_optional_list_validation_alias( + p: Annotated[Optional[List[str]], Query(validation_alias="p_val_alias")] = None, +): + return {"p": p} + + +class QueryModelOptionalListValidationAlias(BaseModel): + p: Optional[List[str]] = Field(None, validation_alias="p_val_alias") + + +@app.get("/model-optional-list-validation-alias") +def read_model_optional_list_validation_alias( + p: Annotated[QueryModelOptionalListValidationAlias, Query()], +): + return {"p": p.p} + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + ["/optional-list-validation-alias", "/model-optional-list-validation-alias"], +) +def test_optional_list_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": False, + "schema": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Val Alias", + }, + "name": "p_val_alias", + "in": "query", + } + ] + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + ["/optional-list-validation-alias", "/model-optional-list-validation-alias"], +) +def test_optional_list_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-list-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-optional-list-validation-alias", + ], +) +def test_optional_list_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.get(f"{path}?p=hello&p=world") + assert response.status_code == 200 + assert response.json() == {"p": None} # /optional-list-validation-alias fails here + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + ["/optional-list-validation-alias", "/model-optional-list-validation-alias"], +) +def test_optional_list_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.get(f"{path}?p_val_alias=hello&p_val_alias=world") + assert response.status_code == 200, ( + response.text # /model-optional-list-validation-alias fails here + ) + assert response.json() == { # /optional-list-validation-alias fails here + "p": ["hello", "world"] + } + + +# ===================================================================================== +# Alias and validation alias + + +@app.get("/optional-list-alias-and-validation-alias") +def read_optional_list_alias_and_validation_alias( + p: Annotated[ + Optional[List[str]], Query(alias="p_alias", validation_alias="p_val_alias") + ] = None, +): + return {"p": p} + + +class QueryModelOptionalListAliasAndValidationAlias(BaseModel): + p: Optional[List[str]] = Field( + None, alias="p_alias", validation_alias="p_val_alias" + ) + + +@app.get("/model-optional-list-alias-and-validation-alias") +def read_model_optional_list_alias_and_validation_alias( + p: Annotated[QueryModelOptionalListAliasAndValidationAlias, Query()], +): + return {"p": p.p} + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias-and-validation-alias", + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": False, + "schema": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Val Alias", + }, + "name": "p_val_alias", + "in": "query", + } + ] + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias-and-validation-alias", + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias-and-validation-alias", + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.get(f"{path}?p=hello&p=world") + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-list-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.get(f"{path}?p_alias=hello&p_alias=world") + assert response.status_code == 200 + assert response.json() == { + "p": None # /optional-list-alias-and-validation-alias fails here + } + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias-and-validation-alias", + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.get(f"{path}?p_val_alias=hello&p_val_alias=world") + assert response.status_code == 200, ( + response.text # /model-optional-list-alias-and-validation-alias fails here + ) + assert response.json() == { + "p": [ # /optional-list-alias-and-validation-alias fails here + "hello", + "world", + ] + } diff --git a/tests/test_request_params/test_query/test_optional_str.py b/tests/test_request_params/test_query/test_optional_str.py new file mode 100644 index 000000000..77da9bee6 --- /dev/null +++ b/tests/test_request_params/test_query/test_optional_str.py @@ -0,0 +1,375 @@ +from typing import Optional + +import pytest +from dirty_equals import IsDict +from fastapi import FastAPI, Query +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field +from typing_extensions import Annotated + +from tests.utils import needs_pydanticv2 + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.get("/optional-str") +async def read_optional_str(p: Optional[str] = None): + return {"p": p} + + +class QueryModelOptionalStr(BaseModel): + p: Optional[str] = None + + +@app.get("/model-optional-str") +async def read_model_optional_str(p: Annotated[QueryModelOptionalStr, Query()]): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/optional-str", "/model-optional-str"], +) +def test_optional_str_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + IsDict( + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P", + }, + "name": "p", + "in": "query", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "required": False, + "schema": {"title": "P", "type": "string"}, + "name": "p", + "in": "query", + } + ) + ] + + +@pytest.mark.parametrize( + "path", + ["/optional-str", "/model-optional-str"], +) +def test_optional_str_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-str", "/model-optional-str"], +) +def test_optional_str(path: str): + client = TestClient(app) + response = client.get(f"{path}?p=hello") + assert response.status_code == 200 + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Alias + + +@app.get("/optional-alias") +async def read_optional_alias( + p: Annotated[Optional[str], Query(alias="p_alias")] = None, +): + return {"p": p} + + +class QueryModelOptionalAlias(BaseModel): + p: Optional[str] = Field(None, alias="p_alias") + + +@app.get("/model-optional-alias") +async def read_model_optional_alias(p: Annotated[QueryModelOptionalAlias, Query()]): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/optional-alias", "/model-optional-alias"], +) +def test_optional_str_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + IsDict( + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Alias", + }, + "name": "p_alias", + "in": "query", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "required": False, + "schema": {"title": "P Alias", "type": "string"}, + "name": "p_alias", + "in": "query", + } + ) + ] + + +@pytest.mark.parametrize( + "path", + ["/optional-alias", "/model-optional-alias"], +) +def test_optional_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-alias", "/model-optional-alias"], +) +def test_optional_alias_by_name(path: str): + client = TestClient(app) + response = client.get(f"{path}?p=hello") + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-alias", + pytest.param( + "/model-optional-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + ], +) +def test_optional_alias_by_alias(path: str): + client = TestClient(app) + response = client.get(f"{path}?p_alias=hello") + assert response.status_code == 200 + assert response.json() == {"p": "hello"} # /model-optional-alias fails here + + +# ===================================================================================== +# Validation alias + + +@app.get("/optional-validation-alias") +def read_optional_validation_alias( + p: Annotated[Optional[str], Query(validation_alias="p_val_alias")] = None, +): + return {"p": p} + + +class QueryModelOptionalValidationAlias(BaseModel): + p: Optional[str] = Field(None, validation_alias="p_val_alias") + + +@app.get("/model-optional-validation-alias") +def read_model_optional_validation_alias( + p: Annotated[QueryModelOptionalValidationAlias, Query()], +): + return {"p": p.p} + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + ["/optional-validation-alias", "/model-optional-validation-alias"], +) +def test_optional_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Val Alias", + }, + "name": "p_val_alias", + "in": "query", + } + ] + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + ["/optional-validation-alias", "/model-optional-validation-alias"], +) +def test_optional_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-optional-validation-alias", + ], +) +def test_optional_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.get(f"{path}?p=hello") + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-optional-validation-alias", + ], +) +def test_optional_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.get(f"{path}?p_val_alias=hello") + assert response.status_code == 200 + assert response.json() == {"p": "hello"} # /optional-validation-alias fails here + + +# ===================================================================================== +# Alias and validation alias + + +@app.get("/optional-alias-and-validation-alias") +def read_optional_alias_and_validation_alias( + p: Annotated[ + Optional[str], Query(alias="p_alias", validation_alias="p_val_alias") + ] = None, +): + return {"p": p} + + +class QueryModelOptionalAliasAndValidationAlias(BaseModel): + p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias") + + +@app.get("/model-optional-alias-and-validation-alias") +def read_model_optional_alias_and_validation_alias( + p: Annotated[QueryModelOptionalAliasAndValidationAlias, Query()], +): + return {"p": p.p} + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Val Alias", + }, + "name": "p_val_alias", + "in": "query", + } + ] + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.get(f"{path}?p=hello") + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.get(f"{path}?p_alias=hello") + assert response.status_code == 200 + assert response.json() == { + "p": None # /optional-alias-and-validation-alias fails here + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/optional-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.get(f"{path}?p_val_alias=hello") + assert response.status_code == 200 + assert response.json() == { + "p": "hello" # /optional-alias-and-validation-alias fails here + } diff --git a/tests/test_request_params/test_query/test_required_str.py b/tests/test_request_params/test_query/test_required_str.py new file mode 100644 index 000000000..aa3a27683 --- /dev/null +++ b/tests/test_request_params/test_query/test_required_str.py @@ -0,0 +1,495 @@ +import pytest +from dirty_equals import IsDict, IsOneOf +from fastapi import FastAPI, Query +from fastapi._compat import PYDANTIC_V2 +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field +from typing_extensions import Annotated + +from tests.utils import needs_pydanticv2 + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.get("/required-str") +async def read_required_str(p: str): + return {"p": p} + + +class QueryModelRequiredStr(BaseModel): + p: str + + +@app.get("/model-required-str") +async def read_model_required_str(p: Annotated[QueryModelRequiredStr, Query()]): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/required-str", "/model-required-str"], +) +def test_required_str_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": {"title": "P", "type": "string"}, + "name": "p", + "in": "query", + } + ] + + +@pytest.mark.parametrize( + "path", + ["/required-str", "/model-required-str"], +) +def test_required_str_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "p"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "p"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@pytest.mark.parametrize( + "path", + ["/required-str", "/model-required-str"], +) +def test_required_str(path: str): + client = TestClient(app) + response = client.get(f"{path}?p=hello") + assert response.status_code == 200 + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Alias + + +@app.get("/required-alias") +async def read_required_alias(p: Annotated[str, Query(alias="p_alias")]): + return {"p": p} + + +class QueryModelRequiredAlias(BaseModel): + p: str = Field(alias="p_alias") + + +@app.get("/model-required-alias") +async def read_model_required_alias(p: Annotated[QueryModelRequiredAlias, Query()]): + return {"p": p.p} # pragma: no cover + + +@pytest.mark.parametrize( + "path", + ["/required-alias", "/model-required-alias"], +) +def test_required_str_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": {"title": "P Alias", "type": "string"}, + "name": "p_alias", + "in": "query", + } + ] + + +@pytest.mark.parametrize( + "path", + ["/required-alias", "/model-required-alias"], +) +def test_required_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "p_alias"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@pytest.mark.parametrize( + "path", + [ + "/required-alias", + pytest.param( + "/model-required-alias", + marks=pytest.mark.xfail( + raises=AssertionError, + condition=PYDANTIC_V2, + reason="Fails only with PDv2 models", + strict=False, + ), + ), + ], +) +def test_required_alias_by_name(path: str): + client = TestClient(app) + response = client.get(f"{path}?p=hello") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "p_alias"], + "msg": "Field required", + "input": IsOneOf( + None, + {"p": "hello"}, # /model-required-alias PDv2 fails here + ), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "p_alias"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@pytest.mark.parametrize( + "path", + [ + "/required-alias", + pytest.param( + "/model-required-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + ], +) +def test_required_alias_by_alias(path: str): + client = TestClient(app) + response = client.get(f"{path}?p_alias=hello") + assert response.status_code == 200, ( # /model-required-alias fails here + response.text + ) + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Validation alias + + +@app.get("/required-validation-alias") +def read_required_validation_alias( + p: Annotated[str, Query(validation_alias="p_val_alias")], +): + return {"p": p} + + +class QueryModelRequiredValidationAlias(BaseModel): + p: str = Field(validation_alias="p_val_alias") + + +@app.get("/model-required-validation-alias") +def read_model_required_validation_alias( + p: Annotated[QueryModelRequiredValidationAlias, Query()], +): + return {"p": p.p} + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + ["/required-validation-alias", "/model-required-validation-alias"], +) +def test_required_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": {"title": "P Val Alias", "type": "string"}, + "name": "p_val_alias", + "in": "query", + } + ] + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-validation-alias", + ], +) +def test_required_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "query", + "p_val_alias", # /required-validation-alias fails here + ], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-validation-alias", + ], +) +def test_required_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.get(f"{path}?p=hello") + assert response.status_code == 422, ( # /required-validation-alias fails here + response.text + ) + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "p_val_alias"], + "msg": "Field required", + "input": IsOneOf(None, {"p": "hello"}), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-validation-alias", + ], +) +def test_required_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.get(f"{path}?p_val_alias=hello") + assert response.status_code == 200, ( # /required-validation-alias fails here + response.text + ) + + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Alias and validation alias + + +@app.get("/required-alias-and-validation-alias") +def read_required_alias_and_validation_alias( + p: Annotated[str, Query(alias="p_alias", validation_alias="p_val_alias")], +): + return {"p": p} + + +class QueryModelRequiredAliasAndValidationAlias(BaseModel): + p: str = Field(alias="p_alias", validation_alias="p_val_alias") + + +@app.get("/model-required-alias-and-validation-alias") +def read_model_required_alias_and_validation_alias( + p: Annotated[QueryModelRequiredAliasAndValidationAlias, Query()], +): + return {"p": p.p} + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + [ + "/required-alias-and-validation-alias", + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": {"title": "P Val Alias", "type": "string"}, + "name": "p_val_alias", + "in": "query", + } + ] + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "query", + "p_val_alias", # /required-alias-and-validation-alias fails here + ], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + [ + "/required-alias-and-validation-alias", + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.get(f"{path}?p=hello") + assert response.status_code == 422 + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "query", + "p_val_alias", # /required-alias-and-validation-alias fails here + ], + "msg": "Field required", + "input": IsOneOf( # /model-alias-and-validation-alias fails here + None, + {"p": "hello"}, + ), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.xfail(raises=AssertionError, strict=False) +@pytest.mark.parametrize( + "path", + [ + "/required-alias-and-validation-alias", + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.get(f"{path}?p_alias=hello") + assert ( + response.status_code == 422 # /required-alias-and-validation-alias fails here + ) + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "p_val_alias"], + "msg": "Field required", + "input": IsOneOf( # /model-alias-and-validation-alias fails here + None, + {"p_alias": "hello"}, + ), + } + ] + } + + +@needs_pydanticv2 +@pytest.mark.parametrize( + "path", + [ + pytest.param( + "/required-alias-and-validation-alias", + marks=pytest.mark.xfail(raises=AssertionError, strict=False), + ), + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.get(f"{path}?p_val_alias=hello") + assert response.status_code == 200, ( + response.text # /required-alias-and-validation-alias fails here + ) + + assert response.json() == {"p": "hello"} From 4b905b614c84fbf4a278bdb69f4a22d52a43721e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 11 Dec 2025 16:16:13 +0000 Subject: [PATCH 254/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index a6fc2f923..5bb7698c8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Refactors + +* ✅ Add set of tests for request parameters and alias. PR [#14358](https://github.com/fastapi/fastapi/pull/14358) by [@YuriiMotov](https://github.com/YuriiMotov). + ### Docs * 📝 Tweak links format. PR [#14505](https://github.com/fastapi/fastapi/pull/14505) by [@tiangolo](https://github.com/tiangolo). From f8b216df30f4d4fd36dccf7c7e885154a2699838 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Thu, 11 Dec 2025 22:25:03 +0100 Subject: [PATCH 255/256] =?UTF-8?q?=F0=9F=8C=90=20Sync=20Russian=20docs=20?= =?UTF-8?q?(#14509)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Translate missing pages * Update outdated translations --- docs/ru/docs/_llm-test.md | 4 +- docs/ru/docs/advanced/additional-responses.md | 4 +- .../ru/docs/advanced/advanced-dependencies.md | 2 +- docs/ru/docs/advanced/behind-a-proxy.md | 10 ++- docs/ru/docs/advanced/dataclasses.md | 6 +- docs/ru/docs/advanced/openapi-callbacks.md | 8 +- .../path-operation-advanced-configuration.md | 10 +-- docs/ru/docs/advanced/response-directly.md | 2 +- docs/ru/docs/advanced/settings.md | 8 +- docs/ru/docs/deployment/cloud.md | 14 +++- docs/ru/docs/deployment/fastapicloud.md | 65 ++++++++++++++++ docs/ru/docs/deployment/index.md | 4 +- .../authentication-error-status-code.md | 17 ++++ docs/ru/docs/how-to/configure-swagger-ui.md | 2 +- .../docs/how-to/custom-request-and-route.md | 12 +-- docs/ru/docs/index.md | 62 ++++++++++++++- docs/ru/docs/project-generation.md | 4 +- docs/ru/docs/resources/index.md | 2 +- docs/ru/docs/tutorial/bigger-applications.md | 78 ++++--------------- docs/ru/docs/tutorial/cookie-param-models.md | 2 +- docs/ru/docs/tutorial/first-steps.md | 57 ++++++++++++++ docs/ru/docs/tutorial/handling-errors.md | 39 ++++------ docs/ru/docs/tutorial/security/index.md | 18 ++--- docs/ru/docs/tutorial/sql-databases.md | 6 +- docs/ru/docs/tutorial/testing.md | 54 +------------ docs/ru/docs/virtual-environments.md | 22 +++++- 26 files changed, 317 insertions(+), 195 deletions(-) create mode 100644 docs/ru/docs/deployment/fastapicloud.md create mode 100644 docs/ru/docs/how-to/authentication-error-status-code.md diff --git a/docs/ru/docs/_llm-test.md b/docs/ru/docs/_llm-test.md index 476cc1924..9a15f6bb2 100644 --- a/docs/ru/docs/_llm-test.md +++ b/docs/ru/docs/_llm-test.md @@ -15,7 +15,7 @@ Тесты: -## Фрагменты кода { #code-snippets} +## Фрагменты кода { #code-snippets } //// tab | Тест @@ -53,7 +53,7 @@ LLM, вероятно, переведёт это неправильно. Инт //// -## Кавычки во фрагментах кода { #quotes-in-code-snippets} +## Кавычки во фрагментах кода { #quotes-in-code-snippets } //// tab | Тест diff --git a/docs/ru/docs/advanced/additional-responses.md b/docs/ru/docs/advanced/additional-responses.md index c63c0c08b..1fc3715e4 100644 --- a/docs/ru/docs/advanced/additional-responses.md +++ b/docs/ru/docs/advanced/additional-responses.md @@ -175,7 +175,7 @@ Например, вы можете добавить дополнительный тип содержимого `image/png`, объявив, что ваша операция пути может возвращать JSON‑объект (с типом содержимого `application/json`) или PNG‑изображение: -{* ../../docs_src/additional_responses/tutorial002.py hl[19:24,28] *} +{* ../../docs_src/additional_responses/tutorial002_py310.py hl[17:22,26] *} /// note | Примечание @@ -237,7 +237,7 @@ new_dict = {**old_dict, "new key": "new value"} Например: -{* ../../docs_src/additional_responses/tutorial004.py hl[13:17,26] *} +{* ../../docs_src/additional_responses/tutorial004_py310.py hl[11:15,24] *} ## Дополнительная информация об ответах OpenAPI { #more-information-about-openapi-responses } diff --git a/docs/ru/docs/advanced/advanced-dependencies.md b/docs/ru/docs/advanced/advanced-dependencies.md index 339c0a363..cc6691b30 100644 --- a/docs/ru/docs/advanced/advanced-dependencies.md +++ b/docs/ru/docs/advanced/advanced-dependencies.md @@ -144,7 +144,7 @@ checker(q="somequery") ### Фоновые задачи и зависимости с `yield`, технические детали { #background-tasks-and-dependencies-with-yield-technical-details } -До FastAPI 0.106.0 вызывать исключения после `yield` было невозможно: код после `yield` в зависимостях выполнялся уже после отправки ответа, поэтому [Обработчики исключений](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} к тому моменту уже отработали. +До FastAPI 0.106.0 вызывать исключения после `yield` было невозможно: код после `yield` в зависимостях выполнялся уже после отправки ответа, поэтому [Обработчики исключений](../tutorial/handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} к тому моменту уже отработали. Так было сделано в основном для того, чтобы можно было использовать те же объекты, «отданные» зависимостями через `yield`, внутри фоновых задач, потому что код после `yield` выполнялся после завершения фоновых задач. diff --git a/docs/ru/docs/advanced/behind-a-proxy.md b/docs/ru/docs/advanced/behind-a-proxy.md index 281cb7f73..7119efe2d 100644 --- a/docs/ru/docs/advanced/behind-a-proxy.md +++ b/docs/ru/docs/advanced/behind-a-proxy.md @@ -64,7 +64,7 @@ https://mysuperapp.com/items/ /// -### Как работают пересылаемые заголовки прокси +### Как работают пересылаемые заголовки прокси { #how-proxy-forwarded-headers-work } Ниже показано, как прокси добавляет пересылаемые заголовки между клиентом и сервером приложения: @@ -443,6 +443,14 @@ $ fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1 /// +/// note | Технические детали + +Свойство `servers` в спецификации OpenAPI является необязательным. + +Если вы не укажете параметр `servers`, а `root_path` равен `/`, то свойство `servers` в сгенерированной схеме OpenAPI по умолчанию будет опущено. Это эквивалентно серверу со значением `url` равным `/`. + +/// + ### Отключить автоматическое добавление сервера из `root_path` { #disable-automatic-server-from-root-path } Если вы не хотите, чтобы FastAPI добавлял автоматический сервер, используя `root_path`, укажите параметр `root_path_in_servers=False`: diff --git a/docs/ru/docs/advanced/dataclasses.md b/docs/ru/docs/advanced/dataclasses.md index 816f74404..c37ce3023 100644 --- a/docs/ru/docs/advanced/dataclasses.md +++ b/docs/ru/docs/advanced/dataclasses.md @@ -4,7 +4,7 @@ FastAPI построен поверх **Pydantic**, и я показывал в Но FastAPI также поддерживает использование `dataclasses` тем же способом: -{* ../../docs_src/dataclasses/tutorial001.py hl[1,7:12,19:20] *} +{* ../../docs_src/dataclasses/tutorial001_py310.py hl[1,6:11,18:19] *} Это по-прежнему поддерживается благодаря **Pydantic**, так как в нём есть встроенная поддержка `dataclasses`. @@ -32,7 +32,7 @@ FastAPI построен поверх **Pydantic**, и я показывал в Вы также можете использовать `dataclasses` в параметре `response_model`: -{* ../../docs_src/dataclasses/tutorial002.py hl[1,7:13,19] *} +{* ../../docs_src/dataclasses/tutorial002_py310.py hl[1,6:12,18] *} Этот dataclass будет автоматически преобразован в Pydantic dataclass. @@ -48,7 +48,7 @@ FastAPI построен поверх **Pydantic**, и я показывал в В таком случае вы можете просто заменить стандартные `dataclasses` на `pydantic.dataclasses`, которая является полностью совместимой заменой (drop-in replacement): -{* ../../docs_src/dataclasses/tutorial003.py hl[1,5,8:11,14:17,23:25,28] *} +{* ../../docs_src/dataclasses/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *} 1. Мы по-прежнему импортируем `field` из стандартных `dataclasses`. diff --git a/docs/ru/docs/advanced/openapi-callbacks.md b/docs/ru/docs/advanced/openapi-callbacks.md index faf58370b..de7e28301 100644 --- a/docs/ru/docs/advanced/openapi-callbacks.md +++ b/docs/ru/docs/advanced/openapi-callbacks.md @@ -31,7 +31,7 @@ Эта часть вполне обычна, большая часть кода вам уже знакома: -{* ../../docs_src/openapi_callbacks/tutorial001.py hl[9:13,36:53] *} +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[7:11,34:51] *} /// tip | Совет @@ -90,7 +90,7 @@ httpx.post(callback_url, json={"description": "Invoice paid", "paid": True}) Сначала создайте новый `APIRouter`, который будет содержать один или несколько обратных вызовов. -{* ../../docs_src/openapi_callbacks/tutorial001.py hl[3,25] *} +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[1,23] *} ### Создайте *операцию пути* для обратного вызова { #create-the-callback-path-operation } @@ -101,7 +101,7 @@ httpx.post(callback_url, json={"description": "Invoice paid", "paid": True}) * Вероятно, в ней должно быть объявление тела запроса, например `body: InvoiceEvent`. * А также может быть объявление модели ответа, например `response_model=InvoiceEventReceived`. -{* ../../docs_src/openapi_callbacks/tutorial001.py hl[16:18,21:22,28:32] *} +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[14:16,19:20,26:30] *} Есть 2 основных отличия от обычной *операции пути*: @@ -169,7 +169,7 @@ https://www.external.org/events/invoices/2expen51ve Теперь используйте параметр `callbacks` в *декораторе операции пути вашего API*, чтобы передать атрибут `.routes` (это, по сути, просто `list` маршрутов/*операций пути*) из этого маршрутизатора обратных вызовов: -{* ../../docs_src/openapi_callbacks/tutorial001.py hl[35] *} +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[33] *} /// tip | Совет diff --git a/docs/ru/docs/advanced/path-operation-advanced-configuration.md b/docs/ru/docs/advanced/path-operation-advanced-configuration.md index fcb3cd47f..78a16a558 100644 --- a/docs/ru/docs/advanced/path-operation-advanced-configuration.md +++ b/docs/ru/docs/advanced/path-operation-advanced-configuration.md @@ -50,7 +50,7 @@ Эта часть не попадёт в документацию, но другие инструменты (например, Sphinx) смогут использовать остальное. -{* ../../docs_src/path_operation_advanced_configuration/tutorial004.py hl[19:29] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial004_py310.py hl[17:27] *} ## Дополнительные ответы { #additional-responses } @@ -155,13 +155,13 @@ //// tab | Pydantic v2 -{* ../../docs_src/path_operation_advanced_configuration/tutorial007.py hl[17:22, 24] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[15:20, 22] *} //// //// tab | Pydantic v1 -{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py hl[17:22, 24] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py hl[15:20, 22] *} //// @@ -179,13 +179,13 @@ //// tab | Pydantic v2 -{* ../../docs_src/path_operation_advanced_configuration/tutorial007.py hl[26:33] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[24:31] *} //// //// tab | Pydantic v1 -{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py hl[26:33] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py hl[24:31] *} //// diff --git a/docs/ru/docs/advanced/response-directly.md b/docs/ru/docs/advanced/response-directly.md index febd40ed4..3c10633e9 100644 --- a/docs/ru/docs/advanced/response-directly.md +++ b/docs/ru/docs/advanced/response-directly.md @@ -34,7 +34,7 @@ В таких случаях вы можете использовать `jsonable_encoder` для преобразования данных перед передачей их в ответ: -{* ../../docs_src/response_directly/tutorial001.py hl[6:7,21:22] *} +{* ../../docs_src/response_directly/tutorial001_py310.py hl[5:6,20:21] *} /// note | Технические детали diff --git a/docs/ru/docs/advanced/settings.md b/docs/ru/docs/advanced/settings.md index a335548c3..0ef46fb13 100644 --- a/docs/ru/docs/advanced/settings.md +++ b/docs/ru/docs/advanced/settings.md @@ -148,7 +148,7 @@ $ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" fastapi run main.p Продолжая предыдущий пример, ваш файл `config.py` может выглядеть так: -{* ../../docs_src/settings/app02/config.py hl[10] *} +{* ../../docs_src/settings/app02_an_py39/config.py hl[10] *} Обратите внимание, что теперь мы не создаем экземпляр по умолчанию `settings = Settings()`. @@ -174,7 +174,7 @@ $ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" fastapi run main.p Далее будет очень просто предоставить другой объект настроек во время тестирования, создав переопределение зависимости для `get_settings`: -{* ../../docs_src/settings/app02/test_main.py hl[9:10,13,21] *} +{* ../../docs_src/settings/app02_an_py39/test_main.py hl[9:10,13,21] *} В переопределении зависимости мы задаем новое значение `admin_email` при создании нового объекта `Settings`, а затем возвращаем этот новый объект. @@ -217,7 +217,7 @@ APP_NAME="ChimichangApp" //// tab | Pydantic v2 -{* ../../docs_src/settings/app03_an/config.py hl[9] *} +{* ../../docs_src/settings/app03_an_py39/config.py hl[9] *} /// tip | Совет @@ -229,7 +229,7 @@ APP_NAME="ChimichangApp" //// tab | Pydantic v1 -{* ../../docs_src/settings/app03_an/config_pv1.py hl[9:10] *} +{* ../../docs_src/settings/app03_an_py39/config_pv1.py hl[9:10] *} /// tip | Совет diff --git a/docs/ru/docs/deployment/cloud.md b/docs/ru/docs/deployment/cloud.md index a400d1843..955db2a15 100644 --- a/docs/ru/docs/deployment/cloud.md +++ b/docs/ru/docs/deployment/cloud.md @@ -4,11 +4,19 @@ В большинстве случаев у основных облачных провайдеров есть руководства по развертыванию FastAPI на их платформе. +## FastAPI Cloud { #fastapi-cloud } + +**FastAPI Cloud** создан тем же автором и командой, стоящими за **FastAPI**. + +Он упрощает процесс **создания образа**, **развертывания** и **доступа** к API с минимальными усилиями. + +Он переносит тот же **опыт разработчика** создания приложений с FastAPI на их **развертывание** в облаке. 🎉 + +FastAPI Cloud — основной спонсор и источник финансирования для open source проектов *FastAPI and friends*. ✨ + ## Облачные провайдеры — спонсоры { #cloud-providers-sponsors } -Некоторые облачные провайдеры ✨ [**спонсируют FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨ — это обеспечивает непрерывное и здоровое развитие FastAPI и его экосистемы. - -И это показывает их искреннюю приверженность FastAPI и его сообществу (вам): они не только хотят предоставить вам хороший сервис, но и стремятся гарантировать, что у вас будет хороший и стабильный фреймворк — FastAPI. 🙇 +Некоторые другие облачные провайдеры ✨ [**спонсируют FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨ тоже. 🙇 Возможно, вы захотите попробовать их сервисы и воспользоваться их руководствами: diff --git a/docs/ru/docs/deployment/fastapicloud.md b/docs/ru/docs/deployment/fastapicloud.md new file mode 100644 index 000000000..9e7430ecb --- /dev/null +++ b/docs/ru/docs/deployment/fastapicloud.md @@ -0,0 +1,65 @@ +# FastAPI Cloud { #fastapi-cloud } + +Вы можете развернуть своё приложение FastAPI в FastAPI Cloud одной командой, присоединяйтесь к списку ожидания, если ещё не сделали этого. 🚀 + +## Вход { #login } + +Убедитесь, что у вас уже есть аккаунт **FastAPI Cloud** (мы пригласили вас из списка ожидания 😉). + +Затем выполните вход: + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +
+ +## Деплой { #deploy } + +Теперь разверните приложение одной командой: + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +Вот и всё! Теперь вы можете открыть своё приложение по этому URL. ✨ + +## О FastAPI Cloud { #about-fastapi-cloud } + +**FastAPI Cloud** создан тем же автором и командой, что и **FastAPI**. + +Он упрощает процесс **создания образа**, **развертывания** и **доступа** к API с минимальными усилиями. + +Он переносит тот же **опыт разработчика**, что вы получаете при создании приложений на FastAPI, на их **развертывание** в облаке. 🎉 + +Он также возьмёт на себя большинство вещей, которые требуются при развертывании приложения, например: + +* HTTPS +* Репликация с автоматическим масштабированием на основе запросов +* и т.д. + +FastAPI Cloud — основной спонсор и источник финансирования open source‑проектов «FastAPI и друзья». ✨ + +## Развертывание у других облачных провайдеров { #deploy-to-other-cloud-providers } + +FastAPI — проект с открытым исходным кодом и основан на стандартах. Вы можете развернуть приложения FastAPI у любого облачного провайдера на ваш выбор. + +Следуйте руководствам вашего облачного провайдера, чтобы развернуть приложения FastAPI у них. 🤓 + +## Развертывание на собственном сервере { #deploy-your-own-server } + +Позже в этом руководстве по **развертыванию** я также расскажу все детали — чтобы вы понимали, что происходит, что нужно сделать и как развернуть приложения FastAPI самостоятельно, в том числе на собственных серверах. 🤓 diff --git a/docs/ru/docs/deployment/index.md b/docs/ru/docs/deployment/index.md index c85fa0d52..ffb77641d 100644 --- a/docs/ru/docs/deployment/index.md +++ b/docs/ru/docs/deployment/index.md @@ -12,10 +12,12 @@ ## Стратегии развёртывания { #deployment-strategies } -В зависимости от вашего конкретного случая, есть несколько способов сделать это. +Есть несколько способов сделать это, в зависимости от вашего конкретного случая и используемых вами инструментов. Вы можете **развернуть сервер** самостоятельно, используя различные инструменты. Например, можно использовать **облачный сервис**, который выполнит часть работы за вас. Также возможны и другие варианты. +Например, мы, команда, стоящая за FastAPI, создали **FastAPI Cloud**, чтобы сделать развёртывание приложений FastAPI в облаке как можно более простым и прямолинейным, с тем же удобством для разработчика, что и при работе с FastAPI. + В этом блоке я покажу вам некоторые из основных концепций, которые вы, вероятно, должны иметь в виду при развертывании приложения **FastAPI** (хотя большинство из них применимо к любому другому типу веб-приложений). В последующих разделах вы узнаете больше деталей и методов, необходимых для этого. ✨ diff --git a/docs/ru/docs/how-to/authentication-error-status-code.md b/docs/ru/docs/how-to/authentication-error-status-code.md new file mode 100644 index 000000000..5675cecc5 --- /dev/null +++ b/docs/ru/docs/how-to/authentication-error-status-code.md @@ -0,0 +1,17 @@ +# Использование старых статус-кодов ошибок аутентификации 403 { #use-old-403-authentication-error-status-codes } + +До версии FastAPI `0.122.0`, когда встроенные утилиты безопасности возвращали ошибку клиенту после неудачной аутентификации, они использовали HTTP статус-код `403 Forbidden`. + +Начиная с версии FastAPI `0.122.0`, используется более подходящий HTTP статус-код `401 Unauthorized`, и в ответе возвращается имеющий смысл HTTP-заголовок `WWW-Authenticate` в соответствии со спецификациями HTTP, RFC 7235, RFC 9110. + +Но если по какой-то причине ваши клиенты зависят от старого поведения, вы можете вернуть его, переопределив метод `make_not_authenticated_error` в ваших Security-классах. + +Например, вы можете создать подкласс `HTTPBearer`, который будет возвращать ошибку `403 Forbidden` вместо стандартной `401 Unauthorized`: + +{* ../../docs_src/authentication_error_status_code/tutorial001_an_py39.py hl[9:13] *} + +/// tip | Совет + +Обратите внимание, что функция возвращает экземпляр исключения, не вызывает его. Выброс выполняется остальным внутренним кодом. + +/// diff --git a/docs/ru/docs/how-to/configure-swagger-ui.md b/docs/ru/docs/how-to/configure-swagger-ui.md index 4793cc9db..9d104423d 100644 --- a/docs/ru/docs/how-to/configure-swagger-ui.md +++ b/docs/ru/docs/how-to/configure-swagger-ui.md @@ -40,7 +40,7 @@ FastAPI включает некоторые параметры конфигур Это включает следующие настройки по умолчанию: -{* ../../fastapi/openapi/docs.py ln[8:23] hl[17:23] *} +{* ../../fastapi/openapi/docs.py ln[9:24] hl[18:24] *} Вы можете переопределить любую из них, указав другое значение в аргументе `swagger_ui_parameters`. diff --git a/docs/ru/docs/how-to/custom-request-and-route.md b/docs/ru/docs/how-to/custom-request-and-route.md index 1b8d7f7ed..feef9670a 100644 --- a/docs/ru/docs/how-to/custom-request-and-route.md +++ b/docs/ru/docs/how-to/custom-request-and-route.md @@ -42,7 +42,7 @@ Таким образом, один и тот же класс маршрута сможет обрабатывать как gzip-сжатые, так и несжатые запросы. -{* ../../docs_src/custom_request_and_route/tutorial001.py hl[8:15] *} +{* ../../docs_src/custom_request_and_route/tutorial001_an_py310.py hl[9:16] *} ### Создать пользовательский класс `GzipRoute` { #create-a-custom-gziproute-class } @@ -54,7 +54,7 @@ Здесь мы используем её, чтобы создать `GzipRequest` из исходного HTTP-запроса. -{* ../../docs_src/custom_request_and_route/tutorial001.py hl[18:26] *} +{* ../../docs_src/custom_request_and_route/tutorial001_an_py310.py hl[19:27] *} /// note | Технические детали @@ -92,18 +92,18 @@ Нужно лишь обработать запрос внутри блока `try`/`except`: -{* ../../docs_src/custom_request_and_route/tutorial002.py hl[13,15] *} +{* ../../docs_src/custom_request_and_route/tutorial002_an_py310.py hl[14,16] *} Если произойдёт исключение, экземпляр `Request` всё ещё будет в области видимости, поэтому мы сможем прочитать тело запроса и использовать его при обработке ошибки: -{* ../../docs_src/custom_request_and_route/tutorial002.py hl[16:18] *} +{* ../../docs_src/custom_request_and_route/tutorial002_an_py310.py hl[17:19] *} ## Пользовательский класс `APIRoute` в роутере { #custom-apiroute-class-in-a-router } Вы также можете задать параметр `route_class` у `APIRouter`: -{* ../../docs_src/custom_request_and_route/tutorial003.py hl[26] *} +{* ../../docs_src/custom_request_and_route/tutorial003_py310.py hl[26] *} В этом примере *операции пути*, объявленные в `router`, будут использовать пользовательский класс `TimedRoute` и получат дополнительный HTTP-заголовок `X-Response-Time` в ответе с временем, затраченным на формирование ответа: -{* ../../docs_src/custom_request_and_route/tutorial003.py hl[13:20] *} +{* ../../docs_src/custom_request_and_route/tutorial003_py310.py hl[13:20] *} diff --git a/docs/ru/docs/index.md b/docs/ru/docs/index.md index 75cd63223..b562cbe5b 100644 --- a/docs/ru/docs/index.md +++ b/docs/ru/docs/index.md @@ -52,14 +52,20 @@ FastAPI — это современный, быстрый (высокопрои -{% if sponsors %} +### Ключевой-спонсор { #keystone-sponsor } + +{% for sponsor in sponsors.keystone -%} + +{% endfor -%} + +### Золотые и серебряные спонсоры { #gold-and-silver-sponsors } + {% for sponsor in sponsors.gold -%} {% endfor -%} {%- for sponsor in sponsors.silver -%} {% endfor %} -{% endif %} @@ -444,6 +450,58 @@ item: Item * **сессии с использованием cookie** * ...и многое другое. +### Разверните приложение (опционально) { #deploy-your-app-optional } + +При желании вы можете развернуть своё приложение FastAPI в FastAPI Cloud, присоединяйтесь к списку ожидания, если ещё не сделали этого. 🚀 + +Если у вас уже есть аккаунт **FastAPI Cloud** (мы пригласили вас из списка ожидания 😉), вы можете развернуть ваше приложение одной командой. + +Перед развертыванием убедитесь, что вы вошли в систему: + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +
+ +Затем разверните приложение: + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +Вот и всё! Теперь вы можете открыть ваше приложение по этой ссылке. ✨ + +#### О FastAPI Cloud { #about-fastapi-cloud } + +**FastAPI Cloud** создан тем же автором и командой, что и **FastAPI**. + +Он упрощает процесс **создания образа**, **развертывания** и **доступа** к API при минимальных усилиях. + +Он переносит тот же **опыт разработчика**, что и при создании приложений на FastAPI, на их **развертывание** в облаке. 🎉 + +FastAPI Cloud — основной спонсор и источник финансирования для проектов с открытым исходным кодом из экосистемы *FastAPI and friends*. ✨ + +#### Развертывание у других облачных провайдеров { #deploy-to-other-cloud-providers } + +FastAPI — это open source и стандартизированный фреймворк. Вы можете развернуть приложения FastAPI у любого облачного провайдера на ваш выбор. + +Следуйте руководствам вашего облачного провайдера по развертыванию приложений FastAPI. 🤓 + ## Производительность { #performance } Независимые бенчмарки TechEmpower показывают приложения **FastAPI**, работающие под управлением Uvicorn, как один из самых быстрых доступных фреймворков Python, уступающий только самим Starlette и Uvicorn (используются внутри FastAPI). (*) diff --git a/docs/ru/docs/project-generation.md b/docs/ru/docs/project-generation.md index 8c5681115..dbedf76fe 100644 --- a/docs/ru/docs/project-generation.md +++ b/docs/ru/docs/project-generation.md @@ -13,8 +13,8 @@ - 🔍 [Pydantic](https://docs.pydantic.dev), используется FastAPI, для валидации данных и управления настройками. - 💾 [PostgreSQL](https://www.postgresql.org) в качестве SQL‑базы данных. - 🚀 [React](https://react.dev) для фронтенда. - - 💃 Используются TypeScript, хуки, [Vite](https://vitejs.dev) и другие части современного фронтенд‑стека. - - 🎨 [Chakra UI](https://chakra-ui.com) для компонентов фронтенда. + - 💃 Используются TypeScript, хуки, Vite и другие части современного фронтенд‑стека. + - 🎨 [Tailwind CSS](https://tailwindcss.com) и [shadcn/ui](https://ui.shadcn.com) для компонентов фронтенда. - 🤖 Автоматически сгенерированный фронтенд‑клиент. - 🧪 [Playwright](https://playwright.dev) для End‑to‑End тестирования. - 🦇 Поддержка тёмной темы. diff --git a/docs/ru/docs/resources/index.md b/docs/ru/docs/resources/index.md index 54be4e5fd..faf80f7f4 100644 --- a/docs/ru/docs/resources/index.md +++ b/docs/ru/docs/resources/index.md @@ -1,3 +1,3 @@ # Ресурсы { #resources } -Дополнительные ресурсы, внешние ссылки, статьи и многое другое. ✈️ +Дополнительные ресурсы, внешние ссылки и многое другое. ✈️ diff --git a/docs/ru/docs/tutorial/bigger-applications.md b/docs/ru/docs/tutorial/bigger-applications.md index b832383cc..5e5d6ada9 100644 --- a/docs/ru/docs/tutorial/bigger-applications.md +++ b/docs/ru/docs/tutorial/bigger-applications.md @@ -85,17 +85,13 @@ from app.routers import items Точно также, как и в случае с классом `FastAPI`, вам нужно импортировать и создать объект класса `APIRouter`. -```Python hl_lines="1 3" title="app/routers/users.py" -{!../../docs_src/bigger_applications/app/routers/users.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[1,3] title["app/routers/users.py"] *} ### Создание *эндпоинтов* с помощью `APIRouter` { #path-operations-with-apirouter } В дальнейшем используйте `APIRouter` для объявления *эндпоинтов*, точно также, как вы используете класс `FastAPI`: -```Python hl_lines="6 11 16" title="app/routers/users.py" -{!../../docs_src/bigger_applications/app/routers/users.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[6,11,16] title["app/routers/users.py"] *} Вы можете думать об `APIRouter` как об "уменьшенной версии" класса FastAPI`. @@ -119,35 +115,7 @@ from app.routers import items Теперь мы воспользуемся простой зависимостью, чтобы прочитать кастомизированный `X-Token` из заголовка: -//// tab | Python 3.9+ - -```Python hl_lines="3 6-8" title="app/dependencies.py" -{!> ../../docs_src/bigger_applications/app_an_py39/dependencies.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python hl_lines="1 5-7" title="app/dependencies.py" -{!> ../../docs_src/bigger_applications/app_an/dependencies.py!} -``` - -//// - -//// tab | Python 3.8+ non-Annotated - -/// tip | Подсказка - -Мы рекомендуем использовать версию `Annotated`, когда это возможно. - -/// - -```Python hl_lines="1 4-6" title="app/dependencies.py" -{!> ../../docs_src/bigger_applications/app/dependencies.py!} -``` - -//// +{* ../../docs_src/bigger_applications/app_an_py39/dependencies.py hl[3,6:8] title["app/dependencies.py"] *} /// tip | Подсказка @@ -180,9 +148,7 @@ from app.routers import items Таким образом, вместо того чтобы добавлять все эти свойства в функцию каждого отдельного *эндпоинта*, мы добавим их в `APIRouter`. -```Python hl_lines="5-10 16 21" title="app/routers/items.py" -{!../../docs_src/bigger_applications/app/routers/items.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[5:10,16,21] title["app/routers/items.py"] *} Так как каждый *эндпоинт* начинается с символа `/`: @@ -241,9 +207,7 @@ async def read_item(item_id: str): Мы используем операцию относительного импорта `..` для импорта зависимости: -```Python hl_lines="3" title="app/routers/items.py" -{!../../docs_src/bigger_applications/app/routers/items.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[3] title["app/routers/items.py"] *} #### Как работает относительный импорт? { #how-relative-imports-work } @@ -313,9 +277,7 @@ from ...dependencies import get_token_header Но помимо этого мы можем добавить новые теги для каждого отдельного *эндпоинта*, а также некоторые дополнительные ответы (`responses`), характерные для данного *эндпоинта*: -```Python hl_lines="30-31" title="app/routers/items.py" -{!../../docs_src/bigger_applications/app/routers/items.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[30:31] title["app/routers/items.py"] *} /// tip | Подсказка @@ -341,17 +303,13 @@ from ...dependencies import get_token_header Мы даже можем объявить [глобальные зависимости](dependencies/global-dependencies.md){.internal-link target=_blank}, которые будут объединены с зависимостями для каждого отдельного маршрутизатора: -```Python hl_lines="1 3 7" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[1,3,7] title["app/main.py"] *} ### Импорт `APIRouter` { #import-the-apirouter } Теперь мы импортируем другие суб-модули, содержащие `APIRouter`: -```Python hl_lines="4-5" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[4:5] title["app/main.py"] *} Так как файлы `app/routers/users.py` и `app/routers/items.py` являются суб-модулями одного и того же Python-пакета `app`, то мы сможем их импортировать, воспользовавшись операцией относительного импорта `.`. @@ -414,17 +372,13 @@ from .routers.users import router Поэтому, для того чтобы использовать обе эти переменные в одном файле, мы импортировали соответствующие суб-модули: -```Python hl_lines="5" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[5] title["app/main.py"] *} ### Подключение маршрутизаторов (`APIRouter`) для `users` и для `items` { #include-the-apirouters-for-users-and-items } Давайте подключим маршрутизаторы (`router`) из суб-модулей `users` и `items`: -```Python hl_lines="10-11" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[10:11] title["app/main.py"] *} /// info | Примечание @@ -465,17 +419,13 @@ from .routers.users import router В данном примере это сделать очень просто. Но давайте предположим, что поскольку файл используется для нескольких проектов, то мы не можем модифицировать его, добавляя префиксы (`prefix`), зависимости (`dependencies`), теги (`tags`), и т.д. непосредственно в `APIRouter`: -```Python hl_lines="3" title="app/internal/admin.py" -{!../../docs_src/bigger_applications/app/internal/admin.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *} Но, несмотря на это, мы хотим использовать кастомный префикс (`prefix`) для подключенного маршрутизатора (`APIRouter`), в результате чего, каждая *операция пути* будет начинаться с `/admin`. Также мы хотим защитить наш маршрутизатор с помощью зависимостей, созданных для нашего проекта. И ещё мы хотим включить теги (`tags`) и ответы (`responses`). Мы можем применить все вышеперечисленные настройки, не изменяя начальный `APIRouter`. Нам всего лишь нужно передать нужные параметры в `app.include_router()`. -```Python hl_lines="14-17" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[14:17] title["app/main.py"] *} Таким образом, оригинальный `APIRouter` не будет модифицирован, и мы сможем использовать файл `app/internal/admin.py` сразу в нескольких проектах организации. @@ -496,9 +446,7 @@ from .routers.users import router Здесь мы это делаем ... просто, чтобы показать, что это возможно 🤷: -```Python hl_lines="21-23" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[21:23] title["app/main.py"] *} и это будет работать корректно вместе с другими *эндпоинтами*, добавленными с помощью `app.include_router()`. diff --git a/docs/ru/docs/tutorial/cookie-param-models.md b/docs/ru/docs/tutorial/cookie-param-models.md index daac764e3..182813afd 100644 --- a/docs/ru/docs/tutorial/cookie-param-models.md +++ b/docs/ru/docs/tutorial/cookie-param-models.md @@ -50,7 +50,7 @@ Вы можете сконфигурировать Pydantic-модель так, чтобы запретить (`forbid`) любые дополнительные (`extra`) поля: -{* ../../docs_src/cookie_param_models/tutorial002_an_py39.py hl[10] *} +{* ../../docs_src/cookie_param_models/tutorial002_an_py310.py hl[10] *} Если клиент попробует отправить **дополнительные cookies**, то в ответ он получит **ошибку**. diff --git a/docs/ru/docs/tutorial/first-steps.md b/docs/ru/docs/tutorial/first-steps.md index c82118cbe..6f59d7205 100644 --- a/docs/ru/docs/tutorial/first-steps.md +++ b/docs/ru/docs/tutorial/first-steps.md @@ -143,6 +143,42 @@ OpenAPI определяет схему API для вашего API. И эта Вы также можете использовать её для автоматической генерации кода для клиентов, которые взаимодействуют с вашим API. Например, для фронтенд-, мобильных или IoT-приложений. +### Разверните приложение (необязательно) { #deploy-your-app-optional } + +При желании вы можете развернуть своё приложение FastAPI в FastAPI Cloud, перейдите и присоединитесь к списку ожидания, если ещё не сделали этого. 🚀 + +Если у вас уже есть аккаунт **FastAPI Cloud** (мы пригласили вас из списка ожидания 😉), вы можете развернуть приложение одной командой. + +Перед развертыванием убедитесь, что вы вошли в систему: + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +
+ +Затем разверните приложение: + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +Готово! Теперь вы можете открыть своё приложение по этому URL. ✨ + ## Рассмотрим поэтапно { #recap-step-by-step } ### Шаг 1: импортируйте `FastAPI` { #step-1-import-fastapi } @@ -314,6 +350,26 @@ https://example.com/items/foo Многие другие объекты и модели будут автоматически преобразованы в JSON (включая ORM и т. п.). Попробуйте использовать те, что вам привычнее, с высокой вероятностью они уже поддерживаются. +### Шаг 6: разверните приложение { #step-6-deploy-it } + +Разверните приложение в **FastAPI Cloud** одной командой: `fastapi deploy`. 🎉 + +#### О FastAPI Cloud { #about-fastapi-cloud } + +**FastAPI Cloud** создан тем же автором и командой, что и **FastAPI**. + +Он упрощает процесс **создания образа**, **развертывания** и **доступа** к API с минимальными усилиями. + +Он переносит тот же **опыт разработчика** при создании приложений с FastAPI на их **развертывание** в облаке. 🎉 + +FastAPI Cloud — основной спонсор и источник финансирования для open-source проектов «FastAPI и друзья». ✨ + +#### Развертывание у других облачных провайдеров { #deploy-to-other-cloud-providers } + +FastAPI — open-source и основан на стандартах. Вы можете развернуть приложения FastAPI у любого облачного провайдера по вашему выбору. + +Следуйте руководствам вашего облачного провайдера, чтобы развернуть приложения FastAPI у них. 🤓 + ## Резюме { #recap } * Импортируйте `FastAPI`. @@ -321,3 +377,4 @@ https://example.com/items/foo * Напишите **декоратор операции пути**, например `@app.get("/")`. * Определите **функцию операции пути**; например, `def root(): ...`. * Запустите сервер разработки командой `fastapi dev`. +* При желании разверните приложение командой `fastapi deploy`. diff --git a/docs/ru/docs/tutorial/handling-errors.md b/docs/ru/docs/tutorial/handling-errors.md index 2378c8b04..63ca8665e 100644 --- a/docs/ru/docs/tutorial/handling-errors.md +++ b/docs/ru/docs/tutorial/handling-errors.md @@ -81,7 +81,7 @@ ## Установка пользовательских обработчиков исключений { #install-custom-exception-handlers } -Вы можете добавить пользовательские обработчики исключений с помощью то же самое исключение - утилиты от Starlette. +Вы можете добавить пользовательские обработчики исключений с помощью тех же утилит обработки исключений из Starlette. Допустим, у вас есть пользовательское исключение `UnicornException`, которое вы (или используемая вами библиотека) можете `вызвать`. @@ -117,7 +117,7 @@ Вы можете переопределить эти обработчики исключений на свои собственные. -### Переопределение исключений проверки запроса { #override-request-validation-exceptions } +### Переопределение обработчика исключений проверки запроса { #override-request-validation-exceptions } Когда запрос содержит недопустимые данные, **FastAPI** внутренне вызывает ошибку `RequestValidationError`. @@ -127,7 +127,7 @@ Обработчик исключения получит объект `Request` и исключение. -{* ../../docs_src/handling_errors/tutorial004.py hl[2,14:16] *} +{* ../../docs_src/handling_errors/tutorial004.py hl[2,14:19] *} Теперь, если перейти к `/items/foo`, то вместо стандартной JSON-ошибки с: @@ -149,36 +149,17 @@ вы получите текстовую версию: ``` -1 validation error -path -> item_id - value is not a valid integer (type=type_error.integer) +Validation errors: +Field: ('path', 'item_id'), Error: Input should be a valid integer, unable to parse string as an integer ``` -#### `RequestValidationError` или `ValidationError` { #requestvalidationerror-vs-validationerror } - -/// warning | Внимание - -Это технические детали, которые можно пропустить, если они не важны для вас сейчас. - -/// - -`RequestValidationError` является подклассом Pydantic `ValidationError`. - -**FastAPI** использует его для того, чтобы, если вы используете Pydantic-модель в `response_model`, и ваши данные содержат ошибку, вы увидели ошибку в журнале. - -Но клиент/пользователь этого не увидит. Вместо этого клиент получит сообщение "Internal Server Error" с кодом состояния HTTP `500`. - -Так и должно быть, потому что если в вашем *ответе* или где-либо в вашем коде (не в *запросе* клиента) возникает Pydantic `ValidationError`, то это действительно ошибка в вашем коде. - -И пока вы не устраните ошибку, ваши клиенты/пользователи не должны иметь доступа к внутренней информации о ней, так как это может привести к уязвимости в системе безопасности. - ### Переопределите обработчик ошибок `HTTPException` { #override-the-httpexception-error-handler } Аналогичным образом можно переопределить обработчик `HTTPException`. Например, для этих ошибок можно вернуть обычный текстовый ответ вместо JSON: -{* ../../docs_src/handling_errors/tutorial004.py hl[3:4,9:11,22] *} +{* ../../docs_src/handling_errors/tutorial004.py hl[3:4,9:11,25] *} /// note | Технические детали @@ -188,6 +169,14 @@ path -> item_id /// +/// warning | Внимание + +Имейте в виду, что `RequestValidationError` содержит информацию об имени файла и строке, где произошла ошибка валидации, чтобы вы могли при желании отобразить её в логах с релевантными данными. + +Но это означает, что если вы просто преобразуете её в строку и вернёте эту информацию напрямую, вы можете допустить небольшую утечку информации о своей системе, поэтому здесь код извлекает и показывает каждую ошибку отдельно. + +/// + ### Используйте тело `RequestValidationError` { #use-the-requestvalidationerror-body } Ошибка `RequestValidationError` содержит полученное `тело` с недопустимыми данными. diff --git a/docs/ru/docs/tutorial/security/index.md b/docs/ru/docs/tutorial/security/index.md index 8fb4bf24f..ebac013b6 100644 --- a/docs/ru/docs/tutorial/security/index.md +++ b/docs/ru/docs/tutorial/security/index.md @@ -1,4 +1,4 @@ -# Настройка авторизации +# Настройка авторизации { #security } Существует множество способов обеспечения безопасности, аутентификации и авторизации. @@ -10,11 +10,11 @@ Но сначала давайте рассмотрим некоторые небольшие концепции. -## Куда-то торопишься? +## Куда-то торопишься? { #in-a-hurry } Если вам не нужна информация о каких-либо из следующих терминов и вам просто нужно добавить защиту с аутентификацией на основе логина и пароля *прямо сейчас*, переходите к следующим главам. -## OAuth2 +## OAuth2 { #oauth2 } OAuth2 - это протокол, который определяет несколько способов обработки аутентификации и авторизации. @@ -24,7 +24,7 @@ OAuth2 включает в себя способы аутентификации Это то, что используют под собой все кнопки "вход с помощью Facebook, Google, X (Twitter), GitHub" на страницах авторизации. -### OAuth 1 +### OAuth 1 { #oauth-1 } Ранее использовался протокол OAuth 1, который сильно отличается от OAuth2 и является более сложным, поскольку он включал прямые описания того, как шифровать сообщение. @@ -34,11 +34,11 @@ OAuth2 не указывает, как шифровать сообщение, о /// tip | Подсказка -В разделе **Развертывание** вы увидите [как настроить протокол HTTPS бесплатно, используя Traefik и Let's Encrypt.](https://fastapi.tiangolo.com/ru/deployment/https/) +В разделе **Развертывание** вы увидите как настроить протокол HTTPS бесплатно, используя Traefik и Let's Encrypt. /// -## OpenID Connect +## OpenID Connect { #openid-connect } OpenID Connect - это еще один протокол, основанный на **OAuth2**. @@ -48,7 +48,7 @@ OpenID Connect - это еще один протокол, основанный Но вход в Facebook не поддерживает OpenID Connect. У него есть собственная вариация OAuth2. -### OpenID (не "OpenID Connect") +### OpenID (не "OpenID Connect") { #openid-not-openid-connect } Также ранее использовался стандарт "OpenID", который пытался решить ту же проблему, что и **OpenID Connect**, но не был основан на OAuth2. @@ -56,7 +56,7 @@ OpenID Connect - это еще один протокол, основанный В настоящее время не очень популярен и не используется. -## OpenAPI +## OpenAPI { #openapi } OpenAPI (ранее известный как Swagger) - это открытая спецификация для создания API (в настоящее время является частью Linux Foundation). @@ -97,7 +97,7 @@ OpenAPI может использовать следующие схемы авт /// -## Преимущества **FastAPI** +## Преимущества **FastAPI** { #fastapi-utilities } Fast API предоставляет несколько инструментов для каждой из этих схем безопасности в модуле `fastapi.security`, которые упрощают использование этих механизмов безопасности. diff --git a/docs/ru/docs/tutorial/sql-databases.md b/docs/ru/docs/tutorial/sql-databases.md index c44f37b9a..1d0346533 100644 --- a/docs/ru/docs/tutorial/sql-databases.md +++ b/docs/ru/docs/tutorial/sql-databases.md @@ -63,9 +63,9 @@ $ pip install sqlmodel * `table=True` сообщает SQLModel, что это *модель-таблица*, она должна представлять **таблицу** в SQL базе данных, это не просто *модель данных* (как обычный класс Pydantic). -* `Field(primary_key=True)` сообщает SQLModel, что `id` — это **первичный ключ** в SQL базе данных (подробнее о первичных ключах можно узнать в документации SQLModel). +* `Field(primary_key=True)` сообщает SQLModel, что `id` — это **первичный ключ** в SQL базе данных (подробнее о первичных ключах SQL можно узнать в документации SQLModel). - Благодаря типу `int | None`, SQLModel будет знать, что этот столбец должен быть `INTEGER` в SQL базе данных и должен допускать значение `NULL`. + **Примечание:** Мы используем `int | None` для поля первичного ключа, чтобы в Python-коде можно было *создать объект без `id`* (`id=None`), предполагая, что база данных *сгенерирует его при сохранении*. SQLModel понимает, что база данных предоставит `id`, и *определяет столбец как `INTEGER` (не `NULL`)* в схеме базы данных. См. документацию SQLModel о первичных ключах для подробностей. * `Field(index=True)` сообщает SQLModel, что нужно создать **SQL индекс** для этого столбца, что позволит быстрее выполнять выборки при чтении данных, отфильтрованных по этому столбцу. @@ -107,7 +107,7 @@ $ pip install sqlmodel Здесь мы создаём таблицы в обработчике события запуска приложения. -Для продакшна вы, вероятно, будете использовать скрипт миграций, который выполняется до запуска приложения. 🤓 +Для продакшн вы, вероятно, будете использовать скрипт миграций, который выполняется до запуска приложения. 🤓 /// tip | Подсказка diff --git a/docs/ru/docs/tutorial/testing.md b/docs/ru/docs/tutorial/testing.md index 0224798b1..7354ed895 100644 --- a/docs/ru/docs/tutorial/testing.md +++ b/docs/ru/docs/tutorial/testing.md @@ -121,63 +121,13 @@ $ pip install httpx Обе *операции пути* требуют наличия в запросе заголовка `X-Token`. -//// tab | Python 3.10+ - -```Python -{!> ../../docs_src/app_testing/app_b_an_py310/main.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!> ../../docs_src/app_testing/app_b_an_py39/main.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python -{!> ../../docs_src/app_testing/app_b_an/main.py!} -``` - -//// - -//// tab | Python 3.10+ без Annotated - -/// tip | Подсказка - -По возможности используйте версию с `Annotated`. - -/// - -```Python -{!> ../../docs_src/app_testing/app_b_py310/main.py!} -``` - -//// - -//// tab | Python 3.8+ без Annotated - -/// tip | Подсказка - -По возможности используйте версию с `Annotated`. - -/// - -```Python -{!> ../../docs_src/app_testing/app_b/main.py!} -``` - -//// +{* ../../docs_src/app_testing/app_b_an_py310/main.py *} ### Расширенный файл тестов { #extended-testing-file } Теперь обновим файл `test_main.py`, добавив в него тестов: -{* ../../docs_src/app_testing/app_b/test_main.py *} +{* ../../docs_src/app_testing/app_b_an_py310/test_main.py *} Если Вы не знаете, как передать информацию в запросе, можете воспользоваться поисковиком (погуглить) и задать вопрос: "Как передать информацию в запросе с помощью `httpx`", можно даже спросить: "Как передать информацию в запросе с помощью `requests`", поскольку дизайн HTTPX основан на дизайне Requests. diff --git a/docs/ru/docs/virtual-environments.md b/docs/ru/docs/virtual-environments.md index 5153cd486..43136298a 100644 --- a/docs/ru/docs/virtual-environments.md +++ b/docs/ru/docs/virtual-environments.md @@ -242,6 +242,26 @@ $ python -m pip install --upgrade pip +/// tip | Подсказка + +Иногда при попытке обновить pip вы можете получить ошибку **`No module named pip`**. + +Если это произошло, установите и обновите pip с помощью команды ниже: + +
+ +```console +$ python -m ensurepip --upgrade + +---> 100% +``` + +
+ +Эта команда установит pip, если он ещё не установлен, а также гарантирует, что установленная версия pip будет не старее, чем версия, доступная в `ensurepip`. + +/// + ## Добавление `.gitignore` { #add-gitignore } Если вы используете **Git** (а вам стоит его использовать), добавьте файл `.gitignore`, чтобы исключить из Git всё, что находится в вашей `.venv`. @@ -834,7 +854,7 @@ I solemnly swear 🐺 * Управлять **виртуальным окружением** ваших проектов * Устанавливать **пакеты** * Управлять **зависимостями и версиями** пакетов вашего проекта -* Обеспечивать наличие **точного** набора пакетов и версий к установке, включая их зависимости, чтобы вы были уверены, что сможете запускать проект в продакшне точно так же, как и на компьютере при разработке — это называется **locking** +* Обеспечивать наличие **точного** набора пакетов и версий к установке, включая их зависимости, чтобы вы были уверены, что сможете запускать проект в продакшн точно так же, как и на компьютере при разработке — это называется **locking** * И многое другое ## Заключение { #conclusion } From 1fcec88ad2df4541b6a81c4786c4d039dc745e1e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 11 Dec 2025 21:25:27 +0000 Subject: [PATCH 256/256] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 5bb7698c8..19e0cb5a1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -19,6 +19,7 @@ hide: ### Translations +* 🌐 Sync Russian docs. PR [#14509](https://github.com/fastapi/fastapi/pull/14509) by [@YuriiMotov](https://github.com/YuriiMotov). * 🌐 Sync German docs. PR [#14488](https://github.com/fastapi/fastapi/pull/14488) by [@nilslindemann](https://github.com/nilslindemann). ### Internal