From 8fa18e5e6ff5ae6bb620bf4a7bd0d1964f10f2de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro?= Date: Wed, 22 Jan 2025 10:41:56 -0300 Subject: [PATCH 001/117] =?UTF-8?q?=F0=9F=8C=90=20Update=20Portuguese=20Tr?= =?UTF-8?q?anslation=20for=20`docs/pt/docs/tutorial/request-forms.md`=20(#?= =?UTF-8?q?13216)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/pt/docs/tutorial/request-forms.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/pt/docs/tutorial/request-forms.md b/docs/pt/docs/tutorial/request-forms.md index 756ceb581..572ddf003 100644 --- a/docs/pt/docs/tutorial/request-forms.md +++ b/docs/pt/docs/tutorial/request-forms.md @@ -6,7 +6,11 @@ Quando você precisar receber campos de formulário ao invés de JSON, você pod Para usar formulários, primeiro instale `python-multipart`. -Ex: `pip install 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: + +```console +$ pip install python-multipart +``` /// From a215687c987ea882ef6c4f0788b6c5a8b71f75ff Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 22 Jan 2025 13:42:19 +0000 Subject: [PATCH 002/117] =?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 a242407ea..5f527d37b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -49,6 +49,7 @@ hide: ### Translations +* 🌐 Update Portuguese Translation for `docs/pt/docs/tutorial/request-forms.md`. PR [#13216](https://github.com/fastapi/fastapi/pull/13216) by [@Joao-Pedro-P-Holanda](https://github.com/Joao-Pedro-P-Holanda). * 🌐 Update Portuguese translation for `docs/pt/docs/advanced/settings.md`. PR [#13209](https://github.com/fastapi/fastapi/pull/13209) by [@ceb10n](https://github.com/ceb10n). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/security/oauth2-jwt.md`. PR [#13205](https://github.com/fastapi/fastapi/pull/13205) by [@ceb10n](https://github.com/ceb10n). * 🌐 Add Indonesian translation for `docs/id/docs/index.md`. PR [#13191](https://github.com/fastapi/fastapi/pull/13191) by [@gerry-sabar](https://github.com/gerry-sabar). From 548f67d465afff233fa1856b2d337d13595e3308 Mon Sep 17 00:00:00 2001 From: Daniel Kusy <36250676+DanielKusyDev@users.noreply.github.com> Date: Wed, 22 Jan 2025 19:02:36 +0100 Subject: [PATCH 003/117] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20`jinja2`?= =?UTF-8?q?=20to=20>=3D3.1.5=20(#13194)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index edfa81522..ae56ebb7e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ standard = [ # For the test client "httpx >=0.23.0", # For templates - "jinja2 >=2.11.2", + "jinja2 >=3.1.5", # For forms and file uploads "python-multipart >=0.0.7", # To validate email fields @@ -79,7 +79,7 @@ all = [ # # For the test client "httpx >=0.23.0", # For templates - "jinja2 >=2.11.2", + "jinja2 >=3.1.5", # For forms and file uploads "python-multipart >=0.0.7", # For Starlette's SessionMiddleware, not commonly used with FastAPI From 1a38cc506e5732a8a0e2d4cfec3059f670a5ef90 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 22 Jan 2025 18:03:00 +0000 Subject: [PATCH 004/117] =?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 5f527d37b..d982a15f2 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -33,6 +33,10 @@ hide: * ✅ Simplify tests for background_tasks. PR [#13166](https://github.com/fastapi/fastapi/pull/13166) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for additional_status_codes. PR [#13149](https://github.com/fastapi/fastapi/pull/13149) by [@tiangolo](https://github.com/tiangolo). +### Upgrades + +* ⬆️ Upgrade `jinja2` to >=3.1.5. PR [#13194](https://github.com/fastapi/fastapi/pull/13194) by [@DanielKusyDev](https://github.com/DanielKusyDev). + ### Docs * ✏️ Update Strawberry integration docs. PR [#13155](https://github.com/fastapi/fastapi/pull/13155) by [@kinuax](https://github.com/kinuax). From 82c74789e8c2ba68f4c88b6e8c80928881578272 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Wed, 22 Jan 2025 19:21:40 +0100 Subject: [PATCH 005/117] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Bump=20Starlette?= =?UTF-8?q?=20to=20allow=20up=20to=200.45.0:=20`>=3D0.40.0,<0.46.0`=20(#13?= =?UTF-8?q?117)?= 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 ae56ebb7e..9510d36c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ classifiers = [ "Topic :: Internet :: WWW/HTTP", ] dependencies = [ - "starlette>=0.40.0,<0.42.0", + "starlette>=0.40.0,<0.46.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", ] From 91c05b9245269502ea3d28569b3cbe2fa556e465 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 22 Jan 2025 18:22:02 +0000 Subject: [PATCH 006/117] =?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 d982a15f2..1951d6559 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -35,6 +35,7 @@ hide: ### Upgrades +* ⬆️ Bump Starlette to allow up to 0.45.0: `>=0.40.0,<0.46.0`. PR [#13117](https://github.com/fastapi/fastapi/pull/13117) by [@Kludex](https://github.com/Kludex). * ⬆️ Upgrade `jinja2` to >=3.1.5. PR [#13194](https://github.com/fastapi/fastapi/pull/13194) by [@DanielKusyDev](https://github.com/DanielKusyDev). ### Docs From 49e82ed2ac63c2aad957d01c857ee1fe597dc4ca Mon Sep 17 00:00:00 2001 From: Daniel Kusy <36250676+DanielKusyDev@users.noreply.github.com> Date: Wed, 22 Jan 2025 19:23:13 +0100 Subject: [PATCH 007/117] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20`python-?= =?UTF-8?q?multipart`=20to=20>=3D0.0.18=20(#13219)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9510d36c3..6fa0f080f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,7 @@ standard = [ # For templates "jinja2 >=3.1.5", # For forms and file uploads - "python-multipart >=0.0.7", + "python-multipart >=0.0.18", # To validate email fields "email-validator >=2.0.0", # Uvicorn with uvloop @@ -81,7 +81,7 @@ all = [ # For templates "jinja2 >=3.1.5", # For forms and file uploads - "python-multipart >=0.0.7", + "python-multipart >=0.0.18", # For Starlette's SessionMiddleware, not commonly used with FastAPI "itsdangerous >=1.1.0", # For Starlette's schema generation, would not be used with FastAPI From e39143d56de41f8d72a3be8847030346cb1d8adc Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 22 Jan 2025 18:24:12 +0000 Subject: [PATCH 008/117] =?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 1951d6559..a806930f5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -35,6 +35,7 @@ hide: ### Upgrades +* ⬆️ Upgrade `python-multipart` to >=0.0.18. PR [#13219](https://github.com/fastapi/fastapi/pull/13219) by [@DanielKusyDev](https://github.com/DanielKusyDev). * ⬆️ Bump Starlette to allow up to 0.45.0: `>=0.40.0,<0.46.0`. PR [#13117](https://github.com/fastapi/fastapi/pull/13117) by [@Kludex](https://github.com/Kludex). * ⬆️ Upgrade `jinja2` to >=3.1.5. PR [#13194](https://github.com/fastapi/fastapi/pull/13194) by [@DanielKusyDev](https://github.com/DanielKusyDev). From abd05a6d30e6e4c9b1496e0b94d362a9979c9f37 Mon Sep 17 00:00:00 2001 From: johnthagen Date: Wed, 22 Jan 2025 13:24:58 -0500 Subject: [PATCH 009/117] =?UTF-8?q?=F0=9F=94=A7=20Add=20Pydantic=202=20tro?= =?UTF-8?q?ve=20classifier=20(#13199)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 6fa0f080f..381eb50bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ classifiers = [ "Framework :: FastAPI", "Framework :: Pydantic", "Framework :: Pydantic :: 1", + "Framework :: Pydantic :: 2", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3 :: Only", From 2b6f63df712f0ab98f071f64b3f8ac215ddf43be Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 22 Jan 2025 18:25:24 +0000 Subject: [PATCH 010/117] =?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 a806930f5..13976b788 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -110,6 +110,7 @@ hide: ### Internal +* 🔧 Add Pydantic 2 trove classifier. PR [#13199](https://github.com/fastapi/fastapi/pull/13199) by [@johnthagen](https://github.com/johnthagen). * 👥 Update FastAPI People - Sponsors. PR [#13231](https://github.com/fastapi/fastapi/pull/13231) by [@tiangolo](https://github.com/tiangolo). * 👷 Refactor FastAPI People Sponsors to use 2 tokens. PR [#13228](https://github.com/fastapi/fastapi/pull/13228) by [@tiangolo](https://github.com/tiangolo). * 👷 Update token for FastAPI People - Sponsors. PR [#13225](https://github.com/fastapi/fastapi/pull/13225) by [@tiangolo](https://github.com/tiangolo). From 7183f0d683558390b973af8ad708e82567d4dc7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 22 Jan 2025 22:48:57 +0000 Subject: [PATCH 011/117] =?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 | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 13976b788..c7f01b82f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,12 @@ hide: ## Latest Changes +### Upgrades + +* ⬆️ Upgrade `python-multipart` to >=0.0.18. PR [#13219](https://github.com/fastapi/fastapi/pull/13219) by [@DanielKusyDev](https://github.com/DanielKusyDev). +* ⬆️ Bump Starlette to allow up to 0.45.0: `>=0.40.0,<0.46.0`. PR [#13117](https://github.com/fastapi/fastapi/pull/13117) by [@Kludex](https://github.com/Kludex). +* ⬆️ Upgrade `jinja2` to >=3.1.5. PR [#13194](https://github.com/fastapi/fastapi/pull/13194) by [@DanielKusyDev](https://github.com/DanielKusyDev). + ### Refactors * ✅ Simplify tests for websockets. PR [#13202](https://github.com/fastapi/fastapi/pull/13202) by [@alejsdev](https://github.com/alejsdev). @@ -33,12 +39,6 @@ hide: * ✅ Simplify tests for background_tasks. PR [#13166](https://github.com/fastapi/fastapi/pull/13166) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for additional_status_codes. PR [#13149](https://github.com/fastapi/fastapi/pull/13149) by [@tiangolo](https://github.com/tiangolo). -### Upgrades - -* ⬆️ Upgrade `python-multipart` to >=0.0.18. PR [#13219](https://github.com/fastapi/fastapi/pull/13219) by [@DanielKusyDev](https://github.com/DanielKusyDev). -* ⬆️ Bump Starlette to allow up to 0.45.0: `>=0.40.0,<0.46.0`. PR [#13117](https://github.com/fastapi/fastapi/pull/13117) by [@Kludex](https://github.com/Kludex). -* ⬆️ Upgrade `jinja2` to >=3.1.5. PR [#13194](https://github.com/fastapi/fastapi/pull/13194) by [@DanielKusyDev](https://github.com/DanielKusyDev). - ### Docs * ✏️ Update Strawberry integration docs. PR [#13155](https://github.com/fastapi/fastapi/pull/13155) by [@kinuax](https://github.com/kinuax). From fe513719ea98abade167d8a89e92f600d9d8f0e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 22 Jan 2025 22:50:29 +0000 Subject: [PATCH 012/117] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.11?= =?UTF-8?q?5.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 c7f01b82f..c8a75756b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.115.7 + ### Upgrades * ⬆️ Upgrade `python-multipart` to >=0.0.18. PR [#13219](https://github.com/fastapi/fastapi/pull/13219) by [@DanielKusyDev](https://github.com/DanielKusyDev). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 823957822..c92279cfd 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.115.6" +__version__ = "0.115.7" from starlette import status as status From 4d60022c88e6ba92a89df4860ba2df3af7e8c926 Mon Sep 17 00:00:00 2001 From: alv2017 Date: Thu, 23 Jan 2025 11:46:41 +0200 Subject: [PATCH 013/117] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20translat?= =?UTF-8?q?ion=20for=20`docs/ru/docs/tutorial/bigger-applications.md`=20(#?= =?UTF-8?q?13154)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ru/docs/tutorial/bigger-applications.md | 556 +++++++++++++++++++ 1 file changed, 556 insertions(+) create mode 100644 docs/ru/docs/tutorial/bigger-applications.md diff --git a/docs/ru/docs/tutorial/bigger-applications.md b/docs/ru/docs/tutorial/bigger-applications.md new file mode 100644 index 000000000..7c3dc288f --- /dev/null +++ b/docs/ru/docs/tutorial/bigger-applications.md @@ -0,0 +1,556 @@ +# Большие приложения, в которых много файлов + +При построении приложения или веб-API нам редко удается поместить всё в один файл. + +**FastAPI** предоставляет удобный инструментарий, который позволяет нам структурировать приложение, сохраняя при этом всю необходимую гибкость. + +/// info | Примечание + +Если вы раньше использовали Flask, то это аналог шаблонов Flask (Flask's Blueprints). + +/// + +## Пример структуры приложения + +Давайте предположим, что наше приложение имеет следующую структуру: + +``` +. +├── app +│   ├── __init__.py +│   ├── main.py +│   ├── dependencies.py +│   └── routers +│   │ ├── __init__.py +│   │ ├── items.py +│   │ └── users.py +│   └── internal +│   ├── __init__.py +│   └── admin.py +``` + +/// tip | Подсказка + +Обратите внимание, что в каждом каталоге и подкаталоге имеется файл `__init__.py` + +Это как раз то, что позволяет импортировать код из одного файла в другой. + +Например, в файле `app/main.py` может быть следующая строка: + +``` +from app.routers import items +``` + +/// + +* Всё помещается в каталоге `app`. В нём также находится пустой файл `app/__init__.py`. Таким образом, `app` является "Python-пакетом" (коллекцией модулей Python). +* Он содержит файл `app/main.py`. Данный файл является частью пакета (т.е. находится внутри каталога, содержащего файл `__init__.py`), и, соответственно, он является модулем пакета: `app.main`. +* Он также содержит файл `app/dependencies.py`, который также, как и `app/main.py`, является модулем: `app.dependencies`. +* Здесь также находится подкаталог `app/routers/`, содержащий `__init__.py`. Он является суб-пакетом: `app.routers`. +* Файл `app/routers/items.py` находится внутри пакета `app/routers/`. Таким образом, он является суб-модулем: `app.routers.items`. +* Точно также `app/routers/users.py` является ещё одним суб-модулем: `app.routers.users`. +* Подкаталог `app/internal/`, содержащий файл `__init__.py`, является ещё одним суб-пакетом: `app.internal`. +* А файл `app/internal/admin.py` является ещё одним суб-модулем: `app.internal.admin`. + + + +Та же самая файловая структура приложения, но с комментариями: + +``` +. +├── app # "app" пакет +│   ├── __init__.py # этот файл превращает "app" в "Python-пакет" +│   ├── main.py # модуль "main", напр.: import app.main +│   ├── dependencies.py # модуль "dependencies", напр.: import app.dependencies +│   └── routers # суб-пакет "routers" +│   │ ├── __init__.py # превращает "routers" в суб-пакет +│   │ ├── items.py # суб-модуль "items", напр.: import app.routers.items +│   │ └── users.py # суб-модуль "users", напр.: import app.routers.users +│   └── internal # суб-пакет "internal" +│   ├── __init__.py # превращает "internal" в суб-пакет +│   └── admin.py # суб-модуль "admin", напр.: import app.internal.admin +``` + +## `APIRouter` + +Давайте предположим, что для работы с пользователями используется отдельный файл (суб-модуль) `/app/routers/users.py`. + +Для лучшей организации приложения, вы хотите отделить операции пути, связанные с пользователями, от остального кода. + +Но так, чтобы эти операции по-прежнему оставались частью **FastAPI** приложения/веб-API (частью одного пакета) + +С помощью `APIRouter` вы можете создать *операции пути* (*эндпоинты*) для данного модуля. + + +### Импорт `APIRouter` + +Точно также, как и в случае с классом `FastAPI`, вам нужно импортировать и создать объект класса `APIRouter`. + +```Python hl_lines="1 3" title="app/routers/users.py" +{!../../docs_src/bigger_applications/app/routers/users.py!} +``` + +### Создание *эндпоинтов* с помощью `APIRouter` + +В дальнейшем используйте `APIRouter` для объявления *эндпоинтов*, точно также, как вы используете класс `FastAPI`: + +```Python hl_lines="6 11 16" title="app/routers/users.py" +{!../../docs_src/bigger_applications/app/routers/users.py!} +``` + +Вы можете думать об `APIRouter` как об "уменьшенной версии" класса FastAPI`. + +`APIRouter` поддерживает все те же самые опции. + +`APIRouter` поддерживает все те же самые параметры, такие как `parameters`, `responses`, `dependencies`, `tags`, и т. д. + +/// tip | Подсказка + +В данном примере, в качестве названия переменной используется `router`, но вы можете использовать любое другое имя. + +/// + +Мы собираемся подключить данный `APIRouter` к нашему основному приложению на `FastAPI`, но сначала давайте проверим зависимости и создадим ещё один модуль с `APIRouter`. + +## Зависимости + +Нам понадобятся некоторые зависимости, которые мы будем использовать в разных местах нашего приложения. + +Мы поместим их в отдельный модуль `dependencies` (`app/dependencies.py`). + +Теперь мы воспользуемся простой зависимостью, чтобы прочитать кастомизированный `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!} +``` + +//// + +/// tip | Подсказка + +Для простоты мы воспользовались неким воображаемым заголовоком. + +В реальных случаях для получения наилучших результатов используйте интегрированные утилиты обеспечения безопасности [Security utilities](security/index.md){.internal-link target=_blank}. + +/// + +## Ещё один модуль с `APIRouter` + +Давайте также предположим, что у вас есть *эндпоинты*, отвечающие за обработку "items", и они находятся в модуле `app/routers/items.py`. + +У вас определены следующие *операции пути* (*эндпоинты*): + +* `/items/` +* `/items/{item_id}` + +Тут всё точно также, как и в ситуации с `app/routers/users.py`. + +Но теперь мы хотим поступить немного умнее и слегка упростить код. + +Мы знаем, что все *эндпоинты* данного модуля имеют некоторые общие свойства: + +* Префикс пути: `/items`. +* Теги: (один единственный тег: `items`). +* Дополнительные ответы (responses) +* Зависимости: использование созданной нами зависимости `X-token` + +Таким образом, вместо того чтобы добавлять все эти свойства в функцию каждого отдельного *эндпоинта*, +мы добавим их в `APIRouter`. + +```Python hl_lines="5-10 16 21" title="app/routers/items.py" +{!../../docs_src/bigger_applications/app/routers/items.py!} +``` + +Так как каждый *эндпоинт* начинается с символа `/`: + +```Python hl_lines="1" +@router.get("/{item_id}") +async def read_item(item_id: str): + ... +``` + +...то префикс не должен заканчиваться символом `/`. + +В нашем случае префиксом является `/items`. + +Мы также можем добавить в наш маршрутизатор (router) список `тегов` (`tags`) и дополнительных `ответов` (`responses`), которые являются общими для каждого *эндпоинта*. + +И ещё мы можем добавить в наш маршрутизатор список `зависимостей`, которые должны вызываться при каждом обращении к *эндпоинтам*. + +/// tip | Подсказка + +Обратите внимание, что также, как и в случае с зависимостями в декораторах *эндпоинтов* ([dependencies in *path operation decorators*](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}), никакого значения в *функцию эндпоинта* передано не будет. + +/// + +В результате мы получим следующие эндпоинты: + +* `/items/` +* `/items/{item_id}` + +...как мы и планировали. + +* Они будут помечены тегами из заданного списка, в нашем случае это `"items"`. + * Эти теги особенно полезны для системы автоматической интерактивной документации (с использованием OpenAPI). +* Каждый из них будет включать предопределенные ответы `responses`. +* Каждый *эндпоинт* будет иметь список зависимостей (`dependencies`), исполняемых перед вызовом *эндпоинта*. + * Если вы определили зависимости в самой операции пути, **то она также будет выполнена**. + * Сначала выполняются зависимости маршрутизатора, затем вызываются зависимости, определенные в декораторе *эндпоинта* ([`dependencies` in the decorator](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}), и, наконец, обычные параметрические зависимости. + * Вы также можете добавить зависимости безопасности с областями видимости (`scopes`) [`Security` dependencies with `scopes`](../advanced/security/oauth2-scopes.md){.internal-link target=_blank}. + +/// tip | Подсказка + +Например, с помощью зависимостей в `APIRouter` мы можем потребовать аутентификации для доступа ко всей группе *эндпоинтов*, не указывая зависимости для каждой отдельной функции *эндпоинта*. + +/// + +/// check | Заметка + +Параметры `prefix`, `tags`, `responses` и `dependencies` относятся к функционалу **FastAPI**, помогающему избежать дублирования кода. + +/// + +### Импорт зависимостей + +Наш код находится в модуле `app.routers.items` (файл `app/routers/items.py`). + +И нам нужно вызвать функцию зависимости из модуля `app.dependencies` (файл `app/dependencies.py`). + +Мы используем операцию относительного импорта `..` для импорта зависимости: + +```Python hl_lines="3" title="app/routers/items.py" +{!../../docs_src/bigger_applications/app/routers/items.py!} +``` + +#### Как работает относительный импорт? + +/// tip | Подсказка + +Если вы прекрасно знаете, как работает импорт в Python, то переходите к следующему разделу. + +/// + +Одна точка `.`, как в данном примере: + +```Python +from .dependencies import get_token_header +``` +означает: + +* Начните с пакета, в котором находится данный модуль (файл `app/routers/items.py` расположен в каталоге `app/routers/`)... +* ... найдите модуль `dependencies` (файл `app/routers/dependencies.py`)... +* ... и импортируйте из него функцию `get_token_header`. + +К сожалению, такого файла не существует, и наши зависимости находятся в файле `app/dependencies.py`. + +Вспомните, как выглядит файловая структура нашего приложения: + + + +--- + +Две точки `..`, как в данном примере: + +```Python +from ..dependencies import get_token_header +``` + +означают: + +* Начните с пакета, в котором находится данный модуль (файл `app/routers/items.py` находится в каталоге `app/routers/`)... +* ... перейдите в родительский пакет (каталог `app/`)... +* ... найдите в нём модуль `dependencies` (файл `app/dependencies.py`)... +* ... и импортируйте из него функцию `get_token_header`. + +Это работает верно! 🎉 + +--- + +Аналогично, если бы мы использовали три точки `...`, как здесь: + +```Python +from ...dependencies import get_token_header +``` + +то это бы означало: + +* Начните с пакета, в котором находится данный модуль (файл `app/routers/items.py` находится в каталоге `app/routers/`)... +* ... перейдите в родительский пакет (каталог `app/`)... +* ... затем перейдите в родительский пакет текущего пакета (такого пакета не существует, `app` находится на самом верхнем уровне 😱)... +* ... найдите в нём модуль `dependencies` (файл `app/dependencies.py`)... +* ... и импортируйте из него функцию `get_token_header`. + +Это будет относиться к некоторому пакету, находящемуся на один уровень выше чем `app/` и содержащему свой собственный файл `__init__.py`. Но ничего такого у нас нет. Поэтому это приведет к ошибке в нашем примере. 🚨 + +Теперь вы знаете, как работает импорт в Python, и сможете использовать относительное импортирование в своих собственных приложениях любого уровня сложности. 🤓 + +### Добавление пользовательских тегов (`tags`), ответов (`responses`) и зависимостей (`dependencies`) + +Мы не будем добавлять префикс `/items` и список тегов `tags=["items"]` для каждого *эндпоинта*, т.к. мы уже их добавили с помощью `APIRouter`. + +Но помимо этого мы можем добавить новые теги для каждого отдельного *эндпоинта*, а также некоторые дополнительные ответы (`responses`), характерные для данного *эндпоинта*: + +```Python hl_lines="30-31" title="app/routers/items.py" +{!../../docs_src/bigger_applications/app/routers/items.py!} +``` + +/// tip | Подсказка + +Последний *эндпоинт* будет иметь следующую комбинацию тегов: `["items", "custom"]`. + +А также в его документации будут содержаться оба ответа: один для `404` и другой для `403`. + +/// + +## Модуль main в `FastAPI` + +Теперь давайте посмотрим на модуль `app/main.py`. + +Именно сюда вы импортируете и именно здесь вы используете класс `FastAPI`. + +Это основной файл вашего приложения, который объединяет всё в одно целое. + +И теперь, когда большая часть логики приложения разделена на отдельные модули, основной файл `app/main.py` будет достаточно простым. + +### Импорт `FastAPI` + +Вы импортируете и создаете класс `FastAPI` как обычно. + +Мы даже можем объявить глобальные зависимости [global dependencies](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!} +``` + +### Импорт `APIRouter` + +Теперь мы импортируем другие суб-модули, содержащие `APIRouter`: + +```Python hl_lines="4-5" title="app/main.py" +{!../../docs_src/bigger_applications/app/main.py!} +``` + +Так как файлы `app/routers/users.py` и `app/routers/items.py` являются суб-модулями одного и того же Python-пакета `app`, то мы сможем их импортировать, воспользовавшись операцией относительного импорта `.`. + +### Как работает импорт? + +Данная строка кода: + +```Python +from .routers import items, users +``` + +означает: + +* Начните с пакета, в котором содержится данный модуль (файл `app/main.py` содержится в каталоге `app/`)... +* ... найдите суб-пакет `routers` (каталог `app/routers/`)... +* ... и из него импортируйте суб-модули `items` (файл `app/routers/items.py`) и `users` (файл `app/routers/users.py`)... + +В модуле `items` содержится переменная `router` (`items.router`), та самая, которую мы создали в файле `app/routers/items.py`, она является объектом класса `APIRouter`. + +И затем мы сделаем то же самое для модуля `users`. + +Мы также могли бы импортировать и другим методом: + +```Python +from app.routers import items, users +``` + +/// info | Примечание + +Первая версия является примером относительного импорта: + +```Python +from .routers import items, users +``` + +Вторая версия является примером абсолютного импорта: + +```Python +from app.routers import items, users +``` + +Узнать больше о пакетах и модулях в Python вы можете из официальной документации Python о модулях + +/// + +### Избегайте конфликтов имен + +Вместо того чтобы импортировать только переменную `router`, мы импортируем непосредственно суб-модуль `items`. + +Мы делаем это потому, что у нас есть ещё одна переменная `router` в суб-модуле `users`. + +Если бы мы импортировали их одну за другой, как показано в примере: + +```Python +from .routers.items import router +from .routers.users import router +``` + +то переменная `router` из `users` переписал бы переменную `router` из `items`, и у нас не было бы возможности использовать их одновременно. + +Поэтому, для того чтобы использовать обе эти переменные в одном файле, мы импортировали соответствующие суб-модули: + +```Python hl_lines="5" title="app/main.py" +{!../../docs_src/bigger_applications/app/main.py!} +``` + +### Подключение маршрутизаторов (`APIRouter`) для `users` и для `items` + +Давайте подключим маршрутизаторы (`router`) из суб-модулей `users` и `items`: + +```Python hl_lines="10-11" title="app/main.py" +{!../../docs_src/bigger_applications/app/main.py!} +``` + +/// info | Примечание + +`users.router` содержит `APIRouter` из файла `app/routers/users.py`. + +А `items.router` содержит `APIRouter` из файла `app/routers/items.py`. + +/// + +С помощью `app.include_router()` мы можем добавить каждый из маршрутизаторов (`APIRouter`) в основное приложение `FastAPI`. + +Он подключит все маршруты заданного маршрутизатора к нашему приложению. + +/// note | Технические детали + +Фактически, внутри он создаст все *операции пути* для каждой операции пути объявленной в `APIRouter`. + +И под капотом всё будет работать так, как будто бы мы имеем дело с одним файлом приложения. + +/// + +/// check | Заметка + +При подключении маршрутизаторов не стоит беспокоиться о производительности. + +Операция подключения займёт микросекунды и понадобится только при запуске приложения. + +Таким образом, это не повлияет на производительность. ⚡ + +/// + +### Подключение `APIRouter` с пользовательскими префиксом (`prefix`), тегами (`tags`), ответами (`responses`), и зависимостями (`dependencies`) + +Теперь давайте представим, что ваша организация передала вам файл `app/internal/admin.py`. + +Он содержит `APIRouter` с некоторыми *эндпоитами* администрирования, которые ваша организация использует для нескольких проектов. + +В данном примере это сделать очень просто. Но давайте предположим, что поскольку файл используется для нескольких проектов, +то мы не можем модифицировать его, добавляя префиксы (`prefix`), зависимости (`dependencies`), теги (`tags`), и т.д. непосредственно в `APIRouter`: + +```Python hl_lines="3" title="app/internal/admin.py" +{!../../docs_src/bigger_applications/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!} +``` + +Таким образом, оригинальный `APIRouter` не будет модифицирован, и мы сможем использовать файл `app/internal/admin.py` сразу в нескольких проектах организации. + +В результате, в нашем приложении каждый *эндпоинт* модуля `admin` будет иметь: + +* Префикс `/admin`. +* Тег `admin`. +* Зависимость `get_token_header`. +* Ответ `418`. 🍵 + +Это будет иметь место исключительно для `APIRouter` в нашем приложении, и не затронет любой другой код, использующий его. + +Например, другие проекты, могут использовать тот же самый `APIRouter` с другими методами аутентификации. + +### Подключение отдельного *эндпоинта* + +Мы также можем добавить *эндпоинт* непосредственно в основное приложение `FastAPI`. + +Здесь мы это делаем ... просто, чтобы показать, что это возможно 🤷: + +```Python hl_lines="21-23" title="app/main.py" +{!../../docs_src/bigger_applications/app/main.py!} +``` + +и это будет работать корректно вместе с другими *эндпоинтами*, добавленными с помощью `app.include_router()`. + +/// info | Сложные технические детали + +**Примечание**: это сложная техническая деталь, которую, скорее всего, **вы можете пропустить**. + +--- + +Маршрутизаторы (`APIRouter`) не "монтируются" по-отдельности и не изолируются от остального приложения. + +Это происходит потому, что нужно включить их *эндпоинты* в OpenAPI схему и в интерфейс пользователя. + +В силу того, что мы не можем их изолировать и "примонтировать" независимо от остальных, *эндпоинты* клонируются (пересоздаются) и не подключаются напрямую. + +/// + +## Проверка автоматической документации API + +Теперь запустите приложение: + +
+ +```console +$ fastapi dev app/main.py + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +Откройте документацию по адресу http://127.0.0.1:8000/docs. + +Вы увидите автоматическую API документацию. Она включает в себя маршруты из суб-модулей, используя верные маршруты, префиксы и теги: + + + +## Подключение существующего маршрута через новый префикс (`prefix`) + +Вы можете использовать `.include_router()` несколько раз с одним и тем же маршрутом, применив различные префиксы. + +Это может быть полезным, если нужно предоставить доступ к одному и тому же API через различные префиксы, например, `/api/v1` и `/api/latest`. + +Это продвинутый способ, который вам может и не пригодится. Мы приводим его на случай, если вдруг вам это понадобится. + +## Включение одного маршрутизатора (`APIRouter`) в другой + +Точно так же, как вы включаете `APIRouter` в приложение `FastAPI`, вы можете включить `APIRouter` в другой `APIRouter`: + +```Python +router.include_router(other_router) +``` + +Удостоверьтесь, что вы сделали это до того, как подключить маршрутизатор (`router`) к вашему `FastAPI` приложению, и *эндпоинты* маршрутизатора `other_router` были также подключены. From b6f6818d76f312fe30d798e52fff16927649db0a Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 23 Jan 2025 09:47:05 +0000 Subject: [PATCH 014/117] =?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 c8a75756b..bc725fafe 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Translations + +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/bigger-applications.md`. PR [#13154](https://github.com/fastapi/fastapi/pull/13154) by [@alv2017](https://github.com/alv2017). + ## 0.115.7 ### Upgrades From 0e2d8d64a427905d673ecee9cfeaf9e74ed979e4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Jan 2025 17:05:11 +0000 Subject: [PATCH 015/117] =?UTF-8?q?=E2=AC=86=20Bump=20pypa/gh-action-pypi-?= =?UTF-8?q?publish=20from=201.12.3=20to=201.12.4=20(#13251)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.12.3 to 1.12.4. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.12.3...v1.12.4) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish 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> --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 38df75928..bf88d59b1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -35,7 +35,7 @@ jobs: TIANGOLO_BUILD_PACKAGE: ${{ matrix.package }} run: python -m build - name: Publish - uses: pypa/gh-action-pypi-publish@v1.12.3 + uses: pypa/gh-action-pypi-publish@v1.12.4 - name: Dump GitHub context env: GITHUB_CONTEXT: ${{ toJson(github) }} From 6bbd315f3e7c08893591729cfb61a19eadb73eae Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 24 Jan 2025 17:05:37 +0000 Subject: [PATCH 016/117] =?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 bc725fafe..b1020d6e4 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -11,6 +11,10 @@ hide: * 🌐 Add Russian translation for `docs/ru/docs/tutorial/bigger-applications.md`. PR [#13154](https://github.com/fastapi/fastapi/pull/13154) by [@alv2017](https://github.com/alv2017). +### Internal + +* ⬆ Bump pypa/gh-action-pypi-publish from 1.12.3 to 1.12.4. PR [#13251](https://github.com/fastapi/fastapi/pull/13251) by [@dependabot[bot]](https://github.com/apps/dependabot). + ## 0.115.7 ### Upgrades From 998a9139d3e34100874712f5a9261cdc67f8d4ed Mon Sep 17 00:00:00 2001 From: Rishat-F <66554797+Rishat-F@users.noreply.github.com> Date: Fri, 24 Jan 2025 22:44:31 +0300 Subject: [PATCH 017/117] =?UTF-8?q?=F0=9F=8C=90=20Update=20Russian=20trans?= =?UTF-8?q?lation=20for=20`docs/ru/docs/tutorial/dependencies/dependencies?= =?UTF-8?q?-in-path-operation-decorators.md`=20(#13252)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dependencies/dependencies-in-path-operation-decorators.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/ru/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md b/docs/ru/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md index f9b9dec25..0e4eb95be 100644 --- a/docs/ru/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md +++ b/docs/ru/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md @@ -18,7 +18,7 @@ Зависимости из dependencies выполнятся так же, как и обычные зависимости. Но их значения (если они были) не будут переданы в *функцию операции пути*. -/// подсказка +/// tip | Подсказка Некоторые редакторы кода определяют неиспользуемые параметры функций и подсвечивают их как ошибку. @@ -28,7 +28,7 @@ /// -/// дополнительная | информация +/// info | Примечание В этом примере мы используем выдуманные пользовательские заголовки `X-Key` и `X-Token`. From 1e44825ef2143e37e51edb86ae44d408d9dbb756 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 24 Jan 2025 19:44:54 +0000 Subject: [PATCH 018/117] =?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 b1020d6e4..f25648b9a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Translations +* 🌐 Update Russian translation for `docs/ru/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md`. PR [#13252](https://github.com/fastapi/fastapi/pull/13252) by [@Rishat-F](https://github.com/Rishat-F). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/bigger-applications.md`. PR [#13154](https://github.com/fastapi/fastapi/pull/13154) by [@alv2017](https://github.com/alv2017). ### Internal From 24eb8eeeba23d93288ef40596d621f149e648e10 Mon Sep 17 00:00:00 2001 From: Rishat-F <66554797+Rishat-F@users.noreply.github.com> Date: Mon, 27 Jan 2025 18:36:13 +0300 Subject: [PATCH 019/117] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20translat?= =?UTF-8?q?ion=20for=20`docs/ru/docs/advanced/async-tests.md`=20(#13227)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ru/docs/advanced/async-tests.md | 99 ++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 docs/ru/docs/advanced/async-tests.md diff --git a/docs/ru/docs/advanced/async-tests.md b/docs/ru/docs/advanced/async-tests.md new file mode 100644 index 000000000..7849ad109 --- /dev/null +++ b/docs/ru/docs/advanced/async-tests.md @@ -0,0 +1,99 @@ +# Асинхронное тестирование + +Вы уже видели как тестировать **FastAPI** приложение, используя имеющийся класс `TestClient`. К этому моменту вы видели только как писать тесты в синхронном стиле без использования `async` функций. + +Возможность использования асинхронных функций в ваших тестах может быть полезнa, когда, например, вы асинхронно обращаетесь к вашей базе данных. Представьте, что вы хотите отправить запросы в ваше FastAPI приложение, а затем при помощи асинхронной библиотеки для работы с базой данных удостовериться, что ваш бекэнд корректно записал данные в базу данных. + +Давайте рассмотрим, как мы можем это реализовать. + +## pytest.mark.anyio + +Если мы хотим вызывать асинхронные функции в наших тестах, то наши тестовые функции должны быть асинхронными. AnyIO предоставляет для этого отличный плагин, который позволяет нам указывать, какие тестовые функции должны вызываться асинхронно. + +## HTTPX + +Даже если **FastAPI** приложение использует обычные функции `def` вместо `async def`, это все равно `async` приложение 'под капотом'. + +Чтобы работать с асинхронным FastAPI приложением в ваших обычных тестовых функциях `def`, используя стандартный pytest, `TestClient` внутри себя делает некоторую магию. Но эта магия перестает работать, когда мы используем его внутри асинхронных функций. Запуская наши тесты асинхронно, мы больше не можем использовать `TestClient` внутри наших тестовых функций. + +`TestClient` основан на HTTPX, и, к счастью, мы можем использовать его (`HTTPX`) напрямую для тестирования API. + +## Пример + +В качестве простого примера, давайте рассмотрим файловую структуру, схожую с описанной в [Большие приложения](../tutorial/bigger-applications.md){.internal-link target=_blank} и [Тестирование](../tutorial/testing.md){.internal-link target=_blank}: + +``` +. +├── app +│   ├── __init__.py +│   ├── main.py +│   └── test_main.py +``` + +Файл `main.py`: + +{* ../../docs_src/async_tests/main.py *} + +Файл `test_main.py` содержит тесты для `main.py`, теперь он может выглядеть так: + +{* ../../docs_src/async_tests/test_main.py *} + +## Запуск тестов + +Вы можете запустить свои тесты как обычно: + +
+ +```console +$ pytest + +---> 100% +``` + +
+ +## Подробнее + +Маркер `@pytest.mark.anyio` говорит pytest, что тестовая функция должна быть вызвана асинхронно: + +{* ../../docs_src/async_tests/test_main.py hl[7] *} + +/// tip | Подсказка + +Обратите внимание, что тестовая функция теперь `async def` вместо простого `def`, как это было при использовании `TestClient`. + +/// + +Затем мы можем создать `AsyncClient` со ссылкой на приложение и посылать асинхронные запросы, используя `await`. + +{* ../../docs_src/async_tests/test_main.py hl[9:12] *} + +Это эквивалентно следующему: + +```Python +response = client.get('/') +``` + +...которое мы использовали для отправки наших запросов с `TestClient`. + +/// tip | Подсказка + +Обратите внимание, что мы используем async/await с `AsyncClient` - запрос асинхронный. + +/// + +/// warning | Внимание + +Если ваше приложение полагается на lifespan события, то `AsyncClient` не запустит эти события. Чтобы обеспечить их срабатывание используйте `LifespanManager` из florimondmanca/asgi-lifespan. + +/// + +## Вызов других асинхронных функций + +Теперь тестовая функция стала асинхронной, поэтому внутри нее вы можете вызывать также и другие `async` функции, не связанные с отправлением запросов в ваше FastAPI приложение. Как если бы вы вызывали их в любом другом месте вашего кода. + +/// tip | Подсказка + +Если вы столкнулись с `RuntimeError: Task attached to a different loop` при вызове асинхронных функций в ваших тестах (например, при использовании MongoDB's MotorClient), то не забывайте инициализировать объекты, которым нужен цикл событий (event loop), только внутри асинхронных функций, например, в `'@app.on_event("startup")` callback. + +/// From 18127b790725a3a6ca378dd99eb2cfd9f2423ccd Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 27 Jan 2025 15:36:36 +0000 Subject: [PATCH 020/117] =?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 f25648b9a..0dc660412 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Translations +* 🌐 Add Russian translation for `docs/ru/docs/advanced/async-tests.md`. PR [#13227](https://github.com/fastapi/fastapi/pull/13227) by [@Rishat-F](https://github.com/Rishat-F). * 🌐 Update Russian translation for `docs/ru/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md`. PR [#13252](https://github.com/fastapi/fastapi/pull/13252) by [@Rishat-F](https://github.com/Rishat-F). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/bigger-applications.md`. PR [#13154](https://github.com/fastapi/fastapi/pull/13154) by [@alv2017](https://github.com/alv2017). From 8f359273b5663aecdc989c762fad3804c3a6df80 Mon Sep 17 00:00:00 2001 From: k94-ishi <32672580+k94-ishi@users.noreply.github.com> Date: Tue, 28 Jan 2025 00:39:04 +0900 Subject: [PATCH 021/117] =?UTF-8?q?=F0=9F=8C=90=20Add=20Japanese=20transla?= =?UTF-8?q?tion=20for=20`docs/ja/docs/environment-variables.md`=20(#13226)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ja/docs/environment-variables.md | 301 ++++++++++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 docs/ja/docs/environment-variables.md diff --git a/docs/ja/docs/environment-variables.md b/docs/ja/docs/environment-variables.md new file mode 100644 index 000000000..507af3a0c --- /dev/null +++ b/docs/ja/docs/environment-variables.md @@ -0,0 +1,301 @@ +# 環境変数 + +/// tip + +もし、「環境変数」とは何か、それをどう使うかを既に知っている場合は、このセクションをスキップして構いません。 + +/// + +環境変数(**env var**とも呼ばれる)はPythonコードの**外側**、つまり**OS**に存在する変数で、Pythonから読み取ることができます。(他のプログラムでも同様に読み取れます。) + +環境変数は、アプリケーションの**設定**の管理や、Pythonの**インストール**などに役立ちます。 + +## 環境変数の作成と使用 + +環境変数は**シェル(ターミナル)**内で**作成**して使用でき、それらにPythonは不要です。 + +//// tab | Linux, macOS, Windows Bash + +
+ +```console +// You could create an env var MY_NAME with +$ export MY_NAME="Wade Wilson" + +// Then you could use it with other programs, like +$ echo "Hello $MY_NAME" + +Hello Wade Wilson +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ + +```console +// Create an env var MY_NAME +$ $Env:MY_NAME = "Wade Wilson" + +// Use it with other programs, like +$ echo "Hello $Env:MY_NAME" + +Hello Wade Wilson +``` + +
+ +//// + +## Pythonで環境変数を読み取る + +環境変数をPythonの**外側**、ターミナル(や他の方法)で作成し、**Python内で読み取る**こともできます。 + +例えば、以下のような`main.py`ファイルを用意します: + +```Python hl_lines="3" +import os + +name = os.getenv("MY_NAME", "World") +print(f"Hello {name} from Python") +``` + +/// tip + +`os.getenv()` の第2引数は、デフォルトで返される値を指定します。 + +この引数を省略するとデフォルト値として`None`が返されますが、ここではデフォルト値として`"World"`を指定しています。 + +/// + +次に、このPythonプログラムを呼び出します。 + +//// tab | Linux, macOS, Windows Bash + +
+ +```console +// Here we don't set the env var yet +$ python main.py + +// As we didn't set the env var, we get the default value + +Hello World from Python + +// But if we create an environment variable first +$ export MY_NAME="Wade Wilson" + +// And then call the program again +$ python main.py + +// Now it can read the environment variable + +Hello Wade Wilson from Python +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ +```console +// Here we don't set the env var yet +$ python main.py + +// As we didn't set the env var, we get the default value + +Hello World from Python + +// But if we create an environment variable first +$ $Env:MY_NAME = "Wade Wilson" + +// And then call the program again +$ python main.py + +// Now it can read the environment variable + +Hello Wade Wilson from Python +``` + +
+ +//// + +環境変数はコードの外側で設定し、内側から読み取ることができるので、他のファイルと一緒に(`git`に)保存する必要がありません。そのため、環境変数をコンフィグレーションや**設定**に使用することが一般的です。 + +また、**特定のプログラムの呼び出し**のための環境変数を、そのプログラムのみ、その実行中に限定して利用できるよう作成できます。 + +そのためには、プログラム起動コマンドと同じコマンドライン上の、起動コマンド直前で環境変数を作成してください。 + +
+ +```console +// Create an env var MY_NAME in line for this program call +$ MY_NAME="Wade Wilson" python main.py + +// Now it can read the environment variable + +Hello Wade Wilson from Python + +// The env var no longer exists afterwards +$ python main.py + +Hello World from Python +``` + +
+ +/// tip + +詳しくは The Twelve-Factor App: Config を参照してください。 + +/// + +## 型とバリデーション + +環境変数は**テキスト文字列**のみを扱うことができます。これは、環境変数がPython外部に存在し、他のプログラムやシステム全体(Linux、Windows、macOS間の互換性を含む)と連携する必要があるためです。 + +つまり、Pythonが環境変数から読み取る**あらゆる値**は **`str`型となり**、他の型への変換やバリデーションはコード内で行う必要があります。 + +環境変数を使用して**アプリケーション設定**を管理する方法については、[高度なユーザーガイド - Settings and Environment Variables](./advanced/settings.md){.internal-link target=_blank}で詳しく学べます。 + +## `PATH`環境変数 + +**`PATH`**という**特別な**環境変数があります。この環境変数は、OS(Linux、macOS、Windows)が実行するプログラムを発見するために使用されます。 + +`PATH`変数は、複数のディレクトリのパスから成る長い文字列です。このパスはLinuxやMacOSの場合は`:`で、Windowsの場合は`;`で区切られています。 + +例えば、`PATH`環境変数は次のような文字列かもしれません: + +//// tab | Linux, macOS + +```plaintext +/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin +``` + +これは、OSはプログラムを見つけるために以下のディレクトリを探す、ということを意味します: + +* `/usr/local/bin` +* `/usr/bin` +* `/bin` +* `/usr/sbin` +* `/sbin` + +//// + +//// tab | Windows + +```plaintext +C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32 +``` + +これは、OSはプログラムを見つけるために以下のディレクトリを探す、ということを意味します: + +* `C:\Program Files\Python312\Scripts` +* `C:\Program Files\Python312` +* `C:\Windows\System32` + +//// + +ターミナル上で**コマンド**を入力すると、 OSはそのプログラムを見つけるために、`PATH`環境変数のリストに記載された**それぞれのディレクトリを探し**ます。 + +例えば、ターミナル上で`python`を入力すると、OSは`python`によって呼ばれるプログラムを見つけるために、そのリストの**先頭のディレクトリ**を最初に探します。 + +OSは、もしそのプログラムをそこで発見すれば**実行し**ますが、そうでなければリストの**他のディレクトリ**を探していきます。 + +### PythonのインストールとPATH環境変数の更新 + +Pythonのインストール時に`PATH`環境変数を更新したいか聞かれるかもしれません。 + +/// tab | Linux, macOS + +Pythonをインストールして、そのプログラムが`/opt/custompython/bin`というディレクトリに配置されたとします。 + +もし、`PATH`環境変数を更新するように答えると、`PATH`環境変数に`/opt/custompython/bin`が追加されます。 + +`PATH`環境変数は以下のように更新されるでしょう: + +``` plaintext +/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/custompython/bin +``` + +このようにして、ターミナルで`python`と入力したときに、OSは`/opt/custompython/bin`(リストの末尾のディレクトリ)にあるPythonプログラムを見つけ、使用します。 + +/// + +/// tab | Windows + +Pythonをインストールして、そのプログラムが`C:\opt\custompython\bin`というディレクトリに配置されたとします。 + +もし、`PATH`環境変数を更新するように答えると、`PATH`環境変数に`C:\opt\custompython\bin`が追加されます。 + +`PATH`環境変数は以下のように更新されるでしょう: + +```plaintext +C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32;C:\opt\custompython\bin +``` + +このようにして、ターミナルで`python`と入力したときに、OSは`C:\opt\custompython\bin\python`(リストの末尾のディレクトリ)にあるPythonプログラムを見つけ、使用します。 + +/// + +つまり、ターミナルで以下のコマンドを入力すると: + +
+ +``` console +$ python +``` + +
+ +/// tab | Linux, macOS + +OSは`/opt/custompython/bin`にある`python`プログラムを**見つけ**て実行します。 + +これは、次のコマンドを入力した場合とほとんど同等です: + +
+ +```console +$ /opt/custompython/bin/python +``` + +
+ +/// + +/// tab | Windows + +OSは`C:\opt\custompython\bin\python`にある`python`プログラムを**見つけ**て実行します。 + +これは、次のコマンドを入力した場合とほとんど同等です: + +
+ +```console +$ C:\opt\custompython\bin\python +``` + +
+ +/// + +この情報は、[Virtual Environments](virtual-environments.md) について学ぶ際にも役立ちます。 + +## まとめ + +これで、**環境変数**とは何か、Pythonでどのように使用するかについて、基本的な理解が得られたはずです。 + +環境変数についての詳細は、Wikipedia: Environment Variable を参照してください。 + +環境変数の用途や適用方法が最初は直感的ではないかもしれませんが、開発中のさまざまなシナリオで繰り返し登場します。そのため、基本を知っておくことが重要です。 + +たとえば、この情報は次のセクションで扱う[Virtual Environments](virtual-environments.md)にも関連します。 From d2f5097dedcfb1eb695d21dbb19a5593cf1bd756 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 27 Jan 2025 15:39:30 +0000 Subject: [PATCH 022/117] =?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 0dc660412..76333d313 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Translations +* 🌐 Add Japanese translation for `docs/ja/docs/environment-variables.md`. PR [#13226](https://github.com/fastapi/fastapi/pull/13226) by [@k94-ishi](https://github.com/k94-ishi). * 🌐 Add Russian translation for `docs/ru/docs/advanced/async-tests.md`. PR [#13227](https://github.com/fastapi/fastapi/pull/13227) by [@Rishat-F](https://github.com/Rishat-F). * 🌐 Update Russian translation for `docs/ru/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md`. PR [#13252](https://github.com/fastapi/fastapi/pull/13252) by [@Rishat-F](https://github.com/Rishat-F). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/bigger-applications.md`. PR [#13154](https://github.com/fastapi/fastapi/pull/13154) by [@alv2017](https://github.com/alv2017). From ff68d0894a7191f7dd107bd449e6bd40b3af1735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 28 Jan 2025 20:34:56 +0000 Subject: [PATCH 023/117] =?UTF-8?q?=F0=9F=94=A8=20Update=20FastAPI=20Peopl?= =?UTF-8?q?e=20Experts=20script,=20refactor=20and=20optimize=20data=20fetc?= =?UTF-8?q?hing=20to=20handle=20rate=20limits=20(#13267)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/actions/people/Dockerfile | 7 - .github/actions/people/action.yml | 10 - .github/actions/people/app/main.py | 682 ----------------------------- .github/workflows/people.yml | 37 +- docs/en/docs/fastapi-people.md | 33 ++ scripts/people.py | 401 +++++++++++++++++ 6 files changed, 462 insertions(+), 708 deletions(-) delete mode 100644 .github/actions/people/Dockerfile delete mode 100644 .github/actions/people/action.yml delete mode 100644 .github/actions/people/app/main.py create mode 100644 scripts/people.py diff --git a/.github/actions/people/Dockerfile b/.github/actions/people/Dockerfile deleted file mode 100644 index 1455106bd..000000000 --- a/.github/actions/people/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM python:3.9 - -RUN pip install httpx PyGithub "pydantic==2.0.2" pydantic-settings "pyyaml>=5.3.1,<6.0.0" - -COPY ./app /app - -CMD ["python", "/app/main.py"] diff --git a/.github/actions/people/action.yml b/.github/actions/people/action.yml deleted file mode 100644 index 71745b874..000000000 --- a/.github/actions/people/action.yml +++ /dev/null @@ -1,10 +0,0 @@ -name: "Generate FastAPI People" -description: "Generate the data for the FastAPI People page" -author: "Sebastián Ramírez " -inputs: - token: - description: 'User token, to read the GitHub API. Can be passed in using {{ secrets.FASTAPI_PEOPLE }}' - required: true -runs: - using: 'docker' - image: 'Dockerfile' diff --git a/.github/actions/people/app/main.py b/.github/actions/people/app/main.py deleted file mode 100644 index b752d9d2b..000000000 --- a/.github/actions/people/app/main.py +++ /dev/null @@ -1,682 +0,0 @@ -import logging -import subprocess -import sys -from collections import Counter, defaultdict -from datetime import datetime, timedelta, timezone -from pathlib import Path -from typing import Any, Container, DefaultDict, Dict, List, Set, Union - -import httpx -import yaml -from github import Github -from pydantic import BaseModel, SecretStr -from pydantic_settings import BaseSettings - -github_graphql_url = "https://api.github.com/graphql" -questions_category_id = "MDE4OkRpc2N1c3Npb25DYXRlZ29yeTMyMDAxNDM0" - -discussions_query = """ -query Q($after: String, $category_id: ID) { - repository(name: "fastapi", owner: "fastapi") { - discussions(first: 100, after: $after, categoryId: $category_id) { - edges { - cursor - node { - number - author { - login - avatarUrl - url - } - title - createdAt - comments(first: 100) { - nodes { - createdAt - author { - login - avatarUrl - url - } - isAnswer - replies(first: 10) { - nodes { - createdAt - author { - login - avatarUrl - url - } - } - } - } - } - } - } - } - } -} -""" - - -prs_query = """ -query Q($after: String) { - repository(name: "fastapi", owner: "fastapi") { - pullRequests(first: 100, after: $after) { - edges { - cursor - node { - number - labels(first: 100) { - nodes { - name - } - } - author { - login - avatarUrl - url - } - title - createdAt - state - comments(first: 100) { - nodes { - createdAt - author { - login - avatarUrl - url - } - } - } - reviews(first:100) { - nodes { - author { - login - avatarUrl - url - } - state - } - } - } - } - } - } -} -""" - -sponsors_query = """ -query Q($after: String) { - user(login: "fastapi") { - sponsorshipsAsMaintainer(first: 100, after: $after) { - edges { - cursor - node { - sponsorEntity { - ... on Organization { - login - avatarUrl - url - } - ... on User { - login - avatarUrl - url - } - } - tier { - name - monthlyPriceInDollars - } - } - } - } - } -} -""" - - -class Author(BaseModel): - login: str - avatarUrl: str - url: str - - -# Discussions - - -class CommentsNode(BaseModel): - createdAt: datetime - author: Union[Author, None] = None - - -class Replies(BaseModel): - nodes: List[CommentsNode] - - -class DiscussionsCommentsNode(CommentsNode): - replies: Replies - - -class Comments(BaseModel): - nodes: List[CommentsNode] - - -class DiscussionsComments(BaseModel): - nodes: List[DiscussionsCommentsNode] - - -class DiscussionsNode(BaseModel): - number: int - author: Union[Author, None] = None - title: str - createdAt: datetime - comments: DiscussionsComments - - -class DiscussionsEdge(BaseModel): - cursor: str - node: DiscussionsNode - - -class Discussions(BaseModel): - edges: List[DiscussionsEdge] - - -class DiscussionsRepository(BaseModel): - discussions: Discussions - - -class DiscussionsResponseData(BaseModel): - repository: DiscussionsRepository - - -class DiscussionsResponse(BaseModel): - data: DiscussionsResponseData - - -# PRs - - -class LabelNode(BaseModel): - name: str - - -class Labels(BaseModel): - nodes: List[LabelNode] - - -class ReviewNode(BaseModel): - author: Union[Author, None] = None - state: str - - -class Reviews(BaseModel): - nodes: List[ReviewNode] - - -class PullRequestNode(BaseModel): - number: int - labels: Labels - author: Union[Author, None] = None - title: str - createdAt: datetime - state: str - comments: Comments - reviews: Reviews - - -class PullRequestEdge(BaseModel): - cursor: str - node: PullRequestNode - - -class PullRequests(BaseModel): - edges: List[PullRequestEdge] - - -class PRsRepository(BaseModel): - pullRequests: PullRequests - - -class PRsResponseData(BaseModel): - repository: PRsRepository - - -class PRsResponse(BaseModel): - data: PRsResponseData - - -# Sponsors - - -class SponsorEntity(BaseModel): - login: str - avatarUrl: str - url: str - - -class Tier(BaseModel): - name: str - monthlyPriceInDollars: float - - -class SponsorshipAsMaintainerNode(BaseModel): - sponsorEntity: SponsorEntity - tier: Tier - - -class SponsorshipAsMaintainerEdge(BaseModel): - cursor: str - node: SponsorshipAsMaintainerNode - - -class SponsorshipAsMaintainer(BaseModel): - edges: List[SponsorshipAsMaintainerEdge] - - -class SponsorsUser(BaseModel): - sponsorshipsAsMaintainer: SponsorshipAsMaintainer - - -class SponsorsResponseData(BaseModel): - user: SponsorsUser - - -class SponsorsResponse(BaseModel): - data: SponsorsResponseData - - -class Settings(BaseSettings): - input_token: SecretStr - github_repository: str - httpx_timeout: int = 30 - - -def get_graphql_response( - *, - settings: Settings, - query: str, - after: Union[str, None] = None, - category_id: Union[str, None] = None, -) -> Dict[str, Any]: - headers = {"Authorization": f"token {settings.input_token.get_secret_value()}"} - # category_id is only used by one query, but GraphQL allows unused variables, so - # keep it here for simplicity - variables = {"after": after, "category_id": category_id} - response = httpx.post( - github_graphql_url, - headers=headers, - timeout=settings.httpx_timeout, - json={"query": query, "variables": variables, "operationName": "Q"}, - ) - if response.status_code != 200: - logging.error( - f"Response was not 200, after: {after}, category_id: {category_id}" - ) - logging.error(response.text) - raise RuntimeError(response.text) - data = response.json() - if "errors" in data: - logging.error(f"Errors in response, after: {after}, category_id: {category_id}") - logging.error(data["errors"]) - logging.error(response.text) - raise RuntimeError(response.text) - return data - - -def get_graphql_question_discussion_edges( - *, - settings: Settings, - after: Union[str, None] = None, -): - data = get_graphql_response( - settings=settings, - query=discussions_query, - after=after, - category_id=questions_category_id, - ) - graphql_response = DiscussionsResponse.model_validate(data) - return graphql_response.data.repository.discussions.edges - - -def get_graphql_pr_edges(*, settings: Settings, after: Union[str, None] = None): - data = get_graphql_response(settings=settings, query=prs_query, after=after) - graphql_response = PRsResponse.model_validate(data) - return graphql_response.data.repository.pullRequests.edges - - -def get_graphql_sponsor_edges(*, settings: Settings, after: Union[str, None] = None): - data = get_graphql_response(settings=settings, query=sponsors_query, after=after) - graphql_response = SponsorsResponse.model_validate(data) - return graphql_response.data.user.sponsorshipsAsMaintainer.edges - - -class DiscussionExpertsResults(BaseModel): - commenters: Counter - last_month_commenters: Counter - three_months_commenters: Counter - six_months_commenters: Counter - one_year_commenters: Counter - authors: Dict[str, Author] - - -def get_discussion_nodes(settings: Settings) -> List[DiscussionsNode]: - discussion_nodes: List[DiscussionsNode] = [] - discussion_edges = get_graphql_question_discussion_edges(settings=settings) - - while discussion_edges: - for discussion_edge in discussion_edges: - discussion_nodes.append(discussion_edge.node) - last_edge = discussion_edges[-1] - discussion_edges = get_graphql_question_discussion_edges( - settings=settings, after=last_edge.cursor - ) - return discussion_nodes - - -def get_discussions_experts( - discussion_nodes: List[DiscussionsNode], -) -> DiscussionExpertsResults: - commenters = Counter() - last_month_commenters = Counter() - three_months_commenters = Counter() - six_months_commenters = Counter() - one_year_commenters = Counter() - authors: Dict[str, Author] = {} - - now = datetime.now(tz=timezone.utc) - one_month_ago = now - timedelta(days=30) - three_months_ago = now - timedelta(days=90) - six_months_ago = now - timedelta(days=180) - one_year_ago = now - timedelta(days=365) - - for discussion in discussion_nodes: - discussion_author_name = None - if discussion.author: - authors[discussion.author.login] = discussion.author - discussion_author_name = discussion.author.login - discussion_commentors: dict[str, datetime] = {} - for comment in discussion.comments.nodes: - if comment.author: - authors[comment.author.login] = comment.author - if comment.author.login != discussion_author_name: - author_time = discussion_commentors.get( - comment.author.login, comment.createdAt - ) - discussion_commentors[comment.author.login] = max( - author_time, comment.createdAt - ) - for reply in comment.replies.nodes: - if reply.author: - authors[reply.author.login] = reply.author - if reply.author.login != discussion_author_name: - author_time = discussion_commentors.get( - reply.author.login, reply.createdAt - ) - discussion_commentors[reply.author.login] = max( - author_time, reply.createdAt - ) - for author_name, author_time in discussion_commentors.items(): - commenters[author_name] += 1 - if author_time > one_month_ago: - last_month_commenters[author_name] += 1 - if author_time > three_months_ago: - three_months_commenters[author_name] += 1 - if author_time > six_months_ago: - six_months_commenters[author_name] += 1 - if author_time > one_year_ago: - one_year_commenters[author_name] += 1 - discussion_experts_results = DiscussionExpertsResults( - authors=authors, - commenters=commenters, - last_month_commenters=last_month_commenters, - three_months_commenters=three_months_commenters, - six_months_commenters=six_months_commenters, - one_year_commenters=one_year_commenters, - ) - return discussion_experts_results - - -def get_pr_nodes(settings: Settings) -> List[PullRequestNode]: - pr_nodes: List[PullRequestNode] = [] - pr_edges = get_graphql_pr_edges(settings=settings) - - while pr_edges: - for edge in pr_edges: - pr_nodes.append(edge.node) - last_edge = pr_edges[-1] - pr_edges = get_graphql_pr_edges(settings=settings, after=last_edge.cursor) - return pr_nodes - - -class ContributorsResults(BaseModel): - contributors: Counter - commenters: Counter - reviewers: Counter - translation_reviewers: Counter - authors: Dict[str, Author] - - -def get_contributors(pr_nodes: List[PullRequestNode]) -> ContributorsResults: - contributors = Counter() - commenters = Counter() - reviewers = Counter() - translation_reviewers = Counter() - authors: Dict[str, Author] = {} - - for pr in pr_nodes: - author_name = None - if pr.author: - authors[pr.author.login] = pr.author - author_name = pr.author.login - pr_commentors: Set[str] = set() - pr_reviewers: Set[str] = set() - for comment in pr.comments.nodes: - if comment.author: - authors[comment.author.login] = comment.author - if comment.author.login == author_name: - continue - pr_commentors.add(comment.author.login) - for author_name in pr_commentors: - commenters[author_name] += 1 - for review in pr.reviews.nodes: - if review.author: - authors[review.author.login] = review.author - pr_reviewers.add(review.author.login) - for label in pr.labels.nodes: - if label.name == "lang-all": - translation_reviewers[review.author.login] += 1 - break - for reviewer in pr_reviewers: - reviewers[reviewer] += 1 - if pr.state == "MERGED" and pr.author: - contributors[pr.author.login] += 1 - return ContributorsResults( - contributors=contributors, - commenters=commenters, - reviewers=reviewers, - translation_reviewers=translation_reviewers, - authors=authors, - ) - - -def get_individual_sponsors(settings: Settings): - nodes: List[SponsorshipAsMaintainerNode] = [] - edges = get_graphql_sponsor_edges(settings=settings) - - while edges: - for edge in edges: - nodes.append(edge.node) - last_edge = edges[-1] - edges = get_graphql_sponsor_edges(settings=settings, after=last_edge.cursor) - - tiers: DefaultDict[float, Dict[str, SponsorEntity]] = defaultdict(dict) - for node in nodes: - tiers[node.tier.monthlyPriceInDollars][node.sponsorEntity.login] = ( - node.sponsorEntity - ) - return tiers - - -def get_top_users( - *, - counter: Counter, - authors: Dict[str, Author], - skip_users: Container[str], - min_count: int = 2, -): - users = [] - for commenter, count in counter.most_common(50): - if commenter in skip_users: - continue - if count >= min_count: - author = authors[commenter] - users.append( - { - "login": commenter, - "count": count, - "avatarUrl": author.avatarUrl, - "url": author.url, - } - ) - return users - - -if __name__ == "__main__": - logging.basicConfig(level=logging.INFO) - settings = Settings() - logging.info(f"Using config: {settings.model_dump_json()}") - g = Github(settings.input_token.get_secret_value()) - repo = g.get_repo(settings.github_repository) - discussion_nodes = get_discussion_nodes(settings=settings) - experts_results = get_discussions_experts(discussion_nodes=discussion_nodes) - pr_nodes = get_pr_nodes(settings=settings) - contributors_results = get_contributors(pr_nodes=pr_nodes) - authors = {**experts_results.authors, **contributors_results.authors} - maintainers_logins = {"tiangolo"} - bot_names = {"codecov", "github-actions", "pre-commit-ci", "dependabot"} - maintainers = [] - for login in maintainers_logins: - user = authors[login] - maintainers.append( - { - "login": login, - "answers": experts_results.commenters[login], - "prs": contributors_results.contributors[login], - "avatarUrl": user.avatarUrl, - "url": user.url, - } - ) - - skip_users = maintainers_logins | bot_names - experts = get_top_users( - counter=experts_results.commenters, - authors=authors, - skip_users=skip_users, - ) - last_month_experts = get_top_users( - counter=experts_results.last_month_commenters, - authors=authors, - skip_users=skip_users, - ) - three_months_experts = get_top_users( - counter=experts_results.three_months_commenters, - authors=authors, - skip_users=skip_users, - ) - six_months_experts = get_top_users( - counter=experts_results.six_months_commenters, - authors=authors, - skip_users=skip_users, - ) - one_year_experts = get_top_users( - counter=experts_results.one_year_commenters, - authors=authors, - skip_users=skip_users, - ) - top_contributors = get_top_users( - counter=contributors_results.contributors, - authors=authors, - skip_users=skip_users, - ) - top_reviewers = get_top_users( - counter=contributors_results.reviewers, - authors=authors, - skip_users=skip_users, - ) - top_translations_reviewers = get_top_users( - counter=contributors_results.translation_reviewers, - authors=authors, - skip_users=skip_users, - ) - - tiers = get_individual_sponsors(settings=settings) - keys = list(tiers.keys()) - keys.sort(reverse=True) - sponsors = [] - for key in keys: - sponsor_group = [] - for login, sponsor in tiers[key].items(): - sponsor_group.append( - {"login": login, "avatarUrl": sponsor.avatarUrl, "url": sponsor.url} - ) - sponsors.append(sponsor_group) - - people = { - "maintainers": maintainers, - "experts": experts, - "last_month_experts": last_month_experts, - "three_months_experts": three_months_experts, - "six_months_experts": six_months_experts, - "one_year_experts": one_year_experts, - "top_contributors": top_contributors, - "top_reviewers": top_reviewers, - "top_translations_reviewers": top_translations_reviewers, - } - github_sponsors = { - "sponsors": sponsors, - } - # For local development - # people_path = Path("../../../../docs/en/data/people.yml") - people_path = Path("./docs/en/data/people.yml") - github_sponsors_path = Path("./docs/en/data/github_sponsors.yml") - people_old_content = people_path.read_text(encoding="utf-8") - github_sponsors_old_content = github_sponsors_path.read_text(encoding="utf-8") - new_people_content = yaml.dump( - people, sort_keys=False, width=200, allow_unicode=True - ) - new_github_sponsors_content = yaml.dump( - github_sponsors, sort_keys=False, width=200, allow_unicode=True - ) - if ( - people_old_content == new_people_content - and github_sponsors_old_content == new_github_sponsors_content - ): - logging.info("The FastAPI People data hasn't changed, finishing.") - sys.exit(0) - people_path.write_text(new_people_content, encoding="utf-8") - github_sponsors_path.write_text(new_github_sponsors_content, encoding="utf-8") - logging.info("Setting up GitHub Actions git user") - subprocess.run(["git", "config", "user.name", "github-actions"], check=True) - subprocess.run( - ["git", "config", "user.email", "github-actions@github.com"], check=True - ) - branch_name = "fastapi-people" - logging.info(f"Creating a new branch {branch_name}") - subprocess.run(["git", "checkout", "-b", branch_name], check=True) - logging.info("Adding updated file") - subprocess.run( - ["git", "add", str(people_path), str(github_sponsors_path)], check=True - ) - logging.info("Committing updated file") - message = "👥 Update FastAPI People" - result = subprocess.run(["git", "commit", "-m", message], check=True) - logging.info("Pushing branch") - subprocess.run(["git", "push", "origin", branch_name], check=True) - logging.info("Creating PR") - pr = repo.create_pull(title=message, body=message, base="master", head=branch_name) - logging.info(f"Created PR: {pr.number}") - logging.info("Finished") diff --git a/.github/workflows/people.yml b/.github/workflows/people.yml index c60c63d1b..6ec3c1ad2 100644 --- a/.github/workflows/people.yml +++ b/.github/workflows/people.yml @@ -6,29 +6,48 @@ on: workflow_dispatch: inputs: debug_enabled: - description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' + description: Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate) required: false - default: 'false' + default: "false" + +env: + UV_SYSTEM_PYTHON: 1 jobs: - fastapi-people: + job: if: github.repository_owner == 'fastapi' runs-on: ubuntu-latest + permissions: + contents: write steps: - name: Dump GitHub context env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - uses: actions/checkout@v4 - # Ref: https://github.com/actions/runner/issues/2033 - - name: Fix git safe.directory in container - run: mkdir -p /home/runner/work/_temp/_github_home && printf "[safe]\n\tdirectory = /github/workspace" > /home/runner/work/_temp/_github_home/.gitconfig + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Setup uv + uses: astral-sh/setup-uv@v5 + with: + version: "0.4.15" + enable-cache: true + cache-dependency-glob: | + requirements**.txt + pyproject.toml + - name: Install Dependencies + run: uv pip install -r requirements-github-actions.txt # Allow debugging with tmate - name: Setup tmate session uses: mxschmitt/action-tmate@v3 if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} with: limit-access-to-actor: true - - uses: ./.github/actions/people - with: - token: ${{ secrets.FASTAPI_PEOPLE }} + env: + GITHUB_TOKEN: ${{ secrets.FASTAPI_PEOPLE }} + - name: FastAPI People Experts + run: python ./scripts/people.py + env: + GITHUB_TOKEN: ${{ secrets.FASTAPI_PEOPLE }} diff --git a/docs/en/docs/fastapi-people.md b/docs/en/docs/fastapi-people.md index ffc579b10..f2ca26013 100644 --- a/docs/en/docs/fastapi-people.md +++ b/docs/en/docs/fastapi-people.md @@ -47,9 +47,11 @@ This is the current list of team members. 😎 They have different levels of involvement and permissions, they can perform [repository management tasks](./management-tasks.md){.internal-link target=_blank} and together we [manage the FastAPI repository](./management.md){.internal-link target=_blank}.
+ {% for user in members["members"] %} + {% endfor %}
@@ -83,9 +85,15 @@ You can see the **FastAPI Experts** for: These are the users that have been [helping others the most with questions in GitHub](help-fastapi.md#help-others-with-questions-in-github){.internal-link target=_blank} during the last month. 🤓
+ {% for user in people.last_month_experts[:10] %} +{% if user.login not in skip_users %} +
@{{ user.login }}
Questions replied: {{ user.count }}
+ +{% endif %} + {% endfor %}
@@ -95,9 +103,15 @@ These are the users that have been [helping others the most with questions in Gi These are the users that have been [helping others the most with questions in GitHub](help-fastapi.md#help-others-with-questions-in-github){.internal-link target=_blank} during the last 3 months. 😎
+ {% for user in people.three_months_experts[:10] %} +{% if user.login not in skip_users %} +
@{{ user.login }}
Questions replied: {{ user.count }}
+ +{% endif %} + {% endfor %}
@@ -107,9 +121,15 @@ These are the users that have been [helping others the most with questions in Gi These are the users that have been [helping others the most with questions in GitHub](help-fastapi.md#help-others-with-questions-in-github){.internal-link target=_blank} during the last 6 months. 🧐
+ {% for user in people.six_months_experts[:10] %} +{% if user.login not in skip_users %} +
@{{ user.login }}
Questions replied: {{ user.count }}
+ +{% endif %} + {% endfor %}
@@ -119,9 +139,15 @@ These are the users that have been [helping others the most with questions in Gi These are the users that have been [helping others the most with questions in GitHub](help-fastapi.md#help-others-with-questions-in-github){.internal-link target=_blank} during the last year. 🧑‍🔬
+ {% for user in people.one_year_experts[:20] %} +{% if user.login not in skip_users %} +
@{{ user.login }}
Questions replied: {{ user.count }}
+ +{% endif %} + {% endfor %}
@@ -133,9 +159,15 @@ Here are the all time **FastAPI Experts**. 🤓🤯 These are the users that have [helped others the most with questions in GitHub](help-fastapi.md#help-others-with-questions-in-github){.internal-link target=_blank} through *all time*. 🧙
+ {% for user in people.experts[:50] %} +{% if user.login not in skip_users %} +
@{{ user.login }}
Questions replied: {{ user.count }}
+ +{% endif %} + {% endfor %}
@@ -149,6 +181,7 @@ These users have [created the most Pull Requests](help-fastapi.md#create-a-pull- They have contributed source code, documentation, etc. 📦
+ {% for user in (contributors.values() | list)[:50] %} {% if user.login not in skip_users %} diff --git a/scripts/people.py b/scripts/people.py new file mode 100644 index 000000000..f61fd31c9 --- /dev/null +++ b/scripts/people.py @@ -0,0 +1,401 @@ +import logging +import secrets +import subprocess +import time +from collections import Counter +from datetime import datetime, timedelta, timezone +from pathlib import Path +from typing import Any, Container, Union + +import httpx +import yaml +from github import Github +from pydantic import BaseModel, SecretStr +from pydantic_settings import BaseSettings + +github_graphql_url = "https://api.github.com/graphql" +questions_category_id = "MDE4OkRpc2N1c3Npb25DYXRlZ29yeTMyMDAxNDM0" + +discussions_query = """ +query Q($after: String, $category_id: ID) { + repository(name: "fastapi", owner: "fastapi") { + discussions(first: 100, after: $after, categoryId: $category_id) { + edges { + cursor + node { + number + author { + login + avatarUrl + url + } + createdAt + comments(first: 50) { + totalCount + nodes { + createdAt + author { + login + avatarUrl + url + } + isAnswer + replies(first: 10) { + totalCount + nodes { + createdAt + author { + login + avatarUrl + url + } + } + } + } + } + } + } + } + } +} +""" + + +class Author(BaseModel): + login: str + avatarUrl: str | None = None + url: str | None = None + + +class CommentsNode(BaseModel): + createdAt: datetime + author: Union[Author, None] = None + + +class Replies(BaseModel): + totalCount: int + nodes: list[CommentsNode] + + +class DiscussionsCommentsNode(CommentsNode): + replies: Replies + + +class DiscussionsComments(BaseModel): + totalCount: int + nodes: list[DiscussionsCommentsNode] + + +class DiscussionsNode(BaseModel): + number: int + author: Union[Author, None] = None + title: str | None = None + createdAt: datetime + comments: DiscussionsComments + + +class DiscussionsEdge(BaseModel): + cursor: str + node: DiscussionsNode + + +class Discussions(BaseModel): + edges: list[DiscussionsEdge] + + +class DiscussionsRepository(BaseModel): + discussions: Discussions + + +class DiscussionsResponseData(BaseModel): + repository: DiscussionsRepository + + +class DiscussionsResponse(BaseModel): + data: DiscussionsResponseData + + +class Settings(BaseSettings): + github_token: SecretStr + github_repository: str + httpx_timeout: int = 30 + + +def get_graphql_response( + *, + settings: Settings, + query: str, + after: Union[str, None] = None, + category_id: Union[str, None] = None, +) -> dict[str, Any]: + headers = {"Authorization": f"token {settings.github_token.get_secret_value()}"} + variables = {"after": after, "category_id": category_id} + response = httpx.post( + github_graphql_url, + headers=headers, + timeout=settings.httpx_timeout, + json={"query": query, "variables": variables, "operationName": "Q"}, + ) + if response.status_code != 200: + logging.error( + f"Response was not 200, after: {after}, category_id: {category_id}" + ) + logging.error(response.text) + raise RuntimeError(response.text) + data = response.json() + if "errors" in data: + logging.error(f"Errors in response, after: {after}, category_id: {category_id}") + logging.error(data["errors"]) + logging.error(response.text) + raise RuntimeError(response.text) + return data + + +def get_graphql_question_discussion_edges( + *, + settings: Settings, + after: Union[str, None] = None, +) -> list[DiscussionsEdge]: + data = get_graphql_response( + settings=settings, + query=discussions_query, + after=after, + category_id=questions_category_id, + ) + graphql_response = DiscussionsResponse.model_validate(data) + return graphql_response.data.repository.discussions.edges + + +class DiscussionExpertsResults(BaseModel): + commenters: Counter[str] + last_month_commenters: Counter[str] + three_months_commenters: Counter[str] + six_months_commenters: Counter[str] + one_year_commenters: Counter[str] + authors: dict[str, Author] + + +def get_discussion_nodes(settings: Settings) -> list[DiscussionsNode]: + discussion_nodes: list[DiscussionsNode] = [] + discussion_edges = get_graphql_question_discussion_edges(settings=settings) + + while discussion_edges: + for discussion_edge in discussion_edges: + discussion_nodes.append(discussion_edge.node) + last_edge = discussion_edges[-1] + # Handle GitHub secondary rate limits, requests per minute + time.sleep(5) + discussion_edges = get_graphql_question_discussion_edges( + settings=settings, after=last_edge.cursor + ) + return discussion_nodes + + +def get_discussions_experts( + discussion_nodes: list[DiscussionsNode], +) -> DiscussionExpertsResults: + commenters = Counter[str]() + last_month_commenters = Counter[str]() + three_months_commenters = Counter[str]() + six_months_commenters = Counter[str]() + one_year_commenters = Counter[str]() + authors: dict[str, Author] = {} + + now = datetime.now(tz=timezone.utc) + one_month_ago = now - timedelta(days=30) + three_months_ago = now - timedelta(days=90) + six_months_ago = now - timedelta(days=180) + one_year_ago = now - timedelta(days=365) + + for discussion in discussion_nodes: + discussion_author_name = None + if discussion.author: + authors[discussion.author.login] = discussion.author + discussion_author_name = discussion.author.login + discussion_commentors: dict[str, datetime] = {} + for comment in discussion.comments.nodes: + if comment.author: + authors[comment.author.login] = comment.author + if comment.author.login != discussion_author_name: + author_time = discussion_commentors.get( + comment.author.login, comment.createdAt + ) + discussion_commentors[comment.author.login] = max( + author_time, comment.createdAt + ) + for reply in comment.replies.nodes: + if reply.author: + authors[reply.author.login] = reply.author + if reply.author.login != discussion_author_name: + author_time = discussion_commentors.get( + reply.author.login, reply.createdAt + ) + discussion_commentors[reply.author.login] = max( + author_time, reply.createdAt + ) + for author_name, author_time in discussion_commentors.items(): + commenters[author_name] += 1 + if author_time > one_month_ago: + last_month_commenters[author_name] += 1 + if author_time > three_months_ago: + three_months_commenters[author_name] += 1 + if author_time > six_months_ago: + six_months_commenters[author_name] += 1 + if author_time > one_year_ago: + one_year_commenters[author_name] += 1 + discussion_experts_results = DiscussionExpertsResults( + authors=authors, + commenters=commenters, + last_month_commenters=last_month_commenters, + three_months_commenters=three_months_commenters, + six_months_commenters=six_months_commenters, + one_year_commenters=one_year_commenters, + ) + return discussion_experts_results + + +def get_top_users( + *, + counter: Counter[str], + authors: dict[str, Author], + skip_users: Container[str], + min_count: int = 2, +) -> list[dict[str, Any]]: + users: list[dict[str, Any]] = [] + for commenter, count in counter.most_common(50): + if commenter in skip_users: + continue + if count >= min_count: + author = authors[commenter] + users.append( + { + "login": commenter, + "count": count, + "avatarUrl": author.avatarUrl, + "url": author.url, + } + ) + return users + + +def get_users_to_write( + *, + counter: Counter[str], + authors: dict[str, Author], + min_count: int = 2, +) -> list[dict[str, Any]]: + users: dict[str, Any] = {} + users_list: list[dict[str, Any]] = [] + for user, count in counter.most_common(60): + if count >= min_count: + author = authors[user] + user_data = { + "login": user, + "count": count, + "avatarUrl": author.avatarUrl, + "url": author.url, + } + users[user] = user_data + users_list.append(user_data) + return users_list + + +def update_content(*, content_path: Path, new_content: Any) -> bool: + old_content = content_path.read_text(encoding="utf-8") + + new_content = yaml.dump(new_content, sort_keys=False, width=200, allow_unicode=True) + if old_content == new_content: + logging.info(f"The content hasn't changed for {content_path}") + return False + content_path.write_text(new_content, encoding="utf-8") + logging.info(f"Updated {content_path}") + return True + + +def main() -> None: + logging.basicConfig(level=logging.INFO) + settings = Settings() + logging.info(f"Using config: {settings.model_dump_json()}") + g = Github(settings.github_token.get_secret_value()) + repo = g.get_repo(settings.github_repository) + + discussion_nodes = get_discussion_nodes(settings=settings) + experts_results = get_discussions_experts(discussion_nodes=discussion_nodes) + + authors = experts_results.authors + maintainers_logins = {"tiangolo"} + maintainers = [] + for login in maintainers_logins: + user = authors[login] + maintainers.append( + { + "login": login, + "answers": experts_results.commenters[login], + "avatarUrl": user.avatarUrl, + "url": user.url, + } + ) + + experts = get_users_to_write( + counter=experts_results.commenters, + authors=authors, + ) + last_month_experts = get_users_to_write( + counter=experts_results.last_month_commenters, + authors=authors, + ) + three_months_experts = get_users_to_write( + counter=experts_results.three_months_commenters, + authors=authors, + ) + six_months_experts = get_users_to_write( + counter=experts_results.six_months_commenters, + authors=authors, + ) + one_year_experts = get_users_to_write( + counter=experts_results.one_year_commenters, + authors=authors, + ) + + people = { + "maintainers": maintainers, + "experts": experts, + "last_month_experts": last_month_experts, + "three_months_experts": three_months_experts, + "six_months_experts": six_months_experts, + "one_year_experts": one_year_experts, + } + + # For local development + # people_path = Path("../docs/en/data/people.yml") + people_path = Path("./docs/en/data/people.yml") + + updated = update_content(content_path=people_path, new_content=people) + + if not updated: + logging.info("The data hasn't changed, finishing.") + return + + logging.info("Setting up GitHub Actions git user") + subprocess.run(["git", "config", "user.name", "github-actions"], check=True) + subprocess.run( + ["git", "config", "user.email", "github-actions@github.com"], check=True + ) + branch_name = f"fastapi-people-experts-{secrets.token_hex(4)}" + logging.info(f"Creating a new branch {branch_name}") + subprocess.run(["git", "checkout", "-b", branch_name], check=True) + logging.info("Adding updated file") + subprocess.run(["git", "add", str(people_path)], check=True) + logging.info("Committing updated file") + message = "👥 Update FastAPI People - Experts" + subprocess.run(["git", "commit", "-m", message], check=True) + logging.info("Pushing branch") + subprocess.run(["git", "push", "origin", branch_name], check=True) + logging.info("Creating PR") + pr = repo.create_pull(title=message, body=message, base="master", head=branch_name) + logging.info(f"Created PR: {pr.number}") + logging.info("Finished") + + +if __name__ == "__main__": + main() From e925c0ec8e4dd5ad0d2d7082de280678cead8d43 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 28 Jan 2025 20:35:19 +0000 Subject: [PATCH 024/117] =?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 76333d313..3db7042fb 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -16,6 +16,7 @@ hide: ### Internal +* 🔨 Update FastAPI People Experts script, refactor and optimize data fetching to handle rate limits. PR [#13267](https://github.com/fastapi/fastapi/pull/13267) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump pypa/gh-action-pypi-publish from 1.12.3 to 1.12.4. PR [#13251](https://github.com/fastapi/fastapi/pull/13251) by [@dependabot[bot]](https://github.com/apps/dependabot). ## 0.115.7 From 3da797aeb894d82be24d4a137046b65c2c1029c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 28 Jan 2025 20:41:08 +0000 Subject: [PATCH 025/117] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20Peopl?= =?UTF-8?q?e=20-=20Experts=20(#13269)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions --- docs/en/data/people.yml | 1676 ++++++++++++++------------------------- 1 file changed, 592 insertions(+), 1084 deletions(-) diff --git a/docs/en/data/people.yml b/docs/en/data/people.yml index 02d1779e0..112567778 100644 --- a/docs/en/data/people.yml +++ b/docs/en/data/people.yml @@ -1,24 +1,35 @@ maintainers: - login: tiangolo - answers: 1885 - prs: 577 - avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=740f11212a731f56798f558ceddb0bd07642afa7&v=4 + answers: 1894 + avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 url: https://github.com/tiangolo experts: +- login: tiangolo + count: 1894 + avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 + url: https://github.com/tiangolo +- login: github-actions + count: 770 + avatarUrl: https://avatars.githubusercontent.com/in/15368?v=4 + url: https://github.com/apps/github-actions - login: Kludex - count: 608 - avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 + count: 644 + avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4 url: https://github.com/Kludex -- login: dmontagu - count: 241 - avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4 - url: https://github.com/dmontagu - login: jgould22 - count: 241 + count: 250 avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 url: https://github.com/jgould22 +- login: dmontagu + count: 240 + avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4 + url: https://github.com/dmontagu +- login: YuriiMotov + count: 223 + avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=e83a39697a2d33ab2ec9bfbced794ee48bc29cec&v=4 + url: https://github.com/YuriiMotov - login: Mause - count: 220 + count: 219 avatarUrl: https://avatars.githubusercontent.com/u/1405026?v=4 url: https://github.com/Mause - login: ycd @@ -26,7 +37,7 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=29682e4b6ac7d5293742ccf818188394b9a82972&v=4 url: https://github.com/ycd - login: JarroVGIT - count: 193 + count: 192 avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 url: https://github.com/JarroVGIT - login: euri10 @@ -41,78 +52,82 @@ experts: count: 126 avatarUrl: https://avatars.githubusercontent.com/u/331403?v=4 url: https://github.com/phy25 -- login: YuriiMotov - count: 104 - avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=e83a39697a2d33ab2ec9bfbced794ee48bc29cec&v=4 - url: https://github.com/YuriiMotov +- login: JavierSanchezCastro + count: 85 + avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 + url: https://github.com/JavierSanchezCastro - login: raphaelauv count: 83 avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 url: https://github.com/raphaelauv -- login: ArcLightSlavik - count: 71 - avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=b0f2c37142f4b762e41ad65dc49581813422bd71&v=4 - url: https://github.com/ArcLightSlavik - login: ghandic count: 71 avatarUrl: https://avatars.githubusercontent.com/u/23500353?u=e2e1d736f924d9be81e8bfc565b6d8836ba99773&v=4 url: https://github.com/ghandic -- login: JavierSanchezCastro - count: 64 - avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 - url: https://github.com/JavierSanchezCastro +- login: ArcLightSlavik + count: 71 + avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=b0f2c37142f4b762e41ad65dc49581813422bd71&v=4 + url: https://github.com/ArcLightSlavik +- login: n8sty + count: 66 + avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 + url: https://github.com/n8sty - login: falkben count: 59 avatarUrl: https://avatars.githubusercontent.com/u/653031?u=ad9838e089058c9e5a0bab94c0eec7cc181e0cd0&v=4 url: https://github.com/falkben -- login: n8sty - count: 56 - avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 - url: https://github.com/n8sty - login: acidjunk count: 50 avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 url: https://github.com/acidjunk -- login: yinziyan1206 - count: 49 - avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 - url: https://github.com/yinziyan1206 - login: sm-Fifteen count: 49 avatarUrl: https://avatars.githubusercontent.com/u/516999?u=437c0c5038558c67e887ccd863c1ba0f846c03da&v=4 url: https://github.com/sm-Fifteen -- login: insomnes - count: 45 - avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 - url: https://github.com/insomnes +- login: yinziyan1206 + count: 49 + avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 + url: https://github.com/yinziyan1206 +- login: adriangb + count: 46 + avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=612704256e38d6ac9cbed24f10e4b6ac2da74ecb&v=4 + url: https://github.com/adriangb - login: Dustyposa count: 45 avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4 url: https://github.com/Dustyposa -- login: adriangb +- login: insomnes count: 45 - avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=612704256e38d6ac9cbed24f10e4b6ac2da74ecb&v=4 - url: https://github.com/adriangb -- login: frankie567 - count: 43 - avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=c159fe047727aedecbbeeaa96a1b03ceb9d39add&v=4 - url: https://github.com/frankie567 + avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 + url: https://github.com/insomnes - login: odiseo0 count: 43 avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=241a71f6b7068738b81af3e57f45ffd723538401&v=4 url: https://github.com/odiseo0 +- login: frankie567 + count: 43 + avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=c159fe047727aedecbbeeaa96a1b03ceb9d39add&v=4 + url: https://github.com/frankie567 - login: includeamin count: 40 avatarUrl: https://avatars.githubusercontent.com/u/11836741?u=8bd5ef7e62fe6a82055e33c4c0e0a7879ff8cfb6&v=4 url: https://github.com/includeamin +- login: sinisaos + count: 39 + avatarUrl: https://avatars.githubusercontent.com/u/30960668?v=4 + url: https://github.com/sinisaos - login: chbndrhnns - count: 38 + count: 37 avatarUrl: https://avatars.githubusercontent.com/u/7534547?v=4 url: https://github.com/chbndrhnns - login: STeveShary count: 37 avatarUrl: https://avatars.githubusercontent.com/u/5167622?u=de8f597c81d6336fcebc37b32dfd61a3f877160c&v=4 url: https://github.com/STeveShary +- login: luzzodev + count: 36 + avatarUrl: https://avatars.githubusercontent.com/u/27291415?v=4 + url: https://github.com/luzzodev - login: krishnardt count: 35 avatarUrl: https://avatars.githubusercontent.com/u/31960541?u=47f4829c77f4962ab437ffb7995951e41eeebe9b&v=4 @@ -123,7 +138,7 @@ experts: url: https://github.com/panla - login: prostomarkeloff count: 28 - avatarUrl: https://avatars.githubusercontent.com/u/28061158?u=72309cc1f2e04e40fa38b29969cb4e9d3f722e7b&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/28061158?u=6918e39a1224194ba636e897461a02a20126d7ad&v=4 url: https://github.com/prostomarkeloff - login: hasansezertasan count: 27 @@ -137,10 +152,6 @@ experts: count: 25 avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4 url: https://github.com/wshayes -- login: acnebs - count: 23 - avatarUrl: https://avatars.githubusercontent.com/u/9054108?v=4 - url: https://github.com/acnebs - login: SirTelemak count: 23 avatarUrl: https://avatars.githubusercontent.com/u/9435877?u=719327b7d2c4c62212456d771bfa7c6b8dbb9eac&v=4 @@ -149,6 +160,10 @@ experts: count: 22 avatarUrl: https://avatars.githubusercontent.com/u/4216559?u=360a36fb602cded27273cbfc0afc296eece90662&v=4 url: https://github.com/nymous +- login: acnebs + count: 22 + avatarUrl: https://avatars.githubusercontent.com/u/9054108?v=4 + url: https://github.com/acnebs - login: chrisK824 count: 22 avatarUrl: https://avatars.githubusercontent.com/u/79946379?u=03d85b22d696a58a9603e55fbbbe2de6b0f4face&v=4 @@ -157,30 +172,38 @@ experts: count: 21 avatarUrl: https://avatars.githubusercontent.com/u/51059348?u=5fe59a56e1f2f9ccd8005d71752a8276f133ae1a&v=4 url: https://github.com/rafsaf +- login: ebottos94 + count: 20 + avatarUrl: https://avatars.githubusercontent.com/u/100039558?u=8b91053b3abe4a9209375e3651e1c1ef192d884b&v=4 + url: https://github.com/ebottos94 - login: nsidnev count: 20 avatarUrl: https://avatars.githubusercontent.com/u/22559461?u=a9cc3238217e21dc8796a1a500f01b722adb082c&v=4 url: https://github.com/nsidnev -- login: ebottos94 - count: 20 - avatarUrl: https://avatars.githubusercontent.com/u/100039558?u=e2c672da5a7977fd24d87ce6ab35f8bf5b1ed9fa&v=4 - url: https://github.com/ebottos94 - login: chris-allnutt count: 20 avatarUrl: https://avatars.githubusercontent.com/u/565544?v=4 url: https://github.com/chris-allnutt -- login: retnikt - count: 18 - avatarUrl: https://avatars.githubusercontent.com/u/24581770?v=4 - url: https://github.com/retnikt +- login: estebanx64 + count: 19 + avatarUrl: https://avatars.githubusercontent.com/u/10840422?u=45f015f95e1c0f06df602be4ab688d4b854cc8a8&v=4 + url: https://github.com/estebanx64 - login: zoliknemet count: 18 avatarUrl: https://avatars.githubusercontent.com/u/22326718?u=31ba446ac290e23e56eea8e4f0c558aaf0b40779&v=4 url: https://github.com/zoliknemet -- login: nkhitrov +- login: retnikt + count: 18 + avatarUrl: https://avatars.githubusercontent.com/u/24581770?v=4 + url: https://github.com/retnikt +- login: sehraramiz count: 17 - avatarUrl: https://avatars.githubusercontent.com/u/28262306?u=66ee21316275ef356081c2efc4ed7a4572e690dc&v=4 - url: https://github.com/nkhitrov + avatarUrl: https://avatars.githubusercontent.com/u/14166324?u=8fac65e84dfff24245d304a5b5b09f7b5bd69dc9&v=4 + url: https://github.com/sehraramiz +- login: caeser1996 + count: 17 + avatarUrl: https://avatars.githubusercontent.com/u/16540232?u=05d2beb8e034d584d0a374b99d8826327bd7f614&v=4 + url: https://github.com/caeser1996 - login: Hultner count: 17 avatarUrl: https://avatars.githubusercontent.com/u/2669034?u=115e53df959309898ad8dc9443fbb35fee71df07&v=4 @@ -189,1170 +212,655 @@ experts: count: 17 avatarUrl: https://avatars.githubusercontent.com/u/1765494?u=5b1ab7c582db4b4016fa31affe977d10af108ad4&v=4 url: https://github.com/harunyasar -- login: caeser1996 +- login: nkhitrov count: 17 - avatarUrl: https://avatars.githubusercontent.com/u/16540232?u=05d2beb8e034d584d0a374b99d8826327bd7f614&v=4 - url: https://github.com/caeser1996 + avatarUrl: https://avatars.githubusercontent.com/u/28262306?u=e19427d8dc296d6950e9c424adacc92d37496fe9&v=4 + url: https://github.com/nkhitrov +- login: jonatasoli + count: 16 + avatarUrl: https://avatars.githubusercontent.com/u/26334101?u=071c062d2861d3dd127f6b4a5258cd8ef55d4c50&v=4 + url: https://github.com/jonatasoli - login: dstlny count: 16 avatarUrl: https://avatars.githubusercontent.com/u/41964673?u=9f2174f9d61c15c6e3a4c9e3aeee66f711ce311f&v=4 url: https://github.com/dstlny +- login: jorgerpo + count: 15 + avatarUrl: https://avatars.githubusercontent.com/u/12537771?u=7444d20019198e34911082780cc7ad73f2b97cb3&v=4 + url: https://github.com/jorgerpo +- login: simondale00 + count: 15 + avatarUrl: https://avatars.githubusercontent.com/u/33907262?u=2721fb37014d50daf473267c808aa678ecaefe09&v=4 + url: https://github.com/simondale00 +- login: ghost + count: 15 + avatarUrl: https://avatars.githubusercontent.com/u/10137?u=b1951d34a583cf12ec0d3b0781ba19be97726318&v=4 + url: https://github.com/ghost +- login: abhint + count: 15 + avatarUrl: https://avatars.githubusercontent.com/u/25699289?u=b5d219277b4d001ac26fb8be357fddd88c29d51b&v=4 + url: https://github.com/abhint +- login: pythonweb2 + count: 14 + avatarUrl: https://avatars.githubusercontent.com/u/32141163?v=4 + url: https://github.com/pythonweb2 last_month_experts: +- login: Kludex + count: 15 + avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4 + url: https://github.com/Kludex - login: YuriiMotov - count: 29 + count: 10 avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=e83a39697a2d33ab2ec9bfbced794ee48bc29cec&v=4 url: https://github.com/YuriiMotov -- login: killjoy1221 +- login: sehraramiz count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/3409962?u=723662989f2027755e67d200137c13c53ae154ac&v=4 - url: https://github.com/killjoy1221 -- login: Kludex - count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 - url: https://github.com/Kludex -- login: JavierSanchezCastro - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 - url: https://github.com/JavierSanchezCastro -- login: hasansezertasan - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/13135006?u=99f0b0f0fc47e88e8abb337b4447357939ef93e7&v=4 - url: https://github.com/hasansezertasan -- login: PhysicallyActive + avatarUrl: https://avatars.githubusercontent.com/u/14166324?u=8fac65e84dfff24245d304a5b5b09f7b5bd69dc9&v=4 + url: https://github.com/sehraramiz +- login: luzzodev + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/27291415?v=4 + url: https://github.com/luzzodev +- login: yokwejuste + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/71908316?u=592c1e42aa0ee5cb94890e0b863e2acc78cc3bbc&v=4 + url: https://github.com/yokwejuste +- login: alv2017 count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/160476156?u=7a8e44f4a43d3bba636f795bb7d9476c9233b4d8&v=4 - url: https://github.com/PhysicallyActive -- login: n8sty - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 - url: https://github.com/n8sty -- login: pedroconceicao - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/32837064?u=5a0e6559bc391442629a28b6923790b54deb4464&v=4 - url: https://github.com/pedroconceicao + avatarUrl: https://avatars.githubusercontent.com/u/31544722?v=4 + url: https://github.com/alv2017 - login: PREPONDERANCE count: 2 avatarUrl: https://avatars.githubusercontent.com/u/112809059?u=30ab12dc9ddba2f94ab90e6ad4ad8bc5cfa7fccd&v=4 url: https://github.com/PREPONDERANCE -- login: aanchlia +- login: nbx3 count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/2835374?u=3c3ed29aa8b09ccaf8d66def0ce82bc2f7e5aab6&v=4 - url: https://github.com/aanchlia -- login: 0sahil + avatarUrl: https://avatars.githubusercontent.com/u/34649527?u=943812f69e0d40adbd3fa1c9b8ef50dd971a2a45&v=4 + url: https://github.com/nbx3 +- login: XiaoXinYo count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/58521386?u=ac00b731c07c712d0baa57b8b70ac8422acf183c&v=4 - url: https://github.com/0sahil -- login: jgould22 + avatarUrl: https://avatars.githubusercontent.com/u/56395004?u=b3b7cb758997f283c271a581833e407229dab82c&v=4 + url: https://github.com/XiaoXinYo +- login: iloveitaly count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 - url: https://github.com/jgould22 + avatarUrl: https://avatars.githubusercontent.com/u/150855?v=4 + url: https://github.com/iloveitaly three_months_experts: +- login: luzzodev + count: 34 + avatarUrl: https://avatars.githubusercontent.com/u/27291415?v=4 + url: https://github.com/luzzodev - login: YuriiMotov - count: 101 + count: 33 avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=e83a39697a2d33ab2ec9bfbced794ee48bc29cec&v=4 url: https://github.com/YuriiMotov -- login: JavierSanchezCastro - count: 20 - avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 - url: https://github.com/JavierSanchezCastro - login: Kludex - count: 17 - avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 + count: 23 + avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4 url: https://github.com/Kludex -- login: jgould22 - count: 14 - avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 - url: https://github.com/jgould22 -- login: killjoy1221 - count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/3409962?u=723662989f2027755e67d200137c13c53ae154ac&v=4 - url: https://github.com/killjoy1221 -- login: hasansezertasan - count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/13135006?u=99f0b0f0fc47e88e8abb337b4447357939ef93e7&v=4 - url: https://github.com/hasansezertasan -- login: PhysicallyActive - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/160476156?u=7a8e44f4a43d3bba636f795bb7d9476c9233b4d8&v=4 - url: https://github.com/PhysicallyActive -- login: n8sty - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 - url: https://github.com/n8sty - login: sehraramiz - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/14166324?v=4 + count: 10 + avatarUrl: https://avatars.githubusercontent.com/u/14166324?u=8fac65e84dfff24245d304a5b5b09f7b5bd69dc9&v=4 url: https://github.com/sehraramiz -- login: acidjunk - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 - url: https://github.com/acidjunk - login: estebanx64 - count: 3 + count: 7 avatarUrl: https://avatars.githubusercontent.com/u/10840422?u=45f015f95e1c0f06df602be4ab688d4b854cc8a8&v=4 url: https://github.com/estebanx64 -- login: PREPONDERANCE - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/112809059?u=30ab12dc9ddba2f94ab90e6ad4ad8bc5cfa7fccd&v=4 - url: https://github.com/PREPONDERANCE -- login: chrisK824 - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/79946379?u=03d85b22d696a58a9603e55fbbbe2de6b0f4face&v=4 - url: https://github.com/chrisK824 -- login: ryanisn - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/53449841?v=4 - url: https://github.com/ryanisn -- login: pythonweb2 +- login: yvallois + count: 6 + avatarUrl: https://avatars.githubusercontent.com/u/36999744?v=4 + url: https://github.com/yvallois +- login: yokwejuste + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/71908316?u=592c1e42aa0ee5cb94890e0b863e2acc78cc3bbc&v=4 + url: https://github.com/yokwejuste +- login: jgould22 + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 + url: https://github.com/jgould22 +- login: alv2017 count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/32141163?v=4 - url: https://github.com/pythonweb2 -- login: omarcruzpantoja + avatarUrl: https://avatars.githubusercontent.com/u/31544722?v=4 + url: https://github.com/alv2017 +- login: viniciusCalcantara count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/15116058?u=4b64c643fad49225d854e1aaecd1ffc6f9071a1b&v=4 - url: https://github.com/omarcruzpantoja -- login: mskrip - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/17459600?u=10019d5c38ae3374dd4a6743b0223e56a78d4855&v=4 - url: https://github.com/mskrip -- login: pedroconceicao - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/32837064?u=5a0e6559bc391442629a28b6923790b54deb4464&v=4 - url: https://github.com/pedroconceicao -- login: Jackiexiao - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/18050469?u=a2003e21a7780477ba00bf87a9abef8af58e91d1&v=4 - url: https://github.com/Jackiexiao -- login: aanchlia - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/2835374?u=3c3ed29aa8b09ccaf8d66def0ce82bc2f7e5aab6&v=4 - url: https://github.com/aanchlia -- login: moreno-p + avatarUrl: https://avatars.githubusercontent.com/u/108818737?u=3d7ffe5808843ee4372f9cc5a559ff1674cf1792&v=4 + url: https://github.com/viniciusCalcantara +- login: PREPONDERANCE count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/164261630?v=4 - url: https://github.com/moreno-p -- login: 0sahil + avatarUrl: https://avatars.githubusercontent.com/u/112809059?u=30ab12dc9ddba2f94ab90e6ad4ad8bc5cfa7fccd&v=4 + url: https://github.com/PREPONDERANCE +- login: nbx3 count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/58521386?u=ac00b731c07c712d0baa57b8b70ac8422acf183c&v=4 - url: https://github.com/0sahil -- login: patrick91 + avatarUrl: https://avatars.githubusercontent.com/u/34649527?u=943812f69e0d40adbd3fa1c9b8ef50dd971a2a45&v=4 + url: https://github.com/nbx3 +- login: XiaoXinYo count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/667029?u=e35958a75ac1f99c81b4bc99e22db8cd665ae7f0&v=4 - url: https://github.com/patrick91 -- login: pprunty + avatarUrl: https://avatars.githubusercontent.com/u/56395004?u=b3b7cb758997f283c271a581833e407229dab82c&v=4 + url: https://github.com/XiaoXinYo +- login: JavierSanchezCastro count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/58374462?u=5736576e586429abc97a803b8bcd4a6d828b8a2f&v=4 - url: https://github.com/pprunty -- login: angely-dev + avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 + url: https://github.com/JavierSanchezCastro +- login: iloveitaly count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/4362224?v=4 - url: https://github.com/angely-dev -- login: mastizada + avatarUrl: https://avatars.githubusercontent.com/u/150855?v=4 + url: https://github.com/iloveitaly +- login: LincolnPuzey count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/1975818?u=0751a06d7271c8bf17cb73b1b845644ab4d2c6dc&v=4 - url: https://github.com/mastizada -- login: sm-Fifteen + avatarUrl: https://avatars.githubusercontent.com/u/18750802?v=4 + url: https://github.com/LincolnPuzey +- login: Knighthawk-Leo count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/516999?u=437c0c5038558c67e887ccd863c1ba0f846c03da&v=4 - url: https://github.com/sm-Fifteen -- login: methane + avatarUrl: https://avatars.githubusercontent.com/u/72437494?u=27c68db94a3107b605e603cc136f4ba83f0106d5&v=4 + url: https://github.com/Knighthawk-Leo +- login: gelezo43 count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/199592?v=4 - url: https://github.com/methane -- login: konstantinos1981 + avatarUrl: https://avatars.githubusercontent.com/u/40732698?u=611f39d3c1d2f4207a590937a78c1f10eed6232c&v=4 + url: https://github.com/gelezo43 +- login: dbfreem count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/39465388?v=4 - url: https://github.com/konstantinos1981 -- login: druidance + avatarUrl: https://avatars.githubusercontent.com/u/9778569?u=f2f1e9135b5e4f1b0c6821a548b17f97572720fc&v=4 + url: https://github.com/dbfreem +- login: AliYmn count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/160344534?v=4 - url: https://github.com/druidance -- login: fabianfalon + avatarUrl: https://avatars.githubusercontent.com/u/18416653?u=98c1fca46c7e4dabe8c39d17b5e55d1511d41cf9&v=4 + url: https://github.com/AliYmn +- login: RichieB2B count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/3700760?u=95f69e31280b17ac22299cdcd345323b142fe0af&v=4 - url: https://github.com/fabianfalon -- login: VatsalJagani + avatarUrl: https://avatars.githubusercontent.com/u/1461970?u=edaa57d1077705244ea5c9244f4783d94ff11f12&v=4 + url: https://github.com/RichieB2B +- login: Synrom count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/20964366?u=43552644be05c9107c029e26d5ab3be5a1920f45&v=4 - url: https://github.com/VatsalJagani -- login: khaledadrani + avatarUrl: https://avatars.githubusercontent.com/u/30272537?v=4 + url: https://github.com/Synrom +- login: iiotsrc count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/45245894?u=49ed5056426a149a5af29d385d8bd3847101d3a4&v=4 - url: https://github.com/khaledadrani -- login: ThirVondukr + avatarUrl: https://avatars.githubusercontent.com/u/131771119?u=bcaf2559ef6266af70b151b7fda31a1ee3dbecb3&v=4 + url: https://github.com/iiotsrc +- login: Kfir-G count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/50728601?u=167c0bd655e52817082e50979a86d2f98f95b1a3&v=4 - url: https://github.com/ThirVondukr + avatarUrl: https://avatars.githubusercontent.com/u/57500876?u=0cd29db046a17f12f382d398141319fca7ff230a&v=4 + url: https://github.com/Kfir-G six_months_experts: - login: YuriiMotov - count: 104 + count: 72 avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=e83a39697a2d33ab2ec9bfbced794ee48bc29cec&v=4 url: https://github.com/YuriiMotov - login: Kludex - count: 104 - avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 + count: 39 + avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4 url: https://github.com/Kludex +- login: sinisaos + count: 37 + avatarUrl: https://avatars.githubusercontent.com/u/30960668?v=4 + url: https://github.com/sinisaos +- login: luzzodev + count: 36 + avatarUrl: https://avatars.githubusercontent.com/u/27291415?v=4 + url: https://github.com/luzzodev - login: JavierSanchezCastro - count: 40 + count: 16 avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 url: https://github.com/JavierSanchezCastro +- login: tiangolo + count: 13 + avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 + url: https://github.com/tiangolo +- login: Kfir-G + count: 13 + avatarUrl: https://avatars.githubusercontent.com/u/57500876?u=0cd29db046a17f12f382d398141319fca7ff230a&v=4 + url: https://github.com/Kfir-G +- login: sehraramiz + count: 10 + avatarUrl: https://avatars.githubusercontent.com/u/14166324?u=8fac65e84dfff24245d304a5b5b09f7b5bd69dc9&v=4 + url: https://github.com/sehraramiz +- login: estebanx64 + count: 10 + avatarUrl: https://avatars.githubusercontent.com/u/10840422?u=45f015f95e1c0f06df602be4ab688d4b854cc8a8&v=4 + url: https://github.com/estebanx64 +- login: ceb10n + count: 9 + avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 + url: https://github.com/ceb10n +- login: yvallois + count: 6 + avatarUrl: https://avatars.githubusercontent.com/u/36999744?v=4 + url: https://github.com/yvallois +- login: n8sty + count: 6 + avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 + url: https://github.com/n8sty +- login: TomFaulkner + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/14956620?v=4 + url: https://github.com/TomFaulkner +- login: yokwejuste + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/71908316?u=592c1e42aa0ee5cb94890e0b863e2acc78cc3bbc&v=4 + url: https://github.com/yokwejuste - login: jgould22 - count: 40 + count: 4 avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 url: https://github.com/jgould22 -- login: hasansezertasan - count: 21 - avatarUrl: https://avatars.githubusercontent.com/u/13135006?u=99f0b0f0fc47e88e8abb337b4447357939ef93e7&v=4 - url: https://github.com/hasansezertasan -- login: n8sty - count: 19 - avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 - url: https://github.com/n8sty -- login: killjoy1221 - count: 9 - avatarUrl: https://avatars.githubusercontent.com/u/3409962?u=723662989f2027755e67d200137c13c53ae154ac&v=4 - url: https://github.com/killjoy1221 -- login: aanchlia - count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/2835374?u=3c3ed29aa8b09ccaf8d66def0ce82bc2f7e5aab6&v=4 - url: https://github.com/aanchlia -- login: estebanx64 - count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/10840422?u=45f015f95e1c0f06df602be4ab688d4b854cc8a8&v=4 - url: https://github.com/estebanx64 -- login: PhysicallyActive - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/160476156?u=7a8e44f4a43d3bba636f795bb7d9476c9233b4d8&v=4 - url: https://github.com/PhysicallyActive -- login: dolfinus - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/4661021?u=a51b39001a2e5e7529b45826980becf786de2327&v=4 - url: https://github.com/dolfinus -- login: Ventura94 - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/43103937?u=ccb837005aaf212a449c374618c4339089e2f733&v=4 - url: https://github.com/Ventura94 -- login: sehraramiz - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/14166324?v=4 - url: https://github.com/sehraramiz -- login: acidjunk - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 - url: https://github.com/acidjunk -- login: shashstormer - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/90090313?v=4 - url: https://github.com/shashstormer -- login: GodMoonGoodman - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/29688727?u=7b251da620d999644c37c1feeb292d033eed7ad6&v=4 - url: https://github.com/GodMoonGoodman -- login: flo-at - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/564288?v=4 - url: https://github.com/flo-at -- login: PREPONDERANCE +- login: alv2017 count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/112809059?u=30ab12dc9ddba2f94ab90e6ad4ad8bc5cfa7fccd&v=4 - url: https://github.com/PREPONDERANCE -- login: chrisK824 + avatarUrl: https://avatars.githubusercontent.com/u/31544722?v=4 + url: https://github.com/alv2017 +- login: viniciusCalcantara count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/79946379?u=03d85b22d696a58a9603e55fbbbe2de6b0f4face&v=4 - url: https://github.com/chrisK824 -- login: angely-dev + avatarUrl: https://avatars.githubusercontent.com/u/108818737?u=3d7ffe5808843ee4372f9cc5a559ff1674cf1792&v=4 + url: https://github.com/viniciusCalcantara +- login: pawelad count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/4362224?v=4 - url: https://github.com/angely-dev -- login: fmelihh + avatarUrl: https://avatars.githubusercontent.com/u/7062874?u=d27dc220545a8401ad21840590a97d474d7101e6&v=4 + url: https://github.com/pawelad +- login: dbfreem count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/99879453?u=671117dba9022db2237e3da7a39cbc2efc838db0&v=4 - url: https://github.com/fmelihh -- login: ryanisn + avatarUrl: https://avatars.githubusercontent.com/u/9778569?u=f2f1e9135b5e4f1b0c6821a548b17f97572720fc&v=4 + url: https://github.com/dbfreem +- login: Isuxiz count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/53449841?v=4 - url: https://github.com/ryanisn -- login: JoshYuJump + avatarUrl: https://avatars.githubusercontent.com/u/48672727?u=34d7b4ade252687d22a27cf53037b735b244bfc1&v=4 + url: https://github.com/Isuxiz +- login: bertomaniac count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/5901894?u=cdbca6296ac4cdcdf6945c112a1ce8d5342839ea&v=4 - url: https://github.com/JoshYuJump -- login: pythonweb2 + avatarUrl: https://avatars.githubusercontent.com/u/10235051?u=14484a96833228a7b29fee4a7916d411c242c4f6&v=4 + url: https://github.com/bertomaniac +- login: PhysicallyActive count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/32141163?v=4 - url: https://github.com/pythonweb2 -- login: omarcruzpantoja + avatarUrl: https://avatars.githubusercontent.com/u/160476156?u=7a8e44f4a43d3bba636f795bb7d9476c9233b4d8&v=4 + url: https://github.com/PhysicallyActive +- login: Minibrams count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/15116058?u=4b64c643fad49225d854e1aaecd1ffc6f9071a1b&v=4 - url: https://github.com/omarcruzpantoja -- login: bogdan-coman-uv + avatarUrl: https://avatars.githubusercontent.com/u/8108085?u=b028dbc308fa8485e0e2e9402b3d03d8deb22bf9&v=4 + url: https://github.com/Minibrams +- login: AIdjis count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/92912507?v=4 - url: https://github.com/bogdan-coman-uv -- login: ahmedabdou14 + avatarUrl: https://avatars.githubusercontent.com/u/88404339?u=2a80d80b054e9228391e32fb9bb39571509dab6a&v=4 + url: https://github.com/AIdjis +- login: svlandeg count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/104530599?u=05365b155a1ff911532e8be316acfad2e0736f98&v=4 - url: https://github.com/ahmedabdou14 -- login: mskrip + avatarUrl: https://avatars.githubusercontent.com/u/8796347?u=556c97650c27021911b0b9447ec55e75987b0e8a&v=4 + url: https://github.com/svlandeg +- login: PREPONDERANCE count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/17459600?u=10019d5c38ae3374dd4a6743b0223e56a78d4855&v=4 - url: https://github.com/mskrip -- login: leonidktoto + avatarUrl: https://avatars.githubusercontent.com/u/112809059?u=30ab12dc9ddba2f94ab90e6ad4ad8bc5cfa7fccd&v=4 + url: https://github.com/PREPONDERANCE +- login: nbx3 count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/159561986?v=4 - url: https://github.com/leonidktoto -- login: pedroconceicao + avatarUrl: https://avatars.githubusercontent.com/u/34649527?u=943812f69e0d40adbd3fa1c9b8ef50dd971a2a45&v=4 + url: https://github.com/nbx3 +- login: yanggeorge count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/32837064?u=5a0e6559bc391442629a28b6923790b54deb4464&v=4 - url: https://github.com/pedroconceicao -- login: hwong557 + avatarUrl: https://avatars.githubusercontent.com/u/2434407?v=4 + url: https://github.com/yanggeorge +- login: XiaoXinYo count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/460259?u=7d2f1b33ea5bda4d8e177ab3cb924a673d53087e&v=4 - url: https://github.com/hwong557 -- login: Jackiexiao + avatarUrl: https://avatars.githubusercontent.com/u/56395004?u=b3b7cb758997f283c271a581833e407229dab82c&v=4 + url: https://github.com/XiaoXinYo +- login: pythonweb2 count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/18050469?u=a2003e21a7780477ba00bf87a9abef8af58e91d1&v=4 - url: https://github.com/Jackiexiao -- login: admo1 + avatarUrl: https://avatars.githubusercontent.com/u/32141163?v=4 + url: https://github.com/pythonweb2 +- login: slafs count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/14835916?v=4 - url: https://github.com/admo1 -- login: binbjz + avatarUrl: https://avatars.githubusercontent.com/u/210173?v=4 + url: https://github.com/slafs +- login: AmirHmZz count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/8213913?u=22b68b7a0d5bf5e09c02084c0f5f53d7503114cd&v=4 - url: https://github.com/binbjz -- login: nameer + avatarUrl: https://avatars.githubusercontent.com/u/38752106?u=07f80e451bda00a9492bbc764e49d24ad3ada8cc&v=4 + url: https://github.com/AmirHmZz +- login: iloveitaly count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/3931725?u=6199fb065df098fc13ac0a5e649f89672b586732&v=4 - url: https://github.com/nameer -- login: moreno-p + avatarUrl: https://avatars.githubusercontent.com/u/150855?v=4 + url: https://github.com/iloveitaly +- login: LincolnPuzey count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/164261630?v=4 - url: https://github.com/moreno-p -- login: 0sahil + avatarUrl: https://avatars.githubusercontent.com/u/18750802?v=4 + url: https://github.com/LincolnPuzey +- login: alejsdev count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/58521386?u=ac00b731c07c712d0baa57b8b70ac8422acf183c&v=4 - url: https://github.com/0sahil -- login: nymous + avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=356f39ff3f0211c720b06d3dbb060e98884085e3&v=4 + url: https://github.com/alejsdev +- login: Knighthawk-Leo count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/4216559?u=360a36fb602cded27273cbfc0afc296eece90662&v=4 - url: https://github.com/nymous -- login: patrick91 + avatarUrl: https://avatars.githubusercontent.com/u/72437494?u=27c68db94a3107b605e603cc136f4ba83f0106d5&v=4 + url: https://github.com/Knighthawk-Leo +- login: gelezo43 count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/667029?u=e35958a75ac1f99c81b4bc99e22db8cd665ae7f0&v=4 - url: https://github.com/patrick91 -- login: pprunty + avatarUrl: https://avatars.githubusercontent.com/u/40732698?u=611f39d3c1d2f4207a590937a78c1f10eed6232c&v=4 + url: https://github.com/gelezo43 +- login: christiansicari count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/58374462?u=5736576e586429abc97a803b8bcd4a6d828b8a2f&v=4 - url: https://github.com/pprunty -- login: JonnyBootsNpants + avatarUrl: https://avatars.githubusercontent.com/u/29756552?v=4 + url: https://github.com/christiansicari +- login: 1001pepi count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/155071540?u=2d3a72b74a2c4c8eaacdb625c7ac850369579352&v=4 - url: https://github.com/JonnyBootsNpants -- login: richin13 + avatarUrl: https://avatars.githubusercontent.com/u/82064861?u=8c6ffdf2275d6970a07294752c545cd2702c57d3&v=4 + url: https://github.com/1001pepi +- login: AliYmn count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/8370058?u=8e37a4cdbc78983a5f4b4847f6d1879fb39c851c&v=4 - url: https://github.com/richin13 -- login: mastizada + avatarUrl: https://avatars.githubusercontent.com/u/18416653?u=98c1fca46c7e4dabe8c39d17b5e55d1511d41cf9&v=4 + url: https://github.com/AliYmn +- login: RichieB2B count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/1975818?u=0751a06d7271c8bf17cb73b1b845644ab4d2c6dc&v=4 - url: https://github.com/mastizada -- login: sm-Fifteen + avatarUrl: https://avatars.githubusercontent.com/u/1461970?u=edaa57d1077705244ea5c9244f4783d94ff11f12&v=4 + url: https://github.com/RichieB2B +- login: Synrom count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/516999?u=437c0c5038558c67e887ccd863c1ba0f846c03da&v=4 - url: https://github.com/sm-Fifteen -- login: amacfie + avatarUrl: https://avatars.githubusercontent.com/u/30272537?v=4 + url: https://github.com/Synrom +- login: ecly + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/8410422?v=4 + url: https://github.com/ecly +- login: iiotsrc + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/131771119?u=bcaf2559ef6266af70b151b7fda31a1ee3dbecb3&v=4 + url: https://github.com/iiotsrc +- login: simondale00 + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/33907262?u=2721fb37014d50daf473267c808aa678ecaefe09&v=4 + url: https://github.com/simondale00 +- login: jd-solanki + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/47495003?u=6e225cb42c688d0cd70e65c6baedb9f5922b1178&v=4 + url: https://github.com/jd-solanki +- login: AumGupta + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/86357151?u=7d05aa606c0611a18f4db16cf26361ce10a6e195&v=4 + url: https://github.com/AumGupta +- login: DeoLeung + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/3764720?u=4c222ef513814de4c7fb3736d0a7adf11d953d43&v=4 + url: https://github.com/DeoLeung +- login: Reemyos + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/44867003?v=4 + url: https://github.com/Reemyos +- login: deight93 + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/37678115?u=a608798b5bd0034183a9c430ebb42fb266db86ce&v=4 + url: https://github.com/deight93 +- login: Jkrox + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/83181939?u=d6a922d97129f7f3916d6a1c166bc011b3a72b7f&v=4 + url: https://github.com/Jkrox +- login: mmzeynalli + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/33568903?u=19efd0c0722730b83a70b7c86c36e5b7d83e07d2&v=4 + url: https://github.com/mmzeynalli +- login: ddahan + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/1933516?u=1d200a620e8d6841df017e9f2bb7efb58b580f40&v=4 + url: https://github.com/ddahan +- login: jfeaver + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/1091338?u=0bcba366447d8fadad63f6705a52d128da4c7ec2&v=4 + url: https://github.com/jfeaver +- login: Wurstnase count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/889657?u=d70187989940b085bcbfa3bedad8dbc5f3ab1fe7&v=4 - url: https://github.com/amacfie -- login: garg10may + avatarUrl: https://avatars.githubusercontent.com/u/8709415?u=f479af475a97aee9a1dab302cfc35d07e9ea245f&v=4 + url: https://github.com/Wurstnase +- login: tristan count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/8787120?u=7028d2b3a2a26534c1806eb76c7425a3fac9732f&v=4 - url: https://github.com/garg10may -- login: methane + avatarUrl: https://avatars.githubusercontent.com/u/1412?u=aab8aaa4cc0f1210ac45fc93873a5909d314c965&v=4 + url: https://github.com/tristan +- login: chandanch count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/199592?v=4 - url: https://github.com/methane -- login: konstantinos1981 + avatarUrl: https://avatars.githubusercontent.com/u/8663552?u=afc484bc0a952c83f1fb6a1583cda443f807cd66&v=4 + url: https://github.com/chandanch +- login: rvishruth count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/39465388?v=4 - url: https://github.com/konstantinos1981 -- login: druidance + avatarUrl: https://avatars.githubusercontent.com/u/79176273?v=4 + url: https://github.com/rvishruth +- login: mattmess1221 count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/160344534?v=4 - url: https://github.com/druidance + avatarUrl: https://avatars.githubusercontent.com/u/3409962?u=723662989f2027755e67d200137c13c53ae154ac&v=4 + url: https://github.com/mattmess1221 +- login: meower1 + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/109747197?u=0a5cc2a6ae74e558f0afc2874da85132e5953d8b&v=4 + url: https://github.com/meower1 one_year_experts: -- login: Kludex - count: 207 - avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 - url: https://github.com/Kludex -- login: jgould22 - count: 118 - avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 - url: https://github.com/jgould22 - login: YuriiMotov - count: 104 + count: 223 avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=e83a39697a2d33ab2ec9bfbced794ee48bc29cec&v=4 url: https://github.com/YuriiMotov +- login: Kludex + count: 83 + avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4 + url: https://github.com/Kludex - login: JavierSanchezCastro - count: 59 + count: 47 avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 url: https://github.com/JavierSanchezCastro +- login: jgould22 + count: 42 + avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 + url: https://github.com/jgould22 +- login: sinisaos + count: 39 + avatarUrl: https://avatars.githubusercontent.com/u/30960668?v=4 + url: https://github.com/sinisaos +- login: luzzodev + count: 36 + avatarUrl: https://avatars.githubusercontent.com/u/27291415?v=4 + url: https://github.com/luzzodev +- login: tiangolo + count: 24 + avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 + url: https://github.com/tiangolo - login: n8sty - count: 40 + count: 23 avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 url: https://github.com/n8sty -- login: hasansezertasan - count: 27 - avatarUrl: https://avatars.githubusercontent.com/u/13135006?u=99f0b0f0fc47e88e8abb337b4447357939ef93e7&v=4 - url: https://github.com/hasansezertasan -- login: chrisK824 - count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/79946379?u=03d85b22d696a58a9603e55fbbbe2de6b0f4face&v=4 - url: https://github.com/chrisK824 -- login: ahmedabdou14 - count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/104530599?u=05365b155a1ff911532e8be316acfad2e0736f98&v=4 - url: https://github.com/ahmedabdou14 -- login: arjwilliams - count: 12 - avatarUrl: https://avatars.githubusercontent.com/u/22227620?v=4 - url: https://github.com/arjwilliams -- login: killjoy1221 - count: 10 - avatarUrl: https://avatars.githubusercontent.com/u/3409962?u=723662989f2027755e67d200137c13c53ae154ac&v=4 - url: https://github.com/killjoy1221 -- login: WilliamStam - count: 10 - avatarUrl: https://avatars.githubusercontent.com/u/182800?v=4 - url: https://github.com/WilliamStam -- login: iudeen - count: 10 - avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 - url: https://github.com/iudeen -- login: nymous - count: 9 - avatarUrl: https://avatars.githubusercontent.com/u/4216559?u=360a36fb602cded27273cbfc0afc296eece90662&v=4 - url: https://github.com/nymous -- login: aanchlia - count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/2835374?u=3c3ed29aa8b09ccaf8d66def0ce82bc2f7e5aab6&v=4 - url: https://github.com/aanchlia - login: estebanx64 - count: 7 + count: 19 avatarUrl: https://avatars.githubusercontent.com/u/10840422?u=45f015f95e1c0f06df602be4ab688d4b854cc8a8&v=4 url: https://github.com/estebanx64 -- login: pythonweb2 - count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/32141163?v=4 - url: https://github.com/pythonweb2 -- login: romabozhanovgithub - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/67696229?u=e4b921eef096415300425aca249348f8abb78ad7&v=4 - url: https://github.com/romabozhanovgithub +- login: sehraramiz + count: 14 + avatarUrl: https://avatars.githubusercontent.com/u/14166324?u=8fac65e84dfff24245d304a5b5b09f7b5bd69dc9&v=4 + url: https://github.com/sehraramiz - login: PhysicallyActive - count: 6 + count: 14 avatarUrl: https://avatars.githubusercontent.com/u/160476156?u=7a8e44f4a43d3bba636f795bb7d9476c9233b4d8&v=4 url: https://github.com/PhysicallyActive -- login: mikeedjones - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/4087139?u=cc4a242896ac2fcf88a53acfaf190d0fe0a1f0c9&v=4 - url: https://github.com/mikeedjones -- login: dolfinus - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/4661021?u=a51b39001a2e5e7529b45826980becf786de2327&v=4 - url: https://github.com/dolfinus -- login: ebottos94 - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/100039558?u=e2c672da5a7977fd24d87ce6ab35f8bf5b1ed9fa&v=4 - url: https://github.com/ebottos94 -- login: Ventura94 - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/43103937?u=ccb837005aaf212a449c374618c4339089e2f733&v=4 - url: https://github.com/Ventura94 -- login: White-Mask +- login: ceb10n + count: 14 + avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 + url: https://github.com/ceb10n +- login: Kfir-G + count: 13 + avatarUrl: https://avatars.githubusercontent.com/u/57500876?u=0cd29db046a17f12f382d398141319fca7ff230a&v=4 + url: https://github.com/Kfir-G +- login: mattmess1221 + count: 11 + avatarUrl: https://avatars.githubusercontent.com/u/3409962?u=723662989f2027755e67d200137c13c53ae154ac&v=4 + url: https://github.com/mattmess1221 +- login: hasansezertasan + count: 10 + avatarUrl: https://avatars.githubusercontent.com/u/13135006?u=99f0b0f0fc47e88e8abb337b4447357939ef93e7&v=4 + url: https://github.com/hasansezertasan +- login: AIdjis + count: 8 + avatarUrl: https://avatars.githubusercontent.com/u/88404339?u=2a80d80b054e9228391e32fb9bb39571509dab6a&v=4 + url: https://github.com/AIdjis +- login: yvallois count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/31826970?u=8625355dc25ddf9c85a8b2b0b9932826c4c8f44c&v=4 - url: https://github.com/White-Mask -- login: sehraramiz + avatarUrl: https://avatars.githubusercontent.com/u/36999744?v=4 + url: https://github.com/yvallois +- login: PREPONDERANCE count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/14166324?v=4 - url: https://github.com/sehraramiz + avatarUrl: https://avatars.githubusercontent.com/u/112809059?u=30ab12dc9ddba2f94ab90e6ad4ad8bc5cfa7fccd&v=4 + url: https://github.com/PREPONDERANCE +- login: pythonweb2 + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/32141163?v=4 + url: https://github.com/pythonweb2 - login: acidjunk count: 5 avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 url: https://github.com/acidjunk -- login: JoshYuJump +- login: gustavosett count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/5901894?u=cdbca6296ac4cdcdf6945c112a1ce8d5342839ea&v=4 - url: https://github.com/JoshYuJump -- login: alex-pobeditel-2004 - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/14791483?v=4 - url: https://github.com/alex-pobeditel-2004 -- login: shashstormer - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/90090313?v=4 - url: https://github.com/shashstormer -- login: wu-clan + avatarUrl: https://avatars.githubusercontent.com/u/99373133?u=1739ca547c3d200f1b72450520bce46a97aab184&v=4 + url: https://github.com/gustavosett +- login: binbjz count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/52145145?u=f8c9e5c8c259d248e1683fedf5027b4ee08a0967&v=4 - url: https://github.com/wu-clan -- login: abhint + avatarUrl: https://avatars.githubusercontent.com/u/8213913?u=22b68b7a0d5bf5e09c02084c0f5f53d7503114cd&v=4 + url: https://github.com/binbjz +- login: chyok count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/25699289?u=b5d219277b4d001ac26fb8be357fddd88c29d51b&v=4 - url: https://github.com/abhint -- login: anthonycepeda + avatarUrl: https://avatars.githubusercontent.com/u/32629225?u=3b7c30e8a09426a1b9284f6e8a0ae53a525596bf&v=4 + url: https://github.com/chyok +- login: TomFaulkner count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/72019805?u=60bdf46240cff8fca482ff0fc07d963fd5e1a27c&v=4 - url: https://github.com/anthonycepeda -- login: GodMoonGoodman + avatarUrl: https://avatars.githubusercontent.com/u/14956620?v=4 + url: https://github.com/TomFaulkner +- login: yokwejuste count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/29688727?u=7b251da620d999644c37c1feeb292d033eed7ad6&v=4 - url: https://github.com/GodMoonGoodman + avatarUrl: https://avatars.githubusercontent.com/u/71908316?u=592c1e42aa0ee5cb94890e0b863e2acc78cc3bbc&v=4 + url: https://github.com/yokwejuste +- login: DeoLeung + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/3764720?u=4c222ef513814de4c7fb3736d0a7adf11d953d43&v=4 + url: https://github.com/DeoLeung - login: flo-at count: 4 avatarUrl: https://avatars.githubusercontent.com/u/564288?v=4 url: https://github.com/flo-at -- login: yinziyan1206 - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 - url: https://github.com/yinziyan1206 -- login: amacfie - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/889657?u=d70187989940b085bcbfa3bedad8dbc5f3ab1fe7&v=4 - url: https://github.com/amacfie -- login: commonism - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/164513?v=4 - url: https://github.com/commonism -- login: dmontagu +- login: GodMoonGoodman count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4 - url: https://github.com/dmontagu -- login: sanzoghenzo + avatarUrl: https://avatars.githubusercontent.com/u/29688727?u=7b251da620d999644c37c1feeb292d033eed7ad6&v=4 + url: https://github.com/GodMoonGoodman +- login: bertomaniac count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/977953?v=4 - url: https://github.com/sanzoghenzo -- login: lucasgadams + avatarUrl: https://avatars.githubusercontent.com/u/10235051?u=14484a96833228a7b29fee4a7916d411c242c4f6&v=4 + url: https://github.com/bertomaniac +- login: alv2017 count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/36425095?v=4 - url: https://github.com/lucasgadams -- login: NeilBotelho + avatarUrl: https://avatars.githubusercontent.com/u/31544722?v=4 + url: https://github.com/alv2017 +- login: msehnout count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/39030675?u=16fea2ff90a5c67b974744528a38832a6d1bb4f7&v=4 - url: https://github.com/NeilBotelho -- login: hhartzer + avatarUrl: https://avatars.githubusercontent.com/u/9369632?u=8c988f1b008a3f601385a3616f9327820f66e3a5&v=4 + url: https://github.com/msehnout +- login: viniciusCalcantara count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/100533792?v=4 - url: https://github.com/hhartzer -- login: binbjz + avatarUrl: https://avatars.githubusercontent.com/u/108818737?u=3d7ffe5808843ee4372f9cc5a559ff1674cf1792&v=4 + url: https://github.com/viniciusCalcantara +- login: pawelad count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/8213913?u=22b68b7a0d5bf5e09c02084c0f5f53d7503114cd&v=4 - url: https://github.com/binbjz -- login: PREPONDERANCE + avatarUrl: https://avatars.githubusercontent.com/u/7062874?u=d27dc220545a8401ad21840590a97d474d7101e6&v=4 + url: https://github.com/pawelad +- login: ThirVondukr count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/112809059?u=30ab12dc9ddba2f94ab90e6ad4ad8bc5cfa7fccd&v=4 - url: https://github.com/PREPONDERANCE -- login: nameer + avatarUrl: https://avatars.githubusercontent.com/u/50728601?u=167c0bd655e52817082e50979a86d2f98f95b1a3&v=4 + url: https://github.com/ThirVondukr +- login: dbfreem + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/9778569?u=f2f1e9135b5e4f1b0c6821a548b17f97572720fc&v=4 + url: https://github.com/dbfreem +- login: Isuxiz count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/3931725?u=6199fb065df098fc13ac0a5e649f89672b586732&v=4 - url: https://github.com/nameer + avatarUrl: https://avatars.githubusercontent.com/u/48672727?u=34d7b4ade252687d22a27cf53037b735b244bfc1&v=4 + url: https://github.com/Isuxiz - login: angely-dev count: 3 avatarUrl: https://avatars.githubusercontent.com/u/4362224?v=4 url: https://github.com/angely-dev -- login: fmelihh +- login: deight93 + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/37678115?u=a608798b5bd0034183a9c430ebb42fb266db86ce&v=4 + url: https://github.com/deight93 +- login: mmzeynalli count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/99879453?u=671117dba9022db2237e3da7a39cbc2efc838db0&v=4 - url: https://github.com/fmelihh + avatarUrl: https://avatars.githubusercontent.com/u/33568903?u=19efd0c0722730b83a70b7c86c36e5b7d83e07d2&v=4 + url: https://github.com/mmzeynalli +- login: Minibrams + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/8108085?u=b028dbc308fa8485e0e2e9402b3d03d8deb22bf9&v=4 + url: https://github.com/Minibrams - login: ryanisn count: 3 avatarUrl: https://avatars.githubusercontent.com/u/53449841?v=4 url: https://github.com/ryanisn -- login: theobouwman +- login: svlandeg count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/16098190?u=dc70db88a7a99b764c9a89a6e471e0b7ca478a35&v=4 - url: https://github.com/theobouwman -- login: methane + avatarUrl: https://avatars.githubusercontent.com/u/8796347?u=556c97650c27021911b0b9447ec55e75987b0e8a&v=4 + url: https://github.com/svlandeg +- login: alexandercronin count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/199592?v=4 - url: https://github.com/methane -top_contributors: -- login: nilslindemann - count: 130 - avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 - url: https://github.com/nilslindemann -- login: jaystone776 - count: 49 - avatarUrl: https://avatars.githubusercontent.com/u/11191137?u=299205a95e9b6817a43144a48b643346a5aac5cc&v=4 - url: https://github.com/jaystone776 -- login: waynerv - count: 25 - avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4 - url: https://github.com/waynerv -- login: tokusumi - count: 24 - avatarUrl: https://avatars.githubusercontent.com/u/41147016?u=55010621aece725aa702270b54fed829b6a1fe60&v=4 - url: https://github.com/tokusumi -- login: SwftAlpc - count: 23 - avatarUrl: https://avatars.githubusercontent.com/u/52768429?u=6a3aa15277406520ad37f6236e89466ed44bc5b8&v=4 - url: https://github.com/SwftAlpc -- login: Kludex - count: 22 - avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 - url: https://github.com/Kludex -- login: hasansezertasan - count: 22 - avatarUrl: https://avatars.githubusercontent.com/u/13135006?u=99f0b0f0fc47e88e8abb337b4447357939ef93e7&v=4 - url: https://github.com/hasansezertasan -- login: dmontagu - count: 17 - avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4 - url: https://github.com/dmontagu -- login: Xewus - count: 14 - avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4 - url: https://github.com/Xewus -- login: euri10 - count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4 - url: https://github.com/euri10 -- login: mariacamilagl - count: 12 - avatarUrl: https://avatars.githubusercontent.com/u/11489395?u=4adb6986bf3debfc2b8216ae701f2bd47d73da7d&v=4 - url: https://github.com/mariacamilagl -- login: AlertRED - count: 12 - avatarUrl: https://avatars.githubusercontent.com/u/15695000?u=f5a4944c6df443030409c88da7d7fa0b7ead985c&v=4 - url: https://github.com/AlertRED -- login: Smlep - count: 11 - avatarUrl: https://avatars.githubusercontent.com/u/16785985?v=4 - url: https://github.com/Smlep -- login: alejsdev - count: 11 - avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=9ca449ad5161af12766ddd1a22988e9b14315f5c&v=4 - url: https://github.com/alejsdev -- login: hard-coders - count: 10 - avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 - url: https://github.com/hard-coders -- login: KaniKim - count: 10 - avatarUrl: https://avatars.githubusercontent.com/u/19832624?u=40f8f7f3f36d5f2365ba2ad0b40693e60958ce70&v=4 - url: https://github.com/KaniKim -- login: xzmeng - count: 9 - avatarUrl: https://avatars.githubusercontent.com/u/40202897?v=4 - url: https://github.com/xzmeng -- login: Serrones - count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/22691749?u=4795b880e13ca33a73e52fc0ef7dc9c60c8fce47&v=4 - url: https://github.com/Serrones -- login: rjNemo - count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/56785022?u=d5c3a02567c8649e146fcfc51b6060ccaf8adef8&v=4 - url: https://github.com/rjNemo -- login: pablocm83 - count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/28315068?u=3310fbb05bb8bfc50d2c48b6cb64ac9ee4a14549&v=4 - url: https://github.com/pablocm83 -- login: RunningIkkyu - count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/31848542?u=494ecc298e3f26197495bb357ad0f57cfd5f7a32&v=4 - url: https://github.com/RunningIkkyu -- login: Alexandrhub - count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/119126536?u=9fc0d48f3307817bafecc5861eb2168401a6cb04&v=4 - url: https://github.com/Alexandrhub -- login: NinaHwang - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/79563565?u=eee6bfe9224c71193025ab7477f4f96ceaa05c62&v=4 - url: https://github.com/NinaHwang -- login: batlopes - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/33462923?u=0fb3d7acb316764616f11e4947faf080e49ad8d9&v=4 - url: https://github.com/batlopes -- login: wshayes - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4 - url: https://github.com/wshayes -- login: samuelcolvin - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/4039449?u=42eb3b833047c8c4b4f647a031eaef148c16d93f&v=4 - url: https://github.com/samuelcolvin -- login: Attsun1031 - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/1175560?v=4 - url: https://github.com/Attsun1031 -- login: ComicShrimp - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/43503750?u=d2fbf412e7730183ce91686ca48d4147e1b7dc74&v=4 - url: https://github.com/ComicShrimp -- login: rostik1410 - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/11443899?u=e26a635c2ba220467b308a326a579b8ccf4a8701&v=4 - url: https://github.com/rostik1410 -- login: tamtam-fitness - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/62091034?u=8da19a6bd3d02f5d6ba30c7247d5b46c98dd1403&v=4 - url: https://github.com/tamtam-fitness -- login: jekirl - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/2546697?u=a027452387d85bd4a14834e19d716c99255fb3b7&v=4 - url: https://github.com/jekirl -- login: jfunez - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/805749?v=4 - url: https://github.com/jfunez -- login: ycd - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=29682e4b6ac7d5293742ccf818188394b9a82972&v=4 - url: https://github.com/ycd -- login: komtaki - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/39375566?u=260ad6b1a4b34c07dbfa728da5e586f16f6d1824&v=4 - url: https://github.com/komtaki -- login: hitrust - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/3360631?u=5fa1f475ad784d64eb9666bdd43cc4d285dcc773&v=4 - url: https://github.com/hitrust -- login: JulianMaurin - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/63545168?u=b7d15ac865268cbefc2d739e2f23d9aeeac1a622&v=4 - url: https://github.com/JulianMaurin -- login: lsglucas - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4 - url: https://github.com/lsglucas -- login: BilalAlpaslan - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/47563997?u=63ed66e304fe8d765762c70587d61d9196e5c82d&v=4 - url: https://github.com/BilalAlpaslan -- login: adriangb - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=612704256e38d6ac9cbed24f10e4b6ac2da74ecb&v=4 - url: https://github.com/adriangb -- login: iudeen - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 - url: https://github.com/iudeen -- login: axel584 - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/1334088?u=9667041f5b15dc002b6f9665fda8c0412933ac04&v=4 - url: https://github.com/axel584 -- login: ivan-abc - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/36765187?u=c6e0ba571c1ccb6db9d94e62e4b8b5eda811a870&v=4 - url: https://github.com/ivan-abc -- login: divums + avatarUrl: https://avatars.githubusercontent.com/u/8014288?u=69580504c51a0cdd756fc47b23bb7f404bd694e7&v=4 + url: https://github.com/alexandercronin +- login: aanchlia count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/1397556?v=4 - url: https://github.com/divums -- login: prostomarkeloff + avatarUrl: https://avatars.githubusercontent.com/u/2835374?u=3c3ed29aa8b09ccaf8d66def0ce82bc2f7e5aab6&v=4 + url: https://github.com/aanchlia +- login: chrisK824 count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/28061158?u=72309cc1f2e04e40fa38b29969cb4e9d3f722e7b&v=4 - url: https://github.com/prostomarkeloff -- login: nsidnev + avatarUrl: https://avatars.githubusercontent.com/u/79946379?u=03d85b22d696a58a9603e55fbbbe2de6b0f4face&v=4 + url: https://github.com/chrisK824 +- login: omarcruzpantoja count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/22559461?u=a9cc3238217e21dc8796a1a500f01b722adb082c&v=4 - url: https://github.com/nsidnev -- login: pawamoy + avatarUrl: https://avatars.githubusercontent.com/u/15116058?u=4b64c643fad49225d854e1aaecd1ffc6f9071a1b&v=4 + url: https://github.com/omarcruzpantoja +- login: ahmedabdou14 count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/3999221?u=b030e4c89df2f3a36bc4710b925bdeb6745c9856&v=4 - url: https://github.com/pawamoy -top_reviewers: -- login: Kludex - count: 158 - avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 - url: https://github.com/Kludex -- login: BilalAlpaslan - count: 86 - avatarUrl: https://avatars.githubusercontent.com/u/47563997?u=63ed66e304fe8d765762c70587d61d9196e5c82d&v=4 - url: https://github.com/BilalAlpaslan -- login: yezz123 - count: 85 - avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=d7062cbc6eb7671d5dc9cc0e32a24ae335e0f225&v=4 - url: https://github.com/yezz123 -- login: iudeen - count: 55 - avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 - url: https://github.com/iudeen -- login: tokusumi - count: 51 - avatarUrl: https://avatars.githubusercontent.com/u/41147016?u=55010621aece725aa702270b54fed829b6a1fe60&v=4 - url: https://github.com/tokusumi -- login: Xewus - count: 50 - avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4 - url: https://github.com/Xewus -- login: hasansezertasan - count: 50 - avatarUrl: https://avatars.githubusercontent.com/u/13135006?u=99f0b0f0fc47e88e8abb337b4447357939ef93e7&v=4 - url: https://github.com/hasansezertasan -- login: waynerv - count: 47 - avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4 - url: https://github.com/waynerv -- login: Laineyzhang55 - count: 47 - avatarUrl: https://avatars.githubusercontent.com/u/59285379?v=4 - url: https://github.com/Laineyzhang55 -- login: ycd - count: 45 - avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=29682e4b6ac7d5293742ccf818188394b9a82972&v=4 - url: https://github.com/ycd -- login: cikay - count: 41 - avatarUrl: https://avatars.githubusercontent.com/u/24587499?u=e772190a051ab0eaa9c8542fcff1892471638f2b&v=4 - url: https://github.com/cikay -- login: alejsdev - count: 38 - avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=9ca449ad5161af12766ddd1a22988e9b14315f5c&v=4 - url: https://github.com/alejsdev -- login: JarroVGIT - count: 34 - avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 - url: https://github.com/JarroVGIT -- login: AdrianDeAnda - count: 33 - avatarUrl: https://avatars.githubusercontent.com/u/1024932?u=b2ea249c6b41ddf98679c8d110d0f67d4a3ebf93&v=4 - url: https://github.com/AdrianDeAnda -- login: ArcLightSlavik - count: 31 - avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=b0f2c37142f4b762e41ad65dc49581813422bd71&v=4 - url: https://github.com/ArcLightSlavik -- login: cassiobotaro - count: 28 - avatarUrl: https://avatars.githubusercontent.com/u/3127847?u=a08022b191ddbd0a6159b2981d9d878b6d5bb71f&v=4 - url: https://github.com/cassiobotaro -- login: lsglucas - count: 27 - avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4 - url: https://github.com/lsglucas -- login: komtaki - count: 27 - avatarUrl: https://avatars.githubusercontent.com/u/39375566?u=260ad6b1a4b34c07dbfa728da5e586f16f6d1824&v=4 - url: https://github.com/komtaki -- login: YuriiMotov - count: 25 - avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=e83a39697a2d33ab2ec9bfbced794ee48bc29cec&v=4 - url: https://github.com/YuriiMotov -- login: Ryandaydev - count: 25 - avatarUrl: https://avatars.githubusercontent.com/u/4292423?u=48f68868db8886fce31a1d802c1003914c6cd7c6&v=4 - url: https://github.com/Ryandaydev -- login: LorhanSohaky - count: 24 - avatarUrl: https://avatars.githubusercontent.com/u/16273730?u=095b66f243a2cd6a0aadba9a095009f8aaf18393&v=4 - url: https://github.com/LorhanSohaky -- login: dmontagu - count: 23 - avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4 - url: https://github.com/dmontagu -- login: nilslindemann - count: 23 - avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 - url: https://github.com/nilslindemann -- login: hard-coders - count: 23 - avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 - url: https://github.com/hard-coders -- login: rjNemo - count: 21 - avatarUrl: https://avatars.githubusercontent.com/u/56785022?u=d5c3a02567c8649e146fcfc51b6060ccaf8adef8&v=4 - url: https://github.com/rjNemo -- login: odiseo0 - count: 20 - avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=241a71f6b7068738b81af3e57f45ffd723538401&v=4 - url: https://github.com/odiseo0 -- login: 0417taehyun - count: 19 - avatarUrl: https://avatars.githubusercontent.com/u/63915557?u=47debaa860fd52c9b98c97ef357ddcec3b3fb399&v=4 - url: https://github.com/0417taehyun -- login: JavierSanchezCastro - count: 19 - avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 - url: https://github.com/JavierSanchezCastro -- login: Smlep - count: 17 - avatarUrl: https://avatars.githubusercontent.com/u/16785985?v=4 - url: https://github.com/Smlep -- login: zy7y - count: 17 - avatarUrl: https://avatars.githubusercontent.com/u/67154681?u=5d634834cc514028ea3f9115f7030b99a1f4d5a4&v=4 - url: https://github.com/zy7y -- login: junah201 - count: 17 - avatarUrl: https://avatars.githubusercontent.com/u/75025529?u=2451c256e888fa2a06bcfc0646d09b87ddb6a945&v=4 - url: https://github.com/junah201 -- login: peidrao - count: 17 - avatarUrl: https://avatars.githubusercontent.com/u/32584628?u=a66902b40c13647d0ed0e573d598128240a4dd04&v=4 - url: https://github.com/peidrao -- login: yanever - count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/21978760?v=4 - url: https://github.com/yanever -- login: SwftAlpc - count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/52768429?u=6a3aa15277406520ad37f6236e89466ed44bc5b8&v=4 - url: https://github.com/SwftAlpc -- login: axel584 - count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/1334088?u=9667041f5b15dc002b6f9665fda8c0412933ac04&v=4 - url: https://github.com/axel584 -- login: codespearhead - count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/72931357?u=0fce6b82219b604d58adb614a761556425579cb5&v=4 - url: https://github.com/codespearhead -- login: Alexandrhub - count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/119126536?u=9fc0d48f3307817bafecc5861eb2168401a6cb04&v=4 - url: https://github.com/Alexandrhub -- login: DevDae - count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/87962045?u=08e10fa516e844934f4b3fc7c38b33c61697e4a1&v=4 - url: https://github.com/DevDae -- login: Aruelius - count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/25380989?u=574f8cfcda3ea77a3f81884f6b26a97068e36a9d&v=4 - url: https://github.com/Aruelius -- login: OzgunCaglarArslan - count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/86166426?v=4 - url: https://github.com/OzgunCaglarArslan -- login: pedabraham - count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/16860088?u=abf922a7b920bf8fdb7867d8b43e091f1e796178&v=4 - url: https://github.com/pedabraham -- login: delhi09 - count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/63476957?u=6c86e59b48e0394d4db230f37fc9ad4d7e2c27c7&v=4 - url: https://github.com/delhi09 -- login: wdh99 - count: 14 - avatarUrl: https://avatars.githubusercontent.com/u/108172295?u=8a8fb95d5afe3e0fa33257b2aecae88d436249eb&v=4 - url: https://github.com/wdh99 -- login: sh0nk - count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/6478810?u=af15d724875cec682ed8088a86d36b2798f981c0&v=4 - url: https://github.com/sh0nk -- login: r0b2g1t - count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/5357541?u=6428442d875d5d71aaa1bb38bb11c4be1a526bc2&v=4 - url: https://github.com/r0b2g1t -- login: RunningIkkyu - count: 12 - avatarUrl: https://avatars.githubusercontent.com/u/31848542?u=494ecc298e3f26197495bb357ad0f57cfd5f7a32&v=4 - url: https://github.com/RunningIkkyu -- login: ivan-abc - count: 12 - avatarUrl: https://avatars.githubusercontent.com/u/36765187?u=c6e0ba571c1ccb6db9d94e62e4b8b5eda811a870&v=4 - url: https://github.com/ivan-abc -- login: AlertRED - count: 12 - avatarUrl: https://avatars.githubusercontent.com/u/15695000?u=f5a4944c6df443030409c88da7d7fa0b7ead985c&v=4 - url: https://github.com/AlertRED -- login: solomein-sv - count: 11 - avatarUrl: https://avatars.githubusercontent.com/u/46193920?u=789927ee09cfabd752d3bd554fa6baf4850d2777&v=4 - url: https://github.com/solomein-sv -top_translations_reviewers: -- login: s111d - count: 146 - avatarUrl: https://avatars.githubusercontent.com/u/4954856?v=4 - url: https://github.com/s111d -- login: Xewus - count: 128 - avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4 - url: https://github.com/Xewus -- login: tokusumi - count: 104 - avatarUrl: https://avatars.githubusercontent.com/u/41147016?u=55010621aece725aa702270b54fed829b6a1fe60&v=4 - url: https://github.com/tokusumi -- login: hasansezertasan - count: 91 - avatarUrl: https://avatars.githubusercontent.com/u/13135006?u=99f0b0f0fc47e88e8abb337b4447357939ef93e7&v=4 - url: https://github.com/hasansezertasan -- login: AlertRED - count: 70 - avatarUrl: https://avatars.githubusercontent.com/u/15695000?u=f5a4944c6df443030409c88da7d7fa0b7ead985c&v=4 - url: https://github.com/AlertRED -- login: Alexandrhub - count: 68 - avatarUrl: https://avatars.githubusercontent.com/u/119126536?u=9fc0d48f3307817bafecc5861eb2168401a6cb04&v=4 - url: https://github.com/Alexandrhub -- login: waynerv - count: 63 - avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4 - url: https://github.com/waynerv -- login: hard-coders - count: 53 - avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 - url: https://github.com/hard-coders -- login: Laineyzhang55 - count: 48 - avatarUrl: https://avatars.githubusercontent.com/u/59285379?v=4 - url: https://github.com/Laineyzhang55 -- login: Kludex - count: 46 - avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 - url: https://github.com/Kludex -- login: komtaki - count: 45 - avatarUrl: https://avatars.githubusercontent.com/u/39375566?u=260ad6b1a4b34c07dbfa728da5e586f16f6d1824&v=4 - url: https://github.com/komtaki -- login: alperiox - count: 42 - avatarUrl: https://avatars.githubusercontent.com/u/34214152?u=2c5acad3461d4dbc2d48371ba86cac56ae9b25cc&v=4 - url: https://github.com/alperiox -- login: Winand - count: 40 - avatarUrl: https://avatars.githubusercontent.com/u/53390?u=bb0e71a2fc3910a8e0ee66da67c33de40ea695f8&v=4 - url: https://github.com/Winand -- login: solomein-sv - count: 38 - avatarUrl: https://avatars.githubusercontent.com/u/46193920?u=789927ee09cfabd752d3bd554fa6baf4850d2777&v=4 - url: https://github.com/solomein-sv -- login: lsglucas - count: 36 - avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4 - url: https://github.com/lsglucas -- login: SwftAlpc - count: 36 - avatarUrl: https://avatars.githubusercontent.com/u/52768429?u=6a3aa15277406520ad37f6236e89466ed44bc5b8&v=4 - url: https://github.com/SwftAlpc -- login: nilslindemann - count: 35 - avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 - url: https://github.com/nilslindemann -- login: rjNemo - count: 34 - avatarUrl: https://avatars.githubusercontent.com/u/56785022?u=d5c3a02567c8649e146fcfc51b6060ccaf8adef8&v=4 - url: https://github.com/rjNemo -- login: akarev0 - count: 33 - avatarUrl: https://avatars.githubusercontent.com/u/53393089?u=6e528bb4789d56af887ce6fe237bea4010885406&v=4 - url: https://github.com/akarev0 -- login: romashevchenko - count: 32 - avatarUrl: https://avatars.githubusercontent.com/u/132477732?v=4 - url: https://github.com/romashevchenko -- login: wdh99 - count: 31 - avatarUrl: https://avatars.githubusercontent.com/u/108172295?u=8a8fb95d5afe3e0fa33257b2aecae88d436249eb&v=4 - url: https://github.com/wdh99 -- login: LorhanSohaky - count: 30 - avatarUrl: https://avatars.githubusercontent.com/u/16273730?u=095b66f243a2cd6a0aadba9a095009f8aaf18393&v=4 - url: https://github.com/LorhanSohaky -- login: cassiobotaro - count: 29 - avatarUrl: https://avatars.githubusercontent.com/u/3127847?u=a08022b191ddbd0a6159b2981d9d878b6d5bb71f&v=4 - url: https://github.com/cassiobotaro -- login: pedabraham - count: 28 - avatarUrl: https://avatars.githubusercontent.com/u/16860088?u=abf922a7b920bf8fdb7867d8b43e091f1e796178&v=4 - url: https://github.com/pedabraham -- login: Smlep - count: 28 - avatarUrl: https://avatars.githubusercontent.com/u/16785985?v=4 - url: https://github.com/Smlep -- login: dedkot01 - count: 28 - avatarUrl: https://avatars.githubusercontent.com/u/26196675?u=e2966887124e67932853df4f10f86cb526edc7b0&v=4 - url: https://github.com/dedkot01 -- login: hsuanchi - count: 28 - avatarUrl: https://avatars.githubusercontent.com/u/24913710?u=0b094ae292292fee093818e37ceb645c114d2bff&v=4 - url: https://github.com/hsuanchi -- login: dpinezich - count: 28 - avatarUrl: https://avatars.githubusercontent.com/u/3204540?u=a2e1465e3ee10d537614d513589607eddefde09f&v=4 - url: https://github.com/dpinezich -- login: maoyibo - count: 27 - avatarUrl: https://avatars.githubusercontent.com/u/7887703?v=4 - url: https://github.com/maoyibo -- login: 0417taehyun - count: 27 - avatarUrl: https://avatars.githubusercontent.com/u/63915557?u=47debaa860fd52c9b98c97ef357ddcec3b3fb399&v=4 - url: https://github.com/0417taehyun -- login: BilalAlpaslan - count: 26 - avatarUrl: https://avatars.githubusercontent.com/u/47563997?u=63ed66e304fe8d765762c70587d61d9196e5c82d&v=4 - url: https://github.com/BilalAlpaslan -- login: zy7y - count: 25 - avatarUrl: https://avatars.githubusercontent.com/u/67154681?u=5d634834cc514028ea3f9115f7030b99a1f4d5a4&v=4 - url: https://github.com/zy7y -- login: mycaule - count: 25 - avatarUrl: https://avatars.githubusercontent.com/u/6161385?u=e3cec75bd6d938a0d73fae0dc5534d1ab2ed1b0e&v=4 - url: https://github.com/mycaule -- login: sh0nk - count: 23 - avatarUrl: https://avatars.githubusercontent.com/u/6478810?u=af15d724875cec682ed8088a86d36b2798f981c0&v=4 - url: https://github.com/sh0nk -- login: axel584 - count: 23 - avatarUrl: https://avatars.githubusercontent.com/u/1334088?u=9667041f5b15dc002b6f9665fda8c0412933ac04&v=4 - url: https://github.com/axel584 -- login: AGolicyn - count: 21 - avatarUrl: https://avatars.githubusercontent.com/u/86262613?u=3c21606ab8d210a061a1673decff1e7d5592b380&v=4 - url: https://github.com/AGolicyn -- login: OzgunCaglarArslan - count: 21 - avatarUrl: https://avatars.githubusercontent.com/u/86166426?v=4 - url: https://github.com/OzgunCaglarArslan -- login: Attsun1031 - count: 20 - avatarUrl: https://avatars.githubusercontent.com/u/1175560?v=4 - url: https://github.com/Attsun1031 -- login: ycd - count: 20 - avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=29682e4b6ac7d5293742ccf818188394b9a82972&v=4 - url: https://github.com/ycd -- login: delhi09 - count: 20 - avatarUrl: https://avatars.githubusercontent.com/u/63476957?u=6c86e59b48e0394d4db230f37fc9ad4d7e2c27c7&v=4 - url: https://github.com/delhi09 -- login: rogerbrinkmann - count: 20 - avatarUrl: https://avatars.githubusercontent.com/u/5690226?v=4 - url: https://github.com/rogerbrinkmann -- login: DevDae - count: 20 - avatarUrl: https://avatars.githubusercontent.com/u/87962045?u=08e10fa516e844934f4b3fc7c38b33c61697e4a1&v=4 - url: https://github.com/DevDae -- login: sattosan - count: 19 - avatarUrl: https://avatars.githubusercontent.com/u/20574756?u=b0d8474d2938189c6954423ae8d81d91013f80a8&v=4 - url: https://github.com/sattosan -- login: ComicShrimp - count: 18 - avatarUrl: https://avatars.githubusercontent.com/u/43503750?u=d2fbf412e7730183ce91686ca48d4147e1b7dc74&v=4 - url: https://github.com/ComicShrimp -- login: junah201 - count: 18 - avatarUrl: https://avatars.githubusercontent.com/u/75025529?u=2451c256e888fa2a06bcfc0646d09b87ddb6a945&v=4 - url: https://github.com/junah201 -- login: simatheone - count: 18 - avatarUrl: https://avatars.githubusercontent.com/u/78508673?u=1b9658d9ee0bde33f56130dd52275493ddd38690&v=4 - url: https://github.com/simatheone -- login: ivan-abc - count: 18 - avatarUrl: https://avatars.githubusercontent.com/u/36765187?u=c6e0ba571c1ccb6db9d94e62e4b8b5eda811a870&v=4 - url: https://github.com/ivan-abc -- login: JavierSanchezCastro - count: 18 - avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 - url: https://github.com/JavierSanchezCastro -- login: bezaca - count: 17 - avatarUrl: https://avatars.githubusercontent.com/u/69092910?u=4ac58eab99bd37d663f3d23551df96d4fbdbf760&v=4 - url: https://github.com/bezaca + avatarUrl: https://avatars.githubusercontent.com/u/104530599?u=d87b866e7c1db970d6f8e8031643818349b046d5&v=4 + url: https://github.com/ahmedabdou14 +- login: nbx3 + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/34649527?u=943812f69e0d40adbd3fa1c9b8ef50dd971a2a45&v=4 + url: https://github.com/nbx3 +- login: yanggeorge + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/2434407?v=4 + url: https://github.com/yanggeorge +- login: XiaoXinYo + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/56395004?u=b3b7cb758997f283c271a581833e407229dab82c&v=4 + url: https://github.com/XiaoXinYo +- login: anantgupta129 + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/66518357?u=6e25dcd84638f17d2c6df5dc26f07fd7c6dc118e&v=4 + url: https://github.com/anantgupta129 +- login: slafs + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/210173?v=4 + url: https://github.com/slafs +- login: CarlosOliveira-23 + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/102637302?u=cf350a4db956f30cbb2c27d3be0d15c282e32b14&v=4 + url: https://github.com/CarlosOliveira-23 +- login: monchin + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/18521800?v=4 + url: https://github.com/monchin +- login: AmirHmZz + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/38752106?u=07f80e451bda00a9492bbc764e49d24ad3ada8cc&v=4 + url: https://github.com/AmirHmZz +- login: Leon0824 + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/1922026?v=4 + url: https://github.com/Leon0824 +- login: iloveitaly + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/150855?v=4 + url: https://github.com/iloveitaly +- login: msukmanowsky + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/362755?u=782e6bf5b9f0356c3f74b4d894fda9f179252086&v=4 + url: https://github.com/msukmanowsky +- login: shurshilov + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/11828278?u=6bcadc5ce4f2f56a514331c9f68eb987d4afe29a&v=4 + url: https://github.com/shurshilov +- login: LincolnPuzey + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/18750802?v=4 + url: https://github.com/LincolnPuzey From 8525b879edfd85c1739e513139aab7120b780044 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 28 Jan 2025 20:41:36 +0000 Subject: [PATCH 026/117] =?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 3db7042fb..20f800d85 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Docs + +* 👥 Update FastAPI People - Experts. PR [#13269](https://github.com/fastapi/fastapi/pull/13269) by [@tiangolo](https://github.com/tiangolo). + ### Translations * 🌐 Add Japanese translation for `docs/ja/docs/environment-variables.md`. PR [#13226](https://github.com/fastapi/fastapi/pull/13226) by [@k94-ishi](https://github.com/k94-ishi). From 326fec16b9e3d61c584182b212436f88d81d0acb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 28 Jan 2025 21:47:33 +0000 Subject: [PATCH 027/117] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20and=20m?= =?UTF-8?q?ove=20`scripts/notify=5Ftranslations.py`,=20no=20need=20for=20a?= =?UTF-8?q?=20custom=20GitHub=20Action=20(#13270)?= 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> --- .../actions/notify-translations/Dockerfile | 7 --- .../actions/notify-translations/action.yml | 10 --- .github/workflows/notify-translations.yml | 20 +++--- .../main.py => scripts/notify_translations.py | 63 +++++++++++-------- 4 files changed, 51 insertions(+), 49 deletions(-) delete mode 100644 .github/actions/notify-translations/Dockerfile delete mode 100644 .github/actions/notify-translations/action.yml rename .github/actions/notify-translations/app/main.py => scripts/notify_translations.py (89%) diff --git a/.github/actions/notify-translations/Dockerfile b/.github/actions/notify-translations/Dockerfile deleted file mode 100644 index b68b4bb1a..000000000 --- a/.github/actions/notify-translations/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM python:3.9 - -RUN pip install httpx PyGithub "pydantic==1.5.1" "pyyaml>=5.3.1,<6.0.0" - -COPY ./app /app - -CMD ["python", "/app/main.py"] diff --git a/.github/actions/notify-translations/action.yml b/.github/actions/notify-translations/action.yml deleted file mode 100644 index c3579977c..000000000 --- a/.github/actions/notify-translations/action.yml +++ /dev/null @@ -1,10 +0,0 @@ -name: "Notify Translations" -description: "Notify in the issue for a translation when there's a new PR available" -author: "Sebastián Ramírez " -inputs: - token: - description: 'Token, to read the GitHub API. Can be passed in using {{ secrets.GITHUB_TOKEN }}' - required: true -runs: - using: 'docker' - image: 'Dockerfile' diff --git a/.github/workflows/notify-translations.yml b/.github/workflows/notify-translations.yml index 187322bca..c96992689 100644 --- a/.github/workflows/notify-translations.yml +++ b/.github/workflows/notify-translations.yml @@ -15,15 +15,14 @@ on: required: false default: 'false' -permissions: - discussions: write - env: UV_SYSTEM_PYTHON: 1 jobs: - notify-translations: + job: runs-on: ubuntu-latest + permissions: + discussions: write steps: - name: Dump GitHub context env: @@ -42,12 +41,19 @@ jobs: cache-dependency-glob: | requirements**.txt pyproject.toml + - name: Install Dependencies + run: uv pip install -r requirements-github-actions.txt # Allow debugging with tmate - name: Setup tmate session uses: mxschmitt/action-tmate@v3 if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} with: limit-access-to-actor: true - - uses: ./.github/actions/notify-translations - with: - token: ${{ secrets.GITHUB_TOKEN }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Notify Translations + run: python ./scripts/notify_translations.py + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NUMBER: ${{ github.event.inputs.number || null }} + DEBUG: ${{ github.event.inputs.debug_enabled || 'false' }} diff --git a/.github/actions/notify-translations/app/main.py b/scripts/notify_translations.py similarity index 89% rename from .github/actions/notify-translations/app/main.py rename to scripts/notify_translations.py index 716232d49..7a43019a6 100644 --- a/.github/actions/notify-translations/app/main.py +++ b/scripts/notify_translations.py @@ -7,12 +7,13 @@ from typing import Any, Dict, List, Union, cast import httpx from github import Github -from pydantic import BaseModel, BaseSettings, SecretStr +from pydantic import BaseModel, SecretStr +from pydantic_settings import BaseSettings awaiting_label = "awaiting-review" lang_all_label = "lang-all" approved_label = "approved-1" -translations_path = Path(__file__).parent / "translations.yml" + github_graphql_url = "https://api.github.com/graphql" questions_translations_category_id = "DIC_kwDOCZduT84CT5P9" @@ -176,19 +177,20 @@ class AllDiscussionsResponse(BaseModel): class Settings(BaseSettings): github_repository: str - input_token: SecretStr + github_token: SecretStr github_event_path: Path github_event_name: Union[str, None] = None httpx_timeout: int = 30 - input_debug: Union[bool, None] = False + debug: Union[bool, None] = False + number: int | None = None class PartialGitHubEventIssue(BaseModel): - number: int + number: int | None = None class PartialGitHubEvent(BaseModel): - pull_request: PartialGitHubEventIssue + pull_request: PartialGitHubEventIssue | None = None def get_graphql_response( @@ -202,9 +204,7 @@ def get_graphql_response( comment_id: Union[str, None] = None, body: Union[str, None] = None, ) -> Dict[str, Any]: - headers = {"Authorization": f"token {settings.input_token.get_secret_value()}"} - # some fields are only used by one query, but GraphQL allows unused variables, so - # keep them here for simplicity + headers = {"Authorization": f"token {settings.github_token.get_secret_value()}"} variables = { "after": after, "category_id": category_id, @@ -228,37 +228,40 @@ def get_graphql_response( data = response.json() if "errors" in data: logging.error(f"Errors in response, after: {after}, category_id: {category_id}") + logging.error(data["errors"]) logging.error(response.text) raise RuntimeError(response.text) return cast(Dict[str, Any], data) -def get_graphql_translation_discussions(*, settings: Settings): +def get_graphql_translation_discussions( + *, settings: Settings +) -> List[AllDiscussionsDiscussionNode]: data = get_graphql_response( settings=settings, query=all_discussions_query, category_id=questions_translations_category_id, ) - graphql_response = AllDiscussionsResponse.parse_obj(data) + graphql_response = AllDiscussionsResponse.model_validate(data) return graphql_response.data.repository.discussions.nodes def get_graphql_translation_discussion_comments_edges( *, settings: Settings, discussion_number: int, after: Union[str, None] = None -): +) -> List[CommentsEdge]: data = get_graphql_response( settings=settings, query=translation_discussion_query, discussion_number=discussion_number, after=after, ) - graphql_response = CommentsResponse.parse_obj(data) + graphql_response = CommentsResponse.model_validate(data) return graphql_response.data.repository.discussion.comments.edges def get_graphql_translation_discussion_comments( *, settings: Settings, discussion_number: int -): +) -> list[Comment]: comment_nodes: List[Comment] = [] discussion_edges = get_graphql_translation_discussion_comments_edges( settings=settings, discussion_number=discussion_number @@ -276,43 +279,49 @@ def get_graphql_translation_discussion_comments( return comment_nodes -def create_comment(*, settings: Settings, discussion_id: str, body: str): +def create_comment(*, settings: Settings, discussion_id: str, body: str) -> Comment: data = get_graphql_response( settings=settings, query=add_comment_mutation, discussion_id=discussion_id, body=body, ) - response = AddCommentResponse.parse_obj(data) + response = AddCommentResponse.model_validate(data) return response.data.addDiscussionComment.comment -def update_comment(*, settings: Settings, comment_id: str, body: str): +def update_comment(*, settings: Settings, comment_id: str, body: str) -> Comment: data = get_graphql_response( settings=settings, query=update_comment_mutation, comment_id=comment_id, body=body, ) - response = UpdateCommentResponse.parse_obj(data) + response = UpdateCommentResponse.model_validate(data) return response.data.updateDiscussionComment.comment -if __name__ == "__main__": +def main() -> None: settings = Settings() - if settings.input_debug: + if settings.debug: logging.basicConfig(level=logging.DEBUG) else: logging.basicConfig(level=logging.INFO) - logging.debug(f"Using config: {settings.json()}") - g = Github(settings.input_token.get_secret_value()) + logging.debug(f"Using config: {settings.model_dump_json()}") + g = Github(settings.github_token.get_secret_value()) repo = g.get_repo(settings.github_repository) if not settings.github_event_path.is_file(): raise RuntimeError( f"No github event file available at: {settings.github_event_path}" ) contents = settings.github_event_path.read_text() - github_event = PartialGitHubEvent.parse_raw(contents) + github_event = PartialGitHubEvent.model_validate_json(contents) + logging.info(f"Using GitHub event: {github_event}") + number = ( + github_event.pull_request and github_event.pull_request.number + ) or settings.number + if number is None: + raise RuntimeError("No PR number available") # Avoid race conditions with multiple labels sleep_time = random.random() * 10 # random number between 0 and 10 seconds @@ -323,8 +332,8 @@ if __name__ == "__main__": time.sleep(sleep_time) # Get PR - logging.debug(f"Processing PR: #{github_event.pull_request.number}") - pr = repo.get_pull(github_event.pull_request.number) + logging.debug(f"Processing PR: #{number}") + pr = repo.get_pull(number) label_strs = {label.name for label in pr.get_labels()} langs = [] for label in label_strs: @@ -415,3 +424,7 @@ if __name__ == "__main__": f"There doesn't seem to be anything to be done about PR #{pr.number}" ) logging.info("Finished") + + +if __name__ == "__main__": + main() From a058d8ecbc41b329bad2f5ca0575c50b7bc171b2 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 28 Jan 2025 21:47:55 +0000 Subject: [PATCH 028/117] =?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 20f800d85..2abee78dc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -20,6 +20,7 @@ hide: ### Internal +* ♻️ Refactor and move `scripts/notify_translations.py`, no need for a custom GitHub Action. PR [#13270](https://github.com/fastapi/fastapi/pull/13270) by [@tiangolo](https://github.com/tiangolo). * 🔨 Update FastAPI People Experts script, refactor and optimize data fetching to handle rate limits. PR [#13267](https://github.com/fastapi/fastapi/pull/13267) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump pypa/gh-action-pypi-publish from 1.12.3 to 1.12.4. PR [#13251](https://github.com/fastapi/fastapi/pull/13251) by [@dependabot[bot]](https://github.com/apps/dependabot). From 0a2b24653b2b0e2a4753a1e239013b26fa1516b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 28 Jan 2025 22:30:15 +0000 Subject: [PATCH 029/117] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Fix=20`notify=5Ftr?= =?UTF-8?q?anslations.py`=20empty=20env=20var=20handling=20for=20PR=20labe?= =?UTF-8?q?l=20events=20vs=20workflow=5Fdispatch=20(#13272)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/notify_translations.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/notify_translations.py b/scripts/notify_translations.py index 7a43019a6..c300624db 100644 --- a/scripts/notify_translations.py +++ b/scripts/notify_translations.py @@ -176,6 +176,8 @@ class AllDiscussionsResponse(BaseModel): class Settings(BaseSettings): + model_config = {"env_ignore_empty": True} + github_repository: str github_token: SecretStr github_event_path: Path From 92b745461cb280aea3c5905c5e241502207064fa Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 28 Jan 2025 22:30:38 +0000 Subject: [PATCH 030/117] =?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 2abee78dc..e65fe9848 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -20,6 +20,7 @@ hide: ### Internal +* ♻️ Fix `notify_translations.py` empty env var handling for PR label events vs workflow_dispatch. PR [#13272](https://github.com/fastapi/fastapi/pull/13272) by [@tiangolo](https://github.com/tiangolo). * ♻️ Refactor and move `scripts/notify_translations.py`, no need for a custom GitHub Action. PR [#13270](https://github.com/fastapi/fastapi/pull/13270) by [@tiangolo](https://github.com/tiangolo). * 🔨 Update FastAPI People Experts script, refactor and optimize data fetching to handle rate limits. PR [#13267](https://github.com/fastapi/fastapi/pull/13267) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump pypa/gh-action-pypi-publish from 1.12.3 to 1.12.4. PR [#13251](https://github.com/fastapi/fastapi/pull/13251) by [@dependabot[bot]](https://github.com/apps/dependabot). From e747f1938a3853c6af695f6447fd8f8749e1ba9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 28 Jan 2025 22:36:15 +0000 Subject: [PATCH 031/117] =?UTF-8?q?=F0=9F=94=A7=20Update=20Sponsors=20badg?= =?UTF-8?q?es=20(#13271)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/data/sponsors_badge.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/en/data/sponsors_badge.yml b/docs/en/data/sponsors_badge.yml index 3e885a2f7..d507a500f 100644 --- a/docs/en/data/sponsors_badge.yml +++ b/docs/en/data/sponsors_badge.yml @@ -29,7 +29,12 @@ logins: - andrew-propelauth - svix - zuplo-oss + - zuplo - Kong - speakeasy-api - jess-render - blockbee-io + - liblaber + - render-sponsorships + - renderinc + - stainless-api From 93e9fed2e84554197a0b480104a9e3c5b8897545 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 28 Jan 2025 22:36:38 +0000 Subject: [PATCH 032/117] =?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 e65fe9848..c0cb88e21 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -20,6 +20,7 @@ hide: ### Internal +* 🔧 Update Sponsors badges. PR [#13271](https://github.com/fastapi/fastapi/pull/13271) by [@tiangolo](https://github.com/tiangolo). * ♻️ Fix `notify_translations.py` empty env var handling for PR label events vs workflow_dispatch. PR [#13272](https://github.com/fastapi/fastapi/pull/13272) by [@tiangolo](https://github.com/tiangolo). * ♻️ Refactor and move `scripts/notify_translations.py`, no need for a custom GitHub Action. PR [#13270](https://github.com/fastapi/fastapi/pull/13270) by [@tiangolo](https://github.com/tiangolo). * 🔨 Update FastAPI People Experts script, refactor and optimize data fetching to handle rate limits. PR [#13267](https://github.com/fastapi/fastapi/pull/13267) by [@tiangolo](https://github.com/tiangolo). From 8c6f10b64a333cb1395da5cb714fd1aa28e7b5ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 28 Jan 2025 23:35:19 +0000 Subject: [PATCH 033/117] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20AnyIO=20?= =?UTF-8?q?max=20version=20for=20tests,=20new=20range:=20`>=3D3.2.1,<5.0.0?= =?UTF-8?q?`=20(#13273)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 4 ++++ requirements-tests.txt | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e0daf7472..793e789e2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -81,6 +81,10 @@ jobs: - name: Install Pydantic v2 if: matrix.pydantic-version == 'pydantic-v2' run: uv pip install --upgrade "pydantic>=2.0.2,<3.0.0" + # TODO: Remove this once Python 3.8 is no longer supported + - name: Install older AnyIO in Python 3.8 + if: matrix.python-version == '3.8' + run: uv pip install "anyio[trio]<4.0.0" - run: mkdir coverage - name: Test run: bash scripts/test.sh diff --git a/requirements-tests.txt b/requirements-tests.txt index 5be052307..91e7fb7aa 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -6,7 +6,7 @@ mypy ==1.8.0 dirty-equals ==0.8.0 sqlmodel==0.0.22 flask >=1.1.2,<4.0.0 -anyio[trio] >=3.2.1,<4.0.0 +anyio[trio] >=3.2.1,<5.0.0 PyJWT==2.8.0 pyyaml >=5.3.1,<7.0.0 passlib[bcrypt] >=1.7.2,<2.0.0 From eab0653a346196bff6928710410890a300aee4ae Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 28 Jan 2025 23:35:44 +0000 Subject: [PATCH 034/117] =?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 c0cb88e21..ad8b85d0e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -20,6 +20,7 @@ hide: ### Internal +* ⬆️ Upgrade AnyIO max version for tests, new range: `>=3.2.1,<5.0.0`. PR [#13273](https://github.com/fastapi/fastapi/pull/13273) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update Sponsors badges. PR [#13271](https://github.com/fastapi/fastapi/pull/13271) by [@tiangolo](https://github.com/tiangolo). * ♻️ Fix `notify_translations.py` empty env var handling for PR label events vs workflow_dispatch. PR [#13272](https://github.com/fastapi/fastapi/pull/13272) by [@tiangolo](https://github.com/tiangolo). * ♻️ Refactor and move `scripts/notify_translations.py`, no need for a custom GitHub Action. PR [#13270](https://github.com/fastapi/fastapi/pull/13270) by [@tiangolo](https://github.com/tiangolo). From bd106fc750fb0d4b114b69e1f1f9ae5dabaada44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 29 Jan 2025 18:02:27 +0000 Subject: [PATCH 035/117] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Add=20support=20fo?= =?UTF-8?q?r=20Python=203.13=20(#13274)?= 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> --- .github/workflows/test.yml | 1 + pyproject.toml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 793e789e2..5e8092641 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -48,6 +48,7 @@ jobs: strategy: matrix: python-version: + - "3.13" - "3.12" - "3.11" - "3.10" diff --git a/pyproject.toml b/pyproject.toml index 381eb50bf..51d63fd44 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Internet :: WWW/HTTP", ] @@ -162,6 +163,8 @@ filterwarnings = [ # Ref: https://github.com/python-trio/trio/pull/3054 # Remove once there's a new version of Trio 'ignore:The `hash` argument is deprecated*:DeprecationWarning:trio', + # Ignore flaky coverage / pytest warning about SQLite connection, only applies to Python 3.13 and Pydantic v1 + 'ignore:Exception ignored in. Date: Wed, 29 Jan 2025 18:02:50 +0000 Subject: [PATCH 036/117] =?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 ad8b85d0e..e21d2521f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -20,6 +20,7 @@ hide: ### Internal +* ⬆️ Add support for Python 3.13. PR [#13274](https://github.com/fastapi/fastapi/pull/13274) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade AnyIO max version for tests, new range: `>=3.2.1,<5.0.0`. PR [#13273](https://github.com/fastapi/fastapi/pull/13273) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update Sponsors badges. PR [#13271](https://github.com/fastapi/fastapi/pull/13271) by [@tiangolo](https://github.com/tiangolo). * ♻️ Fix `notify_translations.py` empty env var handling for PR label events vs workflow_dispatch. PR [#13272](https://github.com/fastapi/fastapi/pull/13272) by [@tiangolo](https://github.com/tiangolo). From c5b5af7c532e66d3ac25bd36630e2cd89f24d049 Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Thu, 30 Jan 2025 12:04:34 +0000 Subject: [PATCH 037/117] =?UTF-8?q?=E2=9C=85=20Simplify=20tests=20for=20re?= =?UTF-8?q?quest=5Ffiles=20(#13182)?= 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: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../test_tutorial003.py | 21 +- .../test_tutorial003_an.py | 273 ----------------- .../test_tutorial003_an_py310.py | 279 ------------------ .../test_tutorial003_an_py39.py | 279 ------------------ .../test_tutorial003_py310.py | 279 ------------------ .../test_request_files/test_tutorial001.py | 42 +-- .../test_request_files/test_tutorial001_02.py | 35 ++- .../test_tutorial001_02_an.py | 208 ------------- .../test_tutorial001_02_an_py310.py | 220 -------------- .../test_tutorial001_02_an_py39.py | 220 -------------- .../test_tutorial001_02_py310.py | 220 -------------- .../test_request_files/test_tutorial001_03.py | 28 +- .../test_tutorial001_03_an.py | 159 ---------- .../test_tutorial001_03_an_py39.py | 167 ----------- .../test_request_files/test_tutorial001_an.py | 218 -------------- .../test_tutorial001_an_py39.py | 228 -------------- .../test_request_files/test_tutorial002.py | 39 ++- .../test_request_files/test_tutorial002_an.py | 249 ---------------- .../test_tutorial002_an_py39.py | 268 ----------------- .../test_tutorial002_py39.py | 279 ------------------ .../test_request_files/test_tutorial003.py | 35 ++- .../test_request_files/test_tutorial003_an.py | 194 ------------ .../test_tutorial003_an_py39.py | 222 -------------- .../test_tutorial003_py39.py | 222 -------------- 24 files changed, 146 insertions(+), 4238 deletions(-) delete mode 100644 tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py delete mode 100644 tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py delete mode 100644 tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py delete mode 100644 tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py delete mode 100644 tests/test_tutorial/test_request_files/test_tutorial001_02_an.py delete mode 100644 tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py delete mode 100644 tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py delete mode 100644 tests/test_tutorial/test_request_files/test_tutorial001_02_py310.py delete mode 100644 tests/test_tutorial/test_request_files/test_tutorial001_03_an.py delete mode 100644 tests/test_tutorial/test_request_files/test_tutorial001_03_an_py39.py delete mode 100644 tests/test_tutorial/test_request_files/test_tutorial001_an.py delete mode 100644 tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py delete mode 100644 tests/test_tutorial/test_request_files/test_tutorial002_an.py delete mode 100644 tests/test_tutorial/test_request_files/test_tutorial002_an_py39.py delete mode 100644 tests/test_tutorial/test_request_files/test_tutorial002_py39.py delete mode 100644 tests/test_tutorial/test_request_files/test_tutorial003_an.py delete mode 100644 tests/test_tutorial/test_request_files/test_tutorial003_an_py39.py delete mode 100644 tests/test_tutorial/test_request_files/test_tutorial003_py39.py diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py index c26f8b89b..d18ceae48 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py @@ -1,13 +1,26 @@ +import importlib + import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient +from ...utils import needs_py39, needs_py310 + -@pytest.fixture(name="client") -def get_client(): - from docs_src.body_multiple_params.tutorial003 import app +@pytest.fixture( + name="client", + params=[ + "tutorial003", + pytest.param("tutorial003_py310", marks=needs_py310), + "tutorial003_an", + pytest.param("tutorial003_an_py39", marks=needs_py39), + pytest.param("tutorial003_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body_multiple_params.{request.param}") - client = TestClient(app) + client = TestClient(mod.app) return client diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py deleted file mode 100644 index 62c7e2fad..000000000 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py +++ /dev/null @@ -1,273 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.body_multiple_params.tutorial003_an import app - - client = TestClient(app) - return client - - -def test_post_body_valid(client: TestClient): - response = client.put( - "/items/5", - json={ - "importance": 2, - "item": {"name": "Foo", "price": 50.5}, - "user": {"username": "Dave"}, - }, - ) - assert response.status_code == 200 - assert response.json() == { - "item_id": 5, - "importance": 2, - "item": { - "name": "Foo", - "price": 50.5, - "description": None, - "tax": None, - }, - "user": {"username": "Dave", "full_name": None}, - } - - -def test_post_body_no_data(client: TestClient): - response = client.put("/items/5", json=None) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "item"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "user"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "importance"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -def test_post_body_empty_list(client: TestClient): - response = client.put("/items/5", json=[]) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "item"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "user"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "importance"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/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" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_update_item_items__item_id__put" - } - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), - }, - }, - "User": { - "title": "User", - "required": ["username"], - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "full_name": IsDict( - { - "title": "Full Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Full Name", "type": "string"} - ), - }, - }, - "Body_update_item_items__item_id__put": { - "title": "Body_update_item_items__item_id__put", - "required": ["item", "user", "importance"], - "type": "object", - "properties": { - "item": {"$ref": "#/components/schemas/Item"}, - "user": {"$ref": "#/components/schemas/User"}, - "importance": {"title": "Importance", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py deleted file mode 100644 index f46430fb5..000000000 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py +++ /dev/null @@ -1,279 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.body_multiple_params.tutorial003_an_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_post_body_valid(client: TestClient): - response = client.put( - "/items/5", - json={ - "importance": 2, - "item": {"name": "Foo", "price": 50.5}, - "user": {"username": "Dave"}, - }, - ) - assert response.status_code == 200 - assert response.json() == { - "item_id": 5, - "importance": 2, - "item": { - "name": "Foo", - "price": 50.5, - "description": None, - "tax": None, - }, - "user": {"username": "Dave", "full_name": None}, - } - - -@needs_py310 -def test_post_body_no_data(client: TestClient): - response = client.put("/items/5", json=None) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "item"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "user"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "importance"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -@needs_py310 -def test_post_body_empty_list(client: TestClient): - response = client.put("/items/5", json=[]) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "item"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "user"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "importance"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/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" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_update_item_items__item_id__put" - } - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), - }, - }, - "User": { - "title": "User", - "required": ["username"], - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "full_name": IsDict( - { - "title": "Full Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Full Name", "type": "string"} - ), - }, - }, - "Body_update_item_items__item_id__put": { - "title": "Body_update_item_items__item_id__put", - "required": ["item", "user", "importance"], - "type": "object", - "properties": { - "item": {"$ref": "#/components/schemas/Item"}, - "user": {"$ref": "#/components/schemas/User"}, - "importance": {"title": "Importance", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py deleted file mode 100644 index 29071cddc..000000000 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py +++ /dev/null @@ -1,279 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.body_multiple_params.tutorial003_an_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_post_body_valid(client: TestClient): - response = client.put( - "/items/5", - json={ - "importance": 2, - "item": {"name": "Foo", "price": 50.5}, - "user": {"username": "Dave"}, - }, - ) - assert response.status_code == 200 - assert response.json() == { - "item_id": 5, - "importance": 2, - "item": { - "name": "Foo", - "price": 50.5, - "description": None, - "tax": None, - }, - "user": {"username": "Dave", "full_name": None}, - } - - -@needs_py39 -def test_post_body_no_data(client: TestClient): - response = client.put("/items/5", json=None) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "item"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "user"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "importance"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -@needs_py39 -def test_post_body_empty_list(client: TestClient): - response = client.put("/items/5", json=[]) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "item"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "user"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "importance"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/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" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_update_item_items__item_id__put" - } - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), - }, - }, - "User": { - "title": "User", - "required": ["username"], - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "full_name": IsDict( - { - "title": "Full Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Full Name", "type": "string"} - ), - }, - }, - "Body_update_item_items__item_id__put": { - "title": "Body_update_item_items__item_id__put", - "required": ["item", "user", "importance"], - "type": "object", - "properties": { - "item": {"$ref": "#/components/schemas/Item"}, - "user": {"$ref": "#/components/schemas/User"}, - "importance": {"title": "Importance", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py deleted file mode 100644 index 133afe9b5..000000000 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py +++ /dev/null @@ -1,279 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.body_multiple_params.tutorial003_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_post_body_valid(client: TestClient): - response = client.put( - "/items/5", - json={ - "importance": 2, - "item": {"name": "Foo", "price": 50.5}, - "user": {"username": "Dave"}, - }, - ) - assert response.status_code == 200 - assert response.json() == { - "item_id": 5, - "importance": 2, - "item": { - "name": "Foo", - "price": 50.5, - "description": None, - "tax": None, - }, - "user": {"username": "Dave", "full_name": None}, - } - - -@needs_py310 -def test_post_body_no_data(client: TestClient): - response = client.put("/items/5", json=None) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "item"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "user"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "importance"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -@needs_py310 -def test_post_body_empty_list(client: TestClient): - response = client.put("/items/5", json=[]) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "item"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "user"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "importance"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/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" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_update_item_items__item_id__put" - } - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), - }, - }, - "User": { - "title": "User", - "required": ["username"], - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "full_name": IsDict( - { - "title": "Full Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Full Name", "type": "string"} - ), - }, - }, - "Body_update_item_items__item_id__put": { - "title": "Body_update_item_items__item_id__put", - "required": ["item", "user", "importance"], - "type": "object", - "properties": { - "item": {"$ref": "#/components/schemas/Item"}, - "user": {"$ref": "#/components/schemas/User"}, - "importance": {"title": "Importance", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001.py b/tests/test_tutorial/test_request_files/test_tutorial001.py index f5817593b..b06919961 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001.py @@ -1,23 +1,28 @@ +import importlib + +import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient -from docs_src.request_files.tutorial001 import app +from ...utils import needs_py39 -client = TestClient(app) +@pytest.fixture( + name="client", + params=[ + "tutorial001", + "tutorial001_an", + pytest.param("tutorial001_an_py39", marks=needs_py39), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.request_files.{request.param}") -file_required = { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - } - ] -} + client = TestClient(mod.app) + return client -def test_post_form_no_body(): +def test_post_form_no_body(client: TestClient): response = client.post("/files/") assert response.status_code == 422, response.text assert response.json() == IsDict( @@ -45,7 +50,7 @@ def test_post_form_no_body(): ) -def test_post_body_json(): +def test_post_body_json(client: TestClient): response = client.post("/files/", json={"file": "Foo"}) assert response.status_code == 422, response.text assert response.json() == IsDict( @@ -73,41 +78,38 @@ def test_post_body_json(): ) -def test_post_file(tmp_path): +def test_post_file(tmp_path, client: TestClient): path = tmp_path / "test.txt" path.write_bytes(b"") - client = TestClient(app) with path.open("rb") as file: response = client.post("/files/", files={"file": file}) assert response.status_code == 200, response.text assert response.json() == {"file_size": 14} -def test_post_large_file(tmp_path): +def test_post_large_file(tmp_path, client: TestClient): default_pydantic_max_size = 2**16 path = tmp_path / "test.txt" path.write_bytes(b"x" * (default_pydantic_max_size + 1)) - client = TestClient(app) with path.open("rb") as file: response = client.post("/files/", files={"file": file}) assert response.status_code == 200, response.text assert response.json() == {"file_size": default_pydantic_max_size + 1} -def test_post_upload_file(tmp_path): +def test_post_upload_file(tmp_path, client: TestClient): path = tmp_path / "test.txt" path.write_bytes(b"") - client = TestClient(app) with path.open("rb") as file: response = client.post("/uploadfile/", files={"file": file}) assert response.status_code == 200, response.text assert response.json() == {"filename": "test.txt"} -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_request_files/test_tutorial001_02.py b/tests/test_tutorial/test_request_files/test_tutorial001_02.py index 42f75442a..9075a1756 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02.py @@ -1,46 +1,63 @@ +import importlib +from pathlib import Path + +import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient -from docs_src.request_files.tutorial001_02 import app +from ...utils import needs_py39, needs_py310 + + +@pytest.fixture( + name="client", + params=[ + "tutorial001_02", + pytest.param("tutorial001_02_py310", marks=needs_py310), + "tutorial001_02_an", + pytest.param("tutorial001_02_an_py39", marks=needs_py39), + pytest.param("tutorial001_02_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.request_files.{request.param}") -client = TestClient(app) + client = TestClient(mod.app) + return client -def test_post_form_no_body(): +def test_post_form_no_body(client: TestClient): response = client.post("/files/") assert response.status_code == 200, response.text assert response.json() == {"message": "No file sent"} -def test_post_uploadfile_no_body(): +def test_post_uploadfile_no_body(client: TestClient): response = client.post("/uploadfile/") assert response.status_code == 200, response.text assert response.json() == {"message": "No upload file sent"} -def test_post_file(tmp_path): +def test_post_file(tmp_path: Path, client: TestClient): path = tmp_path / "test.txt" path.write_bytes(b"") - client = TestClient(app) with path.open("rb") as file: response = client.post("/files/", files={"file": file}) assert response.status_code == 200, response.text assert response.json() == {"file_size": 14} -def test_post_upload_file(tmp_path): +def test_post_upload_file(tmp_path: Path, client: TestClient): path = tmp_path / "test.txt" path.write_bytes(b"") - client = TestClient(app) with path.open("rb") as file: response = client.post("/uploadfile/", files={"file": file}) assert response.status_code == 200, response.text assert response.json() == {"filename": "test.txt"} -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_request_files/test_tutorial001_02_an.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_an.py deleted file mode 100644 index f63eb339c..000000000 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02_an.py +++ /dev/null @@ -1,208 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from docs_src.request_files.tutorial001_02_an import app - -client = TestClient(app) - - -def test_post_form_no_body(): - response = client.post("/files/") - assert response.status_code == 200, response.text - assert response.json() == {"message": "No file sent"} - - -def test_post_uploadfile_no_body(): - response = client.post("/uploadfile/") - assert response.status_code == 200, response.text - assert response.json() == {"message": "No upload file sent"} - - -def test_post_file(tmp_path): - path = tmp_path / "test.txt" - path.write_bytes(b"") - - client = TestClient(app) - with path.open("rb") as file: - response = client.post("/files/", files={"file": file}) - assert response.status_code == 200, response.text - assert response.json() == {"file_size": 14} - - -def test_post_upload_file(tmp_path): - path = tmp_path / "test.txt" - path.write_bytes(b"") - - client = TestClient(app) - with path.open("rb") as file: - response = client.post("/uploadfile/", files={"file": file}) - assert response.status_code == 200, response.text - assert response.json() == {"filename": "test.txt"} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/": { - "post": { - "summary": "Create File", - "operationId": "create_file_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": IsDict( - { - "allOf": [ - { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - ], - "title": "Body", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - ) - } - } - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/uploadfile/": { - "post": { - "summary": "Create Upload File", - "operationId": "create_upload_file_uploadfile__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": IsDict( - { - "allOf": [ - { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - ], - "title": "Body", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - ) - } - } - }, - "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_file_files__post": { - "title": "Body_create_file_files__post", - "type": "object", - "properties": { - "file": IsDict( - { - "title": "File", - "anyOf": [ - {"type": "string", "format": "binary"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "File", "type": "string", "format": "binary"} - ) - }, - }, - "Body_create_upload_file_uploadfile__post": { - "title": "Body_create_upload_file_uploadfile__post", - "type": "object", - "properties": { - "file": IsDict( - { - "title": "File", - "anyOf": [ - {"type": "string", "format": "binary"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "File", "type": "string", "format": "binary"} - ) - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py deleted file mode 100644 index 94b6ac67e..000000000 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py +++ /dev/null @@ -1,220 +0,0 @@ -from pathlib import Path - -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.request_files.tutorial001_02_an_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_post_form_no_body(client: TestClient): - response = client.post("/files/") - assert response.status_code == 200, response.text - assert response.json() == {"message": "No file sent"} - - -@needs_py310 -def test_post_uploadfile_no_body(client: TestClient): - response = client.post("/uploadfile/") - assert response.status_code == 200, response.text - assert response.json() == {"message": "No upload file sent"} - - -@needs_py310 -def test_post_file(tmp_path: Path, client: TestClient): - path = tmp_path / "test.txt" - path.write_bytes(b"") - - with path.open("rb") as file: - response = client.post("/files/", files={"file": file}) - assert response.status_code == 200, response.text - assert response.json() == {"file_size": 14} - - -@needs_py310 -def test_post_upload_file(tmp_path: Path, client: TestClient): - path = tmp_path / "test.txt" - path.write_bytes(b"") - - with path.open("rb") as file: - response = client.post("/uploadfile/", files={"file": file}) - assert response.status_code == 200, response.text - assert response.json() == {"filename": "test.txt"} - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/": { - "post": { - "summary": "Create File", - "operationId": "create_file_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": IsDict( - { - "allOf": [ - { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - ], - "title": "Body", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - ) - } - } - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/uploadfile/": { - "post": { - "summary": "Create Upload File", - "operationId": "create_upload_file_uploadfile__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": IsDict( - { - "allOf": [ - { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - ], - "title": "Body", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - ) - } - } - }, - "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_file_files__post": { - "title": "Body_create_file_files__post", - "type": "object", - "properties": { - "file": IsDict( - { - "title": "File", - "anyOf": [ - {"type": "string", "format": "binary"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "File", "type": "string", "format": "binary"} - ) - }, - }, - "Body_create_upload_file_uploadfile__post": { - "title": "Body_create_upload_file_uploadfile__post", - "type": "object", - "properties": { - "file": IsDict( - { - "title": "File", - "anyOf": [ - {"type": "string", "format": "binary"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "File", "type": "string", "format": "binary"} - ) - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py deleted file mode 100644 index fcb39f8f1..000000000 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py +++ /dev/null @@ -1,220 +0,0 @@ -from pathlib import Path - -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.request_files.tutorial001_02_an_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_post_form_no_body(client: TestClient): - response = client.post("/files/") - assert response.status_code == 200, response.text - assert response.json() == {"message": "No file sent"} - - -@needs_py39 -def test_post_uploadfile_no_body(client: TestClient): - response = client.post("/uploadfile/") - assert response.status_code == 200, response.text - assert response.json() == {"message": "No upload file sent"} - - -@needs_py39 -def test_post_file(tmp_path: Path, client: TestClient): - path = tmp_path / "test.txt" - path.write_bytes(b"") - - with path.open("rb") as file: - response = client.post("/files/", files={"file": file}) - assert response.status_code == 200, response.text - assert response.json() == {"file_size": 14} - - -@needs_py39 -def test_post_upload_file(tmp_path: Path, client: TestClient): - path = tmp_path / "test.txt" - path.write_bytes(b"") - - with path.open("rb") as file: - response = client.post("/uploadfile/", files={"file": file}) - assert response.status_code == 200, response.text - assert response.json() == {"filename": "test.txt"} - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/": { - "post": { - "summary": "Create File", - "operationId": "create_file_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": IsDict( - { - "allOf": [ - { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - ], - "title": "Body", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - ) - } - } - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/uploadfile/": { - "post": { - "summary": "Create Upload File", - "operationId": "create_upload_file_uploadfile__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": IsDict( - { - "allOf": [ - { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - ], - "title": "Body", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - ) - } - } - }, - "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_file_files__post": { - "title": "Body_create_file_files__post", - "type": "object", - "properties": { - "file": IsDict( - { - "title": "File", - "anyOf": [ - {"type": "string", "format": "binary"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "File", "type": "string", "format": "binary"} - ) - }, - }, - "Body_create_upload_file_uploadfile__post": { - "title": "Body_create_upload_file_uploadfile__post", - "type": "object", - "properties": { - "file": IsDict( - { - "title": "File", - "anyOf": [ - {"type": "string", "format": "binary"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "File", "type": "string", "format": "binary"} - ) - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_py310.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_py310.py deleted file mode 100644 index a700752a3..000000000 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02_py310.py +++ /dev/null @@ -1,220 +0,0 @@ -from pathlib import Path - -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.request_files.tutorial001_02_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_post_form_no_body(client: TestClient): - response = client.post("/files/") - assert response.status_code == 200, response.text - assert response.json() == {"message": "No file sent"} - - -@needs_py310 -def test_post_uploadfile_no_body(client: TestClient): - response = client.post("/uploadfile/") - assert response.status_code == 200, response.text - assert response.json() == {"message": "No upload file sent"} - - -@needs_py310 -def test_post_file(tmp_path: Path, client: TestClient): - path = tmp_path / "test.txt" - path.write_bytes(b"") - - with path.open("rb") as file: - response = client.post("/files/", files={"file": file}) - assert response.status_code == 200, response.text - assert response.json() == {"file_size": 14} - - -@needs_py310 -def test_post_upload_file(tmp_path: Path, client: TestClient): - path = tmp_path / "test.txt" - path.write_bytes(b"") - - with path.open("rb") as file: - response = client.post("/uploadfile/", files={"file": file}) - assert response.status_code == 200, response.text - assert response.json() == {"filename": "test.txt"} - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/": { - "post": { - "summary": "Create File", - "operationId": "create_file_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": IsDict( - { - "allOf": [ - { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - ], - "title": "Body", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - ) - } - } - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/uploadfile/": { - "post": { - "summary": "Create Upload File", - "operationId": "create_upload_file_uploadfile__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": IsDict( - { - "allOf": [ - { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - ], - "title": "Body", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - ) - } - } - }, - "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_file_files__post": { - "title": "Body_create_file_files__post", - "type": "object", - "properties": { - "file": IsDict( - { - "title": "File", - "anyOf": [ - {"type": "string", "format": "binary"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "File", "type": "string", "format": "binary"} - ) - }, - }, - "Body_create_upload_file_uploadfile__post": { - "title": "Body_create_upload_file_uploadfile__post", - "type": "object", - "properties": { - "file": IsDict( - { - "title": "File", - "anyOf": [ - {"type": "string", "format": "binary"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "File", "type": "string", "format": "binary"} - ) - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_03.py b/tests/test_tutorial/test_request_files/test_tutorial001_03.py index f02170814..9fbe2166c 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_03.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_03.py @@ -1,33 +1,47 @@ +import importlib + +import pytest from fastapi.testclient import TestClient -from docs_src.request_files.tutorial001_03 import app +from ...utils import needs_py39 + + +@pytest.fixture( + name="client", + params=[ + "tutorial001_03", + "tutorial001_03_an", + pytest.param("tutorial001_03_an_py39", marks=needs_py39), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.request_files.{request.param}") -client = TestClient(app) + client = TestClient(mod.app) + return client -def test_post_file(tmp_path): +def test_post_file(tmp_path, client: TestClient): path = tmp_path / "test.txt" path.write_bytes(b"") - client = TestClient(app) with path.open("rb") as file: response = client.post("/files/", files={"file": file}) assert response.status_code == 200, response.text assert response.json() == {"file_size": 14} -def test_post_upload_file(tmp_path): +def test_post_upload_file(tmp_path, client: TestClient): path = tmp_path / "test.txt" path.write_bytes(b"") - client = TestClient(app) with path.open("rb") as file: response = client.post("/uploadfile/", files={"file": file}) assert response.status_code == 200, response.text assert response.json() == {"filename": "test.txt"} -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_request_files/test_tutorial001_03_an.py b/tests/test_tutorial/test_request_files/test_tutorial001_03_an.py deleted file mode 100644 index acfb749ce..000000000 --- a/tests/test_tutorial/test_request_files/test_tutorial001_03_an.py +++ /dev/null @@ -1,159 +0,0 @@ -from fastapi.testclient import TestClient - -from docs_src.request_files.tutorial001_03_an import app - -client = TestClient(app) - - -def test_post_file(tmp_path): - path = tmp_path / "test.txt" - path.write_bytes(b"") - - client = TestClient(app) - with path.open("rb") as file: - response = client.post("/files/", files={"file": file}) - assert response.status_code == 200, response.text - assert response.json() == {"file_size": 14} - - -def test_post_upload_file(tmp_path): - path = tmp_path / "test.txt" - path.write_bytes(b"") - - client = TestClient(app) - with path.open("rb") as file: - response = client.post("/uploadfile/", files={"file": file}) - assert response.status_code == 200, response.text - assert response.json() == {"filename": "test.txt"} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/": { - "post": { - "summary": "Create File", - "operationId": "create_file_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/uploadfile/": { - "post": { - "summary": "Create Upload File", - "operationId": "create_upload_file_uploadfile__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "Body_create_file_files__post": { - "title": "Body_create_file_files__post", - "required": ["file"], - "type": "object", - "properties": { - "file": { - "title": "File", - "type": "string", - "description": "A file read as bytes", - "format": "binary", - } - }, - }, - "Body_create_upload_file_uploadfile__post": { - "title": "Body_create_upload_file_uploadfile__post", - "required": ["file"], - "type": "object", - "properties": { - "file": { - "title": "File", - "type": "string", - "description": "A file read as UploadFile", - "format": "binary", - } - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_03_an_py39.py b/tests/test_tutorial/test_request_files/test_tutorial001_03_an_py39.py deleted file mode 100644 index 36e5faac1..000000000 --- a/tests/test_tutorial/test_request_files/test_tutorial001_03_an_py39.py +++ /dev/null @@ -1,167 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.request_files.tutorial001_03_an_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_post_file(tmp_path, client: TestClient): - path = tmp_path / "test.txt" - path.write_bytes(b"") - - with path.open("rb") as file: - response = client.post("/files/", files={"file": file}) - assert response.status_code == 200, response.text - assert response.json() == {"file_size": 14} - - -@needs_py39 -def test_post_upload_file(tmp_path, client: TestClient): - path = tmp_path / "test.txt" - path.write_bytes(b"") - - with path.open("rb") as file: - response = client.post("/uploadfile/", files={"file": file}) - assert response.status_code == 200, response.text - assert response.json() == {"filename": "test.txt"} - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/": { - "post": { - "summary": "Create File", - "operationId": "create_file_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/uploadfile/": { - "post": { - "summary": "Create Upload File", - "operationId": "create_upload_file_uploadfile__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "Body_create_file_files__post": { - "title": "Body_create_file_files__post", - "required": ["file"], - "type": "object", - "properties": { - "file": { - "title": "File", - "type": "string", - "description": "A file read as bytes", - "format": "binary", - } - }, - }, - "Body_create_upload_file_uploadfile__post": { - "title": "Body_create_upload_file_uploadfile__post", - "required": ["file"], - "type": "object", - "properties": { - "file": { - "title": "File", - "type": "string", - "description": "A file read as UploadFile", - "format": "binary", - } - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_an.py b/tests/test_tutorial/test_request_files/test_tutorial001_an.py deleted file mode 100644 index 1c78e3679..000000000 --- a/tests/test_tutorial/test_request_files/test_tutorial001_an.py +++ /dev/null @@ -1,218 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from docs_src.request_files.tutorial001_an import app - -client = TestClient(app) - - -def test_post_form_no_body(): - response = client.post("/files/") - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "file"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -def test_post_body_json(): - response = client.post("/files/", json={"file": "Foo"}) - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "file"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -def test_post_file(tmp_path): - path = tmp_path / "test.txt" - path.write_bytes(b"") - - client = TestClient(app) - with path.open("rb") as file: - response = client.post("/files/", files={"file": file}) - assert response.status_code == 200, response.text - assert response.json() == {"file_size": 14} - - -def test_post_large_file(tmp_path): - default_pydantic_max_size = 2**16 - path = tmp_path / "test.txt" - path.write_bytes(b"x" * (default_pydantic_max_size + 1)) - - client = TestClient(app) - with path.open("rb") as file: - response = client.post("/files/", files={"file": file}) - assert response.status_code == 200, response.text - assert response.json() == {"file_size": default_pydantic_max_size + 1} - - -def test_post_upload_file(tmp_path): - path = tmp_path / "test.txt" - path.write_bytes(b"") - - client = TestClient(app) - with path.open("rb") as file: - response = client.post("/uploadfile/", files={"file": file}) - assert response.status_code == 200, response.text - assert response.json() == {"filename": "test.txt"} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create File", - "operationId": "create_file_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - } - }, - "required": True, - }, - } - }, - "/uploadfile/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create Upload File", - "operationId": "create_upload_file_uploadfile__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - } - }, - "required": True, - }, - } - }, - }, - "components": { - "schemas": { - "Body_create_upload_file_uploadfile__post": { - "title": "Body_create_upload_file_uploadfile__post", - "required": ["file"], - "type": "object", - "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} - }, - }, - "Body_create_file_files__post": { - "title": "Body_create_file_files__post", - "required": ["file"], - "type": "object", - "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py b/tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py deleted file mode 100644 index 843fcec28..000000000 --- a/tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py +++ /dev/null @@ -1,228 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.request_files.tutorial001_an_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_post_form_no_body(client: TestClient): - response = client.post("/files/") - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "file"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -@needs_py39 -def test_post_body_json(client: TestClient): - response = client.post("/files/", json={"file": "Foo"}) - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "file"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -@needs_py39 -def test_post_file(tmp_path, client: TestClient): - path = tmp_path / "test.txt" - path.write_bytes(b"") - - with path.open("rb") as file: - response = client.post("/files/", files={"file": file}) - assert response.status_code == 200, response.text - assert response.json() == {"file_size": 14} - - -@needs_py39 -def test_post_large_file(tmp_path, client: TestClient): - default_pydantic_max_size = 2**16 - path = tmp_path / "test.txt" - path.write_bytes(b"x" * (default_pydantic_max_size + 1)) - - with path.open("rb") as file: - response = client.post("/files/", files={"file": file}) - assert response.status_code == 200, response.text - assert response.json() == {"file_size": default_pydantic_max_size + 1} - - -@needs_py39 -def test_post_upload_file(tmp_path, client: TestClient): - path = tmp_path / "test.txt" - path.write_bytes(b"") - - with path.open("rb") as file: - response = client.post("/uploadfile/", files={"file": file}) - assert response.status_code == 200, response.text - assert response.json() == {"filename": "test.txt"} - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create File", - "operationId": "create_file_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - } - }, - "required": True, - }, - } - }, - "/uploadfile/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create Upload File", - "operationId": "create_upload_file_uploadfile__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - } - }, - "required": True, - }, - } - }, - }, - "components": { - "schemas": { - "Body_create_upload_file_uploadfile__post": { - "title": "Body_create_upload_file_uploadfile__post", - "required": ["file"], - "type": "object", - "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} - }, - }, - "Body_create_file_files__post": { - "title": "Body_create_file_files__post", - "required": ["file"], - "type": "object", - "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_request_files/test_tutorial002.py b/tests/test_tutorial/test_request_files/test_tutorial002.py index db1552e5c..446a87657 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial002.py +++ b/tests/test_tutorial/test_request_files/test_tutorial002.py @@ -1,12 +1,35 @@ +import importlib + +import pytest from dirty_equals import IsDict +from fastapi import FastAPI from fastapi.testclient import TestClient -from docs_src.request_files.tutorial002 import app +from ...utils import needs_py39 + + +@pytest.fixture( + name="app", + params=[ + "tutorial002", + "tutorial002_an", + pytest.param("tutorial002_py39", marks=needs_py39), + pytest.param("tutorial002_an_py39", marks=needs_py39), + ], +) +def get_app(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.request_files.{request.param}") -client = TestClient(app) + return mod.app + + +@pytest.fixture(name="client") +def get_client(app: FastAPI): + client = TestClient(app) + return client -def test_post_form_no_body(): +def test_post_form_no_body(client: TestClient): response = client.post("/files/") assert response.status_code == 422, response.text assert response.json() == IsDict( @@ -34,7 +57,7 @@ def test_post_form_no_body(): ) -def test_post_body_json(): +def test_post_body_json(client: TestClient): response = client.post("/files/", json={"file": "Foo"}) assert response.status_code == 422, response.text assert response.json() == IsDict( @@ -62,7 +85,7 @@ def test_post_body_json(): ) -def test_post_files(tmp_path): +def test_post_files(tmp_path, app: FastAPI): path = tmp_path / "test.txt" path.write_bytes(b"") path2 = tmp_path / "test2.txt" @@ -81,7 +104,7 @@ def test_post_files(tmp_path): assert response.json() == {"file_sizes": [14, 15]} -def test_post_upload_file(tmp_path): +def test_post_upload_file(tmp_path, app: FastAPI): path = tmp_path / "test.txt" path.write_bytes(b"") path2 = tmp_path / "test2.txt" @@ -100,14 +123,14 @@ def test_post_upload_file(tmp_path): assert response.json() == {"filenames": ["test.txt", "test2.txt"]} -def test_get_root(): +def test_get_root(app: FastAPI): client = TestClient(app) response = client.get("/") assert response.status_code == 200, response.text assert b"") - path2 = tmp_path / "test2.txt" - path2.write_bytes(b"") - - client = TestClient(app) - with path.open("rb") as file, path2.open("rb") as file2: - response = client.post( - "/files/", - files=( - ("files", ("test.txt", file)), - ("files", ("test2.txt", file2)), - ), - ) - assert response.status_code == 200, response.text - assert response.json() == {"file_sizes": [14, 15]} - - -def test_post_upload_file(tmp_path): - path = tmp_path / "test.txt" - path.write_bytes(b"") - path2 = tmp_path / "test2.txt" - path2.write_bytes(b"") - - client = TestClient(app) - with path.open("rb") as file, path2.open("rb") as file2: - response = client.post( - "/uploadfiles/", - files=( - ("files", ("test.txt", file)), - ("files", ("test2.txt", file2)), - ), - ) - assert response.status_code == 200, response.text - assert response.json() == {"filenames": ["test.txt", "test2.txt"]} - - -def test_get_root(): - client = TestClient(app) - response = client.get("/") - assert response.status_code == 200, response.text - assert b"") - path2 = tmp_path / "test2.txt" - path2.write_bytes(b"") - - client = TestClient(app) - with path.open("rb") as file, path2.open("rb") as file2: - response = client.post( - "/files/", - files=( - ("files", ("test.txt", file)), - ("files", ("test2.txt", file2)), - ), - ) - assert response.status_code == 200, response.text - assert response.json() == {"file_sizes": [14, 15]} - - -@needs_py39 -def test_post_upload_file(tmp_path, app: FastAPI): - path = tmp_path / "test.txt" - path.write_bytes(b"") - path2 = tmp_path / "test2.txt" - path2.write_bytes(b"") - - client = TestClient(app) - with path.open("rb") as file, path2.open("rb") as file2: - response = client.post( - "/uploadfiles/", - files=( - ("files", ("test.txt", file)), - ("files", ("test2.txt", file2)), - ), - ) - assert response.status_code == 200, response.text - assert response.json() == {"filenames": ["test.txt", "test2.txt"]} - - -@needs_py39 -def test_get_root(app: FastAPI): - client = TestClient(app) - response = client.get("/") - assert response.status_code == 200, response.text - assert b"") - path2 = tmp_path / "test2.txt" - path2.write_bytes(b"") - - client = TestClient(app) - with path.open("rb") as file, path2.open("rb") as file2: - response = client.post( - "/files/", - files=( - ("files", ("test.txt", file)), - ("files", ("test2.txt", file2)), - ), - ) - assert response.status_code == 200, response.text - assert response.json() == {"file_sizes": [14, 15]} - - -@needs_py39 -def test_post_upload_file(tmp_path, app: FastAPI): - path = tmp_path / "test.txt" - path.write_bytes(b"") - path2 = tmp_path / "test2.txt" - path2.write_bytes(b"") - - client = TestClient(app) - with path.open("rb") as file, path2.open("rb") as file2: - response = client.post( - "/uploadfiles/", - files=( - ("files", ("test.txt", file)), - ("files", ("test2.txt", file2)), - ), - ) - assert response.status_code == 200, response.text - assert response.json() == {"filenames": ["test.txt", "test2.txt"]} - - -@needs_py39 -def test_get_root(app: FastAPI): - client = TestClient(app) - response = client.get("/") - assert response.status_code == 200, response.text - assert b"") path2 = tmp_path / "test2.txt" @@ -24,7 +47,7 @@ def test_post_files(tmp_path): assert response.json() == {"file_sizes": [14, 15]} -def test_post_upload_file(tmp_path): +def test_post_upload_file(tmp_path, app: FastAPI): path = tmp_path / "test.txt" path.write_bytes(b"") path2 = tmp_path / "test2.txt" @@ -43,14 +66,14 @@ def test_post_upload_file(tmp_path): assert response.json() == {"filenames": ["test.txt", "test2.txt"]} -def test_get_root(): +def test_get_root(app: FastAPI): client = TestClient(app) response = client.get("/") assert response.status_code == 200, response.text assert b"") - path2 = tmp_path / "test2.txt" - path2.write_bytes(b"") - - client = TestClient(app) - with path.open("rb") as file, path2.open("rb") as file2: - response = client.post( - "/files/", - files=( - ("files", ("test.txt", file)), - ("files", ("test2.txt", file2)), - ), - ) - assert response.status_code == 200, response.text - assert response.json() == {"file_sizes": [14, 15]} - - -def test_post_upload_file(tmp_path): - path = tmp_path / "test.txt" - path.write_bytes(b"") - path2 = tmp_path / "test2.txt" - path2.write_bytes(b"") - - client = TestClient(app) - with path.open("rb") as file, path2.open("rb") as file2: - response = client.post( - "/uploadfiles/", - files=( - ("files", ("test.txt", file)), - ("files", ("test2.txt", file2)), - ), - ) - assert response.status_code == 200, response.text - assert response.json() == {"filenames": ["test.txt", "test2.txt"]} - - -def test_get_root(): - client = TestClient(app) - response = client.get("/") - assert response.status_code == 200, response.text - assert b"") - path2 = tmp_path / "test2.txt" - path2.write_bytes(b"") - - client = TestClient(app) - with path.open("rb") as file, path2.open("rb") as file2: - response = client.post( - "/files/", - files=( - ("files", ("test.txt", file)), - ("files", ("test2.txt", file2)), - ), - ) - assert response.status_code == 200, response.text - assert response.json() == {"file_sizes": [14, 15]} - - -@needs_py39 -def test_post_upload_file(tmp_path, app: FastAPI): - path = tmp_path / "test.txt" - path.write_bytes(b"") - path2 = tmp_path / "test2.txt" - path2.write_bytes(b"") - - client = TestClient(app) - with path.open("rb") as file, path2.open("rb") as file2: - response = client.post( - "/uploadfiles/", - files=( - ("files", ("test.txt", file)), - ("files", ("test2.txt", file2)), - ), - ) - assert response.status_code == 200, response.text - assert response.json() == {"filenames": ["test.txt", "test2.txt"]} - - -@needs_py39 -def test_get_root(app: FastAPI): - client = TestClient(app) - response = client.get("/") - assert response.status_code == 200, response.text - assert b"") - path2 = tmp_path / "test2.txt" - path2.write_bytes(b"") - - client = TestClient(app) - with path.open("rb") as file, path2.open("rb") as file2: - response = client.post( - "/files/", - files=( - ("files", ("test.txt", file)), - ("files", ("test2.txt", file2)), - ), - ) - assert response.status_code == 200, response.text - assert response.json() == {"file_sizes": [14, 15]} - - -@needs_py39 -def test_post_upload_file(tmp_path, app: FastAPI): - path = tmp_path / "test.txt" - path.write_bytes(b"") - path2 = tmp_path / "test2.txt" - path2.write_bytes(b"") - - client = TestClient(app) - with path.open("rb") as file, path2.open("rb") as file2: - response = client.post( - "/uploadfiles/", - files=( - ("files", ("test.txt", file)), - ("files", ("test2.txt", file2)), - ), - ) - assert response.status_code == 200, response.text - assert response.json() == {"filenames": ["test.txt", "test2.txt"]} - - -@needs_py39 -def test_get_root(app: FastAPI): - client = TestClient(app) - response = client.get("/") - assert response.status_code == 200, response.text - assert b" Date: Thu, 30 Jan 2025 12:04:59 +0000 Subject: [PATCH 038/117] =?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 e21d2521f..2f9da1465 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Refactors + +* ✅ Simplify tests for request_files. PR [#13182](https://github.com/fastapi/fastapi/pull/13182) by [@alejsdev](https://github.com/alejsdev). + ### Docs * 👥 Update FastAPI People - Experts. PR [#13269](https://github.com/fastapi/fastapi/pull/13269) by [@tiangolo](https://github.com/tiangolo). From d5ecbaceae3a4e19191bf3771a1dbfd33aa1e8f5 Mon Sep 17 00:00:00 2001 From: Rahul Pai <50425728+skarfie123@users.noreply.github.com> Date: Thu, 30 Jan 2025 12:17:09 +0000 Subject: [PATCH 039/117] =?UTF-8?q?=F0=9F=90=9B=20Fix=20`OAuth2PasswordReq?= =?UTF-8?q?uestForm`=20and=20`OAuth2PasswordRequestFormStrict`=20fixed=20`?= =?UTF-8?q?grant=5Ftype`=20"password"=20RegEx=20(#9783)?= 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: Alejandra <90076947+alejsdev@users.noreply.github.com> Co-authored-by: Sofie Van Landeghem Co-authored-by: svlandeg Co-authored-by: Sebastián Ramírez --- fastapi/security/oauth2.py | 4 +-- tests/test_security_oauth2.py | 25 +++++++++++++------ tests/test_security_oauth2_optional.py | 25 +++++++++++++------ ...st_security_oauth2_optional_description.py | 25 +++++++++++++------ .../test_security/test_tutorial003.py | 4 +-- .../test_security/test_tutorial005.py | 4 +-- 6 files changed, 57 insertions(+), 30 deletions(-) diff --git a/fastapi/security/oauth2.py b/fastapi/security/oauth2.py index 6adc55bfe..5ffad5986 100644 --- a/fastapi/security/oauth2.py +++ b/fastapi/security/oauth2.py @@ -63,7 +63,7 @@ class OAuth2PasswordRequestForm: *, grant_type: Annotated[ Union[str, None], - Form(pattern="password"), + Form(pattern="^password$"), Doc( """ The OAuth2 spec says it is required and MUST be the fixed string @@ -217,7 +217,7 @@ class OAuth2PasswordRequestFormStrict(OAuth2PasswordRequestForm): self, grant_type: Annotated[ str, - Form(pattern="password"), + Form(pattern="^password$"), Doc( """ The OAuth2 spec says it is required and MUST be the fixed string diff --git a/tests/test_security_oauth2.py b/tests/test_security_oauth2.py index 7d914d034..2b7e3457a 100644 --- a/tests/test_security_oauth2.py +++ b/tests/test_security_oauth2.py @@ -1,3 +1,4 @@ +import pytest from dirty_equals import IsDict from fastapi import Depends, FastAPI, Security from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict @@ -137,10 +138,18 @@ def test_strict_login_no_grant_type(): ) -def test_strict_login_incorrect_grant_type(): +@pytest.mark.parametrize( + argnames=["grant_type"], + argvalues=[ + pytest.param("incorrect", id="incorrect value"), + pytest.param("passwordblah", id="password with suffix"), + pytest.param("blahpassword", id="password with prefix"), + ], +) +def test_strict_login_incorrect_grant_type(grant_type: str): response = client.post( "/login", - data={"username": "johndoe", "password": "secret", "grant_type": "incorrect"}, + data={"username": "johndoe", "password": "secret", "grant_type": grant_type}, ) assert response.status_code == 422 assert response.json() == IsDict( @@ -149,9 +158,9 @@ def test_strict_login_incorrect_grant_type(): { "type": "string_pattern_mismatch", "loc": ["body", "grant_type"], - "msg": "String should match pattern 'password'", - "input": "incorrect", - "ctx": {"pattern": "password"}, + "msg": "String should match pattern '^password$'", + "input": grant_type, + "ctx": {"pattern": "^password$"}, } ] } @@ -161,9 +170,9 @@ def test_strict_login_incorrect_grant_type(): "detail": [ { "loc": ["body", "grant_type"], - "msg": 'string does not match regex "password"', + "msg": 'string does not match regex "^password$"', "type": "value_error.str.regex", - "ctx": {"pattern": "password"}, + "ctx": {"pattern": "^password$"}, } ] } @@ -248,7 +257,7 @@ def test_openapi_schema(): "properties": { "grant_type": { "title": "Grant Type", - "pattern": "password", + "pattern": "^password$", "type": "string", }, "username": {"title": "Username", "type": "string"}, diff --git a/tests/test_security_oauth2_optional.py b/tests/test_security_oauth2_optional.py index 0da3b911e..046ac5763 100644 --- a/tests/test_security_oauth2_optional.py +++ b/tests/test_security_oauth2_optional.py @@ -1,5 +1,6 @@ from typing import Optional +import pytest from dirty_equals import IsDict from fastapi import Depends, FastAPI, Security from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict @@ -141,10 +142,18 @@ def test_strict_login_no_grant_type(): ) -def test_strict_login_incorrect_grant_type(): +@pytest.mark.parametrize( + argnames=["grant_type"], + argvalues=[ + pytest.param("incorrect", id="incorrect value"), + pytest.param("passwordblah", id="password with suffix"), + pytest.param("blahpassword", id="password with prefix"), + ], +) +def test_strict_login_incorrect_grant_type(grant_type: str): response = client.post( "/login", - data={"username": "johndoe", "password": "secret", "grant_type": "incorrect"}, + data={"username": "johndoe", "password": "secret", "grant_type": grant_type}, ) assert response.status_code == 422 assert response.json() == IsDict( @@ -153,9 +162,9 @@ def test_strict_login_incorrect_grant_type(): { "type": "string_pattern_mismatch", "loc": ["body", "grant_type"], - "msg": "String should match pattern 'password'", - "input": "incorrect", - "ctx": {"pattern": "password"}, + "msg": "String should match pattern '^password$'", + "input": grant_type, + "ctx": {"pattern": "^password$"}, } ] } @@ -165,9 +174,9 @@ def test_strict_login_incorrect_grant_type(): "detail": [ { "loc": ["body", "grant_type"], - "msg": 'string does not match regex "password"', + "msg": 'string does not match regex "^password$"', "type": "value_error.str.regex", - "ctx": {"pattern": "password"}, + "ctx": {"pattern": "^password$"}, } ] } @@ -252,7 +261,7 @@ def test_openapi_schema(): "properties": { "grant_type": { "title": "Grant Type", - "pattern": "password", + "pattern": "^password$", "type": "string", }, "username": {"title": "Username", "type": "string"}, diff --git a/tests/test_security_oauth2_optional_description.py b/tests/test_security_oauth2_optional_description.py index 85a9f9b39..629cddca2 100644 --- a/tests/test_security_oauth2_optional_description.py +++ b/tests/test_security_oauth2_optional_description.py @@ -1,5 +1,6 @@ from typing import Optional +import pytest from dirty_equals import IsDict from fastapi import Depends, FastAPI, Security from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict @@ -142,10 +143,18 @@ def test_strict_login_no_grant_type(): ) -def test_strict_login_incorrect_grant_type(): +@pytest.mark.parametrize( + argnames=["grant_type"], + argvalues=[ + pytest.param("incorrect", id="incorrect value"), + pytest.param("passwordblah", id="password with suffix"), + pytest.param("blahpassword", id="password with prefix"), + ], +) +def test_strict_login_incorrect_grant_type(grant_type: str): response = client.post( "/login", - data={"username": "johndoe", "password": "secret", "grant_type": "incorrect"}, + data={"username": "johndoe", "password": "secret", "grant_type": grant_type}, ) assert response.status_code == 422 assert response.json() == IsDict( @@ -154,9 +163,9 @@ def test_strict_login_incorrect_grant_type(): { "type": "string_pattern_mismatch", "loc": ["body", "grant_type"], - "msg": "String should match pattern 'password'", - "input": "incorrect", - "ctx": {"pattern": "password"}, + "msg": "String should match pattern '^password$'", + "input": grant_type, + "ctx": {"pattern": "^password$"}, } ] } @@ -166,9 +175,9 @@ def test_strict_login_incorrect_grant_type(): "detail": [ { "loc": ["body", "grant_type"], - "msg": 'string does not match regex "password"', + "msg": 'string does not match regex "^password$"', "type": "value_error.str.regex", - "ctx": {"pattern": "password"}, + "ctx": {"pattern": "^password$"}, } ] } @@ -253,7 +262,7 @@ def test_openapi_schema(): "properties": { "grant_type": { "title": "Grant Type", - "pattern": "password", + "pattern": "^password$", "type": "string", }, "username": {"title": "Username", "type": "string"}, diff --git a/tests/test_tutorial/test_security/test_tutorial003.py b/tests/test_tutorial/test_security/test_tutorial003.py index 7a4c99401..37fc2618f 100644 --- a/tests/test_tutorial/test_security/test_tutorial003.py +++ b/tests/test_tutorial/test_security/test_tutorial003.py @@ -149,7 +149,7 @@ def test_openapi_schema(client: TestClient): { "title": "Grant Type", "anyOf": [ - {"pattern": "password", "type": "string"}, + {"pattern": "^password$", "type": "string"}, {"type": "null"}, ], } @@ -158,7 +158,7 @@ def test_openapi_schema(client: TestClient): # TODO: remove when deprecating Pydantic v1 { "title": "Grant Type", - "pattern": "password", + "pattern": "^password$", "type": "string", } ), diff --git a/tests/test_tutorial/test_security/test_tutorial005.py b/tests/test_tutorial/test_security/test_tutorial005.py index c7f791b03..88c3d7815 100644 --- a/tests/test_tutorial/test_security/test_tutorial005.py +++ b/tests/test_tutorial/test_security/test_tutorial005.py @@ -363,7 +363,7 @@ def test_openapi_schema(mod: ModuleType): { "title": "Grant Type", "anyOf": [ - {"pattern": "password", "type": "string"}, + {"pattern": "^password$", "type": "string"}, {"type": "null"}, ], } @@ -372,7 +372,7 @@ def test_openapi_schema(mod: ModuleType): # TODO: remove when deprecating Pydantic v1 { "title": "Grant Type", - "pattern": "password", + "pattern": "^password$", "type": "string", } ), From 30b270be9ac9cf931b0efaac549ba0ad8112f547 Mon Sep 17 00:00:00 2001 From: Shahriyar Rzayev Date: Thu, 30 Jan 2025 13:17:20 +0100 Subject: [PATCH 040/117] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Move=20duplicated?= =?UTF-8?q?=20code=20portion=20to=20a=20static=20method=20in=20the=20`APIK?= =?UTF-8?q?eyBase`=20super=20class=20(#3142)?= 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 --- fastapi/security/api_key.py | 37 ++++++++++++------------------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/fastapi/security/api_key.py b/fastapi/security/api_key.py index d68bdb037..70c2dca8a 100644 --- a/fastapi/security/api_key.py +++ b/fastapi/security/api_key.py @@ -9,7 +9,15 @@ from typing_extensions import Annotated, Doc class APIKeyBase(SecurityBase): - pass + @staticmethod + def check_api_key(api_key: Optional[str], auto_error: bool) -> Optional[str]: + if not api_key: + if auto_error: + raise HTTPException( + status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" + ) + return None + return api_key class APIKeyQuery(APIKeyBase): @@ -101,14 +109,7 @@ class APIKeyQuery(APIKeyBase): async def __call__(self, request: Request) -> Optional[str]: api_key = request.query_params.get(self.model.name) - if not api_key: - if self.auto_error: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" - ) - else: - return None - return api_key + return self.check_api_key(api_key, self.auto_error) class APIKeyHeader(APIKeyBase): @@ -196,14 +197,7 @@ class APIKeyHeader(APIKeyBase): async def __call__(self, request: Request) -> Optional[str]: api_key = request.headers.get(self.model.name) - if not api_key: - if self.auto_error: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" - ) - else: - return None - return api_key + return self.check_api_key(api_key, self.auto_error) class APIKeyCookie(APIKeyBase): @@ -291,11 +285,4 @@ class APIKeyCookie(APIKeyBase): async def __call__(self, request: Request) -> Optional[str]: api_key = request.cookies.get(self.model.name) - if not api_key: - if self.auto_error: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" - ) - else: - return None - return api_key + return self.check_api_key(api_key, self.auto_error) From 041b2e1c4643c9837d2e7f8589351492cf76497a Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 30 Jan 2025 12:17:34 +0000 Subject: [PATCH 041/117] =?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 2f9da1465..a07b2023c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Fixes + +* 🐛 Fix `OAuth2PasswordRequestForm` and `OAuth2PasswordRequestFormStrict` fixed `grant_type` "password" RegEx. PR [#9783](https://github.com/fastapi/fastapi/pull/9783) by [@skarfie123](https://github.com/skarfie123). + ### Refactors * ✅ Simplify tests for request_files. PR [#13182](https://github.com/fastapi/fastapi/pull/13182) by [@alejsdev](https://github.com/alejsdev). From 0541693bc7611da858f71d896a3b9780751c04f8 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 30 Jan 2025 12:17:52 +0000 Subject: [PATCH 042/117] =?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 a07b2023c..b1c34800c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Refactors +* ♻️ Move duplicated code portion to a static method in the `APIKeyBase` super class. PR [#3142](https://github.com/fastapi/fastapi/pull/3142) by [@ShahriyarR](https://github.com/ShahriyarR). * ✅ Simplify tests for request_files. PR [#13182](https://github.com/fastapi/fastapi/pull/13182) by [@alejsdev](https://github.com/alejsdev). ### Docs From 9667ce87a908eecc2be2a215adcb55c7e1b38040 Mon Sep 17 00:00:00 2001 From: Ysabel <5388340+togogh@users.noreply.github.com> Date: Thu, 30 Jan 2025 20:19:10 +0800 Subject: [PATCH 043/117] =?UTF-8?q?=F0=9F=93=9D=20Update=20Request=20Body'?= =?UTF-8?q?s=20`tutorial002`=20to=20deal=20with=20`tax=3D0`=20case=20(#132?= =?UTF-8?q?30)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: svlandeg --- docs_src/body/tutorial002.py | 2 +- docs_src/body/tutorial002_py310.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs_src/body/tutorial002.py b/docs_src/body/tutorial002.py index 7f5183908..5cd86216b 100644 --- a/docs_src/body/tutorial002.py +++ b/docs_src/body/tutorial002.py @@ -17,7 +17,7 @@ app = FastAPI() @app.post("/items/") async def create_item(item: Item): item_dict = item.dict() - if item.tax: + if item.tax is not None: price_with_tax = item.price + item.tax item_dict.update({"price_with_tax": price_with_tax}) return item_dict diff --git a/docs_src/body/tutorial002_py310.py b/docs_src/body/tutorial002_py310.py index 8928b72b8..454c45c88 100644 --- a/docs_src/body/tutorial002_py310.py +++ b/docs_src/body/tutorial002_py310.py @@ -15,7 +15,7 @@ app = FastAPI() @app.post("/items/") async def create_item(item: Item): item_dict = item.dict() - if item.tax: + if item.tax is not None: price_with_tax = item.price + item.tax item_dict.update({"price_with_tax": price_with_tax}) return item_dict From d97647fd572169cf0434919464de5406057e32f4 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 30 Jan 2025 12:19:41 +0000 Subject: [PATCH 044/117] =?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 b1c34800c..76955df17 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -18,6 +18,7 @@ hide: ### Docs +* 📝 Update Request Body's `tutorial002` to deal with `tax=0` case. PR [#13230](https://github.com/fastapi/fastapi/pull/13230) by [@togogh](https://github.com/togogh). * 👥 Update FastAPI People - Experts. PR [#13269](https://github.com/fastapi/fastapi/pull/13269) by [@tiangolo](https://github.com/tiangolo). ### Translations From 3d02a920ab7c4b2d26bab67b10e35fc90a923ce1 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 30 Jan 2025 12:20:24 +0000 Subject: [PATCH 046/117] =?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 76955df17..e343337a7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Refactors +* ✅ Simplify tests for body_multiple_params . PR [#13237](https://github.com/fastapi/fastapi/pull/13237) by [@alejsdev](https://github.com/alejsdev). * ♻️ Move duplicated code portion to a static method in the `APIKeyBase` super class. PR [#3142](https://github.com/fastapi/fastapi/pull/3142) by [@ShahriyarR](https://github.com/ShahriyarR). * ✅ Simplify tests for request_files. PR [#13182](https://github.com/fastapi/fastapi/pull/13182) by [@alejsdev](https://github.com/alejsdev). From 83ab6ac95797395b5664626b66d1c3f1f5b0e8dc Mon Sep 17 00:00:00 2001 From: timothy <53824764+timothy-jeong@users.noreply.github.com> Date: Thu, 30 Jan 2025 21:21:44 +0900 Subject: [PATCH 047/117] =?UTF-8?q?=F0=9F=93=9D=20Change=20the=20word=20"u?= =?UTF-8?q?nwrap"=20to=20"unpack"=20in=20`docs/en/docs/tutorial/extra-mode?= =?UTF-8?q?ls.md`=20(#13061)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: timothy <53824764+jts8257@users.noreply.github.com> Co-authored-by: Sofie Van Landeghem --- docs/en/docs/tutorial/extra-models.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/en/docs/tutorial/extra-models.md b/docs/en/docs/tutorial/extra-models.md index 5fac3f69e..ed1590ece 100644 --- a/docs/en/docs/tutorial/extra-models.md +++ b/docs/en/docs/tutorial/extra-models.md @@ -70,9 +70,9 @@ we would get a Python `dict` with: } ``` -#### Unwrapping a `dict` +#### Unpacking a `dict` -If we take a `dict` like `user_dict` and pass it to a function (or class) with `**user_dict`, Python will "unwrap" it. It will pass the keys and values of the `user_dict` directly as key-value arguments. +If we take a `dict` like `user_dict` and pass it to a function (or class) with `**user_dict`, Python will "unpack" it. It will pass the keys and values of the `user_dict` directly as key-value arguments. So, continuing with the `user_dict` from above, writing: @@ -117,11 +117,11 @@ would be equivalent to: UserInDB(**user_in.dict()) ``` -...because `user_in.dict()` is a `dict`, and then we make Python "unwrap" it by passing it to `UserInDB` prefixed with `**`. +...because `user_in.dict()` is a `dict`, and then we make Python "unpack" it by passing it to `UserInDB` prefixed with `**`. So, we get a Pydantic model from the data in another Pydantic model. -#### Unwrapping a `dict` and extra keywords +#### Unpacking a `dict` and extra keywords And then adding the extra keyword argument `hashed_password=hashed_password`, like in: From 55f8a446c7c02ac6bb26e7adcdeb5ade2408a0ba Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 30 Jan 2025 12:23:00 +0000 Subject: [PATCH 048/117] =?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 e343337a7..e4e8a339a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -19,6 +19,7 @@ hide: ### Docs +* 📝 Change the word "unwrap" to "unpack" in `docs/en/docs/tutorial/extra-models.md`. PR [#13061](https://github.com/fastapi/fastapi/pull/13061) by [@timothy-jeong](https://github.com/timothy-jeong). * 📝 Update Request Body's `tutorial002` to deal with `tax=0` case. PR [#13230](https://github.com/fastapi/fastapi/pull/13230) by [@togogh](https://github.com/togogh). * 👥 Update FastAPI People - Experts. PR [#13269](https://github.com/fastapi/fastapi/pull/13269) by [@tiangolo](https://github.com/tiangolo). From 7128971f1d61e2e1e6f220a5f66baa925b635278 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 30 Jan 2025 13:58:14 +0000 Subject: [PATCH 049/117] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.11?= =?UTF-8?q?5.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 e4e8a339a..9dd9cc65c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.115.8 + ### Fixes * 🐛 Fix `OAuth2PasswordRequestForm` and `OAuth2PasswordRequestFormStrict` fixed `grant_type` "password" RegEx. PR [#9783](https://github.com/fastapi/fastapi/pull/9783) by [@skarfie123](https://github.com/skarfie123). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index c92279cfd..e3e0200ae 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.115.7" +__version__ = "0.115.8" from starlette import status as status From df8f281674a1aec51d98ab74b6d8073bcc298ab0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 31 Jan 2025 17:01:48 +0000 Subject: [PATCH 050/117] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors,=20add?= =?UTF-8?q?=20Permit=20(#13288)?= 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/permit.png | Bin 0 -> 39234 bytes 3 files changed, 4 insertions(+) create mode 100644 docs/en/docs/img/sponsors/permit.png diff --git a/README.md b/README.md index 6492ad745..f6da22b21 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ The key features are: + diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index 4231452e4..f9bf33ae9 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -48,6 +48,9 @@ silver: - url: https://www.stainlessapi.com/?utm_source=fastapi&utm_medium=referral title: Stainless | Generate best-in-class SDKs img: https://fastapi.tiangolo.com/img/sponsors/stainless.png + - url: https://www.permit.io/blog/implement-authorization-in-fastapi?utm_source=github&utm_medium=referral&utm_campaign=fastapi + title: Fine-Grained Authorization for FastAPI + img: https://fastapi.tiangolo.com/img/sponsors/permit.png bronze: - url: https://www.exoflare.com/open-source/?utm_source=FastAPI&utm_campaign=open_source title: Biosecurity risk assessments made easy. diff --git a/docs/en/docs/img/sponsors/permit.png b/docs/en/docs/img/sponsors/permit.png new file mode 100644 index 0000000000000000000000000000000000000000..4f07f22e27326c34108ca177f0a974182bb44914 GIT binary patch literal 39234 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%0A$XJ`3vlcLYN0j|tw?edFE(d{H`gM~vIza4ktrPP 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

=WMDDAr#Fp;TDW7fKOeyNR&^B(cOS7csl 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 Date: Fri, 31 Jan 2025 17:02:11 +0000 Subject: [PATCH 051/117] =?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 9dd9cc65c..12d6487aa 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Internal + +* 🔧 Update sponsors, add Permit. PR [#13288](https://github.com/fastapi/fastapi/pull/13288) by [@tiangolo](https://github.com/tiangolo). + ## 0.115.8 ### Fixes From 633ed1d8aff74a2575512d95c644ba9f2084b801 Mon Sep 17 00:00:00 2001 From: Rishat-F <66554797+Rishat-F@users.noreply.github.com> Date: Mon, 3 Feb 2025 16:33:39 +0300 Subject: [PATCH 052/117] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20translat?= =?UTF-8?q?ion=20for=20`docs/ru/docs/advanced/websockets.md`=20(#13279)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ru/docs/advanced/websockets.md | 186 ++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 docs/ru/docs/advanced/websockets.md diff --git a/docs/ru/docs/advanced/websockets.md b/docs/ru/docs/advanced/websockets.md new file mode 100644 index 000000000..bc9dfcbff --- /dev/null +++ b/docs/ru/docs/advanced/websockets.md @@ -0,0 +1,186 @@ +# Веб-сокеты + +Вы можете использовать веб-сокеты в **FastAPI**. + +## Установка `WebSockets` + +Убедитесь, что [виртуальная среда](../virtual-environments.md){.internal-link target=_blank} создана, активируйте её и установите `websockets`: + +

+ +```console +$ pip install websockets + +---> 100% +``` + +
+ +## Клиент WebSockets + +### Рабочее приложение + +Скорее всего, в вашей реальной продуктовой системе есть фронтенд, реализованный при помощи современных фреймворков React, Vue.js или Angular. + +И наверняка для взаимодействия с бекендом через веб-сокеты вы будете использовать средства фронтенда. + +Также у вас может быть нативное мобильное приложение, коммуницирующее непосредственно с веб-сокетами на бекенд-сервере. + +Либо вы можете сделать какой-либо другой способ взаимодействия с веб-сокетами. + +--- + +Но для этого примера мы воспользуемся очень простым HTML документом с небольшими вставками JavaScript кода. + +Конечно же это неоптимально, и на практике так делать не стоит. + +В реальных приложениях стоит воспользоваться одним из вышеупомянутых способов. + +Для примера нам нужен наиболее простой способ, который позволит сосредоточиться на серверной части веб-сокетов и получить рабочий код: + +{* ../../docs_src/websockets/tutorial001.py hl[2,6:38,41:43] *} + +## Создание `websocket` + +Создайте `websocket` в своем **FastAPI** приложении: + +{* ../../docs_src/websockets/tutorial001.py hl[1,46:47] *} + +/// note | Технические детали + +Вы также можете использовать `from starlette.websockets import WebSocket`. + +**FastAPI** напрямую предоставляет тот же самый `WebSocket` просто для удобства. На самом деле это `WebSocket` из Starlette. + +/// + +## Ожидание и отправка сообщений + +Через эндпоинт веб-сокета вы можете получать и отправлять сообщения. + +{* ../../docs_src/websockets/tutorial001.py hl[48:52] *} + +Вы можете получать и отправлять двоичные, текстовые и JSON данные. + +## Проверка в действии + +Если ваш файл называется `main.py`, то запустите приложение командой: + +
+ +```console +$ fastapi dev main.py + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +Откройте браузер по адресу http://127.0.0.1:8000. + +Вы увидите следующую простенькую страницу: + + + +Вы можете набирать сообщения в поле ввода и отправлять их: + + + +И ваше **FastAPI** приложение с веб-сокетами ответит: + + + +Вы можете отправлять и получать множество сообщений: + + + +И все они будут использовать одно и то же веб-сокет соединение. + +## Использование `Depends` и не только + +Вы можете импортировать из `fastapi` и использовать в эндпоинте вебсокета: + +* `Depends` +* `Security` +* `Cookie` +* `Header` +* `Path` +* `Query` + +Они работают так же, как и в других FastAPI эндпоинтах/*операциях пути*: + +{* ../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82] *} + +/// info | Примечание + +В веб-сокете вызывать `HTTPException` не имеет смысла. Вместо этого нужно использовать `WebSocketException`. + +Закрывающий статус код можно использовать из valid codes defined in the specification. + +/// + +### Веб-сокеты с зависимостями: проверка в действии + +Если ваш файл называется `main.py`, то запустите приложение командой: + +
+ +```console +$ fastapi dev main.py + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +Откройте браузер по адресу http://127.0.0.1:8000. + +Там вы можете задать: + +* "Item ID", используемый в пути. +* "Token", используемый как query-параметр. + +/// tip | Подсказка + +Обратите внимание, что query-параметр `token` будет обработан в зависимости. + +/// + +Теперь вы можете подключиться к веб-сокету и начинать отправку и получение сообщений: + + + +## Обработка отключений и работа с несколькими клиентами + +Если веб-сокет соединение закрыто, то `await websocket.receive_text()` вызовет исключение `WebSocketDisconnect`, которое можно поймать и обработать как в этом примере: + +{* ../../docs_src/websockets/tutorial003_py39.py hl[79:81] *} + +Чтобы воспроизвести пример: + +* Откройте приложение в нескольких вкладках браузера. +* Отправьте из них сообщения. +* Затем закройте одну из вкладок. + +Это вызовет исключение `WebSocketDisconnect`, и все остальные клиенты получат следующее сообщение: + +``` +Client #1596980209979 left the chat +``` + +/// tip | Примечание + +Приложение выше - это всего лишь простой минимальный пример, демонстрирующий обработку и передачу сообщений нескольким веб-сокет соединениям. + +Но имейте в виду, что это будет работать только в одном процессе и только пока он активен, так как всё обрабатывается в простом списке в оперативной памяти. + +Если нужно что-то легко интегрируемое с FastAPI, но более надежное и с поддержкой Redis, PostgreSQL или другого, то можно воспользоваться encode/broadcaster. + +/// + +## Дополнительная информация + +Для более глубокого изучения темы воспользуйтесь документацией Starlette: + +* The `WebSocket` class. +* Class-based WebSocket handling. From 0310af3557ef1fba1d2487f929c5080180642e4b Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 3 Feb 2025 13:34:01 +0000 Subject: [PATCH 053/117] =?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 12d6487aa..a9d86d854 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Translations + +* 🌐 Add Russian translation for `docs/ru/docs/advanced/websockets.md`. PR [#13279](https://github.com/fastapi/fastapi/pull/13279) by [@Rishat-F](https://github.com/Rishat-F). + ### Internal * 🔧 Update sponsors, add Permit. PR [#13288](https://github.com/fastapi/fastapi/pull/13288) by [@tiangolo](https://github.com/tiangolo). From c73e895b8639b6d69e3ff1849825c03b9a8e7b60 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Feb 2025 13:51:01 +0000 Subject: [PATCH 054/117] =?UTF-8?q?=E2=AC=86=20Bump=20inline-snapshot=20fr?= =?UTF-8?q?om=200.18.1=20to=200.19.3=20(#13298)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [inline-snapshot](https://github.com/15r10nk/inline-snapshot) from 0.18.1 to 0.19.3. - [Release notes](https://github.com/15r10nk/inline-snapshot/releases) - [Changelog](https://github.com/15r10nk/inline-snapshot/blob/main/CHANGELOG.md) - [Commits](https://github.com/15r10nk/inline-snapshot/compare/0.18.1...0.19.3) --- updated-dependencies: - dependency-name: inline-snapshot 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-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 91e7fb7aa..4a15844e4 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -10,7 +10,7 @@ anyio[trio] >=3.2.1,<5.0.0 PyJWT==2.8.0 pyyaml >=5.3.1,<7.0.0 passlib[bcrypt] >=1.7.2,<2.0.0 -inline-snapshot==0.18.1 +inline-snapshot==0.19.3 # types types-ujson ==5.10.0.20240515 types-orjson ==3.6.2 From ae724b05ceef1a7d62a8aa549fc9dc34c9bd3ad9 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 3 Feb 2025 13:51:28 +0000 Subject: [PATCH 055/117] =?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 a9d86d854..d3e652d19 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Internal +* ⬆ Bump inline-snapshot from 0.18.1 to 0.19.3. PR [#13298](https://github.com/fastapi/fastapi/pull/13298) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔧 Update sponsors, add Permit. PR [#13288](https://github.com/fastapi/fastapi/pull/13288) by [@tiangolo](https://github.com/tiangolo). ## 0.115.8 From fb19d9895d1b4ca2baf1695204c0f9753ed737fe Mon Sep 17 00:00:00 2001 From: Rafael de Oliveira Marques Date: Fri, 7 Feb 2025 19:01:55 -0300 Subject: [PATCH 056/117] =?UTF-8?q?=F0=9F=8C=90=20Update=20Portuguese=20Tr?= =?UTF-8?q?anslation=20for=20`docs/pt/docs/index.md`=20(#13328)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/pt/docs/index.md | 139 +++++++++++++++++++++++++++--------------- 1 file changed, 89 insertions(+), 50 deletions(-) diff --git a/docs/pt/docs/index.md b/docs/pt/docs/index.md index bc23114dc..138048f06 100644 --- a/docs/pt/docs/index.md +++ b/docs/pt/docs/index.md @@ -11,15 +11,18 @@ Framework FastAPI, alta performance, fácil de aprender, fácil de codar, pronto para produção

- - Test + + Test - - Coverage + + Coverage Package version + + Supported Python versions +

--- @@ -60,7 +63,7 @@ Os recursos chave são: -Outros patrocinadores +Outros patrocinadores ## Opiniões @@ -70,6 +73,18 @@ Os recursos chave são: --- +"_Nós adotamos a biblioteca **FastAPI** para iniciar um servidor **REST** que pode ser consultado para obter **previsões**. [para o Ludwig]_" + +
Piero Molino, Yaroslav Dudin, e Sai Sumanth Miryala - Uber (ref)
+ +--- + +"_A **Netflix** tem o prazer de anunciar o lançamento open-source do nosso framework de orquestração de **gerenciamento de crises**: **Dispatch**! [criado com **FastAPI**]_" + +
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
+ +--- + "*Estou extremamente entusiasmado com o **FastAPI**. É tão divertido!*"
Brian Okken - Python Bytes podcaster (ref)
@@ -90,9 +105,9 @@ Os recursos chave são: --- -"*Nós adotamos a biblioteca **FastAPI** para criar um servidor **REST** que possa ser chamado para obter **predições**. [para o Ludwig]*" +"_Se alguém estiver procurando construir uma API Python para produção, eu recomendaria fortemente o **FastAPI**. Ele é **lindamente projetado**, **simples de usar** e **altamente escalável**. Ele se tornou um **componente chave** para a nossa estratégia API first de desenvolvimento e está impulsionando diversas automações e serviços, como o nosso Virtual TAC Engineer._" -
Piero Molino, Yaroslav Dudin e Sai Sumanth Miryala - Uber (ref)
+
Deon Pillsbury - Cisco (ref)
--- @@ -113,28 +128,20 @@ FastAPI está nos ombros de gigantes: ## Instalação -
- -```console -$ pip install fastapi - ----> 100% -``` - -
- -Você também precisará de um servidor ASGI para produção, tal como Uvicorn ou Hypercorn. +Crie e ative um ambiente virtual, e então instale o FastAPI:
```console -$ pip install "uvicorn[standard]" +$ pip install "fastapi[standard]" ---> 100% ```
+**Nota**: Certifique-se de que você colocou `"fastapi[standard]"` com aspas, para garantir que funcione em todos os terminais. + ## Exemplo ### Crie @@ -184,7 +191,7 @@ async def read_item(item_id: int, q: Union[str, None] = None): **Nota**: -Se você não sabe, verifique a seção _"In a hurry?"_ sobre `async` e `await` nas docs. +Se você não sabe, verifique a seção _"Com pressa?"_ sobre `async` e `await` nas docs. @@ -195,11 +202,24 @@ Rode o servidor com:
```console -$ uvicorn main:app --reload - +$ fastapi dev main.py + + ╭────────── 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 │ + │ │ + ╰─────────────────────────────────────────────────────╯ + +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 [28720] -INFO: Started server process [28722] +INFO: Started reloader process [2248755] using WatchFiles +INFO: Started server process [2248757] INFO: Waiting for application startup. INFO: Application startup complete. ``` @@ -207,13 +227,13 @@ INFO: Application startup complete.
-Sobre o comando uvicorn main:app --reload... +Sobre o comando fastapi dev main.py... -O comando `uvicorn main:app` se refere a: +O comando `fastapi dev` lê o seu arquivo `main.py`, identifica o aplicativo **FastAPI** nele, e inicia um servidor usando o Uvicorn. -* `main`: o arquivo `main.py` (o "módulo" Python). -* `app`: o objeto criado dentro de `main.py` com a linha `app = FastAPI()`. -* `--reload`: faz o servidor recarregar após mudanças de código. Somente faça isso para desenvolvimento. +Por padrão, o `fastapi dev` iniciará com *auto-reload* habilitado para desenvolvimento local. + +Você pode ler mais sobre isso na documentação do FastAPI CLI.
@@ -268,7 +288,7 @@ app = FastAPI() class Item(BaseModel): name: str price: float - is_offer: Union[bool] = None + is_offer: Union[bool, None] = None @app.get("/") @@ -286,7 +306,7 @@ def update_item(item_id: int, item: Item): return {"item_name": item.name, "item_id": item_id} ``` -O servidor deverá recarregar automaticamente (porquê você adicionou `--reload` ao comando `uvicorn` acima). +O servidor `fastapi dev` deverá recarregar automaticamente. ### Evoluindo a Documentação Interativa da API @@ -316,7 +336,7 @@ E agora, vá para Tutorial - Guia do Usuário. +Para um exemplo mais completo incluindo mais recursos, veja Tutorial - Guia do Usuário. **Alerta de Spoiler**: o tutorial - guia do usuário inclui: @@ -416,9 +436,9 @@ Para um exemplo mais completo incluindo mais recursos, veja Injeção de Dependência**. * Segurança e autenticação, incluindo suporte para **OAuth2** com autenticação **JWT tokens** e **HTTP Basic**. * Técnicas mais avançadas (mas igualmente fáceis) para declaração de **modelos JSON profundamente aninhados** (graças ao Pydantic). +* Integrações **GraphQL** com o Strawberry e outras bibliotecas. * Muitos recursos extras (graças ao Starlette) como: * **WebSockets** - * **GraphQL** * testes extrememamente fáceis baseados em HTTPX e `pytest` * **CORS** * **Cookie Sessions** @@ -428,30 +448,49 @@ 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 Benchmarks. +Para entender mais sobre performance, veja a seção Comparações. + +## Dependências + +O FastAPI depende do Pydantic e do Starlette. + -## Dependências opcionais +### Dependências `standard` -Usados por Pydantic: +Quando você instala o FastAPI com `pip install "fastapi[standard]"`, ele vêm com o grupo `standard` (padrão) de dependências opcionais: + +Utilizado pelo Pydantic: * email-validator - para validação de email. -Usados por Starlette: +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()`. + +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. +* `fastapi-cli` - que disponibiliza o comando `fastapi`. + +### Sem as dependências `standard` + +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 + +Existem algumas dependências adicionais que você pode querer instalar. -* httpx - Necessário se você quiser utilizar o `TestClient`. -* jinja2 - Necessário se você quiser utilizar a configuração padrão de templates. -* python-multipart - Necessário se você quiser suporte com "parsing" de formulário, com `request.form()`. -* itsdangerous - Necessário para suporte a `SessionMiddleware`. -* pyyaml - Necessário para suporte a `SchemaGenerator` da Starlette (você provavelmente não precisará disso com o FastAPI). -* graphene - Necessário para suporte a `GraphQLApp`. +Dependências opcionais adicionais do Pydantic: -Usados por FastAPI / Starlette: +* pydantic-settings - para gerenciamento de configurações. +* pydantic-extra-types - tipos extras para serem utilizados com o Pydantic. -* uvicorn - para o servidor que carrega e serve sua aplicação. -* orjson - Necessário se você quer utilizar `ORJSONResponse`. -* ujson - Necessário se você quer utilizar `UJSONResponse`. +Dependências opcionais adicionais do FastAPI: -Você pode instalar todas essas dependências com `pip install fastapi[all]`. +* orjson - Obrigatório se você deseja utilizar o `ORJSONResponse`. +* ujson - Obrigatório se você deseja utilizar o `UJSONResponse`. ## Licença From 3958e5a113fbe9b80e24b46b6d91334ec842beb7 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 7 Feb 2025 22:02:19 +0000 Subject: [PATCH 057/117] =?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 d3e652d19..07d620043 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Translations +* 🌐 Update Portuguese Translation for `docs/pt/docs/index.md`. PR [#13328](https://github.com/fastapi/fastapi/pull/13328) by [@ceb10n](https://github.com/ceb10n). * 🌐 Add Russian translation for `docs/ru/docs/advanced/websockets.md`. PR [#13279](https://github.com/fastapi/fastapi/pull/13279) by [@Rishat-F](https://github.com/Rishat-F). ### Internal From 52c1488a372f1c5808429aebc1fbbfd7ca2ba6be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro?= Date: Fri, 7 Feb 2025 19:02:59 -0300 Subject: [PATCH 058/117] =?UTF-8?q?=F0=9F=8C=90=20Update=20Portuguese=20Tr?= =?UTF-8?q?anslation=20for=20`docs/pt/docs/deployment/https.md`=20(#13317)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/pt/docs/deployment/https.md | 194 +++++++++++++++++++++++++++---- 1 file changed, 171 insertions(+), 23 deletions(-) diff --git a/docs/pt/docs/deployment/https.md b/docs/pt/docs/deployment/https.md index 9a13977ec..9ad419934 100644 --- a/docs/pt/docs/deployment/https.md +++ b/docs/pt/docs/deployment/https.md @@ -14,38 +14,186 @@ Para aprender o básico de HTTPS de uma perspectiva do usuário, verifique 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) 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**. -É uma prática comum ter um programa/servidor HTTP em execução no servidor (máquina, host, etc.) e gerenciar todas as partes HTTPS: 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 apropriado e envie-a de volta ao cliente usando HTTPS. Este servidor é frequentemente chamado de TLS Termination Proxy. +É 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**. + +Algumas das opções que você pode usar como Proxy de Terminação TLS são: + +* Traefik (que também pode gerenciar a renovação de certificados) +* Caddy (que também pode gerenciar a renovação de certificados) +* Nginx +* HAProxy ## Let's 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 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 é realmente 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 de sua vida útil reduzida. 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 + +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 + +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**. + +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**. + +Você provavelmente fará isso apenas uma vez, na primeira vez em que tudo estiver sendo configurado. + +/// tip | Dica + +Essa parte do Nome do Domínio se dá muito antes do HTTPS, mas como tudo depende do domínio e endereço IP público, vale a pena mencioná-la aqui. + +/// + +### 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`. + +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 + +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**. + +### TLS com a Extensão SNI + +**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**. + +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. + +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. + +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. + +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. + +/// + +### Solicitação HTTPS + +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. + + + +### Desencriptando a Solicitação + +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 + +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 + +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. + + + +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 + +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**. + + + +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 + +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. + +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. + * 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). + +## Recapitulando + +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**. 🔒 From f97c8de41a401ce04ef1c5e8f7891f01b54b0261 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 7 Feb 2025 22:04:25 +0000 Subject: [PATCH 059/117] =?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 07d620043..6a8b0f5d1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Translations +* 🌐 Update Portuguese Translation for `docs/pt/docs/deployment/https.md`. PR [#13317](https://github.com/fastapi/fastapi/pull/13317) by [@Joao-Pedro-P-Holanda](https://github.com/Joao-Pedro-P-Holanda). * 🌐 Update Portuguese Translation for `docs/pt/docs/index.md`. PR [#13328](https://github.com/fastapi/fastapi/pull/13328) by [@ceb10n](https://github.com/ceb10n). * 🌐 Add Russian translation for `docs/ru/docs/advanced/websockets.md`. PR [#13279](https://github.com/fastapi/fastapi/pull/13279) by [@Rishat-F](https://github.com/Rishat-F). From 0c24d0607b843027b827b556beef5ead7eb61695 Mon Sep 17 00:00:00 2001 From: Valentyn Date: Sat, 8 Feb 2025 00:06:37 +0200 Subject: [PATCH 060/117] =?UTF-8?q?=F0=9F=8C=90=20Add=20Ukrainian=20transl?= =?UTF-8?q?ation=20for=20`docs/uk/docs/learn/index.md`=20(#13306)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/uk/docs/learn/index.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/uk/docs/learn/index.md diff --git a/docs/uk/docs/learn/index.md b/docs/uk/docs/learn/index.md new file mode 100644 index 000000000..7f9f21e57 --- /dev/null +++ b/docs/uk/docs/learn/index.md @@ -0,0 +1,5 @@ +# Навчання + +У цьому розділі надані вступні та навчальні матеріали для вивчення FastAPI. + +Це можна розглядати як **книгу**, **курс**, або **офіційний** та рекомендований спосіб освоїти FastAPI. 😎 From eee8d4c58a6620f240564655d11b355d3945f802 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 7 Feb 2025 22:07:12 +0000 Subject: [PATCH 061/117] =?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 6a8b0f5d1..b6145c9bb 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Translations +* 🌐 Add Ukrainian translation for `docs/uk/docs/learn/index.md`. PR [#13306](https://github.com/fastapi/fastapi/pull/13306) by [@valentinDruzhinin](https://github.com/valentinDruzhinin). * 🌐 Update Portuguese Translation for `docs/pt/docs/deployment/https.md`. PR [#13317](https://github.com/fastapi/fastapi/pull/13317) by [@Joao-Pedro-P-Holanda](https://github.com/Joao-Pedro-P-Holanda). * 🌐 Update Portuguese Translation for `docs/pt/docs/index.md`. PR [#13328](https://github.com/fastapi/fastapi/pull/13328) by [@ceb10n](https://github.com/ceb10n). * 🌐 Add Russian translation for `docs/ru/docs/advanced/websockets.md`. PR [#13279](https://github.com/fastapi/fastapi/pull/13279) by [@Rishat-F](https://github.com/Rishat-F). From 701f5791d373ec82f3660b9d94b97574d4fbddbe Mon Sep 17 00:00:00 2001 From: Valentyn Date: Sat, 8 Feb 2025 00:08:49 +0200 Subject: [PATCH 062/117] =?UTF-8?q?=F0=9F=8C=90=20Add=20Ukrainian=20transl?= =?UTF-8?q?ation=20for=20`docs/uk/docs/features.md`=20(#13308)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/uk/docs/features.md | 189 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 docs/uk/docs/features.md diff --git a/docs/uk/docs/features.md b/docs/uk/docs/features.md new file mode 100644 index 000000000..7d679d8ee --- /dev/null +++ b/docs/uk/docs/features.md @@ -0,0 +1,189 @@ +# Функціональні можливості + +## Функціональні можливості FastAPI + +**FastAPI** надає вам такі можливості: + +### Використання відкритих стандартів + +* OpenAPI для створення API, включаючи оголошення шляхів, операцій, параметрів, тіл запитів, безпеки тощо. +* Автоматична документація моделей даних за допомогою JSON Schema (оскільки OpenAPI базується саме на JSON Schema). +* Розроблено на основі цих стандартів після ретельного аналізу, а не як додатковий рівень поверх основної архітектури. +* Це також дає змогу автоматично **генерувати код клієнта** багатьма мовами. + +### Автоматична генерація документації + +Інтерактивна документація API та вебінтерфейс для його дослідження. Оскільки фреймворк базується на OpenAPI, є кілька варіантів, два з яких включені за замовчуванням. + +* Swagger UI — дозволяє інтерактивно переглядати API, викликати та тестувати його прямо у браузері. + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* Альтернативна документація API за допомогою ReDoc. + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### Тільки сучасний Python + +FastAPI використовує стандартні **типи Python** (завдяки Pydantic). Вам не потрібно вивчати новий синтаксис — лише стандартний сучасний Python. + +Якщо вам потрібне коротке нагадування про використання типів у Python (навіть якщо ви не використовуєте FastAPI), перегляньте короткий підручник: [Вступ до типів Python](python-types.md){.internal-link target=_blank}. + +Ось приклад стандартного Python-коду з типами: + +```Python +from datetime import date +from pydantic import BaseModel + +# Оголошення змінної як str +# з підтримкою автодоповнення у редакторі +def main(user_id: str): + return user_id + +# Модель Pydantic +class User(BaseModel): + id: int + name: str + joined: date +``` + +Приклад використання цієї моделі: + +```Python +my_user: User = User(id=3, name="John Doe", joined="2018-07-19") + +second_user_data = { + "id": 4, + "name": "Mary", + "joined": "2018-11-30", +} + +my_second_user: User = User(**second_user_data) +``` + +/// info | Інформація + +`**second_user_data` означає: + +Передати ключі та значення словника `second_user_data` як аргументи у вигляді "ключ-значення", еквівалентно `User(id=4, name="Mary", joined="2018-11-30")`. + +/// + +### Підтримка редакторів (IDE) + +Фреймворк спроєктований так, щоб бути легким і інтуїтивно зрозумілим. Усі рішення тестувалися у різних редакторах ще до початку розробки, щоб забезпечити найкращий досвід програмування. + +За результатами опитувань розробників Python однією з найпопулярніших функцій є "автодоповнення". + +**FastAPI** повністю підтримує автодоповнення у всіх місцях, тому вам рідко доведеться повертатися до документації. + +Приклад автодоповнення у редакторах: + +* у Visual Studio Code: + +![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) + +* у PyCharm: + +![editor support](https://fastapi.tiangolo.com/img/pycharm-completion.png) + +### Короткий код +FastAPI має розумні налаштування **за замовчуванням**, але всі параметри можна налаштовувати відповідно до ваших потреб. Однак за замовчуванням все "просто працює". + +### Валідація +* Підтримка валідації для більшості (або всіх?) **типів даних Python**, зокрема: + * JSON-об'єктів (`dict`). + * JSON-списків (`list`) з визначенням типів елементів. + * Рядків (`str`) із мінімальною та максимальною довжиною. + * Чисел (`int`, `float`) з обмеженнями мінімальних та максимальних значень тощо. + +* Валідація складніших типів, таких як: + * URL. + * Email. + * UUID. + * ...та інші. + +Уся валідація виконується через надійний та перевірений **Pydantic**. + +### Безпека та автентифікація + +**FastAPI** підтримує вбудовану автентифікацію та авторизацію, без прив’язки до конкретних баз даних чи моделей даних. + +Підтримуються всі схеми безпеки OpenAPI, включаючи: + +* HTTP Basic. +* **OAuth2** (також із підтримкою **JWT-токенів**). Див. підручник: [OAuth2 із JWT](tutorial/security/oauth2-jwt.md){.internal-link target=_blank}. +* Ключі API в: + * Заголовках. + * Параметрах запиту. + * Cookies тощо. + +А також усі можливості безпеки від Starlette (зокрема **сесійні cookies**). + +Усі вони створені як багаторазові інструменти та компоненти, які легко інтегруються з вашими системами, сховищами даних, реляційними та NoSQL базами даних тощо. + +### Впровадження залежностей + +**FastAPI** містить надзвичайно просту у використанні, але потужну систему впровадження залежностей. + +* Залежності можуть мати власні залежності, утворюючи ієрархію або **"граф залежностей"**. +* Усі залежності автоматично керуються фреймворком. +* Усі залежності можуть отримувати дані з запитів і розширювати **обмеження операції за шляхом** та автоматичну документацію. +* **Автоматична валідація** навіть для параметрів *операцій шляху*, визначених у залежностях. +* Підтримка складних систем автентифікації користувачів, **з'єднань із базами даних** тощо. +* **Жодних обмежень** щодо використання баз даних, фронтендів тощо, але водночас проста інтеграція з усіма ними. + +### Немає обмежень на "плагіни" + +Або іншими словами, вони не потрібні – просто імпортуйте та використовуйте необхідний код. + +Будь-яка інтеграція спроєктована настільки просто (з використанням залежностей), що ви можете створити "плагін" для свого застосунку всього у 2 рядках коду, використовуючи ту саму структуру та синтаксис, що й для ваших *операцій шляху*. + +### Протестовано + +* 100% покриття тестами. +* 100% анотована типами кодова база. +* Використовується у робочих середовищах. + +## Можливості Starlette + +**FastAPI** повністю сумісний із (та побудований на основі) Starlette. Тому будь-який додатковий код Starlette, який ви маєте, також працюватиме. + +**FastAPI** фактично є підкласом **Starlette**. Тому, якщо ви вже знайомі зі Starlette або використовуєте його, більшість функціональності працюватиме так само. + +З **FastAPI** ви отримуєте всі можливості **Starlette** (адже FastAPI — це, по суті, Starlette на стероїдах): + +* Разюча продуктивність. Це один із найшвидших фреймворків на Python, на рівні з **NodeJS** і **Go**. +* Підтримка **WebSocket**. +* Фонові задачі у процесі. +* Події запуску та завершення роботи. +* Клієнт для тестування, побудований на HTTPX. +* Підтримка **CORS**, **GZip**, статичних файлів, потокових відповідей. +* Підтримка **сесій** і **cookie**. +* 100% покриття тестами. +* 100% анотована типами кодова база. + +## Можливості Pydantic + +**FastAPI** повністю сумісний із (та побудований на основі) Pydantic. Тому будь-який додатковий код Pydantic, який ви маєте, також працюватиме. + +Включаючи зовнішні бібліотеки, побудовані також на Pydantic, такі як ORM, ODM для баз даних. + +Це також означає, що в багатьох випадках ви можете передати той самий об'єкт, який отримуєте з запиту, **безпосередньо в базу даних**, оскільки все автоматично перевіряється. + +Те ж саме відбувається й у зворотному напрямку — у багатьох випадках ви можете просто передати об'єкт, який отримуєте з бази даних, **безпосередньо клієнту**. + +З **FastAPI** ви отримуєте всі можливості **Pydantic** (адже FastAPI базується на Pydantic для обробки всіх даних): + +* **Ніякої плутанини** : + * Не потрібно вчити нову мову для визначення схем. + * Якщо ви знаєте типи Python, ви знаєте, як використовувати Pydantic. +* Легко працює з вашим **IDE/лінтером/мозком**: + * Оскільки структури даних Pydantic є просто екземплярами класів, які ви визначаєте; автодоповнення, лінтинг, mypy і ваша інтуїція повинні добре працювати з вашими перевіреними даними. +* Валідація **складних структур**: + * Використання ієрархічних моделей Pydantic. Python `typing`, `List` і `Dict` тощо. + * Валідатори дозволяють чітко і просто визначати, перевіряти й документувати складні схеми даних у вигляді JSON-схеми. + * Ви можете мати глибоко **вкладені JSON об'єкти** та перевірити та анотувати їх всі. +* **Розширюваність**: + * Pydantic дозволяє визначати користувацькі типи даних або розширювати валідацію методами в моделі декоратором `validator`. +* 100% покриття тестами. From 2d7d5dafb0ad59c0a19f996a85ec1210f80a141e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C6=B0=C6=A1ng=20T=E1=BA=A5n=20Th=C3=A0nh?= <51350651+ptt3199@users.noreply.github.com> Date: Sat, 8 Feb 2025 05:09:16 +0700 Subject: [PATCH 063/117] =?UTF-8?q?=F0=9F=8C=90=20Add=20Vietnamese=20trans?= =?UTF-8?q?lation=20for=20`docs/vi/docs/fastapi-cli.md`=20(#13294)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/vi/docs/fastapi-cli.md | 75 +++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 docs/vi/docs/fastapi-cli.md diff --git a/docs/vi/docs/fastapi-cli.md b/docs/vi/docs/fastapi-cli.md new file mode 100644 index 000000000..d9e315ae4 --- /dev/null +++ b/docs/vi/docs/fastapi-cli.md @@ -0,0 +1,75 @@ +# FastAPI CLI + +**FastAPI CLI** là một chương trình dòng lệnh có thể được sử dụng để phục vụ ứng dụng FastAPI của bạn, quản lý dự án FastAPI của bạn và nhiều hoạt động khác. + +Khi bạn cài đặt FastAPI (vd với `pip install "fastapi[standard]"`), nó sẽ bao gồm một gói được gọi là `fastapi-cli`, gói này cung cấp lệnh `fastapi` trong terminal. + +Để chạy ứng dụng FastAPI của bạn cho quá trình phát triển (development), bạn có thể sử dụng lệnh `fastapi dev`: + +
+ +```console +$ fastapi dev main.py + + FastAPI Starting development server 🚀 + + Searching for package file structure from directories with + __init__.py files + Importing from /home/user/code/awesomeapp + + module 🐍 main.py + + code Importing the FastAPI app object from the module with the + following code: + + from main import app + + 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. +``` + +
+ +Chương trình dòng lệnh `fastapi` là **FastAPI CLI**. + +FastAPI CLI nhận đường dẫn đến chương trình Python của bạn (vd `main.py`) và tự động phát hiện đối tượng `FastAPI` (thường được gọi là `app`), xác định quá trình nhập đúng, và sau đó chạy nó (serve). + +Đố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). 😎 + +## `fastapi dev` + +Chạy `fastapi dev` sẽ khởi động quá trình phát triển. + +Mặc định, **auto-reload** được bật, tự động tải lại server khi bạn thay đổi code của bạn. Điều này tốn nhiều tài nguyên và có thể kém ổn định hơn khi nó bị tắt. Bạn nên sử dụng nó cho quá trình phát triển. Nó cũng lắng nghe địa chỉ IP `127.0.0.1`, đó là địa chỉ IP của máy tính để tự giao tiếp với chính nó (`localhost`). + +## `fastapi run` + +Chạy `fastapi run` mặc định sẽ khởi động FastAPI cho quá trình vận hành thực tế. + +Mặc định, **auto-reload** bị tắt. Nó cũng lắng nghe địa chỉ IP `0.0.0.0`, đó là tất cả các địa chỉ IP có sẵn, như vậy nó sẽ được truy cập công khai bởi bất kỳ ai có thể giao tiếp với máy tính. Đây là cách bạn thường chạy nó trong sản phẩm hoàn thiện, ví dụ trong một container. + +Trong hầu hết các trường hợp, bạn sẽ (và nên) có một "proxy điểm cuối (termination proxy)" xử lý HTTPS cho bạn, điều này sẽ phụ thuộc vào cách bạn triển khai ứng dụng của bạn, nhà cung cấp có thể làm điều này cho bạn, hoặc bạn có thể cần thiết lập nó. + +/// tip + +Bạn có thể tìm hiểu thêm về FastAPI CLI trong [tài liệu triển khai](deployment/index.md){.internal-link target=_blank}. + +/// From 2bb94fb90bccd4a0545b477b67f6076ba2e2945c Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 7 Feb 2025 22:09:51 +0000 Subject: [PATCH 064/117] =?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 b6145c9bb..75a024a0b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Translations +* 🌐 Add Ukrainian translation for `docs/uk/docs/features.md`. PR [#13308](https://github.com/fastapi/fastapi/pull/13308) by [@valentinDruzhinin](https://github.com/valentinDruzhinin). * 🌐 Add Ukrainian translation for `docs/uk/docs/learn/index.md`. PR [#13306](https://github.com/fastapi/fastapi/pull/13306) by [@valentinDruzhinin](https://github.com/valentinDruzhinin). * 🌐 Update Portuguese Translation for `docs/pt/docs/deployment/https.md`. PR [#13317](https://github.com/fastapi/fastapi/pull/13317) by [@Joao-Pedro-P-Holanda](https://github.com/Joao-Pedro-P-Holanda). * 🌐 Update Portuguese Translation for `docs/pt/docs/index.md`. PR [#13328](https://github.com/fastapi/fastapi/pull/13328) by [@ceb10n](https://github.com/ceb10n). From 50b307c9f68ad9a966e86d7f62c82d476abd0cc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 7 Feb 2025 22:10:25 +0000 Subject: [PATCH 065/117] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20Peopl?= =?UTF-8?q?e=20-=20Contributors=20and=20Translators=20(#13293)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions Co-authored-by: Alejandra <90076947+alejsdev@users.noreply.github.com> --- docs/en/data/contributors.yml | 41 +++++--- docs/en/data/translation_reviewers.yml | 125 +++++++++++++++---------- docs/en/data/translators.yml | 45 +++++---- 3 files changed, 128 insertions(+), 83 deletions(-) diff --git a/docs/en/data/contributors.yml b/docs/en/data/contributors.yml index f679d7286..0e1a6505b 100644 --- a/docs/en/data/contributors.yml +++ b/docs/en/data/contributors.yml @@ -1,13 +1,18 @@ tiangolo: login: tiangolo - count: 697 + count: 713 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 url: https://github.com/tiangolo dependabot: login: dependabot - count: 89 + count: 90 avatarUrl: https://avatars.githubusercontent.com/in/29110?v=4 url: https://github.com/apps/dependabot +alejsdev: + login: alejsdev + count: 47 + avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=356f39ff3f0211c720b06d3dbb060e98884085e3&v=4 + url: https://github.com/alejsdev github-actions: login: github-actions count: 26 @@ -15,7 +20,7 @@ github-actions: url: https://github.com/apps/github-actions Kludex: login: Kludex - count: 22 + count: 23 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4 url: https://github.com/Kludex pre-commit-ci: @@ -23,11 +28,6 @@ pre-commit-ci: count: 22 avatarUrl: https://avatars.githubusercontent.com/in/68672?v=4 url: https://github.com/apps/pre-commit-ci -alejsdev: - login: alejsdev - count: 21 - avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=356f39ff3f0211c720b06d3dbb060e98884085e3&v=4 - url: https://github.com/alejsdev dmontagu: login: dmontagu count: 17 @@ -108,6 +108,11 @@ hitrust: count: 4 avatarUrl: https://avatars.githubusercontent.com/u/3360631?u=5fa1f475ad784d64eb9666bdd43cc4d285dcc773&v=4 url: https://github.com/hitrust +ShahriyarR: + login: ShahriyarR + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/3852029?u=c9a1691e5ebdc94cbf543086099a6ed705cdb873&v=4 + url: https://github.com/ShahriyarR adriangb: login: adriangb count: 4 @@ -208,11 +213,6 @@ graingert: count: 3 avatarUrl: https://avatars.githubusercontent.com/u/413772?u=64b77b6aa405c68a9c6bcf45f84257c66eea5f32&v=4 url: https://github.com/graingert -ShahriyarR: - login: ShahriyarR - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/3852029?u=c9a1691e5ebdc94cbf543086099a6ed705cdb873&v=4 - url: https://github.com/ShahriyarR jaystone776: login: jaystone776 count: 3 @@ -433,6 +433,11 @@ imba-tjd: count: 2 avatarUrl: https://avatars.githubusercontent.com/u/24759802?u=01e901a4fe004b4b126549d3ff1c4000fe3720b5&v=4 url: https://github.com/imba-tjd +johnthagen: + login: johnthagen + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/10340167?u=47147fc4e4db1f573bee3fe428deeacb3197bc5f&v=4 + url: https://github.com/johnthagen paxcodes: login: paxcodes count: 2 @@ -443,6 +448,11 @@ kaustubhgupta: count: 2 avatarUrl: https://avatars.githubusercontent.com/u/43691873?u=8dd738718ac7ffad4ef31e86b5d780a1141c695d&v=4 url: https://github.com/kaustubhgupta +kinuax: + login: kinuax + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/13321374?u=22dc9873d6d9f2c7e4fc44c6480c3505efb1531f&v=4 + url: https://github.com/kinuax wakabame: login: wakabame count: 2 @@ -503,3 +513,8 @@ AyushSinghal1794: count: 2 avatarUrl: https://avatars.githubusercontent.com/u/89984761?v=4 url: https://github.com/AyushSinghal1794 +DanielKusyDev: + login: DanielKusyDev + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/36250676?u=2ea6114ff751fc48b55f231987a0e2582c6b1bd2&v=4 + url: https://github.com/DanielKusyDev diff --git a/docs/en/data/translation_reviewers.yml b/docs/en/data/translation_reviewers.yml index 6cc09a7c1..6f16893ba 100644 --- a/docs/en/data/translation_reviewers.yml +++ b/docs/en/data/translation_reviewers.yml @@ -5,12 +5,12 @@ s111d: url: https://github.com/s111d Xewus: login: Xewus - count: 139 + count: 140 avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4 url: https://github.com/Xewus ceb10n: login: ceb10n - count: 108 + count: 110 avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 url: https://github.com/ceb10n tokusumi: @@ -33,21 +33,26 @@ AlertRED: count: 81 avatarUrl: https://avatars.githubusercontent.com/u/15695000?u=f5a4944c6df443030409c88da7d7fa0b7ead985c&v=4 url: https://github.com/AlertRED +nazarepiedady: + login: nazarepiedady + count: 81 + avatarUrl: https://avatars.githubusercontent.com/u/31008635?u=8dc25777dc9cb51fb0dbba2f137988953d330b78&v=4 + url: https://github.com/nazarepiedady sodaMelon: login: sodaMelon count: 81 avatarUrl: https://avatars.githubusercontent.com/u/66295123?u=be939db90f1119efee9e6110cc05066ff1f40f00&v=4 url: https://github.com/sodaMelon -nazarepiedady: - login: nazarepiedady - count: 78 - avatarUrl: https://avatars.githubusercontent.com/u/31008635?u=8dc25777dc9cb51fb0dbba2f137988953d330b78&v=4 - url: https://github.com/nazarepiedady Alexandrhub: login: Alexandrhub count: 68 avatarUrl: https://avatars.githubusercontent.com/u/119126536?u=9fc0d48f3307817bafecc5861eb2168401a6cb04&v=4 url: https://github.com/Alexandrhub +alv2017: + login: alv2017 + count: 64 + avatarUrl: https://avatars.githubusercontent.com/u/31544722?v=4 + url: https://github.com/alv2017 waynerv: login: waynerv count: 63 @@ -55,7 +60,7 @@ waynerv: url: https://github.com/waynerv cassiobotaro: login: cassiobotaro - count: 61 + count: 62 avatarUrl: https://avatars.githubusercontent.com/u/3127847?u=a08022b191ddbd0a6159b2981d9d878b6d5bb71f&v=4 url: https://github.com/cassiobotaro mattwang44: @@ -138,26 +143,21 @@ romashevchenko: count: 32 avatarUrl: https://avatars.githubusercontent.com/u/132477732?v=4 url: https://github.com/romashevchenko +alejsdev: + login: alejsdev + count: 32 + avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=356f39ff3f0211c720b06d3dbb060e98884085e3&v=4 + url: https://github.com/alejsdev wdh99: login: wdh99 count: 31 avatarUrl: https://avatars.githubusercontent.com/u/108172295?u=8a8fb95d5afe3e0fa33257b2aecae88d436249eb&v=4 url: https://github.com/wdh99 -alv2017: - login: alv2017 - count: 31 - avatarUrl: https://avatars.githubusercontent.com/u/31544722?v=4 - url: https://github.com/alv2017 LorhanSohaky: login: LorhanSohaky count: 30 avatarUrl: https://avatars.githubusercontent.com/u/16273730?u=095b66f243a2cd6a0aadba9a095009f8aaf18393&v=4 url: https://github.com/LorhanSohaky -alejsdev: - login: alejsdev - count: 30 - avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=356f39ff3f0211c720b06d3dbb060e98884085e3&v=4 - url: https://github.com/alejsdev black-redoc: login: black-redoc count: 29 @@ -246,7 +246,7 @@ axel584: wisderfin: login: wisderfin count: 23 - avatarUrl: https://avatars.githubusercontent.com/u/77553770?u=94478d3e1ef7d36d70479c5bd35d8de28b071c10&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/77553770?u=f3b00a26736ba664e9927a1116c6e8088295e073&v=4 url: https://github.com/wisderfin rostik1410: login: rostik1410 @@ -353,6 +353,11 @@ mastizada: count: 16 avatarUrl: https://avatars.githubusercontent.com/u/1975818?u=0751a06d7271c8bf17cb73b1b845644ab4d2c6dc&v=4 url: https://github.com/mastizada +Joao-Pedro-P-Holanda: + login: Joao-Pedro-P-Holanda + count: 16 + avatarUrl: https://avatars.githubusercontent.com/u/110267046?u=331bd016326dac4cf3df4848f6db2dbbf8b5f978&v=4 + url: https://github.com/Joao-Pedro-P-Holanda JaeHyuckSa: login: JaeHyuckSa count: 16 @@ -363,11 +368,6 @@ Jedore: count: 15 avatarUrl: https://avatars.githubusercontent.com/u/17944025?u=81d503e1c800eb666b3861ca47a3a773bbc3f539&v=4 url: https://github.com/Jedore -Joao-Pedro-P-Holanda: - login: Joao-Pedro-P-Holanda - count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/110267046?u=331bd016326dac4cf3df4848f6db2dbbf8b5f978&v=4 - url: https://github.com/Joao-Pedro-P-Holanda kim-sangah: login: kim-sangah count: 15 @@ -386,7 +386,7 @@ dukkee: mkdir700: login: mkdir700 count: 14 - avatarUrl: https://avatars.githubusercontent.com/u/56359329?u=0ba13427420e7f6e4c83947736de247326f2c292&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/56359329?u=3d6ea8714f5000829b60dcf7b13a75b1e73aaf47&v=4 url: https://github.com/mkdir700 BORA040126: login: BORA040126 @@ -473,6 +473,11 @@ kwang1215: count: 12 avatarUrl: https://avatars.githubusercontent.com/u/74170199?u=2a63ff6692119dde3f5e5693365b9fcd6f977b08&v=4 url: https://github.com/kwang1215 +Rishat-F: + login: Rishat-F + count: 12 + avatarUrl: https://avatars.githubusercontent.com/u/66554797?v=4 + url: https://github.com/Rishat-F AdrianDeAnda: login: AdrianDeAnda count: 11 @@ -513,6 +518,11 @@ KNChiu: count: 11 avatarUrl: https://avatars.githubusercontent.com/u/36751646?v=4 url: https://github.com/KNChiu +gitgernit: + login: gitgernit + count: 11 + avatarUrl: https://avatars.githubusercontent.com/u/129539613?u=d04f10143ab32c93f563ea14bf242d1d2bc991b0&v=4 + url: https://github.com/gitgernit mariacamilagl: login: mariacamilagl count: 10 @@ -538,6 +548,11 @@ RobotToI: count: 10 avatarUrl: https://avatars.githubusercontent.com/u/44951382?u=e41dbc19191ce7abed86694b1a44ea0523e1c60e&v=4 url: https://github.com/RobotToI +vitumenezes: + login: vitumenezes + count: 10 + avatarUrl: https://avatars.githubusercontent.com/u/9680878?u=05fd25cfafdc09382bf8907c37293a696c205754&v=4 + url: https://github.com/vitumenezes fcrozetta: login: fcrozetta count: 10 @@ -626,7 +641,7 @@ marcelomarkus: JoaoGustavoRogel: login: JoaoGustavoRogel count: 9 - avatarUrl: https://avatars.githubusercontent.com/u/29525510?u=1dd3096c6c2be2576fd5e818b1be15b2c9768aa5&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/29525510?u=a0a91251f5e43e132608d55d28ccb8645c5ea405&v=4 url: https://github.com/JoaoGustavoRogel Zhongheng-Cheng: login: Zhongheng-Cheng @@ -673,11 +688,6 @@ camigomezdev: count: 8 avatarUrl: https://avatars.githubusercontent.com/u/16061815?u=25b5ebc042fff53fa03dc107ded10e36b1b7a5b9&v=4 url: https://github.com/camigomezdev -gitgernit: - login: gitgernit - count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/129539613?u=d04f10143ab32c93f563ea14bf242d1d2bc991b0&v=4 - url: https://github.com/gitgernit Serrones: login: Serrones count: 7 @@ -698,11 +708,6 @@ anthonycepeda: count: 7 avatarUrl: https://avatars.githubusercontent.com/u/72019805?u=60bdf46240cff8fca482ff0fc07d963fd5e1a27c&v=4 url: https://github.com/anthonycepeda -vitumenezes: - login: vitumenezes - count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/9680878?u=e7c6865aec49c3c94b8c8edc1198d1eac3e50b26&v=4 - url: https://github.com/vitumenezes fabioueno: login: fabioueno count: 7 @@ -956,7 +961,7 @@ devluisrodrigues: timothy-jeong: login: timothy-jeong count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/53824764?u=659311b6f6aeb0fbb8b527723fd4c83642f04327&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/53824764?u=db3d0cea2f5fab64d810113c5039a369699a2774&v=4 url: https://github.com/timothy-jeong lpdswing: login: lpdswing @@ -1053,6 +1058,11 @@ matiasbertani: count: 4 avatarUrl: https://avatars.githubusercontent.com/u/65260383?u=d5edd86a6e2ab4fb1aab7751931fe045a963afd7&v=4 url: https://github.com/matiasbertani +k94-ishi: + login: k94-ishi + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/32672580?u=bc7c5c07af0656be9fe4f1784a444af8d81ded89&v=4 + url: https://github.com/k94-ishi javillegasna: login: javillegasna count: 4 @@ -1063,6 +1073,16 @@ javillegasna: count: 4 avatarUrl: https://avatars.githubusercontent.com/u/174453744?v=4 url: https://github.com/9zimin9 +ilhamfadillah: + login: ilhamfadillah + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/20577838?u=c56192cf99b55affcaad408b240259c62e633450&v=4 + url: https://github.com/ilhamfadillah +Yarous: + login: Yarous + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/61277193?u=5b462347458a373b2d599c6f416d2b75eddbffad&v=4 + url: https://github.com/Yarous tyronedamasceno: login: tyronedamasceno count: 3 @@ -1151,7 +1171,7 @@ rafsaf: frnsimoes: login: frnsimoes count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/66239468?u=771c4b0c403a42ccf2676ac987ac4999e5ad09bc&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/66239468?u=a405e8f10654251e239a4a1d9dd5bda59216727d&v=4 url: https://github.com/frnsimoes lieryan: login: lieryan @@ -1283,11 +1303,16 @@ celestywang: count: 3 avatarUrl: https://avatars.githubusercontent.com/u/184830753?v=4 url: https://github.com/celestywang -ilhamfadillah: - login: ilhamfadillah +RyaWcksn: + login: RyaWcksn count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/20577838?u=c56192cf99b55affcaad408b240259c62e633450&v=4 - url: https://github.com/ilhamfadillah + avatarUrl: https://avatars.githubusercontent.com/u/42831964?u=0cb4265faf3e3425a89e59b6fddd3eb2de180af0&v=4 + url: https://github.com/RyaWcksn +gerry-sabar: + login: gerry-sabar + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/1120123?v=4 + url: https://github.com/gerry-sabar blaisep: login: blaisep count: 2 @@ -1633,13 +1658,13 @@ logan2d5: count: 2 avatarUrl: https://avatars.githubusercontent.com/u/146642263?u=dbd6621f8b0330d6919f6a7131277b92e26fbe87&v=4 url: https://github.com/logan2d5 -RyaWcksn: - login: RyaWcksn +tiaggo16: + login: tiaggo16 count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/42831964?u=0cb4265faf3e3425a89e59b6fddd3eb2de180af0&v=4 - url: https://github.com/RyaWcksn -gerry-sabar: - login: gerry-sabar + avatarUrl: https://avatars.githubusercontent.com/u/62227573?u=359f4e2c51a4b13c8553ac5af405d635b07bb61f&v=4 + url: https://github.com/tiaggo16 +kiharito: + login: kiharito count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/1120123?v=4 - url: https://github.com/gerry-sabar + avatarUrl: https://avatars.githubusercontent.com/u/38311245?v=4 + url: https://github.com/kiharito diff --git a/docs/en/data/translators.yml b/docs/en/data/translators.yml index 7b199dc08..13859044d 100644 --- a/docs/en/data/translators.yml +++ b/docs/en/data/translators.yml @@ -8,6 +8,11 @@ jaystone776: count: 46 avatarUrl: https://avatars.githubusercontent.com/u/11191137?u=299205a95e9b6817a43144a48b643346a5aac5cc&v=4 url: https://github.com/jaystone776 +ceb10n: + login: ceb10n + count: 26 + avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 + url: https://github.com/ceb10n tokusumi: login: tokusumi count: 23 @@ -23,11 +28,6 @@ hasansezertasan: count: 22 avatarUrl: https://avatars.githubusercontent.com/u/13135006?u=99f0b0f0fc47e88e8abb337b4447357939ef93e7&v=4 url: https://github.com/hasansezertasan -ceb10n: - login: ceb10n - count: 22 - avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 - url: https://github.com/ceb10n waynerv: login: waynerv count: 20 @@ -55,7 +55,7 @@ Xewus: url: https://github.com/Xewus Joao-Pedro-P-Holanda: login: Joao-Pedro-P-Holanda - count: 12 + count: 13 avatarUrl: https://avatars.githubusercontent.com/u/110267046?u=331bd016326dac4cf3df4848f6db2dbbf8b5f978&v=4 url: https://github.com/Joao-Pedro-P-Holanda Smlep: @@ -78,6 +78,11 @@ Vincy1230: count: 9 avatarUrl: https://avatars.githubusercontent.com/u/81342412?u=ab5e256a4077a4a91f3f9cd2115ba80780454cbe&v=4 url: https://github.com/Vincy1230 +Zhongheng-Cheng: + login: Zhongheng-Cheng + count: 9 + avatarUrl: https://avatars.githubusercontent.com/u/95612344?u=a0f7730a3cc7486827965e01a119ad610bda4b0a&v=4 + url: https://github.com/Zhongheng-Cheng rjNemo: login: rjNemo count: 8 @@ -93,11 +98,6 @@ pablocm83: count: 8 avatarUrl: https://avatars.githubusercontent.com/u/28315068?u=3310fbb05bb8bfc50d2c48b6cb64ac9ee4a14549&v=4 url: https://github.com/pablocm83 -Zhongheng-Cheng: - login: Zhongheng-Cheng - count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/95612344?u=a0f7730a3cc7486827965e01a119ad610bda4b0a&v=4 - url: https://github.com/Zhongheng-Cheng batlopes: login: batlopes count: 6 @@ -188,6 +188,11 @@ kwang1215: count: 4 avatarUrl: https://avatars.githubusercontent.com/u/74170199?u=2a63ff6692119dde3f5e5693365b9fcd6f977b08&v=4 url: https://github.com/kwang1215 +alv2017: + login: alv2017 + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/31544722?v=4 + url: https://github.com/alv2017 jfunez: login: jfunez count: 3 @@ -313,11 +318,11 @@ nahyunkeem: count: 3 avatarUrl: https://avatars.githubusercontent.com/u/174440096?u=e12401d492eee58570f8914d0872b52e421a776e&v=4 url: https://github.com/nahyunkeem -alv2017: - login: alv2017 +gerry-sabar: + login: gerry-sabar count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/31544722?v=4 - url: https://github.com/alv2017 + avatarUrl: https://avatars.githubusercontent.com/u/1120123?v=4 + url: https://github.com/gerry-sabar izaguerreiro: login: izaguerreiro count: 2 @@ -481,10 +486,10 @@ saeye: timothy-jeong: login: timothy-jeong count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/53824764?u=659311b6f6aeb0fbb8b527723fd4c83642f04327&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/53824764?u=db3d0cea2f5fab64d810113c5039a369699a2774&v=4 url: https://github.com/timothy-jeong -gerry-sabar: - login: gerry-sabar +Rishat-F: + login: Rishat-F count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/1120123?v=4 - url: https://github.com/gerry-sabar + avatarUrl: https://avatars.githubusercontent.com/u/66554797?v=4 + url: https://github.com/Rishat-F From 495ff5baa9694c773015abfaaf233a5b74ceaec5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 7 Feb 2025 22:10:37 +0000 Subject: [PATCH 066/117] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20GitHu?= =?UTF-8?q?b=20topic=20repositories=20(#13302)?= 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 | 334 +++++++++++++++++------------------ 1 file changed, 167 insertions(+), 167 deletions(-) diff --git a/docs/en/data/topic_repos.yml b/docs/en/data/topic_repos.yml index c1176e55c..302dc3bb5 100644 --- a/docs/en/data/topic_repos.yml +++ b/docs/en/data/topic_repos.yml @@ -1,495 +1,495 @@ - name: full-stack-fastapi-template html_url: https://github.com/fastapi/full-stack-fastapi-template - stars: 28796 + stars: 29409 owner_login: fastapi owner_html_url: https://github.com/fastapi - name: Hello-Python html_url: https://github.com/mouredev/Hello-Python - stars: 27554 + stars: 28113 owner_login: mouredev owner_html_url: https://github.com/mouredev - name: serve html_url: https://github.com/jina-ai/serve - stars: 21225 + stars: 21264 owner_login: jina-ai owner_html_url: https://github.com/jina-ai - name: sqlmodel html_url: https://github.com/fastapi/sqlmodel - stars: 14921 + stars: 15109 owner_login: fastapi owner_html_url: https://github.com/fastapi - name: HivisionIDPhotos html_url: https://github.com/Zeyi-Lin/HivisionIDPhotos - stars: 14025 + stars: 14564 owner_login: Zeyi-Lin owner_html_url: https://github.com/Zeyi-Lin - name: Douyin_TikTok_Download_API html_url: https://github.com/Evil0ctal/Douyin_TikTok_Download_API - stars: 10001 + stars: 10701 owner_login: Evil0ctal owner_html_url: https://github.com/Evil0ctal - name: fastapi-best-practices html_url: https://github.com/zhanymkanov/fastapi-best-practices - stars: 9820 + stars: 10180 owner_login: zhanymkanov owner_html_url: https://github.com/zhanymkanov - name: awesome-fastapi html_url: https://github.com/mjhea0/awesome-fastapi - stars: 8899 + stars: 9061 owner_login: mjhea0 owner_html_url: https://github.com/mjhea0 - name: FastUI html_url: https://github.com/pydantic/FastUI - stars: 8400 + stars: 8644 owner_login: pydantic owner_html_url: https://github.com/pydantic - name: nonebot2 html_url: https://github.com/nonebot/nonebot2 - stars: 6235 + stars: 6312 owner_login: nonebot owner_html_url: https://github.com/nonebot - name: serge html_url: https://github.com/serge-chat/serge - stars: 5685 + stars: 5686 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: 4787 - owner_login: fastapi-users - owner_html_url: https://github.com/fastapi-users - name: FileCodeBox html_url: https://github.com/vastsa/FileCodeBox - stars: 4479 + stars: 4933 owner_login: vastsa owner_html_url: https://github.com/vastsa +- name: fastapi-users + html_url: https://github.com/fastapi-users/fastapi-users + stars: 4849 + owner_login: fastapi-users + owner_html_url: https://github.com/fastapi-users - name: hatchet html_url: https://github.com/hatchet-dev/hatchet - stars: 4413 + stars: 4514 owner_login: hatchet-dev owner_html_url: https://github.com/hatchet-dev - name: chatgpt-web-share html_url: https://github.com/chatpire/chatgpt-web-share - stars: 4322 + stars: 4319 owner_login: chatpire owner_html_url: https://github.com/chatpire -- name: atrilabs-engine - html_url: https://github.com/Atri-Labs/atrilabs-engine - stars: 4115 - owner_login: Atri-Labs - owner_html_url: https://github.com/Atri-Labs +- name: polar + html_url: https://github.com/polarsource/polar + stars: 4216 + owner_login: polarsource + owner_html_url: https://github.com/polarsource - name: strawberry html_url: https://github.com/strawberry-graphql/strawberry - stars: 4084 + stars: 4126 owner_login: strawberry-graphql owner_html_url: https://github.com/strawberry-graphql +- name: atrilabs-engine + html_url: https://github.com/Atri-Labs/atrilabs-engine + stars: 4114 + owner_login: Atri-Labs + owner_html_url: https://github.com/Atri-Labs - name: dynaconf html_url: https://github.com/dynaconf/dynaconf - stars: 3844 + stars: 3874 owner_login: dynaconf owner_html_url: https://github.com/dynaconf - name: poem html_url: https://github.com/poem-web/poem - stars: 3698 + stars: 3746 owner_login: poem-web owner_html_url: https://github.com/poem-web -- name: polar - html_url: https://github.com/polarsource/polar - stars: 3355 - owner_login: polarsource - owner_html_url: https://github.com/polarsource - name: opyrator html_url: https://github.com/ml-tooling/opyrator - stars: 3114 + stars: 3117 owner_login: ml-tooling owner_html_url: https://github.com/ml-tooling - name: farfalle html_url: https://github.com/rashadphz/farfalle - stars: 3022 + stars: 3094 owner_login: rashadphz owner_html_url: https://github.com/rashadphz - name: fastapi-admin html_url: https://github.com/fastapi-admin/fastapi-admin - stars: 3002 + stars: 3040 owner_login: fastapi-admin owner_html_url: https://github.com/fastapi-admin - name: docarray html_url: https://github.com/docarray/docarray - stars: 2998 + stars: 3007 owner_login: docarray owner_html_url: https://github.com/docarray - name: datamodel-code-generator html_url: https://github.com/koxudaxi/datamodel-code-generator - stars: 2845 + stars: 2914 owner_login: koxudaxi owner_html_url: https://github.com/koxudaxi - name: fastapi-realworld-example-app html_url: https://github.com/nsidnev/fastapi-realworld-example-app - stars: 2832 + stars: 2840 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: 2727 - owner_login: tiangolo - owner_html_url: https://github.com/tiangolo -- name: WrenAI - html_url: https://github.com/Canner/WrenAI - stars: 2699 - owner_login: Canner - owner_html_url: https://github.com/Canner - name: LitServe html_url: https://github.com/Lightning-AI/LitServe - stars: 2664 + stars: 2804 owner_login: Lightning-AI owner_html_url: https://github.com/Lightning-AI +- name: uvicorn-gunicorn-fastapi-docker + html_url: https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker + stars: 2730 + owner_login: tiangolo + owner_html_url: https://github.com/tiangolo - name: logfire html_url: https://github.com/pydantic/logfire - stars: 2495 + stars: 2620 owner_login: pydantic owner_html_url: https://github.com/pydantic - name: huma html_url: https://github.com/danielgtaylor/huma - stars: 2479 + stars: 2567 owner_login: danielgtaylor owner_html_url: https://github.com/danielgtaylor - name: tracecat html_url: https://github.com/TracecatHQ/tracecat - stars: 2446 + stars: 2494 owner_login: TracecatHQ owner_html_url: https://github.com/TracecatHQ -- name: RasaGPT - html_url: https://github.com/paulpierre/RasaGPT - stars: 2378 - owner_login: paulpierre - owner_html_url: https://github.com/paulpierre - name: best-of-web-python html_url: https://github.com/ml-tooling/best-of-web-python - stars: 2374 + stars: 2433 owner_login: ml-tooling owner_html_url: https://github.com/ml-tooling +- name: RasaGPT + html_url: https://github.com/paulpierre/RasaGPT + stars: 2386 + owner_login: paulpierre + owner_html_url: https://github.com/paulpierre - name: fastapi-react html_url: https://github.com/Buuntu/fastapi-react - stars: 2274 + stars: 2293 owner_login: Buuntu owner_html_url: https://github.com/Buuntu - name: nextpy html_url: https://github.com/dot-agent/nextpy - stars: 2244 + stars: 2256 owner_login: dot-agent owner_html_url: https://github.com/dot-agent - name: 30-Days-of-Python html_url: https://github.com/codingforentrepreneurs/30-Days-of-Python - stars: 2154 + stars: 2155 owner_login: codingforentrepreneurs owner_html_url: https://github.com/codingforentrepreneurs - name: FastAPI-template html_url: https://github.com/s3rius/FastAPI-template - stars: 2067 + stars: 2121 owner_login: s3rius owner_html_url: https://github.com/s3rius -- name: langserve - html_url: https://github.com/langchain-ai/langserve - stars: 1980 - owner_login: langchain-ai - owner_html_url: https://github.com/langchain-ai - name: sqladmin html_url: https://github.com/aminalaee/sqladmin - stars: 1980 + stars: 2021 owner_login: aminalaee owner_html_url: https://github.com/aminalaee +- name: langserve + html_url: https://github.com/langchain-ai/langserve + stars: 2006 + owner_login: langchain-ai + owner_html_url: https://github.com/langchain-ai - name: fastapi-utils html_url: https://github.com/fastapiutils/fastapi-utils - stars: 1970 + stars: 2002 owner_login: fastapiutils owner_html_url: https://github.com/fastapiutils - name: solara html_url: https://github.com/widgetti/solara - stars: 1950 + stars: 1967 owner_login: widgetti owner_html_url: https://github.com/widgetti -- name: python-week-2022 - html_url: https://github.com/rochacbruno/python-week-2022 - stars: 1836 - owner_login: rochacbruno - owner_html_url: https://github.com/rochacbruno - name: supabase-py html_url: https://github.com/supabase/supabase-py - stars: 1803 + stars: 1848 owner_login: supabase owner_html_url: https://github.com/supabase +- name: python-week-2022 + html_url: https://github.com/rochacbruno/python-week-2022 + stars: 1832 + owner_login: rochacbruno + owner_html_url: https://github.com/rochacbruno - name: mangum html_url: https://github.com/Kludex/mangum - stars: 1760 + stars: 1789 owner_login: Kludex owner_html_url: https://github.com/Kludex - name: manage-fastapi html_url: https://github.com/ycd/manage-fastapi - stars: 1704 + stars: 1711 owner_login: ycd owner_html_url: https://github.com/ycd - name: ormar html_url: https://github.com/collerek/ormar - stars: 1688 + stars: 1701 owner_login: collerek owner_html_url: https://github.com/collerek - name: agentkit html_url: https://github.com/BCG-X-Official/agentkit - stars: 1615 + stars: 1630 owner_login: BCG-X-Official owner_html_url: https://github.com/BCG-X-Official - name: langchain-serve html_url: https://github.com/jina-ai/langchain-serve - stars: 1615 + stars: 1617 owner_login: jina-ai owner_html_url: https://github.com/jina-ai - name: termpair html_url: https://github.com/cs01/termpair - stars: 1613 + stars: 1612 owner_login: cs01 owner_html_url: https://github.com/cs01 - name: coronavirus-tracker-api html_url: https://github.com/ExpDev07/coronavirus-tracker-api - stars: 1591 + stars: 1590 owner_login: ExpDev07 owner_html_url: https://github.com/ExpDev07 - name: piccolo html_url: https://github.com/piccolo-orm/piccolo - stars: 1477 + stars: 1519 owner_login: piccolo-orm owner_html_url: https://github.com/piccolo-orm - name: fastapi-crudrouter html_url: https://github.com/awtkns/fastapi-crudrouter - stars: 1435 + stars: 1449 owner_login: awtkns owner_html_url: https://github.com/awtkns - name: fastapi-cache html_url: https://github.com/long2ice/fastapi-cache - stars: 1412 + stars: 1447 owner_login: long2ice owner_html_url: https://github.com/long2ice - name: openapi-python-client html_url: https://github.com/openapi-generators/openapi-python-client - stars: 1398 + stars: 1434 owner_login: openapi-generators owner_html_url: https://github.com/openapi-generators - name: awesome-fastapi-projects html_url: https://github.com/Kludex/awesome-fastapi-projects - stars: 1386 + stars: 1398 owner_login: Kludex owner_html_url: https://github.com/Kludex - name: awesome-python-resources html_url: https://github.com/DjangoEx/awesome-python-resources - stars: 1371 + stars: 1380 owner_login: DjangoEx owner_html_url: https://github.com/DjangoEx - name: budgetml html_url: https://github.com/ebhy/budgetml - stars: 1342 + stars: 1344 owner_login: ebhy owner_html_url: https://github.com/ebhy - name: slowapi html_url: https://github.com/laurentS/slowapi - stars: 1289 + stars: 1339 owner_login: laurentS owner_html_url: https://github.com/laurentS - name: fastapi-pagination html_url: https://github.com/uriyyo/fastapi-pagination - stars: 1240 + stars: 1263 owner_login: uriyyo owner_html_url: https://github.com/uriyyo - name: fastapi-boilerplate html_url: https://github.com/teamhide/fastapi-boilerplate - stars: 1173 + stars: 1206 owner_login: teamhide owner_html_url: https://github.com/teamhide - name: fastapi-tutorial html_url: https://github.com/liaogx/fastapi-tutorial - stars: 1162 + stars: 1178 owner_login: liaogx owner_html_url: https://github.com/liaogx - name: fastapi-amis-admin html_url: https://github.com/amisadmin/fastapi-amis-admin - stars: 1118 + stars: 1142 owner_login: amisadmin owner_html_url: https://github.com/amisadmin - name: fastapi-code-generator html_url: https://github.com/koxudaxi/fastapi-code-generator - stars: 1095 + stars: 1119 owner_login: koxudaxi owner_html_url: https://github.com/koxudaxi - name: bolt-python html_url: https://github.com/slackapi/bolt-python - stars: 1086 + stars: 1116 owner_login: slackapi owner_html_url: https://github.com/slackapi - name: odmantic html_url: https://github.com/art049/odmantic - stars: 1085 + stars: 1096 owner_login: art049 owner_html_url: https://github.com/art049 - name: langchain-extract html_url: https://github.com/langchain-ai/langchain-extract - stars: 1068 + stars: 1093 owner_login: langchain-ai owner_html_url: https://github.com/langchain-ai - name: fastapi_production_template html_url: https://github.com/zhanymkanov/fastapi_production_template - stars: 1059 + stars: 1078 owner_login: zhanymkanov owner_html_url: https://github.com/zhanymkanov - name: fastapi-alembic-sqlmodel-async html_url: https://github.com/jonra1993/fastapi-alembic-sqlmodel-async - stars: 1031 + stars: 1055 owner_login: jonra1993 owner_html_url: https://github.com/jonra1993 +- name: Kokoro-FastAPI + html_url: https://github.com/remsky/Kokoro-FastAPI + stars: 1047 + owner_login: remsky + owner_html_url: https://github.com/remsky - name: prometheus-fastapi-instrumentator html_url: https://github.com/trallnag/prometheus-fastapi-instrumentator - stars: 1013 + stars: 1036 owner_login: trallnag owner_html_url: https://github.com/trallnag +- name: SurfSense + html_url: https://github.com/MODSetter/SurfSense + stars: 1018 + owner_login: MODSetter + owner_html_url: https://github.com/MODSetter +- name: bedrock-claude-chat + html_url: https://github.com/aws-samples/bedrock-claude-chat + stars: 1010 + owner_login: aws-samples + owner_html_url: https://github.com/aws-samples - name: runhouse html_url: https://github.com/run-house/runhouse - stars: 988 + stars: 1000 owner_login: run-house owner_html_url: https://github.com/run-house - name: lanarky html_url: https://github.com/ajndkr/lanarky - stars: 982 + stars: 986 owner_login: ajndkr owner_html_url: https://github.com/ajndkr - name: autollm html_url: https://github.com/viddexa/autollm - stars: 981 + stars: 982 owner_login: viddexa owner_html_url: https://github.com/viddexa -- name: bedrock-claude-chat - html_url: https://github.com/aws-samples/bedrock-claude-chat - stars: 977 - owner_login: aws-samples - owner_html_url: https://github.com/aws-samples -- name: SurfSense - html_url: https://github.com/MODSetter/SurfSense - stars: 971 - owner_login: MODSetter - owner_html_url: https://github.com/MODSetter - name: restish html_url: https://github.com/danielgtaylor/restish - stars: 954 + stars: 970 owner_login: danielgtaylor owner_html_url: https://github.com/danielgtaylor +- name: fastcrud + html_url: https://github.com/igorbenav/fastcrud + stars: 929 + owner_login: igorbenav + owner_html_url: https://github.com/igorbenav - name: secure html_url: https://github.com/TypeError/secure - stars: 911 + stars: 921 owner_login: TypeError owner_html_url: https://github.com/TypeError - name: langcorn html_url: https://github.com/msoedov/langcorn - stars: 909 + stars: 915 owner_login: msoedov owner_html_url: https://github.com/msoedov -- name: energy-forecasting - html_url: https://github.com/iusztinpaul/energy-forecasting - stars: 884 - owner_login: iusztinpaul - owner_html_url: https://github.com/iusztinpaul - name: vue-fastapi-admin html_url: https://github.com/mizhexiaoxiao/vue-fastapi-admin - stars: 863 + stars: 915 owner_login: mizhexiaoxiao owner_html_url: https://github.com/mizhexiaoxiao +- name: energy-forecasting + html_url: https://github.com/iusztinpaul/energy-forecasting + stars: 891 + owner_login: iusztinpaul + owner_html_url: https://github.com/iusztinpaul - name: authx html_url: https://github.com/yezz123/authx - stars: 850 + stars: 862 owner_login: yezz123 owner_html_url: https://github.com/yezz123 - name: titiler html_url: https://github.com/developmentseed/titiler - stars: 809 + stars: 823 owner_login: developmentseed owner_html_url: https://github.com/developmentseed - name: marker-api html_url: https://github.com/adithya-s-k/marker-api - stars: 792 + stars: 798 owner_login: adithya-s-k owner_html_url: https://github.com/adithya-s-k +- name: FastAPI-boilerplate + html_url: https://github.com/igorbenav/FastAPI-boilerplate + stars: 774 + owner_login: igorbenav + owner_html_url: https://github.com/igorbenav - name: fastapi_best_architecture html_url: https://github.com/fastapi-practices/fastapi_best_architecture - stars: 742 + stars: 766 owner_login: fastapi-practices owner_html_url: https://github.com/fastapi-practices - name: fastapi-mail html_url: https://github.com/sabuhish/fastapi-mail - stars: 728 + stars: 735 owner_login: sabuhish owner_html_url: https://github.com/sabuhish -- name: fastcrud - html_url: https://github.com/igorbenav/fastcrud - stars: 727 - owner_login: igorbenav - owner_html_url: https://github.com/igorbenav - name: annotated-py-projects html_url: https://github.com/hhstore/annotated-py-projects - stars: 722 + stars: 725 owner_login: hhstore owner_html_url: https://github.com/hhstore -- name: FastAPI-boilerplate - html_url: https://github.com/igorbenav/FastAPI-boilerplate - stars: 716 - owner_login: igorbenav - owner_html_url: https://github.com/igorbenav +- name: fastapi-do-zero + html_url: https://github.com/dunossauro/fastapi-do-zero + stars: 723 + owner_login: dunossauro + owner_html_url: https://github.com/dunossauro - name: lccn_predictor html_url: https://github.com/baoliay2008/lccn_predictor - stars: 707 + stars: 718 owner_login: baoliay2008 owner_html_url: https://github.com/baoliay2008 +- name: fastapi-observability + html_url: https://github.com/blueswen/fastapi-observability + stars: 718 + owner_login: blueswen + owner_html_url: https://github.com/blueswen - name: chatGPT-web html_url: https://github.com/mic1on/chatGPT-web - stars: 706 + stars: 708 owner_login: mic1on owner_html_url: https://github.com/mic1on -- name: fastapi-do-zero - html_url: https://github.com/dunossauro/fastapi-do-zero - stars: 702 - owner_login: dunossauro - owner_html_url: https://github.com/dunossauro +- name: learn-generative-ai + html_url: https://github.com/panaverse/learn-generative-ai + stars: 701 + owner_login: panaverse + owner_html_url: https://github.com/panaverse - name: linbing html_url: https://github.com/taomujian/linbing - stars: 699 + stars: 700 owner_login: taomujian owner_html_url: https://github.com/taomujian -- name: fastapi-observability - html_url: https://github.com/blueswen/fastapi-observability - stars: 698 - owner_login: blueswen - owner_html_url: https://github.com/blueswen - name: FastAPI-Backend-Template html_url: https://github.com/Aeternalis-Ingenium/FastAPI-Backend-Template - stars: 682 + stars: 692 owner_login: Aeternalis-Ingenium owner_html_url: https://github.com/Aeternalis-Ingenium -- name: learn-generative-ai - html_url: https://github.com/panaverse/learn-generative-ai - stars: 673 - owner_login: panaverse - owner_html_url: https://github.com/panaverse +- name: starlette-admin + html_url: https://github.com/jowilf/starlette-admin + stars: 692 + owner_login: jowilf + owner_html_url: https://github.com/jowilf - name: fastapi-jwt-auth html_url: https://github.com/IndominusByte/fastapi-jwt-auth - stars: 668 + stars: 674 owner_login: IndominusByte owner_html_url: https://github.com/IndominusByte - name: pity html_url: https://github.com/wuranxu/pity - stars: 660 + stars: 663 owner_login: wuranxu owner_html_url: https://github.com/wuranxu -- name: starlette-admin - html_url: https://github.com/jowilf/starlette-admin - stars: 653 - owner_login: jowilf - owner_html_url: https://github.com/jowilf - name: fastapi_login html_url: https://github.com/MushroomMaula/fastapi_login - stars: 650 + stars: 656 owner_login: MushroomMaula owner_html_url: https://github.com/MushroomMaula From b6b031b456d825fd8378d280d5d7d7c167784bb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 7 Feb 2025 22:10:51 +0000 Subject: [PATCH 067/117] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20Peopl?= =?UTF-8?q?e=20-=20Experts=20(#13303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions Co-authored-by: Alejandra <90076947+alejsdev@users.noreply.github.com> --- docs/en/data/people.yml | 130 +++++++++++++++++++++------------------- 1 file changed, 67 insertions(+), 63 deletions(-) diff --git a/docs/en/data/people.yml b/docs/en/data/people.yml index 112567778..7f910ab34 100644 --- a/docs/en/data/people.yml +++ b/docs/en/data/people.yml @@ -13,7 +13,7 @@ experts: avatarUrl: https://avatars.githubusercontent.com/in/15368?v=4 url: https://github.com/apps/github-actions - login: Kludex - count: 644 + count: 645 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4 url: https://github.com/Kludex - login: jgould22 @@ -116,6 +116,10 @@ experts: count: 39 avatarUrl: https://avatars.githubusercontent.com/u/30960668?v=4 url: https://github.com/sinisaos +- login: luzzodev + count: 37 + avatarUrl: https://avatars.githubusercontent.com/u/27291415?v=4 + url: https://github.com/luzzodev - login: chbndrhnns count: 37 avatarUrl: https://avatars.githubusercontent.com/u/7534547?v=4 @@ -124,10 +128,6 @@ experts: count: 37 avatarUrl: https://avatars.githubusercontent.com/u/5167622?u=de8f597c81d6336fcebc37b32dfd61a3f877160c&v=4 url: https://github.com/STeveShary -- login: luzzodev - count: 36 - avatarUrl: https://avatars.githubusercontent.com/u/27291415?v=4 - url: https://github.com/luzzodev - login: krishnardt count: 35 avatarUrl: https://avatars.githubusercontent.com/u/31960541?u=47f4829c77f4962ab437ffb7995951e41eeebe9b&v=4 @@ -188,6 +188,10 @@ experts: count: 19 avatarUrl: https://avatars.githubusercontent.com/u/10840422?u=45f015f95e1c0f06df602be4ab688d4b854cc8a8&v=4 url: https://github.com/estebanx64 +- login: sehraramiz + count: 18 + avatarUrl: https://avatars.githubusercontent.com/u/14166324?u=8fac65e84dfff24245d304a5b5b09f7b5bd69dc9&v=4 + url: https://github.com/sehraramiz - login: zoliknemet count: 18 avatarUrl: https://avatars.githubusercontent.com/u/22326718?u=31ba446ac290e23e56eea8e4f0c558aaf0b40779&v=4 @@ -196,10 +200,6 @@ experts: count: 18 avatarUrl: https://avatars.githubusercontent.com/u/24581770?v=4 url: https://github.com/retnikt -- login: sehraramiz - count: 17 - avatarUrl: https://avatars.githubusercontent.com/u/14166324?u=8fac65e84dfff24245d304a5b5b09f7b5bd69dc9&v=4 - url: https://github.com/sehraramiz - login: caeser1996 count: 17 avatarUrl: https://avatars.githubusercontent.com/u/16540232?u=05d2beb8e034d584d0a374b99d8826327bd7f614&v=4 @@ -224,6 +224,10 @@ experts: count: 16 avatarUrl: https://avatars.githubusercontent.com/u/41964673?u=9f2174f9d61c15c6e3a4c9e3aeee66f711ce311f&v=4 url: https://github.com/dstlny +- login: ceb10n + count: 15 + avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 + url: https://github.com/ceb10n - login: jorgerpo count: 15 avatarUrl: https://avatars.githubusercontent.com/u/12537771?u=7444d20019198e34911082780cc7ad73f2b97cb3&v=4 @@ -240,13 +244,9 @@ experts: count: 15 avatarUrl: https://avatars.githubusercontent.com/u/25699289?u=b5d219277b4d001ac26fb8be357fddd88c29d51b&v=4 url: https://github.com/abhint -- login: pythonweb2 - count: 14 - avatarUrl: https://avatars.githubusercontent.com/u/32141163?v=4 - url: https://github.com/pythonweb2 last_month_experts: - login: Kludex - count: 15 + count: 14 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4 url: https://github.com/Kludex - login: YuriiMotov @@ -254,11 +254,11 @@ last_month_experts: avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=e83a39697a2d33ab2ec9bfbced794ee48bc29cec&v=4 url: https://github.com/YuriiMotov - login: sehraramiz - count: 8 + count: 7 avatarUrl: https://avatars.githubusercontent.com/u/14166324?u=8fac65e84dfff24245d304a5b5b09f7b5bd69dc9&v=4 url: https://github.com/sehraramiz - login: luzzodev - count: 4 + count: 5 avatarUrl: https://avatars.githubusercontent.com/u/27291415?v=4 url: https://github.com/luzzodev - login: yokwejuste @@ -269,6 +269,10 @@ last_month_experts: count: 3 avatarUrl: https://avatars.githubusercontent.com/u/31544722?v=4 url: https://github.com/alv2017 +- login: Trinkes + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/9466879?v=4 + url: https://github.com/Trinkes - login: PREPONDERANCE count: 2 avatarUrl: https://avatars.githubusercontent.com/u/112809059?u=30ab12dc9ddba2f94ab90e6ad4ad8bc5cfa7fccd&v=4 @@ -287,19 +291,19 @@ last_month_experts: url: https://github.com/iloveitaly three_months_experts: - login: luzzodev - count: 34 + count: 33 avatarUrl: https://avatars.githubusercontent.com/u/27291415?v=4 url: https://github.com/luzzodev - login: YuriiMotov - count: 33 + count: 31 avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=e83a39697a2d33ab2ec9bfbced794ee48bc29cec&v=4 url: https://github.com/YuriiMotov - login: Kludex - count: 23 + count: 24 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4 url: https://github.com/Kludex - login: sehraramiz - count: 10 + count: 11 avatarUrl: https://avatars.githubusercontent.com/u/14166324?u=8fac65e84dfff24245d304a5b5b09f7b5bd69dc9&v=4 url: https://github.com/sehraramiz - login: estebanx64 @@ -326,6 +330,10 @@ three_months_experts: count: 3 avatarUrl: https://avatars.githubusercontent.com/u/108818737?u=3d7ffe5808843ee4372f9cc5a559ff1674cf1792&v=4 url: https://github.com/viniciusCalcantara +- login: Trinkes + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/9466879?v=4 + url: https://github.com/Trinkes - login: PREPONDERANCE count: 2 avatarUrl: https://avatars.githubusercontent.com/u/112809059?u=30ab12dc9ddba2f94ab90e6ad4ad8bc5cfa7fccd&v=4 @@ -358,10 +366,6 @@ three_months_experts: count: 2 avatarUrl: https://avatars.githubusercontent.com/u/40732698?u=611f39d3c1d2f4207a590937a78c1f10eed6232c&v=4 url: https://github.com/gelezo43 -- login: dbfreem - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/9778569?u=f2f1e9135b5e4f1b0c6821a548b17f97572720fc&v=4 - url: https://github.com/dbfreem - login: AliYmn count: 2 avatarUrl: https://avatars.githubusercontent.com/u/18416653?u=98c1fca46c7e4dabe8c39d17b5e55d1511d41cf9&v=4 @@ -388,47 +392,47 @@ six_months_experts: avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=e83a39697a2d33ab2ec9bfbced794ee48bc29cec&v=4 url: https://github.com/YuriiMotov - login: Kludex - count: 39 + count: 40 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4 url: https://github.com/Kludex +- login: luzzodev + count: 37 + avatarUrl: https://avatars.githubusercontent.com/u/27291415?v=4 + url: https://github.com/luzzodev - login: sinisaos count: 37 avatarUrl: https://avatars.githubusercontent.com/u/30960668?v=4 url: https://github.com/sinisaos -- login: luzzodev - count: 36 - avatarUrl: https://avatars.githubusercontent.com/u/27291415?v=4 - url: https://github.com/luzzodev - login: JavierSanchezCastro count: 16 avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 url: https://github.com/JavierSanchezCastro -- login: tiangolo - count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 - url: https://github.com/tiangolo - login: Kfir-G count: 13 avatarUrl: https://avatars.githubusercontent.com/u/57500876?u=0cd29db046a17f12f382d398141319fca7ff230a&v=4 url: https://github.com/Kfir-G +- login: tiangolo + count: 12 + avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 + url: https://github.com/tiangolo - login: sehraramiz - count: 10 + count: 11 avatarUrl: https://avatars.githubusercontent.com/u/14166324?u=8fac65e84dfff24245d304a5b5b09f7b5bd69dc9&v=4 url: https://github.com/sehraramiz -- login: estebanx64 - count: 10 - avatarUrl: https://avatars.githubusercontent.com/u/10840422?u=45f015f95e1c0f06df602be4ab688d4b854cc8a8&v=4 - url: https://github.com/estebanx64 - login: ceb10n - count: 9 + count: 10 avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 url: https://github.com/ceb10n +- login: estebanx64 + count: 7 + avatarUrl: https://avatars.githubusercontent.com/u/10840422?u=45f015f95e1c0f06df602be4ab688d4b854cc8a8&v=4 + url: https://github.com/estebanx64 - login: yvallois count: 6 avatarUrl: https://avatars.githubusercontent.com/u/36999744?v=4 url: https://github.com/yvallois - login: n8sty - count: 6 + count: 5 avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 url: https://github.com/n8sty - login: TomFaulkner @@ -483,6 +487,10 @@ six_months_experts: count: 3 avatarUrl: https://avatars.githubusercontent.com/u/8796347?u=556c97650c27021911b0b9447ec55e75987b0e8a&v=4 url: https://github.com/svlandeg +- login: Trinkes + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/9466879?v=4 + url: https://github.com/Trinkes - login: PREPONDERANCE count: 2 avatarUrl: https://avatars.githubusercontent.com/u/112809059?u=30ab12dc9ddba2f94ab90e6ad4ad8bc5cfa7fccd&v=4 @@ -619,17 +627,13 @@ six_months_experts: count: 2 avatarUrl: https://avatars.githubusercontent.com/u/3409962?u=723662989f2027755e67d200137c13c53ae154ac&v=4 url: https://github.com/mattmess1221 -- login: meower1 - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/109747197?u=0a5cc2a6ae74e558f0afc2874da85132e5953d8b&v=4 - url: https://github.com/meower1 one_year_experts: - login: YuriiMotov count: 223 avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=e83a39697a2d33ab2ec9bfbced794ee48bc29cec&v=4 url: https://github.com/YuriiMotov - login: Kludex - count: 83 + count: 81 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4 url: https://github.com/Kludex - login: JavierSanchezCastro @@ -645,7 +649,7 @@ one_year_experts: avatarUrl: https://avatars.githubusercontent.com/u/30960668?v=4 url: https://github.com/sinisaos - login: luzzodev - count: 36 + count: 37 avatarUrl: https://avatars.githubusercontent.com/u/27291415?v=4 url: https://github.com/luzzodev - login: tiangolo @@ -660,18 +664,18 @@ one_year_experts: count: 19 avatarUrl: https://avatars.githubusercontent.com/u/10840422?u=45f015f95e1c0f06df602be4ab688d4b854cc8a8&v=4 url: https://github.com/estebanx64 +- login: ceb10n + count: 15 + avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 + url: https://github.com/ceb10n - login: sehraramiz - count: 14 + count: 15 avatarUrl: https://avatars.githubusercontent.com/u/14166324?u=8fac65e84dfff24245d304a5b5b09f7b5bd69dc9&v=4 url: https://github.com/sehraramiz - login: PhysicallyActive count: 14 avatarUrl: https://avatars.githubusercontent.com/u/160476156?u=7a8e44f4a43d3bba636f795bb7d9476c9233b4d8&v=4 url: https://github.com/PhysicallyActive -- login: ceb10n - count: 14 - avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 - url: https://github.com/ceb10n - login: Kfir-G count: 13 avatarUrl: https://avatars.githubusercontent.com/u/57500876?u=0cd29db046a17f12f382d398141319fca7ff230a&v=4 @@ -812,6 +816,18 @@ one_year_experts: count: 3 avatarUrl: https://avatars.githubusercontent.com/u/104530599?u=d87b866e7c1db970d6f8e8031643818349b046d5&v=4 url: https://github.com/ahmedabdou14 +- login: Trinkes + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/9466879?v=4 + url: https://github.com/Trinkes +- login: Leon0824 + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/1922026?v=4 + url: https://github.com/Leon0824 +- login: CarlosOliveira-23 + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/102637302?u=cf350a4db956f30cbb2c27d3be0d15c282e32b14&v=4 + url: https://github.com/CarlosOliveira-23 - login: nbx3 count: 2 avatarUrl: https://avatars.githubusercontent.com/u/34649527?u=943812f69e0d40adbd3fa1c9b8ef50dd971a2a45&v=4 @@ -832,10 +848,6 @@ one_year_experts: count: 2 avatarUrl: https://avatars.githubusercontent.com/u/210173?v=4 url: https://github.com/slafs -- login: CarlosOliveira-23 - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/102637302?u=cf350a4db956f30cbb2c27d3be0d15c282e32b14&v=4 - url: https://github.com/CarlosOliveira-23 - login: monchin count: 2 avatarUrl: https://avatars.githubusercontent.com/u/18521800?v=4 @@ -844,10 +856,6 @@ one_year_experts: count: 2 avatarUrl: https://avatars.githubusercontent.com/u/38752106?u=07f80e451bda00a9492bbc764e49d24ad3ada8cc&v=4 url: https://github.com/AmirHmZz -- login: Leon0824 - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/1922026?v=4 - url: https://github.com/Leon0824 - login: iloveitaly count: 2 avatarUrl: https://avatars.githubusercontent.com/u/150855?v=4 @@ -860,7 +868,3 @@ one_year_experts: count: 2 avatarUrl: https://avatars.githubusercontent.com/u/11828278?u=6bcadc5ce4f2f56a514331c9f68eb987d4afe29a&v=4 url: https://github.com/shurshilov -- login: LincolnPuzey - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/18750802?v=4 - url: https://github.com/LincolnPuzey From 27d0ccc11cd3621890d24cac0d9236cb4b040081 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 7 Feb 2025 22:11:28 +0000 Subject: [PATCH 068/117] =?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 75a024a0b..ac7111cf2 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Translations +* 🌐 Add Vietnamese translation for `docs/vi/docs/fastapi-cli.md`. PR [#13294](https://github.com/fastapi/fastapi/pull/13294) by [@ptt3199](https://github.com/ptt3199). * 🌐 Add Ukrainian translation for `docs/uk/docs/features.md`. PR [#13308](https://github.com/fastapi/fastapi/pull/13308) by [@valentinDruzhinin](https://github.com/valentinDruzhinin). * 🌐 Add Ukrainian translation for `docs/uk/docs/learn/index.md`. PR [#13306](https://github.com/fastapi/fastapi/pull/13306) by [@valentinDruzhinin](https://github.com/valentinDruzhinin). * 🌐 Update Portuguese Translation for `docs/pt/docs/deployment/https.md`. PR [#13317](https://github.com/fastapi/fastapi/pull/13317) by [@Joao-Pedro-P-Holanda](https://github.com/Joao-Pedro-P-Holanda). From 25ee2357d767eaaf46f334c4aecb2fd008703cdc Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 7 Feb 2025 22:12:00 +0000 Subject: [PATCH 069/117] =?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 ac7111cf2..7d063b168 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -18,6 +18,7 @@ hide: ### Internal +* 👥 Update FastAPI People - Contributors and Translators. PR [#13293](https://github.com/fastapi/fastapi/pull/13293) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump inline-snapshot from 0.18.1 to 0.19.3. PR [#13298](https://github.com/fastapi/fastapi/pull/13298) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔧 Update sponsors, add Permit. PR [#13288](https://github.com/fastapi/fastapi/pull/13288) by [@tiangolo](https://github.com/tiangolo). From 6e8da9d00a6df82edb1c0179d2c14f07818b378b Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 7 Feb 2025 22:13:23 +0000 Subject: [PATCH 070/117] =?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 7d063b168..485454f6a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -18,6 +18,7 @@ hide: ### Internal +* 👥 Update FastAPI GitHub topic repositories. PR [#13302](https://github.com/fastapi/fastapi/pull/13302) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People - Contributors and Translators. PR [#13293](https://github.com/fastapi/fastapi/pull/13293) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump inline-snapshot from 0.18.1 to 0.19.3. PR [#13298](https://github.com/fastapi/fastapi/pull/13298) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔧 Update sponsors, add Permit. PR [#13288](https://github.com/fastapi/fastapi/pull/13288) by [@tiangolo](https://github.com/tiangolo). From 640a5b6fc3204e953ae82bb99c66d7c0995c5e44 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 7 Feb 2025 22:14:11 +0000 Subject: [PATCH 071/117] =?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 485454f6a..18ef5911c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -18,6 +18,7 @@ hide: ### Internal +* 👥 Update FastAPI People - Experts. PR [#13303](https://github.com/fastapi/fastapi/pull/13303) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI GitHub topic repositories. PR [#13302](https://github.com/fastapi/fastapi/pull/13302) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People - Contributors and Translators. PR [#13293](https://github.com/fastapi/fastapi/pull/13293) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump inline-snapshot from 0.18.1 to 0.19.3. PR [#13298](https://github.com/fastapi/fastapi/pull/13298) by [@dependabot[bot]](https://github.com/apps/dependabot). From e814707cd1daebcdd765e80a35316f8316496b30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 7 Feb 2025 22:15:49 +0000 Subject: [PATCH 072/117] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20Peopl?= =?UTF-8?q?e=20-=20Sponsors=20(#13295)?= 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 | 63 ++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/docs/en/data/github_sponsors.yml b/docs/en/data/github_sponsors.yml index 55fe3dda9..feb4e727f 100644 --- a/docs/en/data/github_sponsors.yml +++ b/docs/en/data/github_sponsors.yml @@ -2,6 +2,9 @@ sponsors: - - login: bump-sh avatarUrl: https://avatars.githubusercontent.com/u/33217836?v=4 url: https://github.com/bump-sh + - login: renderinc + avatarUrl: https://avatars.githubusercontent.com/u/36424661?v=4 + url: https://github.com/renderinc - login: Nixtla avatarUrl: https://avatars.githubusercontent.com/u/79945230?v=4 url: https://github.com/Nixtla @@ -20,9 +23,6 @@ sponsors: - login: zuplo avatarUrl: https://avatars.githubusercontent.com/u/85497839?v=4 url: https://github.com/zuplo - - login: render-sponsorships - avatarUrl: https://avatars.githubusercontent.com/u/189296666?v=4 - url: https://github.com/render-sponsorships - login: porter-dev avatarUrl: https://avatars.githubusercontent.com/u/62078005?v=4 url: https://github.com/porter-dev @@ -44,6 +44,9 @@ sponsors: - login: databento avatarUrl: https://avatars.githubusercontent.com/u/64141749?v=4 url: https://github.com/databento + - login: permitio + avatarUrl: https://avatars.githubusercontent.com/u/71775833?v=4 + url: https://github.com/permitio - - login: mercedes-benz avatarUrl: https://avatars.githubusercontent.com/u/34240465?v=4 url: https://github.com/mercedes-benz @@ -95,9 +98,6 @@ sponsors: - - 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=cbf098fc04c0473523d373b0dd2145b4ec99ef93&v=4 - url: https://github.com/vincentkoc - login: ProteinQure avatarUrl: https://avatars.githubusercontent.com/u/33707203?v=4 url: https://github.com/ProteinQure @@ -107,6 +107,9 @@ sponsors: - login: otosky avatarUrl: https://avatars.githubusercontent.com/u/42260747?u=69d089387c743d89427aa4ad8740cfb34045a9e0&v=4 url: https://github.com/otosky + - login: khadrawy + avatarUrl: https://avatars.githubusercontent.com/u/13686061?u=59f25ef42ecf04c22657aac4238ce0e2d3d30304&v=4 + url: https://github.com/khadrawy - login: mjohnsey avatarUrl: https://avatars.githubusercontent.com/u/16784016?u=38fad2e6b411244560b3af99c5f5a4751bc81865&v=4 url: https://github.com/mjohnsey @@ -215,6 +218,9 @@ sponsors: - login: anomaly avatarUrl: https://avatars.githubusercontent.com/u/3654837?v=4 url: https://github.com/anomaly + - login: vincentkoc + avatarUrl: https://avatars.githubusercontent.com/u/25068?u=fbd5b2d51142daa4bdbc21e21953a3b8b8188a4a&v=4 + url: https://github.com/vincentkoc - login: jstanden avatarUrl: https://avatars.githubusercontent.com/u/63288?u=c3658d57d2862c607a0e19c2101c3c51876e36ad&v=4 url: https://github.com/jstanden @@ -248,6 +254,9 @@ sponsors: - login: TrevorBenson avatarUrl: https://avatars.githubusercontent.com/u/9167887?u=dccbea3327a57750923333d8ebf1a0b3f1948949&v=4 url: https://github.com/TrevorBenson + - login: kaangiray26 + avatarUrl: https://avatars.githubusercontent.com/u/11297495?u=e85327a77db45906d44f3ff06dd7f3303c644096&v=4 + url: https://github.com/kaangiray26 - login: wdwinslow avatarUrl: https://avatars.githubusercontent.com/u/11562137?u=dc01daafb354135603a263729e3d26d939c0c452&v=4 url: https://github.com/wdwinslow @@ -263,9 +272,9 @@ sponsors: - login: dannywade avatarUrl: https://avatars.githubusercontent.com/u/13680237?u=418ee985bd41577b20fde81417fb2d901e875e8a&v=4 url: https://github.com/dannywade - - login: khadrawy - avatarUrl: https://avatars.githubusercontent.com/u/13686061?u=59f25ef42ecf04c22657aac4238ce0e2d3d30304&v=4 - url: https://github.com/khadrawy + - login: gorhack + avatarUrl: https://avatars.githubusercontent.com/u/4141690?u=ec119ebc4bdf00a7bc84657a71aa17834f4f27f3&v=4 + url: https://github.com/gorhack - login: Ryandaydev avatarUrl: https://avatars.githubusercontent.com/u/4292423?u=48f68868db8886fce31a1d802c1003914c6cd7c6&v=4 url: https://github.com/Ryandaydev @@ -314,9 +323,9 @@ sponsors: - login: mobyw avatarUrl: https://avatars.githubusercontent.com/u/44370805?v=4 url: https://github.com/mobyw - - login: ArtyomVancyan - avatarUrl: https://avatars.githubusercontent.com/u/44609997?v=4 - url: https://github.com/ArtyomVancyan + - login: PelicanQ + avatarUrl: https://avatars.githubusercontent.com/u/77930606?v=4 + url: https://github.com/PelicanQ - login: TheR1D avatarUrl: https://avatars.githubusercontent.com/u/16740832?u=b0dfdbdb27b79729430c71c6128962f77b7b53f7&v=4 url: https://github.com/TheR1D @@ -341,6 +350,9 @@ sponsors: - login: dvlpjrs avatarUrl: https://avatars.githubusercontent.com/u/32254642?u=fbd6ad0324d4f1eb6231cf775be1c7bd4404e961&v=4 url: https://github.com/dvlpjrs + - login: ArtyomVancyan + avatarUrl: https://avatars.githubusercontent.com/u/44609997?v=4 + url: https://github.com/ArtyomVancyan - login: caviri avatarUrl: https://avatars.githubusercontent.com/u/45425937?u=4e14bd64282bad8f385eafbdb004b5a279366d6e&v=4 url: https://github.com/caviri @@ -356,9 +368,6 @@ sponsors: - 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: tochikuji avatarUrl: https://avatars.githubusercontent.com/u/851759?v=4 url: https://github.com/tochikuji @@ -380,9 +389,9 @@ sponsors: - login: Alisa-lisa avatarUrl: https://avatars.githubusercontent.com/u/4137964?u=e7e393504f554f4ff15863a1e01a5746863ef9ce&v=4 url: https://github.com/Alisa-lisa - - login: Graeme22 - avatarUrl: https://avatars.githubusercontent.com/u/4185684?u=498182a42300d7bcd4de1215190cb17eb501136c&v=4 - url: https://github.com/Graeme22 + - login: hcristea + avatarUrl: https://avatars.githubusercontent.com/u/7814406?u=61d7a4fcf846983a4606788eac25e1c6c1209ba8&v=4 + url: https://github.com/hcristea - login: ddanier avatarUrl: https://avatars.githubusercontent.com/u/113563?u=ed1dc79de72f93bd78581f88ebc6952b62f472da&v=4 url: https://github.com/ddanier @@ -434,6 +443,9 @@ sponsors: - login: artempronevskiy avatarUrl: https://avatars.githubusercontent.com/u/12235104?u=03df6e1e55c9c6fe5d230adabb8dd7d43d8bbe8f&v=4 url: https://github.com/artempronevskiy + - login: Graeme22 + avatarUrl: https://avatars.githubusercontent.com/u/4185684?u=498182a42300d7bcd4de1215190cb17eb501136c&v=4 + url: https://github.com/Graeme22 - login: danielunderwood avatarUrl: https://avatars.githubusercontent.com/u/4472301?v=4 url: https://github.com/danielunderwood @@ -458,9 +470,6 @@ sponsors: - login: harsh183 avatarUrl: https://avatars.githubusercontent.com/u/7780198?v=4 url: https://github.com/harsh183 - - login: hcristea - avatarUrl: https://avatars.githubusercontent.com/u/7814406?u=61d7a4fcf846983a4606788eac25e1c6c1209ba8&v=4 - url: https://github.com/hcristea - - login: larsyngvelundin avatarUrl: https://avatars.githubusercontent.com/u/34173819?u=74958599695bf83ac9f1addd935a51548a10c6b0&v=4 url: https://github.com/larsyngvelundin @@ -479,9 +488,15 @@ sponsors: - login: FabulousCodingFox avatarUrl: https://avatars.githubusercontent.com/u/78906517?u=924a27cbee3db7e0ece5cc1509921402e1445e74&v=4 url: https://github.com/FabulousCodingFox - - login: anqorithm - avatarUrl: https://avatars.githubusercontent.com/u/61029571?u=468256fa4e2d9ce2870b608299724bebb7a33f18&v=4 - url: https://github.com/anqorithm + - login: gateremark + avatarUrl: https://avatars.githubusercontent.com/u/91592218?u=969314eb2cfb035196f4d19499ec6f5050d7583a&v=4 + url: https://github.com/gateremark + - login: morzan1001 + avatarUrl: https://avatars.githubusercontent.com/u/47593005?u=c30ab7230f82a12a9b938dcb54f84a996931409a&v=4 + url: https://github.com/morzan1001 + - login: Toothwitch + avatarUrl: https://avatars.githubusercontent.com/u/1710406?u=5eebb23b46cd26e48643b9e5179536cad491c17a&v=4 + url: https://github.com/Toothwitch - login: ssbarnea avatarUrl: https://avatars.githubusercontent.com/u/102495?u=c7bd9ddf127785286fc939dd18cb02db0a453bce&v=4 url: https://github.com/ssbarnea From 38d409dd67513de0983814f7b5e1918c6ed1521e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C6=B0=C6=A1ng=20T=E1=BA=A5n=20Th=C3=A0nh?= <51350651+ptt3199@users.noreply.github.com> Date: Sat, 8 Feb 2025 05:17:13 +0700 Subject: [PATCH 073/117] =?UTF-8?q?=F0=9F=8C=90=20Add=20Vietnamese=20trans?= =?UTF-8?q?lation=20for=20`docs/vi/docs/environment-variables.md`=20(#1328?= =?UTF-8?q?7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/vi/docs/environment-variables.md | 300 ++++++++++++++++++++++++++ 1 file changed, 300 insertions(+) create mode 100644 docs/vi/docs/environment-variables.md diff --git a/docs/vi/docs/environment-variables.md b/docs/vi/docs/environment-variables.md new file mode 100644 index 000000000..dd06f8959 --- /dev/null +++ b/docs/vi/docs/environment-variables.md @@ -0,0 +1,300 @@ +# Biến môi trường (Environment Variables) + +/// tip + +Nếu bạn đã biết về "biến môi trường" và cách sử dụng chúng, bạn có thể bỏ qua phần này. + +/// + +Một biến môi trường (còn được gọi là "**env var**") là một biến mà tồn tại **bên ngoài** đoạn mã Python, ở trong **hệ điều hành**, và có thể được đọc bởi đoạn mã Python của bạn (hoặc bởi các chương trình khác). + +Các biến môi trường có thể được sử dụng để xử lí **các thiết lập** của ứng dụng, như một phần của **các quá trình cài đặt** Python, v.v. + +## Tạo và Sử dụng các Biến Môi Trường + +Bạn có thể **tạo** và sử dụng các biến môi trường trong **shell (terminal)**, mà không cần sử dụng Python: + +//// tab | Linux, macOS, Windows Bash + +
+ +```console +// Bạn có thể tạo một biến môi trường MY_NAME với +$ export MY_NAME="Wade Wilson" + +// Sau đó bạn có thể sử dụng nó với các chương trình khác, như +$ echo "Hello $MY_NAME" + +Hello Wade Wilson +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ +```console +// Tạo một biến môi trường MY_NAME +$ $Env:MY_NAME = "Wade Wilson" + +// Sử dụng nó với các chương trình khác, như là +$ echo "Hello $Env:MY_NAME" + +Hello Wade Wilson +``` + +
+ +//// + +## Đọc các Biến Môi Trường trong Python + +Bạn cũng có thể tạo các biến môi trường **bên ngoài** đoạn mã Python, trong terminal (hoặc bằng bất kỳ phương pháp nào khác), và sau đó **đọc chúng trong Python**. + +Ví dụ, bạn có một file `main.py` với: + +```Python hl_lines="3" +import os + +name = os.getenv("MY_NAME", "World") +print(f"Hello {name} from Python") +``` + +/// tip + +Tham số thứ hai cho `os.getenv()` là giá trị mặc định để trả về. + +Nếu không được cung cấp, nó mặc định là `None`, ở đây chúng ta cung cấp `"World"` là giá trị mặc định để sử dụng. + +/// + +Sau đó bạn có thể gọi chương trình Python: + +//// tab | Linux, macOS, Windows Bash + +
+ +```console +// Ở đây chúng ta chưa cài đặt biến môi trường +$ python main.py + +// Vì chúng ta chưa cài đặt biến môi trường, chúng ta nhận được giá trị mặc định + +Hello World from Python + +// Nhưng nếu chúng ta tạo một biến môi trường trước đó +$ export MY_NAME="Wade Wilson" + +// Và sau đó gọi chương trình lại +$ python main.py + +// Bây giờ nó có thể đọc biến môi trường + +Hello Wade Wilson from Python +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ +```console +// Ở đây chúng ta chưa cài đặt biến môi trường +$ python main.py + +// Vì chúng ta chưa cài đặt biến môi trường, chúng ta nhận được giá trị mặc định + +Hello World from Python + +// Nhưng nếu chúng ta tạo một biến môi trường trước đó +$ $Env:MY_NAME = "Wade Wilson" + +// Và sau đó gọi chương trình lại +$ python main.py + +// Bây giờ nó có thể đọc biến môi trường + +Hello Wade Wilson from Python +``` + +
+ +//// + +Vì các biến môi trường có thể được tạo bên ngoài đoạn mã Python, nhưng có thể được đọc bởi đoạn mã Python, và không cần được lưu trữ (commit vào `git`) cùng với các file khác, nên chúng thường được sử dụng để lưu các thiết lập hoặc **cấu hình**. + +Bạn cũng có thể tạo ra một biến môi trường dành riêng cho một **lần gọi chương trình**, chỉ có thể được sử dụng bởi chương trình đó, và chỉ trong thời gian chạy của chương trình. + +Để làm điều này, tạo nó ngay trước chương trình đó, trên cùng một dòng: + +
+ +```console +// Tạo một biến môi trường MY_NAME cho lần gọi chương trình này +$ MY_NAME="Wade Wilson" python main.py + +// Bây giờ nó có thể đọc biến môi trường + +Hello Wade Wilson from Python + +// Biến môi trường không còn tồn tại sau đó +$ python main.py + +Hello World from Python +``` + +
+ +/// tip + +Bạn có thể đọc thêm về điều này tại The Twelve-Factor App: Config. + +/// + +## Các Kiểu (Types) và Kiểm tra (Validation) + +Các biến môi trường có thể chỉ xử lí **chuỗi ký tự**, vì chúng nằm bên ngoài đoạn mã Python và phải tương thích với các chương trình khác và phần còn lại của hệ thống (và thậm chí với các hệ điều hành khác, như Linux, Windows, macOS). + +Điều này có nghĩa là **bất kỳ giá trị nào** được đọc trong Python từ một biến môi trường **sẽ là một `str`**, và bất kỳ hành động chuyển đổi sang kiểu dữ liệu khác hoặc hành động kiểm tra nào cũng phải được thực hiện trong đoạn mã. + +Bạn sẽ học thêm về việc sử dụng biến môi trường để xử lí **các thiết lập ứng dụng** trong [Hướng dẫn nâng cao - Các thiết lập và biến môi trường](./advanced/settings.md){.internal-link target=_blank}. + +## Biến môi trường `PATH` + +Có một biến môi trường **đặc biệt** được gọi là **`PATH`** được sử dụng bởi các hệ điều hành (Linux, macOS, Windows) nhằm tìm các chương trình để thực thi. + +Giá trị của biến môi trường `PATH` là một chuỗi dài được tạo bởi các thư mục được phân tách bởi dấu hai chấm `:` trên Linux và macOS, và bởi dấu chấm phẩy `;` trên Windows. + +Ví dụ, biến môi trường `PATH` có thể có dạng như sau: + +//// tab | Linux, macOS + +```plaintext +/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin +``` + +Điều này có nghĩa là hệ thống sẽ tìm kiếm các chương trình trong các thư mục: + +* `/usr/local/bin` +* `/usr/bin` +* `/bin` +* `/usr/sbin` +* `/sbin` + +//// + +//// tab | Windows + +```plaintext +C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32 +``` + +Điều này có nghĩa là hệ thống sẽ tìm kiếm các chương trình trong các thư mục: + +* `C:\Program Files\Python312\Scripts` +* `C:\Program Files\Python312` +* `C:\Windows\System32` + +//// + +Khi bạn gõ một **lệnh** trong terminal, hệ điều hành **tìm kiếm** chương trình trong **mỗi thư mục** được liệt kê trong biến môi trường `PATH`. + +Ví dụ, khi bạn gõ `python` trong terminal, hệ điều hành tìm kiếm một chương trình được gọi `python` trong **thư mục đầu tiên** trong danh sách đó. + +Nếu tìm thấy, nó sẽ **sử dụng** nó. Nếu không tìm thấy, nó sẽ tiếp tục tìm kiếm trong **các thư mục khác**. + +### Cài đặt Python và cập nhật biến môi trường `PATH` + +Khi bạn cài đặt Python, bạn có thể được hỏi nếu bạn muốn cập nhật biến môi trường `PATH`. + +//// tab | Linux, macOS + +Giả sử bạn cài đặt Python vào thư mục `/opt/custompython/bin`. + +Nếu bạn chọn cập nhật biến môi trường `PATH`, thì cài đặt sẽ thêm `/opt/custompython/bin` vào biến môi trường `PATH`. + +Nó có thể có dạng như sau: + +```plaintext +/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/custompython/bin +``` + +Như vậy, khi bạn gõ `python` trong terminal, hệ thống sẽ tìm thấy chương trình Python trong `/opt/custompython/bin` (thư mục cuối) và sử dụng nó. + +//// + +//// tab | Windows + +Giả sử bạn cài đặt Python vào thư mục `C:\opt\custompython\bin`. + +Nếu bạn chọn cập nhật biến môi trường `PATH`, thì cài đặt sẽ thêm `C:\opt\custompython\bin` vào biến môi trường `PATH`. + +Nó có thể có dạng như sau: + +```plaintext +C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32;C:\opt\custompython\bin +``` + +Như vậy, khi bạn gõ `python` trong terminal, hệ thống sẽ tìm thấy chương trình Python trong `C:\opt\custompython\bin` (thư mục cuối) và sử dụng nó. + +//// + +Vậy, nếu bạn gõ: + +
+ +```console +$ python +``` + +
+ +//// tab | Linux, macOS + +Hệ thống sẽ **tìm kiếm** chương trình `python` trong `/opt/custompython/bin` và thực thi nó. + +Nó tương đương với việc bạn gõ: + +
+ +```console +$ /opt/custompython/bin/python +``` + +
+ +//// + +//// tab | Windows + +Hệ thống sẽ **tìm kiếm** chương trình `python` trong `C:\opt\custompython\bin\python` và thực thi nó. + +Nó tương đương với việc bạn gõ: + +
+ +```console +$ C:\opt\custompython\bin\python +``` + +
+ +//// + +Thông tin này sẽ hữu ích khi bạn học về [Môi trường ảo](virtual-environments.md){.internal-link target=_blank}. + +## Kết luận + +Với những thông tin này, bạn có thể hiểu được **các biến môi trường là gì** và **cách sử dụng chúng trong Python**. + +Bạn có thể đọc thêm về chúng tại Wikipedia cho Biến môi trường. + +Trong nhiều trường hợp, cách các biến môi trường trở nên hữu ích và có thể áp dụng không thực sự rõ ràng ngay từ đầu, nhưng chúng sẽ liên tục xuất hiện trong rất nhiều tình huống khi bạn phát triển ứng dụng, vì vậy việc hiểu biết về chúng là hữu ích. + +Chẳng hạn, bạn sẽ cần những thông tin này khi bạn học về [Môi trường ảo](virtual-environments.md). From f9352c18de97dc8867e69c3a0695092495c2ff1f Mon Sep 17 00:00:00 2001 From: Valentyn Date: Sat, 8 Feb 2025 00:17:53 +0200 Subject: [PATCH 074/117] =?UTF-8?q?=F0=9F=8C=90=20Add=20Ukrainian=20transl?= =?UTF-8?q?ation=20for=20`docs/uk/docs/tutorial/static-files.md`=20(#13285?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/uk/docs/tutorial/static-files.md | 40 +++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 docs/uk/docs/tutorial/static-files.md diff --git a/docs/uk/docs/tutorial/static-files.md b/docs/uk/docs/tutorial/static-files.md new file mode 100644 index 000000000..a84782d8f --- /dev/null +++ b/docs/uk/docs/tutorial/static-files.md @@ -0,0 +1,40 @@ +# Статичні файли + +Ви можете автоматично надавати статичні файли з каталогу, використовуючи `StaticFiles`. + +## Використання `StaticFiles` + +* Імпортуйте `StaticFiles`. +* "Під'єднати" екземпляр `StaticFiles()` з вказанням необхідного шляху. + +{* ../../docs_src/static_files/tutorial001.py hl[2,6] *} + +/// note | Технічні деталі + +Ви також можете використовувати `from starlette.staticfiles import StaticFiles`. + +**FastAPI** надає той самий `starlette.staticfiles`, що й `fastapi.staticfiles` для зручності розробників. Але фактично він безпосередньо походить із Starlette. + +/// + +### Що таке "Під'єднання" + +"Під'єднання" означає додавання повноцінного "незалежного" застосунку за певним шляхом, який потім обробляє всі під шляхи. + +Це відрізняється від використання `APIRouter`, оскільки під'єднаний застосунок є повністю незалежним. OpenAPI та документація вашого основного застосунку не будуть знати нічого про ваш під'єднаний застосунок. + +Ви можете дізнатися більше про це в [Посібнику для просунутих користувачів](../advanced/index.md){.internal-link target=_blank}. + +## Деталі + +Перше `"/static"` вказує на під шлях, за яким буде "під'єднано" цей новий "застосунок". Тому будь-який шлях, який починається з `"/static"`, буде оброблятися ним. + +`directory="static"` визначає каталог, що містить ваші статичні файли. + +`name="static"` це ім'я, яке можна використовувати всередині **FastAPI**. + +Усі ці параметри можуть бути змінені відповідно до потреб і особливостей вашого застосунку. + +## Додаткова інформація + +Детальніше про налаштування та можливості можна дізнатися в документації Starlette про статичні файли. From 8a6d81afad05ad57efbce7d72bf5d803029b93bc Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 7 Feb 2025 22:17:59 +0000 Subject: [PATCH 075/117] =?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 18ef5911c..c52fb0f63 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -18,6 +18,7 @@ hide: ### Internal +* 👥 Update FastAPI People - Sponsors. PR [#13295](https://github.com/fastapi/fastapi/pull/13295) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People - Experts. PR [#13303](https://github.com/fastapi/fastapi/pull/13303) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI GitHub topic repositories. PR [#13302](https://github.com/fastapi/fastapi/pull/13302) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People - Contributors and Translators. PR [#13293](https://github.com/fastapi/fastapi/pull/13293) by [@tiangolo](https://github.com/tiangolo). From e86ef5e57d17be4e868b495102961b59844bd879 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C6=B0=C6=A1ng=20T=E1=BA=A5n=20Th=C3=A0nh?= <51350651+ptt3199@users.noreply.github.com> Date: Sat, 8 Feb 2025 05:19:18 +0700 Subject: [PATCH 076/117] =?UTF-8?q?=F0=9F=8C=90=20Add=20Vietnamese=20trans?= =?UTF-8?q?lation=20for=20`docs/vi/docs/virtual-environments.md`=20(#13282?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/vi/docs/virtual-environments.md | 842 +++++++++++++++++++++++++++ 1 file changed, 842 insertions(+) create mode 100644 docs/vi/docs/virtual-environments.md diff --git a/docs/vi/docs/virtual-environments.md b/docs/vi/docs/virtual-environments.md new file mode 100644 index 000000000..22d8e153e --- /dev/null +++ b/docs/vi/docs/virtual-environments.md @@ -0,0 +1,842 @@ +# Môi trường ảo (Virtual Environments) + +Khi bạn làm việc trong các dự án Python, bạn có thể sử dụng một **môi trường ảo** (hoặc một cơ chế tương tự) để cách ly các gói bạn cài đặt cho mỗi dự án. + +/// info +Nếu bạn đã biết về các môi trường ảo, cách tạo chúng và sử dụng chúng, bạn có thể bỏ qua phần này. 🤓 + +/// + +/// tip + +Một **môi trường ảo** khác với một **biến môi trường (environment variable)**. + +Một **biến môi trường** là một biến trong hệ thống có thể được sử dụng bởi các chương trình. + +Một **môi trường ảo** là một thư mục với một số tệp trong đó. + +/// + +/// info + +Trang này sẽ hướng dẫn bạn cách sử dụng các **môi trường ảo** và cách chúng hoạt động. + +Nếu bạn đã sẵn sàng sử dụng một **công cụ có thể quản lý tất cả mọi thứ** cho bạn (bao gồm cả việc cài đặt Python), hãy thử uv. + +/// + +## Tạo một Dự án + +Đầu tiên, tạo một thư mục cho dự án của bạn. + +Cách tôi thường làm là tạo một thư mục có tên `code` trong thư mục `home/user`. + +Và trong thư mục đó, tôi tạo một thư mục cho mỗi dự án. + +
+ +```console +// Đi đến thư mục home +$ cd +// Tạo một thư mục cho tất cả các dự án của bạn +$ mkdir code +// Vào thư mục code +$ cd code +// Tạo một thư mục cho dự án này +$ mkdir awesome-project +// Vào thư mục dự án +$ cd awesome-project +``` + +
+ +## Tạo một Môi trường ảo + +Khi bạn bắt đầu làm việc với một dự án Python **trong lần đầu**, hãy tạo một môi trường ảo **trong thư mục dự án của bạn**. + +/// tip + +Bạn cần làm điều này **một lần cho mỗi dự án**, không phải mỗi khi bạn làm việc. +/// + +//// tab | `venv` + +Để tạo một môi trường ảo, bạn có thể sử dụng module `venv` có sẵn của Python. + +
+ +```console +$ python -m venv .venv +``` + +
+ +/// details | Cách các lệnh hoạt động + +* `python`: sử dụng chương trình `python` +* `-m`: gọi một module như một script, chúng ta sẽ nói về module đó sau +* `venv`: sử dụng module `venv` được cài đặt sẵn của Python +* `.venv`: tạo môi trường ảo trong thư mục mới `.venv` + +/// + +//// + +//// tab | `uv` + +Nếu bạn có `uv` được cài đặt, bạn có thể sử dụng nó để tạo một môi trường ảo. + +
+ +```console +$ uv venv +``` + +
+ +/// tip + +Mặc định, `uv` sẽ tạo một môi trường ảo trong một thư mục có tên `.venv`. + +Nhưng bạn có thể tùy chỉnh nó bằng cách thêm một đối số với tên thư mục. + +/// + +//// + +Lệnh này tạo một môi trường ảo mới trong một thư mục có tên `.venv`. + +/// details | `.venv` hoặc tên khác + +Bạn có thể tạo môi trường ảo trong một thư mục khác, nhưng thường người ta quy ước đặt nó là `.venv`. + +/// + +## Kích hoạt Môi trường ảo + +Kích hoạt môi trường ảo mới để bất kỳ lệnh Python nào bạn chạy hoặc gói nào bạn cài đặt sẽ sử dụng nó. + +/// tip + +Làm điều này **mỗi khi** bạn bắt đầu một **phiên terminal mới** để làm việc trên dự án. + +/// + +//// tab | Linux, macOS + +
+ +```console +$ source .venv/bin/activate +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ +```console +$ .venv\Scripts\Activate.ps1 +``` + +
+ +//// + +//// tab | Windows Bash + +Nếu bạn sử dụng Bash cho Windows (ví dụ: Git Bash): + +
+ +```console +$ source .venv/Scripts/activate +``` + +
+ +//// + +/// tip + +Mỗi khi bạn cài đặt thêm một **package mới** trong môi trường đó, hãy **kích hoạt** môi trường đó lại. + +Điều này đảm bảo rằng khi bạn sử dụng một **chương trình dòng lệnh (CLI)** được cài đặt từ gói đó, bạn sẽ dùng bản cài đặt từ môi trường ảo của mình thay vì bản được cài đặt toàn cục khác có thể có phiên bản khác với phiên bản bạn cần. + +/// + +## Kiểm tra xem Môi trường ảo đã được Kích hoạt chưa + +Kiểm tra xem môi trường ảo đã được kích hoạt chưa (lệnh trước đó đã hoạt động). + +/// tip + +Điều này là **không bắt buộc**, nhưng nó là một cách tốt để **kiểm tra** rằng mọi thứ đang hoạt động như mong đợi và bạn đang sử dụng đúng môi trường ảo mà bạn đã định. + +/// + +//// tab | Linux, macOS, Windows Bash + +
+ +```console +$ which python + +/home/user/code/awesome-project/.venv/bin/python +``` + +
+ +Nếu nó hiển thị `python` binary tại `.venv/bin/python`, trong dự án của bạn (trong trường hợp `awesome-project`), thì tức là nó hoạt động. 🎉 + +//// + +//// tab | Windows PowerShell + +
+ +```console +$ Get-Command python + +C:\Users\user\code\awesome-project\.venv\Scripts\python +``` + +
+ +Nếu nó hiển thị `python` binary tại `.venv\Scripts\python`, trong dự án của bạn (trong trường hợp `awesome-project`), thì tức là nó hoạt động. 🎉 + +//// + +## Nâng cấp `pip` + +/// tip + +Nếu bạn sử dụng `uv` bạn sử dụng nó để cài đặt thay vì `pip`, thì bạn không cần cập nhật `pip`. 😎 + +/// + +Nếu bạn sử dụng `pip` để cài đặt gói (nó được cài đặt mặc định với Python), bạn nên **nâng cấp** nó lên phiên bản mới nhất. + +Nhiều lỗi khác nhau trong khi cài đặt gói được giải quyết chỉ bằng cách nâng cấp `pip` trước. + +/// tip + +Bạn thường làm điều này **một lần**, ngay sau khi bạn tạo môi trường ảo. + +/// + +Đảm bảo rằng môi trường ảo đã được kích hoạt (với lệnh trên) và sau đó chạy: + +
+ +```console +$ python -m pip install --upgrade pip + +---> 100% +``` + +
+ +## Thêm `.gitignore` + +Nếu bạn sử dụng **Git** (nên làm), hãy thêm một file `.gitignore` để Git bỏ qua mọi thứ trong `.venv`. + +/// tip + +Nếu bạn sử dụng `uv` để tạo môi trường ảo, nó đã tự động làm điều này cho bạn, bạn có thể bỏ qua bước này. 😎 + +/// + +/// tip + +Làm điều này **một lần**, ngay sau khi bạn tạo môi trường ảo. + +/// + +
+ +```console +$ echo "*" > .venv/.gitignore +``` + +
+ +/// details | Cách lệnh hoạt động + +* `echo "*"`: sẽ "in" văn bản `*` trong terminal (phần tiếp theo sẽ thay đổi điều đó một chút) +* `>`: bất kỳ văn bản nào được in ra terminal bởi lệnh trước `>` không được in ra mà thay vào đó được viết vào file ở phía bên phải của `>` +* `.gitignore`: tên của file mà văn bản sẽ được viết vào + +Và `*` với Git có nghĩa là "mọi thứ". Vì vậy, nó sẽ bỏ qua mọi thứ trong thư mục `.venv`. + +Lệnh này sẽ tạo một file `.gitignore` với nội dung: + +```gitignore +* +``` + +/// + +## Cài đặt gói (packages) + +Sau khi kích hoạt môi trường, bạn có thể cài đặt các gói trong đó. + +/// tip + +Thực hiện điều này **một lần** khi cài đặt hoặc cập nhật gói cần thiết cho dự án của bạn. + +Nếu bạn cần cập nhật phiên bản hoặc thêm một gói mới, bạn sẽ **thực hiện điều này lại**. + +/// + +### Cài đặt gói trực tiếp + +Nếu bạn cần cập nhật phiên bản hoặc thêm một gói mới, bạn sẽ **thực hiện điều này lại**. + +/// tip +Để quản lý dự án tốt hơn, hãy liệt kê tất cả các gói và phiên bản cần thiết trong một file (ví dụ `requirements.txt` hoặc `pyproject.toml`). + +/// + +//// tab | `pip` + +
+ +```console +$ pip install "fastapi[standard]" + +---> 100% +``` + +
+ +//// + +//// tab | `uv` + +Nếu bạn có `uv`: + +
+ +```console +$ uv pip install "fastapi[standard]" +---> 100% +``` + +
+ +//// + +### Cài đặt từ `requirements.txt` + +Nếu bạn có một tệp `requirements.txt`, bạn có thể sử dụng nó để cài đặt các gói. + +//// tab | `pip` + +
+ +```console +$ pip install -r requirements.txt +---> 100% +``` + +
+ +//// + +//// tab | `uv` + +Nếu bạn có `uv`: + +
+ +```console +$ uv pip install -r requirements.txt +---> 100% +``` + +
+ +//// + +/// details | `requirements.txt` + +Một tệp `requirements.txt` với một số gói sẽ trông như thế này: + +```requirements.txt +fastapi[standard]==0.113.0 +pydantic==2.8.0 +``` + +/// + +## Chạy Chương trình của bạn + +Sau khi kích hoạt môi trường ảo, bạn có thể chạy chương trình của mình, nó sẽ sử dụng Python trong môi trường ảo của bạn với các gói bạn đã cài đặt. + +
+ +```console +$ python main.py + +Hello World +``` + +
+ +## Cấu hình Trình soạn thảo của bạn + +Nếu bạn sử dụng một trình soạn thảo, hãy đảm bảo bạn cấu hình nó để sử dụng cùng môi trường ảo mà bạn đã tạo (trình soạn thảo sẽ tự động phát hiện môi trường ảo) để bạn có thể nhận được tính năng tự động hoàn thành câu lệnh (autocomplete) và in lỗi trực tiếp trong trình soạn thảo (inline errors). + +Ví dụ: + +* VS Code +* PyCharm + +/// tip + +Bạn thường chỉ cần làm điều này **một lần**, khi bạn tạo môi trường ảo. + +/// + +## Huỷ kích hoạt Môi trường ảo + +Khi bạn hoàn tất việc làm trên dự án của bạn, bạn có thể **huỷ kích hoạt** môi trường ảo. + +
+ +```console +$ deactivate +``` + +
+ +Như vậy, khi bạn chạy `python`, nó sẽ không chạy từ môi trường ảo đó với các gói đã cài đặt. + +## Sẵn sàng để Làm việc + +Bây giờ bạn đã sẵn sàng để làm việc trên dự án của mình rồi đấy. + +/// tip + +Bạn muốn hiểu tất cả những gì ở trên? + +Tiếp tục đọc. 👇🤓 + +/// + +## Tại sao cần Môi trường ảo + +Để làm việc với FastAPI, bạn cần cài đặt Python. + +Sau đó, bạn sẽ cần **cài đặt** FastAPI và bất kỳ **gói** nào mà bạn muốn sử dụng. + +Để cài đặt gói, bạn thường sử dụng lệnh `pip` có sẵn với Python (hoặc các phiên bản tương tự). + +Tuy nhiên, nếu bạn sử dụng `pip` trực tiếp, các gói sẽ được cài đặt trong **môi trường Python toàn cục** của bạn (phần cài đặt toàn cục của Python). + +### Vấn đề + +Vậy, vấn đề gì khi cài đặt gói trong môi trường Python toàn cục? + +Trong một vài thời điểm, bạn sẽ phải viết nhiều chương trình khác nhau phụ thuộc vào **các gói khác nhau**. Và một số dự án bạn thực hiện lại phụ thuộc vào **các phiên bản khác nhau** của cùng một gói. 😱 + +Ví dụ, bạn có thể tạo một dự án được gọi là `philosophers-stone`, chương trình này phụ thuộc vào một gói khác được gọi là **`harry`, sử dụng phiên bản `1`**. Vì vậy, bạn cần cài đặt `harry`. + +```mermaid +flowchart LR + stone(philosophers-stone) -->|phụ thuộc| harry-1[harry v1] +``` + +Sau đó, vào một vài thời điểm sau, bạn tạo một dự án khác được gọi là `prisoner-of-azkaban`, và dự án này cũng phụ thuộc vào `harry`, nhưng dự án này cần **`harry` phiên bản `3`**. + +```mermaid +flowchart LR + azkaban(prisoner-of-azkaban) --> |phụ thuộc| harry-3[harry v3] +``` + +Bây giờ, vấn đề là, nếu bạn cài đặt các gói toàn cục (trong môi trường toàn cục) thay vì trong một **môi trường ảo cục bộ**, bạn sẽ phải chọn phiên bản `harry` nào để cài đặt. + +Nếu bạn muốn chạy `philosophers-stone` bạn sẽ cần phải cài đặt `harry` phiên bản `1`, ví dụ với: + +
+ +```console +$ pip install "harry==1" +``` + +
+ +Và sau đó bạn sẽ có `harry` phiên bản `1` được cài đặt trong môi trường Python toàn cục của bạn. + +```mermaid +flowchart LR + subgraph global[môi trường toàn cục] + harry-1[harry v1] + end + subgraph stone-project[dự án philosophers-stone ] + stone(philosophers-stone) -->|phụ thuộc| harry-1 + end +``` + +Nhưng sau đó, nếu bạn muốn chạy `prisoner-of-azkaban`, bạn sẽ cần phải gỡ bỏ `harry` phiên bản `1` và cài đặt `harry` phiên bản `3` (hoặc chỉ cần cài đặt phiên bản `3` sẽ tự động gỡ bỏ phiên bản `1`). + +
+ +```console +$ pip install "harry==3" +``` + +
+ +Và sau đó bạn sẽ có `harry` phiên bản `3` được cài đặt trong môi trường Python toàn cục của bạn. + +Và nếu bạn cố gắng chạy `philosophers-stone` lại, có khả năng nó sẽ **không hoạt động** vì nó cần `harry` phiên bản `1`. + +```mermaid +flowchart LR + subgraph global[môi trường toàn cục] + harry-1[harry v1] + style harry-1 fill:#ccc,stroke-dasharray: 5 5 + harry-3[harry v3] + end + subgraph stone-project[dự án philosophers-stone ] + stone(philosophers-stone) -.-x|⛔️| harry-1 + end + subgraph azkaban-project[dự án prisoner-of-azkaban ] + azkaban(prisoner-of-azkaban) --> |phụ thuộc| harry-3 + end +``` + +/// tip + +Mặc dù các gói Python thường cố gắng **tránh các thay đổi làm hỏng code** trong **phiên bản mới**, nhưng để đảm bảo an toàn, bạn nên chủ động cài đặt phiên bản mới và chạy kiểm thử để xác nhận mọi thứ vẫn hoạt động đúng. + +/// + +Bây giờ, hãy hình dung về **nhiều** gói khác nhau mà tất cả các dự án của bạn phụ thuộc vào. Rõ ràng rất khó để quản lý. Điều này dẫn tới việc là bạn sẽ có nhiều dự án với **các phiên bản không tương thích** của các gói, và bạn có thể không biết tại sao một số thứ không hoạt động. + +Hơn nữa, tuỳ vào hệ điều hành của bạn (vd Linux, Windows, macOS), có thể đã có Python được cài đặt sẵn. Trong trường hợp ấy, một vài gói nhiều khả năng đã được cài đặt trước với các phiên bản **cần thiết cho hệ thống của bạn**. Nếu bạn cài đặt các gói trong môi trường Python toàn cục, bạn có thể sẽ **phá vỡ** một số chương trình đã được cài đặt sẵn cùng hệ thống. + +## Nơi các Gói được Cài đặt + +Khi bạn cài đặt Python, nó sẽ tạo ra một vài thư mục và tệp trong máy tính của bạn. + +Một vài thư mục này là những thư mục chịu trách nhiệm có tất cả các gói bạn cài đặt. + +Khi bạn chạy: + +
+ +```console +// Đừng chạy lệnh này ngay, đây chỉ là một ví dụ 🤓 +$ pip install "fastapi[standard]" +---> 100% +``` + +
+ +Lệnh này sẽ tải xuống một tệp nén với mã nguồn FastAPI, thường là từ PyPI. + +Nó cũng sẽ **tải xuống** các tệp cho các gói khác mà FastAPI phụ thuộc vào. + +Sau đó, nó sẽ **giải nén** tất cả các tệp đó và đưa chúng vào một thư mục trong máy tính của bạn. + +Mặc định, nó sẽ đưa các tệp đã tải xuống và giải nén vào thư mục được cài đặt cùng Python của bạn, đó là **môi trường toàn cục**. + +## Những Môi trường ảo là gì? + +Cách giải quyết cho vấn đề có tất cả các gói trong môi trường toàn cục là sử dụng một **môi trường ảo cho mỗi dự án** bạn làm việc. + +Một môi trường ảo là một **thư mục**, rất giống với môi trường toàn cục, trong đó bạn có thể cài đặt các gói cho một dự án. + +Vì vậy, mỗi dự án sẽ có một môi trường ảo riêng của nó (thư mục `.venv`) với các gói riêng của nó. + +```mermaid +flowchart TB + subgraph stone-project[dự án philosophers-stone ] + stone(philosophers-stone) --->|phụ thuộc| harry-1 + subgraph venv1[.venv] + harry-1[harry v1] + end + end + subgraph azkaban-project[dự án prisoner-of-azkaban ] + azkaban(prisoner-of-azkaban) --->|phụ thuộc| harry-3 + subgraph venv2[.venv] + harry-3[harry v3] + end + end + stone-project ~~~ azkaban-project +``` + +## Kích hoạt Môi trường ảo nghĩa là gì + +Khi bạn kích hoạt một môi trường ảo, ví dụ với: + +//// tab | Linux, macOS + +
+ +```console +$ source .venv/bin/activate +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ +```console +$ .venv\Scripts\Activate.ps1 +``` + +
+ +//// + +//// tab | Windows Bash + +Nếu bạn sử dụng Bash cho Windows (ví dụ Git Bash): + +
+ +```console +$ source .venv/Scripts/activate +``` + +
+ +//// + +Lệnh này sẽ tạo hoặc sửa đổi một số [biến môi trường](environment-variables.md){.internal-link target=_blank} mà sẽ được sử dụng cho các lệnh tiếp theo. + +Một trong số đó là biến `PATH`. + +/// tip + +Bạn có thể tìm hiểu thêm về biến `PATH` trong [Biến môi trường](environment-variables.md#path-environment-variable){.internal-link target=_blank} section. + +/// + +Kích hoạt môi trường ảo thêm đường dẫn `.venv/bin` (trên Linux và macOS) hoặc `.venv\Scripts` (trên Windows) vào biến `PATH`. + +Giả sử rằng trước khi kích hoạt môi trường, biến `PATH` như sau: + +//// tab | Linux, macOS + +```plaintext +/usr/bin:/bin:/usr/sbin:/sbin +``` + +Nghĩa là hệ thống sẽ tìm kiếm chương trình trong: + +* `/usr/bin` +* `/bin` +* `/usr/sbin` +* `/sbin` + +//// + +//// tab | Windows + +```plaintext +C:\Windows\System32 +``` + +Nghĩa là hệ thống sẽ tìm kiếm chương trình trong: + +* `C:\Windows\System32` + +//// + +Sau khi kích hoạt môi trường ảo, biến `PATH` sẽ như sau: + +//// tab | Linux, macOS + +```plaintext +/home/user/code/awesome-project/.venv/bin:/usr/bin:/bin:/usr/sbin:/sbin +``` + +Nghĩa là hệ thống sẽ bắt đầu tìm kiếm chương trình trong: + +```plaintext +/home/user/code/awesome-project/.venv/bin +``` + +trước khi tìm kiếm trong các thư mục khác. + +Vì vậy, khi bạn gõ `python` trong terminal, hệ thống sẽ tìm thấy chương trình Python trong: + +```plaintext +/home/user/code/awesome-project/.venv/bin/python +``` + +và sử dụng chương trình đó. + +//// + +//// tab | Windows + +```plaintext +C:\Users\user\code\awesome-project\.venv\Scripts;C:\Windows\System32 +``` + +Nghĩa là hệ thống sẽ bắt đầu tìm kiếm chương trình trong: + +```plaintext +C:\Users\user\code\awesome-project\.venv\Scripts +``` + +trước khi tìm kiếm trong các thư mục khác. + +Vì vậy, khi bạn gõ `python` trong terminal, hệ thống sẽ tìm thấy chương trình Python trong: + +```plaintext +C:\Users\user\code\awesome-project\.venv\Scripts\python +``` + +và sử dụng chương trình đó. + +//// + +Một chi tiết quan trọng là nó sẽ đưa địa chỉ của môi trường ảo vào **đầu** của biến `PATH`. Hệ thống sẽ tìm kiếm nó **trước** khi tìm kiếm bất kỳ Python nào khác có sẵn. Vì vậy, khi bạn chạy `python`, nó sẽ sử dụng Python **từ môi trường ảo** thay vì bất kỳ Python nào khác (ví dụ, Python từ môi trường toàn cục). + +Kích hoạt một môi trường ảo cũng thay đổi một vài thứ khác, nhưng đây là một trong những điều quan trọng nhất mà nó thực hiện. + +## Kiểm tra một Môi trường ảo + +Khi bạn kiểm tra một môi trường ảo đã được kích hoạt chưa, ví dụ với: + +//// tab | Linux, macOS, Windows Bash + +
+ +```console +$ which python + +/home/user/code/awesome-project/.venv/bin/python +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ +```console +$ Get-Command python + +C:\Users\user\code\awesome-project\.venv\Scripts\python +``` + +
+ +//// + + +Điều đó có nghĩa là chương trình `python` sẽ được sử dụng là chương trình **trong môi trường ảo**. + +Bạn sử dụng `which` trên Linux và macOS và `Get-Command` trên Windows PowerShell. + +Cách hoạt động của lệnh này là nó sẽ đi và kiểm tra biến `PATH`, đi qua **mỗi đường dẫn theo thứ tự**, tìm kiếm chương trình được gọi là `python`. Khi nó tìm thấy nó, nó sẽ **hiển thị cho bạn đường dẫn** đến chương trình đó. + +Điều quan trọng nhất là khi bạn gọi `python`, đó chính là chương trình `python` được thực thi. + +Vì vậy, bạn có thể xác nhận nếu bạn đang ở trong môi trường ảo đúng. + +/// tip + +Dễ dàng kích hoạt một môi trường ảo, cài đặt Python, và sau đó **chuyển đến một dự án khác**. + +Và dự án thứ hai **sẽ không hoạt động** vì bạn đang sử dụng **Python không đúng**, từ một môi trường ảo cho một dự án khác. + +Thật tiện lợi khi có thể kiểm tra `python` nào đang được sử dụng 🤓 + +/// + +## Tại sao lại Huỷ kích hoạt một Môi trường ảo + +Ví dụ, bạn có thể làm việc trên một dự án `philosophers-stone`, **kích hoạt môi trường ảo**, cài đặt các gói và làm việc với môi trường ảo đó. + +Sau đó, bạn muốn làm việc trên **dự án khác** `prisoner-of-azkaban`. + +Bạn đi đến dự án đó: + +
+ +```console +$ cd ~/code/prisoner-of-azkaban +``` + +
+ +Nếu bạn không tắt môi trường ảo cho `philosophers-stone`, khi bạn chạy `python` trong terminal, nó sẽ cố gắng sử dụng Python từ `philosophers-stone`. + +
+ +```console +$ cd ~/code/prisoner-of-azkaban + +$ python main.py + +// Lỗi khi import sirius, nó không được cài đặt 😱 +Traceback (most recent call last): + File "main.py", line 1, in + import sirius +``` + +
+ +Nếu bạn huỷ kích hoạt môi trường ảo hiện tại và kích hoạt môi trường ảo mới cho `prisoner-of-azkaban`, khi bạn chạy `python`, nó sẽ sử dụng Python từ môi trường ảo trong `prisoner-of-azkaban`. + +
+ +```console +$ cd ~/code/prisoner-of-azkaban + +// Bạn không cần phải ở trong thư mục trước để huỷ kích hoạt, bạn có thể làm điều đó ở bất kỳ đâu, ngay cả sau khi đi đến dự án khác 😎 +$ deactivate + +// Kích hoạt môi trường ảo trong prisoner-of-azkaban/.venv 🚀 +$ source .venv/bin/activate + +// Bây giờ khi bạn chạy python, nó sẽ tìm thấy gói sirius được cài đặt trong môi trường ảo này ✨ +$ python main.py + +I solemnly swear 🐺 + +(Tôi long trọng thề 🐺 - câu này được lấy từ Harry Potter, chú thích của người dịch) +``` + +
+ +## Các cách làm tương tự + +Đây là một hướng dẫn đơn giản để bạn có thể bắt đầu và hiểu cách mọi thứ hoạt động **bên trong**. + +Có nhiều **cách khác nhau** để quản lí các môi trường ảo, các gói phụ thuộc (requirements), và các dự án. + +Một khi bạn đã sẵn sàng và muốn sử dụng một công cụ để **quản lí cả dự án**, các gói phụ thuộc, các môi trường ảo, v.v. Tôi sẽ khuyên bạn nên thử uv. + +`uv` có thể làm nhiều thứ, chẳng hạn: + +* **Cài đặt Python** cho bạn, bao gồm nhiều phiên bản khác nhau +* Quản lí **các môi trường ảo** cho các dự án của bạn +* Cài đặt **các gói (packages)** +* Quản lí **các thành phần phụ thuộc và phiên bản** của các gói cho dự án của bạn +* Đảm bảo rằng bạn có một **tập hợp chính xác** các gói và phiên bản để cài đặt, bao gồm các thành phần phụ thuộc của chúng, để bạn có thể đảm bảo rằng bạn có thể chạy dự án của bạn trong sản xuất chính xác như trong máy tính của bạn trong khi phát triển, điều này được gọi là **locking** +* Và còn nhiều thứ khác nữa + +## Kết luận + +Nếu bạn đã đọc và hiểu hết những điều này, khá chắc là bây giờ bạn đã **biết nhiều hơn** về môi trường ảo so với kha khá lập trình viên khác đấy. 🤓 + +Những hiểu biết chi tiết này có thể sẽ hữu ích với bạn trong tương lai khi mà bạn cần gỡ lỗi một vài thứ phức tạp, và bạn đã có những hiểu biết về **ngọn ngành gốc rễ cách nó hoạt động**. 😎 From 8cde8dc2a9e02dd70746a11dd906909449749b44 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Feb 2025 22:22:35 +0000 Subject: [PATCH 077/117] =?UTF-8?q?=E2=AC=86=20Bump=20pillow=20from=2011.0?= =?UTF-8?q?.0=20to=2011.1.0=20(#13300)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [pillow](https://github.com/python-pillow/Pillow) from 11.0.0 to 11.1.0. - [Release notes](https://github.com/python-pillow/Pillow/releases) - [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) - [Commits](https://github.com/python-pillow/Pillow/compare/11.0.0...11.1.0) --- updated-dependencies: - dependency-name: pillow 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> Co-authored-by: Alejandra <90076947+alejsdev@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 6f391675a..9af2a85d9 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -8,7 +8,7 @@ pyyaml >=5.3.1,<7.0.0 # For Material for MkDocs, Chinese search jieba==0.42.1 # For image processing by Material for MkDocs -pillow==11.0.0 +pillow==11.1.0 # For image processing by Material for MkDocs cairosvg==2.7.1 mkdocstrings[python]==0.26.1 From 83332ff9b272d5df8e6e979e1cbd3e72ee5b2e84 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Feb 2025 22:22:54 +0000 Subject: [PATCH 078/117] =?UTF-8?q?=E2=AC=86=20Bump=20mkdocs-material=20fr?= =?UTF-8?q?om=209.5.18=20to=209.6.1=20(#13301)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.5.18 to 9.6.1. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.5.18...9.6.1) --- updated-dependencies: - dependency-name: mkdocs-material 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> Co-authored-by: Alejandra <90076947+alejsdev@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 9af2a85d9..cd2e4e58e 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,6 +1,6 @@ -e . -r requirements-docs-tests.txt -mkdocs-material==9.5.18 +mkdocs-material==9.6.1 mdx-include >=1.4.1,<2.0.0 mkdocs-redirects>=1.2.1,<1.3.0 typer == 0.12.5 From 42a3b1526e771d81d0b90c7bccce2d097a7a7b2c Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 7 Feb 2025 22:23:11 +0000 Subject: [PATCH 079/117] =?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 c52fb0f63..a6c7b24e8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Translations +* 🌐 Add Vietnamese translation for `docs/vi/docs/environment-variables.md`. PR [#13287](https://github.com/fastapi/fastapi/pull/13287) by [@ptt3199](https://github.com/ptt3199). * 🌐 Add Vietnamese translation for `docs/vi/docs/fastapi-cli.md`. PR [#13294](https://github.com/fastapi/fastapi/pull/13294) by [@ptt3199](https://github.com/ptt3199). * 🌐 Add Ukrainian translation for `docs/uk/docs/features.md`. PR [#13308](https://github.com/fastapi/fastapi/pull/13308) by [@valentinDruzhinin](https://github.com/valentinDruzhinin). * 🌐 Add Ukrainian translation for `docs/uk/docs/learn/index.md`. PR [#13306](https://github.com/fastapi/fastapi/pull/13306) by [@valentinDruzhinin](https://github.com/valentinDruzhinin). From 49b18c87a0b98f36abc6962fc55a3b7bc151e51e Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 7 Feb 2025 22:24:04 +0000 Subject: [PATCH 080/117] =?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 a6c7b24e8..b2020cb25 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Translations +* 🌐 Add Ukrainian translation for `docs/uk/docs/tutorial/static-files.md`. PR [#13285](https://github.com/fastapi/fastapi/pull/13285) by [@valentinDruzhinin](https://github.com/valentinDruzhinin). * 🌐 Add Vietnamese translation for `docs/vi/docs/environment-variables.md`. PR [#13287](https://github.com/fastapi/fastapi/pull/13287) by [@ptt3199](https://github.com/ptt3199). * 🌐 Add Vietnamese translation for `docs/vi/docs/fastapi-cli.md`. PR [#13294](https://github.com/fastapi/fastapi/pull/13294) by [@ptt3199](https://github.com/ptt3199). * 🌐 Add Ukrainian translation for `docs/uk/docs/features.md`. PR [#13308](https://github.com/fastapi/fastapi/pull/13308) by [@valentinDruzhinin](https://github.com/valentinDruzhinin). From 4740ccdcceb23ef825803421a64adbf36d5a84f3 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 7 Feb 2025 22:25:02 +0000 Subject: [PATCH 081/117] =?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 b2020cb25..dd53a3ba1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Translations +* 🌐 Add Vietnamese translation for `docs/vi/docs/virtual-environments.md`. PR [#13282](https://github.com/fastapi/fastapi/pull/13282) by [@ptt3199](https://github.com/ptt3199). * 🌐 Add Ukrainian translation for `docs/uk/docs/tutorial/static-files.md`. PR [#13285](https://github.com/fastapi/fastapi/pull/13285) by [@valentinDruzhinin](https://github.com/valentinDruzhinin). * 🌐 Add Vietnamese translation for `docs/vi/docs/environment-variables.md`. PR [#13287](https://github.com/fastapi/fastapi/pull/13287) by [@ptt3199](https://github.com/ptt3199). * 🌐 Add Vietnamese translation for `docs/vi/docs/fastapi-cli.md`. PR [#13294](https://github.com/fastapi/fastapi/pull/13294) by [@ptt3199](https://github.com/ptt3199). From 828079cb6ea89a27c2aabf8f66ec9af94f496322 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 7 Feb 2025 22:25:56 +0000 Subject: [PATCH 082/117] =?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 dd53a3ba1..633c29661 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -21,6 +21,7 @@ hide: ### Internal +* ⬆ Bump pillow from 11.0.0 to 11.1.0. PR [#13300](https://github.com/fastapi/fastapi/pull/13300) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👥 Update FastAPI People - Sponsors. PR [#13295](https://github.com/fastapi/fastapi/pull/13295) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People - Experts. PR [#13303](https://github.com/fastapi/fastapi/pull/13303) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI GitHub topic repositories. PR [#13302](https://github.com/fastapi/fastapi/pull/13302) by [@tiangolo](https://github.com/tiangolo). From c6c45ae488aea220450e2b9223075f1eb621172b Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 7 Feb 2025 22:27:05 +0000 Subject: [PATCH 083/117] =?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 633c29661..449b880dc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -21,6 +21,7 @@ hide: ### Internal +* ⬆ Bump mkdocs-material from 9.5.18 to 9.6.1. PR [#13301](https://github.com/fastapi/fastapi/pull/13301) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump pillow from 11.0.0 to 11.1.0. PR [#13300](https://github.com/fastapi/fastapi/pull/13300) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👥 Update FastAPI People - Sponsors. PR [#13295](https://github.com/fastapi/fastapi/pull/13295) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People - Experts. PR [#13303](https://github.com/fastapi/fastapi/pull/13303) by [@tiangolo](https://github.com/tiangolo). From ad33193f2c3d7732e804d0d5abdd08ab2ad4796d Mon Sep 17 00:00:00 2001 From: 11kkw <11kkw17@gmail.com> Date: Sun, 9 Feb 2025 23:54:09 +0900 Subject: [PATCH 084/117] =?UTF-8?q?=F0=9F=8C=90=20Add=20Korean=20translati?= =?UTF-8?q?on=20for=20`docs/ko/docs/tutorial/dependencies/dependencies-wit?= =?UTF-8?q?h-yield.md`=20(#13257)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dependencies/dependencies-with-yield.md | 275 ++++++++++++++++++ 1 file changed, 275 insertions(+) create mode 100644 docs/ko/docs/tutorial/dependencies/dependencies-with-yield.md diff --git a/docs/ko/docs/tutorial/dependencies/dependencies-with-yield.md b/docs/ko/docs/tutorial/dependencies/dependencies-with-yield.md new file mode 100644 index 000000000..ff174937d --- /dev/null +++ b/docs/ko/docs/tutorial/dependencies/dependencies-with-yield.md @@ -0,0 +1,275 @@ +# yield를 사용하는 의존성 + +FastAPI는 작업 완료 후 추가 단계를 수행하는 의존성을 지원합니다. + +이를 구현하려면 `return` 대신 `yield`를 사용하고, 추가로 실행할 단계 (코드)를 그 뒤에 작성하세요. + +/// tip | 팁 + +각 의존성마다 `yield`는 한 번만 사용해야 합니다. + +/// + +/// note | 기술 세부사항 + +다음과 함께 사용할 수 있는 모든 함수: + +* `@contextlib.contextmanager` 또는 +* `@contextlib.asynccontextmanager` + +는 **FastAPI**의 의존성으로 사용할 수 있습니다. + +사실, FastAPI는 내부적으로 이 두 데코레이터를 사용합니다. + +/// + +## `yield`를 사용하는 데이터베이스 의존성 + +예를 들어, 이 기능을 사용하면 데이터베이스 세션을 생성하고 작업이 끝난 후에 세션을 종료할 수 있습니다. + +응답을 생성하기 전에는 `yield`문을 포함하여 그 이전의 코드만이 실행됩니다: + +{* ../../docs_src/dependencies/tutorial007.py hl[2:4] *} + +yield된 값은 *경로 작업* 및 다른 의존성들에 주입되는 값 입니다: + +{* ../../docs_src/dependencies/tutorial007.py hl[4] *} + +`yield`문 다음의 코드는 응답을 생성한 후 보내기 전에 실행됩니다: + +{* ../../docs_src/dependencies/tutorial007.py hl[5:6] *} + +/// tip | 팁 + +`async` 함수와 일반 함수 모두 사용할 수 있습니다. + +**FastAPI**는 일반 의존성과 마찬가지로 각각의 함수를 올바르게 처리할 것입니다. + +/// + +## `yield`와 `try`를 사용하는 의존성 + +`yield`를 사용하는 의존성에서 `try` 블록을 사용한다면, 의존성을 사용하는 도중 발생한 모든 예외를 받을 수 있습니다. + +예를 들어, 다른 의존성이나 *경로 작업*의 중간에 데이터베이스 트랜잭션 "롤백"이 발생하거나 다른 오류가 발생한다면, 해당 예외를 의존성에서 받을 수 있습니다. + +따라서, 의존성 내에서 `except SomeException`을 사용하여 특정 예외를 처리할 수 있습니다. + +마찬가지로, `finally`를 사용하여 예외 발생 여부와 관계 없이 종료 단계까 실행되도록 할 수 있습니다. + +{* ../../docs_src/dependencies/tutorial007.py hl[3,5] *} + +## `yield`를 사용하는 하위 의존성 + +모든 크기와 형태의 하위 의존성과 하위 의존성의 "트리"도 가질 수 있으며, 이들 모두가 `yield`를 사용할 수 있습니다. + +**FastAPI**는 `yield`를 사용하는 각 의존성의 "종료 코드"가 올바른 순서로 실행되도록 보장합니다. + +예를 들어, `dependency_c`는 `dependency_b`에 의존할 수 있고, `dependency_b`는 `dependency_a`에 의존할 수 있습니다. + +{* ../../docs_src/dependencies/tutorial008_an_py39.py hl[6,14,22] *} + +이들 모두는 `yield`를 사용할 수 있습니다. + +이 경우 `dependency_c`는 종료 코드를 실행하기 위해, `dependency_b`의 값 (여기서는 `dep_b`로 명명)이 여전히 사용 가능해야 합니다. + +그리고, `dependency_b`는 종료 코드를 위해 `dependency_a`의 값 (여기서는 `dep_a`로 명명) 이 사용 가능해야 합니다. + +{* ../../docs_src/dependencies/tutorial008_an_py39.py hl[18:19,26:27] *} + +같은 방식으로, `yield`를 사용하는 의존성과 `return`을 사용하는 의존성을 함께 사용할 수 있으며, 이들 중 일부가 다른 것들에 의존할 수 있습니다. + +그리고 `yield`를 사용하는 다른 여러 의존성을 필요로 하는 단일 의존성을 가질 수도 있습니다. + +원하는 의존성을 원하는 대로 조합할 수 있습니다. + +**FastAPI**는 모든 것이 올바른 순서로 실행되도록 보장합니다. + +/// note | 기술 세부사항 + +파이썬의 Context Managers 덕분에 이 기능이 작동합니다. + +**FastAPI**는 이를 내부적으로 컨텍스트 관리자를 사용하여 구현합니다. + +/// + +## `yield`와 `HTTPException`를 사용하는 의존성 + +`yield`와 `try` 블록이 있는 의존성을 사용하여 예외를 처리할 수 있다는 것을 알게 되었습니다. + +같은 방식으로, `yield` 이후의 종료 코드에서 `HTTPException`이나 유사한 예외를 발생시킬 수 있습니다. + +/// tip | 팁 + +이는 다소 고급 기술이며, 대부분의 경우 경로 연산 함수 등 나머지 애플리케이션 코드 내부에서 예외 (`HTTPException` 포함)를 발생시킬 수 있으므로 실제로는 필요하지 않을 것입니다. + +하지만 필요한 경우 사용할 수 있습니다. 🤓 + +/// + +{* ../../docs_src/dependencies/tutorial008b_an_py39.py hl[18:22,31] *} + +예외를 처리하고(또는 추가로 다른 `HTTPException`을 발생시키기 위해) 사용할 수 있는 또 다른 방법은 [사용자 정의 예외 처리기](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}를 생성하는 것 입니다. + +## `yield`와 `except`를 사용하는 의존성 + +`yield`를 사용하는 의존성에서 `except`를 사용하여 예외를 포착하고 예외를 다시 발생시키지 않거나 (또는 새 예외를 발생시키지 않으면), FastAPI는 해당 예외가 발생했는지 알 수 없습니다. 이는 일반적인 Python 방식과 동일합니다: + +{* ../../docs_src/dependencies/tutorial008c_an_py39.py hl[15:16] *} + +이 경우, `HTTPException`이나 유사한 예외를 발생시키지 않기 때문에 클라이언트는 HTTP 500 Internal Server Error 응답을 보게 되지만, 서버는 어떤 오류가 발생했는지에 대한 **로그**나 다른 표시를 전혀 가지지 않게 됩니다. 😱 + +### `yield`와 `except`를 사용하는 의존성에서 항상 `raise` 하기 + +`yield`가 있는 의존성에서 예외를 잡았을 때는 `HTTPException`이나 유사한 예외를 새로 발생시키지 않는 한, 반드시 원래의 예외를 다시 발생시켜야 합니다. + +`raise`를 사용하여 동일한 예외를 다시 발생시킬 수 있습니다: + +{* ../../docs_src/dependencies/tutorial008d_an_py39.py hl[17] *} + +이제 클라이언트는 동일한 *HTTP 500 Internal Server Error* 오류 응답을 받게 되지만, 서버 로그에는 사용자 정의 예외인 `InternalError"가 기록됩니다. 😎 + +## `yield`를 사용하는 의존성의 실행 순서 + +실행 순서는 아래 다이어그램과 거의 비슷합니다. 시간은 위에서 아래로 흐릅니다. 그리고 각 열은 상호 작용하거나 코드를 실행하는 부분 중 하나입니다. + +```mermaid +sequenceDiagram + +participant client as Client +participant handler as Exception handler +participant dep as Dep with yield +participant operation as Path Operation +participant tasks as Background tasks + + Note over client,operation: Can raise exceptions, including HTTPException + client ->> dep: Start request + Note over dep: Run code up to yield + opt raise Exception + dep -->> handler: Raise Exception + handler -->> client: HTTP error response + end + dep ->> operation: Run dependency, e.g. DB session + opt raise + operation -->> dep: Raise Exception (e.g. HTTPException) + opt handle + dep -->> dep: Can catch exception, raise a new HTTPException, raise other exception + end + handler -->> client: HTTP error response + end + + operation ->> client: Return response to client + Note over client,operation: Response is already sent, can't change it anymore + opt Tasks + operation -->> tasks: Send background tasks + end + opt Raise other exception + tasks -->> tasks: Handle exceptions in the background task code + end +``` + +/// info | 정보 + +클라이언트에 **하나의 응답** 만 전송됩니다. 이는 오류 응답 중 하나일 수도 있고,*경로 작업*에서 생성된 응답일 수도 있습니다. + +이러한 응답 중 하나가 전송된 후에는 다른 응답을 보낼 수 없습니다. + +/// + +/// tip | 팁 + +이 다이어그램은 `HTTPException`을 보여주지만, `yield`를 사용하는 의존성에서 처리한 예외나 [사용자 정의 예외처리기](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}.를 사용하여 처리한 다른 예외도 발생시킬 수 있습니다. + +어떤 예외가 발생하든, `HTTPException`을 포함하여 yield를 사용하는 의존성으로 전달됩니다. 대부분의 경우 예외를 다시 발생시키거나 새로운 예외를 발생시켜야 합니다. + +/// + +## `yield`, `HTTPException`, `except` 및 백그라운드 작업을 사용하는 의존성 + +/// warning | 경고 + +이러한 기술적 세부 사항은 대부분 필요하지 않으므로 이 섹션을 건너뛰고 아래에서 계속 진행해도 됩니다. + +이러한 세부 정보는 주로 FastAPI 0.106.0 이전 버전에서 `yield`가 있는 의존성의 리소스를 백그라운드 작업에서 사용했던 경우메 유용합니다. + +/// + +### `yield`와 `except`를 사용하는 의존성, 기술 세부사항 + +FastAPI 0.110.0 이전에는 `yield`가 포함된 의존성을 사용한 후 해당 의존성에서 `except`가 포함된 예외를 캡처하고 다시 예외를 발생시키지 않으면 예외가 자동으로 예외 핸들러 또는 내부 서버 오류 핸들러로 발생/전달되었습니다. + +이는 처리기 없이 전달된 예외(내부 서버 오류)에서 처리되지 않은 메모리 소비를 수정하고 일반 파이썬 코드의 동작과 일치하도록 하기 위해 0.110.0 버전에서 변경되었습니다. + +### 백그라운드 작업과 `yield`를 사용하는 의존성, 기술 세부사항 + +FastAPI 0.106.0 이전에는 `yield` 이후에 예외를 발생시키는 것이 불가능했습니다. `yield`가 있는 의존성 종료 코드는 응답이 전송된 이후에 실행되었기 때문에, [예외 처리기](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}가 이미 실행된 상태였습니다. + +이는 주로 백그라운드 작업 내에서 의존성에서 "yield된" 동일한 객체를 사용할 수 있도록 하기 위해 이런 방식으로 설계되었습니다. 종료 코드는 백그라운드 작업이 완료된 후에 실행되었기 때문입니다 + +하지만 이렇게 하면 리소스를 불필요하게 양보한 의존성(예: 데이터베이스 연결)에서 보유하면서 응답이 네트워크를 통해 이동할 때까지 기다리는 것을 의미하기 때문에 FastAPI 0.106.0에서 변경되었습니다. + +/// tip | 팁 + +또한 백그라운드 작업은 일반적으로 자체 리소스(예: 자체 데이터베이스 연결)를 사용하여 별도로 처리해야 하는 독립적인 로직 집합입니다. + +따라서 이렇게 하면 코드가 더 깔끔해집니다. + +/// + +만약 이전에 이러한 동작에 의존했다면, 이제는 백그라운드 작업 내부에서 백그라운드 작업을 위한 리소스를 생성하고, `yield`가 있는 의존성의 리소스에 의존하지 않는 데이터만 내부적으로 사용해야합니다. + +예를 들어, 동일한 데이터베이스 세션을 사용하는 대신, 백그라운드 작업 내부에서 새로운 데이터베이스 세션을 생성하고 이 새로운 세션을 사용하여 데이터베이스에서 객체를 가져와야 합니다. 그리고 데이터베이스 객체를 백그라운드 작업 함수의 매개변수로 직접 전달하는 대신, 해당 객체의 ID를 전달한 다음 백그라운드 작업 함수 내부에서 객체를 다시 가져와야 합니다 + +## 컨텍스트 관리자 + +### "컨텍스트 관리자"란? + +"컨텍스트 관리자"는 Python에서 `with` 문에서 사용할 수 있는 모든 객체를 의미합니다. + +예를 들어, `with`를 사용하여 파일을 읽을 수 있습니다: + +```Python +with open("./somefile.txt") as f: + contents = f.read() + print(contents) +``` + +내부적으로 `open("./somefile.txt")` 는 "컨텍스트 관리자(Context Manager)"라고 불리는 객체를 생성합니다. + +`with` 블록이 끝나면, 예외가 발생했더라도 파일을 닫도록 보장합니다. + +`yield`가 있는 의존성을 생성하면 **FastAPI**는 내부적으로 이를 위한 컨텍스트 매니저를 생성하고 다른 관련 도구들과 결합합니다. + +### `yield`를 사용하는 의존성에서 컨텍스트 관리자 사용하기 + +/// warning | 경고 + +이것은 어느 정도 "고급" 개념입니다. + +**FastAPI**를 처음 시작하는 경우 지금은 이 부분을 건너뛰어도 좋습니다. + +/// + +Python에서는 다음을 통해 컨텍스트 관리자를 생성할 수 있습니다. 두 가지 메서드가 있는 클래스를 생성합니다: `__enter__()` and `__exit__()`. + +**FastAPI**의 `yield`가 있는 의존성 내에서 +`with` 또는 `async with`문을 사용하여 이들을 활용할 수 있습니다: + +{* ../../docs_src/dependencies/tutorial010.py hl[1:9,13] *} + +/// tip | 팁 + +컨텍스트 관리자를 생성하는 또 다른 방법은 다음과 같습니다: + +* `@contextlib.contextmanager` 또는 +* `@contextlib.asynccontextmanager` + +이들은 단일 `yield`가 있는 함수를 꾸미는 데 사용합니다. + +이것이 **FastAPI**가 `yield`가 있는 의존성을 위해 내부적으로 사용하는 방식입니다. + +하지만 FastAPI 의존성에는 이러한 데코레이터를 사용할 필요가 없습니다(그리고 사용해서도 안됩니다). + +FastAPI가 내부적으로 이를 처리해 줄 것입니다. + +/// From 57a9a64435351717ca5a29304cd7736de755c9b0 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 9 Feb 2025 14:54:33 +0000 Subject: [PATCH 085/117] =?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 449b880dc..cda5e9626 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Translations +* 🌐 Add Korean translation for `docs/ko/docs/tutorial/dependencies/dependencies-with-yield.md`. PR [#13257](https://github.com/fastapi/fastapi/pull/13257) by [@11kkw](https://github.com/11kkw). * 🌐 Add Vietnamese translation for `docs/vi/docs/virtual-environments.md`. PR [#13282](https://github.com/fastapi/fastapi/pull/13282) by [@ptt3199](https://github.com/ptt3199). * 🌐 Add Ukrainian translation for `docs/uk/docs/tutorial/static-files.md`. PR [#13285](https://github.com/fastapi/fastapi/pull/13285) by [@valentinDruzhinin](https://github.com/valentinDruzhinin). * 🌐 Add Vietnamese translation for `docs/vi/docs/environment-variables.md`. PR [#13287](https://github.com/fastapi/fastapi/pull/13287) by [@ptt3199](https://github.com/ptt3199). From 126a9b33c92113748de4398c0bcb65600024b03c Mon Sep 17 00:00:00 2001 From: Emil Sadek Date: Mon, 10 Feb 2025 03:18:47 -0800 Subject: [PATCH 086/117] =?UTF-8?q?=F0=9F=93=9D=20Fix=20test=20badge=20(#1?= =?UTF-8?q?3313)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix test badge * Fix test badge in docs --------- Co-authored-by: Emil Sadek Co-authored-by: Alejandra <90076947+alejsdev@users.noreply.github.com> --- 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 f6da22b21..d5d5ced52 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@

- Test + Test Coverage diff --git a/docs/en/docs/index.md b/docs/en/docs/index.md index cbe71c87d..4a2777f25 100644 --- a/docs/en/docs/index.md +++ b/docs/en/docs/index.md @@ -12,7 +12,7 @@

- Test + Test Coverage From eea196f4a505c2584fafda691f2a0033c4db6622 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 10 Feb 2025 11:19:36 +0000 Subject: [PATCH 087/117] =?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 cda5e9626..013cbfc7c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Docs + +* 📝 Fix test badge. PR [#13313](https://github.com/fastapi/fastapi/pull/13313) by [@esadek](https://github.com/esadek). + ### Translations * 🌐 Add Korean translation for `docs/ko/docs/tutorial/dependencies/dependencies-with-yield.md`. PR [#13257](https://github.com/fastapi/fastapi/pull/13257) by [@11kkw](https://github.com/11kkw). From 5cbb81cc2686c50e1fe6da48986fac0498a157dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C6=B0=C6=A1ng=20T=E1=BA=A5n=20Th=C3=A0nh?= <51350651+ptt3199@users.noreply.github.com> Date: Sat, 15 Feb 2025 18:08:22 +0700 Subject: [PATCH 088/117] =?UTF-8?q?=F0=9F=8C=90=20=20Add=20Vietnamese=20tr?= =?UTF-8?q?anslation=20for=20`docs/vi/docs/tutorial/static-files.md`=20(#1?= =?UTF-8?q?1291)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/vi/docs/tutorial/static-files.md | 40 +++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 docs/vi/docs/tutorial/static-files.md diff --git a/docs/vi/docs/tutorial/static-files.md b/docs/vi/docs/tutorial/static-files.md new file mode 100644 index 000000000..ecf8c2485 --- /dev/null +++ b/docs/vi/docs/tutorial/static-files.md @@ -0,0 +1,40 @@ +# Tệp tĩnh (Static Files) + +Bạn có thể triển khai tệp tĩnh tự động từ một thư mục bằng cách sử dụng StaticFiles. + +## Sử dụng `Tệp tĩnh` + +- Nhập `StaticFiles`. +- "Mount" a `StaticFiles()` instance in a specific path. + +{* ../../docs_src/static_files/tutorial001.py hl[2,6] *} + +/// note | Chi tiết kỹ thuật + +Bạn cũng có thể sử dụng `from starlette.staticfiles import StaticFiles`. + +**FastAPI** cung cấp cùng `starlette.staticfiles` như `fastapi.staticfiles` giúp đơn giản hóa việc sử dụng, nhưng nó thực sự đến từ Starlette. + +/// + +### "Mounting" là gì + +"Mounting" có nghĩa là thêm một ứng dụng "độc lập" hoàn chỉnh vào một đường dẫn cụ thể, sau đó ứng dụng đó sẽ chịu trách nhiệm xử lý tất cả các đường dẫn con. + +Điều này khác với việc sử dụng `APIRouter` vì một ứng dụng được gắn kết là hoàn toàn độc lập. OpenAPI và tài liệu từ ứng dụng chính của bạn sẽ không bao gồm bất kỳ thứ gì từ ứng dụng được gắn kết, v.v. + +Bạn có thể đọc thêm về điều này trong [Hướng dẫn Người dùng Nâng cao](../advanced/index.md){.internal-link target=\_blank}. + +## Chi tiết + +Đường dẫn đầu tiên `"/static"` là đường dẫn con mà "ứng dụng con" này sẽ được "gắn" vào. Vì vậy, bất kỳ đường dẫn nào bắt đầu bằng `"/static"` sẽ được xử lý bởi nó. + +Đường dẫn `directory="static"` là tên của thư mục chứa tệp tĩnh của bạn. + +Tham số `name="static"` đặt tên cho nó để có thể được sử dụng bên trong **FastAPI**. + +Tất cả các tham số này có thể khác với `static`, điều chỉnh chúng với phù hợp với ứng dụng của bạn. + +## 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. From 3aeaa0a6a826466669d9faf13762524ee795e8b2 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 15 Feb 2025 11:08:48 +0000 Subject: [PATCH 089/117] =?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 013cbfc7c..7e8f57054 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Translations +* 🌐 Add Vietnamese translation for `docs/vi/docs/tutorial/static-files.md`. PR [#11291](https://github.com/fastapi/fastapi/pull/11291) by [@ptt3199](https://github.com/ptt3199). * 🌐 Add Korean translation for `docs/ko/docs/tutorial/dependencies/dependencies-with-yield.md`. PR [#13257](https://github.com/fastapi/fastapi/pull/13257) by [@11kkw](https://github.com/11kkw). * 🌐 Add Vietnamese translation for `docs/vi/docs/virtual-environments.md`. PR [#13282](https://github.com/fastapi/fastapi/pull/13282) by [@ptt3199](https://github.com/ptt3199). * 🌐 Add Ukrainian translation for `docs/uk/docs/tutorial/static-files.md`. PR [#13285](https://github.com/fastapi/fastapi/pull/13285) by [@valentinDruzhinin](https://github.com/valentinDruzhinin). From 03b24b5a524dc6b27e10a58dac9538b737f0defa Mon Sep 17 00:00:00 2001 From: ScrollDude <93028356+Stepakinoyan@users.noreply.github.com> Date: Sat, 15 Feb 2025 20:15:23 +0900 Subject: [PATCH 090/117] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20translat?= =?UTF-8?q?ion=20for=20`docs/ru/docs/advanced/response-cookies.md`=20(#133?= =?UTF-8?q?27)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ru/docs/advanced/response-cookies.md | 48 +++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 docs/ru/docs/advanced/response-cookies.md diff --git a/docs/ru/docs/advanced/response-cookies.md b/docs/ru/docs/advanced/response-cookies.md new file mode 100644 index 000000000..e04ff577c --- /dev/null +++ b/docs/ru/docs/advanced/response-cookies.md @@ -0,0 +1,48 @@ + +# Cookies в ответе + +## Использование параметра `Response` + +Вы можете объявить параметр типа `Response` в вашей функции эндпоинта. + +Затем установить cookies в этом временном объекте ответа. + +{* ../../docs_src/response_cookies/tutorial002.py hl[1, 8:9] *} + +После этого можно вернуть любой объект, как и раньше (например, `dict`, объект модели базы данных и так далее). + +Если вы указали `response_model`, он всё равно будет использоваться для фильтрации и преобразования возвращаемого объекта. + +**FastAPI** извлечет cookies (а также заголовки и коды состояния) из временного ответа и включит их в окончательный ответ, содержащий ваше возвращаемое значение, отфильтрованное через `response_model`. + +Вы также можете объявить параметр типа Response в зависимостях и устанавливать cookies (и заголовки) там. + +## Возвращение `Response` напрямую + +Вы также можете установить cookies, если возвращаете `Response` напрямую в вашем коде. + +Для этого создайте объект `Response`, как описано в разделе [Возвращение ответа напрямую](response-directly.md){.target=_blank}. + +Затем установите cookies и верните этот объект: + +{* ../../docs_src/response_cookies/tutorial001.py hl[10:12] *} + +/// tip | Примечание +Имейте в виду, что если вы возвращаете ответ напрямую, вместо использования параметра `Response`, **FastAPI** отправит его без дополнительной обработки. + +Убедитесь, что ваши данные имеют корректный тип. Например, они должны быть совместимы с JSON, если вы используете `JSONResponse`. + +Также убедитесь, что вы не отправляете данные, которые должны были быть отфильтрованы через `response_model`. +/// + +### Дополнительная информация + +/// note | Технические детали +Вы также можете использовать `from starlette.responses import Response` или `from starlette.responses import JSONResponse`. + +**FastAPI** предоставляет `fastapi.responses`, которые являются теми же объектами, что и `starlette.responses`, просто для удобства. Однако большинство доступных типов ответов поступает непосредственно из **Starlette**. + +Для установки заголовков и cookies `Response` используется часто, поэтому **FastAPI** также предоставляет его через `fastapi.responses`. +/// + +Чтобы увидеть все доступные параметры и настройки, ознакомьтесь с документацией Starlette. From 15afe2e301d06a64a356181a4e7216818316e731 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 15 Feb 2025 11:15:45 +0000 Subject: [PATCH 091/117] =?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 7e8f57054..0166e6cc5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Translations +* 🌐 Add Russian translation for `docs/ru/docs/advanced/response-cookies.md`. PR [#13327](https://github.com/fastapi/fastapi/pull/13327) by [@Stepakinoyan](https://github.com/Stepakinoyan). * 🌐 Add Vietnamese translation for `docs/vi/docs/tutorial/static-files.md`. PR [#11291](https://github.com/fastapi/fastapi/pull/11291) by [@ptt3199](https://github.com/ptt3199). * 🌐 Add Korean translation for `docs/ko/docs/tutorial/dependencies/dependencies-with-yield.md`. PR [#13257](https://github.com/fastapi/fastapi/pull/13257) by [@11kkw](https://github.com/11kkw). * 🌐 Add Vietnamese translation for `docs/vi/docs/virtual-environments.md`. PR [#13282](https://github.com/fastapi/fastapi/pull/13282) by [@ptt3199](https://github.com/ptt3199). From 39b4692525acfffdfba8e12ee674d9590b02b19f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lee=20Yesong=20=28=EC=9D=B4=EC=98=88=EC=86=A1=29?= Date: Sat, 15 Feb 2025 20:19:12 +0900 Subject: [PATCH 092/117] =?UTF-8?q?=F0=9F=8C=90=20Update=20Korean=20transl?= =?UTF-8?q?ation=20for=20`docs/ko/docs/tutorial/security/simple-oauth2.md`?= =?UTF-8?q?=20(#13335)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ko/docs/tutorial/security/simple-oauth2.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/ko/docs/tutorial/security/simple-oauth2.md b/docs/ko/docs/tutorial/security/simple-oauth2.md index ddc7430af..f10c4f588 100644 --- a/docs/ko/docs/tutorial/security/simple-oauth2.md +++ b/docs/ko/docs/tutorial/security/simple-oauth2.md @@ -32,7 +32,7 @@ OAuth2는 (우리가 사용하고 있는) "패스워드 플로우"을 사용할 * `instagram_basic`은 페이스북/인스타그램에서 사용합니다. * `https://www.googleapis.com/auth/drive`는 Google에서 사용합니다. -/// 정보 +/// info | 정보 OAuth2에서 "범위"는 필요한 특정 권한을 선언하는 문자열입니다. @@ -61,7 +61,7 @@ OAuth2의 경우 문자열일 뿐입니다. * `scope`는 선택적인 필드로 공백으로 구분된 문자열로 구성된 큰 문자열입니다. * `grant_type`(선택적으로 사용). -/// 팁 +/// tip | 팁 OAuth2 사양은 실제로 `password`라는 고정 값이 있는 `grant_type` 필드를 *요구*하지만 `OAuth2PasswordRequestForm`은 이를 강요하지 않습니다. @@ -72,7 +72,7 @@ OAuth2 사양은 실제로 `password`라는 고정 값이 있는 `grant_type` * `client_id`(선택적으로 사용) (예제에서는 필요하지 않습니다). * `client_secret`(선택적으로 사용) (예제에서는 필요하지 않습니다). -/// 정보 +/// info | 정보 `OAuth2PasswordRequestForm`은 `OAuth2PasswordBearer`와 같이 **FastAPI**에 대한 특수 클래스가 아닙니다. @@ -86,7 +86,7 @@ OAuth2 사양은 실제로 `password`라는 고정 값이 있는 `grant_type` ### 폼 데이터 사용하기 -/// 팁 +/// tip | 팁 종속성 클래스 `OAuth2PasswordRequestForm`의 인스턴스에는 공백으로 구분된 긴 문자열이 있는 `scope` 속성이 없고 대신 전송된 각 범위에 대한 실제 문자열 목록이 있는 `scopes` 속성이 있습니다. @@ -126,7 +126,7 @@ OAuth2 사양은 실제로 `password`라는 고정 값이 있는 `grant_type` 따라서 해커는 다른 시스템에서 동일한 암호를 사용하려고 시도할 수 없습니다(많은 사용자가 모든 곳에서 동일한 암호를 사용하므로 이는 위험할 수 있습니다). -//// tab | P파이썬 3.7 이상 +//// tab | 파이썬 3.7 이상 {* ../../docs_src/security/tutorial003.py hl[80:83] *} @@ -150,7 +150,7 @@ UserInDB( ) ``` -/// 정보 +/// info | 정보 `**user_dict`에 대한 자세한 설명은 [**추가 모델** 문서](../extra-models.md#about-user_indict){.internal-link target=_blank}를 다시 읽어봅시다. @@ -166,7 +166,7 @@ UserInDB( 이 간단한 예제에서는 완전히 안전하지 않고, 동일한 `username`을 토큰으로 반환합니다. -/// 팁 +/// tip | 팁 다음 장에서는 패스워드 해싱 및 JWT 토큰을 사용하여 실제 보안 구현을 볼 수 있습니다. @@ -176,7 +176,7 @@ UserInDB( {* ../../docs_src/security/tutorial003.py hl[85] *} -/// 팁 +/// tip | 팁 사양에 따라 이 예제와 동일하게 `access_token` 및 `token_type`이 포함된 JSON을 반환해야 합니다. @@ -202,7 +202,7 @@ UserInDB( {* ../../docs_src/security/tutorial003.py hl[58:66,69:72,90] *} -/// 정보 +/// info | 정보 여기서 반환하는 값이 `Bearer`인 추가 헤더 `WWW-Authenticate`도 사양의 일부입니다. From fc94d904c969ba1b85aa9d9a966ad8dee27e7652 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 15 Feb 2025 11:19:48 +0000 Subject: [PATCH 093/117] =?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 0166e6cc5..8fcf68e5f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Translations +* 🌐 Update Korean translation for `docs/ko/docs/tutorial/security/simple-oauth2.md`. PR [#13335](https://github.com/fastapi/fastapi/pull/13335) by [@yes0ng](https://github.com/yes0ng). * 🌐 Add Russian translation for `docs/ru/docs/advanced/response-cookies.md`. PR [#13327](https://github.com/fastapi/fastapi/pull/13327) by [@Stepakinoyan](https://github.com/Stepakinoyan). * 🌐 Add Vietnamese translation for `docs/vi/docs/tutorial/static-files.md`. PR [#11291](https://github.com/fastapi/fastapi/pull/11291) by [@ptt3199](https://github.com/ptt3199). * 🌐 Add Korean translation for `docs/ko/docs/tutorial/dependencies/dependencies-with-yield.md`. PR [#13257](https://github.com/fastapi/fastapi/pull/13257) by [@11kkw](https://github.com/11kkw). From 030012bf4c225a2b5dbb3c6b3443bd2051e614f8 Mon Sep 17 00:00:00 2001 From: 11kkw <11kkw17@gmail.com> Date: Sat, 15 Feb 2025 20:21:20 +0900 Subject: [PATCH 094/117] =?UTF-8?q?=F0=9F=8C=90=20Add=20Korean=20translati?= =?UTF-8?q?on=20for=20`docs/ko/docs/advanced/custom-response.md`=20(#13265?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ko/docs/advanced/custom-response.md | 313 +++++++++++++++++++++++ 1 file changed, 313 insertions(+) create mode 100644 docs/ko/docs/advanced/custom-response.md diff --git a/docs/ko/docs/advanced/custom-response.md b/docs/ko/docs/advanced/custom-response.md new file mode 100644 index 000000000..2001956fa --- /dev/null +++ b/docs/ko/docs/advanced/custom-response.md @@ -0,0 +1,313 @@ +# 사용자 정의 응답 - HTML, Stream, 파일, 기타 + +기본적으로, **FastAPI** 응답을 `JSONResponse`를 사용하여 반환합니다. + +이를 재정의 하려면 [응답을 직접 반환하기](response-directly.md){.internal-link target=_blank}에서 본 것처럼 `Response`를 직접 반환하면 됩니다. + +그러나 `Response` (또는 `JSONResponse`와 같은 하위 클래스)를 직접 반환하면, 데이터가 자동으로 변환되지 않으며 (심지어 `response_model`을 선언했더라도), 문서화가 자동으로 생성되지 않습니다(예를 들어, 생성된 OpenAPI의 일부로 HTTP 헤더 `Content-Type`에 특정 "미디어 타입"을 포함하는 경우). + +하지만 *경로 작업 데코레이터*에서 `response_class` 매개변수를 사용하여 원하는 `Response`(예: 모든 `Response` 하위 클래스)를 선언할 수도 있습니다. + +*경로 작업 함수*에서 반환하는 내용은 해당 `Response`안에 포함됩니다. + +그리고 만약 그 `Response`가 `JSONResponse`와 `UJSONResponse`의 경우 처럼 JSON 미디어 타입(`application/json`)을 가지고 있다면, *경로 작업 데코레이터*에서 선언한 Pydantic의 `response_model`을 사용해 자동으로 변환(및 필터링) 됩니다. + +/// note | 참고 + +미디어 타입이 없는 응답 클래스를 사용하는 경우, FastAPI는 응답에 내용이 없을 것으로 예상하므로 생성된 OpenAPI 문서에서 응답 형식을 문서화하지 않습니다. + +/// + +## `ORJSONResponse` 사용하기 + +예를 들어, 성능을 극대화하려는 경우, orjson을 설치하여 사용하고 응답을 `ORJSONResponse`로 설정할 수 있습니다. + +사용하고자 하는 `Response` 클래스(하위 클래스)를 임포트한 후, **경로 작업 데코레이터*에서 선언하세요. + +대규모 응답의 경우, 딕셔너리를 반환하는 것보다 `Response`를 반환하는 것이 훨씬 빠릅니다. + +이유는 기본적으로, FastAPI가 내부의 모든 항목을 검사하고 JSON으로 직렬화할 수 있는지 확인하기 때문입니다. 이는 사용자 안내서에서 설명된 [JSON 호환 가능 인코더](../tutorial/encoder.md){.internal-link target=_blank}를 사용하는 방식과 동일합니다. 이를 통해 데이터베이스 모델과 같은 **임의의 객체**를 반환할 수 있습니다. + +하지만 반환하는 내용이 **JSON으로 직렬화 가능**하다고 확신하는 경우, 해당 내용을 응답 클래스에 직접 전달할 수 있으며, FastAPI가 반환 내용을 `jsonable_encoder`를 통해 처리한 뒤 응답 클래스에 전달하는 오버헤드를 피할 수 있습니다. + +{* ../../docs_src/custom_response/tutorial001b.py hl[2,7] *} + +/// info | 정보 + +`response_class` 매개변수는 응답의 "미디어 타입"을 정의하는 데에도 사용됩니다. + +이 경우, HTTP 헤더 `Content-Type`은 `application/json`으로 설정됩니다. + +그리고 이는 OpenAPI에 그대로 문서화됩니다. + +/// + +/// tip | 팁 + +`ORJSONResponse`는 FastAPI에서만 사용할 수 있고 Starlette에서는 사용할 수 없습니다. + +/// + +## HTML 응답 + +**FastAPI**에서 HTML 응답을 직접 반환하려면 `HTMLResponse`를 사용하세요. + +* `HTMLResponse`를 임포트 합니다. +* *경로 작업 데코레이터*의 `response_class` 매개변수로 `HTMLResponse`를 전달합니다. + +{* ../../docs_src/custom_response/tutorial002.py hl[2,7] *} + +/// info | 정보 + +`response_class` 매개변수는 응답의 "미디어 타입"을 정의하는 데에도 사용됩니다. + +이 경우, HTTP 헤더 `Content-Type`은 `text/html`로 설정됩니다. + +그리고 이는 OpenAPI에 그대로 문서화 됩니다. + +/// + +### `Response` 반환하기 + +[응답을 직접 반환하기](response-directly.md){.internal-link target=_blank}에서 본 것 처럼, *경로 작업*에서 응답을 직접 반환하여 재정의할 수도 있습니다. + +위의 예제와 동일하게 `HTMLResponse`를 반환하는 코드는 다음과 같을 수 있습니다: + +{* ../../docs_src/custom_response/tutorial003.py hl[2,7,19] *} + +/// warning | 경고 + +*경로 작업 함수*에서 직접 반환된 `Response`는 OpenAPI에 문서화되지 않습니다(예를들어, `Content-Type`이 문서화되지 않음) 자동 대화형 문서에서도 표시되지 않습니다. + +/// + +/// info | 정보 + +물론 실제 `Content-Type` 헤더, 상태 코드 등은 반환된 `Response` 객체에서 가져옵니다. + +/// + +### OpenAPI에 문서화하고 `Response` 재정의 하기 + +함수 내부에서 응답을 재정의하면서 동시에 OpenAPI에서 "미디어 타입"을 문서화하고 싶다면, `response_class` 매게변수를 사용하면서 `Response` 객체를 반환할 수 있습니다. + +이 경우 `response_class`는 OpenAPI *경로 작업*을 문서화하는 데만 사용되고, 실제로는 여러분이 반환한 `Response`가 그대로 사용됩니다. + +### `HTMLResponse`직접 반환하기 + +예를 들어, 다음과 같이 작성할 수 있습니다: + +{* ../../docs_src/custom_response/tutorial004.py hl[7,21,23] *} + +이 예제에서, `generate_html_response()` 함수는 HTML을 `str`로 반환하는 대신 이미 `Response`를 생성하고 반환합니다. + +`generate_html_response()`를 호출한 결과를 반환함으로써, 기본적인 **FastAPI** 기본 동작을 재정의 하는 `Response`를 이미 반환하고 있습니다. + +하지만 `response_class`에 `HTMLResponse`를 함께 전달했기 때문에, FastAPI는 이를 OpenAPI 및 대화형 문서에서 `text/html`로 HTML을 문서화 하는 방법을 알 수 있습니다. + + + +## 사용 가능한 응답들 + +다음은 사용할 수 있는 몇가지 응답들 입니다. + +`Response`를 사용하여 다른 어떤 것도 반환 할수 있으며, 직접 하위 클래스를 만들 수도 있습니다. + +/// note | 기술 세부사항 + +`from starlette.responses import HTMLResponse`를 사용할 수도 있습니다. + +**FastAPI**는 개발자인 여러분의 편의를 위해 `starlette.responses`를 `fastapi.responses`로 제공 하지만, 대부분의 사용 가능한 응답은 Starlette에서 직접 가져옵니다. + +/// + +### `Response` + +기본 `Response` 클래스는 다른 모든 응답 클래스의 부모 클래스 입니다. + +이 클래스를 직접 반환할 수 있습니다. + +다음 매개변수를 받을 수 있습니다: + +* `content` - `str` 또는 `bytes`. +* `status_code` - HTTP 상태코드를 나타내는 `int`. +* `headers` - 문자열로 이루어진 `dict`. +* `media_type` - 미디어 타입을 나타내는 `str` 예: `"text/html"`. + +FastAPI (실제로는 Starlette)가 자동으로 `Content-Length` 헤더를 포함시킵니다. 또한 `media_type`에 기반하여 `Content-Type` 헤더를 포함하며, 텍스트 타입의 경우 문자 집합을 추가 합니다. + +{* ../../docs_src/response_directly/tutorial002.py hl[1,18] *} + +### `HTMLResponse` + +텍스트 또는 바이트를 받아 HTML 응답을 반환합니다. 위에서 설명한 내용과 같습니다. + +### `PlainTextResponse` + +텍스트 또는 바이트를 받아 일반 텍스트 응답을 반환합니다. + +{* ../../docs_src/custom_response/tutorial005.py hl[2,7,9] *} + +### `JSONResponse` + +데이터를 받아 `application/json`으로 인코딩된 응답을 반환합니다. + +이는 위에서 설명했듯이 **FastAPI**에서 기본적으로 사용되는 응답 형식입니다. + +### `ORJSONResponse` + + `orjson`을 사용하여 빠른 JSON 응답을 제공하는 대안입니다. 위에서 설명한 내용과 같습니다. + +/// info | 정보 + +이를 사용하려면 `orjson`을 설치해야합니다. 예: `pip install orjson`. + +/// + +### `UJSONResponse` + +`ujson`을 사용한 또 다른 JSON 응답 형식입니다. + +/// info | 정보 + +이 응답을 사용하려면 `ujson`을 설치해야합니다. 예: 'pip install ujson`. + +/// + +/// warning | 경고 + +`ujson` 은 일부 예외 경우를 처리하는 데 있어 Python 내장 구현보다 덜 엄격합니다. + +/// + +{* ../../docs_src/custom_response/tutorial001.py hl[2,7] *} + +/// tip | 팁 + +`ORJSONResponse`가 더 빠른 대안일 가능성이 있습니다. + +/// + +### `RedirectResponse` + +HTTP 리디렉션 응답을 반환합니다. 기본적으로 상태 코드는 307(임시 리디렉션)으로 설정됩니다. + +`RedirectResponse`를 직접 반환할 수 있습니다. + +{* ../../docs_src/custom_response/tutorial006.py hl[2,9] *} + +--- + +또는 `response_class` 매개변수에서 사용할 수도 있습니다: + + +{* ../../docs_src/custom_response/tutorial006b.py hl[2,7,9] *} + +이 경우, *경로 작업* 함수에서 URL을 직접 반환할 수 있습니다. + +이 경우, 사용되는 `status_code`는 `RedirectResponse`의 기본값인 `307` 입니다. + +--- + +`status_code` 매개변수를 `response_class` 매개변수와 함께 사용할 수도 있습니다: + +{* ../../docs_src/custom_response/tutorial006c.py hl[2,7,9] *} + +### `StreamingResponse` + +비동기 제너레이터 또는 일반 제너레이터/이터레이터를 받아 응답 본문을 스트리밍 합니다. + +{* ../../docs_src/custom_response/tutorial007.py hl[2,14] *} + +#### 파일과 같은 객체를 사용한 `StreamingResponse` + +파일과 같은 객체(예: `open()`으로 반환된 객체)가 있는 경우, 해당 파일과 같은 객체를 반복(iterate)하는 제너레이터 함수를 만들 수 있습니다. + +이 방식으로, 파일 전체를 메모리에 먼저 읽어들일 필요 없이, 제너레이터 함수를 `StreamingResponse`에 전달하여 반환할 수 있습니다. + +이 방식은 클라우드 스토리지, 비디오 처리 등의 다양한 라이브러리와 함께 사용할 수 있습니다. + +{* ../../docs_src/custom_response/tutorial008.py hl[2,10:12,14] *} + +1. 이것이 제너레이터 함수입니다. `yield` 문을 포함하고 있으므로 "제너레이터 함수"입니다. +2. `with` 블록을 사용함으로써, 제너레이터 함수가 완료된 후 파일과 같은 객체가 닫히도록 합니다. 즉, 응답 전송이 끝난 후 닫힙니다. +3. 이 `yield from`은 함수가 `file_like`라는 객체를 반복(iterate)하도록 합니다. 반복된 각 부분은 이 제너레이터 함수(`iterfile`)에서 생성된 것처럼 `yield` 됩니다. + + 이렇게 하면 "생성(generating)" 작업을 내부적으로 다른 무언가에 위임하는 제너레이터 함수가 됩니다. + + 이 방식을 사용하면 `with` 블록 안에서 파일을 열 수 있어, 작업이 완료된 후 파일과 같은 객체가 닫히는 것을 보장할 수 있습니다. + +/// tip | 팁 + +여기서 표준 `open()`을 사용하고 있기 때문에 `async`와 `await`를 지원하지 않습니다. 따라서 경로 작업은 일반 `def`로 선언합니다. + +/// + +### `FileResponse` + +파일을 비동기로 스트리밍하여 응답합니다. + +다른 응답 유형과는 다른 인수를 사용하여 객체를 생성합니다: + +* `path` - 스트리밍할 파일의 경로. +* `headers` - 딕셔너리 형식의 사용자 정의 헤더. +* `media_type` - 미디어 타입을 나타내는 문자열. 설정되지 않은 경우 파일 이름이나 경로를 사용하여 추론합니다. +* `filename` - 설정된 경우 응답의 `Content-Disposition`에 포함됩니다. + +파일 응답에는 적절한 `Content-Length`, `Last-Modified`, 및 `ETag` 헤더가 포함됩니다. + +{* ../../docs_src/custom_response/tutorial009.py hl[2,10] *} + +또한 `response_class` 매개변수를 사용할 수도 있습니다: + +{* ../../docs_src/custom_response/tutorial009b.py hl[2,8,10] *} + +이 경우, 경로 작업 함수에서 파일 경로를 직접 반환할 수 있습니다. + +## 사용자 정의 응답 클래스 + +`Response`를 상속받아 사용자 정의 응답 클래스를 생성하고 사용할 수 있습니다. + +예를 들어, 포함된 `ORJSONResponse` 클래스에서 사용되지 않는 설정으로 orjson을 사용하고 싶다고 가정해봅시다. + +만약 들여쓰기 및 포맷된 JSON을 반환하고 싶다면, `orjson.OPT_INDENT_2` 옵션을 사용할 수 있습니다. + +`CustomORJSONResponse`를 생성할 수 있습니다. 여기서 핵심은 `Response.render(content)` 메서드를 생성하여 내용을 `bytes`로 반환하는 것입니다: + +{* ../../docs_src/custom_response/tutorial009c.py hl[9:14,17] *} + +이제 다음 대신: + +```json +{"message": "Hello World"} +``` + +이 응답은 이렇게 반환됩니다: + +```json +{ + "message": "Hello World" +} +``` + +물론 JSON 포맷팅보다 더 유용하게 활용할 방법을 찾을 수 있을 것입니다. 😉 + +## 기본 응답 클래스 + +**FastAPI** 클래스 객체 또는 `APIRouter`를 생성할 때 기본적으로 사용할 응답 클래스를 지정할 수 있습니다. + +이를 정의하는 매개변수는 `default_response_class`입니다. + +아래 예제에서 **FastAPI**는 모든 경로 작업에서 기본적으로 `JSONResponse` 대신 `ORJSONResponse`를 사용합니다. + +{* ../../docs_src/custom_response/tutorial010.py hl[2,4] *} + +/// tip | 팁 + +여전히 이전처럼 *경로 작업*에서 `response_class`를 재정의할 수 있습니다. + +/// + +## 추가 문서화 + +OpenAPI에서 `responses`를 사용하여 미디어 타입 및 기타 세부 정보를 선언할 수도 있습니다: [OpenAPI에서 추가 응답](additional-responses.md){.internal-link target=_blank}. From d51936754d9ab943181472edadc869c317d8a5b0 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 15 Feb 2025 11:21:51 +0000 Subject: [PATCH 095/117] =?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 8fcf68e5f..ffb5224c1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Translations +* 🌐 Add Korean translation for `docs/ko/docs/advanced/custom-response.md`. PR [#13265](https://github.com/fastapi/fastapi/pull/13265) by [@11kkw](https://github.com/11kkw). * 🌐 Update Korean translation for `docs/ko/docs/tutorial/security/simple-oauth2.md`. PR [#13335](https://github.com/fastapi/fastapi/pull/13335) by [@yes0ng](https://github.com/yes0ng). * 🌐 Add Russian translation for `docs/ru/docs/advanced/response-cookies.md`. PR [#13327](https://github.com/fastapi/fastapi/pull/13327) by [@Stepakinoyan](https://github.com/Stepakinoyan). * 🌐 Add Vietnamese translation for `docs/vi/docs/tutorial/static-files.md`. PR [#11291](https://github.com/fastapi/fastapi/pull/11291) by [@ptt3199](https://github.com/ptt3199). From 10a13d05c4d31978e0255f76baf54b394430c89c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 15 Feb 2025 11:22:43 +0000 Subject: [PATCH 096/117] =?UTF-8?q?=E2=AC=86=20Bump=20cloudflare/wrangler-?= =?UTF-8?q?action=20from=203.13=20to=203.14=20(#13350)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [cloudflare/wrangler-action](https://github.com/cloudflare/wrangler-action) from 3.13 to 3.14. - [Release notes](https://github.com/cloudflare/wrangler-action/releases) - [Changelog](https://github.com/cloudflare/wrangler-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/cloudflare/wrangler-action/compare/v3.13...v3.14) --- updated-dependencies: - dependency-name: cloudflare/wrangler-action 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> --- .github/workflows/deploy-docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index d9ed61910..0b3096143 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -64,7 +64,7 @@ jobs: BRANCH: ${{ ( github.event.workflow_run.head_repository.full_name == github.repository && github.event.workflow_run.head_branch == 'master' && 'main' ) || ( github.event.workflow_run.head_sha ) }} # TODO: Use v3 when it's fixed, probably in v3.11 # https://github.com/cloudflare/wrangler-action/issues/307 - uses: cloudflare/wrangler-action@v3.13 + uses: cloudflare/wrangler-action@v3.14 # uses: cloudflare/wrangler-action@v3 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} From b81d29fc0060cb97f5fffb70344c640f3f98a9ad Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 15 Feb 2025 11:23:13 +0000 Subject: [PATCH 097/117] =?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 ffb5224c1..c69b20e66 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -30,6 +30,7 @@ hide: ### Internal +* ⬆ Bump cloudflare/wrangler-action from 3.13 to 3.14. PR [#13350](https://github.com/fastapi/fastapi/pull/13350) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-material from 9.5.18 to 9.6.1. PR [#13301](https://github.com/fastapi/fastapi/pull/13301) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump pillow from 11.0.0 to 11.1.0. PR [#13300](https://github.com/fastapi/fastapi/pull/13300) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👥 Update FastAPI People - Sponsors. PR [#13295](https://github.com/fastapi/fastapi/pull/13295) by [@tiangolo](https://github.com/tiangolo). From ac893a4446c3157fdf0ac4bbabcda08031676ea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hyogeun=20Oh=20=28=EC=98=A4=ED=9A=A8=EA=B7=BC=29?= Date: Sat, 15 Feb 2025 20:37:58 +0900 Subject: [PATCH 098/117] =?UTF-8?q?=F0=9F=8C=90=20Update=20Korean=20transl?= =?UTF-8?q?ation=20for=20`docs/ko/docs/help-fastapi.md`=20(#13262)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ko/docs/help-fastapi.md | 287 ++++++++++++++++++++++++----------- 1 file changed, 197 insertions(+), 90 deletions(-) diff --git a/docs/ko/docs/help-fastapi.md b/docs/ko/docs/help-fastapi.md index 932952b4a..06435d4bb 100644 --- a/docs/ko/docs/help-fastapi.md +++ b/docs/ko/docs/help-fastapi.md @@ -1,162 +1,269 @@ -* # FastAPI 지원 - 도움말 받기 +# FastAPI 지원 - 도움 받기 - **FastAPI** 가 마음에 드시나요? +**FastAPI** 가 마음에 드시나요? - FastAPI, 다른 사용자, 개발자를 응원하고 싶으신가요? +FastAPI, 다른 사용자, 개발자를 응원하고 싶으신가요? - 혹은 **FastAPI** 에 대해 도움이 필요하신가요? +혹은 **FastAPI** 에 대해 도움이 필요하신가요? - 아주 간단하게 응원할 수 있습니다 (몇 번의 클릭만으로). +아주 간단하게 응원할 수 있습니다 (몇 번의 클릭만으로). - 또한 도움을 받을 수 있는 방법도 몇 가지 있습니다. +또한 도움을 받을 수 있는 방법도 몇 가지 있습니다. - ## 뉴스레터 구독 +## 뉴스레터 구독 - [**FastAPI와 친구** 뉴스레터](https://github.com/fastapi/fastapi/blob/master/newsletter)를 구독하여 최신 정보를 유지할 수 있습니다{.internal-link target=_blank}: +[**FastAPI and friends** 뉴스레터](newsletter.md){.internal-link target=\_blank}를 구독하여 최신 정보를 유지할 수 있습니다: - - FastAPI 와 그 친구들에 대한 뉴스 🚀 - - 가이드 📝 - - 특징 ✨ - - 획기적인 변화 🚨 - - 팁과 요령 ✅ +* FastAPI and friends에 대한 뉴스 🚀 +* 가이드 📝 +* 기능 ✨ +* 획기적인 변화 🚨 +* 팁과 요령 ✅ - ## 트위터에서 FastAPI 팔로우하기 +## 트위터에서 FastAPI 팔로우하기 - [Follow @fastapi on **Twitter**](https://twitter.com/fastapi) 를 팔로우하여 **FastAPI** 에 대한 최신 뉴스를 얻을 수 있습니다. 🐦 +**Twitter**의 @fastapi를 팔로우하여 **FastAPI** 에 대한 최신 뉴스를 얻을 수 있습니다. 🐦 - ## Star **FastAPI** in GitHub +## Star **FastAPI** in GitHub - GitHub에서 FastAPI에 "star"를 붙일 수 있습니다(오른쪽 상단의 star 버튼을 클릭): https://github.com/fastapi/fastapi. ⭐️ +GitHub에서 FastAPI에 "star"를 붙일 수 있습니다 (오른쪽 상단의 star 버튼을 클릭): https://github.com/fastapi/fastapi. ⭐️ - 스타를 늘림으로써, 다른 사용자들이 좀 더 쉽게 찾을 수 있고, 많은 사람들에게 유용한 것임을 나타낼 수 있습니다. +스타를 늘림으로써, 다른 사용자들이 좀 더 쉽게 찾을 수 있고, 많은 사람들에게 유용한 것임을 나타낼 수 있습니다. - ## GitHub 저장소에서 릴리즈 확인 +## GitHub 저장소에서 릴리즈 확인 - GitHub에서 FastAPI를 "watch"할 수 있습니다 (오른쪽 상단 watch 버튼을 클릭): https://github.com/fastapi/fastapi. 👀 +GitHub에서 FastAPI를 "watch"할 수 있습니다 (오른쪽 상단 watch 버튼을 클릭): https://github.com/fastapi/fastapi. 👀 - 여기서 "Releases only"을 선택할 수 있습니다. +여기서 "Releases only"을 선택할 수 있습니다. - 이렇게하면, **FastAPI** 의 버그 수정 및 새로운 기능의 구현 등의 새로운 자료 (최신 버전)이 있을 때마다 (이메일) 통지를 받을 수 있습니다. +이렇게하면, **FastAPI** 의 버그 수정 및 새로운 기능의 구현 등의 새로운 자료 (최신 버전)이 있을 때마다 (이메일) 통지를 받을 수 있습니다. - ## 개발자와의 연결 +## 개발자와의 연결 - 개발자인 [me (Sebastián Ramírez / `tiangolo`)](https://tiangolo.com/) 와 연락을 취할 수 있습니다. +개발자(Sebastián Ramírez / `tiangolo`)와 연락을 취할 수 있습니다. - 여러분은 할 수 있습니다: +여러분은 할 수 있습니다: - - [**GitHub**에서 팔로우하기](https://github.com/tiangolo). - - 당신에게 도움이 될 저의 다른 오픈소스 프로젝트를 확인하십시오. - - 새로운 오픈소스 프로젝트를 만들었을 때 확인하려면 팔로우 하십시오. +* **GitHub**에서 팔로우하기.. + * 당신에게 도움이 될 저의 다른 오픈소스 프로젝트를 확인하십시오. + * 새로운 오픈소스 프로젝트를 만들었을 때 확인하려면 팔로우 하십시오. +* **Twitter** 또는 Mastodon에서 팔로우하기. + * FastAPI의 사용 용도를 알려주세요 (그것을 듣는 것을 좋아합니다). + * 발표나 새로운 툴 출시 소식을 받아보십시오. + * **Twitter**의 @fastapi를 팔로우 (별도 계정에서) 할 수 있습니다. +* **LinkedIn**에서 팔로우하기.. + * 새로운 툴의 발표나 출시 소식을 받아보십시오. (단, Twitter를 더 자주 사용합니다 🤷‍♂). +* **Dev.to** 또는 **Medium**에서 제가 작성한 내용을 읽어 보십시오 (또는 팔로우). + * 다른 기사나 아이디어들을 읽고, 제가 만들어왔던 툴에 대해서도 읽으십시오. + * 새로운 기사를 읽기 위해 팔로우 하십시오. - - [**Twitter**에서 팔로우하기](https://twitter.com/tiangolo). - - FastAPI의 사용 용도를 알려주세요 (그것을 듣는 것을 좋아합니다). - - 발표 또는 새로운 툴 출시할 때 들으십시오. - - [follow @fastapi on Twitter](https://twitter.com/fastapi) (별도 계정에서) 할 수 있습니다. +## **FastAPI**에 대한 트윗 - - [**Linkedin**에서의 연결](https://www.linkedin.com/in/tiangolo/). - - 새로운 툴의 발표나 릴리스를 들을 수 있습니다 (단, Twitter를 더 자주 사용합니다 🤷‍♂). +**FastAPI**에 대해 트윗 하고 FastAPI가 마음에 드는 이유를 알려주세요. 🎉 - - [**Dev.to**](https://dev.to/tiangolo) 또는 [**Medium**](https://medium.com/@tiangolo)에서 제가 작성한 내용을 읽어 보십시오(또는 팔로우). - - 다른 기사나 아이디어들을 읽고, 제가 만들어왔던 툴에 대해서도 읽으십시오. - - 새로운 기사를 읽기 위해 팔로우 하십시오. +**FastAPI**가 어떻게 사용되고 있는지, 어떤 점이 마음에 들었는지, 어떤 프로젝트/회사에서 사용하고 있는지 등에 대해 듣고 싶습니다. - ## **FastAPI**에 대한 트윗 +## FastAPI에 투표하기 - [**FastAPI**에 대해 트윗](https://twitter.com/compose/tweet?text=I'm loving @fastapi because... https://github.com/fastapi/fastapi) 하고 FastAPI가 마음에 드는 이유를 알려주세요. 🎉 +* Slant에서 **FastAPI** 에 대해 투표하십시오. +* AlternativeTo에서 **FastAPI** 에 대해 투표하십시오. +* StackShare에서 **FastAPI** 에 대해 투표하십시오. - **FastAPI**가 어떻게 사용되고 있는지, 어떤 점이 마음에 들었는지, 어떤 프로젝트/회사에서 사용하고 있는지 등에 대해 듣고 싶습니다. +## GitHub의 이슈로 다른사람 돕기 - ## FastAPI에 투표하기 +다른 사람들의 질문에 도움을 줄 수 있습니다: - - [Slant에서 **FastAPI** 에 대해 투표하십시오](https://www.slant.co/options/34241/~fastapi-review). - - [AlternativeTo**FastAPI** 에 대해 투표하십시오](https://alternativeto.net/software/fastapi/). +* GitHub 디스커션 +* GitHub 이슈 - ## GitHub의 이슈로 다른사람 돕기 +많은 경우, 여러분은 이미 그 질문에 대한 답을 알고 있을 수도 있습니다. 🤓 - [존재하는 이슈](https://github.com/fastapi/fastapi/issues)를 확인하고 그것을 시도하고 도와줄 수 있습니다. 대부분의 경우 이미 답을 알고 있는 질문입니다. 🤓 +만약 많은 사람들의 문제를 도와준다면, 공식적인 [FastAPI 전문가](fastapi-people.md#fastapi-experts){.internal-link target=\_blank} 가 될 것입니다. 🎉 - 많은 사람들의 문제를 도와준다면, 공식적인 [FastAPI 전문가](https://github.com/fastapi/fastapi/blob/master/docs/en/docs/fastapi-people.md#experts) 가 될 수 있습니다{.internal-link target=_blank}. 🎉 +가장 중요한 점은: 친절하려고 노력하는 것입니다. 사람들은 좌절감을 안고 오며, 많은 경우 최선의 방식으로 질문하지 않을 수도 있습니다. 하지만 최대한 친절하게 대하려고 노력하세요. 🤗 - ## GitHub 저장소 보기 +**FastAPI** 커뮤니티의 목표는 친절하고 환영하는 것입니다. 동시에, 괴롭힘이나 무례한 행동을 받아들이지 마세요. 우리는 서로를 돌봐야 합니다. - GitHub에서 FastAPI를 "watch"할 수 있습니다 (오른쪽 상단 watch 버튼을 클릭): https://github.com/fastapi/fastapi. 👀 +--- - "Releases only" 대신 "Watching"을 선택하면 다른 사용자가 새로운 issue를 생성할 때 알림이 수신됩니다. +다른 사람들의 질문 (디스커션 또는 이슈에서) 해결을 도울 수 있는 방법은 다음과 같습니다. - 그런 다음 이런 issues를 해결 할 수 있도록 도움을 줄 수 있습니다. +### 질문 이해하기 - ## 이슈 생성하기 +* 질문하는 사람이 가진 **목적**과 사용 사례를 이해할 수 있는지 확인하세요. - GitHub 저장소에 [새로운 이슈 생성](https://github.com/fastapi/fastapi/issues/new/choose) 을 할 수 있습니다, 예를들면 다음과 같습니다: +* 질문 (대부분은 질문입니다)이 **명확**한지 확인하세요. - - **질문**을 하거나 **문제**에 대해 질문합니다. - - 새로운 **기능**을 제안 합니다. +* 많은 경우, 사용자가 가정한 해결책에 대한 질문을 하지만, 더 **좋은** 해결책이 있을 수 있습니다. 문제와 사용 사례를 더 잘 이해하면 더 나은 **대안적인 해결책**을 제안할 수 있습니다. - **참고**: 만약 이슈를 생성한다면, 저는 여러분에게 다른 사람들을 도와달라고 부탁할 것입니다. 😉 +* 질문을 이해할 수 없다면, 더 **자세한 정보**를 요청하세요. - ## Pull Request를 만드십시오 +### 문제 재현하기 - Pull Requests를 이용하여 소스코드에 [컨트리뷰트](https://github.com/fastapi/fastapi/blob/master/docs/en/docs/contributing.md){.internal-link target=_blank} 할 수 있습니다. 예를 들면 다음과 같습니다: +대부분의 경우, 질문은 질문자의 **원본 코드**와 관련이 있습니다. - - 문서에서 찾은 오타를 수정할 때. +많은 경우, 코드의 일부만 복사해서 올리지만, 그것만으로는 **문제를 재현**하기에 충분하지 않습니다. - - FastAPI를 [편집하여](https://github.com/fastapi/fastapi/edit/master/docs/en/data/external_links.yml) 작성했거나 찾은 문서, 비디오 또는 팟캐스트를 공유할 때. +* 질문자에게 최소한의 재현 가능한 예제를 제공해달라고 요청하세요. 이렇게 하면 코드를 **복사-붙여넣기**하여 직접 실행하고, 동일한 오류나 동작을 확인하거나 사용 사례를 더 잘 이해할 수 있습니다. - - 해당 섹션의 시작 부분에 링크를 추가했는지 확인하십시오. +* 너그러운 마음이 든다면, 문제 설명만을 기반으로 직접 **예제를 만들어**볼 수도 있습니다. 하지만, 이는 시간이 많이 걸릴 수 있으므로, 먼저 질문을 명확히 해달라고 요청하는 것이 좋습니다. - - 당신의 언어로 [문서 번역하는데](https://github.com/fastapi/fastapi/blob/master/docs/en/docs/contributing.md#translations){.internal-link target=_blank} 기여할 때. +### 해결책 제안하기 - - 또한 다른 사용자가 만든 번역을 검토하는데 도움을 줄 수도 있습니다. +* 질문을 충분히 이해한 후에는 가능한 **답변**을 제공할 수 있습니다. - - 새로운 문서의 섹션을 제안할 때. +* 많은 경우, 질문자의 **근본적인 문제나 사용 사례**를 이해하는 것이 중요합니다. 그들이 시도하는 방법보다 더 나은 해결책이 있을 수 있기 때문입니다. - - 기존 문제/버그를 수정할 때. +### 해결 요청하기 - - 새로운 feature를 추가할 때. +질문자가 답변을 확인하고 나면, 당신이 문제를 해결했을 가능성이 높습니다. 축하합니다, **당신은 영웅입니다**! 🦸 - ## 채팅에 참여하십시오 +* 이제 문제를 해결했다면, 질문자에게 다음을 요청할 수 있습니다. - 👥 [디스코드 채팅 서버](https://discord.gg/VQjSZaeJmf) 👥 에 가입하고 FastAPI 커뮤니티에서 다른 사람들과 어울리세요. + * GitHub 디스커션에서: 댓글을 **답변**으로 표시하도록 요청하세요. + * GitHub 이슈에서: 이슈를 **닫아달라고** 요청하세요. - /// tip +## GitHub 저장소 보기 - 질문이 있는 경우, [GitHub 이슈 ](https://github.com/fastapi/fastapi/issues/new/choose) 에서 질문하십시오, [FastAPI 전문가](https://github.com/fastapi/fastapi/blob/master/docs/en/docs/fastapi-people.md#experts) 의 도움을 받을 가능성이 높습니다{.internal-link target=_blank} . +GitHub에서 FastAPI를 "watch"할 수 있습니다 (오른쪽 상단 watch 버튼을 클릭): https://github.com/fastapi/fastapi. 👀 - /// +"Releases only" 대신 "Watching"을 선택하면, 새로운 이슈나 질문이 생성될 때 알림을 받을 수 있습니다. 또한, 특정하게 새로운 이슈, 디스커션, PR 등만 알림 받도록 설정할 수도 있습니다. - ``` - 다른 일반적인 대화에서만 채팅을 사용하십시오. - ``` +그런 다음 이런 이슈들을 해결 할 수 있도록 도움을 줄 수 있습니다. - 기존 [지터 채팅](https://gitter.im/fastapi/fastapi) 이 있지만 채널과 고급기능이 없어서 대화를 하기가 조금 어렵기 때문에 지금은 디스코드가 권장되는 시스템입니다. +## 이슈 생성하기 - ### 질문을 위해 채팅을 사용하지 마십시오 +GitHub 저장소에 새로운 이슈 생성을 할 수 있습니다, 예를들면 다음과 같습니다: - 채팅은 더 많은 "자유로운 대화"를 허용하기 때문에, 너무 일반적인 질문이나 대답하기 어려운 질문을 쉽게 질문을 할 수 있으므로, 답변을 받지 못할 수 있습니다. +* **질문**을 하거나 **문제**에 대해 질문합니다. +* 새로운 **기능**을 제안 합니다. - GitHub 이슈에서의 템플릿은 올바른 질문을 작성하도록 안내하여 더 쉽게 좋은 답변을 얻거나 질문하기 전에 스스로 문제를 해결할 수도 있습니다. 그리고 GitHub에서는 시간이 조금 걸리더라도 항상 모든 것에 답할 수 있습니다. 채팅 시스템에서는 개인적으로 그렇게 할 수 없습니다. 😅 +**참고**: 만약 이슈를 생성한다면, 저는 여러분에게 다른 사람들을 도와달라고 부탁할 것입니다. 😉 - 채팅 시스템에서의 대화 또한 GitHub에서 처럼 쉽게 검색할 수 없기 때문에 대화 중에 질문과 답변이 손실될 수 있습니다. 그리고 GitHub 이슈에 있는 것만 [FastAPI 전문가](https://github.com/fastapi/fastapi/blob/master/docs/en/docs/fastapi-people.md#experts)가 되는 것으로 간주되므로{.internal-link target=_blank} , GitHub 이슈에서 더 많은 관심을 받을 것입니다. +## Pull Requests 리뷰하기 - 반면, 채팅 시스템에는 수천 명의 사용자가 있기 때문에, 거의 항상 대화 상대를 찾을 가능성이 높습니다. 😄 +다른 사람들의 pull request를 리뷰하는 데 도움을 줄 수 있습니다. - ## 개발자 스폰서가 되십시오 +다시 한번 말하지만, 최대한 친절하게 리뷰해 주세요. 🤗 - [GitHub 스폰서](https://github.com/sponsors/tiangolo) 를 통해 개발자를 경제적으로 지원할 수 있습니다. +--- - 감사하다는 말로 커피를 ☕️ 한잔 사줄 수 있습니다. 😄 +Pull Rrquest를 리뷰할 때 고려해야 할 사항과 방법은 다음과 같습니다: - 또한 FastAPI의 실버 또는 골드 스폰서가 될 수 있습니다. 🏅🎉 +### 문제 이해하기 - ## FastAPI를 강화하는 도구의 스폰서가 되십시오 +* 먼저, 해당 pull request가 해결하려는 **문제를 이해하는지** 확인하세요. GitHub 디스커션 또는 이슈에서 더 긴 논의가 있었을 수도 있습니다. - 문서에서 보았듯이, FastAPI는 Starlette과 Pydantic 라는 거인의 어깨에 타고 있습니다. +* Pull request가 필요하지 않을 가능성도 있습니다. **다른 방식**으로 문제를 해결할 수 있다면, 그 방법을 제안하거나 질문할 수 있습니다. - 다음의 스폰서가 될 수 있습니다 +### 스타일에 너무 신경 쓰지 않기 - - [Samuel Colvin (Pydantic)](https://github.com/sponsors/samuelcolvin) - - [Encode (Starlette, Uvicorn)](https://github.com/sponsors/encode) +* 커밋 메시지 스타일 같은 것에 너무 신경 쓰지 않아도 됩니다. 저는 직접 커밋을 수정하여 squash and merge를 수행할 것입니다. - ------ +* 코드 스타일 규칙도 걱정할 필요 없습니다. 이미 자동화된 도구들이 이를 검사하고 있습니다. - 감사합니다! 🚀 +스타일이나 일관성 관련 요청이 필요한 경우, 제가 직접 요청하거나 필요한 변경 사항을 추가 커밋으로 수정할 것입니다. + +### 코드 확인하기 + +* 코드를 읽고, **논리적으로 타당**한지 확인한 후 로컬에서 실행하여 문제가 해결되는지 확인하세요. + +* 그런 다음, 확인했다고 **댓글**을 남겨 주세요. 그래야 제가 검토했음을 알 수 있습니다. + +/// info + +불행히도, 제가 단순히 여러 개의 승인만으로 PR을 신뢰할 수는 없습니다. + +3개, 5개 이상의 승인이 달린 PR이 실제로는 깨져 있거나, 버그가 있거나, 주장하는 문제를 해결하지 못하는 경우가 여러 번 있었습니다. 😅 + +따라서, 정말로 코드를 읽고 실행한 뒤, 댓글로 확인 내용을 남겨 주는 것이 매우 중요합니다. 🤓 + +/// + +* PR을 더 단순하게 만들 수 있다면 그렇게 요청할 수 있지만, 너무 까다로울 필요는 없습니다. 주관적인 견해가 많이 있을 수 있기 때문입니다 (그리고 저도 제 견해가 있을 거예요 🙈). 따라서 핵심적인 부분에 집중하는 것이 좋습니다. + +### 테스트 + +* PR에 **테스트**가 포함되어 있는지 확인하는 데 도움을 주세요. + +* PR을 적용하기 전에 테스트가 **실패**하는지 확인하세요. 🚨 + +* PR을 적용한 후 테스트가 **통과**하는지 확인하세요. ✅ + +* 많은 PR에는 테스트가 없습니다. 테스트를 추가하도록 **상기**시켜줄 수도 있고, 직접 테스트를 **제안**할 수도 있습니다. 이는 시간이 많이 소요되는 부분 중 하나이며, 그 부분을 많이 도와줄 수 있습니다. + +* 그리고 시도한 내용을 댓글로 남겨주세요. 그러면 제가 확인했다는 걸 알 수 있습니다. 🤓 + +## Pull Request를 만드십시오 + +Pull Requests를 이용하여 소스코드에 [컨트리뷰트](contributing.md){.internal-link target=\_blank} 할 수 있습니다. 예를 들면 다음과 같습니다: + +* 문서에서 발견한 오타를 수정할 때. +* FastAPI 관련 문서, 비디오 또는 팟캐스트를 작성했거나 발견하여 이 파일을 편집하여 공유할 때. + * 해당 섹션의 시작 부분에 링크를 추가해야 합니다. +* 당신의 언어로 [문서 번역하는데](contributing.md#translations){.internal-link target=\_blank} 기여할 때. + * 다른 사람이 작성한 번역을 검토하는 것도 도울 수 있습니다. +* 새로운 문서의 섹션을 제안할 때. +* 기존 문제/버그를 수정할 때. + * 테스트를 반드시 추가해야 합니다. +* 새로운 feature를 추가할 때. + * 테스트를 반드시 추가해야 합니다. + * 관련 문서가 필요하다면 반드시 추가해야 합니다. + +## FastAPI 유지 관리에 도움 주기 + +**FastAPI**의 유지 관리를 도와주세요! 🤓 + +할 일이 많고, 그 중 대부분은 **여러분**이 할 수 있습니다. + +지금 할 수 있는 주요 작업은: + +* [GitHub에서 다른 사람들의 질문에 도움 주기](#github_1){.internal-link target=_blank} (위의 섹션을 참조하세요). +* [Pull Request 리뷰하기](#pull-requests){.internal-link target=_blank} (위의 섹션을 참조하세요). + +이 두 작업이 **가장 많은 시간을 소모**하는 일입니다. 그것이 FastAPI 유지 관리의 주요 작업입니다. + +이 작업을 도와주신다면, **FastAPI 유지 관리에 도움을 주는 것**이며 그것이 **더 빠르고 더 잘 발전하는 것**을 보장하는 것입니다. 🚀 + +## 채팅에 참여하십시오 + +👥 디스코드 채팅 서버 👥 에 가입하고 FastAPI 커뮤니티에서 다른 사람들과 어울리세요. + +/// tip + +질문이 있는 경우, GitHub 디스커션 에서 질문하십시오, [FastAPI Experts](fastapi-people.md#fastapi-experts){.internal-link target=_blank} 의 도움을 받을 가능성이 높습니다. + +다른 일반적인 대화에서만 채팅을 사용하십시오. + +/// + +### 질문을 위해 채팅을 사용하지 마십시오 + +채팅은 더 많은 "자유로운 대화"를 허용하기 때문에, 너무 일반적인 질문이나 대답하기 어려운 질문을 쉽게 질문을 할 수 있으므로, 답변을 받지 못할 수 있습니다. + +GitHub 이슈에서의 템플릿은 올바른 질문을 작성하도록 안내하여 더 쉽게 좋은 답변을 얻거나 질문하기 전에 스스로 문제를 해결할 수도 있습니다. 그리고 GitHub에서는 시간이 조금 걸리더라도 항상 모든 것에 답할 수 있습니다. 채팅 시스템에서는 개인적으로 그렇게 할 수 없습니다. 😅 + +채팅 시스템에서의 대화 또한 GitHub에서 처럼 쉽게 검색할 수 없기 때문에 대화 중에 질문과 답변이 손실될 수 있습니다. 그리고 GitHub 이슈에 있는 것만 [FastAPI Expert](fastapi-people.md#fastapi-experts){.internal-link target=_blank}가 되는 것으로 간주되므로, GitHub 이슈에서 더 많은 관심을 받을 것입니다. + +반면, 채팅 시스템에는 수천 명의 사용자가 있기 때문에, 거의 항상 대화 상대를 찾을 가능성이 높습니다. 😄 + +## 개발자 스폰서가 되십시오 + +GitHub 스폰서 를 통해 개발자를 경제적으로 지원할 수 있습니다. + +감사하다는 말로 커피를 ☕️ 한잔 사줄 수 있습니다. 😄 + +또한 FastAPI의 실버 또는 골드 스폰서가 될 수 있습니다. 🏅🎉 + +## FastAPI를 강화하는 도구의 스폰서가 되십시오 + +문서에서 보았듯이, FastAPI는 Starlette과 Pydantic 라는 거인의 어깨에 타고 있습니다. + +다음의 스폰서가 될 수 있습니다 + +* Samuel Colvin (Pydantic) +* Encode (Starlette, Uvicorn) + +--- + +감사합니다! 🚀 From db554ca09426dfa7031a1b07a8b016b48c82c222 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 15 Feb 2025 11:38:21 +0000 Subject: [PATCH 099/117] =?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 c69b20e66..88bff8dbc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Translations +* 🌐 Update Korean translation for `docs/ko/docs/help-fastapi.md`. PR [#13262](https://github.com/fastapi/fastapi/pull/13262) by [@Zerohertz](https://github.com/Zerohertz). * 🌐 Add Korean translation for `docs/ko/docs/advanced/custom-response.md`. PR [#13265](https://github.com/fastapi/fastapi/pull/13265) by [@11kkw](https://github.com/11kkw). * 🌐 Update Korean translation for `docs/ko/docs/tutorial/security/simple-oauth2.md`. PR [#13335](https://github.com/fastapi/fastapi/pull/13335) by [@yes0ng](https://github.com/yes0ng). * 🌐 Add Russian translation for `docs/ru/docs/advanced/response-cookies.md`. PR [#13327](https://github.com/fastapi/fastapi/pull/13327) by [@Stepakinoyan](https://github.com/Stepakinoyan). From 261bc2d3875ad95ec570a2cb15df6a54af39207f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Haoyu=20=28Daniel=29=20YANG=20=E6=9D=A8=E6=B5=A9=E5=AE=87?= Date: Sat, 15 Feb 2025 12:42:54 +0100 Subject: [PATCH 100/117] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Remove=20duplicate?= =?UTF-8?q?=20title=20in=20docs=20`body-multiple-params`=20(#13345)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/tutorial/body-multiple-params.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/en/docs/tutorial/body-multiple-params.md b/docs/en/docs/tutorial/body-multiple-params.md index 9fced9652..71b308bb4 100644 --- a/docs/en/docs/tutorial/body-multiple-params.md +++ b/docs/en/docs/tutorial/body-multiple-params.md @@ -10,8 +10,6 @@ And you can also declare body parameters as optional, by setting the default to {* ../../docs_src/body_multiple_params/tutorial001_an_py310.py hl[18:20] *} -## Multiple body parameters - /// note Notice that, in this case, the `item` that would be taken from the body is optional. As it has a `None` default value. From 540d8ff398ececd31c5299d0f6823523efb69c5d Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 15 Feb 2025 11:43:16 +0000 Subject: [PATCH 101/117] =?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 88bff8dbc..d5440404b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* ✏️ Remove duplicate title in docs `body-multiple-params`. PR [#13345](https://github.com/fastapi/fastapi/pull/13345) by [@DanielYang59](https://github.com/DanielYang59). * 📝 Fix test badge. PR [#13313](https://github.com/fastapi/fastapi/pull/13313) by [@esadek](https://github.com/esadek). ### Translations From 1e6d95ce6d9ab5bc13796d2d068540e9c765e4c1 Mon Sep 17 00:00:00 2001 From: alv2017 Date: Sat, 15 Feb 2025 16:37:48 +0200 Subject: [PATCH 102/117] =?UTF-8?q?=E2=9C=85=20Simplify=20tests=20for=20`d?= =?UTF-8?q?ependency=5Ftesting`=20(#13223)?= 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: Alejandra <90076947+alejsdev@users.noreply.github.com> --- .../test_tutorial001.py | 53 +++++++++---- .../test_tutorial001_an.py | 56 -------------- .../test_tutorial001_an_py310.py | 75 ------------------- .../test_tutorial001_an_py39.py | 75 ------------------- .../test_tutorial001_py310.py | 75 ------------------- 5 files changed, 40 insertions(+), 294 deletions(-) delete mode 100644 tests/test_tutorial/test_testing_dependencies/test_tutorial001_an.py delete mode 100644 tests/test_tutorial/test_testing_dependencies/test_tutorial001_an_py310.py delete mode 100644 tests/test_tutorial/test_testing_dependencies/test_tutorial001_an_py39.py delete mode 100644 tests/test_tutorial/test_testing_dependencies/test_tutorial001_py310.py diff --git a/tests/test_tutorial/test_testing_dependencies/test_tutorial001.py b/tests/test_tutorial/test_testing_dependencies/test_tutorial001.py index af26307f5..00ee6ab1e 100644 --- a/tests/test_tutorial/test_testing_dependencies/test_tutorial001.py +++ b/tests/test_tutorial/test_testing_dependencies/test_tutorial001.py @@ -1,25 +1,48 @@ -from docs_src.dependency_testing.tutorial001 import ( - app, - client, - test_override_in_items, - test_override_in_items_with_params, - test_override_in_items_with_q, +import importlib +from types import ModuleType + +import pytest + +from ...utils import needs_py39, needs_py310 + + +@pytest.fixture( + name="test_module", + params=[ + "tutorial001", + pytest.param("tutorial001_py310", marks=needs_py310), + "tutorial001_an", + pytest.param("tutorial001_an_py39", marks=needs_py39), + pytest.param("tutorial001_an_py310", marks=needs_py310), + ], ) +def get_test_module(request: pytest.FixtureRequest) -> ModuleType: + mod: ModuleType = importlib.import_module( + f"docs_src.dependency_testing.{request.param}" + ) + return mod -def test_override_in_items_run(): +def test_override_in_items_run(test_module: ModuleType): + test_override_in_items = test_module.test_override_in_items + test_override_in_items() -def test_override_in_items_with_q_run(): +def test_override_in_items_with_q_run(test_module: ModuleType): + test_override_in_items_with_q = test_module.test_override_in_items_with_q + test_override_in_items_with_q() -def test_override_in_items_with_params_run(): +def test_override_in_items_with_params_run(test_module: ModuleType): + test_override_in_items_with_params = test_module.test_override_in_items_with_params + test_override_in_items_with_params() -def test_override_in_users(): +def test_override_in_users(test_module: ModuleType): + client = test_module.client response = client.get("/users/") assert response.status_code == 200, response.text assert response.json() == { @@ -28,7 +51,8 @@ def test_override_in_users(): } -def test_override_in_users_with_q(): +def test_override_in_users_with_q(test_module: ModuleType): + client = test_module.client response = client.get("/users/?q=foo") assert response.status_code == 200, response.text assert response.json() == { @@ -37,7 +61,8 @@ def test_override_in_users_with_q(): } -def test_override_in_users_with_params(): +def test_override_in_users_with_params(test_module: ModuleType): + client = test_module.client response = client.get("/users/?q=foo&skip=100&limit=200") assert response.status_code == 200, response.text assert response.json() == { @@ -46,7 +71,9 @@ def test_override_in_users_with_params(): } -def test_normal_app(): +def test_normal_app(test_module: ModuleType): + app = test_module.app + client = test_module.client app.dependency_overrides = None response = client.get("/items/?q=foo&skip=100&limit=200") assert response.status_code == 200, response.text diff --git a/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an.py b/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an.py deleted file mode 100644 index fc1f9149a..000000000 --- a/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an.py +++ /dev/null @@ -1,56 +0,0 @@ -from docs_src.dependency_testing.tutorial001_an import ( - app, - client, - test_override_in_items, - test_override_in_items_with_params, - test_override_in_items_with_q, -) - - -def test_override_in_items_run(): - test_override_in_items() - - -def test_override_in_items_with_q_run(): - test_override_in_items_with_q() - - -def test_override_in_items_with_params_run(): - test_override_in_items_with_params() - - -def test_override_in_users(): - response = client.get("/users/") - assert response.status_code == 200, response.text - assert response.json() == { - "message": "Hello Users!", - "params": {"q": None, "skip": 5, "limit": 10}, - } - - -def test_override_in_users_with_q(): - response = client.get("/users/?q=foo") - assert response.status_code == 200, response.text - assert response.json() == { - "message": "Hello Users!", - "params": {"q": "foo", "skip": 5, "limit": 10}, - } - - -def test_override_in_users_with_params(): - response = client.get("/users/?q=foo&skip=100&limit=200") - assert response.status_code == 200, response.text - assert response.json() == { - "message": "Hello Users!", - "params": {"q": "foo", "skip": 5, "limit": 10}, - } - - -def test_normal_app(): - app.dependency_overrides = None - response = client.get("/items/?q=foo&skip=100&limit=200") - assert response.status_code == 200, response.text - assert response.json() == { - "message": "Hello Items!", - "params": {"q": "foo", "skip": 100, "limit": 200}, - } diff --git a/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an_py310.py b/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an_py310.py deleted file mode 100644 index a3d27f47f..000000000 --- a/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an_py310.py +++ /dev/null @@ -1,75 +0,0 @@ -from ...utils import needs_py310 - - -@needs_py310 -def test_override_in_items_run(): - from docs_src.dependency_testing.tutorial001_an_py310 import test_override_in_items - - test_override_in_items() - - -@needs_py310 -def test_override_in_items_with_q_run(): - from docs_src.dependency_testing.tutorial001_an_py310 import ( - test_override_in_items_with_q, - ) - - test_override_in_items_with_q() - - -@needs_py310 -def test_override_in_items_with_params_run(): - from docs_src.dependency_testing.tutorial001_an_py310 import ( - test_override_in_items_with_params, - ) - - test_override_in_items_with_params() - - -@needs_py310 -def test_override_in_users(): - from docs_src.dependency_testing.tutorial001_an_py310 import client - - response = client.get("/users/") - assert response.status_code == 200, response.text - assert response.json() == { - "message": "Hello Users!", - "params": {"q": None, "skip": 5, "limit": 10}, - } - - -@needs_py310 -def test_override_in_users_with_q(): - from docs_src.dependency_testing.tutorial001_an_py310 import client - - response = client.get("/users/?q=foo") - assert response.status_code == 200, response.text - assert response.json() == { - "message": "Hello Users!", - "params": {"q": "foo", "skip": 5, "limit": 10}, - } - - -@needs_py310 -def test_override_in_users_with_params(): - from docs_src.dependency_testing.tutorial001_an_py310 import client - - response = client.get("/users/?q=foo&skip=100&limit=200") - assert response.status_code == 200, response.text - assert response.json() == { - "message": "Hello Users!", - "params": {"q": "foo", "skip": 5, "limit": 10}, - } - - -@needs_py310 -def test_normal_app(): - from docs_src.dependency_testing.tutorial001_an_py310 import app, client - - app.dependency_overrides = None - response = client.get("/items/?q=foo&skip=100&limit=200") - assert response.status_code == 200, response.text - assert response.json() == { - "message": "Hello Items!", - "params": {"q": "foo", "skip": 100, "limit": 200}, - } diff --git a/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an_py39.py b/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an_py39.py deleted file mode 100644 index f03ed5e07..000000000 --- a/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an_py39.py +++ /dev/null @@ -1,75 +0,0 @@ -from ...utils import needs_py39 - - -@needs_py39 -def test_override_in_items_run(): - from docs_src.dependency_testing.tutorial001_an_py39 import test_override_in_items - - test_override_in_items() - - -@needs_py39 -def test_override_in_items_with_q_run(): - from docs_src.dependency_testing.tutorial001_an_py39 import ( - test_override_in_items_with_q, - ) - - test_override_in_items_with_q() - - -@needs_py39 -def test_override_in_items_with_params_run(): - from docs_src.dependency_testing.tutorial001_an_py39 import ( - test_override_in_items_with_params, - ) - - test_override_in_items_with_params() - - -@needs_py39 -def test_override_in_users(): - from docs_src.dependency_testing.tutorial001_an_py39 import client - - response = client.get("/users/") - assert response.status_code == 200, response.text - assert response.json() == { - "message": "Hello Users!", - "params": {"q": None, "skip": 5, "limit": 10}, - } - - -@needs_py39 -def test_override_in_users_with_q(): - from docs_src.dependency_testing.tutorial001_an_py39 import client - - response = client.get("/users/?q=foo") - assert response.status_code == 200, response.text - assert response.json() == { - "message": "Hello Users!", - "params": {"q": "foo", "skip": 5, "limit": 10}, - } - - -@needs_py39 -def test_override_in_users_with_params(): - from docs_src.dependency_testing.tutorial001_an_py39 import client - - response = client.get("/users/?q=foo&skip=100&limit=200") - assert response.status_code == 200, response.text - assert response.json() == { - "message": "Hello Users!", - "params": {"q": "foo", "skip": 5, "limit": 10}, - } - - -@needs_py39 -def test_normal_app(): - from docs_src.dependency_testing.tutorial001_an_py39 import app, client - - app.dependency_overrides = None - response = client.get("/items/?q=foo&skip=100&limit=200") - assert response.status_code == 200, response.text - assert response.json() == { - "message": "Hello Items!", - "params": {"q": "foo", "skip": 100, "limit": 200}, - } diff --git a/tests/test_tutorial/test_testing_dependencies/test_tutorial001_py310.py b/tests/test_tutorial/test_testing_dependencies/test_tutorial001_py310.py deleted file mode 100644 index 776b916ff..000000000 --- a/tests/test_tutorial/test_testing_dependencies/test_tutorial001_py310.py +++ /dev/null @@ -1,75 +0,0 @@ -from ...utils import needs_py310 - - -@needs_py310 -def test_override_in_items_run(): - from docs_src.dependency_testing.tutorial001_py310 import test_override_in_items - - test_override_in_items() - - -@needs_py310 -def test_override_in_items_with_q_run(): - from docs_src.dependency_testing.tutorial001_py310 import ( - test_override_in_items_with_q, - ) - - test_override_in_items_with_q() - - -@needs_py310 -def test_override_in_items_with_params_run(): - from docs_src.dependency_testing.tutorial001_py310 import ( - test_override_in_items_with_params, - ) - - test_override_in_items_with_params() - - -@needs_py310 -def test_override_in_users(): - from docs_src.dependency_testing.tutorial001_py310 import client - - response = client.get("/users/") - assert response.status_code == 200, response.text - assert response.json() == { - "message": "Hello Users!", - "params": {"q": None, "skip": 5, "limit": 10}, - } - - -@needs_py310 -def test_override_in_users_with_q(): - from docs_src.dependency_testing.tutorial001_py310 import client - - response = client.get("/users/?q=foo") - assert response.status_code == 200, response.text - assert response.json() == { - "message": "Hello Users!", - "params": {"q": "foo", "skip": 5, "limit": 10}, - } - - -@needs_py310 -def test_override_in_users_with_params(): - from docs_src.dependency_testing.tutorial001_py310 import client - - response = client.get("/users/?q=foo&skip=100&limit=200") - assert response.status_code == 200, response.text - assert response.json() == { - "message": "Hello Users!", - "params": {"q": "foo", "skip": 5, "limit": 10}, - } - - -@needs_py310 -def test_normal_app(): - from docs_src.dependency_testing.tutorial001_py310 import app, client - - app.dependency_overrides = None - response = client.get("/items/?q=foo&skip=100&limit=200") - assert response.status_code == 200, response.text - assert response.json() == { - "message": "Hello Items!", - "params": {"q": "foo", "skip": 100, "limit": 200}, - } From cbd7b986e7108ea5cb79eb653e37e0073bd22645 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 15 Feb 2025 14:38:14 +0000 Subject: [PATCH 103/117] =?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 d5440404b..28821455a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Refactors + +* ✅ Simplify tests for `dependency_testing`. PR [#13223](https://github.com/fastapi/fastapi/pull/13223) by [@alv2017](https://github.com/alv2017). + ### Docs * ✏️ Remove duplicate title in docs `body-multiple-params`. PR [#13345](https://github.com/fastapi/fastapi/pull/13345) by [@DanielYang59](https://github.com/DanielYang59). From f6872dd29849a07a8cfd475c7c56dfa3f3ea5808 Mon Sep 17 00:00:00 2001 From: alv2017 Date: Sat, 15 Feb 2025 16:42:41 +0200 Subject: [PATCH 104/117] =?UTF-8?q?=E2=9C=85=20Simplify=20tests=20for=20`a?= =?UTF-8?q?pp=5Ftesting`=20(#13220)?= 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: Alejandra <90076947+alejsdev@users.noreply.github.com> --- .../test_tutorial/test_testing/test_main_b.py | 25 +++++++++++++++++-- .../test_testing/test_main_b_an.py | 10 -------- .../test_testing/test_main_b_an_py310.py | 13 ---------- .../test_testing/test_main_b_an_py39.py | 13 ---------- .../test_testing/test_main_b_py310.py | 13 ---------- 5 files changed, 23 insertions(+), 51 deletions(-) delete mode 100644 tests/test_tutorial/test_testing/test_main_b_an.py delete mode 100644 tests/test_tutorial/test_testing/test_main_b_an_py310.py delete mode 100644 tests/test_tutorial/test_testing/test_main_b_an_py39.py delete mode 100644 tests/test_tutorial/test_testing/test_main_b_py310.py diff --git a/tests/test_tutorial/test_testing/test_main_b.py b/tests/test_tutorial/test_testing/test_main_b.py index 1e1836f5b..aa7f969c6 100644 --- a/tests/test_tutorial/test_testing/test_main_b.py +++ b/tests/test_tutorial/test_testing/test_main_b.py @@ -1,7 +1,28 @@ -from docs_src.app_testing.app_b import test_main +import importlib +from types import ModuleType +import pytest -def test_app(): +from ...utils import needs_py39, needs_py310 + + +@pytest.fixture( + name="test_module", + params=[ + "app_b.test_main", + pytest.param("app_b_py310.test_main", marks=needs_py310), + "app_b_an.test_main", + pytest.param("app_b_an_py39.test_main", marks=needs_py39), + pytest.param("app_b_an_py310.test_main", marks=needs_py310), + ], +) +def get_test_module(request: pytest.FixtureRequest) -> ModuleType: + mod: ModuleType = importlib.import_module(f"docs_src.app_testing.{request.param}") + return mod + + +def test_app(test_module: ModuleType): + test_main = test_module test_main.test_create_existing_item() test_main.test_create_item() test_main.test_create_item_bad_token() diff --git a/tests/test_tutorial/test_testing/test_main_b_an.py b/tests/test_tutorial/test_testing/test_main_b_an.py deleted file mode 100644 index e53fc3224..000000000 --- a/tests/test_tutorial/test_testing/test_main_b_an.py +++ /dev/null @@ -1,10 +0,0 @@ -from docs_src.app_testing.app_b_an import test_main - - -def test_app(): - test_main.test_create_existing_item() - test_main.test_create_item() - test_main.test_create_item_bad_token() - test_main.test_read_nonexistent_item() - test_main.test_read_item() - test_main.test_read_item_bad_token() diff --git a/tests/test_tutorial/test_testing/test_main_b_an_py310.py b/tests/test_tutorial/test_testing/test_main_b_an_py310.py deleted file mode 100644 index c974e5dc1..000000000 --- a/tests/test_tutorial/test_testing/test_main_b_an_py310.py +++ /dev/null @@ -1,13 +0,0 @@ -from ...utils import needs_py310 - - -@needs_py310 -def test_app(): - from docs_src.app_testing.app_b_an_py310 import test_main - - test_main.test_create_existing_item() - test_main.test_create_item() - test_main.test_create_item_bad_token() - test_main.test_read_nonexistent_item() - test_main.test_read_item() - test_main.test_read_item_bad_token() diff --git a/tests/test_tutorial/test_testing/test_main_b_an_py39.py b/tests/test_tutorial/test_testing/test_main_b_an_py39.py deleted file mode 100644 index 71f99726c..000000000 --- a/tests/test_tutorial/test_testing/test_main_b_an_py39.py +++ /dev/null @@ -1,13 +0,0 @@ -from ...utils import needs_py39 - - -@needs_py39 -def test_app(): - from docs_src.app_testing.app_b_an_py39 import test_main - - test_main.test_create_existing_item() - test_main.test_create_item() - test_main.test_create_item_bad_token() - test_main.test_read_nonexistent_item() - test_main.test_read_item() - test_main.test_read_item_bad_token() diff --git a/tests/test_tutorial/test_testing/test_main_b_py310.py b/tests/test_tutorial/test_testing/test_main_b_py310.py deleted file mode 100644 index e30cdc073..000000000 --- a/tests/test_tutorial/test_testing/test_main_b_py310.py +++ /dev/null @@ -1,13 +0,0 @@ -from ...utils import needs_py310 - - -@needs_py310 -def test_app(): - from docs_src.app_testing.app_b_py310 import test_main - - test_main.test_create_existing_item() - test_main.test_create_item() - test_main.test_create_item_bad_token() - test_main.test_read_nonexistent_item() - test_main.test_read_item() - test_main.test_read_item_bad_token() From 2c937aabefe45b109edde6bc929721c2343804eb Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 15 Feb 2025 14:43:05 +0000 Subject: [PATCH 105/117] =?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 28821455a..9eacfea86 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Refactors +* ✅ Simplify tests for `app_testing`. PR [#13220](https://github.com/fastapi/fastapi/pull/13220) by [@alv2017](https://github.com/alv2017). * ✅ Simplify tests for `dependency_testing`. PR [#13223](https://github.com/fastapi/fastapi/pull/13223) by [@alv2017](https://github.com/alv2017). ### Docs From 9ec452a154e15e4af0f57f51a4fe1fee58879c4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 15 Feb 2025 17:23:59 +0100 Subject: [PATCH 106/117] =?UTF-8?q?=F0=9F=93=9D=20Update=20docs=20for=20Qu?= =?UTF-8?q?ery=20Params=20and=20String=20Validations,=20remove=20obsolete?= =?UTF-8?q?=20Ellipsis=20docs=20(`...`)=20(#13377)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tutorial/query-params-str-validations.md | 16 --- .../tutorial/query-params-str-validations.md | 28 ----- .../tutorial/query-params-str-validations.md | 117 +++--------------- .../tutorial/query-params-str-validations.md | 16 --- .../tutorial/query-params-str-validations.md | 28 ----- .../tutorial/query-params-str-validations.md | 27 ---- .../tutorial006b.py | 11 -- .../tutorial006b_an.py | 12 -- .../tutorial006b_an_py39.py | 13 -- .../tutorial006c.py | 2 +- .../tutorial006c_an.py | 2 +- .../tutorial006c_an_py310.py | 2 +- .../tutorial006c_an_py39.py | 2 +- .../tutorial006c_py310.py | 2 +- .../tutorial006d.py | 11 -- .../tutorial006d_an.py | 12 -- .../tutorial006d_an_py39.py | 13 -- 17 files changed, 20 insertions(+), 294 deletions(-) delete mode 100644 docs_src/query_params_str_validations/tutorial006b.py delete mode 100644 docs_src/query_params_str_validations/tutorial006b_an.py delete mode 100644 docs_src/query_params_str_validations/tutorial006b_an_py39.py delete mode 100644 docs_src/query_params_str_validations/tutorial006d.py delete mode 100644 docs_src/query_params_str_validations/tutorial006d_an.py delete mode 100644 docs_src/query_params_str_validations/tutorial006d_an_py39.py diff --git a/docs/de/docs/tutorial/query-params-str-validations.md b/docs/de/docs/tutorial/query-params-str-validations.md index f181d501c..de8879ce8 100644 --- a/docs/de/docs/tutorial/query-params-str-validations.md +++ b/docs/de/docs/tutorial/query-params-str-validations.md @@ -315,22 +315,6 @@ Wenn Sie einen Parameter erforderlich machen wollen, während Sie `Query` verwen {* ../../docs_src/query_params_str_validations/tutorial006_an_py39.py hl[9] *} -### Erforderlich mit Ellipse (`...`) - -Es gibt eine Alternative, die explizit deklariert, dass ein Wert erforderlich ist. Sie können als Default das Literal `...` setzen: - -{* ../../docs_src/query_params_str_validations/tutorial006b_an_py39.py hl[9] *} - -/// info - -Falls Sie das `...` bisher noch nicht gesehen haben: Es ist ein spezieller einzelner Wert, Teil von Python und wird „Ellipsis“ genannt (Deutsch: Ellipse). - -Es wird von Pydantic und FastAPI verwendet, um explizit zu deklarieren, dass ein Wert erforderlich ist. - -/// - -Dies wird **FastAPI** wissen lassen, dass dieser Parameter erforderlich ist. - ### Erforderlich, kann `None` sein Sie können deklarieren, dass ein Parameter `None` akzeptiert, aber dennoch erforderlich ist. Das zwingt Clients, den Wert zu senden, selbst wenn er `None` ist. diff --git a/docs/em/docs/tutorial/query-params-str-validations.md b/docs/em/docs/tutorial/query-params-str-validations.md index dbaab5735..fd077bf8f 100644 --- a/docs/em/docs/tutorial/query-params-str-validations.md +++ b/docs/em/docs/tutorial/query-params-str-validations.md @@ -148,22 +148,6 @@ q: Union[str, None] = Query(default=None, min_length=3) {* ../../docs_src/query_params_str_validations/tutorial006.py hl[7] *} -### ✔ ⏮️ ❕ (`...`) - -📤 🎛 🌌 🎯 📣 👈 💲 ✔. 👆 💪 ⚒ `default` 🔢 🔑 💲 `...`: - -{* ../../docs_src/query_params_str_validations/tutorial006b.py hl[7] *} - -/// info - -🚥 👆 🚫 👀 👈 `...` ⏭: ⚫️ 🎁 👁 💲, ⚫️ 🍕 🐍 & 🤙 "❕". - -⚫️ ⚙️ Pydantic & FastAPI 🎯 📣 👈 💲 ✔. - -/// - -👉 🔜 ➡️ **FastAPI** 💭 👈 👉 🔢 ✔. - ### ✔ ⏮️ `None` 👆 💪 📣 👈 🔢 💪 🚫 `None`, ✋️ 👈 ⚫️ ✔. 👉 🔜 ⚡ 👩‍💻 📨 💲, 🚥 💲 `None`. @@ -178,18 +162,6 @@ Pydantic, ❔ ⚫️❔ 🏋️ 🌐 💽 🔬 & 🛠️ FastAPI, ✔️ /// -### ⚙️ Pydantic `Required` ↩️ ❕ (`...`) - -🚥 👆 💭 😬 ⚙️ `...`, 👆 💪 🗄 & ⚙️ `Required` ⚪️➡️ Pydantic: - -{* ../../docs_src/query_params_str_validations/tutorial006d.py hl[2,8] *} - -/// tip - -💭 👈 🌅 💼, 🕐❔ 🕳 🚚, 👆 💪 🎯 🚫 `default` 🔢, 👆 🛎 🚫 ✔️ ⚙️ `...` 🚫 `Required`. - -/// - ## 🔢 🔢 📇 / 💗 💲 🕐❔ 👆 🔬 🔢 🔢 🎯 ⏮️ `Query` 👆 💪 📣 ⚫️ 📨 📇 💲, ⚖️ 🙆‍♀ 🎏 🌌, 📨 💗 💲. diff --git a/docs/en/docs/tutorial/query-params-str-validations.md b/docs/en/docs/tutorial/query-params-str-validations.md index 1bf16334d..511099186 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -6,13 +6,13 @@ Let's take this application as example: {* ../../docs_src/query_params_str_validations/tutorial001_py310.py hl[7] *} -The query parameter `q` is of type `Union[str, None]` (or `str | None` in Python 3.10), that means that it's of type `str` but could also be `None`, and indeed, the default value is `None`, so FastAPI will know it's not required. +The query parameter `q` is of type `str | None`, that means that it's of type `str` but could also be `None`, and indeed, the default value is `None`, so FastAPI will know it's not required. /// note FastAPI will know that the value of `q` is not required because of the default value `= None`. -The `Union` in `Union[str, None]` will allow your editor to give you better support and detect errors. +Having `str | None` will allow your editor to give you better support and detect errors. /// @@ -25,29 +25,9 @@ We are going to enforce that even though `q` is optional, whenever it is provide To achieve that, first import: * `Query` from `fastapi` -* `Annotated` from `typing` (or from `typing_extensions` in Python below 3.9) +* `Annotated` from `typing` -//// tab | Python 3.10+ - -In Python 3.9 or above, `Annotated` is part of the standard library, so you can import it from `typing`. - -```Python hl_lines="1 3" -{!> ../../docs_src/query_params_str_validations/tutorial002_an_py310.py!} -``` - -//// - -//// tab | Python 3.8+ - -In versions of Python below Python 3.9 you import `Annotated` from `typing_extensions`. - -It will already be installed with FastAPI. - -```Python hl_lines="3-4" -{!> ../../docs_src/query_params_str_validations/tutorial002_an.py!} -``` - -//// +{* ../../docs_src/query_params_str_validations/tutorial002_an_py310.py hl[1,3] *} /// info @@ -145,54 +125,23 @@ As in this case (without using `Annotated`) we have to replace the default value So: -```Python -q: Union[str, None] = Query(default=None) -``` - -...makes the parameter optional, with a default value of `None`, the same as: - -```Python -q: Union[str, None] = None -``` - -And in Python 3.10 and above: - ```Python q: str | None = Query(default=None) ``` ...makes the parameter optional, with a default value of `None`, the same as: -```Python -q: str | None = None -``` - -But the `Query` versions declare it explicitly as being a query parameter. - -/// info - -Keep in mind that the most important part to make a parameter optional is the part: ```Python -= None -``` - -or the: - -```Python -= Query(default=None) +q: str | None = None ``` -as it will use that `None` as the default value, and that way make the parameter **not required**. - -The `Union[str, None]` part allows your editor to provide better support, but it is not what tells FastAPI that this parameter is not required. - -/// +But the `Query` version declares it explicitly as being a query parameter. Then, we can pass more parameters to `Query`. In this case, the `max_length` parameter that applies to strings: ```Python -q: Union[str, None] = Query(default=None, max_length=50) +q: str | None = Query(default=None, max_length=50) ``` This will validate the data, show a clear error when the data is not valid, and document the parameter in the OpenAPI schema *path operation*. @@ -201,7 +150,7 @@ This will validate the data, show a clear error when the data is not valid, and Keep in mind that when using `Query` inside of `Annotated` you cannot use the `default` parameter for `Query`. -Instead use the actual default value of the function parameter. Otherwise, it would be inconsistent. +Instead, use the actual default value of the function parameter. Otherwise, it would be inconsistent. For example, this is not allowed: @@ -255,7 +204,7 @@ This specific regular expression pattern checks that the received parameter valu If you feel lost with all these **"regular expression"** ideas, don't worry. They are a hard topic for many people. You can still do a lot of stuff without needing regular expressions yet. -But whenever you need them and go and learn them, know that you can already use them directly in **FastAPI**. +Now you know that whenever you need them you can use them in **FastAPI**. ### Pydantic v1 `regex` instead of `pattern` @@ -296,7 +245,7 @@ q: str instead of: ```Python -q: Union[str, None] = None +q: str | None = None ``` But we are now declaring it with `Query`, for example like: @@ -304,15 +253,7 @@ But we are now declaring it with `Query`, for example like: //// tab | Annotated ```Python -q: Annotated[Union[str, None], Query(min_length=3)] = None -``` - -//// - -//// tab | non-Annotated - -```Python -q: Union[str, None] = Query(default=None, min_length=3) +q: Annotated[str | None, Query(min_length=3)] = None ``` //// @@ -321,42 +262,14 @@ So, when you need to declare a value as required while using `Query`, you can si {* ../../docs_src/query_params_str_validations/tutorial006_an_py39.py hl[9] *} -### Required with Ellipsis (`...`) - -There's an alternative way to explicitly declare that a value is required. You can set the default to the literal value `...`: - -{* ../../docs_src/query_params_str_validations/tutorial006b_an_py39.py hl[9] *} - -/// info - -If you hadn't seen that `...` before: it is a special single value, it is part of Python and is called "Ellipsis". - -It is used by Pydantic and FastAPI to explicitly declare that a value is required. - -/// - -This will let **FastAPI** know that this parameter is required. - ### Required, can be `None` You can declare that a parameter can accept `None`, but that it's still required. This would force clients to send a value, even if the value is `None`. -To do that, you can declare that `None` is a valid type but still use `...` as the default: +To do that, you can declare that `None` is a valid type but simply do not declare a default value: {* ../../docs_src/query_params_str_validations/tutorial006c_an_py310.py hl[9] *} -/// tip - -Pydantic, which is what powers all the data validation and serialization in FastAPI, has a special behavior when you use `Optional` or `Union[Something, None]` without a default value, you can read more about it in the Pydantic docs about Required fields. - -/// - -/// tip - -Remember that in most of the cases, when something is required, you can simply omit the default, so you normally don't have to use `...`. - -/// - ## Query parameter list / multiple values When you define a query parameter explicitly with `Query` you can also declare it to receive a list of values, or said in another way, to receive multiple values. @@ -396,7 +309,7 @@ The interactive API docs will update accordingly, to allow multiple values: ### Query parameter list / multiple values with defaults -And you can also define a default `list` of values if none are provided: +You can also define a default `list` of values if none are provided: {* ../../docs_src/query_params_str_validations/tutorial012_an_py39.py hl[9] *} @@ -419,7 +332,7 @@ the default of `q` will be: `["foo", "bar"]` and your response will be: #### Using just `list` -You can also use `list` directly instead of `List[str]` (or `list[str]` in Python 3.9+): +You can also use `list` directly instead of `list[str]`: {* ../../docs_src/query_params_str_validations/tutorial013_an_py39.py hl[9] *} @@ -427,7 +340,7 @@ You can also use `list` directly instead of `List[str]` (or `list[str]` in Pytho Keep in mind that in this case, FastAPI won't check the contents of the list. -For example, `List[int]` would check (and document) that the contents of the list are integers. But `list` alone wouldn't. +For example, `list[int]` would check (and document) that the contents of the list are integers. But `list` alone wouldn't. /// diff --git a/docs/es/docs/tutorial/query-params-str-validations.md b/docs/es/docs/tutorial/query-params-str-validations.md index f378b9dce..9cb76156f 100644 --- a/docs/es/docs/tutorial/query-params-str-validations.md +++ b/docs/es/docs/tutorial/query-params-str-validations.md @@ -321,22 +321,6 @@ Así que, cuando necesites declarar un valor como requerido mientras usas `Query {* ../../docs_src/query_params_str_validations/tutorial006_an_py39.py hl[9] *} -### Requerido con Puntos suspensivos (`...`) - -Hay una manera alternativa de declarar explícitamente que un valor es requerido. Puedes establecer el valor por defecto al valor literal `...`: - -{* ../../docs_src/query_params_str_validations/tutorial006b_an_py39.py hl[9] *} - -/// info | Información - -Si no habías visto eso `...` antes: es un valor especial único, es parte de Python y se llama "Ellipsis". - -Se usa por Pydantic y FastAPI para declarar explícitamente que un valor es requerido. - -/// - -Esto le permitirá a **FastAPI** saber que este parámetro es requerido. - ### Requerido, puede ser `None` Puedes declarar que un parámetro puede aceptar `None`, pero que aún así es requerido. Esto obligaría a los clientes a enviar un valor, incluso si el valor es `None`. diff --git a/docs/ru/docs/tutorial/query-params-str-validations.md b/docs/ru/docs/tutorial/query-params-str-validations.md index 32a98ff22..13b7015db 100644 --- a/docs/ru/docs/tutorial/query-params-str-validations.md +++ b/docs/ru/docs/tutorial/query-params-str-validations.md @@ -291,22 +291,6 @@ q: Union[str, None] = Query(default=None, min_length=3) {* ../../docs_src/query_params_str_validations/tutorial006_an_py39.py hl[9] *} -### Обязательный параметр с Ellipsis (`...`) - -Альтернативный способ указать обязательность параметра запроса - это указать параметр `default` через многоточие `...`: - -{* ../../docs_src/query_params_str_validations/tutorial006b_an_py39.py hl[9] *} - -/// info | Дополнительная информация - -Если вы ранее не сталкивались с `...`: это специальное значение, часть языка Python и называется "Ellipsis". - -Используется в Pydantic и FastAPI для определения, что значение требуется обязательно. - -/// - -Таким образом, **FastAPI** определяет, что параметр является обязательным. - ### Обязательный параметр с `None` Вы можете определить, что параметр может принимать `None`, но всё ещё является обязательным. Это может потребоваться для того, чтобы пользователи явно указали параметр, даже если его значение будет `None`. @@ -321,18 +305,6 @@ Pydantic, мощь которого используется в FastAPI для /// -### Использование Pydantic's `Required` вместо Ellipsis (`...`) - -Если вас смущает `...`, вы можете использовать `Required` из Pydantic: - -{* ../../docs_src/query_params_str_validations/tutorial006d_an_py39.py hl[4,10] *} - -/// tip | Подсказка - -Запомните, когда вам необходимо объявить query-параметр обязательным, вы можете просто не указывать параметр `default`. Таким образом, вам редко придётся использовать `...` или `Required`. - -/// - ## Множество значений для query-параметра Для query-параметра `Query` можно указать, что он принимает список значений (множество значений). diff --git a/docs/zh/docs/tutorial/query-params-str-validations.md b/docs/zh/docs/tutorial/query-params-str-validations.md index 2fba671f7..c2f9a7e9f 100644 --- a/docs/zh/docs/tutorial/query-params-str-validations.md +++ b/docs/zh/docs/tutorial/query-params-str-validations.md @@ -108,21 +108,6 @@ q: Union[str, None] = Query(default=None, min_length=3) {* ../../docs_src/query_params_str_validations/tutorial006.py hl[7] *} -### 使用省略号(`...`)声明必需参数 - -有另一种方法可以显式的声明一个值是必需的,即将默认参数的默认值设为 `...` : - -{* ../../docs_src/query_params_str_validations/tutorial006b.py hl[7] *} - -/// info - -如果你之前没见过 `...` 这种用法:它是一个特殊的单独值,它是 Python 的一部分并且被称为“Ellipsis”(意为省略号 —— 译者注)。 -Pydantic 和 FastAPI 使用它来显式的声明需要一个值。 - -/// - -这将使 **FastAPI** 知道此查询参数是必需的。 - ### 使用`None`声明必需参数 你可以声明一个参数可以接收`None`值,但它仍然是必需的。这将强制客户端发送一个值,即使该值是`None`。 @@ -137,18 +122,6 @@ Pydantic 是 FastAPI 中所有数据验证和序列化的核心,当你在没 /// -### 使用Pydantic中的`Required`代替省略号(`...`) - -如果你觉得使用 `...` 不舒服,你也可以从 Pydantic 导入并使用 `Required`: - -{* ../../docs_src/query_params_str_validations/tutorial006d.py hl[2,8] *} - -/// tip - -请记住,在大多数情况下,当你需要某些东西时,可以简单地省略 `default` 参数,因此你通常不必使用 `...` 或 `Required` - -/// - ## 查询参数列表 / 多个值 当你使用 `Query` 显式地定义查询参数时,你还可以声明它去接收一组值,或换句话来说,接收多个值。 diff --git a/docs_src/query_params_str_validations/tutorial006b.py b/docs_src/query_params_str_validations/tutorial006b.py deleted file mode 100644 index a8d69c889..000000000 --- a/docs_src/query_params_str_validations/tutorial006b.py +++ /dev/null @@ -1,11 +0,0 @@ -from fastapi import FastAPI, Query - -app = FastAPI() - - -@app.get("/items/") -async def read_items(q: str = Query(default=..., min_length=3)): - results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} - if q: - results.update({"q": q}) - return results diff --git a/docs_src/query_params_str_validations/tutorial006b_an.py b/docs_src/query_params_str_validations/tutorial006b_an.py deleted file mode 100644 index ea3b02583..000000000 --- a/docs_src/query_params_str_validations/tutorial006b_an.py +++ /dev/null @@ -1,12 +0,0 @@ -from fastapi import FastAPI, Query -from typing_extensions import Annotated - -app = FastAPI() - - -@app.get("/items/") -async def read_items(q: Annotated[str, Query(min_length=3)] = ...): - results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} - if q: - results.update({"q": q}) - return results diff --git a/docs_src/query_params_str_validations/tutorial006b_an_py39.py b/docs_src/query_params_str_validations/tutorial006b_an_py39.py deleted file mode 100644 index 687a9f544..000000000 --- a/docs_src/query_params_str_validations/tutorial006b_an_py39.py +++ /dev/null @@ -1,13 +0,0 @@ -from typing import Annotated - -from fastapi import FastAPI, Query - -app = FastAPI() - - -@app.get("/items/") -async def read_items(q: Annotated[str, Query(min_length=3)] = ...): - results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} - if q: - results.update({"q": q}) - return results diff --git a/docs_src/query_params_str_validations/tutorial006c.py b/docs_src/query_params_str_validations/tutorial006c.py index 2ac148c94..0a0e820da 100644 --- a/docs_src/query_params_str_validations/tutorial006c.py +++ b/docs_src/query_params_str_validations/tutorial006c.py @@ -6,7 +6,7 @@ app = FastAPI() @app.get("/items/") -async def read_items(q: Union[str, None] = Query(default=..., min_length=3)): +async def read_items(q: Union[str, None] = Query(min_length=3)): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: results.update({"q": q}) diff --git a/docs_src/query_params_str_validations/tutorial006c_an.py b/docs_src/query_params_str_validations/tutorial006c_an.py index 10bf26a57..55c4f4adc 100644 --- a/docs_src/query_params_str_validations/tutorial006c_an.py +++ b/docs_src/query_params_str_validations/tutorial006c_an.py @@ -7,7 +7,7 @@ app = FastAPI() @app.get("/items/") -async def read_items(q: Annotated[Union[str, None], Query(min_length=3)] = ...): +async def read_items(q: Annotated[Union[str, None], Query(min_length=3)]): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: results.update({"q": q}) diff --git a/docs_src/query_params_str_validations/tutorial006c_an_py310.py b/docs_src/query_params_str_validations/tutorial006c_an_py310.py index 1ab0a7d53..2995d9c97 100644 --- a/docs_src/query_params_str_validations/tutorial006c_an_py310.py +++ b/docs_src/query_params_str_validations/tutorial006c_an_py310.py @@ -6,7 +6,7 @@ app = FastAPI() @app.get("/items/") -async def read_items(q: Annotated[str | None, Query(min_length=3)] = ...): +async def read_items(q: Annotated[str | None, Query(min_length=3)]): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: results.update({"q": q}) diff --git a/docs_src/query_params_str_validations/tutorial006c_an_py39.py b/docs_src/query_params_str_validations/tutorial006c_an_py39.py index ac1273331..76a1cd49a 100644 --- a/docs_src/query_params_str_validations/tutorial006c_an_py39.py +++ b/docs_src/query_params_str_validations/tutorial006c_an_py39.py @@ -6,7 +6,7 @@ app = FastAPI() @app.get("/items/") -async def read_items(q: Annotated[Union[str, None], Query(min_length=3)] = ...): +async def read_items(q: Annotated[Union[str, None], Query(min_length=3)]): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: results.update({"q": q}) diff --git a/docs_src/query_params_str_validations/tutorial006c_py310.py b/docs_src/query_params_str_validations/tutorial006c_py310.py index 82dd9e5d7..88b499c7a 100644 --- a/docs_src/query_params_str_validations/tutorial006c_py310.py +++ b/docs_src/query_params_str_validations/tutorial006c_py310.py @@ -4,7 +4,7 @@ app = FastAPI() @app.get("/items/") -async def read_items(q: str | None = Query(default=..., min_length=3)): +async def read_items(q: str | None = Query(min_length=3)): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: results.update({"q": q}) diff --git a/docs_src/query_params_str_validations/tutorial006d.py b/docs_src/query_params_str_validations/tutorial006d.py deleted file mode 100644 index a8d69c889..000000000 --- a/docs_src/query_params_str_validations/tutorial006d.py +++ /dev/null @@ -1,11 +0,0 @@ -from fastapi import FastAPI, Query - -app = FastAPI() - - -@app.get("/items/") -async def read_items(q: str = Query(default=..., min_length=3)): - results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} - if q: - results.update({"q": q}) - return results diff --git a/docs_src/query_params_str_validations/tutorial006d_an.py b/docs_src/query_params_str_validations/tutorial006d_an.py deleted file mode 100644 index ea3b02583..000000000 --- a/docs_src/query_params_str_validations/tutorial006d_an.py +++ /dev/null @@ -1,12 +0,0 @@ -from fastapi import FastAPI, Query -from typing_extensions import Annotated - -app = FastAPI() - - -@app.get("/items/") -async def read_items(q: Annotated[str, Query(min_length=3)] = ...): - results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} - if q: - results.update({"q": q}) - return results diff --git a/docs_src/query_params_str_validations/tutorial006d_an_py39.py b/docs_src/query_params_str_validations/tutorial006d_an_py39.py deleted file mode 100644 index 687a9f544..000000000 --- a/docs_src/query_params_str_validations/tutorial006d_an_py39.py +++ /dev/null @@ -1,13 +0,0 @@ -from typing import Annotated - -from fastapi import FastAPI, Query - -app = FastAPI() - - -@app.get("/items/") -async def read_items(q: Annotated[str, Query(min_length=3)] = ...): - results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} - if q: - results.update({"q": q}) - return results From 2e788bbbdce346ab53f972cc87ecceeba2299bbe Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 15 Feb 2025 16:24:24 +0000 Subject: [PATCH 107/117] =?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 9eacfea86..6a3ec5b73 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -14,6 +14,7 @@ hide: ### Docs +* 📝 Update docs for Query Params and String Validations, remove obsolete Ellipsis docs (`...`). PR [#13377](https://github.com/fastapi/fastapi/pull/13377) by [@tiangolo](https://github.com/tiangolo). * ✏️ Remove duplicate title in docs `body-multiple-params`. PR [#13345](https://github.com/fastapi/fastapi/pull/13345) by [@DanielYang59](https://github.com/DanielYang59). * 📝 Fix test badge. PR [#13313](https://github.com/fastapi/fastapi/pull/13313) by [@esadek](https://github.com/esadek). From 08b817a84294a7e606f8437d11ee75fd3ef207a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 15 Feb 2025 17:28:09 +0100 Subject: [PATCH 108/117] =?UTF-8?q?=F0=9F=94=A5=20Remove=20manual=20type?= =?UTF-8?q?=20annotations=20in=20JWT=20tutorial=20to=20avoid=20typing=20ex?= =?UTF-8?q?pectations=20(JWT=20doesn't=20provide=20more=20types)=20(#13378?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs_src/security/tutorial004.py | 2 +- docs_src/security/tutorial004_an.py | 2 +- docs_src/security/tutorial004_an_py310.py | 2 +- docs_src/security/tutorial004_an_py39.py | 2 +- docs_src/security/tutorial004_py310.py | 2 +- docs_src/security/tutorial005_an.py | 2 +- docs_src/security/tutorial005_an_py310.py | 2 +- docs_src/security/tutorial005_an_py39.py | 6 +++--- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs_src/security/tutorial004.py b/docs_src/security/tutorial004.py index 91d161b8a..222589618 100644 --- a/docs_src/security/tutorial004.py +++ b/docs_src/security/tutorial004.py @@ -95,7 +95,7 @@ async def get_current_user(token: str = Depends(oauth2_scheme)): ) try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) - username: str = payload.get("sub") + username = payload.get("sub") if username is None: raise credentials_exception token_data = TokenData(username=username) diff --git a/docs_src/security/tutorial004_an.py b/docs_src/security/tutorial004_an.py index df50754af..e2221cd39 100644 --- a/docs_src/security/tutorial004_an.py +++ b/docs_src/security/tutorial004_an.py @@ -96,7 +96,7 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): ) try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) - username: str = payload.get("sub") + username = payload.get("sub") if username is None: raise credentials_exception token_data = TokenData(username=username) diff --git a/docs_src/security/tutorial004_an_py310.py b/docs_src/security/tutorial004_an_py310.py index eff54ef01..a3f74fc0e 100644 --- a/docs_src/security/tutorial004_an_py310.py +++ b/docs_src/security/tutorial004_an_py310.py @@ -95,7 +95,7 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): ) try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) - username: str = payload.get("sub") + username = payload.get("sub") if username is None: raise credentials_exception token_data = TokenData(username=username) diff --git a/docs_src/security/tutorial004_an_py39.py b/docs_src/security/tutorial004_an_py39.py index 0455b500c..b33d677ed 100644 --- a/docs_src/security/tutorial004_an_py39.py +++ b/docs_src/security/tutorial004_an_py39.py @@ -95,7 +95,7 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): ) try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) - username: str = payload.get("sub") + username = payload.get("sub") if username is None: raise credentials_exception token_data = TokenData(username=username) diff --git a/docs_src/security/tutorial004_py310.py b/docs_src/security/tutorial004_py310.py index 78bee22a3..d46ce26bf 100644 --- a/docs_src/security/tutorial004_py310.py +++ b/docs_src/security/tutorial004_py310.py @@ -94,7 +94,7 @@ async def get_current_user(token: str = Depends(oauth2_scheme)): ) try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) - username: str = payload.get("sub") + username = payload.get("sub") if username is None: raise credentials_exception token_data = TokenData(username=username) diff --git a/docs_src/security/tutorial005_an.py b/docs_src/security/tutorial005_an.py index 5b67cb145..2e8bb3bdb 100644 --- a/docs_src/security/tutorial005_an.py +++ b/docs_src/security/tutorial005_an.py @@ -117,7 +117,7 @@ async def get_current_user( ) try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) - username: str = payload.get("sub") + username = payload.get("sub") if username is None: raise credentials_exception token_scopes = payload.get("scopes", []) diff --git a/docs_src/security/tutorial005_an_py310.py b/docs_src/security/tutorial005_an_py310.py index 297193e35..90781587f 100644 --- a/docs_src/security/tutorial005_an_py310.py +++ b/docs_src/security/tutorial005_an_py310.py @@ -116,7 +116,7 @@ async def get_current_user( ) try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) - username: str = payload.get("sub") + username = payload.get("sub") if username is None: raise credentials_exception token_scopes = payload.get("scopes", []) diff --git a/docs_src/security/tutorial005_an_py39.py b/docs_src/security/tutorial005_an_py39.py index 1acf47bdc..a5192d8d6 100644 --- a/docs_src/security/tutorial005_an_py39.py +++ b/docs_src/security/tutorial005_an_py39.py @@ -1,5 +1,5 @@ from datetime import datetime, timedelta, timezone -from typing import Annotated, List, Union +from typing import Annotated, Union import jwt from fastapi import Depends, FastAPI, HTTPException, Security, status @@ -44,7 +44,7 @@ class Token(BaseModel): class TokenData(BaseModel): username: Union[str, None] = None - scopes: List[str] = [] + scopes: list[str] = [] class User(BaseModel): @@ -116,7 +116,7 @@ async def get_current_user( ) try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) - username: str = payload.get("sub") + username = payload.get("sub") if username is None: raise credentials_exception token_scopes = payload.get("scopes", []) From 2b3b4161220da0ed854f0d556aa497a8e3e574db Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 15 Feb 2025 16:28:29 +0000 Subject: [PATCH 109/117] =?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 6a3ec5b73..7d8ca5ba0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -14,6 +14,7 @@ hide: ### Docs +* 🔥 Remove manual type annotations in JWT tutorial to avoid typing expectations (JWT doesn't provide more types). PR [#13378](https://github.com/fastapi/fastapi/pull/13378) by [@tiangolo](https://github.com/tiangolo). * 📝 Update docs for Query Params and String Validations, remove obsolete Ellipsis docs (`...`). PR [#13377](https://github.com/fastapi/fastapi/pull/13377) by [@tiangolo](https://github.com/tiangolo). * ✏️ Remove duplicate title in docs `body-multiple-params`. PR [#13345](https://github.com/fastapi/fastapi/pull/13345) by [@DanielYang59](https://github.com/DanielYang59). * 📝 Fix test badge. PR [#13313](https://github.com/fastapi/fastapi/pull/13313) by [@esadek](https://github.com/esadek). From 5451d05bc84fc8888dfc9cd8a9f3b2c3987c751e Mon Sep 17 00:00:00 2001 From: alv2017 Date: Sat, 15 Feb 2025 18:31:57 +0200 Subject: [PATCH 110/117] =?UTF-8?q?=E2=9C=85=20Simplify=20tests=20for=20`q?= =?UTF-8?q?uery=5Fparams=5Fstr=5Fvalidations`=20(#13218)?= 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: Alejandra <90076947+alejsdev@users.noreply.github.com> --- .../test_tutorial010.py | 23 ++- .../test_tutorial010_an.py | 168 ----------------- .../test_tutorial010_an_py310.py | 175 ------------------ .../test_tutorial010_an_py39.py | 175 ------------------ .../test_tutorial010_py310.py | 175 ------------------ .../test_tutorial011.py | 31 +++- .../test_tutorial011_an.py | 108 ----------- .../test_tutorial011_an_py310.py | 118 ------------ .../test_tutorial011_an_py39.py | 118 ------------ .../test_tutorial011_py310.py | 118 ------------ .../test_tutorial011_py39.py | 118 ------------ .../test_tutorial012.py | 29 ++- .../test_tutorial012_an.py | 96 ---------- .../test_tutorial012_an_py39.py | 106 ----------- .../test_tutorial012_py39.py | 106 ----------- .../test_tutorial013.py | 28 ++- .../test_tutorial013_an.py | 96 ---------- .../test_tutorial013_an_py39.py | 106 ----------- .../test_tutorial014.py | 30 ++- .../test_tutorial014_an.py | 81 -------- .../test_tutorial014_an_py310.py | 91 --------- .../test_tutorial014_an_py39.py | 91 --------- .../test_tutorial014_py310.py | 91 --------- 23 files changed, 117 insertions(+), 2161 deletions(-) delete mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an.py delete mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py310.py delete mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py39.py delete mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial010_py310.py delete mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an.py delete mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py310.py delete mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py39.py delete mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py310.py delete mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py39.py delete mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an.py delete mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an_py39.py delete mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial012_py39.py delete mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an.py delete mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an_py39.py delete mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an.py delete mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py310.py delete mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py39.py delete mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial014_py310.py diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py index 4f52d6ff7..e08e16963 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py @@ -1,14 +1,29 @@ +import importlib + import pytest from dirty_equals import IsDict from fastapi._compat import PYDANTIC_VERSION_MINOR_TUPLE from fastapi.testclient import TestClient +from ...utils import needs_py39, needs_py310 + -@pytest.fixture(name="client") -def get_client(): - from docs_src.query_params_str_validations.tutorial010 import app +@pytest.fixture( + name="client", + params=[ + "tutorial010", + pytest.param("tutorial010_py310", marks=needs_py310), + "tutorial010_an", + pytest.param("tutorial010_an_py39", marks=needs_py39), + pytest.param("tutorial010_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module( + f"docs_src.query_params_str_validations.{request.param}" + ) - client = TestClient(app) + client = TestClient(mod.app) return client diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an.py deleted file mode 100644 index 5daca1e70..000000000 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an.py +++ /dev/null @@ -1,168 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi._compat import PYDANTIC_VERSION_MINOR_TUPLE -from fastapi.testclient import TestClient - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.query_params_str_validations.tutorial010_an import app - - client = TestClient(app) - return client - - -def test_query_params_str_validations_no_query(client: TestClient): - response = client.get("/items/") - assert response.status_code == 200 - assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} - - -def test_query_params_str_validations_item_query_fixedquery(client: TestClient): - response = client.get("/items/", params={"item-query": "fixedquery"}) - assert response.status_code == 200 - assert response.json() == { - "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], - "q": "fixedquery", - } - - -def test_query_params_str_validations_q_fixedquery(client: TestClient): - response = client.get("/items/", params={"q": "fixedquery"}) - assert response.status_code == 200 - assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} - - -def test_query_params_str_validations_item_query_nonregexquery(client: TestClient): - response = client.get("/items/", params={"item-query": "nonregexquery"}) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "string_pattern_mismatch", - "loc": ["query", "item-query"], - "msg": "String should match pattern '^fixedquery$'", - "input": "nonregexquery", - "ctx": {"pattern": "^fixedquery$"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "ctx": {"pattern": "^fixedquery$"}, - "loc": ["query", "item-query"], - "msg": 'string does not match regex "^fixedquery$"', - "type": "value_error.str.regex", - } - ] - } - ) - - -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "description": "Query string for the items to search in the database that have a good match", - "required": False, - "deprecated": True, - "schema": IsDict( - { - "anyOf": [ - { - "type": "string", - "minLength": 3, - "maxLength": 50, - "pattern": "^fixedquery$", - }, - {"type": "null"}, - ], - "title": "Query string", - "description": "Query string for the items to search in the database that have a good match", - # See https://github.com/pydantic/pydantic/blob/80353c29a824c55dea4667b328ba8f329879ac9f/tests/test_fastapi.sh#L25-L34. - **( - {"deprecated": True} - if PYDANTIC_VERSION_MINOR_TUPLE >= (2, 10) - else {} - ), - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Query string", - "maxLength": 50, - "minLength": 3, - "pattern": "^fixedquery$", - "type": "string", - "description": "Query string for the items to search in the database that have a good match", - } - ), - "name": "item-query", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py310.py deleted file mode 100644 index 89da4d82e..000000000 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py310.py +++ /dev/null @@ -1,175 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi._compat import PYDANTIC_VERSION_MINOR_TUPLE -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.query_params_str_validations.tutorial010_an_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_query_params_str_validations_no_query(client: TestClient): - response = client.get("/items/") - assert response.status_code == 200 - assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} - - -@needs_py310 -def test_query_params_str_validations_item_query_fixedquery(client: TestClient): - response = client.get("/items/", params={"item-query": "fixedquery"}) - assert response.status_code == 200 - assert response.json() == { - "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], - "q": "fixedquery", - } - - -@needs_py310 -def test_query_params_str_validations_q_fixedquery(client: TestClient): - response = client.get("/items/", params={"q": "fixedquery"}) - assert response.status_code == 200 - assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} - - -@needs_py310 -def test_query_params_str_validations_item_query_nonregexquery(client: TestClient): - response = client.get("/items/", params={"item-query": "nonregexquery"}) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "string_pattern_mismatch", - "loc": ["query", "item-query"], - "msg": "String should match pattern '^fixedquery$'", - "input": "nonregexquery", - "ctx": {"pattern": "^fixedquery$"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "ctx": {"pattern": "^fixedquery$"}, - "loc": ["query", "item-query"], - "msg": 'string does not match regex "^fixedquery$"', - "type": "value_error.str.regex", - } - ] - } - ) - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "description": "Query string for the items to search in the database that have a good match", - "required": False, - "deprecated": True, - "schema": IsDict( - { - "anyOf": [ - { - "type": "string", - "minLength": 3, - "maxLength": 50, - "pattern": "^fixedquery$", - }, - {"type": "null"}, - ], - "title": "Query string", - "description": "Query string for the items to search in the database that have a good match", - # See https://github.com/pydantic/pydantic/blob/80353c29a824c55dea4667b328ba8f329879ac9f/tests/test_fastapi.sh#L25-L34. - **( - {"deprecated": True} - if PYDANTIC_VERSION_MINOR_TUPLE >= (2, 10) - else {} - ), - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Query string", - "maxLength": 50, - "minLength": 3, - "pattern": "^fixedquery$", - "type": "string", - "description": "Query string for the items to search in the database that have a good match", - } - ), - "name": "item-query", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py39.py deleted file mode 100644 index f5f692b06..000000000 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py39.py +++ /dev/null @@ -1,175 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi._compat import PYDANTIC_VERSION_MINOR_TUPLE -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.query_params_str_validations.tutorial010_an_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_query_params_str_validations_no_query(client: TestClient): - response = client.get("/items/") - assert response.status_code == 200 - assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} - - -@needs_py39 -def test_query_params_str_validations_item_query_fixedquery(client: TestClient): - response = client.get("/items/", params={"item-query": "fixedquery"}) - assert response.status_code == 200 - assert response.json() == { - "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], - "q": "fixedquery", - } - - -@needs_py39 -def test_query_params_str_validations_q_fixedquery(client: TestClient): - response = client.get("/items/", params={"q": "fixedquery"}) - assert response.status_code == 200 - assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} - - -@needs_py39 -def test_query_params_str_validations_item_query_nonregexquery(client: TestClient): - response = client.get("/items/", params={"item-query": "nonregexquery"}) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "string_pattern_mismatch", - "loc": ["query", "item-query"], - "msg": "String should match pattern '^fixedquery$'", - "input": "nonregexquery", - "ctx": {"pattern": "^fixedquery$"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "ctx": {"pattern": "^fixedquery$"}, - "loc": ["query", "item-query"], - "msg": 'string does not match regex "^fixedquery$"', - "type": "value_error.str.regex", - } - ] - } - ) - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "description": "Query string for the items to search in the database that have a good match", - "required": False, - "deprecated": True, - "schema": IsDict( - { - "anyOf": [ - { - "type": "string", - "minLength": 3, - "maxLength": 50, - "pattern": "^fixedquery$", - }, - {"type": "null"}, - ], - "title": "Query string", - "description": "Query string for the items to search in the database that have a good match", - # See https://github.com/pydantic/pydantic/blob/80353c29a824c55dea4667b328ba8f329879ac9f/tests/test_fastapi.sh#L25-L34. - **( - {"deprecated": True} - if PYDANTIC_VERSION_MINOR_TUPLE >= (2, 10) - else {} - ), - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Query string", - "maxLength": 50, - "minLength": 3, - "pattern": "^fixedquery$", - "type": "string", - "description": "Query string for the items to search in the database that have a good match", - } - ), - "name": "item-query", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_py310.py deleted file mode 100644 index 5b62c969f..000000000 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_py310.py +++ /dev/null @@ -1,175 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi._compat import PYDANTIC_VERSION_MINOR_TUPLE -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.query_params_str_validations.tutorial010_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_query_params_str_validations_no_query(client: TestClient): - response = client.get("/items/") - assert response.status_code == 200 - assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} - - -@needs_py310 -def test_query_params_str_validations_item_query_fixedquery(client: TestClient): - response = client.get("/items/", params={"item-query": "fixedquery"}) - assert response.status_code == 200 - assert response.json() == { - "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], - "q": "fixedquery", - } - - -@needs_py310 -def test_query_params_str_validations_q_fixedquery(client: TestClient): - response = client.get("/items/", params={"q": "fixedquery"}) - assert response.status_code == 200 - assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} - - -@needs_py310 -def test_query_params_str_validations_item_query_nonregexquery(client: TestClient): - response = client.get("/items/", params={"item-query": "nonregexquery"}) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "string_pattern_mismatch", - "loc": ["query", "item-query"], - "msg": "String should match pattern '^fixedquery$'", - "input": "nonregexquery", - "ctx": {"pattern": "^fixedquery$"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "ctx": {"pattern": "^fixedquery$"}, - "loc": ["query", "item-query"], - "msg": 'string does not match regex "^fixedquery$"', - "type": "value_error.str.regex", - } - ] - } - ) - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "description": "Query string for the items to search in the database that have a good match", - "required": False, - "deprecated": True, - "schema": IsDict( - { - "anyOf": [ - { - "type": "string", - "minLength": 3, - "maxLength": 50, - "pattern": "^fixedquery$", - }, - {"type": "null"}, - ], - "title": "Query string", - "description": "Query string for the items to search in the database that have a good match", - # See https://github.com/pydantic/pydantic/blob/80353c29a824c55dea4667b328ba8f329879ac9f/tests/test_fastapi.sh#L25-L34. - **( - {"deprecated": True} - if PYDANTIC_VERSION_MINOR_TUPLE >= (2, 10) - else {} - ), - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Query string", - "maxLength": 50, - "minLength": 3, - "pattern": "^fixedquery$", - "type": "string", - "description": "Query string for the items to search in the database that have a good match", - } - ), - "name": "item-query", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py index 5ba39b05d..f4da25752 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py @@ -1,26 +1,47 @@ +import importlib + +import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient -from docs_src.query_params_str_validations.tutorial011 import app +from ...utils import needs_py39, needs_py310 + + +@pytest.fixture( + name="client", + params=[ + "tutorial011", + pytest.param("tutorial011_py39", marks=needs_py310), + pytest.param("tutorial011_py310", marks=needs_py310), + "tutorial011_an", + pytest.param("tutorial011_an_py39", marks=needs_py39), + pytest.param("tutorial011_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module( + f"docs_src.query_params_str_validations.{request.param}" + ) -client = TestClient(app) + client = TestClient(mod.app) + return client -def test_multi_query_values(): +def test_multi_query_values(client: TestClient): url = "/items/?q=foo&q=bar" response = client.get(url) assert response.status_code == 200, response.text assert response.json() == {"q": ["foo", "bar"]} -def test_query_no_values(): +def test_query_no_values(client: TestClient): url = "/items/" response = client.get(url) assert response.status_code == 200, response.text assert response.json() == {"q": None} -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_query_params_str_validations/test_tutorial011_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an.py deleted file mode 100644 index 3942ea77a..000000000 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an.py +++ /dev/null @@ -1,108 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from docs_src.query_params_str_validations.tutorial011_an import app - -client = TestClient(app) - - -def test_multi_query_values(): - url = "/items/?q=foo&q=bar" - response = client.get(url) - assert response.status_code == 200, response.text - assert response.json() == {"q": ["foo", "bar"]} - - -def test_query_no_values(): - url = "/items/" - response = client.get(url) - assert response.status_code == 200, response.text - assert response.json() == {"q": None} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": IsDict( - { - "anyOf": [ - {"type": "array", "items": {"type": "string"}}, - {"type": "null"}, - ], - "title": "Q", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Q", - "type": "array", - "items": {"type": "string"}, - } - ), - "name": "q", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py310.py deleted file mode 100644 index f2ec38c95..000000000 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py310.py +++ /dev/null @@ -1,118 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.query_params_str_validations.tutorial011_an_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_multi_query_values(client: TestClient): - url = "/items/?q=foo&q=bar" - response = client.get(url) - assert response.status_code == 200, response.text - assert response.json() == {"q": ["foo", "bar"]} - - -@needs_py310 -def test_query_no_values(client: TestClient): - url = "/items/" - response = client.get(url) - assert response.status_code == 200, response.text - assert response.json() == {"q": None} - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": IsDict( - { - "anyOf": [ - {"type": "array", "items": {"type": "string"}}, - {"type": "null"}, - ], - "title": "Q", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Q", - "type": "array", - "items": {"type": "string"}, - } - ), - "name": "q", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py39.py deleted file mode 100644 index cd7b15679..000000000 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py39.py +++ /dev/null @@ -1,118 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.query_params_str_validations.tutorial011_an_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_multi_query_values(client: TestClient): - url = "/items/?q=foo&q=bar" - response = client.get(url) - assert response.status_code == 200, response.text - assert response.json() == {"q": ["foo", "bar"]} - - -@needs_py39 -def test_query_no_values(client: TestClient): - url = "/items/" - response = client.get(url) - assert response.status_code == 200, response.text - assert response.json() == {"q": None} - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": IsDict( - { - "anyOf": [ - {"type": "array", "items": {"type": "string"}}, - {"type": "null"}, - ], - "title": "Q", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Q", - "type": "array", - "items": {"type": "string"}, - } - ), - "name": "q", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py310.py deleted file mode 100644 index bdc729516..000000000 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py310.py +++ /dev/null @@ -1,118 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.query_params_str_validations.tutorial011_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_multi_query_values(client: TestClient): - url = "/items/?q=foo&q=bar" - response = client.get(url) - assert response.status_code == 200, response.text - assert response.json() == {"q": ["foo", "bar"]} - - -@needs_py310 -def test_query_no_values(client: TestClient): - url = "/items/" - response = client.get(url) - assert response.status_code == 200, response.text - assert response.json() == {"q": None} - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": IsDict( - { - "anyOf": [ - {"type": "array", "items": {"type": "string"}}, - {"type": "null"}, - ], - "title": "Q", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Q", - "type": "array", - "items": {"type": "string"}, - } - ), - "name": "q", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py39.py deleted file mode 100644 index 26ac56b2f..000000000 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py39.py +++ /dev/null @@ -1,118 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.query_params_str_validations.tutorial011_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_multi_query_values(client: TestClient): - url = "/items/?q=foo&q=bar" - response = client.get(url) - assert response.status_code == 200, response.text - assert response.json() == {"q": ["foo", "bar"]} - - -@needs_py39 -def test_query_no_values(client: TestClient): - url = "/items/" - response = client.get(url) - assert response.status_code == 200, response.text - assert response.json() == {"q": None} - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": IsDict( - { - "anyOf": [ - {"type": "array", "items": {"type": "string"}}, - {"type": "null"}, - ], - "title": "Q", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Q", - "type": "array", - "items": {"type": "string"}, - } - ), - "name": "q", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012.py index 1436db384..549a90519 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012.py @@ -1,25 +1,44 @@ +import importlib + +import pytest from fastapi.testclient import TestClient -from docs_src.query_params_str_validations.tutorial012 import app +from ...utils import needs_py39 + + +@pytest.fixture( + name="client", + params=[ + "tutorial012", + pytest.param("tutorial012_py39", marks=needs_py39), + "tutorial012_an", + pytest.param("tutorial012_an_py39", marks=needs_py39), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module( + f"docs_src.query_params_str_validations.{request.param}" + ) -client = TestClient(app) + client = TestClient(mod.app) + return client -def test_default_query_values(): +def test_default_query_values(client: TestClient): url = "/items/" response = client.get(url) assert response.status_code == 200, response.text assert response.json() == {"q": ["foo", "bar"]} -def test_multi_query_values(): +def test_multi_query_values(client: TestClient): url = "/items/?q=baz&q=foobar" response = client.get(url) assert response.status_code == 200, response.text assert response.json() == {"q": ["baz", "foobar"]} -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_query_params_str_validations/test_tutorial012_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an.py deleted file mode 100644 index 270763f1d..000000000 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an.py +++ /dev/null @@ -1,96 +0,0 @@ -from fastapi.testclient import TestClient - -from docs_src.query_params_str_validations.tutorial012_an import app - -client = TestClient(app) - - -def test_default_query_values(): - url = "/items/" - response = client.get(url) - assert response.status_code == 200, response.text - assert response.json() == {"q": ["foo", "bar"]} - - -def test_multi_query_values(): - url = "/items/?q=baz&q=foobar" - response = client.get(url) - assert response.status_code == 200, response.text - assert response.json() == {"q": ["baz", "foobar"]} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Q", - "type": "array", - "items": {"type": "string"}, - "default": ["foo", "bar"], - }, - "name": "q", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an_py39.py deleted file mode 100644 index 548391683..000000000 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an_py39.py +++ /dev/null @@ -1,106 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.query_params_str_validations.tutorial012_an_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_default_query_values(client: TestClient): - url = "/items/" - response = client.get(url) - assert response.status_code == 200, response.text - assert response.json() == {"q": ["foo", "bar"]} - - -@needs_py39 -def test_multi_query_values(client: TestClient): - url = "/items/?q=baz&q=foobar" - response = client.get(url) - assert response.status_code == 200, response.text - assert response.json() == {"q": ["baz", "foobar"]} - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Q", - "type": "array", - "items": {"type": "string"}, - "default": ["foo", "bar"], - }, - "name": "q", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_py39.py deleted file mode 100644 index e7d745154..000000000 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_py39.py +++ /dev/null @@ -1,106 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.query_params_str_validations.tutorial012_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_default_query_values(client: TestClient): - url = "/items/" - response = client.get(url) - assert response.status_code == 200, response.text - assert response.json() == {"q": ["foo", "bar"]} - - -@needs_py39 -def test_multi_query_values(client: TestClient): - url = "/items/?q=baz&q=foobar" - response = client.get(url) - assert response.status_code == 200, response.text - assert response.json() == {"q": ["baz", "foobar"]} - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Q", - "type": "array", - "items": {"type": "string"}, - "default": ["foo", "bar"], - }, - "name": "q", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial013.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013.py index 1ba1fdf61..f2f5f7a85 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial013.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013.py @@ -1,25 +1,43 @@ +import importlib + +import pytest from fastapi.testclient import TestClient -from docs_src.query_params_str_validations.tutorial013 import app +from ...utils import needs_py39 + + +@pytest.fixture( + name="client", + params=[ + "tutorial013", + "tutorial013_an", + pytest.param("tutorial013_an_py39", marks=needs_py39), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module( + f"docs_src.query_params_str_validations.{request.param}" + ) -client = TestClient(app) + client = TestClient(mod.app) + return client -def test_multi_query_values(): +def test_multi_query_values(client: TestClient): url = "/items/?q=foo&q=bar" response = client.get(url) assert response.status_code == 200, response.text assert response.json() == {"q": ["foo", "bar"]} -def test_query_no_values(): +def test_query_no_values(client: TestClient): url = "/items/" response = client.get(url) assert response.status_code == 200, response.text assert response.json() == {"q": []} -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_query_params_str_validations/test_tutorial013_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an.py deleted file mode 100644 index 343261748..000000000 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an.py +++ /dev/null @@ -1,96 +0,0 @@ -from fastapi.testclient import TestClient - -from docs_src.query_params_str_validations.tutorial013_an import app - -client = TestClient(app) - - -def test_multi_query_values(): - url = "/items/?q=foo&q=bar" - response = client.get(url) - assert response.status_code == 200, response.text - assert response.json() == {"q": ["foo", "bar"]} - - -def test_query_no_values(): - url = "/items/" - response = client.get(url) - assert response.status_code == 200, response.text - assert response.json() == {"q": []} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Q", - "type": "array", - "items": {}, - "default": [], - }, - "name": "q", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an_py39.py deleted file mode 100644 index 537d6325b..000000000 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an_py39.py +++ /dev/null @@ -1,106 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.query_params_str_validations.tutorial013_an_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_multi_query_values(client: TestClient): - url = "/items/?q=foo&q=bar" - response = client.get(url) - assert response.status_code == 200, response.text - assert response.json() == {"q": ["foo", "bar"]} - - -@needs_py39 -def test_query_no_values(client: TestClient): - url = "/items/" - response = client.get(url) - assert response.status_code == 200, response.text - assert response.json() == {"q": []} - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Q", - "type": "array", - "items": {}, - "default": [], - }, - "name": "q", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014.py index 7bce7590c..edd40bb1a 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014.py @@ -1,23 +1,43 @@ +import importlib + +import pytest from fastapi.testclient import TestClient -from docs_src.query_params_str_validations.tutorial014 import app +from ...utils import needs_py39, needs_py310 + + +@pytest.fixture( + name="client", + params=[ + "tutorial014", + pytest.param("tutorial014_py310", marks=needs_py310), + "tutorial014_an", + pytest.param("tutorial014_an_py39", marks=needs_py39), + pytest.param("tutorial014_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module( + f"docs_src.query_params_str_validations.{request.param}" + ) -client = TestClient(app) + client = TestClient(mod.app) + return client -def test_hidden_query(): +def test_hidden_query(client: TestClient): response = client.get("/items?hidden_query=somevalue") assert response.status_code == 200, response.text assert response.json() == {"hidden_query": "somevalue"} -def test_no_hidden_query(): +def test_no_hidden_query(client: TestClient): response = client.get("/items") assert response.status_code == 200, response.text assert response.json() == {"hidden_query": "Not found"} -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_query_params_str_validations/test_tutorial014_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an.py deleted file mode 100644 index 2182e87b7..000000000 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an.py +++ /dev/null @@ -1,81 +0,0 @@ -from fastapi.testclient import TestClient - -from docs_src.query_params_str_validations.tutorial014_an import app - -client = TestClient(app) - - -def test_hidden_query(): - response = client.get("/items?hidden_query=somevalue") - assert response.status_code == 200, response.text - assert response.json() == {"hidden_query": "somevalue"} - - -def test_no_hidden_query(): - response = client.get("/items") - assert response.status_code == 200, response.text - assert response.json() == {"hidden_query": "Not found"} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py310.py deleted file mode 100644 index 344004d01..000000000 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py310.py +++ /dev/null @@ -1,91 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.query_params_str_validations.tutorial014_an_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_hidden_query(client: TestClient): - response = client.get("/items?hidden_query=somevalue") - assert response.status_code == 200, response.text - assert response.json() == {"hidden_query": "somevalue"} - - -@needs_py310 -def test_no_hidden_query(client: TestClient): - response = client.get("/items") - assert response.status_code == 200, response.text - assert response.json() == {"hidden_query": "Not found"} - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py39.py deleted file mode 100644 index 5d4f6df3d..000000000 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py39.py +++ /dev/null @@ -1,91 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.query_params_str_validations.tutorial014_an_py39 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_hidden_query(client: TestClient): - response = client.get("/items?hidden_query=somevalue") - assert response.status_code == 200, response.text - assert response.json() == {"hidden_query": "somevalue"} - - -@needs_py310 -def test_no_hidden_query(client: TestClient): - response = client.get("/items") - assert response.status_code == 200, response.text - assert response.json() == {"hidden_query": "Not found"} - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_py310.py deleted file mode 100644 index dad49fb12..000000000 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_py310.py +++ /dev/null @@ -1,91 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.query_params_str_validations.tutorial014_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_hidden_query(client: TestClient): - response = client.get("/items?hidden_query=somevalue") - assert response.status_code == 200, response.text - assert response.json() == {"hidden_query": "somevalue"} - - -@needs_py310 -def test_no_hidden_query(client: TestClient): - response = client.get("/items") - assert response.status_code == 200, response.text - assert response.json() == {"hidden_query": "Not found"} - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } From e24a5002929245ceb0b5fa8e72f684ad6421b3a8 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 15 Feb 2025 16:32:36 +0000 Subject: [PATCH 111/117] =?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 7d8ca5ba0..01665ac54 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Refactors +* ✅ Simplify tests for `query_params_str_validations`. PR [#13218](https://github.com/fastapi/fastapi/pull/13218) by [@alv2017](https://github.com/alv2017). * ✅ Simplify tests for `app_testing`. PR [#13220](https://github.com/fastapi/fastapi/pull/13220) by [@alv2017](https://github.com/alv2017). * ✅ Simplify tests for `dependency_testing`. PR [#13223](https://github.com/fastapi/fastapi/pull/13223) by [@alv2017](https://github.com/alv2017). From e24299b2ffb027967c4cedb25dade42121edc17f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Haoyu=20=28Daniel=29=20YANG=20=E6=9D=A8=E6=B5=A9=E5=AE=87?= Date: Sat, 15 Feb 2025 17:33:33 +0100 Subject: [PATCH 112/117] =?UTF-8?q?=F0=9F=93=9D=20Add=20more=20precise=20d?= =?UTF-8?q?escription=20of=20HTTP=20status=20code=20range=20in=20docs=20(#?= =?UTF-8?q?13347)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/tutorial/response-status-code.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/en/docs/tutorial/response-status-code.md b/docs/en/docs/tutorial/response-status-code.md index 711042a46..41bf02a8f 100644 --- a/docs/en/docs/tutorial/response-status-code.md +++ b/docs/en/docs/tutorial/response-status-code.md @@ -53,16 +53,16 @@ These status codes have a name associated to recognize them, but the important p In short: -* `100` and above are for "Information". You rarely use them directly. Responses with these status codes cannot have a body. -* **`200`** and above are for "Successful" responses. These are the ones you would use the most. +* `100 - 199` are for "Information". You rarely use them directly. Responses with these status codes cannot have a body. +* **`200 - 299`** are for "Successful" responses. These are the ones you would use the most. * `200` is the default status code, which means everything was "OK". * Another example would be `201`, "Created". It is commonly used after creating a new record in the database. * A special case is `204`, "No Content". This response is used when there is no content to return to the client, and so the response must not have a body. -* **`300`** and above are for "Redirection". Responses with these status codes may or may not have a body, except for `304`, "Not Modified", which must not have one. -* **`400`** and above are for "Client error" responses. These are the second type you would probably use the most. +* **`300 - 399`** are for "Redirection". Responses with these status codes may or may not have a body, except for `304`, "Not Modified", which must not have one. +* **`400 - 499`** are for "Client error" responses. These are the second type you would probably use the most. * An example is `404`, for a "Not Found" response. * For generic errors from the client, you can just use `400`. -* `500` and above are for server errors. You almost never use them directly. When something goes wrong at some part in your application code, or server, it will automatically return one of these status codes. +* `500 - 599` are for server errors. You almost never use them directly. When something goes wrong at some part in your application code, or server, it will automatically return one of these status codes. /// tip From 7e67a91b08c0076ea09c69f9c52fac630c5cad7b Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 15 Feb 2025 16:33:58 +0000 Subject: [PATCH 113/117] =?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 01665ac54..fa1ff20f5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -15,6 +15,7 @@ hide: ### Docs +* 📝 Add more precise description of HTTP status code range in docs. PR [#13347](https://github.com/fastapi/fastapi/pull/13347) by [@DanielYang59](https://github.com/DanielYang59). * 🔥 Remove manual type annotations in JWT tutorial to avoid typing expectations (JWT doesn't provide more types). PR [#13378](https://github.com/fastapi/fastapi/pull/13378) by [@tiangolo](https://github.com/tiangolo). * 📝 Update docs for Query Params and String Validations, remove obsolete Ellipsis docs (`...`). PR [#13377](https://github.com/fastapi/fastapi/pull/13377) by [@tiangolo](https://github.com/tiangolo). * ✏️ Remove duplicate title in docs `body-multiple-params`. PR [#13345](https://github.com/fastapi/fastapi/pull/13345) by [@DanielYang59](https://github.com/DanielYang59). From c868581ce7a108bfbef7c3fb4b42fa663dfdea97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 18 Feb 2025 16:18:14 +0100 Subject: [PATCH 114/117] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors:=20Add?= =?UTF-8?q?=20LambdaTest=20(#13389)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/data/sponsors.yml | 3 +++ docs/en/docs/img/sponsors/lambdatest.png | Bin 0 -> 6320 bytes 2 files changed, 3 insertions(+) create mode 100644 docs/en/docs/img/sponsors/lambdatest.png diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index f9bf33ae9..91b23937c 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -58,3 +58,6 @@ bronze: - url: https://testdriven.io/courses/tdd-fastapi/ title: Learn to build high-quality web apps with best practices img: https://fastapi.tiangolo.com/img/sponsors/testdriven.svg + - 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 diff --git a/docs/en/docs/img/sponsors/lambdatest.png b/docs/en/docs/img/sponsors/lambdatest.png new file mode 100644 index 0000000000000000000000000000000000000000..674cbcb893dfec5a29dc72452215734603dcede6 GIT binary patch literal 6320 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 aNn)J05mscJEx>;p5Rc`b$`#9)2K^5%?KE=$ literal 0 HcmV?d00001 From 235300c1d2a7d8a011685adc1c9aa2c195c0368f Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 18 Feb 2025 15:18:42 +0000 Subject: [PATCH 115/117] =?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 fa1ff20f5..c61e9d7e5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -41,6 +41,7 @@ hide: ### Internal +* 🔧 Update sponsors: Add LambdaTest. PR [#13389](https://github.com/fastapi/fastapi/pull/13389) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump cloudflare/wrangler-action from 3.13 to 3.14. PR [#13350](https://github.com/fastapi/fastapi/pull/13350) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-material from 9.5.18 to 9.6.1. PR [#13301](https://github.com/fastapi/fastapi/pull/13301) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump pillow from 11.0.0 to 11.1.0. PR [#13300](https://github.com/fastapi/fastapi/pull/13300) by [@dependabot[bot]](https://github.com/apps/dependabot). From e157cf4b9625b2ccdd6a3214ba552bcde9452912 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hyogeun=20Oh=20=28=EC=98=A4=ED=9A=A8=EA=B7=BC=29?= Date: Wed, 19 Feb 2025 01:52:15 +0900 Subject: [PATCH 116/117] =?UTF-8?q?=F0=9F=90=9B=20Fix=20issue=20with=20Swa?= =?UTF-8?q?gger=20theme=20change=20example=20in=20the=20official=20tutoria?= =?UTF-8?q?l=20(#13289)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs_src/configure_swagger_ui/tutorial002.py | 2 +- .../test_tutorial/test_configure_swagger_ui/test_tutorial002.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs_src/configure_swagger_ui/tutorial002.py b/docs_src/configure_swagger_ui/tutorial002.py index cc569ce45..cc75c2196 100644 --- a/docs_src/configure_swagger_ui/tutorial002.py +++ b/docs_src/configure_swagger_ui/tutorial002.py @@ -1,6 +1,6 @@ from fastapi import FastAPI -app = FastAPI(swagger_ui_parameters={"syntaxHighlight.theme": "obsidian"}) +app = FastAPI(swagger_ui_parameters={"syntaxHighlight": {"theme": "obsidian"}}) @app.get("/users/{username}") diff --git a/tests/test_tutorial/test_configure_swagger_ui/test_tutorial002.py b/tests/test_tutorial/test_configure_swagger_ui/test_tutorial002.py index 166901188..d06a385b5 100644 --- a/tests/test_tutorial/test_configure_swagger_ui/test_tutorial002.py +++ b/tests/test_tutorial/test_configure_swagger_ui/test_tutorial002.py @@ -12,7 +12,7 @@ def test_swagger_ui(): '"syntaxHighlight": false' not in response.text ), "not used parameters should not be included" assert ( - '"syntaxHighlight.theme": "obsidian"' in response.text + '"syntaxHighlight": {"theme": "obsidian"}' in response.text ), "parameters with middle dots should be included in a JSON compatible way" assert ( '"dom_id": "#swagger-ui"' in response.text From 286fd694eaf006481c9fdc59a8324e4405fd0683 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 18 Feb 2025 16:52:41 +0000 Subject: [PATCH 117/117] =?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 c61e9d7e5..5f3d98e5c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -15,6 +15,7 @@ hide: ### Docs +* 🐛 Fix issue with Swagger theme change example in the official tutorial. PR [#13289](https://github.com/fastapi/fastapi/pull/13289) by [@Zerohertz](https://github.com/Zerohertz). * 📝 Add more precise description of HTTP status code range in docs. PR [#13347](https://github.com/fastapi/fastapi/pull/13347) by [@DanielYang59](https://github.com/DanielYang59). * 🔥 Remove manual type annotations in JWT tutorial to avoid typing expectations (JWT doesn't provide more types). PR [#13378](https://github.com/fastapi/fastapi/pull/13378) by [@tiangolo](https://github.com/tiangolo). * 📝 Update docs for Query Params and String Validations, remove obsolete Ellipsis docs (`...`). PR [#13377](https://github.com/fastapi/fastapi/pull/13377) by [@tiangolo](https://github.com/tiangolo).